《天书夜读:从汇编语言到windows内核编程》十六 反病毒、木马开发实例

时间:2022-09-01 03:44:48

1) HOOK ZwCreateSection:Inline Hook的运用

  Windows加载可执行文件,必须生成一个SectionObject映射进内存,所谓的SectionObject在WDK给出的解释是:A section object represents a section of memory that can be shared. A process  can use a section object to share parts of its memory address space (memory sections) with other processes. Section objects also provide the mechanism by which a process can map a file into its memory address space(一个可以被共享的内存区域,一个进程可以使用SectionObject与其它的进程共享它的一部分内存地址(内存区域),SectionObject也为一个进程把文件加载到内存空间提供了一个机制)。函数原型:

 

1 NTSTATUS ZwCreateSection(
2 OUT PHANDLE SectionHandle,
3 IN ACCESS_MASK DesiredAccess,
4 IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
5 IN PLARGE_INTEGER MaximumSize OPTIONAL,
6 IN ULONG SectionPageProtection,
7 IN ULONG AllocationAttributes,
8 IN HANDLE FileHandle OPTIONAL
9 );

 

  关注两个参数,一个是FileHandle,这是要映射的文件的句柄,可用于获取文件完整路径;另一个是SectionPageProtection,如果被设置为PAGE_EXECUTE则是可执行的(其它还有:PAGE_READONLY,PARG_READWRITE和PAGE_WRITECOPY)。反汇编一下这个函数:

 

1 nt!ZwCreateSection:
2 80501124 b832000000 mov eax,32h
3 80501129 8d542404 lea edx,[esp+4]
4 8050112d 9c pushfd ; 将32位标志寄存器EFLAGS压入堆栈
5 8050112e 6a08 push 8
6 80501130 e87c130400 call nt!KiSystemService (805424b1)
7 80501135 c21c00 ret 1Ch

 

  使用《十五》节笔记中的inline Hook只需要修改要HOOK的API函数就能实现对ZwCreateSection的Hook。我测试了一下,并不像作者说的所有程序都会调用这个API。记事本、纸牌、IE等均会调用ZwCreateSection,而扫雷、空中接龙、控制台命令以及基本是大多数运用程序都不会调用ZwCreateSection,这样子的话,勾这个API来反病毒就没意义了。接下来介绍的是HOOK以后在自己的函数中用MD5校验文件,然后查询MD5数据库看它是否合法,合法就调用原来的ZwCreateSection,不合法时弹出等待,用户点击“禁止”时不再执行原来的ZwCreateSection而直接返回失败值,否则点击“允许”时继续执行原ZwCreateSection。

 

 

2)在内核中生成设备对象:生成设备对象以便与R3层用户程序通讯。说实话这个挂钩ZwCreateSession的东西很不好用(至少在XPSP3上)。下面改用HOOK另一个函数来测试一下本章内容,这个函数是NtCreateProcessEx,原型如下(注,测试系统为XPSP3,有人说vista之后创建进程不再使用这个内核函数,还未经测试)

 

 1 //NtCreateProcessEx原型:
2 NTSTATUS __stdcall NtCreateProcessEx(OUT PHANDLE ProcessHandle,
3 IN ACCESS_MASK DesiredAccess,
4 IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
5 IN HANDLE ParentProcess,
6 IN BOOLEAN InheritObjectTable,
7 IN HANDLE SectionHandle OPTIONAL,
8 IN HANDLE DebugPort OPTIONAL,
9 IN HANDLE ExceptionPort OPTIONAL,
10 IN BOOLEAN InJob
11 );

 

  NtCreateProcessEx函数的代码判断调用时状态是否为内核模式,内核模式不需要检查参数,而非内核模式要检查ProcessHandle参数代表的句柄是否可写,然后把真正的创建工作交给PspCreateProcess函数。创建EPROCESS对象等相关操作都在这个函数中进行。PspCreateProcess被三个函数调用,NtCreateProcessEx、PsCreateSystemProcess、PspInitPhase。其中PspInitPhase是在系统初始化的早期调用的,它创建的进程的句柄保存在全局变量PspInitialSystemProcess中。PsCreateSystemProcess可用于创建系统进程对象,它创建的进程都是PspInitialSystemProcess的子进程。所以,PspCreateProcess函数负责创建系统的所有进程,包括System进程。这里不再深究暂时,如果不出意外一般运用程序创建进程都是通过NtCreateProcessEx,其它的先不去理会。

  这里沿着作者差不多的思想,大概要做的事情是,HOOK住NtCreateProcessEx,在每个程序启动时,中断下来(不分青红皂白的中断,为了简单,不去搞MD5监测了),然后向用户弹窗,两个按钮,“启动”和“禁止”,然后根据用户的选择,来继续执行原来的NtCreateProcessEx还是直接返回NULL。要传递当前进程的文件路径给用户,首先需要获取它,关注参数是:SectionHandle。其原理是:

  通过SectionHandle得到SectionObject,即指向_SECTION_OBJECT结构的指针。通过_SECTION_OBJECT得到SegmentObject成员,指向一个_SEGMENT_OBJECT结构。_SEGMENT_OBJECT结构的BaseAddress成员,指向一个_CONTROL_AREA结构。这个_CONTROL_AREA结构的FilePointer成员,就是指向描述文件的_FILE_OBJECT结构的指针。_FILE_OBJECT结构的DeviceObject成员中得到所在盘符(调用RtlVolumeDeviceToDosName),加上FileName成员指定的路径,就是这个文件的全路径。各结构体的信息可从windbg中查看。

  获得文件路径以后,给运用程序通知包,并等待用户响应,根据判断做出两种可能的选择。对于继续执行很好办,之前的HOOK一直在做这方面的事情;另一种情况是禁止进程的创建,我开始一直尝试返回各种错误,结果R3总是会有错误提示弹出框,直到返回0x80070000。这个值是网络上查找到的,给的解释是:如果内核返回0x80070000,错误代码就会被转换成0,应用层使用GetLastError()获得的返回值就是0,因为0表示没有错误,所以就不会弹出错误对话框。

  此外,之前提过的两种HOOK,一种是在函数入口为绝对跳转指令时修改它(适用于特殊情况);还有一种是inlinehook,它拷贝函数入口代码,而写入绝对跳转指令(在入口代码7字节内有相对跳转则要特殊处理);现在采用另一种HOOk方法,修改SSDT表,实现跳转。关于SSDT表有哪些函数,以及函数的序号是什么,自行谷歌。这里需要知道的只有一个,NtCreateProcessEx在SSDT表中的下标是48(十进制)。其它细节方面的内容,交给代码注释吧,下面是完整代码:

 

  1 #include "xde.h"
