理解Inline Hook,HookApi通信

Inline hook通俗的说就是对函数执行流程进行修改,达到控制函数过滤操作的目的。理论上我们可以在函数任何地方把原来指令替换成我们的跳转指令,也确实有些人在inline的时候做的很深,来躲避inline 的检测,前提是必须对函数的流程和指令非常熟悉,且这种深层次的inlline 不具有通用性,稳定性也是问题。

Inline hook原理:解析函数开头的几条指令,把他们Copy到数组保存起来,然后用一个调用我们的函数的几条指令来替换,如果要执行原函数,则在我们函数处理完毕,再执行我们保存起来的开头几条指令,然后调回我们取指令之后的地址执行。用下图来解释:

wpsFE0C.tmp

整个Inline hook的过程就大体这样。

我们来说一个简单的实例可能会更好理解一些。

既然上面讲到了Inline Hook的原理,那我们就来实践一下,就拿应用层和驱动层的通信来实践一下吧。

前面不是说应用层和驱动层通信是通过DeviceIoControl传递控制码来实现通信的吗?

当然不止这一个方法啊,只要是带有输入输出参数的函数,我们在输入参数传入不同的标识,相当于IO_CTL宏一样的标识。然后在内核拦截这个函数,看看传入的是不是应用层发来的通信请求,如果是的话,那么我们进行请求处理,然后把结果copy到输出参数中,这样不就不实现了应用层与驱动层的通信了吗?

那我们应该怎么拦截呢?正好应用上面提到的Inline Hook的方法吧。选择哪个函数呢?就用ReadFile,这个函数传入文件句柄,返回读到的数据缓冲区。

首先通过函数名得到函数的地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
DWORD GetFunctionAddressBySsdt(WCHAR *zwFunctionName, DWORD index)
{
    UNICODE_STRING UnicodeFunctionName;
    ANSI_STRING AnsiFunction;
    char lpszFunction[100];

    RtlInitUnicodeString(&UnicodeFunctionName, zwFunctionName);
    RtlUnicodeStringToAnsiString(&AnsiFunction, &UnicodeFunctionName, TRUE);
    memset(lpszFunction, 0, sizeof(lpszFunction));

    strncpy(lpszFunction, AnsiFunction.Buffer, AnsiFunction.Length);

    if(!GetFunctionIndexByName(lpszFunction, &index))
    {
        RtlFreeAnsiString(&AnsiFunction);
        return 0;
    }

    RtlFreeAnsiString(&AnsiFunction);

    if(index <= KeServiceDescriptorTable->ntoskrnl.NumberOfService)
    {
        return KeServiceDescriptorTable->ntoskrnl.ServiceTableBase[index];
    }

    return 0;
}

解析函数开头的几条指令,把他们Copy到另一个地方保存起来,然后用一个调用我们的函数的几条指令来替换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
void UnHookFunctionHeader(WCHAR *FunctionName, BOOLEAN bSsdt,DWORD index,PVOID HookZone,int patchCodeLen)
{
    UNICODE_STRING uniFunctionName;
    DWORD oldFunctionAddress;

    if(bSsdt)
    {
        if(FunctionName != NULL)
        {
            oldFunctionAddress = GetFunctionAddressBySsdt(FunctionName, 0);
            if(0 == oldFunctionAddress)
            {
                return;
            }
        }else{
            oldFunctionAddress = GetFunctionAddressBySsdt(NULL, index);
            if(0 == oldFunctionAddress)
            {
                return;
            }
        }
    }else{
        RtlInitUnicodeString(&uniFunctionName, FunctionName);
        oldFunctionAddress = (DWORD)MmGetSystemRoutineAddress(&uniFunctionName);
        if(0 == oldFunctionAddress)
        {
            return;
        }
    }

    if(patchCodeLen > 0)
    {
        _asm
        {
            cli;
            mov     eax,cr0;
            and     eax, not 10000h;
            mov     cr0,eax;
        }

        memcpy((PVOID)oldFunctionAddress, (PVOID)HookZone, patchCodeLen);

        _asm
        {
            mov     eax,cr0;
            or      eax,10000h;
            mov     cr0,eax;
            sti;
        }
    }
}

