开发Windows内核Hook

本文摘自《天书夜读:从汇编语言到Windows内核编程》第13章:开发Windows内核Hook

13.1 XP下Hook系统调用IoCallDriver
基础知识
Hook(钩子)的意义是将一些系统调用的过程替换成自己编写的函数,这样,系统中任何对该调用的调用都将被拦截到。编程者可以加入自己的处理,来改变或者监控系统的行为。

从这里开始,我希望用新的内核阅读能力做一些更有用的事情。很多安全软件用了系统内核调用Hook技术,当然威胁系统安全的软件也一样在使用着它们,这虽然是MS所不希望看到的,但却不是我们所可以不关心的。XP下几乎做任何已经导出的系统调用的Hook都非常容易,这正是Hook流行的原因。

IoCallDriver是一个非常重要的调用,在这里可以过滤到所有的系统请求。IoCallDriver的另一个常用的名字是IofCallDriver,几乎所有的内核驱动都调用了IofCallDriver。即使你在开发中写下IoCallDriver,编译后的代码中也只能发现IofCallDriver这个符号。

技术原理
IofCallDriver的反汇编非常简单。

; Exported entry 42. IofCallDriver
; LONG __cdecl IofCallDriver(PDEVICE_OBJECT DeviceObject, PIRP Irp)
public @IofCallDriver@8
@IofCallDriver@8 proc near
jmp ds:_pIofCallDriver
@IofCallDriver@8 endp
几乎所有的系统调用都如此:进入之后,立刻出现一个jmp …,跳到真实的调用处,这形成了一个系统调用跳转表。这真是一个糟糕的地方,因为只要修改这个地址,一切系统调用的Hook都是简单轻松的了。

特殊提示
不过初学者可能会产生一些错误的想法:既然是jmp ds:_pIofCallDriver,那么我只要把符号_pIofCallDriver的值修改掉,一切不就OK了吗?

任何符号(函数名和变量名)对机器而言都不存在,是WinDbg或其他工具根据符号表显示给人类看的,只能看不能改。

但是读者看见的符号,在机器码中并非符号,而是已经编译得出的数字。假设_pIofCallDriver = 8099c000h,那么这条指令本质上是:

jmp ds: 8099c000h
这个值可以被修改,但是_pIofCallDriver这个符号无法被修改。假设其他地方也调用了这个符号的话,在这里对值的修改,并不影响其他地方对这个符号的调用,那些地方依然是8099c000h。

这样一来,如果我们想修改符号的值,似乎唯一的办法是对8099c000h进行全面查找替换。但是不幸的是,除了_pIofCallDriver这个符号之外,其他的数据也可能为这个值。所以结论为:符号的值,不可以修改。

示例代码
下面开始考虑修改jmp ds: _pIofCallDriver,在32位下,jmp后面直接就是32位地址。回顾一下前面看过的反汇编引擎的技术,很容易想到第一个字节是前缀,第二个字节是jmp这个操作码,后面4个字节就是32位的跳转地址。当前要修改它还有一个前提,就是我能找到这个指令本身所在。用MmGetSystemRoutineAddress可以得到IofCallDriver的地址,而这个jmp正是第一条指令。那么写法如下:

// 首先定义一个函数指针类型
typdef NTSTATUS FASTCALL (*PMY_IOFCALLDIVER_FP)( IN PDEVICE_OBJECT,IN OUT PIRP);
// 以下函数用函数newIofCallDriver去代替现有的IofCallDriver
// 同时返回旧的IofCallDriver所跳转的实际地址,如果失败,则返
// 回空
PMY_IOFCALLDIVER_F MyHookIofCallDriverXP(
IN PMY_IOFCALLDIVER_F newIofCallDriver,
IN BOOLEAN hookOrUnhook)
{
UNICODE_STRING functionName;
PBYTE address;
static PMY_IOFCALLDIVER_F oldIofCallDriverBody = NULL;
RtlInitUnicodeString( &functionName, L"IofCallDriver" );

// 得到IofCallDriver的入口地址
address = MmGetSystemRoutineAddress( &functionName );
if(address == NULL)
    return NULL;
if(hookOrUnhook)
{
    // 获得入口地址后,加2字节的地址就是旧的
    // IofCallDriver的执行体的地址
    oldIofCallDriverBody =
        (PMY_IOFCALLDIVER_F)(*(PLONG)(address + 2));
    InterlockedExchange((PLONG)(address + 2),
        newIofCallDriver);
    return oldIofCallDriverBody;
    }
    else
{
    // 复原
    if(oldIofCallDriverBody == NULL)
    {
        return NULL;
        InterlockedExchange((PLONG)(address + 2),
            oldIofCallDriverBody);
            return oldIofCallDriverBody;
    }
}

以上函数不是线程安全的,应该避免多线程同时调用它,调用后新的IofCallDriver将调用你设定的函数,同时你得到了旧的IofCallDriver的实现体指针。新的函数调用完后,读者可以选择继续调用旧的或者不再调用而直接结束,从而形成Hook。

发表评论