2 #include <ntddk.h>
3 //#include <wdmsec.h>
4 //#pragma comment(lib,"wdmsec.lib")
5
6 #define INITCODE code_seg("INIT") /*指的代码运行后 就从内存释放掉*/
7 #define PAGECODE code_seg("PAGE") /*表示内存不足时,可以被置换到硬盘*/
8
9 #define DEVNAME L"\\Device\\testDDk_Device"
10 #define SYMNAME L"\\??\\TestLinkName"
11
12 #define MAXPATHLEN 1024
13
14 //IRP_MJ_DEVICE_CONTROL类型IRP用于R3运用程序写,内核读数据时的控制码宏定义
15 #define MY_DVC_IN_CODE (ULONG)CTL_CODE(\
16 FILE_DEVICE_UNKNOWN,/*设备类型*/\
17 0xa01,/*用户自定义功能号*/\
18 METHOD_BUFFERED,/*传输方式:带缓冲区*/\
19 FILE_WRITE_DATA/*调用者需求权限:文件写*/)
20
21 //IRP_MJ_DEVICE_CONTROL类型IRP用于R3运用程序读,内核写数据时的控制码宏定义
22 #define MY_DVC_OUT_CODE (ULONG)CTL_CODE(\
23 FILE_DEVICE_UNKNOWN,/*设备类型*/\
24 0xa02,/*用户自定义功能号*/\
25 METHOD_BUFFERED,/*传输方式:带缓冲区*/\
26 FILE_READ_DATA/*调用者需求权限:文件读*/)
27
28 #define DELAY_ONE_MICROSECOND (-10)
29 #define DELAY_ONE_MILLISECOND (DELAY_ONE_MICROSECOND*1000)
30
31 typedef unsigned long ULONG;
32 typedef ULONG* PULONG;
33
34 //SSDT获取进程名相关
35 typedef struct _SYSTEM_SERVICE_TABLE
36 {
37 PVOID *ServiceTableBase;
38 PULONG *ServiceCounterTableBase;
39 ULONG NumberOfServices;
40 ULONG ParamTableBase;
41 }SSDT, *PSSDT;
42
43
44 __declspec(dllimport) SSDT KeServiceDescriptorTable;
45
46 //要交互的数据
47 static char gFullFilePath[MAXPATHLEN] = {0};//用于保存文件名,R0到R3
48 static BOOLEAN gResult;//用户的判断结果,继续执行时为TRUE
49
50 //全局事件变量
51 static KEVENT g_my_notify_event;//当有进程即将被创建时通知用户
52 static KEVENT g_my_continue_event;//当用户选择完毕后继续执行
53
54 //用于线程等待,由于__declspec(naked)类型函数不能定义局部变量,所以采用全局变量
55 static LARGE_INTEGER timeout;
56
57
58 //函数声明
59 BOOLEAN InlineHookIofCallDriverXp(IN PUNICODE_STRING pFuncName,//要HOOK的API名称
60 IN ULONG uMyFuncAddr,//自定义的中继函数地址
61 IN BOOLEAN bHookOrUnhook//是否HOOK
62 );//InlineHook
63 MyIofCallDriver(void);//自定义InlineHook中继函数
64 MyNtCreateProcessEx(void);//SDDT HOOK NtCreateProcessEx
65 void MySleep(LONG msec);//sleep函数
66 NTSTATUS GetFullName(IN HANDLE KeyHandle,OUT char *fullname);//获取文件完整路径
67 BOOLEAN SDDTHookNtCreateProcessExXp(IN BOOLEAN bHookOrUnhook);//修改sddt跳转表实现hook NtCreateProcess
68 NTSTATUS CreateMyDevice (IN PDRIVER_OBJECT pDriverObject);//创建设备对象与符号链接
69 NTSTATUS ddk_DispatchRoutine_CONTROL(IN PDEVICE_OBJECT pDevobj,IN PIRP pIrp);//派遣函数
70 DRIVER_INITIALIZE DriverEntry;//入口,添加设备,启动HOOk
71 DRIVER_UNLOAD DriverUnload;//卸载,删除设备,停止HOOk
72
73 //全局变量
74 ULONG gDestCodeAddr = 0;//旧代码保存区域的地址
75 ULONG gBackCodeAddr = 0;//跳回继续执行地址
76 UNICODE_STRING apiFuncName;//要HOOK的API名字
77
78 /////////////////////////////////////////////////////////////////
79 #pragma PAGECODE
80 //DriverEntry,入口函数。相当于main
81 NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObject, PUNICODE_STRING sRegPath)
82 {
83
84 HANDLE hThread = NULL;
85 NTSTATUS status;
86 /*
87 //对ZwCreateSection进行HOOK
88 RtlInitUnicodeString(&apiFuncName,L"NtCreateProcess");
89 if (InlineHookIofCallDriverXp(&apiFuncName,(ULONG)MyIofCallDriver,TRUE) == FALSE)
90 {
91 KdPrint(("Inline Hook failed!\n"));
92 }*/
93 SDDTHookNtCreateProcessExXp(TRUE);
94
95
96 //创建设备对象与符号链接
97 CreateMyDevice (pDriverObject);
98 //派遣函数
99 pDriverObject->MajorFunction[IRP_MJ_CREATE]=ddk_DispatchRoutine_CONTROL;//IRP_MJ_CREATE相关IRP处理函数
100 pDriverObject->MajorFunction[IRP_MJ_CLOSE]=ddk_DispatchRoutine_CONTROL;//IRP_MJ_CREATE相关IRP处理函数
101 pDriverObject->MajorFunction[IRP_MJ_READ]=ddk_DispatchRoutine_CONTROL;//IRP_MJ_CREATE相关IRP处理函数
102 pDriverObject->MajorFunction[IRP_MJ_CLOSE]=ddk_DispatchRoutine_CONTROL;//IRP_MJ_CREATE相关IRP处理函数
103 pDriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL]=ddk_DispatchRoutine_CONTROL; //IRP_MJ_CREATE相关IRP处理函数
104
105 //初始化信号量
106 KeInitializeEvent(&g_my_notify_event,
107 SynchronizationEvent,//自动复位
108 FALSE//初始为无信号状态
109 );
110 KeInitializeEvent(&g_my_continue_event,
111 SynchronizationEvent,//自动复位
112 FALSE//初始为无信号状态
113 );
114 //初始化timeout
115 timeout.QuadPart = 0;
116
117 pDriverObject->DriverUnload = DriverUnload;
118 return STATUS_SUCCESS;
119 }
120
121
122 //提供一个Unload函数值是为了让这个程序能动态卸载,方便调试
123 VOID DriverUnload(PDRIVER_OBJECT pDriverObject)
124 {
125 PDEVICE_OBJECT pDev;//用来取得要删除设备对象
126 UNICODE_STRING symLinkName; //
127 //打印一句
128 KdPrint(("Our driver is unloading...\n"));
129 /*
130 //停止HOOk
131 if(InlineHookIofCallDriverXp(&apiFuncName,(ULONG)MyIofCallDriver,FALSE) == FALSE)
132 {
133 KdPrint(("UnHook failed!\n"));
134 }
135 KdPrint(("UnHook succeed!\n"));
136 */
137
138 SDDTHookNtCreateProcessExXp(FALSE);
139
140 pDev=pDriverObject->DeviceObject;//从驱动对象取得设备对象,所有设备对象连成一条链,这里假定只有一个设备
141 IoDeleteDevice(pDev); //删除设备
142
143
144 RtlInitUnicodeString(&symLinkName,SYMNAME);
145
146 //删除符号链接
147 IoDeleteSymbolicLink(&symLinkName);
148 KdPrint(("驱动成功被卸载... ")); //sprintf,printf
149 //取得要删除设备对象
150 //删掉所有设备
151
152 }
153
154 //读取IRP_MJ_DEVICE_CONTROL类型IRP包中的数据信息:驱动读
155 NTSTATUS MyDeviceIoCtrlIn(PIRP pIrp)
156 {
157 PIO_STACK_LOCATION irpsp = IoGetCurrentIrpStackLocation(pIrp);
158 //得到输入缓存长度
159 ULONG in_len = irpsp->Parameters.DeviceIoControl.InputBufferLength;
160 //输入输出缓冲是公用内存的
161 PVOID buffer = pIrp->AssociatedIrp.SystemBuffer;
162
163 //输出缓冲区信息
164 gResult = *((BOOLEAN *)buffer);
165
166 //设置有信号:通知内核用户判断完毕
167 KeSetEvent(&g_my_continue_event,
168 IO_NO_INCREMENT,//预备给被唤醒线程临时提升优先级的增量
169 FALSE//之后是否跟KeWaitForXXX等待(其它或者自身)事件
170 );
171
172 pIrp->IoStatus.Information = sizeof(BOOLEAN);//设置IRP读取的字节数
173 pIrp->IoStatus.Status=STATUS_SUCCESS;//设置IRP处理状态
174 IoCompleteRequest(pIrp,IO_NO_INCREMENT);//指示完成此IRP的处理
175 return STATUS_SUCCESS; //返回成功,这样,发起I/O操作的Win32API将会返回TRUE,使用GetLastError和设置的IPR处理状态一致
176 }
177
178 //读取IRP_MJ_DEVICE_CONTROL类型IRP包中的数据信息:驱动写
179 NTSTATUS MyDeviceIoCtrlOut(PIRP pIrp)
180 {
181 PIO_STACK_LOCATION irpsp = IoGetCurrentIrpStackLocation(pIrp);
182 //获得输出缓冲区
183 PVOID buffer = pIrp->AssociatedIrp.SystemBuffer;
184 //获取输出缓冲区长度
185 ULONG out_len = irpsp->Parameters.DeviceIoControl.OutputBufferLength;
186
187 //等待有事件信号
188 while(KeWaitForSingleObject(&g_my_notify_event,
189 Executive,//等待原因,驱动程序设置为Executive
190 KernelMode,//内核模式
191 FALSE,//不允许警告
192 &timeout//立即返回
193 ) == STATUS_TIMEOUT)
194 {
195 //当检测无信号时,让出时间片一段时间
196 //这里测试sleep不能解决系统很卡的问题,暂时无解
197 MySleep(100);
198 }
199
200 //双方缓冲区都必须分配为最大路径
201 //长度足够,填写输出缓冲区
202 memcpy(buffer,(PVOID)&gFullFilePath,MAXPATHLEN);
203
204
205 //返回成功
206 pIrp->IoStatus.Information = MAXPATHLEN;//写入的长度
207 pIrp->IoStatus.Status = STATUS_SUCCESS;
208 IoCompleteRequest(pIrp,IO_NO_INCREMENT);//指示完成此IRP的处理
209 return STATUS_SUCCESS; //返回成功,这样,发起I/O操作的Win32API将会返回TRUE,使用GetLastError和设置的IPR处理状态一致
210
211 }
212
213
214 //分类处理
215 NTSTATUS ddk_DispatchRoutine_CONTROL(IN PDEVICE_OBJECT pDevobj, IN PIRP pIrp)
216 {
217 //IoGetCurrentIrpStackLocation得到调用者堆栈的指针
218 PIO_STACK_LOCATION irpsp = IoGetCurrentIrpStackLocation(pIrp);
219
220 //判断IRP请求包的类型
221 switch(irpsp->MajorFunction)
222 {
223 case IRP_MJ_CREATE://处理打开请求(CreateFile)
224 KdPrint(("IRP_MJ_CREATE\n"));
225 break;
226 case IRP_MJ_CLOSE://处理关闭请求(CloseHandle)
227 KdPrint(("IRP_MJ_CLOSE\n"));
228 break;
229 case IRP_MJ_READ://处理读请求(ReadFile)
230 KdPrint(("IRP_MJ_READ\n"));
231 break;
232 case IRP_MJ_WRITE://处理写请求(WriteFile)
233 KdPrint(("IRP_MJ_WRITE\n"));
234 break;
235 case IRP_MJ_DEVICE_CONTROL://处理设备控制信息(DeviceIoControl)
236 {
237 //首先得到功能号
238 ULONG code = irpsp->Parameters.DeviceIoControl.IoControlCode;
239 KdPrint(("IRP_MJ_DEVICE_CONTROL\n"));
240 if ( code == MY_DVC_IN_CODE )//内核读取数据
241 {
242 KdPrint(("MY_DVC_IN_CODE\n"));
243 return MyDeviceIoCtrlIn(pIrp);
244 }
245 else if ( code == MY_DVC_OUT_CODE)//内核传出数据
246 {
247 KdPrint(("MY_DVC_OUT_CODE\n"));
248 return MyDeviceIoCtrlOut(pIrp);
249 }
250 break;
251 }
252 default:
253 KdPrint(("其它处理\n"));
254 }
255
256 //判断是输入还是输出操作
257
258
259 pIrp->IoStatus.Information=0;//设置IRP操作的字节数为0,这里无实际意义
260 pIrp->IoStatus.Status=STATUS_SUCCESS;//设置IRP处理状态
261 IoCompleteRequest(pIrp,IO_NO_INCREMENT);//指示完成此IRP的处理
262 return STATUS_SUCCESS; //返回成功,这样,发起I/O操作的Win32API将会返回TRUE,使用GetLastError和设置的IPR处理状态一致
263 }
264
265 /////////////////////////////////////////////////////////////////
266 #pragma INITCODE
267 NTSTATUS CreateMyDevice (IN PDRIVER_OBJECT pDriverObject)
268 {
269 NTSTATUS status;
270 PDEVICE_OBJECT pDevObj;/*用来返回创建设备结构体的指针*/
271 //设备对象(DEVICE_OBJECT)由驱动创建。一个驱动可以创建多个设备对象。
272 //通过驱动对象(DRIVER_OBJECT),可以找到由该驱动创建的所有设备对象。
273 //一个驱动创建的所有设备对象链成一条链。该驱动的驱动对象可以找到这个链,
274 //一个设备对象也可以找到创建它的驱动的驱动对象。DEVICE_OBJECT是设备对象存在的形式
275
276 //创建设备名称
277 UNICODE_STRING devName;
278 UNICODE_STRING symLinkName; // 结构体,包含了宽字节字符缓冲区与其长度
279 //UNICODE_STRING sddl = RTL_CONSTANT_STRING(L"D:P(A;;GA;;;WD)");//权限描述符字串
280 //const GUID MYGUID_CLASS_MYCDO = {0x26e0d1e0L,0x8189,0x12e0,{0x99,0x14,0x88,0x00,0x22,0x30,0x19,0x13}};
281
282 RtlInitUnicodeString(&devName,DEVNAME);//对devName初始化字串为 "\\Device\\testDDK_Device"
283 //这个宽字节的路径“\\Device\\ ”部分不能改变,后面是设备名称
284
285 //创建设备对象
286 status = IoCreateDevice/*Secure*/( pDriverObject, //驱动程序对象指针。在入库函数DriverEntry过程里接收
287 0,//指定驱动程序为设备扩展对象定义的结构体大小
288 &devName,//设备名称,必须是完整的设备路径名,设置为NULL则是无名设备
289 FILE_DEVICE_UNKNOWN,//设备类型
290 0, TRUE,//驱动程序的其它信息以及指定设备是否是独占的,TRUE则是
291 //&sddl,(LPCGUID)&MYGUID_CLASS_MYCDO,//sddl,guid
292 &pDevObj);//输出,用来保存PDEVICE_OBJECT结构体指针,这个指针指向设备对象自身
293 if (!NT_SUCCESS(status))
294 {
295 if (status==STATUS_INSUFFICIENT_RESOURCES)
296 {
297 KdPrint(("资源不足 STATUS_INSUFFICIENT_RESOURCES"));
298 }
299 if (status==STATUS_OBJECT_NAME_EXISTS )
300 {
301 KdPrint(("指定对象名存在"));
302 }
303 if (status==STATUS_OBJECT_NAME_COLLISION)
304 {
305 KdPrint(("//对象名有冲突"));
306 }
307 KdPrint(("设备创建失败...++++++++"));
308 return status;
309 }
310 KdPrint(("设备创建成功...++++++++"));
311 // IoCreateDevice 会把新创建的这个设备对象,链入驱动的设备链中
312
313 pDevObj->Flags |= DO_BUFFERED_IO;
314 //创建符号链接
315
316
317 RtlInitUnicodeString(&symLinkName,SYMNAME);
318
319 status = IoCreateSymbolicLink( &symLinkName,// Unicode字符串指针,是一个用户态可见的名称
320 &devName );// Unicode字符串指针,是驱动程序创建的设备对象名称。
321 if (!NT_SUCCESS(status)) /*status等于0*/
322 {
323 IoDeleteDevice( pDevObj );//删除驱动设备
324 return status;
325 }
326 return STATUS_SUCCESS;
327 }
328
329 ///////////////////////////////////////////////////////////////////
330 #pragma PAGECODE
331 //修改sddt跳转表实现hook NtCreateProcessEx
332 BOOLEAN SDDTHookNtCreateProcessExXp(IN BOOLEAN bHookOrUnhook)
333 {
334 PULONG SSDTBASE;
335 SSDTBASE = (PULONG) KeServiceDescriptorTable.ServiceTableBase;
336
337 //CR0的WP位进行清0
338 __asm{
339 cli
340 push eax
341 mov eax,cr0
342 and eax,0xfffeffff
343 mov cr0,eax
344 pop eax
345 }
346
347 if ( bHookOrUnhook )
348 {
349 gBackCodeAddr = SSDTBASE[48];//在跳转表中的47
350 SSDTBASE[48] = (ULONG)MyNtCreateProcessEx;
351 }
352 else
353 {
354 SSDTBASE[48] = gBackCodeAddr;
355 }
356
357 //恢复CR0
358 __asm{
359 push eax
360 mov eax,cr0
361 or eax,0x10000
362 mov cr0,eax
363 pop eax
364 sti
365 }
366 }
367
368 //对xp的IofCallDriver函数进行inline hook
369 //成功返回TRUE,失败返回FALSE
370 BOOLEAN InlineHookIofCallDriverXp(IN PUNICODE_STRING pFuncName,//要HOOK的API名称
371 IN ULONG uMyFuncAddr,//自定义的中继函数地址
372 IN BOOLEAN bHookOrUnhook//是否HOOK
373 )
374 {
375 //局部变量
376 ULONG uHookApiAddr = 0;//被HOOK的API起始地址
377 ULONG start_address = 0;//解析指令时一条指令的起始地址
378 size_t length = 0;//解析指令返回的实际长度
379 size_t total_length = 0;//源API中要被转移的指令的长度
380 size_t myjmp_length = 0;//汇编JMP指令返回的实际长度
381 struct xde_instr code_instr = {0};//指令结构体
382 struct xde_instr myjmp = {0};//JMP指令结构体
383 unsigned char code[12];//使用较大空间容纳JMP指令
384 //KIRQL irql;//用于提高中断级
385
386 //目标API入口地址(微软未公开函数不一定能获取到)
387 __asm{
388 int 3
389 }
390 uHookApiAddr = (ULONG)MmGetSystemRoutineAddress(pFuncName);
391 if ( (PVOID)uHookApiAddr == NULL )
392 {
393 return FALSE;
394 }
395 //写入绝对跳转指令到结构体
396 myjmp.opcode = 0xea;//写操作码(jmp机器指令)
397 myjmp.addrsize = 4;//地址长度为4个字节
398 myjmp.datasize = 2;//段选择子作为立即数,长2个字节
399 myjmp.addr_d[0] = uMyFuncAddr;//填写要跳转的地址(自定义函数)
400 myjmp.data_w[0] = 8;//段选择子。内核代码段总是为8
401 myjmp_length = xde_asm(code,&myjmp);//汇编一条指令
402
403 //解析要HOOK的API开头须移动多少字节的指令
404 if ( bHookOrUnhook == TRUE )//HOOK
405 {
406 start_address = uHookApiAddr;//一条指令的起始地址
407 }
408 else//UnHook
409 {
410 start_address = gBackCodeAddr;//一条指令的起始地址
411 }
412 //要是起始地址为空,则失败
413 if ( start_address == 0 )
414 {
415 return FALSE;
416 }
417 while ( total_length < myjmp_length )//当长度不足以用来填充JMP指令时
418 {
419 //解析一条指令
420 length = xde_disasm((unsigned char *)start_address,&code_instr);
421 if( length == 0 )//解析失败
422 {
423 return FALSE;
424 }
425 //计算已经反汇编的指令总长度
426 total_length += length;
427 //解析指令起始地址移动
428 start_address += length;
429 }
430 gBackCodeAddr = start_address;//保存跳回的地址
431
432 //提高中断级,禁止线程切换,发生问题
433 //要求当前函数的IRQL <= DISPATCH_LEVEL,我这里报错
434 //irql = KeRaiseIrqlToDpcLevel();
435
436 //CR0的WP位进行清0
437 __asm{
438 cli //中断级无法改变,改为关闭中断写法
439 mov eax,cr0
440 push eax
441 and eax,0xfffeffff
442 mov cr0,eax
443 }
444
445 if ( bHookOrUnhook == TRUE )//HOOK
446 {
447
448 //在IofCallDriver开头写入跳转指令之前,需要先转移原来的代码
449 if ( gDestCodeAddr == 0 )//目标地址未获取
450 {
451 MyIofCallDriver();//进行获取
452 }
453 __asm{
454 mov esi,uHookApiAddr //IofCallDriver的原来起始地址(源地址)
455 mov edi,gDestCodeAddr //要拷贝到的地址(目的地址)
456 mov ecx,total_length //总长度
457 cld //清方向标志位,也就是使DF的值为0
458 rep movsb //拷贝(字符串传送指令,按字节传送数据)
459 }
460
461 //写入绝对跳转指令JMP
462 __asm{
463 lea esi,code //存放JMP指令的缓冲区(源地址)
464 mov edi,uHookApiAddr //要拷贝到的地址(目的地址)
465 mov ecx,myjmp_length //总长度
466 cld //清方向标志位,也就是使DF的值为0
467 rep movsb //拷贝(字符串传送指令,按字节传送数据)
468 }
469
470 }
471 else//UnHOOK
472 {
473 //判断旧代码保存区域地址是否为空
474 if ( gDestCodeAddr == 0 )
475 {
476 //恢复CR0
477 __asm{
478 pop eax
479 mov cr0,eax
480 sti
481 }
482 return FALSE;
483 }
484 //将旧代码保存区域的旧代码复制回目标API起始地址
485 __asm{
486 mov esi,gDestCodeAddr //旧代码保存区起始地址(源地址)
487 mov edi,uHookApiAddr //API起始地址(目的地址)
488 mov ecx,total_length //总长度
489 cld //清方向标志位,也就是使DF的值为0
490 rep movsb //拷贝(字符串传送指令,按字节传送数据)
491 }
492 }
493
494
495 //恢复CR0
496 __asm{
497 pop eax
498 mov cr0,eax
499 sti
500 }
501
502 //降低中断级
503 //KeLowerIrql(irql);
504
505 return TRUE;
506 }
507
508
509
510 static __declspec(naked) MyNtCreateProcessEx(void)
511 {
512 //首先将所有通用寄存器压入栈保存
513 __asm{
514 pushad
515 }
516
517 //下面写入自己要做的事情:查找文件名
518 //获取文件名
519 __asm{
520 push offset gFullFilePath
521 push [ebp + 0x1C] //第6个参数,偏移5*6 + 8 = 38 = 0x1C
522 call GetFullName
523 }
524
525 //设置有信号:通知用户有新进程被创建
526 KeSetEvent(&g_my_notify_event,
527 IO_NO_INCREMENT,//预备给被唤醒线程临时提升优先级的增量
528 FALSE//之后是否跟KeWaitForXXX等待(其它或者自身)事件
529 );
530
531 //等待有事件信号:等待用户判断完毕
532 while(KeWaitForSingleObject(&g_my_continue_event,
533 Executive,//等待原因,驱动程序设置为Executive
534 KernelMode,//内核模式
535 FALSE,//不允许警告
536 &timeout//立即返回
537 ) == STATUS_TIMEOUT)
538 {
539 //当检测无信号时,让出时间片一段时间
540 //这里测试sleep不能解决系统很卡的问题,暂时无解
541 MySleep(100);
542 }
543
544 if ( gResult == FALSE )//如果是阻止进程
545 {
546 __asm{
547 popad //恢复现场
548 mov eax,0x80070000
549 ret 0x24 //9个参数,36个字节
550 }
551 }
552 //否则,继续运行进程
553 __asm{
554 //恢复现场
555 popad
556 }
557 //跳回原来的地方继续执行
558 __asm{
559 jmp gBackCodeAddr
560 }
561 }
562
563
564 static __declspec(naked) MyIofCallDriver(void)
565 {
566
567 //判断标号是否被读出
568 if( gDestCodeAddr == 0 )
569 {
570 //将标号old_codes标号地址放入gDestCodeAddr
571 __asm{
572 push old_codes
573 pop gDestCodeAddr
574 ret
575 }
576 }
577
578 //首先将所有通用寄存器压入栈保存
579 __asm{
580 pushad
581 }
582
583 //下面写入自己要做的事情:查找文件名
584 //获取文件名
585 __asm{
586 push offset gFullFilePath
587 push [ebp + 0x1C]
588 call GetFullName
589 }
590
591
592 //下面为存放旧代码区域
593 __asm{
594 //恢复现场
595 popad
596 old_codes:
597 //写入足够多的nop,以够容纳旧代码
598 nop
599 nop
600 nop
601 nop
602 nop
603 nop
604 nop
605 nop
606 nop
607 nop
608 nop
609 nop
610 nop
611 nop
612 nop
613 nop
614 nop
615 nop
616 nop
617 }
618 //跳回原来的地方继续执行
619 __asm{
620 jmp gBackCodeAddr
621 }
622 }
623
624
625 //获取文件完整路径,传入:SectionHandle,传出:char *缓冲区指针
626 NTSTATUS __stdcall GetFullName(IN HANDLE KeyHandle,OUT char *fullname)
627 {
628 NTSTATUS ns;
629 PVOID pKey = NULL,pFile = NULL;
630 UNICODE_STRING fullUniName;
631 ANSI_STRING akeyname;
632 ULONG actualLen;
633 UNICODE_STRING dosName;
634
635 fullUniName.Buffer=NULL;
636 fullUniName.Length=0;
637 fullname[0]=0x00;
638 //根据提供的Handle值得到Object,并增加引用计数
639 ns = ObReferenceObjectByHandle(
640 IN KeyHandle,//对象句柄
641 IN 0,//对象需求权限
642 IN NULL,//对象类型
643 IN KernelMode,//内核模式
644 OUT &pKey,//获取到的对象指针
645 OUT NULL//驱动设置这个为NULL
646 );
647 if( !NT_SUCCESS(ns) )
648 return ns;
649
650 //分配内存
651 fullUniName.Buffer = ExAllocatePool( PagedPool, MAXPATHLEN*2);//1024*2
652 fullUniName.MaximumLength = MAXPATHLEN*2;
653
654 __try
655 {
656 pFile=(PVOID)*(ULONG *)((char *)pKey + 20);//SECTION_OBJECT结构体的Segment域(SEGMENT_OBJECT结构体指针)
657 pFile=(PVOID)*(ULONG *)((char *)pFile);//SEGMENT_OBJECT结构体的BaseAddress域(CONTROL_AREA结构体指针)
658 pFile=(PVOID)*(ULONG *)((char *)pFile+36);//CONTROL_AREA结构的FilePointer域(FILE_OBJECT结构体指针)
659
660 //增加由对象指针指代对象的指针引用计数
661 ObReferenceObjectByPointer(
662 IN pFile,//对象指针
663 IN 0,//对象需求权限
664 IN NULL,//对象类型
665 IN KernelMode//内核模式
666 );
667 //取得文件对象所在盘符
668 RtlVolumeDeviceToDosName(
669 ((PFILE_OBJECT)pFile)->DeviceObject,//指向一个卷驱动设备对象
670 &dosName//用于保存盘符宽字节缓冲
671 );
672 RtlCopyUnicodeString(&fullUniName,&dosName);
673 //获取完整路径
674 RtlAppendUnicodeStringToString(&fullUniName,&((PFILE_OBJECT)pFile)->FileName);
675
676 //降低引用计数
677 ObDereferenceObject(pFile);
678 ObDereferenceObject(pKey);
679
680 //宽字节转窄字节
681 RtlUnicodeStringToAnsiString( &akeyname,&fullUniName,TRUE);
682 if(akeyname.Length < MAXPATHLEN)
683 {
684 memcpy(fullname,akeyname.Buffer,akeyname.Length);
685 fullname[akeyname.Length]=0x00;
686 }
687 else
688 {
689 memcpy(fullname,akeyname.Buffer,MAXPATHLEN);
690 fullname[MAXPATHLEN-1]=0x00;
691 }
692
693 //释放内存
694 RtlFreeAnsiString( &akeyname );
695 ExFreePool(dosName.Buffer);
696 ExFreePool(fullUniName.Buffer);
697
698 return STATUS_SUCCESS;
699
700 }
701 __except(1)
702 {
703 //异常,清理工作
704 if(fullUniName.Buffer) ExFreePool( fullUniName.Buffer );
705 if(pKey) ObDereferenceObject(pKey);
706 return STATUS_SUCCESS;
707 }
708 }
709
710 void MySleep(LONG msec)
711 {
712 LARGE_INTEGER my_interval;
713 my_interval.QuadPart = DELAY_ONE_MILLISECOND;
714 my_interval.QuadPart *= msec;
715 KeDelayExecutionThread(KernelMode,0,&my_interval);
716 }

  

  运用程序新建MFC工程,核心代码如下:

 

  1 #define SYMNAME "\\\\.\\TestLinkName"
2 #define MAXPATH 1024
3
4
5 #define MY_DVC_IN_CODE (ULONG)CTL_CODE(\
6 FILE_DEVICE_UNKNOWN,/*设备类型*/\
7 0xa01,/*用户自定义功能号*/\
8 METHOD_BUFFERED,/*传输方式:带缓冲区*/\
9 FILE_WRITE_DATA/*调用者需求权限:文件读*/)
10
11 #define MY_DVC_OUT_CODE (ULONG)CTL_CODE(\
12 FILE_DEVICE_UNKNOWN,/*设备类型*/\
13 0xa02,/*用户自定义功能号*/\
14 METHOD_BUFFERED,/*传输方式:带缓冲区*/\
15 FILE_READ_DATA/*调用者需求权限:文件读*/)
16
17 DWORD MyThread(CMFCTestDlg *p);
18
19 //点击测试按钮
20 void CMFCTestDlg::OnBnClickedButton1()
21 {
22 // TODO: 在此添加控件通知处理程序代码
23
24 if ( thread == NULL )//测试线程未开启
25 {
26 //隐藏界面
27 ShowWindow(SW_HIDE);
28 bshow = TRUE;
29 running = TRUE;
30 TestBtn.SetWindowText(_T("停止测试"));
31 StartBtn.ShowWindow(SW_SHOW);
32 StopBtn.ShowWindow(SW_SHOW);
33 //启动测试线程
34 thread = CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)MyThread,this,NULL,NULL);
35 }
36 else
37 {
38 //退出测试线程
39 running = FALSE;
40 bshow = FALSE;
41 result = TRUE;
42 //等待超时则强行结束
43 if(WaitForSingleObject(thread,10000) == WAIT_TIMEOUT)
44 TerminateThread(thread,0);
45 //等待线程退出
46 CloseHandle(thread);
47 thread = NULL;
48 ShowWindow(SW_SHOW);
49 StartBtn.ShowWindow(SW_HIDE);
50 StopBtn.ShowWindow(SW_HIDE);
51 TestBtn.SetWindowText(_T("开始测试"));
52 }
53 }
54
55 //启动按钮
56 void CMFCTestDlg::OnBnClickedButton2()
57 {
58 // TODO: 在此添加控件通知处理程序代码
59 bshow = FALSE;
60 result = TRUE;
61 }
62
63 //禁止按钮
64 void CMFCTestDlg::OnBnClickedButton3()
65 {
66 // TODO: 在此添加控件通知处理程序代码
67 bshow = FALSE;
68 result = FALSE;
69 }
70
71
72 DWORD MyThread(CMFCTestDlg *p)
73 {
74
75 char filepath[MAXPATH] = {0};//文件路径缓冲
76 DWORD length = 0;//接受缓冲区数据长度
77
78 HANDLE device = CreateFile(SYMNAME,
79 GENERIC_READ|GENERIC_WRITE,0,0,
80 OPEN_EXISTING,
81 FILE_ATTRIBUTE_SYSTEM,0);
82
83 if ( device == INVALID_HANDLE_VALUE )
84 {
85 //打开失败
86 AfxMessageBox(_T("打开符号链接失败"));
87 return FALSE;
88 }
89
90 while ( p->running )//无限循环
91 {
92 //等待有新进程被创建
93 BOOL ret = DeviceIoControl(device,
94 MY_DVC_OUT_CODE, //功能号
95 NULL, //没有输入缓冲,要传递的信息,预先填好
96 NULL, //输入缓冲长度为
97 &filepath, //输出缓冲
98 MAXPATH, //输出缓冲长度
99 &length, //返回的长度
100 NULL);
101 if (!ret)
102 {
103 // DeviceIoControl失败
104 AfxMessageBox(_T("DeviceIoControl失败"));
105 }
106
107 CString sFilePath;
108 sFilePath.Format("文件路径:%s",filepath);
109 p->ShowWindow(SW_SHOW);//显示界面
110 p->FilePathText.SetWindowText(sFilePath);
111
112 //等待用户判断完毕,通知内核操作
113 while( p->bshow == TRUE ) Sleep(500);
114 p->ShowWindow(SW_HIDE);//隐藏界面
115 p->bshow = TRUE;
116 ret = DeviceIoControl(device,
117 MY_DVC_IN_CODE, //功能号
118 &p->result, //输入缓冲,要传递的信息,预先填好
119 sizeof(BOOL), //输入缓冲长度
120 NULL, //没有输出缓冲
121 NULL, //输出缓冲长度为
122 &length, //返回的长度
123 NULL);
124 if (!ret)
125 {
126 // DeviceIoControl失败
127 AfxMessageBox(_T("DeviceIoControl失败"));
128 }
129
130 }
131 CloseHandle(device);
132 return TRUE;
133 }

 

  运行效果(为了压缩GIF图片,所以画面比较模糊):

   《天书夜读:从汇编语言到windows内核编程》十六 反病毒、木马开发实例

  第一:测试步骤为,先启动运用程序,再加载驱动,让驱动运行,再点击“开始测试”。顺序错误可能蓝屏。

  第二:在等待用户响应的时候系统会变得很卡很卡。

   对存在的问题不想再深究了,要学的基本学了,剩下的是工程技术问题,不是知识问题。对于系统卡的问题,想到的一个解决方案是Hook住NtCreateProcessEx以后立即返回,并禁止程序启动,而在用户判断完毕并发送IRP包之后,如果选择要启动进程,才重启进程;如果是禁止运行,那什么也不做。这个方案可能对带命令行的启动会有问题,未测。

  运用程序这边点击停止测试无响应问题,我想其本质还是跟上述同一个原因。

 

 

