[转]详谈内核三步走Inline Hook实现
(一)Inline hook原理
Inline hook通俗的说就是对函数执行流程进行修改,达到控制函数过滤操作的目的。理论上我们可以在函数任何地方把原来指令替换成我们的跳转指令,也确实有些人在inline
的时候做的很深,来躲避inline 的检测,前提是必须对函数的流程和指令非常熟悉,且这种深层次的inlline 不具有通用性,稳定性也是问题。本文讨论的是具有通用性的两类inline的实现。
Inline hook原理:解析函数开头的几条指令,把他们Copy到数组保存起来,然后用一个调用我们的函数的几条指令来替换,如果要执行原函数,则在我们函数处理完毕,再执行我们保存起来的开头几条指令,然后调回我们取指令之后的地址执行。
整个Inline hook的过程就大体这样,中间牵扯到对函数的检查,地址的获取就直接调用函数即可。
本文所要讨论的两类Inline hook都是基于上面原理。
说明三点:
1
、堆栈平衡是重中之重,参数压栈也需要格外注意
2
、CR0寄存器中的WP位控制处理器是否允许往只读内存页写入,为0禁用保护机制。
3
、提高中断级别到DISPATCH_LEVEL,禁止线程切换产生的中断
�
(二)inline hook应用
Inline hook可分为两类:
(
1
)inline 导出函数,选择ObReferenceObjectByHandle做例子。
(
2
)inline 未导出函数,选择KiInsertQueueApc做例子。
导出函数前几个字节可以利用windbg自己查看是什么内容,而未导出函数就需要自己解析指令确定需要hook几个字节,其间还有很多问题需要注意。当大家真正的弄懂了我这篇文章,回头再看inline hook就会觉得inline也不过如此。
下面通过2个例子来讲inline hook的使用(这部分知识网上也有很多,但都很零散不系统,本文部分思路及代码的确参考了网上资源,有抄袭之嫌,希望读者谅解。我一直强调“授人以鱼不如授人以渔”,代码并不重要,关键是思想。)
1
、inline hook ObReferenceObjectByHandle保护进程
ObReferenceObjectByHandle属于ntoskrnl.exe导出函数,在内核中调用频繁。
NtCreateProcess创建进程需要调用ObReferenceObjectByHandle,NtTerminateProcess需要调用ObReferenceObjectByHandle,基于这我们就可以利用Hook来保护进程同时屏蔽进程的创建。
效果:已经运行的记事本任务管理器无法结束
流程:
HookObReferenceObjectByHandle
------
DetourMyObReferenceObjectByHa ndle
----------
UnHookObReferenceObjectByHandle
核心代码分析如下:
//
=======================================inline HOOK ObReferenceObjectByHandle===========================
//
ObReferenceObjectByHandle是ntoskrnl.exe导出函数,采用HOOK前五个字节的方式
//
字节型数据 unsigned char
ULONG CR0VALUE;
BYTE OriginalBytes[
5
]
=
{
0
};
//
保存原始函数前五个字节 �
BYTE JmpAddress[
5
]
=
{
0xE9
,
0
,
0
,
0
,
0
};
//
跳转到HOOK函数的地址
extern
POBJECT_TYPE
*
PsProcessType;
NTKERNELAPI NTSTATUS ObReferenceObjectByHandle(
�
IN HANDLE Handle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_TYPE ObjectType OPTIONAL,
IN KPROCESSOR_MODE AccessMode,
OUT PVOID
*
Object,
OUT POBJECT_HANDLE_INFORMATION HandleInformation OPTIONAL
�
);
//
HOOK函数
NTSTATUS DetourMyObReferenceObjectByHandle(
�
IN HANDLE Handle, �
IN ACCESS_MASK DesiredAccess
IN POBJECT_TYPE ObjectType OPTIONAL,�
IN KPROCESSOR_MODE AccessMode,
OUT PVOID
*
Object,
OUT POBJECT_HANDLE_INFORMATION HandleInformation OPTIONAL);
//
//
hook流程 HookObReferenceObjectByHandle---DetourMyObReferenceObjectByHandle---UnHookObReferenceObjectByHandle
void
HookObReferenceObjectByHandle()
{
�
//
赋值前面定义的数组
KIRQL Irql;
KdPrint((
"
[ObReferenceObjectByHandle] :0x%x
"
,ObReferenceObjectByHandle));
//
地址验证
//
保存函数前五个字节内容
RtlCopyMemory(OriginalBytes,(BYTE
*
)ObReferenceObjectByHandle,
5
);
//
保存新函数五个字节之后偏移
*
(ULONG
*
)(JmpAddress
+
1
)
=
(ULONG)DetourMyObReferenceObjectByHandle
-
((ULONG)ObReferenceObjectByHandle
+
5
);
//
开始inline hook
//
关闭内存写保护
_asm
�
{
push eax
�
mov eax, cr0�
mov CR0VALUE, eax�
and eax, 0fffeffffh �
mov cr0, eax
pop eax
}
�
//
提升IRQL中断级
Irql
=
KeRaiseIrqlToDpcLevel();
//
函数开头五个字节写JMP�
RtlCopyMemory((BYTE
*
)ObReferenceObjectByHandle,JmpAddress,
5
);
//
恢复Irql
KeLowerIrql(Irql);
//
开启内存写保护
�
__asm
�
{ �
�
push eax
�
mov eax, CR0VALUE�
�
mov cr0, eax
�
pop eax
�
}
�
}
_declspec (naked) NTSTATUS OriginalObReferenceObjectByHandle(IN HANDLE Handle,
�
IN ACCESS_MASK DesiredAccess,
�
IN POBJECT_TYPE ObjectType OPTIONAL,
�
IN KPROCESSOR_MODE AccessMode,
�
OUT PVOID
*
Object,
�
OUT POBJECT_HANDLE_INFORMATION HandleInformation OPTIONAL)
�
{
�
_asm
�
{ �
�
mov edi,edi
push ebp
mov ebp,esp
mov eax,ObReferenceObjectByHandle
add eax,
5
jmp eax �
�
}
�
}
NTSTATUS DetourMyObReferenceObjectByHandle(
�
IN HANDLE Handle,
�
IN ACCESS_MASK DesiredAccess,
�
IN POBJECT_TYPE ObjectType OPTIONAL,
�
IN KPROCESSOR_MODE AccessMode,
�
OUT PVOID
*
Object,
�
OUT POBJECT_HANDLE_INFORMATION HandleInformation OPTIONAL)
�
{
�
NTSTATUS status;
�
//
调用原函数
�
status
=
OriginalObReferenceObjectByHandle(Handle,DesiredAccess,ObjectType,AccessMode,Object,HandleInformation);
�
if
((status
==
STATUS_SUCCESS)
&&
(DesiredAccess
==
1
))
//
这样写在我机器出蓝屏,虚拟机也一样蓝,,看来要学的挺多的
�
{ �
�
if
(ObjectType
==
*
PsProcessType)
�
{
�
if
( _stricmp((
char
*
)((ULONG)(
*
Object)
+
0x174
),
"
notepad.exe
"
)
==
0
)
�
{ �
�
ObDereferenceObject(
*
Object);
�
return
STATUS_INVALID_HANDLE;
�
}
�
}
�
}
�
return
status;
�
}
void
UnHookObReferenceObjectByHandle()
{
�
//
把五个字节再写回到原函数
�
KIRQL Irql;
�
//
关闭写保护
�
_asm
�
{
�
push eax
�
mov eax, cr0�
�
mov CR0VALUE, eax�
�
and eax, 0fffeffffh �
�
mov cr0, eax
�
pop eax
�
}
�
//
提升IRQL到Dpc
�
Irql
=
KeRaiseIrqlToDpcLevel();
�
RtlCopyMemory((BYTE
*
)ObReferenceObjectByHandle,OriginalBytes,
5
);
�
KeLowerIrql(Irql);
�
//
开启写保护
�
__asm
�
{ �
�
push eax
mov eax, CR0VALUE�
mov cr0, eax
�
pop eax
�
}
}
驱动加载后,结束记事本程序如下:
�
(图 一)
详细分析:
1
、ObReferenceObjectByHandle分析
NTSTATUS�
ObReferenceObjectByHandle(
IN HANDLE Handle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_TYPE ObjectType OPTIONAL,
IN KPROCESSOR_MODE AccessMode,
OUT PVOID
*
Object,
OUT POBJECT_HANDLE_INFORMATION HandleInformation OPTIONAL
);
函数原型如上,由句柄获取对象指针,函数返回值:
STATUS_SUCCESS 调用成功
STATUS_OBJECT_TYPE_MISMATCH �
STATUS_ACCESS_DENIED 权限不够
STATUS_INVALID_HANDLE 无效句柄
调用NtTerminateProcess需要调用ObReferenceObjectByHandle,因此我们通过对函数返回值进程修改来达到保护进程。但是NtCreateProcess(最终调用的PspCreateProcess)同样调用这个函数,如果不加区分的话,创建进程同样被禁止了,那么如何区分到底是谁在调用呢。参考WRK,我发现可以通过第二个参数DesiredAccess来判别,创建进程和结束进程第二个参数明显不同,PROCESS_CREATE_PROCESS和PROCESS_TERMINATE,问题就解决了。
PspCreateProcess位于 WRK
-
v1.
2
\
base
\ntos\ps\create.c
调用ObReferenceObjectByHandle代码:
Status
=
ObReferenceObjectByHandle (ParentProcess,
PROCESS_CREATE_PROCESS,
PsProcessType,
PreviousMode,
&
Parent,
NULL);
NtTerminateProcess位于 WRK
-
v1.
2
\
base
\ntos\ps\psdelete.c
调用ObReferenceObjectByHandle代码:
st
=
ObReferenceObjectByHandle (ProcessHandle,
PROCESS_TERMINATE,
PsProcessType,
KeGetPreviousModeByThread(
&
Self
->
Tcb),
&
Process,
NULL);
DesiredAccess参数说明:
#define
PROCESS_TERMINATE (0x0001)
//
winnt
#define
PROCESS_CREATE_THREAD (0x0002)
//
winnt
#define
PROCESS_SET_SESSIONID (0x0004)
//
winnt
#define
PROCESS_VM_OPERATION (0x0008)
//
winnt
#define
PROCESS_VM_READ (0x0010)
//
winnt
#define
PROCESS_VM_WRITE (0x0020)
//
winnt
//
begin_ntddk begin_wdm begin_ntifs
#define
PROCESS_DUP_HANDLE (0x0040)
//
winnt
//
end_ntddk end_wdm end_ntifs
#define
PROCESS_CREATE_PROCESS (0x0080)
//
winnt
#define
PROCESS_SET_QUOTA (0x0100)
//
winnt
#define
PROCESS_SET_INFORMATION (0x0200)
//
winnt
#define
PROCESS_QUERY_INFORMATION (0x0400)
//
winnt
#define
PROCESS_SET_PORT (0x0800)
#define
PROCESS_SUSPEND_RESUME (0x0800)
//
winnt
2
、函数调用说明
C语言中我们调用一个函数就直接写函数名就可以,但是实际是进行了下面的操作:
把函数参数压入堆栈,压入函数返回地址,调用函数,为新函数开辟堆栈空间申请局部变量,
恢复堆栈保持堆栈平衡
(_stdcall调用方式)汇编代码就是:
Push 参数4
Push 参数3
Push 参数2
Push 参数1
Call 函数 ;call指令同时完成2个操作,一是把返回地址压入堆栈,二跳转到调用函数入口地址
Push ebp
Mov ebp,esp
Sub esp, XX ;开辟栈帧空间
……
Add esp ,XX
Pop ebp
Retn ;恢复堆栈平衡
堆栈详细情况:
ESP
局部变量
EBP
返回地址
参数1
参数2
参数3
参数4
堆栈是由高地址到低地址。
参数就通过EBP来去,四字节对齐的
参数4
----------------------
EBP
+
0x14
参数3
----------------------
EBP
+
0x10
参数2
----------------------
EBP
+
0xc
参数1
---------------------
EBP
+
0x8
局部变量则通过Ebp
-
XX来获取
因此inline的时候要时刻考虑堆栈平衡,破坏了堆栈平衡就会导致函数崩溃。
我通常inline hook的思路就是三步走:
HOOK函数
-----
DetourMy处理函数
----------
UnHook函数
处理函数中对返回结果或者中间数据进行修改处理,然后调用原始函数。由于在我们处理的时候原始函数已经被hook了,所以我自己构造了一个原始函数,但是由于参数在我们hook前已经压人堆栈了,所以这里我们不用重新开辟栈帧,因此声名函数类型为_declspec (naked)
。有人就会问那么你调用处理函数的时候,参数不是重复压栈了,这里请注意,我们是通过JMP方式跳转到我们处理函数入口地址的,而不是Call的形式,所以并没有执行上面所说的函数调用过程,参数仍然是原始函数的。也就是说在真个inline hook过程中我们不能破坏原始栈帧的EBP。
关于函数调用很栈帧的相关联系可能比较难理解,我也在尽肯能的用通俗的话来解释清楚,有什么不理解的地方或者个人见解欢迎大家跟我交流。
2
、inline hook KiInsertQueueApc对抗插APC杀进程
KiInsertQueueAPc为内核未导出函数,我下面提供的代码可以作为未导出函数inline的通用模板来使用,大家根据自己需要进行修改,基于inline ObReferenceObjectByHandle已经把原理分析了,这部分我就不详加分析,仍然采用的但不走,Hook函数
---
DetourMy函数
---
UnHook函数
直接看核心代码:
//
===================inline hook KiInsertQueueApc====================
//
KiInsertQueueApc为内核未导出函数,可以从导出函数KeInsertQueueApc定位
//
修改KiInsertQueueApc开头5字节
//
处理函数思路:apc-->kthread---apc_state--eprocess--进程名字
//
HookKiInsertQueueApc---DetourMyKiInsertQueueApc---UnHookKiInsertQueueApc
ULONG CR0VALUE;
ULONG g_KiInsertQueueApc;
�
BYTE JmpAddress[
5
]
=
{
0xE9
,
0
,
0
,
0
,
0
};
//
跳转到HOOK函数的地址
BYTE OriginalBytes[
5
]
=
{
0
};
//
保存原始函数前五个字
VOID FASTCALL DetourMyKiInsertQueueApc(IN PKAPC Apc,IN KPRIORITY Increment);
VOID WPOFF()
{
_asm
�
{
�
push eax
�
mov eax, cr0�
�
mov CR0VALUE, eax�
�
and eax, 0fffeffffh �
�
mov cr0, eax
�
pop eax
cli
�
};
�
}
VOID WPON()
{
__asm
�
{ �
sti
push eax
�
mov eax, CR0VALUE�
�
mov cr0, eax
�
pop eax
�
};
}
//
1、获取KiInsertQueueApc地址
ULONG GetFunctionAddr( IN PCWSTR FunctionName)
//
PCWSTR常量指针,指向16位UNICODE
{
UNICODE_STRING UniCodeFunctionName;
RtlInitUnicodeString(
&
UniCodeFunctionName, FunctionName );
return
(ULONG)MmGetSystemRoutineAddress(
&
UniCodeFunctionName ); �
}
ULONG GetKiInsertQueueApcAddr()
{
ULONG sp_code1
=
0x28
,sp_code2
=
0xe8
,sp_code3
=
0xd88a
;
//
特征码,sp_code3 windbg显示错误,应该为d88a
ULONG address
=
0
;
PUCHAR addr;
PUCHAR p;
addr
=
(PUCHAR)GetFunctionAddr(L
"
KeInsertQueueApc
"
);
for
(p
=
addr;p
<
p
+
PAGE_SIZE;p
++
)
{
if
((
*
(p
-
1
)
==
sp_code1)
&&
(
*
p
==
sp_code2)
&&
(
*
(PUSHORT)(p
+
5
)
==
sp_code3))
{
address
=*
(PULONG)(p
+
1
)
+
(ULONG)(p
+
5
);
break
;
}
}
KdPrint((
"
[KeInsertQueueApc] addr %x\n
"
,(ULONG)addr));
KdPrint((
"
[KiInsertQueueApc] address %x\n
"
,address));
return
address;
}
VOID HookKiInsertQueueApc()
{ �
KIRQL Irql;
g_KiInsertQueueApc
=
GetKiInsertQueueApcAddr();
KdPrint((
"
[KiInsertQueueApc] KiInsertQueueApc %x\n
"
,g_KiInsertQueueApc));
//
保存原函数的前字节内容
RtlCopyMemory (OriginalBytes, (BYTE
*
)g_KiInsertQueueApc,
5
);
//
新函数对原函数的偏移地址
*
( (ULONG
*
)(JmpAddress
+
1
) )
=
(ULONG)DetourMyKiInsertQueueApc
-
(ULONG)g_KiInsertQueueApc
-
5
;
//
禁止系统写保护,提升IRQL到DPC
WPOFF();
Irql
=
KeRaiseIrqlToDpcLevel();
//
inline hook函数
RtlCopyMemory ( (BYTE
*
)g_KiInsertQueueApc, JmpAddress,
5
);
//
恢复写保护,降低IRQL
KeLowerIrql(Irql);
WPON(); �
}
//
原函数
_declspec (naked) VOID FASTCALL OriginalKiInsertQueueApc(IN PKAPC Apc,IN KPRIORITY Increment)
{
_asm
{
//
前五个字节
mov edi,edi
push ebp
mov ebp,esp
�
mov eax,g_KiInsertQueueApc
add eax,
5
jmp eax
}
}
//
处理函数
//
apc--kthread--apc_state--eprocess
VOID FASTCALL DetourMyKiInsertQueueApc(IN PKAPC Apc,IN KPRIORITY Increment)
{
ULONG thread;
ULONG process;
if
(MmIsAddressValid((PULONG)((ULONG)Apc
+
0x008
)))
//
地址验证 KAPC结构+008--->kthread
thread
=*
((PULONG)((ULONG)Apc
+
0x008
));
else
return
;
if
(MmIsAddressValid((PULONG)((ULONG)thread
+
0x044
)))
//
kthread+30-->KAPC_STATE+10-->eprocess
process
=*
((PULONG)((ULONG)thread
+
0x044
));
else
return
;
if
(MmIsAddressValid((PULONG)((ULONG)process
+
0x174
)))
//
eprocess+174---->进程名字
{
if
((_stricmp((
char
*
)((ULONG)process
+
0x174
),
"
notepad.exe
"
)
==
0
)
&&
(Increment
==
2
))
{
return
;
}
else
OriginalKiInsertQueueApc(Apc,Increment);
}
else
return
;
}
//
卸载函数
VOID UnHookKiInsertQueueApc()
{
KIRQL Irql;
WPOFF();
Irql
=
KeRaiseIrqlToDpcLevel();
//
inline hook函数
RtlCopyMemory ( (BYTE
*
)g_KiInsertQueueApc, OriginalBytes,
5
);
//
恢复写保护,降低IRQL
KeLowerIrql(Irql);
WPON(); �
}
考虑到大家水平不一,对一些问题我详细如下:
1
、特征码的寻找
利用windbg的kernel debug来查找:
uf KeInsertQueueApc
nt
!
KeInsertQueueApc
+
0x3b
:
804e6d0a 8b450c mov eax,dword ptr [ebp
+
0Ch]
804e6d0d 8b5514 mov edx,dword ptr [ebp
+
14h]
804e6d10
894724
mov dword ptr [edi
+
24h],eax
804e6d13 8b4510 mov eax,dword ptr [ebp
+
10h]
804e6d16 8bcf mov ecx,edi
804e6d18
894728
mov dword ptr [edi
+
28h],eax
804e6d1b e8523fffff call nt
!
KiInsertQueueApc (804dac72)
804e6d20 8ad8 (错误) mov bl,al
特征码就是sp_code1
=
0x28
sp_code2
=
0xe8
sp_code3
=
0xd88a
(windbg显示有误,应该是d88a
)
这种方法就是通过已导出函数定位未导出函数通常使用的方法,具有通用性。详细见代码。
2
、取EPRocess的过程
Apc
-----
kthread
-----
apc_state—eprocess
dt _KAPC 偏移0x008指向KTHREAD
dt _KTHREAD 偏移0x034指向KAPC_STATE
dt _KAPC_STATE 偏移0x10指向EPROCESS
dt _EPROCESS 偏移0x174指向进程名
(三)总结
很多人觉得inline hook比较难,处理起来很麻烦。但是我相信看完我这篇文章,你一定不会这么认为了,inline hook其实只要细心,注意细节跟别的hook没什么两样。本人采用的三步走inline hook做到了把inline简单化,同时有保证了堆栈的平衡。
由于代码采用的硬编码,编译环境是sp3
+
VMware,请根据自己操作系统自行修改。欢迎读者跟我交流。
=======================