主要是汇编和调试方法要弄懂,做完这几个题应该也算入门了。感觉目前 ctf 中异构的 pwn 题以 arm 架构为主,mips 架构的比较少见,找到的题基本都是 hws 出的。题目囤了快半年,终于被我 pick 起来了😢 。之前卡住的原因主要出在工具上,前前后后踩了不少小白才会踩的坑,在复现前必须记录一下。
tools
分运行环境和逆向工具两部分。Buildroot 和 gdb-multiarch,应该有手就行。这里记录的都是我出过问题的工具。
MIPS运行环境
虚拟机是 x86 的平台,要运行 MIPS 指令集的程序就需要模拟运行环境。可以理解为虚拟机中的虚拟机 (? 原理是 Linux 的虚拟化技术: KVM是什么?一文带你快速了解Linux KVM 虚拟化 - Red Hat
目前用的最多的虚拟器就是 QEMU 吧。它工作的原理也好理解,就是仿真,通过动态的二进制转换在 x86 架构的系统上执行其他架构的程序。说到这不得不提,这个二进制转换并不只能是复杂指令集向精简指令集的转换的,反之也行。具体例子:给手机装Windows11!还能玩大型游戏?!_哔哩哔哩_bilibili 。
1 | sudo apt install --install-suggests qemu |
QEMU 有多个模式,最常见的是 qemu-user 和 qemu-system。顾名思义,qemu-user 是用户模式,用户一般只需运行程序,不需要接触到系统层面的配置。安装好后能在 /usr/bin/
下找到不同架构的模拟运行程序,图中的这些都是 qemu-user ,用它可以在 x86 平台上直接运行对应架构的程序。“直接运行”,也就是实现了交叉编译和交叉侦错。qemu 的这些模拟器跟 arm-linux-gnueabihf
、aarch64-linux-gnu
这些交叉编译器相似,而且安装完后它们都是在 /usr
下的。
而 qemu-system 就是系统模式。模拟一个系统的运行包括镜像选择、网卡配置等用户模式设置不了的内容,它的运行相对也就多步骤些。需要目标架构的内核,模拟出目标系统后有用户名、密码,将程序上传到该系统执行、侦错就需要它能与 Linux 系统通信(也就是配置网卡、联网)… 这种情况下,异构的程序并非直接在 x86 上运行,而是在异构系统上运行,直接在 x86 上运行的是异构系统。
讲了这么多理解的部分,是因为我觉着先理解后使用挺重要的。刚学的时候直接复制粘贴就用,不清楚工具的本质,也分不清它们的区别。(以至于国赛的时候给了个名字陌生的 qemu-user 就不会用了emmm)轩哥的文章给了很多链接,也写的很详细,看完应该差不多了:QEMU使用记录 Clang裁缝店 。
MIPSROP
用的是支持 MIPS 反汇编的 IDA Pro 7.5,下载链接: VanHoevenTR/IDA_Pro_7.5: IDA Pro 7.5 leak 。
直接解析二进制文件的时候会报错:
1 | ...\ida\plugins\idapython3_64.dll) error: 找不到指定的模块 |
原因是 python 的版本不正确。此时我的是 3.8 版本的,查了下,需要的是 3.9 的。(安装 3.9 的时候可以顺便勾选自动添加 PATH 的选项,之后用 pip 安装的时候就不用再配置或者找到 python 的目录了。)
前面的链接里有个工具包 ida75sp3_python39_win.zip
,用于配置 3.9 的环境。解压到 ida 根目录,全部覆盖,再运行 idapyswitch.exe
就可以了。不是 Windows 的可以下载对应版本:Python 3.9 support for IDA 7.5 – Hex Rays 。
之后运行报 cannot import name 'ida_shims' from 'shims'
的错,就是少了 shims 这个模块的意思。把
ida/plugins at master · tacnetsol/ida 的 shims 插件放进 plugins 目录下,或者用 install.py 把插件全下完也可以。到这一步基本上没什么报错了,也能进行 MIPS 的反汇编。除了这个 PowerPC Big Endian 的固件反汇编不出来: 基于 VxWorks 的嵌入式设备固件分析方法介绍 。正常来说下面这种 MIPS 的elf 是没问题的,so 这个 PowerPC Big Endian 的我决定复现完再去整。
前面的插件已经包括 mipsrop 。如果装好了的话能在 Edit -> Plugins
中看到。每次使用前需点击 Search -> mips rop gadgets
激活这个插件,idaPython 中 看到 MIPS ROP Finder activated
就是可用状态了。使用命令可以输入 mipsrop.help()
查看。
缓冲区溢出
题目来源:HWS赛题 入门 MIPS Pwn 、https://ctf.show/challenges。
重点关注 MIPS32 与 X86 架构下 函数调用与栈的不同之处。
32个通用寄存器表:
编号 | 寄存器名称 | 寄存器描述 |
---|---|---|
$0 | $zero | 始终为0,用于辅助运算判断、创建伪指令等 |
$1 | $at | 保留寄存器,用作汇编器的暂时变量 |
$2、$3 | $v0、$v1 | values,子程序返回值/非浮点结果(不够用时用内存) |
$4/5/6/7 | $a0/1/2/3 | aruments,函数的前四个参数(不够时用堆栈处理) |
$8~$15 | $t0/1/…/7 | temporaries,计算时用的临时寄存器 |
$16~$23 | $s0/1/…/7 | saved values,保留现场 |
$24、$25 | $t8、$t9 | temporaries,补充前面的 $t0 ~ $t7 |
$26、$27 | $k0、$k1 | 中断处理函数用于保存系统参数 |
$28 | $gp | global pointer,全局指针 |
$29 | $sp | stack pointer,堆栈指针,指向栈顶 |
$30 | $fp | frame pointer,保存栈指针(GNU)或$s8(SGI) |
$31 | $ra | return address,返回地址 |
- 返回地址:直接存入 $ra,不是堆栈。但嵌套调用的时候,被调函数1会先把原 $ra 的值压入堆栈,再进行下一次调用,此时 $ra 存放的是被调函数2的返回地址。
栈溢出漏洞的利用在叶子和非叶子函数间的主要区别就是,覆盖的是谁的返回地址。非叶子的情况和 x86 很像,比如上图的 B 函数,栈帧中存的是自己的返回地址(A),这时栈溢出能修改的就是它本身的返回地址。而像 C 这种叶子函数,本身的返回地址在 $ra 寄存器中,但payload足够长的话,能覆盖到 B 的栈帧,修改其父函数的返回地址。
Mplogin
mips-32-little
两种运行方式。一种是把题目提供的 so 文件放在本地的 /lib/
下、直接运行,一种是用 qemu-user运行、需要指定lib路径(注意是lib文件夹,不是so文件)。前面说到异构程序的运行需要交叉编译的工具辅助,这里的第一种方式并没有用到,看来关键在动态链接库。
漏洞点在输入密码的函数中。可以覆盖到 Password 输入时的长度参数位置,从而输入Passsword 时有栈溢出,也就是第二个 read 的时候。程序什么保护都没开,直接往栈上写 shellcode 跳转过去就可以了。
栈地址怎么泄露呢。输密码之前还有一个函数,看栈帧的时候发现,输入的变量v1后面就是 $fp 和 $ra 的值。输出用的是 printf 函数,v1填满时就能泄露出来。这个函数是由 main 调用的,是叶子函数。因此 $ra 存的是main的地址,$fp存的是进入这个函数前的栈顶地址。
该函数执行完后栈空间会回收,栈顶变回进入前的地址,下次进入设置密码函数时的栈顶也是这里。因此泄露这个地址就相当于拿到了栈溢出漏洞函数的栈顶地址。我在调试的过程中发现,在ubuntu 16中运行程序,栈溢出泄露不出数据,换成18又可以了…… 当踩个坑吧,还不知道为何如此😰。
exp:
1 | from pwn import* |
pwn
mips-32-big
程序整体逻辑是:
- 输入组数,后创建 n 组大小为 0x24 的空间(栈上)。
- 输入n次 id 和用户名,在对应组的位置存储。
堆溢出和栈溢出(长度和id均未规定)漏洞都有。可以通过 memcpy 的溢出,覆盖到返回地址。由于堆溢出漏洞,可写入 0x300 的 payload,应该是足够长的。
先确定偏移吧。说下调试过程。
一开始用的是 gdb-multiarch
,target remote
后下断点,再 continue
,每次都报 L1_MAP_ADDR_SPACE_BITS
的错误或者断不下来。用 ida remote debug
应该可以。用移植到 ubuntu 上的 ida 会更好些,避免文件复制到 Windows 后符号链接丢失等问题。如果还是想在 qemu-user 的状态下进行尝试,还是能用 x86 的 gdb 调的:路由器漏洞利用入门- 简书 。但 cyclic 用起来很不方便,因为程序是用 qemu 运行的,因此要复制到脚本上再运行调试。其实直接看栈帧也可以算出来,用 gdb-multiarch
remote 过去后,不下断点也能看到返回地址有没有被覆盖。
用户名经 strchr 函数返回后,把起始位置赋给 cur_i
。这里的输入位置与 $ra
的距离 就是 0x14+0x24+0x58=0x90
。
因为返回地址范围不合法,即使不下断点也能看出是否覆盖到了:
接下来就是写shellcode了。堆地址、栈地址、libc没有办法泄露,加上没有开启地址随机化,可以使用 rop gadget。利用 MIPSROP
,找到下面的这些 gadget。基本上都是 add $sp,(...)
后,储存在一个寄存器 A 中,再 jmp 到另一个寄存器 B 上执行。
1 | MIPS ROP Finder activated, found 842 controllable jumps between 0x00400000 and 0x004740A0 |
显然 addiu 到寄存器 A 的内容是我们输入的、可控的,可以在这范围内写 shellcode。之后 jmp 到寄存器 B,就需要再 jmp 回来到 A 存储的位置执行 shellcode。比如上面的第一条 gadget ,$s0 的内容应该是 jmp $a2
,第二条gadget的 $s2 应该是 jmp $a2
。而这些 s 寄存器在 MIPS 中是用于 saved value
的,有保存现场就会有恢复现场,因此在每个函数末尾 return 的时候都会有 lw $s?,?+var_?($sp)
的操作。前面 loc_400a2c
的图就是这个 pwn 函数的返回步骤,可以看到 $s0~$s7
全用上了,用 mipsrop 找到的 gadgets 都可用,可通过栈上的内容来控制 s寄存器的内容。从一个寄存器 jmp 到另一个寄存器,用的是临时寄存器,一般用 $t9:
1 | Python>mipsrop.find("move $t9,$a2") |
算好 $s0 、$a2的位置就可以了。用其他 gadget 的话,改下offset就可以了。
1 | from pwn import* |
顺便提一下,如果 ubuntu16 下的 shellcode 生成不了,可以检查下是不是 binutils 缺少 mips 架构版本的问题,正常来说应该是会返回一个 gnu 数组的。
1 | >>> from pwn import* |
原因是 ubuntu 16 自带的 binutils 版本旧,安装下对应的 gnu 就可以了。或者用 ubuntu18 以上的版本。
1 | apt-get install binutils-mips-linux-gnu |
ctfshow 内部赛 babystack
mips-32-little
ret2shellcode:
1 | from pwn import* |