转载

利用Volatility进行Windows内存取证分析(二):内核对象、内核池学习小记

*本文原创作者:ExpLife,本文属FreeBuf原创奖励计划,未经许可禁止转载

简介

windows下内存取证的内容概括起来讲就是检测系统中有没有恶意程序,有没有隐藏进程,有没有隐藏文件,有没有隐藏服务,有没有隐藏的驱动,有没有隐藏网络端口等等.而进程,文件,驱动等等部件在内存中是以内核对象的形式独立存在的.而通常形形色色的rootkit会尝试修改操作系统的内部数据结构来达到隐藏自身的目的.安全研究员要想在内存取证中做到游刃有余,就需要对windows各种内存对象相关概念进行深入的了解.

windows执行体对象

大量的内存取证涉及到查找定位和分析执行体对象.Windows绝大多数代码是由C语言编写,其中大量使用了C语言的结构体来组织相关数据和属性.这里面有些结构体被称之为执行体对象,因为这些结构体是由Windows对象管理器(即由NT内核模块实现的一个内核组件)进行管理的(创建,保护,删除等等).严格意义上来说,只有当操作系统为一个结构体准备了用于管理诸如名称,访问控制,引用计数这些数据的各种头部的时候,该结构体才是真正意义上的执行体对象.换句话说,也就是所有的执行体对象都是结构体,但是并不是所有的结构体都是执行体对象.因为执行体对象是对象管理器分配并创建的,所以所有的执行体对象都有类似的特征.例如,所有的执行体对象都有前置头,而由其他子系统比如TCP/IP栈(tcpip.sys)分配的结构体就没有这些头.

表2-1中列举出了大部分与内存取证相关的执行体对象类型.我们应该逐一对这些对象类型进行熟悉.实际上,Volatility框架中至少有一个插件能够分析以下列表中列举出的每个对象.

表2-1:取证相关的Windows对象
对象名称                  结构                      描述
File                      _FILE_OBJECT              一个打开的文件的实例代表一个进程或者一个内核模块访问了这个文件,包括权限,存储文件内容和名称的那部分内存区域
Process                   _EPROCESS                 让所有的线程在一个私有的虚拟地址空间执行以及保存打开相应资源(譬如:文件,注册表键值等等)的句柄的一个容器
SymbolicLink              _OBJECT_SYMBOLIC_LINK     被创建用来支持别名,这些别名可以来用帮助映射网络共享路径,可移动媒体设备驱动信件
Token                     _TOKEN                    存储进程和线程的安全上下文信息(例如安全标识符[SID]和特权级别)
Thread                    _ETHREAD                  代表一个进程中可被调度执行的实体以及其CPU上下文的一个对象
Mutant                    _KMUTANT                  代表互斥体,典型地用来同步操作或者针对特定资源的访问控制的一个对象
WindowStation             tagWINDOWSTATION          针对进程,桌面,剪切板,原子表的一个安全边界
Desktop                   tagDESKTOP                代表可显示屏幕并且包含其用户对象,例如窗口,菜单,按钮的一个对象
Driver                    _DRIVER_OBJECT            代表一个已经加载的内核模式驱动程序的映像,并且包含该驱动的输入/输出控制处理例程指针
Key                       _CM_KEY_BODY              一个打开的注册表键的实例,并且包含该键值及其数据信息
Type                      _OBJECT_TYPE              通过元数据来描述所有其他对象常见属性的一个对象

我们可以使用微软官方的工具包SysInternals中的WinObj这款工具来查看指定版本的Windows中完整的执行体对象列表.

该工具包的下载地址: https://download.sysinternals.com/files/SysinternalsSuite.zip

对象头

所有的执行体对象之间一个最显著的特征的就是拥有一个对象头(_OBJECT_HEADER)以及零个或者多个可选的头.对象头在内存中位于执行体对象结构的前面.同样,可选头都是按固定的顺序排列在对象头的前面的.抽象的内存布局图2-1:

利用Volatility进行Windows内存取证分析(二):内核对象、内核池学习小记

图2-1

如图所示,如果给出了_OBJECT_HEADER地址的话,我们就能找到执行体对象结构(例如上图中的_FILE_OBJECT),反之亦然,很简单因为二者是直接紧挨着的;并且各个操作系统中_OBJECT_HEADER的大小都是一致的.可选头的存在与否依赖于对象头的InfoMask成员.我们首先一起来看一下64位Windows 7中对象头的完整结构.

>>> dt("_OBJECT_HEADER")
'_OBJECT_HEADER' (56 bytes)
0x0 : PointerCount ['long long']                                       包含了该对象的指针总数,包括内核模式引用的
0x8 : HandleCount ['long long']                                        包含该对象打开句柄的数量
0x8 : NextToFree ['pointer64', ['void']]
0x10 : Lock ['_EX_PUSH_LOCK']
0x18 : TypeIndex ['unsigned char']                                     该值告诉我们该对象的类型(例如,进程,线程,文件)
0x19 : TraceFlags ['unsigned char']
0x1a : InfoMask ['unsigned char']                                      该值告诉我们是否存在哪种可选头 
0x1b : Flags ['unsigned char']
0x20 : ObjectCreateInfo ['pointer64', ['_OBJECT_CREATE_INFORMATION']]
0x20 : QuotaBlockCharged ['pointer64', ['void']]
0x28 : SecurityDescriptor ['pointer64', ['void']]                      储存该对象安全限制的信息,例如,哪些用户可以对它以读,写,删除等方式进行访问
0x30 : Body ['_QUAD']                                                  该成员仅仅是一个占位符,代表包含在该对象中结构的起始位置

可选头

一个对象的可选头包含了用于描述该对象各种类型的元数据.很明显,因为它们是可选的,所以并不是所有的类型的对象都拥有可选头;甚至于相同类型对象的不同实例也可能包含可选头的不同组合.例如,内核并不会跟踪Idle和System进程的配额统计(资源的使用状况),所以这两个_EPROCESS对象就没有_OBJECT_HEADER_QUOTA_INFO头.此外,一个被多个进程共享的互斥体仅仅需要一个名称.因此,已命名的互斥体将拥有一个_OBJECT_HEADER_NAME_INFO头,而未命名的互斥体则没有.虽然很多可选头能够用于内存取证,但是安全研究员最常分析的还是名称头.

表2-2中列举出了64位Windows 7系统下可用的可选头.如果某个对象的_OBJECT_HEADER.InfoMask值被设置为了位掩码这一列的值,那么相应的可选头就会存在.由前面的描述我们可以得知,可选头位于_OBJECT_HEADER起始地址的负偏移处.精确的距离依赖于其他头的存在以及它们的大小(表2-2中的大小这一列).

表2-2:64位Windows 7中的可选头
名称                   结构                            位掩码                     大小(按字节)                 描述
Creator Info           _OBJECT_HEADER_CREATOR_INFO    0x1                        32                         储存着该对象创建者的信息
Name Info              _OBJECT_HEADER_NAME_INFO       0x2                        32                         储存该对象的名称
Handle Info            _OBJECT_HEADER_HANDLE_INFO     0x4                        16                         维护进程打开句柄相应数据的对象
Quota Info             _OBJECT_HEADER_QUOTA_INFO      0x8                        32                         跟踪使用情况和资源统计
Process Info           _OBJECT_HEADER_PROCESS_INFO    0x10                       16                         标识拥有进程

注意:从Windows 8和Windows Server 2012开始,引入了一个包含审计信息(_OBJECT_HEADER_AUDIT_INFO)新的可选头,位掩码为0×40.

关于各版本操作系统中对象头格式的变化可以参考下面文章:

http://www.codemachine.com/article_objectheader.html

对象类型对象

_OBJECT_HEADER的TypeIndex成员是nt!ObTypeIndexTable–(一个类型对象_OBJECT_TYPE的数组)的索引.从表2-1我们可以得知,类型对象包含了可以用来描述所有其他对象常见属性的元数据.该数据对于内存取证来说至关重要,因为我们可以用它来确定跟随在_OBJECT_HEADER后面对象的类型.例如,进程句柄表项指向了对象头.因此,当我们枚举句柄表中项的时候,跟随在对象头后面数据的类型是任意的-它可能是_FILE_OBJECT,_EPROCESS,或者其他的执行体对象.我们可以通过定位TypeIndex的值,然后找到该索引对应的_OBJECT_TYPE,然后得到其Name成员.对象类型的名称和结构名称的映射关系见表2-1:

64位Windows 7中的对象类型结构如下:

