译者:興趣使然的小胃
预估稿费:200RMB
投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿
一、前言
在本文中,我们介绍了一款僵尸程序,这款僵尸程序通过暴力破解SSH凭据,使用一系列攻击技术来攻击IoT设备,借助这些设备达到传播目的。这款恶意软件的主要传播者为某位中国黑客,这名黑客对C++及C语言中的构造方法较为了解,但用的最多的还是C++中的std::string
类。这款僵尸程序几年前针对的只是x86_64平台,但现在情况有所改变。根据我们对Linux/AES.DDoS
僵尸程序导出符号及C++构造函数的观察,我们判断该应用所使用的是C++语言。
本文中我们用到的分析工具包括:
1、gdb-peda:https://github.com/longld/peda
2、BinaryNinja:https://binary.ninja/
3、ltrace:https://linux.die.net/man/1/ltrace
4、radare2:http://rada.re/r/
二、样本概况
如果我们使用file命令分析这个程序,我们可以得到如下结果:
ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, for GNU/Linux 2.6.14, not stripped.
因此,这是一个32位ELF可执行文件(UNIX系统上的COFF格式),ARM架构,静态链接了所有的库文件。IoT僵尸程序经常在程序中静态链接libc库,因为许多系统上的程序库通常处于不完整甚至损坏状态。因此这类程序一般不会动态链接可执行程序,相反它们会采用静态链接方案。这款恶意软件没有剔除这些有价值信息,因此我们可以在程序中找到一些有意义的对象名,使分析工作大为简化。出于某些奇怪的原因,攻击者使用已有12年历史的一个Linux内核来编译这款软件,这表明恶意软件很有可能是在某个IoT设备或某台老式主机上编译而成。
样本哈希值如下:
MD5:125679536fd4dd82106482ea1b4c1726
SHA1:6caf6a6cf1bc03a038e878431db54c2127bfc2c1
三、ARM快速简介
ARM是一种32位指令集架构,因此所有的寄存器都是32位寄存器。在ARM架构中,标准的调用约定是将参数存放于r0到r3寄存器中。r0到r3只有4个寄存器,因此如果函数使用的参数个数大于3个,那么剩余的参数可以存放于栈上。我们可以使用16个寄存器,每个寄存器都有其特殊用途,具体用途如下:
1、r0、r1、r2、r3用于参数传递,r0通常用来保存函数返回值;
2、r4、r5、r6、r7、r8、r9用来存放程序内部变量;
3、r10存放当前栈限制指针;
4、r11存放栈帧指针;
5、r12也可以用作程序内部变量寄存器,但我们无法保证调用期间该寄存器保持不变;
6、r13存放堆栈指针(stack pointer,SP);
7、r14为连接寄存器(link register),指向调用者;
8、r15存放程序计数器。
掌握这些基本知识后,我们可以继续开展后续研究工作。
四、分析main函数
恶意软件会从原始入口点(main)启动,该函数地址为0x13DEC。我们可以使用rabin2查找原始入口点,具体命令为rabin2 -s kfts | grep "type=FUNC name=main"
,这条命令的输出结果如下所示:
vaddr=0x00013dec paddr=0x0000bdec ord=5366 fwd=NONE sz=688 bind=GLOBAL type=FUNC name=main
这个程序没有经过加壳处理。main方法会执行名为get_executable_name的一个函数分支,该函数通过readlink(..)
读取符号链接/proc/self/exe
。如果在进程内部读取这个符号链接,会得到程序运行时所处的具体位置。从反汇编结果中我们可知,程序创建了一个std::string
字符串,将包含当前执行路径的char数组复制到该字符串中。随后的本地持久化过程中会用到这个字符串。
随后,程序会检查自身是否处于运行状态,根据结果决定是否添加为启动程序。在check_running
流程中,程序会执行“ps -e”命令,休眠2秒,然后查找结果中是否包含当前的程序名。如果已包含当前程序名,则跳转到exit分支,退出代码为0(存放于r0寄存器中),以完全关闭当前进程。如果不包含当前程序名,则执行持久化分支。简而言之,如果之前该已处于运行状态,则程序会直接退出执行。
五、持久化方法
恶意软件通过往/etc/rc.local
以及/etc/init.d/boot.local
文件(auto_boot函数)中添加内容实现本地持久化,在覆盖这些文件之前,恶意软件会先检查之前是否已执行过覆盖操作。当所有的系统服务启动完毕后,/etc/rc.local
会执行某些命令。然而,恶意软件所使用的持久化技术相对而言较为业余,它构造了一个shell命令,然后使用system
函数来执行这条命令(理论上这种方法相当于调用exec,然后挂起等待被调用的程序返回)。
恶意软件使用sed程序将某个格式化字符串写入目标文件(样本中有几处用到了字符串操作,比如sed -i -e '2 i%s/%s' /etc/rc.local
等),然后使用system
方法调用该字符串完成命令执行过程,如前文所述。所有格式化字符串操作所用的缓冲区大小为300字节,缓冲区的虚拟地址为0x9F48。从技术角度来看,由于恶意软件没有过滤输入数据,因此我们可以操控恶意软件所在的路径,利用字符串格式化漏洞技术攻击该程序。攻击成功后,我们可以修改栈状态、读取本地变量、覆盖函数及数据地址等。这种持久化方法是这款僵尸程序使用的唯一的持久化方法。
六、信息收集
随后,进程执行fork语句,通过setsid(..)
脱离父进程。脱离父进程会话时,该进程会关闭从父进程继承的所有文件描述符(0-3)。随后,恶意软件创建一个线程,调用SendInfo
函数,通过该函数收集信息,这类信息包括系统中的CPU数量、网速、系统CPU负载、网络适配器的本地地址等。
接下来程序会调用get_occupy
子函数。程序遍历系统中的所有CPU,计算平均负载。该循环以r3寄存器作为计数器,然后调用blt指令。当blt的第一个操作数小于第二个操作数时,就会跳转到其他分支。在x86平台上,这条指令等价于jle指令。需要注意的是,在早期版本中,这款恶意软件会创建一个backdoorA线程来执行类似操作,但这种情况现已不复存在。
恶意软件通过读取/proc/net/dev
文件来获取网络适配器信息。打开文件后,恶意软件从文件头部开始解析整个文件,从默认适配器上获取本地IP地址。
奇怪的地方在于,这款恶意软件会创建一些没有实际意义的统计数据。比如,该软件会生成一个随机的值,将该值作为网速值加以使用。恶意软件在fake_net_speed
子函数中以time函数的结果为种子来调用srandom函数,以生成随机值。具体过程是将第一个参数“0”传递给time函数(通常情况下,该函数接受指向time_t的一个指针,这里我们不需要传入任何结构体),随后,time的返回值保存到r3寄存器中,再传入r0,最终作为srandom的参数加以使用。可能是编译器因为某种原因变得比较奇怪才出现这种执行流程。
通过子函数生成随机值后,恶意软件利用sprintf
语句生成代表网速的一个字符串,其单位为MBps。这种情况非常奇怪,表明恶意软件因为某种原因生成了一个虚假的网速值,我能想到的唯一解释是,某人聘请了一些技艺不精的码农,而这些码农无法实现这一功能,因此他们只好造假来迎合客户或老板的需求。
子函数会将这些值发给主C2服务器。
七、通信初始化
分析完这些过程后,接下来我们面对的是恶意软件的核心功能,在这个过程中,恶意软件会连接到C2服务器,并接收服务器返回的指令。恶意软件会在程序主体区域调用ConnectServer
函数(地址为0xCA1C),该函数中会调用ServerConnectCli
(地址为0xB5BC)函数以获得C2服务器对应的socket,将socket存放到MainSocket
这个全局变量中。我们来看看ServerConnectCli
这个函数。
ServerConnectCli
首先会创建协议类型为TCP的一个socket,如果无法成功创建socket,则会跳转到位于0xB654
处的分支,通过perror(..)
函数显示可读的错误信息。
如果你对C比较熟悉,那么上述代码等同于如下语句:
r0 = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
作者部分混淆了恶意软件要连接的端口号。原始的端口号存放于r3寄存器中,通过lsl
指令将这个值左移16个字节(0x10),然后再往右移16个字节,最终得到混淆后的端口号。这个过程对应的汇编代码如下所示:
mov r3, r0
strh r3, [r3, #0x104]
lsl r3, r3, #0x10 // shift r3 RIGHT by 0x10
lrl r3, r3, #0x10 // shift r3 LEFT by 0x10
mov r0, r3
bl htons
对应的伪代码如下:
r3 = ((r3 << 0x10) >> 0x10)
恶意软件在0xB1B8
处使用了名为AnalysisAddress
的一个函数,乍一看人们可能会认为这是作者用来转移研究人员注意力的函数,但实际上该函数的唯一功能就是填充一个hostent
结构体,后续的连接过程中会用到这个结构体。该函数传入存放于r0寄存器中的一个参数(参数位于0xC1FC8
处,值为61.147.91.53
),返回值(gethostbyname
函数的返回结果)存放到r3寄存器中,恶意软件会基于该返回值做一些后续处理。
恶意软件使用IP_DROP_SOURCE_MEMBERSHIP
标志两次调用setsockopt
函数,该步骤完成后,恶意软件调用connect
函数向C2服务器发起初始连接请求。恶意软件在socket上使用了select
以及getsockopt
方法,以确保非阻塞socket连接成功。如果连接不成功,恶意软件会关闭socket并退出执行。
如果ServerConnectCli
返回了可用的socket,恶意软件会将该值保存到MainSocket全局变量中。随后,恶意软件会继续收集已感染设备的更多统计信息。首先,它会获取运行该程序的用户名,将用户名保存到r11寄存器中。从代码中可知,当uname
调用失败时,程序会将“Unknown”字符串保存到目标缓冲区中,该缓冲区的地址保存在r11寄存器中,如果uname
调用成功,则会跳转到0xCA8C
分支。
恶意软件通过GetCpuInfo
函数收集已感染主机的更多信息。这个过程比较简单,这里稍微介绍一下。恶意软件会打开/proc/cpuinfo
虚拟文件,按单字(WORD)大小逐块读取数据,直至读取到EOF(-1)字符,然后调用fclose
函数释放已打开的文件。解析完毕后,恶意软件可以获取主机的CPU数以及CPU时钟频率(单位为MHz)。
随后,恶意软件调用sysinfo(..)
函数,将结果存放于sysinfo
结构体中(结构体地址存放于r3寄存器中)。这个结构体中包含一些成员变量,比如交换分区大小、RAM总量等等,这些信息格式化后会存放到一个字符串中。
作者在这里中留下了“Hacker”字符串,似乎在表明自己玩世不恭的态度。恶意软件所用的格式化字符串为VERSONEX:Linux-%s|%d|%d MHz|%dMB|%dMB|%s
(攻击者出现了拼写上的错误,这里应为“version”)。奇怪的是,在调用snprintf
之前,程序先调用了sprintf
函数(snprintf
函数知道缓冲区的长度,因此可以避免出现格式化字符串漏洞)。码农通常倾向于使用同一个函数,因此这种情况表明这款恶意软件的维护人员应不止一个人。
在控制流程中,恶意软件通过MainSocket变量所对应的socket来发送信息,如果信息发送失败,恶意软件会跳转到另一个执行分支,关闭当前socket。
在ARM平台上,如果上一条比较指令的两个操作数相等(设置相应的标志位),那么beq
指令就会跳转到0xCBD4
地址处。前面提到过,该子函数的功能是关闭socket。如果函数调用成功,恶意软件会调用select
函数,准备读取来自C2服务器的响应数据。在读取C2服务器的响应数据之前,程序首先会清空大小为0x1380
的缓冲区。如果缓冲区无法成功置零,程序会再次打印错误信息,跳转到另一个子函数,关闭socket并清空上下文环境。
八、总结
根据本文分析,我们可知有多个开发者在维护这款恶意软件。此外,我们也知道开发者在socket编程方面有一定经验。作者在程序中也用到了帕斯卡拼写法(如“LikeThis”)来命名一些函数(如GetCpuInfo
),这表明作者可能习惯于在Windows平台上完成开发工作。
本文到此为止,后面我们会继续分析这款僵尸程序的攻击手法及程序所支持的具体命令。