首页
论坛
课程
招聘
[原创]【DLL注入编写与分析系列之二】x64平台PsSetCreateProcessNotifyRoutineEx之DLL注入
2022-2-10 22:34 12634

[原创]【DLL注入编写与分析系列之二】x64平台PsSetCreateProcessNotifyRoutineEx之DLL注入

2022-2-10 22:34
12634

【DLL注入编写与分析系列之一】x64平台SSDT HOOK之NtResumeThread注入

1、前言

今天的主题内容是PsSetCreateProcessNotifyRoutineEx的DLL注入,有一些关键点,很值得学习,还是老规矩,先说明几点:
1、今天是win7_sp1_x64平台下,同时注入x86、x64、Wow64程序;
2、重点不要放在代码上,要放在windows底层机制上,今天我会比之前的帖子说明底层机制更详细点,借用了老外的一些底层知识;
3、代码参考了《加密和解密》,在此感谢下先。因为不能直接在VS2019的x64位下编译,我修改了很多地方(shellcode重新写了),至于改了其他哪些地方,我就不记得了,不过现在可以用VS2019直接编译运行。

2、注入的基本思路

通过PsSetCreateProcessNotifyRoutineEx的回调函数,获取目标进程的相关信息后,修改目标进程ntdll.dll中的ZwTestAlert的入口代码,在系统调用ZwTestAlert时,就会执行shellcode,达到注入的目的。思路有了,现在要回答、解决2个关键问题:
1、为什么HOOK的是ZwTestAlert函数?系统加载PE流程是怎么样的?
2、ZwTestAlert地址不可写,怎么改变其属性?当然可以用ZwProtectVirtualMemory函数,但问题是,你知道用哪个ZwProtectVirtualMemory函数吗?

3、前提知识

3.1 相比SSDT HOOK的优势

很简单,不需要干掉page guard。这也是我要讲今天主题的一个原因。

3.2 PsSetCreateProcessNotifyRoutineEx函数介绍

和这个PsSetCreateProcessNotifyRoutineEx函数相似的,还有一个函数,就是PsSetCreateProcessNotifyRoutine,它们的区别是,前者可以阻止进程运行,后者只能监控进程的创建和退出。
PsSetCreateProcessNotifyRoutineEx的回调函数声明为:

1
VOID MyCreateProcessNotifyRoutine(__inout PEPROCESS  Process, __in HANDLE  ProcessId, __in_opt PPS_CREATE_NOTIFY_INFO  CreateInfo)

基本用法是:

1
PsSetCreateProcessNotifyRoutineEx(MyCreateProcessNotifyRoutine, FALSE);

在回调函数里面获取目标进程的相关信息,HOOK ZwTestAlert 函数之后,就可以做你想做的事情了。
关于这个函数更详细的说明请参考下面网址:
https://www.cnblogs.com/priarieNew/p/9758980.html

2.3 x86、x64、wow64代码兼容问题

牢牢记住,x64系统,驱动使用的是64位编译器;x86系统,驱动使用的是x86编译器;但是wow64程序,驱动使用的是64位编译器,环境在64位下,但注入的程序却是32位的,所以得出以下结论:
A、x86、x64的注入代码可以写到一起,只是声明变量的时候,要使用PVOID、SIZE_T、ULONG_PTR等来定义,这样不管是使用x64编译器,还是x86编译器,变量大小都可以兼容;
B、wow64的注入代码,声明变量的时候使用x86的常规声明,不能混入原生x86、x64系统,因为wow64,必须使用x64编译器,但变量声明却是32位的。

4、关键问题解决

4.1 为什么HOOK的是ZwTestAlert函数

4.1.1 PE加载流程

我们知道,PE的加载流程是:
a、调用CreateProcess启动进程
b、生产内核对象,分配4GB进程空间
c、加载ntdll.dll
d、创建进程的主线程;
e、主线程初始化输入表,系统加载器加载dll文件以及完成重定位、IAT
f、主线程为每个DLL调_DLLMainCRTStartup函数
g、主线程根据EXE是GUI(图形化)程序还是CUI(控制台)程序执行MainCRTStartup,
h、最后一步执行WinMain(或者main函数)
知道了PE加载流程,我们就知道我们的HOOK大概在哪个位置了,下面的流程是从创建进程的主线程那里开始分析。