>>> dt("_OBJECT_TYPE")
'_OBJECT_TYPE' (208 bytes)
0x0 : TypeList ['_LIST_ENTRY']
0x10 : Name ['_UNICODE_STRING']                           对象类型名称的Unicode字符串(Process,File,Key等等)
0x20 : DefaultObject ['pointer64', ['void']]
0x28 : Index ['unsigned char']
0x2c : TotalNumberOfObjects ['unsigned long']             存在于该系统中的特定对象类型对象的总数
0x30 : TotalNumberOfHandles ['unsigned long']             此特定类型对象打开句柄的总数
0x34 : HighWaterNumberOfObjects ['unsigned long']
0x38 : HighWaterNumberOfHandles ['unsigned long']
0x40 : TypeInfo ['_OBJECT_TYPE_INITIALIZER']              一个_OBJECT_TYPE_INITIALIZER结构,告诉我们用于分配这些对象实例内存的类型(例如,分页还是非分页内存)
0xb0 : TypeLock ['_EX_PUSH_LOCK']                   
0xb8 : Key ['unsigned long']                              一个4字节的标签用于唯一标识分配包含特定类型对象的内存
0xc0 : CallbackList ['_LIST_ENTRY']                                           

TypeInfo和Key成员为内存取证提供了两条非常有价值的线索–它们可以告诉我们去分页还是非分页内存中寻找,然后通过特殊的4字节标记来定位特定对象的类型(例如,所有的进程或者所有的文件)的所有实例.下面我们通过为Volatility的volshell(类似于python的命令行,我们可以通过该插件提供的方法来实现特定的功能)插件编写一个简短的脚本来导出这些信息:

在编写这个脚本之前,我们首先进入volshell这个插件的控制台,然后输入hh()命令查看一下帮助文档:

root@kali:/opt/volatility# python vol.py -f demo.vmem --profile=Win7SP1x64 volshell
Volatility Foundation Volatility Framework 2.6
Current context: System @ 0xfffffa8000caf990, pid=4, ppid=0 DTB=0x187000
Welcome to volshell! Current memory image is:
file:///opt/volatility/demo.vmem
To get help, type 'hh()'
>>> hh()
Use addrspace() for Kernel/Virtual AS
Use addrspace().base for Physical AS
Use proc() to get the current process object
  and proc().get_process_address_space() for the current process AS
  and proc().get_load_modules() for the current process DLLs
addrspace()                              : Get the current kernel/virtual address space. 
cc(offset=None, pid=None, name=None, physical=False) : Change current shell context.
db(address, length=128, space=None)      : Print bytes as canonical hexdump.
dd(address, length=128, space=None)      : Print dwords at address.
dis(address, length=128, space=None, mode=None) : Disassemble code at a given address.
dq(address, length=128, space=None)      : Print qwords at address.
dt(objct, address=None, space=None, recursive=False, depth=0) : Describe an object or show type info.
find(needle, max=1, shift=0, skip=0, count=False, length=128) : 
getmods()                                : Generator for kernel modules (scripting).
getprocs()                               : Generator of process objects (scripting).
hh(cmd=None)                             : Get help on a command.
list_entry(head, objname, offset=-1, fieldname=None, forward=True, space=None) : Traverse a _LIST_ENTRY.
modules()                                : Print loaded modules in a table view.
proc()                                   : Get the current process object.
ps()                                     : Print active processes in a table view.
sc()                                     : Show the current context.
For help on a specific command, type 'hh(<command>)'
>>> 

我们来逐一看一下该插件提供的方法:

addrspace():用于获取当前内核或者虚拟地址空间
addrspace().base:用于获取当前物理地址空间
cc(offset=None, pid=None, name=None, physical=False):用于改变当前shell的上下文
db(address, length=128, space=None): 以16进制dump的形式打印该地址处的字节
dd(address, length=128, space=None): 打印该地址处的双字节
dis(address, length=128, space=None, mode=None): 打印该地址处的反汇编代码
dq(address, length=128, space=None): 打印该地址处的四字节
dt(objct, address=None, space=None, recursive=False, depth=0): 描述一个对象或者显示类型信息
getmods(): 内核模块生成器(脚本化)
getprocs(): 进程对象生成器(脚本化)
hh(cmd=None): 获取一个命令的帮助
list_entry(head, objname, offset=-1, fieldname=None, forward=True, space=None): 遍历一个_LIST_ENTRY
modules(): 以表格的视图打印已加载的模块
proc(): 获取当前进程对象
proc().get_process_address_space(): 获取当前进程的地址空间
proc().get_load_modules(): 获取当前进程已加载的DLL
ps(): 以表格的视图打印处于活动状态的进程
sc(): 显示当前上下文

遍历ObTypeIndexTable表获取各种类型对象信息的脚本如下:

kernel_space = addrspace()
ObTypeIndexTable = 0xfffff80004032300
ptrs = obj.Object(theType = "Array", targetType = "Pointer", offset = ObTypeIndexTable, count = 100, vm = kernel_space)
for i, ptr in enumerate(ptrs):
objtype = ptr.dereference_as("_OBJECT_TYPE")
if objtype.is_valid():
print i, str(objtype.Name), "in",str(objtype.TypeInfo.PoolType),"with key",str(objtype.Key)

该脚本需要重点说明的地方:

第二行,由于ObTypeIndexTable这个类型索引表在windows内核是以一个全局变量导出的,所以我们可以通过windbg双机调试,然后通过dd ObTypeIndexTable查看该表的首地址.笔者这里配置了实验虚拟机是64位的windows 7,而该表的首地址通过图2-2可知为0xfffff80004032300

利用Volatility进行Windows内存取证分析(二):内核对象、内核池学习小记

图2-2

然后第三行是构造一个首地址为ObTypeIndexTable,元素个数为100的指针数组对象.这个函数并不是volshell提供的,而是Volatility框架中obj包中实现的一个函数,如图2-3可知,Object这个函数是一个变参的函数,因为最后一个形参是**kwargs,这就表示后面的变参会组成一个字典传递进去.如果想知道可以其详细的调用方法可以查看一下该函数参考引用.如图2-4,可以看到该函数的各种调用例子,红色框框标注出来的正是我们想要的形式.

利用Volatility进行Windows内存取证分析(二):内核对象、内核池学习小记

图2-3

利用Volatility进行Windows内存取证分析(二):内核对象、内核池学习小记

图2-4

然后下面的for循环结合enumerate这个迭代函数实现对ptrs这个数组的遍历,dereference_as这个方法是将空指针类型解释为指向_OBJECT_TYPE的指针,这样我们就可以访问_OBJECT_TYPE中成员了,例如,对象类型名称,内核池的类型,以及4字节的标记.脚本执行得到的信息如下:

2 Type in NonPagedPool with key ObjT
3 Directory in PagedPool with key Dire
4 SymbolicLink in PagedPool with key Symb
5 Token in PagedPool with key Toke
6 Job in NonPagedPool with key Job 
7 Process in NonPagedPool with key Proc
8 Thread in NonPagedPool with key Thre
9 UserApcReserve in NonPagedPool with key User
10 IoCompletionReserve in NonPagedPool with key IoCo
11 DebugObject in NonPagedPool with key Debu
12 Event in NonPagedPool with key Even
13 EventPair in NonPagedPool with key Even
14 Mutant in NonPagedPool with key Muta
15 Callback in NonPagedPool with key Call
16 Semaphore in NonPagedPool with key Sema
17 Timer in NonPagedPool with key Time
18 Profile in NonPagedPool with key Prof
19 KeyedEvent in PagedPool with key Keye
20 WindowStation in NonPagedPool with key Wind
21 Desktop in NonPagedPool with key Desk
22 TpWorkerFactory in NonPagedPool with key TpWo
23 Adapter in NonPagedPool with key Adap
24 Controller in NonPagedPool with key Cont
25 Device in NonPagedPool with key Devi
26 Driver in NonPagedPool with key Driv
27 IoCompletion in NonPagedPool with key IoCo
28 File in NonPagedPool with key File
29 TmTm in NonPagedPool with key TmTm
30 TmTx in NonPagedPool with key TmTx
31 TmRm in NonPagedPool with key TmRm
32 TmEn in NonPagedPool with key TmEn
33 Section in PagedPool with key Sect
34 Session in NonPagedPool with key Sess
35 Key in PagedPool with key Key 
36 ALPC Port in NonPagedPool with key ALPC
37 PowerRequest in NonPagedPool with key Powe
38 WmiGuid in NonPagedPool with key WmiG
39 EtwRegistration in NonPagedPool with key EtwR
40 EtwConsumer in NonPagedPool with key EtwC
41 FilterConnectionPort in NonPagedPool with key Filt
42 FilterCommunicationPort in NonPagedPool with key Filt
43 PcwObject in PagedPool with key PcwO

