Windows Kernel Callback-1

image-20260526234952301

EDR工作原理

关于EDR的基本构成,我十分推荐该文章:https://blog.whiteflag.io/blog/from-windows-drivers-to-a-almost-fully-working-edr/

总而言之,一个基础的EDR实现应该要有以下部分:

  • 静态文件扫描器
  • Ring3的API hook
  • Ring0的内核回调
  • Minifilter文件微过滤器
  • WFP网络过滤
  • ETW事件
  • AMSI反恶意软件接口

image-20260526234952301

其中Ring0的内核回调是比较重要的主机信息收集源,可以分为三类:

  • 内核回调函数
  • Minifilter文件微过滤器
  • WFP网络过滤

这篇文章简单记录对于五个内核回调函数的学习。

内核安全机制

驱动签名机制

Driver Signature Enforcement(DSE,驱动程序签名强制执行) 是 Windows Vista 64 位系统引入的一项核心安全机制,要求所有加载到内核模式的驱动程序必须包含有效的数字签名,否则系统将拒绝加载。其目的是防止恶意软件或未经验证的代码进入高特权级的内核空间,从而提升系统稳定性与安全性。

从 Windows 10 / Windows Server 2016 开始,微软进一步收紧了这一策略。内核模式驱动程序不再允许使用普通代码签名证书,而必须通过 Windows 硬件开发中心仪表板(现已整合为 硬件开发中心) 进行提交和签名。这一过程要求开发者持有 EV 证书(Extended Validation,扩展验证证书),它是一种经过严格法律和实体身份验证的高级别证书,仅发放给合法的商业实体。

内核保护机制

PatchGuard(Kernel Patch Protection,内核补丁保护)之前文章已经接触过,主要用于保护 Windows 内核的关键数据结构、代码和关键函数表不被未经授权地修改,直接导致了反病毒软件不能劫持 SSDT 或内核中的任何关键结构。主要保护以下几部分:

  • 系统服务表(SSDT,System Service Descriptor Table)
  • 中断描述符表(IDT,Interrupt Descriptor Table)
  • 全局描述符表(GDT,Global Descriptor Table)
  • 内核代码段(内核映像本身)
  • 处理器控制寄存器(如 MSR,Model-Specific Registers)
  • 某些关键驱动和系统 DLL 的结构

CI(Code Integrity,代码完整性)是 Windows 内核中用于对驱动程序及关键系统文件实施加载时及运行时完整性校验的核心安全机制。它不负责制定是否强制签名的策略(该策略由 DSE 承担),而是具体执行以下校验操作:计算文件哈希值,验证其是否与内嵌签名或目录文件(.cat)中记录的哈希值一致,以防止文件被篡改;同时校验证书链的有效性及证书是否被吊销。DSE 决定了是否需要签名,而 CI 负责确认签名是否真实、内容是否完整。


记录一下其他的安全机制

代码完整性
- DSE — 驱动签名强制
- CI — 内核模块加载时签名/哈希校验
- HVCI — Hypervisor 强制 W^X,内核页不能同时可写可执行
- Secure Boot — UEFI 阶段 bootloader→winload 签名链校验
反篡改
- PatchGuard (KPP) — 定时校验 SSDT/IDT/代码段等关键结构,被改即蓝屏
- Driver Verifier — 开发期池损坏/IRQL/死锁实时检测
- KCFG — 内核控制流保护,间接调用前验目标地址
内存保护
- KASLR — 内核基址每次启动随机化
- SMEP — Ring 0 不能执行用户态页面
- SMAP — Ring 0 不能直接访问用户态内存
- KDP — 关键内核数据页由 Hypervisor 强制只读
虚拟化安全 (VBS)
- VBS (VTL 0/1) — 双虚拟信任层隔离
- Credential Guard — VTL1 隔离开 LSASS 密钥
缓解攻击
- Kernel CET — 硬件影子栈防内核 ROP/JOP
- kCFG - 内核控制流保护
对象保护
- ObjManager Callbacks — 进程/线程句柄权限控制
- Token Security │ 限制低权限进程对高权限 token 的操作
- Trust Label │ Windows 11 为进程打信任标记,限制跨信任级交互
检测与审计
- ETW TI — 内核级事件追踪给 EDR 供数据
- Process/Thread/Image/Registry Callbacks — 回调监控框架

简单驱动

使用一些工具可以直观查看内核相关信息:

https://learn.microsoft.com/en-us/sysinternals/downloads/debugview

https://github.com/hfiref0x/WinObjEx64

https://github.com/AxtMueller/Windows-Kernel-Explorer

https://github.com/QAX-Anti-Virus/QDoctor

OpenArk:https://mefcl.lanzouu.com/b0j0nxlvi 密码:2bcr

编写驱动

Visual Studio 2022 环境下,下载匹配的 SDK 和 WDK,搜索vs2022驱动开发环境搭建相关文章了解细节。选择 Empty WDM Driver 模板并创建项目

image-20260516174958399

hello world驱动,在vs编译时要注意 KdPrint 仅在 Debug 版本中生效,在 Release 版本编译时会被直接移除

#include <ntddk.h>

void DriverUnload(_In_ PDRIVER_OBJECT DriverObject)
{
UNREFERENCED_PARAMETER(DriverObject);
KdPrint(("unload ok\n"));
}

extern "C"
NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath)
{
UNREFERENCED_PARAMETER(RegistryPath);

KdPrint(("DriverEntry ok\n"));

DriverObject->DriverUnload = DriverUnload;

return STATUS_SUCCESS;
}

加载自定义无签名驱动需要开启测试签名模式

#打开测试签名模式,并重启
bcdedit /set testsigning on

如果win10测试机开启了安全启动,即使开启了测试签名模式依然会失败,此时需要通过BIOS禁用驱动程序强制签名(DSE)

image-20260516202018725

image-20260516185649909

如果自己添加一个数字签名,只开启测试签名模式就可以

image-20260516202148167

捕获 KdPrint 输出还需要添加注册表项 DEFAULT 的值为 0000000f

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Debug Print Filter

开启DbgView并设置:

  • Capture -> Capture Kernel (捕获内核输出)

  • Capture -> Enable Verbose Kernel Output (启用详细内核输出)

加载驱动程序:

#安装驱动
sc create DriverTest type= kernel binPath= "C:\Users\admin\Desktop\tt\sec\HelloWorldDriver.sys"
#加载驱动
sc start DriverTest
#卸载驱动
sc stop DriverTest

image-20260516202620943

双机调试

内核的调试在被调试机器上,整个系统都会卡住,所以内核的调试需要两台机器,这里使用网络调试

被调试机器:

bcdedit /debug on
bcdedit /dbgsettings net hostip:<调试机ip> port:50000 key:1.2.3.4
重启

调试机器:

image-20260517191626594

顺利连接开始调试

image-20260517191922588

内核通知回调例程

Windows 内核回调机制是内核暴露给驱动程序的通知/拦截框架,核心思想是 提前在内核中注册一个函数指针,在特定事件发生时内核会调用该函数。简而言之,利用注册函数API将自定义回调函数注册到内核中,等待事件发生后,从事件发生到函数调用过程可以称为回调。

回调函数、回调例程函数、回调通知函数、通知回调例程 等都是同一个意思。

进程

用于注册或删除进程通知回调函数的注册函数API有

  • PsSetCreateProcessNotifyRoutine
  • PsSetCreateProcessNotifyRoutineEx:可以阻止进程创建
  • PsSetCreateProcessNotifyRoutineEx2:Windows 10 1607引入,拓展功能

PsSetCreateProcessNotifyRoutineEx 函数原型,限制最多注册64个回调函数

NTSTATUS PsSetCreateProcessNotifyRoutineEx(
[in] PCREATE_PROCESS_NOTIFY_ROUTINE_EX NotifyRoutine,
[in] BOOLEAN Remove
);
  • Remove:第二个参数表示将例程从 回调函数列表 中添加或移除

  • NotifyRoutine:第一个参数为驱动程序实现的回调函数,原型如下

typedef
VOID
(*PCREATE_PROCESS_NOTIFY_ROUTINE_EX) (
_Inout_ PEPROCESS Process,
_In_ HANDLE ProcessId,
_Inout_opt_ PPS_CREATE_NOTIFY_INFO CreateInfo
);

