阅读时间: 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/映像路径)

本节聚焦”线程创建”这一环
后续可叠加模块与内存画像,降低误报


实现步骤总览

  1. 启动内核会话(KERNEL_LOGGER_NAME)

    • 准备 PROPERTIES 结构
    • 填充会话名和缓冲区
    • 调用 StartTrace 启动
  2. 只启用线程事件(EVENT_TRACE_FLAG_THREAD)

    • 设置 EnableFlags
    • 减少无关数据
    • 提升监听效率
  3. 打开会话,注册事件回调

    • 配置 LOGFILEW 结构
    • 指定 EventRecordCallback
    • OpenTrace 获取句柄
    • ProcessTrace 阻塞读取
  4. 在回调里筛选”线程启动”事件

    • 检查 Opcode == START
    • 只处理创建那一刻
    • 跳过就绪和退出
  5. 读取关键字段,判断是否远程线程

    • 用 TDH 解析属性
    • 提取创建者和目标 PID
    • 判定:创建者 ≠ 目标
    • 打印远程线程信息

关键代码讲解

第1步:从 main 函数出发

先把主流程写出来
让程序能跑起来
再逐步补齐依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int main()
{
std::cout << "=== 远程线程创建行为检测 ===\n\n";
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;
}

主流程三步走:

  1. 启动 ETW 内核会话,只订阅线程事件,把事件回调绑定好
  2. 阻塞读取事件流;每条事件都会进入回调
  3. 先关读取,再停会话,资源收干净

第2步:补齐必要的包含与全局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#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步:准备会话属性并启动会话

准备缓冲区:

1
2
3
4
5
6
7
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());

填充属性:

1
2
3
4
5
6
7
8
9
10
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());

启动会话:

1
2
3
4
5
6
7
8
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步:打开追踪并注册回调

1
2
3
4
5
6
7
8
9
10
11
12
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

1
2
3
4
void WINAPI ThreadEventCallback(PEVENT_RECORD eventRecord)
{
if (!eventRecord) return;
if (eventRecord->EventHeader.EventDescriptor.Opcode != EVENT_TRACE_TYPE_START) return;

第二步:提取字段,获取关键数据

1
2
3
4
5
6
7
8
9
10
11
12
13
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);

第三步:判定输出,识别远程线程

1
2
3
4
5
6
7
8
9
10
    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 属性读取函数

1
2
3
4
5
6
7
8
9
10
11
12
13
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步:完善清理函数

1
2
3
4
5
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);
}

完整检测代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
/**
* @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 内核会话需要)

操作步骤

  1. 启动本节检测程序,输入目标进程 PID(0 表示所有进程)
  2. 运行上一节的 Shellcode 注入 PoC
  3. 返回本程序窗口,观察是否打印”远程线程创建”

检测效果验证

远程线程创建检测效果

如图所示,程序成功检测到:

  • ✅ 远程线程创建事件
  • ✅ 发起进程和目标进程的 PID
  • ✅ 新创建线程的 ID 和起始地址
  • ✅ 即使使用 Shellcode 中转,行为层依然能捕获

优势、边界与白名单

优势

  • ✅ 与起始地址无关,覆盖 Shellcode 中转
  • ✅ 行为层检测,更难被简单改造绕过
  • ✅ 开销低,适合常驻监控

边界与可能的误报

  • ⚠️ 调试器、自动化工具可能合法创建远程线程
  • ⚠️ 某些安全/运维产品也会触发同类事件

建议:

  • 建立进程白名单(调试器、已知工具)
  • 结合时间窗口与频次阈值(短时间内大量远程线程更可疑)
  • 结合目标模块/内存区域属性进一步评分

常见故障排查

  • StartTrace 返回 5(ACCESS_DENIED)

    • 用管理员权限运行
  • OpenTrace 失败

    • 先 StopTrace 清理残留会话
  • 不打印任何事件

    • 确认 EVENT_TRACE_FLAG_THREAD 已启用
    • 目标 PID 过滤是否设置为 0(监听全局以便对照)

实战延伸

  • 用户态: 继续叠加模块归属检测
  • 内核态: 基于 NtCreateThreadEx 的回调或回溯调用栈
  • 关联: 把”谁创建了线程”与”线程执行了什么”结合成事件链

下节课预告

我们已经能抓到远程线程创建
攻击者下一步会怎么做?

不创建新线程
改为 APC 或劫持现有线程

下节课见:攻击篇 - 不使用远程线程的注入(APC/线程劫持)


互动讨论

欢迎在评论区讨论以下问题:

  1. ETW 除了监听线程创建,还可以监听哪些系统事件?
  2. 如果攻击者关闭 ETW 会话,防御者如何应对?
  3. 在实际应用中如何降低 ETW 监听的性能开销?

⚠️ 本课程内容仅供防御性安全研究与教学使用,请勿用于非法用途。