获取SYS加载方源头进程的方案-基于ETW技术

admin 2026-07-10 05:49:13 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文介绍基于ETW技术获取SYS驱动加载发起方进程的方案。常规ImageLoad回调只能获取SystemPID,无法识别真实发起方。文章详细分析了应用层通过SCM和RPC/ALPC加载驱动的流程,并提出通过关联KernelImageLoad、ALPC和RPC事件,在时间窗口内溯源到services.exe的RPC客户端进程,从而确定加载源。提供了基于krabsetw库的代码实现,包括KernelTrace和UserTrace的联合采集与事件关联逻辑。 综合评分: 85 文章分类: 红队,渗透测试,安全工具,内网渗透,应急响应


cover_image

获取SYS加载方源头进程的方案-基于ETW技术

沧海浮萍_ 沧海浮萍_

看雪学苑

2026年7月9日 17:59 上海

在小说阅读器读本章

去阅读

在dll/sys的加载中,常规手段上有两种方法(当然还有其他的方法)实现这个过程的感知,一个是通过IMG镜像回调,另一个是通过MF文件过滤器的IRP_MJ_ACQUIRE_FOR_SECTION_SYNCHRONIZATION 这个IRP,这两个方法都能发现镜像加载的过程。

对于dll来说,加载宿主的PID就在参数中,但是对于SYS驱动来说,获取到的PID基本就是System(4)这个进程了。

但在EDR或者其他安全产品中,我们始终需要想办法找到“发起方”,这个时候通过参数里的信息就不能满足了,需要借助其他辅助信息旁路来获取对应的宿主信息(当然不能说绝对,借助InfinityHook也是可以一步到位的),这里介绍的就是ETW。

通过ETW是如何拿到这个SYS加载的发起方呢?这里就需要简单介绍一下SYS在应用层常规的加载技术:

常见的驱动加载工具(例如 sc.exe、InstDrv、自研 loader)并不是直接把 SYS 映射进内核。 它们通常调用 OpenSCManagerW、CreateServiceW/OpenServiceW、StartServiceW, 通过本地 RPC/ALPC 把请求发送给 services.exe。随后由 SCM 在 services.exe 中完成驱动服务启动逻辑, 并调用 NtLoadDriver 进入内核。因此,ImageLoad 事件里看到 System 并不代表真实发起者就是 System。

下面是应用层加载流程的简述:

  1. 应用层加载 SYS 的典型 API 链:应用层加载驱动最常见的方式是把驱动注册成一个“驱动服务”。

核心 API 链如下:

OpenSCManagerW()
    -> CreateServiceW(..., SERVICE_KERNEL_DRIVER, ..., ImagePath, ...)
       或 OpenServiceW()
    -> StartServiceW()
    -> CloseServiceHandle()
典型代码形态如下:

SC_HANDLE scm = OpenSCManagerW(
    NULL,
    NULL,
    SC_MANAGER_CONNECT | SC_MANAGER_CREATE_SERVICE);

SC_HANDLE svc = CreateServiceW(
    scm,
    L"EDRSim",
    L"EDRSim",
    SERVICE_START | DELETE | SERVICE_STOP,
    SERVICE_KERNEL_DRIVER,
    SERVICE_DEMAND_START,
    SERVICE_ERROR_NORMAL,
    L"C:\\Users\\Administrator\\Desktop\\bin\\EDRSim.sys",
    NULL,
    NULL,
    NULL,
    NULL,
    NULL);

StartServiceW(svc, 0, NULL);

API说明:

2.CreateServiceW 写入的服务注册表结构:CreateServiceW 会在 SCM 数据库中创建服务对象。对本机系统而言,服务配置最终落在注册表:

HKLM\SYSTEM\CurrentControlSet\Services\<ServiceName>

对于驱动服务,常见字段如下:

当后续调用StartServiceW时,用户态传递的是服务名,而不是直接把SYS 文件交给内核。 SCM 会把服务名解析到上述注册表路径,再构造 Native API 所需的驱动服务路径:

\Registry\Machine\System\CurrentControlSet\Services\<ServiceName>
  1. 从 Win32 API 到 services.exe:RPC 与 ALPC

OpenSCManagerW、CreateServiceW、OpenServiceW、StartServiceW这些Win32 API 暴露在 advapi32.dll中。应用程序调用它们时,实际并不会在当前进程内直接执行服务数据库修改和驱动加载。这些请求会被转换成发往SCM 服务进程 services.exe的RPC 调用。

