近日翻到一个比较新颖的样本,在最终后门载荷释放前运用了不少免杀手段,包括堆栈欺骗,实现反射性调用API,以及DLL侧加载、DLL挖空、HOOK规避等手法,对其执行流程和部分手法做详细分析记录。
样本概述
- 初始载荷
初始载荷如下,其中loader.exe是作者编译的一个shellcode加载器。
- 执行流程
有合法签名可执行文件(taskhost.exe)侧载恶意 DLL (sbiedll.dll)。这个恶意DLL作为中间载荷,解密密文载荷DAT文件(sbiedll.dat)获取二阶段有效负载。解密的有效负载MoonWalk充当后门,滥用Google Drive进行命令和控制(C2) 通信。流程图如下:
- 精心构造的配置信息
样本主要使用“AES-CFB”算法解密载荷,多次使用MD5 hash算法做载荷校验或将hash值作为AES算法的KEY、IV值。主要配置信息如下:
- 较为丰富的杀软规避
- DLL 侧加载执行恶意载荷
- 指定特定的执行参数,期望参数“--type driver”。
- FNV1a hash动态解析 API。
- 堆栈欺骗调用win API,规避win API调用过程检测(构造伪造堆栈、修改堆栈指针、调用 API、恢复原始堆栈)
- 使用了大量的windows底层函数(如内核函数NtCreateFile等)。
- 使用当前植入主机的GUID md5值作为AES IV值,生成唯一、绑定载荷。
- hook规避、断点检查。
- MD5 hash算法和AES-CFB算法进行解密,MD5hash算法进行完整性检查
样本分析
侧加载
1. “白加黑”侧加载执行初始恶意载荷。由Sandboxie签名的合法可执行文件 (taskhost.exe) 侧载恶意 DLL (sbiedll.dll)。
“sbiedll.dll”中初始化了三个导出函数,但实际上都指向相同的地址,函数名称“SbieDll_Hook”。
2. 编写一个加载器执行恶意导出函数“SbieDll_Hook”,同步虚拟内存地址,动态、静态结合调试主要功能。
配置信息解密
3. 利用MD5校验加密配置信息,确保配置的完整性。计算密文段的MD5 hash来于硬编码在样本中的正确hash值做比对。
两段密文hash值:
FE93E8E1C5C3032A26D783A78A820587
1E8EE70F02D60E389D8F721E8CE6DF1F
4. AES-CFB算法进行解密,获取解密配置信息入下,其中包括明文字符“sbiedll.dat”、“--type”以及其他配置信息或密钥供后续使用。
杀软规避
5. 执行进程是否使用正确的参数启动,验证期望参数包含“--type driver”,如果此验证检查失败,则终止进程。计参数的MD5哈希值,多次计算结果与硬编码的哈希值进行比较。
硬编码hash值:
E2D45D57C7E2941B65C6CCD64AF4223E
6. 利用加盐的FNV-1a哈希算法来逃避针对WINAPI的静态检测。在哈希计算过程中添加盐值不同样本的哈希值不同,提高了逃避静态检测的能力。
上图中伪代码通过指针运算和类型转换访问内存,还原C代码,所需API地址通过结构体成员访问。
sImportTable->ntdll_LdrLoadDll = ResolveImport(L"ntdll", 0xFE0B07B0, 0xCA7BB6AC);
函数体内部关键步骤入下:
- 初始化哈希值。
- 对输入字符串(DLL名或函数名)进行哈希计算。
- 对盐值进行哈希计算。
首先,它对输入字符串(代表 DLL 或函数名称)进行哈希处理。然后,它单独对盐值进行哈希处理。此两步哈希处理过程相当于对输入字符串和盐的连接进行哈希处理。
while ( 1 )
{
Fun_FNV1a_Hash = 0x811C9DC5; // FNV-1a 初始哈希值
InputString = (unsigned __int8 *)(a1 + *v12);
v18 = -1i64;
do
++v18;
while ( InputString[v18] ); // 输入字符长度计算
for ( i = &InputString[(unsigned int)v18]; InputString < i; Fun_FNV1a_Hash = 0x1000193 * (Fun_FNV1a_Hash ^ v20) )// 计算输入字符串FNV-1a哈希
v20 = *InputString++;
v21 = (unsigned __int8 *)&unk_7FFFBEAD7D78; // Salt value: CB 24 B4 BA
do
{
v22 = *v21++;
Fun_FNV1a_Hash = 0x1000193 * (Fun_FNV1a_Hash ^ v22);// 对盐值进行哈希计算
}
while ( v21 < byte_7FFFBEAD7D7C );
盐值,CB 24 B4 BA
最终将API函数地址放置与连续的内存地址中,即结构体成员变量中。下图以API函数“LookupAccountSidw”为例。
此外ResolveImport中还有内置了一些API函数调用相关功能,此样本中暂时没用到。
7. DodgeBox在完成API初始化后,它就会扫描并解除从System32目录加载的 DLL。此过程包括遍历 .pdata每个 DLL 的部分,检索每个函数的起始和结束地址,并计算每个函数字节的 FNV1a 哈希值。计算存储在磁盘上的相同函数字节的相应哈希值。如果两个哈希值不同,则可以检测到潜在的篡改,将用磁盘中的原始版本替换内存中的函数。
此过程包括遍历 .pdata每个 DLL ,步骤方法入下:
p_InMemoryOrderModuleList = &NtCurrentTeb()->ProcessEnvironmentBlock->Ldr->InMemoryOrderModuleList;
当前线程的线程环境块(TEB)->进程环境块(PEB)->已加载模块的信息链表InMemoryOrderModuleList。
获取链表信息后再循环遍历加载模块列表的每个节点。
p_InMemoryOrderModuleList = &NtCurrentTeb()->ProcessEnvironmentBlock->Ldr->InMemoryOrderModuleList;
for ( i = p_InMemoryOrderModuleList->Flink; i != p_InMemoryOrderModuleList; i = i->Flink )
{
Flink = (__int64)i[2].Flink;
v3 = (const wchar_t *)i[4].Flink;
if ( ((__int64)i[5].Blink & 0x60000000) != 0x20000000
&& (*(_WORD *)(*(int *)(Flink + 60) + Flink + 22) & 0x2000) != 0 )
DAT文件有效载荷解密及加载
8. 在当前进程堆中开辟内存,读取“sbiedll.dat”文件流。
在完成路径等相关字符初始化后,利用NtReadFile读取文件流至当前进程的堆空间中。
9. DAT文件有效载荷解密
读取计算机唯一标识符GUID,计算MD5。检索注册表“SOFTWARE\Microsoft\Cryptography MachineGuid”值获取系统GUID,计算其MD5值,作为后续解密AES算法的IV值。
检查文件的前四个字节。如果这些字节非零,则表示 DAT载荷投递在了特定目标上,如果DAT文件不是特定于机器的,DodgeBox将继续使用 AES-CFB 加密解密文件,利用存储在配置文件中的密钥参数。从“sbiedll.dat”载荷的第五个字节开始解密,解密获取一个隐藏PE头的PE文件。
使用前机器GUID的MD5哈希作为AES IV重新加密载荷。
在完成有效载荷解密后,样本将载荷与当前植入机器绑定,使用原有的AES密钥key,但是将 GUID 的 MD5 哈希作为 AES IV。这种方法保证了解密的 DAT 文件无法在任何其他机器上解密,从而增强了有效载荷的安全性。
10. 关于堆栈调用欺骗可以再做一些更详细的说明。这里借鉴原始报告的描述。
首先设置堆栈,精心构造堆栈的内容,包括插入返回地址和所需的参数,确保API调用能按预期进行。
真实API调用,在准备好堆栈和寄存器后,执行jmp指令,将控制流重定向到目标 API。这种方式使得 API 调用看起来像是从正常位置发起的。
这种方法结合了堆栈伪造和 API 调用技术,通过控制堆栈和寄存器的状态来确保 API 调用能够成功并达到隐蔽的效果。这种复杂的技术可以有效地绕过静态分析和动态监控,增加了恶意代码的隐蔽性。
样本的加盐 FNV1a 哈希的 Python 实现如下所示:
func fnv1aSalted(data, salt []byte, seedValue uint32) uint32 {
combinedData := append(data, salt...)
hash := seedValue
prime := uint32(0x01000193)
for _, byteValue := range combinedData {
hash ^= uint32(byteValue)
hash *= prime
hash &= 0xFFFFFFFF
}
return hash
}
func main() {
ntdll := []byte("n\x00t\x00d\x00l\x00l\x00")
salt := []byte{0xba, 0xb4, 0x24, 0xcb}
hash1 := fnv1aSalted(ntdll, salt, 0x811c9dc5)
fmt.Printf("Hash for ntdll: 0x%08x\n", hash1)
ldrLoadDll := []byte("LdrLoadDll")
hash2 := fnv1aSalted(ldrLoadDll, salt, 0x811c9dc5)
fmt.Printf("Hash for LdrLoadDll: 0x%08x\n", hash2)
}
整体样本尤其是一些免杀首发相对复杂,因为中途快照出现了些问题,注释等信息缺失,所以记录思路显得有点混乱,不过还是基本描述清除了相关手法。