Hook框架选择
基于微软规范的框架
优势:高稳定性、开发难度降低、系统大版本升级兼容
劣势:自由度低、只知其然不知其所以然、框架中未提供功能束手无策
相关框架 例如:
文件系统相关:miniFilter、SFilter
进程、注册表相关:ObRegisterCallbacks注册回调
网络:TDI、WFP
微软规范以外的框架
优势:自由度高、没有做不到只有想不到,更接近底层
劣势:难度高 需要对处理的功能前后都了解、错误或不完善的处理会引发未知的错误
相关框架 例如:
InfinityHook、SSDTHook、IDTHook 等
如果选择微软规范以外的框架,win7 64位系统以后 我们碰到了一只拦路虎——PG (PatchGuard)
我们有两个选择 干掉PG 或者 绕过PG
干掉PG
需要找到所有检测点,每个监测点触发的条件和时间都不定,很难过掉所有的检测
绕过PG
InfinityHook 利用Windows的事件跟踪机制,可以与 PatchGuard 同时稳定运行
简单介绍一下InfinityHook
开启 Event Tracing 后,当用户层程序进行系统调用时,首先会调用PerfInfoLogSysCallEntry函数来对此次系统调用进行记录。然后才调用系统调用函数。
PerfInfoLogSysCallEntry内部又会调用驱动注册的WMI_LOGGER_CONTEXT结构中的GetCpuClock指向的函数。
这样就可以通过修改GetCpuClock指针为自己的代理函数,而且此时系统调用函数被放置在栈上,因此在该函数中能从栈中获取系统调用函数的值并进行修改为自己的hook函数。
获取内核中的函数地址
内核中导出的函数
这里我用 ZwQueryInformationProcess 举例
定义函数类型
1 2 3 4 5 6 7 |
|
声明函数指针
1 |
|
获取函数地址
1 2 3 4 5 |
|
内核未导出的函数
请注意! 调用未导出函数,可能会导致驱动不能兼容高版本;
调用导出函数,也可能会导致驱动不能兼容高版本。
例如:
1 2 3 4 |
|
KeAttachProcess 已过时,Win10.19041 开始不再支持
改用 KeStackAttachProcess
1 2 3 4 |
|
获取 SSDT ShadowSSDT 地址
应用程序调用WinAPI,GUI无关的对应到 ntdll.dll 中的 NtXXX函数,GUI相关的对应到 win32u.dll 中的 NtXXX函数
NtXXX函数中 通过 mov eax,0x***
将系统服务号放入EAX(win32u.dll 中真实系统服务号为0x*** - 0x1000
)
服务分发函数 KiSystemService 根据传入的系统服务号,通过SSDT表 找到内核中的地址。
SSDT 系统服务描述符表 (System Services Descriptor Table)
SSDT 对应 ntoskrnl.exe 中的服务函数,这些函数实现了如文件管理、进程管理、设备管理等等相关的功能。
ShadowSSDT 对应 win32k.sys 中的服务函数,重点实现创建窗口、查找窗口、窗口绘图等 Gdi 与用户交互相关的功能。
Win7 32 位系统中,SSDT 在内核 Ntoskrnl.exe 中导出,直接获取导出符号 KeServiceDescriptorTable。
而在 64 位系统中,SSDT 表并没有在内核 Ntoskrnl.exe 中导出,我们不能像 32 位系统中那样直接获取导出符号 KeServiceDescriptorTable。
Win7 x64 与 Win10 64(Win10低版本)中 通过 __readmsr(0xC0000082)
获取内核函数 KiSystemCall64 的地址
KiSystemCall64 中调用了 KeServiceDescriptorTable 和 KeServiceDescriptorTableShadow
IDA查看 KiSystemCall64 的反汇编
1 2 3 4 5 6 7 8 9 |
|
KeServiceDescriptorTable 特征码 4c8d15
KeServiceDescriptorTableShadow 特征码 4c8d1d
win10 高版本中 __readmsr(0xC0000082) 返回 KiSystemCall64Shadow 函数
而 KiSystemCall64Shadow 无法直接搜索到 KeServiceDescriptorTable,IDA查看 KiSystemCall64Shadow 的反汇编
1 2 3 4 5 6 7 8 9 10 |
|
最终获取到的函数地址由 功能代码 变为 jmp跳转
代码从 win32k!NtXXX 跳转到了 win32kfull!NtXXX
win32k.sys 不再直接处理来自用户层的系统服务调用,而真正去处理用户的系统服务调用的函数,实际上是 win32kfull.sys 中的同名函数
这里贴的代码是获取 ShadowSSDT 地址,如果需要获取SSDT 地址,简单修改即可
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 52 53 54 55 56 57 58 |
|
定义服务描述符表
1 2 3 4 5 6 7 |
|
用法举例
1 2 |
|
获取系统服务号
手动获取
GUI无关的 使用IDA 查看 ntdll.dll,找到 NtXXX 函数,查看反汇编
找到 mov eax,0x***
,0x***
就是系统服务号
GUI相关的 使用IDA 查看 win32u.dll,找到 NtXXX 函数,查看反汇编
找到 mov eax,0x***
,0x*** - 0x1000
就是系统服务号
注意! 有一部分内核函数的系统服务号,会随着系统版本、补丁发生改变,导致驱动不兼容多版本,建议采用代码自动获取的方式。
获取并判断系统版本
手动获取的系统服务号不能兼容多个版本,所以还需要获取并判断系统版本
枚举windows版本
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
获取并判断系统版本
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 |
|
用法举例
1 2 3 4 |
|
代码自动获取
代码自动获取系统服务号相当于是用代码模拟了手动获取的方式
- 将dll读入内存中
- 检查是否位有效的PE文件
- 根据PE文件结构 PE头->扩展头->数据目录->导出表
- 获取以函数名字导出的函数个数、导出函数名称表,遍历找到我们要获取的函数地址
- 根据函数地址,找 mov eax,0x* 的特征 0xB8 注意!** Rva到文件偏移的转换
下面贴出代码
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 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 |
|
获取GUI相关的函数地址,还需附加GUI进程
只有在有 GUI 的线程当中,win32k.sys 的内存才可以被访问。
调用 KeAttachProcess 将当前线程附加到GUI进程的地址空间
KeAttachProcess 需要用到被附加进程的PEPROCESS
获取进程 PEPROCESS
目前最好用的是,遍历PID 对比进程名
其他方式在高版本可能不再兼容
这个直接贴代码
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 |
|
获取函数地址
用法示例
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 |
|
替换被Hook的函数 的函数实现
获取函数原型
如果为内核中导出的函数,MSDN可以查到函数原型
例如:ZwQueryInformationProcess
如果为内核未导出的函数
- 浏览器搜索,可能大佬已经先一步分享了函数原型
- IDA查看ntdll.dll、win32u.dll、ntoskrnl.exe、win32k.sys(IDA分析的函数原因很可能不准确,作为参考)
- 自己写一个测试程序,单独调用R3的API,OD调试,跟踪到NtXXX函数,分析参数;如果参数是堆地址等,分析NtXXX返回后 堆填充的内容,如果不需要用到所有的返回信息,不需自己定义一个结构体,参数类型给成 PVOID 即可,记录需要用到的信息所在的字节偏移。
测试代码,测试R3 APIEnumDisplaySettingsW
R0 APINtUserEnumDisplaySettings
1 2 3 4 5 |
|
X64dbg 执行NtXXX 函数前
可以看到有四个参数,参数三是 RtlAllocateHeap 申请的堆地址
X64dbg 执行NtXXX 函数后,分析返回的信息,提取有用的部分
如果被Hook的函数是一个高频函数,如何准确定位到是自己的程序调用
思路:
- 获取当前线程所属进程的 PEPROCESS
- 通过 PEPROCESS 获取当前进程句柄
-
通过 进程句柄 调用 ZwQueryInformationProcess 获取 进程名
实现代码
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
52
53
54
55
/
*
+
+
Routine Description:
获取指定进程的完整进程名
Arguments:
pEproc
-
指定进程的EPROCESS地址
Return Value:
成功则返回进程名,失败返回NULL
Comments:
该函数返回的进程名,由调用则负责释放(ExFreePool)
-
-
*
/
PUNICODE_STRING GetProcNameByEproc(IN PEPROCESS pEproc) {
NTSTATUS NtStatus;
HANDLE hProc
=
NULL;
PBYTE pBuf
=
NULL;
ULONG ulSize
=
32
;
PAGED_CODE();
/
/
1.
pEproc
-
-
> handle
NtStatus
=
ObOpenObjectByPointer(pEproc,
OBJ_KERNEL_HANDLE,
NULL,
0
,
NULL,
KernelMode,
&hProc
);
if
(!NT_SUCCESS(NtStatus))
return
NULL;
/
/
2.
ZwQueryInformationProcess
while
(TRUE)
{
pBuf
=
(PBYTE)ExAllocatePoolWithTag(NonPagedPool, ulSize, ALLOC_TAG);
if
(!pBuf) {
ZwClose(hProc);
return
NULL;
}
NtStatus
=
ZwQueryInformationProcess(hProc,
ProcessImageFileName,
pBuf,
ulSize,
&ulSize);
if
(NtStatus !
=
STATUS_INFO_LENGTH_MISMATCH)
break
;
ExFreePool(pBuf);
}
ZwClose(hProc);
if
(!NT_SUCCESS(NtStatus)) {
ExFreePool(pBuf);
return
NULL;
}
/
/
3.
over
return
(PUNICODE_STRING)pBuf;
}
用法示例
1
2
3
4
5
6
/
*
调试用
*
/
PUNICODE_STRING CurProcName
=
GetProcNameByEproc(IoGetCurrentProcess());
if
((CurProcName
-
>Length !
=
0
) && (wcsstr(CurProcName
-
>
Buffer
, L
"test.exe"
))) {
DbgBreakPoint();
}
ExFreePool(CurProcName);
分析不同情况下的返回值类型,调用的 R3 API 如何进行处理这些返回值
例如:
系统设置是如何获取的显示分辨率列表
系统设置->显示设置->显示分辨率
设置 是通过,调用 EnumDisplaySettingsW,
参数二 从0开始,每次调用+1,每次获取一条显示信息,直到返回值为0,枚举出了所有显示信息;
而内核中的处理是,当参数二为0时,操作系统将初始化并缓存有关显示设备的信息。当您将参数二设置为非零值调用EnumDisplaySettings时,该函数将返回上次在参数二为0的情况下调用该函数时缓存的信息。
内核NtAPI 返回成功 STATUS_SUCCESS 返回值 rax = 0
而当最后一个显示信息被返回后,再次参数二+1调用,内核会返回 STATUS_INVALID_PARAMETER_2 rax = 0xC00000F0L
当系统枚举显示信息时,我们如果在挂钩的函数中,直接返回 STATUS_INVALID_PARAMETER_2,系统将停止枚举分辨率。
分析驱动如何返回信息到R3,返回值如何修改
还是用 NtUserEnumDisplaySettings 举例
NtUserEnumDisplaySettings 使用第三个参数,也是R3 API 申请的堆来保存要返回的显示信息
如果我们要修改R3 EnumDisplaySettingsW 拿到的显示信息,可以在挂钩的内核函数中 直接修改(当前线程与申请堆的线程所属进程一致时,可以直接修改)
第一次调用返回我们设置好的显示信息,第二次调用返回 STATUS_INVALID_PARAMETER_2,系统的设置就只能选择我们设置的分辨率,而没有其他选项。
驱动与R3通讯
驱动与R3通讯,比较简单 驱动中 只需创建设备、将设备对象和符号链接绑定;R3的应用程序打开设备调用DeviceIoControl与驱动通讯。
需要注意的是: 我们只用到了DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL]
,但是其他的功能函数也需要填充,否则R3的应用程序打开设备时将报错。
通讯 驱动部分代码
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 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
|
用法示例 (在 DriverEntry 中调用)
1 2 3 4 5 |
|
通讯 R3应用程序部分代码
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 |
|
通讯示例
1 2 3 4 5 6 7 |
|
其他
声明参数未引用
1 |
|
中断请求级别
高IRQL的代码,可以中断(抢占)低IRQL的代码的执行过程,从而得到执行机会
常见的IRQL:
IRQLd等级 | 介绍
-------- | -----
PASSIVE_LEVEL | 应用层线程以及大部分内核函数处于该IRQL,可以无限制使用所有内核API,可以访问分页及非分页内存
APC_LEVEL | 异步方法调用(APC)或页错误时处于该IRQL,可以使用大部分内核API,可以访问分页及非分页内存
DISPATCH_LEVEL | 延迟方法调用(DPC)时处于该IRQL,可以使用特定的内核API,只能访问非分页内存
注意! 在调用任何一个内核API前,必须查看WDK文档,了解这个内核API的中断要求!
获取当前IRQL
1 |
|
参考
1 2 3 4 5 6 |
|