文章总结: 本文介绍基于ETW技术获取SYS驱动加载发起方进程的方案。常规ImageLoad回调只能获取SystemPID,无法识别真实发起方。文章详细分析了应用层通过SCM和RPC/ALPC加载驱动的流程,并提出通过关联KernelImageLoad、ALPC和RPC事件,在时间窗口内溯源到services.exe的RPC客户端进程,从而确定加载源。提供了基于krabsetw库的代码实现,包括KernelTrace和UserTrace的联合采集与事件关联逻辑。 综合评分: 85 文章分类: 红队,渗透测试,安全工具,内网渗透,应急响应
获取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。
下面是应用层加载流程的简述:
- 应用层加载 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>
- 从 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. 根据服务名定位 HKLM\SYSTEM\CurrentControlSet\Services\<ServiceName>
2. 检查 Type/Start/ErrorControl/ImagePath 等配置;
3. 构造 Native 注册表路径:
\Registry\Machine\System\CurrentControlSet\Services\<ServiceName>
4. 调用 NtLoadDriver(RegistryPath),到达内核;
5. 内核读取服务项,解析 ImagePath;
6. 内核映射 .sys 镜像,创建 DRIVER_OBJECT;
7. 调用 DriverEntry(DriverObject, RegistryPath);
从内核角度看,ZwLoadDriver的参数不是普通DOS 文件路径,而是驱动服务注册表路径。 这也是为什么很多驱动加载流程必须先写入 Services注册表项,随后才能启动。
接下来就是ETW 的事件关联了,由于应用程序加载sys与ETW事件的获取严格意义上是“异步”的,所以需要借助事件与时间的关联来进行识别:
当捕获到 .sys Kernel ImageLoad事件时:
1. 取 image path 和时间戳 T2
2. 在 T2 前的短时间窗口内查找 services.exe 相关 SCM RPC/ALPC 请求
3. 优先匹配 StartService/OpenService/CreateService 语义
4. 若能拿到服务名,则解析 Services\Name\ImagePath 与 image path 归一化比较
5. 输出 RPC/ALPC 客户端进程作为 loader source
6. 若找不到 SCM 链路,再降级输出 System/Unknown,并标记为“未完成源头关联”
根据前面一大堆啰嗦的流程描述,下面开始看代码:
【为了提高开发效率,这里的ETW解析库直接使用了开源项目“krabsetw”编译出来的静态库“etwtracelib.lib”】
我们在“etwtracelib”项目中增加几个关心的ETW回调,并创建一个lib的导出函数“EtwCopyStartTrace”供exe使用,核心代码如下:
// 导出函数入口。函数内部同时启动 Kernel Trace 和 User
// Trace,前者采集 ImageLoad/Process/ALPC,后者采集 RPC。该函数不会主动
// 返回,调用方应放到工作线程里运行。
extern "C" void EtwCopyStartTrace()
{
try
{
krabs::kernel_trace kernelTrace(L"ETWMonitor-Kernel");
krabs::user_trace rpcTrace(L"ETWMonitor-RPC");
krabs::kernel::image_load_provider imageLoadProvider;
krabs::kernel::process_provider processProvider;
krabs::kernel::alpc_provider alpcProvider;
krabs::provider<> rpcProvider(L"Microsoft-Windows-RPC");
//******** 下面是几个关键的ETW追踪函数,实现几个事件的联合跟踪,这几个函数的细节代码篇幅过大,就不贴了,大家可以下载代码自己看 *******
EtwCopySetupImageLoadProvider(imageLoadProvider);
EtwCopySetupProcessProvider(processProvider);
EtwCopySetupAlpcProvider(alpcProvider);
EtwCopySetupRpcProvider(rpcProvider);
// Kernel providers can share one kernel trace. The RPC provider is a
// normal user-mode provider and must be enabled on a user trace.
kernelTrace.enable(imageLoadProvider);
kernelTrace.enable(processProvider);
kernelTrace.enable(alpcProvider);
rpcTrace.enable(rpcProvider);
// krabs::trace::start is blocking, so each trace runs on its own
// thread. The outer loop intentionally keeps the library alive.
std::thread kernelThread([&kernelTrace]()
{
try
{
kernelTrace.start();
}
catch (const std::runtime_error& error)
{
std::cerr << error.what() << std::endl;
kernelTrace.stop();
}
});
std::thread rpcThread([&rpcTrace]()
{
try
{
rpcTrace.start();
}
catch (const std::runtime_error& error)
{
std::cerr << error.what() << std::endl;
rpcTrace.stop();
}
});
while (true)
{
Sleep(1000);
}
kernelTrace.stop();
rpcTrace.stop();
kernelThread.join();
rpcThread.join();
}
catch (const std::runtime_error& error)
{
std::cerr << error.what() << std::endl;
}
}
下面是调用lib的exe测试程序的代码:
/*
* ETWMonitor 测试程序。
*
* 设计说明:
* 该程序只负责注册回调、启动 etwtracelib 中的 ETW 采集逻辑,并打印
* SYS 加载结果。RPC/ALPC 溯源、路径归一化、ImageLoad 解析都放在
* etwtracelib 副本库中实现。
*
*/
#include <Windows.h>
#include <cstdio>
#include <thread>
#if defined(_M_X64) && defined(NDEBUG)
#pragma comment(lib, "etwtracelib.lib")
#elif defined(_M_X64)
#pragma comment(lib, "etwtracelib.lib")
#else
#pragma comment(lib, "etwtracelib.lib")
#endif
// 必须和 etwtracelib 副本里的 IMAGE_LOAD_OPERATE 保持二进制兼容。
// iTypeId == 4 时,cBuf 指向该结构;如果库侧字段布局变化,这里也要同步更新。
typedef struct _IMAGE_LOAD_OPERATE
{
ULONG ulLoaderPid; // 修正后的加载发起方 PID;SYS 场景下通常来自 RPC/ALPC 溯源。
ULONG ulTargetPid; // 镜像加载目标进程 PID;SYS 通常没有普通用户态目标进程。
ULONG ulImageType; // 1 表示 DLL,2 表示 SYS。
char cLoaderPath[1024]; // 修正后的加载发起方进程路径。
char cTargetProcessPath[1024]; // 镜像加载目标进程路径。
char cImagePath[1024]; // 被加载的 DLL/SYS 镜像路径。
}
IMAGE_LOAD_OPERATE, * PIMAGE_LOAD_OPERATE;
typedef void(*FUNC_DoCmd)(int iTypeId, char* cBuf, int ilen);
// 静态库对外暴露的新入口名。副本故意与原始工程不同,便于区分符号。
extern "C" void EtwCopyStartTrace();
extern "C" void EtwCopySetCallback(FUNC_DoCmd pFunc);
// 库回调函数。库侧会同时上报 DLL/SYS,但当前 exe 只验证 SYS 加载溯源,
// 因此这里再过滤一次,只打印 ulImageType == 2 的记录。
staticvoid EtwCopyOnEtwImageEvent(int iTypeId, char* cBuf, int ilen){
// iTypeId 4 对应 IMAGE_LOAD_OPERATE。长度检查用于防止库/测试程序版本
// 不一致时 memcpy 越界或解析错位。
if (iTypeId != 4 || ilen != sizeof(IMAGE_LOAD_OPERATE))
{
return;
}
IMAGE_LOAD_OPERATE imageInfo = { 0 };
memcpy(&imageInfo, cBuf, sizeof(imageInfo));
if (imageInfo.ulImageType != 2)
{
// 控制台暂时只输出 SYS;DLL 事件仍然由库侧采集,只是在测试程序中过滤。
return;
}
printf("IMAGE[SYS] loaderPid:%lu loader:%s targetPid:%lu target:%s image:%s\n",
imageInfo.ulLoaderPid,
imageInfo.cLoaderPath,
imageInfo.ulTargetPid,
imageInfo.cTargetProcessPath,
imageInfo.cImagePath);
}
int main(){
// 先注册回调,再启动 ETW trace;否则早期 ImageLoad 事件可能已经被采集,
// 但由于没有回调而无法交给测试程序打印。
EtwCopySetCallback(EtwCopyOnEtwImageEvent);
// EtwCopyStartTrace 是阻塞式监控循环,所以放到工作线程中运行。
// 主线程 join 等待,使该程序表现为一个持续运行的控制台监控器。
std::thread traceThread([]()
{
EtwCopyStartTrace();
});
traceThread.join();
return 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技术》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。








评论