恶意软件分析学习指南-三-

122 阅读1小时+

恶意软件分析学习指南(三)

原文:annas-archive.org/md5/6464eec061058ae554d0950e983941aa

译者:飞龙

协议:CC BY-NC-SA 4.0

第九章:恶意软件混淆技术

混淆一词指的是掩盖有意义信息的过程。恶意软件作者通常使用各种混淆技术来隐藏信息,并修改恶意内容,以使安全分析师难以进行检测和分析。对手通常使用编码/加密技术来隐藏信息,防止安全产品检测。除了使用编码/加密之外,攻击者还会使用像打包器这样的程序来混淆恶意的二进制内容,这使得分析和逆向工程变得更加困难。在本章中,我们将讨论如何识别这些混淆技术,以及如何解码/解密和解包恶意二进制文件。我们将首先研究编码/加密技术,之后再研究解包技术。

对手通常使用编码和加密的原因如下:

  • 为了隐蔽命令与控制通信

  • 为了避开基于签名的解决方案,如入侵防御系统

  • 为了混淆恶意软件使用的配置文件内容

  • 为了加密将从受害系统中泄露的信息

  • 为了在恶意二进制文件中混淆字符串,以避开静态分析

在我们深入了解恶意软件如何使用加密算法之前,先让我们了解一些基础概念和本章中会使用的术语。明文指的是未加密的消息;这可能是命令与控制(C2)流量或恶意软件想要加密的文件内容。密文指的是加密后的消息;这可能是恶意软件从 C2 服务器接收到的加密可执行文件或加密命令。

恶意软件通过将明文密钥一起传递给加密函数来加密,生成密文。生成的密文通常会被恶意软件用于写入文件或通过网络发送:

以相同的方式,恶意软件可能会从 C2 服务器或文件接收加密内容,然后通过将加密内容密钥传递给解密函数来解密,过程如下:

在分析恶意软件时,你可能想了解某一特定内容是如何加密或解密的。为此,你将主要关注识别加密或解密函数,以及用于加密或解密内容的密钥。例如,如果你想确定网络内容是如何加密的,那么你很可能会在网络输出操作之前找到加密函数(如HttpSendRequest())。同样,如果你想了解 C2 的加密内容是如何解密的,那么你很可能会在通过 API(如InternetReadFile())从 C2 获取内容后找到解密函数。

一旦确定了加密/解密功能,检查这些函数将帮助你了解内容是如何被加密/解密的、使用了什么密钥,以及采用了什么算法来混淆数据。

1. 简单编码

大多数时候,攻击者使用非常简单的编码算法,如Base64 编码异或加密来混淆数据。攻击者之所以使用简单的算法,是因为它们易于实现,占用的系统资源较少,且足以让安全产品和安全分析人员无法轻易识别内容。

1.1 凯撒密码

凯撒密码,也称为移位密码,是一种传统的密码算法,是最简单的编码技术之一。它通过将明文中的每个字母按一定的固定位置向下移动来加密信息。例如,如果你将字符'A'向下移动3个位置,那么你将得到'D''B'则变为'E',依此类推,当移位达到'X'时会回绕到'A'

1.1.1 凯撒密码的工作原理

理解凯撒密码的最佳方式是将字母从AZ写下来,并为这些字母分配索引,从025,如下所示。换句话说,'A'对应索引0'B'对应索引1,依此类推。所有字母从AZ的集合叫做字符集

现在,假设你想将字母偏移三位,那么3就是你的密钥。要加密字母'A',将字母A的索引(即0)加到密钥3上;这就得到0+3 = 3。然后,使用结果3作为索引来查找对应的字母,即'D',所以'A'被加密为'D'。要加密'B',你将字母'B'的索引(即1)加到密钥3上,这就得到4,索引4对应字母'E',因此'B'被加密为'E',依此类推。

之前技术的问题出现在当我们到达'X'时,它的索引为23。当我们将23+3时得到26,但我们知道索引26没有对应的字符,因为最大索引值是25。我们也知道索引26应该回绕到索引0(对应字母'A')。为了解决这个问题,我们使用取模操作,取字符集的长度。在这种情况下,字符集ABCDEFGHIJKLMNOPQRSTUVWXYZ的长度是26。现在,为了加密'X',我们使用'X'的索引(23)加上密钥(3),然后对字符集长度(26)进行取模操作,结果为0,这个结果作为索引来查找对应的字符,即'A'

(23+3)%26 = 0

取模操作允许你回到开头。你可以使用相同的逻辑来加密字符集中的所有字符(从AZ),并在回绕到起点时继续。在凯撒密码中,你可以使用以下公式获取加密(密文)字符的索引:

(i + key) % (length of the character set)

where i = index of plaintext character

以相同的方式,你可以使用以下方法获取明文(解密后的)字符的索引:

(j - key) % (length of the character set)

where j = index of ciphertext character

以下图示显示了使用 3 作为密钥(移动三个位)的字符集、加密和解密文本 "ZEUS" 的过程。加密后,文本 "ZEUS" 被转换为 "CHXV",然后解密过程将其还原为 "ZEUS"

1.1.2 使用 Python 解密凯撒密码

以下是一个简单的 Python 脚本示例,它将字符串 "CHXV" 解密回 "ZEUS"

>>> chr_set = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
>>> key = 3
>>> cipher_text = "CHXV"
>>> plain_text = ""
>>> for ch in cipher_text:
 j = chr_set.find(ch.upper())
 plain_index = (j-key) % len(chr_set)
 plain_text += chr_set[plain_index]
>>> print plain_text
ZEUS

一些恶意软件样本可能使用了修改版的凯撒(移位)密码;在这种情况下,你可以修改前面提到的脚本以满足你的需求。APT1 组使用的恶意软件 WEBC2-GREENCAT 从 C2 服务器获取内容,并使用修改版的凯撒密码对内容进行解密。它使用了一个包含 66 个字符的字符集 abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._/-,密钥为 56

1.2 Base64 编码

使用凯撒密码,攻击者可以加密字母,但它并不足以加密二进制数据。攻击者使用其他各种编码/加密算法来加密二进制数据。Base64 编码允许攻击者将二进制数据编码为 ASCII 字符串格式。因此,你经常会看到攻击者在 HTTP 等明文协议中使用 Base64 编码的数据。

1.2.1 将数据转换为 Base64

标准的 Base64 编码由以下 64 个字符组成。每 3 个字节(24 位)的二进制数据将被转换为字符集中的四个字符。每个转换后的字符是 6 位大小。除了以下字符外,= 字符用于填充:

ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/

为了理解数据是如何转换为 Base64 编码的,首先,通过将 063 的索引分配给字符集中的字母,构建 Base64 索引表,如下所示。根据下表,索引 0 对应字母 A,索引 62 对应字符 +,依此类推:

现在,假设我们要对文本 "One" 进行 Base64 编码。为此,我们需要将字母转换为相应的位值,如下所示:

O -> 0x4f -> 01001111
n -> 0x6e -> 01101110
e -> 0x65 -> 01100101

Base64 算法一次处理 3 个字节(24 位);在这种情况下,我们有恰好 24 位,它们被按顺序排列,如下所示:

010011110110111001100101

然后将这 24 位数据拆分为四个部分,每部分包含 6 位,并转换为相应的十进制值。然后使用这些十进制值作为索引,查找 Base64 索引表中对应的值,因此文本 One 编码为 T25l

010011 -> 19 -> base64 table lookup -> T
110110 -> 54 -> base64 table lookup -> 2
111001 -> 57 -> base64 table lookup -> 5
100101 -> 37 -> base64 table lookup -> l

解码 Base64 是一个逆向过程,但理解 Base64 编码或解码的工作原理并非必须,因为有 Python 模块和工具可以帮助你解码 Base64 编码的数据,而无需理解算法。理解这一点有助于在攻击者使用自定义版本的 Base64 编码时应对。

1.2.2 编码与解码 Base64

要在 Python(2.x) 中使用 Base64 编码数据,可以使用以下代码:

>>> import base64
>>> plain_text = "One"
>>> encoded = base64.b64encode(plain_text)
>>> print encoded
T25l

要在 Python 中解码 base64 数据,请使用以下代码:

>>> import base64
>>> encoded = "T25l"
>>> decoded = base64.b64decode(encoded)
>>> print decoded
One

CyberChef 是由 GCHQ 开发的一款优秀的网络应用程序,允许你在浏览器中执行各种编码/解码、加密/解密、压缩/解压缩和数据分析操作。你可以访问 CyberChef 网站 gchq.github.io/CyberChef/,更多信息请参考 github.com/gchq/CyberChef

你还可以使用像 ConverterNET 这样的工具 (www.kahusecurity.com/tools/) 来编码/解码 base64 数据。ConvertNET 提供多种功能,允许你将数据转换为不同格式的输入/输出。要进行编码,输入要编码的文本并点击“Text to Base64”按钮。要进行解码,输入已编码的数据并点击“Base64 to Text”按钮。下图展示了使用 ConverterNET 对字符串 Hi 进行 Base64 编码的过程:

编码字符串末尾的 = 字符是填充字符。回想一下,算法将三字节的输入转换为四个字符,而 Hi 只有两个字符,所以它被填充以使其变为三个字符;每当使用填充时,你都会在 Base64 编码字符串的末尾看到 = 字符。这意味着有效的 Base64 编码字符串的长度总是 4 的倍数。

1.2.3 解码自定义 Base64

攻击者使用不同的 Base64 编码变体;目的是防止 Base64 解码工具成功解码数据。在本节中,你将了解这些技巧中的一些。

一些恶意软件样本会从末尾去掉填充字符 (=)。稍后会展示一个恶意软件样本(Trojan Qidmorks)的 C2 通信。以下的 POST 有效负载看起来像是使用 base64 编码进行编码的:

当你尝试解码 POST 有效负载时,会收到如下的 Incorrect 填充错误:

这个错误的原因是编码字符串的长度 (150) 不是 4 的倍数。换句话说,Base64 编码的数据缺少两个字符,这很可能是填充字符(==):

>>> encoded = "Q3VycmVudFZlcnNpb246IDYuMQ0KVXNlciBwcml2aWxlZ2llcyBsZXZlbDogMg0KUGFyZW50IHByb2Nlc3M6IFxEZXZpY2VcSGFyZGRpc2tWb2x1bWUxXFdpbmRvd3NcZXhwbG9yZXIuZXhlDQoNCg"
>>> len(encoded)
150

向编码字符串添加两个填充字符(==)可以成功解码数据,如下所示。从解码后的数据中可以看到,恶意软件将操作系统版本(表示 Windows 7 的6.1)、用户的权限级别和父进程发送到 C2 服务器:

有时,恶意软件作者会使用base64编码的轻微变种。例如,攻击者可以使用一个字符集,其中字符-_代替了+/(第 63 和 64 个字符),如图所示:

ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_

一旦你识别出在原始字符集中被替换的字符来编码数据,那么你可以使用类似如下的代码。这里的思路是将修改过的字符替换回标准字符集中的原始字符,然后进行解码:

>>> import base64
>>> encoded = "cGFzc3dvcmQxMjM0IUA_PUB-"
>>> encoded = encoded.replace("-","+").replace("_","/")
>>> decoded = base64.b64decode(encoded)
>>> print decoded
password1234!@?=@~

有时候,恶意软件作者会改变字符集中的字符顺序。例如,他们可能会使用以下字符集,而不是标准字符集:

0123456789+/ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz

当攻击者使用非标准的Base64字符集时,你可以使用以下代码解码数据。请注意,在以下代码中,除了64个字符外,变量chr_setnon_chr_set还包括填充字符=(第 65 个字符),这是正确解码所必需的:

>>> import base64
>>> chr_set = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="
>>> non_chr_set = "0123456789+/ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz="
>>> encoded = "G6JgP6w="
>>> re_encoded = ""
>>> for en_ch in encoded:
 re_encoded += en_ch.replace(en_ch, chr_set[non_chr_set.find(en_ch)])
>>> decoded = base64.b64decode(re_encoded)
>>> print decoded
Hello

你还可以通过使用ConverterNET工具执行自定义 Base64 解码,选择 Conversions | Convert Custom Base64。只需在 Alphabet 字段中输入自定义的Base64字符集,然后在 Input 字段中输入要解码的数据,按下 Decode 按钮,如下所示:

1.2.4 识别 Base64

你可以通过查找包含Base64字符集(字母数字字符,+/)的长字符串来识别使用了 Base64 编码的二进制文件。以下截图显示了恶意二进制文件中的Base64字符集,表明恶意软件可能使用了Base64编码:

你可以使用字符串交叉引用功能(在第五章中介绍)来定位Base64字符集所在的代码,如下图所示。尽管在解码Base64数据时,了解Base64字符集在代码中使用的位置并非必须,但有时定位它会很有用,比如当恶意软件作者将Base64编码与其他加密算法一起使用时。例如,如果恶意软件使用某种加密算法加密 C2 网络流量,然后使用Base64编码;在这种情况下,定位Base64字符集可能会让你进入Base64函数。然后,你可以分析Base64函数或识别调用Base64函数的函数(使用Xrefs to功能),这可能会引导你到加密函数:

你可以在x64dbg中使用字符串交叉引用;为此,确保调试器在模块内的某个地方暂停,然后右键点击反汇编窗口(CPU 窗口),选择搜索 | 当前模块 | 字符串引用。

检测二进制文件中是否存在Base64字符集的另一种方法是使用YARA规则(YARA 在第二章,静态分析中有介绍),如这里所示:

rule base64
{
strings:
    $a="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
    $b="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
condition:
    $a or $b
}

1.3 XOR 编码

除了Base64编码之外,恶意软件作者常用的另一种编码算法是XOR编码算法。XOR是一种按位操作(类似于ANDORNOT),它作用于操作数的相应位。下表显示了XOR操作的性质。在XOR操作中,当两个位相同时,结果为0;否则,结果为1

ABA^B
`0**0**0`
`1**0**1`
`0**1**1`
`1**1**0`

例如,当你对24进行XOR,即2 ^ 4时,结果是6。它的工作原理如下:

                2: 0000 0010
                4: 0000 0100
---------------------------
Result After XOR : 0000 0110 (6)

1.3.1 单字节 XOR

在单字节XOR中,明文中的每个字节都与加密密钥进行XOR操作。例如,如果攻击者想要使用0x40作为密钥加密明文cat,那么文本中的每个字符(字节)都会与0x40进行XOR运算,结果是密文#!4。下图显示了每个字符的加密过程:

XOR的另一个有趣的性质是,当你用相同的密钥对密文进行XOR操作时,可以恢复明文。例如,如果你将之前例子中的密文#!40x40(密钥)进行XOR运算,你就会得到cat。这意味着如果你知道密钥,那么同一个函数可以用来同时加密和解密数据。以下是一个简单的 Python 脚本,用于执行XOR解密(同一个函数也可以用于执行XOR加密):

def xor(data, key):
    translated = ""
    for ch in data:
        translated += chr(ord(ch) ^ key)
    return translated

if __name__ == "__main__":
   out = xor("#!4", 0x40)
   print out

在了解了XOR编码算法之后,让我们看一个键盘记录器的例子,它将所有的按键输入编码到一个文件中。当该示例被执行时,它会记录按键输入,并使用CreateFileA() API 打开一个文件(所有按键输入将记录到此文件中),如后面所示。然后,它使用WriteFile() API 将记录的按键输入写入文件。请注意恶意软件在调用CreateFileA()之后、WriteFile()之前调用了一个函数(重命名为enc_function);这个函数在将内容写入文件之前对其进行编码。enc_function接受两个参数;第一个参数是包含待加密数据的缓冲区,第二个参数是缓冲区的长度:

检查 enc_function 显示恶意软件使用单字节 XOR。它从数据缓冲区读取每个字符,并使用 0x5A 的密钥进行编码,如下所示。在以下的 XOR 循环中,edx 寄存器指向数据缓冲区,esi 寄存器包含缓冲区的长度,而 ecx 寄存器作为数据缓冲区的索引,在每次循环结束时递增,当索引值 (ecx) 小于缓冲区的长度 (esi) 时,循环继续:

1.3.2 通过暴力破解寻找 XOR 密钥

在单字节 XOR 中,密钥的长度为一个字节,因此只有 255 个可能的密钥 (0x0 - 0xff)0 除外作为 key,因为任何值与 0XOR 运算都会得到相同的结果(即无加密)。由于只有 255 个密钥,你可以尝试对加密数据进行所有可能的密钥破解。如果你知道在解密后的数据中要查找的内容,这种技术非常有用。例如,当执行一个恶意软件样本时,假设恶意软件获取了计算机的主机名 mymachine,并与一些数据连接后,进行单字节 XOR 加密,最终加密为密文 lkwpjeia>i}ieglmja。假设该密文被在 C2 通信中外泄。现在,要确定用于加密密文的密钥,你可以分析加密函数或进行暴力破解。以下 Python 命令实现了暴力破解技术;因为我们期望解密后的字符串包含 "mymachine",所以脚本会尝试用所有可能的密钥解密加密字符串(密文),并在找到 "mymachine" 时显示密钥和解密后的内容。在后面提到的示例中,你可以看到密钥被确定为 4,解密后的内容 hostname:mymachine 包含了主机名 mymachine

>>> def xor_brute_force(content, to_match):
 for key in range(256):
 translated = ""
 for ch in content:
 translated += chr(ord(ch) ^ key)
 if to_match in translated:
 print "Key %s(0x%x): %s" % (key, key, translated)

>>> xor_brute_force("lkwpjeia>i}ieglmja", "mymachine")
Key 4(0x4): hostname:mymachine

你也可以使用诸如 ConverterNET 之类的工具来暴力破解并确定密钥。操作步骤是,选择工具 | 密钥搜索/转换。在弹出的窗口中,输入加密内容和匹配字符串,点击搜索按钮。如果找到密钥,它会显示在结果字段中,如下所示:

暴力破解技术在确定用于加密 PE 文件(如 EXE 或 DLL)的 XOR 密钥时非常有用。只需在解密后的内容中查找匹配的字符串 MZThis program cannot be run in DOS mode

1.3.3 NULL 忽略 XOR 编码

XOR 编码中,当一个空字节 (0x00) 与密钥 XOR 时,会返回该密钥,如下所示:

>>> ch = 0x00
>>> key = 4
>>> ch ^ key
4

这意味着,每当一个包含大量空字节的缓冲区被编码时,单字节 XOR 密钥会变得非常明显。在以下示例中,plaintext变量被分配了一个字符串,其中包含三个空字节,并使用密钥0x4b(字符K)加密,且加密后的输出以十六进制字符串格式和文本格式同时打印。请注意,plaintext变量中的三个null字节在加密内容中被转换为XOR密钥值0x4b 0x4b 0x4b(或KKK)。这种XOR的特性使得如果没有忽略空字节,容易识别出密钥。

>>> plaintext = "hello\x00\x00\x00"
>>> key = 0x4b 
>>> enc_text = ""
>>> for ch in plaintext:
 x = ord(ch) ^ key
 enc_hex += hex(x) + " "
 enc_text += chr(x)

>>> print enc_hex
0x23 0x2e 0x27 0x27 0x24 0x4b 0x4b 0x4b
>>> print enc_text
#.''$KKK

以下截图展示了一个恶意软件样本的XOR加密通信(HeartBeat RAT)。请注意,字节0x2出现在整个加密内容中;这是因为恶意软件使用0x2XOR密钥对一个包含空字节的较大缓冲区进行了加密。有关该恶意软件的逆向工程更多信息,请参考作者在 Cysinfo 会议上的演讲:cysinfo.com/session-10-part-1-reversing-decrypting-communications-of-heartbeat-rat/

为避免空字节问题,恶意软件作者在加密过程中忽略空字节(0x00)加密密钥,如这里提到的命令所示。请注意,在下面的代码中,明文字符使用密钥0x4b进行加密,除了空字节(0x00)和加密密钥字节(0x4b);因此,在加密后的输出中,空字节被保留,而不会泄露加密密钥。如你所见,当攻击者使用此技术时,仅通过查看加密内容很难确定密钥

>>> plaintext = "hello\x00\x00\x00"
>>> key = 0x4b
>>> enc_text = ""
>>> for ch in plaintext:
 if ch == "\x00" or ch == chr(key):
 enc_text += ch
 else:
 enc_text += chr(ord(ch) ^ key)

>>> enc_text
"#.''$\x00\x00\x00"

1.3.4 多字节 XOR 编码

攻击者通常使用多字节XOR,因为它能更好地防御暴力破解技术。例如,如果恶意软件作者使用 4 字节XOR密钥加密数据,然后进行暴力破解,你将需要尝试4,294,967,295 (0xFFFFFFFF)个可能的密钥,而不是255 (0xFF)个密钥。以下截图展示了恶意软件(Taidoor)的XOR解密循环。在这种情况下,Taidoor从其资源部分提取了加密的 PE(exe)文件,并使用 4 字节XOR密钥0xEAD4AA34对其进行了解密。

以下截图展示了Resource Hacker工具中的加密资源。可以通过右键点击资源并选择将资源保存为*.bin 文件来提取并保存该资源。

以下是一个 Python 脚本,它使用4 字节 XOR密钥0xEAD4AA34解码编码的资源,并将解码后的内容写入文件(decrypted.bin):

import os
import struct
import sys

def four_byte_xor(content, key ):
    translated = ""
    len_content = len(content)
    index = 0
    while (index < len_content):
        data = content[index:index+4]
        p = struct.unpack("I", data)[0]
        translated += struct.pack("I", p ^ key)
        index += 4
    return translated

in_file = open("rsrc.bin", 'rb')
out_file = open("decrypted.bin", 'wb')
xor_key = 0xEAD4AA34
rsrc_content = in_file.read()
decrypted_content = four_byte_xor(rsrc_content,xor_key)
out_file.write(decrypted_content)

解密后的内容是一个 PE(可执行文件),如下所示:

$ xxd decrypted.bin | more
00000000:  4d5a 9000 0300 0000 0400 0000 ffff 0000  MZ..............
00000010:  b800 0000 0000 0000 4000 0000 0000 0000  ........@.......
00000020:  0000 0000 0000 0000 0000 0000 0000 0000  ................
00000030:  0000 0000 0000 0000 0000 0000 f000 0000  ................
00000040:  0e1f ba0e 00b4 09cd 21b8 014c cd21 5468  ........!..L.!Th
00000050:  6973 2070 726f 6772 616d 2063 616e 6e6f  is program canno
00000060:  7420 6265 2072 756e 2069 6e20 444f 5320  t be run in DOS

1.3.5 识别 XOR 编码

要识别 XOR 编码,在 IDA 中加载二进制文件并通过选择 搜索 | 文本 来搜索 XOR 指令。在弹出的对话框中,输入 xor 并选择 查找所有匹配项,如下所示:

当你点击“确定”时,所有 XOR 的出现位置会被显示出来。通常可以看到 XOR 操作,其中操作数是相同的寄存器,如 xor eax,eaxxor ebx,ebx。这些指令是编译器用来清零寄存器值的,你可以忽略这些指令。要识别 XOR 编码,可以查找 (a) 具有常量值的寄存器(或内存引用)与 XOR 操作,如下所示,或者 (b) 查找寄存器(或内存引用)之间的 XOR 操作。你可以通过双击条目跳转到代码:

以下是一些可以用来确定 XOR 密钥的工具。除了使用 XOR 编码外,攻击者还可能使用 ROL, ROT 或 SHIFT 操作来编码数据。这里提到的 XORSearchBalbuzard 除了支持 XOR 外,还支持 ROLROTShift 操作。CyberChef 支持几乎所有类型的编码、加密和压缩算法:

2. 恶意软件加密

恶意软件作者常常使用简单的编码技术,因为仅仅掩盖数据就足够了,但有时,攻击者也会使用加密。要识别二进制文件中加密功能的使用,可以查找加密指示符(签名),例如:

  • 引用加密函数的字符串或导入

  • 加密常量

  • 加密例程使用的独特指令序列

2.1 使用 Signsrch 识别加密签名

一个有用的工具是 Signsrch,可以用来搜索文件或进程中的加密签名,下载地址是 aluigi.altervista.org/mytoolz.htm。该工具依赖加密签名来检测加密算法。加密签名位于文本文件 signsrch.sig 中。在以下输出中,当使用 -e 选项运行 signsrch 时,它会显示在二进制文件中检测到的 DES 签名的相对虚拟地址:

C:\signsrch>signsrch.exe -e kav.exe

Signsrch 0.2.4
by Luigi Auriemma
e-mail: aluigi@autistici.org
web: aluigi.org
  optimized search function by Andrew http://www.team5150.com/~andrew/
  disassembler engine by Oleh Yuschuk

- open file "kav.exe"
- 91712 bytes allocated
- load signatures
- open file C:\signsrch\signsrch.sig
- 3075 signatures in the database
- start 1 threads
- start signatures scanning:

  offset num description [bits.endian.size]
  --------------------------------------------
00410438 1918 DES initial permutation IP [..64]
00410478 2330 DES_fp [..64]
004104b8 2331 DES_ei [..48]
004104e8 2332 DES_p32i [..32]
00410508 1920 DES permuted choice table (key) [..56]
00410540 1921 DES permuted choice key (table) [..48]
00410580 1922 DES S-boxes [..512]
[Removed]

一旦知道了加密指示器所在的地址,你可以使用 IDA 导航到该地址。例如,如果你想导航到地址 00410438DES 初始置换 IP),在 IDA 中加载二进制文件,然后选择 Jump | Jump to address(或按 G 热键),并输入地址,如下所示:

一旦点击确认(OK),你将到达包含指示器的地址(在此案例中,DES 初始置换 IP,标记为DES_ip),如下图所示:

现在,要了解此加密指示器在代码中如何使用,你可以使用交叉引用(Xrefs-to)功能。使用交叉引用(Xrefs to)功能显示 DES_ip 在地址 0x4032E0sub_4032B0 函数中被引用(loc_4032E0):

现在,直接导航到地址 0x4032E0 会带你进入 DES 加密函数,如下图所示。找到加密函数后,你可以使用交叉引用进一步检查它,以了解加密函数被调用的上下文以及用于加密数据的密钥:

与使用 -e 选项定位签名然后手动导航到使用该签名的代码不同,你可以使用 -F 选项,它会直接给出加密指示器在代码中使用的第一条指令的地址。在以下输出中,运行 signsrch 并加上 -F 选项,会直接显示加密指示器 DES 初始置换 IPDES_ip)在代码中使用的地址 0x4032E0

C:\signsrch>signsrch.exe -F kav.exe

[removed]

  offset num description [bits.endian.size]
  --------------------------------------------
[removed]
004032e0 1918 DES initial permutation IP [..64]
00403490 2330 DES_fp [..64]

-e-F 选项会显示相对于 PE 头中指定的 首选基地址 的地址。例如,如果二进制文件的 首选基地址0x00400000,则 -e-F 选项返回的地址是通过将相对虚拟地址与首选基地址 0x00400000 相加来确定的。当你运行(或调试)二进制文件时,它可能会加载到除首选基地址以外的任何地址(例如 0x01350000)。如果你希望定位运行中进程或在调试二进制文件时(在 IDA 或 x64dbg 中)中的加密指示器地址,你可以使用 signsrch 命令并加上 -P <pid 或进程名称> 选项。-P 选项会自动确定可执行文件加载的基地址,然后计算加密签名的虚拟地址,如下所示:

C:\signsrch>signsrch.exe -P kav.exe

[removed]

- 01350000 0001b000 C:\Users\test\Desktop\kav.exe
- pid 3068
- base address 0x01350000
- offset 01350000 size 0001b000
- 110592 bytes allocated
- load signatures
- open file C:\signsrch\signsrch.sig
- 3075 signatures in the database
- start 1 threads
- start signatures scanning:

  offset num description [bits.endian.size]
  --------------------------------------------
  01360438 1918 DES initial permutation IP [..64]
 01360478 2330 DES_fp [..64]
 013604b8 2331 DES_ei [..48]

除了检测加密算法,Signsrch 还可以检测压缩算法、一些反调试代码以及 Windows 加密函数,这些函数通常以 Crypt 开头,如 CryptDecrypt()CryptImportKey()

2.2 使用 FindCrypt2 检测加密常量

Findcrypt2 (www.hexblog.com/ida_pro/files/findcrypt2.zip) 是一个 IDA Pro 插件,用于在内存中搜索由多种不同算法使用的加密常量。要使用该插件,请下载它,并将 findcrypt.plw 文件复制到 IDA 插件文件夹中。现在,当你加载二进制文件时,插件会自动运行,或者你可以通过选择 编辑 | 插件 | Find crypt v2 手动调用它。插件的结果会显示在输出窗口中:

FindCrypt2 插件也可以在调试模式下运行。如果你使用的是 IDA 6.x 或更低版本,FindCrypt2 能很好地工作;在撰写本书时,它似乎无法与 IDA 7.x 版本兼容(可能是由于 IDA 7.x API 的变化)。

2.3 使用 YARA 检测加密签名

另一种识别二进制文件中加密使用的方法是通过扫描二进制文件并使用包含加密签名的 YARA 规则。你可以编写自己的 YARA 规则,也可以下载其他安全研究人员编写的 YARA 规则(例如在 github.com/x64dbg/yarasigs/blob/master/crypto_signatures.yara),然后使用这些 YARA 规则扫描二进制文件。

x64dbg 集成了 YARA;如果你希望在调试时扫描二进制文件中的加密签名,这非常有用。你可以将二进制文件加载到 x64dbg 中(确保执行在二进制文件中的某个位置暂停),然后右键单击 CPU 窗口并选择 YARA(或按 Ctrl + Y);这将弹出显示此处的 YARA 对话框。点击文件并定位包含 YARA 规则的文件。你还可以通过点击目录按钮加载包含 YARA 规则的多个文件:

以下截图显示了通过使用包含加密签名的 YARA 规则扫描恶意二进制文件后,检测到的 加密常量。现在,你可以右键单击任何条目并选择 “Follow in Dump” 以查看转储窗口中的数据,或者如果签名与加密例程相关联,你可以双击任何条目以跳转到相应的代码:

