FlokiBot 银行木马详细分析
☀ 介 绍
FlokiBot 是最近一款针对于欧洲和巴西联邦共和国的银行木马,作为一款恶意软件工具集,它在一些黑客论坛上被卖到$1000。它通过垃圾邮件和渗透代码工具包来传播。虽说它是继承于ZeuS(宙斯),FlokiBot 也做了很多有趣的改进。有诸如内存截取(RAM scraping),定制的 dropper 这样的新特性,还有似乎从泄露了源码的Carberp 那里借鉴了几行代码。
FlokiBot 与其 dropper 都有很多常用或不常用的混淆技术,我们将解开它们的神秘面纱,并着重讨论如何使用 IDA 和 IDAPython 脚本来静态脱壳。因为你们已经在最近很多恶意软件上接触过这些技术了,所以我觉得这是一次很好的锻炼。
在看完写的这篇关于 FlokiBot 的 dropper 文章:. 之后,我决定看一下 FlokiBot。尽管大多数关于 FlokiBot 的文章都着重讲它的dropper,我还是想讲得更详细一些,然后再讲讲它的 payload;我们将会看到它有很多有有趣的特性,而且不是你平常所看到的 ZeuS 不一样,虽然它的很多代码是来自 ZeuS 和 Carberp leaks。还是比逆向勒索软件好。
Hash值:
$ rahash2 -a md5,sha1,sha256 -qq floki_dropper.vir 37768af89b093b96ab7671456de894bc 5ae4f380324ce93243504092592c7b275420a338 4bdd8bbdab3021d1d8cc23c388db83f1673bdab44288fccae932660eb11aec2a $ rahash2 -a md5,sha1,sha256 -qq floki_payload32.vir da4ea4e44ea3bb65e254b02b2cbc67e8 e8542a465810ff1396a316d1c46e96e042bf4189 9f1d2d251f693787dfc0ba8e64907e204f3cf2c7320f66007106caac0424a1f3
☀ FlokiBot Dropper
导入:模块/API 哈希处理与系统调用(syscall)
dropper 通过比较经过哈希处理的库名与内置哈希值来加载模块。哈希进程用到了一个基础的 CRC32,之后这个 CRC32 还要跟两个字节的密钥异或,那两个密钥随样本的不同而不同。
有两种方法来检索动态链接库(dll)库名:一是用 来检查 的结构并读取 的值来获取进程已经加载的dll。第二种方法是,通过在 Windows 系统文件夹中罗列库名。
下面的模块都被 dropper 导入了:
CRC Library Method ------------------------------------------------------ 84C06AAD ntdll.dll load_imports_peb 6AE6ABEF kernel32.dll load_imports_peb 2C2B3C88 948B9CAB C7F4511A wininet.dll load_imports_folder F734DCF8 ws2_32.dll load_imports_folder F16EE30D advapi32.dll load_imports_folder C8A18E35 shell32.dll load_imports_folder E20BF2CB shlwapi.dll load_imports_folder 1A50B19C secur32.dll load_imports_folder 630A1C77 crypt32.dll load_imports_folder 0248AE46 user32.dll load_imports_peb BD00960A 4FF44795 gdi32.dll load_imports_peb E069944C ole32.dll load_imports_folder CAAD3C25
之后,FlokiBot 将采取同样的操作来定位和加载这些模块用到的API.首先,当CRC后的名字是匹配的,那么它将检索 LdrGetProcedureAddress 在 ntdll 中的地址,并用它来获取其他 API 的句柄。这样做的话,函数的地址就仅对 debugger 可见。如此,分析代码将会非常困难,因我我们不知道调用了哪个 API。去混淆的一种方法将在下一章提到。
FlokiBot 与其 dropper 的另一有趣之处在于它们调用一些原生 API 函数的方式。这些函数位于 ntdll 中,并且以 Nt* 或者 Zw*为前缀。它们在实现的时候与其他 API 有些许不同,因为它们要用到 syscall 特别是 int 0x2e。下面的截图说明了它们是如何在 ntdll 中实现的。
正如我们所看到,系统调用的值放在 eax(在我64位Windows 7上,NtAllocateVirtualMemory 是在0x15 ),而且参数被传到 edx。x86 和 64 位的所有系统调用号(syscall number)都可以在这张网页找到:.
在检查 ntdll 中的 API 时,FlokiBot 会先检查函数的第一个操作码是否为 0xB8 = MOV EAX, 若是,并且 CRC 后的 API 名也复合,它将提取 MOV EAX 后面的四个字节,也就是系统调用号,并将它保存在 dwSyscallArray 数组中。
在我的虚拟机上,当所有的系统调用号都被 dropper 提取后,dwSyscallArray 长这样。
Index API Syscall number ------------------------------------------------------- 0x0 NtCreateSection 0x47 0x1 NtMapViewOfSection 0x25 0x2 NtAllocateVirtualMemory 0x15 0x3 NtWriteVirtualMemory 0x37 0x4 NtProtectVirtualMemory 0x4D 0x5 NtResumeThread 0x4F 0x6 NtOpenProcess 0x23 0x7 NtDuplicateObject 0x39 0x8 NtUnmapViewOfSection 0x27
当 FlokiBot 需要调用某个原生函数的时候,它将调用自身的一个函数,那个函数直接从 dwSyscallArray 中检索系统调用号,传参,触发中断 0x2E。这些都跟它在 ntdll 中的实现方式一样。这就是为什么你不会看到任何这些 API 调用的轨迹,而且专门用来钩住这些 API 的监测工具也监测不到有调用它们。
☀ API 调用去混淆
既然 FlokiBot 的 payload 用到了相同的函数和数据结构,你可以用“IDAPython完全静态去混淆”模块,稍稍修改 IDAPython 脚本就可以将 dropper 的 API 调用去混淆。
☀ 解除挂钩模块
FlokiBot的一个有趣之处就在于它的 dropper 和 payload 都有解除挂钩操作。思路是卸载检测工具,沙箱和杀毒软件中的钩子。尽管这并不是恶意软件第一次使用这样的功能,比如说,Carberp 就有一个能 的功能,还有最近的 Carbanak ,但这样的功能能真的非常罕见,应该值得注意。在这一部分,我将描述 FlokiBot 是如何解除挂钩的。
首先,FlokiBot 通过罗列 System32 文件夹中的 dll 来获得 ntdll.dll 的句柄,然后用我们上面所提到的哈希处理过程,最后调用 MapViewOfFile 来映射它在内存中的位置。结果就是,FlokiBot 有两个库在内存中映射的版本:一个是在导入期间导入的,这个可能会被监测工具的钩子改变,另一个是它直接从磁盘中映射的,这个是干净的。
NTDLL在磁盘中的映射——干净版本
由dropper导入的NTDLL——可能被钩住
现在,设置了正确的权限,FlokiBot 把映射干净 DLL 代码块的地址、导入的 dll 的地址入栈,然后调用解除挂钩操作。
因为它要重写它的内存里的一些数据以删除钩子,恶意软件需要改变导入的 NTDLL 代码导出段的内存保护机制。而它是通过用 int 0x2E 和之前提取的系统调用号(在我的 windows 版本是 0x4D)调用 NtProtectVirtualMemory 来做到的。我们可以看到如果某个钩子被发现,某一部分的代码就会变得可写。
解除挂钩函数可以描述为三步:对于 NTDLL 导出的每一个函数……
比较两个映射库的第一个操作码
如果它们不一致,说明导入的 ntdll 里的函数已经被钩住了。
改变导入的dll的内存保护机制,让它变成可写。
用从被映射到磁盘的 dll 复制来的操作码修补这个操作码。
解除挂钩操作的主要程序如下:
这样以来,很多监测工具、杀毒软件和沙箱都无法追踪恶意软件的调用。这个非常有用,如果你想要避免来自像 . 这些网上沙箱的自动分析。
☀ 从资源中提取 Bot
它的 dropper 有 3 个明确命名的资源: key, bot32 和 bot64 。Bot 被 RtlCompressBuffer() 和 LZNT1 压缩,然后用有 16 字节密钥的 RC4 加密它。在我的样本里,密钥是:
A3 40 75 AD 2E C4 30 23 82 95 4C 89 A4 A7 84 00
你可以从 Talos 团队的 Github: . 中找到可以备份这个 payload 和配置文件的 Python 脚本。要注意的是,它们并不能自己正确运行,因为它们要注入一个进程,还需要一些被 dropper 在内存中改写的数据。我们会在下一部分详谈注入进程。
提取资源的常用方法:
BOOL __userpurge extract_bot_from_rsrc@<eax>(int a1@<edi>, HMODULE hModule)
{
HRSRC v2; // eax@1
int v3; // eax@2
const void *v4; // esi@5
HRSRC v5; // eax@7
int v6; // eax@8
HRSRC v7; // eax@10
unsigned int v8; // eax@11
int v10; // [sp+4h] [bp-4h]@1
v10 = 0;
v2 = FindResourceW(hModule, L"key", (LPCWSTR)0xA);
if ( v2 )
v3 = extract_rsrc(hModule, (int)&v10, v2);
else
v3 = 0;
if ( v3 )
{
v4 = (const void *)v10;
if ( v10 )
{
qmemcpy((void *)(a1 + 84), (const void *)v10, 0x10u);
free_heap(v4);
}
}
v5 = FindResourceW(hModule, L"bot32", (LPCWSTR)0xA);
if ( v5 )
v6 = extract_rsrc(hModule, a1 + 4, v5);
else
v6 = 0;
*(_DWORD *)(a1 + 12) = v6;
v7 = FindResourceW(hModule, L"bot64", (LPCWSTR)0xA);
if ( v7 )
v8 = extract_rsrc(hModule, a1 + 8, v7);
else
v8 = 0;
*(_DWORD *)(a1 + 16) = v8;
return *(_DWORD *)(a1 + 4) && *(_DWORD *)(a1 + 12) > 0u && *(_DWORD *)(a1 + 8) && v8 > 0;
}
☀ 注入过程
dropper 并不是用常用的用 NtMapViewOfSection 和 NtWriteVirtualMemory 来将 payload 注入到 explorer.exe (或者svchost.exe,如果失败的话) 。它是用写并运行一个可以在进程内存中解密解压 payload 的 shellcode 来完成的。这很不常见,很有趣。dropping 的过程可以用下面的图片来总结:
dropper 在 explorer.exe / svchost.exe 里写一个 trampoline shellcode 和它自己的一个函数。
当运行时,trampoline 就会调用那个函数。
函数自动运行,并且动态解决导入、读取 dropper 的资源,并将它们提取到自己的进程内存中。(比如,在 explorer.exe / svchost.exe 的地址空间里)
最后,dropper 在目标进程中运行 bot payload 的入口点(entrypoint)。
第一个写在 explorer.exe 的shellcode(称为 trampoline )会休眠100ms,然后调用一个函数,那个函数 dropper 在进程内存中映射在 0x80000000 ,在该 dropper 中默认称为 sub_405E18 。这第二个阶段是要提取 bot payload,解密并解压它们。所有这些都发生在 explorer.exe / svchost.exe 内存中。
$ rasm2 -a x86 -b 32 -D '558BEC51C745FCFF10B4766864000000FF55FCC745FC000008006800000900FF55FC83C4048BE55DC3'
0x00000000 1 55 push ebp
0x00000001 2 8bec mov ebp, esp
0x00000003 1 51 push ecx
0x00000004 7 c745fcff10b476 mov dword [ebp - 4], 0x76b410ff ; address of sleep()
0x0000000b 5 6864000000 push 0x64
0x00000010 3 ff55fc call dword [ebp - 4] ; sleep()
0x00000013 7 c745fc00000800 mov dword [ebp - 4], 0x80000
0x0000001a 5 6800000900 push 0x90000
0x0000001f 3 ff55fc call dword [ebp - 4] ; sub_405E18, 2nd stage 0x00000022 3 83c404 add esp, 4
0x00000025 2 8be5 mov esp, ebp
0x00000027 1 5d pop ebp
0x00000028 1 c3 ret
sub_405E18 将通过与 dropper 和 payload 相同的进程来导入它需要的资源,用有些许不用的 crc32 和新的异或密钥。
int __stdcall sub_405E18(int a1)
{
[...]
if ( a1 && *(_DWORD *)(a1 + 4) && *(_DWORD *)a1 != -1 )
{
v1 = 0;
v34 = 0i64;
v35 = 0i64;
v36 = 0i64;
do /* CRC Polynoms */
{
v2 = v1 >> 1;
if ( v1 & 1 )
v2 ^= 0xEDB88320;
if ( v2 & 1 )
v3 = (v2 >> 1) ^ 0xEDB88320;
else
v3 = v2 >> 1;
[...]
if ( v8 & 1 )
v9 = (v8 >> 1) ^ 0xEDB88320;
else
v9 = v8 >> 1;
v40[v1++] = v9;
}
while ( v1 < 0x100 );
v10 = shellcode_imp_dll((int)v40, 0x6AE6AF84);
v11 = shellcode_imp_dll((int)v40, 0x84C06EC6);
v30 = v12;
v13 = v11;
LODWORD(v34) = shellcode_imp_api(v10, (int)v40, 0x9CE3DCC);
DWORD1(v34) = shellcode_imp_api(v10, (int)v40, 0xDF2761CD);
DWORD2(v34) = shellcode_imp_api(v10, (int)v40, 0xF7C79EC4);
LODWORD(v35) = shellcode_imp_api(v10, (int)v40, 0xCD53C55B);
DWORD1(v36) = shellcode_imp_api(v10, (int)v40, 0xC97C2F79);
LODWORD(v36) = shellcode_imp_api(v10, (int)v40, 0x3FC18D0B);
DWORD2(v36) = shellcode_imp_api(v13, (int)v40, 0xD09F7D6);
DWORD1(v35) = shellcode_imp_api(v13, (int)v40, 0x9EEE7B06);
DWORD2(v35) = shellcode_imp_api(v13, (int)v40, 0xA4160E3A);
DWORD3(v35) = shellcode_imp_api(v13, (int)v40, 0x90480F70);
DWORD3(v36) = shellcode_imp_api(v13, (int)v40, 0x52FE165E);
v14 = ((int (__stdcall *)(_DWORD, _DWORD, signed int, signed int))v34)(0, *(_DWORD *)(a1 + 8), 0x3000, 64);
[...]
}
前两个哈希值,0x6AE6AF84 和 0x84C06EC6 应该是 'kernel32.dll'和'ntdll.dll' 的。我用 Python 来实现哈希过程,证实导入的那两个 DLL 确实是 kernel32 和 ntdll,然后我修改我的 Python 程序去解析它的导出表,想要知道函数导入的 API 的名字。我运行程序得到下面的 API。
Python>run
[+] kernel32.dll (6AE6AF84) : Parsing...
0x09CE3DCC --> VirtualAlloc
0xDF2761CD --> OpenProcess
0xF7C79EC4 --> ReadProcessMemory
0xCD53C55B --> VirtualFree
0xC97C2F79 --> GetProcAddress
0x3FC18D0B --> LoadLibraryA
[+] ntdll.dll (84C06EC6) : Parsing...
0x0D09F7D6 --> NtClose
0x9EEE7B06 --> NtCreateSection
0xA4160E3A --> NtMapViewOfSection
0x90480F70 --> NtUnmapViewOfSection
0x52FE165E --> RtlDecompressBuffer
用这些函数,进程中的代码将可以读取 dropper 的资源(bot和RC4密钥)并且映射 payload 在内存中的位置。最后,远处终止的线程内容将会被修改,这样它的 EIP 将指向第一个 shellcode,该线程继续。
流程图
☀ FlokiBot Payload
这个 payload 是基于熟知并已经分析过的 ZeuS 木马,所以我不会每件事都详细描述。至于 dropper,我会更着重讲去混淆部分以及 FlokiBot 改进部分的实现。
配置
我运行 ConfigDump.py 程序,然后得到下面的C&C :
$ python ConfigDump.py payload_32.vir
Successfully dumped config.bin.
URL: https://extensivee[.]bid/000L7bo11Nq36ou9cfjfb0rDZ17E7ULo_4agents/gate[.]php
用 IDAPython 完全静态去混淆
本文由 看雪翻译小组 lumou 编译,来源 Arnaud Delmas
❤ 往期热门内容推荐
......
更多优秀文章点击左下角“阅读原文”查看!
看雪论坛:http://bbs.pediy.com/
微信公众号 ID:ikanxue
微博:看雪安全
投稿、合作:www.kanxue.com