恶意样本分析-12-使用内存取证检测高级恶意软件

11. 使用内存取证检测高级恶意软件

在前一章中,我们研究了不同的Volatility插件,它们有助于从内存映像中提取有价值的信息。在本章中,我们将继续我们的内存取证之旅,我们将看到更多的插件,这些插件将帮助你从被高级恶意软件感染的内存映像中提取取证痕迹,这些恶意软件使用了隐身和隐藏技术。在下一节中,我们将重点介绍使用内存取证来检测代码注入技术。下一节将讨论在第8章“代码注入和挂钩”中已经涉及到的一些概念,所以强烈建议在阅读下一节之前阅读这一章。

1. 检测代码注入

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

1.1 通过采用信息

要从内存映像中获取VAD信息,可以使用vadinfo Volatility插件。下面以vadinfo为例,使用进程ID (pid 2180)显示explorer.exe进程的内存区域。在下面的输出中,内核内存中地址为0x8724d718的第一个VAD节点描述了进程内存中的内存范围0x00db0000-0x0102ffff及其内存保护PAGE_EXECUTE_WRITECOPY。由于第一个节点描述的是一个包含内存映射的可执行映像(explorer.exe)的内存范围,因此它还提供了磁盘上的完整路径。第二个节点0x8723fb50描述了0x004b0000-0x004effff的内存范围,它不包含任何内存映射文件。类似地,地址0x8723fb78的第三个节点显示进程内存范围的信息0x77690,000-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)或shell代码,那么内存 需要分配一个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....$.......

有时,显示内存区域的内容可能不足以识别恶意代码。当shell代码被注入时尤其如此,在这种情况下,您需要反汇编内容。例如,如果您使用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.....

如果您怀疑内存区域包含shell代码,可以使用dis命令 在volshell中,在给定地址反汇编代码。从下面代码中显示的反汇编输出中,您可以看出shell代码已经注入到这个内存区域,因为它包含有效的CPU指令。为了验证内存区域是否包含任何恶意代码,您需要进一步分析它,以确定上下文。这是因为注入的代码看起来也类似于合法代码:

1.3 转储进程内存区域

