icesword 驱动部分分析

信息来源:驱动开发网(www
. zndev . com)
文章作者:wuyanfeng

icesword
. exe 在执行的时候会放出一个驱动程序 ispubdrv . sys  .
icesword . exe 装载 这个驱动,这个驱动安装后就不会卸载。直到系统重新启动。这可能是因为驱动中调用了
PsSetCreateThreadNotifyRoutine 函数
. 下面是这个函数在 ddk 中的介绍。

///////////////////////////////////////////////
PsSetCreateThreadNotifyRoutine registers a driver - supplied callback that is subsequently notified when a  new  thread is created  and  when such a thread is deleted .

NTSTATUS
PsSetCreateThreadNotifyRoutine
(
  
IN PCREATE_THREAD_NOTIFY_ROUTINE NotifyRoutine
  
);

Any driver that successfully registers such a callback must remain loaded until the system itself is shut down .

//////////////////////////////////////////////

虽然ddk 中说成功的调用PsSetCreateThreadNotifyRoutine 函数就要保留驱动直到系统重新启动,但是还是有办法做到可以卸载的。

icesword 是如何列出隐藏进程?

icesword 是通过 PspCidTable 这个表来遍厉进程的, PspCidTable 是一个没有被 ntoskrnl
. exe 导出的。这就涉及到如何定位
PspCidTable 的问题。icesword 是通过搜索特征串的方式定位 PspCidTalbe
PspCidTable 是一个 HANDLE_TALBE 结构 .
PsLookupProcessByProcessId 函数中会引用 PspCidTalbe 变量。icesword 从 PsLookupProcessByProcessId 函数的前几十个字节
内搜索 PspCidTalbe 变量。那有人就可能会想,那我把 PsLookupProcessByProcessId   这个函数给 patch 了
, 他不就找不到
PspCidTalbe 变量了吗? 对你说的没错,是可以这样。当然我们能想到这点。icesword 的作者也能想到这点。作者为了防止你这么做
也采取了相应的对策。他采取的对策就是运行前效验恢复的方法。它在执行关键的系统函数时会比较函数的头几十个字节是否被修改。
如果被修改了它就会把被修改的给恢复成系统原来的内容,那我们可能就会提出一个疑问,如果我在它启动之前就 patch 了他要效验
的函数,它怎么能知道系统原来的内容呢?这个问题提的好。现在就让我们来看看 icesword 的作者是怎么做到这一点的。我们就拿
PsLookupProcessByProcessId   函数来说吧。PsLookupProcessByProcessId   函数是 ntoskrnl
. exe 文件导出的。作者不是用我们通常
的方法来定位 PsLookupProcessByProcessId   函数的,也就是说 ispubdrv
. sys 并没有导入这个函数。同样也没有通过
MmGetSystemRoutineAddress 函数来得PsLookupProcessByProcessId的地址。那他是怎么获的 PsLookupProcessByProcessId 的地址的呢?
那可能有的人就会想到他是通过 自己打开ntoskrnl
. exe 文件然后分析导出函数做的,对icesword 的作者就是这么做的。当然他这里还是
有技巧在的。作者操作文件也没有用我们写驱动程序时常用的操作文件的方式来访问文件。我们平时在驱动程序里面打开和读写文件时
大多是使用 ZwCreateFile
, ZwOpenFile , ZwReadFile , ZwWriteFile , NtCreateFile 等等函数 . 这样的话作者就可以避免一些文件过滤程序。
作者打开文件用 IoCreateFile 函数。在读文件的时候作者没有用正常的 文件相关的 API 函数,而是用的 IofCallDriver 来做的。因为
本人对驱动不熟悉,也不清楚他 IoCallDriver 做什么用,只知道调用完了这个 IoCallDriver 函数后,数据就被读出来了。 这就防止了
通常的文件读写过滤程序。自己分析 pe 文件,找出他要定位的函数的导出地址。然后他会把函数的前 几十个字节读出来,当然这里又涉及
到代码重定位的问题。(熟悉 pe 的人可能都会理解重定位的问题的问题,这里我就不讲了。如果有不理解的可以自己参考 PE 文件格式的相关
文档。)作者自己把读出来的代码片段自己做重定为。这样他就得到了函数开头部分的原始代码。作者通过这种方法,就获得了原始的效验数据
。这样他运行系统函数的时候就保证函数没有被 patch 过。当然了如果你不怕麻烦的话可以把自己的 patch 放到更深的调用路径上。
这样即使是用 windbg 
softice , syser 调试器下断点调试,也是断不住的。当然了你也不能用调试器调试,因为 icesword . exe 会在一个
timer 中不停的重新设置 
int  1 , int  的中断处理函数。设置成 windows ntoskrnl . exe 中的缺省处理函数。即使你用硬件断点寄存器也是不
管用的。那有的人就会说既然设置成 windows ntoskrnl
. exe 中的缺省处理函数就可以使用 windbg 双机调试 . icesword 也做了处理 ,
icesword 会通过 KdDebuggerEnabled 变量判断是否允许内核调试。如果允许调试的话 icesword 会调用 KdDisableDebugger 函数禁止内核调试。



