Decrypting-Dropshot-Malware-with-Radare2-and-Cutter–Part1

用Radare2和Cutter解密APT33的Dropshot恶意程序 - 第一部分

译自:

https://www.megabeets.net/decrypting-dropshot-with-radare2-and-cutter-part-1/

序言

作为一个逆向工程师和恶意程序研究员,工具对我来说非常重要。我花费了很多时间搭建最好的恶意程序分析环境并选择对我的需求来说最棒的工具。过去两年中,Radare2是我在自动化逆向、脚本编写、CTF等等情境下的趁手工具。但这也意味着,我几乎从不用Radare2分析Windows环境下的恶意程序。主要原因是Radare2的命令行操作太过笨拙、复杂。IDA Pro更适合这种工作,能够快速查看函数、数据结构、重命名、注释等等。IDA Pro用起来感觉更符合直觉,这正是我做恶意软件分析时所需要的。然后Cutter问世了。

Cutter

几年以来,Radare2社区尝试过开发很多Radare2的图形界面,但没有一个比得上Cutter的。Cutter是一个基于QT C++的Radare图形前端。我觉得Cutter才是配得上Radare2的GUI。以下引用自Cutter的Github主页

Cutter并不是面向Radare2的现有用户开发的,它是为了受困于Radare2陡峭的学习曲线的用户所开发的。因为他们不喜欢命令行界面或是Radare2实在太难学了。

Cutter是一个只开发了一年的新项目,并且是唯一一个官方宣布的Radare的GUI项目。Cutter不仅通过友好且现代图形界面让用户充分使用Radare2功能,而且实现了跨平台。在篇文章中我将展示Cutter的功能以及如何使用,虽然这工具很符合直觉,你可能只靠自己就学会了。

下载并安装Cutter

Cutter全平台可用(Linux, OS X, Windows)。你既可以直接从这里下载,也能从源码自己编译来获取实时更新:

首先克隆仓库:

1
2
git clone --recurse-submodules https://github.com/radareorg/cutter
cd cutter

Linux编译

1
./build.sh

Windows编译

1
2
prepare_r2.bat
build.bat

如果出错了请看这里

Dropshot \ StoneDrill

Dropshot也叫做StoneDrill是一款和APT33组织相关的格盘恶意软件(对硬盘进行恶意清理行为)。该组织主要目标是沙特阿拉伯境内的组织。Dropshot是一个复杂的恶意软件样本,采用了高级反模拟技术并且有很多功能。该恶意软件极可能和Shamoon恶意软件有关。Dropshot先后被KasperskyFireEye深入分析。在这篇文章中关注于分析Dropshot如何在内部解密字符串来规避分析。在第二部分会分析Dropshot中包含实际载荷的加密资源的解密过程。

该样本可以再这里下载(密码:infected)。

注意这是一个真正的恶意软件,而且会破坏硬盘数据!请小心处理

推荐使用Linux机器进行静态分析。

开始

既然安装好了Cutter,现在启动并开始分析。启动Cutter后在’Open File’标签下选择新文件点击’open’。打开文件之后出现’Load Options’窗口,通过这个会话窗口告知Radare2如何分析这个文件。展开’Advanced options’,就能够选择特定的架构、CPU、文件格式等等。

为了分析的更加准确,我移动Analysis下面的拖动条来指定分析模式。然后去掉Autorename functions based on context (aan)选框来关闭函数的自动重命名功能。因为对于这个样本aan所使用的算法会使得一部分函数名难以理解。

点击’OK’之后就能看到Cutter的主窗口了,你的可能和图中的不一样但界面很好配置。比如点击”View->Preferences”,你就能修改主题颜色并且配置生成的汇编代码样式。窗口中的空间很灵活可以随意摆放,也能从”Windows”菜单中添加想要的新控件,花几分钟配置一下可以让Cutter更加趁手。

基本静态分析

当开始分析一款恶意软件,我经常先进行静态分析。基本的静态分析有时能判别该程序是否是恶意的,以及它的功能和基本情况。虽然基本的静态分析很直接而且方便,但是对于复杂的恶意软件来说依旧很不高效。所以在阅读汇编代码之前,让我们看看几款控件能告诉我们什么。

字符串控件