4.1.2 ZwTestAlert函数执行时机

先来点看起来高大上的东西:
when the first thread in process start - this is special case. need do many extra jobs for process initialization. at this time only two modules loaded in process - EXE and ntdll.dll . LdrInitializeThunk call LdrpInitializeProcess for this job. if very briefly:

  1. different process structures is initialized
  2. loading all DLL (and their dependents) to which EXE statically linked - but not call they EPs !
  3. called LdrpDoDebuggerBreak - this function look - are debugger attached to process, and if yes - int 3 called - so debugger receive exception message - STATUS_BREAKPOINT - most debuggers can begin UI debugging only begin from this point. however exist debugger(s) which let as debug process from LdrInitializeThunk - all my screenshots from this kind debugger
  4. important point - until in process executed code only from ntdll.dll (and may be from kernel32.dll) - code from another DLLs, any third-party code not executed in process yet.
  5. optional loaded shim dll to process - Shim Engine initialized. but this is OPTIONAL
  6. walk by loaded DLL list and call its EPs with DLL_PROCESS_DETACH
  7. TLS Initializations and TLS callbacks called (if exists)
  8. ZwTestAlert is called - this call check are exist APC in thread queue, and execute its. this point exist in all version from NT4 to win 10. this let as for example create process in suspended state and then insert APC call ( QueueUserAPC ) to it thread (PROCESS_INFORMATION.hThread) - as result this call will be executed after process will be fully initialized, all DLL_PROCESS_DETACH called, but before EXE entry point. in context of first process thread.
  9. and NtContinue called finally - this restore saved thread context and we finally jump to thread EP.

上面是对PE加载流程一些更详细的说明,我来翻译下:

 

当进程中的第一个线程创建时,进程还需要做很多初始化工作。此时进程中只加载了两个模块,EXE和ntdll.dll。LdrInitialize Thunk为此调用LdrpInitalizeProcess(这就是第一篇贴子说的,Ldr初始化未完成的情况),总体流程如下:
1、初始化进程各种结构;
2、加载EXE静态链接的所有dll及dll的依赖项,但这不是EP(Entry Point);
3、调用LdrpDoDebuggerBreak:判断是否有调试器附加进程。如果有,则执行int 3指令,让调试器接收STATUS_BREAKPOINT的异常消息。多数带UI界面的调试器就是从这里开始调试的。但是存在一些调试器,也可以让调试从LdrInitializeThunk开始。
4、 很重要的一点:ntdll.dll(也可能kernel32.dll)中的代码执行完之前,其他任何dll的和第三方库的代码不会被执行;
5、Windows 兼容性引擎初始化,加载兼容性dll(如果有)。(ShinEngHOOK在此处)
6、加载DLL,并执行DLL中的DLL_PROCESS_DETACH;
7、TLS初始化并且执行TLS的回调函数(如果有)(一些反调试就在此处);
8、调用ZwTestAlert函数。这个函数会检查线程的APC队列是否有函数,如果有,则执行这些函数。这个机制从 NT4 到 win10 版本的操作系统都存在。如果以suspended 状态创建进程,然后把APC函数插入到线程,在进程初始化完成后,将会执行这些APC函数。在到达EXE的EP点之前,所有的DLL_PROCESS_DETACH都会被调用。(这儿可以APC HOOK)
9、最后调用NtContinue函数,这将保存线程的context ,并且到达线程的EP点。

 

我觉得,这儿的内容是今天晚上介绍这篇HOOK的最重要的。因为,从这个流程入手,你至少明白了以下几点:
1、ShinEngHOOK的时机;
2、TLS反调试的时机;
3、APC HOOK的时机;
4、ZwTestAlert函数被HOOK的原因。
5、发散思维,当然也可以HOOK NtConitnue函数。

4.2 哪个ZwProtectVirtualMemory函数

