概述

在 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 位)
段寄存器FSGS
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.ExceptionListSEH 链入口(x64 下不再使用,保留兼容)
GS:[0x08]NtTib.StackBase线程堆栈基址(高地址端)
GS:[0x10]NtTib.StackLimit线程堆栈上限(低地址端)
GS:[0x18]NtTib.SubSystemTib子系统 TIB
GS:[0x20]NtTib.FiberData / PRCBFiber 数据 / 处理器区域控制块
GS:[0x28]NtTib.ArbitraryUserPointer任意用户数据槽
GS:[0x30]NtTib.SelfTEB 的线性自引用指针
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]ProcessEnvironmentBlockPEB 指针
GS:[0x68]LastErrorValueGetLastError() 的值
GS:[0x6C]CountOfOwnedCriticalSections拥有的临界区计数
GS:[0x70]CsrClientThreadCSR 客户端线程
GS:[0x78]Win32ThreadInfoWin32 线程信息
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++ 内建函数(推荐)

C++cpp
#include <intrin.h>
 
// 获取当前线程的 TEB 地址
PVOID GetTeb()
{
    return (PVOID)__readgsqword(0x30);
}

__readgsqword 是 MSVC 提供的编译器内建函数,读取 GS 段中指定偏移处的 8 字节(64 位)值。对应 32 位的 __readfsdword

3.2 GCC 内联汇编

Cc
void *GetTeb(void)
{
    void *pTeb;
    __asm__("movq %%gs:0x30, %0" : "=r"(pTeb));
    return pTeb;
}

3.3 GCC 命名地址空间(C 扩展)

Cc
void *GetTeb(void)
{
    return *(void * __seg_gs *)0x30;
}

NOTE

需要 GCC 支持 __SEG_GS 宏(GCC 6+)。此方式在 -O1 或更高优化级别下等价于内联汇编版本。

3.4 纯汇编(.asm 文件)

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 语言描述:

C++cpp
// 先获取 TEB,再从 TEB + 0x60 处读取 PEB 指针
#define x64_GetTeb()   ((PVOID)__readgsqword(0x30))
#define x64_GetPeb()   ((PVOID)(*(PVOID*)((BYTE*)x64_GetTeb() + 0x60)))

4.2 更简洁的直接方式

C++cpp
#include <intrin.h>
 
// 直接从 GS:[0x60] 读取 PEB 地址
PVOID GetPeb()
{
    return (PVOID)__readgsqword(0x60);
}

4.3 NtCurrentTeb() 官方方式

Windows SDK(winnt.h)提供了 NtCurrentTeb() 内联函数,内部实现就是读取 GS:[0x30]

C++cpp
#include <winnt.h>
 
PTEB pTeb = NtCurrentTeb();
PPEB pPeb = pTeb->ProcessEnvironmentBlock;

5. PEB 结构与关键成员

得到 PEB 后,可以访问许多进程级信息。以下是 x64 下 PEB 的关键成员及偏移:

PEB 偏移字段说明
+0x000InheritedAddressSpace继承地址空间
+0x001ReadImageFileExecOptions读取映像文件执行选项
+0x002BeingDebugged是否正在被调试
+0x003BitField位域(含 IsProtectedProcess 等)
+0x008Mutant互斥体句柄
+0x010ImageBaseAddress进程映像基址
+0x018LdrPEB_LDR_DATA 指针(已加载模块链表)
+0x020ProcessParameters进程参数(命令行、路径等)
+0x068AtlThunkSListPtrATL Thunk 单链表指针
+0x070Reserved7保留
+0x080SessionId终端服务会话 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,关键成员:

偏移字段说明
+0x000InLoadOrderLinks加载顺序链表节点
+0x010InMemoryOrderLinks内存顺序链表节点
+0x020InInitializationOrderLinks初始化顺序链表节点
+0x030DllBase模块基址
+0x038EntryPoint入口点
+0x040SizeOfImage模块大小
+0x048FullDllName模块完整路径 (UNICODE_STRING)

6.3 获取 Kernel32 基址的代码示例

C++cpp
#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 代码示例

C++cpp
#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

StackLimitDeallocationStack 的区别:StackLimit 包含了保护页(guard pages),而 DeallocationStack 是实际分配的堆栈起始地址。SetThreadStackGuarantee 函数会修改这两个值之间的关系。

8. 获取 PID 和 TID

C++cpp
#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

虽然偏移 0x400x48 存放的是 8 字节指针,但 PID 和 TID 实际只有 4 字节有效值,所以用 __readgsdword(读 4 字节)即可。用 __readgsqword(读 8 字节)也行,取低 32 位即可。

9. 模拟 GetLastError / SetLastError

C++cpp
#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 BaseFS:[0x04]GS:[0x08]
Stack LimitFS:[0x08]GS:[0x10]
TEB 自引用FS:[0x18]GS:[0x30]
PIDFS:[0x20]GS:[0x40]
TIDFS:[0x24]GS:[0x48]
TLS 指针FS:[0x2C]GS:[0x58]
PEB 指针FS:[0x30]GS:[0x60]
LastErrorFS:[0x34]GS:[0x68]

TIP

x64 偏移并非简单地是 x86 的 2 倍,因为结构体中混合了指针(8 字节)和非指针字段(4 字节)。但在 NT_TIB 部分,由于全是指针,偏移确实约为 2 倍。

11. 实战:Shellcode 中通过 GS 获取 PEB 和模块基址

在 Shellcode 或注入场景中,不能直接调用 Windows API(可能不知道地址),需要通过 GS → TEB → PEB → Ldr 链表自行查找:

asm
; 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 的情况下获取进程的全部关键信息。

14. 结构图(PlantUML)

14.1 GS → TEB → PEB → Ldr 整体关系

PlantUML diagram

14.2 x86 vs x64 偏移对照

PlantUML diagram

14.3 通过 GS 获取信息的调用链路

PlantUML diagram