逆向工程实战手册 第 43 章

第 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 常见于解密后执行)

动手练习

  1. 用 WinDbg/x64dbg 观察一个进程的 PEB 结构,读取 BeingDebugged、Ldr 模块列表。
  2. 写一个小程序调用 IsDebuggerPresent,在调试器中观察它是怎么读 PEB 的(反汇编看 fs/gs 访问)。
  3. 用 Process Explorer/API Monitor 观察 CreateProcess 后 DLL 加载顺序(对照 43.1 流程)。
  4. 分析一个样本的异常使用:列出它注册的所有 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)