原文 Step-by-Step Reverse Engineering Malware: ZeroAccess / Max++ / Smiscer Crimeware Rootkit

总序
这分成四个部分的系列文章,是一个完全的一步一步来分析ZeroAccess Rootkit的教程。它也被叫做Smiscar恶意软件,或叫做Max++ rootkit。透过循着教程一步步的看下去,你可以更深入更好的理解到在分析复杂到此种程度的现代rootkit时的思考过程。我们还推荐你把文中提到的工具们下载下来、以及找一个ZeroAccess的样本,并在阅读文章的同时,自己能亲手亲自看一看的逆向它。如果你想使用文中分析的具体这个样本的话,可以从这儿下载到它: Max++ Malware。注意样本的压缩包已经用密码保护起来以避免误操作了,引号中 "infected"。从本文楼层的附件中也可以下载到它。
InfoSec(原作者所在的公司)把ZeroAccess定义作一个复杂且高端的超级rootkit程序。它拥有4个主要模块,也就是我们接下来每篇文章将要对应着做详细分析的。ZeroAccess它是一个模块化的恶意rootkit,它就像一个给应用程序提供生存空间的操作系统般的平台,其主功能是能够安装可控数量的病毒程序到中毒机器上。它的能力还让它自己以及它所安装的病毒程序对于专业级别用户也几乎是完全不可能清理掉的了,同时,它还对自己做了些让专业的病毒分析师难以着手分析的处理。
文章的最后,在整个分析结尾的时候,我们会去跟踪Zeroccess Rootkit犯罪分子所在的源头。我们会发现这整个rootkit的目的是要建立起来一个强壮,不可检测而且不可切除的平台,以供病毒在中毒机器上进行繁衍和破坏。我们也会看到在现在,ZeroAccess正在用来传播一个叫做FakeAntivirus(假杀毒)的犯罪程序,它的罪行是哄骗用户花70美元(大约400 RMB)以移除掉自己这个杀毒软件。假设100%的受害用户都付了这笔钱的话,那就将是一笔数额达到 $17,500,0000(约10 5000 0000 RMB)的巨额黑金。现实的,不会有100%的用户付给罪犯这钱,但假定会有约30$的用户受骗的话,结果也有总额达到 $5,250,0000 (3 1500 0000 RMB)的黑金流进RBN网络犯罪团伙的腰包了。

ZeroAccess Rootkit拥有如下的特征和能力:
# 先进稳固的劫持(hook)住了操作系统-这使得在不影响操作系统稳定性的前提下,很难移除掉它
# 使用底层API,从硬盘上分出一块完全在受害机器上隐藏着的新分区(盘符),这把对它进行的传统磁盘扫描变成不可能的,或也是很难做到了。
# 复杂并有高可靠性的修改了被害操作系统的驱动,让病毒代码可以依此来启动。
# 高技巧的过杀软机制
# 抗扫描技术- ZeroAccess使用底层的磁盘和文件系统调用来对抗常见的磁盘中、内存中扫描技术
# 提供一个强键有力的恢复其他病毒,并可以安装新病毒的平台
# 通过APC(Asynchronous Procedure Calls 异步过程调用)插入到所有的用户空间和内核空间的进程、还会跟着插进去自己的模块,以在内核级别监视住整个系统的功能,并且可以无缝的插入代码到任何被监视的模块中。

在我们这个教程中,我们会从头到尾同时不对病毒做干预的进行分析,我们跟着一条病毒对全新又无辜的用户机子感染的过程来进行我们的分析。这种分析方法,实际在文中的表现就是,我们会把rootkit的注入方法和用来感染机器的执行流程,以类似于年度总结的形式描述出来。年度总结似的分析体现给我们的会是一种思路性的rootkit注入过程,它所表现出来的注入思路有大量以前分析过的其他高端rootkit也使用到了。所以,有必要提醒你,在这一篇章中认真的感觉rootkit进行注入的流程,然后,在以后的病毒分析中你会很经常的发现这个经验是用的上的。

通常的,一个rootkit感染进一个主机时,它执行流程可以一步步的分为下面的格式:
1.传播rootkit,以使其可以接触到被害系统的运输工具. (如对用户引导性的下载,或是客户端上的溢出漏洞,或下载者)
2.用户模式的感染中介程序
3.对驱动的解密和执行起驱动 
4.驱动在内核中进行系统级别的隐藏
5.从受害主机上冒出来,并从内核级别进行数据的监视/盗取
5.把盗取到的数据发送到一个隐蔽的数据通道里

我们也对应上面的一步步,把ZeroAccess的分析划成了一系列的文章:
章节1: 总序。还有对用户模式的感染中介程序的反混淆,以及逆向分析
章节2: 逆向分析内核模式中的数据盗取功能驱动
章节3: 逆向分析内核模式中的进程注入功能的驱动
章节4: 利用逆向工程跟踪到使用ZaeroAccess rootkit的罪恶之源

我们的分析将从用户层的代理程序开始,最后结束于rootkit丢出来的二个内核中的病毒设备驱动程序。

从源头开始分析:
ZeroAccess rootkit,在传播的源头是以一个恶意的可执行程序的形式存在的,它首先是通过引导性的下载来传播。引导性的下载有三个类型,每个都代表一种意外的从网上就下载到东西的情况:
1.下载行为是一个用户所允许了的下载,但用户当时并不明白将有什么后果(例,她下载了一个未知发行者或伪造发行者的可执行程序,或是ActiveX组件、Java小程序)
2.任何的,用户不知道的下载行为
3.广告程序、计算机病毒或任何其他能在用户机器上进行用户不知道它存在的隐式下载程序

引导性下载,可能会出现在用户访问一个网站,或者打开一个e-mail时,也可能因为他相信了虚构弹窗广告而点击了一个而产生。比方说,谋个弹出广告会说,一个计算机内部的错误被发现了,还有的可能弹出来一个看起来是无害的广告,但却带个关闭按钮。不论怎样,只要用户点击到了伪造的位置,内容提供者(比方浏览网页内容的IE)可能会认为是用户"同意做某事"了,于是内容提供商就会去下载一个虽然也许是不知道,并且还不想要甚至就是病毒性质的程序了。一个带有触发windows metafile漏洞的网页就,是一个这种引导下载的例子。

回到说ZeroAccess,它是有一些很强力的rootkit特征的:
*抗文件系统扫描的能力,它感染了关键的系统驱动(disk.sys, atapi.sys),还盗取了PIC驱动对象以及做了IRP Hooking
*感染了系统的设备驱动
*在内核模式中对用户模式进程做监听,并对其注入rootkit的DLL
*DLL的隐藏,过杀软能力
*面对修复感染行动时的绝对耐打

章节1: 对用户模式的下载者代理程序的反混淆与逆向分析