回调函数中重要的是第三个参数 CreateInfo,PPS_CREATE_NOTIFY_INFO类型的对象 非空时表示进程正在创建,其包含了被创建进程的详细信息,结构如下

typedef struct _PS_CREATE_NOTIFY_INFO {
SIZE_T Size;
union {
ULONG Flags;
struct {
ULONG FileOpenNameAvailable : 1;
ULONG IsSubsystemProcess : 1;
ULONG Reserved : 30;
};
};
HANDLE ParentProcessId;
CLIENT_ID CreatingThreadId;
struct _FILE_OBJECT *FileObject;
PCUNICODE_STRING ImageFileName;
PCUNICODE_STRING CommandLine;
NTSTATUS CreationStatus;
} PS_CREATE_NOTIFY_INFO, *PPS_CREATE_NOTIFY_INFO;

包含父进程ID、命令行、映像文件名等,可通过设置 CreateInfo->CreationStatus = STATUS_ACCESS_DENIED 来阻止进程启动。

示例

给出一个例子,实现禁止进程名为notepad.exe启动。通过用户态的client程序与驱动进行IOCTL通信,控制是否开启拦截进程

common.h

#pragma once

#define DEVICE_NAME L"\\Device\\ProcessMon"
#define SYMLINK_NAME L"\\DosDevices\\ProcessMon"
#define DEVICE_USER_NAME L"\\\\.\\ProcessMon"

// IOCTL codes
#define IOCTL_ENABLE_BLOCK CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_DISABLE_BLOCK CTL_CODE(FILE_DEVICE_UNKNOWN, 0x801, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_QUERY_STATS CTL_CODE(FILE_DEVICE_UNKNOWN, 0x802, METHOD_BUFFERED, FILE_ANY_ACCESS)

typedef struct _PROCESS_MON_STATS {
volatile LONG TotalCreated;
volatile LONG TotalBlocked;
BOOLEAN BlockEnabled;
LONG Padding;
} PROCESS_MON_STATS, * PPROCESS_MON_STATS;

驱动main.cpp

#include <ntddk.h>
#include "common.h"

static PROCESS_MON_STATS g_Stats = { 0, 0, TRUE, 0 };

extern "C" NTKERNELAPI UCHAR* PsGetProcessImageFileName(IN PEPROCESS Process);
void OnProcessNotify(_Inout_ PEPROCESS Process, _In_ HANDLE ProcessId, _Inout_opt_ PPS_CREATE_NOTIFY_INFO CreateInfo);


void DriverUnload(_In_ PDRIVER_OBJECT DriverObject)
{
KdPrint(("[Y0ng] Unloading\n"));

// 注销回调函数
PsSetCreateProcessNotifyRoutineEx(OnProcessNotify, TRUE);

// 删除符号连接
UNICODE_STRING symlinkName;
RtlInitUnicodeString(&symlinkName, SYMLINK_NAME);
IoDeleteSymbolicLink(&symlinkName);

// 删除设备对象
IoDeleteDevice(DriverObject->DeviceObject);

KdPrint(("[Y0ng] Unload Ok\n"));
}

NTSTATUS DispatchCreateClose(_In_ PDEVICE_OBJECT DeviceObject, _In_ PIRP Irp)
{
UNREFERENCED_PARAMETER(DeviceObject);
Irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Information = 0;
IoCompleteRequest(Irp, 0);
return STATUS_SUCCESS;
}

NTSTATUS DispatchIoControl(_In_ PDEVICE_OBJECT DeviceObject, _In_ PIRP Irp)
{
UNREFERENCED_PARAMETER(DeviceObject);

PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
ULONG code = stack->Parameters.DeviceIoControl.IoControlCode;
NTSTATUS status = STATUS_SUCCESS;
ULONG infoLen = 0;

switch (code) {
case IOCTL_ENABLE_BLOCK:
KdPrint(("[Y0ng] IOCTL: Enable block\n"));
g_Stats.BlockEnabled = TRUE;
break;

case IOCTL_DISABLE_BLOCK:
KdPrint(("[Y0ng] IOCTL: Disable block\n"));
g_Stats.BlockEnabled = FALSE;
break;

case IOCTL_QUERY_STATS: {
ULONG outLen = stack->Parameters.DeviceIoControl.OutputBufferLength;
if (outLen >= sizeof(PROCESS_MON_STATS)) {
RtlCopyMemory(Irp->AssociatedIrp.SystemBuffer,&g_Stats, sizeof(PROCESS_MON_STATS));
infoLen = sizeof(PROCESS_MON_STATS);
}
else {
status = STATUS_BUFFER_TOO_SMALL;
}
break;
}

default:
status = STATUS_INVALID_DEVICE_REQUEST;
break;
}

Irp->IoStatus.Status = status;
Irp->IoStatus.Information = infoLen;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return status;
}

extern "C" NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath)
{
UNREFERENCED_PARAMETER(RegistryPath);
KdPrint(("[Y0ng] DriverEntry\n"));

// 全局状态变量
NTSTATUS status = STATUS_SUCCESS;
bool SetsymLink , SetprocessCallback = false;
PDEVICE_OBJECT DeviceObject = nullptr;
UNICODE_STRING deviceName;
UNICODE_STRING symlinkName;

do
{
// 创建设备对象
RtlInitUnicodeString(&deviceName, DEVICE_NAME);
status = IoCreateDevice(DriverObject, 0, &deviceName, FILE_DEVICE_UNKNOWN, 0, FALSE, &DeviceObject);
if (!NT_SUCCESS(status)) {
KdPrint(("[Y0ng] IoCreateDevice failed: 0x%08X\n", status));
break;
}

// 创建符号链接
RtlInitUnicodeString(&symlinkName, SYMLINK_NAME);
status = IoCreateSymbolicLink(&symlinkName, &deviceName);
if (!NT_SUCCESS(status)) {
KdPrint(("[Y0ng] IoCreateSymbolicLink failed: 0x%08X\n", status));
break;
}
SetsymLink = true;

// 创建进程回调函数
status = PsSetCreateProcessNotifyRoutineEx(OnProcessNotify, FALSE);
if (!NT_SUCCESS(status)) {
KdPrint(("[Y0ng] PsSetCreateProcessNotifyRoutineEx failed: 0x%08X\n", status));
break;
}
SetprocessCallback = true;


} while (false);

//全局状态检查
if (!NT_SUCCESS(status)) {
if (SetprocessCallback)
PsSetCreateProcessNotifyRoutineEx(OnProcessNotify, TRUE);
if (SetsymLink)
IoDeleteSymbolicLink(&symlinkName);
if (DeviceObject)
IoDeleteDevice(DeviceObject);
}

// 设置分发函数
DriverObject->DriverUnload = DriverUnload;
DriverObject->MajorFunction[IRP_MJ_CREATE] = DriverObject->MajorFunction[IRP_MJ_CLOSE] = DispatchCreateClose;
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DispatchIoControl;

return status;
}


// 进程回调
VOID OnProcessNotify(_Inout_ PEPROCESS Process, _In_ HANDLE ProcessId, _Inout_opt_ PPS_CREATE_NOTIFY_INFO CreateInfo)
{
UNREFERENCED_PARAMETER(Process);

// CreateInfo == NULL 表示进程退出,忽略
if (!CreateInfo) return;

InterlockedIncrement(&g_Stats.TotalCreated);

UCHAR* FileName = PsGetProcessImageFileName(Process);

// ---- 打印进程信息 ----
KdPrint(("[Y0ng] ========================================\n"));
KdPrint(("[Y0ng] Process Created:\n"));
KdPrint(("[Y0ng] PID : %lu\n", HandleToULong(ProcessId)));
KdPrint(("[Y0ng] ParentPID : %lu\n", HandleToULong(CreateInfo->ParentProcessId)));
KdPrint(("[Y0ng] FileName : %s\n", FileName));
KdPrint(("[Y0ng] Image : %wZ\n", CreateInfo->ImageFileName));
KdPrint(("[Y0ng] CmdLine : %wZ\n", CreateInfo->CommandLine));
KdPrint(("[Y0ng] ----------------------------------------\n"));

// 检查是否需要阻止
if (!g_Stats.BlockEnabled) return;

if (_stricmp((char*)FileName, "notepad.exe") == 0) {
CreateInfo->CreationStatus = STATUS_ACCESS_DENIED;
DbgPrint("[Y0ng] *** BLOCKED notepad.exe (PID=%lu) ***\n", HandleToULong(ProcessId));
InterlockedIncrement(&g_Stats.TotalBlocked);
}
}

