Inline hook通俗的说就是对函数执行流程进行修改,达到控制函数过滤操作的目的。理论上我们可以在函数任何地方把原来指令替换成我们的跳转指令,也确实有些人在inline的时候做的很深,来躲避inline 的检测,前提是必须对函数的流程和指令非常熟悉,且这种深层次的inlline 不具有通用性,稳定性也是问题。
Inline hook原理:解析函数开头的几条指令,把他们Copy到数组保存起来,然后用一个调用我们的函数的几条指令来替换,如果要执行原函数,则在我们函数处理完毕,再执行我们保存起来的开头几条指令,然后调回我们取指令之后的地址执行。用下图来解释:
整个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的反汇编代码:
可以看出NtReadFile的索引号我4Ch。
执行到这句:
*(DWORD*)&jmpCode[1] = NewFunctionAddress – (oldFunctionAddrss + 5);
这里是计算jmp后面的地址=目标地址-当前地址-5
让NtReadFileRet指向第一条未进行替换的汇编语句。
*lpRet = (PVOID)(oldFunctionAddrss + *patchCodeLen);
未拷贝前:
未拷贝前和我们定义的是一样的,注意最后一句直接jmp到NtReadFileRet所指的地址,继续执行原函数的汇编语句。
memcpy((PVOID)HookZone, (PVOID)oldFunctionAddrss, *patchCodeLen);
然后我们把前面几条需要替换的汇编代码拷贝到NtReadFileHookZone。下面是拷贝后NtReadFileHookZone的反汇编。
注意看画框中的汇编语句,就是原NtReadFile的前两句汇编。
memset((PVOID)oldFunctionAddrss, 0x90, *patchCodeLen);
memcpy((PVOID)oldFunctionAddrss, jmpCode, 5);
然后我们把原NtReadFile前面的汇编语句给替换掉,直接jmp到我们自己实现的函数中。
我们再来看看原始的NtReadFile函数:
然后可以看到上面在自定义函数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所指的地址。反汇编这个地址:
是不是继续接着原NtReadFile函数来执行的。
到现在应该明白inline hook其实就是改变原函数的执行流程这句话的含义了吧。
至于恢复inline hook,只要把修改的前几句汇编语句写回去即可。