我在这里吃了大亏,希望你别像我一样。
在ntdll里面,Zw和Nt函数都是一样的,都在同一个地址,都同时存在于导出表(比如ZwReadFile、NtReadFile都在导出表,都在同一个地址, 而且他们最终都会调用到ntoskrnl中的NtReadFile中去),但是ntoskrnl.exe导出的ZwReadFile和NtReadFile却是不同的。
Ntoskrnl导出的NtReadFile是真正的执行函数 而ZwReadFile仍然是一个stub函数。
内核态调用ZwProtectVirtualMemory会将Previous Mode设置为Kernel Mode 然后再调用到NtProtectVirtualMemory中。而在内核态直接调用NtProtectVirtualMemory不会改变Previous Mode。而在NtProtectVirtualMemory中会检测当前调用来自用户态还是内核态如果是来自内核态不会检测参数,而如果是来自用户态,就会做一系列的参数检测。而我们知道内核组件可能运行在任意进程的上下文中。当它调用NtProtectVirtualMemory时,因为Previous Mode很可能是User Mode,而我们的参数请求的内核态的地址,这时通常就会产STATUS_ACCESS_VIOLATION
错误。
说了这么多,意思就是:
1、在内核态,不能使用ntdll.dll里面的ZwProtectVirtualMemory\NtProtectVirtualMemory
2、在内核态,也不能使用ntoskrnl.exe导出的NtProtectVirtualMemory
3、在内核态,要使用ntoskrnl.exe导出的ZwProtectVirtualMemory

4.3 Zw函数流程总结

1、Zw函数会在KiSystemService中将ETHREAD中的PreviousMode改为KernelMode,最后在Nt函数中如果是KernelMode就会跳过对参数是否可写的验证,如果是UserMode就会验证。如果是UserMode,访问内核地址会报错,所以如果内核中直接调用Nt函数,需要手动将PreviousMode修改为KernelMode,否则无法访问内核地址,而修改PreviousMode并且通过系统服务表获取SSDT函数这个过程是很复杂的,直接调用内核导出的Zw函数就行,不过在调用Zw函数的时候需要自己对地址的可写性验证。而且通过Zw函数调用会在系统空间堆栈上有个属于本次调用的自陷框架。
2、32位下Zw函数会将内核模式保存在CS最后一位上,调用KiSystemService修改PreviousMode为KernelMode,接着跳转到KiFastCallEntry中间的地方,初始化一些寄存器,最后通过call ebx的方式调用Nt函数,最后通过KiSystemExit返回。
3、64位下Zw函数会调用KiServiceInternal,在这个函数中修改PerviousMode为KernelMode,然后跳转到KiSystemCall64中的KiSystemServiceStart部分,接着在KiSystemServiceRepeat部分通过jmp r11调用Nt函数,最后通过KiSystemServiceExit函数返回。

4.4 如何获取ntoskrnl.exe中的未导出函数ZwProtectVirtualMemory

A、映射ntdll.dll到ring0空间
B、获取ZwprotectVirtualMemory在SSDT表中的Index;
获取方法,见:
【DLL注入编写与分析系列之一】x64平台SSDT HOOK之NtResumeThread注入
C、获取ZwprotectVirtualMemory函数地址
看来看看ntoskrnl.exe中函数的布局:

由图可知,两个Zw函数之间的长度大小是一样的,所以,可以这样计算:
A、获取ZwClose和ZwOpenProcess函数的Index(ZwProtectVirtualMemory无法这样获取,因为没有导出)

1
2
3
4
5
#ifdef _WIN64
#define GetSysCallIndexFromKernel(fun) (*(LONG*)((BYTE*)fun + 0x15))
#else
#define GetSysCallIndexFromKernel(fun) (*(LONG*)((BYTE*)fun + 1))
#endif
1
2
IndexOfClose = GetSysCallIndexFromKernel(ZwClose);
IndexOfOpenProcess = GetSysCallIndexFromKernel(ZwOpenProcess);

B、通过ZwClose-ZwOpenProces除以Index之间的差值,就得到每个函数的大小FuncSize
C、通过ZwClose-FuncSizeIndexofClose就得到Zw系列函数的BaseAddress
D、然后:BaseAddress+Funcsize
IndexOfZwProtectVirtualMemory就得到ZwProtectVirtualMemory函数地址了。
之所以能够这样利用,就是因为:每个Zw函数之间的大小是相等的。