则在我们函数处理完毕,再执行我们保存起来的开头几条指令,然后调回我们取指令之后的地址执行。前面我们把原始函数的前几条汇编指令保存在下面的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
__declspec(naked) NTSTATUS NtReadFileHookZone(,...)
{
    _asm
    {
        _emit 0x90;
        _emit 0x90;
        _emit 0x90;
        _emit 0x90;
        _emit 0x90;
        _emit 0x90;
        _emit 0x90;
        _emit 0x90;
        _emit 0x90;
        _emit 0x90;
        _emit 0x90;
        _emit 0x90;
        _emit 0x90;
        _emit 0x90;
        _emit 0x90;
        _emit 0x90;
        jmp [NtReadFileRet];
    }
}

_declspec(naked)

就是告诉编译器,在编译的时候,不要优化代码,通俗的说就是:没代码,完全要自己写

注意最后会跳到我们替换成jmp指令的几条指令的后一条。

下面是我们自己的定义的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
NTSTATUS __stdcall NewNtReadFile(
    _In_      HANDLE FileHandle,
    _In_opt_  HANDLE Event,
    _In_opt_  PIO_APC_ROUTINE ApcRoutine,
    _In_opt_  PVOID ApcContext,
    _Out_     PIO_STATUS_BLOCK IoStatusBlock,
    _Out_     PVOID Buffer,
    _In_      ULONG Length,
    _In_opt_  PLARGE_INTEGER ByteOffset,
    _In_opt_  PULONG Key
    )
{
    ULONG i;
    NTSTATUS status;
    ZWREADFILE OldZwReadFile;

    OldZwReadFile = (ZWREADFILE)NtReadFileHookZone;
    //这里进行我们自己的处理
return OldZwReadFile(FileHandle,
        Event,
        ApcRoutine,
        ApcContext,
        IoStatusBlock,
        Buffer,
        Length,
        ByteOffset,
        Key
        );
}

我们发现我们自己的处理函数又调用回了NtReadFileHookZone这个函数。

到此Inline Hook ReadFile就基本完成了。

通信的时候我们在应用定义唯一的句柄号,然后驱动层根据不同的自定义文件句柄来进行不同的处理。

我们使用WinDbg分析一下会更加清晰。

好吧,我们先来看看没进行inline hook前NtReadFile的反汇编代码:

image

可以看出NtReadFile的索引号我4Ch。

执行到这句:

*(DWORD*)&jmpCode[1] = NewFunctionAddress – (oldFunctionAddrss + 5);

这里是计算jmp后面的地址=目标地址-当前地址-5

让NtReadFileRet指向第一条未进行替换的汇编语句。

*lpRet = (PVOID)(oldFunctionAddrss + *patchCodeLen);

未拷贝前:

image未拷贝前和我们定义的是一样的,注意最后一句直接jmp到NtReadFileRet所指的地址,继续执行原函数的汇编语句。

memcpy((PVOID)HookZone, (PVOID)oldFunctionAddrss, *patchCodeLen);

然后我们把前面几条需要替换的汇编代码拷贝到NtReadFileHookZone。下面是拷贝后NtReadFileHookZone的反汇编。

image

注意看画框中的汇编语句,就是原NtReadFile的前两句汇编。

memset((PVOID)oldFunctionAddrss, 0x90, *patchCodeLen);

memcpy((PVOID)oldFunctionAddrss, jmpCode, 5);

然后我们把原NtReadFile前面的汇编语句给替换掉,直接jmp到我们自己实现的函数中。

我们再来看看原始的NtReadFile函数:

image直接跳到我们自定义的函数当中了。

然后可以看到上面在自定义函数NewNtReadFile中,在执行自己的处理,我们又调回NtReadFileHookZone部分执行了。

反汇编NewNtReadFile:

82240048 c745fc80012482  mov     dword ptr [ebp-4],offset Security!NtReadFileHookZone (82240180)

这里把NtReadFileHookZone 的地址赋给指针 dword ptr [ebp-4]

最后返回的时候,我们看到直接call  NtReadFileHookZone

82240164 ff55fc          call    dword ptr [ebp-4]

而在NtReadFileHookZone 中,我们执行完原NtReadFile的前两句汇编之后又jmp到了NtReadFileRet所指的地址。反汇编这个地址:

image

是不是继续接着原NtReadFile函数来执行的。

到现在应该明白inline hook其实就是改变原函数的执行流程这句话的含义了吧。

至于恢复inline hook,只要把修改的前几句汇编语句写回去即可。

本文链接:http://www.alonemonkey.com/understand-inlinehook.html