Linux恶意软件分析:自定义加密算法的不足

一、前言

Linux是我最喜欢的操作系统之一,人们很少能看到针对该系统的恶意软件,因此我很好奇,当我的蜜罐捕捉到Linux恶意软件时是怎样的一种场景。本文介绍了我对Linux恶意软件样本的一次分析过程,重点分析了该软件所用的解密函数。通过这个案例,我们就能知道为何使用自己的加密算法并不是特别安全。

 

二、样本分析

与常见的分析过程一样,首先我把该样本提交到VirusTotal,观察检测结果,如下所示:

从结果中可知,59家厂商中只有34家成功识别出这一款恶意软件,考虑到这是一个Linux程序,这个结果也比较正常。该程序没有经过封装,VirusTotal的分析结果中也没有给出特别有趣的信息。接下来,我通过rabin2工具获取了该样本的一些基本信息,如下所示:

arch     x86
binsz    646674
bintype  elf
bits     32
canary   false
class    ELF32
crypto   false
endian   little
havecode true
lang     c
linenum  true
lsyms    true
machine  Intel 80386
maxopsz  16
minopsz  1
nx       true
os       linux
pcalign  0
pic      false
relocs   true
rpath    NONE
static   true
stripped false
subsys   linux
va       true

同样没有得到比较有趣的信息。这款Linux程序使用C语言编写而成。然而,如果我们在rabin2中使用-E标志,我们可以分析其中的导出信息,事情开始变得有趣起来:

[Exports]
956 0x00020070 0x08068070 GLOBAL   FUNC 1365 __vsyslog_chk
957 0x00073c00 0x080bbc00 GLOBAL OBJECT   36 _nl_C_LC_CTYPE
959 0x00021790 0x08069790 GLOBAL   FUNC    8 __stack_chk_fail_local
965 0x0008b9e8 0x080d49e8 GLOBAL OBJECT    4 __morecore
966 0x0001fc80 0x08067c80 GLOBAL   FUNC   41 __getdtablesize
967 0x00014f80 0x0805cf80 GLOBAL   FUNC   40 _IO_remove_marker
969 0x00009090 0x08051090 GLOBAL   FUNC  291 __libc_sigaction
970 0x00051ef0 0x08099ef0 GLOBAL   FUNC   69 __isnanl
971 0x00042ae0 0x0808aae0 GLOBAL   FUNC  170 __libc_pread
974 0x0001db60 0x08065b60 GLOBAL   FUNC   34 strcpy
975 0x0003c460 0x08084460 GLOBAL   FUNC  200 _IO_wdefault_xsgetn
976 0x00012080 0x0805a080 GLOBAL   FUNC    9 __fcloseall
977 0x00020630 0x08068630 GLOBAL   FUNC   44 __syslog
978 0x00000ba3 0x08048ba3 GLOBAL   FUNC   74 V8ULRand
979 0x0000e600 0x08056600 GLOBAL   FUNC  234 __setstate_r
980 0x0006ce50 0x080b4e50 GLOBAL   FUNC  213 _dl_vsym

从输出结果中我们可以清楚地看到所有的对象信息及函数名称。作者可能会在调试该恶意软件的时候用到这些信息。现在我们不需要使用radare2来为这些函数设置名称,因为一切已近在眼前。接下来,我们可以将程序传给radare2,查找主函数。

我首先注意到的就是如下一段代码:

从代码注释中,我们可以看到名为strHost的一个字符串,同时我们也知道strHost作为参数被传递给DecryptData函数。据此我们可以猜测,strHost中可能会保存恶意软件需要连接的某些主机名或者地址信息,DecryptData会解密这个字符串,解析出正确结果,还原“yy123-e4213-mfs”对应的真实值。现在,我们可以跳转到DecryptData,分析代码内容。

首先,我们可以看到某个计数器变量被赋值为0(请注意:我使用afvn命令重命名了该函数中的所有变量,以便后续分析)。接下来,代码开始循环检查计数器变量值是否小于字符串的长度(即以参数传入的字符串)。如果小于字符串长度,则会跳转到该循环对应的主逻辑代码中。

如果你在逆向分析方面不是特别熟练,这段代码看起来可能比较复杂,这里我会详细介绍这段代码的功能。程序会将字符串的当前索引传递给另一个计数器变量(不要去纠结作者为什么这么做,有许多方法可以完成这个任务),然后使用0x55555556这个十六进制数来声明一个新的变量,这个值充当了“魔术数”(magic number)角色,可以提高除法运算的性能。这个数字对应的十进制数值为1431655766。如果我们将某个数乘以这个数,然后将结果逻辑右移32位,所得的结果与原数除以3的结果相同。虽然这是我第一次看到这个魔术数,我会尽力去解释这个过程,因为该过程的确非常有趣。