5、代码编写详细思路

A、首先获取ZwProtectVirtualMemory函数的地址;
B、设置回调函数MyCreateProcessNotifyRoutine:

1
PsSetCreateProcessNotifyRoutineEx(MyCreateProcessNotifyRoutine, FALSE);

C、判断是否是Wow64位程序
通过

1
status = ZwQueryInformationProcess(hProcess, ProcessWow64Information, &Wow64Info, sizeof(ULONG_PTR), &uReturnLen);

wow64加载,SysWow64\ntdll.dll,其他则加载System32\ntdll.dll
D、通过ZwProtectVirtualMemory函数修改ZwTestAlert函数地址的属性
E、编写shellcode
x86/x64shellcode:
对驱动来说,编译32位的驱动就只能在32位系统运行,编译64位的驱动就只能在64位系统运行,所以对原生系统来说,编译的时候是分别用32位、64位编译,因此它们的shellcode可以写到一个函数里面,用类似SIZE_T、ULONG_PTR的定义兼容32位、64位。
wow64 sehllcode:
要注入wow64位程序,说明该程序依然是运行在64位程序上,驱动依然是64位的,编译的时候就要用64位编译。但是目标进程却是32位的,因此,shellcode里面的定义就要用32位的定义,比如ULONG之类的定义,去注入32位程序。没办法三种情况一起兼容。对驱动来说,编译32位的驱动就只能在32位系统运行,编译64位的驱动就只能在64位系统运行,不存在32位运行在64位的情况。

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
push rax //(确保rsp 16字节对齐,下面call时,Movaps指令16字节对齐)
push rax
push rbx
push rcx
push rdx
push rbp
push rsp
push rsi
push rdi
push r8
push r9
push r10
push r11
push r12
push r13
push r14
push r15 
pushfq    
call @Next
@Next:
pop rbx
and bx,0//(定位到shellcode开头)
mov rcx,rbx
call @LoadDllAndRestoreExeEntry
popfq
pop r15
pop r14
pop r13
pop r12
pop r11
pop r10
pop r9
pop r8
pop rdi
pop rsi
pop rsp
pop rbp
pop rdx
pop rcx
pop rbx
pop rax
pop rax
sub dword ptr ds:[rsp],6 
ret
@LoadDllAndRestoreExeEntry:
mov rdi,qword ptr ds:[rbx+0xF8]
lea rsi,qword ptr ds:[rbx+0x108]
xor rcx,rcx
add rcx,2
rep movs qword ptr ds:[rdi],qword ptr ds:[rsi]
lea rax,qword ptr ds:[rbx+0xE8]
mov qword ptr ds:[rsp+0x20],rax
mov r9,qword ptr ds:[rbx+0xE8]
lea r8,qword ptr ds:[rbx+0xF0]
lea rdx,qword ptr ds:[rbx+0xE0]
or rcx,0xFFFFFFFFFFFFFFFF
call qword ptr ds:[rbx+0x100]//用ZwProtectVirtualMemory
lea r9,[rbx+0xC0]
mov r8,[rbx+0xC8]
mov rdx,[rbx+0xD0]
xor rcx,rcx
jmp qword ptr ds:[rbx+0xD8]

Shellcode汇编关键思路:
1、恢复ZwTestAlert入口地址;
2、调用ZwProtectVirtualMemory恢复ZwTestAlert地址属性
3、调用LdrLoadDll注入Dll

 

shellcode是我重新写的。

 

F、构造call 指令,跳转到shellcode
x64:FF 15
x86:E8

6 代码

贴子变优了,或者加精了,或者要的同学多,再上传代码吧。


[2022夏季班]《安卓高级研修班(网课)》月薪两万班招生中~

