第 5 章 进程与线程 --- 5.2 Windows 进程的用户空间
5.2.0 框架图
┌──────────────────────────────────────────────────────────────────────────────────┐
│ Windows 用户地址空间布局 │
│ │
│ 32 位系统(默认 2GB 用户空间): │
│ │
│ 地址范围 内容 说明 │
│ ──────────────── ──────────────────────────────────── ────────────────── │
│ 0x00000000- NULL 指针保护区 访问即触发异常 │
│ 0x0000FFFF │
│ │
│ 0x00010000- 用户代码、数据、堆、栈 应用程序主要区域 │
│ 0x7FFDFFFF │
│ │ │
│ ├── 0x00400000 通常是可执行文件基址 │
│ ├── 0x00220000 通常是 NTDLL.DLL 基址 │
│ ├── 0x7FFDF000 PEB 位置(固定偏移) │
│ └── 0x7FFDE000 TEB 位置(每个线程一个) │
│ │
│ 0x7FFE0000- KUSER_SHARED_DATA 用户态只读内核数据 │
│ 0x7FFEFFFF │
│ │
│ 0x7FFF0000- 系统映射区域 保留给系统使用 │
│ 0x7FFFFFFF │
│ │
│ 0x80000000- 内核空间 3GB 开关时为 0xC0000000 │
│ 0xFFFFFFFF │
│ │
│ 64 位系统(用户空间 ~8TB): │
│ │
│ 0x00000000`00000000- 空指针保护区 │
│ 0x00000000`0000FFFF │
│ │
│ 0x00000000`00010000- 用户代码、数据 应用程序区域 │
│ 0x00007FFF`FFFFFFFF │
│ │
│ 0xFFFFFE00`00000000- KUSER_SHARED_DATA(64 位位置) │
│ 0xFFFFFE00`00010000 │
│ │
│ 0xFFFFF780`00000000- 系统 DLL 区域(ntdll, kernel32 等) │
│ 0xFFFFF7FF`FFFFFFFF │
│ │
│ 0xFFFF8000`00000000- 内核空间 │
│ 0xFFFFFFFF`FFFFFFFF │
└──────────────────────────────────────────────────────────────────────────────────┘
5.2.0.1 设计意图
核心问题
用户态进程能看到什么?用户态与内核态如何通信?PEB 和 TEB 在其中扮演什么角色?
设计哲学 :「用户态可观察 + 内核态可控制」
想象一下,Windows 操作系统就像一个庞大的现代化办公大楼:
- 用户态空间:这是大楼里各个公司(进程)租用的办公室。每个办公室有自己的家具、设备和员工(线程)。
- 内核态空间:这是大楼的基础设施层,包括电力系统、供水系统、电梯、安保等。普通员工(用户态代码)不能直接进入这个区域。
- PEB :每个办公室门口的信息公告板,上面贴着公司名称、员工名单、紧急联系人等信息。
- TEB :每个员工(线程)随身携带的工作证,记录着个人信息、当前任务、紧急联系方式。
- KUSER_SHARED_DATA :大楼入口处的公告栏,张贴着所有公司都能看到的公共信息(如当前时间、大楼公告等)。
本节定位
本节深入分析 Windows 进程用户空间的核心数据结构:PEB(Process Environment Block)和 TEB(Thread Environment Block)。读完本节后,读者应当能够:
- 理解 PEB/TEB 的内存布局
- 掌握关键字段的含义和用途
- 理解用户态与内核态的通信机制
- 了解安全相关的字段和机制
5.2.1 PEB(Process Environment Block)
PEB:进程的"身份证"和"信息中心"
如果把进程比作一家正在营业的商店 ,那么 PEB 就是这家商店的营业执照 和店内信息看板。它记录了商店的基本信息、经营范围、员工名单、库存情况等所有重要数据。
┌──────────────────────────────────────────────────────────────────────────────────┐
│ PEB 内存布局 │
│ │
│ PEB 结构(偏移从 PEB 基址开始): │
│ │
│ 偏移 字段名 类型 说明 │
│ ──────── ──────────────────────── ──────────────────── ───────────────── │
│ 0x00 InheritedAddressSpace BOOLEAN 是否继承地址空间 │
│ 0x01 ReadImageFileExecOptions BOOLEAN 读取执行选项 │
│ 0x02 BeingDebugged BOOLEAN 是否正在被调试 │
│ 0x03 BitField UCHAR 位标志 │
│ 0x04 Mutant HANDLE 进程互斥体 │
│ 0x08 ImageBaseAddress PVOID 可执行文件基址 │
│ 0x0C Ldr PPEB_LDR_DATA 模块加载器数据 │
│ 0x10 ProcessParameters PRTL_USER_PROCESS_PARAMETERS 进程参数 │
│ 0x14 SubSystemData PVOID 子系统数据 │
│ 0x18 ProcessHeap PVOID 默认进程堆 │
│ 0x1C FastPEBLockRoutine PPEBLOCKROUTINE 快速 PEB 锁定例程 │
│ 0x20 FastPEBUnlockRoutine PPEBLOCKROUTINE 快速 PEB 解锁例程 │
│ 0x24 ActiveProcessAffinityMask KAFFINITY 活动 CPU 亲和性 │
│ 0x28 GdiHandleBuffer ULONG[34] GDI 句柄缓冲区 │
│ 0xAC PostProcessInitRoutine PPOSTPROCESSINITROUTINE 后初始化例程 │
│ 0xB0 TlsExpansionSlots PVOID TLS 扩展槽位 │
│ 0xB4 SecureProcess BOOLEAN 是否安全进程 │
│ 0xB5 Spare1 UCHAR 保留 │
│ 0xB6 Spare2 UCHAR 保留 │
│ 0xB7 Spare3 UCHAR 保留 │
│ 0xB8 Spare4 ULONG 保留 │
│ 0xBC TlsBitmap PVOID TLS 位图 │
│ 0xC0 TlsBitmapBits ULONG[2] TLS 位图位 │
│ 0xC8 ReadOnlySharedMemoryBase PVOID 只读共享内存基址 │
│ 0xCC HotpatchInformation PVOID 热补丁信息 │
│ 0xD0 ReadOnlyStaticServerData PVOID 只读静态服务器数据 │
│ 0xD4 AnsiCodePageData PVOID ANSI 代码页数据 │
│ 0xD8 OemCodePageData PVOID OEM 代码页数据 │
│ 0xDC UnicodeCaseTableData PVOID Unicode 大小写表 │
│ 0xE0 NumberOfProcessors ULONG 处理器数量 │
│ 0xE4 NtGlobalFlag ULONG NT 全局标志 │
│ 0xE8 CriticalSectionTimeout LARGE_INTEGER 临界区超时时间 │
│ 0xF0 HeapSegmentReserve SIZE_T 堆段保留大小 │
│ 0xF4 HeapSegmentCommit SIZE_T 堆段提交大小 │
│ 0xF8 HeapDeCommitTotalFreeThreshold SIZE_T 堆释放阈值 │
│ 0xFC HeapDeCommitFreeBlockThreshold SIZE_T 堆释放块阈值 │
│ 0x100 NumberOfHeaps ULONG 堆数量 │
│ 0x104 MaximumNumberOfHeaps ULONG 最大堆数量 │
│ 0x108 ProcessHeaps PVOID* 堆数组指针 │
│ 0x10C GdiSharedHandleTable PVOID GDI 共享句柄表 │
│ 0x110 ProcessStarterHelper PVOID 进程启动助手 │
│ 0x114 GdiDCAttributeList ULONG GDI DC 属性列表 │
│ 0x118 LoaderLock PVOID 加载器锁 │
│ 0x11C OSMajorVersion ULONG OS 主版本号 │
│ 0x120 OSMinorVersion ULONG OS 次版本号 │
│ 0x124 OSBuildNumber USHORT OS 构建号 │
│ 0x126 OSCSDVersion USHORT OS CSD 版本 │
│ 0x128 OSPlatformId ULONG OS 平台 ID │
│ 0x12C ImageSubsystem ULONG 镜像子系统 │
│ 0x130 ImageSubsystemMajorVersion USHORT 子系统主版本 │
│ 0x132 ImageSubsystemMinorVersion USHORT 子系统次版本 │
│ 0x134 ImageProcessAffinityMask KAFFINITY 镜像 CPU 亲和性 │
│ 0x138 GdiHandleBuffer32 PVOID GDI 句柄缓冲区 32 │
│ 0x13C LoaderReserve PVOID 加载器保留 │
│ 0x140 LoaderCommit PVOID 加载器提交 │
│ 0x144 LoaderStackCommit PVOID 加载器栈提交 │
│ 0x148 PatchInformation PVOID 补丁信息 │
│ 0x14C Cookie ULONG 进程 Cookie │
│ 0x150 Spare5 ULONG[4] 保留 │
│ 0x160 TlsExpansionBitmap PVOID TLS 扩展位图 │
│ 0x164 TlsExpansionBitmapBits ULONG[32] TLS 扩展位图位 │
│ 0x1E4 SessionId ULONG 会话 ID │
│ 0x1E8 AppCompatFlags ULONG 兼容标志 │
│ 0x1EC AppCompatFlagsUser ULONG 用户兼容标志 │
│ 0x1F0 pShimData PVOID Shim 数据 │
│ 0x1F4 AppCompatInfo PVOID 兼容信息 │
│ 0x1F8 CSDVersion UNICODE_STRING CSD 版本字符串 │
│ 0x204 ActivationContextData PVOID 激活上下文数据 │
│ 0x208 ProcessAssemblyStorageMap PVOID 程序集存储映射 │
│ 0x20C SystemDefaultActivationContextData PVOID 系统默认激活上下文 │
│ 0x210 SystemAssemblyStorageMap PVOID 系统程序集存储映射 │
│ 0x214 MinimumStackCommit SIZE_T 最小栈提交 │
│ │
│ PEB 在内存中的位置: 32 位: 0x7FFDF000, 64 位: 0x7FFF00000000 │
│ 获取方式: NtCurrentTeb()->ProcessEnvironmentBlock │
└──────────────────────────────────────────────────────────────────────────────────┘
PEB 关键字段详解
BeingDebugged
c
// 检查是否被调试
if (NtCurrentTeb()->ProcessEnvironmentBlock->BeingDebugged) {
// 正在被调试
}
用途:调试器附加时设置为 TRUE,常用作反调试检测。
ImageBaseAddress
c
// 获取可执行文件基址
PVOID ImageBase = NtCurrentTeb()->ProcessEnvironmentBlock->ImageBaseAddress;
用途:指向当前进程可执行文件的加载基址。
Ldr (PEB_LDR_DATA)
c
typedef struct _PEB_LDR_DATA {
ULONG Length;
BOOLEAN Initialized;
PVOID SsHandle;
LIST_ENTRY InLoadOrderModuleList; // 加载顺序链表
LIST_ENTRY InMemoryOrderModuleList; // 内存顺序链表
LIST_ENTRY InInitializationOrderModuleList; // 初始化顺序链表
PVOID EntryInProgress;
} PEB_LDR_DATA, *PPEB_LDR_DATA;
用途:维护进程已加载的模块列表(DLL)。
ProcessParameters (RTL_USER_PROCESS_PARAMETERS)
c
typedef struct _RTL_USER_PROCESS_PARAMETERS {
ULONG MaximumLength;
ULONG Length;
ULONG Flags;
ULONG DebugFlags;
PVOID ConsoleHandle;
ULONG ConsoleFlags;
HANDLE StandardInput;
HANDLE StandardOutput;
HANDLE StandardError;
CURDIR CurrentDirectory;
UNICODE_STRING DllPath;
UNICODE_STRING ImagePathName;
UNICODE_STRING CommandLine;
PVOID Environment;
ULONG StartingX;
ULONG StartingY;
ULONG CountX;
ULONG CountY;
ULONG CountCharsX;
ULONG CountCharsY;
ULONG FillAttribute;
ULONG WindowFlags;
ULONG ShowWindowFlags;
UNICODE_STRING WindowTitle;
UNICODE_STRING DesktopInfo;
UNICODE_STRING ShellInfo;
UNICODE_STRING RuntimeData;
} RTL_USER_PROCESS_PARAMETERS, *PRTL_USER_PROCESS_PARAMETERS;
用途:存储进程启动时的参数(命令行、环境变量、工作目录等)。
ProcessHeap
用途:指向进程默认堆的指针,由 RtlCreateHeap 创建。
NtGlobalFlag
用途:调试相关标志,包含多种调试选项。
5.2.2 TEB(Thread Environment Block)
TEB:线程的"个人档案"和"工作手册"
如果说 PEB 是商店的营业执照 ,那么 TEB 就是每个员工的个人档案。想象一下,商店里的每个员工都有一个专属的档案夹,里面记录着:
-
个人信息:姓名、工号、所属部门(ClientId)
-
工作状态:当前任务、工作进度、上次出错记录(LastErrorValue)
-
工作工具:计算器、笔记本、绘图工具(TlsSlots、GDI 相关字段)
-
紧急联系人:遇到问题时该找谁(Peb 指针,指向商店信息)
┌──────────────────────────────────────────────────────────────────────────────────┐
│ TEB 内存布局 │
│ │
│ TEB 结构(偏移从 TEB 基址开始): │
│ │
│ 偏移 字段名 类型 说明 │
│ ──────── ──────────────────────── ──────────────────── ───────────────── │
│ 0x00 NtTib NT_TIB 线程信息块 │
│ 0x1C EnvironmentPointer PVOID 环境指针 │
│ 0x20 ClientId CLIENT_ID 客户端 ID (PID/TID) │
│ 0x28 ActiveRpcHandle PVOID 活动 RPC 句柄 │
│ 0x2C ThreadLocalStoragePointer PVOID TLS 指针 │
│ 0x30 Peb PPEB 进程环境块 │
│ 0x34 LastErrorValue NTSTATUS 最后错误值 │
│ 0x38 CountOfOwnedCriticalSections ULONG 临界区数量 │
│ 0x3C CsrClientThread PVOID CSR 客户端线程 │
│ 0x40 Win32ThreadInfo PVOID Win32 线程信息 │
│ 0x44 User32Reserved ULONG[26] User32 保留 │
│ 0xAC UserReserved ULONG[5] 用户保留 │
│ 0xC0 WOW32Reserved PVOID WOW32 保留 │
│ 0xC4 CurrentLocale LCID 当前区域设置 │
│ 0xC8 FpSoftwareStatusRegister ULONG FP 软件状态寄存器 │
│ 0xCC SystemReserved1 PVOID[3] 系统保留 │
│ 0xD8 ExceptionCode NTSTATUS 异常代码 │
│ 0xDC ActivationContextStack PACTIVATION_CONTEXT_STACK 激活上下文栈 │
│ 0x130 Instrumentation PVOID 检测数据 │
│ 0x134 WinSockData PVOID WinSock 数据 │
│ 0x138 GdiTebBatch PVOID GDI 批处理 │
│ 0x13C GdiBatchCount ULONG GDI 批处理计数 │
│ 0x140 GdiSharedHandleTable PVOID GDI 共享句柄表 │
│ 0x144 GdiStarterBitmap PVOID GDI 启动位图 │
│ 0x148 GdiBrushBitmap PVOID GDI 画刷位图 │
│ 0x14C GdiPenBitmap PVOID GDI 画笔位图 │
│ 0x150 PaletteTable PVOID 调色板表 │
│ 0x154 RealClientId CLIENT_ID 真实客户端 ID │
│ 0x15C GdiCachedProcessHandle HANDLE GDI 缓存进程句柄 │
│ 0x160 GdiClientPID ULONG GDI 客户端 PID │
│ 0x164 GdiClientTID ULONG GDI 客户端 TID │
│ 0x168 GdiThreadLocalInfo PVOID GDI 线程本地信息 │
│ 0x16C Win32ClientInfo ULONG Win32 客户端信息 │
│ 0x170 glDispatchTable PVOID[233] OpenGL 分发表 │
│ 0x4CC glReserved1 ULONG[29] OpenGL 保留 │
│ 0x544 glReserved2 PVOID OpenGL 保留 │
│ 0x548 glSection PVOID OpenGL 段 │
│ 0x54C glSectionOffset ULONG OpenGL 段偏移 │
│ 0x550 glTable PVOID OpenGL 表 │
│ 0x554 glCurrentRC PVOID OpenGL 当前 RC │
│ 0x558 glContext PVOID OpenGL 上下文 │
│ 0x55C LastStatusValue NTSTATUS 最后状态值 │
│ 0x560 StaticUnicodeString UNICODE_STRING 静态 Unicode 字符串 │
│ 0x56C StaticUnicodeBuffer WCHAR[261] 静态 Unicode 缓冲区 │
│ 0x770 DeallocationStack PVOID 释放栈 │
│ 0x774 TlsSlots PVOID[TLS_MINIMUM_AVAILABLE] TLS 槽位 │
│ 0x804 TlsExpansionSlots PVOID* TLS 扩展槽位 │
│ │
│ TEB 在内存中的位置: 每个线程独立,通过 FS:[0] 访问(32 位)或 GS:[0] 访问(64 位) │
│ 获取方式: NtCurrentTeb() │
└──────────────────────────────────────────────────────────────────────────────────┘
TEB 关键字段详解
NT_TIB (NtTib)
c
typedef struct _NT_TIB {
struct _EXCEPTION_REGISTRATION_RECORD *ExceptionList;
PVOID StackBase; // 栈基址
PVOID StackLimit; // 栈界限
PVOID SubSystemTib;
union {
PVOID FiberData;
ULONG Version;
};
PVOID ArbitraryUserPointer;
struct _NT_TIB *Self; // 指向自身
} NT_TIB;
用途:存储线程栈信息和异常链。
ClientId
c
typedef struct _CLIENT_ID {
HANDLE UniqueProcess; // 进程 ID
HANDLE UniqueThread; // 线程 ID
} CLIENT_ID;
用途:标识线程所属的进程和线程本身。
Peb
用途:指向所属进程的 PEB。
LastErrorValue
用途:存储线程的最后错误码(SetLastError/GetLastError 使用)。
TlsSlots
用途:线程局部存储槽位数组,大小为 TLS_MINIMUM_AVAILABLE(64)。
5.2.3 用户态栈与内核态栈
栈:线程的"工作台"和"记事本"
想象一下,每个员工(线程)在工作时都有两张桌子:
-
用户态栈 :这是员工在自己办公室里的办公桌。桌面上放着当前正在处理的文件、笔记本和计算器。员工可以自由地在这张桌子上工作,不需要经过任何人批准。
-
内核态栈 :这是员工在大楼机房里的专用工作台。当员工需要使用大楼的基础设施(比如使用电梯、查看电力系统状态)时,必须到这个工作台来操作。这个工作台受到严格的安保控制。
当员工(线程)需要执行系统调用时,就像从自己的办公桌走到机房的工作台:
-
保存工作:把当前正在处理的文件(寄存器状态)整理好放在机房工作台上
-
切换场地:从办公室(用户态)移动到机房(内核态)
-
执行操作:在机房工作台上执行需要的操作(系统调用处理)
-
恢复工作:回到办公室,继续之前的工作
┌──────────────────────────────────────────────────────────────────────────────────┐
│ 用户态栈与内核态栈 │
│ │
│ 用户态线程结构: │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 用户态地址空间 │ │
│ │ │ │
│ │ TEB (0x7FFDE000) │ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ NT_TIB: │ │ │
│ │ │ StackBase = 0x0012FFF0 (栈顶) │ │ │
│ │ │ StackLimit = 0x00120000 (栈底) │ │ │
│ │ └─────────────────────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ 用户态栈 │ │
│ │ 0x0012FFF0 ───────────────────────────────────────► 0x00120000 │ │
│ │ ↑ │ │
│ │ 高地址 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ 系统调用 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 内核态地址空间 │ │
│ │ │ │
│ │ ETHREAD │ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ KTHREAD: │ │ │
│ │ │ StackBase = 0x80500000 (内核栈顶) │ │ │
│ │ │ StackLimit = 0x804FF000 (内核栈底) │ │ │
│ │ └─────────────────────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ 内核态栈 │ │
│ │ 0x80500000 ───────────────────────────────────────► 0x804FF000 │ │
│ │ ↑ │ │
│ │ 高地址 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 系统调用时的栈切换: │
│ │
│ KiSystemService (系统调用入口): │
│ 1. 保存用户态上下文到内核栈 │
│ 2. 切换到内核栈 (SS:ESP = 内核栈段:内核栈指针) │
│ 3. 执行系统调用处理函数 │
│ 4. 恢复用户态上下文 │
│ 5. 切换回用户态栈 │
│ │
│ 64 位系统的 IST (Interrupt Stack Table): │
│ - IST0: 系统调用栈 │
│ - IST1: 调试异常栈 │
│ - IST2: NMI 栈 │
│ - IST3-7: 保留 │
└──────────────────────────────────────────────────────────────────────────────────┘
5.2.4 KUSER_SHARED_DATA
KUSER_SHARED_DATA:大楼里的"公告栏"
想象一下,在办公大楼的入口大厅里,有一个电子公告板,上面显示着:
- 当前时间:所有公司的员工都能看到准确的时间
- 大楼状态:电力系统状态、网络状态、可用停车位数量
- 公告信息:大楼通知、维护计划、安全提醒
- 设施信息:电梯状态、会议室预约情况
这个公告板有几个特点:
-
只读访问:任何人都可以看,但只有大楼管理员(内核态)可以更新
-
即时更新:管理员更新后,所有人立即看到最新信息
-
公共位置:每个人都知道公告板在哪里,不需要问路
┌──────────────────────────────────────────────────────────────────────────────────┐
│ KUSER_SHARED_DATA │
│ │
│ 地址: │
│ - 32 位: 0x7FFE0000 │
│ - 64 位: 0xFFFFFE0000000000 │
│ │
│ 关键字段: │
│ │
│ 偏移 字段名 类型 说明 │
│ ──────── ──────────────────────── ──────────────────── ───────────────── │
│ 0x000 TickCountLow ULONG 低 32 位 TickCount │
│ 0x004 TickCountMultiplier ULONG TickCount 乘数 │
│ 0x008 InterruptTime LARGE_INTEGER 中断时间 │
│ 0x010 SystemTime LARGE_INTEGER 系统时间 │
│ 0x018 TimeZoneBias LARGE_INTEGER 时区偏移 │
│ 0x020 ImageNumberLow ULONG 镜像号(低) │
│ 0x024 ImageNumberHigh ULONG 镜像号(高) │
│ 0x028 NtSystemRoot UNICODE_STRING NT 系统根目录 │
│ 0x038 MaxStackTraceDepth ULONG 最大堆栈深度 │
│ 0x03C CryptoExponent ULONG 加密指数 │
│ 0x040 TimeZoneId ULONG 时区 ID │
│ 0x044 Reserved ULONG 保留 │
│ 0x048 Reserved2 ULONG 保留 │
│ 0x04C TimeZoneBiasStamp LARGE_INTEGER 时区偏移戳 │
│ 0x054 Cookie ULONG 安全 Cookie │
│ 0x058 ConsoleSessionId ULONG 控制台会话 ID │
│ 0x05C ImageNumber ULONG 镜像号 │
│ 0x060 NtProductType UCHAR NT 产品类型 │
│ 0x061 ProductTypeIsValid BOOLEAN 产品类型有效 │
│ 0x062 Reserved3 UCHAR 保留 │
│ 0x063 Reserved4 UCHAR 保留 │
│ 0x064 NtMajorVersion ULONG NT 主版本号 │
│ 0x068 NtMinorVersion ULONG NT 次版本号 │
│ 0x06C ProcessorFeatures ULONG[64] 处理器特性 │
│ 0x16C Reserved5 ULONG[32] 保留 │
│ 0x20C ActiveConsoleId ULONG 活动控制台 ID │
│ 0x210 DismountCount ULONG 卸载计数 │
│ 0x214 ComPlusPackage ULONG COM+ 包 │
│ 0x218 LastSystemRITEventTick LARGE_INTEGER 最后系统 RIT 事件 │
│ 0x220 NumberOfPhysicalPages ULONG 物理页数 │
│ 0x224 LowPhysicalMemoryLimit ULONG 低物理内存限制 │
│ 0x228 HighPhysicalMemoryLimit ULONG 高物理内存限制 │
│ 0x22C AllocationGranularity ULONG 分配粒度 │
│ 0x230 NonPagedSystemSize ULONG 非分页系统大小 │
│ 0x234 PagedSystemSize ULONG 分页系统大小 │
│ 0x238 CommitLimit ULONG 提交限制 │
│ 0x23C CommitPeak ULONG 提交峰值 │
│ 0x240 CommitTotal ULONG 提交总数 │
│ 0x244 PhysicalMemorySize ULONG 物理内存大小 │
│ 0x248 PhysicalMemoryUsed ULONG 物理内存使用量 │
│ 0x24C SystemCacheSize ULONG 系统缓存大小 │
│ 0x250 SystemCachePeak ULONG 系统缓存峰值 │
│ 0x254 PoolPagedUsed ULONG 分页池使用量 │
│ 0x258 PoolNonPagedUsed ULONG 非分页池使用量 │
│ 0x25C SystemDriverPages ULONG 系统驱动页数 │
│ 0x260 SystemCodePages ULONG 系统代码页数 │
│ 0x264 TotalSystemPages ULONG 总系统页数 │
│ 0x268 Reserved6 ULONG 保留 │
│ 0x26C Reserved7 ULONG 保留 │
│ 0x270 Reserved8 ULONG 保留 │
│ 0x274 Reserved9 ULONG 保留 │
│ 0x278 TimeAdjustment ULONGLONG 时间调整 │
│ 0x280 BootId ULONG 启动 ID │
│ 0x284 SystemRootPathName UNICODE_STRING 系统根路径名称 │
│ │
│ 访问方式: 用户态直接访问该地址,无需系统调用 │
│ 安全性: 用户态只读,内核态可写 │
└──────────────────────────────────────────────────────────────────────────────────┘
5.2.5 进程参数(RTL_USER_PROCESS_PARAMETERS)
RTL_USER_PROCESS_PARAMETERS:商店开业时的"开业许可证"
想象一下,当一家新商店要开业时,需要向管理部门提交一份开业申请表,上面写着:
- 商店名称和地址:ImagePathName(可执行文件路径)
- 经营范围:CommandLine(命令行参数)
- 营业场所:CurrentDirectory(当前工作目录)
- 联系方式:Environment(环境变量,包含各种配置信息)
- 店面要求:窗口大小、位置、标题等
这份申请表在商店开业前就准备好了,商店老板(进程)可以随时查看这些信息。
┌──────────────────────────────────────────────────────────────────────────────────┐
│ RTL_USER_PROCESS_PARAMETERS │
│ │
│ 结构定义(简化版): │
│ │
│ typedef struct _RTL_USER_PROCESS_PARAMETERS { │
│ ULONG MaximumLength; // 结构最大长度 │
│ ULONG Length; // 实际长度 │
│ ULONG Flags; // 标志 │
│ ULONG DebugFlags; // 调试标志 │
│ PVOID ConsoleHandle; // 控制台句柄 │
│ ULONG ConsoleFlags; // 控制台标志 │
│ HANDLE StandardInput; // 标准输入 │
│ HANDLE StandardOutput; // 标准输出 │
│ HANDLE StandardError; // 标准错误 │
│ CURDIR CurrentDirectory; // 当前目录 │
│ UNICODE_STRING DllPath; // DLL 搜索路径 │
│ UNICODE_STRING ImagePathName; // 可执行文件路径 │
│ UNICODE_STRING CommandLine; // 命令行参数 │
│ PVOID Environment; // 环境变量块 │
│ ULONG StartingX; // 窗口起始 X 坐标 │
│ ULONG StartingY; // 窗口起始 Y 坐标 │
│ ULONG CountX; // 窗口宽度 │
│ ULONG CountY; // 窗口高度 │
│ ULONG CountCharsX; // 字符宽度 │
│ ULONG CountCharsY; // 字符高度 │
│ ULONG FillAttribute; // 填充属性 │
│ ULONG WindowFlags; // 窗口标志 │
│ ULONG ShowWindowFlags; // 显示窗口标志 │
│ UNICODE_STRING WindowTitle; // 窗口标题 │
│ UNICODE_STRING DesktopInfo; // 桌面信息 │
│ UNICODE_STRING ShellInfo; // Shell 信息 │
│ UNICODE_STRING RuntimeData; // 运行时数据 │
│ } RTL_USER_PROCESS_PARAMETERS, *PRTL_USER_PROCESS_PARAMETERS; │
│ │
│ 获取方式: PEB->ProcessParameters │
│ 创建时机: BasePushProcessParameters (在进程启动前) │
└──────────────────────────────────────────────────────────────────────────────────┘
5.2.6 模块列表(PEB_LDR_DATA)
PEB_LDR_DATA:商店的"员工花名册"
想象一下,商店开业后,需要一份员工花名册,记录所有在店里工作的员工(DLL)。这份花名册有三个版本:
-
招聘顺序名单(InLoadOrderModuleList):按招聘时间排序,记录谁先入职谁后入职。就像 ntdll.dll 是第一个来的,然后是 kernel32.dll,再是 user32.dll。
-
座位位置名单(InMemoryOrderModuleList):按员工座位位置排序,从办公室前面到后面。就像低地址的 DLL 在前面,高地址的 DLL 在后面。
-
培训顺序名单(InInitializationOrderModuleList):按培训顺序排序,需要先培训的先排。就像基础模块先初始化,依赖它的模块后初始化。
每个员工档案(LDR_DATA_TABLE_ENTRY)里记录着:
-
座位号(DllBase):员工坐在哪个位置
-
工作职责(EntryPoint):员工主要负责什么工作
-
入职时间(TimeDateStamp):什么时候入职的
┌──────────────────────────────────────────────────────────────────────────────────┐
│ PEB_LDR_DATA 模块列表 │
│ │
│ PEB_LDR_DATA 结构: │
│ │
│ typedef struct _PEB_LDR_DATA { │
│ ULONG Length; // 结构长度 │
│ BOOLEAN Initialized; // 是否已初始化 │
│ PVOID SsHandle; // 会话句柄 │
│ LIST_ENTRY InLoadOrderModuleList; // 加载顺序链表 │
│ LIST_ENTRY InMemoryOrderModuleList; // 内存顺序链表 │
│ LIST_ENTRY InInitializationOrderModuleList; // 初始化顺序链表 │
│ PVOID EntryInProgress; // 正在处理的条目 │
│ } PEB_LDR_DATA, *PPEB_LDR_DATA; │
│ │
│ LDR_DATA_TABLE_ENTRY 结构(链表节点): │
│ │
│ typedef struct _LDR_DATA_TABLE_ENTRY { │
│ LIST_ENTRY InLoadOrderLinks; // 加载顺序链接 │
│ LIST_ENTRY InMemoryOrderLinks; // 内存顺序链接 │
│ LIST_ENTRY InInitializationOrderLinks; // 初始化顺序链接 │
│ PVOID DllBase; // DLL 基址 │
│ PVOID EntryPoint; // 入口点 │
│ ULONG SizeOfImage; // 镜像大小 │
│ UNICODE_STRING FullDllName; // 完整路径 │
│ UNICODE_STRING BaseDllName; // 基础名称 │
│ ULONG Flags; // 标志 │
│ WORD LoadCount; // 加载计数 │
│ WORD TlsIndex; // TLS 索引 │
│ LIST_ENTRY HashLinks; // 哈希链接 │
│ PVOID SectionPointer; // 段指针 │
│ ULONG CheckSum; // 校验和 │
│ ULONG TimeDateStamp; // 时间戳 │
│ PVOID LoadedImports; // 已加载的导入 │
│ PVOID EntryPointActivationContext; // 入口点激活上下文 │
│ PVOID PatchInformation; // 补丁信息 │
│ } LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY; │
│ │
│ 三个链表的用途: │
│ │
│ 1. InLoadOrderModuleList: │
│ - 按照 LoadLibrary 的顺序排列 │
│ - 通常: ntdll.dll → kernel32.dll → user32.dll → ... │
│ │
│ 2. InMemoryOrderModuleList: │
│ - 按照内存地址顺序排列 │
│ - 低地址 DLL 在前,高地址 DLL 在后 │
│ │
│ 3. InInitializationOrderModuleList: │
│ - 按照 DllMain 的调用顺序排列 │
│ - 依赖 DLL 先初始化 │
│ │
│ 遍历示例: │
│ │
│ PPEB_LDR_DATA Ldr = NtCurrentTeb()->ProcessEnvironmentBlock->Ldr; │
│ PLIST_ENTRY ListEntry = Ldr->InLoadOrderModuleList.Flink; │
│ while (ListEntry != &Ldr->InLoadOrderModuleList) { │
│ PLDR_DATA_TABLE_ENTRY Entry = CONTAINING_RECORD(ListEntry, │
│ LDR_DATA_TABLE_ENTRY, │
│ InLoadOrderLinks); │
│ // 处理 Entry... │
│ ListEntry = ListEntry->Flink; │
│ } │
└──────────────────────────────────────────────────────────────────────────────────┘
5.2.7 关键设计决策的深度分析
5.2.7.1 PEB/TEB 放在用户态的合理性
为什么 PEB/TEB 必须放在用户态?
- 性能优化:用户态代码可以直接访问 PEB/TEB,不需要系统调用
- 调试支持:调试器可以直接读取 PEB/TEB 了解进程/线程状态
- 兼容性:很多用户态代码依赖 PEB/TEB 中的信息
- 动态链接:LDR 需要在用户态访问模块列表
5.2.7.2 NtCurrentTeb() 的实现机理
c
// 32 位实现
__declspec(naked) PTEB NtCurrentTeb(void) {
__asm {
mov eax, fs:[0x18] // FS:[0x18] 指向 TEB->Self
ret
}
}
// 64 位实现
__declspec(naked) PTEB NtCurrentTeb(void) {
__asm {
mov rax, gs:[0x30] // GS:[0x30] 指向 TEB
ret
}
}
为什么使用 FS/GS 段寄存器?
- 线程本地存储:每个线程有独立的 FS/GS 段基址
- 快速访问:单条指令即可获取 TEB 指针
- 历史原因:从 Windows NT 早期延续下来的设计
5.2.7.3 KUSER_SHARED_DATA 的单向只读性
为什么 KUSER_SHARED_DATA 是用户态只读?
- 安全性:防止用户态代码修改系统关键数据
- 性能优化:内核态可以直接写入,用户态可以直接读取,不需要系统调用
- 一致性:保证数据的一致性(内核写入后立即对用户态可见)
5.2.7.4 TLS 槽位的大小选择
c
#define TLS_MINIMUM_AVAILABLE 64
为什么是 64?
- 历史原因:Windows NT 3.1 以来的传统
- 足够使用:大多数应用程序不需要超过 64 个 TLS 槽位
- 扩展支持:超过 64 个时使用 TlsExpansionSlots
5.2.8 概念解释
5.2.8.1 PEB / TEB / NT_TIB
- PEB:进程环境块,存储进程级别的用户态信息
- TEB:线程环境块,存储线程级别的用户态信息
- NT_TIB:线程信息块,是 TEB 的第一个字段,存储栈信息
5.2.8.2 TLS(Thread Local Storage)
线程局部存储,允许每个线程拥有独立的数据副本。
5.2.8.3 KUSER_SHARED_DATA
内核态和用户态共享的数据区域,用户态只读,内核态可写。
5.2.8.4 Stack Cookie (/GS)
栈安全机制,在函数栈帧中插入随机值,函数返回前检查是否被修改。
5.2.9 为什么要这样设计
5.2.9.1 为什么 PEB/TEB 必须放在用户态而不是只在内核态?
答案:性能和兼容性。
- 性能:用户态代码可以直接访问,不需要系统调用
- 兼容性:很多旧代码依赖 PEB/TEB
- 调试:调试器需要直接访问这些结构
5.2.9.2 为什么每个线程有独立的 TEB?
答案:线程独立性。
- 每个线程有独立的栈、寄存器、TLS
- TEB 存储线程特定的信息
- 支持线程安全的操作(如 GetLastError)
5.2.9.3 为什么 PEB 的 LDR 链表有三个?
答案:不同的遍历需求。
- LoadOrder:按照加载顺序,用于依赖分析
- MemoryOrder:按照内存地址,用于内存操作
- InitializationOrder:按照初始化顺序,用于 DllMain 调用
5.2.9.4 KUSER_SHARED_DATA 为什么用 0x7FFE0000 这个"魔数"地址?
答案:历史原因和内存布局。
- 32 位系统中,0x7FFE0000 是用户空间的最高地址附近
- 这个地址不会与应用程序代码冲突
- 是 Windows NT 早期设计的延续
5.2.9.5 为什么 LastErrorValue 在 TEB 中而不是 PEB?
答案:线程安全。
- 每个线程应该有独立的错误码
- 如果放在 PEB 中,多线程会互相覆盖
- GetLastError/SetLastError 需要线程安全
5.2.10 增强子节 1:PEB/TEB 的安全视角
┌──────────────────────────────────────────────────────────────────────────────────┐
│ PEB/TEB 的安全视角 │
│ │
│ 反调试检测技术: │
│ │
│ 1. BeingDebugged 检测: │
│ │
│ if (PEB->BeingDebugged) { │
│ // 被调试! │
│ } │
│ │
│ 绕过方法: 修改 PEB->BeingDebugged 为 0 │
│ │
│ 2. NtGlobalFlag 检测: │
│ │
│ if (PEB->NtGlobalFlag & FLG_HEAP_ENABLE_TAIL_CHECK) { │
│ // 被调试! │
│ } │
│ │
│ 3. 检查调试端口: │
│ │
│ NtQueryInformationProcess(ProcessHandle, │
│ ProcessDebugPort, │
│ &DebugPort, │
│ sizeof(DebugPort), │
│ NULL); │
│ if (DebugPort != NULL) { │
│ // 被调试! │
│ } │
│ │
│ 4. 检查父进程: │
│ │
│ // 获取父进程 PID │
│ DWORD ParentPid = PEB->InheritedFromUniqueProcessId; │
│ // 检查是否是调试器进程(如 windbg.exe, x64dbg.exe) │
│ │
│ Stack Cookie (/GS): │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 函数栈帧结构 (启用 /GS): │ │
│ │ │ │
│ │ High Address │ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ 返回地址 │ │ │
│ │ ├─────────────────────────────────────────────────────────────┤ │ │
│ │ │ Stack Cookie (随机值) │ │ │
│ │ ├─────────────────────────────────────────────────────────────┤ │ │
│ │ │ 局部变量 │ │ │
│ │ └─────────────────────────────────────────────────────────────┘ │ │
│ │ Low Address │ │
│ │ │ │
│ │ 保护机制: │ │
│ │ 1. 函数入口: 生成随机 Cookie 并存入栈中 │ │
│ │ 2. 函数返回前: 检查 Cookie 是否被修改 │ │
│ │ 3. 如果修改: 调用 __security_error_handler │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ Cookie 的生成位置: PEB->Cookie │
│ 获取方式: RtlRandomizeCookie() 在进程启动时调用 │
└──────────────────────────────────────────────────────────────────────────────────┘
5.2.11 增强子节 2:用户态堆(Heap)
┌──────────────────────────────────────────────────────────────────────────────────┐
│ 用户态堆(Heap) │
│ │
│ 进程默认堆: PEB->ProcessHeap │
│ │
│ 堆结构: │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ HEAP 结构 │ │
│ │ │ │
│ │ 字段: │ │
│ │ - SegmentList: 段链表 │ │
│ │ - FreeList: 空闲块链表 │ │
│ │ - LookasideList: 快速分配缓存 │ │
│ │ - Flags: 堆标志 │ │
│ │ - Size: 堆大小 │ │
│ │ - AllocationSize: 分配大小 │ │
│ │ - UserFlags: 用户标志 │ │
│ │ - VirtualMemoryThreshold: 虚拟内存阈值 │ │
│ │ - Signature: 签名(用于验证) │ │
│ │ - ForceFlags: 强制标志 │ │
│ │ - Reserved: 保留 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ LFH (Low Fragmentation Heap): │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ LFH 工作原理 │ │
│ │ │ │
│ │ 1. 将堆划分为多个大小类别 (4 字节, 8 字节, 16 字节, ...) │ │
│ │ 2. 每个类别维护一个空闲块链表 │ │
│ │ 3. 分配时: 在对应类别中查找空闲块 │ │
│ │ 4. 释放时: 将块放回对应类别的空闲链表 │ │
│ │ 5. 优点: 减少碎片,提高分配速度 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 堆分配函数: │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ RtlAllocateHeap(HeapHandle, Flags, Size) │ │
│ │ RtlFreeHeap(HeapHandle, Flags, BaseAddress) │ │
│ │ RtlReAllocateHeap(HeapHandle, Flags, BaseAddress, NewSize) │ │
│ │ RtlSizeHeap(HeapHandle, Flags, BaseAddress) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 堆调试技术: │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ PageHeap: 启用页堆检测 │ │
│ │ !heap: WinDbg 命令查看堆信息 │ │
│ │ Application Verifier: 自动检测堆错误 │ │
│ │ GFlags: 启用堆调试标志 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────────────┘
5.2.12 小结
5.2.12.1 关键知识点
| 主题 | 关键点 |
|---|---|
| PEB | 进程环境块,存储进程级用户态信息 |
| TEB | 线程环境块,存储线程级用户态信息 |
| NT_TIB | TEB 的第一个字段,存储栈信息 |
| KUSER_SHARED_DATA | 用户态只读的内核数据 |
| PEB_LDR_DATA | 模块加载器数据,三个链表 |
| TLS | 线程局部存储,64 个槽位 |
| Stack Cookie | /GS 安全机制 |
5.2.12.2 设计原则
- 用户态可访问:PEB/TEB/KUSER_SHARED_DATA 都在用户态可访问地址
- 线程独立:TEB 每个线程一份,保证线程安全
- 快速访问:通过 FS/GS 段寄存器快速获取 TEB
- 分层设计:PEB(进程级) + TEB(线程级)
5.2.12.3 常见陷阱
- 直接访问固定地址:PEB/TEB 地址在不同系统版本可能变化
- 假设字段位置不变:PEB/TEB 结构可能因 Windows 版本而变化
- 忽略对齐:结构体字段可能有对齐填充
- 多线程竞争:修改 PEB 字段需要同步
5.2.12.4 后续学习路径
- 5.3 节:系统调用 NtCreateProcess()
- 第 6 章:线程调度
- WinDBG 调试技巧
源码位置:sdk/include/ndk/peb_teb.h(file:///d:/reactos/sdk/include/ndk/peb_teb.h)、ntoskrnl/mm/pe.c(file:///d:/reactos/ntoskrnl/mm/pe.c)