通过字符串空间没看到什么有趣的东西。有些看起来像文件名的字符串,比如”C-Dlt-C-Org-T.vbs”和”C-Dlt-C-Trsh-T.tmp”。还能看到一些熟悉API的函数名和库名,但没有什么需要注意的。

熵分析控件(Entropy)

另一个值得一看的属性是文件的熵。简单地说,熵(当前语境)是给定数值集合中随机性的量度。计算出的文件的熵在不同的文件中是相似的,一般在0.0到8.0之间。熵的值可靠地反映了该文件是否被打包、压缩或者内部包含打包、压缩的数据。一个打包过的二进制文件很可能有高熵值。至于多高,有的人说6.0足够高了,有的人说7.0及以上。我偏向于6.8作为二进制文件经过压缩或打包的标志。

通过Dashboard控件能简单查看熵值。

可以看到这个文件的熵值是7.1,很有可能是经过压缩打包的文件。通过Section控件能够详细地看到各个区段的熵值:

看看.rsrc段的熵值有多高,记住最高的熵值就是8.0了。毫无疑问,有些有意思的数据就存放在.rsrc段。我们将在这系列的第二部分详细分析。

理解字符串解密过程

当我浏览Dropshot的代码时,我发现他使用了一个不太复杂的方法去解密它的内置字符串(大部分)。该函数在分析中被注意到是因为它被调用了很多次而且主要是在LoadLibraryAGetProcAddress的调用之前。所以看起来是该程序是动态加载库和函数来迷惑分析人员的,该方法在恶意软件的编写中相当常见。这篇文章的目的不是为了分析清楚Dropshot的每个部分,而是介绍Cutter和Radare2脚本能够怎样使恶意软件分析人员受益,所以我会跳过寻找解密函数位置的详细过程。

正如之前所说的,我找到解密函数归功于它的高频次出现和在程序流中的重要性。如果你想凭自己的能力寻找它,请现在完成。

现在公布答案,解密函数位于0x4012a0,接受两个参数。下图会展示该函数的被调用现场。

图中的函数(0x4017a0)传递两个参数进入解密函数(0x4012a0)。第一个参数是地址0x41b8cc,第二个参数是0xb(十进制11)。是时候重命名这个解密函数方便分析了。简单的点击fcn.004012a0然后输入Shift + N或者右键点击Rename fcn.004012a0,输入新名字(strings_decrypter)然后点击’OK’。

然后我们能看到strings_decrypter的输出(eax)和另一个值1被作为参数被传给另一个函数0x4013b0。查看一下这个函数:

如果传递参数是0(eax == 0)该函数执行右分支,反之则是左分支。不论如何都会调用LoadLibraryA并传递经过解密函数得到的字符串作为参数。事实上右分支会加载ntdll.dll,左分支会加载kernel32.dll。简单来说,这个函数会加载需要的库来使用其中的函数,所以重命名该函数为load_ntdll_or_kernel32

在按需加载了需要的库之后,该函数对应库的句柄和解密的字符串调用GetProcAddress。我们能确定这个字符串是kernel32.dll导出的API函数名之一,几条指令之后这个加载的函数被调用。

正因尚且还不知道什么API函数会被调用,我们需要理解strings_decrypter如何工作以及传送的是参数实际上是什么。

分析解密函数

这是Cutter生成的解密函数图:

显然我们不会一步一步推敲这个函数,但仍需弄清总体思路。已知该函数接受两个参数,第一个是地址(arg_8h),第二个是数字(arg_ch)。在第一块(始于0x4012a0),能看到通过VirtualAlloc分配了大小为arg+ch+1的一块缓存区。然后把分配的缓存区地址保存到local_8h,把它重命名为buffer

之后0被存入local+4h。下一块是循环的开始,存储在arg_ch的值被传入edx每轮与local_4h进行比对。能想到arg_ch存储的是长度并且local_4h是循环的下标,所以分别重命名为lengthindex。现在弄明白了两个局部变量和一个参数,还剩下保存着地址的arg_8h。在这个例子中能看到0x41b8cc被传输进了strings_decrypter函数。打开Hexdump控件然后在上端的文本框输入0x41b8cc定位到该地址,能看到从0x41b8cc起始到0x0041b8e1结束的2字节字符串。选取这一段,就能通过右侧的工具栏就能够方便的生成它的C数组形式数据。

这功能确实挺方便的,Cutter能生成不同类型的数组形式来方便脚本编写,例如:

C half-words(Little Endian):

1
2
3
4
#define _BUFFER_SIZE 11
const uint16_t buffer[11] = {
0x0005, 0x0006, 0x000e, 0x0006, 0x001c, 0x0006, 0x0007, 0x000b,
0x000e, 0x0006, 0x0022};

Python:

1
2
3
4
import struct
buf = struct.pack ("22B", *[
0x05,0x00,0x06,0x00,0x0e,0x00,0x06,0x00,0x1c,0x00,0x06,
0x00,0x07,0x00,0x0b,0x00,0x0e,0x00,0x06,0x00,0x22,0x00])

Javascript:

1
var buffer = new Buffer("BQAGAA4ABgAcAAYABwALAA4ABgAiAA==", 'base64');

这数组会方便之后的脚本编写,现在先继续strings_decrypter的分析工作。进入循环能看到eax会保存index并且edx会保存之前提到的数组,之后[ecx + eax*2]的一字节传入edx。所以这里edx等于half_word_array[index*2]。之后缓存区被移到eax并被加上index的值,把eax设置到缓存区上特定偏移的位置。然后在0x004012eb能看到从一串预定义的字符串的[edx]偏移位置读入1字节到cl,双击那串字符串能看到全貌:AaCcdDeFfGhiKLlMmnNoOpPrRsSTtUuVvwWxyZz32.\EbgjHI _YQB:"/@\x0a\x0d\x1a。最后从cl拷贝那1比特到buffer的特定偏移位置,这个循环持续length次。

在这所有之后可以下结论说arg_8h保存的地址是一个偏移量构成的数组,每个偏移量对应预定义字符串中的字符,并且length是将要构造出的解密字符串的长度。由上,Dropshot通过给出偏移量数组的地址和字符串长度来构造出解密字符串。接下来用Python确认一下。

这又可以用到Cutter的一个功能,内置Jupyter notebook。我们不需要打开额外的Python命令行,直接可以使用Jupyter控件。

这功能太棒了!

接下来快速写个PoC试试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# The pre-defined decryption table (the string)
decryption_table = 'AaCcdDeFfGhiKLlMmnNoOpPrRsSTtUuVvwWxyZz32.\EbgjHI _YQB:"/@\x0a\x0d\x1a'

# The offsets array (0x41b8cc) which is passed to the function
offsets_array = [
0x05,0x00,0x06,0x00,0x0e,0x00,0x06,0x00,0x1c,0x00,0x06,
0x00,0x07,0x00,0x0b,0x00,0x0e,0x00,0x06,0x00,0x22,0x00]

# The length which is passed to the function
length = 11

decrypted_string = ''

for i in range(length):
decrypted_string += decryption_table[ offsets_array[ i*2 ] ]

print ("Decrypted: %s" % (decrypted_string))

在Jupyter中运行:

好的,能看到成功解密得到了”DeleteFileW”这个API函数名,于是能确认arg_8h和推测的一样。

现在弄明白了strings_decrypter是如何工作甚至解密了一条字符串,那就看看那里调用了这个函数并且全部解密来看一看。要看到strings_decrypter的交叉引用,点击它的名字并且按”X”。这会打开xrefs窗口,Cutter会显示出每个引用的预览信息来简化工作。

能看到不少strings_decrypter的调用,多的不适合手动解密,这便是Radare2和Cutter通过脚本配合完成工作的合适情景了。

通过脚本自动解密字符串

得益于r2pipe,脚本控制Radare2非常简单。

r2pipe的API基于r_core_cm_str()这一函数,他接受一个字符串指令然后返回字符串结果

r2pipe支持很多编程语言包括Python, NodeJS, Rust, C等等。

幸运的是Cutter内置的Jupyter控件包括了r2pipe这个库,按照过程写出r2pipe脚本:

  • 声明已知的常量、变量和函数包括解密函数、解密所用的预定义字符串
  • 导出预定义字符串并保存
  • 迭代所有访问预定义字符串的下标并保存变换后的字符
  • 手动解密所有字符串
  • 打印出解密好的字符串并在汇编代码中进行注释方便之后的进一步分析

首先定义我们已知的部分的地址:预定义字符串(解密表)和解密函数。

1
2
3
4
5
6
7
import cutter