我们这里用的是一个硬编码的地址,但是有可能大家在自己的机器下通过windbg进行双机调试的时候会发现输入x nt!ObTypeIndexTable或者dd nt!ObTypeIndexTable得到的值不一样.那么我们可以来试一试objtypescan这个插件,这个插件中并没有用到硬编码的地址,但是依然可以得到跟上面脚本类似的结果,如下:

root@kali:/opt/volatility# python vol.py -f demo.vmem --profile=Win7SP1x64 objtypescan
Volatility Foundation Volatility Framework 2.6
Offset                       nObjects           nHandles Key      Name                           PoolType            
------------------ ------------------ ------------------ -------- ------------------------------ --------------------
0x000000003eecd630                0x0                0x0 Filt     FilterCommunicationPort        NonPagedPool        
0x000000003f29f2d0               0x15               0x15 PcwO     PcwObject                      PagedPool           
0x000000003f5b6b80                0x0                0x0 Filt     FilterConnectionPort           NonPagedPool        
0x000000003fe813a0              0x5ff              0x555 EtwR     EtwRegistration                NonPagedPool        
0x000000003fe823a0                0x5                0x5 EtwC     EtwConsumer                    NonPagedPool        
0x000000003fe83900               0x36               0x35 Keye     KeyedEvent                     PagedPool           
0x000000003fe83a50                0x0                0x0 Prof     Profile                        NonPagedPool        
0x000000003fe91860               0x10                0x0 Call     Callback                       NonPagedPool        
0x000000003febb3f0              0x35b              0x361 Sema     Semaphore                      NonPagedPool        
0x000000003fed02d0               0x64               0x64 TpWo     TpWorkerFactory                NonPagedPool        
0x000000003fed0420                0xa               0x2a Desk     Desktop                        NonPagedPool        
0x000000003fed0570                0x6               0x47 Wind     WindowStation                  NonPagedPool        
0x000000003fed7630               0x9b               0x9b Time     Timer                          NonPagedPool        
0x000000003ff35900              0x388              0x383 Key      Key                            PagedPool           
0x000000003ff39d50              0x2d6              0x2d2 ALPC     ALPC Port                      NonPagedPool        
0x000000003ff3ef30                0x6                0x2 Powe     PowerRequest                   NonPagedPool        
0x000000003ff45080              0x117                0x5 Symb     SymbolicLink                   PagedPool           
0x000000003ff45730                0x1                0x1 IoCo     IoCompletionReserve            NonPagedPool        
0x000000003ff45880                0x0                0x0 User     UserApcReserve                 NonPagedPool        
0x000000003ff459d0              0x233              0x3f7 Thre     Thread                         NonPagedPool        
0x000000003ff45b20               0x2a               0xfd Proc     Process                        NonPagedPool        
0x000000003ff45c70                0x1                0x1 Job      Job                            NonPagedPool        
0x000000003ff45e90              0x3e2              0x20f Toke     Token                          PagedPool           
0x000000003ff46a50                0x0                0x0 Debu     DebugObject                    NonPagedPool        
0x000000003ff59860              0x198              0x1a4 Muta     Mutant                         NonPagedPool        
0x000000003ff5e570              0xe59              0xeba Even     Event                          NonPagedPool        
0x000000003ff5f570                0x0                0x0 Even     EventPair                      NonPagedPool        
0x000000003ff65600                0x0                0x0 TmTx     TmTx                           NonPagedPool        
0x000000003ff65750                0x7                0x7 TmTm     TmTm                           NonPagedPool        
0x000000003ff658a0              0xab7              0x30a File     File                           NonPagedPool        
0x000000003ff659f0               0x72               0x6e IoCo     IoCompletion                   NonPagedPool        
0x000000003ff65b40               0x76                0x0 Driv     Driver                         NonPagedPool        
0x000000003ff65c90              0x21f                0x0 Devi     Device                         NonPagedPool        
0x000000003ff65de0                0x2                0x0 Cont     Controller                     NonPagedPool        
0x000000003ff65f30               0x24                0x0 Adap     Adapter                        NonPagedPool        
0x000000003ff66660                0x2                0xf Sess     Session                        NonPagedPool        
0x000000003ff66c90              0x307              0x153 Sect     Section                        PagedPool           
0x000000003ff66de0                0x0                0x0 TmEn     TmEn                           NonPagedPool        
0x000000003ff66f30                0xe                0xe TmRm     TmRm                           NonPagedPool        
0x000000003ff7f320               0x16               0x16 WmiG     WmiGuid                        NonPagedPool        
0x000000003ffb7a00               0x28               0x63 Dire     Directory                      PagedPool           
0x000000003ffb7b50               0x2a                0x0 ObjT     Type                           NonPagedPool

左边对的这个地址是位于内核内存中的_OBJECT_TYPE结构的地址.我们可以发现该objtypescan这个插件打印的结果与我们通过volshell脚本遍历nt!ObTypeIndexTable得到的结果中包含的信息是相同的,但是顺序不太一样. 通过以上打印出来的信息,我们可以得知进程对象位于非分页内存中,令牌对象位于分页内存中.因此,我们可以推断出内存中会总是包含_EPROCESS对象,而有些_TOKEN对象可能会被交换到磁盘上面去.此外,我们尝试在内存中寻找实际的对象的时候就有一个潜在的特征码(Proc是标识进程的,Toke是标识令牌的)可以扫描了.当然,我们可能还需要比这四字节更加健壮的特征码,不然的话可能就会遭遇误报.为了减少误报,我们就需要来了解下一个概念-内核池的分配.

内核池分配

内核池是可以被分割为更小块的用于存储任何类型的数据–内核模式组件(NT内核模块,第三方设备驱动等等),请求的一系列内存.与堆比较相似,每个分配的块都有一个包含统计和调试信息的头(_POOL_HEADER),我们可以使用这些额外的数据来推断出该内存块属于哪个驱动程序,并且我们还可以在一定程序上推断出包含该分配内存的结构或者对象的类型.这不仅对解决内存泄漏和内存破坏的问题有帮助,而且对内存取证也是很有功效的.深入理解内核池的技术细节,不仅仅对内存取证有用,对于内核池的漏洞利用也是非常有帮助的.关于如何进行内核池方面的漏洞利用可以扩展阅读Tarjei Mandt的这篇文章-Kernel Pool Exploitation on Windows.7 by Tarjei Mandt: http://www.mista.nu/research/MANDT-kernelpool-PAPER.pdf

利用Volatility进行Windows内存取证分析(二):内核对象、内核池学习小记

图5 2-5 位于池中的执行体对象

64位Windows 7系统,内核池的头结构如下:

>>> dt("_POOL_HEADER")
 '_POOL_HEADER' (16 bytes)
0x0   : BlockSize     ['BitField', {'end_bit': 24, 'start_bit': 16, 'native_type': 'unsigned long'}]      分配总的大小,包含池头部,对象头部,以及其他可选的头部
0x0   : PoolIndex     ['BitField', {'end_bit': 16, 'start_bit': 8, 'native_type': 'unsigned long'}]
0x0   : PoolType      ['BitField', {'end_bit': 32, 'start_bit': 24, 'native_type': 'unsigned long'}]      系统内存的类型(分页,非分页的,等等)
0x0   : PreviousSize  ['BitField', {'end_bit': 8, 'start_bit': 0, 'native_type': 'unsigned long'}]
0x0   : Ulong1        ['unsigned long']
0x4   : PoolTag       ['unsigned long']   一个4字节的值,通常由ASCII码字符组成,唯一的表示该分配产生的代码路径.在Win8和Win2012中,其中一个字符可能会被修改为保护位
0x8   : AllocatorBackTraceIndex        ['unsigned short']
0x8   : ProcessBilled                  ['pointer64', ['_EPROCESS']]        
0xa   : PoolTagHash                    ['unsigned short']

内核池分配的API函数

在创建一个执行体对象实例之前,操作系统会从内核池中分配一块足够大的内存来存放对象和它的头部.通常在分配内核池的时候使用的最多的API函数是ExAllocatePoolWithTag,关于该函数的详细介绍请见MSDN: https://msdn.microsoft.com/en-us/library/windows/hardware/ff544520(v=vs.85).aspx ,函数原型如下:

PVOID ExAllocatePoolWithTag(
  _In_ POOL_TYPE PoolType,
  _In_ SIZE_T    NumberOfBytes,
  _In_ ULONG     Tag
);

PoolType这个参数指定了用于分配的系统内存的类型.NonPagedPool(0)和PagedPool(1)是枚举类型的值,分别表示非分页内存和分页内存.之前我们已经知道了,并不是所有的执行体对象类型都用的是非分页内存-我们可以通过定位特定对象的_OBJECT_TYPE.TypeInfo.PoolType成员来加以区分.这里还有一些其他的标志大家可能用来控制该内存是否可执行,是否缓存对其等等.更详细的信息请参考: http://msdn.microsoft.com/en-us/library/windows/hardware/ff559707(v=vs.85).aspx .

