首页
社区
课程
招聘
[原创]获取SYS加载方源头进程的方案-基于ETW技术
发表于: 1天前 459

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

1天前
459

        前一个贴子给大家介绍了在内核中借助ALPC消息扫描技术,实现的DNS发起方的探查,很多朋友留言,表达了自己的见解,也提到了ETW技术,这里也算是做个结尾,再来介绍基于ETW技术的“驱动加载方exe回溯”的方法,如果上一个帖子的方案与这个帖子的方案结合,基本上DNS发起方源头探查与SYS加载发起方源头探查就都能覆盖了,当然,也有一些特殊手段实现的SYS加载,像是借壳加载这种,那需要其他的特殊手段(Section对象访问+DriverObject对象访问等)来实现监控了。废话不多说,我们来介绍技术流程。

        在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>


3. 从 Win32 API 到 services.exe:RPC 与 ALPC

    OpenSCManagerWCreateServiceWOpenServiceWStartServiceW 这些 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 的记录。
static void 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;
}




验证结果如图:




[招生]科锐逆向工程师培训(2026年7月3日实地,远程教学同时开班, 第56期)!

上传的附件:
收藏
免费 1
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回