这里顺便在说两个分析 icesword 中遇到的反调试小陷阱 这里把代码片段列出来,希望作者原谅

. text : 000xxxF0    mov  [ ebp + IoControlCode ],  eax
. text : 000xxxF3    mov eax , [ esp + 5Ch - 6Ch ] ;  反调试代码
. text : 000xxxF7    push eax
. text : 000xxxF8    mov eax , [ esp + 60h - 6Ch ]
.
text : 000xxxFC    pop ebx
. text : 000xxxFD    cmp eax ebx
. text : 000xxxFF    jz  short  loc_1240B  如果没有被调试则会跳转
. text : 000xxx01    mov eax 200EDBh
. text : 000xxx06    not  eax
. text : 000xxx08    push eax
. text : 000xxx09    pop edi
. text : 000xxx0A    stosd

. text : 000xxxF3    mov eax , [ esp + 5Ch + 6Ch 当单步执行到这条指令或者在这条指令上设置断点的时候,因为当调试器在这条指令上弹出的时候会
用到被调试程序的堆栈来保存 EFLAGS
, CS , EIP (如果  int  1 , 或  int  处理函数用任务门就可以解决这个问题。)例如 当代码执行到这条指令时
ESP 
805E4320h    执行完这条指令是 eax 的值为  [ ESP + 5Ch - 6Ch ]=[ ESP - 10h ]=[ 805E4320h - 10h ]=[ 805E4310h 的值。
当单步执行到 
. text : 000xxxF8    mov eax , [ esp + 60h - 6Ch 指令的时候 ESP = 805E432Ch  以为其中入栈了一个 eax 所以 ESP = 805E432Ch ,
执行完  . text : 000xxxF8    mov eax , [ esp + 60h - 6Ch 条指令的时候 eax  = [ ESP + 60h - 6Ch ]=[ ESP - Ch ]=[ 805E432Ch - Ch ]=[ 805E4310h ]
如果不调试的情况下 读的是同一个地址的值,所以两个值比较应该是相同的 也就是  . text : 000xxxFD    cmp eax ebx 这条指令的比较结果
应该是相同的。这个指令 
. text : 000xxxFF    jz  short  loc_1240B 执行后直接跳转到。
如果是被调试器调试的情况下 
. text : 000xxxFF    jz  short  loc_1240B 不会跳转。 如果不跳转时下面的代码 会覆盖掉系统的当前 ETHREAD
指针。接下来在调用很多系统函数都会导致系统崩溃,并且是崩溃到系统模块里面,这样给你定位错误带来误导。哈哈



. text : 000xxx68    push  Alignment
. text : 000xxx6A    push  40h  Length
. text : 000xxx6C    push CurrentEProcessObject  Address
. text : 000xxx72    call ds : ProbeForRead

这里是故意做个异常来实现跳转。如果你在 
. text : 000xxx72    call ds : ProbeForRead 指令上单步执行的时候调试器会跑飞了,
也就是说从调试器退出了,没有继续跟踪下去。


接下来说我们的 PspCidTable 我们找到了 PspCidTable 变量后, PspCidTable 
[ 这个 HANDLE_TABLE 的句柄表中,保存着所有进程和线程对象的指针。
PID(进程ID)和 ThreadID(线程ID)就是在这个句柄表中的索引。这个 HANDLE_TABLE 不属于任何进程,也没有链在 HANDLE_TABLE 链上。全局变量
PspCidTable 中是指向这个 HANDLE_TABLE 的指针。这个 HANDLE_TABLE 还有一点和别的 HANDLE_TABLE 都不同,就是它的 HANDLE_TABLE_ENTRY 中的
第一个
32bit  放着的是对象体指针(当然需要转换)而不是对象头指针(对象指针就是对象体指针)。 ] ( 特别注明 在 [] 的话不是俺写的是在网上抄来的
这里特别感谢 “JIURL玩玩Win2k进程线程篇 HANDLE_TABLE” 文章的作者:JIURL 
)
我们之要想到办法遍历这个 PspCidTable 句柄表就可以遍历到系统的所有进程。icesword 为了遍历这个表他使用了系统为公开的 ntoskrnl . exe
的导出函数 ExEnumHandleTable 。

icesword 定位到 ntoskrnl
. exe 导出的 ExEnumHandleTable函数。
这个函数是未公开的函数。
这个函数的函数原形可能是 VOID STDCALL ExEnumHandleTable 
( PULONG HandleTable PVOID Callback PVOID Param PHANDLE Handle OPTIONAL );

其中的参数 PULONG HandleTable 就可以用 PspCidTable 做参数 .
PVOID Callback 的类型为  bool  (* EXENUMHANDLETABLECALLBACK )( HANDLE_TALBE_ENTRY *, DWORD PID , PVOID Param 函数指针。
PVOID Param 参数就是传送给回调函数的参数。
PHANDLE Handle OPTIONAL 这个参数俺还没搞懂什么意思。在说俺也用不到他,所以也不管他了随他去吧。

当调用 ExEnumHandleTable 函数的时候 函数在每次枚举到表中的一个句柄时都会调用一次回调函数。

当调用的 Callback 回调函数返回值为 
时继续枚举句柄表,如果返回  时则停止枚举。