第 43 章 Windows 内部机制深解(PE 加载 / 异常 / 线程 / 内存)
逆向 Windows 程序时,一半的困惑来自"不知道 Windows 在背后做了什么"。本章系统讲透四个最常碰到的内部机制:PE 加载过程、异常处理体系、线程与 APC、内存管理。这是理解 Windows 恶意软件和注入技术的前提。
📍 知识点地图 | 主题:Windows内部机制 | 前置:第38章 | 后续:第44章 | 核心概念:PE加载、SEH/VEH、TEB/PEB、APC
43.1 PE 加载过程(程序怎么跑起来的)
CreateProcess
→ 读取 PE 头 → 校验
→ 映射节区到内存(按 SectionAlignment 对齐)
→ 处理重定位(基址随机化 ASLR)
→ 解析导入表(递归加载依赖 DLL + IAT 填充)
→ 处理延迟导入/绑定导入
→ 执行 TLS 回调(!)
→ 跳转入口点(AddressOfEntryPoint)
对逆向的意义:
□ 入口点不是"程序起点"(CRT 初始化在前,main 在后)
□ TLS 回调在入口点之前执行(恶意代码藏匿点 + 反调试点)
□ 导入表解析过程被 EDR 观察(加载顺序可被检测)
PE 头快速回顾(逆向必备)
DOS 头: e_magic(MZ) + e_lfanew(指向 PE 头)
PE 头: Signature(PE\0\0)
可选头: AddressOfEntryPoint / ImageBase / SizeOfImage
节区表: .text .rdata .data .idata .reloc(虚拟大小 vs 文件大小)
重定位表: 基址随机化时修正绝对地址
43.2 异常处理体系(SEH / VEH / 内核异常)
分层模型
用户态异常 → SEH(结构化异常处理,栈上 __try/__except)
→ VEH(向量化异常处理,全局注册)
→ 未处理异常 → 内核
内核处理 → 终止进程(崩溃)
x64 下 SEH 表: 编译期生成(.pdata 目录),运行时由 RtlDispatchException 驱动
恶意软件怎么用异常(逆向视角)
| 用途 | 机制 | 识别 |
|---|---|---|
| 反调试 | 故意制造异常观察处理器 | 大量异常相关 API |
| 混淆逻辑 | 把真实逻辑放异常处理器 | 异常路径藏代码 |
| 自毁 | 未处理异常 → 退出 | 意外崩溃 |
| 反分析 | 异常处理中断调试器 | 检测调试器响应差异 |
| 隐晦调用 | 异常改变执行流 | 调用链不在栈上 |
关键 API
AddVectoredExceptionHandler 注册 VEH
RtlAddFunctionTable 注册动态 SEH(JIT 代码用)
SetUnhandledExceptionFilter 未处理异常钩子
NtQueryInformationProcess 调试状态查询
43.3 线程模型与 APC(注入技术的底层)
线程机制
CreateThread → 内核创建线程对象 → 用户态从起始地址执行
线程环境: 每个线程独立栈 + TEB(线程环境块)+ 寄存器上下文
TEB: 通过 GS 段寄存器访问(fs:[0x30] 老式)/ [gs:0x30](x64)
TEB/PEB(逆向高频访问点)
PEB(进程环境块):
BeingDebugged (PEB+0x02) ← IsDebuggerPresent 读这里!
ProcessHeap (PEB+0x18)
Ldr (PEB+0x18) ← 加载模块列表(枚举模块用)
ProcessParameters(PEB+0x20) ← 命令行/环境变量
TEB(线程环境块):
StackBase/StackLimit 栈边界
ThreadLocalStoragePointer TLS
ProcessEnvironmentBlock 指向 PEB
反调试/反分析绕不开 PEB:改 PEB+0x02 就能骗过 IsDebuggerPresent。
APC(异步过程调用)
原理: 把函数"排队"到目标线程执行(目标线程进入可告警等待时执行)
类型:
UserAPC: 用户态排队(QueueUserAPC)
KernelAPC: 内核排队
注入用途(恶意软件常用):
1. QueueUserAPC 远程排队 → 目标线程执行 shellcode(APC 注入)
2. 绕过 CreateRemoteThread 检测(更隐蔽)
3. Early Bird: 进程创建早期就注入(主线程执行前)
43.4 内存管理机制
虚拟内存布局(x64)
0x0000000000000000 - 0x00007FFFFFFFFFFF 用户态(128TB)
0xFFFF800000000000 - 0xFFFFFFFFFFFFFFFF 内核态
用户态内部:
高地址: DLL(ntdll/kernel32 等)
中地址: 可执行文件镜像 + 堆
低地址: 栈、其他
(ASLR 使位置随机化)
关键概念
分配粒度: 64KB(页对齐)
页面保护: PAGE_READONLY/EXECUTE/READWRITE/RWX(后两者是注入信号)
提交 vs 保留: Committed(有物理页) vs Reserved(只留地址)
工作集: 当前在物理内存中的页
VAD(虚拟地址描述符): 内核记录每个区域的信息(内存取证/EDR 依赖它)
注入检测的底层信号(内存视角)
□ 区域权限 RWX(可写可执行)→ 高风险
□ 区域不在任何模块映射中 → 无模块代码
□ 区域有私有提交 + 高熵内容 → shellcode 候选
□ 调用栈返回地址不在模块内 → 可疑执行
43.5 Windows 内部机制在逆向中的综合应用
场景 1: 反调试绕过
IsDebuggerPresent → 改 PEB.BeingDebugged
NtQueryInformationProcess → Hook 或 patch
场景 2: 模块枚举(找隐藏 DLL)
常规: PEB.Ldr 双向链表遍历
隐蔽: 手动解析内存中的 PE(不依赖 Ldr)
场景 3: 注入理解
CreateRemoteThread / QueueUserAPC / 异常注入 → 全在 43.2-43.3 机制之上
场景 4: 内存取证
VAD/页面保护/工作集 → 第 26 章工具读的就是这些结构
场景 5: 壳与脱壳
加壳程序修改内存保护 + 动态解析 → 理解内存机制才能脱
43.6 实战速查表
检测调试器: PEB.BeingDebugged / NtQueryInformationProcess / 时间差 / 断点扫描
隐藏执行: APC / 异常路径 / TLS 回调 / 驱动回调
模块操作: Ldr 链表 / 手动映射 / 模块 stomping(复用已加载模块空间)
权限操作: 页面保护切换(RW→RX 常见于解密后执行)
动手练习
- 用 WinDbg/x64dbg 观察一个进程的 PEB 结构,读取 BeingDebugged、Ldr 模块列表。
- 写一个小程序调用 IsDebuggerPresent,在调试器中观察它是怎么读 PEB 的(反汇编看 fs/gs 访问)。
- 用 Process Explorer/API Monitor 观察 CreateProcess 后 DLL 加载顺序(对照 43.1 流程)。
- 分析一个样本的异常使用:列出它注册的所有 VEH/SEH,判断反调试还是逻辑混淆。
深入阅读
- 仓库:
skills/reverse-engineering/anti-analysis.md(Windows 反调试检测点全表) - 仓库:
skills/edr-bypass-re/references/hook-survey.md(EDR 对内部机制的 hook) - 仓库:
skills/reverse-engineering/kernel-driver-reverse.md(内核驱动 + IRP) - 仓库:
skills/reverse-engineering/patterns.md(异常处理混淆、RtlInstallFunctionTableCallback) - 仓库:
CTF-Sandbox-Orchestrator/competition-identity-windows/(Windows 身份 CTF)