探索黑客技术攻防,实战研究与安全创新

导航菜单

实现Win64上的内核级InlineHook引擎

内核级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);
}


效果如下:


图片1.png

用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]


本文到此结束,如有不足敬请指出。