# Declaration of decryption-table related variables
decryption_table = 0x41BA3C
decryption_table_end = 0x41BA77
decryption_table_len = decryption_table_end - decryption_table
decryption_function = 0x4012A0

之后需要让Radare2分析二进制文件来检测交叉引用和函数。aa是Radare2的基本的分析指令,而cutter.cmd是接受radare2指令并返回对应输出的交互函数。

1
cutter.cmd('aa')

之后导出解密表的内容到一个变量。pxj是用来打印16进制值的,j后缀标识返回JSON,并且cutter.cmdj会帮我们解析JSON输出内容、

1
2
3
# Dump the decryption table to a variable
decryption_table_content = cutter.cmdj(
"pxj %d @ %d" % (decryption_table_len, decryption_table))

现在我们有了所需的数据去迭代获取解密函数中对应的下标内容。

用Python的for迭代axtj指令的输出,这指令分析到地址的交叉引用并列出。每次循环中首先获取调用解密函数的两个参数,分别是偏移数组所在地址和字符串长度。通过pdj -2 @ <some xref address>可以获取,pdj显示汇编结果、-2显示当前地址之前的2个汇编指令。

1
2
3
4
5
6
7
8
9
10
11
# Iterate x-refs to the decryption function
for xref in cutter.cmdj('axtj %d' % decryption_function):
# Get the arguments passed to the decryption function: length and encrypted string
length_arg, offsets_arg = cutter.cmdj('pdj -2 @ %d' % (xref['from']))

# String variable to store the decrypted string
decrypted_string = ""

# Guard rail to avoid exception
if (not 'val' in length_arg):
continue

接下来是关键的解密部分,既然已经做过了PoC那么直接用for循环实现逻辑:

1
2
3
4
# Manually decrypt the encrypted string
for i in range(0, length_arg['val']):
decrypted_string += chr(decryption_table_content[cutter.cmdj(
'pxj 1 @ %d' % (offsets_arg['val'] + (i*2)))[0]])

好了现在decrypted_string保存着解密的结果,接下来打印出来并且完成注释就行。CC指令用来添加注释:

1
2
3
4
5
# Print the decrypted and the address it was referenced to the console
print(decrypted_string + " @ " + hex(xref['from']))

# Add comments to each call of the decryption function
cutter.cmd('CC Decrypted: %s @ %d' % (decrypted_string, xref['from']))

最后组合在一起就得到了最终的脚本:

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
import cutter

# Declaration of decryption-table related variables
decryption_table = 0x41BA3C
decryption_table_end = 0x41BA77
decryption_table_len = decryption_table_end - decryption_table
decryption_function = 0x4012A0

cutter.cmd('aa')

# Dump the decryption table to a variable
decryption_table_content = cutter.cmdj(
"pxj %d @ %d" % (decryption_table_len, decryption_table))

# Iterate x-refs to the decryption function
for xref in cutter.cmdj('axtj %d' % decryption_function):
# Get the arguments passed to the decryption function: length and encrypted string
length_arg, offsets_arg = cutter.cmdj('pdj -2 @ %d' % (xref['from']))

# String variable to store the decrypted string
decrypted_string = ""

# Guard rail to avoid exception
if (not 'val' in length_arg):
continue

# Manually decrypt the encrypted string
for i in range(0, length_arg['val']):
decrypted_string += chr(decryption_table_content[cutter.cmdj(
'pxj 1 @ %d' % (offsets_arg['val'] + (i*2)))[0]])

# Print the decrypted and the address it was referenced to the console
print(decrypted_string + " @ " + hex(xref['from']))

# Add comments to each call of the decryption function
cutter.cmd('CC Decrypted: %s @ %d' % (decrypted_string, xref['from']))

# Refresh the interface
cutter.refresh()

把脚本粘贴进Cutter的Jupyter控件然后执行,完成后查看注释控件就能看到脚本的成果:

还能看到汇编中的行内注释:

真棒!完成了,解密了所有的API函数名并且完成了行内注释来方便之后的分析。最后的脚本能在这里找到。

尾声

这是用Radare2和Cutter解密APT33的Dropshot恶意程序的第一部分。我们熟悉了Cutter、Radare2 GUI并且用r2pipe写了一个解密脚本。

这系列的下一部分会分析Dropshot如何解密其中的实际载荷,所以敬请期待!