开启拦截

image-20260517180519236

关闭拦截

image-20260517180616819

全局回调数组

定位回调函数地址

在上面提到的回调函数列表实际上是一个64长度的全局数组(x64下),每当注册回调函数后,回调函数指针会被添加到 EX_FAST_REF 结构的数组中,实际上这些结构指针存储在 nt!PspCreateProcessNotifyRoutine 数组中。

从注册函数来看,nt!PsSetCreateProcessNotifyRoutineEx 调用了 nt!PspSetCreateProcessNotifyRoutine ,关于该函数的分析有很多文章,nt!PspSetCreateProcessNotifyRoutine

image-20260517195859856

查看代码发现通过 ExAllocateCallBack 函数将注册回调函数封装成 _EX_CALLBACK_ROUTINE_BLOCK 类型,前8位是 EX_RUNDOWN_REF 结构可忽略,Function 存储了真正的回调函数地址。

typedef struct _EX_CALLBACK_ROUTINE_BLOCK
{
EX_RUNDOWN_REF RundownProtect;
PEX_CALLBACK_FUNCTION Function;
PVOID Context;
} EX_CALLBACK_ROUTINE_BLOCK, *PEX_CALLBACK_ROUTINE_BLOCK;

然后再通过 ExCompareExchangeCallBack 函数插入到 PspCreateProcessNotifyRoutine 数组中,该数组中是 EX_FAST_REF 16字节对齐 (低 4 位永远是 0) 的结构类型。

image-20260517200708053

那么通过数组反推回调函数真正的地址需要:

回调函数地址 = *(PspCreateProcessNotifyRoutine[i] & 0xFFFFFFFFFFFFFFF0) + 8

查看数组,有13个回调

dqs nt!PspCreateProcessNotifyRoutine

image-20260518011143638

以第一个为例

1: kd> ? (ffffb709`3545024f & 0xFFFFFFFFFFFFFFF0) + 8
Evaluate expression: -80224800406968 = ffffb709`35450248

1: kd> dq ffffb709`35450248 L1
ffffb709`35450248 fffff802`7cf390c0

1: kd> u fffff802`7cf390c0
nt!ViCreateProcessCallback:
fffff802`7cf390c0 4883ec28 sub rsp,28h
fffff802`7cf390c4 833d753f8e0000 cmp dword ptr [nt!ViVerifierEnabled (fffff802`7d81d040)],0
fffff802`7cf390cb 488bc2 mov rax,rdx
fffff802`7cf390ce 0f858c4d1400 jne nt!ViCreateProcessCallback+0x144da0 (fffff802`7d07de60)
fffff802`7cf390d4 4883c428 add rsp,28h
fffff802`7cf390d8 c3 ret
fffff802`7cf390d9 cc int 3
fffff802`7cf390da cc int 3
1: kd> ln fffff802`7cf390c0
Browse module
Set bu breakpoint

(fffff802`7cf390c0) nt!ViCreateProcessCallback | (fffff802`7cf390e0) nt!PsReferenceSiloContext
Exact matches:
nt!ViCreateProcessCallback (void)

第二个 cng.sys

1: kd> dq ((ffffb709`356848af & 0xFFFFFFFFFFFFFFF0) + 8) L1
ffffb709`356848a8 fffff802`7e997070

1: kd> lmDva fffff802`7e997070
Browse full module list
start end module name
fffff802`7e990000 fffff802`7ea4b000 cng (pdb symbols) c:\symbols\cng.pdb\A991BA58B5368F30B1E2322692E985C51\cng.pdb
Loaded symbol image file: cng.sys
Image path: \SystemRoot\System32\drivers\cng.sys
Image name: cng.sys
Browse all global symbols functions data Symbol Reload
Image was built with /Brepro flag.
Timestamp: 8AF25707 (This is a reproducible build file hash, not a timestamp)
CheckSum: 000B8A74
ImageSize: 000BB000
Mapping Form: Loaded
Translations: 0000.04b0 0000.04e4 0409.04b0 0409.04e4
Information from resource tables:

1: kd> u fffff802`7e997070
cng!CngCreateProcessNotifyRoutine:
fffff802`7e997070 4883ec28 sub rsp,28h
fffff802`7e997074 488364244800 and qword ptr [rsp+48h],0
fffff802`7e99707a 488bc2 mov rax,rdx
fffff802`7e99707d 4584c0 test r8b,r8b
fffff802`7e997080 7406 je cng!CngCreateProcessNotifyRoutine+0x18 (fffff802`7e997088)
fffff802`7e997082 4883c428 add rsp,28h
fffff802`7e997086 c3 ret
fffff802`7e997087 cc int 3

与第三方工具比对地址类型正确

image-20260518014534113

自定义驱动

1: kd> dqs nt!PspCreateProcessNotifyRoutine
fffff802`7d8ec5e0 ffffb709`3545024f
fffff802`7d8ec5e8 ffffb709`356848af
fffff802`7d8ec5f0 ffffb709`35ccbd0f
fffff802`7d8ec5f8 ffffb709`35ccbdcf
fffff802`7d8ec600 ffffb709`35ccc57f
fffff802`7d8ec608 ffffb709`35d2a31f
fffff802`7d8ec610 ffffb709`35d2a9af
fffff802`7d8ec618 ffffb709`35d2b30f
fffff802`7d8ec620 ffffb709`35d2b03f
fffff802`7d8ec628 ffffb709`35d2af1f
fffff802`7d8ec630 ffffb709`35d2dbbf
fffff802`7d8ec638 ffffb709`393607af
fffff802`7d8ec640 ffffb709`393661df
fffff802`7d8ec648 ffffb709`3af4f5ef
fffff802`7d8ec650 00000000`00000000
fffff802`7d8ec658 00000000`00000000

1: kd> dq ((ffffb709`3af4f5ef & 0xFFFFFFFFFFFFFFF0) + 8) L1
ffffb709`3af4f5e8 fffff802`967511c0

1: kd> u fffff802`967511c0
CallBackDriver!OnProcessNotify [E:\Project\Driver\CallBackDriver\main.cpp @ 143]:
fffff802`967511c0 4c89442418 mov qword ptr [rsp+18h],r8
fffff802`967511c5 4889542410 mov qword ptr [rsp+10h],rdx
fffff802`967511ca 48894c2408 mov qword ptr [rsp+8],rcx
fffff802`967511cf 4883ec38 sub rsp,38h
fffff802`967511d3 48837c245000 cmp qword ptr [rsp+50h],0
fffff802`967511d9 7505 jne CallBackDriver!OnProcessNotify+0x20 (fffff802`967511e0)
fffff802`967511db e9fe000000 jmp CallBackDriver!OnProcessNotify+0x11e (fffff802`967512de)
fffff802`967511e0 488d05191e0000 lea rax,[CallBackDriver!g_Stats (fffff802`96753000)]

1: kd> lmDva fffff802`967511c0
Browse full module list
start end module name
fffff802`96750000 fffff802`96757000 CallBackDriver (private pdb symbols) E:\Project\Driver\CallBackDriver\x64\Debug\CallBackDriver.pdb
Loaded symbol image file: CallBackDriver.sys
Image path: CallBackDriver.sys
Image name: CallBackDriver.sys
Browse all global symbols functions data Symbol Reload
Timestamp: Sun May 17 17:59:24 2026 (6A09917C)
CheckSum: 00008006
ImageSize: 00007000
Mapping Form: Loaded
Translations: 0000.04b0 0000.04e4 0409.04b0 0409.04e4
Information from resource tables:

如果去除该数组值,就会导致自定义驱动失效

1: kd> eq fffff802`7d8ec648 0

定位回调函数数组

上面搞清楚了回调函数在数组中的地址计算方式,那么如何 定位数组的地址 呢?

PspCreateProcessNotifyRoutine 不是导出符号,所以要想办法定位它,常见方法是 获取某个导出函数地址并向后进行特征码扫描,回顾 PspSetCreateProcessNotifyRoutine 导出函数里有一条关键指令,使用RIP 相对寻址

4c8d2d3f2c5500     lea     r13, [ntkrnlmp!PspCreateProcessNotifyRoutine (fffff8027d8ec5e0)]

