前言
大家好,在下最近打算开个坑写一下ARK工具的笔记。第一次接触windows内核,文中一定有许多错误,恳请指正,谢谢你的时间!
鉴于文末贴了代码,所以设置为点赞或回复可观看,嘿嘿。
GDT (Global Descriptor Table)是告诉CPU"内存怎么分段、每段有什么权限"的一张表,ARK工具通过读取这张表来检查系统有没有被篡改。
GDT数组在内存中,每个元素是8字节的段描述符。每个段描述符用于表述三个关键字段:
所以这是命名角度的问题
从CPU角度: 线性地址 (Linear Address)
从程序角度: 虚拟地址 (Virtual Address)
举例说明有分页的情况
每个CPU核心都有自己的**GDTR**寄存器,指向各自的GDT表,进程在哪个核心上跑,指向的就是哪个核心的GDT表,可所有核心的GDT表内容是一样的。
测试:给win7 sp1 32位虚拟机设置为2处理器2内核数量,跑一下遍历的程序,发现GDT的四个基址不同,大小相同。

·· 疑问:如果在64位系统上跑,给出的也是32位大小的GDT表地址,不合理,难道给的是假的?
那么我们拿表的时候就要考虑多核问题,精确的拿到每个核心的GDT表。
模仿一下PCHunter拿的信息
思路
秉持着在R3能干的活不要在R0干的理念。
R3遍历核心读GDT地址->发送每个核心的GDTR给R0->R0读表数据发给R3->R3解析数据。
原始的段描述符转换成UI数据的解析方法

上面这张图片出自221K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6K6k6h3N6E0k6h3&6@1k6X3q4#2L8s2c8Q4x3X3g2U0L8$3#2Q4x3V1k6S2i4K6u0r3x3e0p5&6x3o6l9H3x3o6l9@1x3o6p5^5y4K6x3H3y4l9`.`.

在下常常被自己的函数解耦能力感动(破防,真是献丑了
Imgui的UI代码就不贴了,贴点功能代码
增加了对系统段的解析,涉及系统调用和各种门。
原来GDT(全局描述符表)中既可以存放段描述符(代码段、数据段),也可以存放门描述符(调用门、陷阱门、中断门)。
在GDT表中,段描述符的s字段决定了段的类型:
系统段的type字段含义:
代码段权限:
数据段权限:
UI显示的过滤条件自定

struct SegmentDescriptor {
unsigned Limit1:16;
unsigned Base1:16;
unsigned Base2:8;
unsigned type:4;
unsigned s:1;
unsigned dpl:2;
unsigned p:1;
unsigned Limit2:4;
unsigned avl:1;
unsigned res:1;
unsigned db:1;
unsigned g:1;
unsigned Base3:8;
};
struct SegmentDescriptor {
unsigned Limit1:16;
unsigned Base1:16;
unsigned Base2:8;
unsigned type:4;
unsigned s:1;
unsigned dpl:2;
unsigned p:1;
unsigned Limit2:4;
unsigned avl:1;
unsigned res:1;
unsigned db:1;
unsigned g:1;
unsigned Base3:8;
};
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 (用户级)
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: 解析段选择子CS
CS = 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 (平坦模式),则线性地址=偏移地址
步骤1: 解析段选择子CS
CS = 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 (平坦模式),则线性地址=偏移地址
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
}
printf("GDT Base:%08x Limit:%08x\n", gdtr.Base, gdtr.Limit);
Mask <<= 1;
}
system("pause");
return 0;
}
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
}
printf("GDT Base:%08x Limit:%08x\n", gdtr.Base, gdtr.Limit);
Mask <<= 1;
}
system("pause");
return 0;
}
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释放缓冲区──────┤
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释放缓冲区──────┤
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 (段可用到页尾)
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 (段可用到页尾)
typedef struct _GDT_INFO {
UINT cpuIndex;
USHORT selector;
ULONG64 base;
ULONG limit;
UCHAR g;
UCHAR dpl;
UCHAR type;
UCHAR system;
BOOL p;
} GDT_INFO, * PGDT_INFO;
typedef struct SegmentDescriptor {
unsigned Limit1 : 16;
unsigned Base1 : 16;
unsigned Base2 : 8;
unsigned type : 4;
unsigned s : 1;
unsigned dpl : 2;
unsigned p : 1;
unsigned Limit2 : 4;
unsigned avl : 1;
unsigned res : 1;
unsigned db : 1;
unsigned g : 1;
unsigned Base3 : 8;
}*PSEGDESC;
#pragma pack(push, 1)
typedef struct GDTR {
unsigned short Limit;
unsigned int 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);
std::vector<GDT_INFO> GetGDTVec();
typedef struct _GDT_INFO {
UINT cpuIndex;
USHORT selector;
ULONG64 base;
ULONG limit;
UCHAR g;
UCHAR dpl;
UCHAR type;
UCHAR system;
BOOL p;
} GDT_INFO, * PGDT_INFO;
typedef struct SegmentDescriptor {
unsigned Limit1 : 16;
unsigned Base1 : 16;
unsigned Base2 : 8;
unsigned type : 4;
unsigned s : 1;
unsigned dpl : 2;
unsigned p : 1;
unsigned Limit2 : 4;
unsigned avl : 1;
unsigned res : 1;
unsigned db : 1;
unsigned g : 1;
unsigned Base3 : 8;
}*PSEGDESC;
#pragma pack(push, 1)
typedef struct GDTR {
unsigned short Limit;
unsigned int 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);
std::vector<GDT_INFO> GetGDTVec();
PSEGDESC 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;
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++) {
PSEGDESC pDesc = (PSEGDESC)((PUCHAR)pGdtData + index * sizeof(SegmentDescriptor));
GDT_INFO gdtInfo = { 0 };
gdtInfo.cpuIndex = i;
gdtInfo.selector = index * sizeof(SegmentDescriptor);
gdtInfo.base = pDesc->Base1 |(pDesc->Base2 << 16) |(pDesc->Base3 << 24);
gdtInfo.limit = pDesc->Limit1 | (pDesc->Limit2 << 16);
gdtInfo.g = pDesc->g;
if (gdtInfo.g) {
gdtInfo.limit = (gdtInfo.limit << 12) | 0xFFF;
}
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;
}
PSEGDESC ArkR3::GetSingeGDT(UINT cpuIndex, PGDTR pGdtr)
传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2025-7-18 00:56
被X66iaM编辑
,原因: 勘误