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反恶意软件接口
其中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 模板并创建项目
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)
如果自己添加一个数字签名,只开启测试签名模式就可以
捕获 KdPrint 输出还需要添加注册表项 DEFAULT 的值为 0000000f
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Debug Print Filter
开启DbgView并设置:
加载驱动程序:
#安装驱动 sc create DriverTest type= kernel binPath= "C:\Users\admin\Desktop\tt\sec\HelloWorldDriver.sys" #加载驱动 sc start DriverTest #卸载驱动 sc stop DriverTest
双机调试 内核的调试在被调试机器上,整个系统都会卡住,所以内核的调试需要两台机器,这里使用网络调试
被调试机器:
bcdedit /debug on bcdedit /dbgsettings net hostip:<调试机ip> port:50000 key:1.2.3.4 重启
调试机器:
顺利连接开始调试
内核通知回调例程 Windows 内核回调机制是内核暴露给驱动程序的通知/拦截框架,核心思想是 提前在内核中注册一个函数指针,在特定事件发生时内核会调用该函数 。简而言之,利用注册函数API将自定义回调函数注册到内核中,等待事件发生后,从事件发生到函数调用过程可以称为回调。
回调函数、回调例程函数、回调通知函数、通知回调例程 等都是同一个意思。
进程 用于注册或删除进程通知回调函数的注册函数API有
PsSetCreateProcessNotifyRoutine
PsSetCreateProcessNotifyRoutineEx:可以阻止进程创建
PsSetCreateProcessNotifyRoutineEx2:Windows 10 1607引入,拓展功能
PsSetCreateProcessNotifyRoutineEx 函数原型,限制最多注册64个回调函数
NTSTATUS PsSetCreateProcessNotifyRoutineEx( [in] PCREATE_PROCESS_NOTIFY_ROUTINE_EX NotifyRoutine, [in] BOOLEAN Remove );
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); } }
开启拦截
关闭拦截
全局回调数组 定位回调函数地址
在上面提到的回调函数列表实际上是一个64长度的全局数组(x64下),每当注册回调函数后,回调函数指针会被添加到 EX_FAST_REF 结构的数组中,实际上这些结构指针存储在 nt!PspCreateProcessNotifyRoutine 数组中。
从注册函数来看,nt!PsSetCreateProcessNotifyRoutineEx 调用了 nt!PspSetCreateProcessNotifyRoutine ,关于该函数的分析有很多文章,nt!PspSetCreateProcessNotifyRoutine
查看代码发现通过 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) 的结构类型。
那么通过数组反推回调函数真正的地址需要:
回调函数地址 = *(PspCreateProcessNotifyRoutine[i] & 0xFFFFFFFFFFFFFFF0) + 8
查看数组,有13个回调
dqs nt!PspCreateProcessNotifyRoutine
以第一个为例
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
与第三方工具比对地址类型正确
自定义驱动
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
给出定位函数
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
映像加载 用于注册映像加载时通知回调函数的注册函数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
注册表 用于注册访问、修改注册表时通知回调函数的注册函数API有
CmRegisterCallback
CmRegisterCallbackEx
用于删除访问、修改注册表时通知回调函数的函数API有
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 降序遍历链表找到插入点 -> 做双向链表四针插入 -> 释放锁
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偏移就能获取所有的回调函数地址
不断遍历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
对象 对象是对资源(例如文件、进程、令牌、注册表键)的一种抽象表示,内核通知支持的对象类型有 进程、线程、桌面 。用于在尝试打开或复制进程、线程、桌面的 句柄 时通知回调函数的注册函数API有
用于删除对象句柄通知回调函数的函数API有
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; }
但是发现通过任务管理器 - 进程 右键可以结束进程,搜索得到
通过任务管理器关闭进程,实际上有两种方式:
1.直接杀掉进程,调用系统API,TerminateProcess 2.结束任务,这种方式会发送一个WM_CLOSE的消息给程序,如果程序在一定时间内还没有退出的话,才会调用TerminateProcess
如果被保护的程序没有处理这个消息,或消息的处理方式是默认的话,就不会调用TerminateProcess,而是正常程序自身退出,所以如果你想保护自己的程序不被退出,你还需要在你的程序中处理WM_CLOSE这个消息
全局回调双向链表 定位回调函数地址
通过反编译 ObRegisterCallbacks,从 cbReg.OperationRegistration 数组中 取值为 v13
随后调用 ObpInsertCallbackByAltitude 根据函数名推测是将回调函数根据Altitude的值排序插入到内核对象中,向上追溯这两个参数
V17:v13的 ObjectType,即 PsProcessType、PsThreadType、ExDesktopObjectType
V16:某种结构体,包含了 LIST_ENTRY、回调前函数PreOperation、回调后函数PostOperation等
反编译 ObpInsertCallbackByAltitude
看不懂问AI,在0xc8的偏移处插入了V16
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
清除回调函数 进程、线程、映像三者类似,定位回调函数地址后直接将地址置空即可
注册表的回调直接删除会触发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进程保护机制与对抗技术