概述
在 x86(32 位)Windows 中,
FS段寄存器指向当前线程的 TEB(Thread Environment Block)。到了 x64(64 位)Windows,这一职责交给了GS段寄存器。本文整理 x64 下 GS 寄存器的关键偏移、通过 GS 访问 TEB/PEB/堆栈的方法,以及相关代码示例。
相关参考
[toc]
1. x86 → x64:从 FS 到 GS
| 特性 | x86 (32 位) | x64 (64 位) |
|---|---|---|
| 段寄存器 | FS | GS |
| TEB 自引用偏移 | FS:[0x18] | GS:[0x30] |
| PEB 指针偏移 | FS:[0x30] | GS:[0x60] |
| 指针宽度 | 4 字节 | 8 字节 |
| 内联汇编 | VS 支持 | VS 不支持(需用内建函数或单独 asm) |
NOTE
Windows x64 使用基于表展开(table-based unwinding)的异常处理机制,而非 x86 的 SEH 链。因此
GS:[0x00]处的 SEH 链指针在 64 位下不再像 x86 那样使用。
2. GS 寄存器关键偏移速查表
以下是 x64 Windows 中通过 GS 段寄存器访问 TEB 各字段的常用偏移:
| GS 偏移 | 对应 TEB 字段 | 说明 |
|---|---|---|
GS:[0x00] | NtTib.ExceptionList | SEH 链入口(x64 下不再使用,保留兼容) |
GS:[0x08] | NtTib.StackBase | 线程堆栈基址(高地址端) |
GS:[0x10] | NtTib.StackLimit | 线程堆栈上限(低地址端) |
GS:[0x18] | NtTib.SubSystemTib | 子系统 TIB |
GS:[0x20] | NtTib.FiberData / PRCB | Fiber 数据 / 处理器区域控制块 |
GS:[0x28] | NtTib.ArbitraryUserPointer | 任意用户数据槽 |
GS:[0x30] | NtTib.Self | TEB 的线性自引用指针 |
GS:[0x38] | EnvironmentPointer | 环境变量指针 |
GS:[0x40] | ClientId.UniqueProcess | 进程 ID (PID) |
GS:[0x48] | ClientId.UniqueThread | 线程 ID (TID) |
GS:[0x50] | ActiveRpcHandle | 活跃 RPC 句柄 |
GS:[0x58] | ThreadLocalStoragePointer | 线程本地存储 (TLS) 指针 |
GS:[0x60] | ProcessEnvironmentBlock | PEB 指针 |
GS:[0x68] | LastErrorValue | GetLastError() 的值 |
GS:[0x6C] | CountOfOwnedCriticalSections | 拥有的临界区计数 |
GS:[0x70] | CsrClientThread | CSR 客户端线程 |
GS:[0x78] | Win32ThreadInfo | Win32 线程信息 |
GS:[0x188] | _ETHREAD(KTHREAD 部分) | 内核线程结构(通过 GS:[0x188] 可间接访问) |
记忆技巧
核心偏移:
0x30= TEB 自身,0x40= PID,0x48= TID,0x60= PEB,0x68= LastError。 x86 对应:0x18= TEB,0x20= PID,0x24= TID,0x30= PEB,0x34= LastError。 偏移大致是 x86 的 2 倍,因为指针从 4 字节变为 8 字节。
3. 通过 GS 获取 TEB
TEB 的自引用指针位于 GS:[0x30],这是获取 TEB 地址最直接的方式。
3.1 C/C++ 内建函数(推荐)
#include <intrin.h>
// 获取当前线程的 TEB 地址
PVOID GetTeb()
{
return (PVOID)__readgsqword(0x30);
}__readgsqword 是 MSVC 提供的编译器内建函数,读取 GS 段中指定偏移处的 8 字节(64 位)值。对应 32 位的 __readfsdword。
3.2 GCC 内联汇编
void *GetTeb(void)
{
void *pTeb;
__asm__("movq %%gs:0x30, %0" : "=r"(pTeb));
return pTeb;
}3.3 GCC 命名地址空间(C 扩展)
void *GetTeb(void)
{
return *(void * __seg_gs *)0x30;
}NOTE
需要 GCC 支持
__SEG_GS宏(GCC 6+)。此方式在-O1或更高优化级别下等价于内联汇编版本。
3.4 纯汇编(.asm 文件)
GetTeb PROC
mov rax, gs:[30h]
ret
GetTeb ENDP由于 VS2005 起 x64 不支持内联汇编,需要将汇编代码放在独立的 .asm 文件中,并通过项目属性设置为 MASM 汇编。
4. 通过 GS → TEB → PEB
虽然 GS:[0x60] 直接存放了 PEB 的地址,但由于 Vista/7 后的 ASLR(地址空间布局随机化)机制,推荐先获取 TEB 再从中读取 PEB 指针,更可靠。
4.1 原理
GS:[0x30] → TEB 地址
TEB + 0x60 → PEB 地址(ProcessEnvironmentBlock 字段)
用 C 语言描述:
// 先获取 TEB,再从 TEB + 0x60 处读取 PEB 指针
#define x64_GetTeb() ((PVOID)__readgsqword(0x30))
#define x64_GetPeb() ((PVOID)(*(PVOID*)((BYTE*)x64_GetTeb() + 0x60)))4.2 更简洁的直接方式
#include <intrin.h>
// 直接从 GS:[0x60] 读取 PEB 地址
PVOID GetPeb()
{
return (PVOID)__readgsqword(0x60);
}4.3 NtCurrentTeb() 官方方式
Windows SDK(winnt.h)提供了 NtCurrentTeb() 内联函数,内部实现就是读取 GS:[0x30]:
#include <winnt.h>
PTEB pTeb = NtCurrentTeb();
PPEB pPeb = pTeb->ProcessEnvironmentBlock;5. PEB 结构与关键成员
得到 PEB 后,可以访问许多进程级信息。以下是 x64 下 PEB 的关键成员及偏移:
| PEB 偏移 | 字段 | 说明 |
|---|---|---|
+0x000 | InheritedAddressSpace | 继承地址空间 |
+0x001 | ReadImageFileExecOptions | 读取映像文件执行选项 |
+0x002 | BeingDebugged | 是否正在被调试 |
+0x003 | BitField | 位域(含 IsProtectedProcess 等) |
+0x008 | Mutant | 互斥体句柄 |
+0x010 | ImageBaseAddress | 进程映像基址 |
+0x018 | Ldr | PEB_LDR_DATA 指针(已加载模块链表) |
+0x020 | ProcessParameters | 进程参数(命令行、路径等) |
+0x068 | AtlThunkSListPtr | ATL Thunk 单链表指针 |
+0x070 | Reserved7 | 保留 |
+0x080 | SessionId | 终端服务会话 ID |
5.1 WinDbg 查看 PEB
0:009> dt _PEB
ntdll!_PEB
+0x000 InheritedAddressSpace : 0 ''
+0x001 ReadImageFileExecOptions : 0 ''
+0x002 BeingDebugged : 0x1 ''
+0x003 BitField : 0x8 ''
+0x008 Mutant : 0xffffffff`ffffffff Void
+0x010 ImageBaseAddress : 0x00000000`ff310000 Void
+0x018 Ldr : 0x00000000`77222640 _PEB_LDR_DATA
+0x020 ProcessParameters : ...
6. 通过 PEB → LDR_DATA 获取模块基址
PEB 的 Ldr 字段(偏移 +0x018)指向 _PEB_LDR_DATA 结构,其中包含三个双向链表,枚举了进程已加载的所有模块:
PEB + 0x018 → PEB_LDR_DATA
+0x010 InLoadOrderModuleList (按加载顺序)
+0x020 InMemoryOrderModuleList (按内存顺序)
+0x030 InInitializationOrderModuleList(按初始化顺序)
6.1 PEB_LDR_DATA 结构
0:009> dt _PEB_LDR_DATA
ntdll!_PEB_LDR_DATA
+0x000 Length : 0x58
+0x004 Initialized : 0x1 ''
+0x008 SsHandle : (null)
+0x010 InLoadOrderModuleList : _LIST_ENTRY
+0x020 InMemoryOrderModuleList : _LIST_ENTRY
+0x030 InInitializationOrderModuleList : _LIST_ENTRY
6.2 LDR_DATA_TABLE_ENTRY 结构
每个链表节点是一个 _LDR_DATA_TABLE_ENTRY,关键成员:
| 偏移 | 字段 | 说明 |
|---|---|---|
+0x000 | InLoadOrderLinks | 加载顺序链表节点 |
+0x010 | InMemoryOrderLinks | 内存顺序链表节点 |
+0x020 | InInitializationOrderLinks | 初始化顺序链表节点 |
+0x030 | DllBase | 模块基址 |
+0x038 | EntryPoint | 入口点 |
+0x040 | SizeOfImage | 模块大小 |
+0x048 | FullDllName | 模块完整路径 (UNICODE_STRING) |
6.3 获取 Kernel32 基址的代码示例
#include <windows.h>
#include <intrin.h>
// 通过 PEB → Ldr → InLoadOrderModuleList 遍历模块
PVOID GetKernel32Base()
{
// 1. 获取 PEB
PPEB pPeb = (PPEB)__readgsqword(0x60);
// 2. 获取 Ldr
PPEB_LDR_DATA pLdr = pPeb->Ldr;
// 3. 遍历 InLoadOrderModuleList
PLIST_ENTRY pHead = &pLdr->InLoadOrderModuleList;
PLIST_ENTRY pNode = pHead->Flink;
while (pNode != pHead)
{
PLDR_DATA_TABLE_ENTRY pEntry =
CONTAINING_RECORD(pNode, LDR_DATA_TABLE_ENTRY, InLoadOrderLinks);
// FullDllName.Buffer 是模块完整路径
if (pEntry->FullDllName.Buffer)
{
// 简单匹配 "kernel32.dll"(不区分大小写)
wchar_t* name = pEntry->FullDllName.Buffer;
// ... 比较逻辑 ...
}
pNode = pNode->Flink;
}
return NULL; // 未找到
}TIP
遍历
InInitializationOrderModuleList同样可以获得模块列表,通常第二个节点就是 Kernel32(第一个是 ntdll.dll)。这是 Shellcode 中经典的获取 API 地址的方法。
7. 通过 GS 获取堆栈信息
TEB 的 NtTib 子结构中包含了线程堆栈的边界信息:
| GS 偏移 | 字段 | 说明 |
|---|---|---|
GS:[0x08] | StackBase | 堆栈基址(高地址,栈底) |
GS:[0x10] | StackLimit | 堆栈上限(低地址,栈顶警戒线) |
GS:[0x1478] | DeallocationStack | 实际堆栈起始地址(真正的栈限制) |
7.1 代码示例
#include <intrin.h>
#include <stdio.h>
void PrintStackInfo()
{
ULONG64 stackBase = __readgsqword(0x08);
ULONG64 stackLimit = __readgsqword(0x10);
ULONG64 deallocStack = __readgsqword(0x1478);
printf("Stack Base : 0x%llX\n", stackBase);
printf("Stack Limit : 0x%llX\n", stackLimit);
printf("Deallocation Stack: 0x%llX\n", deallocStack);
printf("Stack Size : %llu bytes\n", stackBase - stackLimit);
}NOTE
StackLimit和DeallocationStack的区别:StackLimit包含了保护页(guard pages),而DeallocationStack是实际分配的堆栈起始地址。SetThreadStackGuarantee函数会修改这两个值之间的关系。
8. 获取 PID 和 TID
#include <intrin.h>
#include <stdio.h>
void PrintPidTid()
{
DWORD pid = (DWORD)__readgsdword(0x40); // ClientId.UniqueProcess
DWORD tid = (DWORD)__readgsdword(0x48); // ClientId.UniqueThread
printf("PID: %lu\n", pid);
printf("TID: %lu\n", tid);
}NOTE
虽然偏移
0x40和0x48存放的是 8 字节指针,但 PID 和 TID 实际只有 4 字节有效值,所以用__readgsdword(读 4 字节)即可。用__readgsqword(读 8 字节)也行,取低 32 位即可。
9. 模拟 GetLastError / SetLastError
#include <intrin.h>
DWORD MyGetLastError()
{
return __readgsdword(0x68);
}
void MySetLastError(DWORD error)
{
__writegsdword(0x68, error);
}这等价于 Windows API 的 GetLastError() 和 SetLastError(),但省去了函数调用开销。
10. x86 vs x64 偏移对照表
| 内容 | x86 (FS) | x64 (GS) |
|---|---|---|
| SEH 链 | FS:[0x00] | GS:[0x00](x64 不使用 SEH 链) |
| Stack Base | FS:[0x04] | GS:[0x08] |
| Stack Limit | FS:[0x08] | GS:[0x10] |
| TEB 自引用 | FS:[0x18] | GS:[0x30] |
| PID | FS:[0x20] | GS:[0x40] |
| TID | FS:[0x24] | GS:[0x48] |
| TLS 指针 | FS:[0x2C] | GS:[0x58] |
| PEB 指针 | FS:[0x30] | GS:[0x60] |
| LastError | FS:[0x34] | GS:[0x68] |
TIP
x64 偏移并非简单地是 x86 的 2 倍,因为结构体中混合了指针(8 字节)和非指针字段(4 字节)。但在
NT_TIB部分,由于全是指针,偏移确实约为 2 倍。
11. 实战:Shellcode 中通过 GS 获取 PEB 和模块基址
在 Shellcode 或注入场景中,不能直接调用 Windows API(可能不知道地址),需要通过 GS → TEB → PEB → Ldr 链表自行查找:
; x64 MASM 风格伪代码
; 1. 获取 PEB
mov rax, gs:[60h] ; rax = PEB 地址
; 2. 获取 Ldr
mov rax, [rax + 18h] ; rax = PEB_LDR_DATA 地址
; 3. 获取 InMemoryOrderModuleList
mov rax, [rax + 20h] ; rax = 第一个 LDR_DATA_TABLE_ENTRY.InMemoryOrderLinks
; 4. 第一个模块通常是 exe 自身,第二个是 ntdll.dll
mov rax, [rax] ; 第二个节点(ntdll.dll)
; 5. 第三个通常是 Kernel32.dll
mov rax, [rax] ; 第三个节点
; 6. 获取 DllBase(InMemoryOrderLinks 偏移为 0x10,DllBase 在 +0x30,所以 +0x20)
mov rbx, [rax + 20h] ; rbx = Kernel32 基址WARNING
上述偏移和链表顺序在不同 Windows 版本中可能略有差异。
InInitializationOrderModuleList通常更稳定,ntdll 是第一个,Kernel32 是第二个。实际使用前请用 WinDbg 验证目标系统。
12. 内核模式下的 GS 用法
在内核模式中,GS 寄存器的用法与用户模式不同:
- 用户模式:
GS指向当前线程的 TEB - 内核模式:
GS指向当前 CPU 的KPCR(Kernel Processor Control Region)
内核模式下通过 GS:[0x188] 可以访问当前 CPU 的 _ETHREAD(当前运行的线程对象)。
; 内核模式下获取当前线程
mov rax, gs:[188h] ; rax = 当前 _ETHREAD
13. 总结
GS 段寄存器
├── GS:[0x08] → StackBase (堆栈基址)
├── GS:[0x10] → StackLimit (堆栈上限)
├── GS:[0x30] → TEB 自引用指针
│ └── TEB + 0x60 → PEB
│ ├── PEB + 0x002 → BeingDebugged
│ ├── PEB + 0x010 → ImageBaseAddress
│ ├── PEB + 0x018 → Ldr (PEB_LDR_DATA)
│ │ ├── InLoadOrderModuleList
│ │ ├── InMemoryOrderModuleList
│ │ └── InInitializationOrderModuleList
│ │ └── 遍历获得各模块 DllBase
│ └── PEB + 0x020 → ProcessParameters (命令行等)
├── GS:[0x40] → PID
├── GS:[0x48] → TID
├── GS:[0x60] → PEB (直接获取,但建议通过 TEB 间接获取)
└── GS:[0x68] → LastErrorValue
掌握 GS 寄存器的使用是 x64 Windows 逆向工程、Shellcode 编写和底层编程的基础技能。通过 GS → TEB → PEB → Ldr 这条链路,可以在不调用任何 API 的情况下获取进程的全部关键信息。