3)此外,在挂钩NtCreateProcessEx而查找资料的过程中,发现有人说Hook NtCreateSection比Hook NtCreateProcessEx要好(作者是不是本来就想挂钩NtCreateSection的?我就没看过有谁挂钩过ZwCreateSection,真是奇怪),我找了下相关资料,win32系统在使用CreateFileMapping创建文件映像时,它会调用ZwCreateSection(Ntdll.dll),然后通过系统调用Ntoskrnl.exe中的NtCreateSection函数。这个未亲自跟踪测试,所以不保证正确。然后,NtCreateSection函数原型为:

 

1 NTSTATUS    NtCreateSection( 
2 OUT PHANDLE SectionHandle ,
3 IN ACCESS_MASK DesiredAccess ,
4 IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
5 IN PLARGE_INTEGER MaximumSize OPTIONAL,
6 IN ULONG SectionPageProtection ,
7 IN ULONG AllocationAttributes ,
8 IN HANDLE FileHandle OPTIONAL
9 );

 

  这里关注3个参数:SectionPageProtection与FileHandle同(1)中ZwCreateSection中的同名参数。而AlloCationAttributes在(1)中未提,而这里找到了一点信息:

 

参数

可执行模块

不可执行模块

SectionPageProtection

包括PAGE_EXECUTE