像 RC4 这样的加密算法不使用加密常量,因此很难通过加密签名来检测它。攻击者通常使用 RC4 加密数据,因为它易于实现;RC4 中使用的步骤在这篇 Talos 博客文章中有详细解释:blog.talosintelligence.com/2014/06/an-introduction-to-recognizing-and.html

2.4 使用 Python 解密

在你识别出加密算法和用于加密数据的密钥后,可以使用PyCryptowww.dlitz.net/software/pycrypto/)Python 模块来解密数据。要安装PyCrypto,你可以使用apt-get install python-cryptopip install pycrypto,或者从源代码编译。PyCrypto 支持多种哈希算法,如MD2MD4MD5RIPEMDSHA1SHA256。它还支持加密算法,如AESARC2BlowfishCASTDESDES3 (Triple DES)IDEARC5ARC4

以下 Python 命令演示了如何使用Pycrypto模块生成MD5SHA1SHA256哈希:

>>> from Crypto.Hash import MD5,SHA256,SHA1
>>> text = "explorer.exe"
>>> MD5.new(text).hexdigest()
'cde09bcdf5fde1e2eac52c0f93362b79'
>>> SHA256.new(text).hexdigest()
'7592a3326e8f8297547f8c170b96b8aa8f5234027fd76593841a6574f098759c'
>>> SHA1.new(text).hexdigest()
'7a0fd90576e08807bde2cc57bcf9854bbce05fe3'

要解密内容,请从Crypto.Cipher导入适当的加密模块。以下示例演示了如何在 ECB 模式下使用 DES 进行加密和解密:

>>> from Crypto.Cipher import DES
>>> text = "hostname=blank78"
>>> key = "14834567"
>>> des = DES.new(key, DES.MODE_ECB)
>>> cipher_text = des.encrypt(text)
>>> cipher_text
'\xde\xaf\t\xd5)sNj`\xf5\xae\xfd\xb8\xd3f\xf7'
>>> plain_text = des.decrypt(cipher_text)
>>> plain_text
'hostname=blank78'

3. 自定义编码/加密

有时,攻击者使用自定义编码/加密方案,这使得识别加密方式(及密钥)变得困难,也使逆向工程更加复杂。自定义编码方法之一是使用编码和加密的组合来混淆数据;例如,Etumbotwww.arbornetworks.com/blog/asert/illuminating-the-etumbot-apt-backdoor/)就是这样的恶意软件。当执行Etumbot恶意软件样本时,它会从 C2 服务器获取RC4密钥;然后,它使用获取的RC4密钥对系统信息(如主机名、用户名和 IP 地址)进行加密,接着使用自定义Base64对加密内容进行进一步编码,并将其外泄到 C2。包含混淆内容的 C2 通信稍后会展示。有关此样本的逆向工程细节,请参考作者的演讲和视频演示(cysinfo.com/12th-meetup-reversing-decrypting-malware-communications/):

要解混淆内容,首先需要使用自定义Base64解码,然后再使用RC4解密;这些步骤可以通过以下 Python 命令完成。输出将显示解密后的系统信息:

>>> import base64
>>> from Crypto.Cipher import ARC4
>>> rc4_key = "e65wb24n5"
>>> cipher_text = "kRp6OKW9r90_2_KvkKcQ_j5oA1D2aIxt6xPeFiJYlEHvM8QMql38CtWfWuYlgiXMDFlsoFoH"
>>> content = cipher_text.replace('_','/').replace('-','=')
>>> b64_decode = base64.b64decode(content)
>>> rc4 = ARC4.new(rc4_key)
>>> plain_text = rc4.decrypt(b64_decode)
>>> print plain_text
MYHOSTNAME|Administrator|192.168.1.100|No Proxy|04182|

一些恶意软件作者并没有使用标准的编码/加密算法的组合,而是实现了全新的编码/加密方案。一个这样的恶意软件例子是APT1组使用的恶意软件。这个恶意软件将一个字符串解密为网址;为此,恶意软件调用了一个用户定义的函数(在后面提到的截图中将其重命名为Decrypt_Func),该函数实现了自定义的加密算法。Decrypt_Func接受三个参数;第 1 个参数是包含加密内容的缓冲区,第 2 个参数是存储解密内容的缓冲区,第 3 个参数是缓冲区的长度。在下面的截图中,执行在调用Decrypt_Func之前暂停,显示了第 1 个参数(包含加密内容的缓冲区):

根据你的目标,你可以分析Decrypt_Func来理解算法的工作原理,然后按照作者的演示中所讲的内容编写解密器(cysinfo.com/8th-meetup-understanding-apt1-malware-techniques-using-malware-analysis-reverse-engineering/),或者你也可以让恶意软件为你解密内容。要让恶意软件解密内容,只需跳过Decrypt_Func(它会执行完解密功能),然后检查第 2 个参数(存储解密内容的缓冲区)。下图显示了解密后的缓冲区(第 2 个参数),其中包含恶意网址:

前面提到的让恶意软件解码数据的技术,如果解密函数被调用的次数不多,还是比较有用的。如果解密函数在程序中被调用了多次,使用调试器脚本自动化解码过程会更高效(详见 第六章,恶意二进制调试),而不是手动操作。为了演示这一点,请看下面这段 64 位恶意软件的代码片段(在下面的截图中)。注意恶意软件多次调用了一个函数(在稍后的截图中将其重命名为dec_function);如果你查看代码,会发现一个加密的字符串作为第 1 个参数(存放在rcx寄存器中)传递给了这个函数,执行该函数后,eax中的返回值包含了存储解密内容的缓冲区地址:

下图显示了对dec_function交叉引用;如你所见,程序中多次调用了这个函数:

每次调用dec_function时,它都会解密一个字符串。为了解密传递给这个函数的所有字符串,我们可以编写一个IDAPython脚本(例如下面显示的脚本):

import idautils
import idaapi
import idc

for name in idautils.Names():
    if name[1] == "dec_function":
        ea= idc.get_name_ea_simple("dec_function")
        for ref in idautils.CodeRefsTo(ea, 1):
            idc.add_bpt(ref)
idc.start_process('', '', '')
while True:
    event_code = idc.wait_for_next_event(idc.WFNE_SUSP, -1)
    if event_code < 1 or event_code == idc.PROCESS_EXITED:
        break
    rcx_value = idc.get_reg_value("RCX")
    encoded_string = idc.get_strlit_contents(rcx_value)
    idc.step_over()
    evt_code = idc.wait_for_next_event(idc.WFNE_SUSP, -1)
    if evt_code == idc.BREAKPOINT:
        rax_value = idc.get_reg_value("RAX")
    decoded_string = idc.get_strlit_contents(rax_value)
    print "{0} {1:>25}".format(encoded_string, decoded_string)
    idc.resume_process()

由于我们已经将解密函数重命名为dec_function,它可以从 IDA 中的名称窗口访问。之前的脚本会遍历名称窗口来识别dec_function并执行以下步骤:

  1. 如果dec_function存在,它会确定dec_function的地址。

  2. 它使用dec_function的地址来确定dec_function的交叉引用(Xrefs to),从而列出所有调用dec_function的地址。

  3. 它在dec_function被调用的所有地址上设置断点。

  4. 它会自动启动调试器,当在dec_function处命中断点时,它会从rcx寄存器指向的地址读取加密字符串。需要注意的是,为了让 IDA 调试器自动启动,请确保选择调试器(例如,本地 Windows 调试器),可以通过工具栏区域选择,或者选择Debugger | Select debugger

  5. 然后,它会跳过该函数,执行解密函数(dec_function),并读取返回值(rax),该返回值包含解密后字符串的地址。接着,它会打印出解密后的字符串。

  6. 它重复之前的步骤,解密每个传递给dec_function的字符串。

运行完之前的脚本后,加密字符串及其对应的解密字符串会显示在输出窗口中,如下所示。从输出中可以看到,恶意软件在运行时解密文件名、注册表名称和 API 函数名称,以避免引起怀疑。换句话说,这些是攻击者希望从静态分析中隐藏的字符串:

4. 恶意软件解包

攻击者会尽力保护其二进制文件免受防病毒检测,并使恶意软件分析师很难进行静态分析和逆向工程。恶意软件作者通常使用压缩器加密器参见 第二章,静态分析,了解压缩器及其检测方法),来混淆可执行内容。压缩器是一种程序,它将普通的可执行文件压缩其内容,并生成一个新的混淆可执行文件。加密器与压缩器类似,不是压缩二进制文件,而是加密它。换句话说,压缩器或加密器将可执行文件转换为一种难以分析的形式。当一个二进制文件被压缩时,它显示的信息非常少;你不会找到任何包含有价值信息的字符串,导入的函数数量会较少,程序指令也会被模糊化。要理解一个被压缩的二进制文件,你需要去除应用于程序的混淆层(解包);为此,首先了解压缩器的工作原理非常重要。

当一个普通的可执行文件经过打包器处理时,可执行内容会被压缩,并添加一个解包存根解压例程)。打包器随后修改可执行文件的入口点,将其指向存根的位置,并生成一个新的打包可执行文件。当打包的二进制文件被执行时,解包存根会在运行时提取原始二进制文件,并通过将控制权转移到*原始入口点(OEP)*来触发原始二进制文件的执行,以下图所示:

解包一个打包的二进制文件时,可以使用自动化工具或手动操作。自动化方法节省时间,但并非完全可靠(有时有效,有时无效),而手动方法虽然耗时,但一旦掌握技能,它是最可靠的方法。

4.1 手动解包

要解包被打包器打包的二进制文件,通常需要执行以下一般步骤:

  1. 第一步是确定OEP;如前所述,当一个打包的二进制文件被执行时,它会提取原始二进制文件,并在某个时刻将控制权转移到OEP。原始入口点(OEP)是恶意软件的第一条指令的地址(恶意代码的起始位置),即在打包之前的位置。在此步骤中,我们需要识别打包二进制文件中的指令,这条指令会跳转(引导我们)到 OEP。

  2. 下一步是执行程序,直到到达 OEP;其目的是让恶意软件的存根在内存中自解包并暂停在 OEP 处(在执行恶意代码之前)。

  3. 第三步是将解包后的进程从内存转储到磁盘。

  4. 最后一步是修复导入地址表(IAT),即转储文件的导入地址表。

在接下来的几节中,我们将详细探讨这些步骤。为了演示之前的概念,我们将使用一个被UPX 打包器打包的恶意软件(upx.github.io/)。接下来几节中介绍的工具和技术将帮助你了解手动解包过程。

4.1.1 确定 OEP

本节将帮助您理解识别打包二进制文件中 OEP 的技巧。在以下截图中,使用 pestudiowww.winitor.com/)检查打包二进制文件显示了许多提示,表明该文件被打包。打包二进制文件包含三个部分,UPX0UPX1.rsrc。从截图中可以看出,打包二进制文件的入口点在 UPX1 部分,因此执行从这里开始,这一部分包含将在运行时解压原始可执行文件的解压存根。另一个指示是,UPX0 部分的原始大小为 0,但虚拟大小为 0x1f000;这表明 UPX0 部分在磁盘上不占用任何空间,但在内存中占用空间;具体来说,它在内存中占用了 0x1f000 字节的大小(这是因为恶意软件在内存中解压可执行文件,并在运行时将其存储在 UPX0 部分)。此外,UPX0 部分具有 执行 权限,很可能是因为解压原始二进制文件后,恶意代码将在 UPX0 中开始执行:

另一个指示是,打包二进制文件包含了混淆的字符串,当您在 IDA 中加载二进制文件时,IDA 识别出导入地址表(IAT)位于非标准位置,并显示以下警告;这是由于 UPX 打包了所有部分和 IAT

该二进制文件仅包含一个内建函数和 5 个导入函数;所有这些指示都表明该二进制文件已被打包:

要找到 OEP,您需要定位程序中将控制转移到 OEP 的指令。根据打包器的不同,这可能简单也可能具有挑战性;通常,您需要关注那些将控制转移到不明确目的地的指令。检查打包二进制文件中函数的流程图会看到跳转到一个位置,该位置在 IDA 中被标红:

红色是 IDA 无法分析的标志,因为跳转目的地不明确。以下截图显示了跳转指令:

双击 跳转目的地 (byte_40259B) 显示跳转将到达 UPX0(从 UPX1)。换句话说,执行时,恶意软件将在 UPX1 执行解压缩存根,这将解压原始二进制文件,将解压后的代码复制到 UPX0,并且跳转指令很可能将控制转移到 UPX0 中解压后的代码(从 UPX1)。

到这个阶段,我们已经找到了我们认为会跳转到OEP的指令。下一步是将二进制文件加载到调试器中,并在执行跳转的指令处设置断点,然后执行直到到达该指令。为此,二进制文件已加载到x64dbg中(你也可以使用 IDA 调试器并按照相同的步骤操作),并在跳转指令处设置了断点,执行直到跳转指令。如下面的截图所示,执行在该跳转指令处暂停。

现在你可以假设恶意软件已经完成解压;接下来,你可以按一次F7(进入),这将带你到原始入口点地址0x0040259B。此时,我们来到了恶意软件的第一条指令(解压后):

4.1.2 使用 Scylla 转储进程内存

现在我们已经找到了 OEP,下一步是将进程内存转储到磁盘。为了转储进程,我们将使用名为Scylla的工具(github.com/NtQuery/Scylla);这是一个很棒的工具,可以用来转储进程内存并重建导入地址表。x64dbg的一个优点是它集成了Scylla,你可以通过点击插件 | Scylla 来启动它(或者按Ctrl + I)。要转储进程内存,当执行在 OEP 处暂停时,启动 Scylla,确保 OEP 字段设置为正确的地址,如下所示;如果没有,你需要手动设置它,然后点击 Dump 按钮并将转储的可执行文件保存到磁盘(在这种情况下,它被保存为packed_dump.exe):

现在,当你将转储的可执行文件加载到 IDA 时,你将看到所有内置函数的完整列表(这在打包程序中是不可见的),并且函数代码不再被混淆,但导入仍然不可见,API 调用显示的是地址而不是名称。为了克服这个问题,你需要重建打包二进制文件的导入表:

4.1.3 修复导入表

要修复导入,返回到Scylla,点击 IAT Autosearch 按钮,它将扫描进程内存以定位导入表;如果找到了,它将填充 VA 和大小字段并显示适当的值。要获取导入列表,点击 Get Imports 按钮。使用这种方法确定的导入函数列表如下所示。有时,你可能会在结果中看到无效条目(没有勾选标记的条目);在这种情况下,右键点击这些条目并选择 Cut Thunk 来删除它们:

在使用前一步确定了导入函数后,您需要将补丁应用到已转储的可执行文件(packed_dump.exe)。为此,请点击“Fix Dump”按钮,启动文件浏览器,您可以选择之前转储的文件。Scylla 会使用已确定的导入函数对二进制文件进行补丁处理,并创建一个新的文件,文件名末尾会包含 _SCY(例如 packed_dumped_SCY.exe)。现在,当您在 IDA 中加载已修补的文件时,您将看到导入函数的引用,如下所示:

当您处理某些打包工具时,Scylla 中的 IAT 自动搜索按钮可能无法找到模块的导入表;在这种情况下,您可能需要额外的努力来手动确定导入表的起始位置和大小,并将它们输入到 VA 和大小字段中。

4.2 自动解包

有各种工具可以解包使用常见打包工具如 UPXFSGAsPack 打包的恶意软件。自动化工具对于已知的打包工具非常有效,可以节省时间,但请记住,它并不总是有效;这时,手动解包技巧将派上用场。ReversingLabs 的 TitanMist (www.reversinglabs.com/open-source/titanmist.html) 是一个很棒的工具,包含各种 打包工具签名解包脚本。下载并解压后,您可以使用如下命令运行它来对打包的二进制文件进行解包;使用 -i 来指定输入文件(打包文件),-o 指定输出文件名,-t 指定解包器的类型。在后面提到的命令中,TitanMist 被用来处理一个使用 UPX 打包的二进制文件;请注意它是如何自动识别打包工具并执行解包过程的。该工具自动识别了 OEP 和导入表,转储了进程,修复了导入项,并将补丁应用到转储的进程中:

C:\TitanMist>TitanMist.exe -i packed.exe -o unpacked.exe -t python

Match found!
│ Name: UPX
│ Version: 0.8x - 3.x
│ Author: Markus and Laszlo
│ Wiki url: http://kbase.reversinglabs.com/index.php/UPX
│ Description:

Unpacker for UPX 1.x - 3.x packed files
ReversingLabs Corporation / www.reversinglabs.com
[x] Debugger initialized.
[x] Hardware breakpoint set.
[x] Import at 00407000.
[x] Import at 00407004.
[x] Import at 00407008.[Removed]
[x] Import at 00407118.
[x] OEP found: 0x0040259B.
[x] Process dumped.
[x] IAT begin at 0x00407000, size 00000118.
[X] Imports fixed.
[x] No overlay found.
[x] File has been realigned.
[x] File has been unpacked to unpacked.exe.
[x] Exit Code: 0.
█ Unpacking succeeded! 

另一个选择是使用 IDA Pro 的Universal PE Unpacker 插件。这个插件依赖于调试恶意软件,以确定代码何时跳转到 OEP。有关此插件的详细信息,请参考这篇文章(www.hex-rays.com/products/ida/support/tutorials/unpack_pe/unpacking.pdf)。要调用此插件,将二进制文件加载到 IDA 中,选择编辑 | 插件 | Universal PE unpacker。运行插件后,程序将在调试器中启动,并尝试在打包程序解包完成时暂停。将*UPX-*打包的恶意软件(与手动解包中使用的示例相同)加载到 IDA 中并启动插件后,将显示以下对话框。在以下截图中,IDA 将起始地址和结束地址设置为UPX0段的范围;这个范围被视为OEP范围。换句话说,当执行到达此段时(从包含解压缩存根的UPX1段),IDA 将暂停程序执行,给你机会采取进一步的行动:

在以下截图中,注意 IDA 如何自动确定 OEP 地址,并显示出以下对话框:

如果点击“Yes”按钮,执行将停止,进程将退出,但在此之前,IDA 会自动确定导入地址表(IAT),并创建一个新段来重建程序的导入部分。此时,你可以分析已解包的代码。以下截图展示了新重建的导入地址表:

如果不点击“YES”按钮,而是点击“No”按钮,IDA 将会在 OEP 处暂停调试器的执行,接下来你可以选择调试已解包的代码,或者手动导出可执行文件,使用像Scylla这样的工具修复导入项,通过输入正确的 OEP(如在第 4.1 节手动解包中讲解的那样)。

x64dbg中,你可以使用解包脚本进行自动化解包,这些脚本可以从github.com/x64dbg/Scripts下载。要进行解包,确保二进制文件已加载并暂停在入口点。根据你使用的打包工具,你需要通过右键点击脚本面板然后选择加载脚本 | 打开(或 Ctrl + O)来加载相应的解包脚本。以下截图展示了 UPX 解包脚本的内容:

加载脚本后,右键点击脚本窗格并选择“运行”以运行脚本。如果脚本成功解包,将弹出一个消息框,显示“脚本完成”,并且执行会暂停在 OEP 处。以下截图显示了在运行 UPX 解包脚本后,自动在 OEP 处设置的断点(在 CPU 窗格中)。现在,你可以开始调试解包后的代码,或者使用Scylla来转储进程并修复导入(如第 4.1 节手动解包所述):

除了前面提到的工具外,还有其他各种资源可以帮助你进行自动解包。请参见Ether Unpack Serviceether.gtisc.gatech.edu/web_unpack/FUU (Faster Universal Unpacker)github.com/crackinglandia/fuu.

总结

恶意软件作者使用混淆技术来隐藏数据并避开安全分析员的检测。在本章中,我们介绍了恶意软件作者常用的各种编码、加密和打包技术,并探讨了不同的去混淆策略。在下一章,你将接触到内存取证的概念,并了解如何利用内存取证来调查恶意软件的能力。

第十章:使用内存取证进行恶意软件猎杀

到目前为止,我们已经涵盖了使用静态分析、动态分析和代码分析来分析恶意软件的概念、工具和技术。在本章中,您将了解另一种技术,称为内存取证(或内存分析)

内存取证(或内存分析)是一种调查技术,涉及从计算机的物理内存(RAM)中查找和提取取证工件。计算机的内存存储着关于系统运行时状态的宝贵信息。获取并分析内存将揭示用于取证调查的必要信息,例如系统上正在运行的应用程序、这些应用程序正在访问的对象(文件、注册表等)、活动的网络连接、加载的模块、加载的内核驱动程序等。因此,内存取证在事件响应和恶意软件分析中被广泛使用。

在事件响应过程中,大多数情况下,您将无法访问恶意软件样本,但可能只有嫌疑系统的内存映像。例如,安全产品可能会警告某个系统存在可能的恶意行为,在这种情况下,您可以获取嫌疑系统的内存映像,以确认感染并查找恶意工件的内存取证。

除了在事件响应中使用内存取证外,您还可以将其作为恶意软件分析的一部分(拥有恶意软件样本时)使用,以获取有关感染后恶意软件行为的额外信息。例如,当您拥有恶意软件样本时,除了执行静态、动态和代码分析外,还可以在隔离环境中执行样本,然后获取感染计算机的内存并检查内存映像,以了解感染后恶意软件的行为。

另一个使用内存取证的原因是,某些恶意软件样本可能不会将恶意组件写入磁盘(仅存在于内存中)。因此,磁盘取证或文件系统分析可能会失败。在这种情况下,内存取证可以极大地帮助查找恶意组件。

一些恶意软件样本通过挂钩或修改操作系统结构来欺骗操作系统和实时取证工具。在这种情况下,内存取证非常有用,因为它可以绕过恶意软件用来隐藏自身的技巧。本章介绍了内存取证的概念,并涵盖了用于获取和分析内存映像的工具。

1. 内存取证步骤

无论是作为事件响应的一部分还是用于恶意软件分析,内存取证的一般步骤如下:

  • 内存获取:这涉及将目标机器的内存(或转储)到磁盘。根据您是调查受感染系统还是将内存取证作为恶意软件分析的一部分,目标机器可以是您怀疑受感染的系统(在您的网络上),也可以是您实验室环境中执行恶意软件样本的分析机器。

  • 内存分析:将内存转储到磁盘后,这一步涉及分析转储的内存以查找和提取取证物件。

2. 内存获取

内存获取是将易失性内存(RAM)获取到非易失性存储(磁盘文件)的过程。有各种工具可让您获取物理机器的内存。以下是一些允许您将物理内存(转储)到 Windows 的工具。其中一些工具是商业软件,许多工具可以在注册后免费下载。以下工具适用于 x86(32 位)和 x64(64 位)机器:

2.1 使用 DumpIt 进行内存获取

DumpIt是一款出色的内存获取工具,可让您在 Windows 上转储物理内存。它支持获取 32 位(x86)和 64 位(x64)机器的内存。DumpIt 是Comae memory toolkit的一部分,该工具包含各种独立工具,可帮助进行内存获取和在不同文件格式之间进行转换。要下载最新版本的Comae memory toolkit,您需要在my.comae.io上注册账户。创建账户后,您可以登录并下载最新版本的Comae memory toolkit

下载 Comae 工具包后,解压缩存档文件,并根据你希望转储 32 位或 64 位机器的内存,进入相应的 32 位或 64 位目录。该目录包含多个文件,其中包括DumpIt.exe。本节主要介绍如何使用 DumpIt 进行内存转储。如果你有兴趣了解该目录中其他工具的功能,请阅读readme.txt文件。

使用DumpIt获取内存的最简单方法是右键点击DumptIt.exe文件,并选择以管理员身份运行。默认情况下,DumpIt 将内存转储到一个文件中,该文件是Microsoft 崩溃转储(.dmp 扩展名),然后可以使用内存分析工具,如Volatility(接下来将介绍),或者使用 Microsoft 调试器,如WinDbg,对其进行分析。

你也可以从命令行运行DumpIt;这样你将拥有多个选项。要显示不同的选项,请以管理员身份运行cmd.exe,进入包含DumpIt.exe的目录,并输入以下命令:

C:\Comae-Toolkit-3.0.20180307.1\x64>DumpIt.exe /?
  DumpIt 3.0.20180307.1
  Copyright (C) 2007 - 2017, Matthieu Suiche <http://www.msuiche.net>
  Copyright (C) 2012 - 2014, MoonSols Limited <http://www.moonsols.com>
  Copyright (C) 2015 - 2017, Comae Technologies FZE <http://www.comae.io>

Usage: DumpIt [Options] /OUTPUT <FILENAME>

Description:
  Enables users to create a snapshot of the physical memory as a local file.

Options:
   /TYPE, /T Select type of memory dump (e.g. RAW or DMP) [default: DMP]
   /OUTPUT, /O Output file to be created. (optional)
   /QUIET, /Q Do not ask any questions. Proceed directly.
   /NOLYTICS, /N Do not send any usage analytics information to Comae Technologies. This is used to  
    improve our services.
   /NOJSON, /J Do not save a .json file containing metadata. Metadata are the basic information you will 
    need for the analysis.
   /LIVEKD, /L Enables live kernel debugging session.
   /COMPRESS, /R Compresses memory dump file.
   /APP, /A Specifies filename or complete path of debugger image to execute.
   /CMDLINE, /C Specifies debugger command-line options.
   /DRIVERNAME, /D Specifies the name of the installed device driver image.

要从命令行获取 Microsoft 崩溃转储的内存,并将输出保存到你选择的文件名中,可以使用/o/OUTPUT选项,如下所示:

C:\Comae-Toolkit-3.0.20180307.1\x64>DumpIt.exe /o memory.dmp

  DumpIt 3.0.20180307.1
  Copyright (C) 2007 - 2017, Matthieu Suiche <http://www.msuiche.net>
  Copyright (C) 2012 - 2014, MoonSols Limited <http://www.moonsols.com>
  Copyright (C) 2015 - 2017, Comae Technologies FZE <http://www.comae.io>

    Destination path: \??\C:\Comae-Toolkit-3.0.20180307.1\x64\memory.dmp

    Computer name:             PC

    --> Proceed with the acquisition ? [y/n] y

    [+] Information:
    Dump Type:                  Microsoft Crash Dump

    [+] Machine Information:
    Windows version: 6.1.7601
    MachineId: A98B4D56-9677-C6E4-03F5-902A1D102EED
    TimeStamp: 131666114153429014
    Cr3: 0x187000
    KdDebuggerData: 0xfffff80002c460a0
    Current date/time: [2018-03-27 (YYYY-MM-DD) 8:03:35 (UTC)]
    + Processing... Done.
    Acquisition finished at: [2018-03-27 (YYYY-MM-DD) 8:04:57 (UTC)]
    Time elapsed: 1:21 minutes:seconds (81 secs)
    Created file size: 8589410304 bytes (8191 Mb)
    Total physical memory size: 8191 Mb
    NtStatus (troubleshooting): 0x00000000
    Total of written pages: 2097022
    Total of inacessible pages: 0
    Total of accessible pages: 2097022
    SHA-256: 3F5753EBBA522EF88752453ACA1A7ECB4E06AEA403CD5A4034BCF037CA83C224
    JSON path: C:\Comae-Toolkit-3.0.20180307.1\x64\memory.json

要将内存作为原始内存转储而非默认的 Microsoft 崩溃转储,你可以使用/t/TYPE选项进行指定,如下所示:

C:\Comae-Toolkit-3.0.20180307.1\x64>DumpIt.exe /t RAW

  DumpIt 3.0.20180307.1
  Copyright (C) 2007 - 2017, Matthieu Suiche <http://www.msuiche.net>
  Copyright (C) 2012 - 2014, MoonSols Limited <http://www.moonsols.com>
  Copyright (C) 2015 - 2017, Comae Technologies FZE <http://www.comae.io>

  WARNING: RAW memory snapshot files are considered obsolete and as a legacy format.

  Destination path:  \??\C:\Comae-Toolkit-3.0.20180307.1\x64\memory.bin
  Computer name:             PC

  --> Proceed with the acquisition? [y/n] y

  [+] Information:
  Dump Type:                  Raw Memory Dump

  [+] Machine Information:
  Windows version:            6.1.7601
  MachineId:                  A98B4D56-9677-C6E4-03F5-902A1D102EED
  TimeStamp:                  131666117379826680
  Cr3:                        0x187000
  KdDebuggerData:             0xfffff80002c460a0
  Current date/time:          [2018-03-27 (YYYY-MM-DD) 8:08:57 (UTC)]

[.......REMOVED.........]

如果你希望从包含大量内存的服务器中获取内存,可以使用DumpIt中的/R/COMPRESS选项,这将创建一个.zdmpComae 压缩崩溃转储)文件,从而减少文件大小,并加快获取速度。然后可以使用 Comae Stardust 企业平台分析该转储文件(.zdmp):my.comae.io。更多细节,请参阅以下博客文章:blog.comae.io/rethinking-logging-for-critical-assets-685c65423dc0

在大多数情况下,你可以通过暂停虚拟机(Virtual Machine (VM))来获取内存。例如,在 VMware Workstation/VMware Fusion 上执行恶意软件样本后,你可以暂停虚拟机,这将把客户机的内存(RAM)写入主机磁盘上的一个.vmem扩展名的文件中。对于那些无法通过暂停获取内存的应用程序(如 VirtualBox),你可以在客户机内使用 DumpIt。

3. Volatility 概述

一旦获取了被感染系统的内存,下一步就是分析获取的内存镜像。Volatility (www.volatilityfoundation.org/releases) 是一个用 Python 编写的开源高级内存取证框架,允许你从内存镜像中分析和提取数字证据。Volatility 可以在各种平台上运行(Windows、macOS 和 Linux),并支持分析来自 32 位和 64 位版本的 Windows、macOS 和 Linux 操作系统的内存。

3.1 安装 Volatility

Volatility 以多种格式分发,并可从 www.volatilityfoundation.org/releases 下载。在撰写本书时,Volatility 的最新版本是 2.6。根据你打算在其上运行 Volatility 的操作系统,按照适当操作系统的安装过程进行安装。

3.1.1 Volatility 独立可执行文件