以数字6为例,该数字除以3得到的结果为2。在这种情况下,6 * 1431655766的结果为8589934596,二进制表示如下:

1000000000000000000000000000000100

现在如果我们将结果右移32位,得到如下结果:

0000000000000000000000000000000010

该结果对应的正是十进制的2!这是非常有趣的一个小技巧,我们可以借此了解计算机架构的工作过程。这的确是除法运算,但并没有用到我们常见的除法操作!大家可以阅读这篇文章了解这方面的更多信息。

回到这个程序上面来,程序会通过这个魔术数来处理计数器,将某个变量的值赋值为0、1或者2。如果变量值为1,则跳转到某个代码片段,否则就跳转到另一个代码片段。接下来我们来看一下这两个代码分支。

这两个分支功能基本相同,只有些许差别。刚开始时这两个分支都会检查当前索引处的字符的十六进制值是否小于或等于0x20(空格符)。如果满足该条件,就不处理这个字符,增加计数器值,继续下一次迭代过程。如果不满足该条件,那么代码会检查该值是否等于0x7f(ASCII表中的DELETE字符)。如果等于该值,则不处理这个字符,继续下一次迭代过程。如果不等于,那么继续解密过程。这样做会将待处理的字符限制在可写的ASCII字符范围内,即0x20–0x7f。

因此,如果计算出来的魔术变量等于1,则我们会进入左边分支,在字符范围检查过程后,将该字母减1。也就是说,B会变成A,D会变成C,以此类推。如果魔术变量不等于1,那么则将字母加1,A变成B,C变成D,以此类推。整个过程就是这么简单,并不可怕,根据该过程我们很容易就能解密出原始字符串。现在来看看strHost字符串(即“yy123-e4213-mfs”)的处理结果。

首先,将该字符串中的每个字符都对应成0、1或者2,具体值与魔术数的处理结果相同。

y y 1 2 3 - e 4 2 1 3 - m f s
0 1 2 0 1 2 0 1 2 0 1 2 0 1 2

接下来,对于每个字符,如果对应的值不为1,那么我会将其ASCII值1,对于值为1的那些字符,我会将其ASCII值1。

zx232.f3322.net

从结果可知,这款恶意软件很有可能会连接到zx232.f3322.net这个主机,该主机很有可能是命令与控制中心。

我写了个python脚本,可以解压使用这种加密方式的所有字符串:

def decrypt_string(string, length):
    counter = 0 #local_4h
    local_18 = 0
    local_1c = 0
    new_string = ''
    while counter < length:
        if not local_18 == 1:
            letter = string[counter]
            if not letter == ' ':
                new_string += chr(ord(letter)+1)
        else:
            letter = string[counter]
            if not letter == ' ':
                new_string += chr(ord(letter)-1)
        counter += 1
        local_18 += 1
        if local_18 > 2:
            local_18 = 0

该样本中找到的其他变量以及解密后的结果如下所示:

该样本的剩余部分非常简单。样本会将自身拷贝至/etc/.zl/tmp/.lz以及/etc/init.d/.zl文件。接下来,恶意软件会在rc2.drc3.drc4.d以及rc5.d中创建副本,并以符号链接方式将这些副本链接到/etc/init.d中的.zl文件。由于系统在启动时会调用这些文件,因此这样做就能达到本地持久化目标。如果你在系统中看到这些文件,就可以将其作为基于主机的标识符来识别这款恶意软件。接下来,恶意软件会ping地址为zx232.f3322.net服务器的54188端口,希望收到某些响应数据,我们可以将其作为基于网络的标识符来识别这款恶意软件。我还没有看到这款恶意软件收到过服务器返回的响应数据,这可能由几个原因所造成。比如,服务器可能已经下线、控制者当时不想发送命令等等。我猜测程序会从服务器那收到命令,然后再执行这些命令。

 

三、总结

这款恶意软件最有趣的部分就是解密函数了,因此从这篇文章中,我们可以吸取一些教训,那就是使用自定义的加密算法并不是个好主意。对于这个样本,我们可利用这种算法解密许多字符串,了解恶意软件的具体功能。分析这个样本的过程非常有趣,希望大家阅读本文时也能兴趣盎然。在恶意软件分析方面我还是一名新手,因此如果大家有什么意见或者建议欢迎随时向我反馈。大家可以通过我的Twitter以及Linkedin页面来联系我。

感谢阅读本文,希望大家享受逆向分析过程。

(完)