内核对象有效性的判定

来源:互联网 发布:手机接收wifi端口 编辑:程序博客网 时间:2024/06/12 23:46

一. 内核对象


1.  进程对象(EPROCESS)

较简单的方法是判断EPROCESS中的ExitTime是否为0,如果是0则说明该进程正在运行。但是由于该值不会对进程的运行产生任何影响,如果rootkit会在此处填值,使判断失效。
根据《Windows 核心编程 第五版》 P231中的叙述,Windows中的进程内核对象中存在一个布尔变量,标示进程是否终止,使得WaitForSingleObject能够等待进程结束的时间通知。
 
[Windows XP sp2]
kd> dt -b _eprocess 820c8d50 
nt!_EPROCESS
   +0x000 Pcb              : _KPROCESS
      +0x000 Header           : _DISPATCHER_HEADER
         +0x000 Type             : 0x3 ''
         +0x001 Absolute         : 0 ''
         +0x002 Size             : 0x1b ''
         +0x003 Inserted         : 0 ''
         +0x004 SignalState      : 1
         +0x008 WaitListHead     : _LIST_ENTRY [ 0x820c8d58 - 0x820c8d58 ]
            +0x000 Flink            : 0x820c8d58 
            +0x004 Blink            : 0x820c8d58
 
可以看到SignalState正是上面说的布尔值。SignalState为TRUE表示此时该进程已被结束。
试验后发现,在windbg中手动修改SignalState为1后,用WaitForSingleObject等待进程结束的调用立即返回成功(WAIT_OBJECT_0),但该进程并未受影响。
通过记录进程结束前和进程结束后的EPROCESS结构,试图发现对无效进程内核对象的判别有用的结构成员。
使用UE附带的比较工具比较后,可能有用的成员如下
 
[Windows XP sp2]
结束前                                                         结束后
 
+0x004 SignalState      : 0                             +0x004 SignalState      : 1                       // 修改为1后,只影响WaitForSingalObject等API。
+0x060 StackCount       : 1                            +0x060 StackCount       : 0
+0x0a8 CommitCharge     : 0x1fa                     +0x0a8 CommitCharge     : 0
+0x0c4 ObjectTable      : 0xe20c78f8               +0x0c4 ObjectTable      : (null)                 // 修改为0后,会蓝屏。
+0x11c VadRoot          : 0x81bfd498                +0x11c VadRoot          : (null)                  // 修改为0后,会蓝屏。
+0x120 VadHint          : 0x81fa8860                 +0x120 VadHint          : (null)
+0x130 Win32Process     : 0xe20a8008             +0x130 Win32Process     : (null)
+0x138 SectionObject    : 0xe15ad810              +0x138 SectionObject    : (null)                // 修改后,不影响进程。
+0x1f8 Vm                                                   
> +0x008 Flags
> +0x000 AddressSpaceBeingDeleted : 0y0        +0x000 AddressSpaceBeingDeleted : 0y1
+0x248 ProcessExiting   : 0y0                         +0x248 ProcessExiting   : 0y1
+0x248 ProcessDelete    : 0y0                         +0x248 ProcessDelete    : 0y1
+0x248 VmDeleted        : 0y0                         +0x248 VmDeleted        : 0y1
 
假如进程退出了,但是其它进程仍然打开着此进程的句柄并未关闭,内核对象就不会销毁。反隐时应当将这种情况与隐藏进程的内核对象区别开来。通过上述不同点,可以看出进程运行比较重要的几个成员可以用于识别进程是否已经结束:
VadRoot
SectionObject
 
其他的不太确定,有待进一步研究。

2. 线程对象(ETHREAD)

[Windows XP sp2] 
结束前                                                         结束后
 
+0x004 SignalState      : 0                             +0x004 SignalState      : 1
+0x248 Terminated       : 0y0                         +0x248 Terminated       : 0y1
 
