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
尝试找到漏洞点,但是不知道为啥这个就是搜不出来。
于是,我们可以通过内存搜索的方式,搜素 HEX
,这里 ssid
对应的是 73 73 69 64
。
然后我们把选项选到 IDA View-A
中,然后拖到程序最上面后,按 ALT
+ B
进行搜索,如下图所示
设置完毕后,点击 OK
它会搜索到第一个。
开头和末尾的
\x00
是我不希望有其他字符的干扰,想进行全字匹配。
然后我们就找到了一个,然后交叉引用检查是否是漏洞点,若不是那就再用 Ctrl
+ B
重复搜索下一个,直到找到。
经过甄别,发现在偏移 0xe35b8
处才是。
这里我们找到后,就使用交叉引用跳转过去,是在函数 form_fast_setting_wifi_set
的开头这个位置。
到这里我们可以很清晰看到第 34
和 35
行存在两个 strcpy
将获取的 ssid
数据直接拷贝到了 s
和 dest
。 相应的这两个变量分别定义在第 12
行和 13
行,是两个局部变量,因此是拷贝到了栈空间,并且可以清晰看到没有对源数据的长度进行检测,而目标缓冲区只有 64
字节的长度,因此存在经典栈溢出漏洞。
这里我就懒得去一步一步分析控制流找到这个参数具体是哪部分受控的,我们直接简单粗暴,往这个接口中灌入很长的
ssid
参数,观测程序是否崩溃来看我们是否能正常触发这个接口,如果不能再进一步调试找条件。
0x03 漏洞调试
还是和之前一样,所以这里我们就不再阐述在什么地方执行什么指令的。详情可以参考前两个博文。
运行程序
1 | sudo chroot . ./qemu-arm-static ./bin/httpd |
先运行一个 poc
观测程序崩溃
1 | #!/usr/bin/python |
运行后,程序崩溃了。
这里每次程序 poc 程序都要运行两次才会触发,我也不清楚为啥,但是不影响使用。
接下来,我们期望定位到造成崩溃的数据是什么,从而构建 payload
。这里我们将 payload
字符进行替换,在 0x67090
处下断点 ,观测第二次 strcpy
执行前,其获取的 src
的值,找到崩溃处地址值是 yaaa
,然后得到偏移是 96
。
你可以下断点进行观测
b *0x6707C
strcpy
执行前的参数
但我们会发现,此次崩溃并非直接覆盖到了返回地址,导致 PC
值异常而崩溃,事实上,此时的崩溃多半是一个源地址不可读崩溃。
这是由于在原来的程序中,存在两个连续的代码如下,其中 s
和 src
都是局部变量,而且由于传参规则,程序通过 fp
索引得到 src
会比 s
的地址高(这一点可以通过调试看到,也可以在 ida
中看到),所以当我们从 s
开始覆盖值时,一直覆盖到当前函数的返回地址时,一定会将 src
的值给覆盖了。
1 | strcpy(s, src); |
因此,当第一行 strcpy
执行完毕溢出时会将 src
的值修改掉,这样第二次 strcpy
可能会因为访问非法地址而崩溃,因此我们需要保证第一次溢出之后,覆盖到 src
的地方是一个任意的可读地址并且不含有 \x00
即可(防止截断)。
这里我就偷懒了,直接在网上找了个别人的在
libc
中的可读不含0地址:libc_base+0x64144
这里可能还有一个疑问,第二次的数据拷贝是否会将第一次精心布置的栈空间给破环了呢?
事实上不会的。因为 dest
局部变量定义在 s
局部变量的前面,这导致他在栈中的地址是低于 s
的,因此从更小的地方覆盖过来,不会将最终的 PC
破坏掉。
1 | +------+-----+ |
由于程序后面有个控制流会从当前函数的参数提取值赋予 v19
所以不能直接一次性给太多的数据,不然会溢出将参数都改了,导致这个 v19
获取的值是畸形,程序后面会崩溃。
所以我们可以通过调试,让溢出能刚好覆盖到返回地址即可,其他的都不需要多覆盖了,要想得到这个值只需要在函数退出的地方下断点即可得到退出时返回到 PC
的栈帧的地址,然后和一开始覆盖的地址做差即可。
所以我们在函数 form_fast_setting_wifi_set
退出前下断点进行观测即可。
所以我们在调试中,同时下两个断点,第一个用于观测 strcpy
执行时的参数即 s
的地址,然后第二个用于观测返回时哪个栈地址会压入 PC
两个相减就是我们要溢出的返回地址的偏移
1 | b *0x6707C |
写一个小小的不会溢出的 PoC
1 | #!/usr/bin/python |
然后我们观测到第一次 strcpy
执行时,第一个参数即 s
的地址是 0xfffef318
然后我们直接 c
让其命中第二个断点,即返回返回前,此时我们得到了压入 PC
的栈地址是 0xfffef394
于是我们直接计算得到会覆盖到 PC
的数据的偏移为 0xfffef394-0xfffef318=0x7c=124
,所以我们可以进一步修正我们的 PoC
libc_base
的获取方法还是和之前一样,这里不再赘述
1 | #!/usr/bin/python |
然后不用下断点,直接运行程序,可以看到程序崩溃,并且 PC=0xdeadbeee
。
0xdeadbeef
->0xdeadbeee
从f
变成e
也是arm
指令转换,将最后一位置为0
了,这里我们也不再赘述。
所以我们就成功劫持了控制流了,接下来,就按照 《CVE-2018-18708 调试复现分析》 的方式进行利用即可。
因为环境都一样,只开了 NX
所以我们还是构建 ROP 链即可 。
一样的 ROP 原语
1 | ROPgadget --binary ./lib/libc.so.0 --only 'mov|blx' |
于是我们就可以组装出一个可以用的 exp
1 | #!/usr/bin/python |
简单运行一下,程序成功输出 Hello, Ron on CVE-2018-16333
。
差不多了,和前面的太像了。
0x04 内存布局变化图
最终在调试过程中的一些关键位置时的内存变化图如下
0x05 exp
1 | #!/usr/bin/python |
0x06 参考链接
简单看了看漏洞点位置 https://www.anquanke.com/post/id/231445