快速开始使用 Volatility 的方法是使用独立可执行文件。独立可执行文件适用于 Windows、macOS 和 Linux 操作系统。独立可执行文件的优点在于你无需安装 Python 解释器或 Volatility 依赖项,因为它已经打包了 Python 2.7 解释器和所有必需的依赖项。

在 Windows 上,一旦下载了独立可执行文件,你可以通过在命令行中执行带有 -h (--help) 选项的独立可执行文件来检查 Volatility 是否准备就绪,如下所示。帮助选项会显示 Volatility 中可用的各种选项和插件:

C:\volatility_2.6_win64_standalone>volatility_2.6_win64_standalone.exe -h
Volatility Foundation Volatility Framework 2.6
Usage: Volatility - A memory forensics analysis platform.

Options:
  -h, --help            list all available options and their default values.
                        Default values may be set in the configuration file
                        (/etc/volatilityrc)
  --conf-file=.volatilityrc
                        User based configuration file
  -d, --debug           Debug volatility
[.....REMOVED....]

同样地,你可以下载 Linux 或 macOS 的独立可执行文件,并通过在命令行中执行带有 -h(或 --help)选项的独立可执行文件来检查 Volatility 是否准备就绪,如下所示:

$ ./volatility_2.6_lin64_standalone -h

# ./volatility_2.6_mac64_standalone -h

3.1.2 Volatility 源码包

Volatility 也以源码包的形式分发;你可以在 Windows、macOS 或 Linux 操作系统上运行它。Volatility 依赖于各种插件来执行任务,其中一些插件依赖于第三方 Python 包。要运行 Volatility,你需要安装 Python 2.7 解释器及其依赖项。网页:github.com/volatilityfoundation/volatility/wiki/Installation#recommended-packages 包含了一些 Volatility 插件所需的第三方 Python 包的列表。你可以通过阅读文档来安装这些依赖项。一旦安装了所有依赖项,下载 Volatility 源代码包,解压缩并运行 Volatility,如下所示:

$ python vol.py -h
Volatility Foundation Volatility Framework 2.6
Usage: Volatility - A memory forensics analysis platform.

Options:
  -h, --help             list all available options and their default values.
                         Default values may be set in the configuration file
                         (/etc/volatilityrc)
  --conf-file=/root/.volatilityrc
                         User based configuration file
  -d, --debug            Debug volatility
[...REMOVED...]

本书中提到的所有示例都使用 Volatility Python 脚本(python vol.py)来自源代码包。你可以自由选择独立可执行文件,但请记住将 python vol.py 替换为独立可执行文件的名称。

3.2 使用 Volatility

Volatility 由多个插件组成,这些插件可以从内存镜像中提取不同的信息。python vol.py -h 选项显示支持的插件。例如,如果你希望列出内存镜像中的正在运行的进程,可以使用 pslist 插件,或者如果你希望列出网络连接,可以使用另一个插件。无论你使用哪个插件,都将使用以下命令语法。使用 -f,你可以指定内存镜像文件的路径,--profile 告诉 Volatility 内存镜像来自哪个系统和架构。根据你想从内存镜像中提取的信息类型,插件可能会有所不同:

$ python vol.py -f <memory image file> --profile=<PROFILE> <PLUGIN> [ARGS]

以下命令使用 pslist 插件列出从运行 Windows 7(32 位)服务包 1 获取的内存镜像中的正在运行的进程:

$ python vol.py -f mem_image.raw --profile=Win7SP1x86 pslist
Volatility Foundation Volatility Framework 2.6
Offset(V)  Name        PID PPID Thds Hnds  Sess Wow64  Start
---------- ---------- ---- ---- ---- ----  ---- ----- ---------------------
0x84f4a958 System         4    0  86  448  ----    0  2016-08-13 05:54:20
0x864284e0 smss.exe     272    4   2   29  ----    0  2016-08-13 05:54:20
0x86266030 csrss.exe    356  340   9  504     0    0  2016-08-13 05:54:22
0x86e0a1a0 wininit.exe  396  340   3   75     0    0  2016-08-13 05:54:22
0x86260bd0 csrss.exe    404  388  10  213     1    0  2016-08-13 05:54:22
0x86e78030 winlogon.exe 460  388   3  108     1    0  2016-08-13 05:54:22

[....REMOVED....]

有时,你可能不知道该为 Volatility 提供哪个配置文件。在这种情况下,你可以使用 imageinfo 插件,它会确定正确的配置文件。以下命令显示了 imageinfo 插件建议的多个配置文件;你可以使用任何一个建议的配置文件:

$ python vol.py -f mem_image.raw imageinfo
Volatility Foundation Volatility Framework 2.6
INFO    : volatility.debug    : Determining profile based on KDBG search...
          Suggested Profile(s): Win7SP1x86_23418, Win7SP0x86, Win7SP1x86
                    AS Layer1 : IA32PagedMemoryPae (Kernel AS)
                    AS Layer2 : FileAddressSpace (Users/Test/Desktop/mem_image.raw)
                     PAE type : PAE
                          DTB : 0x185000L
                         KDBG : 0x82974be8L
         Number of Processors : 1
    Image Type (Service Pack) : 0
               KPCR for CPU 0 : 0x82975c00L
            KUSER_SHARED_DATA : 0xffdf0000L
          Image date and time : 2016-08-13 06:00:43 UTC+0000
    Image local date and time : 2016-08-13 11:30:43 +0530

大多数 Volatility 插件,如 pslist,依赖于从 Windows 操作系统结构中提取信息。这些结构在不同版本的 Windows 中有所不同;配置文件(--profile)告诉 Volatility 使用哪些数据结构、符号和算法。

前面提到的帮助选项 -h (--help) 显示适用于所有 Volatility 插件的帮助。你也可以使用相同的 -h (--help) 选项来确定插件支持的各种选项和参数。要做到这一点,只需在插件名称旁输入 -h (--help)。以下命令显示 pslist 插件的帮助选项:

$ python vol.py -f mem_image.raw --profile=Win7SP1x86 pslist -h

到此,你应该已经了解如何在获取的内存镜像上运行 Volatility 插件,以及如何确定插件支持的各种选项。在接下来的章节中,你将学习不同插件的使用方法,以及如何使用它们从内存镜像中提取取证数据。

4. 列举进程

当你在调查内存镜像时,你主要关注的是识别系统中是否有可疑的进程正在运行。Volatility 提供了多种插件,允许你枚举进程。Volatility 的 pslist 插件列出了来自内存镜像的进程,类似于 任务管理器 在实时系统上列出进程的方式。在以下输出中,运行 pslist 插件对一个被恶意软件(Perseus)感染的内存镜像进行分析,显示了两个可疑的进程:svchost..exe (pid 3832) 和 suchost..exe (pid 3924)。这两个进程之所以可疑,是因为它们的进程名称在 .exe 扩展名之前多了一个 字符(这不正常)。在一个干净的系统中,你会发现多个 svchost.exe 进程正在运行。通过创建 svchost..exesuchost..exe 这样的进程,攻击者试图通过让这些进程看起来与合法的 svchost.exe 进程相似来掩盖其身份:

$ python vol.py -f perseus.vmem --profile=Win7SP1x86 pslist
Volatility Foundation Volatility Framework 2.6
Offset(V)  Name        PID  PPID  Thds Hnds Sess Wow64   Start 
---------- ----------- ---- ----- ---- ---- ---- ----- -------------------
0x84f4a8e8 System          4    0   88  475 ----   0   2016-09-23 09:21:47
0x8637b020 smss.exe      272    4    2   29 ----   0   2016-09-23 09:21:47
0x86c19310 csrss.exe     356  340    8  637    0   0   2016-09-23 09:21:49
0x86c13458 wininit.exe   396  340    3   75    0   0   2016-09-23 09:21:49
0x86e84a08 csrss.exe     404  388    9  191    1   0   2016-09-23 09:21:49
0x87684030 winlogon.exe  452  388    4  108    1   0   2016-09-23 09:21:49
0x86284228 services.exe  496  396   11  242    0   0   2016-09-23 09:21:49
0x876ab030 lsass.exe     504  396    9  737    0   0   2016-09-23 09:21:49
0x876d1a70 svchost.exe   620  496   12  353    0   0   2016-09-23 09:21:49
0x864d36a8 svchost.exe   708  496    6  302    0   0   2016-09-23 09:21:50
0x86b777c8 svchost.exe   760  496   24  570    0   0   2016-09-23 09:21:50
0x8772a030 svchost.exe   852  496   28  513    0   0   2016-09-23 09:21:50
0x87741030 svchost.exe   920  496   46 1054    0   0   2016-09-23 09:21:50
0x877ce3c0 spoolsv.exe  1272  496   15  338    0   0   2016-09-23 09:21:50
0x95a06a58 svchost.exe  1304  496   19  306    0   0   2016-09-23 09:21:50
0x8503f0e8 svchost..exe 3832 3712   11  303    0   0   2016-09-23 09:24:55
0x8508bb20 suchost..exe 3924 3832   11  252    0   0   2016-09-23 09:24:55
0x861d1030 svchost.exe  3120  496   12  311    0   0   2016-09-23 09:25:39

[......REMOVED..............]

运行 Volatility 插件很容易;你可以在不理解其工作原理的情况下运行插件。理解插件的工作原理将帮助你评估结果的准确性,也将帮助你在攻击者使用隐蔽技术时选择正确的插件。那么问题是,pslist 是如何工作的呢?要理解这一点,首先需要了解什么是进程,以及 Windows 内核 是如何跟踪进程的。

4.1 过程概述

进程 是一个对象。Windows 操作系统是基于对象的(不要与面向对象语言中使用的对象一词混淆)。对象指的是系统资源,例如进程、文件、设备、目录、突变体等,它们由内核中的一个组件——对象管理器 管理。为了了解 Windows 上所有的对象类型,你可以使用 WinObj 工具docs.microsoft.com/en-us/sysinternals/downloads/winobj)。要查看 WinObj 中的对象类型,作为管理员启动 WinObj,然后在左侧窗格中点击 ObjectTypes,这将显示所有 Windows 对象。

对象(如进程、文件、线程等)在 C 中表示为结构。 这意味着进程对象有一个与之关联的结构,该结构称为_EPROCESS结构。 _EPROCESS结构位于内核内存中,Windows 内核使用EPROCESS结构来内部表示一个进程。 _EPROCESS结构包含与进程相关的各种信息,例如进程的名称进程 ID父进程 ID与进程关联的线程数进程的创建时间等。 现在,返回到pslist输出,并注意为特定进程显示了什么类型的信息。 例如,如果查看pslist输出的第二个条目,它显示了smss.exe进程的名称,其进程 ID 为(272),父进程 ID 为4,等等。 正如您可能已经猜到的那样,与进程相关的信息来自其_EPROCESS结构。

4.1.1 检查 _EPROCESS 结构

要检查_EPROCESS结构及其包含的信息类型,您可以使用诸如WinDbg之类的内核调试器。 WinDbg有助于探索和理解操作系统数据结构,这通常是内存取证的重要方面。 要安装WinDbg,您需要安装"Windows 调试工具"包,该包作为Microsoft SDK的一部分(有关不同的安装类型,请参考docs.microsoft.com/en-us/windows-hardware/drivers/debugger/index)。 安装完成后,您可以在安装目录中找到WinDbg.exe(在我的情况下,它位于C:\Program Files (x86)\Windows Kits\8.1\Debuggers\x64)。 接下来,从Sysinternalsdocs.microsoft.com/en-us/sysinternals/downloads/livekd)下载LiveKD实用程序,解压缩并将livekd.exe复制到WinDbg的安装目录中。 LiveKD使您能够在实时系统上执行本地内核调试。 要通过livekd启动WinDbg,打开命令提示符(作为管理员),导航到WinDbg 安装目录,并运行livekd-w开关,如下所示。 您还可以将Windbg安装目录添加到路径环境变量中,以便您可以从任何路径启动LiveKD

C:\Program Files (x86)\Windows Kits\8.1\Debuggers\x64>livekd -w

livekd -w命令会自动启动Windbg,加载符号,并为您提供一个准备接受命令的kd>提示符,如下面的屏幕截图所示。 要探索数据结构(如_EPROCESS),您将在命令提示符旁边键入适当的命令:

现在,回到我们对_EPROCESS结构的讨论,为了探索_EPROCESS结构,我们将使用Display Type命令(dt)dt命令可用于探索表示变量、结构或联合体的符号。在以下输出中,dt命令用于显示在nt模块(内核执行文件的名称)中定义的_EPROCESS结构。EPROCESS结构包含多个字段,存储进程的各种元数据。以下是 64 位 Windows 7 系统的示例(为了保持简洁,已删除部分字段):

kd> dt nt!_EPROCESS
   +0x000 Pcb : _KPROCESS
   +0x160 ProcessLock : _EX_PUSH_LOCK
   +0x168 CreateTime : _LARGE_INTEGER
   +0x170 ExitTime : _LARGE_INTEGER
   +0x178 RundownProtect : _EX_RUNDOWN_REF
   +0x180 UniqueProcessId : Ptr64 Void
   +0x188 ActiveProcessLinks : _LIST_ENTRY
   +0x198 ProcessQuotaUsage : [2] Uint8B
   +0x1a8 ProcessQuotaPeak : [2] Uint8B
   [REMOVED]
   +0x200 ObjectTable : Ptr64 _HANDLE_TABLE
   +0x208 Token : _EX_FAST_REF
   +0x210 WorkingSetPage : Uint8B
   +0x218 AddressCreationLock : _EX_PUSH_LOCK
   [REMOVED]
   +0x290 InheritedFromUniqueProcessId : Ptr64 Void
   +0x298 LdtInformation : Ptr64 Void
   +0x2a0 Spare : Ptr64 Void
   [REMOVED]
   +0x2d8 Session : Ptr64 Void
   +0x2e0 ImageFileName : [15] UChar
   +0x2ef PriorityClass : UChar
   [REMOVED]

以下是我们将在本讨论中使用的_EPROCESS结构中一些有趣的字段:

  • CreateTime: 时间戳,指示进程首次启动的时间

  • ExitTime: 时间戳,指示进程退出的时间

  • UniqueProcessID: 一个整数,引用*进程 ID(PID)*的进程

  • ActiveProcessLinks: 一个双向链表,链接系统上所有正在运行的活动进程

  • InheritedFromUniqueProcessId: 一个整数,指定父进程的 PID

  • ImageFileName: 一个包含 16 个 ASCII 字符的数组,存储进程可执行文件的名称

了解如何检查_EPROCESS结构后,现在让我们来看看特定进程的_EPROCESS结构。为此,让我们首先使用WinDbg列出所有活动进程。您可以使用!process扩展命令打印特定进程或所有进程的元数据。在以下命令中,第一个参数0列出所有进程的元数据。您还可以通过指定_EPROCESS结构的地址来显示单个进程的信息。第二个参数表示详细程度:

kd> !process 0 0
**** NT ACTIVE PROCESS DUMP ****
PROCESS fffffa806106cb30
    SessionId: none Cid: 0004 Peb: 00000000 ParentCid: 0000
    DirBase: 00187000 ObjectTable: fffff8a0000016d0 HandleCount: 539.
    Image: System

PROCESS fffffa8061d35700
    SessionId: none Cid: 00fc Peb: 7fffffdb000 ParentCid: 0004
    DirBase: 1faf16000 ObjectTable: fffff8a0002d26b0 HandleCount: 29.
    Image: smss.exe

PROCESS fffffa8062583b30
    SessionId: 0 Cid: 014c Peb: 7fffffdf000 ParentCid: 0144
    DirBase: 1efb70000 ObjectTable: fffff8a00af33ef0 HandleCount: 453.
    Image: csrss.exe

[REMOVED]

有关 WinDbg 命令的详细信息,请参考位于 WinDbg 安装文件夹中的 Debugger.chm 帮助。您还可以参考以下在线资源:windbg.info/doc/1-common-cmds.htmlwindbg.info/doc/2-windbg-a-z.html

从前面的输出中,让我们来看第二项,描述的是 smss.exe。在 PROCESS 旁边的地址 fffffa8061d35700 是与此 smss.exe 实例关联的 _EPROCESS 结构的地址。Cid 字段的值为 00fc(十进制为 252),即进程 ID,ParentCid 字段的值为 0004,表示父进程的进程 ID。你可以通过检查 smss.exe_EPROCESS 结构中的字段值来验证这一点。你可以在 Display Type (dt) 命令的后面加上 _EPROCESS 结构的地址,如下代码所示。在以下输出中,注意字段 UniqueProcessId(进程 ID)、InheritedFromUniqueProcessId(父进程 ID)和 ImageFileName(进程可执行文件名)的值。这些值与之前从 !process 0 0 命令中获得的结果相匹配:

kd> dt nt!_EPROCESS fffffa8061d35700
   +0x000 Pcb : _KPROCESS
   +0x160 ProcessLock : _EX_PUSH_LOCK
   +0x168 CreateTime : _LARGE_INTEGER 0x01d32dde`223f3e88
   +0x170 ExitTime : _LARGE_INTEGER 0x0
   +0x178 RundownProtect : _EX_RUNDOWN_REF
   +0x180 UniqueProcessId : 0x00000000`000000fc Void
   +0x188 ActiveProcessLinks : _LIST_ENTRY [ 0xfffffa80`62583cb8 - 0xfffffa80`6106ccb8 ]
   +0x198 ProcessQuotaUsage : [2] 0x658
   [REMOVED]
   +0x290 InheritedFromUniqueProcessId : 0x00000000`00000004 Void
   +0x298 LdtInformation : (null) 
   [REMOVED]
   +0x2d8 Session : (null) 
   +0x2e0 ImageFileName : [15] "smss.exe"
   +0x2ef PriorityClass : 0x2 ''
   [REMOVED]

到目前为止,我们知道操作系统将所有关于进程的元数据保存在 _EPROCESS 结构中,该结构存在于内核内存中。这意味着,如果你能找到某个特定进程的 _EPROCESS 结构的地址,就能获取到关于该进程的所有信息。那么,问题是,如何获取系统中所有正在运行的进程的信息呢?为此,我们需要了解 Windows 操作系统是如何追踪活动进程的。

4.1.2 理解 ActiveProcessLinks

Windows 使用一个圆形双向链表来追踪所有活动的进程,每个节点都是一个 _EPROCESS 结构。_EPROCESS 结构中包含一个名为 ActiveProcessLinks 的字段,类型是 LIST_ENTRY_LIST_ENTRY 是另一种结构,包含两个成员,如下代码所示。Flink(正向链接)指向下一个 _EPROCESS 结构的 _LIST_ENTRY,而 Blink(反向链接)指向前一个 _EPROCESS 结构的 _LIST_ENTRY

kd> dt nt!_LIST_ENTRY
   +0x000 Flink : Ptr64 _LIST_ENTRY
   +0x008 Blink : Ptr64 _LIST_ENTRY

FlinkBlink 一起构成了一个进程对象链表,可以像下面这样可视化:

一个需要注意的点是,FlinkBlink 并不指向 _EPROCESS 结构的开始位置。Flink 指向下一个 _EPROCESS 结构的 _LIST_ENTRY 结构的起始位置(第一个字节),而 Blink 指向前一个 _EPROCESS 结构的 _LIST_ENTRY 结构的第一个字节。之所以重要,是因为一旦你找到了某个进程的 _EPROCESS 结构,你可以通过 Flink 向前(正向)遍历双向链表,或者通过 Blink 向后(反向)遍历链表,然后减去偏移量来获取下一个或上一个进程的 _EPROCESS 结构的开始位置。为了帮助你理解这意味着什么,我们来看一下 smss.exe_EPROCESS 结构中 FlinkBlink 字段的值:

kd> dt -b -v nt!_EPROCESS fffffa8061d35700
struct _EPROCESS, 135 elements, 0x4d0 bytes
.....
   +0x180 UniqueProcessId : 0x00000000`000000fc 
   +0x188 ActiveProcessLinks : struct _LIST_ENTRY, 2 elements, 0x10 bytes
 [ 0xfffffa80`62583cb8 - 0xfffffa80`6106ccb8 ]
      +0x000 Flink : 0xfffffa80`62583cb8 
      +0x008 Blink : 0xfffffa80`6106ccb8

Flink的值为0xfffffa8062583cb8;这是下一个_EPROCESS结构的ActiveProcessLinksFlink)的起始地址。由于在我们的示例中,ActiveProcessLinks位于_EPROCESS结构起始处的偏移量0x188,你可以通过从Flink值中减去0x188来到达下一个进程的_EPROCESS结构的起始位置。在以下输出中,注意通过减去0x188我们到达了下一个进程的_EPROCESS结构,即csrss.exe

