当我们编写32位和64位的汇编程序时,调用系统API的方式是不同的。
这里是两段汇编代码
section .data
msg db "Hello, World!", 10
len equ $ - msg
section .text
global _start
_start:
; ssize_t write(int fd, const void *buf, size_t count)
mov eax, 4 ; sys_write
mov ebx, 1 ; stdout
mov ecx, msg
mov edx, len
int 0x80
; void exit(int status)
mov eax, 1 ; sys_exit
xor ebx, ebx
int 0x80
使用 int 0x80 系统调用完成输出。
section .data
msg db "Hello, World!", 10
len equ $ - msg
section .text
global _start
_start:
; ssize_t write(int fd, const void *buf, size_t count)
mov rax, 1 ; sys_write
mov rdi, 1 ; stdout
lea rsi, [rel msg]
mov rdx, len
syscall
; void exit(int status)
mov rax, 60 ; sys_exit
xor rdi, rdi
syscall
这里使用的是 syscall 完成输出。
那么一个很自然的问题就来了:32位和64位程序使用的指令集甚至调用约定都不同,为什么我们能在64位的Windows操作系统上直接运行32位的软件呢?本文将带你探索Windows系统实现这一兼容性的核心机制,并演示如何利用它——这项技术被称为“天堂之门”。
Wow64分析
32位程序在64位Windows上的运行,是通过一个名为 wow64 的模拟器层实现的。

根据《深入解析Windows操作系统》中关于 CreateProcess 的流程描述,创建进程时涉及几个关键步骤,其中与Wow64相关的有:
- 打开要执行的映像:系统会分析可执行文件的类型。

- 创建Windows进程执行体对象:主要是初始化
EPROCESS 内核对象。如果进程运行在 Wow64 环境下,系统会进行额外处理:
- 检查是否使用大页面内存。
- 分配一个辅助结构
EWOW64PROCESS 来管理32位进程的特定信息。
- 在映射系统DLL时,除了常规的64位
Ntdll.dll,还会专门为Wow64进程映射一份32位的 Ntdll.dll。
操作系统相关知识
要理解“天堂之门”,我们需要回顾一些操作系统的基础概念。在x86架构的保护模式下,有几个关键的段寄存器(Segment Register):
- CS (Code Segment Register):指向当前代码段的基址。
- DS (Data Segment Register):指向当前数据段的基址。
- SS (Stack Segment Register):指向当前堆栈段的基址。
- ES/FS/GS (Extra Segment Registers):额外的数据段寄存器,其中FS在Windows中常用于指向线程环境块(TEB)。
在保护模式下,段寄存器里存储的并非直接的物理地址,而是一个称为 段选择子(Segment Selector) 的结构。它是一个“索引+权限声明”的组合。
CPU通过查询全局描述符表(GDT)或局部描述符表(LDT),根据段选择子找到对应的段描述符,从而获得真正的段基址和访问权限。这就实现了内存隔离和保护。例如,在Windows中,一些常见的代码段选择子含义如下:
| CS 值 |
含义 |
0x23 |
32 位用户代码段 |
0x33 |
64 位用户代码段 |
0x10 |
内核代码段 |
段选择子的结构通常如下所示(以16位为例):
typedef struct selector
{
unsigned char RPL :2; // 请求特权级
unsigned char TI :1; // 0=GDT, 1=LDT
unsigned short index :13; // 描述符表索引
} __attribute__((packed)) selector;
// 位布局
// 15 3 2 1 0
// +----------------+--+--+
// | Index |TI|RPL|
// +----------------+--+--+
以 CS=0x33=0b00110011 为例,解析后:
- RPL =
11b = 3(用户态)
- TI =
0
- index =
110b = 6
CPU 就会去 GDT 的第6项查找,发现那是一个64位、ring3级别的代码段描述符,于是便进入64位用户模式执行。