最后于 2022-2-10 22:50 被ExploitCN编辑 ,原因:
收藏
点赞7
打赏
分享
最新回复 (18)
雪    币: 3020
活跃值: 活跃值 (1694)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
欧阳休 活跃值 2022-2-11 08:57
2
0
有空再看!
雪    币: 5237
活跃值: 活跃值 (901)
能力值: ( LV2,RANK:15 )
在线值:
发帖
回帖
粉丝
如斯咩咩咩 活跃值 2022-2-11 09:58
3
1
等加精了再看吧
雪    币: 1595
活跃值: 活跃值 (1850)
能力值: ( LV2,RANK:15 )
在线值:
发帖
回帖
粉丝
はつゆき 活跃值 2022-2-11 09:58
4
0
好评返现了属于是
雪    币: 5878
活跃值: 活跃值 (4977)
能力值: ( LV9,RANK:310 )
在线值:
发帖
回帖
粉丝
PlaneJun 活跃值 6 2022-2-11 10:05
5
0
雪    币: 4828
活跃值: 活跃值 (853)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
ldljlzw 活跃值 2022-2-11 11:55
6
0
我也是等加精了再看吧
雪    币: 228
活跃值: 活跃值 (1356)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
杰克王 活跃值 2022-2-11 13:03
7
0
好评了,返现呢?
雪    币: 3211
活跃值: 活跃值 (1310)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
木志本柯 活跃值 2022-2-11 13:19
8
0
在64系统32位程序好像也可以装载64dll 打开一个32位程序有System32\ntdll.dll  拖到ida里识别确实是64的dll  大兄弟可以研究研究
雪    币: 2645
活跃值: 活跃值 (1943)
能力值: ( LV8,RANK:120 )
在线值:
发帖
回帖
粉丝
ExploitCN 活跃值 2 2022-2-11 14:20
9
0
木志本柯 在64系统32位程序好像也可以装载64dll 打开一个32位程序有System32\ntdll.dll 拖到ida里识别确实是64的dll 大兄弟可以研究研究
System32\ntdll.dll是64位,SysWOW64\ntdll是32位
雪    币: 10301
活跃值: 活跃值 (6094)
能力值: ( LV13,RANK:375 )
在线值:
发帖
回帖
粉丝
TkBinary 活跃值 5 2022-2-11 17:09
10
0
已阅,也可以看看 火绒注入(相关网上流露出的代码) 也有这一部分。或者说已经实现好了。大概原理一样。 至于ShellCode每个人有每个人的写法。 谢谢分享。 你可以瞅一下 win10 win11下 应该都不一样了。win7下调试的时候发现是可以用的。
雪    币: 176
活跃值: 活跃值 (298)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
hkx3upper 活跃值 2022-2-23 16:56
11
0
ZwProtectVirtualMemory导出了,只是未声明,可以声明一下或者MmGetSystemRoutineAddress就能用
雪    币: 8
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
刻骨柔情 活跃值 2022-6-14 00:53
12
0
都已经加优了,承诺该兑现了。
雪    币: 4828
活跃值: 活跃值 (853)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
ldljlzw 活跃值 2022-6-14 09:37
13
0
写的太好了,精华!
雪    币: 1345
活跃值: 活跃值 (1305)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
fengyunabc 活跃值 1 2022-6-14 10:12
14
0
写得不错,一些“为什么”也写得明白。赞!
雪    币: 1345
活跃值: 活跃值 (1305)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
fengyunabc 活跃值 1 2022-6-14 10:12
15
0
hkx3upper ZwProtectVirtualMemory导出了,只是未声明,可以声明一下或者MmGetSystemRoutineAddress就能用
win7没导出。。。。。。
雪    币: 91
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
@一语成谶 活跃值 2022-6-21 17:56
16
0
有点难啊  一头污水 无从下手  好像看懂了些  又什么也不懂  都不知道问什么....
雪    币: 847
活跃值: 活跃值 (1367)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
凌哥 活跃值 2022-6-24 14:36
17
0
好评返现了属于是
雪    币: 29924
活跃值: 活跃值 (417)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
bestbird 活跃值 2022-6-24 18:49
18
0

madCodeHook的驱动注入很多年前就是用这个方法了

最后于 2022-6-24 18:52 被bestbird编辑 ,原因: 添加附件
上传的附件:
雪    币: 65
活跃值: 活跃值 (389)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
jordonA 活跃值 2天前
19
0
bestbird madCodeHook的驱动注入很多年前就是用这个方法了
能提供一下相关的源码吗
游客
登录 | 注册 方可回帖
返回