本机场景通常使用本地 RPC 协议序列 ncalrpc。从 Win32 API 视角看,这是 RPC runtime 的内部细节; 从 ETW 视角看,本地 RPC 在现代 Windows 中会落到 LPC/ALPC 通信上,因此可以通过 Kernel ALPC 相关事件观察到.

4.链路拓扑

5.services.exe 内部如何触发 NtLoadDriver

services.exe 收到 StartServiceW 对应的 SCM RPC 请求后,会检查服务配置。 如果服务类型是 SERVICE_KERNEL_DRIVER 或 SERVICE_FILE_SYSTEM_DRIVER,它不会像普通 Win32 服务那样创建用户态服务进程, 而是进入驱动服务启动路径。

核心动作可以概括为:

1.&nbsp;根据服务名定位 HKLM\SYSTEM\CurrentControlSet\Services\<ServiceName>
2.&nbsp;检查 Type/Start/ErrorControl/ImagePath 等配置;
3.&nbsp;构造 Native 注册表路径:
&nbsp; &nbsp;\Registry\Machine\System\CurrentControlSet\Services\<ServiceName>
4.&nbsp;调用 NtLoadDriver(RegistryPath),到达内核;
5.&nbsp;内核读取服务项,解析 ImagePath;
6.&nbsp;内核映射 .sys 镜像,创建 DRIVER_OBJECT;
7. 调用 DriverEntry(DriverObject, RegistryPath);

从内核角度看,ZwLoadDriver的参数不是普通DOS 文件路径,而是驱动服务注册表路径。 这也是为什么很多驱动加载流程必须先写入 Services注册表项,随后才能启动。

接下来就是ETW   的事件关联了,由于应用程序加载sys与ETW事件的获取严格意义上是“异步”的,所以需要借助事件与时间的关联来进行识别:

当捕获到 .sys Kernel ImageLoad事件时:
&nbsp; &nbsp;&nbsp;1.&nbsp;取 image&nbsp;path&nbsp;和时间戳 T2
&nbsp; &nbsp;&nbsp;2.&nbsp;在 T2 前的短时间窗口内查找 services.exe 相关 SCM RPC/ALPC 请求
&nbsp; &nbsp;&nbsp;3.&nbsp;优先匹配 StartService/OpenService/CreateService 语义
&nbsp; &nbsp;&nbsp;4.&nbsp;若能拿到服务名,则解析 Services\Name\ImagePath 与 image&nbsp;path&nbsp;归一化比较
&nbsp; &nbsp;&nbsp;5.&nbsp;输出 RPC/ALPC 客户端进程作为 loader source
&nbsp; &nbsp;&nbsp;6.&nbsp;若找不到 SCM 链路,再降级输出 System/Unknown,并标记为“未完成源头关联”

根据前面一大堆啰嗦的流程描述,下面开始看代码:

【为了提高开发效率,这里的ETW解析库直接使用了开源项目“krabsetw”编译出来的静态库“etwtracelib.lib”】

我们在“etwtracelib”项目中增加几个关心的ETW回调,并创建一个lib的导出函数“EtwCopyStartTrace”供exe使用,核心代码如下:

// 导出函数入口。函数内部同时启动 Kernel Trace 和 User
// Trace,前者采集 ImageLoad/Process/ALPC,后者采集 RPC。该函数不会主动
// 返回,调用方应放到工作线程里运行。
extern "C" void&nbsp;EtwCopyStartTrace()
{
&nbsp; &nbsp; try
&nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; krabs::kernel_trace&nbsp;kernelTrace(L"ETWMonitor-Kernel");
&nbsp; &nbsp; &nbsp; &nbsp; krabs::user_trace&nbsp;rpcTrace(L"ETWMonitor-RPC");
&nbsp; &nbsp; &nbsp; &nbsp; krabs::kernel::image_load_provider imageLoadProvider;
&nbsp; &nbsp; &nbsp; &nbsp; krabs::kernel::process_provider processProvider;
&nbsp; &nbsp; &nbsp; &nbsp; krabs::kernel::alpc_provider alpcProvider;
&nbsp; &nbsp; &nbsp; &nbsp; krabs::provider<>&nbsp;rpcProvider(L"Microsoft-Windows-RPC");

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;//******** 下面是几个关键的ETW追踪函数,实现几个事件的联合跟踪,这几个函数的细节代码篇幅过大,就不贴了,大家可以下载代码自己看 *******
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;EtwCopySetupImageLoadProvider(imageLoadProvider);
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;EtwCopySetupProcessProvider(processProvider);
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;EtwCopySetupAlpcProvider(alpcProvider);
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;EtwCopySetupRpcProvider(rpcProvider);

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// Kernel providers can share one kernel trace. The RPC provider is a
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// normal user-mode provider and must be enabled on a user trace.
&nbsp; &nbsp; &nbsp; &nbsp; kernelTrace.enable(imageLoadProvider);
&nbsp; &nbsp; &nbsp; &nbsp; kernelTrace.enable(processProvider);
&nbsp; &nbsp; &nbsp; &nbsp; kernelTrace.enable(alpcProvider);
&nbsp; &nbsp; &nbsp; &nbsp; rpcTrace.enable(rpcProvider);
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// krabs::trace::start is blocking, so each trace runs on its own
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// thread. The outer loop intentionally keeps the library alive.
&nbsp; &nbsp; &nbsp; &nbsp; std::thread&nbsp;kernelThread([&kernelTrace]()
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; try
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; kernelTrace.start();
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; catch (const std::runtime_error& error)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; std::cerr << error.what() << std::endl;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; kernelTrace.stop();
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; });
&nbsp; &nbsp; &nbsp; &nbsp; std::thread&nbsp;rpcThread([&rpcTrace]()
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; try
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; rpcTrace.start();
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; catch (const std::runtime_error& error)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; std::cerr << error.what() << std::endl;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; rpcTrace.stop();
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; });
&nbsp; &nbsp; &nbsp; &nbsp; while (true)
&nbsp; &nbsp; &nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;Sleep(1000);
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; kernelTrace.stop();
&nbsp; &nbsp; &nbsp; &nbsp; rpcTrace.stop();
&nbsp; &nbsp; &nbsp; &nbsp; kernelThread.join();
&nbsp; &nbsp; &nbsp; &nbsp; rpcThread.join();
&nbsp; &nbsp; }
&nbsp; &nbsp; catch (const std::runtime_error& error)
&nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; std::cerr << error.what() << std::endl;
&nbsp; &nbsp; }
}

下面是调用lib的exe测试程序的代码:

/*
&nbsp;* ETWMonitor 测试程序。
&nbsp;*
&nbsp;* 设计说明:
&nbsp;* &nbsp; 该程序只负责注册回调、启动 etwtracelib 中的 ETW 采集逻辑,并打印
&nbsp;* &nbsp; SYS 加载结果。RPC/ALPC 溯源、路径归一化、ImageLoad 解析都放在
&nbsp;* &nbsp; etwtracelib 副本库中实现。
&nbsp;*
&nbsp;*/
#include&nbsp;<Windows.h>
#include&nbsp;<cstdio>
#include&nbsp;<thread>
#if&nbsp;defined(_M_X64) && defined(NDEBUG)
#pragma&nbsp;comment(lib,&nbsp;"etwtracelib.lib")
#elif&nbsp;defined(_M_X64)
#pragma&nbsp;comment(lib,&nbsp;"etwtracelib.lib")
#else
#pragma&nbsp;comment(lib,&nbsp;"etwtracelib.lib")
#endif
// 必须和 etwtracelib 副本里的 IMAGE_LOAD_OPERATE 保持二进制兼容。
// iTypeId == 4 时,cBuf 指向该结构;如果库侧字段布局变化,这里也要同步更新。
typedef&nbsp;struct&nbsp;_IMAGE_LOAD_OPERATE
{
&nbsp; &nbsp; ULONG ulLoaderPid; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// 修正后的加载发起方 PID;SYS 场景下通常来自 RPC/ALPC 溯源。
&nbsp; &nbsp; ULONG ulTargetPid; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// 镜像加载目标进程 PID;SYS 通常没有普通用户态目标进程。
&nbsp; &nbsp; ULONG ulImageType; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// 1 表示 DLL,2 表示 SYS。
&nbsp; &nbsp;&nbsp;char&nbsp;cLoaderPath[1024]; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 修正后的加载发起方进程路径。
&nbsp; &nbsp;&nbsp;char&nbsp;cTargetProcessPath[1024]; &nbsp;// 镜像加载目标进程路径。
&nbsp; &nbsp;&nbsp;char&nbsp;cImagePath[1024]; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// 被加载的 DLL/SYS 镜像路径。
}
IMAGE_LOAD_OPERATE, * PIMAGE_LOAD_OPERATE;
typedef&nbsp;void(*FUNC_DoCmd)(int&nbsp;iTypeId,&nbsp;char* cBuf,&nbsp;int&nbsp;ilen);