4C 8D 2D | 3F 2C 55 00
↑ ↑ ↑ ↑
│ │ │ └── disp32(4字节,小端)
│ │ └── ModR/M: Mod=00 Reg=R13 R/M=101(RIP相对)
│ └── 指令=LEA
└── REX前缀 (0100 1100): R=1 → 把reg字段扩展一位,101→R13

RIP 相对寻址公式:

目标地址 = 下一条指令的地址 + disp32
= (当前指令地址 + 当前指令长度) + disp32
= RIP + disp32

那么就定位 4c 8d 2d 特征码,根据公式

PspCreateProcessNotifyRoutine 
= 本条指令地址 + 指令长度 + disp32
= 0xfffff802`7d39999a + 7 + 0x00552C3F
= 0xfffff802`7d8ec5e0

image-20260518221745732

给出定位函数

INT64 GetPspCreateProcessNotifyRoutineArray() {
INT64 PsSetCallbacksNotifyRoutineAddress = GetFuncAddress((CHAR*)"PsSetCreateProcessNotifyRoutine");
if (PsSetCallbacksNotifyRoutineAddress == 0) return 0;
//定位PspSetCreateProcessNotifyRoutine函数地址
INT count = 0;
BYTE* buffer = (BYTE*)malloc(1);
while (1) {
DriverReadMemery((VOID*)PsSetCallbacksNotifyRoutineAddress, buffer,1);
if (*buffer == 0xE8 || *buffer == 0xE9) {
break;
}
PsSetCallbacksNotifyRoutineAddress = PsSetCallbacksNotifyRoutineAddress + 1;
if (count == 200) {
printf("未找到Pspsetcreateprocessnotifyroutine 函数地址\n");
return 0;
}
count++;
}

//获取Pspsetcreateprocessnotifyroutine 函数的偏移地址
UINT64 PspOffset = 0;
for (int i = 4, k = 24; i > 0; i--, k = k - 8){

DriverReadMemery((VOID*)(PsSetCallbacksNotifyRoutineAddress + i), buffer, 1);
PspOffset = ((UINT64)*buffer << k) + PspOffset;
}
// 检查符号位
if ((PspOffset & 0x00000000ff000000) == 0x00000000ff000000)
PspOffset = PspOffset | 0xffffffff00000000; // 负偏移情况下的符号扩展

INT64 PspSetCallbackssNotifyRoutineAddress = PsSetCallbacksNotifyRoutineAddress + PspOffset + 5;

//printf("PspSetCallbackssNotifyRoutineAddress: %I64x\n", PspSetCallbackssNotifyRoutineAddress);

//获取PspCreateProcessNotifyRoutineArray 数组地址
//寻找lea 指令 来定位数组地址
BYTE SearchByte1 = 0x4C;
BYTE SearchByte2 = 0x8D;
BYTE bArray[3] = {0};
count = 0;
INT64 back = PspSetCallbackssNotifyRoutineAddress;
BOOL stop = FALSE;
while (count <= 200) {
DriverReadMemery((VOID*)PspSetCallbackssNotifyRoutineAddress, bArray, 3);
if (bArray[0] == SearchByte1 && bArray[1] == SearchByte2) {

if ((bArray[2] == 0x0D) || (bArray[2] == 0x15) || (bArray[2] == 0x1D) || (bArray[2] == 0x25) || (bArray[2] == 0x2D) || (bArray[2] == 0x35) || (bArray[2] == 0x3D))
{
break;
}
}
PspSetCallbackssNotifyRoutineAddress = PspSetCallbackssNotifyRoutineAddress + 1;
if (count == 200)
{
SearchByte1 = 0x48;
count = -1;
PspSetCallbackssNotifyRoutineAddress = back;
if (stop)
{
printf("未找到lea 指令,无法定位PspSetCallbackssNotifyRoutineAddress 数组\n");
return 0;
}
stop = true;
}
count++;
}

PspOffset = 0;
for (int i = 6, k = 24; i > 2; i--, k = k - 8) {

DriverReadMemery((VOID*)(PspSetCallbackssNotifyRoutineAddress + i), buffer, 1);
PspOffset = ((UINT64)*buffer << k) + PspOffset;
}

if ((PspOffset & 0x00000000ff000000) == 0x00000000ff000000)
PspOffset = PspOffset | 0xffffffff00000000;

INT64 PspCreateProcessNotifyRoutineAddress = PspSetCallbackssNotifyRoutineAddress + PspOffset + 7;

return PspCreateProcessNotifyRoutineAddress;
}

线程

用于注册线程通知回调函数的注册函数API有

  • PsSetCreateThreadNotifyRoutine
  • PsSetCreateThreadNotifyRoutineEx

用于删除线程通知回调函数的函数API有

  • PsRemoveCreateThreadNotifyRoutine

PsSetCreateThreadNotifyRoutine 函数原型

NTSTATUS PsSetCreateThreadNotifyRoutine(
[in] PCREATE_THREAD_NOTIFY_ROUTINE NotifyRoutine
);

只有一个参数为驱动程序实现的回调函数,原型如下

typedef
VOID
(*PCREATE_THREAD_NOTIFY_ROUTINE)(
_In_ HANDLE ProcessId,
_In_ HANDLE ThreadId,
_In_ BOOLEAN Create
);

参数分别是:进程ID、线程ID、线程是被创建还是销毁的标志

示例

VOID ThreadNotifyRoutine(
_In_ HANDLE ProcessId,
_In_ HANDLE ThreadId,
_In_ BOOLEAN Create)
{
// 线程退出,忽略
if (!Create) return;

InterlockedIncrement(&g_Stats.TotalCreated);

// ---- 获取进程名 ----
PEPROCESS process = NULL;
PCUNICODE_STRING imageName = NULL;

if (NT_SUCCESS(PsLookupProcessByProcessId(ProcessId, &process))) {
imageName = PsGetProcessImageFileName(process);
}

// ---- 打印线程信息 ----
DbgPrint("[Y0ng] ========================================\n");
DbgPrint("[Y0ng] Thread Created:\n");
DbgPrint("[Y0ng] TID : %lu\n", HandleToULong(ThreadId));
DbgPrint("[Y0ng] PID : %lu\n", HandleToULong(ProcessId));
DbgPrint("[Y0ng] Process : %wZ\n", imageName);
DbgPrint("[Y0ng] ----------------------------------------\n");
}

全局回调数组

分析思路同进程,回调函数存储在 nt!PspCreateThreadNotifyRoutine 64长度数组中。

定位回调函数地址

回调函数地址 = *(PspCreateThreadNotifyRoutine[i] & 0xFFFFFFFFFFFFFFF0) + 8
1: kd> dqs fffff8027d8ec3e0 L5
fffff802`7d8ec3e0 ffffb709`35ccbe5f
fffff802`7d8ec3e8 ffffb709`35ccc27f
fffff802`7d8ec3f0 ffffb709`3936023f
fffff802`7d8ec3f8 ffffb709`3936632f
fffff802`7d8ec400 ffffb709`3936605f <-