NumberOfBytes这个参数指定了要分配的字节数.驱动程序可以在调用ExAllocatePoolWithTag的时候直接指定需要存储在内存块中数据的大小.但是执行体对象就有点不同了,根据我们前面了解的知识,因为它们需要额外的空间来存储对象头以及可选头.位于内核中名称为ObCreateObject的这个函数是所有执行体对象被创建的关键点.它决定了被请求结构的大小(例如,64位Windows 7中_EPROCESS的大小为1232字节),并且在调用ExAllocatePoolWithTag之前_OBJECT_HEADER和其他可选头的大小会被加进去.

Tag参数指定了一个4字节的值,通常由ASCII码字符组成,唯一的表示该分配产生的代码路径.就执行体对象的这种情况来说,标志是从_OBJECT_TYPE.Key成员得到的-这就解释了为什么特定类型的所有对象的标志都一样.

假设某个进程想要使用Windows API函数来创建一个新的文件,将会按照以下步骤进行:

1.该进程调用CreateFileA(ASCII)或者CreateFileW(Unicode)-二者都是由kernel32.dll导出的.
2.创建文件的API转入ntdll.dll,然后在其中会通过系统调用进入内核,接着到达原生态的NtCreateFile函数处.
3.NtCreateFile将调用ObCreateObject来请求一个新的文件对象类型.
4.ObCreateObject计算_FILE_OBJECT的大小,包含需要为它的可选头额外准备的空间.
5.ObCreateObject找到文件对象对应的_OBJECT_TYPE结构,并且确实是分配分页内存还是非分页内存,还有需要用到的四字节标记.
6.指定合适的大小,内存类型,标记从而调用ExAllocatePollWithTag.

经过以上步骤以后,一个全新的_FILE_OBJECT就存在于内存中了,并且该分配的内存被指定的四字节值标记了.这还没完呢,还有一个指向对象头的指针会被添加到调用进程的句柄表中,并且一个系统范围内的池标记会在跟踪数据库中得到相应的更新,以及_FILE_OBJECT的各个成员会被该文件所创建的路径和要求的访问权限(例如,读,写,删除)初始化.

使用相同的大小,标记以及内存类型的顺序分配不一定在内存中就是连续排列的.虽然操作系统会尝试将类似大小的内存块组在一起.如果所请求的大小没有可利用的空闲块,内核就会从下一个最大尺寸的组中选择一个块.这样的结果就我们就会看到包含相同类型对象的内存分配被分散到了整个内核池中.此外,这允许小一点的结构占据之前用于存储更大一些结构的内存块—如果内存没有进行适当的清理的话,那么创建一个松散空间的条件就得不到满足.

更详细的信息,请参见Andreas Schuster的The Impact of Windows Pool AllocationStrategies on Memory Forensics: http://dfrws.org/2008/proceedings/p58-schuster.pdf

释放和重用

继续一个进程创建一个新文件的例子,_FILE_OBJECT相应的生命周期(例如,位于物理内存中的时间)依赖于多种因素.最重要的一个因素就是进程在读或者写完新的文件以后多久调用CloseHandle.在这个时间点,如果没有其他进程正在使用这个文件对象的话,这块内存将被释放回内核池的空闲链表中,而它可以基于不同的目的进行重分配.在等待被重分配或者在新数据被写入到内存块之间的这段时间,许多源_FILE_OBJECT依然是保存完整的.在此状态下的内存块会持续多长时间取决于系统的活动等级.如果机器正在猛烈抖动,并且正在被请求的内存块的大小小于等于_FILE_OBJECT的大小的话,它将很快被覆盖.除此之外,该对象可以在进程创建这个文件结束后持续存在几天或者几周.在过去,有童鞋经常会问:在一个网络连接关闭后,为了能够保留证据,我们必须在多久之内获取内存转储?答案是,它是不可预测的,每台机器都有可能不一样.这里重申一下,当池内存块被释放的时候,它们只是简单的被标记为了空闲,不会立即被覆盖.这个概念同样可以被应用于磁盘取证.当一个NTFS文件被删除的时候,仅仅只是主文件表(MFT)项被修改以反映状态的改变.而该文件的内容会保持不变一直到扇区被重新分配给新的文件并且发生写操作时.基于以上事实,我们在已经被操作系统释放的内存中找到执行体对象.也就是说,不仅仅是正在处于活动状态的执行体对象我们可以定位到,我们还可以找到过去存在的系统资源.

池标记扫描

池标记扫描或者简单点儿说池扫描,是指寻找基于前面提到的四字节标记分配的内存.例如,要查找进程对象,我们可以找到指向活动进程的双向链表起始地址的内核符号,然后遍历该链表中的项.另一种选择,池扫描,涉及到搜索整个内存dump文件中的Proc标记(与_EPROCESS相关的四字节标记).后面这种方式的好处在于我们可以找到历史项目(不再运行的进程或者已经结束运行的进程),还有可以挫败某些rootkit隐藏技术,例如,直接内核对象修改(DKOM),该技术依赖与修改活动对象的链表.

当我们执行池标记扫描的时候,四字节的标记只是我们的一个出发点.如果我们完全依赖于这四字节的标记,会得到大量的误报.所以,Volatility构建了一个更加强大的特征码.这是以我们前面介绍过的知识为基础的.例如,分配的大小以及内存的类型(分页,非分页)在减少误报中扮演了一个重要角色.如果我们正在寻找一个100字节的_EPROCESS,而我们在一个30字节的分配内存中找到了Proc,显然它不可能是一个真正的进程,因为该内存块太小了.

除了(标记,大小,内存类型)这些初始指标以外,Volatility池扫描器的基础设施还允许我们为每种对象类型添加自定义的约束条件.例如,如果一个进程创建的时间戳应该非零,我们可以以这个条件来配置扫描器.这样扫描器就只会报告创建时间非零的项了.

池标记源

表2-3列举出了Volatility通过池扫描用于寻找所列举执行体对象的初始指标.该表中的最小大小在添加_EPROCESS(针对进程),_OBJECT_HEADER和_POOL_HEADER的时候会用于计算.

表2-3: Volatility现有的插件里用到的池标记数据(针对从Windows XP 到 7)
对象                        标记                标记(受保护的)              最小大小(Win7 x64)                内存类型                   插件
Process(进程)               Proc               Pro/xe3                     1304                           (Nonpaged)非分页           psscan
Threads(线程)               Thrd               Thr/xe4                     1248                            (Nonpaged)非分页           thrdscan
Desktops(桌面)              Desk               Des/xeb                     296                            (Nonpaged)非分页           deskscan
Window Stations(窗口站)     Wind               Win/xe4                     224                            (Nonpaged)非分页           wndscan
Mutants(突变体)             Mute               Mut/xe5                     128                            (Nonpaged)非分页           mutantscan
File Objects(文件)          File              Fil/xe5                     288                             (Nonpaged)非分页           filescan
Drivers(驱动)               Driv              Dri/xf6                     408                             (Nonpaged)非分页           driverscan
Symbolic Links(符号链接)    Link              Lin/xeb                     104                             (Nonpaged)非分页          symlinkscan

虽然表2-3中的数据是通过查看Volatility的源代码得到的,但是理解从哪里可以获取这些原始信息也是非常重要的.例如,当一个新版本的Windows发布的时候我们需要调整某些域

或者创建一个现有的Volatility插件没有涵盖到的用于查找执行体对象的池扫描器.此外,如果一个恶意内核驱动程序分配内核池用以存储它的数据(配置信息,命令和控制数据包,需要隐藏的系统资源的名称等等),那么我们就需要一种方法来获取内存块中的这些指标.

还有,我们可能会注意到表2-3中的标记(受保护的)这一列.关于内核池标记最不常见的一个复杂特性就是受保护位.当我们通过ExFreePoolWithTag释放一个内核池的时候,我们必须设置跟ExAllocatePoolWithTag一样的标记.这项技术是操作系统用来防止驱动程序意外释放内存而设计的.如果传递给释放函数的标记不匹配,那么系统将抛出一个异常.这对与内存取证来说有很大的影响,因为这样的话我们就需要寻找受保护的内核池标记.

内核池标记中受保护的位并不是所有的内存分配都会设置,仅限与某些执行体对象类型.此外,从Windows 8和Windows Server 2012开始,貌似内核池标记中受保护位取消了.关于受保护的位更详细的信息可以参考这篇文章: http://msmvps.com/blogs/windrvr/archive/2007/06/15/tag-you-re-it.aspx