// 静态库对外暴露的新入口名。副本故意与原始工程不同,便于区分符号。
extern&nbsp;"C"&nbsp;void&nbsp;EtwCopyStartTrace();
extern&nbsp;"C"&nbsp;void&nbsp;EtwCopySetCallback(FUNC_DoCmd pFunc);

// 库回调函数。库侧会同时上报 DLL/SYS,但当前 exe 只验证 SYS 加载溯源,
// 因此这里再过滤一次,只打印 ulImageType == 2 的记录。
staticvoid&nbsp;EtwCopyOnEtwImageEvent(int&nbsp;iTypeId,&nbsp;char* cBuf,&nbsp;int&nbsp;ilen){
&nbsp; &nbsp;&nbsp;// iTypeId 4 对应 IMAGE_LOAD_OPERATE。长度检查用于防止库/测试程序版本
&nbsp; &nbsp;&nbsp;// 不一致时 memcpy 越界或解析错位。
&nbsp; &nbsp;&nbsp;if&nbsp;(iTypeId !=&nbsp;4&nbsp;|| ilen !=&nbsp;sizeof(IMAGE_LOAD_OPERATE))
&nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return;
&nbsp; &nbsp; }
&nbsp; &nbsp; IMAGE_LOAD_OPERATE imageInfo = {&nbsp;0&nbsp;};
&nbsp; &nbsp;&nbsp;memcpy(&imageInfo, cBuf,&nbsp;sizeof(imageInfo));
&nbsp; &nbsp;&nbsp;if&nbsp;(imageInfo.ulImageType !=&nbsp;2)
&nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 控制台暂时只输出 SYS;DLL 事件仍然由库侧采集,只是在测试程序中过滤。
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return;
&nbsp; &nbsp; }
&nbsp; &nbsp;&nbsp;printf("IMAGE[SYS] loaderPid:%lu loader:%s targetPid:%lu target:%s image:%s\n",
&nbsp; &nbsp; &nbsp; &nbsp; imageInfo.ulLoaderPid,
&nbsp; &nbsp; &nbsp; &nbsp; imageInfo.cLoaderPath,
&nbsp; &nbsp; &nbsp; &nbsp; imageInfo.ulTargetPid,
&nbsp; &nbsp; &nbsp; &nbsp; imageInfo.cTargetProcessPath,
&nbsp; &nbsp; &nbsp; &nbsp; imageInfo.cImagePath);
}
int&nbsp;main(){
&nbsp; &nbsp;&nbsp;// 先注册回调,再启动 ETW trace;否则早期 ImageLoad 事件可能已经被采集,
&nbsp; &nbsp;&nbsp;// 但由于没有回调而无法交给测试程序打印。
&nbsp; &nbsp;&nbsp;EtwCopySetCallback(EtwCopyOnEtwImageEvent);
&nbsp; &nbsp;&nbsp;// EtwCopyStartTrace 是阻塞式监控循环,所以放到工作线程中运行。
&nbsp; &nbsp;&nbsp;// 主线程 join 等待,使该程序表现为一个持续运行的控制台监控器。
&nbsp; &nbsp;&nbsp;std::thread&nbsp;traceThread([]()
&nbsp; &nbsp; &nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; EtwCopyStartTrace();
&nbsp; &nbsp; &nbsp; &nbsp; });
&nbsp; &nbsp; traceThread.join();
&nbsp; &nbsp;&nbsp;return&nbsp;0;
}

验证结果如图:

#

看雪ID:沧海浮萍_

https://bbs.kanxue.com/user-home-804146.htm

*本文为看雪论坛优秀文章,由 沧海浮萍_ 原创,转载请注明来自看雪社区

第十届安全开发者峰会【议题征集】-欢迎投稿

往期推荐

libmsaoaidsec.so 检测监测——绕过 Hook 脚本

Android 逆向 Multi-Agent 系统设计

整理Windows 全架构 Hook 技术图谱:从 Ring3 到固件层 34 种实现

libmsaoaidsec.so 检测体系分析

从0到1构建一个Hook工具之Frida-like风格的Hook

球分享

球点赞

球在看

点击阅读原文查看更多


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:看雪学苑 沧海浮萍_ 沧海浮萍_《获取SYS加载方源头进程的方案-基于ETW技术》

评论:0   参与:  0