1: kd> dq ((ffffb709`3936605f & 0xFFFFFFFFFFFFFFF0) + 8) L1
ffffb709`39366058 fffff802`966f9ed0

1: kd> ln fffff802`966f9ed0
Browse module
Set bu breakpoint

(fffff802`966f9ed0) KslD!tk::COSCallback::CreateThreadNotifyRoutineEx | (fffff802`966f9f70) KslD!tk::COSCallback::CreateProcessNotifyRoutineEx
Exact matches:

定位回调函数数组

PspCreateThreadNotifyRoutine 不是导出符号,所以定位 PspSetCreateThreadNotifyRoutine 导出函数中的特征码 48 8d 0d

PspCreateThreadNotifyRoutine
= 本条指令地址 + 指令长度 + disp32
= 0xfffff802`7d3998ba + 7 + 0x00552B1F
= 0xfffff802`7d8ec3e0

image-20260518234618195

映像加载

用于注册映像加载时通知回调函数的注册函数API有

  • PsSetLoadImageNotifyRoutine
  • PsSetLoadImageNotifyRoutineEx

用于删除映像加载时通知回调函数的函数API有

  • PsRemoveLoadImageNotifyRoutine

PsSetLoadImageNotifyRoutine 函数原型

NTSTATUS PsSetLoadImageNotifyRoutine(
[in] PLOAD_IMAGE_NOTIFY_ROUTINE NotifyRoutine
);

只有一个参数为驱动程序实现的回调函数,原型如下

typedef
VOID
(*PLOAD_IMAGE_NOTIFY_ROUTINE)(
_In_opt_ PUNICODE_STRING FullImageName,
_In_ HANDLE ProcessId, // pid into which image is being mapped
_In_ PIMAGE_INFO ImageInfo
);
  • FullImageName:NT格式映像名称

  • ProcessId:载入映像的进程ID

  • ImageInfo:映像附加信息结构体,如下

typedef struct _IMAGE_INFO {
union {
ULONG Properties;
struct {
ULONG ImageAddressingMode : 8; // Code addressing mode
ULONG SystemModeImage : 1; // System mode image
ULONG ImageMappedToAllPids : 1; // Image mapped into all processes
ULONG ExtendedInfoPresent : 1; // IMAGE_INFO_EX available
ULONG MachineTypeMismatch : 1; // Architecture type mismatch
ULONG ImageSignatureLevel : 4; // Signature level
ULONG ImageSignatureType : 3; // Signature type
ULONG ImagePartialMap : 1; // Nonzero if entire image is not mapped
ULONG Reserved : 12;
};
};
PVOID ImageBase; // 基址
ULONG ImageSelector;
SIZE_T ImageSize; // 大小
ULONG ImageSectionNumber;
} IMAGE_INFO, *PIMAGE_INFO;

全局回调数组

分析思路同进程,回调函数存储在 nt!PspLoadImageNotifyRoutine 64长度数组中。

定位回调函数地址

回调函数地址 = *(PspLoadImageNotifyRoutine[i] & 0xFFFFFFFFFFFFFFF0) + 8
1: kd> dqs fffff8027d8ec1e0 L5
fffff802`7d8ec1e0 ffffb709`35ccc12f
fffff802`7d8ec1e8 ffffb709`35d2b0ff
fffff802`7d8ec1f0 ffffb709`35d2ba8f
fffff802`7d8ec1f8 ffffb709`3936650f <-
fffff802`7d8ec200 00000000`00000000

1: kd> dq ((ffffb709`3936650f & 0xFFFFFFFFFFFFFFF0) + 8) L1
ffffb709`39366508 fffff802`966f9d90

1: kd> ln fffff802`966f9d90
Browse module
Set bu breakpoint

(fffff802`966f9d90) KslD!tk::COSCallback::LoadImageNotifyRoutine | (fffff802`966f9e30) KslD!tk::COSCallback::CreateThreadNotifyRoutine
Exact matches:

定位回调函数数组

PspLoadImageNotifyRoutine 不是导出符号,所以定位 PsSetLoadImageNotifyRoutineEx 导出函数中的特征码 48 8d 0d

PspLoadImageNotifyRoutine
= 本条指令地址 + 指令长度 + disp32
= 0xfffff802`7d399691 + 7 + 0x00552B48
= 0xfffff802`7d8ec1e0

image-20260519003551466

注册表

用于注册访问、修改注册表时通知回调函数的注册函数API有

  • CmRegisterCallback
  • CmRegisterCallbackEx

用于删除访问、修改注册表时通知回调函数的函数API有

  • CmUnRegisterCallback

CmRegisterCallbackEx 函数原型

NTSTATUS CmRegisterCallbackEx(
[in] PEX_CALLBACK_FUNCTION Function,
[in] PCUNICODE_STRING Altitude,
[in] PVOID Driver,
[in, optional] PVOID Context,
[out] PLARGE_INTEGER Cookie,
PVOID Reserved
);
  • Altitude:回调高度
  • Driver:驱动对象
  • Context:可选值,传递给注册回调函数的第一个参数,一般为 nullptr
  • Cookie:注册成功时的结果,在注销时必须传递给 CmUnRegisterCallback
  • Reserved:保留

第一个参数Function为驱动程序实现的回调函数,原型如下

EX_CALLBACK_FUNCTION ExCallbackFunction;

NTSTATUS ExCallbackFunction(
[in] PVOID CallbackContext,
[in, optional] PVOID Argument1,
[in, optional] PVOID Argument2
)
{...}
  • CallbackContext:CmRegisterCallbackEx的Context原样传递过来
  • Argument1:一个 REG_NOTIFY_CLASS 类型的值,该值标识正在执行的操作类型,以及表明这是操作前回调还是操作后回调
  • Argument2:指向包含与Argument1指定操作相关信息的结构体的指针,结构类型取决于 Argument1 的值
REG_NOTIFY_CLASS(Argument1) Information Struct(Argument2) Description
RegNtPreCreateKey REG_PRE_CREATE_KEY_INFORMATION 创建密钥,预通知调用
RegNtPreCreateKeyEx REG_CREATE_KEY_INFORMATION 创建密钥(扩展),预通知调用
RegNtPreOpenKey REG_PRE_OPEN_KEY_INFORMATION 打开现有密钥,预通知调用
RegNtPreOpenKeyEx REG_OPEN_KEY_INFORMATION 打开现有密钥(扩展),预通知调用
RegNtDeleteKey REG_DELETE_KEY_INFORMATION 删除密钥,预通知调用
RegNtPreDeleteKey REG_DELETE_KEY_INFORMATION 删除密钥,预通知调用
RegNtSetValueKey REG_SET_VALUE_KEY_INFORMATION 为键设置值项,预通知调用
RegNtPreSetValueKey REG_SET_VALUE_KEY_INFORMATION 为键设置值项,预通知调用
RegNtDeleteValueKey REG_DELETE_VALUE_KEY_INFORMATION 删除键的值项,预通知调用
RegNtPreDeleteValueKey REG_DELETE_VALUE_KEY_INFORMATION 删除键的值项,预通知调用
RegNtSetInformationKey REG_SET_INFORMATION_KEY_INFORMATION 设置密钥的元数据,预通知调用
RegNtPreSetInformationKey REG_SET_INFORMATION_KEY_INFORMATION 设置密钥的元数据,预通知调用
RegNtRenameKey REG_RENAME_KEY_INFORMATION 重命名密钥,预通知调用
RegNtPreRenameKey REG_RENAME_KEY_INFORMATION 重命名密钥,预通知调用
RegNtEnumerateKey REG_ENUMERATE_KEY_INFORMATION 枚举键的子项,预通知调用
RegNtPreEnumerateKey REG_ENUMERATE_KEY_INFORMATION 枚举键的子项,预通知调用
RegNtEnumerateValueKey REG_ENUMERATE_VALUE_KEY_INFORMATION 枚举键的值项,预通知调用
RegNtPreEnumerateValueKey REG_ENUMERATE_VALUE_KEY_INFORMATION 枚举键的值项,预通知调用
RegNtQueryKey REG_QUERY_KEY_INFORMATION 读取密钥的元数据,预通知调用
RegNtPreQueryKey REG_QUERY_KEY_INFORMATION 读取密钥的元数据,预通知调用
RegNtQueryValueKey REG_QUERY_VALUE_KEY_INFORMATION 读取键的值项,预通知调用
RegNtPreQueryValueKey REG_QUERY_VALUE_KEY_INFORMATION 读取键的值项,预通知调用
RegNtQueryMultipleValueKey REG_QUERY_MULTIPLE_VALUE_KEY_INFORMATION 查询键的多个值条目,预通知调用
RegNtPreQueryMultipleValueKey REG_QUERY_MULTIPLE_VALUE_KEY_INFORMATION 查询键的多个值条目,预通知调用
RegNtKeyHandleClose REG_KEY_HANDLE_CLOSE_INFORMATION 关闭键句柄,预通知调用
RegNtPreKeyHandleClose REG_KEY_HANDLE_CLOSE_INFORMATION 关闭键句柄,预通知调用
RegNtPreFlushKey REG_FLUSH_KEY_INFORMATION 将密钥写入磁盘,预通知调用
RegNtPreLoadKey REG_LOAD_KEY_INFORMATION 从文件加载注册表配置单元,预通知调用
RegNtPreUnLoadKey REG_UNLOAD_KEY_INFORMATION 卸载注册表配置单元,预通知调用
RegNtPreQueryKeySecurity REG_QUERY_KEY_SECURITY_INFORMATION 获取注册表项的安全信息,预通知调用
RegNtPreSetKeySecurity REG_SET_KEY_SECURITY_INFORMATION 设置注册表项的安全信息,预通知调用
RegNtPreRestoreKey REG_RESTORE_KEY_INFORMATION 还原注册表项的信息,预通知调用
RegNtPreSaveKey REG_SAVE_KEY_INFORMATION 保存注册表项的信息,预通知调用
RegNtPreReplaceKey REG_REPLACE_KEY_INFORMATION 替换注册表项的信息,预通知调用
RegNtPreQueryKeyName REG_QUERY_KEY_NAME 获取注册表项的完整路径,预通知调用
RegNtPreSaveMergedKey REG_SAVE_MERGED_KEY_INFORMATION 将两个注册表子树的合并视图保存到文件中,预通知调用
RegNtPostCreateKey REG_POST_CREATE_KEY_INFORMATION 创建密钥,通知后调用
RegNtPostCreateKeyEx REG_POST_OPERATION_INFORMATION 创建密钥(扩展),通知后调用
RegNtPostOpenKey REG_POST_OPEN_KEY_INFORMATION 打开现有密钥,通知后调用
RegNtPostOpenKeyEx REG_POST_OPERATION_INFORMATION 打开现有密钥(扩展),通知后调用
RegNtPostDeleteKey REG_POST_OPERATION_INFORMATION 删除密钥,通知后调用
RegNtPostSetValueKey REG_POST_OPERATION_INFORMATION 为键设置值项,通知后调用
RegNtPostDeleteValueKey REG_POST_OPERATION_INFORMATION 删除键的值项,通知后调用
RegNtPostSetInformationKey REG_POST_OPERATION_INFORMATION 设置密钥的元数据,通知后调用
RegNtPostRenameKey REG_POST_OPERATION_INFORMATION 重命名密钥,通知后调用
RegNtPostEnumerateKey REG_POST_OPERATION_INFORMATION 枚举键的子项,通知后调用
RegNtPostEnumerateValueKey REG_POST_OPERATION_INFORMATION 枚举键的值项,通知后调用
RegNtPostQueryKey REG_POST_OPERATION_INFORMATION 读取密钥的元数据,通知后调用
RegNtPostQueryValueKey REG_POST_OPERATION_INFORMATION 读取键的值项,通知后调用
RegNtPostQueryMultipleValueKey REG_POST_OPERATION_INFORMATION 查询键的多个值条目,通知后调用
RegNtPostKeyHandleClose REG_POST_OPERATION_INFORMATION 关闭键句柄,通知后调用
RegNtPostFlushKey REG_POST_OPERATION_INFORMATION 将密钥写入磁盘,通知后调用
RegNtPostLoadKey REG_POST_OPERATION_INFORMATION 从文件加载注册表配置单元,通知后调用
RegNtPostUnLoadKey REG_POST_OPERATION_INFORMATION 卸载注册表配置单元,通知后调用
RegNtPostQueryKeySecurity REG_POST_OPERATION_INFORMATION 获取注册表项的安全信息,通知后调用
RegNtPostSetKeySecurity REG_POST_OPERATION_INFORMATION 设置注册表项的安全信息,通知后调用
RegNtPostRestoreKey REG_POST_OPERATION_INFORMATION 还原注册表项的信息,通知后调用
RegNtPostSaveKey REG_POST_OPERATION_INFORMATION 保存注册表项的信息,通知后调用
RegNtPostReplaceKey REG_POST_OPERATION_INFORMATION 替换注册表项的信息,通知后调用
RegNtPostQueryKeyName REG_POST_OPERATION_INFORMATION 获取注册表项的完整路径,通知后调用
RegNtPostSaveMergedKey REG_POST_OPERATION_INFORMATION 将两个注册表子树的合并视图保存到文件中,通知后调用
RegNtCallbackObjectContextCleanup REG_CALLBACK_CONTEXT_CLEANUP_INFORMATION 驱动程序已调用 CmUnRegisterCallback 或回调刚完成处理
MaxRegNtNotifyClass 枚举类型最大值

如果指定注册表项是需要保护的则直接返回 STATUS_ACCESS_DENIED,示例

// 注册表回调函数
NTSTATUS OnRegistryNotify(_In_ PVOID CallbackContext, _In_opt_ PVOID Argument1, _In_opt_ PVOID Argument2)
{
NTSTATUS status = STATUS_SUCCESS;
UNICODE_STRING ustrRegPath;
// 获取操作类型
LONG lOperateType = (REG_NOTIFY_CLASS)Argument1;
...
// 判断操作
switch (lOperateType)
{
// 创建注册表之前
case RegNtPreCreateKey:
{
// 获取注册表路径
GetFullPath(&ustrRegPath, ((PREG_CREATE_KEY_INFORMATION)Argument2)->RootObject);
DbgPrint("[创建注册表][%wZ][%wZ]\n", &ustrRegPath, ((PREG_CREATE_KEY_INFORMATION)Argument2)->CompleteName);
break;
}
// 删除键之前
case RegNtPreDeleteKey:
{
// 获取注册表路径
GetFullPath(&ustrRegPath, ((PREG_DELETE_KEY_INFORMATION)Argument2)->Object);
DbgPrint("[删除键][%wZ] \n", &ustrRegPath);

// 如果要删除指定注册表项则拒绝
PWCH pszRegister = L"\\REGISTRY\\MACHINE\\SOFTWARE\\lyshark.com";
if (wcscmp(ustrRegPath.Buffer, pszRegister) == 0)
{
DbgPrint("[lyshark] 注册表项删除操作已被拦截! \n");
// 拒绝操作
status = STATUS_ACCESS_DENIED;
}

break;
}
// 修改键值之前
case RegNtPreSetValueKey:
{
// 获取注册表路径
GetFullPath(&ustrRegPath, ((PREG_SET_VALUE_KEY_INFORMATION)Argument2)->Object);
DbgPrint("[修改键值][%wZ][%wZ] \n", &ustrRegPath, ((PREG_SET_VALUE_KEY_INFORMATION)Argument2)->ValueName);
break;
}
default:
break;
}
...
return status;
}

全局回调双向链表

定位回调函数地址

在尝试定位回调函数存储位置时发现调用链

nt!CmRegisterCallbackEx -> nt!CmpRegisterCallbackInternal -> nt!CmpInsertCallbackInListByAltitude

结合AI解释汇编能看出来操作的主要对象是 nt!CallbackListHead,一句话总结为:拿锁 -> 分配 Cookie -> 按 Altitude 降序遍历链表找到插入点 -> 做双向链表四针插入 -> 释放锁

image-20260522223341692

nt!CallbackListHead 是一个双向链表的head,可表示为:

//0x10 bytes (sizeof)
struct LIST_ENTRY64
{
ULONGLONG Flink; //0x0
ULONGLONG Blink; //0x8
};

链表中包含一种未公开的结构体类型,该结构包含指向回调函数的指针。该结构体可表示为:

typedef struct _CMREG_CALLBACK {
struct LIST_ENTRY64
{
ULONGLONG Flink; //0x0
ULONGLONG Blink; //0x8
} List;
ULONG Unknown1;
ULONG Unknown2;
LARGE_INTEGER Cookie;
PVOID Unknown3;
PEX_CALLBACK_FUNCTION Function; // +0x28 <-
} CMREG_CALLBACK, *PCMREG_CALLBACK;

使用AI给出的结构体为,两者正确与否不得而知

typedef struct _CMREG_CALLBACK {
LIST_ENTRY List; // +0x00
ULONG Unknown1; // +0x10
ULONG Unknown2; // +0x14
LARGE_INTEGER Cookie; // +0x18
PVOID Context; // +0x20
PEX_CALLBACK_FUNCTION Function; // +0x28 <-
UNICODE_STRING Altitude; // +0x30
} CMREG_CALLBACK, *PCMREG_CALLBACK;

总之在该结构的0x28偏移处存储了回调函数的地址,所以只需要遍历双向链表的每个结构体0x28偏移就能获取所有的回调函数地址

image-20260523004405168

不断遍历Flink即可实现找到所有回调地址

1: kd> ? nt!CallbackListHead
Evaluate expression: -8785397251216 = fffff802`7d848370
---------------------------------------------
1: kd> dqs nt!CallbackListHead L1
fffff802`7d848370 ffffa103`f27d3e40
1: kd> dqs ffffa103`f27d3e40+0x28 L1
ffffa103`f27d3e68 fffff802`7f31d0d0 WdFilter!MpRegCallback
1: kd> ln fffff802`7f31d0d0
Browse module
Set bu breakpoint

(fffff802`7f31d0d0) WdFilter!MpRegCallback | (fffff802`7f31da60) WdFilter!MpRegPostCreateKeyEx
Exact matches:

---------------------------------------------
1: kd> dqs ffffa103`f27d3e40 L1
ffffa103`f27d3e40 ffffa103`f2942240
1: kd> dqs ffffa103`f2942240+0x28 L1
ffffa103`f2942268 fffff802`80715dd0 UCPD+0x5dd0
1: kd> ln fffff802`80715dd0
Browse module
Set bu breakpoint

---------------------------------------------
1: kd> dqs ffffa103`f2942240 L1
ffffa103`f2942240 ffffa103`f7918090
1: kd> dqs ffffa103`f7918090+0x28 l1
ffffa103`f79180b8 fffff802`7d1d4fd0 nt!VrpRegistryCallback
1: kd> ln fffff802`7d1d4fd0
Browse module
Set bu breakpoint

(fffff802`7d1d4fd0) nt!VrpRegistryCallback | (fffff802`7d1d5160) nt!VrpShouldOperateOnCall
Exact matches:
nt!VrpRegistryCallback (void)

---------------------------------------------
1: kd> dqs ffffa103`f7918090 L1
ffffa103`f7918090 fffff802`7d848370 nt!CallbackListHead

定位回调双向链表

CallbackListHead 不是导出符号,所以定位 CmUnRegisterCallback 导出函数中的特征码 48 8d 0d

image-20260523004916524

对象

对象是对资源(例如文件、进程、令牌、注册表键)的一种抽象表示,内核通知支持的对象类型有 进程、线程、桌面。用于在尝试打开或复制进程、线程、桌面的 句柄 时通知回调函数的注册函数API有

  • ObRegisterCallbacks

用于删除对象句柄通知回调函数的函数API有

  • ObUnRegisterCallbacks

ObRegisterCallbacks 函数原型

NTSTATUS ObRegisterCallbacks(
[in] POB_CALLBACK_REGISTRATION CallbackRegistration,
[out] PVOID *RegistrationHandle
);
  • RegistrationHandle:注册成功后返回值,ObUnRegisterCallbacks使用
  • CallbackRegistration:_OB_CALLBACK_REGISTRATION 结构用于提供驱动程序要针对什么操作进行注册的必要细节

_OB_CALLBACK_REGISTRATION 结构如下

typedef struct _OB_CALLBACK_REGISTRATION {
USHORT Version;
USHORT OperationRegistrationCount;
UNICODE_STRING Altitude;
PVOID RegistrationContext;
OB_OPERATION_REGISTRATION *OperationRegistration;
} OB_CALLBACK_REGISTRATION, *POB_CALLBACK_REGISTRATION;
  • Version:常量 OB_FLT_REGISTRATION_VERSION
  • OperationRegistrationCount:OperationRegistration的数量
  • Altitude:回调高度
  • RegistrationContext:由驱动程序定义,会传递给回调函数
  • OperationRegistration:一个 _OB_OPERATION_REGISTRATION 结构体 数组 的指针,包含对哪种对象的哪种行为进行哪种回调函数的细节

_OB_OPERATION_REGISTRATION 结构如下

typedef struct _OB_OPERATION_REGISTRATION {
POBJECT_TYPE *ObjectType;
OB_OPERATION Operations;
POB_PRE_OPERATION_CALLBACK PreOperation;
POB_POST_OPERATION_CALLBACK PostOperation;
} OB_OPERATION_REGISTRATION, *POB_OPERATION_REGISTRATION;
  • ObjectType:触发回调的对象类型,以下值之一

    • PsProcessType:进程句柄操作
    • PsThreadType:线程句柄操作
    • ExDesktopObjectType:桌面句柄操作,仅支持 Windows 10
  • Operations:触发回调的操作类型,以下值之一

    • OB_OPERATION_HANDLE_CREATE:一个新的进程、线程或桌面句柄 已经或即将打开(例如用户API: CreateProcess| OpenProcess | CreateThread | OpenThread | CreateDesktop | OpenDesktop)
    • OB_OPERATION_HANDLE_DUPLICATE:进程、线程或桌面句柄 已被复制或将要复制(例如用户API:DuplicateHandle)
  • PreOperation:要注册的操作执行前回调函数

  • PostOperation:要注册的操作执行后回调函数

来看一下操作前回调函数的原型

POB_PRE_OPERATION_CALLBACK PobPreOperationCallback;

OB_PREOP_CALLBACK_STATUS PobPreOperationCallback(
[in] PVOID RegistrationContext,
[in] POB_PRE_OPERATION_INFORMATION OperationInformation
)
  • RegistrationContext:前面的初始化中传入的RegistrationContext参数
  • OperationInformation:接收一个 OB_PRE_OPERATION_INFORMATION 结构作为参数,结构如下
typedef struct _OB_PRE_OPERATION_INFORMATION {
OB_OPERATION Operation;
union {
ULONG Flags;
struct {
ULONG KernelHandle : 1;
ULONG Reserved : 31;
};
};
PVOID Object;
POBJECT_TYPE ObjectType;
PVOID CallContext;
POB_PRE_OPERATION_PARAMETERS Parameters;
} OB_PRE_OPERATION_INFORMATION, *POB_PRE_OPERATION_INFORMATION;
  • Operation:触发回调的操作类型
  • KernelHandle:指定句柄是否为内核句柄
  • Object:指针指向句柄的实际对象,进程 -> EPROCESS地址、线程 -> PETHREAD地址
  • ObjectType:触发回调的对象类型
  • CallContext:被传递给操作后回调函数
  • Parameters:指明了基于操作的附加信息的联合,联合体定义如下
typedef union _OB_PRE_OPERATION_PARAMETERS {
OB_PRE_CREATE_HANDLE_INFORMATION CreateHandleInformation; // 创建 打开 操作
OB_PRE_DUPLICATE_HANDLE_INFORMATION DuplicateHandleInformation; // 复制 操作
} OB_PRE_OPERATION_PARAMETERS, *POB_PRE_OPERATION_PARAMETERS;

对于创建 打开 操作来说,会收到如下信息

typedef struct _OB_PRE_CREATE_HANDLE_INFORMATION {
ACCESS_MASK DesiredAccess;
ACCESS_MASK OriginalDesiredAccess;
} OB_PRE_CREATE_HANDLE_INFORMATION, *POB_PRE_CREATE_HANDLE_INFORMATION;
  • DesiredAccess:访问掩码,用于设置要授予句柄的访问权限,默认等于OriginalDesiredAccess
  • OriginalDesiredAccess:调用者设置的原始访问权限

对于复制 操作来说,会收到如下信息

typedef struct _OB_PRE_DUPLICATE_HANDLE_INFORMATION {
ACCESS_MASK DesiredAccess;
ACCESS_MASK OriginalDesiredAccess;
PVOID SourceProcess;
PVOID TargetProcess;
} OB_PRE_DUPLICATE_HANDLE_INFORMATION, *POB_PRE_DUPLICATE_HANDLE_INFORMATION;
  • DesiredAccess:访问掩码,用于设置要授予句柄的访问权限,默认等于OriginalDesiredAccess,可以通过修改此值来设定句柄的访问权限
  • OriginalDesiredAccess:调用者设置的原始访问权限
  • SourceProcess:句柄来源进程的进程对象指针
  • TargetProcess:句柄目标进程的进程对象指针

关于操作后回调函数是在句柄操作完成后被调用,所以操作后回调函数不能对句柄进行任何修改,只能用于查看结果查询信息,结构类似操作前回调

示例

给出一个进程保护的例子,通过设置DesiredAccess来实现保护某些进程不被强行终止

extern "C" NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath)
{
UNREFERENCED_PARAMETER(RegistryPath);
KdPrint(("[Y0ng] DriverEntry\n"));
...
// 对象回调函数
OB_OPERATION_REGISTRATION opRegs[] = { 0 };
OB_CALLBACK_REGISTRATION cbReg = { 0 };
opRegs[0].ObjectType = PsProcessType;
opRegs[0].Operations = OB_OPERATION_HANDLE_CREATE | OB_OPERATION_HANDLE_DUPLICATE;
opRegs[0].PreOperation = OnPreOpenProcessObj;
opRegs[0].PostOperation = NULL;

cbReg.Version = OB_FLT_REGISTRATION_VERSION;
cbReg.OperationRegistrationCount = 1;
cbReg.Altitude = RTL_CONSTANT_STRING(L"12345.6171");
cbReg.RegistrationContext = NULL;
cbReg.OperationRegistration = opRegs;

status = ObRegisterCallbacks(&cbReg, &g_Stats.RegHandle);
if (!NT_SUCCESS(status)) {
KdPrint(("[Y0ng] ObRegisterCallbacks failed: 0x%08X\n", status));
break;
}
SetObCallback = true;

...
return status;
}

OB_PREOP_CALLBACK_STATUS OnPreOpenProcessObj(PVOID RegistrationContext, POB_PRE_OPERATION_INFORMATION OperationInformation)
{
UNREFERENCED_PARAMETER(RegistrationContext);

if (OperationInformation == NULL)
return OB_PREOP_SUCCESS;

if(OperationInformation->KernelHandle)
return OB_PREOP_SUCCESS;

// 对象类型 只处理进程类型的句柄
if (OperationInformation->ObjectType == *PsProcessType) {

// 只保护notepad.exe进程
PUCHAR name = PsGetProcessImageFileName((PEPROCESS)OperationInformation->Object);
if (strcmp((const char*)name, "notepad.exe") != 0 ) {
return OB_PREOP_SUCCESS;
}

// 获取调用方 PID
HANDLE callerPid = PsGetCurrentProcessId();
// 获取目标 PID
HANDLE targetPid = PsGetProcessId((PEPROCESS)OperationInformation->Object);
// 目标访问权限
ACCESS_MASK desired = OperationInformation->Parameters->CreateHandleInformation.DesiredAccess;
KdPrint(("[Y0ng] caller=%llu target=%llu access=0x%08X\n", (ULONG64)(ULONG_PTR)callerPid, (ULONG64)(ULONG_PTR)targetPid, desired));

if (g_Stats.BlockEnabled) {
// 操作类型 句柄创建 和 句柄复制
if (OperationInformation->Operation == OB_OPERATION_HANDLE_CREATE)
{
OperationInformation->Parameters->CreateHandleInformation.DesiredAccess &= ~PROCESS_TERMINATE;
KdPrint(("[Y0ng] Protected CreateHandle!\n"));
}
if (OperationInformation->Operation == OB_OPERATION_HANDLE_DUPLICATE) {
OperationInformation->Parameters->DuplicateHandleInformation.DesiredAccess &= ~PROCESS_TERMINATE;
KdPrint(("[Y0ng] Protected DuplicateHandle!\n"));
}
}
}

return OB_PREOP_SUCCESS;
}

image-20260524193842767

但是发现通过任务管理器 - 进程 右键可以结束进程,搜索得到

通过任务管理器关闭进程,实际上有两种方式:

1.直接杀掉进程,调用系统API,TerminateProcess
2.结束任务,这种方式会发送一个WM_CLOSE的消息给程序,如果程序在一定时间内还没有退出的话,才会调用TerminateProcess

如果被保护的程序没有处理这个消息,或消息的处理方式是默认的话,就不会调用TerminateProcess,而是正常程序自身退出,所以如果你想保护自己的程序不被退出,你还需要在你的程序中处理WM_CLOSE这个消息

image-20260524193953135

全局回调双向链表

定位回调函数地址

通过反编译 ObRegisterCallbacks,从 cbReg.OperationRegistration 数组中 取值为 v13

image-20260525224525623

随后调用 ObpInsertCallbackByAltitude 根据函数名推测是将回调函数根据Altitude的值排序插入到内核对象中,向上追溯这两个参数

  • V17:v13的 ObjectType,即 PsProcessType、PsThreadType、ExDesktopObjectType
  • V16:某种结构体,包含了 LIST_ENTRY、回调前函数PreOperation、回调后函数PostOperation等

反编译 ObpInsertCallbackByAltitude

image-20260525230211417

看不懂问AI,在0xc8的偏移处插入了V16

image-20260525230350756

V17 - ObjectType 的结构类型为 _OBJECT_TYPE

//0xd8 bytes (sizeof)
struct _OBJECT_TYPE
{
struct _LIST_ENTRY TypeList; //0x0
struct _UNICODE_STRING Name; //0x10
VOID* DefaultObject; //0x20
UCHAR Index; //0x28
ULONG TotalNumberOfObjects; //0x2c
ULONG TotalNumberOfHandles; //0x30
ULONG HighWaterNumberOfObjects; //0x34
ULONG HighWaterNumberOfHandles; //0x38
struct _OBJECT_TYPE_INITIALIZER TypeInfo; //0x40
struct _EX_PUSH_LOCK TypeLock; //0xb8
ULONG Key; //0xc0
struct _LIST_ENTRY CallbackList; //0xc8
};

其中 CallbackList,是一个 _CALLBACK_ENTRY_ITEM 结构组成的双向链表,就是V16的结构

typedef struct _CALLBACK_ENTRY_ITEM {
LIST_ENTRY EntryItemList;
OB_OPERATION Operations;
CALLBACK_ENTRY* CallbackEntry;
POBJECT_TYPE ObjectType;
POB_PRE_OPERATION_CALLBACK PreOperation; //offset 0x28
POB_POST_OPERATION_CALLBACK PostOperation; //offset 0x30
}CALLBACK_ENTRY_ITEM;

那么回调函数的定位就需要通过 ObjectType(PsProcessType、PsThreadType、ExDesktopObjectType)

ObjectType.CallbackList -> *Flink -> _CALLBACK_ENTRY_ITEM.PreOperation
ObjectType.CallbackList -> *Flink -> _CALLBACK_ENTRY_ITEM.PostOperation

定位回调双向链表

PsProcessType、PsThreadType、ExDesktopObjectType 都是导出符号,直接定位它们地址就可以了

1: kd> dqs poi(poi(nt!PsProcessType)+0xc8)+0x28 L1
ffffa103`f272bc58 fffff802`80711e20
1: kd> ln fffff802`80711e20
Browse module
Set bu breakpoint
---------------------------------------------
1: kd> dqs poi(poi(poi(nt!PsProcessType)+0xc8))+0x28 L1
ffffa103`ff78fa68 fffff802`967ee170 WdFilter!MpCreateInstanceContext+0x50
1: kd> ln fffff802`967ee170
Browse module
Set bu breakpoint

(fffff802`967ee120) WdFilter!MpCreateInstanceContext+0x50 | (fffff802`967ef12c) WdFilter!MpInitFileStateGenericTable
---------------------------------------------
1: kd> dqs poi(poi(poi(poi(nt!PsProcessType)+0xc8)))+0x28 L1
ffffa103`fcc7e148 fffff802`967511b0 <Unloaded_CallBackDriver.sys>+0x11b0
1: kd> ln fffff802`967511b0
Browse module
Set bu breakpoint

(fffff802`967511b0) <Unloaded_CallBackDriver.sys>+0x11b0

image-20260526003214103

清除回调函数

进程、线程、映像三者类似,定位回调函数地址后直接将地址置空即可

注册表的回调直接删除会触发PG,所以利用双向链表操纵前后节点摘掉中间节点可以实现删除回调函数

对象的回调函数,可以直接将地址置空即可,也可以摘掉中间节点

对于对象,注册表还可以利用 Altitude回调高度的顺序性质在不破坏回调函数情况下使EDR回调失效:https://cloud.tencent.com/developer/article/2316142

代码直接参考大佬项目:https://github.com/myzxcg/RealBlindingEDR 。在AI如此发达的如今,代码已经不是问题了( 其实不会手搓 )

Refrence

From Windows drivers to a almost fully working EDR

白驱动 Kill AV/EDR(上)

白驱动 Kill AV/EDR(下)

AV/EDR 完全致盲 - 清除6大内核回调实现

Kernel Karnage

Mimidrv In Depth: Exploring Mimikatz’s Kernel Driver

Understanding Telemetry: Kernel Callbacks

内核驱动系列文章

《Windows 内核安全编程技术实践》

《Windows Kernel Programming》

《Evading EDR: The Definitive Guide to Defeating Endpoint Detection Systems》

MISC:

自写反内核工具(Anti RootKit)之回调枚举和进程线程枚举

Experimenting with Object Initializers in Windows – See PG-compliance Disclaimer

ObRegisterCallbacks 的装载和卸载

从进程终结到内核级防护:深度解析Windows进程保护机制与对抗技术