前言
开了这个坑,但前文都是x86的,感觉差点意思,发笔记凑活一下- -
写玩具的时候收集的一些api、结构体、遍历信息的方法,全靠看雪前辈们的文章啊-。-,帮助新同学省去检索信息的时间。能力有限,欢迎大家补充指正。
下文内容在win7 7601和win10 19h1经验证可行。

解析pdb
R3:70dK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6w2N6$3q4F1M7%4V1&6z5q4)9J5c8V1g2S2M7%4W2b7k6r3t1`.
R0:0edK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6a6P5s2W2Y4k6h3^5I4j5e0q4Q4x3V1k6G2P5r3N6W2L8W2m8V1j5R3`.`. 他用的ksocket库只能发http请求,所以我只用他解析pdb,让R3调UrlDownloadToFile下载。
也可以让ida下pdb 直接使用dia2dump解析即可
大家有什么在R0下载符号的方法吗
GDT
用不了__asm了,用函数拿gdtr,联合编译asm也行
#include <immintrin.h>
extern "C" void _sgdt(void*);
GDTR gdtr = { 0 };
_sgdt(&gdtr);
我在R3拿居然是错的
在x64下,TSS(任务状态段)和LDT(局部描述符表)等系统段描述符扩展为16字节,占用连续两个GDT项。
前8字节与32位一样,后8字节用于补充TSS/LDT的高32位base地址和保留字段。
所以解析数据的时候遇到s=0且type为TSS/LDT类型(type=2,9,11),和下一项8字节的数据合并一起解析。
平坦模式导致数据段和代码段的base/limit字段失效,只需关注系统段的base/limit。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | 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 l : 1;
unsigned db : 1;
unsigned g : 1;
unsigned Base3 : 8;
} SegmentDescriptor, *PSEGDESC;
typedef struct SystemDescriptor64 {
SegmentDescriptor low;
unsigned Base4 : 32;
unsigned reserved : 32;
} SystemDescriptor64;
|
但是我观察许多ARK并没有合并显示 仍然保留了合并的表项
保留的
我没保留

虽然数值解析的一致 但是openark仍然显示了0x48
IDT
与GDT一样 一个核心一张表
使用KeSetSystemAffinityThreadEx切核心+__sidt(&idtr)读
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | // 中断描述符结构 (x64)
typedef struct InterruptDescriptor {
USHORT OffsetLow; // 处理程序地址低16位
USHORT Selector; // 段选择子
USHORT IstIndex : 3; // IST索引
USHORT Reserved0 : 5; // 保留
USHORT Type : 4; // 门类型
USHORT Reserved1 : 1; // 保留
USHORT Dpl : 2; // 描述符特权级
USHORT Present : 1; // 存在位
USHORT OffsetMiddle; // 处理程序地址中16位
ULONG OffsetHigh; // 处理程序地址高32位
ULONG Reserved2; // 保留
} *PINTDESC;
|

SSDT
在 64位Windows 中,SSDT表的结构与32位不同:
- 32位:SSDT表直接存储函数地址
- 64位:SSDT表存储的是相对偏移量,需要计算得到真实地址
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | ULONG_PTR SSDT_GetPfnAddr(ULONG dwIndex, PULONG lpBase)
{
ULONG_PTR lpAddr = NULL;
ULONG dwOffset = lpBase[dwIndex];
//按16位对齐省空间,所以>>4;负偏移有+-问题,所以|0xF00..
if (dwOffset & 0x80000000)
dwOffset = (dwOffset >> 4) | 0xF0000000;
else
dwOffset >>= 4;
lpAddr = (ULONG_PTR)((PUCHAR)lpBase + (LONG)dwOffset);
return lpAddr;
}
|
- 右移4位:Windows x64中,SSDT偏移量以16字节对齐,所以低4位总是0,可以节省空间
- 符号扩展:处理负偏移(向前的地址),通过| 0xF0000000进行符号扩展
- 相对地址计算:最终地址 = SSDT基址 + 计算出的偏移量
抄这篇https://bbs.kanxue.com/thread-248117.htm
ShadowSSDT
拿到KeServiceDescriptorTableShadow后找第二张表是Shadow,解析方式与SSDT一致。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | NTSTATUS EnumShadowSSDT(PSSDT_INFO SsdtBuffer, PULONG SsdtCount)
{
INIT_PDB;
PSYSTEM_SERVICE_DESCRIPTOR_TABLE ShadowTableArray =
(PSYSTEM_SERVICE_DESCRIPTOR_TABLE)ntos.GetPointer("KeServiceDescriptorTableShadow");
Log("[XM] KeServiceDescriptorTableShadow Array: %p", ShadowTableArray);
if (!ShadowTableArray) {
Log("[XM] KeServiceDescriptorTableShadow == null");
return STATUS_UNSUCCESSFUL;
}
// 访问数组的第二个元素 [1] - 这才是真正的 ShadowSSDT
PSYSTEM_SERVICE_DESCRIPTOR_TABLE ShadowTable = &ShadowTableArray[1];
Log("[XM] ShadowSSDT [0]: Base=%p, Count=%d",
ShadowTableArray[0].Base, ShadowTableArray[0].NumberOfServices);
Log("[XM] ShadowSSDT [1]: Base=%p, Count=%d",
ShadowTable->Base, ShadowTable->NumberOfServices);
if (!ShadowTable->Base || ShadowTable->NumberOfServices == 0) {
Log("[XM] ShadowSSDT not available");
Log("[XM] ShadowSSDT [1] not available");
}
ULONG nums = ShadowTable->NumberOfServices;
PULONG shadowSsdt = ShadowTable->Base;
*SsdtCount = nums;
Log("[XM] ShadowSSDT found: %d services", nums);
for (ULONG i = 0; i < nums; i++) {
SsdtBuffer[i].Index = i + 0x1000; // ShadowSSDT的调用号从0x1000开始
ULONG_PTR pfnAddr = SSDT_GetPfnAddr(i, shadowSsdt);
SsdtBuffer[i].FunctionAddress = (PVOID)pfnAddr;
Log("[XM] ShadowSSDT[%d]: Raw=0x%X, Decoded=0x%p", i, shadowSsdt[i], pfnAddr);
}
return STATUS_SUCCESS;
}
|
遍历进程
1.前文讲的遍历进程链表
PsInitialSystemProcess → EPROCESS.ActiveProcessLinks → 下一个EPROCESS → ...
2.用PsLookupProcessByProcessId
函数内部会查句柄表,相比链表更安全

由于pid是四的倍数
for(pid=0;pid<65535;pid+=4) → 句柄表查找 → 返回EPROCESS指针
3.内存特征EPROCESS
有什么表敌我都知道 最后还得暴力搜内存
_DISPATCHER_HEADER.type= 0x3
验证EPROCESS进程链表的后继的前驱是不是自己
页目录表是否有效 低12位是否为0(页对齐)等等方法
4.听闻还有一种从线程调度反推进程的方法
遍历模块
利用PsLoadModuleList 杖举驱动模块 - KTr - 博客园
或者 ZwQueryInformation
枚举常规回调
https://bbs.kanxue.com/thread-148895.htm
**进程 线程 模块 **都用数组存放回调函数指针 ,数组里最多塞下64个回调指针:
PspCreateProcessNotifyRoutine
PspCreateThreadNotifyRoutine
PspLoadImageNotifyRoutine
1 2 3 4 5 6 | typedef struct _EX_CALLBACK_ROUTINE_BLOCK {
EX_RUNDOWN_REF RundownProtect;
PEX_CALLBACK_FUNCTION Function;
PVOID Context;
} EX_CALLBACK_ROUTINE_BLOCK, * PEX_CALLBACK_ROUTINE_BLOCK;
|
通过索引/函数地址删除回调
PsSetCreateProcessNotifyRoutine(functionAddr, TRUE);
蓝屏 注册表 关机 都用链表存放回调函数指针 :
注册表
85bK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6U0L8r3!0#2k6q4)9J5k6i4c8W2L8X3y4W2L8Y4c8Q4x3X3g2U0L8$3#2Q4x3V1k6V1k6i4k6W2L8r3!0H3k6i4u0Q4x3V1k6S2M7Y4c8A6j5$3I4W2i4K6u0r3x3U0x3$3y4U0R3K6y4H3`.`.
1 2 3 4 5 6 7 8 9 | typedef struct _CM_NOTIFY_ENTRY {
LIST_ENTRY ListEntryHead;
ULONG UnKnown1;
ULONG UnKnown2;
LARGE_INTEGER Cookie;
PVOID Context;
PVOID Function;
} CM_NOTIFY_ENTRY, *PCM_NOTIFY_ENTRY;
|
定位链表遍历就行

注册表回调得用cookie删除
1 2 3 4 | LARGE_INTEGER cookie;
cookie.QuadPart = (LONGLONG)deleteKey;
NTSTATUS status = CmUnRegisterCallback(cookie);
|
**蓝屏 **
结构体Wdm.h都有
标准蓝屏回调 (KeBugCheckCallback)
- 符号: KeBugCheckCallbackListHead
- 结构: KBUGCHECK_CALLBACK_RECORD
- 用途: 在蓝屏时保存数据、记录状态
KeBugCheckCallbackListHead
1 2 3 4 5 6 7 8 9 | typedef struct _KBUGCHECK_CALLBACK_RECORD {
LIST_ENTRY Entry;
PKBUGCHECK_CALLBACK_ROUTINE CallbackRoutine;
_Field_size_bytes_opt_(Length) PVOID Buffer;
ULONG Length;
PUCHAR Component;
ULONG_PTR Checksum;
UCHAR State;
} KBUGCHECK_CALLBACK_RECORD, *PKBUGCHECK_CALLBACK_RECORD;
|
蓝屏回调通过 结构指针 删除
KeDeregisterBugCheckCallback(recordPtr);
蓝屏原因回调 (KeRegisterBugCheckReasonCallback)
- 符号: KeBugCheckReasonCallbackListHead
- 结构: KBUGCHECK_REASON_CALLBACK_RECORD
- 用途: 根据蓝屏原因执行特定处理
1 2 3 4 5 6 7 8 | typedef struct _KBUGCHECK_REASON_CALLBACK_RECORD {
LIST_ENTRY Entry;
PKBUGCHECK_REASON_CALLBACK_ROUTINE CallbackRoutine;
PUCHAR Component;
ULONG_PTR Checksum;
KBUGCHECK_CALLBACK_REASON Reason;
UCHAR State;
} KBUGCHECK_REASON_CALLBACK_RECORD, * PKBUGCHECK_REASON_CALLBACK_RECORD;
|
**关机 **
IopNotifyShutdownQueueHead
1 2 3 4 5 6 | typedef struct _SHUTDOWN_PACKET {
LIST_ENTRY ListEntry;
PDEVICE_OBJECT DeviceObject;
PIRP Irp;
} SHUTDOWN_PACKET, *PSHUTDOWN_PACKET;
|
注册 → IoRegisterShutdownNotification → 创建SHUTDOWN_PACKET → 插入IopNotifyShutdownQueueHead
关机触发 → 遍历链表 → 向每个设备发送IRP_MJ_SHUTDOWN
删除 → IoUnregisterShutdownNotification → 从链表移除SHUTDOWN_PACKET
枚举对象回调
ObTypeIndexTable→ _OBJECT_TYPE → CallbackList → CALLBACK_BODY → Pre/PostCallbackRoutine
1: kd> x nt!ObTypeIndexTable
fffff806`25e1bd70 nt!ObTypeIndexTable = <no type information>
*************************************************************************
1: kd> dt nt!_OBJECT_TYPE
+0x000 TypeList : _LIST_ENTRY
+0x010 Name : _UNICODE_STRING
+0x020 DefaultObject : Ptr64 Void
+0x028 Index : UChar
+0x02c TotalNumberOfObjects : Uint4B
+0x030 TotalNumberOfHandles : Uint4B
+0x034 HighWaterNumberOfObjects : Uint4B
+0x038 HighWaterNumberOfHandles : Uint4B
+0x040 TypeInfo : _OBJECT_TYPE_INITIALIZER
+0x0b8 TypeLock : _EX_PUSH_LOCK
+0x0c0 Key : Uint4B
+0x0c8 CallbackList : _LIST_ENTRY
测试1 获取ObTypeIndexTable中每个ObType并打印出Name
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | void ForTest() {
INIT_PDB;
ULONG_PTR ObTypeIndexTable = ntos.GetPointer("ObTypeIndexTable");
Log("[XM] ObTypeIndexTable地址: %p", ObTypeIndexTable);
//遍历 先打印出所有的对象名称看看
int maxType = 100;
for (int i = 0; i < maxType; i++) {
// 取出POBJECT_TYPE指针
ULONG_PTR objTypeAddr = *(ULONG_PTR*)(ObTypeIndexTable + i * sizeof(ULONG_PTR));
if (!objTypeAddr) continue;
// OBJECT_TYPE结构体Name字段偏移
size_t nameoffset = ntos.GetOffset("_OBJECT_TYPE", "Name");
Log("[XM] nameoffset: %p", nameoffset);
ULONG_PTR nameAddr = objTypeAddr + nameoffset;
// 读取UNICODE_STRING
UNICODE_STRING* pName = (UNICODE_STRING*)nameAddr;
if (!MmIsAddressValid(pName) || !MmIsAddressValid(pName->Buffer)) continue;
// 打印对象类型名
Log("[XM] 对象类型[%d] 地址: %p 名称: %ws", i, objTypeAddr, pName->Buffer);
}
}
|

