阅读时间: 3-4 分钟
前置知识: 理解进程注入原理、Shellcode 绕过技巧、Windows API 基础
学习目标: 掌握 ETW 事件追踪,实现行为层检测
📺 配套视频教程
本文配套视频教程已发布在 B 站,建议结合视频学习效果更佳:
💡 提示: 点击视频右下角可全屏观看,建议配合文章食用!
视频链接: https://www.bilibili.com/video/BV1oU4gzCEUj/
承接上节
上节我们确认了一件事
只看起始地址会被 Shellcode 绕过
也留下了三个关键问题
- 为什么突然出现新线程?
- 谁创建了这个线程?
- 正常程序会这样做吗?
本节给出直接答案
从”谁创建了线程”入手
用 ETW 还原”发起进程 → 目标进程”的创建关系
做一次可落地的行为检测
本节目标与策略
目标很明确:
不看线程从哪里开始
只看是谁创建了它
策略很简单:
- 订阅线程创建事件
- 拿到创建者 PID 与目标 PID
- 不相等就是远程线程
不关心起始地址是不是 LoadLibrary
Shellcode 中转同样会被覆盖
这是一种行为检测
比地址特征更本质
也更难被简单绕过
什么是 ETW?
ETW,全称 Event Tracing for Windows
系统级事件追踪
性能开销小,覆盖面广
在安全产品与反作弊场景里
ETW 是常规的数据采集通道
用于还原进程间的行为链
常见监控维度包括:
- 线程/进程事件(创建、退出)
- 句柄打开与跨进程访问(OpenProcess/OpenThread)
- 内存分配与权限变化(VirtualAllocEx/VirtualProtectEx)
- 模块加载与映像校验(LoadLibrary/映像路径)
本节聚焦”线程创建”这一环
后续可叠加模块与内存画像,降低误报
实现步骤总览
启动内核会话(
KERNEL_LOGGER_NAME)- 准备
PROPERTIES结构 - 填充会话名和缓冲区
- 调用
StartTrace启动
- 准备
只启用线程事件(
EVENT_TRACE_FLAG_THREAD)- 设置
EnableFlags - 减少无关数据
- 提升监听效率
- 设置
打开会话,注册事件回调
- 配置
LOGFILEW结构 - 指定
EventRecordCallback - 调
OpenTrace获取句柄 - 用
ProcessTrace阻塞读取
- 配置
在回调里筛选”线程启动”事件
- 检查
Opcode == START - 只处理创建那一刻
- 跳过就绪和退出
- 检查
读取关键字段,判断是否远程线程
- 用 TDH 解析属性
- 提取创建者和目标 PID
- 判定:创建者 ≠ 目标
- 打印远程线程信息
关键代码讲解
第1步:从 main 函数出发
先把主流程写出来
让程序能跑起来
再逐步补齐依赖
int main()
{
std::cout << "=== 远程线程创建行为检测 ===nn";
std::cout << "请输入需要监控的目标进程 PID (0 表示所有进程): ";
DWORD pid = 0; std::cin >> pid;
if (!StartMonitorSession(pid)) {
std::cout << "初始化 ETW 监听失败(需要管理员权限)n";
system("pause");
return 1;
}
std::cout << "开始监听...n";
ProcessTrace(&g_Context.traceHandle, 1, nullptr, nullptr);
StopMonitorSession();
std::cout << "监听结束n";
system("pause");
return 0;
}主流程三步走:
- 启动 ETW 内核会话,只订阅线程事件,把事件回调绑定好
- 阻塞读取事件流;每条事件都会进入回调
- 先关读取,再停会话,资源收干净
第2步:补齐必要的包含与全局
#include <windows.h>
#include <evntrace.h>
#include <tdh.h>
#include <iostream>
#include <memory>
#pragma comment(lib, "tdh.lib")
#pragma comment(lib, "advapi32.lib")
static const GUID SystemTraceControlGuid = { 0x9e814aad, 0x3204, 0x11d2, { 0x9a, 0x82, 0x00, 0x60, 0x08, 0xa8, 0x69, 0x39 } };
struct MonitorContext {
TRACEHANDLE sessionHandle;
TRACEHANDLE traceHandle;
DWORD targetPid;
EVENT_TRACE_PROPERTIES* properties;
std::unique_ptr<BYTE[]> propertyBuffer;
};
static MonitorContext g_Context = {};第3步:准备会话属性并启动会话
准备缓冲区:
g_Context.targetPid = targetPid;
std::wstring name = KERNEL_LOGGER_NAME;
size_t bytes = sizeof(EVENT_TRACE_PROPERTIES) + (name.size() + 1) * sizeof(wchar_t);
g_Context.propertyBuffer = std::make_unique<BYTE[]>(bytes);
ZeroMemory(g_Context.propertyBuffer.get(), bytes);
g_Context.properties = reinterpret_cast<EVENT_TRACE_PROPERTIES*>(g_Context.propertyBuffer.get());填充属性:
g_Context.properties->Wnode.BufferSize = static_cast<ULONG>(bytes);
g_Context.properties->Wnode.Flags = WNODE_FLAG_TRACED_GUID;
g_Context.properties->Wnode.Guid = SystemTraceControlGuid;
g_Context.properties->Wnode.ClientContext = 1;
g_Context.properties->LogFileMode = EVENT_TRACE_REAL_TIME_MODE;
g_Context.properties->EnableFlags = EVENT_TRACE_FLAG_THREAD;
g_Context.properties->LoggerNameOffset = sizeof(EVENT_TRACE_PROPERTIES);
LPWSTR loggerName = reinterpret_cast<LPWSTR>(g_Context.propertyBuffer.get() + sizeof(EVENT_TRACE_PROPERTIES));
wcscpy_s(loggerName, name.size() + 1, name.c_str());启动会话:
StopTrace(0, name.c_str(), g_Context.properties);
ULONG s = StartTrace(&g_Context.sessionHandle, name.c_str(), g_Context.properties);
if (s != ERROR_SUCCESS) {
std::cout << "StartTrace 失败 错误码: " << s << "n";
return false;
}
return true;第4步:打开追踪并注册回调
EVENT_TRACE_LOGFILEW log = {};
log.LoggerName = const_cast<LPWSTR>(name.c_str());
log.ProcessTraceMode = PROCESS_TRACE_MODE_EVENT_RECORD | PROCESS_TRACE_MODE_REAL_TIME;
log.EventRecordCallback = ThreadEventCallback;
g_Context.traceHandle = OpenTrace(&log);
if (g_Context.traceHandle == reinterpret_cast<TRACEHANDLE>(INVALID_HANDLE_VALUE)) {
std::cout << "OpenTrace 失败n";
StopTrace(g_Context.sessionHandle, name.c_str(), g_Context.properties);
return false;
}
return true;第5步:实现回调,判定远程线程
第一步:过滤事件,只处理 START
void WINAPI ThreadEventCallback(PEVENT_RECORD eventRecord)
{
if (!eventRecord) return;
if (eventRecord->EventHeader.EventDescriptor.Opcode != EVENT_TRACE_TYPE_START) return;第二步:提取字段,获取关键数据
DWORD creatorPid = eventRecord->EventHeader.ProcessId;
DWORD targetPid = 0;
DWORD threadId = 0;
ULONGLONG startAddr = 0;
GetEventProperty(eventRecord, L"ProcessId", targetPid) ||
GetEventProperty(eventRecord, L"TargetProcessId", targetPid);
GetEventProperty(eventRecord, L"ThreadId", threadId) ||
GetEventProperty(eventRecord, L"NewThreadId", threadId);
GetEventProperty(eventRecord, L"StartAddress", startAddr) ||
GetEventProperty(eventRecord, L"Win32StartAddr", startAddr);第三步:判定输出,识别远程线程
if (creatorPid != 0 && targetPid != 0 && creatorPid != targetPid) {
if (g_Context.targetPid != 0 && targetPid != g_Context.targetPid) return;
std::cout << "[!] 检测到远程线程创建n"
<< " 发起进程 PID: " << creatorPid << "n"
<< " 目标进程 PID: " << targetPid << "n"
<< " 线程 ID: " << threadId << "n"
<< std::hex << " 起始地址: 0x" << startAddr << std::dec << "n"
<< "========================n";
}
}第6步:补齐 TDH 属性读取函数
template<typename T>
bool GetEventProperty(PEVENT_RECORD eventRecord, LPCWSTR propertyName, T& value)
{
PROPERTY_DATA_DESCRIPTOR property = {};
property.PropertyName = reinterpret_cast<ULONGLONG>(propertyName);
property.ArrayIndex = ULONG_MAX;
ULONG size = 0;
if (TdhGetPropertySize(eventRecord, 0, nullptr, 1, &property, &size) != ERROR_SUCCESS) return false;
std::vector<BYTE> buffer(size);
if (TdhGetProperty(eventRecord, 0, nullptr, 1, &property, size, buffer.data()) != ERROR_SUCCESS) return false;
value = *reinterpret_cast<T*>(buffer.data());
return true;
}第7步:完善清理函数
void StopMonitorSession()
{
if (g_Context.traceHandle != 0) CloseTrace(g_Context.traceHandle);
if (g_Context.sessionHandle != 0) StopTrace(g_Context.sessionHandle, KERNEL_LOGGER_NAME, g_Context.properties);
}完整检测代码
/**
* @file 03-防御篇-检测远程线程创建行为.cpp
* @brief 基于 ETW 的远程线程创建行为检测工具
* @details 使用 Windows ETW (Event Tracing for Windows) 技术监控系统级别的线程创建事件
* 可以检测到包括 Shellcode 注入在内的所有远程线程创建行为
* @author 教学示例
* @date 2025
* @note 需要管理员权限运行
*/
#include <windows.h>
#include <evntrace.h>
#include <tdh.h>
#include <iostream>
#include <vector>
#include <memory>
#pragma comment(lib, "tdh.lib")
#pragma comment(lib, "advapi32.lib")
/**
* @brief Windows 系统跟踪控制 GUID
* @details 用于启动内核级别的事件跟踪会话
*/
static const GUID SystemTraceControlGuid = { 0x9e814aad, 0x3204, 0x11d2, { 0x9a, 0x82, 0x00, 0x60, 0x08, 0xa8, 0x69, 0x39 } };
/**
* @struct MonitorContext
* @brief ETW 监控上下文结构体
* @details 保存 ETW 会话的所有必要信息
*/
struct MonitorContext {
TRACEHANDLE sessionHandle; ///< ETW 会话句柄
TRACEHANDLE traceHandle; ///< ETW 跟踪句柄
DWORD targetPid; ///< 目标进程 PID(0 表示监控所有进程)
EVENT_TRACE_PROPERTIES* properties; ///< ETW 会话属性指针
std::unique_ptr<BYTE[]> propertyBuffer; ///< 属性缓冲区(包含会话名称)
};
/**
* @brief 全局监控上下文
*/
static MonitorContext g_Context = {};
void StopMonitorSession(); // 前向声明,供控制台回调调用
/**
* @brief 处理控制台信号,确保 Ctrl+C 等方式退出时清理 ETW 会话
*/
BOOL WINAPI ConsoleCtrlHandler(DWORD ctrlType) {
switch (ctrlType) {
case CTRL_C_EVENT:
case CTRL_BREAK_EVENT:
case CTRL_CLOSE_EVENT:
case CTRL_LOGOFF_EVENT:
case CTRL_SHUTDOWN_EVENT:
std::cout << "n[提示] 收到退出信号,正在停止监听..." << std::endl;
StopMonitorSession();
return TRUE;
default:
return FALSE;
}
}
// ==================== 辅助函数:解析事件属性 ====================
/**
* @brief 从 ETW 事件中读取指定类型的属性(模板函数)
* @tparam T 属性值类型(如 DWORD、ULONGLONG)
* @param eventRecord ETW 事件记录指针
* @param propertyName 属性名称(宽字符串)
* @param[out] value 输出参数,存储读取到的值
* @return 成功返回 true,失败返回 false
* @details 使用 TDH (Trace Data Helper) API 解析事件属性
* @see TdhGetPropertySize, TdhGetProperty
*/
template<typename T>
bool GetEventProperty(PEVENT_RECORD eventRecord, LPCWSTR propertyName, T& value) {
// 准备属性描述符
PROPERTY_DATA_DESCRIPTOR property = {};
property.PropertyName = reinterpret_cast<ULONGLONG>(propertyName);
property.ArrayIndex = ULONG_MAX;
// 获取属性大小
ULONG size = 0;
if (TdhGetPropertySize(eventRecord, 0, nullptr, 1, &property, &size) != ERROR_SUCCESS) {
return false;
}
// 读取属性值
std::vector<BYTE> buffer(size);
if (TdhGetProperty(eventRecord, 0, nullptr, 1, &property, size, buffer.data()) != ERROR_SUCCESS) {
return false;
}
value = *reinterpret_cast<T*>(buffer.data());
return true;
}
// ==================== 步骤一:处理线程创建事件 ====================
/**
* @brief ETW 线程事件回调函数
* @param eventRecord ETW 事件记录指针
* @details 当系统中有线程创建时,ETW 会调用此回调函数
* 本函数负责过滤出远程线程创建事件并打印信息
* @note 此函数由 ProcessTrace 在内部调用,不应手动调用
*/
void ThreadEventCallback(PEVENT_RECORD eventRecord) {
if (!eventRecord) return;
// 1.1 过滤:只处理线程启动事件
if (eventRecord->EventHeader.EventDescriptor.Opcode != EVENT_TRACE_TYPE_START) {
return;
}
// 1.2 提取关键信息
DWORD creatorPid = eventRecord->EventHeader.ProcessId; ///< 创建线程的进程 PID
DWORD targetPid = 0; ///< 线程所属进程 PID
DWORD threadId = 0; ///< 线程 ID
ULONGLONG startAddr = 0; ///< 线程起始地址
// 尝试获取目标进程 PID(不同 Windows 版本字段名可能不同)
if (!GetEventProperty(eventRecord, L"ProcessId", targetPid)) {
GetEventProperty(eventRecord, L"TargetProcessId", targetPid);
}
// 1.3 判断是否为远程线程(创建者 != 目标进程)
if (creatorPid == 0 || targetPid == 0 || creatorPid == targetPid) {
return; // 普通线程,忽略
}
// 1.3.1 过滤:忽略系统进程(PID 4)发起的远程线程
if (creatorPid == 4) {
return;
}
// 1.4 过滤:如果指定了监控目标,只显示目标进程的事件
if (g_Context.targetPid != 0 && targetPid != g_Context.targetPid) {
return;
}
// 1.5 获取线程 ID 和起始地址
if (!GetEventProperty(eventRecord, L"ThreadId", threadId)) {
GetEventProperty(eventRecord, L"NewThreadId", threadId);
}
if (!GetEventProperty(eventRecord, L"StartAddress", startAddr)) {
GetEventProperty(eventRecord, L"Win32StartAddr", startAddr);
}
// 1.6 打印检测结果(红色高亮显示)
HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleTextAttribute(hConsole, FOREGROUND_RED | FOREGROUND_INTENSITY);
std::cout << "n[!] 检测到远程线程创建" << std::endl;
SetConsoleTextAttribute(hConsole, FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE);
std::cout << " 发起进程 PID: " << creatorPid << std::endl;
std::cout << " 目标进程 PID: " << targetPid << std::endl;
std::cout << " 线程 ID: " << threadId << std::endl;
std::cout << " 起始地址: 0x" << std::hex << startAddr << std::dec << std::endl;
std::cout << "========================" << std::endl;
}
// ==================== 步骤二:启动 ETW 会话 ====================
/**
* @brief 启动 ETW 监听会话
* @param targetPid 目标进程 PID,传入 0 表示监听所有进程
* @return 成功返回 true,失败返回 false
* @details 完整的启动流程:
* 1. 准备会话属性缓冲区
* 2. 初始化 EVENT_TRACE_PROPERTIES 结构
* 3. 停止可能存在的旧会话(清理残留)
* 4. 调用 StartTrace 启动内核跟踪
* 5. 调用 OpenTrace 打开跟踪会话并注册回调
* @note 需要管理员权限,否则 StartTrace 会返回错误码 5 (ACCESS_DENIED)
* @see StartTrace, OpenTrace, EVENT_TRACE_PROPERTIES
*/
bool StartMonitorSession(DWORD targetPid) {
g_Context.targetPid = targetPid;
// 2.1 准备会话属性缓冲区
// 缓冲区包含:EVENT_TRACE_PROPERTIES + 会话名称字符串
std::wstring sessionName = KERNEL_LOGGER_NAME;
size_t bufferSize = sizeof(EVENT_TRACE_PROPERTIES) + (sessionName.size() + 1) * sizeof(wchar_t);
g_Context.propertyBuffer = std::make_unique<BYTE[]>(bufferSize);
// 2.2 初始化会话属性
auto ResetProperties = [&]() {
ZeroMemory(g_Context.propertyBuffer.get(), bufferSize);
g_Context.properties = reinterpret_cast<EVENT_TRACE_PROPERTIES*>(g_Context.propertyBuffer.get());
g_Context.properties->Wnode.BufferSize = static_cast<ULONG>(bufferSize);
g_Context.properties->Wnode.Flags = WNODE_FLAG_TRACED_GUID;
g_Context.properties->Wnode.Guid = SystemTraceControlGuid;
g_Context.properties->Wnode.ClientContext = 1; // 使用 QueryPerformanceCounter 作为时间戳
g_Context.properties->LogFileMode = EVENT_TRACE_REAL_TIME_MODE; // 实时模式,不写入日志文件
g_Context.properties->EnableFlags = EVENT_TRACE_FLAG_THREAD; // 监听线程事件
g_Context.properties->LoggerNameOffset = sizeof(EVENT_TRACE_PROPERTIES);
LPWSTR loggerName = reinterpret_cast<LPWSTR>(g_Context.propertyBuffer.get() + sizeof(EVENT_TRACE_PROPERTIES));
wcscpy_s(loggerName, sessionName.size() + 1, sessionName.c_str());
};
ResetProperties();
// 2.3 停止可能存在的旧会话(避免冲突)
ULONG stopStatus = StopTrace(0, sessionName.c_str(), g_Context.properties);
if (stopStatus == ERROR_SUCCESS) {
std::cout << "[提示] 清理了残留的 ETW 会话,等待资源释放..." << std::endl;
Sleep(500); // 增加等待时间,让系统有足够时间释放资源
} else if (stopStatus != ERROR_WMI_INSTANCE_NOT_FOUND) {
std::cout << "[提示] StopTrace 返回: " << stopStatus << std::endl;
}
ResetProperties(); // StopTrace 会覆写属性结构,重新写回关键字段
// 2.4 启动新的跟踪会话(带重试机制)
ULONG status = ERROR_ALREADY_EXISTS;
for (int retry = 0; retry < 3 && status == ERROR_ALREADY_EXISTS; retry++) {
if (retry > 0) {
std::cout << "[提示] 会话仍在释放中,等待后重试 (" << retry << "/3)..." << std::endl;
Sleep(500);
}
status = StartTrace(&g_Context.sessionHandle, sessionName.c_str(), g_Context.properties);
}
if (status != ERROR_SUCCESS) {
std::cout << "StartTrace 失败,错误码: " << status << std::endl;
if (status == ERROR_ALREADY_EXISTS) {
std::cout << "[建议] 会话仍被占用,请稍后再试或重启系统" << std::endl;
}
return false;
}
// 2.5 打开跟踪会话并注册事件回调
EVENT_TRACE_LOGFILEW logFile = {};
logFile.LoggerName = const_cast<LPWSTR>(sessionName.c_str());
logFile.ProcessTraceMode = PROCESS_TRACE_MODE_EVENT_RECORD | PROCESS_TRACE_MODE_REAL_TIME;
logFile.EventRecordCallback = ThreadEventCallback; // 注册回调函数
g_Context.traceHandle = OpenTrace(&logFile);
if (g_Context.traceHandle == reinterpret_cast<TRACEHANDLE>(INVALID_HANDLE_VALUE)) {
std::cout << "OpenTrace 失败" << std::endl;
StopTrace(g_Context.sessionHandle, sessionName.c_str(), g_Context.properties);
return false;
}
return true;
}
// ==================== 步骤三:停止监听 ====================
/**
* @brief 停止 ETW 监听会话并清理资源
* @details 清理流程:
* 1. 关闭跟踪句柄 (CloseTrace)
* 2. 停止跟踪会话 (StopTrace)
* @note 程序退出前必须调用此函数,否则 ETW 会话可能残留在系统中
* @see CloseTrace, StopTrace
*/
void StopMonitorSession() {
if (g_Context.traceHandle != 0) {
CloseTrace(g_Context.traceHandle);
g_Context.traceHandle = 0; // 重置句柄
}
if (g_Context.sessionHandle != 0) {
EVENT_TRACE_PROPERTIES stopProps = {};
stopProps.Wnode.BufferSize = sizeof(stopProps);
StopTrace(g_Context.sessionHandle, KERNEL_LOGGER_NAME, &stopProps);
g_Context.sessionHandle = 0; // 重置句柄
}
// 清空缓冲区,释放内存
g_Context.propertyBuffer.reset();
g_Context.properties = nullptr;
g_Context.targetPid = 0;
}
// ==================== 主程序 ====================
/**
* @brief 主函数
* @return 成功返回 0,失败返回 1
* @details 程序流程:
* 1. 输入目标进程 PID
* 2. 启动 ETW 监听会话
* 3. 处理事件(阻塞等待)
* 4. 清理资源
*/
int main() {
std::cout << "=== 远程线程创建行为检测 ===" << std::endl;
std::cout << "当前进程 PID: " << GetCurrentProcessId() << std::endl;
std::cout << std::endl;
std::cout << "请输入需要监控的目标进程 PID (0 表示所有进程): ";
DWORD targetPid = 0;
std::cin >> targetPid;
// 注册控制台信号处理,确保异常退出也能清理会话
SetConsoleCtrlHandler(ConsoleCtrlHandler, TRUE);
// 步骤 1:启动 ETW 监听
if (!StartMonitorSession(targetPid)) {
std::cout << "初始化 ETW 监听失败(需要管理员权限)" << std::endl;
system("pause");
return 1;
}
std::cout << "开始监听..." << std::endl;
// 步骤 2:处理事件(阻塞调用,Ctrl+C 会自动中断)
ProcessTrace(&g_Context.traceHandle, 1, nullptr, nullptr);
// 步骤 3:清理资源
StopMonitorSession();
std::cout << "监听结束" << std::endl;
system("pause");
return 0;
}运行与验证
环境与权限
- Windows 10/11 x64
- Visual Studio 2022,x64 Debug
- 以管理员权限运行(ETW 内核会话需要)
操作步骤
- 启动本节检测程序,输入目标进程 PID(0 表示所有进程)
- 运行上一节的 Shellcode 注入 PoC
- 返回本程序窗口,观察是否打印”远程线程创建”
检测效果验证