任何参数,可能包括PAGE_EXECUTE

AlloCationAttributes

包括SEG_IMAGE

不包括SEC_IMAGE

 

  所以,从这里来看,要确定一个可执行模块(注意:包括exe,dll等等)必须由两个参数同时确定,这么说的话,《天书夜读》作者又犯错了。我测试了一下挂钩NtCreateProcessEx与挂钩NtCreateSection的区别,发现前者只在exe文件加载时被调用;而后者不仅仅是exe主进程,其所有的dll文件等等可执行模块被加载时都会被调用,也就是说它可以对进程中的每个可执行模块分别进行检测。由此可以知道,挂钩后者的确是比较好的。此外,中继函数定义为__declspec(naked)类型,不能带有参数,局部变量与返回值,这样是为了保护现场,如果中继函数中自行定义的一些处理比较复杂,难以用汇编实现,可自行另外定义一个普通的处理函数,然后在汇中继函数中使用汇编方式调用它。下面是使用挂钩NtCreateSection方法时代码的变动:

  宏定义:

1 //#define TB_APIORDER 48          //NtCreateProcessEx
2 #define TB_APIORDER 50 //NtCreateSection
3 #define SEC_IMAGE 0x1000000 //可执行模块

  中继函数:

 1 //由MyNtCreateProcessEx改名为MyNtCreateSection