CallbackList的结构体未公开 网上各种叫法 给我整蒙了
这篇有讲:https://bbs.kanxue.com/thread-277238.htm
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | typedef struct _CALLBACK_NODE {
USHORT Version; // 版本号,目前是0x100, 可通过ObGetFilterVersion获取该值
USHORT CallbackBodyCount; // 本节点上CallbackBody的数量
PVOID Context; // 注册回调时设定的0B_CALLBACK_REGISTRATION.RegistrationContext
UNICODE_STRING Altitude; // 指向Altitude字符串
char CallbackBody[1]; // 原本是CALLBACK_BODY CallbackBody[1] -> CALLBACK_BODY数组, 其元素个数为CallbackCount
// 我用不到 改成了char
} CALLBACK_NODE, * PCALLBACK_NODE;
typedef struct _CALLBACK_BODY {
LIST_ENTRY ListEntry;
/* 系统中同类型对象的的CALLBACK_NODE通过这个链表串在一起, 对应于_OBJECT_TYPE->TypeList */
OB_OPERATION Operations;
/* 注册回调时设定的OB_OPERATION_REGISTRATION.Operations成员(OB_OPERATION_HANDLE_CREATE... ) */
ULONG Active;
PCALLBACK_NODE CallbackNode; // 指向该CallbackBody
POBJECT_TYPE ObjectType;
POB_PRE_OPERATION_CALLBACK PreCallbackRoutine;
POB_POST_OPERATION_CALLBACK PostCallbackRoutine;
EX_RUNDOWN_REF RundownProtection; // Run-down Protection
} CALLBACK_BODY, * PCALLBACK_BODY;
|
遍历ObTypeIndexTable得到_OBJECT_TYPE
拿到_OBJECT_TYPE->CallbackList之后遍历链表 每个节点按CALLBACK_BODY解析。
PreCallbackRoutine和PostCallbackRoutine是对象回调函数数组。
卸载对象回调传CALLBACK_BODY->CallbackNode给ObUnRegisterCallbacks即可。
IO派遣函数/设备栈
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 3: kd> dt nt!_DRIVER_OBJECT -o
Type : Int2B
Size : Int2B
DeviceObject : Ptr64 _DEVICE_OBJECT
Flags : Uint4B
DriverStart : Ptr64 Void
DriverSize : Uint4B
DriverSection : Ptr64 Void
DriverExtension : Ptr64 _DRIVER_EXTENSION
DriverName : _UNICODE_STRING
HardwareDatabase : Ptr64 _UNICODE_STRING
FastIoDispatch : Ptr64 _FAST_IO_DISPATCH
DriverInit : Ptr64 long
DriverStartIo : Ptr64 void
DriverUnload : Ptr64 void
MajorFunction : [28] Ptr64 long
|
遍历驱动对象方法 有代码: https://bbs.kanxue.com/thread-276245.htm
得知道这一段连续的数据结构
_OBJECT_HEADER_NAME_INFO (size: 0x20)
_OBJECT_HEADER (offset: 0x30)
Driver Object