天堂之门 Heaven‘s Gate
“天堂之门”技术正是利用了Wow64的机制。其核心思想是:在一个32位的Wow64进程中,通过修改 CS 段寄存器为 0x33(64位代码段),并配合远跳转或远调用指令,使CPU切换到64位模式,从而直接执行64位代码。
这样做的一个潜在用途是绕过安全软件的检测。因为许多安全产品(EDR)的钩子(Hook)可能只安装在32位的 ntdll.dll 中,而通过“天堂之门”调用64位的 ntdll 函数,就可能绕过这些钩子。此外,在逆向工程分析时,向32位程序中注入64位Shellcode会导致反汇编工具(如32位IDA)解析错误,增加分析难度。
这种技术的典型代码模式如下:
; 当前处于32位上下文
push 0x33 ; 64位代码段选择子
push entry64 ; 64位代码入口地址
retf ; 远返回,切换至64位模式
; ===== 进入64位上下文 =====
entry64:
; 此处可以执行64位指令
; 例如调用64位的 ntdll 函数
; ...
push 0x23 ; 32位代码段选择子
push back_to_32 ; 返回的32位地址
retf ; 远返回,切换回32位模式
下面我们结合一个实际的 C++ 项目来演示。我们将使用 Minhook 库来挂钩函数,并使用一个名为 wow64pp 的辅助框架来简化“天堂之门”的调用。关于 Minhook 的基础使用,可以参考其官方文档。
wow64pp 框架(header-only,仅一个头文件)封装了模式切换的繁琐细节。其内部实现“长跳”进入64位模式的核心汇编逻辑类似下图所示:


其中的 push 0x33 指令正是为了接下来的远返回(retf)指令准备一个新的64位代码段选择子。
这个框架使用起来很方便,但需要注意两个限制:
- 只能调用
ntdll.dll 中的函数。根据社区(如看雪论坛)的讨论,尝试加载 kernel32.dll 或 user32.dll 可能会使进程状态混乱(同时混用32位和64位的GUI资源等),导致不可预料的问题。
- 多参数传参需要小心处理。由于涉及32位到64位调用约定的转换,复杂参数的函数可能无法正确调用。例如,尝试调用
NtCreateFile 可能失败,而调用 NtQuerySystemTime(单个指针参数)则相对容易成功。
以下是演示代码的主要逻辑:
#define NOMINMAX
#include "extern/minhook/minhook.h"
#include "extern/wow64pp/wow64pp.hpp"
#include <iostream>
#include <Windows.h>
#include <winternl.h>
typedef NTSTATUS(NTAPI* NtQuerySystemTime_t)(
PLARGE_INTEGER SystemTime
);
NtQuerySystemTime_t fpNtQuerySystemTime = nullptr;
NTSTATUS NTAPI HookedNtQuerySystemTime(
PLARGE_INTEGER SystemTime
)
{
std::cout << "[hook] NtQuerySystemTime called (x86 stub), ";
NTSTATUS status = fpNtQuerySystemTime(SystemTime);
if (NT_SUCCESS(status))
{
std::cout << "[hook] SystemTime = "
<< SystemTime->QuadPart << std::endl;
}
return status;
}
int main()
{
LARGE_INTEGER systemTime = { 0 };
// 1. 正常调用一次32位的 NtQuerySystemTime
NtQuerySystemTime(&systemTime);
// 2. Hook 32位的 NtQuerySystemTime 并再次调用(会被钩子捕获)
if (MH_Initialize() != MH_OK)
{
std::cerr << "[x] init hook failed\n";
return 1;
}
LPVOID pTarget = GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "NtQuerySystemTime");
std::cout << " now hook NtQuerySystemTime: 0x" << std::hex << pTarget << std::endl;
if (MH_CreateHook(pTarget, &HookedNtQuerySystemTime, reinterpret_cast<LPVOID*>(&fpNtQuerySystemTime)) != MH_OK)
{
std::cerr << “[x] create hook failed\n“;
return 1;
}
MH_EnableHook(pTarget);
NtQuerySystemTime(&systemTime);
// 3. 使用“天堂之门”技术调用64位的 NtQuerySystemTime(绕过32位的钩子)
auto ntdllHandle = wow64pp::module_handle("ntdll.dll");
std::cout << “ found ntdll (x64): 0x“ << std::hex << ntdllHandle << ”\n“;
auto NtQuerySystemTime64 = wow64pp::import(ntdllHandle, ”NtQuerySystemTime“);
std::cout << ” found NtQuerySystemTime (x64): 0x“ << std::hex << NtQuerySystemTime64 << ”\n“;
auto status = wow64pp::call_function(
NtQuerySystemTime64,
&systemTime // PLARGE_INTEGER
);
std::cout << ”[wow64] NtQuerySystemTime status: 0x“ << std::hex << status << ”\n“;
std::cout << ”[wow64] SystemTime (100ns since 1601): “ << systemTime.QuadPart << ”\n“;
MH_DisableHook(pTarget);
return 0;
}
程序执行后的输出结果类似下图,可以看到32位钩子生效了一次,而通过Wow64调用的64位函数则绕过了该钩子:

后记与思考
“天堂之门”技术听起来很厉害,但在现代安全对抗中,其直接的“免杀”或绕过效果已经相当有限。例如,它无法绕过内核层的钩子或ETW(Windows事件跟踪)等更底层的检测机制。同时,模式切换的代码本身具有比较明显的特征,容易被行为检测识别。
不过,这项技术在逆向工程分析领域仍能制造一些麻烦,因为向32位程序中注入64位的Shellcode会导致反汇编工具解析错误,这确实能干扰分析人员的初步判断。
如果你对Windows系统架构和底层编程感兴趣,可以通过文末的参考资料和项目代码进行更深入的研究。本文的完整项目结构如下,供复现参考:

主要构建文件内容如下:
extern\minhook\CMakeLists.txt
cmake_minimum_required(VERSION 3.11)
project(minhook)
set(MINHOOK_SOURCES
buffer.c
hook.c
trampoline.c
hde/hde32.c
hde/hde64.c
)
set(MINHOOK_INCLUDE
buffer.h
trampoline.h
hde/hde32.h
hde/hde64.h
hde/pstdint.h
hde/table32.h
hde/table64.h
)
add_library(${PROJECT_NAME} STATIC
${MINHOOK_SOURCES}
${MINHOOK_INCLUDE}
)
target_include_directories(${PROJECT_NAME} PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}
)
extern\wow64pp\CMakeLists.txt
cmake_minimum_required(VERSION 3.11)
project(wow64pp LANGUAGES CXX)
# 创建一个 interface 库
add_library(${PROJECT_NAME} INTERFACE)
# 添加 include 目录
target_include_directories(${PROJECT_NAME} INTERFACE
${CMAKE_CURRENT_SOURCE_DIR}
)
CMakeLists.txt
cmake_minimum_required(VERSION 3.11)
project(gate_example LANGUAGES CXX)
set(CMAKE_INCLUDE_CURRENT_DIR ON)
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_subdirectory(extern/minhook)
add_subdirectory(extern/wow64pp)
set(PROJECT_INCLUDE
include/HeavensGate.hpp
)
set(PROJECT_SOURCE
src/main.cpp
)
# Specify MSVC UTF-8 encoding
add_compile_options("$<$<C_COMPILER_ID:MSVC>:/utf-8>")
add_compile_options("$<$<CXX_COMPILER_ID:MSVC>:/utf-8>")
add_executable(${PROJECT_NAME} ${PROJECT_INCLUDE} ${PROJECT_SOURCE})
target_link_libraries(${PROJECT_NAME} PRIVATE minhook wow64pp)
target_compile_definitions(${PROJECT_NAME} PRIVATE UNICODE _UNICODE)
引用与扩展阅读
- Microsoft Docs: WOW64 Implementation Details
- 看雪论坛相关讨论
- Rewolf's blog article on Wow64