看到过一篇文章讲注入线程运行后,修改线程起始地址,造成该线程为正常线程的假象。这一点也需要想办法防范。这一点对于事后检查,尤其重要。

3. 文件对象(FILE_OBJECT)

 略

4. DLL

DLL比较特殊,姑且放到内核对象的讨论中吧。
[Windows XP sp2]
 _PEB  ->  _PEB_LDR_DATA  ->  _LDR_DATA_TABLE_ENTRY
 
这里的LIST_ENTRY也可以摘掉,如果再抹掉PE头,隐藏的dll就很难发现了。

5. 驱动对象(DRIVER_OBJECT)

在实际内存搜索DRIVER_OBJECT时,会发现正常系统中搜索到的对象与PsLoadedModuleList中的有一些差异,多几个同时少几个。
a. 少的是那些作为内核态DLL被加载进内存的模块。这些模块的入口点函数只有一个参数:注册表路径,并且这个参数的值所表明的注册表路径也未必真的存在,也许只是个形式而已。因此,这种特殊的模块不会具有自己的驱动对象,仅仅是一大段可以执行的代码。
b. 多出的几个对象为Windows的内建对象,由nt kernel、hal、win32k等创建。例如在Windows 2000中,nt kernel具有两个内建驱动对象:PnpManager和WMI,HAL的是ACPI_HAL,Win32k的是Win32k。这些内建驱动对象与普通驱动对象不同,Flags的值一般为4(DRVO_BUILTIN_DRIVER)。
如果要想将自身的驱动对象隐藏的难以发现,完全可以不用清零,只修改关键几个地方,例如将DriverSection置0,同时Flags改为4,以蒙混过关。但是要注意,内建驱动的Dispatch是有效的,并且指向的地址一定在那几个系统模块的地址范围内,而木马驱动如果使用Dispatch,则一定会指向自己模块的地址范围,或者为了隐藏可能不使用Dispatch,从而全都置0。所以,通过检查Dispatch,能够更精确的判断出异常的驱动对象。如果木马驱动更狡猾,在系统模块的指令缝隙中插跳转指令来实现Dispatch的代理,则可以通过使用符号文件直接判断Dispatch中的指针是否指向有效的函数地址,当然这种工作就要放到应用层了,可以从驱动传出相关信息由应用程序来分析。

从实现的角度出发,第一步需要达到的效果应当是能够通过检查驱动对象是否被清零判断异常驱动对象,能够通过检查DriverSection和Dispatch区分内建驱动对象和隐藏的驱动对象;第二步需要达到的效果是通过应用层检查Dispatch判断是否为内建驱动对象。

WRK中提供了DRIVER_OBJECT和LDR_DATA_TABLE_ENTRY的Flags取值定义:

//
// Define Driver Object (DRVO) flags
//
#define DRVO_UNLOAD_INVOKED             0x00000001
#define DRVO_LEGACY_DRIVER              0x00000002
#define DRVO_BUILTIN_DRIVER             0x00000004    // Driver objects for Hal, PnP Mgr
// end_wdm
#define DRVO_REINIT_REGISTERED          0x00000008
#define DRVO_INITIALIZED                0x00000010
#define DRVO_BOOTREINIT_REGISTERED      0x00000020
#define DRVO_LEGACY_RESOURCES           0x00000040
// end_ntddk end_nthal end_ntifs end_ntosp
#define DRVO_BASE_FILESYSTEM_DRIVER     0x00000080   // A driver that is at the bottom of the filesystem stack.
// begin_ntddk begin_nthal begin_ntifs begin_ntosp

6. 设备对象(DEVICE_OBJECT)

 略

7. 其他

 略

二. 内核Pool

1. Pool中的内核对象 

