聚星亭

吾笨笨且懒散兮 急须改之而奋进
posts - 74, comments - 166, trackbacks - 0, articles - 0
  C++博客 :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理

[转载] inline hook 怎样才安全?

Posted on 2009-03-30 17:33 besterChen 阅读(1309) 评论(0)  编辑 收藏 引用 所属分类: 软件安全中的JJXX

         说明:本文转载于 : iceboy @ baidu.hi         

         很久很久以前, 电脑一般是单核的, 即电脑上一般只有一个处理器. 这样, 如果我们要修改一段内核代码, 似乎 IoCreateMdl(), MmBuildMdlForNonPagedPool() / MmLockAndProbePages() 然后 KeRaiseIrqlToDpcLevel() 以后, 就可以很安全地改写内存的数据. 因为系统只有一个 CPU, 这个 CPU 在 Irql >= DPC_LEVEL 的时候不会被调度, 因此在改写代码期间 eip 不可能指向被 hook 的代码, 但是到了多处理器电脑上这一切都变了.

         当一个 cpu 在 DPC_LEVEL 的时候, 它是不会被调度的. 问题是, 系统中有不止一个 cpu, 其它的 cpu 还是想干啥干啥. 如果用上述方法, 其它 cpu 很有可能在你代码修改到一半的时候去执行, 就会造成系统崩溃, 蓝屏重启.

         我们可以做一个实验:

#include <ntddk.h>

int a;

VOID XXThread(PVOID XXContext)
{
DbgPrint(
"Thread created.\n");
while (1) {
   
if (a == 1) DbgPrint("Irql=%d, Processor=%d", KeGetCurrentIrql(), KeGetCurrentProcessorNumber());
   
else if (a == 2) {
    DbgPrint(
"Thread terminated.\n");
    PsTerminateSystemThread(
0);
   }
}
}

NTSTATUS DriverEntry(PDRIVER_OBJECT XXObject, PUNICODE_STRING XXPath)
{
HANDLE hThread;
KIRQL OldIrql;
ULONG i;

= 0;
PsCreateSystemThread(
&hThread, , XXThread, 0);

OldIrql 
= KeRaiseIrqlToDpcLevel();
DbgPrint(
"Irql=%d, Processor=%d", KeGetCurrentIrql(), KeGetCurrentProcessorNumber());
= 1;
for (i = 0; i < 100..0; i++) __asm nop; // wait for some time
= 2;
KeLowerIrql(OldIrql);

// wait for hThread and close it

return STATUS_UNSUCCESSFUL;
}


         这个地方没有装 DDK, 程序是随便写的, 大意即是如此. 我以前做这个实验时写过一个类似的程序, 并证明了 Irql 是 CPU 相关的这一事实. 也就是说, 在多处理器系统上, 上述 inline hook 的方法不再安全. 然而, 越来越多的电脑使用双核, 甚至三核、四核的处理器, 我们如果不注意这个问题, 就是对用户不负责任. (PsNull3 中使用 CreateMdl + ProbeAndLockPages + RaiseIrql + cli + WPOFF 大杂烩方法, 实际上仍然可以证明不安全)

         这个问题该怎么解决呢? 我也没有成熟的想法. 问题的本质在于在我们修改一段代码的前后, 这段代码中的任何语句都不应该被执行. 于是有以下思路 (我们按照双核处理器电脑讨论):

         1. inline hook 一般是 5 个字节, 总之一般不超过 8 个字节, 我们可以找一条能够一次操作 64bit 的指令. 问题是, 真的存在这样的指令吗? (lock xxx, movq xxx, mmx) 不知道 cpu 执行它的时候, 是一次完成的呢, 还是用了一段微程序分步完成. (我们的 cpu 是 32bit 的~) 即使存在这样的指令, 也不安全. 假设被 hook 的指令是这样的:

 

mov      edi, edi
push     ebp
mov      ebp, esp

 

         这是一个典型的函数开头. 我们假设 cpu0 执行到 push ebp 这一条指令 (eip = $+0x2). 这个时候 cpu1 执行传说中的 move_qword 指令:

 

movq [$+0x0], mm0

 

         其中 mm0 中包含这样的指令: jmp 12345678, 其机器码是 E9 AA BB CC DD.

         问题出现了. 当 cpu1 执行完这一条指令的时候, cpu0 的 eip 指向如下字节流: BB CC DD XX XX XX. 结果显然是系统崩溃.


2. 有了以上的经验和教训, 我们很自然地想到, 在修改代码的时候, 霸占所有的 cpu. 好在我们有以下导出符号帮忙:

KeNumberProcessors
KeGetCurrentProcessorNumber()
KeInitializeDpc()
KeSetTargetProcessorDpc()
KeInsertQueueDpc()
KeWaitForSingleObject()

 

         我们可以先 Raise Irql, 然后 Get Current Processor Number, 然后向所有非本 Processor Number 的 Processors 注入 Dpc, 让它 Stall Execution 或者 Wait For XXX Event (我错了...). 一切似乎都很美好?

神奇的MJ0011(3537xxxxx) 20:02:20
DPC下没法WAIT

         以上代码有严重问题, 可能导致系统死锁. 假设有两个程序几乎同时试图霸占整个系统, 它们同时先后执行了 KeRaiseIrqlToXXX(), 这时两个 cpu 都处于 IRQL = DPC_LEVEL 的状态, 并且都试图注入 DPC 到另一个 cpu 中, 糟糕的是, 它们都等待这个 DPC 创建完毕. 然而, 两个 DPC 都只有在 KeLowerIrql() 之后才开始执行. 它们就这样一直等啊等...

         KeStallExecutionProcessor() 或许可以在一定程度上解决这个问题, 但是仍不完美.

神奇的MJ0011(3537xxxxx) 20:11:50
是我的DPC等待我现在的CPU
神奇的MJ0011(3537xxxxx) 20:11:56
不是我的CPU等待我投递的DPC

         我又错了... 再思考思考~

3. 很多函数的代码开头都有 mov edi, edi 这一行, 按照某传说的说法, 这是微软大发慈悲, 让编译器在函数首部加入这样一行指令. 并在这一指令前, 上一个函数后, 留下 5 个 nop. 这就可以让我们有充分的时间在前面 5 个 nop 中写入一个跳转指令, 并使用一条原子指令将 mov edi, edi 改成 jmp $-0x5 (也是两个字节).

         这个方法也有不少问题. 首先, 不是所有的函数开头都有这一行指令, 比如 KiFastCallEntry 似乎就没有. 其次, 使用这个方法需要大家都遵循一个规范, 即 hook 函数的开头也必须是 nop nop nop nop nop mov edi, edi, 并且在恢复 hook 的时候需要把下一个 hook 接上去, 这又涉及到计算偏移等问题. 很多程序作者都是自私的, 不考虑钩子的共存问题, 或者说, 它们也没有能力使用好函数头的这一条语句. 据我所知, KV 的程序员在这一点做得很好, 如果函数头有 mov edi, edi, 它会从下一条指令开始 hook. 而 KAV 的程序员则是不顾一切地在函数头写上一个 jmp. 无奈的是, 总有人在函数头写上 jmp, 这样, 这个方法就不能用了. 否则用户会说: 某某程序和某某大公司的某某软件不兼容, 肯定是这个程序作者的问题.

------

         那么, 有没有更好的方法呢? 欢迎大家讨论.


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   博问   Chat2DB   管理