我们的Rootkit是一个被混淆了的程序,存在于我们面前的,是个通常名字为‘Max++ downloader install_2010.exe',加壳的可执行文件。我们分析的具体文件的hash值是:
MD5: d8f6566c5f9caa795204a40b3aaaafa2
SHA1: d0b7cd496387883b265d649e811641f743502c41
SHA256: d22425d964751152471cca7e8166cc9e03c1a4a2e8846f18b665bb3d350873db
 
对它的基本分析,显示出这个可执行文件有如下的PE区段和导入表:
Sections: .text .rdata .rsrc
Imports: COMCTL32.dll
 
这个导入表中只剩下些对我们分析很无力的信息。这在一般意义上就是说,我们的这个rootkit程序所必须要和不必须用的那些API函数,都是它在运行时才进行API导入的。我们现在到入口点观察下:


程序入口的代码,以一个函数的标准来说很标准,只是,除了一个特别的有意思指令除外,如你所见的,在00413BD5 我们遇到了一个出众的int 2dh指令.
int 2dh指令是windows内核模式调试机制的一部分,它用来提供个主动访问调试机制的接口。当一个int 2d被调用的时候,系统会创建出一个相应EXCEPTION_RECORD结构,并在里面填充异常代码为STATUS_BREAKPOINT,结构的其他部分则填充成具体异常时的线程环境。默认的对这个异常的处理,是调用KiDebugRoutine来完成的。
int 2dh是被ntoskrnl.exe用来中断给调试服务的指令,但在用户模式我们也是可以使用它的。如果我们试着去在正常情况使用它的话(没调试器附加上去时),我们肯定会得到一个标准的异常。然而,若附加了一个调试器上去的话,这个标准的异常就不会出现了。
(更多关于int 2dh的信息可以到此阅读 )

当int 2dh被调用到的时候,我们遇到了第一次ZeroAccess抗逆向分析的行为,也是它的一个花指令。系统在碰到这个中断时,会将程序的EIP加上一,直接越过中断位置后的一个字节,这就把中断后的那个指令的opcode给断开了。而这,会实质性的导致指令的执行,跟反汇编器里明白显示出来的指令流程不再一样了,也即调试器的反汇编的指令顺序成了错误的。
为了让它可以正常执行,我们需要一个能模拟int 2dh正确处理流程的方法,需要实现越过指令一字节,来允许我们跳转到被切开的那条指令的东西。为了这个,我们就要给OD加上StrongOD Olly了,你可以从这儿下载到它 (unpack.cn上的是最新版)

StrongOD安装好后,我们就可以在单步过int 2dh指令了,其后我们遇到了下面的流程:


这儿对我们而言最有意思的指令Call 00413bb4。就在它后面,我们看到了一堆新的垃圾指令,那就先进入这个call看看,你看到的会是下面的代码块:


再一次的,我们碰到了个int 2dh,因为它,我们要先拐到RETN指令其后的一个字节处好继续执行。在它后面的指令,作用是解密出周围的一片代码,往下再跟踪一段后,我们就到达了下图的地方:


上图的调用会解密出另一个代码块,在它执行完后,代码跳转到了下面的地方:


一个FS:[18],这地址对应的是线程的TEB结构(Thread Environment Block),程序找到TEB的目的,是为了获取到PEB(Process Environment Block)的地址,它保存在TEB的地址+ 0x30的地方。
而PEB+ 0xC 指向的元素,是个PPEB_LDR_DATA LdrData
要你是在用WinDBG做分析的话,你现在可以使用一个快捷技巧来得到在 结构->偏移->元素之间的具体关系,只需使用指令:

0:004> dt nt!_PEB_LDR_DATA
 ntdll!_PEB_LDR_DATA
 +0×000 Length           : Uint4B
 +0×004 Initialized      : UChar
 +0×008 SsHandle         : Ptr32 Void
 +0x00c InLoadOrderModuleList : _LIST_ENTRY
 +0×014 InMemoryOrderModuleList : _LIST_ENTRY
 +0x01c InInitializationOrderModuleList : _LIST_ENTRY
 +0×024 EntryInProgress  : Ptr32 Void
 +0×028 ShutdownInProgress : UChar
 +0x02c ShutdownThreadId : Ptr32 Void
 
如你所见的,上述的恶意代码找到了_PEB_LDR_DATA + 0x1C,通过对照WinDBG的输出你会发现ECX现在指向的是InInitializationOrderModuleList元素。而在其后的代码,负责的就是定位到导入表函数的地址,接着现在就像空中飞人一样构造出了一个程序所用的导入表。导入表完成后,又有了一片非常复杂之的一串串嵌套调用,很明晰的,是用作解压ZeroAccess关键代码的一层一层的东西。我们现在是不会去分析它的。因为这段代码是相当的长,还因为是用复制粘贴方式写出来的,它无比的无趣,最后还跟我们要分析的rootkit功能部分是没一点点关系的。

对导入表函数的地址加密做的很成功,只有在调用到具体函数的时候,那个函数的地址才会被临时的解压出来。让我们看一个API调用具体怎么样的:


Call 00401172负责解密API 地址,并把值返回在EAX中,后面的JMP EAX就是调用API了。在上面一个代码片段中,所调用的API是VirtualAlloc.这儿申请的内存,将被用来保存此感染代理程序之后要抖啊抖出来的几个指令块。那些被感染代理抖出来的块,最终要构成一个新可执行文件的。
现在,主模块构造好(主模块我们指的是此感染载体程序,我们刚才也叫它为感染代理)并且丢出了数个文件到受害机器的硬盘上,当然,它们在主模块的内存中也是保存着的。不管它在硬盘还是内存中,它们被调用到的顺序总是一样的:


继续,让我们试着判定出这段代码是在解压出些什么东西来。我们现在要在0040162B处下一个断点,它是上图Next Block跳转后的第一个指令。而Next Block代码块的结束,代表着代码解密过程已经执行结束了,执行到此,我们已经可以在程序申请到的内存中找到"MZ"标记了,这让我们确定解压出的可执行程序,已经是可以执行的地步了。在继续下一步分析前,我们建议你把完整的可执行程序dump到硬盘上,可以用Olly的备份功能实现.
你也可以直接从这儿下载: dumped.rar

下面的代码块,被用一个VEH(Vectored Exception Handler )给保护起来了,它是由RtlAddVectoredExceptionHandler 和RtlRemoveVectoredExceptionHandler加上去和卸载的。跟进去,我们碰到的是一段真的很重要的代码块。它的功能都是调用未文档化的API实现的,包括用 LdrLoadDll载入lz32.dll,以及要创建一个Section Object.