内核对象是由对象管理器调用ObCreateObject来创建的,这个函数中调用了ExAllocatePoolWithTag,参数Tag根据内核对象的类型而定。比如'corP'(进程Proc)、'erhT'(线程Thre)、'virD'(驱动Driv)、'iveD'(设备Devi)等等。
通过WinDbg的!poolfind命令可以找到这些标记的内存块,并能显示出状态:
[Windows XP sp2]
kd> !poolfind Driv
unable to get PoolTrackTable - pool tagging is likely disabled or you have the wrong symbols
unable to get large pool allocation table - either wrong symbols or pool tagging is disabled

Searching NonPaged pool (80fed000 : 82200000) for Tag: Driv

81c0c298 size:   f8 previous size:   18  (Allocated) Driv (Protected)
81c0f3b8 size:   f8 previous size:    8  (Allocated) Driv (Protected)
81c12218 size:    8 previous size:   40  (Free)      Dri.
81c124a8 size:  110 previous size:   38  (Free)      Dri.


第一列是个地址,对应的结构是_POOL_HEADER:
[Windows XP sp2]
kd> dt _pool_header 
nt!_POOL_HEADER
   +0x000 PreviousSize     : Pos 0, 9 Bits            // 这个值乘以8(内核对象都是8字节对齐)表示上次分配的pool的大小(字节数)。
   +0x000 PoolIndex        : Pos 9, 7 Bits             // 这个值代表本block在pool链表中的索引。
   +0x002 BlockSize        : Pos 0, 9 Bits             // 这个值乘以8(内核对象都是8字节对齐)表示本次分配的pool的大小(字节数)。
   +0x002 PoolType         : Pos 9, 7 Bits            // 表示本次分配的pool的类型,0表示这个pool已释放(如果是内核对象的pool,表明该内核对象不再有效),不为0时-1后与_POOL_TYPE对应。
   +0x000 Ulong1           : Uint4B                     // 上述总计4字节的联合。
   +0x004 ProcessBilled    : Ptr32 _EPROCESS     // 下面PoolTag的联合。
   +0x004 PoolTag          : Uint4B                    // 这个值就是pool的Tag。对于内核对象,这个值等于分配pool时的Tag|0x80000000(猜测这最高的一位表示"Protected"-见上述!poolfind的输出)。
   +0x004 AllocatorBackTraceIndex : Uint2B       // 联合
   +0x006 PoolTagHash      : Uint2B                 // 联合
 
[Windows XP sp2]
kd> dt _pool_type
nt!_POOL_TYPE
   NonPagedPool = 0
   PagedPool = 1
   NonPagedPoolMustSucceed = 2
   DontUseThisType = 3
   NonPagedPoolCacheAligned = 4
   PagedPoolCacheAligned = 5
   NonPagedPoolCacheAlignedMustS = 6
   MaxPoolType = 7
   NonPagedPoolSession = 32
   PagedPoolSession = 33
   NonPagedPoolMustSucceedSession = 34
   DontUseThisTypeSession = 35
   NonPagedPoolCacheAlignedSession = 36
   PagedPoolCacheAlignedSession = 37
   NonPagedPoolCacheAlignedMustSSession = 38
 
 
所以,我们可以得到一个在内存中查找隐藏对象的较精确的办法:直接查找_POOL_HEADER。有效性的判断可以通过查看_POOL_HEADER中PoolType的值是否为0,BlockSize是否为有效数值等办法实现。当然,这个结构中的成员被修改后会对进程产生什么影响还需要进一步试验才能得知。
 

2. Pool的范围

 NonPaged Pool 有两个,第一个从MmNonPagedPoolStart开始,大小为MmSizeOfNonPagedPoolInBytes,第二个从MmNonPagedPoolExpansionStart开始到MmNonPagedPoolEnd结束。
 
 

 三. 对策

有些结构成员仅仅是个标示,改了也不会影响功能和稳定。
1. 修改PoolType为其他None Paged Pool类型
2. 修改PoolTag为任意值
3. 修改内核对象第一个字节(Type)为任意值

原创粉丝点击