如图所示,程序成功检测到:
- ✅ 远程线程创建事件
- ✅ 发起进程和目标进程的 PID
- ✅ 新创建线程的 ID 和起始地址
- ✅ 即使使用 Shellcode 中转,行为层依然能捕获
优势、边界与白名单
优势
- ✅ 与起始地址无关,覆盖 Shellcode 中转
- ✅ 行为层检测,更难被简单改造绕过
- ✅ 开销低,适合常驻监控
边界与可能的误报
- ⚠️ 调试器、自动化工具可能合法创建远程线程
- ⚠️ 某些安全/运维产品也会触发同类事件
建议:
- 建立进程白名单(调试器、已知工具)
- 结合时间窗口与频次阈值(短时间内大量远程线程更可疑)
- 结合目标模块/内存区域属性进一步评分
常见故障排查
StartTrace 返回 5(ACCESS_DENIED)
- 用管理员权限运行
OpenTrace 失败
- 先 StopTrace 清理残留会话
不打印任何事件
- 确认
EVENT_TRACE_FLAG_THREAD已启用 - 目标 PID 过滤是否设置为 0(监听全局以便对照)
- 确认
实战延伸
- 用户态: 继续叠加模块归属检测
- 内核态: 基于
NtCreateThreadEx的回调或回溯调用栈 - 关联: 把”谁创建了线程”与”线程执行了什么”结合成事件链
下节课预告
我们已经能抓到远程线程创建
攻击者下一步会怎么做?
不创建新线程
改为 APC 或劫持现有线程
下节课见:攻击篇 – 不使用远程线程的注入(APC/线程劫持)
互动讨论
欢迎在评论区讨论以下问题:
- ETW 除了监听线程创建,还可以监听哪些系统事件?
- 如果攻击者关闭 ETW 会话,防御者如何应对?
- 在实际应用中如何降低 ETW 监听的性能开销?
⚠️ 本课程内容仅供防御性安全研究与教学使用,请勿用于非法用途。