kd> dt nt!_EPROCESS (0xfffffa8062583cb8-0x188)
   +0x000 Pcb : _KPROCESS
   +0x160 ProcessLock : _EX_PUSH_LOCK
   [REMOVED]
   +0x180 UniqueProcessId : 0x00000000`0000014c Void
   +0x188 ActiveProcessLinks : _LIST_ENTRY [ 0xfffffa80`625acb68 - 0xfffffa80`61d35888 ]
   +0x198 ProcessQuotaUsage : [2] 0x2c18
   [REMOVED]
   +0x288 Win32WindowStation : (null) 
   +0x290 InheritedFromUniqueProcessId : 0x00000000`00000144 Void
   [REMOVED]
   +0x2d8 Session : 0xfffff880`042ae000 Void
   +0x2e0 ImageFileName : [15] "csrss.exe"
   +0x2ef PriorityClass : 0x2 ''

如你所见,通过遍历双向链表,可以列出系统上所有正在运行的活动进程的信息。在实时系统中,像任务管理器Process Explorer这样的工具使用 API 函数,这些函数最终依赖于找到并遍历存在于内核内存中的同一个_EPROCESS结构的双向链表。pslist插件也包含了查找和遍历来自内存镜像的相同_EPROCESS结构双向链表的逻辑。为此,pslist插件找到一个名为_PsActiveProcessHead的符号,该符号定义在ntoskrnl.exe(或ntkrnlpa.exe)中。该符号指向_EPROCESS结构双向链表的起始位置;然后,pslist遍历_EPROCESS结构的双向链表来列出所有正在运行的进程。

有关本书中所涉及的 Volatility 插件的详细工作原理和逻辑,请参阅 Michael Hale Ligh、Andrew Case、Jamie Levy 和 Aaron Walters 的《内存取证艺术:检测 Windows、Linux 和 Mac 内存中的恶意软件和威胁》(The Art of Memory Forensics: Detecting Malware and Threats in Windows, Linux, and Mac Memory)。

如前所述,像pslist这样的插件支持多种选项和参数;通过在插件名称后输入-h (--help)可以显示这些选项。pslist的一个选项是--output-file。你可以使用此选项将pslist的输出重定向到文件,如下所示:

$ python vol.py -f perseus.vmem --profile=Win7SP1x86 pslist --output-file=pslist.txt

另一个选项是-p (--pid)。使用此选项,如果你知道某个进程的进程 IDPID),你可以获取该进程的信息:

$ python vol.py -f perseus.vmem --profile=Win7SP1x86 pslist -p 3832
Volatility Foundation Volatility Framework 2.6
Offset(V) Name          PID  PPID Thds Hnds Wow64 Start
---------- ------------ ---- ---- ---- ---- ----- -------------------
0x8503f0e8 svchost..exe 3832 3712  11   303   0   2016-09-23 09:24:55

4.2 使用 psscan 列出进程

psscan是另一个列出系统上运行进程的 Volatility 插件。与pslist不同,psscan并不遍历_EPROCESS对象的双向链表。相反,它扫描物理内存以查找进程对象的特征码。换句话说,psscan使用与pslist插件不同的方法来列出进程。你可能会想,当pslist插件也能做同样的事情时,为什么还需要psscan插件?答案就在于psscan使用的技术。由于它采用的方式,它可以检测已终止的进程和隐藏的进程。攻击者可以隐藏进程,从而防止取证分析师在实时取证过程中发现恶意进程。那么,问题是,攻击者如何隐藏一个进程呢?要理解这一点,你需要了解一种攻击技术,称为DKOM(直接内核对象操控)

4.2.1 直接内核对象操控(DKOM)

DKOM是一种涉及修改内核数据结构的技术。通过使用 DKOM,可以隐藏一个进程或驱动程序。为了隐藏一个进程,攻击者找到他/她想要隐藏的恶意进程的_EPROCESS结构,并修改ActiveProcessLinks字段。具体来说,前一个_EPROCESS块的Flink被指向下一个_EPROCESS块的Flink,而下一个_EPROCESS块的Blink则指向前一个_EPROCESS块的Flink。由于这一操作,恶意进程相关的_EPROCESS块被从双向链表中解除链接(如图所示):

通过解除链接一个进程,攻击者可以使恶意进程对依赖遍历双向链表来枚举活动进程的实时取证工具隐藏。如你所猜测的,这种技术同样会使恶意进程对pslist插件隐藏(该插件也依赖遍历双向链表)。以下是一个被prolaco rootkit 感染的系统的pslistpsscan输出,prolaco rootkit 通过执行DKOM来隐藏一个进程。为了简洁起见,以下输出中的一些条目已被截断。当你对比pslistpsscan的输出时,会注意到psscan输出中有一个额外的进程nvid.exepid 1700),但在pslist中并未出现:

$ python vol.py -f infected.vmem --profile=WinXPSP3x86 pslist
Volatility Foundation Volatility Framework 2.6
Offset(V)  Name         PID  PPID Thds Hnds Sess Wow64  Start
--------- ------------- ---- ---- ---- ---- ---- ----- -------------------
0x819cc830 System          4    0 56   256  ----    0
0x814d8380 smss.exe      380    4  3    19  ----    0  2014-06-11 14:49:36
0x818a1868 csrss.exe     632  380 11   423     0    0  2014-06-11 14:49:36
0x813dc1a8 winlogon.exe  656  380 24   524     0    0  2014-06-11 14:49:37
0x81659020 services.exe  700  656 15   267     0    0  2014-06-11 14:49:37
0x81657910 lsass.exe     712  656 24   355     0    0  2014-06-11 14:49:37
0x813d7688 svchost.exe   884  700 21   199     0    0  2014-06-11 14:49:37
0x818f5d10 svchost.exe   964  700 10   235     0    0  2014-06-11 14:49:38
0x813cf5a0 svchost.exe  1052  700 84  1467     0    0  2014-06-11 14:49:38
0x8150b020 svchost.exe  1184  700 16   211     0    0  2014-06-11 14:49:40
0x81506c68 spoolsv.exe  1388  700 15   131     0    0  2014-06-11 14:49:40
0x81387710 explorer.exe 1456 1252 16   459     0    0  2014-06-11 14:49:55
$ python vol.py -f infected.vmem --profile=WinXPSP3x86 psscan
 Volatility Foundation Volatility Framework 2.6
 Offset(P)          Name         PID  PPID  PDB       Time created
 ------------------ ------------ ---- ---- ---------- -------------------
 0x0000000001587710 explorer.exe 1456 1252 0x08440260 2014-06-11 14:49:55
 0x00000000015cf5a0 svchost.exe  1052  700 0x08440120 2014-06-11 14:49:38
 0x00000000015d7688 svchost.exe   884  700 0x084400e0 2014-06-11 14:49:37
 0x00000000015dc1a8 winlogon.exe  656  380 0x08440060 2014-06-11 14:49:37
 0x00000000016ba360 nvid.exe     1700 1660 0x08440320 2014-10-17 09:16:10
 0x00000000016d8380 smss.exe      380    4 0x08440020 2014-06-11 14:49:36
 0x0000000001706c68 spoolsv.exe  1388  700 0x084401a0 2014-06-11 14:49:40
 0x000000000170b020 svchost.exe  1184  700 0x08440160 2014-06-11 14:49:40
 0x0000000001857910 lsass.exe     712  656 0x084400a0 2014-06-11 14:49:37
 0x0000000001859020 services.exe  700  656 0x08440080 2014-06-11 14:49:37
 0x0000000001aa1868 csrss.exe     632  380 0x08440040 2014-06-11 14:49:36
 0x0000000001af5d10 svchost.exe   964  700 0x08440100 2014-06-11 14:49:38
 0x0000000001bcc830 System          4    0 0x00319000

如前所述,psscan能够检测隐藏进程的原因在于它使用了不同的技术来列出进程,即池标签扫描

4.2.2 理解池标签扫描

如果你还记得,我之前提到过将进程、文件、线程等系统资源称为对象(或执行对象)。这些执行对象由内核中的一个组件——对象管理器来管理。每个执行对象都有一个与之相关的结构(如进程对象的_EPROCESS)。执行对象结构前面有一个_OBJECT_HEADER结构,包含有关对象类型的信息以及一些引用计数。_OBJECT_HEADER结构前面可能会有零个或多个可选头结构。换句话说,你可以将对象视为执行对象结构、对象头和可选头的组合,如下图所示:

存储对象需要内存,而这些内存由 Windows 内存管理器从内核池中分配。内核池是一个可以被分割成更小块的内存范围,用于存储诸如对象之类的数据。内核池被划分为分页池(其内容可以被交换到磁盘)和非分页池(其内容永久驻留在内存中)。对象(如进程和线程)被保存在内核的非分页池中,这意味着它们将始终驻留在物理内存中。

当 Windows 内核接收到创建对象的请求时(可能是由于进程调用了如CreateProcessCreateFile的 API),内存将被分配给该对象,可能来自分页池或非分页池(具体取决于对象类型)。这个分配通过在对象前面加上一个_POOL_HEADER结构来标记,以便在内存中,每个对象都会有一个可预测的结构,类似于下面的截图所示。_POOL_HEADER结构包含一个名为PoolTag的字段,存储一个四字节的标签(称为池标签)。这个池标签可以用来识别对象。对于进程对象,标签是Proc,对于文件对象,标签是File,依此类推。_POOL_HEADER结构还包含一些字段,告诉内存分配的大小以及它所描述的内存类型(分页池非分页池):

你可以将所有驻留在内核内存的非分页池中的进程对象(最终映射到物理内存)视为带有Proc标签的对象。正是这个标签,Volatility 的psscan使用它作为起点来识别进程对象。具体来说,它扫描物理内存中的Proc标签,以识别与进程对象相关联的池标签分配,并通过使用更强大的签名和启发式方法进一步确认。一旦psscan找到进程对象,它会从其_EPROCESS结构中提取必要的信息。psscan会重复这一过程,直到找到所有的进程对象。事实上,许多 Volatility 插件依赖于池标签扫描来识别并提取内存镜像中的信息。

psscan 插件不仅可以检测隐藏的进程,还能通过其使用的方法检测已终止的进程。当一个对象被销毁(例如当进程被终止时),包含该对象的内存分配会被释放回内核池,但内存中的内容不会立即被覆盖,这意味着进程对象仍然可能保留在内存中,除非该内存被分配用于其他用途。如果包含已终止进程对象的内存没有被覆盖,那么 psscan 可以检测到已终止的进程。

有关池标签扫描的详细信息,请参阅 Andreas Schuster 的论文《在 Microsoft Windows 内存转储中搜索进程和线程》或阅读书籍《记忆取证的艺术》。

此时,你应该已经了解了 Volatility 插件是如何工作的;大多数插件使用类似的逻辑。总的来说,关键信息存在于由内核维护的数据结构中。插件依赖于从这些数据结构中查找并提取信息。查找和提取取证信息的方法有所不同;有些插件依赖于遍历双向链表(例如 pslist),有些则使用池标签扫描技术(例如 psscan)来提取相关信息。

4.3 确定进程关系

在检查进程时,确定进程之间的父子关系是很有用的。在恶意软件调查过程中,这有助于你了解哪些其他进程与恶意进程相关联。pstree 插件通过使用 pslist 的输出并以树状视图格式化,显示父子进程关系。在以下示例中,运行 pstree 插件对感染的内存镜像进行分析,显示了进程关系;子进程向右缩进并以点号表示。通过输出,你可以看到 OUTLOOK.EXE 是由 explorer.exe 进程启动的。这是正常的,因为每当你通过双击启动一个应用程序时,启动该应用程序的正是资源管理器。OUTLOOK.EXE (pid 4068) 启动了 EXCEL.EXE (pid 1124),而后者又调用了 cmd.exe (pid 4056) 来执行恶意进程 doc6.exe (pid 2308)。通过查看这些事件,你可以推测用户打开了通过电子邮件发送的恶意 Excel 文档,该文档可能利用了一个漏洞或执行了宏代码,从而下载并通过 cmd.exe 执行了恶意软件。

$ python vol.py -f infected.raw --profile=Win7SP1x86 pstree
Volatility Foundation Volatility Framework 2.6
Name                      Pid  PPid Thds Hnds Time
------------------------ ---- ----- ---- ---- -------------------
[REMOVED]
0x86eb4780:explorer.exe   1608 1572  35   936 2016-05-11 12:15:10
. 0x86eef030:vmtoolsd.exe 1708 1608   5   160 2016-05-11 12:15:10
. 0x851ee2b8:OUTLOOK.EXE  4068 1608  17  1433 2018-04-15 02:14:23
.. 0x8580a3f0:EXCEL.EXE   1124 4068  11   377 2018-04-15 02:14:35
... 0x869d1030:cmd.exe    4056 1124   5   117 2018-04-15 02:14:41
.... 0x85b02d40:doc6.exe  2308 4056   1    50 2018-04-15 02:14:59

由于 pstree 插件依赖于 pslist 插件,因此无法列出隐藏或已终止的进程。另一种确定进程关系的方法是使用 psscan 插件生成父子关系的可视化表示。以下 psscan 命令以 dot 格式打印输出,然后可以使用图形可视化软件(如 Graphviz www.graphviz.org/XDot)打开(可以通过在 Linux 系统上运行 sudo apt install xdot 来安装 XDot):

$ python vol.py -f infected.vmem --profile=Win7SP1x86 psscan --output=dot --output-file=infected.dot

使用 XDot 打开 infected.dot 文件,显示之前讨论的进程之间的关系:

4.4 使用 psxview 列出进程

之前,你已经看到如何操控进程列表来隐藏进程;你还理解了 psscan 如何通过池标签扫描来检测隐藏的进程。事实证明,_POOL_HEADERpsscan 依赖的字段)仅用于调试目的,并不会影响操作系统的稳定性。这意味着攻击者可以安装一个内核驱动程序,运行在内核空间,并修改池标签或 _POOL_HEADER 中的任何其他字段。通过修改池标签,攻击者可以防止依赖 池标签扫描 的插件正常工作。换句话说,通过修改池标签,攻击者可以将进程从 psscan 中隐藏。为了解决这个问题,psxview 插件依赖于从不同来源提取进程信息。它通过七种不同的方式列举进程。通过比较来自不同来源的输出,可以检测到恶意软件引起的差异。在下面的截图中,psxview 使用七种不同的技术列出了进程。每个进程的信息以单独的行显示,使用的技术显示为包含 TrueFalse 的列。某一列下的 False 值表示该进程未通过相应方法找到。在以下输出中,psxview 使用所有方法检测到隐藏的进程 nvid.exepid 1700),除了 pslist 方法:

在前面的截图中,你会注意到一些进程显示为“假”值。例如,cmd.exe 进程除 psscan 方法外,在其他任何方法中都不存在。你可能会认为 cmd.exe 被隐藏了,但事实并非如此;你看到 False 的原因是 cmd.exe 已经终止(你可以从 ExitTime 列看出这一点)。因此,所有其他技术都未能找到它,而 psscan 方法能找到它,因为池标签扫描能够检测已终止的进程。换句话说,某一列中的 False 值并不一定意味着该进程对该方法是隐藏的;它也可能意味着这种情况是预期中的(取决于该方法如何以及从哪里获取进程信息)。要知道这是否是预期的,你可以使用 -R (--apply-rules) 选项,如下所示。在下面的截图中,注意 False 值是如何被替换为 Okay 的**。** Okay 表示 False,但这是预期的行为。在使用 -R--apply-rules)运行 psxview 插件后,如果你仍然看到 False 值(例如以下截图中的 nvid.exepid 1700),那么这强烈表明该进程对该方法是隐藏的:

5. 列出进程句柄

在调查过程中,一旦锁定了一个恶意进程,你可能希望了解该进程正在访问哪些对象(如进程、文件、注册表项等)。这将帮助你了解与恶意软件相关的组件以及它们的操作方式。例如,一个键盘记录器可能正在访问一个日志文件以记录捕获的按键,或者恶意软件可能已经打开了一个配置文件的句柄。

要访问一个对象,进程首先需要通过调用 CreateFileCreateMutex 等 API 来打开该对象的句柄。一旦打开了对象的句柄,它就可以使用该句柄执行后续操作,如写入文件或读取文件。句柄是对象的间接引用;可以把句柄看作是代表一个对象的东西(句柄不是对象本身)。这些对象存在于内核内存中,而进程运行在用户空间中,因此进程无法直接访问对象,它通过使用句柄来代表该对象。

每个进程都有一个私有的句柄表,存储在内核内存中。这个表包含与进程相关联的所有内核对象,例如文件、进程和网络套接字。问题是,这个表是如何填充的?当内核接收到来自进程的请求以创建对象(通过如 CreateFile 等 API),该对象将在 内核内存 中创建。指向该对象的指针会放入进程句柄表中的第一个可用槽中,并将相应的索引值返回给进程。该索引值就是表示该对象的句柄,进程会使用该句柄执行后续操作。

在一个实时系统中,你可以使用 Process Hacker 工具检查特定进程访问的内核对象。为此,启动 Process Hacker 并以 管理员 身份运行,右键单击任意进程,然后选择 Handles 标签。以下截图显示了 csrss.exe 进程的进程句柄。csrss.exe 是一个合法的操作系统进程,参与每个进程和线程的创建。因此,你会看到 csrss.exe 打开了大多数进程(除了它自己和它的父进程)的句柄。以下截图中,第三列是 句柄值,第四列显示了 对象的地址 在内核内存中的位置。例如,第一个进程 wininit.exe 位于内核内存地址 0x8705c410(它的 _EPROCESS 结构的地址),表示该对象的句柄值是 0x60

psxview 插件使用的方法之一依赖于遍历 csrss.exe 进程的句柄表来识别进程对象。如果存在多个 csrss.exe 实例,psxview 将解析所有 csrss.exe 实例的句柄表,以列出正在运行的进程,除了 csrss.exe 进程及其父进程(smss.exesystem 进程)。

从内存镜像中,你可以通过 handles 插件获取所有被进程访问的内核对象的列表。以下截图展示了 pid 356 进程的句柄。如果你在没有 -p 选项的情况下运行 handles 插件,它将显示所有进程的句柄信息:

你还可以使用 -t 选项来过滤特定对象类型(如 FileKeyProcessMutant 等)的结果。在以下示例中,handles 插件被运行在一个感染了 Xtreme RAT 的内存镜像上。handles 插件被用来列出恶意进程(pid 1772)打开的互斥体。从以下输出中,你可以看到 Xtreme RAT 创建了一个名为 oZ694XMhk6yxgbTA0 的互斥体,以标记其在系统中的存在。像 Xtreme RAT 创建的这样的互斥体可以作为一个很好的主机基础指示器,用于主机基础监控:

$ python vol.py -f xrat.vmem --profile=Win7SP1x86 handles -p 1772 -t Mutant
Volatility Foundation Volatility Framework 2.6
Offset(V)  Pid  Handle Access   Type    Details
---------- ---- ------ -------- ------ -----------------------------  
0x86f0a450 1772 0x104  0x1f0001 Mutant oZ694XMhk6yxgbTA0
0x86f3ca58 1772 0x208  0x1f0001 Mutant _!MSFTHISTORY!_
0x863ef410 1772 0x280  0x1f0001 Mutant WininetStartupMutex
0x86d50ca8 1772 0x29c  0x1f0001 Mutant WininetConnectionMutex
0x8510b8f0 1772 0x2a0  0x1f0001 Mutant WininetProxyRegistryMutex
0x861e1720 1772 0x2a8  0x100000 Mutant RasPbFile
0x86eec520 1772 0x364  0x1f0001 Mutant ZonesCounterMutex
0x86eedb18 1772 0x374  0x1f0001 Mutant ZoneAttributeCacheCounterMutex

在以下的内存镜像示例中,感染了TDL3 rootkitsvchost.exe进程(pid 880)打开了恶意 DLL 文件和与 rootkit 相关的内核驱动程序的文件句柄:

$ python vol.py -f tdl3.vmem handles -p 880 -t File
Volatility Foundation Volatility Framework 2.6
Offset(V)  Pid Handle Access   Type  Details
---------- --- ------ -------- ---- ----------------------------
0x89406028 880 0x50   0x100001 File  \Device\KsecDD
0x895fdd18 880 0x100  0x100000 File  \Device\Dfs
[REMOVED]
0x8927b9b8 880 0x344  0x120089 File [REMOVED]\system32\TDSSoiqh.dll
0x89285ef8 880 0x34c  0x120089 File [REMOVED]\system32\drivers\TDSSpqxt.sys

6. 列出 DLL 文件

在本书中,你已经看到了恶意软件使用 DLL 实现恶意功能的例子。因此,除了调查进程外,你可能还需要检查加载的库列表。要列出已加载的模块(可执行文件和 DLL),你可以使用 Volatility 的 dlllist 插件。dlllist 插件还显示与进程相关联的完整路径。让我们以名为Ghost RAT的恶意软件为例。它将恶意功能实现为Service DLL,因此,恶意 DLL 被 svchost.exe 进程加载(有关 Service DLL 的更多信息,请参阅第七章,恶意软件功能和持久性)。以下是 dlllist 的输出,其中可以看到由 svchost.exe 进程(pid 800)加载的一个具有非标准扩展名 (.ddf) 的可疑模块。第一列,Base,指定了基地址,即模块加载在内存中的地址:

$ python vol.py -f ghost.vmem --profile=Win7SP1x86 dlllist -p 880
Volatility Foundation Volatility Framework 2.6
******************************************************************
svchost.exe pid: 880
Command line : C:\Windows\system32\svchost.exe -k netsvcs

Base       Size     LoadCount Path
---------- -------- --------- --------------------------------
0x00f30000 0x8000   0xffff    C:\Windows\system32\svchost.exe
0x76f60000 0x13c000 0xffff    C:\Windows\SYSTEM32\ntdll.dll
0x75530000 0xd4000  0xffff    C:\Windows\system32\kernel32.dll
0x75160000 0x4a000  0xffff    C:\Windows\system32\KERNELBASE.dll
0x75480000 0xac000  0xffff    C:\Windows\system32\msvcrt.dll
0x77170000 0x19000  0xffff    C:\Windows\SYSTEM32\sechost.dll
0x76700000 0x15c000 0x62      C:\Windows\system32\ole32.dll
0x76c30000 0x4e000  0x19c     C:\Windows\system32\GDI32.dll
0x770a0000 0xc9000  0x1cd     C:\Windows\system32\USER32.dll
[REMOVED]
0x74fe0000 0x4b000  0xffff    C:\Windows\system32\apphelp.dll
0x6bbb0000 0xf000   0x1       c:\windows\system32\appinfo.dll
0x10000000 0x26000  0x1       c:\users\test\application data\acdsystems\acdsee\imageik.ddf
0x71200000 0x32000  0x3       C:\Windows\system32\WINMM.dll

dlllist 插件从名为进程环境块(PEB)的结构中获取有关加载模块的信息。如果你回想一下第八章,代码注入与钩子,在讲解进程内存组件时,我提到过PEB 结构位于进程内存中(用户空间)。PEB 包含关于进程可执行文件加载位置、磁盘上的完整路径以及已加载模块(可执行文件和 DLL)的元数据。dlllist 插件查找每个进程的PEB 结构并获取上述信息。那么,问题是,如何找到 PEB 结构呢?_EPROCESS 结构有一个名为 Peb 的字段,里面包含指向PEB的指针。这意味着,一旦插件找到了 _EPROCESS 结构,它就可以找到PEB。需要记住的一点是,_EPROCESS 位于内核内存(内核空间),而 PEB 位于进程内存(用户空间)。

要在调试器中获取PEB的地址,你可以使用 !process 扩展命令,该命令显示 _EPROCESS 结构的地址。它还指定了PEB的地址。从以下输出中可以看到,explorer.exe 进程的 PEB 位于其进程内存中的地址 7ffd3000,而其 _EPROCESS 结构位于 0x877ced28(在其内核内存中):

kd> !process 0 0
**** NT ACTIVE PROCESS DUMP ****
.........
PROCESS 877cb4a8 SessionId: 1 Cid: 05f0 Peb: 7ffdd000 ParentCid: 0360
    DirBase: beb47300 ObjectTable: 99e54a08 HandleCount: 70.
    Image: dwm.exe
PROCESS 877ced28 SessionId: 1 Cid: 0600 Peb: 7ffd3000 ParentCid: 05e8
    DirBase: beb47320 ObjectTable: 99ee5890 HandleCount: 766.
    Image: explorer.exe

另一种确定 PEB 地址的方法是使用 display type (dt) 命令。你可以通过检查 explorer.exe 进程的 EPROCESS 结构中的 Peb 字段来找到其PEB的地址,如下所示:

kd> dt nt!_EPROCESS 877ced28
   [REMOVED]
   +0x168 Session : 0x8f44e000 Void
   +0x16c ImageFileName : [15] "explorer.exe"
   [REMOVED]
   +0x1a8 Peb : 0x7ffd3000 _PEB
   +0x1ac PrefetchTrace : _EX_FAST_REF

你现在知道如何找到PEB,接下来,我们来了解一下PEB包含什么样的信息。要获取给定进程的PEB的人类可读摘要,首先,你需要切换到你想要检查其PEB的进程上下文。这可以通过使用.process扩展命令来完成。该命令接受_EPROCESS结构的地址。以下命令将当前进程上下文设置为explorer.exe进程:

kd> .process 877ced28
Implicit process is now 877ced28

然后,你可以使用!peb扩展命令,后跟PEB的地址。在以下输出中,为了简洁起见,部分信息被省略。ImageBaseAddress字段指定了进程可执行文件(explorer.exe)在内存中加载的地址。PEB还包含另一个结构,称为 Ldr 结构(类型为_PEB_LDR_DATA),它维护三个双向链表,分别是InLoadOrderModuleListInMemoryOrderModuleListInInitializationOrderModuleList。这三种双向链表中的每一项都包含关于模块的信息(包括进程可执行文件和 DLL)。通过遍历这些双向链表中的任何一个,你都可以获取有关模块的信息。InLoadOrderModuleList按照模块加载的顺序组织模块,InMemoryOrderModuleList按照模块在进程内存中驻留的顺序组织模块,InInitializationOrderModuleList按照其DllMain函数执行的顺序组织模块:

kd> !peb 0x7ffd3000
PEB at 7ffd3000
    InheritedAddressSpace: No
    ReadImageFileExecOptions: No
    BeingDebugged: No
    ImageBaseAddress: 000b0000
    Ldr 77dc8880
    Ldr.Initialized: Yes
    Ldr.InInitializationOrderModuleList: 00531f98 . 03d3b558
    Ldr.InLoadOrderModuleList: 00531f08 . 03d3b548
    Ldr.InMemoryOrderModuleList: 00531f10 . 03d3b550
    [REMOVED]

换句话说,这三种 PEB 链表都包含关于已加载模块的信息,例如基地址、大小、与模块相关的完整路径等。需要记住的一个重要点是,InInitializationOrderModuleList不包含进程可执行文件的信息,因为可执行文件与 DLL 的初始化方式不同。

为了帮助你更好地理解,以下图示使用Explorer.exe作为示例(这一概念也适用于其他进程)。当Explorer.exe执行时,它的进程可执行文件会以某个地址(假设是0xb0000)加载到进程内存中,并具有PAGE_EXECUTE_WRITECOPYWCX)保护。相关的 DLL 也会被加载到进程内存中。进程内存还包括 PEB 结构,其中包含有关explorer.exe在内存中加载位置(基地址)的元数据。PEB 中的Ldr结构维护三个双向链表;每个元素是一个结构(类型为_LDR_DATA_TABLE_ENTRY),它包含有关已加载模块的信息(基地址、完整路径等)。dlllist插件依赖于遍历InLoadOrderModuleList来获取模块的信息:

从这三个 PEB 列表中获取模块信息的问题在于,它们容易受到 DKOM 攻击。这三个 PEB 列表都位于用户空间,这意味着攻击者可以将恶意 DLL 加载到进程的地址空间中,并将恶意 DLL 从一个或所有 PEB 列表中解绑,从而隐藏起来,避开依赖于遍历这些列表的工具。为了解决这个问题,我们可以使用另一个插件,名为 ldrmodules

6.1 使用 ldrmodules 检测隐藏的 DLL

ldrmodules 插件将进程内存中的三个 PEB 列表中的模块信息与存在于内核内存中的一种数据结构——VADs虚拟地址描述符)的信息进行比较。内存管理器使用 VADs 跟踪进程内存中哪些虚拟地址已被保留(或空闲)。VAD 是一种二叉树结构,存储有关进程内存中虚拟连续内存区域的信息。对于每个进程,内存管理器维护一组 VADs,每个 VAD 节点描述一个虚拟连续的内存区域。如果进程内存区域包含内存映射文件(例如可执行文件、DLL),则 VAD 节点存储其基地址、文件路径和内存保护信息。以下示例应能帮助你理解这个概念。在以下截图中,内核空间中的一个 VAD 节点描述了进程可执行文件(explorer.exe)的加载位置、其完整路径和内存保护。类似地,其他 VAD 节点将描述进程内存范围,包括包含映射的可执行映像(如 DLL)的范围:

为了获取模块的信息,ldrmodules 插件枚举所有包含映射可执行映像的 VAD 节点,并将结果与三个 PEB 列表进行比较,以识别任何不一致之处。以下是来自一个被 TDSS rootkit(我们之前看到的)感染的内存镜像中进程的模块列表。你可以看到,ldrmodules 插件成功识别出一个名为 TDSSoiqh.dll 的恶意 DLL,该 DLL 在所有三个 PEB 列表(InLoadInInitInMem)中都隐藏了。InInit 值对于 svchost.exe 被设置为 False,这对于可执行文件来说是预期的,正如前面所提到的:

$ python vol.py -f tdl3.vmem --profile=WinXPSP3x86 ldrmodules -p 880
Volatility Foundation Volatility Framework 2.6
Pid Process     Base     InLoad InInit InMem MappedPath
--- ----------- --------  ----- ------- ----- ----------------------------
880 svchost.exe 0x10000000 False False False \WINDOWS\system32\TDSSoiqh.dll
880 svchost.exe 0x01000000 True  False True  \WINDOWS\system32\svchost.exe
880 svchost.exe 0x76d30000 True  True  True  \WINDOWS\system32\wmi.dll
880 svchost.exe 0x76f60000 True  True  True  \WINDOWS\system32\wldap32.dll
[REMOVED]

7. 转储可执行文件和 DLL

在识别了恶意进程或 DLL 后,您可能希望转储它以便进一步调查(例如提取字符串、运行 YARA 规则、反汇编或使用杀毒软件扫描)。要将进程可执行文件从内存转储到磁盘,可以使用procdump插件。要转储进程可执行文件,您需要知道其进程 ID 或物理偏移量。在下面的示例中,内存镜像被Perseus 恶意软件感染(之前在讨论pslist插件时有提到),procdump插件用于转储其恶意进程可执行文件svchost..exepid 3832)。使用-D--dump-dir)选项,您可以指定要转储可执行文件的目录名称。转储的文件名基于进程的 pid,例如executable.PID.exe

$ python vol.py -f perseus.vmem --profile=Win7SP1x86 procdump -p 3832 -D dump/
Volatility Foundation Volatility Framework 2.6
Process(V) ImageBase  Name         Result
---------- ---------- ------------ -----------------------
0x8503f0e8 0x00b90000 svchost..exe OK: executable.3832.exe


$ cd dump

$ file executable.3832.exe
executable.3832.exe: PE32 executable (GUI) Intel 80386 Mono/.Net assembly, for MS Windows

要转储带有物理偏移量的进程,可以使用-o (--offset)选项。如果您想从内存中转储隐藏进程,这个选项非常有用。在下面的示例中,内存镜像被prolaco恶意软件感染(之前在讨论psscan插件时有提到),通过物理偏移量转储了隐藏进程。物理偏移量是通过psscan插件确定的。您也可以通过psxview插件获取物理偏移量。使用procdump插件时,如果没有指定-p (--pid)-o (--offset)选项,则会转储系统上所有正在运行的活动进程的可执行文件:

$ python vol.py -f infected.vmem --profile=WinXPSP3x86 psscan
Volatility Foundation Volatility Framework 2.6
Offset(P)          Name    PID  PPID PDB        Time created 
------------------ ------- ---- ---- ---------- -------------------- 
[REMOVED]
0x00000000016ba360 nvid.exe 1700 1660 0x08440320 2014-10-17 09:16:10
$ python vol.py -f infected.vmem --profile=WinXPSP3x86 procdump -o 0x00000000016ba360 -D dump/ 
Volatility Foundation Volatility Framework 2.6
Process(V) ImageBase  Name     Result
---------- ---------- -------- -----------------------
0x814ba360 0x00400000 nvid.exe OK: executable.1700.exe

与进程可执行文件类似,您可以使用dlldump插件将恶意 DLL 转储到磁盘。要转储 DLL,您需要指定加载该 DLL 的进程 ID(-p选项)和 DLL 的基址,使用-b (--base)选项。您可以从dlllistldrmodules输出中获取 DLL 的基址。在下面的示例中,内存镜像被Ghost RAT恶意软件感染(我们在讨论dlllist插件时有提到),svchost.exepid 880)进程加载的恶意 DLL 被dlldump插件转储:

$ python vol.py -f ghost.vmem --profile=Win7SP1x86 dlllist -p 880 
Volatility Foundation Volatility Framework 2.6
************************************************************************
svchost.exe pid: 880
Command line : C:\Windows\system32\svchost.exe -k netsvcs

Base        Size  LoadCount  Path
---------- ------ --------   ------
[REMOVED]
0x10000000 0x26000 0x1 c:\users\test\application data\acd systems\acdsee\imageik.ddf
$ python vol.py -f ghost.vmem --profile=Win7SP1x86 dlldump -p 880 -b 0x10000000 -D dump/
Volatility Foundation Volatility Framework 2.6
Name       Module Base    Module Name       Result
---------- ------------ ---------------- --------------------------
svchost.exe 0x010000000  imageik.ddf      module.880.ea13030.10000000.dll

8. 列出网络连接和套接字

大多数恶意程序会执行一些网络活动,要么是下载附加组件,要么是接收攻击者的命令,要么是窃取数据,或是在系统上创建远程后门。检查网络活动将帮助你确定恶意软件在感染系统上的网络操作。在许多情况下,将运行在感染系统上的进程与网络上检测到的活动关联起来非常有用。为了确定在 Vista 之前的系统(如 Windows XP 和 2003)上的活动网络连接,你可以使用 connections 插件。以下命令显示了使用 connections 插件从感染了 BlackEnergy 恶意软件的内存转储中打印活动连接的示例。从以下输出中,你可以看到进程 ID 为 756 的进程负责了端口 443 上的 C2 通信。在运行 pslist 插件之后,你可以得知 756 的 pid 与 svchost.exe 进程相关联:

$ python vol.py -f be3.vmem --profile=WinXPSP3x86 connections
Volatility Foundation Volatility Framework 2.6
Offset(V)  Local Address         Remote Address   Pid
---------- ------------------   --------------  -------
0x81549748 192.168.1.100:1037   X.X.32.230:443   756
$ python vol.py -f be3.vmem --profile=WinXPSP3x86 pslist -p 756
Volatility Foundation Volatility Framework 2.6
Offset(V)  Name        PID PPID Thds Hnds Sess Wow64  Start               
---------- ----------- --- ---- ---- ---- ---- ------ --------------------
0x8185a808 svchost.exe 756 580  22   442  0    0      2016-01-13 18:38:10

另一个你可以在 Vista 之前的系统上使用来列出网络连接的插件是 connscan。它使用池标签扫描方法来确定连接。因此,它也可以检测到已终止的连接。在以下的内存镜像感染 TDL3 rootkit 的示例中,connections 插件没有返回任何结果,而 connscan 插件则显示了网络连接。这并不一定意味着连接被隐藏,只是意味着在获取内存镜像时,网络连接并未处于活动状态(或已被终止):

$ python vol.py -f tdl3.vmem --profile=WinXPSP3x86 connections
Volatility Foundation Volatility Framework 2.6
Offset(V)  Local Address Remote Address Pid
---------- ------------- -------------- ----
$ python vol.py -f tdl3.vmem --profile=WinXPSP3x86 connscan
Volatility Foundation Volatility Framework 2.6
Offset(P)  Local Address         Remote Address    Pid
---------- ------------------   ---------------   -----
0x093812b0 192.168.1.100:1032   XX.XXX.92.121:80   880

有时,你可能想要获取关于打开的套接字及其关联进程的信息。在 Vista 之前的系统上,你可以使用 socketssockscan 插件来获取打开端口的信息。sockets 插件打印出打开套接字的列表,而 sockscan 插件使用池标签扫描方法。因此,它可以检测到已关闭的端口。

在 Vista 及更高版本的系统(如 Windows 7)上,你可以使用 netscan 插件来显示网络连接和套接字。netscan 插件使用池标签扫描方法,类似于 sockscanconnscan 插件。在以下的内存镜像被 Darkcomet RAT 感染的示例中,netscan 插件显示了在端口 81 上的 C2 通信,这是由恶意进程 dmt.exe (pid 3768) 发起的:

$ python vol.py -f darkcomet.vmem --profile=Win7SP1x86 netscan
Volatility Foundation Volatility Framework 2.6
Proto Local Address   Foreign Address     State       Pid Owner
TCPv4 192.168.1.60:139    0.0.0.0:0       LISTENING      4 System 
UDPv4 192.168.1.60:137    *:*                            4 System
UDPv4 0.0.0.0:0           *:*                           1144 svchost.exe
TCPv4 0.0.0.0:49155       0.0.0.0:0       LISTENING     496 services.exe 
UDPv4 0.0.0.0:64471       *:*                           1064 svchost.exe
[REMOVED]
UDPv4 0.0.0.0:64470       *:*                           1064 svchost.exe
TCPv4 192.168.1.60:49162  XX.XXX.228.199:81 ESTABLISHED 3768 dmt.exe

9. 检查注册表

从取证角度来看,注册表可以提供有关恶意软件上下文的宝贵信息。在讨论第七章《恶意软件功能与持久性》中的持久性方法时,您已经看到恶意程序如何通过向注册表中添加条目来实现重启后的生存。除了持久性,恶意软件还使用注册表来存储配置数据、加密密钥等。要打印注册表键、子键及其值,您可以使用printkey插件,并通过-K--key)参数提供所需的注册表键路径。在以下受Xtreme Rat感染的内存镜像示例中,它将恶意可执行文件C:\Windows\InstallDir\system.exe添加到“运行”注册表键中。因此,每次系统启动时,恶意可执行文件都会被执行:

$ python vol.py -f xrat.vmem --profile=Win7SP1x86 printkey -K "Microsoft\Windows\CurrentVersion\Run"
Volatility Foundation Volatility Framework 2.6
Legend: (S) = Stable (V) = Volatile

----------------------------
Registry: \SystemRoot\System32\Config\SOFTWARE
Key name: Run (S)
Last updated: 2018-04-22 06:36:43 UTC+0000

Subkeys:

Values:
REG_SZ VMware User Process : (S) "C:\Program Files\VMware\VMware Tools\vmtoolsd.exe" -n vmusr
REG_EXPAND_SZ HKLM : (S) C:\Windows\InstallDir\system.exe

在以下示例中,Darkcomet RAT在注册表中添加了一条条目,通过rundll32.exe加载其恶意的DLL (mph.dll)

$ python vol.py -f darkcomet.vmem --profile=Win7SP1x86 printkey -K "Software\Microsoft\Windows\CurrentVersion\Run"
Volatility Foundation Volatility Framework 2.6
Legend: (S) = Stable (V) = Volatile

----------------------------
Registry: \??\C:\Users\Administrator\ntuser.dat
Key name: Run (S)
Last updated: 2016-09-23 10:01:53 UTC+0000

Subkeys:

Values:
REG_SZ Adobe cleanup : (S) rundll32.exe "C:\Users\Administrator\Local Settings\Application Data\Adobe updater\mph.dll", StartProt
----------------------------

还有其他注册表键存储着以二进制形式存在的宝贵信息,对于取证调查员来说,这些信息极具价值。Volatility 插件,如userassistshellbagsshimcache,可以解析这些包含二进制数据的注册表键,并以更易读的格式显示信息。

Userassist注册表键包含了用户在系统上执行的程序列表以及程序执行的时间。要打印userassist注册表信息,您可以使用 Volatility 的userassist插件,如下所示。在以下示例中,一个名为(info.doc.exe)的可疑可执行文件在2018-04-30 06:42:37从**E:**驱动器(可能是 USB 驱动器)执行:

$ python vol.py -f inf.vmem --profile=Win7SP1x86 userassist
Volatility Foundation Volatility Framework 2.6
----------------------------
Registry: \??\C:\Users\test\ntuser.dat 

[REMOVED]

REG_BINARY E:\info.doc.exe : 
Count: 1
Focus Count: 0
Time Focused: 0:00:00.500000
Last updated: 2018-04-30 06:42:37 UTC+0000
Raw Data:
0x00000000 00 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00
0x00000010 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf

在调查恶意软件事件时,shimcacheshellbags插件非常有用。shimcache插件有助于证明恶意软件在系统上的存在及其运行时间。shellbags插件则可以提供有关对文件、文件夹、外部存储设备和网络资源访问的信息。

10. 服务调查

在第七章《恶意软件功能与持久性》中,我们探讨了攻击者如何通过安装或修改现有服务来在系统上保持持久性。在本节中,我们将重点讨论如何从内存镜像中调查服务。要列出服务及其信息,例如显示名称服务类型启动类型,您可以使用svcscan插件。在以下示例中,恶意软件创建了一个类型为WIN32_OWN_PROCESS的服务,显示名称和服务名称均为svchost。从二进制路径中可以看出,svchost.exe是恶意的,因为它是从非标准路径C:\Windows而不是C:\Windows\System32运行的:

$ python vol.py -f svc.vmem --profile=Win7SP1x86 svcscan
Volatility Foundation Volatility Framework 2.6
[REMOVED]
Offset: 0x58e660
Order: 396
Start: SERVICE_AUTO_START
Process ID: 4080
Service Name: svchost
Display Name: svchost
Service Type: SERVICE_WIN32_OWN_PROCESS
Service State: SERVICE_RUNNING
Binary Path: C:\Windows\svchost.exe

对于作为 DLL 实现的服务(即服务 DLL),你可以通过向svcscan插件传递-v (--verbose)选项来显示服务 DLL(或内核驱动程序)的完整路径。-v选项会打印与服务相关的详细信息。以下是一个示例,展示了作为 DLL 运行的恶意软件服务。服务状态被设置为SERVICE_START_PENDING,启动类型设置为SERVICE_AUTO_START,这告诉你该服务尚未启动,并将在系统启动时自动启动:

$ python vol.py -f svc.vmem --profile=Win7SP1x86 svcscan
[REMOVED]
Offset: 0x5903a8
Order: 396
Start: SERVICE_AUTO_START
Process ID: -
Service Name: FastUserSwitchingCompatibility
Display Name: FastUserSwitchingCompatibility
Service Type: SERVICE_WIN32_SHARE_PROCESS
Service State: SERVICE_START_PENDING
Binary Path: -
ServiceDll: C:\Windows\system32\FastUserSwitchingCompatibilityex.dll
ImagePath: %SystemRoot%\System32\svchost.exe -k netsvcs

一些恶意程序劫持现有的未使用或已禁用的服务以保持在系统上的持久性。BlackEnergy就是这种恶意软件的一个例子,它替换了磁盘上名为aliide.sys的合法内核驱动程序。该内核驱动程序与一个名为aliide的服务相关联。替换驱动程序后,它修改了与aliide服务相关的注册表项并设置为自动启动(即系统启动时自动启动该服务)。此类攻击很难被检测到。检测此类修改的一种方法是保持一个干净内存镜像中所有服务的列表,并将其与可疑镜像中的服务列表进行比较,以寻找任何修改。以下是来自干净内存镜像中的 aliide 服务配置。合法的 aliide 服务被设置为按需启动(即需要手动启动该服务),且服务处于停止状态:

$ python vol.py -f win7_clean.vmem --profile=Win7SP1x64 svcscan
Offset: 0x871c30
Order: 11
Start: SERVICE_DEMAND_START
Process ID: -
Service Name: aliide
Display Name: aliide
Service Type: SERVICE_KERNEL_DRIVER
Service State: SERVICE_STOPPED
Binary Path: -

以下是受BlackEnergy**感染的内存镜像中的svcscan输出。**经过修改后,aliide服务被设置为自动启动(即系统启动时自动启动该服务),但仍处于停止状态。这意味着在系统重启后,服务将自动启动并加载恶意的aliide.sys驱动程序。有关此BlackEnergy劫持程序的详细分析,请参阅作者的博客文章:cysinfo.com/blackout-memory-analysis-of-blackenergy-big-dropper/

$ python vol.py -f be3_big.vmem --profile=Win7SP1x64 svcscan
Offset: 0x881d30
Order: 12
Start: SERVICE_AUTO_START
Process ID: -
Service Name: aliide
Display Name: aliide
Service Type: SERVICE_KERNEL_DRIVER
Service State: SERVICE_STOPPED
Binary Path: -

11. 提取命令历史

在攻陷系统后,攻击者可能会在命令行中执行各种命令,以枚举网络上的用户、组和共享,或者攻击者可能会将像 Mimikatz 这样的工具(github.com/gentilkiwi/mimikatz)传输到被攻陷的系统并执行,以提取 Windows 凭据。Mimikatz 是一款开源工具,由 Benjamin Delpy 于 2011 年编写。它是收集 Windows 系统凭据最流行的工具之一。Mimikatz 有多种版本,例如编译版(github.com/gentilkiwi/mimikatz),并且它是 PowerShell 模块的一部分,比如 PowerSploitgithub.com/PowerShellMafia/PowerSploit)和 PowerShell Empiregithub.com/EmpireProject/Empire)。

命令历史记录能够提供关于攻击者在被攻陷系统上活动的宝贵信息。通过检查命令历史记录,你可以确定执行过的命令、调用的程序,以及攻击者访问的文件和文件夹。两个 Volatility 插件,cmdscanconsoles 可以从内存镜像中提取命令历史记录。这些插件从 csrss.exe(Windows 7 之前)或 conhost.exe(Windows 7 及以后版本)进程中提取命令历史记录。

要了解这些插件的详细工作原理,可以阅读书籍《内存取证的艺术》或阅读 Richard Stevens 和 Eoghan Casey 的研究论文《从物理内存提取 Windows 命令行细节》(www.dfrws.org/2010/proceedings/2010-307.pdf)。

cmdscan 插件列出了 cmd.exe 执行的命令。以下示例展示了系统上窃取凭据的活动。从 cmdscan 输出中,你可以看到一个名为 net.exe 的应用程序是通过命令行 cmd.exe 调用的。从 net.exe 提取的命令可以看出,命令 privilege::debugsekurlsa::logonpasswords 与 Mimikatz 相关。在这种情况下,Mimikatz 应用程序被重命名为 net.exe

$ python vol.py -f mim.vmem --profile=Win7SP1x64 cmdscan
[REMOVED]
CommandProcess: conhost.exe Pid: 2772
CommandHistory: 0x29ea40 Application: cmd.exe Flags: Allocated, Reset
CommandCount: 2 LastAdded: 1 LastDisplayed: 1
FirstCommand: 0 CommandCountMax: 50
ProcessHandle: 0x5c
Cmd #0 @ 0x29d610: cd \
Cmd #1 @ 0x27b920: cmd.exe /c %temp%\net.exe
Cmd #15 @ 0x260158: )
Cmd #16 @ 0x29d3b0: )
[REMOVED]
**************************************************
CommandProcess: conhost.exe Pid: 2772
CommandHistory: 0x29f080 Application: net.exe Flags: Allocated, Reset
CommandCount: 2 LastAdded: 1 LastDisplayed: 1
FirstCommand: 0 CommandCountMax: 50
ProcessHandle: 0xd4
Cmd #0 @ 0x27ea70: privilege::debug
Cmd #1 @ 0x29b320: sekurlsa::logonpasswords
Cmd #23 @ 0x260158: )
Cmd #24 @ 0x29ec20: '

cmdscan 插件显示攻击者执行的命令。为了判断命令是否成功执行,你可以使用 consoles 插件。运行 consoles 插件后,你可以看到 net.exe 确实是一个 Mimikatz 应用程序,并且为了提取凭据,Mimikatz 命令通过 Mimikatz shell 执行。从输出中你可以看出,凭据已成功提取,并且密码以明文形式被获取:

$ python vol.py -f mim.vmem --profile=Win7SP1x64 consoles
----
CommandHistory: 0x29ea40 Application: cmd.exe Flags: Allocated, Reset
CommandCount: 2 LastAdded: 1 LastDisplayed: 1
FirstCommand: 0 CommandCountMax: 50
ProcessHandle: 0x5c
Cmd #0 at 0x29d610: cd \
Cmd #1 at 0x27b920: cmd.exe /c %temp%\net.exe
----
Screen 0x280ef0 X:80 Y:300
Dump:
Microsoft Windows [Version 6.1.7600] 
Copyright (c) 2009 Microsoft Corporation. All rights reserved. 

C:\Windows\system32>cd \ 

C:\>cmd.exe /c %temp%\net.exe

[REMOVED] 

mimikatz # privilege::debug 
Privilege '20' OK 

mimikatz # sekurlsa::logonpasswords                                                                             
Authentication Id : 0 ; 269689 (00000000:00041d79) 
Session : Interactive from 1 
User Name : test 
Domain : PC 
Logon Server : PC 
Logon Time : 5/4/2018 10:00:59 AM 
SID : S-1-5-21-1752268255-3385687637-2219068913-1000 
        msv : 
         [00000003] Primary 
         * Username : test 
         * Domain : PC 
         * LM : 0b5e35e143b092c3e02e0f3aaa0f5959 
         * NTLM : 2f87e7dcda37749436f914ae8e4cfe5f 
         * SHA1 : 7696c82d16a0c107a3aba1478df60e543d9742f1 
        tspkg : 
         * Username : test 
         * Domain : PC 
         * Password : cleartext 
        wdigest : 
         * Username : test 
         * Domain : PC 
         * Password : cleartext 
        kerberos : 
         * Username : test 
         * Domain : PC 
         * Password : cleartext 

在 Windows 8.1 及更高版本上,你可能无法使用 Mimikatz 以明文形式转储密码,但 Mimikatz 为攻击者提供了多种功能。攻击者可能使用提取的 NTLM 哈希值来冒充账户。有关 Mimikatz 的详细信息及如何使用它提取 Windows 凭证,请阅读adsecurity.org/?page_id=1821

总结

内存取证是一种出色的技术,可以从计算机内存中查找和提取取证证据。除了在恶意软件调查中使用内存取证外,你还可以将其作为恶意软件分析的一部分,以获取有关恶意软件行为和特征的更多信息。本章介绍了不同的 Volatility 插件,帮助你了解在受感染系统上发生的事件,并提供对恶意软件活动的洞察。在下一章,我们将使用更多 Volatility 插件来确定高级恶意软件的功能,你将学会如何使用这些插件提取取证证据。

第十一章:检测使用内存取证技术的高级恶意软件

在前一章中,我们查看了不同的 Volatility 插件,这些插件有助于从内存镜像中提取有价值的信息。在本章中,我们将继续探讨内存取证,并将介绍几个插件,这些插件将帮助您从感染了使用隐蔽和隐藏技术的高级恶意软件的内存镜像中提取取证物证。在接下来的部分中,我们将专注于使用内存取证检测代码注入技术。下一部分讨论了在第八章代码注入与挂钩中已经涵盖的一些概念,因此强烈建议在阅读下一部分之前先阅读该章节。

1. 检测代码注入

如果你还记得第八章中的代码注入与挂钩,代码注入是一种用于将恶意代码(如 EXE、DLL 或 shellcode)注入到合法进程内存并在合法进程上下文中执行的技术。为了将代码注入到远程进程中,恶意软件通常会分配一个带有执行权限的内存(PAGE_EXECUTE_READWRITE),然后将代码注入到远程进程的已分配内存中。要检测注入到远程进程中的代码,可以根据内存保护和内存内容查找可疑内存范围。一个引人注目的问题是,什么是可疑的内存范围,如何获取关于进程内存范围的信息?如果你还记得上一章(在检测隐藏的 DLL 使用 ldrmodules 部分),Windows 在内核空间维护了一个名为*虚拟地址描述符(VADs)*的二叉树结构,每个 VAD 节点描述了进程内存中的一个虚拟连续内存区域。如果进程内存区域包含内存映射文件(如可执行文件、DLL 等),则其中一个 VAD 节点存储有关其基地址、文件路径和内存保护的信息。以下描绘并非 VAD 的精确表示,但应有助于理解该概念。在以下屏幕截图中,内核空间中的一个 VAD 节点描述了关于进程可执行文件(explorer.exe)加载位置、完整路径及内存保护的信息。类似地,其他 VAD 节点将描述进程内存范围,包括包含映射的可执行映像(如 DLL)的范围。这意味着 VAD 可用于确定每个连续进程内存范围的内存保护,并且还可以提供关于包含内存映像文件(如可执行文件或 DLL)的内存区域的信息:

1.1 获取 VAD 信息

要从内存映像中获取 VAD 信息,可以使用vadinfo Volatility 插件。在以下示例中,vadinfo用于显示explorer.exe进程的内存区域,通过其进程 ID(pid 2180)。在以下输出中,位于内核内存中地址0x8724d718的第一个 VAD 节点描述了进程内存中0x00db0000-0x0102ffff的内存范围及其内存保护PAGE_EXECUTE_WRITECOPY。由于第一个节点描述的是包含内存映射可执行镜像(explorer.exe)的内存范围,因此它还给出了其在磁盘上的完整路径。第二个节点0x8723fb50描述了0x004b0000-0x004effff的内存范围,该范围不包含任何内存映射文件。类似地,位于地址0x8723fb78的第三个节点显示了0x77690000-0x777cbfff的进程内存范围的信息,其中包含ntdll.dll及其内存保护:

$ python vol.py -f win7.vmem --profile=Win7SP1x86 vadinfo -p 2180
Volatility Foundation Volatility Framework 2.6

VAD node @ 0x8724d718 Start 0x00db0000 End 0x0102ffff Tag Vadm
Flags: CommitCharge: 4, Protection: 7, VadType: 2
Protection: PAGE_EXECUTE_WRITECOPY
Vad Type: VadImageMap
ControlArea @87240008 Segment 82135000
NumberOfSectionReferences: 1 NumberOfPfnReferences: 215
NumberOfMappedViews: 1 NumberOfUserReferences: 2
Control Flags: Accessed: 1, File: 1, Image: 1
FileObject @8723f8c0, Name: \Device\HarddiskVolume1\Windows\explorer.exe
First prototype PTE: 82135030 Last contiguous PTE: fffffffc
Flags2: Inherit: 1, LongVad: 1

VAD node @ 0x8723fb50 Start 0x004b0000 End 0x004effff Tag VadS
Flags: CommitCharge: 43, PrivateMemory: 1, Protection: 4
Protection: PAGE_READWRITE
Vad Type: VadNone

VAD node @ 0x8723fb78 Start 0x77690000 End 0x777cbfff Tag Vad 
Flags: CommitCharge: 9, Protection: 7, VadType: 2
Protection: PAGE_EXECUTE_WRITECOPY
Vad Type: VadImageMap
ControlArea @8634b790 Segment 899fc008
NumberOfSectionReferences: 2 NumberOfPfnReferences: 223
NumberOfMappedViews: 40 NumberOfUserReferences: 42
Control Flags: Accessed: 1, File: 1, Image: 1
FileObject @8634bc38, Name: \Device\HarddiskVolume1\Windows\System32\ntdll.dll
First prototype PTE: 899fc038 Last contiguous PTE: fffffffc
Flags2: Inherit: 1
[REMOVED]

要使用 Windbg 内核调试器获取进程的 VAD 信息,首先需要使用.process命令切换到所需的进程上下文,并跟随_EPROCESS结构的地址。切换上下文后,使用!vad扩展命令显示进程的内存区域。

1.2 使用 VAD 检测注入的代码

一个重要的点是,当可执行镜像(如 EXE 或 DLL)正常加载到内存时,操作系统会为该内存区域分配PAGE_EXECUTE_WRITECOPY(WCX)的内存保护。应用程序通常不允许使用像VirtualAllocEx这样的 API 调用来分配具有PAGE_EXECUTE_WRITECOPY保护的内存。换句话说,如果攻击者想要注入 PE 文件(如 EXE 或 DLL)或 shellcode,那么需要分配具有PAGE_EXECUTE_READWRITE(RWX)保护的内存。通常,你会发现很少有内存范围具有PAGE_EXECUTE_READWRITE的内存保护。具有PAGE_EXECUTE_READWRITE保护的内存范围不一定是恶意的,因为程序可能会出于合法目的分配具有该保护的内存。为了检测代码注入,我们可以查找包含PAGE_EXECUTE_READWRITE内存保护的内存范围,并检查和验证其内容以确认是否存在恶意代码。为了帮助你理解这一点,我们来看一个被SpyEye感染的内存映像的例子。该恶意软件将代码注入到合法的explorer.exe进程(pid 1608)中。vadinfo插件显示了在explorer.exe进程中两个具有可疑PAGE_EXECUTE_READWRITE内存保护的内存范围:

$ python vol.py -f spyeye.vmem --profile=Win7SP1x86 vadinfo -p 1608
[REMOVED]
VAD node @ 0x86fd9ca8 Start 0x03120000 End 0x03124fff Tag VadS
Flags: CommitCharge: 5, MemCommit: 1, PrivateMemory: 1, Protection: 6
Protection: PAGE_EXECUTE_READWRITE
Vad Type: VadNone

VAD node @ 0x86fd0d00 Start 0x03110000 End 0x03110fff Tag VadS
Flags: CommitCharge: 1, MemCommit: 1, PrivateMemory: 1, Protection: 6
Protection: PAGE_EXECUTE_READWRITE
Vad Type: VadNone

仅通过内存保护,难以得出前述内存区域是否包含恶意代码的结论。为了确定是否有恶意代码,我们可以转储这些内存区域的内容。要显示内存区域的内容,可以使用volshell插件。以下命令在explorer.exe进程(pid 1608)的上下文中调用volshell(交互式 Python shell)。db命令将转储给定内存地址的内容。要获取帮助信息并显示支持的volshell命令,只需在volshell中输入hh()。使用db命令转储内存地址0x03120000(前面vadinfo输出中的第一个条目)的内容,显示出PE文件的存在。PAGE_EXECUTE_READWRITE的内存保护和 PE 文件的存在明显表明可执行文件并非正常加载,而是被注入到explorer.exe进程的地址空间中:

$ python vol.py -f spyeye.vmem --profile=Win7SP1x86 volshell -p 1608
Volatility Foundation Volatility Framework 2.6
Current context: explorer.exe @ 0x86eb4780, pid=1608, ppid=1572 DTB=0x1eb1a340
Python 2.7.13 (default, Jan 19 2017, 14:48:08)

>>> db(0x03120000)
0x03120000 4d 5a 90 00 03 00 00 00 04 00 00 00 ff ff 00 00 MZ..............
0x03120010 b8 00 00 00 00 00 00 00 40 00 00 00 00 00 00 00 ........@.......
0x03120020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0x03120030 00 00 00 00 00 00 00 00 00 00 00 00 d8 00 00 00 ................
0x03120040 0e 1f ba 0e 00 b4 09 cd 21 b8 01 4c cd 21 54 68 ........!..L.!Th
0x03120050 69 73 20 70 72 6f 67 72 61 6d 20 63 61 6e 6e 6f is.program.canno
0x03120060 74 20 62 65 20 72 75 6e 20 69 6e 20 44 4f 53 20 t.be.run.in.DOS.
0x03120070 6d 6f 64 65 2e 0d 0d 0a 24 00 00 00 00 00 00 00 mode....$.......

有时候,仅显示内存区域的内容不足以识别恶意代码。尤其是当 shellcode 被注入时,这种情况尤为常见。在这种情况下,你需要对内容进行反汇编。例如,如果你使用db命令转储地址0x03110000的内容(这是前面vadinfo输出中的第二个条目),你将看到以下的十六进制转储。从输出来看,很难判断这是否是恶意代码:

>>> db(0x03110000)
0x03110000 64 a1 18 00 00 00 c3 55 8b ec 83 ec 54 83 65 fc d......U....T.e.
0x03110010 00 64 a1 30 00 00 00 8b 40 0c 8b 40 1c 8b 40 08 .d.0....@..@..@.
0x03110020 68 34 05 74 78 50 e8 83 00 00 00 59 59 89 45 f0 h4.txP.....YY.E.
0x03110030 85 c0 74 75 8d 45 ac 89 45 f4 8b 55 f4 c7 02 6b ..tu.E..E..U...k
0x03110040 00 65 00 83 c2 04 c7 02 72 00 6e 00 83 c2 04 c7 .e......r.n.....

如果你怀疑内存区域包含 shellcode,可以在volshell中使用dis命令对给定地址的代码进行反汇编。从以下代码显示的反汇编输出来看,你可能会发现 shellcode 已经被注入到此内存区域,因为它包含有效的 CPU 指令。为了验证内存区域是否包含恶意代码,你需要进一步分析,以确定其上下文。因为注入的代码也可能与合法代码相似:

>>> dis(0x03110000)
0x3110000 64a118000000 MOV EAX, [FS:0x18]
0x3110006 c3           RET
0x3110007 55           PUSH EBP
0x3110008 8bec         MOV EBP, ESP
0x311000a 83ec54       SUB ESP, 0x54
0x311000d 8365fc00     AND DWORD [EBP-0x4], 0x0
0x3110011 64a130000000 MOV EAX, [FS:0x30]
0x3110017 8b400c       MOV EAX, [EAX+0xc]
0x311001a 8b401c       MOV EAX, [EAX+0x1c]
0x311001d 8b4008       MOV EAX, [EAX+0x8]
0x3110020 6834057478   PUSH DWORD 0x78740534
0x3110025 50           PUSH EAX
0x3110026 e883000000   CALL 0x31100ae
[REMOVED]

1.3 转储进程内存区域

在识别到进程内存中的注入代码(PE 文件或 shellcode)后,你可能希望将其转储到磁盘以便进一步分析(例如提取字符串、进行 YARA 扫描或反汇编)。要转储由 VAD 节点描述的内存区域,可以使用vaddump插件。例如,如果你想转储位于地址0x03110000的包含 shellcode 的内存区域,可以提供-b (--base)选项,并跟上基地址,如下所示。如果不指定-b (--base)选项,插件将把所有内存区域转储到单独的文件中:

$ python vol.py -f spyeye.vmem --profile=Win7SP1x86 vaddump -p 1608 -b 0x03110000 -D dump/
Volatility Foundation Volatility Framework 2.6
Pid  Process      Start      End        Result
---- -----------  ---------- ---------- ---------------------------
1608 explorer.exe 0x03110000 0x03110fff dump/explorer.exe.1deb4780.0x03110000-0x03110fff.dmp

一些恶意软件程序使用隐蔽技术来绕过检测。例如,某个恶意软件可能会注入一个 PE 文件,并在加载到内存后擦除 PE 头。在这种情况下,如果你查看十六进制转储,它不会显示 PE 文件的任何迹象;可能需要某些手动分析来验证代码。有关此类恶意软件样本的示例,可以参考一篇名为 "用 Volatility 恢复 CoreFlood 二进制文件" 的博客文章 (mnin.blogspot.in/2008/11/recovering-coreflood-binaries-with.html)。

1.4 使用 malfind 检测注入的代码

到目前为止,我们已经了解了如何使用 vadinfo 手动识别可疑的内存区域。你也已经理解了如何使用 vaddump 转储内存区域。还有另一个 Volatility 插件叫做 malfind,它基于之前讲解的内存内容和 VAD 特征自动化了识别可疑内存区域的过程。在以下示例中,当 malfind 在被 SpyEye 感染的内存镜像上运行时,它会自动识别出可疑的内存区域(包含 PE 文件和 shellcode)。除此之外,它还显示了从基地址开始的十六进制转储和反汇编。如果你没有指定 -p (--pid) 选项,malfind 会识别系统上所有正在运行的进程的可疑内存区域:

$ python vol.py -f spyeye.vmem --profile=Win7SP1x86 malfind -p 1608
Volatility Foundation Volatility Framework 2.6

Process: explorer.exe Pid: 1608 Address: 0x3120000
Vad Tag: VadS Protection: PAGE_EXECUTE_READWRITE
Flags: CommitCharge: 5, MemCommit: 1, PrivateMemory: 1, Protection: 6

0x03120000 4d 5a 90 00 03 00 00 00 04 00 00 00 ff ff 00 00 MZ..............
0x03120010 b8 00 00 00 00 00 00 00 40 00 00 00 00 00 00 00 ........@.......
0x03120020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0x03120030 00 00 00 00 00 00 00 00 00 00 00 00 d8 00 00 00 ................

0x03120000 4d DEC EBP
0x03120001 5a POP EDX
0x03120002 90 NOP
0x03120003 0003 ADD [EBX], AL
0x03120005 0000 ADD [EAX], AL

Process: explorer.exe Pid: 1608 Address: 0x3110000
Vad Tag: VadS Protection: PAGE_EXECUTE_READWRITE
Flags: CommitCharge: 1, MemCommit: 1, PrivateMemory: 1, Protection: 6

0x03110000 64 a1 18 00 00 00 c3 55 8b ec 83 ec 54 83 65 fc d......U....T.e.
0x03110010 00 64 a1 30 00 00 00 8b 40 0c 8b 40 1c 8b 40 08 .d.0....@..@..@.
0x03110020 68 34 05 74 78 50 e8 83 00 00 00 59 59 89 45 f0 h4.txP.....YY.E.
0x03110030 85 c0 74 75 8d 45 ac 89 45 f4 8b 55 f4 c7 02 6b ..tu.E..E..U...k

0x03110000 64a118000000 MOV EAX, [FS:0x18]
0x03110006 c3 RET
0x03110007 55 PUSH EBP
0x03110008 8bec MOV EBP, ESP
0x0311000a 83ec54 SUB ESP, 0x54
0x0311000d 8365fc00 AND DWORD [EBP-0x4], 0x0
0x03110011 64a130000000 MOV EAX, [FS:0x30]

2. 调查空洞进程注入

在前面章节中讲解的代码注入技术中,恶意代码被注入到合法进程的进程地址空间中。空洞进程注入(或称 进程空洞化)也是一种代码注入技术,但不同之处在于,在这种技术中,合法进程的进程可执行文件会被恶意可执行文件替换。在进入空洞进程注入的检测之前,先让我们了解一下它是如何工作的。在下一节中将详细介绍空洞进程注入的工作原理。空洞进程注入的详细信息已在 第八章,代码注入与钩子(章节) 中讲解。你还可以查看作者关于空洞进程注入的演示和视频演示 (cysinfo.com/7th-meetup-reversing-and-investigating-malware-evasive-tactics-hollow-process-injection/),以便更好地理解这个主题。

2.1 空洞进程注入步骤

以下步骤描述了恶意软件通常如何执行进程空洞化。假设有两个进程,A 和 B。在这种情况下,进程 A 是恶意进程,进程 B 是合法进程(也称为远程进程),例如 explorer.exe

  • 进程 A 启动合法进程 B,并使其处于挂起模式。结果,进程 B 的可执行区段被加载到内存中,PEB(进程环境块)标识了合法进程的完整路径。PEB 结构中的 ImageBaseAddress 字段指向合法进程可执行文件加载的基地址。

  • 进程 A 获取将要注入远程进程的恶意可执行文件。这个可执行文件可以来自恶意软件进程的资源区段,也可以来自磁盘上的文件。

  • 进程 A 确定合法进程 B 的基址,以便它可以取消映射合法进程的可执行区段。恶意软件可以通过读取 PEB(在我们的例子中是 PEB.ImageBaseAddress)来确定基址。

  • 进程 A 然后解除分配合法进程的可执行区段。

  • 进程 A 然后在合法进程 B 中分配内存,并赋予执行权限。这个内存分配通常是在可执行文件先前加载的相同地址上进行的。

  • 进程 A 然后将恶意可执行文件的 PE 头和 PE 区段写入分配的内存中。

  • 进程 A 然后将挂起线程的起始地址更改为注入的可执行文件入口点的地址,并恢复合法进程的挂起线程。结果,合法进程现在开始执行恶意代码。

Stuxnet 就是一个执行空洞进程注入的恶意软件,它使用上述步骤。具体来说,Stuxnet 在挂起模式下创建了合法的 lsass.exe 进程。结果,lsass.exePAGE_EXECUTE_WRITECOPY(WCX) 保护加载到内存中。此时(空洞前),PEBVAD 都包含有关 lsass.exe 内存保护、基址和完整路径的相同元数据。接着,Stuxnet 将合法进程可执行文件(lsass.exe)空洞化,并在与 lsass.exe 先前加载的相同区域中分配一个具有 PAGE_EXECUTE_READWRITE (RWX) 保护的新内存,随后将恶意可执行文件注入已分配的内存中并恢复挂起的线程。由于空洞化进程可执行文件,它在 VADPEB 之间创建了进程路径信息的不一致,即 PEB 中的进程路径仍然包含 lsass.exe 的完整路径,而 VAD 中则不显示完整路径。此外,空洞前后的内存保护存在不一致:空洞前是 (WCX),空洞后是 (RWX)。下面的图示将帮助你理解空洞前发生的情况,以及空洞化进程后在 PEBVAD 中创建的不一致。

对 Stuxnet 的完整分析,使用内存取证技术,由 Michael Hale Ligh 在以下博客文章中介绍:mnin.blogspot.in/2011/06/examining-stuxnets-footprint-in-memory.html

2.2 检测空洞进程注入

要检测空洞进程注入,可以查看PEBVAD之间的差异,以及内存保护的差异。还可以检查父子进程关系的差异。在下面的Stuxnet示例中,您可以看到系统上有两个lsass.exe进程正在运行。第一个lsass.exe进程(pid 708)的父进程是winlogon.exepid 652),而第二个lsass.exe进程(pid 1732)的父进程(pid 1736)已经终止。根据进程信息,您可以判断pid 1732lsass.exe是可疑进程,因为在干净的系统中,lsass.exe的父进程在 Vista 之前的系统上是winlogon.exe,而在 Vista 及以后的系统上是wininit.exe

$ python vol.py -f stux.vmem --profile=WinXPSP3x86 pslist | grep -i lsass
Volatility Foundation Volatility Framework 2.6
0x818c1558 lsass.exe 708 652 24 343 0 0 2016-05-10 06:47:24+0000 
0x81759da0 lsass.exe 1732 1736 5 86 0 0 2018-05-12 06:39:42

$ python vol.py -f stux.vmem --profile=WinXPSP3x86 pslist -p 652
Volatility Foundation Volatility Framework 2.6
Offset(V) Name          PID PPID Thds Hnds Sess Wow64  Start                
---------- ------------ ---- ---- ---- ---- --- ------ ------------------
0x818321c0 winlogon.exe 652  332  23  521    0      0  2016-05-10 06:47:24

$ python vol.py -f stux.vmem --profile=WinXPSP3x86 pslist -p 1736
Volatility Foundation Volatility Framework 2.6
ERROR : volatility.debug : Cannot find PID 1736\. If its terminated or unlinked, use psscan and then supply --offset=OFFSET

如前所述,通过比较PEBVAD结构,您可以检测到空洞进程注入。dlllist插件从PEB中获取模块信息,显示了lsass.exepid 1732)的完整路径和其加载的基地址(0x01000000)

lsass.exe pid: 1732
Command line : "C:\WINDOWS\\system32\\lsass.exe"
Service Pack 3

Base Size  Load    Count  Path
---------- ------- ------ -------------------------------
0x01000000 0x6000  0xffff C:\WINDOWS\system32\lsass.exe
0x7c900000 0xaf000 0xffff C:\WINDOWS\system32\ntdll.dll
0x7c800000 0xf6000 0xffff C:\WINDOWS\system32\kernel32.dll
0x77dd0000 0x9b000 0xffff C:\WINDOWS\system32\ADVAPI32.dll
[REMOVED]

ldrmodules插件依赖于内核中的 VAD,但没有显示lsass.exe的完整路径名称。由于恶意软件解除映射了lsass.exe进程的可执行部分,完整路径名称不再与地址0x01000000关联:

$ python vol.py -f stux.vmem --profile=WinXPSP3x86 ldrmodules -p 1732
Volatility Foundation Volatility Framework 2.6
Pid  Process   Base       InLoad InInit InMem    MappedPath
---- --------- ---------- ------ ------ ------ ----------------------------
[REMOVED]
1732 lsass.exe 0x7c900000 True  True   True   \WINDOWS\system32\ntdll.dll
1732 lsass.exe 0x71ad0000 True  True   True   \WINDOWS\system32\wsock32.dll
1732 lsass.exe 0x77f60000 True  True   True   \WINDOWS\system32\shlwapi.dll
1732 lsass.exe 0x01000000 True  False  True 
1732 lsass.exe 0x76b40000 True  True   True   \WINDOWS\system32\winmm.dll
[REMOVED]

由于恶意软件通常会在空洞化并注入可执行文件之前,分配具有PAGE_EXECUTE_READWRITE权限的内存,因此可以查找该内存保护。malfind插件在同一地址(0x01000000)识别到可疑的内存保护,这正是可执行文件lsass.exe被加载的地方:

Process: lsass.exe Pid: 1732 Address: 0x1000000
Vad Tag: Vad Protection: PAGE_EXECUTE_READWRITE
Flags: CommitCharge: 2, Protection: 6

0x01000000 4d 5a 90 00 03 00 00 00 04 00 00 00 ff ff 00 00 MZ..............
0x01000010 b8 00 00 00 00 00 00 00 40 00 00 00 00 00 00 00 ........@.......
0x01000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0x01000030 00 00 00 00 00 00 00 00 00 00 00 00 d0 00 00 00 ................

0x01000000 4d DEC EBP
0x01000001 5a POP EDX
0x01000002 90 NOP

如果您希望将malfind检测到的可疑内存区域转储到磁盘,您可以指定-D,后跟目录名,将所有可疑的内存区域转储到该目录。

2.3 空洞进程注入变种

在以下示例中,我们将介绍一个名为Skeeyah的恶意软件,它以略微不同的方式执行空洞进程注入。这是与第八章中介绍的相同样本,代码注入和钩子(第 3.6 节 空洞进程注入)。以下是Skeeyah执行的步骤:

  • 它以挂起模式启动svchost.exe进程。结果,svchost.exe被加载到内存中(在此案例中,加载到地址0x1000000)。

  • 它通过读取PEB.ImageBaseAddress确定svchost.exe的基地址,然后释放svchost.exe的可执行部分。

  • 它并没有在svchost.exe之前加载的相同区域(0x1000000)分配内存,而是分配了一个不同的地址0x00400000,并且具有readwriteexecute权限。

  • 它随后用新分配的地址0x00400000覆盖了svchost.exe进程的PEB.ImageBaseAdress。这将svchost.exePEB中的基地址从0x1000000更改为0x00400000(该地址包含注入的可执行文件)。

  • 然后,它将挂起线程的起始地址更改为注入的可执行文件入口点的地址,并恢复该线程。

下图显示了进程空洞化前后的差异。具体来说,空洞化后的 PEB 认为svchost.exe被加载到0x00400000。之前表示svchost.exe(加载地址为0x1000000)的VAD节点不再存在,因为当恶意软件将svchost.exe进程可执行文件空洞化时,相关的条目已从VAD树中移除:

要检测这种空洞进程注入变种,可以遵循相同的方法论。根据空洞进程注入的执行方式,结果会有所不同。进程列表显示了多个svchost.exe进程实例,这是正常的。除了最后一个svchost.exe (pid 1824),所有svchost.exe进程的父进程都是services.exepid 696)。在干净的系统上,所有svchost.exe进程都是由services.exe启动的。当你查看svchost.exepid 1824)的父进程时,你会发现其父进程已终止。根据进程信息,你可以判断最后一个svchost.exe (pid 1824)是可疑的:

$ python vol.py -f skeeyah.vmem --profile=WinXPSP3x86 pslist | grep -i svchost
Volatility Foundation Volatility Framework 2.6
0x815cfaa0 svchost.exe  876  696  20  202  0 0 2016-05-10 06:47:25
0x818c5a78 svchost.exe  960  696   9  227  0 0 2016-05-10 06:47:25
0x8181e558 svchost.exe 1044  696  68  1227 0 0 2016-05-10 06:47:25
0x818c7230 svchost.exe 1104  696   5  59   0 0 2016-05-10 06:47:25
0x81743da0 svchost.exe 1144  696  15  210  0 0 2016-05-10 06:47:25
0x817ba390 svchost.exe 1824 1768   1  26   0 0 2016-05-12 14:43:43

$ python vol.py -f skeeyah.vmem --profile=WinXPSP3x86 pslist -p 696
Volatility Foundation Volatility Framework 2.6
Offset(V)  Name         PID PPID Thds Hnds Sess Wow64  Start  
---------- ------------ --- ---- ---- ---- ---- ------ --------------------
0x8186c980 services.exe 696 652   16  264   0    0     2016-05-10 06:47:24

$ python vol.py -f skeeyah.vmem --profile=WinXPSP3x86 pslist -p 1768
Volatility Foundation Volatility Framework 2.6
ERROR : volatility.debug : Cannot find PID 1768\. If its terminated or unlinked, use psscan and then supply --offset=OFFSET

依赖于PEBdlllist插件显示了svchost.exepid 1824)的完整路径,并报告基地址为0x00400000

$ python vol.py -f skeeyah.vmem --profile=WinXPSP3x86 dlllist -p 1824
Volatility Foundation Volatility Framework 2.6
************************************************************************
svchost.exe pid: 1824
Command line : "C:\WINDOWS\system32\svchost.exe"
Service Pack 3

Base       Size    LoadCount  Path
---------- ------- ---------- ----------------------------------
0x00400000 0x7000   0xffff     C:\WINDOWS\system32\svchost.exe
0x7c900000 0xaf000  0xffff     C:\WINDOWS\system32\ntdll.dll
0x7c800000 0xf6000  0xffff     C:\WINDOWS\system32\kernel32.dll
[REMOVED]

另一方面,依赖于内核中的VADldrmodules插件并未显示svchost.exe的任何条目,如下图所示:

malfind显示在地址0x00400000存在一个 PE 文件,并且具有可疑的内存保护PAGE_EXECUTE_READWRITE,这表明该可执行文件是被注入的,而不是正常加载的:

$ python vol.py -f skeeyah.vmem --profile=WinXPSP3x86 malfind -p 1824
Volatility Foundation Volatility Framework 2.6
Process: svchost.exe Pid: 1824 Address: 0x400000
Vad Tag: VadS Protection: PAGE_EXECUTE_READWRITE
Flags: CommitCharge: 7, MemCommit: 1, PrivateMemory: 1, Protection: 6

0x00400000 4d 5a 90 00 03 00 00 00 04 00 00 00 ff ff 00 00 MZ..............
0x00400010 b8 00 00 00 00 00 00 00 40 00 00 00 00 00 00 00 ........@.......
0x00400020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0x00400030 00 00 00 00 00 00 00 00 00 00 00 00 e0 00 00 00 ................

0x00400000 4d DEC EBP
0x00400001 5a POP EDX
[REMOVED]

攻击者使用不同变种的空洞进程注入来绕过、偏转和转移取证分析。有关这些规避技术的详细信息,以及如何使用自定义 Volatility 插件来检测它们,请观看作者在 Black Hat 上的演讲:“恶意软件作者不希望你知道的——空洞进程注入的规避技巧”youtu.be/9L9I1T5QDg4)。另外,你也可以阅读作者的博客文章,链接如下:cysinfo.com/detecting-deceptive-hollowing-techniques/

3. 检测 API 钩子

在将恶意代码注入目标进程后,恶意软件可以挂钩目标进程的 API 调用,以控制其执行路径并将其重定向到恶意代码。挂钩技术的详细内容在第八章中讨论,代码注入与挂钩(在挂钩技术部分)。在本节中,我们将主要关注如何使用内存取证技术检测此类挂钩技术。为了识别进程和内核内存中的 API 挂钩,可以使用 apihooks Volatility 插件。在以下Zeus bot的示例中,一个可执行文件被注入到 explorer.exe 进程的内存中,地址为 0x2c70000,这一点通过 malfind 插件检测到:

$ python vol.py -f zeus.vmem --profile=Win7SP1x86 malfind

Process: explorer.exe Pid: 1608 Address: 0x2c70000
Vad Tag: Vad Protection: PAGE_EXECUTE_READWRITE
Flags: Protection: 6

0x02c70000 4d 5a 00 00 00 00 00 00 00 00 00 00 00 00 00 00 MZ..............
0x02c70010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0x02c70020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0x02c70030 00 00 00 00 00 00 00 00 00 00 00 00 d8 00 00 00 ................

在以下输出中,apihooks 插件检测到用户模式 API HttpSendRequestA(在 wininet.dll 中)的挂钩。被挂钩的 API 随后被重定向到地址 0x2c7ec48(挂钩地址)。挂钩地址位于注入的可执行文件(挂钩模块)的地址范围内。挂钩模块的名称未知,因为它通常不是从磁盘加载的(而是被注入的)。具体来说,在 API 函数 HttpSendRequestA 的起始地址(0x753600fc)处,有一条跳转指令,将 HttpSendRequestA 的执行流重定向到注入可执行文件中的地址 0x2c7ec48

$ python vol.py -f zeus.vmem --profile=Win7SP1x86 apihooks -p 1608

Hook mode: Usermode
Hook type: Inline/Trampoline
Process: 1608 (explorer.exe)
Victim module: wininet.dll (0x752d0000 - 0x753c4000)
Function: wininet.dll!HttpSendRequestA at 0x753600fc
Hook address: 0x2c7ec48
Hooking module: <unknown>

Disassembly(0):
0x753600fc e947eb918d   JMP 0x2c7ec48
0x75360101 83ec38       SUB ESP, 0x38
0x75360104 56           PUSH ESI
0x75360105 6a38         PUSH 0x38
0x75360107 8d45c8       LEA EAX, [EBP-0x38]

4. 内核模式根套件

像根套件这样的恶意程序可以加载一个内核驱动程序,在内核模式下运行代码。一旦它在内核空间中运行,就可以访问操作系统的内部代码,监控系统事件,通过修改内部数据结构、挂钩函数和修改调用表来规避检测。内核模式驱动程序通常具有 .sys 扩展名,并驻留在 %windir%\system32\drivers 目录下。内核驱动程序通常通过创建一个内核驱动程序服务(如第七章中描述的,恶意软件功能与持久性,服务*部分)来加载。

Windows 实施了各种安全机制,旨在防止内核空间中未经授权的代码执行。这使得 rootkit 难以安装内核驱动程序。在 64 位 Windows 上,微软实施了内核模式代码签名(KMCS),要求内核模式驱动程序在加载到内存中时必须经过数字签名。另一个安全机制是内核补丁保护(KPP),也称为PatchGuard,它防止对核心系统组件、数据结构和调用表(如 SSDT、IDT 等)的修改。这些安全机制对大多数 rootkit 有效,但同时迫使攻击者提出先进技术,使他们能够安装未签名驱动程序并绕过这些安全机制。一种方法是安装Bootkit。Bootkit 感染系统启动过程的早期阶段,甚至在操作系统完全加载之前。另一种方法是利用内核或第三方驱动程序的漏洞来安装未签名驱动程序。在本章的其余部分,我们将假设攻击者已经成功安装了内核模式驱动程序(使用Bootkit或利用内核级漏洞),并且我们将专注于内核内存取证,这涉及识别恶意驱动程序。

在一个干净的 Windows 系统上,你会发现数百个内核模块,因此找到恶意内核模块需要一些工作。在接下来的章节中,我们将看一些常见的定位和提取恶意内核模块的技术。我们将从列出内核模块开始。

5. 列出内核模块

要列出内核模块,你可以使用modules插件。该插件依赖于遍历由PsLoadedModuleList指向的元数据结构(KLDR_DATA_TABLE_ENTRY)的双向链表(这种技术类似于遍历_EPROCESS结构的双向链表,如第十章*,使用内存取证猎杀恶意软件*,在理解 ActiveProcessLinks部分中描述)。列出内核模块可能并不总是帮助你从加载的数百个内核模块中识别出恶意内核驱动程序,但它可以用于发现可疑指标,例如内核驱动程序具有奇怪的名称,或者内核模块从非标准路径或临时路径加载。modules插件按照加载顺序列出内核模块,这意味着如果最近安装了 rootkit 驱动程序,你很可能会在列表的末尾找到该模块,前提是该模块没有被隐藏并且在获取内存镜像之前系统没有重新启动。

在下面的受Laqma rootkit 感染的内存映像示例中,模块列表显示了位于列表末尾的恶意驱动程序Laqmalanmandrv.sys,该驱动程序来自C:\Windows\System32目录,而大多数其他内核驱动程序则加载自*S*ystemRoot\System32\DRIVERS\*。从列表中还可以看到,核心操作系统组件,如 NT 内核模块(ntkrnlpa.exentoskrnl.exe)和硬件抽象层(hal.dll)最先加载,然后是启动驱动程序(如kdcom.dll),它们会在启动时自动启动,接着是其他驱动程序:

$ python vol.py -f laqma.vmem --profile=Win7SP1x86 modules
Volatility Foundation Volatility Framework 2.6
Offset(V)  Name          Base       Size     File
---------- ------------  ---------- -------- ---------------------------------
0x84f41c98 ntoskrnl.exe 0x8283d000 0x410000 \SystemRoot\system32\ntkrnlpa.exe
0x84f41c20 hal.dll      0x82806000 0x37000  \SystemRoot\system32\halmacpi.dll
0x84f41ba0 kdcom.dll    0x80bc5000 0x8000   \SystemRoot\system32\kdcom.dll
[REMOVED]
0x86e36388 srv2.sys     0xa46e1000 0x4f000  \SystemRoot\System32\DRIVERS\srv2.sys
0x86ed6d68 srv.sys      0xa4730000 0x51000  \SystemRoot\System32\DRIVERS\srv.sys
0x86fe8f90 spsys.sys    0xa4781000 0x6a000  \SystemRoot\system32\drivers\spsys.sys
0x861ca0d0 lanmandrv.sys 0xa47eb000 0x2000  \??\C:\Windows\System32\lanmandrv.sys

由于遍历双向链表容易受到 DKOM 攻击(详见第十章*,《使用内存取证狩猎恶意软件》第 4.2.1 节 直接内核对象操作(DKOM)),因此可以通过解除链接来隐藏内核驱动程序。为了解决这个问题,可以使用另一个名为modscan的插件。modscan插件依赖于池标签扫描方法(详见第十章,《使用内存取证狩猎恶意软件》第 4.2.2 节 理解池标签扫描)。换句话说,它扫描物理地址空间,寻找与内核模块相关的池标签(MmLd)。通过池标签扫描,它可以检测到未链接的模块和先前加载的模块。modscan插件按照在物理地址空间中找到的顺序显示内核模块,而不是按加载顺序显示。在下面的Necurs* rootkit 示例中,modscan插件显示了恶意内核驱动程序(2683608180e436a1.sys),其名称完全由十六进制字符组成:

$ python vol.py -f necurs.vmem --profile=Win7SP1x86 modscan
Volatility Foundation Volatility Framework 2.6
Offset(P)          Name                 Base       Size   File
------------------ -------------------- ---------- ------ --------
0x0000000010145130 Beep.SYS             0x880f2000 0x7000 \SystemRoot\System32\Drivers\Beep.SYS
0x000000001061bad0 secdrv.SYS           0xa46a9000 0xa000 \SystemRoot\System32\Drivers\secdrv.SYS
0x00000000108b9120 rdprefmp.sys         0x88150000 0x8000 \SystemRoot\system32\drivers\rdprefmp.sys
0x00000000108b9b10 USBPORT.SYS          0x9711e000 0x4b000 \SystemRoot\system32\DRIVERS\USBPORT.SYS
0x0000000010b3b4a0 rdbss.sys            0x96ef6000 0x41000 \SystemRoot\system32\DRIVERS\rdbss.sys
[REMOVED]
0x000000001e089170 2683608180e436a1.sys 0x851ab000 0xd000 \SystemRoot\System32\Drivers\2683608180e436a1.sys
0x000000001e0da478 usbccgp.sys          0x9700b000 0x17000 \SystemRoot\system32\DRIVERS\usbccgp.sys

当你运行modules插件来检查受Necurs rootkit 感染的内存映像时,它不会显示那个恶意驱动程序(2683608180e436a1.sys):

$ python vol.py -f necurs.vmem --profile=Win7SP1x86 modules | grep 2683608180e436a1

由于modscan使用池标签扫描方法,可以检测已卸载的模块(前提是内存没有被覆盖),因此恶意驱动程序2683608180e436a1.sys可能已经被快速加载和卸载,或者它被隐藏了。为了确认驱动程序是否已卸载或隐藏,你可以使用unloadedmodules插件,它会显示已卸载模块的列表以及每个模块被卸载的时间。在以下输出中,恶意驱动程序2683608180e436a1.sys的缺失表明该驱动程序未被卸载,它被隐藏了。从以下输出中,你可以看到另一个恶意驱动程序2b9fb.sys,它曾被快速加载和卸载(在modulesmodscan列出的模块中没有显示,如以下代码所示)。unloadedmodules插件在调查过程中可以证明有用,帮助检测 rootkit 快速加载和卸载驱动程序的尝试,以使其不出现在模块列表中:

$ python vol.py -f necurs.vmem --profile=Win7SP1x86 unloadedmodules
Volatility Foundation Volatility Framework 2.6
Name              StartAddress EndAddress Time
----------------- ------------ ---------- -------------------
dump_dumpfve.sys  0x00880bb000 0x880cc000 2016-05-11 12:15:08 
dump_LSI_SAS.sys  0x00880a3000 0x880bb000 2016-05-11 12:15:08 
dump_storport.sys 0x0088099000 0x880a3000 2016-05-11 12:15:08 
parport.sys       0x0094151000 0x94169000 2016-05-11 12:15:09 
2b9fb.sys         0x00a47eb000 0xa47fe000 2018-05-21 10:57:52 

$ python vol.py -f necurs.vmem --profile=Win7SP1x86 modules | grep -i 2b9fb.sys
$ python vol.py -f necurs.vmem --profile=Win7SP1x86 modscan | grep -i 2b9fb.sys

5.1 使用 driverscan 列出内核模块

列出内核模块的另一种方法是使用driverscan插件,如以下代码所示。driverscan插件从名为DRIVER_OBJECT的结构中获取与内核模块相关的信息。具体来说,driverscan插件通过池标签扫描来查找物理地址空间中的驱动程序对象。第一列Offset(P)指定了找到DRIVER_OBJECT结构的物理地址,第二列Start包含模块的基地址,Driver Name列显示驱动程序的名称。例如,驱动程序名称\Driver\BeepBeep.sys相同,最后一行显示与Necurs rootkit 相关的恶意驱动程序\Driver\2683608180e436a1driverscan插件是列出内核模块的另一种方法,当 rootkit 试图隐藏在modulesmodscan插件下时,这种方法非常有用:

$ python vol.py -f necurs.vmem --profile=Win7SP1x86 driverscan
Volatility Foundation Volatility Framework 2.6
Offset(P)           Start     Size   Service Key  Name   Driver Name
------------------ --------  ------- ----------- ------ -----------
0x00000000108b9030 0x88148000 0x8000  RDPENCDD  RDPENCDD \Driver\RDPENCDD
0x00000000108b9478 0x97023000 0xb7000 DXGKrnl   DXGKrnl  \Driver\DXGKrnl
0x00000000108b9870 0x88150000 0x8000  RDPREFMP  RDPREFMP \Driver\RDPREFMP
0x0000000010b3b1d0 0x96ef6000 0x41000 rdbss     rdbss    \FileSystem\rdbss
0x0000000011781188 0x88171000 0x17000 tdx       tdx      \Driver\tdx
0x0000000011ff6a00 0x881ed000 0xd000  kbdclass  kbdclass \Driver\kbdclass
0x0000000011ff6ba0 0x880f2000 0x7000  Beep      Beep     \Driver\Beep
[REMOVED]
0x000000001e155668 0x851ab000 0xd000 2683608180e436a1 26836...36a1 \Driver\2683608180e436a1

要使用内核调试器(Windbg)列出内核模块,使用lm k命令,如下所示。要获取详细输出,可以使用lm kv命令:

kd> lm k
start end module name
80bb4000 80bbc000 kdcom (deferred) 
82a03000 82a3a000 hal (deferred) 
82a3a000 82e56000 nt (pdb symbols)
8b200000 8b20e000 WDFLDR (deferred) 
8b20e000 8b22a800 vmhgfs (deferred) 
8b22b000 8b2b0000 mcupdate_GenuineIntel (deferred) 
8b2b0000 8b2c1000 PSHED (deferred) 
8b2c1000 8b2c9000 BOOTVID (deferred) 
8b2c9000 8b30b000 CLFS (deferred) 
[REMOVED]

一旦你识别出恶意内核模块,你可以使用moddump插件将其从内存转储到磁盘。要将模块转储到磁盘,你需要指定模块的基地址,可以通过modulesmodscandriverscan插件获取。以下示例中,Necurs rootkit的恶意驱动程序通过其基地址转储到磁盘,如下所示:

$ python vol.py -f necurs.vmem --profile=Win7SP1x86 moddump -b 0x851ab000 -D dump/
Volatility Foundation Volatility Framework 2.6
Module Base   Module Name      Result
-----------  --------------    ------
0x0851ab000    UNKNOWN         OK: driver.851ab000.sys

6. I/O 处理

在讨论driverscan插件时,我曾提到driverscanDRIVER_OBJECT结构中获取模块信息。你是不是在想DRIVER_OBJECT结构是什么?这个问题很快就会清楚。在本节中,你将理解用户模式与内核模式组件之间的交互、设备驱动程序的作用以及它与 I/O 管理器的交互。通常,rootkit 包括一个用户模式组件(EXE 或 DLL)和一个内核模式组件(设备驱动程序)。rootkit 的用户模式组件通过特定机制与内核模式组件进行通信。从取证角度来看,理解这些通信是如何工作的以及涉及的组件非常重要。本节将帮助你理解这种通信机制,并为接下来的主题打下基础。

让我们尝试理解当用户模式应用程序执行输入/输出(I/O)操作时发生了什么,以及它在高层次上是如何处理的。在讨论第八章中的 API 调用流程时,代码注入与钩取(在Windows API 调用流程部分),我用一个用户模式应用程序通过WriteFile() API 执行写操作的例子,这最终会调用内核执行中的NtWriteFile()系统服务例程(ntoskrnl.exe),然后将请求指引给 I/O 管理器,接着 I/O 管理器请求设备驱动程序执行 I/O 操作。在这里,我将再次回顾这个话题,提供更多细节,并重点强调内核空间组件(主要是设备驱动程序和 I/O 管理器)。以下图示说明了写请求的流程(其他类型的 I/O 请求,如读取,也类似;它们只使用不同的 API):

以下几点讨论了设备驱动程序和 I/O 管理器在高层次的作用:

  1. 设备驱动程序通常会创建一个或多个设备,并指定它可以处理的操作类型(打开、读取和写入)。它还指定了处理这些操作的例程地址。这些例程被称为分发例程或 IRP 处理程序。

  2. 创建设备后,驱动程序会发布该设备,以便用户模式应用程序可以访问。

  3. 用户模式的应用程序可以使用 API 调用,如CreateFile,打开已公开的设备句柄,并使用ReadFileWriteFile API 在设备上执行 I/O 操作,如读取和写入。用于执行文件 I/O 操作的 API,如CreateFileReadWriteWriteFile,也适用于设备。这是因为设备被视为虚拟文件。

  4. 当用户模式应用程序在广告设备上执行 I/O 操作时,请求将被路由到 I/O 管理器。 I/O 管理器确定处理设备的驱动程序,并请求驱动程序通过传递 IRP(I/O 请求数据包)来完成操作。 IRP 是一个包含执行操作和 I/O 操作所需缓冲区信息的数据结构。

驱动程序读取 IRP,验证它,并在通知 I/O 管理器有关操作状态之前完成请求的操作。 然后,I/O 管理器将状态和数据返回给用户应用程序。

在这个阶段,前面的内容可能对您来说很陌生,但不要让它让您灰心:当您完成本节时,一切将变得清晰。 接下来,我们将看一下设备驱动程序的作用,然后是 I/O 管理器的作用。

6.1 设备驱动程序的作用

当驱动程序加载到系统中时,I/O 管理器会创建一个驱动程序对象(DRIVER_OBJECT结构)。 然后,I/O 管理器通过将指向DRIVER_OBJECT结构的指针作为参数调用驱动程序的初始化例程DriverEntry(类似于main()WinMain()函数)。 驱动程序对象(DRIVER_OBJECT结构)代表系统上的一个驱动程序。 DriverEntry例程将使用DRIVER_OBJECT来填充具有处理特定 I/O 请求的驱动程序各种入口点的结构。 通常,在DriverEntry例程中,驱动程序会使用一个名为IoCreateDeviceIoCreateDevice-Secure的 API 创建代表逻辑或物理设备的设备对象(DEVICE_OBJECT结构)。 当驱动程序创建设备对象时,可以选择为设备分配名称,也可以创建多个设备。 创建设备后,将更新指向第一个创建的设备的指针在驱动程序对象中。 为了帮助您更好地理解这一点,让我们列出加载的内核模块,并查看一个简单内核模块的驱动程序对象。 作为示例,我们将检查null.sys内核驱动程序。 根据微软文档,Null 设备驱动程序提供了 Unix 环境中\dev\null的功能等效物。 当系统在内核初始化阶段启动时,null.sys被加载到系统中。 在内核模块列表中,您可以看到null.sys加载在基地址8bcde000处:

kd> lm k
start end module name
80ba2000 80baa000 kdcom (deferred) 
81e29000 81e44000 luafv (deferred) 
[REMOVED]
8bcde000 8bce5000 Null (deferred)

由于null.sys已经加载,在驱动程序初始化过程中,它的驱动对象(DRIVER_OBJECT结构)将会填充元数据。在驱动程序对象中查看它包含什么信息。你可以使用!drvobj扩展命令显示驱动程序对象信息。从以下输出可以看到,表示null.sys的驱动对象位于地址86a33180Device Object list下的86aa2750是指向由null.sys创建的设备对象的指针。如果驱动程序创建了多个设备,你将会在Device Object list下看到多个条目:

kd> !drvobj Null
Driver object (86a33180) is for:
 \Driver\Null
Driver Extension List: (id , addr)

Device Object list:
86aa2750

你可以使用驱动程序对象地址86a33180来通过dt (display type)命令检查null.sys_DRIVER_OBJECT结构。从以下输出可以看到,DriverStart字段包含驱动程序的基地址(0x8bcde000),DriverSize字段包含driver(0x7000)的大小,Drivername是驱动对象的名称(\Driver\Null)。DriverInit字段保存指向驱动初始化例程DriverEntry)的指针。DriverUnload字段包含指向驱动程序卸载例程的指针,该例程通常会在卸载过程中释放驱动程序创建的资源。MajorFunction字段是最重要的字段之一,它指向一个包含 28 个主要功能指针的表。这个表将会填充调度例程的地址,我们将在本节稍后查看MajorFunction表。前面讲到的driverscan插件会对驱动程序对象执行池标签扫描,并通过读取这些字段中的某些信息获取与内核模块相关的信息,如基地址、大小和驱动程序名称:

kd> dt nt!_DRIVER_OBJECT 86a33180
   +0x000 Type : 0n4
   +0x002 Size : 0n168
   +0x004 DeviceObject : 0x86aa2750 _DEVICE_OBJECT
   +0x008 Flags : 0x12
   +0x00c DriverStart : 0x8bcde000 Void
   +0x010 DriverSize : 0x7000
   +0x014 DriverSection : 0x86aa2608 Void
   +0x018 DriverExtension : 0x86a33228 _DRIVER_EXTENSION
   +0x01c DriverName : _UNICODE_STRING "\Driver\Null"
   +0x024 HardwareDatabase : 0x82d86270 _UNICODE_STRING "\REGISTRY\MACHINE\HARDWARE\DESCRIPTION\SYSTEM"
   +0x028 FastIoDispatch : 0x8bce0000 _FAST_IO_DISPATCH
   +0x02c DriverInit : 0x8bce20bc long Null!GsDriverEntry+0
   +0x030 DriverStartIo : (null) 
   +0x034 DriverUnload : 0x8bce1040 void Null!NlsUnload+0
   +0x038 MajorFunction : [28] 0x8bce107c

DRIVER_OBJECT结构中的DeviceObject字段包含指向驱动程序(null.sys)创建的设备对象的指针。你可以使用设备对象地址0x86aa2750来确定驱动程序创建的设备的名称。在本例中,Null是由驱动程序null.sys创建的设备名称:

kd> !devobj 86aa2750
Device object (86aa2750) is for:
 Null \Driver\Null DriverObject 86a33180
Current Irp 00000000 RefCount 0 Type 00000015 Flags 00000040
Dacl 8c667558 DevExt 00000000 DevObjExt 86aa2808 
ExtensionFlags (0x00000800) DOE_DEFAULT_SD_PRESENT
Characteristics (0x00000100) FILE_DEVICE_SECURE_OPEN
Device queue is not busy.

你也可以通过在display type (dt)命令后面指定设备对象地址来查看实际的DEVICE_OBJECT结构,如下所示的代码。如果驱动程序创建了多个设备,那么DEVICE_OBJECT结构中的NextDevice字段将指向下一个设备对象。由于null.sys驱动程序只创建了一个设备,NextDevice字段被设置为null

kd> dt nt!_DEVICE_OBJECT 86aa2750
   +0x000 Type : 0n3
   +0x002 Size : 0xb8
   +0x004 ReferenceCount : 0n0
   +0x008 DriverObject : 0x86a33180 _DRIVER_OBJECT
   +0x00c NextDevice : (null) 
   +0x010 AttachedDevice : (null) 
   +0x014 CurrentIrp : (null) 
   +0x018 Timer : (null) 
   +0x01c Flags : 0x40
   +0x020 Characteristics : 0x100
   +0x024 Vpb : (null) 
   +0x028 DeviceExtension : (null) 
   +0x02c DeviceType : 0x15
   +0x030 StackSize : 1 ''
   [REMOVED]

从上面的输出可以看到,DEVICE_OBJECT包含一个DriverObject字段,它指向回驱动程序对象。换句话说,可以通过设备对象确定相关联的驱动程序。这就是 I/O 管理器如何在收到特定设备的 I/O 请求时,确定关联的驱动程序的方式。这个概念可以通过以下图示来可视化:

你可以使用像 DeviceTree (www.osronline.com/article.cfm?article=97) 这样的图形界面工具来查看驱动程序创建的设备。以下是该工具显示 null.sys 驱动程序创建的 Null 设备的截图:

当驱动程序创建一个设备时,设备对象会被放置在 Windows 对象管理器的命名空间中的 \Device 目录下。要查看对象管理器的命名空间信息,可以使用 WinObj 工具 (docs.microsoft.com/en-us/sysinternals/downloads/winobj)。以下截图显示了由 null.sys\Device 目录下创建的设备(Null)。你还可以看到其他驱动程序创建的设备:

创建在 \Device 目录下的设备对于用户模式下运行的应用程序是不可访问的。换句话说,如果一个用户模式应用程序想要对设备执行 I/O 操作,它不能通过将设备名称(如 \Device\Null)作为参数传递给 CreateFile 函数来直接打开设备句柄。CreateFile 函数不仅用于创建或打开文件,还可以用于打开设备句柄。如果用户模式应用程序无法访问设备,那它如何执行 I/O 操作呢?为了使设备对用户模式应用程序可访问,驱动程序需要宣传该设备。这可以通过为设备创建一个符号链接来实现。驱动程序可以使用内核 API IoCreateSymbolicLink 来创建符号链接。当为设备(如 \Device\Null)创建符号链接时,你可以在对象管理器命名空间中的 \GLOBAL?? 目录下找到它,也可以使用 WinObj 工具查看。以下截图中,你可以看到 NUL 是由 null.sys 驱动程序为 \Device\Null 设备创建的符号链接名称:

符号链接也被称为 MS-DOS 设备名称。用户模式应用程序可以直接使用符号链接的名称(MS-DOS 设备名称)来通过约定 \\.\<symboliclink name> 打开设备的句柄。例如,要打开 \Device\Null 的句柄,用户模式应用程序只需将 \\.\NUL 作为第一个参数(lpFilename)传递给 CreateFile 函数,该函数会返回设备的文件句柄。具体来说,在对象管理器的 GLOBAL?? 目录下的任何符号链接都可以通过 CreateFile 函数打开。如下面的截图所示,C: 盘符仅仅是 \Device\HarddiskVolume1 的符号链接。在 Windows 中,I/O 操作是在虚拟文件上执行的。换句话说,设备、目录、管道和文件都被视为虚拟文件(可以通过 CreateFile 函数打开):

此时,你已经知道驱动程序在初始化过程中会创建设备,并通过符号链接向用户应用程序宣传它。现在,问题是,驱动程序如何告诉 I/O 管理器它支持哪些类型的操作(如打开、读取、写入等)?在初始化过程中,驱动程序通常会做的另一件事是更新Major function table(分派例程数组),并将分派例程的地址填充到DRIVER_OBJECT结构中。检查主要功能表将帮助你了解驱动程序支持哪些操作(如打开、读取、写入等)以及与特定操作相关的分派例程地址。主要功能表是一个包含28个函数指针的数组;索引值027表示特定操作。例如,索引值0对应于主要功能代码IRP_MJ_CREATE,索引值3对应于主要功能代码IRP_MJ_READ,依此类推。换句话说,如果应用程序想要打开文件或设备对象的句柄,请求将被发送给 I/O 管理器,然后 I/O 管理器使用IRP_MJ_CREATE主要功能代码作为索引查找主要功能表中的分派例程地址,来处理该请求。对于读取操作,也是同样的方式,使用IRP_MJ_READ作为索引来确定分派例程的地址。

以下!drvobj命令显示由null.sys驱动程序填充的分派例程数组。对于驱动程序不支持的操作,会指向ntoskrnl.exent)中的IopInvalidDeviceRequest。根据这些信息,你可以知道null.sys仅支持IRP_MJ_CREATE(打开)、IRP_MJ_CLOSE(关闭)、IRP_MJ_READ(读取)、IRP_MJ_WRITE(写入)、IRP_MJ_QUERY_INFORMATION(查询信息)和IRP_MJ_LOCK_CONTROL(锁控制)操作。任何请求执行这些支持的操作将会被分派到适当的分派例程。例如,当用户应用程序执行write操作时,写入设备的请求会被分派到null.sys驱动程序卸载例程中的MajorFunction[IRP_MJ_WRITE]函数,该函数位于地址8bce107c。在null.sys的情况下,所有支持的操作都被分派到相同的地址8bce107c。通常情况下,情况并非如此;你会看到处理不同操作的不同例程地址:

kd> !drvobj Null 2
Driver object (86a33180) is for:
 \Driver\Null
DriverEntry: 8bce20bc Null!GsDriverEntry
DriverStartIo: 00000000 
DriverUnload: 8bce1040 Null!NlsUnload
AddDevice: 00000000 

Dispatch routines:
[00] IRP_MJ_CREATE                   8bce107c Null!NlsUnload+0x3c
[01] IRP_MJ_CREATE_NAMED_PIPE        82ac5fbe nt!IopInvalidDeviceRequest
[02] IRP_MJ_CLOSE                    8bce107c Null!NlsUnload+0x3c
[03] IRP_MJ_READ                     8bce107c Null!NlsUnload+0x3c
[04] IRP_MJ_WRITE                    8bce107c Null!NlsUnload+0x3c
[05] IRP_MJ_QUERY_INFORMATION        8bce107c Null!NlsUnload+0x3c
[06] IRP_MJ_SET_INFORMATION          82ac5fbe nt!IopInvalidDeviceRequest
[07] IRP_MJ_QUERY_EA                 82ac5fbe nt!IopInvalidDeviceRequest
[08] IRP_MJ_SET_EA                   82ac5fbe nt!IopInvalidDeviceRequest
[09] IRP_MJ_FLUSH_BUFFERS            82ac5fbe nt!IopInvalidDeviceRequest
[0a] IRP_MJ_QUERY_VOLUME_INFORMATION 82ac5fbe nt!IopInvalidDeviceRequest
[0b] IRP_MJ_SET_VOLUME_INFORMATION   82ac5fbe nt!IopInvalidDeviceRequest
[0c] IRP_MJ_DIRECTORY_CONTROL        82ac5fbe nt!IopInvalidDeviceRequest
[0d] IRP_MJ_FILE_SYSTEM_CONTROL      82ac5fbe nt!IopInvalidDeviceRequest
[0e] IRP_MJ_DEVICE_CONTROL           82ac5fbe nt!IopInvalidDeviceRequest
[0f] IRP_MJ_INTERNAL_DEVICE_CONTROL  82ac5fbe nt!IopInvalidDeviceRequest
[10] IRP_MJ_SHUTDOWN                 82ac5fbe nt!IopInvalidDeviceRequest
[11] IRP_MJ_LOCK_CONTROL             8bce107c Null!NlsUnload+0x3c
[12] IRP_MJ_CLEANUP                  82ac5fbe nt!IopInvalidDeviceRequest
[13] IRP_MJ_CREATE_MAILSLOT          82ac5fbe nt!IopInvalidDeviceRequest
[14] IRP_MJ_QUERY_SECURITY           82ac5fbe nt!IopInvalidDeviceRequest
[15] IRP_MJ_SET_SECURITY             82ac5fbe nt!IopInvalidDeviceRequest
[16] IRP_MJ_POWER                    82ac5fbe nt!IopInvalidDeviceRequest
[17] IRP_MJ_SYSTEM_CONTROL           82ac5fbe nt!IopInvalidDeviceRequest
[18] IRP_MJ_DEVICE_CHANGE            82ac5fbe nt!IopInvalidDeviceRequest
[19] IRP_MJ_QUERY_QUOTA              82ac5fbe nt!IopInvalidDeviceRequest
[1a] IRP_MJ_SET_QUOTA                82ac5fbe nt!IopInvalidDeviceRequest
[1b] IRP_MJ_PNP                      82ac5fbe nt!IopInvalidDeviceRequest 

你也可以在DeviceTree工具中查看支持的操作,如以下截图所示:

到此为止,你已经知道驱动程序创建设备,将其广告宣传以供用户应用程序使用,并且还更新了调度例程数组(主要功能表),告知 I/O 管理器它支持的操作。现在,让我们来看一下 I/O 管理器的作用,并了解从用户应用程序接收到的 I/O 请求是如何调度到驱动程序的。

6.2 I/O 管理器的作用

当 I/O 请求到达 I/O 管理器时,I/O 管理器定位驱动程序并创建一个 IRP(I/O 请求包),它是一个包含描述 I/O 请求信息的数据结构。对于读取、写入等操作,I/O 管理器创建的 IRP 还包含一个内核内存中的缓冲区,用于驱动程序存储从设备读取的数据或写入设备的数据。I/O 管理器创建的 IRP 随后传递给正确的驱动程序调度例程。驱动程序接收 IRP,IRP 中包含描述要执行的操作(如打开、读取或写入)的主要功能代码(IRP_MJ_XXX)。在启动 I/O 操作之前,驱动程序会进行检查,以确保一切正常(例如,提供的读取或写入操作的缓冲区足够大),然后启动 I/O 操作。如果需要对硬件设备执行 I/O 操作,驱动程序通常会经过 HAL 例程。工作完成后,驱动程序将 IRP 返回给 I/O 管理器,告诉它请求的 I/O 操作已完成,或者因为 IRP 必须传递给驱动程序堆栈中的另一个驱动程序进行进一步处理。I/O 管理器在任务完成时释放 IRP,或者将 IRP 传递给设备堆栈中的下一个驱动程序以完成 IRP。任务完成后,I/O 管理器将状态和数据返回给用户模式应用程序。

到此为止,你应该已经理解了 I/O 管理器的作用。有关 I/O 系统和设备驱动程序的详细信息,请参考 Pavel Yosifovich、Alex Ionescu、Mark E. Russinovich 和 David A. Solomon 所著的《Windows Internals, Part 1: 第七版》一书。

6.3 与设备驱动程序的通信

现在,让我们重新审视用户模式组件和内核模式组件之间的交互。我们将回到 null.sys 驱动程序的例子,并从用户模式触发一个写入操作到它的设备(\Device\Null),并监控发送到 null.sys 驱动程序的 IRP。为了监控发送到驱动程序的 IRP 包,我们可以使用 IrpTracker 工具(www.osronline.com/article.cfm?article=199)。要启动 IrpTracker,请以管理员身份启动,点击文件 | 选择驱动程序,输入驱动程序的名称(在此例中为 null),如以下截图所示,然后点击确认按钮:

现在,要触发 I/O 操作,你可以打开命令提示符并输入以下命令。这将把字符串 "hello" 写入空设备。正如之前提到的,符号链接名称是用户模式应用程序(如 cmd.exe)可以使用的;这就是我指定设备符号链接名称(NUL)来写入内容的原因:

C:\>echo "hello" > NUL

设备被视为虚拟文件,在写入设备之前,会通过 CreateFile() 打开设备的句柄(CreateFile() 是一个用于创建/打开文件或设备的 API)。CreateFile() API 最终会调用 ntoskrnl.exe 中的 NtCreateFile(),该函数将请求发送给 I/O 管理器。I/O 管理器根据符号链接名称查找与设备关联的驱动程序,并调用其与 IRP_MJ_CREATE 主功能代码对应的分派例程。在设备的句柄被打开后,写入操作将使用 WriteFile() 执行,该操作将调用 NtWriteFile。该请求将被 I/O 管理器分派到与 IRP_MJ_WRITE 主功能代码对应的驱动程序例程。以下截图显示了与 IRP_MJ_CREATEIRP_MJ_WRITE 对应的驱动程序分派例程调用及其完成状态:

在这一点上,你应该已经理解了执行 I/O 操作的用户模式代码如何与内核模式驱动程序进行通信。Windows 还支持另一种机制,允许用户模式代码直接与内核模式设备驱动程序通信。这是通过使用通用 API DeviceIoControl(由kernel32.dll导出)来完成的。该 API 接受设备的句柄作为其中一个参数。它接受的另一个参数是控制代码,称为IOCTL(I/O 控制)代码,它是一个 32 位的整数值。每个控制代码标识一个要执行的特定操作以及在哪个设备上执行该操作。用户模式应用程序可以打开设备句柄(使用CreateFile),调用DeviceIoControl,并传递 Windows 操作系统提供的标准控制代码来直接对设备执行输入输出操作,例如硬盘驱动器、磁带驱动器或 CD-ROM 驱动器。此外,设备驱动程序(例如 rootkit 驱动程序)可以定义自己的设备特定控制代码,用户模式的 rootkit 组件可以通过DeviceIoControl API 与驱动程序进行通信。当用户模式组件通过传递IOCTL代码调用DeviceIoControl时,它会调用ntdll.dll中的NtDeviceIoControlFile,该函数将线程切换到内核模式,并调用 Windows 执行系统ntoskrnl.exe中的系统服务例程NtDeviceIoControlFile。Windows 执行系统调用 I/O 管理器,I/O 管理器构建包含 IOCTL 代码的 IRP 数据包,然后将其路由到由IRP_MJ_DEVICE_CONTROL标识的内核调度例程。以下图示展示了用户模式代码与内核模式驱动程序之间的通信概念:

6.4 I/O 请求到分层驱动程序

到目前为止,你已经理解了如何通过一个由单一驱动程序控制的简单设备来处理 I/O 请求;I/O 请求可以经过多个驱动程序层次;这些层次的 I/O 处理方式基本相同。以下截图展示了 I/O 请求如何在达到硬件设备之前通过多个驱动程序层的一个例子:

这个概念通过一个例子来更好地理解,因此我们通过以下命令触发对c:\abc.txt的写操作。当该命令执行时,netstat将打开abc.txt的句柄并写入其中:

C:\Windows\system32>netstat -an -t 60 > C:\abc.txt

这里需要注意的一点是,文件名*(C:\abc.txt)*也包含了文件所在设备的名称,即,C:驱动器是设备\Device\HarddiskVolume1的符号链接名称(你可以使用之前提到的WinObj工具进行验证)。这意味着写操作将被路由到与设备\Device\HarddiskVolume1相关联的驱动程序。当netstat.exe打开abc.txt时,I/O 管理器创建一个文件对象(FILE_OBJECT结构)并在文件对象中存储指向设备对象的指针,然后将句柄返回给netstat.exe.。以下是来自ProcessHacker工具的截图,显示了已由netstat.exe.打开的C:\abc.txt句柄。对象地址0x85f78ce8代表文件对象:

你可以通过以下方式使用对象地址检查文件对象(FILE_OBJECT)。从输出中,你可以看到FileName字段包含了文件的名称,而DeviceObject字段包含了指向设备对象(DEVICE_OBJECT)的指针:

kd> dt nt!_FILE_OBJECT 0x85f78ce8
   +0x000 Type : 0n5
   +0x002 Size : 0n128
   +0x004 DeviceObject : 0x868e7e20 _DEVICE_OBJECT
   +0x008 Vpb : 0x8688b658 _VPB
   +0x00c FsContext : 0xa74fecf0 Void
   [REMOVED]
   +0x030 FileName : _UNICODE_STRING "\abc.txt"
   +0x038 CurrentByteOffset : _LARGE_INTEGER 0xe000

如前所述,通过设备对象,可以确定设备的名称和相关的驱动程序。这是 I/O 管理器决定将 I/O 请求传递给哪个驱动程序的方式。以下输出显示了设备的名称HarddiskVolume1及其相关的驱动程序volmgr.sys.AttachedDevice字段告诉你,fvevol.sys驱动程序下有一个没有命名的设备对象(868e7b28),它位于设备对象HarddiskVolume1之上,在设备栈中:

kd> !devobj 0x868e7e20
Device object (868e7e20) is for:
 HarddiskVolume1 \Driver\volmgr DriverObject 862e0bd8
Current Irp 00000000 RefCount 13540 Type 00000007 Flags 00201150
Vpb 8688b658 Dacl 8c7b3874 DevExt 868e7ed8 DevObjExt 868e7fc0 Dope 86928870 DevNode 86928968 
ExtensionFlags (0x00000800) DOE_DEFAULT_SD_PRESENT
Characteristics (0000000000) 
AttachedDevice (Upper) 868e7b28 \Driver\fvevol
Device queue is not busy.

要确定 I/O 请求经过的驱动程序层级,你可以使用!devstack内核调试命令并传递设备对象地址,以显示与特定设备对象相关的设备栈(分层设备对象)。以下输出显示了与\Device\HarddiskVolume1相关的设备栈,该设备由volmgr.sys.拥有。第四列中的>字符表示该条目与设备HarddiskVolume1相关,且该行之上的条目是位于volmgr.sys.之上的驱动程序列表。这意味着 I/O 请求将首先传递给volsnap.sys,根据请求的类型,volsnap.sys可以处理 IRP 请求并将请求传递给栈中的其他驱动程序,最终到达volmgr.sys

kd> !devstack 0x868e7e20
  !DevObj !DrvObj !DevExt ObjectName
  85707658 \Driver\volsnap 85707710 
  868e78c0 \Driver\rdyboost 868e7978 
  868e7b28 \Driver\fvevol 868e7be0 
> 868e7e20 \Driver\volmgr 868e7ed8 HarddiskVolume1

要查看设备树,你可以使用我们之前提到的 GUI 工具DeviceTree。该工具将驱动程序显示在树的外侧,而它们的设备则缩进一级。附加设备会进一步缩进,如下图所示。你可以将以下截图与之前的!devstack输出进行对比,从而了解如何解读这些信息:

理解这种分层方法很重要,因为有时,rootkit 驱动程序可以插入或附加到目标设备的堆栈上方或下方以接收IRP。通过这种技术,rootkit 驱动程序可以在将IRP传递给合法驱动程序之前,记录或修改IRP。例如,键盘记录器可以通过插入一个恶意驱动程序(该驱动程序位于键盘功能驱动程序上方)来记录按键。

7. 显示设备树

你可以使用 Volatility 中的devicetree插件,以与DeviceTree工具相同的格式显示设备树。以下高亮的条目显示了与volmgr.sys相关联的HarddiskVolume1设备堆栈:

$ python vol.py -f win7_x86.vmem --profile=Win7SP1x86 devicetree

DRV 0x05329db8 \Driver\WMIxWDM
---| DEV 0x85729a38 WMIAdminDevice FILE_DEVICE_UNKNOWN
---| DEV 0x85729b60 WMIDataDevice FILE_DEVICE_UNKNOWN
[REMOVED]

DRV 0xbf2e0bd8 \Driver\volmgr
---| DEV 0x868e7e20 HarddiskVolume1 FILE_DEVICE_DISK
------| ATT 0x868e7b28 - \Driver\fvevol FILE_DEVICE_DISK
---------| ATT 0x868e78c0 - \Driver\rdyboost FILE_DEVICE_DISK
------------| ATT 0x85707658 - \Driver\volsnap FILE_DEVICE_DISK
[REMOVED]

为了帮助你理解devicetree插件在取证调查中的使用,下面我们来看一个创建自己设备来存储恶意二进制文件的恶意软件。在接下来的 ZeroAccess rootkit 示例中,我使用了cmdline插件,它显示进程的命令行参数。这对于确定进程的完整路径非常有用(你也可以使用dlllist插件)。从输出中,你可以看到最后一个svchost.exe进程是从一个可疑的命名空间中运行的:

svchost.exe pid: 624
Command line : C:\Windows\system32\svchost.exe -k DcomLaunch
svchost.exe pid: 712
Command line : C:\Windows\system32\svchost.exe -k RPCSS
svchost.exe pid: 764
Command line : C:\Windows\System32\svchost.exe -k LocalServiceNetworkRestricted
svchost.exe pid: 876
Command line : C:\Windows\System32\svchost.exe -k LocalSystemNetworkRestricted
[REMOVED]

svchost.exe pid: 1096
Command line : "\\.\globalroot\Device\svchost.exe\svchost.exe"

从之前的讨论中,如果你还记得,\\.\<symbolic link name> 是从用户模式使用符号链接名称访问设备的约定。当驱动程序为设备创建符号链接时,它会被添加到\GLOBAL??目录中,该目录位于对象管理器命名空间中(可以使用WinObj工具查看,正如我们之前讨论的)。在这种情况下,globalroot是符号链接的名称。那么,问题是,\\.\globalroot是什么?事实证明,\\.\globalroot指的是\GLOBAL??命名空间。换句话说,\\.\globalroot\Device\svchost.exe\svchost.exe路径与\Device\svchost.exe\svchost.exe路径是相同的。此时,你知道 ZeroAccess rootkit 创建了自己的设备(svchost.exe)来隐藏其恶意二进制文件svchost.exe。要识别创建此设备的驱动程序,你可以使用devicetree插件。从以下输出中,你可以看出svchost.exe设备是由00015300.sys驱动程序创建的:

$ python vol.py -f zaccess1.vmem --profile=Win7SP1x86 devicetree
[REMOVED]
DRV 0x1fc84478 \Driver\00015300
---| DEV 0x84ffbf08 svchost.exe FILE_DEVICE_DISK

在接下来的BlackEnergy恶意软件示例中,它将磁盘上的合法aliide.sys驱动程序替换为恶意驱动程序,以劫持现有服务(如在第十章,使用内存取证猎杀恶意软件部分中所述)。当服务启动时,恶意驱动程序创建一个设备与恶意用户模式组件(注入到合法svchost.exe中的 DLL)进程进行通信。以下devicetree输出显示了恶意驱动程序创建的设备:

$ python vol.py -f be3_big_restart.vmem --profile=Win7SP1x64 devicetree | grep -i aliide -A1
Volatility Foundation Volatility Framework 2.6
DRV 0x1e45fbe0 \Driver\aliide
---| DEV 0xfffffa8008670e40 {C9059FFF-1C49-4445-83E8-4F16387C3800} FILE_DEVICE_UNKNOWN

为了了解恶意驱动程序支持的操作类型,您可以使用 Volatility 的 driverirp 插件,因为它可以显示与特定驱动程序或所有驱动程序相关的主要 IRP 函数。从以下输出中,您可以看出恶意 aliide 驱动程序支持 IRP_MJ_CREATE (打开)IRP_MJ_CLOSE (关闭)IRP_MJ_DEVICE_CONTROL (DeviceIoControl) 操作。驱动程序不支持的操作通常指向 ntoskrnl.exe 中的 IopInvalidDeviceRequest,这也是您看到所有其他不受支持的操作指向 0xfffff80002a5865cntoskrnl.exe 中的原因:

$ python vol.py -f be3_big_restart.vmem --profile=Win7SP1x64 driverirp -r aliide
Volatility Foundation Volatility Framework 2.6
--------------------------------------------------
DriverName: aliide
DriverStart: 0xfffff88003e1d000
DriverSize: 0x14000
DriverStartIo: 0x0
   0 IRP_MJ_CREATE                  0xfffff88003e1e160 aliide.sys
   1 IRP_MJ_CREATE_NAMED_PIPE       0xfffff80002a5865c ntoskrnl.exe
   2 IRP_MJ_CLOSE                   0xfffff88003e1e160 aliide.sys
   3 IRP_MJ_READ                    0xfffff80002a5865c ntoskrnl.exe
   4 IRP_MJ_WRITE                   0xfffff80002a5865c ntoskrnl.exe
  [REMOVED]
  12 IRP_MJ_DIRECTORY_CONTROL       0xfffff80002a5865c ntoskrnl.exe
  13 IRP_MJ_FILE_SYSTEM_CONTROL     0xfffff80002a5865c ntoskrnl.exe
  14 IRP_MJ_DEVICE_CONTROL          0xfffff88003e1e160 aliide.sys
  15 IRP_MJ_INTERNAL_DEVICE_CONTROL 0xfffff80002a5865c ntoskrnl.exe
  [REMOVED]

8. 检测内核空间劫持

在讨论劫持技术时(参见第八章,代码注入与劫持部分),我们看到一些恶意软件通过修改调用表(IAT 劫持)以及一些修改 API 函数(内联劫持)来控制程序的执行路径,并将其重定向到恶意代码。其目标是阻止对 API 的调用,监控传递给 API 的输入参数,或过滤从 API 返回的输出参数。第八章中涉及的代码注入与劫持技术主要集中在用户空间的劫持技术。如果攻击者能够安装一个内核驱动程序,类似的功能也可以在内核空间实现。内核空间中的劫持是一种比用户空间更强大的方法,因为内核组件在整个系统的运作中扮演着非常重要的角色。这使得攻击者能够以高权限执行代码,从而具备隐藏恶意组件、绕过安全软件或拦截执行路径的能力。在本节中,我们将了解内核空间中的不同劫持技术,以及如何使用内存取证来检测这些技术。

8.1 检测 SSDT 劫持

内核空间中的系统服务描述符表SSDT)包含指向由内核执行文件(ntoskrnl.exentkrnlpa.exe等)导出的系统服务例程(内核函数)的指针。当应用程序调用像 WriteFile()ReadFile()CreateProcess() 等 API 时,它会调用 ntdll.dll 中的存根,从而将线程切换到内核模式。运行在内核模式中的线程会查询SSDT以确定要调用的内核函数的地址。下图通过 WriteFile() 示例展示了这一概念(对于其他 API,概念类似):

一般来说,ntoskrnl.exe导出核心内核 API 函数,如NtReadFile()NtWriteFile()等。在 x86 平台中,这些内核函数的指针直接存储在 SSDT 中,而在 x64 平台中,SSDT 不包含指针。相反,它存储一个编码的整数,通过解码该整数来确定内核函数的地址。无论实现方式如何,概念都是相同的,SSDT 被查询以确定特定内核函数的地址。以下是Windows7 x86平台上的WinDbg命令,用于显示 SSDT 的内容。表格中的条目包含指向ntoskrnl.exent)实现的函数的指针。条目的顺序和数量会因操作系统版本而异:

kd> dps nt!KiServiceTable
82a8f5fc 82c8f06a nt!NtAcceptConnectPort
82a8f600 82ad2739 nt!NtAccessCheck
82a8f604 82c1e065 nt!NtAccessCheckAndAuditAlarm
82a8f608 82a35a1c nt!NtAccessCheckByType
82a8f60c 82c9093d nt!NtAccessCheckByTypeAndAuditAlarm
82a8f610 82b0f7a4 nt!NtAccessCheckByTypeResultList
82a8f614 82d02611 nt!NtAccessCheckByTypeResultListAndAuditAlarm
[REMOVED]

还有第二个表格,类似于 SSDT,称为SSDT shadow。此表格存储由win32k.sys导出的与 GUI 相关的函数指针。要显示这两个表格的条目,可以使用ssdt volatility 插件,如下所示。SSDT[0]表示原生的SSDT 表格,而SSDT[1]表示SSDT shadow

$ python vol.py -f win7_x86.vmem --profile=Win7SP1x86 ssdt
Volatility Foundation Volatility Framework 2.6
[x86] Gathering all referenced SSDTs from KTHREADs...
Finding appropriate address space for tables...
SSDT[0] at 82a8f5fc with 401 entries
  Entry 0x0000: 0x82c8f06a (NtAcceptConnectPort) owned by ntoskrnl.exe
  Entry 0x0001: 0x82ad2739 (NtAccessCheck) owned by ntoskrnl.exe
  Entry 0x0002: 0x82c1e065 (NtAccessCheckAndAuditAlarm) owned by ntoskrnl.exe
  Entry 0x0003: 0x82a35a1c (NtAccessCheckByType) owned by ntoskrnl.exe
  [REMOVED]
SSDT[1] at 96c37000 with 825 entries
  Entry 0x1000: 0x96bc0e6d (NtGdiAbortDoc) owned by win32k.sys
  Entry 0x1001: 0x96bd9497 (NtGdiAbortPath) owned by win32k.sys
  Entry 0x1002: 0x96a272c1 (NtGdiAddFontResourceW) owned by win32k.sys
  Entry 0x1003: 0x96bcff67 (NtGdiAddRemoteFontToDC) owned by win32k.sys

在 SSDT hooking 的情况下,攻击者将特定函数的指针替换为恶意函数的地址。例如,如果攻击者希望拦截写入文件的数据,可以将NtWriteFile()的指针更改为指向攻击者选择的恶意函数的地址。如下图所示:

为了检测 SSDT hooking,可以查看 SSDT 表格中不指向ntoskrnl.exewin32k.sys地址的条目。以下代码是Mader rootkit 的示例,它 hook 了多个与注册表相关的函数,并将它们指向恶意驱动程序core.sys。在此阶段,您可以使用modulesmodscandriverscan确定core.sys的基址,然后使用moddump插件将其转储到磁盘以供进一步分析:

$ python vol.py -f mader.vmem --profile=WinXPSP3x86 ssdt | egrep -v "(ntoskrnl|win32k)"
Volatility Foundation Volatility Framework 2.6
[x86] Gathering all referenced SSDTs from KTHREADs...
Finding appropriate address space for tables...
SSDT[0] at 80501b8c with 284 entries
  Entry 0x0019: 0xf66eb74e (NtClose) owned by core.sys
 Entry 0x0029: 0xf66eb604 (NtCreateKey) owned by core.sys
 Entry 0x003f: 0xf66eb6a6 (NtDeleteKey) owned by core.sys
 Entry 0x0041: 0xf66eb6ce (NtDeleteValueKey) owned by core.sys
 Entry 0x0062: 0xf66eb748 (NtLoadKey) owned by core.sys
 Entry 0x0077: 0xf66eb4a7 (NtOpenKey) owned by core.sys
 Entry 0x00c1: 0xf66eb6f8 (NtReplaceKey) owned by core.sys
 Entry 0x00cc: 0xf66eb720 (NtRestoreKey) owned by core.sys
 Entry 0x00f7: 0xf66eb654 (NtSetValueKey) owned by core.sys

使用 SSDT hooking 对攻击者的缺点在于它很容易被检测到,而且 Windows 的 64 位版本由于内核补丁保护KPP)机制,也被称为PatchGuard,会阻止 SSDT hooking(en.wikipedia.org/wiki/Kernel_Patch_Protection)。由于 SSDT 中的条目在不同版本的 Windows 中有所不同,并且在新版本中可能会发生变化,因此恶意软件作者很难编写一个可靠的 rootkit。

8.2 检测 IDT Hooking

中断描述符表IDT)存储着被称为ISR(中断服务例程或中断处理程序)的函数地址。这些函数处理中断和处理器异常。就像挂钩 SSDT 一样,攻击者可能会挂钩 IDT 中的条目,以将控制权重定向到恶意代码。要显示 IDT 条目,可以使用 idt Volatility 插件。一个挂钩 IDT 的恶意软件示例是Uroburos (Turla) rootkit。该 rootkit 挂钩了位于 0xc3 (INT C3) 索引的中断处理程序。在干净的系统中,0xC3 处的中断处理程序指向的地址位于 ntoskrnl.exe 的内存中。以下输出显示了来自干净系统的条目:

$ python vol.py -f win7.vmem --profile=Win7SP1x86 idt
Volatility Foundation Volatility Framework 2.6
   CPU   Index   Selector   Value        Module      Section 
------   ------  ---------- ----------  ---------    ------------
     0    0         0x8     0x82890200  ntoskrnl.exe  .text 
     0    1         0x8     0x82890390  ntoskrnl.exe  .text 
     0    2         0x58    0x00000000  NOT USED 
     0    3         0x8     0x82890800  ntoskrnl.exe  .text 
     [REMOVED]
     0    C1        0x8     0x8282f3f4  hal.dll       _PAGELK 
     0    C2        0x8     0x8288eea4  ntoskrnl.exe  .text 
     0    C3        0x8     0x8288eeae  ntoskrnl.exe  .text

以下输出显示了挂钩条目。您可以看到 IDT 中的 0xC3 条目指向一个 UNKNOWN 模块中的地址。换句话说,挂钩条目位于 ntoskrnl.exe 模块的范围之外:

$ python vol.py -f turla1.vmem --profile=Win7SP1x86 idt
Volatility Foundation Volatility Framework 2.6
   CPU   Index   Selector   Value        Module      Section 
------   ------  ---------- ----------  ---------    ------------
     0    0         0x8     0x82890200  ntoskrnl.exe  .text 
     0    1         0x8     0x82890390  ntoskrnl.exe  .text 
     0    2         0x58    0x00000000  NOT USED 
     0    3         0x8     0x82890800  ntoskrnl.exe  .text 
     [REMOVED]
     0    C1        0x8     0x8282f3f4  hal.dll       _PAGELK 
     0    C2        0x8     0x8288eea4  ntoskrnl.exe  .text 
     0    C3        0x8     0x85b422b0  UNKNOWN 

要详细分析 Uroburos rootkit,并了解 rootkit 用于触发挂钩中断处理程序的技术,请参阅以下博客文章:www.gdatasoftware.com/blog/2014/06/23953-analysis-of-uroburos-using-windbg

8.3 识别内核空间内联钩取

攻击者可以通过修改内核函数或现有内核驱动程序中的函数,使用 jmp 指令将执行流重新路由到恶意代码,而不是替换 SSDT 中的指针,这样更容易被识别。如本章前面所述,您可以使用 apihooks 插件来检测内核空间中的内联钩取。通过指定 -P 参数,您可以告诉 apihooks 插件仅扫描内核空间中的钩取。在以下 TDL3 rootkit 的示例中,apihooks 检测到了内核函数 IofCallDriverIofCompleteRequest 中的钩取。被挂钩的 API 函数被重定向到 0xb878dfb20xb878e6bb 这些地址,位于一个名称未知的恶意模块中(可能是因为它通过解除链接 KLDR_DATA_TABLE_ENTRY 结构来隐藏自己):

$ python vol.py -f tdl3.vmem --profile=WinXPSP3x86 apihooks -P
Volatility Foundation Volatility Framework 2.6
************************************************************************
Hook mode: Kernelmode
Hook type: Inline/Trampoline
Victim module: ntoskrnl.exe (0x804d7000 - 0x806cf580)
Function: ntoskrnl.exe!IofCallDriver at 0x804ee120
Hook address: 0xb878dfb2
Hooking module: <unknown>

Disassembly(0):
0x804ee120 ff2500c25480 JMP DWORD [0x8054c200]
0x804ee126 cc           INT 3
0x804ee127 cc           INT 3
[REMOVED]

************************************************************************
Hook mode: Kernelmode
Hook type: Inline/Trampoline
Victim module: ntoskrnl.exe (0x804d7000 - 0x806cf580)
Function: ntoskrnl.exe!IofCompleteRequest at 0x804ee1b0
Hook address: 0xb878e6bb
Hooking module: <unknown>

Disassembly(0):
0x804ee1b0 ff2504c25480 JMP DWORD [0x8054c204]
0x804ee1b6 cc           INT 3
0x804ee1b7 cc           INT 3
[REMOVED]

即使钩子模块的名称未知,仍然可以检测到恶意的内核模块。在这种情况下,我们知道 API 函数被重定向到恶意模块中以0xb87开头的地址,这意味着恶意模块一定驻留在以0xb87开头的某个地址处。运行modules插件并未检测到该地址范围内的任何模块(因为它被隐藏),而modscan插件则检测到一个名为TDSSserv.sys的内核模块,该模块加载在基地址0xb878c000,大小为0x11000。换句话说,内核模块TDSSserv.sys的起始地址是0xb878c000,结束地址是0xb879d000(0xb878c000+0x11000)。你可以清楚地看到钩子地址0xb878dfb20xb878e6bb落在TDSSserv.sys的地址范围内。此时,我们已经成功识别出恶意驱动程序。你现在可以将驱动程序转储到磁盘以便进一步分析:

$ python vol.py -f tdl3.vmem --profile=WinXPSP3x86 modules | grep -i 0xb878
Volatility Foundation Volatility Framework 2.6

$ python vol.py -f tdl3.vmem --profile=WinXPSP3x86 modscan | grep -i 0xb878
Volatility Foundation Volatility Framework 2.6
0x0000000009773c98 TDSSserv.sys 0xb878c000 0x11000 \systemroot\system32\drivers\TDSSserv.sys

8.4 检测 IRP 函数钩子

rootkit 可以通过修改主功能表(调度例程数组)中的条目,而不是钩住内核 API 函数,将它们指向恶意模块中的某个例程。例如,rootkit 可以通过覆盖驱动程序主功能表中与IRP_MJ_WRITE相关的地址,来检查写入磁盘或网络的数据缓冲区。以下图示说明了这一概念:

通常,驱动程序的 IRP 处理程序函数会指向它们自己模块中的某个位置。例如,null.sysIRP_MJ_WRITE相关例程指向null.sys中的一个地址,但有时驱动程序会将处理程序函数转发到另一个驱动程序。以下是磁盘驱动程序将处理程序函数转发到CLASSPNP.SYS(存储类设备驱动程序)的示例:

$ python vol.py -f win7_clean.vmem --profile=Win7SP1x64 driverirp -r disk
Volatility Foundation Volatility Framework 2.6
--------------------------------------------------
DriverName: Disk
DriverStart: 0xfffff88001962000
DriverSize: 0x16000
DriverStartIo: 0x0
   0 IRP_MJ_CREATE                0xfffff88001979700 CLASSPNP.SYS
   1 IRP_MJ_CREATE_NAMED_PIPE     0xfffff8000286d65c ntoskrnl.exe
   2 IRP_MJ_CLOSE                 0xfffff88001979700 CLASSPNP.SYS
   3 IRP_MJ_READ                  0xfffff88001979700 CLASSPNP.SYS
   4 IRP_MJ_WRITE                 0xfffff88001979700 CLASSPNP.SYS
   5 IRP_MJ_QUERY_INFORMATION     0xfffff8000286d65c ntoskrnl.exe
   [REMOVED] 

要检测 IRP 钩子,可以关注指向另一个驱动程序的 IRP 处理程序函数,并且由于驱动程序可以将 IRP 处理程序转发到另一个驱动程序,你需要进一步调查以确认钩子。如果你在实验室环境中分析 rootkit,则可以从干净的内存映像中列出所有驱动程序的 IRP 函数,并将其与受感染的内存映像中的 IRP 函数进行比较,以查找任何修改。在以下示例中,ZeroAccess rootkit钩住了磁盘驱动程序的 IRP 函数,并将它们重定向到一个恶意模块中的函数,而该模块的地址未知(因为该模块被隐藏):

DriverName: Disk
DriverStart: 0xba8f8000
DriverSize: 0x8e00
DriverStartIo: 0x0
   0 IRP_MJ_CREATE                0xbabe2bde Unknown
 1 IRP_MJ_CREATE_NAMED_PIPE     0xbabe2bde Unknown
 2 IRP_MJ_CLOSE                 0xbabe2bde Unknown
 3 IRP_MJ_READ                  0xbabe2bde Unknown
 4 IRP_MJ_WRITE                 0xbabe2bde Unknown
 5 IRP_MJ_QUERY_INFORMATION     0xbabe2bde Unknown
   [REMOVED]

以下是modscan的输出,显示与ZeroAccess相关的恶意驱动程序(具有可疑名称)及其在内存中加载的基地址(该地址可用于将驱动程序转储到磁盘):

$ python vol.py -f zaccess_maxplus.vmem --profile=WinXPSP3x86 modscan | grep -i 0xbabe
Volatility Foundation Volatility Framework 2.6
0x0000000009aabf18 * 0xbabe0000 0x8000 \*

一些 rootkit 使用间接 IRP 钩子来避免引起怀疑。在以下示例中,Gapz Bootkit 钩住了 null.sysIRP_MJ_DEVICE_CONTROL。乍一看,一切似乎正常,因为对应于 IRP_MJ_DEVICE_CONTROL 的 IRP 处理程序地址指向 null.sys 内部。然而,仔细观察会发现差异;在一个干净的系统上,IRP_MJ_DEVICE_CONTROL 会指向 ntoskrnl.exe 中的地址(nt!IopInvalidDeviceRequest)。在这种情况下,它指向了 null.sys 中的 0x880ee040。通过反汇编地址 0x880ee040(使用 volshell 插件),您可以看到它跳转到 0x8518cad9,这个地址位于 null.sys 的范围之外:

$ python vol.py -f gapz.vmem --profile=Win7SP1x86 driverirp -r null
Volatility Foundation Volatility Framework 2.6
--------------------------------------------------
DriverName: Null
DriverStart: 0x880eb000
DriverSize: 0x7000
DriverStartIo: 0x0
   0 IRP_MJ_CREATE                   0x880ee07c Null.SYS
   1 IRP_MJ_CREATE_NAMED_PIPE        0x828ee437 ntoskrnl.exe
   2 IRP_MJ_CLOSE                    0x880ee07c Null.SYS
   3 IRP_MJ_READ                     0x880ee07c Null.SYS
   4 IRP_MJ_WRITE                    0x880ee07c Null.SYS
   5 IRP_MJ_QUERY_INFORMATION        0x880ee07c Null.SYS
   [REMOVED]
  13 IRP_MJ_FILE_SYSTEM_CONTROL      0x828ee437 ntoskrnl.exe
  14 IRP_MJ_DEVICE_CONTROL           0x880ee040 Null.SYS
  15 IRP_MJ_INTERNAL_DEVICE_CONTROL  0x828ee437 ntoskrnl.exe

$ python vol.py -f gapz.vmem --profile=Win7SP1x86 volshell
[REMOVED]
>>> dis(0x880ee040)
0x880ee040 8bff        MOV EDI, EDI
0x880ee042 e992ea09fd  JMP 0x8518cad9
0x880ee047 6818e10e88  PUSH DWORD 0x880ee118

有关 Gapz Bootkit 使用的隐身技术的详细信息,请阅读 Eugene Rodionov 和 Aleksandr Matrosov 所写的白皮书(www.welivesecurity.com/wp-content/uploads/2013/04/gapz-bootkit-whitepaper.pdf),标题为“留意 Gapz:有史以来最复杂的 Bootkit 分析”。

如前所述,检测标准的钩子技术相对简单。例如,您可以查看一些迹象,如 SSDT 条目未指向ntoskrnl.exe/win32k.sys,或者 IRP 函数指向其他地方,或者函数开始处存在跳转指令。为了避免这种检测,攻击者可以在保持调用表条目在范围内的同时实现钩子,或者将跳转指令放置在代码深处。为此,他们需要依赖于修补系统模块或第三方驱动程序。修补系统模块的问题在于,Windows 内核补丁保护(PatchGuard) 会阻止修补调用表(如 SSDT 或 IDT)以及 64 位系统上的核心系统模块。由于这些原因,攻击者要么使用依赖绕过这些保护机制的技术(如安装Bootkit/利用内核模式漏洞),要么使用受支持的方式(这些方式同样适用于 64 位系统)执行恶意代码,以便与其他合法驱动程序融合并减少被检测的风险。在接下来的章节中,我们将探讨 rootkit 使用的一些受支持的技术。

9. 内核回调和定时器

Windows 操作系统允许驱动程序注册回调例程,当特定事件发生时,该例程会被调用。例如,如果一个 rootkit 驱动程序希望监控系统上所有进程的执行和终止,它可以通过调用内核函数PsSetCreateProcessNotifyRoutinePsSetCreateProcessNotifyRoutineExPsSetCreateProcessNotifyRoutineEx2来为进程事件注册回调例程。当进程事件发生(启动或退出)时,rootkit 的回调例程将被调用,从而采取必要的措施,例如防止进程启动。以同样的方式,rootkit 驱动程序可以注册回调例程,在映像(EXE 或 DLL)被加载到内存中、文件和注册表操作执行时,或系统即将关闭时接收通知。换句话说,回调功能使 rootkit 驱动程序能够监控系统活动,并根据活动采取必要的行动。你可以通过以下链接获取一些 rootkit 可能用来注册回调例程的已记录和未记录的内核函数列表:www.codemachine.com/article_kernel_callback_functions.html。这些内核函数在不同的头文件(ntddk.hWdm.h 等)中定义,位于Windows 驱动程序工具包 (WDK) 中。获取已记录内核函数详细信息的最快方法是进行快速的 Google 搜索,这将引导你到 WDK 在线文档中的相应链接。

回调的工作方式是,特定的驱动程序创建一个回调对象,该对象是一个包含函数指针列表的结构。创建的回调对象会被发布,以便其他驱动程序可以使用。其他驱动程序随后可以将它们的回调例程注册到创建回调对象的驱动程序中 (docs.microsoft.com/en-us/windows-hardware/drivers/kernel/callback-objects)。创建回调的驱动程序可以与注册回调的内核驱动程序相同,也可以不同。要查看系统范围的回调例程,可以使用callbacks Volatility 插件。在一个干净的 Windows 系统中,通常可以看到由各种驱动程序安装的许多回调,这意味着并非所有callbacks输出中的条目都是恶意的;需要进一步分析以从可疑的内存镜像中识别恶意驱动程序。

在以下示例中,Mader rootkit 执行了SSDT hooking(在本章的检测 SSDT hooking 部分讨论),并安装了一个进程创建回调例程,以监控系统上所有进程的执行或终止。特别地,当进程事件发生时,位于恶意模块 core.sys 中地址 0xf66eb050 的回调例程将被调用。Module 列指定实现回调函数的内核模块的名称。Details 列提供安装回调的内核对象的名称或描述。在识别出恶意驱动程序后,您可以进一步调查它,或将其转储到磁盘以进行进一步分析(反汇编、AV 扫描、字符串提取等),如下所示的 moddump 命令:

$ python vol.py -f mader.vmem --profile=WinXPSP3x86 callbacks
Volatility Foundation Volatility Framework 2.6
Type                             Callback    Module        Details
---------------------------      ----------  ----------   -------
IoRegisterShutdownNotification  0xf9630c6a  VIDEOPRT.SYS  \Driver\VgaSave
IoRegisterShutdownNotification  0xf9630c6a  VIDEOPRT.SYS  \Driver\vmx_svga
IoRegisterShutdownNotification  0xf9630c6a  VIDEOPRT.SYS  \Driver\mnmdd
IoRegisterShutdownNotification  0x805f5d66  ntoskrnl.exe  \Driver\WMIxWDM
IoRegisterFsRegistrationChange  0xf97c0876  sr.sys         -
GenericKernelCallback           0xf66eb050  core.sys       -
PsSetCreateProcessNotifyRoutine 0xf66eb050  core.sys       -
KeBugCheckCallbackListHead      0xf96e85ef  NDIS.sys      Ndis miniport
[REMOVED]
$ python vol.py -f mader.vmem --profile=WinXPSP3x86 modules | grep -i core Volatility Foundation Volatility Framework 2.6
0x81772bf8  core.sys  0xf66e9000  0x12000   \system32\drivers\core.sys
$ python vol.py -f mader.vmem --profile=WinXPSP3x86 moddump -b 0xf66e9000 -D dump/
Volatility Foundation Volatility Framework 2.6
Module Base    Module Name      Result
-----------   ----------------- ------
0x0f66e9000    core.sys         OK: driver.f66e9000.sys

在以下示例中,TDL3 rootkit 安装了进程回调和映像加载回调通知。这使得 rootkit 能够监控进程事件,并在可执行映像(EXE、DLL 或内核模块)映射到内存时收到通知。条目中的模块名称设置为 UNKNOWN;这告诉您回调例程存在于一个未知模块中,这种情况发生在 rootkit 驱动程序通过取消链接 KLDR_DATA_TABLE_ENTRY 结构或运行一个孤立线程(隐藏或与内核模块分离的线程)来尝试隐藏自己时。在这种情况下,UNKNOWN 条目使您更容易发现可疑条目:

$ python vol.py -f tdl3.vmem --profile=WinXPSP3x86 callbacks
Volatility Foundation Volatility Framework 2.6
Type                            Callback    Module        Details
------------------------        ----------  --------      -------
[REMOVED]
IoRegisterShutdownNotification  0x805cdef4  ntoskrnl.exe  \FileSystem\RAW
IoRegisterShutdownNotification  0xba8b873a  MountMgr.sys  \Driver\MountMgr
GenericKernelCallback           0xb878f108  UNKNOWN        -
IoRegisterFsRegistrationChange  0xba6e34b8  fltMgr.sys     -
GenericKernelCallback           0xb878e8e9  UNKNOWN        -
PsSetLoadImageNotifyRoutine     0xb878f108  UNKNOWN        -
PsSetCreateProcessNotifyRoutine 0xb878e8e9  UNKNOWN        -
KeBugCheckCallbackListHead      0xba5f45ef  NDIS.sys      Ndis miniport
[REMOVED]

即使模块名称为 UNKNOWN,通过回调例程地址,我们也可以推断出恶意模块应该位于从地址 0xb878 开始的内存区域中。从 modules 插件的输出中,您可以看到该模块已经取消链接,但 modscan 插件仍然能够检测到加载在 0xb878c000 并大小为 0x11000 的内核模块。显然,所有回调例程地址都位于该模块的范围内。现在,已知内核模块的基地址,您可以使用 moddump 插件将其转储以进行进一步分析:

$ python vol.py -f tdl3.vmem --profile=WinXPSP3x86 modules | grep -i 0xb878
Volatility Foundation Volatility Framework 2.6

$ python vol.py -f tdl3.vmem --profile=WinXPSP3x86 modscan | grep -i 0xb878
Volatility Foundation Volatility Framework 2.6
0x9773c98 TDSSserv.sys 0xb878c000 0x11000 \system32\drivers\TDSSserv.sys

像回调函数一样,rootkit 驱动程序可能会创建一个定时器,并在指定时间到期时获得通知。rootkit 驱动程序可以利用此功能来调度定期执行的操作。其工作原理是,rootkit 创建一个定时器并提供一个称为*DPC(延迟过程调用)*的回调例程,当定时器过期时,该回调例程会被调用。当回调例程被调用时,rootkit 可以执行恶意操作。换句话说,定时器是 rootkit 执行其恶意代码的另一种方式。有关内核定时器如何工作的详细信息,请参阅以下 Microsoft 文档:docs.microsoft.com/en-us/windows-hardware/drivers/kernel/timer-objects-and-dpcs

要列出内核定时器,你可以使用timers Volatility 插件。需要注意的是,定时器本身并不具有恶意性质,它只是 Windows 的一项功能,因此在干净的系统上,你会看到一些合法的驱动程序安装了定时器。像回调一样,可能需要进一步分析才能识别恶意模块。由于大多数根工具尝试隐藏它们的驱动程序,结果就会生成一些明显的痕迹,帮助你快速识别恶意模块。在以下示例中,ZeroAccess 根工具安装了一个 6000 毫秒的定时器。当时间到期时,UNKNOWN模块中的0x814f9db0地址的例程被调用。Module列中的UNKNOWN表明该模块可能被隐藏,但例程地址指向了恶意代码所在的内存范围:

$ python vol.py -f zaccess1.vmem --profile=WinXPSP3x86 timers
Volatility Foundation Volatility Framework 2.6
Offset(V)  DueTime                Period(ms) Signaled Routine   Module
---------- ---------------------  --------- -------- -------- ------
0x805516d0 0x00000000:0x6b6d9546  60000      Yes    0x804f3eae ntoskrnl.exe
0x818751f8 0x80000000:0x557ed358  0          -      0x80534e48 ntoskrnl.exe
0x81894948 0x00000000:0x64b695cc  10000      -      0xf9cbc6c4 watchdog.sys
0xf6819990 0x00000000:0x78134eb2  60000      Yes    0xf68021f8 HTTP.sys
[REMOVED]
0xf7228d60 0x00000000:0x714477b4  60000      Yes    0xf7220266 ipnat.sys
0x814ff790 0x00000000:0xc4b6c5b4  60000      -      0x814f9db0 UNKNOWN
0x81460728 0x00000000:0x760df068  0          -      0x80534e48 ntoskrnl.exe
[REMOVED]

除了定时器,ZeroAccess还安装了回调以监控注册表操作。同样,回调例程的地址指向相同的内存范围(以0x814f开头):

$ python vol.py -f zaccess1.vmem --profile=WinXPSP3x86 callbacks
Volatility Foundation Volatility Framework 2.6
Type                           Callback    Module         Details
------------------------------ ----------  -----------    -------
IoRegisterShutdownNotification 0xf983e2be  ftdisk.sys    \Driver\Ftdisk
IoRegisterShutdownNotification 0x805cdef4  ntoskrnl.exe  \FileSystem\RAW
IoRegisterShutdownNotification 0x805f5d66  ntoskrnl.exe  \Driver\WMIxWDM
GenericKernelCallback          0x814f2d60  UNKNOWN       -
KeBugCheckCallbackListHead     0xf96e85ef  NDIS.sys      Ndis miniport
CmRegisterCallback             0x814f2d60  UNKNOWN       -

尝试使用modulesmodscandriverscan插件查找UNKNOWN模块并没有返回任何结果:

$ python vol.py -f zaccess1.vmem --profile=WinXPSP3x86 modules | grep -i 0x814f

$ python vol.py -f zaccess1.vmem --profile=WinXPSP3x86 modscan | grep -i 0x814f

$ python vol.py -f zaccess1.vmem --profile=WinXPSP3x86 driverscan | grep -i 0x814f

检查driverscan列出的内容时,发现了可疑的条目,其中基址和大小被清零(这不正常,可能是一种绕过技巧)。基址清零解释了为什么modulesmodscandriverscan没有返回任何结果。输出还显示,恶意驱动程序的名称仅由数字组成,这进一步引发了怀疑:

$ python vol.py -f zaccess1.vmem --profile=WinXPSP3x86 driverscan
Volatility Foundation Volatility Framework 2.6
0x00001abf978  1  0  0x00000000  0x0  \Driver\00009602  \Driver\00009602
0x00001b017e0  1  0  0x00000000  0x0  \Driver\00009602  \Driver\00009602

通过将基址清零,根工具使得取证分析师很难确定内核模块的起始地址,这也阻止了我们导出恶意模块。我们仍然知道恶意代码的驻留位置(地址以0x814f开头)。一个引人注目的问题是,我们如何利用这些信息来确定基址?一种方法是取一个地址,减去一定字节数(比如倒着减),直到找到MZ签名,但这种方法的问题在于很难确定需要减去多少字节。最快的方法是使用yarascan插件,因为这个插件允许你在内存中扫描模式(字符串、十六进制字节或正则表达式)。由于我们正在寻找驻留在内核内存中并以0x814f开头的模块,可以使用带有-K选项的yarascan(该选项只扫描内核内存)来查找MZ签名。通过输出结果,你可以看到在地址0x814f1b80处有一个可执行文件。你可以将这个地址指定为基址,通过moddump插件将恶意模块导出到磁盘。导出的模块大小约为 53.2 KB,对应的十六进制为0xd000字节。换句话说,模块从地址0x814f1b80开始,到0x814feb80结束。所有的回调地址都在这个模块的地址范围内:

$ python vol.py -f zaccess1.vmem --profile=WinXPSP3x86 yarascan -K -Y "MZ" | grep -i 0x814f
Volatility Foundation Volatility Framework 2.6
0x814f1b80 4d 5a 90 00 03 00 00 00 04 00 00 00 ff ff 00 00 MZ..............
0x814f1b90 b8 00 00 00 00 00 00 00 40 00 00 00 00 00 00 00 ........@.......
0x814f1ba0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0x814f1bb0 00 00 00 00 00 00 00 00 00 00 00 00 d0 00 00 00 ................
0x814f1bc0 0e 1f ba 0e 00 b4 09 cd 21 b8 01 4c cd 21 54 68 ........!..L.!Th
0x814f1bd0 69 73 20 70 72 6f 67 72 61 6d 20 63 61 6e 6e 6f is.program.canno
0x814f1be0 74 20 62 65 20 72 75 6e 20 69 6e 20 44 4f 53 20 t.be.run.in.DOS.
0x814f1bf0 6d 6f 64 65 2e 0d 0d 0a 24 00 00 00 00 00 00 00 mode....$.......
$ python vol.py -f zaccess1.vmem --profile=WinXPSP3x86 moddump -b 0x814f1b80 -D dump/
Module Base  Module Name          Result
-----------  -------------------- ------
0x0814f1b80  UNKNOWN              OK: driver.814f1b80.sys

$ ls -al
[REMOVED]
-rw-r--r-- 1 ubuntu ubuntu 53248 Jun 9 15:25 driver.814f1b80.sys

为了确认该转储模块是否为恶意模块,它被提交到VirusTotal。来自 AV 厂商的结果确认它是ZeroAccess Rootkit(也被称为Sirefef):

概要

恶意软件作者使用各种先进技术来安装其内核驱动程序,并绕过 Windows 安全机制。一旦内核驱动程序安装完成,它可以修改系统组件或第三方驱动程序,从而绕过、偏转并转移你的取证分析。在本章中,你了解了一些最常见的 rootkit 技术,并且我们看到了如何通过内存取证来检测这些技术。内存取证是一项强大的技术,将其作为恶意软件分析的一部分,能极大地帮助你理解对手的战术。恶意软件作者经常想出新的方法来隐藏其恶意组件,因此,仅仅知道如何使用工具是不够的;理解其背后的概念也变得至关重要,以识别攻击者绕过取证工具的努力。