Hook深度研究:监视WOW64程序在系统中的执行情况
字数 2140 2025-08-18 17:33:08
WoW64程序深度监控:挂钩64位NTDLL的技术研究
1. 背景与概述
WoW64 (Windows 32-bit on Windows 64-bit) 是Windows系统允许32位应用程序在64位系统上运行的兼容层。在WoW64进程中,存在两个版本的NTDLL:
- 32位NTDLL:负责将系统调用转换到WoW64环境,并调整以适应x64 ABI
- 64位NTDLL:由WoW64环境调用,最终负责用户模式到内核模式的转换
传统安全产品通常只挂钩32位模块,这容易被攻击者绕过。通过挂钩64位NTDLL,安全产品可以获得更全面的进程行为监控能力。
2. 64位DLL注入技术
2.1 wow64log.dll劫持
原理:
- WoW64环境初始化时会尝试从system32目录加载wow64log.dll
- 默认情况下该DLL不存在,可被利用作为注入点
实现步骤:
- 创建64位DLL并命名为wow64log.dll
- 将其放入system32目录
- 系统会自动加载到所有WoW64进程中
优缺点:
- 优点:简单易实现,兼容所有Windows版本
- 缺点:无法控制注入时机和进程选择
2.2 天堂之门(Heaven's Gate)技术
原理:
- 利用CS段寄存器切换(0x33)将执行模式从32位切换到64位
- 直接调用64位NTDLL的LdrLoadDll函数
实现步骤:
- 在目标进程执行32位代码
- 通过天堂之门切换到64位模式
- 调用64位LdrLoadDll加载目标DLL
关键代码:
; 切换到64位模式
push 0x33 ; 64位代码段选择子
call $+5 ; 将返回地址压栈
add [rsp], 5 ; 调整返回地址
retf ; 远返回切换到64位模式
; 64位代码开始
; 调用LdrLoadDll...
2.3 APC注入技术
原理:
- 使用异步过程调用(APC)在目标线程上下文中执行代码
- 通过适配器thunk加载DLL
实现步骤:
- 分配内存并写入适配器thunk代码
- 初始化APC并设置NormalRoutine为thunk地址
- 将APC插入目标线程队列
适配器thunk结构:
typedef struct {
PVOID LdrLoadDll; // 64位LdrLoadDll地址
PWSTR DllPath; // DLL路径
} APC_CONTEXT, *PAPC_CONTEXT;
CFG问题:
- 适配器thunk位于4GB以下,被标记在WoW64 CFG位图
- 但KiUserApcDispatcher会检查本机CFG位图,导致验证失败
2.4 解决CFG问题的方案
方案1:临时清除WoW64Process标志
原理:
- 修改EPROCESS的Wow64Process成员(偏移量0x428)
- 使系统将进程视为原生64位进程
实现伪代码:
// 保存原始值
OriginalWow64Process = *(PVOID*)(EProcess + 0x428);
// 清除Wow64Process标志
*(PVOID*)(EProcess + 0x428) = NULL;
// 分配内存 - 现在会标记在本机CFG位图
AllocateMemoryForThunk();
// 恢复原始值
*(PVOID*)(EProcess + 0x428) = OriginalWow64Process;
缺点:
- EPROCESS结构不稳定,偏移可能变化
- 多线程环境下有风险
方案2:无thunk APC注入
原理:
- 直接使用LdrLoadDll作为APC例程
- 利用KiUserApcDispatcher传递的隐藏参数
关键发现:
- KiUserApcDispatcher会将上下文结构指针放入R9
- LdrLoadDll的ModuleHandle参数恰好使用R9
- 上下文结构前几个成员在APC执行后不再需要
实现步骤:
- 初始化APC,NormalRoutine设为LdrLoadDll地址
- NormalContext包含DLL路径等信息
- 系统自动处理参数传递
3. 挂钩64位NTDLL
3.1 准备工作
依赖处理:
- 只依赖原生NTDLL,避免其他64位DLL
- 替换所有Win32 API为NTDLL对应函数:
- VirtualProtect → NtProtectVirtualMemory
- memcpy → RtlCopyMemory
- memset → RtlFillMemory
项目配置:
- 链接器设置/NODEFAULTLIB
- 显式添加NTDLL.lib
- 禁用运行时检查(/RTC)
- 禁用缓冲区安全检查(/GS-)
- 指定DllMain为入口点
3.2 内联挂钩实现
标准挂钩流程:
- 分配"蹦床"内存
- 复制目标函数前几条指令到蹦床
- 在蹦床中添加跳回原函数的指令
- 修改目标函数开头为跳转到蹦床
- 蹦床中跳转到detour函数
WoW64特殊问题:
- 64位NTDLL位于4GB以上地址空间
- 蹦床位于4GB以下,距离超过2GB限制
- 标准相对跳转(E9)无法使用
解决方案:
; 6字节长跳转方案
push low_32_bits ; 将低32位地址压栈
ret ; 弹出地址并跳转
3.3 递归问题处理
问题描述:
- detour函数中调用被挂钩函数会导致无限递归
- 传统TLS方案不可用(缺少CRT/kernel32)
解决方案:
- 利用WoW64.dll未使用的TEB TLS插槽
- 硬编码插槽位置存储递归深度
实现代码:
// 获取当前线程TEB
PTEB teb = NtCurrentTeb();
// 使用预定义的未使用插槽
LONG* depthCounter = (LONG*)&teb->TlsSlots[10];
if (InterlockedIncrement(depthCounter) == 1) {
// 实际detour逻辑
}
InterlockedDecrement(depthCounter);
4. 完整实现流程
-
注入64位DLL:
- 选择适合的注入方法(APC/天堂之门等)
- 处理CFG验证问题
-
初始化挂钩引擎:
- 准备只依赖NTDLL的环境
- 实现必要的内存操作函数
-
设置挂钩:
- 为目标函数创建蹦床
- 安装长距离跳转
- 处理递归控制
-
监控与处理:
- 在detour函数中分析调用参数
- 执行自定义逻辑
- 必要时调用原始函数
5. 限制与注意事项
-
系统版本差异:
- Windows 8.1 Update 3后NTDLL位置变化
- EPROCESS结构偏移可能不同
-
安全缓解措施:
- 动态代码限制可能阻止注入
- CFG导出抑制会影响挂钩
- 代码完整性保护需要特殊处理
-
稳定性考虑:
- 避免关键系统函数无限递归
- 确保内存操作原子性
- 处理多线程竞争条件
附录:关键数据结构
APC_CONTEXT结构
typedef struct _APC_CONTEXT {
PVOID LdrLoadDll; // 64位LdrLoadDll地址
PWSTR DllPath; // 要加载的DLL路径
ULONG Flags; // 加载标志
PHANDLE ModuleHandle; // 输出模块句柄
} APC_CONTEXT, *PAPC_CONTEXT;
蹦床布局
+---------------------+
| 原始函数前几条指令 |
+---------------------+
| JMP 回原始函数+5 |
+---------------------+
| JMP 到detour函数 | (可选)
+---------------------+
CFG位图选择逻辑
PVOID MiSelectCfgBitMap(PEPROCESS Process, PVOID Address) {
if (Process->Wow64Process == NULL) {
return Process->CfgBitMap; // 原生进程
}
if (Address >= (PVOID)0x100000000) {
return Process->CfgBitMap; // 高地址空间
}
return Process->Wow64CfgBitMap; // WoW64低地址空间
}