X66iaM 2025-09-15 18:00 上海
看雪论坛作者ID:X66iaM

🌟 **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
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:IP2.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 (平坦模式),则线性地址=偏移地址
获取GDT◆GDTR寄存器: 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
**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表。SegmentDescriptor结构体中,由Base1、Base2、Base3拼接而成4.界限- 段的大小。SegmentDescriptor结构体中,由Limit1、Limit2拼接而成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数据的解析方法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 (段可用到页尾)
//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 = 0; index < 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; }}
s字段决定了段的类型:◆s = 1:代码段或数据段◆s = 0:系统段系统段类型(s=0时)系统段的type字段含义: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
*本文为看雪论坛优秀文章,由 X66iaM 原创,转载请注明来自看雪社区
1.25折门票开售!看雪·第九届安全开发者峰会(SDC 2025)
# 往期推荐
无"痕"加载驱动模块之傀儡驱动 (上)
为 CobaltStrike 增加 SMTP Beacon
隐蔽通讯常见种类介绍
buuctf-re之CTF分析
物理读写/无附加读写实验
AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。
鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