2 static __declspec(naked) MyNtCreateProcessEx(void)
3 {
4 //首先将所有通用寄存器压与将位标志寄存器EFLAGS压入堆栈保存
5 __asm{
6 pushfd
7 pushad
8 }
9
10 //判断是否是可执行模块,如果不是则直接执行旧代码
11 __asm{
12 mov eax,[ebp + 0x18] //SectionPageProtection为第五个参数,偏移*4 + 8 = 24 = 0x18
13 test eax,PAGE_EXECUTE //检测是否是可执行模块(exe,dll等等)
14 jz ExcuteOldCode //不是可执行模块则跳转
15 mov eax,[ebp + 0x1C] //AllocationAttributes为第六个参数,偏移*4 + 8 = 24 = 0x1C
16 test eax,SEC_IMAGE //继续过滤
17 jz ExcuteOldCode
18 }
19
20 //下面写入自己要做的事情:查找文件名
21 //获取文件名
22 __asm{
23 push offset gFullFilePath
24 //push [ebp + 0x1C] //SectionHandle为第个参数,偏移*4 + 8 = 28 = 0x1C
25 push [ebp + 0x20] //FileHandle为第个参数,偏移*4 + 8 = 32 = 0x20
26 call GetFullName
27 }
28
29 //下面检测可执行模块是不是为.exe后缀
30 __asm{
31 lea esi,gFullFilePath
32 xor eax,eax
33 lab: //移动到字符串尾部
34 mov al,[esi]
35 inc esi
36 test al,al
37 jnz lab
38 sub esi,2
39 //后退比较“exe”个字符
40 mov al,[esi]
41 xor al,'e'
42 jnz ExcuteOldCode
43 dec esi
44 mov al,[esi]
45 xor al,'x'
46 jnz ExcuteOldCode
47 dec esi
48 mov al,[esi]
49 xor al,'e'
50 jnz ExcuteOldCode
51 }
52
53 //设置有信号:通知用户有新进程被创建
54 KeSetEvent(&g_my_notify_event,
55 IO_NO_INCREMENT,//预备给被唤醒线程临时提升优先级的增量
56 FALSE//之后是否跟KeWaitForXXX等待(其它或者自身)事件
57 );
58
59 //等待有事件信号:等待用户判断完毕
60 while(KeWaitForSingleObject(&g_my_continue_event,
61 Executive,//等待原因,驱动程序设置为Executive
62 KernelMode,//内核模式
63 FALSE,//不允许警告
64 &timeout//立即返回
65 ) == STATUS_TIMEOUT)
66 {
67 //当检测无信号时,让出时间片一段时间
68 //这里测试sleep不能解决系统很卡的问题,暂时无解
69 MySleep(100);
70 }
71
72 if ( gResult == FALSE )//如果是阻止进程
73 {
74 //返回STATUS_ACCESS_DENIED:提示不是有效Win32程序
75 //返回STATUS_S:提示句柄无效
76 __asm{
77 popad
78 mov ebx, dword ptr[esp+8]
79 mov dword ptr[ebx],0 //将第一个传出参数值为
80 mov eax,0xC0000022 //写返回结果
81 popfd
82 ret 0x1C //7个参数
83 }
84
85 }
86 ExcuteOldCode:
87 //否则,继续运行进程
88 __asm{
89 //恢复现场
90 popad
91 popfd
92 }
93 //跳回原来的地方继续执行
94 __asm{
95 jmp gBackCodeAddr
96 }
97 }

  在NTSTATUS GetFullName(IN HANDLE KeyHandle,OUT char *fullname)函数中,注释下面几行:

1 //pFile=(PVOID)*(ULONG *)((char *)pKey + 20);//SECTION_OBJECT结构体的Segment域(SEGMENT_OBJECT结构体指针)
2 //pFile=(PVOID)*(ULONG *)((char *)pFile);//SEGMENT_OBJECT结构体的BaseAddress域(CONTROL_AREA结构体指针)
3 //pFile=(PVOID)*(ULONG *)((char *)pFile+36);//CONTROL_AREA结构的FilePointer域(FILE_OBJECT结构体指针)

  在注释位置添加:

1 pFile = pKey;

  此外,传入的不再是SectionHandle而是FileHandle。

  其它地方基本不用改,这里有个地方很坑的是,禁止以后无法阻止弹窗(在中继函数中)。所以NtCreateSection适合于进程的监控,在禁止进程运行上面,如果要阻止弹出,还要另想办法。

 

 

4)github完整项目地址:https://github.com/smilehao/windows_kenel_hook