内核级InlineHook是RK/ARK实现各种功能的重要手段之一。在WIN32下,已经有了完善的反汇编引擎和挂钩引擎,但是在WIN64下,这一切还是空白。之前发的那一篇Win64内核InlineHook的文章,还不太方便实际应用,因为没有找到合适的x64反汇编引擎,需要硬编码以及一堆的机器码。经过我几个月的研究,终于找到了合适的反汇编引擎以及无需机器码的挂钩方法,在此分享给大家。
无论是用户态InlineHook还是内核级InlineHook,都要遵循一个原则,就是指令不能截断,否则会出大错误。所以,反汇编引擎在InlineHook引擎中的作用,就是判断指令的长度。
首先介绍我找到的x64反汇编引擎,LDE64。LDE64是LengthDisassembleEngineforx64的缩写,此反汇编引擎小巧玲珑,最大的优点就是能带进驱动里使用(很多外国开源的反汇编引擎都无法带进驱动,还需要很多莫名其妙的非标准C++函数库)。不过,LDE64的作者也够小气的,没有直接给出源代码,但是给了一个十几KB的shellcode,当你需要反汇编时,直接调用shellcode就行了。核心代码如下:
unsignedcharszShellCode[12800]={...}//详细机器码请见源码文件
typedefint(*LDE_DISASM)(void*p,intdw); LDE_DISASMLDE; voidLDE_init() { LDE=ExAllocatePool(NonPagedPool,12800); memcpy(LDE,szShellCode,12800); }
需要使用时,先调用LDE_init进行初始化,然后再调用LDE函数即可。LDE函数要求输入地址和平台类型,返回一条指令的字节长度。接下来编写一个自定义函数,返回要Patch的字节数目。虽然在Win64上写一个跨4G跳转指令理论上只需要14字节,但是14字节并不一定就是N个完整的指令,所以必须得到N个完整指令的长度(N条指令的长度要大于等于14):
ULONGGetPatchSize(PUCHARAddress) { ULONGLenCount=0,Len=0; while(LenCount=14)//至少需要14字节 { Len=LDE(Address,64); Address=Address+Len; LenCount=LenCount+Len; } returnLenCount; }
接下来,说一下实现内核级InlineHook的思路:
1.获得待HOOK函数的地址(Address)
2.获得要修改的字节数目(N)
3.保存这头N字节的机器码
4.创建『原函数』(把复制头N字节,再跳转到Address+N的地方)
5.修改函数头,跳转到代理函数里解释一下跳转的代码。我之前使用的跳转流程是:
MOVRAX,绝对地址
后来感觉修改RAX不太好(虽然RAX是易失性寄存器),于是换了方式:
JMPQWORDPTR[本条指令结束后的地址]
以上指令的机器码是:FF2500000000。
代码如下:
//传入:待HOOK函数地址,代理函数地址,接收原始函数地址的指针,接收补丁长度的指针;返回:原来头N字节的数据
PVOIDHookKernelApi(INPVOIDApiAddress,INPVOIDProxy_ApiAddress,OUTPVOID *Original_ApiAddress,OUTULONG*PatchSize) { KIRQLirql; UINT64tmpv; PVOIDhead_n_byte,ori_func; UCHAR jmp_code[]=\xFF\x25\x00\x00\x00\x00\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF; UCHAR jmp_code_orifunc[]=\xFF\x25\x00\x00\x00\x00\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF ; //Howmanybytesshoulebepatch *PatchSize=GetPatchSize((PUCHAR)ApiAddress); //step1:Readcurrentdata head_n_byte=kmalloc(*PatchSize); irql=WPOFFx64(); memcpy(head_n_byte,ApiAddress,*PatchSize); WPONx64(irql); //step2:Createorifunction ori_func=kmalloc(*PatchSize+14); //原始机器码+跳转机器码 RtlFillMemory(ori_func,*PatchSize+14,0x90); tmpv=(ULONG64)ApiAddress+*PatchSize; memcpy(jmp_code_orifunc+6,tmpv,8); memcpy((PUCHAR)ori_func,head_n_byte,*PatchSize); memcpy((PUCHAR)ori_func+*PatchSize,jmp_code_orifunc,14); *Original_ApiAddress=ori_func; //跳转到没被打补丁的那个字节 //step3:filljmpcode tmpv=(UINT64)Proxy_ApiAddress; memcpy(jmp_code+6,tmpv,8); //step4:FillNOPandhook irql=WPOFFx64(); RtlFillMemory(ApiAddress,*PatchSize,0x90); memcpy(ApiAddress,jmp_code,14); WPONx64(irql); //returnoricode returnhead_n_byte; }
反挂钩就简单了,直接把头N字节回复即可:
//传入:被HOOK函数地址,原始数据,补丁长度 VOIDUnhookKernelApi(INPVOIDApiAddress,INPVOIDOriCode,INULONGPatchSize) { KIRQLirql; irql=WPOFFx64(); memcpy(ApiAddress,OriCode,PatchSize); WPONx64(irql); }
接下来是示例,很简单地调用一下以上两个函数即可!
NTSTATUSProxy_PsLookupProcessByProcessId(HANDLEProcessId,PEPROCESS *Process) { NTSTATUSst; st=((PSLOOKUPPROCESSBYPROCESSID)ori_pslp)(ProcessId,Process); if(NT_SUCCESS(st)) { if(*Process==(PEPROCESS)my_eprocess) { *Process=0; st=STATUS_ACCESS_DENIED; } } returnst; } VOIDHookPsLookupProcessByProcessId() { pslp_head_n_byte = HookKernelApi(GetFunctionAddr(LPsLookupProcessByProcessId), (PVOID)Proxy_PsLookupProcessByProcessId, ori_pslp, pslp_patch_size); } VOIDUnhookPsLookupProcessByProcessId() { UnhookKernelApi(GetFunctionAddr(LPsLookupProcessByProcessId), pslp_head_n_byte, pslp_patch_size); }
效果如下:
用WINDBG检测一下:
挂钩前:
lkdupslookupprocessbyprocessid nt!PsLookupProcessByProcessId: fffff800`0194c75048895c2408 fffff800`0194c75548896c2410 fffff800`0194c75a4889742418 fffff800`0194c75f57 mov qwordptr[rsp+8],rbx mov qwordptr[rsp+10h],rbp mov qwordptr[rsp+18h],rsi push push push sub rdi fffff800`0194c7604154 r12 fffff800`0194c7624155 r13 fffff800`0194c7644883ec20 rsp,20h fffff800`0194c76865488b3c2588010000movrdi,qwordptrgs:[188h]
挂钩后:
lkdupslookupprocessbyprocessid nt!PsLookupProcessByProcessId: fffff800`0194c750ff2500000000 jmp qwordptr [nt!PsLookupProcessByProcessId+0x6(fffff800`0194c756)] fffff800`0194c756dc50e9 fffff800`0194c75906 fcom ??? qwordptr[rax-17h] al,0FFh fffff800`0194c75a80f8ff fffff800`0194c75dff9057415441 fffff800`0194c76355 cmp call push sub qwordptr[rax+41544157h] rbp fffff800`0194c7644883ec20 rsp,20h fffff800`0194c76865488b3c2588010000movrdi,qwordptrgs:[188h]
看样子代码好像乱了,其实不是的。因为跨4G跳转指令是14字节,而我们修改了PsLookupProcessByProcessId的头15字节(正好三条指令),前6字节是指令,后9字节并不是指令,而是数据(前8字节是绝对地址)和填充码(最后1字节没有意义)。所以这么看就对了:
lkduPsLookupProcessByProcessId+0xf nt!PsLookupProcessByProcessId+0xf: fffff800`0194c75f57 push push push sub rdi fffff800`0194c7604154 fffff800`0194c7624155 fffff800`0194c7644883ec20 r12 r13 rsp,20h fffff800`0194c76865488b3c2588010000movrdi,qwordptrgs:[188h] fffff800`0194c7714533e4 fffff800`0194c774488bea xor mov r12d,r12d rbp,rdx fffff800`0194c77766ff8fc4010000dec wordptr[rdi+1C4h]
本文到此结束,如有不足敬请指出。