实测能跑
拿到驱动对象就可以拿设备对象 然后遍历attachdevice 和MajorFunction
查被过滤的设备 检测派遣函数指针是否在xx模块内
网络端口
R3有API 可以拿到端口pid
有示例代码
getTcpTable 函数 (iphlpapi.h) - Win32 apps
getExtendedTcpTable 函数 (iphlpapi.h) - Win32 apps
openark的kernel api-network.cpp中也有R3实现
解除文件占用
强制关闭文件句柄
在目标进程上下文中调用 ZwClose() 关闭文件句柄
R3获取文件路径 转换成设备路径 传给R0
↓
ObQueryNameString(文件对象) 获取完整文件路径
↓
RtlCompareUnicodeString(对象路径, 目标路径) 字符串匹配
↓
PsLookupProcessByProcessId 获取EPROCESS对象 注意这个函数使用完要减少引用计数
↓
KeStackAttachProcess 切换到目标进程上下文
↓
ZwClose(句柄值) 在目标进程中关闭文件句柄
↓
KeUnstackDetachProcess 恢复原始进程上下文
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 | NTSTATUS UnlockFile(WCHAR* filePath) {
//遍历所有进程句柄表
NTSTATUS Status;
PSYSTEM_HANDLE_INFORMATION_EX HandlesEx;
PSYSTEM_HANDLE_TABLE_ENTRY_INFO_EX HandleInfoEx;
POBJECT_NAME_INFORMATION ObjectNameInfo;
PVOID Buffer;
ULONG BufferSize = 4096;
ULONG ReturnLength;
ULONG_PTR i;
UNICODE_STRING ustrName;
RtlInitUnicodeString(&ustrName, filePath);
Log("[XM] UnlockFile ustr: %wZ", &ustrName);
ObjectNameInfo = (POBJECT_NAME_INFORMATION)ExAllocatePoolWithTag(NonPagedPool, 4096, 'ULFL');
if (!ObjectNameInfo) {
return STATUS_NO_MEMORY;
}
retry:
Buffer = ExAllocatePoolWithTag(NonPagedPool, BufferSize, 'ULFL');
if (!Buffer) {
ExFreePool(ObjectNameInfo);
return STATUS_NO_MEMORY;
}
Status = ZwQuerySystemInformation(SystemExtendedHandleInformation,
Buffer,
BufferSize,
&ReturnLength
);
if (Status == STATUS_INFO_LENGTH_MISMATCH) {
ExFreePool(Buffer);
BufferSize = ReturnLength;
goto retry;
}
if (NT_SUCCESS(Status)) {
HandlesEx = (PSYSTEM_HANDLE_INFORMATION_EX)Buffer;
Log("[XM] 开始遍历句柄,总数: %llu", HandlesEx->NumberOfHandles);
for (i = 0; i < HandlesEx->NumberOfHandles; i++) {
HandleInfoEx = &(HandlesEx->Handles[i]);
Status = ObReferenceObjectByPointer(HandleInfoEx->Object, 0,
*IoFileObjectType, KernelMode);//*IoFileObjectType
if (NT_SUCCESS(Status)) {
Status = ObQueryNameString(HandleInfoEx->Object, ObjectNameInfo, 4096, &ReturnLength);
if (NT_SUCCESS(Status)) {
if (RtlCompareUnicodeString(&ObjectNameInfo->Name, &ustrName, TRUE) == 0) {
Log("[XM] 找到匹配句柄: PID=%llu, Handle=%llu, Path=%wZ",
HandleInfoEx->UniqueProcessId,
HandleInfoEx->HandleValue,
&ObjectNameInfo->Name);
//切换进程
PEPROCESS Process = NULL;
Status = PsLookupProcessByProcessId((HANDLE)HandleInfoEx->UniqueProcessId, &Process);
if (NT_SUCCESS(Status)) {
KAPC_STATE ApcState;
KeStackAttachProcess(Process, &ApcState);
Status = ZwClose((HANDLE)HandleInfoEx->HandleValue);
Log("[XM] unlock UniqueProcessId:%llu HandleValue:%llu Name:%wZ",
HandleInfoEx->UniqueProcessId,
HandleInfoEx->HandleValue,
&ObjectNameInfo->Name);
KeUnstackDetachProcess(&ApcState);
ObDereferenceObject(Process);
}
}
}
ObDereferenceObject(HandleInfoEx->Object);
}
}
}
ExFreePool(ObjectNameInfo);
ExFreePool(Buffer);
return Status;
}
|
文件粉碎
先解锁后删除
ZwDeleteFile(&ustrName);
路径转换
- C:\Users\XiaM\Desktop\1.txt
- 名称: DOS路径 (DOS Path) / Win32路径
- 用途: 用户和应用程序使用的标准路径
- ??\C:\Users\XiaM\Desktop\1.txt
- 名称: NT路径 (NT Path) / 符号链接路径
- 用途: NT内核的路径表示,?? 是符号链接目录
- \Device\HarddiskVolume1\Users\XiaM\Desktop\1.txt
像UnlockFile功能 需要在R3获取文件路径 转换为设备路径
1 2 3 4 5 6 7 8 9 10 11 12 | DOS路径到设备路径转换
std::wstring ConvertToDevicePath(const std::wstring& dosPath) {
WCHAR driveLetter[3] = {dosPath[0], L':', L'\0'};
WCHAR deviceName[MAX_PATH]; // 查询DOS设备对应的真实设备
if (QueryDosDeviceW(driveLetter, deviceName, MAX_PATH)) {
// 拼接:设备名 + 路径部分
std::wstring result = deviceName; // \Device\HarddiskVolume3
result += dosPath.substr(2); // + \Users\XiaM\Desktop\1.txt
return result;
}
return L"";
}
|
强制关闭进程
PspTerminateProcess
PspTerminateThreadByPointer
https://bbs.kanxue.com/thread-270012.htm
[培训]Windows内核深度攻防:从Hook技术到Rootkit实战!
最后于 2025-8-10 18:49
被X66iaM编辑
,原因: