看雪学院 09月15日
深入解析Windows内核GDT,学习ARK工具内存检测原理
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

本文作者X66iaM分享了他在Windows内核编程初探中的学习笔记,重点解析了全局描述符表(GDT)的结构和作用。文章详细介绍了GDT中段描述符的构成,包括基址、界限和访问权限等关键字段,以及段选择子的解析方式。作者还阐述了逻辑地址、线性地址和物理地址之间的转换过程,并探讨了在R3用户态和R0内核态下如何获取和处理GDT信息,为理解ARK等工具的内存检测机制提供了基础。

🌟 **GDT核心解析**:文章详细介绍了全局描述符表(GDT)作为CPU内存分段和权限管理的基石,其每个8字节的段描述符包含基址、界限和访问权限等关键信息。作者通过结构体定义和字段解析,清晰地展示了如何理解这些描述符,为后续的内存检测分析奠定了基础。

💻 **地址转换与段机制**:文中深入浅出地解释了逻辑地址、线性地址和物理地址之间的转换流程。通过一个生动的例子,作者演示了CPU如何通过段选择子查询GDT,再结合页表转换,最终定位到实际的物理内存地址,揭示了x86架构下内存访问的复杂性。

🚀 **R3与R0协同获取GDT**:作者提出了一种高效的GDT信息获取方案,即利用R3用户态的`sgdt`指令获取GDTR,再通过与R0内核驱动的通信,将各个CPU核心的GDT数据复制到用户态进行解析。这种分工合作的方式,既避免了在内核态进行复杂的解析工作,又保证了数据的准确性。

🛠️ **系统段与应用段的区分**:文章补充说明了GDT中不仅包含段描述符,还包含门描述符,并详细列举了系统段(如TSS、门)和应用段(代码段、数据段)的类型及其权限解析方法。这使得对GDT的理解更加全面,能够区分不同类型的段及其安全含义。

X66iaM 2025-09-15 18:00 上海

看雪论坛作者ID:X66iaM

最近打算开个坑写一下ARK工具的笔记。第一次接触windows内核,恳请指正。

01

前置知识

GDT是什么GDT (Global Descriptor Table)是告诉CPU"内存怎么分段、每段有什么权限"的一张表,ARK工具通过读取这张表来检查系统有没有被篡改。

GDT数组在内存中,每个元素是8字节的段描述符。每个段描述符用于表述三个关键字段:

Base Address: 段的基址

Limit: 段的界限(大小)

Access: 访问权限

struct SegmentDescriptor {// ========== 历史字段 (来自80286) ==========unsigned Limit1:16;    // 界限低16位 - 从80286继承unsigned Base1:16;     // 基址低16位 - 从80286继承  unsigned Base2:8;      // 基址中8位 - 从80286继承unsigned type:4;       // 段类型 - 从80286继承,但扩展了unsigned s:1;          // 系统段标志 - 从80286继承unsigned dpl:2;        // 特权级 - 从80286继承unsigned p:1;          // 存在位 - 从80286继承// ========== 80386新增字段 ==========unsigned Limit2:4;     // 界限高4位 - 新增,支持32位界限unsigned avl:1;        // 软件可用位 - 新增unsigned res:1;        // 保留位 - 新增,为未来扩展unsigned db:1;         // 操作数大小 - 新增,支持16/32位切换unsigned g:1;          // 粒度位 - 新增,突破界限限制unsigned Base3:8;      // 基址高8位 - 新增,支持32位基址};

段选择子(CS)的16位结构

15                    3   2   1   0┌──────────────────────┬───┬───────┐      索引 (13位)     │TI │RPL(2位)│    (Index 13 bits)             └──────────────────────┴───┴───────┘- 15-3:索引字段 (13位) = 8192个可能的描述符 索引0特殊: 指向NULL描述符(必须为0)有效索引: 1-8191- 2:TI位 (1位)      = 表指示器选择使用哪个段描述符表TI = 0:使用GDT (Global Descriptor Table)TI = 1:使用LDT (Local Descriptor Table)- 1-0:RPL字段 (2位)   = 请求特权级RPL = 0:Ring 0 (内核级)RPL = 1:Ring 1 (系统服务级,很少用)  RPL = 2:Ring 2 (设备驱动级,很少用)RPL = 3:Ring 3 (用户级)

内存地址类型与转换1.Logical Address (逻辑地址): 程序中使用的地址 CS:IP

2.Linear Address (线性地址): 分段后的地址

3.Physical Address (物理地址): 实际的内存地址

4.Virtual Address(虚拟地址):在x86架构中  在没有分页(实模式)的情况下,线性地址就是物理地址。在有分页(保护模式)的情况下,线性地址就是虚拟地址,虚拟地址通过页表转换为物理地址。

所以这是命名角度的问题

从CPU角度: 线性地址 (Linear Address)

◆强调这是分段后、分页前的地址

◆Intel手册的正式术语

从程序角度: 虚拟地址 (Virtual Address)

◆强调这是程序看到的"虚拟"地址空间

◆操作系统和编程中更常用

举例说明有分页的情况

程序说: "我要访问 CS:0x1000"  (逻辑地址)       |       |CS是段选择子,通过计算转换为索引 TI RPL        ↓CPU查GDT: "GDT[索引] 描述的基址是0x400000" (分段转换)       ↓  得到: 0x400000 + 0x1000 = 0x401000 (线性地址/虚拟地址)       ↓再查页表: "线性地址0x401000对应物理地址0x800000" (分页转换)       ↓最终访问: 物理内存0x800000位置 (物理地址)

内存访问段机制的流程

步骤1: 解析段选择子CSCS = 0x0020 (假设)实际不是直接取第20项,而是:1. 索引 = 0x20 >> 3 = 4 (指向GDT第4项)2. TI = (0x20 >> 2) & 1 = 0 (使用GDT)3. RPL = 0x20 & 3 = 0 (请求特权级0)步骤2: 从GDT获取段描述符1. 根据索引4,从GDT[4]读取8字节段描述符2. 解析出基址、界限、权限等信息步骤3: 地址计算1. 存在位检查: if (P位 == 0) → 段不存在 异常2. 特权级检查: if (max(CPL,RPL) > DPL) → 特权级违例3. 类型检查: 执行/读取/写入权限是否匹配4. 界限检查: if (偏移 > 段界限) → 段界限违例步骤4: 页表转换1. 线性地址 = 段基址 + 偏移地址2. 如果基址=0 (平坦模式),则线性地址=偏移地址

获取GDTGDTR寄存器: CPU内部的寄存器,指向当前使用的GDT表。

SGDT指令: 这是x86的特权指令,把GDTR寄存器的内容读出来。

◆在R3使用sgdt指令获取GDTR寄存器值是可以的。但是不能读写GDT表。(只能知道它在哪

◆涉及的API组合:

R3

的:GetySystemInfo(&systemInfo) 拿到systemInfo.dwNuberOfProcessors即核心数。SetProcessAffinityMask(GetCurrentProcess(),mask) 确定在哪个核心上跑,注意mask数值是按位的 。

R0的:KeQueryActiveProcessors()获取活跃核心的Mask;KeSetSystemAffinityThread(Mask)强制线程在指定核心运行;

注意这个API的说明,我的XP虚拟机是会卡死的,不理解 KeSetSystemAffinityThread 函数 (wdm.h)

https://learn.microsoft.com/zh-cn/windows-hardware/drivers/ddi/wdm/nf-wdm-kesetsystemaffinitythread

每个CPU核心都有自己的**GDTR**寄存器,指向各自的GDT表,进程在哪个核心上跑,指向的就是哪个核心的GDT表,可所有核心的GDT表内容是一样的。

测试:给win7 sp1 32位虚拟机设置为2处理器2内核数量,跑一下遍历的程序,发现GDT的四个基址不同,大小相同。

int main(){  GDTR gdtr;  SYSTEM_INFO SystemInfo;  GetSystemInfo(&SystemInfo);int Mask = 1;printf("GDT dwNumberOfProcessors:%d\n", SystemInfo.dwNumberOfProcessors);for (int i = 0; i < SystemInfo.dwNumberOfProcessors; i++) {    SetProcessAffinityMask(GetCurrentProcess(), Mask);    __asm {      sgdt gdtr      //lgdt GDT    }printf("GDT Base:%08x Limit:%08x\n", gdtr.Base, gdtr.Limit);    Mask <<= 1;  }system("pause");return 0;}

疑问:如果在64位系统上跑,给出的也是32位大小的GDT表地址,不合理,难道给的是假的?

那么我们拿表的时候就要考虑多核问题,精确的拿到每个核心的GDT表。

02

通过R0和R3通信遍历GDT表

模仿一下PCHunter拿的信息

1.Cpu序号- 这个是我们在遍历多核CPU时自己维护的,不是从GDT表本身来的

2.段选择子- 这个是段描述符在GDT表中的索引,我们遍历时的i值

3.基址- 段的起始线性地址。SegmentDescriptor结构体中,由Base1Base2Base3拼接而成

4.界限- 段的大小。SegmentDescriptor结构体中,由Limit1Limit2拼接而成

5.段粒度- 界限的单位(字节或4KB)。SegmentDescriptor结构体中的g字段

6.段特权级- 0=内核级,3=用户级。SegmentDescriptor结构体中的dpl字段

7.类型- 段的具体类型和权限。SegmentDescriptor结构体中的type字段

思路

秉持着在R3能干的活不要在R0干的理念。

R3遍历核心读GDT地址->发送每个核心的GDTR给R0->R0读表数据发给R3->R3解析数据。

R3(用户态)                    R0(内核态驱动)│                           │├─1. sgdt获取GDTR──────────┤├─2. 计算GDT大小(Limit+1)─┤├─3. malloc申请缓冲区─────┤├─4. 发送[CPU索引+GDTR+缓冲区]──►││                           ├─5. KeSetSystemAffinityThread切换到对应核心│                           ├─6. 直接从GDT基址复制数据│◄─7. 返回[完整GDT数据]─────┤├─8. 解析段描述符数据───────┤├─9. 转换为GDT_INFO格式───┤├─10. 显示在UI界面────────┤├─11. free释放缓冲区──────┤

原始的段描述符转换成UI数据的解析方法

上面这张图片出自https://segmentfault.com/a/1190000040187304

Base字段重组:Base3(8位)     Base2(8位)     Base1(16位)字节7         字节4          字节3-2┌──────────┬──────────┬────────────────┐│63......56│39......32│31.............16│  ← 段描述符中的原始位置└──────────┴──────────┴────────────────┘     ↓           ↓            ↓  << 24       << 16         不移位     ↓           ↓            ↓┌──────────┬──────────┬────────────────┐│31......24│23......16│15.............0│  ← 32位完整基址└──────────┴──────────┴────────────────┘Limit字段重组:Limit2(4位)        Limit1(16位)字节6低4位         字节1-0  ┌──────────────┬────────────────────┐│51..........48│15................0│  ← 段描述符中的原始位置└──────────────┴────────────────────┘       ↓                  ↓    << 16              不移位       ↓                  ↓┌──────────────┬────────────────────┐│19..........16│15................0│  ← 20位完整界限└──────────────┴────────────────────┘段粒度转换:G=0 (字节粒度):  界限值直接使用: 假设limit = 0x12345 → 段大小 = 0x12345 字节G=1 (页粒度):    界限值需要转换: 假设limit = 0x123 → 段大小 = (0x123 << 12) | 0xFFF  转换过程:  原始limit: 0x00000123 (20位)            ↓ << 12 (左移12位,乘以4KB)  扩展后:   0x00123000 (32位)            ↓ | 0xFFF (低12位置1)  最终:     0x00123FFF (段可用到页尾)

03

部分代码和效果图

下常常被自己的函数解耦能力感动(破防,真是献丑了

Imgui的UI代码就不贴了,贴点功能代码

//R3  //单个段描述符信息在UI上展示的信息typedef struct _GDT_INFO {    UINT    cpuIndex;       // CPU序号    USHORT  selector;       // 段选择子(在GDT中的索引 * 8)    ULONG64 base;           // 基址(Base1+Base2+Base3拼接)    ULONG   limit;          // 界限(Limit1+Limit2拼接)    UCHAR   g;              // 段粒度(0=字节,1=4KB)    UCHAR   dpl;            // 段特权级(0=内核,3=用户)    UCHAR   type;           // 段类型    UCHAR   system;         // 系统段标志    BOOL    p;              // 段存在位} GDT_INFO, * PGDT_INFO;typedef struct SegmentDescriptor {// ========== 历史字段 (来自80286) ==========unsigned Limit1 : 16;    // 界限低16位 - 从80286继承unsigned Base1 : 16;     // 基址低16位 - 从80286继承  unsigned Base2 : 8;      // 基址中8位 - 从80286继承unsigned type : 4;       // 段类型 - 从80286继承,但扩展了unsigned s : 1;          // 系统段标志 - 从80286继承unsigned dpl : 2;        // 特权级 - 从80286继承unsigned p : 1;          // 存在位 - 从80286继承// ========== 80386新增字段 ==========unsigned Limit2 : 4;     // 界限高4位 - 新增,支持32位界限unsigned avl : 1;        // 软件可用位 - 新增unsigned res : 1;        // 保留位 - 新增,为未来扩展unsigned db : 1;         // 操作数大小 - 新增,支持16/32位切换unsigned g : 1;          // 粒度位 - 新增,突破界限限制unsigned Base3 : 8;      // 基址高8位 - 新增,支持32位基址}*PSEGDESC;#pragma pack(push, 1)typedef struct GDTR {unsignedshort Limit;unsignedint   Base;}*PGDTR;#pragma pack(pop)typedef struct GDT_DATA_REQ {unsigned CpuIndex;    GDTR Gdtr;}*PGDT_DATA_REQ;std::vector<GDT_INFO> _gdtVec;PSEGDESC ArkR3::GetSingeGDT(UINT cpuIndex, PGDTR pGdtr);          //获得单核GDT表数据指针std::vector<GDT_INFO> GetGDTVec();                                //返回所有核心GDT数组_gdtVec

//R3PSEGDESC ArkR3::GetSingeGDT(UINT cpuIndex, PGDTR pGdtr){    DWORD gdtSize = pGdtr->Limit + 1;    PSEGDESC pBuffer = (PSEGDESC)malloc(gdtSize);if (!pBuffer) {        LogErr("GetSingeGDT malloc err");return nullptr;    }    GDT_DATA_REQ req = { 0 };    req.CpuIndex = cpuIndex;    req.Gdtr = *pGdtr;    DWORD dwRetBytes = 0;    DeviceIoControl(m_hDriver, CTL_GET_GDT_DATA, &req, sizeof(GDT_DATA_REQ),        pBuffer, gdtSize, &dwRetBytes, NULL);return pBuffer;}std::vector<GDT_INFO> ArkR3::GetGDTVec(){    _gdtVec.clear();    GDTR gdtr = { 0 };    SYSTEM_INFO SystemInfo;    GetSystemInfo(&SystemInfo);    Log("GetGDTVec GDT dwNumberOfProcessors:%d\n", SystemInfo.dwNumberOfProcessors);for (UINT i = 0; i < SystemInfo.dwNumberOfProcessors; i++) {        DWORD_PTR mask = 1UL << i;  //Mask按位 和 i 一致         SetProcessAffinityMask(GetCurrentProcess(), mask);        __asm {            sgdt gdtr        }        Log("GetGDTVec CPU %d: GDTR Base=%p, Limit=%X\n", i, (void*)gdtr.Base, gdtr.Limit);        PSEGDESC pGdtData = GetSingeGDT(i, &gdtr);if (pGdtData) {            DWORD descCount = (gdtr.Limit + 1) / 8;  // 段描述符数量            Log("GetGDTVec CPU %d: 解析 %d 个段描述符\n", i, descCount);for (UINT index = 0index < descCount; index++) {//pDesc是段描述符指针 下面将原始数据转换成UI上显示的数据格式                 PSEGDESC pDesc = (PSEGDESC)((PUCHAR)pGdtData + index * sizeof(SegmentDescriptor));// 解析成GDT_INFO                GDT_INFO gdtInfo = { 0 };                gdtInfo.cpuIndex = i;                gdtInfo.selector = index * sizeof(SegmentDescriptor);// 基址重组 32bit = Base1(16) + Base2(8) + Base3(8)                gdtInfo.base = pDesc->Base1 |(pDesc->Base2 << 16) |(pDesc->Base3 << 24);// 界限重组:Limit1(16) + Limit2(4)                gdtInfo.limit = pDesc->Limit1 | (pDesc->Limit2 << 16);// 段粒度                 gdtInfo.g = pDesc->g;if (gdtInfo.g) {                    gdtInfo.limit = (gdtInfo.limit << 12) | 0xFFF;  // 低12bit置1 = 4K                }                gdtInfo.dpl = pDesc->dpl;           // 段特权级                gdtInfo.type = pDesc->type;         // 段类型                gdtInfo.system = pDesc->s;          // 系统段标志                gdtInfo.p = pDesc->p;               // 存在位                _gdtVec.emplace_back(gdtInfo);                Log("GetGDTVec  [%02d] Sel:0x%04X Base:0x%08X Limit:0x%08X DPL:%d Type:0x%X %s\n",index, gdtInfo.selector, (DWORD)gdtInfo.base, gdtInfo.limit,                    gdtInfo.dpl, gdtInfo.type, gdtInfo.p ? "P" : "NP");            }            free(pGdtData);        }else {            Log("GetGDTVec CPU %d: pGdtData nullptr\n", i);        }    }    Log("GetGDTVec成功获取 %zu 个段描述符信息\n", _gdtVec.size());return _gdtVec;}

//R0NTSTATUS DispatchDeviceControl  _In_ struct _DEVICE_OBJECT* DeviceObject, _Inout_ struct _IRP* Irp){   UNREFERENCED_PARAMETER(DeviceObject);    KdPrint(("[test] %s\n", __FUNCTION__));PIO_STACK_LOCATIONstack = IoGetCurrentIrpStackLocation(Irp);ULONGcode = stack->Parameters.DeviceIoControl.IoControlCode;NTSTATUSstatus = STATUS_INVALID_DEVICE_REQUEST;ULONG_PTRinfo =0;switch (code) {case CTL_GET_GDT_DATA:        __try{PGDT_DATA_REQgdtReq = (PGDT_DATA_REQ)Irp->AssociatedIrp.SystemBuffer;ULONGcpuIndex = gdtReq->CpuIndex;GDTRgdtr = gdtReq->Gdtr;ULONGgdtSize = gdtr.Limit + 1;            KdPrint(("[test] CpuIndex: %d GdtBase: %08x GdtLimit: %08x, Size: %d\n",                cpuIndex, gdtr.Base, gdtr.Limit, gdtSize));KAFFINITYaffinity = 1UL << cpuIndex; // 将CPU索引转换为位掩码            KeSetSystemAffinityThread(affinity);            RtlCopyMemory(Irp->AssociatedIrp.SystemBuffer, (PVOID)gdtr.Base, gdtSize);            info = gdtSize;            status = STATUS_SUCCESS;            KdPrint(("[test] CTL_GET_GDT_DATA finish"));        }        __except (EXCEPTION_EXECUTE_HANDLER) {            status = STATUS_UNSUCCESSFUL;            KdPrint(("[test] CTL_GET_GDT_DATA exception\n"));        }break;    }}

04

补充1:系统段解析

增加了对系统段的解析,涉及系统调用和各种门。

原来GDT(全局描述符表)中既可以存放段描述符(代码段、数据段),也可以存放门描述符(调用门、陷阱门、中断门)。

在GDT表中,段描述符的s字段决定了段的类型:

s = 1:代码段或数据段

s = 0:系统段

系统段类型(s=0时)系统段的type字段含义:

Type

描述

1

16-bit TSS (Available) - 16位任务状态段(可用)

2

LDT - 局部描述符表

3

16-bit TSS (Busy) - 16位任务状态段(忙碌)

4

16-bit Call Gate - 16位调用门

5

Task Gate - 任务门

6

16-bit Interrupt Gate - 16位中断门

7

16-bit Trap Gate - 16位陷阱门

9

32-bit TSS (Available) - 32位任务状态段(可用)

11

32-bit TSS (Busy) - 32位任务状态段(忙碌)

12

32-bit Call Gate - 32位调用门

14

32-bit Interrupt Gate - 32位中断门

15

32-bit Trap Gate - 32位陷阱门

应用段类型(s=1时)type & 8 = 1:代码段

type & 8 = 0:数据段

代码段权限

type & 2 = 1:可读可执行

type & 2 = 0:仅可执行

数据段权限

type & 2 = 1:可读可写

type & 2 = 0:仅可读

//系统段解析const char* segmentType = " ";if (gdtInfo.system == 0) {          // 系统段    switch (gdtInfo.type) {    case 1: segmentType = "16-bit TSS (Available)"break;    case 2: segmentType = "LDT"break;    case 3: segmentType = "16-bit TSS (Busy)"break;    case 4: segmentType = "16-bit Call Gate"break;    case 5: segmentType = "Task Gate"break;    case 6: segmentType = "16-bit Interrupt Gate"break;    case 7: segmentType = "16-bit Trap Gate"break;    case 9: segmentType = "32-bit TSS (Available)"break;    case 11: segmentType = "32-bit TSS (Busy)"break;    case 12: segmentType = "32-bit Call Gate"break;    case 14: segmentType = "32-bit Interrupt Gate"break;    case 15: segmentType = "32-bit Trap Gate"break;    }}else {if (gdtInfo.type & 8) {  // 代码段        segmentType = (gdtInfo.type & 2) ? "Code (R E)" : "Code (E)";    }else {  // 数据段        segmentType = (gdtInfo.type & 2) ? "Data (R W E)" : "Data (R E)";    }}strcpy_s(gdtInfo.typeDesc, sizeof(gdtInfo.typeDesc), segmentType);

UI显示的过滤条件自定

看雪ID:X66iaM

https://bbs.kanxue.com/user-home-1003898.htm

*本文为看雪论坛优秀文章,由 X66iaM 原创,转载请注明来自看雪社区

1.25折门票开售!看雪·第九届安全开发者峰会(SDC 2025)

# 往期推荐

无"痕"加载驱动模块之傀儡驱动 (上)

为 CobaltStrike 增加 SMTP Beacon

隐蔽通讯常见种类介绍

buuctf-re之CTF分析

物理读写/无附加读写实验

球分享

球点赞

球在看

点击阅读原文查看更多

阅读原文

跳转微信打开

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

Windows内核 GDT ARK工具 内存检测 段描述符 地址转换 内核驱动 逆向工程 系统安全 X66iaM Windows Kernel Global Descriptor Table ARK Tool Memory Detection Segment Descriptor Address Translation Kernel Driver Reverse Engineering System Security
相关文章