Section Object代表的是,一个可以被进程间共享的内存区块,一个进程可以利用一个Section Object和其他的进程共享一片内存地址。而Section Object还提供了让进程可以把一个文件映射(map)到它的内存空间的能力。
注意,看一下上图红线圈起来的部分,程序在此执行进了一个储存在EAX中的地址003C24FB。如你所见,这地址是属于刚刚LdrLoadDll载入的那个lz32.dll的地址的。因为这个call,执行流程跃进了lz32.dll模块的地址空间内,但是事实上,包裹里面现在不是LZ32.dll,包的却是感染代理解压出的那段真正的核心恶意程序。 (#:这里面的代码,是在前面那个call里处理完成的。这儿它和lz32.dll的关系是不可共存关系。lz32.dll的实际作用是提供一个利用LdrLoadDll PE载入功能的机会而:程序用一个在ZwMapViewSection单步断点的断点断在LdrLoadDll里面,将lz32.dll载入过程时ZwMapViewSection的使用的地址修改成前面那个Section Object的地址,让操作系统本为lz32.dll准备的PE文件载入过程用在程序自己的PE文件上。最终,释放PE文件头所在的一个0x1000大小内存块完成这个看着就很无力的反调试流程)
新的程序,我们一跳过去就看到下面这个样子的代码了:


如果跟踪进 Call 003C23DB,我们会发现一个有超长的函数,这儿就是再下一步的感染过程。再遵循实际严格点的说,我们在这儿发现的是内核模块的安装过程。我们会看到一系列的新奇代码,为的是躲开那些经典杀软的拦截,包括对Section Objects的利用,还有把自己的东西放进入系统文件映射中等。
让我们检查下这整个感染代理的最核心过程吧。我们现在会一片片的把它给分析干净:


当分析一个复杂病毒的复杂片段时,把句柄窗口、模块窗口都打开在OD界面上是一个很好的习惯和练习。这可帮助你保持能跟紧在病毒载入、卸载模块,以及及时的发现它在打开一个文件/object/线程/等东西。现在,让我们来看看在003C2461处调用到了的Call 003C1C2C 中发生了些什么(在ExitProcess的后面)。

一进来,我们就看到对\system32\drivers目录中驱动的枚举,接着又有下面的一块代码:


在这儿碰到的是一个很有意思的算法。它在枚举了驱动程序之后,一个随机数也被制造出来了,把它挤进[0- 0xff]的范围中后,随机数被用来随机的从获取到的驱动列表中选择一个驱动出来。最后,还有一个字符串的格式化,字符串样子是:

  \._driver_name_

让我们打开句柄窗口,看看现在发生了什么事:

如你可见的,一个根据随机选中的驱动来创建的Section Object冒出来了,在下面,这个Section就要被映射(View)到程序的内存空间了。
值得注意的是,这个Section访问权限的值被设置成了0xf001f。我们先说下为什么这个权限值得注意。在分析一个病毒的过程中,感觉上你就像在进行一个法医调查,做调查的一个基础,是要调查好对手访问到了几个部分,据此我们可以为我们的调查指明正确要投入力气的方向。逆向分析中的这种信息,可以通过检查变量们上所附的访问权限来获取的。
让我们找找0xf001f访问权限到底指的是什么,winnt.h中有:

  #define SECTION_ALL_ACCESS 0xf001f
 
SECTION_ALL_ACCESS意味着,这句柄指向的SECTION_OBJECT中的数据能允许的行为已包括了读取、写入、查询还有执行。这个,可不就是最佳的恶意代码滋生地点了。让我们继续分析:


上述代码把之前选好的那个驱动,一起领着然后把它写入到注册表:

\registry\MACHINE\SYSTEM\CurrentControlSet\services\

在这个CurrentControlSet下面的\services 项里面,它包含了设备驱动、文件系统驱动、还有win32服务驱动的设置的信息。对于每一个服务,都要在这儿有一个对应于服务名称的子键。
我们的注册表键的名字会是字符串 \._driver_name_
我们的服务启动类型为0x3,意思是 -> Load on Demand
服务类型为0x1,意思是 -> Kernel Device Driver
可执行文件路径是 -> \*



事情总是这样的,现在驱动程序的文件被打开了,接着,它的文件句柄被穷尽怪异的使用了,这次是用ZwFsControlCode,用到的是一个FSCTL(File System Control Code文件系统控制码)。再看下运行时给它的参数,显示出来的FSCTL代码是9C040。这个代码的含义是FSCTL_SET_COMPRESSION。它为某个硬盘卷上面的一个文件或者一个目录设置个压缩标志位,当然,先需要对应的文件系统支持对文件和目录进行预操作的压缩功能。

在其后,一个新的可执行文件会被通过之前用到过解压方法给解压出来,并通过ZwLoadDriver来完成驱动的载入。(#在ZwLoadDriver之前,先用了个ZwCreateSymbolicLinkObject把驱动文件名"\*"和之前随机出的驱动做了硬连接了)。我们分析的样本中这个驱动载入过程会应用在二个设备驱动上面:
1. 第一个驱动是随机选中的驱动,它会完成IRP Hooking,和对某些Object,包括对disk.sys/pic.sys的Object的盗取(之后我们要比现在详细得多的对这个事情进行个分析)
2. 第二个驱动,叫做B48DADF8.sys,它是负责望风,警惕着进程创建行为的驱动,还包含着一个像小说写的一样精彩的DLL注入系统(之后我们也要对它进行一个详细得多的分析)

驱动的注入一完成,我们就发现了下面的代码:


在这儿,我们看到了名叫fmifs.dll的DLL。这个DLL是用来安装文件系统的格式化管理器,同时它还提供了一个完整的文件系统管理函数集合。
这一次被使用到的函数是 FormatEx。下面是有文档说明的FormatEx的一点点信息:
VOID
 STDCALL
 FormatEx(
 PWCHAR        DriveRoot,
 DWORD        MediaFlag,
 PWCHAR Format,
 PWCHAR        Label,
 BOOL        QuickFormat,
 DWORD        ClusterSize,
 PFMIFSCALLBACK    Callback
 );
 
这个函数,如它名字所示的是用来格式化硬盘卷的。在我们的情况中盘符号是 \\?\C2CAD972#4079#4fd3#A68D#AD34CC121074 ,而要格式化成的格式是NTFS。这个进行格式化的地方,就是这个让这个rootkit显得超群而卓尔不凡的地方。它调用fmifs.dll的API创建了一个隐藏的分区(盘符),这个分区将会用来保存rootkit的驱动还有ZeroAcess感染媒介所丢出来的那个DLL。即便是现在被感染了,这些恶意文件的存在也还是完全隐藏在受害者的面前,还是查不到它们的(something we teach in our ethical hacking course: http://resources.infosecinstitute.com/ethical-hacking-training/)

格式化了新区之后,感染代理接着要做的事情是生产新物品,使用和前面说过的相同的解压过程,其他的几个病毒程序也将被放进刚刚创建的那个隐藏分区中去。有两个文件:
*B48DADF8.sys
*max++.00,x86.dll

在最后,它们都会保存在隐藏盘符中,\\?\C2CAD972#4079#4fd3#A68D#AD34CC121074\L\。
我们分析到现在,在总体上对于ZeroAccess在用户模式的所作所为,已经有了一个很好的认识了,那就可以把我们的注意都投向内核模式那边了,为此,我们就要分析两个新的驱动文件和它丢出来的那个DLL。

让我们在接下来继续跟踪这个rootkit的工作流程。如果你是沿着我们的分析路线来逆向的,接下来的分析就应该根据代码执行的逻辑关系来进到感染代理丢出来的二进制文件了。我们的第一个随机名称的未命名驱动文件分析已经完成了,内容是在第二个章节中

#我很不喜欢这样的乱补充,不过有一个地方自己跟的时候不注意的话真的很头疼。在进入那个病毒PE模块后,第二个call里设置进程的权限,然后注入到了一般是smss.exe的某个进程中,创建了一个SUSPEND的线程并且在配置好了堆栈后给它设置EIP启动了,具体是用ZwSetInformationFile来删除文件自身的。为什么提这个呢,为了把那二个驱动给复制出来,我运行了它好多次,每次都来删一下,7、8次后就给气的头皮发麻了。

上传的附件 Max++ downloader install_2010.rar

  • 标 题:答复
  • 作 者:XPoy
  • 时 间:2011-10-09 11:08:16

原文 ZeroAccess Malware Part 2: The Kernel-Mode Device Driver Stealth Rootkit

章节2: 逆向分析内核模式中的盗取功能驱动

在ZeroAccess病毒逆向分析系列的第二个部分,我们将要分析第一部分时那个用户模式感染代理扔出来的第一个驱动程序。这个驱动的基本目的是要,支撑出一个强健的功能性和组件化的供ZeroAccess恶意软件生长存活的平台。这个rootkit使的是低阶的磁盘访问函数,让它能够创建出来一个新的盘符,完全从受害者系统上隐形,完全从杀软面前隐形。考虑下实际会是什样的情况,某某谁发奋要去删除掉这个rootkit,他格式化了自己的所有的盘符,当然包括操作系统所在那个盘符(比如是C:\),然后重新安装了新的windows。但ZeroAcess在这一波清扫行动下会完美的幸存下来,接着重新安装它自己到新鲜的一份windows中。这对于任何试着攻击ZeroAccess的人说真的是超级沮丧的一条现实。除此以外,我们也会调查分析下Rootkit军团内部广泛使用的IRP Hooking过程,其用处是规避开检测行为以及对隐藏功能提供的支持。回说ZeroAccess,其灵活选择注入数个系统驱动文件的本事,也让它比其他rookit在无声行动的路上走得更远。在本文的最后,我们将要涉及到如何利用rootkit规则中的漏洞来使用现有实际工具来检测到它存活的踪迹。

首先,我们来报告下当场分析的这个文件的标记数据和hash值:
FileSize: 132.00 KB (135168 bytes)
 
MD5: 83CB83EB5B7D818F0315CC149785D532
 
SHA-1: 39C8FCEE00D53B4514D01A8F645FDF5CF677FFD2
 
没有有效的版本信息。

没有有效的资源信息。

#(必要的补充,在章节1有一个特别的地方是ZwCreateSymbolicLinkObject所创建的符号链接,它把\*和随机产生的那个驱动对应到一起了,因此LoadDriver ( "\*")实际载入的是system32\driver下的那个随机驱动。你也可以从自己的那儿得到实例。只是随机驱动使得它的Hash没有参考价值,可以保证的部分是,代码是一样的。附件包含一个实例,ndiswan.sys)

分析开始前,第一件我们要注意的事情是这个驱动带有调试符号的PE文件,调试符号对我们的反汇编是很有帮助的。另外如果你是用ida分析的话,可以通过载入Tyle Library来提高可读性的,你应该先到type libraries(shift+F11)中把ntddk.h确定这个头文件是被ida使用的,不然导入表的函数的符号结构类型的参数就是没有解析的。
下面跟着的是一张图,是数个代码块之间的执行关系的框架:


在现代高端rootkit中,感染代理在解压和投放出来后的第一个操作是要,把它的存在从用户和杀软的视野中遮盖住。这个驱动程序中的相关部分包含了一批操作,它安装了一个对隐形的支持体系,使它的感染变的非常有弹性

、也几乎不可能清除掉了。当然,简单说它做的事情也就是,把从用户模式的感染代理开始的整个感染过程给完成。
最有可行性和简单的迫近rootkit分析的方法是,直接挂载入执行中的模块。我们将要开启一个内核模式调试器,比方说Syser。在我们现在的情况中,整个ZeroAccess的代码都是位于DriverEntry的(驱动程序的main ()函数)

,我们也将发现数个dispatch流程和系统线程,它们会制造一些非线性执行的代码流。
让我们从头检查下代码:


如果你还记得,我们称为选中者驱动的那个东西是被感染了的,它还被储存到了注册表中了,它的注册表项目还是一个用"点"('.')来开始的。在上面的代码块中,我们看到了驱动在检查这个注册表键。接着下面,你可以看见

ResultLength,一个属于OBJECT_ATTRIBUTES 结构元素的变量名,结构是用来设置属性的,而同一个结构,可能会有数个的对象都使用它。我们接着分析样本:


我们看到OBJECT_ATTRIBUTES 的元素都用NULL值(EAX中)来填充了,里面只除了ObjectName 里会特别的设置成RegistryPath外,然后我们还遇到了二个子函数。第一个函数实现了注册表键的枚举,它又接着删掉了它,并返回了

删除行为的结果。后一个函数完成的是差不多的事情,只是这次删的键是:

\\registry\\MACHINE\\SYSTEM\\CurrentControlSet\\Enum\\root\\LEGACY_*driver_name*
 
下面我们就看到进去了一个重要的调用:

100037A5 mov Object, eax ; Object = DriverObject
 

100037AA call sub_100036CA
 
在这个子函数里面,我们将会看到IRP hooking的流程。


__IRP Hooking__
 让我们开始看这块代码:


在此我们碰到了ZeroAccess Rootkit的一个立身根本的功能,闪闪发光亮晶晶的磁盘驱动 IRP Hooking流程。它这样实现的依据: disk.sys是一个大量负责跟硬件交换数据的驱动,每一个操作,OS所处理的所有磁盘储存相关

的都必须通过'Driver/disk。如果你对这句话所描述的情况不是很输没有个严格的认识,这儿有一个虚拟的windows磁盘储存结构图可以帮到你:


原图地址在: http://technet.microsoft.com/en-us/library/ee619734%28WS.10%29.aspx

红色箭头指出的是ZeroAcess活着和工作在的地方,你可以看到这已经是磁盘储存驱动们的结构最底层了。硬件的最接近者,最强劲的rootkit可以存在的地方。ZeroAccess所用的技术是一种大量使用的概念性东西,同时也被

发现是最有效果的了。
IRP Hooking的原理是用rootkit的自定义IRP处理函数替换原始IRP dispatch流程。如果一个rootkit把这hook成功的干出来了,所有的IRP都要受控,要被重定位发往rootkit的会做手脚的代码了。它经常是专门用来监视的,

或许也有隐藏功能以及欺骗机子用户的功能。从概念级别说,rootkit操纵数据的类型可以分为三个最概况的主要行为:
* 当输入的数据是要储存和传输的时候,做的是监视工作
* 当数据是要返回给其他进程时,做的是隐藏工作并且也多跟着对进程使用的函数进行的相应修改
* 假数据返回的时候,做的是欺骗用户的工作。

在我们的例子中,返回的数据是被控制用来掩盖住病毒程序在受害机器的存在和活动。
让我们现在折回到最近一张代码的截图,如你可见的,IRP 处理函数的地址被插进了 Object+ 0x38 的位置(是个DRIVER_OBJECT 结构的一个指针,待会儿再谈其结构),其指向的元素是PDRIVER_DISPATCH MajorFunction。这

是一个保存了驱动的数个dispatch流程指针的数组。数组所使用的索引值是IRP_MJ_XXX,它是对应着各个IRP 主函数的宏。
我们看到原始的 \Disk IRP dispatch 表被填充成了恶意rootkit的dispatch函数。实质的说说,病毒的IRP 处理函数一样需要去处理那个类别数量达到叫所有人有深深印象的 I/O 请求包的,以期能够全部截获住碰到rootkit

核心文件的请求。如果它判断到rootkit文件被访问接触到了,它将要返回一个假冒的结果,而且会自己把IRP标记成完成的状态(STATUS)。
让我们看一下这个整个IRP表都被填充成rootkit函数的代码:
紧跟着的这个函数接受的参数是前面叙述到的object 指针还有个PIRP IRP。 PRIP IRP正是要分派的IRP
。首先,object要先滤过一个ZeroAccess特别关注的驱动,如果两个object匹配了,代码就走向了calls sub_1000292A。

这个子函数也接受一个参数,还是IRP它自身的指针PIRP IRP。从子函数返回后,其返回值直接被本处理函数返回去。sub_1000292A 的内部,我们碰到了另一个看起来就很标致的IRP解析流程,这次的代码处理的内容非常直接

的分成三类:
*访问到ZeroAccess核心文件时的忽悠处理
*电源IRPs
*病毒自己的IRP 请求

作忽悠处理的I/O请求处理起来总是有规范的一个方式,代码的原型看起来总是像:

Irp->IoStatus.Status = FakeFailureStatus;
 
这之后还会调用IofCompleteRequest 函数来完成IRP。
电源的IRP是通过PoStartNextPowerIrp 来管理的,还有相似的其他几个函数。
结尾的地方,我们到了ZeroAccess自用的IRP通道。因为按照绿色有利的IRP传递,也需要标记出来是哪个进程传递来的请求,所以rootkit对是否为病毒进程的检查也是依此完成的:
   
  Irp->Tail.Overlay.OriginalFileObject
 
现在,让我们回到主IRP[处理函数。在object没相等的情况中,会先检查下object的CurrentIrpStackLocation 是否为0x16,0x16的情况下,驱动通过调用PoStartNextPowerIrp来处理这个IRP。调用这个函数的直接影响是让

驱动知道了电源IRP已经处理完成了。
对于一个驱动而言,必须在当前的IRP堆栈层次指向的是本驱动的堆栈时调用PoStartNextPowerIrp。调用函数PoStartNextPowerIrp后立即是一个取Irp->Tail.Overlay.CurrentStackLocation 值的操作(这也是

IoGetCurrentIrpStackLocation的未文档化的实现方式)。我们在这后面又碰到一个PoCallDriver把电源IRP发送到驱动堆栈中更低层驱动的函数,它的后边就是退出dispatcher流程了。看完了这条dispatcher的代码,我们接

着看下一条dispatcher程序:


在这儿我们碰到了一个条件跳转。它的跳转需要满足数个条件,其一是对sub_1000273D的调用来返回的NTSTATUS值(#因为STATUS的错误值是大于0x80000000的,所以jge其实是判断是否为标志错误的STATUS),保存了这个返回

值的变量被我们叫做resStatOperation。这个时候,如果那条件跳转判断失败了,我们立即走到了一段结束代码。这儿会设置IRP的IO_STATUS成员,并且用IofCompleteRequest 把IRP标记成完成的。注意,这是对截获的IRP进

行的。#意味着现在是fake的情况。
编译出来这段完成IRP的源码看起来应该会是这个样子的:

  Irp->IoStatus.Information = 0;
 
  Irp->IoStatus.Status = resStatOperation;
 
  IofCompleteRequest(Irp, 1);
 
  return resStatOperation;
 

至于那些和隐秘行动、文件隐藏无关的IRP,都是轻而易举的传递给了底层驱动并且是由原始正确的dispatch流程处理的。如你所见的,在这段代码块中,整个分发流程的判断部分基于的是CurrentStackLocation 内的值,#这儿的原文是纰漏的,不仅仅是简单的根据CurrentStackLocation 来处理的,softworm大牛指出问题后也分享了他的分析结果,

引用:
如果是读写请求,则取TRANSFER_PACKET.OriginalIrp,在10006F03的调用是个对IO_STACK_LOCATION的回溯,
找到栈顶irpSp->FileObject,构造Irp取文件全路径计算Hash,检测是否读写RK驱动文件,如果是,则对读请求
返回原始数据,写请求则仍然写入RK的东西。
驱动程序开发中的IRP分发,如果没接触过的话,这儿可能有点难以理解,我们就在IRP分发上面也解释一下。
I/O 包(IRP)结构包含了两个部分:
*结构头
*可变数量的局部堆栈
IRP的局部堆栈包含着由主函数和辅助构成函数集合,通常其中的重要部分总是主函数部分,原因是主函数标示出来了一个驱动被IO管理器调用时,对发送过来的IRP有着怎样的dispatch行为。
__IRP Hooking的结束__

现在到了我们卷土重到DriverEntry研究的时候了。

在call sub_10003108 里面我们遇到了一片重要的代码:


特别尤其重要的IoCreateDevice 的参数,用红色箭头标出来的那个.FILE_DEVICE_DISK可以用类似于操作结构的方式创建一个磁盘出来。如果设备创建成功,驱动对象(object)就要被转换成一个临时对象。这样做是因为一个

临时对象可以等下再删除掉,换句说它是可以从设备命名空间中移除的设备,方法是对它进行解引用。ObDereferenceObject 解引用会递减对象的引用计数器减1.如果对象是创建作(在我们的例子里是转换成)一个临时对象的

,当它的引用计数到达0的时候,这个对象就可能会被操作系统给删除掉。
就像你可以在代码中看到的,我们在后面紧跟着就看到了一串字符串:

\systemroot\system32\config\12345678.sav
 
让我们接着检查下代码的下一段逻辑:


完整的字符串 12345678.sav被当做参数传递给了 call sub_10002F87。在这个调用里面,我们遇到了一点点很弱的代码混淆。解码的算法是相当的简单,可以通过一个XOR+ ADDTION的计算来完成,计算用的key是从一个

windows注册表值中提出来的。
提一下,当在逆向一个内核模式的rootkit时候,在你看到ZwCreateFile的时候,就要检查在这个调用的第四个参数,它是个IO_STATUS_BLOCK 结构的指针。它包含了函数所投递的IRP最终的完成状态,意味着你可以通过它来

判断出文件操作是否已经完成,创建/打开/覆盖/替换/等等的操作。
现在分析到了这么远的地方,我们可以很确定猜到这个随机的-sav文件是被当做一个配置文件来使用的了。这是个扩展用来保存信息的文件,肯定还有一个对于原始属性干净、未感染的系统驱动备份出来。当一个用户或一个

文件扫描器访问到被感染的驱动时,因为ZeroAccess在底层截断了设备驱动,文件将会不动声色的换成原始的一个。这将彻头彻尾的欺骗住任何一个去检查系统驱动的进程。
让我们继续看我们的这段代码。正如你看到的,在这儿rootkit检查的东西正是和上述一样的思路,它把IoStatusBlock->Information 和一个0x2的值做比较。这个值代表的是FILE_CREATE。如果一个文件有FILE_CREATE状态,

则rootkit会调用ZwFsControlCode 给这个文件发送一个FSCTL_SET_COMPRESSION 的控制码。
之后的ZwSetInformationFile 函数的作用是用来改变一个文件对象(file object)的几种文件信息。在我们的例子中,我们的FileInformationClass中放的是FileEndOfFileInformation,修改的是当前文件结束位置的信息,

具体位置是由FILE_END_OF_FILE_INFORMATION 的结构提供的。这个操作既可以用来打断一个文件,也可以用来扩张它的大小。调用者必须带着FILE_WRITE_DATA 标志打开一个文件以让文件结尾可以被设置,标记是在打开文件

的DesiredAccess 参数中设置的
让我们继续看下一段代码:


ObReferenceObjectByHandle 函数提供的是对一个对象句柄访问的验证,同时,如果这个访问是被授权的,它会返回指向对象的结构体的指针。在解引用了我们的文件对象后,通过调用IoGetRelatedDeviceObject我们就能得

到和它相关的设备对象的指针。
若你还记得,rootkit自己的设备驱动是用FILE_DEVICE_DISK建立的,那你应很容易猜到那设备就代表着rootkit要操作的那一个磁盘卷。
就跟你从代码看到的一样,这儿是一个对deviceObj->SectorSize 的引用。
借助于DEVICE_OBJECT 的文档说明,我们可以了解到关于SectorSize 成员的一些信息:

"这个成员储存了卷的每扇区大小,以字节来表达。I/O管理器使用这个元素在中间缓冲被禁止的时候,确定所有的发出的读操作、写操作还有文件位置设置操作的内容都是对齐着的。在创建一个设备对象时,系统默认的每扇

区字节被默认使用。"

DISK 的结构将服务于rootkit,并提供给rootkit一个简便够用的方法来管理它的文件,也即是,它可以把自己的那个rootkit设备当做一个普通磁盘来使用。
在这儿如果你回头去看一下,这个驱动的DriverEntry 开始部分代码的话,会发现我们有遇到一个'.'字符的检查,找到的话就按照我们上面所跟踪的流程来执行,但找不到的时候,代码的执行就跳到下面的最后一片代码了:


上面的一段完全注释好了的。EBX指向的字符串是随机选择的那个系统驱动,call sub_10002F87从一个注册表键中折腾出来一个"Snifer67"的字符串。下面你可以看到一个我们改名做HashCheck的汗水。它需要三个参数, 

HANDLE SourceString, int, PULONG HashValue:


如果hash效验出错了,就跳转到call sub_100036E9结束掉驱动的初始化,里面主要的是把MDL释放的操作。没出错的话,代码的执行就重定向到call sub_100022C3中了,其时如下所示:


我们在sub_100022C3遇到的这个东西是一个内核模式和用户模式之间互动的方法,叫做内存共享。借助内存共享,可以把内核内存映射到用户模式重。有二个常用的用来利用内心共享的技术,它们是:

*共享对象和共享映射(view)
*映射内部缓冲

我们早就见过Section Object怎么在用户模式工作了,在内核模式中概念是没改变多少的。改变的是这次我们要处理的是MDL管理的内存了,我们还需要些额外的安全检查,因为在内核和用户空间之间共享可能会成为一个非常

危险的行为。在打开一个Section后,通过ZwMapViewOfSection来建立一个映射。我们假设你现在很想要知道这个Section是在哪儿打开了,那么一个快速的查看方法就是打开句柄表来看看。要打开它,第一步就是定位到句柄

是在哪儿储存的。简单的把你的调试器的内存窗口设置到现实ZwOpenSection的SectionHandle 参数即可。
打开一个Section如果成功了,在内存中你将可以看到对应的句柄,与此同时我们还可以查询出关于这个句柄的更详细更多信息。你的调试器的语法可能是:
Syser的: handle handle_number
WinDbg的: !handle handle_number ff
这儿是一个WinDbg的输出样例:

> !handle 1c0 ff

Handle 1c0

Type Section
 
Attributes 0
 
GrantedAccess 0×6:
 
None
 
MapWrite,MapRead
 
HandleCount 22
 
PointerCount 24
 
Name \BaseNamedObjects\windows_shell_global_counters
 
Object Specific Information
 

在我们的情况中,是在之前随机选中的那个驱动打开的Section Object和映射。这儿有一点很关键要搞清楚,ZwMapViewOfSection 映射到用户空间时,是映射到特定一个进程的内存空间中的。把驱动的映像(view)映射到系统

进程中,可以避免用户进程篡改内存的内容,而且也让它只能在内核模式访问。
现在让我们接着看下一个call里的代码:


MmAllocatePagesForMdl 函数申请一片用0填充的,未分页的,物理内存页面给一个MDL。若申请成功了,在在ESI中我们就得到了MDL的指针,MmMapLockedPagesSpecifyCache 使用它来映射物理内存页面,同时允许调用者设置

所映射内存的缓存方式。参数BaseAddress 指定了把MDL映射到的用户地址的开始位置。当这个参数为NULL时,系统将会选择映射的开始地址。EBX中包含的是映射页面后基地址的返回值。在这的后面,是一个标准的memcpy,

反汇编器已经在截图中注释好了。
这个调用的返回值是true/false,返回值是由ZwMapViewOfSection的成功和失败来决定的。

有一个函数失败了的话,代码的执行就回跳转到MDL清理函数的部分。清理掉之前所述的那些MDL内存,然后退出驱动的初始化。全都成功的时候,我们就航行到了这个驱动的下一片区域了。再一次的,让我们澄清下全部的这

些在这个随机选中的驱动中进行的操作都是为了一个目的,为了给ZeroAccess作者所交付过来的病毒们接种,还有确保rootkit在任何一种清理和杀毒操作下幸存下来
让我们阅读下一段代码吧:


这一块满含病毒逆向工作者感兴趣的函数。让我们先把第一个call打量一番,call sub_10002D9F,采用了一个之前在分析映像文件时已经接触过了的SourceString。进一步的分析如下所示:


你应当可以理解这一段代码是在干什么,它和之前看过的内存共享映像文件的流程是相当类似的。这一次SectionObject 是使给了随机选中的那个驱动。(#和上面的映射连接了)
现在让我们开始研究第二个调用的内容吧:


这是一片很有意思的代码。ObReferenceObjectByName 是一个未文档化的内核导出函数,其声明格式如下:

NTSYSAPI NTSTATUS NTAPI ObReferenceObjectByName(
 
PUNICODE_STRING ObjectName,
 
ULONG Attributes,
 
PACCESS_STATE AccessState,
 
ACCESS_MASK DesiredAccess,
 
POBJECT_TYPE ObjectType,

KPROCESSOR_MODE AccessMode,
 
PVOID ParseContext OPTIONAL,
 
OUT PVOID* Object);
 
这个函数接受一个对象的名称作参数,然后返回一个指向对象结构的指针,同时修改引用计数加一,想要得到的对象的类型是由第五个参数说明的( POBJECT_TYPE )。在我们的实例中,它会是IoDriverObjectType
ObReferenceObjectByName 是一个很便利的函数,在rookit里已经广泛应用于盗取对象和在IRP的 Hooking中来调用函数了。对照实际情况,我们有发现一个盗取对象的意图。你可能还记得在我们的分析中早就发生了一个IRP 

Hook了。rootkit据此函数能够查找到一个驱动所对应的驱动对象结构(DRIVER_OBJECT)的内存指针,接着它就可以用来访问、检查、修改这个驱动的结构了。
来,让我们看一段没注释的内容。我们想给你展现WinDbg 加上-b选项时的输出,以及DRIVER_OBJECT的结构:

0:001> dt nt!_DRIVER_OBJECT -b
 
ntdll!_DRIVER_OBJECT
 
+0×000 Type : Int2B
 
+0×002 Size : Int2B
 
+0×004 DeviceObject : Ptr32
 
+0×008 Flags : Uint4B
 
+0x00c DriverStart : Ptr32
 
+0×010 DriverSize : Uint4B
 
+0×014 DriverSection : Ptr32
 
+0×018 DriverExtension : Ptr32
 
+0x01c DriverName : _UNICODE_STRING
 
+0×000 Length : Uint2B
 
+0×002 MaximumLength : Uint2B
 
+0×004 Buffer : Ptr32
 
+0×024 HardwareDatabase : Ptr32
 
+0×028 FastIoDispatch : Ptr32
 
+0x02c DriverInit : Ptr32
 
+0×030 DriverStartIo : Ptr32
 
+0×034 DriverUnload : Ptr32
 
+0×038 MajorFunction : Ptr32
 
这代码挺好理解的。从基地址加上一个额外的值,就可以指到想要的DRIVER_OBJECT元素,蓝色标出来的几项是rootkit替换了的。
要是看看\Driver\Disk的最后一项的话,我们可以对蓝色有一个更清晰的认识的(你可以通过一个进行中的调试活动来查看)。
在现在分析的函数最后面,它又调用了个ObfDereferenceObject,其目的是解引用一次驱动对象,前面对ObReferenceObjectByName的调用会修改引用计数。我们想要在这特地表示一下的是,ObDereferenceObject 中的"f",

这个"f",是指它是一个优化了的未文档化的版本,在对它的调用前面,我们并没有看到典型的函数调用入参过程。它是一个fastcall的调用约定的函数。
眼前让我们继续看下一个调用吧:


KeInitializeQueue 初始化一个队列对象(queue object),用来给线程按序等待和处理信条的。就在KeInitializeQueue其后,我们马上看到在一个对象解引用,还有一个调用PsCreateSystemThread, 创建运行在系统进程中

的系统线程,当然,这个函数它也返回一个线程句柄的。可以观察到创建调用的最后一个参数压入的是盗取的驱动对象的StartContext,这个参数是提供给线程开始执行时入口的单个参数。
这样我们就遇到了一个对线性代码执行流程的打断,我们需要在StartRoutine 下一个断点,以可以让调试器捕捉住这个系统线程里发生些了什么。

__系统线程分析__让我们请点出这个系统线程的代码做了些什么:


就像下面的DPC(Deferred Procedure Call),这系统线程是服务给网络传输的。

__系统线程分析的结束__

现在我们到了DriverEntry代码的最后一片了,一个IoAllocateWorkItem 被调用到了,这个函数申请一个工作项(work item),它的返回值是一个指向IO_WORKITEM 结构的指针。
一个驱动需要延迟执行的过程的话,它可以用工作项来实现,工作项包括一个驱动回调过程的指针,这可以一个执行先等待一个信号。驱动把工作项插入队列,其后一个做工作的系统线程把工作项从队列删除,线程还可以选

择性运行驱动的回调过程。系统也负责维护这些系统线程的线程池,具体是一个系统线程一次一个工作项的分配。
这块有趣的是,DPC的启动处理是需要一定长度的进程时间,或是进程进入一个阻塞型的调用才行,但它也是可以委派给一个拥有一个或多个工作项的系统线程的。当一个DPC在运行时,所有的线程都被从运行中阻断了。负责

处理工作项的系统工作线程运行在的IRQL==PASSIVE_LEVEL。所以在工作项的处理中也是可以包含阻塞型的函数调用的。例如,一个系统工作线程就可以等待一个调度用对象(dispatcher object,如KeWaitForSingleObject)
接着分析后面的,如果IoAllocateWorkItem 返回了NULL值(当没有足够资源了的时候这就会发生),执行会直接跳转到下面的IoCreateDriver,否则则会安装一个内核计时器(Kernel Timer),并且会调用起一个DPC。让我们来

看看具体的这儿的一段代码实际是什么。
KeInitializeTimer 填充KTIMER 结构,调用成功的KeInitializeDpc则创建一个自定义的DPC,最终KeSetTimerEx 设置在一个绝对或相对间隔后置位的时间对象。

BOOLEAN KeSetTimerEx(
 
__inout PKTIMER Timer,
 
__in LARGE_INTEGER DueTime,
 
__in LONG Period,
 
__in_opt PKDPC Dpc
 
);
 

实质性的,因为我们是存活了一个DPC,所以整段上述流程就因它而成了一个经典的CustomTimerDpc (自定义计时器的DPC)安装过程,这个DPC会在时间对象的间隔触发后执行。
对我们而言,这儿发生的事就是另一个对于线性代码执行流程的打断,来源是由设备驱动调用的KeInitializeDpc。DPC提供了打断进当前运行中的线程去执行的能力(在我们的实例中是计时器的触发),同时它的能力还有让一

个过程在IRQL==DISPATCH_LEVEL的执行。 DPC可以通过在KeInitializeDpc的参数DeferredRoutine 指针处设置一个断点,来让调试器可以跟踪到它。
__DPC流程的分析__这是DPC安装时设置的地址的核心指令:


我们需要继续查看由IoQueueWorkItem 的参数所指向的WorkerRoutine。先越过不必要的细节们,直接对WorkerRoutine进行简单阅览,过程中我们会发现RtlIpv4StringToAddressExA 函数。它的作用是转换一个字符串的IPv4

地址和端口数字到一个二进制化的IPv4地址和端口。借于IDA的名称窗口的CrossReferences(交叉引用)查看DPC的WorkerRountine,我们可以看到下列的字符串:
\Device\Tcp
 
\Device\Udp
 
db ‘GET /%s?m=%S HTTP/1.1‘,0Dh,0Ah
 
db ‘Host: %s‘,0Dh,0Ah
 
db ‘User-Agent: Opera/9.29 (Windows NT 5.1; U; en)‘,0Dh,0Ah
 
db ‘Connection: close‘,0Dh,0Ah
 
还有
db ‘GET /install/setup.php?m=%S HTTP/1.1‘,0Dh,0Ah
 
db ‘Host: %s‘,0Dh,0Ah

db ‘User-Agent: Opera/9.29 (Windows NT 5.1; U; en)‘,0Dh,0Ah
 
db ‘Connection: close‘,0Dh,0Ah
 
这DPC是实现的是从TDI(Transport Data Interface 数据传输层)访问网络的功能,这是立马可以明白的事实,因为它里面使用了TDI提供者 \Device\Tcp 和 \Device\Udp。这DPC是要下载来另一个病毒文件的:

\??\C2CAD972#4079#4fd3#A68D#AD34CC121074\

Vulnerabilities in the ZeroAccess Rootkit.
 每一个rootkit都有比其他的更强一些的部分。在我们的例子里ZeroAccess rootkit在文件系统上的功能非常出众。当逆向工程病毒到现在这个程度的时候,我们就发现了这个强劲结构里的一些可以利用的薄弱之处。表现就

是我们注意到了一些常见的感染了rootkit的特殊情况.
在这个驱动中最不隐蔽的部分是:
*系统线程
*内核计时器和DPC
*未定的原始系统模块
让我们从一个调查人员的角度来看看DPC注入。一个DPC的存在,除了一个简单的由KDPC结构定位出来的,保存着回调指针的LIST_ENTRY 结构外,就没别的了。这个结构是一个DEVICE_OBJECT结构的元素,所以一个简单的方法

是检索出这个驱动对象,并浏览进去定位到存在的注册了的DPC过程。为了进行这个检索,我们可以使用KernelDetective 工具,它真的是非常方便在内核进行鉴定调查的一个好工具。


DPC还和一个时间对象是相关联的,所以我们还可以枚举出所有的内核计时器:


正如你见到的,对应的计时器就是可疑的,因为它跟一个无名称的模块关联到一起了,时间隔周期也对应之前在截图上见过的一个。把DPC指向的代码往下滚动些,我们就确信了ZeroAccess的存在。


像你记得的,这个驱动还通过PsCreateSystemThread创建了一个系统线程。这个操作看起来是极其明显的,因为它创建的是一个系统进程里的对象。系统进程的地址空间是初始化为空的,里面也是拿来映射系统的,它还从系

统初始化进程上继承到了它的访问令牌和其他的属相,系统进程还特殊在创建时有的是一个空的句柄表。
所有的这些,都意味着查找一个rootkit时,你也可以把系统线程的情况作为检查的一部分。它的对象是结结实实的好找和好枚举;我们可以用Tuluka(http://www.tuluka.org/)工具来快速发现可疑的系统线程。


__DPC过程分析的结束__

最终的,在安装CustomTimerDpc的之后,我们前进到了整个驱动的最后一片代码,也即调用到IoCreateDriver 的地方。
这是另外一个未文档化的内核导出的函数:

NTSTATUS WINAPI IoCreateDriver(
 
UNICODE_STRING *name,
 
PDRIVER_INITIALIZE init ) ;
 
这个函数为一个内核中的没被载入成驱动的组件提供使它可以创建一个驱动对象的功能。如果这驱动对象的创建成功了,函数参数中的初始化函数就会被用和传递给DriverEntry的参数一样的调用到。
所以,我们接着转进新的DriverEntry 过程。
__New DriverEntry__

这儿是新的DriverEntry的代码:


通过ZwOpenDirectoryObject 打开了对象目录(object directory),随后申请了一块Pool内存,这块内存将被用在保持ZwQueryDirectoryObject的输出:


在这块代码中,rootkit在对象目录中循环,而且在每个迭代都组成一个下面格式的字符串:

\\device\\ide\\device_name

继而用IoGetDeviceObjectPointer从对象名称枚举出一个DEVICE_OBJECT的指针。凭其和元素的关系这个指针给了我们下几个元素:

DeviceObject = Object->DeviceObject;
 
drvObject = DeviceObject->DriverObject;
 
ObfReferenceObject(DeviceObject);
 
ObMakeTemporaryObject(DeviceObject);
 
ObfDereferenceObject(Object);
 
这儿,我们同时拥有设备对象(DeviceObject) 和驱动对象(DriverObject)了:


IoCreateDevice创建对应的设备对象(Device Object),随后验证是否DeviceObject->DeviceType 对应设备的类型是否为一个FILE_DEVICE_CONTROLLER。若是,会执行之前说过的对象盗取流程。
概况的说上面的代码,就是rootkit搜索过设备的堆栈,并把负责处理受害机器的IDE类型设备给选取出来。
IDE设备是由atapi驱动创建的。下面插图中的前二个设备是用给CD和硬盘的。后二个才是与Mini-Port驱动协同工作的控制器,这个也是为什么ZeroAccess要特别寻找FILE_DEVICE_CONTROLLER类型的设备(\idePort1和

\idePort0)


上面的代码说明,ZeroAccess不仅仅要在disk.sys中添加设备,还必须在atapi.sys的堆栈中添加设备来实现它的盗取功能,
让我们现在查看下设备树,并解剖出ZeroAccess的感染对驱动和设备树的改变:


我们拿到了ZeroAccess Rootkit感染的一部分最重大证据了,我们发现了二个Atapi DRV实例的存在,其中的一个还有未命名设备的存在。这个作风也是很宽泛的一批rootkit所有的。同时,这个输出也完全的跟上面分析的驱

动代码指令匹的工作流程配得上。
在第二个atapi.sys实例中,我们发现了一些不那么明显的rootkit踪迹。我们看到二个新的属于atapi驱动的设备:
* \PciIde0Channel1-1
* \PciIde0Channel0-0

这儿,我们遇到了另一个盗取对象的例子,也是为了文件系统的隐藏而作的IRP Hook,只是,这一次是基于\Device\PCI的。
而分析完它,就将完成我们第一个驱动的分析过程,在接下来的章节3中,我们来逆向分析内核模式的设备驱动注入 >>
上传的附件 ndiswan.rar