池标记文件

正如之前所提到的,微软创建池标记是为了调试和审计的目的.因此,Windows驱动开发包(DDK或者WDK)和Windows调试工具的安装目录中可以找到一个名为pooltag.txt的文件,这个文件我们可以用于查找.例如,给定一个池标记,我们可以确定该内存分配的目的以及拥有这块内存的内核驱动.因为该文件中包含有描述信息,所以我们也可以从诸如”进程对象”或者”文件对象”这样的关键字入手,进而找出其池标记.图2-6是从WDK 7600中找到的pooltag.txt文件中截取出来的部分内容:

利用Volatility进行Windows内存取证分析(二):内核对象、内核池学习小记

图2-6

高亮显示的这一行表示进程对象使用是Proc作为池标记,并且是有nt!ps(NT内核模块的进程子系统)分配的.现在我们知道了进程对象的池标记,我们还需要找到内存分配的合适大小以及内存的类型(分页,非分页).pooltag.txt文件仅包含微软的内核模式组件的池标记.它并不包含第三方或者恶意驱动程序的池标记.这里有一个在线的用于提交池标记描述信息的数据库: http://alter.org.ua/docs/win/pooltag, 注意这个网站上面的信息是匿名提交的,所以准确性得不到保证.

PoolMon工具

PoolMon( http://msdn.microsoft.com/en-us/library/windows/hardware/ff550442(v=vs.85).aspx )是微软的驱动开发包(DDK)中附带的一个内存池监视工具.它会实时更新当前运行系统的池标记以及以下一些信息:

* 内存类型(分页或者非分页)
* 分配的个数
* 释放的个数
* 分配占用的字节总数
* 每次分配的平均字节数

一个使用PooMon输出结果的示例如图2-7所示.-b开关会按数据的字节数进行排序,以至于内存比较密集的标记会先被列出来.

利用Volatility进行Windows内存取证分析(二):内核对象、内核池学习小记

图2-7

正如我们所看到的,CM31在字节数中排名最高.因为从系统启动开始,就有52383次调用ExAllocatePoolWithTag并设置CM31标记,它们其中2006个已经被释放了.当前分配的块中有50377个块不同,总共消耗237424640字节(接近226M)的内存.平均每次分配4712个字节.CM25这个标记在CM31后面不远处,用了大约12M内存.在标记名称中的CM代表配置管理器-这是一个用于管理Windows注册表的内核组件.因此,我们可以得出一个结论,至少有238MB的内存被预留用与存储注册表的键值和数据.不要想当然的从内存dump中提取出这所有的238MB内容,因为这些标记都是位于分页内存中的,它们其中的某些数据可能已经被交换到磁盘上去了.

另一个我们感兴趣的点就是关于File标记(这些内存被分配用以存储_FILE_OBJECT结构).正如PoolMon所显示的,自从上次系统重启已经有超过2000000块内存已经被创建了,这是合情合理的,因为每次文件被创建或者打开的时候就会有_FILE_OBJECT被分配.但是由于文件对象非常小,所以总共才消耗了5.2MB内存(平均每次分配396个字节).

通过阅读PoolMon的输出信息我们不仅了解了操作系统内部的工作原理,同时还对pooltag.txt的信息进行了补充.将二者结合起来,我们就有了池标记,描述信息,拥有者内核驱动,分配的大小,以及内存类型-我们用以扫描内存dump文件中各个分配实例所需要的一切信息现在都有了.

Windows内核调试器同样能够帮助我们确定相关的池标记.例如,!poolfind命令可以搜索我们指定的池标记并且诶告诉我们内存类型以及大小.用法如下所示

kd> !poolfind Proc
Searching NonPaged pool (fffffa8000c02000 : ffffffe000000000) for Tag: Proc
*fffffa8000c77000 size: 430 previous size: 0 (Allocated) Proc (Protected)
*fffffa8001346000 size: 430 previous size: 0 (Allocated) Proc (Protected)
*fffffa8001361000 size: 430 previous size: 0 (Allocated) Proc (Protected)
*fffffa800138f7a0 size: 430 previous size: 30 (Free) Pro.
*fffffa80013cb1e0 size: 430 previous size: c0 (Allocated) Proc (Protected)
*fffffa80013e4460 size: 430 previous size: f0 (Allocated) Proc (Protected)
*fffffa80014fd000 size: 430 previous size: 0 (Allocated) Proc (Protected)
*fffffa800153ebd0 size: 10 previous size: 70 (Free) Pro.
省略

为了简介以上输出只截取了一部分,在显示结果中,当前已分配的块有6个,还有2个被标记为释放了.已释放块中有一个的尺寸与已分配块的尺寸相同(430),而另一个却很小(10).因此,很可能位于0xfffffa800138f7a0地址处的已释放块包含了一个已结束的_EPROCESS;而位于0xfffffa800153ebd0地址处的块已经被另作它用了.

池跟踪器表

因为PoolMon主要是用于提供池标记统计的实时情况更新,所以它必须运行在一个处于运行状态的系统中.但是如果你只有内存dump怎么办?幸运的是,内存通常会包含PoolMon统计信息中的那些数据.

它们可以通过存储活动进程链表以及已加载模块链表的相同内核调试器数据块(_KDDEBUGGER_DATA64)来访问.尤其是,PoolTrackTable成员指向了一个_POOL_TRACKER_TABLE结构的数组-每一个都对应于正在使用中的唯一池标记.64位Windows 7下该结构的形式如下:

>>> dt("_POOL_TRACKER_TABLE")
'_POOL_TRACKER_TABLE' (40 bytes)
0x0 : Key ['long']                                                这就是4字节的标记
0x4 : NonPagedAllocs ['long']                                     非分页内存中分配了多少字节
0x8 : NonPagedFrees ['long']                                      非分页内存中释放了多少字节
0x10 : NonPagedBytes ['unsigned long long']                       非分页内存中总共消耗了多少字节 
0x18 : PagedAllocs ['unsigned long']                              分页内存中分配了多少字节
0x1c : PagedFrees ['unsigned long']                               分页内存中释放了多少字节
0x20 : PagedBytes ['unsigned long long']                          分页内存中总共消耗了多少字节

正如我们所看到的,每一个跟踪器表都有一个Key,也就是四字节的标记.剩下的成员告诉我们分页以及非分页内存各自的分配,释放,消耗的总字节数情况.虽然这些信息不是实时更新的(但是这依然是有实际意义的,因为系统已经不再运行了),我们至少可以确定刚获取内存dump时的状态.下面是一个使用pooltracker插件并且设置了几个执行体对象标记作为过滤选项的示例.以Np开头的列名表示非分页内存,以Pg开头的列名表示分页内存:

root@kali:/opt/volatility# python vol.py -f demo.vmem --profile=Win7SP1x64 pooltracker --tags=Proc,File,Driv,Thre
Volatility Foundation Volatility Framework 2.6
Tag    NpAllocs NpFrees  NpBytes  PgAllocs PgFrees  PgBytes  Driver               Reason
------ -------- -------- -------- -------- -------- -------- -------------------- ------
Thre       1322      759   717760        0        0        0                      
File      72717    69974   910480        0        0        0                      
Proc         63       21    55712        0        0        0                      
Driv        147        1    70736        0        0        0      

我们可以得知这所有的这4个标记都是位于非分页内存中的,因为只有(NpAllocs,NpFrees,NpBytes)这3列非零.为了确定每次大约分配了多大尺寸,我们可以用当前分配数除以总字节数.例如,对于Thre标记(线程对象)来说,平均大小为717760 / (1322 – 759) = 1 275字节.因此,想要找到所有包含线程的内存分配,我们需要在非分页内存中寻找至少占1275个字节,并且被Thre标记所标记的内存块.

这里有几个对于池跟踪器表非常重要的点:

* 过滤选项:如果我们运行pooltracker插件的时候没有指定--tags参数,那么将显示所有池标记的统计信息.
* 详细显示: 我们可以通过使用--tagfile选项来从pooltag.txt文件中集齐所有的数据,因此输出中会有带有描述信息的标签以及拥有者驱动.
* 支持的系统:Windows在XP和2003后面版本的系统才开始向池跟踪器表中写入统计信息.因此,pooltracker插件只能工作在Vista以上版本的操作系统.
* 限制:池跟踪器表仅仅记录统计信息;它并不记录所有带有特定标记的分配内存的地址信息.

下面我们来看一个利用池扫描来进行枚举内存中进程的一个插件psscan,关于该插件的是如何利用池标记扫描的原理请参见源代码.首先查看一下该插件的帮助.

root@kali:/opt/volatility# python vol.py -f demo.vmem --profile=Win7SP1x64 psscan --help
Volatility Foundation Volatility Framework 2.6
Usage: Volatility - A memory forensics analysis platform.
Options:
  -h, --help            list all available options and their default values.
                        Default values may be set in the configuration file
                        (/etc/volatilityrc)
  --conf-file=/root/.volatilityrc
                        User based configuration file
  -d, --debug           Debug volatility
  --plugins=PLUGINS     Additional plugin directories to use (colon separated)
  --info                Print information about all registered objects
  --cache-directory=/root/.cache/volatility
                        Directory where cache files are stored
  --cache               Use caching
  --tz=TZ               Sets the (Olson) timezone for displaying timestamps
                        using pytz (if installed) or tzset
  -f FILENAME, --filename=FILENAME
                        Filename to use when opening an image
  --profile=Win7SP1x64  Name of the profile to load (use --info to see a list
                        of supported profiles)
  -l file:///opt/volatility/demo.vmem, --location=file:///opt/volatility/demo.vmem
                        A URN location from which to load an address space
  -w, --write           Enable write support
  --dtb=DTB             DTB Address
  --shift=SHIFT         Mac KASLR shift address
  --output=text         Output in this format (support is module specific, see
                        the Module Output Options below)
  --output-file=OUTPUT_FILE
                        Write output in this file
  -v, --verbose         Verbose information
  -g KDBG, --kdbg=KDBG  Specify a KDBG virtual address (Note: for 64-bit
                        Windows 8 and above this is the address of
                        KdCopyDataBlock)
  --force               Force utilization of suspect profile
  --cookie=COOKIE       Specify the address of nt!ObHeaderCookie (valid for
                        Windows 10 only)
  -k KPCR, --kpcr=KPCR  Specify a specific KPCR address
  -V, --virtual         Scan virtual space instead of physical
  -W, --show-unallocated
                        Skip unallocated objects (e.g. 0xbad0b0b0)
  -A START, --start=START
                        The starting address to begin scanning
  -G LENGTH, --length=LENGTH
                        Length (in bytes) to scan from the starting address
Module Output Options: dot, greptext, html, json, sqlite, text, xlsx
---------------------------------
Module PSScan
---------------------------------
Pool scanner for process objects

我们来看一下该插件的最后四个选项的含义:

-V/--virtual:当扫描池分配的时候,我们可以使用虚拟地址空间或者物理地址空间.默认情况下,Volatility使用物理地址空间,因为它涵盖的内存会
尽可能的多-甚至是当前不在内核页表中的块.这可以确保从内存中的松散空间中恢复对象.设置-V/--virtual这个选项仅仅只会扫描当前内核已经映射
的活动分页.
-W/--show-unallocated:该选项控制插件是否显示已经被操作系统明确标记为未分配的对象.详情请参考Andreas Schuster的文章:http://computer.forensikblog.de/
en/2009/04/0xbad0b0b0.html
-A START, --start=START 和 -G LENGTH, --length=LENGTH:如果我们只想扫描指定范围的内存而不是所有内存,那么可以使用该选项来指定内存范围的起始地址和
长度.这个地址确定了物理或者虚拟内存(依赖与是否-V/--virtual标志设置了)的位置.

ppscan插件可用于扫描内存dump中进程列表,它有一个有意思的地方在于可以扫描已经结束的进程.这就可以引用于一个场景了:一个攻击人员已经获取了内网中的某台主机

shell权限,然后他想进一步扩大战果,势必要进行内网扫描,看看有哪些存活的主机,那么他就有可能用到ipconfig,ping这些命令.而安全研究员在获取到该主机的内存dump的时候,攻击人员的操作可能已经结束了.

这里笔者模拟的这种场景,将msf的反弹后门植入到测试用的Win7SP1x64系统中,然后通过Meterpreter的shell执行ipconfig查看受控主机网络适配器信息.然后对抓取到的内存dump执行如下枚举进程的命令:

root@kali:/opt/volatility# python vol.py -f demo.vmem --profile=Win7SP1x64 psscan
Volatility Foundation Volatility Framework 2.6
Offset(P)          Name                PID   PPID PDB                Time created                   Time exited                   
------------------ ---------------- ------ ------ ------------------ ------------------------------ ------------------------------
0x00000000065edb30 cmd.exe            3348   1624 0x000000003c852000 2017-01-09 03:23:45 UTC+0000   2017-01-09 03:24:01 UTC+0000  
0x0000000027684530 SearchProtocol     2588   2352 0x0000000027598000 2017-01-07 17:18:55 UTC+0000                                 
0x00000000285b16b0 WmiPrvSE.exe       2944    636 0x000000002782a000 2017-01-07 17:19:02 UTC+0000                                 
0x0000000029126060 svchost.exe        2756    520 0x0000000026e9f000 2017-01-07 17:18:56 UTC+0000                                 
0x0000000029b9d3b0 wmpnetwk.exe       2460    520 0x000000002981a000 2017-01-07 17:18:52 UTC+0000                                 
0x000000002a989b30 SearchIndexer.     2352    520 0x000000002a594000 2017-01-07 17:18:51 UTC+0000                                 
0x000000002b3feb30 SearchFilterHo     2608   2352 0x0000000029fa3000 2017-01-07 17:18:55 UTC+0000                                 
0x00000000306b6b30 VSSVC.exe          2276    520 0x000000002b70e000 2017-01-07 17:18:48 UTC+0000                                 
0x0000000030784060 vmtoolsd.exe       1652   1624 0x000000003233a000 2017-01-07 17:18:45 UTC+0000                                 
0x00000000325f7340 taskhost.exe       2480    520 0x000000001f36a000 2017-01-09 03:23:12 UTC+0000                                 
0x00000000337125c0 dllhost.exe         964    520 0x0000000027d00000 2017-01-07 17:18:43 UTC+0000                                 
0x00000000372319e0 dllhost.exe        1164    520 0x0000000036e79000 2017-01-07 17:18:42 UTC+0000                                 
0x000000003d601060 VGAuthService.     1312    520 0x00000000092d8000 2017-01-07 17:18:29 UTC+0000                                 
0x000000003d72e560 dwm.exe            1588    864 0x0000000038985000 2017-01-07 17:18:32 UTC+0000                                 
0x000000003d73f5f0 explorer.exe       1624   1580 0x000000003a6b8000 2017-01-07 17:18:33 UTC+0000                                 
0x000000003d80b8e0 svchost.exe         824    520 0x0000000010036000 2017-01-07 17:18:23 UTC+0000                                 
0x000000003d81fb30 svchost.exe         864    520 0x0000000010344000 2017-01-07 17:18:23 UTC+0000                                 
0x000000003d835b30 svchost.exe         888    520 0x0000000010149000 2017-01-07 17:18:23 UTC+0000                                 
0x000000003d85a060 msdtc.exe          1148    520 0x0000000031e87000 2017-01-07 17:18:44 UTC+0000                                 
0x000000003d86b060 audiodg.exe         976    824 0x000000000d99c000 2017-01-07 17:18:24 UTC+0000                                 
0x000000003d897290 svchost.exe         312    520 0x000000000ef55000 2017-01-07 17:18:25 UTC+0000                                 
0x000000003d8c1060 svchost.exe         532    520 0x000000000e95f000 2017-01-07 17:18:25 UTC+0000                                 
0x000000003d935930 spoolsv.exe        1072    520 0x000000000ca70000 2017-01-07 17:18:26 UTC+0000                                 
0x000000003d948100 svchost.exe        1100    520 0x000000000cb58000 2017-01-07 17:18:27 UTC+0000                                 
0x000000003d969aa0 WmiPrvSE.exe       2140    636 0x0000000030fcc000 2017-01-07 17:18:46 UTC+0000                                 
0x000000003d9dab30 taskhost.exe       1496    520 0x00000000078ef000 2017-01-07 17:18:32 UTC+0000                                 
0x000000003da812e0 winlogon.exe        456    408 0x000000000af80000 2017-01-07 17:18:11 UTC+0000                                 
0x000000003daa5860 vmtoolsd.exe       1428    520 0x0000000039f97000 2017-01-07 17:18:31 UTC+0000                                 
0x000000003dae0b30 lsass.exe           528    424 0x00000000130d1000 2017-01-07 17:18:13 UTC+0000                                 
0x000000003dae6b30 services.exe        520    424 0x000000001161d000 2017-01-07 17:18:13 UTC+0000                                 
0x000000003daecb30 lsm.exe             536    424 0x000000001315b000 2017-01-07 17:18:13 UTC+0000                                 
0x000000003db9c820 svchost.exe         636    520 0x0000000011363000 2017-01-07 17:18:20 UTC+0000                                 
0x000000003dba3b30 svchost.exe         724    520 0x000000000f18d000 2017-01-07 17:18:22 UTC+0000                                 
0x000000003dbbcb30 vmacthlp.exe        692    520 0x0000000010efe000 2017-01-07 17:18:21 UTC+0000                                 
0x000000003df5b5a0 WmiApSrv.exe       1048    520 0x000000001fca7000 2017-01-07 17:19:05 UTC+0000                                 
0x000000003df695c0 csrss.exe           416    408 0x0000000014ffa000 2017-01-07 17:18:10 UTC+0000                                 
0x000000003df6d420 wininit.exe         424    356 0x0000000013606000 2017-01-07 17:18:10 UTC+0000                                 
0x000000003e15ab30 csrss.exe           364    356 0x000000000c680000 2017-01-07 17:18:07 UTC+0000                                 
0x000000003f05b650 smss.exe            280      4 0x000000001a282000 2017-01-07 17:17:58 UTC+0000                                 
0x000000003f26bb30 svchost.exe        1224    520 0x0000000009d1e000 2017-01-07 17:18:28 UTC+0000                                 
0x000000003fc6bb30 conhost.exe        3532    364 0x000000000d0ff000 2017-01-09 03:24:11 UTC+0000   2017-01-09 03:24:11 UTC+0000  
0x000000003fc71b30 cmd.exe            3524   1428 0x000000000ab2e000 2017-01-09 03:24:11 UTC+0000   2017-01-09 03:24:11 UTC+0000  
0x000000003fe43060 ipconfig.exe       3544   3524 0x000000002b48b000 2017-01-09 03:24:11 UTC+0000   2017-01-09 03:24:11 UTC+0000  
0x000000003fe5e520 conhost.exe        3356    416 0x000000003c2e3000 2017-01-09 03:23:45 UTC+0000   2017-01-09 03:24:01 UTC+0000  
0x000000003ff2c990 System                4      0 0x0000000000187000 2017-01-07 17:17:57 UTC+0000 

输出中可以看到有5个进程带有退出时间,也就是说这些进程已经结束了.这里conhost这个进程我们不用关心,它是命令行程序宿主进程,一般来说有一个cmd就有一个conhost.那么剩下的就是cmd和ipconfig了.一般来说ipconfig.exe执行只需要几秒钟(或者时间更短),所以我们会感觉该命令在启动以后很快就退出了.如果我们遍历操作系统内部的活动进程列表,我们就看不到这个进程,因为它在我们获取内存dump之前已经结束了但是利用池标记扫描技术在物理地址空间中进行一番扫描,我们还是能够发掘攻击人员针对这台主机进行网络侦查的一些蛛丝马迹,因为使用到了ipconfig这个命令.这个示例是池标记扫描的最常见的例子之一.后面我们还会接触到运用池扫描技术来检测内核级别的rootkit.

池扫描的限制

未使用池标记的内存:ExAllocatePoolWithTag是微软推荐内核驱动以及内核模式组件用来分配内存的API函数,但是并不是唯一的选择.编写驱动的时候还可以使ExAllocatePool这个函数已经过时了,但是在很多版本的Windows中仍然可以使用.这个API函数在分配内存的时候并不带池标记-也就是说我们不太好跟踪或者扫描这种内存了.

误报:因为池扫描技术是依赖与匹配和启发式算法,所以误报是在所难免的.

大池内存分配:池标记扫描技术仅仅针对小于4096字节的内存块.幸运的是,所有的执行体对象的大小都小于4096.

任意的池标记:一个驱动在分配内存的时候用的最多的一个标记是”Ddk “.这个标记在整个操作系统中随处可见,并且第三方代码没有指定池标记的时候,操作系统也会帮我们加上.换句话说,如果一个恶意驱动使用了”Ddk “作为池标记,那么该内存块将会和其他内存块混合在一起.

伪造的池标记: 来自Walters&Petroni的一篇文章:( https://www.blackhat.com/presentations/bh-dc-07/Walters/Paper/bh-dc-07-Walters-WP.pdf )一个驱动伪造一个对象来误导安全研究院,从而拖延了研究员取证的时间.

修改池标记:因为池标记的引入是为了便于我们调试,它们的改变与否并不会影响操作系统的稳定性.所有有些内核级别的Rootkit会尝试修改池标记(或者_POOL_HEADER中的其他值,比如,内存块的大小,内存类型),这对于实际运行的系统来说并没有什么明显的差异,但是这种修改会影响Volatility池扫描器的正常运作.

由于池扫描的以上这些限制,所以后面我们还会接触到其他的几种进程扫描技术.

大页池

正如之前所提到的,Windows内核会尝试将相似大小的内存块组合在一起.但是,如果请求的大小超过了一个分页(4096字节),那么这块内存将会从一个特殊的池中(大页池)

进行分配.但是这种情况下,_POOL_HEADER,位于小一点的内存分配基地址处的四字节池标记这里压根就不存在.因为找不到池标记,所以池扫描器针对这种内存将会失败.

图2-8显示这两种内存块之间的差异.

利用Volatility进行Windows内存取证分析(二):内核对象、内核池学习小记

图2-8

正如我们所看到的,大页池并不存储_POOL_HEADER结构.为了研究这个结论,我们可以编写一个POC,这个POC就是一个挂钩ObCreateObject API函数并且递增用于存储_EPROCESS对象内存块大小的内核驱动程序.正如我们所猜测的那样,Volatility的ppscan插件无法找到新的进程,因为Proc标记不存在.此路行不通,我们换种思路,我们可以从图2-8中看出大页跟踪器表是直接指向大页池中的对象的.

大页跟踪器表

大页跟踪器表跟我们之前提到的池跟踪器表明显不同.池跟踪器表(_POOL_TRACKER_TABLE)是针对小内存块的,其中存储了内存分配的数量以及字节使用情况统计等信息;但是并没有告诉我们所有内存分配的地址(因此需要扫描).大页跟踪器表恰恰相反,不存储统计信息,但是其中包含了内存分配的地址.如果我们可以找到大页跟踪器表,它们就可以帮助我们找到位于内核内存中的大内存池块.不幸的是,指向_POOL_TRACKER_BIG_PAGES结构数组的内核符号nt!PoolBigPageTable既没有被导出也没有被拷贝到内核调试器数据块中.但是我们会发现这个符号与nt!PoolTrackTable(被拷贝到了内核调试器数据块中)有紧密的关系.因此,只要我们能找到池跟踪器表,我们就能很容易的找到大页跟踪器表.

以下是64位下的Windows 7中大页跟踪器表的结构:

>>> dt("_POOL_TRACKER_BIG_PAGES")
'_POOL_TRACKER_BIG_PAGES' (24 bytes)
0x0 : Va ['pointer64', ['void']]                               虚拟地址的缩写形式,指向了该内存分配的基地址
0x8 : Key ['unsigned long']                                    池标记,记住这个池标记跟Va指向的内存分配完全没有关系
0xc : PoolType ['unsigned long']                               内存类型(分页或者非分页)
0x10 : NumberOfBytes ['unsigned long long']                    该内存分配的大小

大池插件

我们可以使用bigpools来获取内存dump中由大内核池分配的内存块的信息.示例如下:

root@kali:/opt/volatility# python vol.py -f demo.vmem --profile=Win7SP1x64 bigpools
Volatility Foundation Volatility Framework 2.6
Allocation         Tag      PoolType                   NumberOfBytes
------------------ -------- -------------------------- -------------
-
0xfffff8a003747000 CM31     PagedPoolCacheAligned      0x1000L
0xfffff8a00f9a8001 CM31     PagedPoolCacheAligned      0x1000L
0xfffff8a004a4f000 CM31     PagedPoolCacheAligned      0x1000L
0xfffff8a00861d001 CM31     PagedPoolCacheAligned      0x1000L
0xfffffa8002fca000 Cont     NonPagedPool 0x1000L
0xfffff8a00a47a001 CM53     PagedPool 0x1000L
0xfffff8a00293c000 CMA?     PagedPool 0x1000L
0xfffff8a00324a000 CM25     PagedPool 0x1000L
省略

这些内存分配的列告诉了我们这些内存分配是从内核内存中的什么地址开始的.如果想查看这些内存范围中的内容的话,可以通过volshell插件转储这些地址处的数据.我们可以看到某些内存分配的起始地址是以1结尾的(0xfffff8a00f9a8001),并且位于分页内存中;但是我们可以通过研究统计得知,1意味着被标记为了释放.因此,我们尝试显示这些地址处的数据的时候,会发现它们并不是包含我们所期望的值(基于池标记).例如,比较几个CM31块:

root@kali:/opt/volatility# python vol.py -f demo.vmem --profile=Win7SP1x64 volshell
Volatility Foundation Volatility Framework 2.6
Current context: process System, pid=4, ppid=0 DTB=0x187000
Welcome to volshell! Current memory image is:
To get help, type 'hh()'
>>> db(0xfffff8a003747000, length=0x1000)
0xfffff8a003747000 6862 696e 00b0 1000 0010 0000 0000 0000 hbin............
0xfffff8a003747010 0000 0000 0000 0000 0000 0000 0000 0000 ................
0xfffff8a003747020 e0ff ffff 766b 0400 0400 0080 0100 0000 ....vk..........
0xfffff8a003747030 0400 0000 0100 0000 3134 3036 0000 0000 ........1406....
0xfffff8a003747040 e0ff ffff 766b 0400 0400 0080 0300 0000 ....vk..........
0xfffff8a003747050 0400 0000 0100 1000 3134 3039 90ae 1000 ........1409....
0xfffff8a003747060 e0ff ffff 766b 0400 0400 0080 0000 0000 ....vk..........
省略
>>> db(0xfffff8a00f9a8001, length=0x1000)
Memory unreadable at fffff8a00f9a8001
>>> db(0xfffff8a00861d001, length=0x1000)
0xfffff8a00861d001 0081 034d 6d53 7478 0025 0025 0042 00c0 ...MmStx.%.%.B..
0xfffff8a00861d011 180d 1a00 0000 00c0 0430 6b4c 0380 fac0 .........0kL....
0xfffff8a00861d021 0430 6b4c 0380 fac0 0430 6b4c 0380 fac0 .0kL.....0kL....
0xfffff8a00861d031 0430 6b4c 0380 fac0 0430 6b4c 0380 fac0 .0kL.....0kL....
0xfffff8a00861d041 0430 6b4c 0380 fac0 0430 6b4c 0380 fac0 .0kL.....0kL....
0xfffff8a00861d051 0430 6b4c 0380 fac0 0430 6b4c 0380 fac0 .0kL.....0kL....
省略

第一个地址(0xfffff8a003747000)在开头的地方包含hbin以及几个vk实例.我们之前提到过,CM31中的CM代表配置管理器,用于管理内核中的注册表组件,其中的.hbin和vk是注册表HBIN块以及相应值的特征码.第二个(0xfffff8a00f9a8001)和第三个(0xfffff8a00861d001)地址都被标记为了释放,但是这二者之间有明显的不同.其中一个是不可访问,很可能已经被置换到磁盘上去了-因为它们是位于分页内存中的.而另一个可能已经被重分配或者覆盖了,因为hbin特征码已经找不到了.

探索大页池

在一个正常系统中,会有成千上万的内存块会从大页池中分配,所以我们可以需要使用–tags选项来为该插件设置过滤规则(用逗号把标记分开).如果我们不进行过滤

的话,也可以将插件输出的重定向到一个文本文件中,然后基于标记出现的频率来进行排序.示例如下:

root@kali:/opt/volatility# python vol.py -f demo.vmem --profile=Win7SP1x64 bigpools > bigpools.txt
$ awk '{print $2}' bigpools.txt | sort | uniq -c | sort -rn
9009 CM31
3142 CM53
2034 CM25
1757 PfTt : Pf Translation tables
1291 Cont : Contiguous physical memory allocations for device drivers
940 MmSt : Mm section object prototype ptes
540 MmAc : Mm access log buffers
529 CMA?
442 MmRe : ASLR relocation blocks
432 CM16
237 Obtb : object tables via EX handle.c
105 Pp
99 SpSy
78 InPa : Inet Port Assignments
省略

以上池标记的描述信息不是命令自动生成的,是查找了pooltag.txt然后添加进去的.根据以上描述,我们会发现大页池中包含一些非常有意思的东西- 从转换表到内存管理器(Mm)页表项(PTE)以及访问日志,地址空间布局随机化(ASLR)的相关细节,句柄表(对象表)和Internet端口分配.要弄明白这些数据我们需要理解这些结构的格式以及其宿主驱动程序,但是了解了这些描述信息以及知道到哪里去找这些内存块也同样有助于我们对其中原理的深入研究.

分发头扫描

有几中执行体对象类型(例如进程,线程,互斥体)是可以进行同步操作的.这也就意味着其他线程可以同步或者等待这些对象的启动,完成,或者执行其他类型的动作.为了启用这一功能,内核会将关于该对象当前状态的信息存储到一个叫做_DISPATCHER_HEADER的子结构中,该结构位于执行体对象结构的非常靠前的地方(比方说,偏移零处).更加重要的一点是,该头部中包含了几个与特定版本Windows操作系统相关的值,所以我们可以为每个配置文件构建一个特征码用于查找.关于分发头在内存取证中的用途的更详细信息请参见Andreas Schuster的文章Searching for Processes and Threads in Microsoft Windows MemoryDumps ( http://www.dfrws.org/2006/proceedings/2-Schuster.pdf ).

以下是32位Windows XP SP2系统中的分发头结构:

>>> dt("_DISPATCHER_HEADER")
'_DISPATCHER_HEADER' (16 bytes)
0x0 : Type ['unsigned char']
0x1 : Absolute ['unsigned char']
0x2 : Size ['unsigned char']
0x3 : Inserted ['unsigned char']
0x4 : SignalState ['long']
0x8 : WaitListHead ['_LIST_ENTRY']

Andreas Schuster发现Absolute和Inserted这两个域在Windows 2000,XP,以及2003系统中总是为零.Type和Size这两个域是硬编码的值,分别指定了对象类型以及对象的大小.例如,在32位的Windows XP系统中,Type为3表示进程,并且Size为0x1b,并且以四字节/x03/x00/x1b/x00做了标记.跟池标记很相似,我们可以通过扫描内存dump寻找该特征码的实例从而找到所有_EPROCESS对象.早期的名为PTFinder( http://computer.forensikblog.de/en/2007/11/ptfinder-version-0305.html )的内存取证工具就是基于这个原理的.

分发头扫描技术的不利之处在于它只能帮我们扫描同步的对象.例如,文件对象不能同步,所以它们并没有内嵌_DISPATCHER_HEADER.因此,我们不能通过这种方式来定位_FILE_OBJECT实例.此外,从Windows 2003开始,_DISPATCHER_HEADER结构的成员已经被扩展到了10个而不是6个;并且Windows 7已经多达了30个.引入了这么多的成员,它为我们构建一个稳定的特征码带来了不确定性.

利用分发头扫描进程的例子我们可以在Volatility的源代码中找到,即contrib/plugins/pspdispscan.py这个文件.它仅仅是做为32位Windows XP上面的一个POC存在.

总结

Windows对象管理器在管理许多内核对象的时候起到了至关重要的作用.我们只有深入了解Windows对象管理器的内部机理,然后再配合强大的工具,在分析恶意代码的时候才能做到游刃有余.

由于命令较多,为了便于以后查找,这里将命令汇总了一下:

volshell控制台中的命令:

dt("_OBJECT_HEADER")

dt("_OBJECT_TYPE")

dt("_POOL_HEADER")

dt("_POOL_TRACKER_TABLE")

dt("_POOL_TRACKER_BIG_PAGES")

dt("_DISPATCHER_HEADER")

db(0xfffff8a003747000, length=0x1000)

db(0xfffff8a00f9a8001, length=0x1000)

db(0xfffff8a00861d001, length=0x1000)

Volatility框架的插件命令: python vol.py -f demo.vmem --profile=Win7SP1x64 volshell python vol.py -f demo.vmem --profile=Win7SP1x64 objtypescan

python vol.py -f demo.vmem --profile=Win7SP1x64 pooltracker --tags=Proc,File,Driv,Thre

python vol.py -f demo.vmem --profile=Win7SP1x64 bigpools > bigpools.txt python vol.py -f demo.vmem --profile=Win7SP1x64 psscan

windbg调试器的命令: kd> !poolfind Proc 内存取证常用命令: python vol.py -f demo.vmem --profile=Win7SP1x64 psscan -- 扫描进程列表用的,该插件基于内核池扫描技术,所以可以扫描出某些已经结束了的进程,明显攻击者在进行内网渗透时候执行的某些系统命令所对应的进程我们能够扫描出来.

参考链接

The Art of Memory Forensics

深入解析Windows操作系统

http://www.volatilityfoundation.org/

https://github.com/volatilityfoundation/volatility

*本文原创作者:ExpLife,本文属FreeBuf原创奖励计划,未经许可禁止转载

原文  http://www.freebuf.com/sectool/124800.html
正文到此结束
Loading...