在确定进程内存中注入的代码(PE文件或shellcode)之后,您可能希望将其转储到磁盘以进行进一步分析(用于提取字符串、执行YARA扫描或进行反汇编)。要转储由VAD节点描述的内存区域,可以使用vaddump插件。例如,如果希望转储地址为0x03110000的包含shell代码的内存区域,可以提供-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头。在这种情况下,如果你正在查看十六进制转储,它不会给你任何PE文件存在的指示;可能需要一定程度的手工分析来验证代码。在一篇题为“用Volatility恢复CoreFlood 二进制文件”(http://mnin.blogspot/2008/11/recovering-coreflood-binaries-with.html)的博客文章中提到了这样一个恶意软件样本的例子。

1.4 使用malfind检测注入的代码

到目前为止,我们已经了解了如何使用vadinfo手动识别可疑的内存区域。您还了解了如何使用vaddump转储一个内存区域。还有另一个名为malfind的Volatility插件,它根据内存内容和前面介绍的VAD特征自动识别可疑内存区域。在下面的示例中,当针对感染了SpyEye的内存映像运行malfind时,它会自动识别可疑的内存区域(包含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
0x03120001 5a
0x03120002 90
0x03120003 0003  ADD [EBX], AL
0x03120005 0000  ADD [EAX], AL
Process: explorer.exe Pid: 1608 Address: 0x3110000 Vad Tag: VadS Protection: PAGE_EXECUTE_READWRITE
DEC EBP
POP EDX
NOP
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
0x03110007 55
0x03110008 8bec
0x0311000a 83ec54
0x0311000d 8365fc00
0x03110011 64a130000000  MOV EAX, [FS:0x30]

2. 调查伪进程注入(Hollow Process Injection)

在前面介绍的代码注入技术中,恶意代码被注入到合法进程的进程地址空间中。伪进程注入(或进程空化)也是一种代码注入技术,但不同的是,在这种技术中,内存中合法进程的进程可执行文件被替换为恶意可执行文件。在讨论伪进程的检测之前,让我们先了解一下它是如何工作的。关于中空过程注入的详细信息在第8章代码注入和挂钩(章节)中介绍过。您还可以查看作者关于伪进程注入的演示和视频演示(https://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在挂起模式下创建合法的lasss.exe进程。因此,lasss.exe通过PAGE_EXECUTE_WRITECOPY(WCX)保护加载到内存中。此时(在空化之前),PEB和VAD都包含关于lasss.exe的内存保护、基址和完整路径的相同元数据信息。然后,Stuxnet将合法的进程可执行文件(lasss.exe)挖空,并在之前加载lasss.exe的同一区域内,用PAGE_EXECUTE_READWRITE (RWX)保护分配一个新的内存,然后在分配的内存中注入恶意的可执行文件并恢复挂起的线程。由于掏空了进程可执行文件,导致VAD和PEB之间的进程路径信息存在差异,即PEB中的进程路径仍然包含lasss.exe的完整路径,而VAD不显示完整路径。此外,在空化之前(WCX)和空化之后(RWX)存在内存保护差异。下面的图表可以帮助你可视化空心化之前发生了什么,以及空心化过程后在PEB和VAD中产生的差异:

使用内存取证技术对震网病毒进行了完整的分析,Michael Hale Ligh在下面的博客文章中写道:http://mnin.blogspot.in/2011/06/examining-stuxnets-footprint-in-memory.html。

2.2 识别伪进程注入

为了检测中伪进程注入,您可以寻找PEB和VAD之间产生的差异,以及内存保护差异。您还可以查找父子流程关系中的差异。在下面的Stuxnet示例中,您可以看到系统上运行着两个lasss.exe进程。第一个lasss.exe进程(pid 708)有一个父进程winlogon.exe (pid 652),而第二个lasss.exe进程(pid 1732)有一个终止的父进程(pid 1736)。根据进程信息,你可以判断pid为1732的lasss.exe是可疑的进程,因为在一个干净的系统上,winlogon.exe将是lasss.exe在pre-Vista机器上的父进程,wininit.exe将是lasss.exe在Vista和以后的系统上的父进程:

$ 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

如前所述,您可以通过比较PEB和VAD结构来检测伪进程。dlllist插件从PEB获取模块信息,显示lasss.exe (pid 1732)的完整路径和它加载的base地址(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,它不会向lasss.exe显示完整的路径名称。由于恶意软件解除了lasss.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插件在可执行lasss.exe加载的同一地址(0x01000000)识别了可疑的内存保护:

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的恶意软件,它以一种略微不同的方式执行伪进程注入。这是在第8章代码注入和挂钩(第3.6节中空过程注入)中介绍的同一个示例。以下是Skeeyah所执行的步骤:

  • 它以挂起模式启动svchost.exe进程。作为一个 结果,svchost.exe被加载到内存中(在本例中,地址为0x1000000)。
  • 它通过读取PEB来确定svchost.exe的基址。ImageBaseAddress,然后释放svchost.exe的可执行部分。
  • 它不是在之前加载svchost.exe的同一区域分配内存(0x1000000),而是在不同的地址0x00400000,具有读、写和执行权限。
  • 然后覆盖PEB。svchost.exe进程的imagebaseaddress,带有新分配的地址0x00400000。这会将PEB中的svchost.exe的基址从0x1000000更改为0x00400000(其中包含注入的可执行文件)。
  • 然后,它将挂起的线程的起始地址更改为注入的可执行文件的入口点的地址,并恢复线程。

下面的截图显示了空化前后的差异。具体来说,空化后的PEB认为svchost.exe在0x00400000处加载。之前表示svchost.exe(加载在0x1000000)的VAD节点不再存在,因为当恶意软件掏空svchost.exe进程可执行文件时,VAD树中删除了该节点的条目:

要检测伪进程的这种变化,可以遵循相同的方法。根据伪进程的执行方式,结果会有所不同。进程列表显示了svchost.exe进程的多个实例,这是正常的。除了最后一个svchost.exe (pid 1824)之外,所有svchost.exe进程都有一个父进程services.exe (pid 696)。在一个干净的系统中,所有svchos.exe进程都是由services.exe启动的。当您查看svchost.exe (pid 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

dlllist插件(依赖于PEB)显示svchost.exe (pid 1824)的完整路径,并报告base地址为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
0x7c900000 0xaf000  0xffff
0x7c800000 0xf6000  0xffff
[REMOVED]
C:\WINDOWS\system32\svchost.exe
C:\WINDOWS\system32\ntdll.dll
C:\WINDOWS\system32\kernel32.dll

另一方面,ldrmodules插件(依赖于内核中的VAD)并没有显示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性插件检测它们的详细信息,请观看作者的黑帽演讲:“恶意软件作者不想让你知道的东西-规避伪进程注入”(https://youtu.be/9L9I1T5QDg4)。或者,您可以阅读作者的博客文章在以下链接:https://cysinfo.com/detecting-deception-hollow-techniques/

3. 检测API钩子

在将恶意代码注入目标进程后,恶意软件可以在目标进程发出的API调用时钩住,以控制其执行路径,并将其重新定位到恶意代码。关于钩子技术的细节已经在第8章,代码注入和钩子(钩子技术一节)中介绍过了。在本节中,我们将主要关注使用内存取证来检测这种挂钩技术。要识别进程和内核内存中的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 httpendrequestA(在wininet.dll中)中检测钩子。然后,被钩住的API被重定向到地址0x2c7ec48(钩子地址)。钩子地址在注入的可执行文件(钩子模块)的地址范围内。钩子模块的名称是未知的,因为它通常不是从磁盘加载的(而是注入的)。具体来说,在API函数httpendrequesta的起始地址(0x753600fc),有一个跳转指令,它将httpendrequesta的执行流重定向到注入的可执行文件中的地址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
0x75360101 83ec38
0x75360104 56
0x75360105 6a38
0x75360107 8d45c8
JMP 0x2c7ec48
SUB ESP, 0x38
PUSH ESI
PUSH 0x38
LEA EAX, [EBP-0x38]

4. 内核模式rootkit

恶意程序(如rootkit)可以加载内核驱动程序以内核模式运行代码。一旦它在内核空间中运行,它就可以访问内部操作系统代码,并可以监视系统事件,通过修改内部数据结构、钩子函数和修改调用表来逃避检测。内核模式驱动程序的扩展名通常是.sys,它驻留在%windir%\system32\drivers中。一个内核驱动通常是通过创建一个内核驱动服务类型的服务来加载的(如第7章,恶意软件的功能和持久性,在服务部分)。

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

在一个干净的windows系统上,您会发现数百个内核模块,因此找到恶意内核模块需要一些工作。在下面的部分中,我们将研究一些用于定位和提取恶意内核模块的常用技术。我们将从列出内核模块开始。

5. 清单内核模块

要列出内核模块,可以使用modules插件。这个插件依赖于行走双向链表的元数据结构(KLDR_DATA_TABLE_ENTRY)指出PsLoadedModuleList(这种技术类似于行走_EPROCESS双向链表的结构,如第十章所述,狩猎恶意软件使用内存取证,在理解ActiveProcessLinks部分)。清单内核模块可能并不总是帮助你识别恶意内核驱动程序的数以百计的加载内核模块,但是它可以用于发现一个可疑的指标如一个内核驱动程序有一个奇怪的名字,或从非标准内核模块加载路径或临时路径。内核模块的模块插件列表的顺序加载,这意味着如果一个rootkit司机最近安装了,你很可能会发现模块的列表,提供模块不是隐藏和系统内存映像收购之前没有重启。

在下面的例子中,一个被Laqma rootkit感染的内存映像中,模块列表显示了Laqma的恶意驱动程序lanmandrv.sys,位于列表的最后,从C:\Windows\System32目录运行,而大多数其他内核驱动程序是从SystemRoot\System32\drivers加载的。从清单中,您还可以看到,等核心操作系统组件NT内核模块(ntkrnlpa.exe或ntoskrnl.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
[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
\SystemRoot\system32\kdcom.dll

由于浏览双链接列表容易受到DKOM攻击(在第10章,使用内存取证查找恶意软件,4.2.1节直接内核对象操作(DKOM)中描述),通过解除链接可以从列表中隐藏内核驱动程序。为了克服这个问题,您可以使用另一个名为modscan的插件。modscan插件依赖于池标签扫描方法(在第10章,使用内存取证来捕获恶意软件,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
   \SystemRoot\System32\Drivers\Beep.SYS
   0x000000001061bad0 secdrv.SYS
   \SystemRoot\System32\Drivers\secdrv.SYS
   0x00000000108b9120 rdprefmp.sys
   \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

当您运行模块插件针对感染Necurs rootkit的内存映像时,它不会显示恶意驱动程序(2683608180e436a1.sys):

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

由于modscan使用池标记扫描方法,它可以检测未加载的模块(假设内存没有被覆盖),因此恶意驱动程序2683608180e436a1是有可能的。Sys被迅速加载和卸载,或者被隐藏起来。要确认驱动程序是被卸载还是被隐藏,你可以使用unloaddmodules插件,它会显示被卸载的模块列表以及每个模块被卸载的时间。在下面的输出中,无恶意驱动,2683608180e436a1.Sys,告诉你这个驱动程序没有被卸载,它是隐藏的。从下面的输出中,您可以看到另一个恶意驱动程序2b9fb.Sys,以前是快速加载和卸载的(没有出现在modules和modscan清单中,如下面的输出所示)。unloaddmodules插件可以在检测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插件从名为DRIVER_OBJECT的结构中获取与内核模块相关的信息。具体来说,driverscan插件使用池标记扫描来查找物理地址空间中的驱动程序对象。第一列Offset(P)指定了发现DRIVER_OBJECT结构的物理地址,第二列Start包含模块的基址,Driver Name列显示了Driver的名称。例如,驱动程序的名称\driver\Beep与Beep.Sys,最后一项显示恶意 驱动程序,\Driver\2683608180e436a1,与Necurs rootkit关联。driverscan插件是列出内核模块的另一种方式,当rootkit试图隐藏模块和modscan插件时,它会很有用:

$ 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插件将其从内存转储到磁盘。要将模块转储到磁盘,您需要指定模块的基本地址,您可以从modules、modscan或driverscan插件获得该基本地址。以将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插件时,我提到过driverscan从DRIVER_OBJECT结构中获取模块信息。你想知道DRIVER_OBJECT结构是?这一点很快就会清楚。在本节中,您将了解用户模式和内核模式组件之间的交互、设备驱动程序的角色以及它与I/O管理器的交互。通常,一个rootkit由一个用户模式组件(EXE或DLL)和一个内核模式组件(设备驱动程序)组成。rootkit的用户模式组件使用特定的机制与内核模式组件通信。从取证的角度来看,必须了解这些通信的工作方式以及所涉及的组件。本节将帮助您理解通信机制,并为接下来的主题奠定基础。

让我们试着理解当用户模式应用程序执行输入/输出(I/O)操作时发生了什么,以及在高级别上如何处理它。在讨论在第8章API调用流时,代码注入和连接(在Windows API调用流部分),我使用一个用户模式应用程序的示例使用WriteFile () API执行写操作,而最终调用NtWriteFile()系统服务程序在内核中执行(ntoskrnl.exe),然后将请求定向到I/O管理器,然后I/O管理器请求设备驱动程序执行I/O操作。在这里,我将再次详细讨论这个主题,重点是内核空间组件(主要是设备驱动程序和I/O管理器)。下面的图表说明了写请求的流程(其他类型的I/O请求,如read类似;它们只是使用了不同的api): 以下几点讨论了设备驱动程序和I/O管理器在高级别上的角色:

  1. 设备驱动程序通常创建一个或多个设备,并指定它可以为该设备处理什么类型的操作(打开、读取和写入)。它还指定处理这些操作的例程的地址。这些例程称为分派例程或IRP处理程序。
  2. 在创建设备之后,驱动程序会发布该设备,以便用户模式应用程序可以访问它。
  3. 用户模式应用程序可以使用API调用,如CreateFile,来打开处理发布的设备,并使用ReadFile和WriteFile API对设备进行读写等I/O操作。用于对文件进行I/O操作的api(如CreateFile、ReadWrite、WriteFile)也适用于设备。这是因为设备被视为一个虚拟文件。
  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管理器调用驱动程序的初始化例程,DriverEntry(类似于main()或WinMain()函数),通过传递一个指针到DRIVER_OBJECT结构作为参数。一个驱动对象(DRIVER_OBJECT结构)代表系统上的一个单独的驱动。DriverEntry例程将使用DRIVER_OBJECT用驱动的各种入口点来填充它,以处理特定的I/O请求。通常,在DriverEntry例程中,驱动程序创建一个代表逻辑或物理设备的设备对象(DEVICE_OBJECT结构)。设备是通过一个叫做IoCreateDevice或IoCreateDevice-Secure的API创建的。当驱动程序创建一个设备对象时,它可以有选择地为设备分配名称,它也可以创建多个设备。设备创建后,指向第一个创建的设备的指针在驱动程序对象中更新。为了帮助您更好地理解这一点,让我们列出已加载的内核模块,并查看一个简单内核模块的驱动程序对象。对于本例,我们将检查null.sys内核驱动程序。根据Microsoft文档,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扩展名命令显示驱动对象信息。从下面的输出中,驱动程序对象表示为空。Sys的地址是86a33180。设备对象列表下面的值86aa2750是指向null.sys创建的设备对象的指针。如果驱动程序创建了多个设备,你会在设备对象列表中看到多个条目:

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

你可以使用驱动程序对象地址86a33180来检查_DRIVER_OBJECT结构为空。可以通过dt (display type)命令查看。从下面的输出中,您可以看到DriverStart字段保存了驱动程序的基址(0x8bcde000),DriverSize字段包含驱动的大小(0x7000),driverame是驱动对象的名称(\ 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 driver只创建一个设备,NextDevice字段设置为空:

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请求时,它可以确定相关驱动程序的方式。这个概念可以通过以下图表来可视化:

您可以使用GUI工具,如DeviceTree (http://www.osronline.com/article.cfm?article=97)查看驱动程序创建的设备。下面是一个工具的屏幕截图,显示了Null设备创建的Null.sys驱动:

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

运行在用户模式下的应用程序无法访问在\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作为CreateFile函数的第一个参数(lpFilename),它返回设备的文件句柄。具体地说,对象管理器目录GLOBAL中的任何符号链接。可以使用CreateFile函数打开。如下图所示,C:卷只是一个到\Device\HarddiskVolume1的符号链接。在Windows操作系统中,I/O操作是在虚拟文件上进行的。换句话说,设备、目录、管道和文件都被视为虚拟文件(可以使用CreateFile函数打开): 此时,您知道驱动程序在其初始化过程中创建设备,并使用符号链接将其发布给用户应用程序使用。现在,问题是,驱动程序如何告诉I/O管理器它支持设备的什么类型的操作(打开、读、写,等等)?在初始化期间,驱动程序通常做的另一件事是用DRIVER_OBJECT结构中分派例程的地址更新Major函数表(分派例程数组)。通过查看主要函数表,您可以了解驱动程序支持的操作类型(打开、读取、写入等),以及与特定操作关联的调度例程的地址。主函数表是一个包含28个函数指针的数组;索引值0到27表示一个特定的操作。例如,索引值0对应于主函数代码IRP_MJ_CREATE,索引值3对应于主函数代码IRP_MJ_READ,以此类推。换句话说,如果应用程序想打开一个文件或设备对象的句柄,请求将被发送到I/O管理器,然后使用将IRP_MJ_CREATE主函数代码作为主函数表的索引,以查找将处理此请求的调度例程的地址。与读取操作相同,使用IRP_MJ_READ作为索引来确定分派例程的地址。

以下!drvobj命令显示由null.sys驱动程序填充的分派例程数组。驱动程序不支持的操作指向ntoskrnl.exe (nt)中的IopInvalidDeviceRequest。根据这个信息,你可以判断为null.sys仅支持IRP_MJ_CREATE (open)、IRP_MJ_CLOSE (close)、IRP_MJ_READ (read)、IRP_MJ_WRITE (write)、IRP_MJ_QUERY_INFORMATION(查询信息)、IRP_MJ_LOCK_CONTROL(锁控制)操作。执行任何支持的操作的任何请求都将被分派到适当的分派例程。例如,当用户应用程序执行写操作时,对设备的写请求将被分配到MajorFunction[IRP_MJ_WRITE]函数,该函数恰好位于null.sys驱动的卸载程序中的8bce107c地址。在nul.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
[01] IRP_MJ_CREATE_NAMED_PIPE
[02] IRP_MJ_CLOSE
[03] IRP_MJ_READ
[04] IRP_MJ_WRITE
[05] IRP_MJ_QUERY_INFORMATION
[06] IRP_MJ_SET_INFORMATION
[07] IRP_MJ_QUERY_EA
[08] IRP_MJ_SET_EA
[09] IRP_MJ_FLUSH_BUFFERS
[0a] IRP_MJ_QUERY_VOLUME_INFORMATION 82ac5fbe nt!IopInvalidDeviceRequest
[0b] IRP_MJ_SET_VOLUME_INFORMATION
[0c] IRP_MJ_DIRECTORY_CONTROL
[0d] IRP_MJ_FILE_SYSTEM_CONTROL
[0e] IRP_MJ_DEVICE_CONTROL
82ac5fbe nt!IopInvalidDeviceRequest
82ac5fbe nt!IopInvalidDeviceRequest
82ac5fbe nt!IopInvalidDeviceRequest
82ac5fbe nt!IopInvalidDeviceRequest
[0f] IRP_MJ_INTERNAL_DEVICE_CONTROL  82ac5fbe nt!IopInvalidDeviceRequest
[10] IRP_MJ_SHUTDOWN
[11] IRP_MJ_LOCK_CONTROL
[12] IRP_MJ_CLEANUP
[13] IRP_MJ_CREATE_MAILSLOT
[14] IRP_MJ_QUERY_SECURITY
[15] IRP_MJ_SET_SECURITY
[16] IRP_MJ_POWER
[17] IRP_MJ_SYSTEM_CONTROL
[18] IRP_MJ_DEVICE_CHANGE
[19] IRP_MJ_QUERY_QUOTA
[1a] IRP_MJ_SET_QUOTA
[1b] IRP_MJ_PNP
82ac5fbe nt!IopInvalidDeviceRequest
8bce107c Null!NlsUnload+0x3c
82ac5fbe nt!IopInvalidDeviceRequest
82ac5fbe nt!IopInvalidDeviceRequest
82ac5fbe nt!IopInvalidDeviceRequest
82ac5fbe nt!IopInvalidDeviceRequest
82ac5fbe nt!IopInvalidDeviceRequest
82ac5fbe nt!IopInvalidDeviceRequest
82ac5fbe nt!IopInvalidDeviceRequest
82ac5fbe nt!IopInvalidDeviceRequest
82ac5fbe nt!IopInvalidDeviceRequest
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操作已经完成,要么因为它必须被传递给另一个驱动程序,以便在驱动程序堆栈中进行进一步的处理。如果任务完成,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: 7th Edition》。

6.3 与设备驱动程序通信

现在,让我们回顾一下用户模式组件和内核模式组件之间的交互。我们会回到null.sys的例子驱动程序从用户模式触发对其设备(\device\Null)的写操作,并监视IRP发送到Null.sys的系统驱动程序。为了监视发送给驱动程序的IRP包,我们可以使用IrpTracker工具(https://www.osronline.com/article.cfm?article=199)。要以管理员身份监视IrpTracker的启动,单击File | Select Driver并输入驱动程序的名称(在本例中为null),如下面的截图所示,然后选择OK按钮:

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

C:\>echo "hello" > NUL

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

此时,您应该了解执行I/O操作的用户模式代码如何与内核模式驱动程序通信。Windows支持另一种机制,它允许用户模式代码直接与内核模式设备驱动程序通信。这是使用称为DeviceIoControl的通用API(由kernel32.dll导出)完成的。这个API接受设备的句柄作为参数之一。它接受的另一个参数是控制代码,称为IOCTL (I/O控制)代码,它是一个32位整数值。每个控制代码标识要执行的特定操作以及执行该操作的设备类型。用户态应用程序可以打开设备句柄(使用CreateFile),调用DeviceIoControl,并通过Windows操作系统提供的标准控制代码,对设备进行直接的输入输出操作,如硬盘驱动器、磁带驱动器、光盘驱动器等。另外,一个设备驱动程序(一个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:是符号链接的名称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。attachddevice字段告诉您有一个未命名的设备对象(868e7b28)与fvevol.sys驱动位于设备堆栈中设备对象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请求将首先被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的输出进行比较,以了解如何解释这些信息:

理解这种分层的方法是很重要的,因为有时候,一个新手驱动可以插入或附加在目标设备的堆栈下面或上面来接收IRP。使用这种技术,rootkit驱动程序可以在将IRP传递给合法驱动程序之前记录或修改它。例如,键盘记录器可以通过插入位于键盘函数驱动程序之上的恶意驱动程序来记录击键。

7. 显示设备树

您可以使用volatile中的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。要识别创建该设备的驱动程序,可以使用设备树插件。从下面的输出中,可以看出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。使用恶意驱动程序来劫持现有服务(在调查服务一节的第10章,使用内存取证来捕获恶意软件)。当服务启动时,恶意驱动程序创建一个设备来与恶意用户模式组件(DLL注入到合法的svchost.exe进程中)通信。以下设备树输出显示了恶意驱动创建的设备:

$ 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

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

$ 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
   1 IRP_MJ_CREATE_NAMED_PIPE
   2 IRP_MJ_CLOSE
   3 IRP_MJ_READ
4 IRP_MJ_WRITE
[REMOVED]
12 IRP_MJ_DIRECTORY_CONTROL
13 IRP_MJ_FILE_SYSTEM_CONTROL
14 IRP_MJ_DEVICE_CONTROL
15 IRP_MJ_INTERNAL_DEVICE_CONTROL 0xfffff80002a5865c ntoskrnl.exe [REMOVED]

8. 检测内核空间挂钩

当讨论钩子技术时(在第8章,代码注入和钩子)在钩子技术一节中,我们看到了一些恶意程序如何修改调用表(IAT钩子)和一些修改API函数(内联钩子)来控制程序的执行路径,并将其重新路由到恶意代码。目标是阻止对API的调用,监视传递给API的输入参数,或过滤从API返回的输出参数。在第8章,代码注入和hook,主要关注用户空间中的hook技术。如果攻击者设法安装内核驱动程序,在内核空间中也可能有类似的功能。与在用户空间中挂接相比,在内核空间中挂接是一种更强大的方法,因为内核组件在整个系统的操作中扮演着非常重要的角色。它允许攻击者以较高的权限执行代码,使他们能够隐藏恶意组件的存在、绕过安全软件或拦截执行路径。在本节中,我们将了解内核空间中的不同挂钩技术,以及如何使用内存取证来检测这些技术。

8.1 检测SSDT挂钩

内核空间中的系统服务描述符表(SSDT)包含内核执行程序(ntoskrnl.exe、ntkrnlpa.exe等)导出的系统服务例程(内核函数)的指针。当应用程序调用WriteFile()、ReadFile()或CreateProcess()等API时,它会调用ntdll.dll中的存根,它会将线程切换到内核模式。在内核模式下运行的线程会查询SSDT以确定要调用的内核函数的地址。下面的截图用一个WriteFile()的例子说明了这个概念(这个概念和其他api类似):

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

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影子。该表存储了指向win32k.sys导出的gui相关函数的指针。要显示这两个表的条目,可以使用ssdtVolatility插件,如下所示。SSDT[0]为本机SSDT表,SSDT[1]为SSDT影子:

$ 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挂钩,可以在SSDT表中查找不指向ntoskrnl.exe或win32k.sys中的地址的条目。以下代码是一个示例 Mader rootkit,它钩住各种与注册表相关的函数,并将它们指向恶意驱动程序core.sys。在这个阶段,您可以确定核心的基址。Sys使用模块、modscan或驱动程序,然后使用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挂接的缺点是它很容易被检测到,而且Windows的64位版本由于内核补丁保护(KPP)机制,也被称为PatchGuard (https://en.wikipedia.org/wiki/Kernel_Patch_ Protection),阻止了SSDT挂接。由于SSDT中的条目在不同版本的Windows中有所不同,并且在较新的版本中可能会发生变化,因此恶意软件作者很难编写可靠的rootkit。

8.2 检测IDT挂钩

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

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

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

8.3 识别内联内核钩子

攻击者可以使用jmp指令修改现有内核驱动程序中的一个或多个内核函数,从而将执行流重定向到恶意代码,而不是替换SSDT中的指针(这使其易于识别)。正如本章前面提到的,你可以使用apihooks插件来检测内核空间中的内联挂接。通过指定-P参数,你可以告诉apihooks插件只扫描内核空间中的钩子。在下面这个TDL3 rootkit的例子中,apihook检测内核函数IofCallDriver和IofCompleteRequest中的钩子。被钩子连接的API函数被重定向到名称未知的恶意模块中的0xb878dfb2和0xb878e6bb地址(可能是因为它通过解除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)。你可以清楚地看到钩子地址0xb878dfb2和0xb878e6bb在tdssserver.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的IRP_MJ_WRITE相关联的例程。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
   1 IRP_MJ_CREATE_NAMED_PIPE
2 IRP_MJ_CLOSE
3 IRP_MJ_READ
4 IRP_MJ_WRITE
5 IRP_MJ_QUERY_INFORMATION [REMOVED]
0xfffff88001979700 CLASSPNP.SYS
0xfffff8000286d65c ntoskrnl.exe
0xfffff88001979700 CLASSPNP.SYS 0xfffff88001979700 CLASSPNP.SYS 0xfffff88001979700 CLASSPNP.SYS 0xfffff8000286d65c ntoskrnl.exe

要检测IRP钩子,您可以关注指向另一个驱动程序的IRP处理程序函数,由于该驱动程序可以将IRP处理程序转发给另一个驱动程序,您需要进一步研究它以确认钩子。如果您正在实验室设置中分析rootkit,那么您可以从一个干净的内存映像中列出所有驱动程序的IRP函数,并将它们与受感染的内存映像中的IRP函数进行比较,以便进行任何修改。在下面的例子中,ZeroAccess rootkit钩子磁盘驱动的IRP函数,并将它们重定向到地址未知的恶意模块中的函数(因为模块是隐藏的):

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

modscan的以下输出显示了与ZeroAccess相关的恶意驱动程序(具有一个可疑的名称)和它在内存中加载的base地址(可以用来将驱动程序转储到磁盘):

$ 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.sys的IRP_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
      1 IRP_MJ_CREATE_NAMED_PIPE
      2 IRP_MJ_CLOSE
      3 IRP_MJ_READ
      4 IRP_MJ_WRITE
      5 IRP_MJ_QUERY_INFORMATION
[REMOVED]
13 IRP_MJ_FILE_SYSTEM_CONTROL
14 IRP_MJ_DEVICE_CONTROL
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
As discussed so far, detecting standard hooking techniques is fairly straightforward. For instance, you can look for signs such as SSDT entries not pointing to ntoskrnl.exe/win32k.sys or IRP functions pointing to somewhere else, or jump instructions at the start of the function. To avoid such detections, an attacker can implement hooks while keeping call table entries within the range, or place the jump instructions deep inside the code. To do this, they need to rely on patching the system modules or third-party drivers. The problem with patching system modules is that Windows Kernel Patch Protection (PatchGuard) prevents patching call tables (such as SSDT or IDT) and the core system modules on 64-bit systems. For these reasons, attackers either use techniques that rely on bypassing these protection mechanisms (such as installing a Bootkit/exploiting kernel-mode vulnerabilities) or they use supported ways (which also work on 64-bit systems) to execute their malicious code to blend in with other legitimate drivers and reduce the risk of detection. In the next section, we will look at some of the supported techniques used by the rootkits.
[ 465 ]
0x880ee07c Null.SYS
0x828ee437 ntoskrnl.exe
0x880ee07c Null.SYS
0x880ee07c Null.SYS
0x880ee07c Null.SYS
0x880ee07c Null.SYS
0x828ee437 ntoskrnl.exe
0x880ee040 Null.SYS

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

如上所述,检测标准挂钩技术相当简单。例如,您可以查找诸如SSDT条目没有指向ntoskrnl.exe/win32k.sys这样的迹象或IRP函数指向其他地方,或在函数开始处跳转指令。为了避免这种检测,攻击者可以实现钩子,同时将调用表条目保持在范围内,或者将跳转指令放置在代码深处。要做到这一点,他们需要依赖于给系统模块或第三方驱动程序打补丁。打补丁系统模块的问题是,Windows内核补丁保护(PatchGuard)阻止对64位系统上的调用表(如SSDT或IDT)和核心系统模块打补丁。由于这些原因,攻击者使用技术,依靠绕过这些保护机制(如安装Bootkit/利用内核漏洞)或者他们支持的方式(也在64位系统上工作)来执行他们的恶意代码融入其他合法司机和降低检测的风险。在下一节中,我们将研究rootkit所使用的一些受支持的技术。

9. 内核回调函数和计时器

Windows操作系统允许一个驱动程序注册一个回调例程,当一个特定的事件发生时,这个回调例程将被调用。例如,如果一个rootkit驱动希望监控的执行和终止所有进程上运行系统,它可以注册回调例程的过程事件通过调用内核函数PsSetCreateProcessNotifyRoutine PsSetCreateProcessNotifyRoutineEx或PsSetCreateProcessNotifyRoutineEx2。当进程事件发生(启动或退出)时,rootkit的回调例程将被调用,然后可以采取必要的操作,例如阻止进程启动。以同样的方式,rootkit驱动程序可以注册一个回调例程来接收通知,当映像(EXE或DLL)加载到内存时,当文件和注册表操作执行时,或当系统即将关闭时。换句话说,回调功能使rootkit驱动程序能够监视系统活动,并根据活动采取必要的操作。在以下链接中,您可以获得一些文档化和无文档化的内核函数列表,rootkit可能会使用这些函数来注册回调例程:https://www.codemachine.com/article_kernel_callback_functions.html。内核函数在Windows驱动程序工具包(WDK)中的不同头文件(ntddk.h、Wdm.h等)中定义。获取文档中内核函数的详细信息的最快方法是进行快速谷歌搜索,这应该会将您带到WDK在线文档中的适当链接。

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

在下面的例子中,Mader rootkit执行了SSDT挂钩(在本章检测SSDT挂钩一节中讨论),还安装了一个进程创建回调例程来监视系统上运行的所有进程的执行或终止。特别是,当进程事件发生时,恶意模块核心内地址为0xf66eb050的回调例程。系统调用。Module列指定了回调函数在其中实现的内核模块的名称。Details列给出安装回调的内核对象的名称或描述。识别出恶意驱动程序后,可以进一步调查它,或者将其转储到磁盘进行进一步分析(反汇编、反病毒扫描、字符串提取,等等),如这里的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 [REMOVED]
-
-
-
Ndis miniport
$ 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
-----------
0x0f66e9000
 Module Name      Result
----------------- ------
 core.sys         OK: driver.f66e9000.sys

在下面的例子中,TDL3 rootkit安装进程回调和镜像加载回调通知。这允许rootkit监控进程事件,并在可执行映像(EXE、DLL或内核模块)映射到内存时获得通知。条目中的模块名称设置为UNKNOWN;这告诉你,回调例程存在于一个未知模块中,如果rootkit驱动程序试图通过解除KLDR_DATA_TABLE_ENTRY结构的链接来隐藏,或者如果一个rootkit正在运行一个孤儿线程(一个隐藏或从内核模块分离的线程),就会发生这种情况。在这种情况下,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 [REMOVED]
-
-
- - -
Ndis miniport

即使模块名是UNKNOWN,根据回调例程地址,我们可以推断恶意模块应该位于地址为0xb878的内存区域的某个地方。从模块插件的输出中,您可以看到模块本身已经解除了链接,但是modscan插件能够检测到加载在0xb878c000且大小为0x11000的内核模块。显然,所有回调例程地址都在这个模块的范围内。现在已经知道了内核模块的base地址,你可以使用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文档:https://docs.microsoft.com/en-us/windows-hardware/drivers/kernel/timer-objects-and-dpcs。

要列出内核计时器,可以使用timersVolatility插件。需要注意的一点是,计时器本身并不是恶意的;这是Windows的功能,所以在一个干净的系统上,你会看到一些合法的驱动程序安装了计时器。与回调函数一样,可能需要进一步分析来识别恶意模块。由于大多数rootkit试图隐藏它们的驱动程序,因此,会创建明显的工件,可以帮助您快速识别恶意模块。在下面的例子中,ZeroAccess rootkit安装了一个6000毫秒的计时器。当这段时间过去时,将调用UNKNOWN模块中地址0x814f9db0的例程。Module列中的UNKNOWN告诉我们模块可能是隐藏的,但是例程地址指向恶意代码存在的内存范围:

$ python vol.py -f zaccess1.vmem --profile=WinXPSP3x86 timers

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

$ python vol.py -f zaccess1.vmem --profile=WinXPSP3x86 callbacks

尝试使用modules, modscan,和driverscan插件来查找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

检查驱动器列表发现了可疑的条目,其中基址和大小被归零(这是不正常的,可能是一个绕过的伎俩)。将基址归零解释了为什么模块、modscan和驱动程序不返回任何结果。输出还显示恶意驱动程序的名称仅由数字组成,这增加了怀疑:

$ python vol.py -f zaccess1.vmem --profile=WinXPSP3x86 driverscan

通过清空基地地址,rootkit使得司法分析人员很难确定内核模块的起始地址,这也阻止了我们转储恶意模块。我们仍然知道恶意代码的所在位置(以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。反病毒软件供应商的结果证实,它是ZeroAccess Rootkit(也被称为Sirefef):

总结

恶意软件的作者使用各种先进的技术来安装他们的内核驱动程序,并绕过Windows安全机制。一旦安装了内核驱动程序,它就可以修改系统组件或第三方驱动程序来绕过、转移和转移司法分析。在本章中,你看了一些最常见的rootkit技术,我们看到了如何使用内存取证来检测这样的技术。内存取证是一种强大的技术,使用它作为恶意软件分析工作的一部分将极大地帮助您了解攻击者的战术。恶意软件的作者经常想出新的方法来隐藏他们的恶意组件,所以仅仅知道如何使用这些工具是不够的;理解底层概念对于识别攻击者绕过取证工具的努力是很重要的。