Loongarch ROP
比赛时发现了这是LoongArch的ROP,然后不太会找gadget就放弃了。赛后看大佬的writeup,发现只要找到一个关键的来自_dl_runtime_resolve的gadget,就可以万事大吉了。
复现参考:CTFtime.org / UTCTF 2023 / Bing Chilling / Writeup
环境准备
我们都知道 Linux 下的可执行文件是 ELF 格式,但 ELF 也分架构,比如这个 binary 就并不是 amd64 架构的,而是 Loongarch 龙架构。
1 | $ file hello |
可以看到其 ELF Header 中的 arch 字段值为 0x102,是一个 file 未知的架构。在网上查询 0x102,可以知道这是龙架构。为了调试这个 binary,我们需要一台龙架构真机……或者是一个龙芯模拟器。此外,我们还需要能够静态分析这个 binary 的工具,比如 objdump。
著名的模拟器 qemu 在其 7.1.0 版本引入了对龙架构模拟的支持,因此我们安装下最新的 qemu 就行了。
从 Releases · loongson/build-tools (github.com) 这里可以找到一些龙架构的交叉编译(跨架构生成 ELF)的工具,其中就包括龙架构的 objdump。
最后,为了动态调试,可能还需要一个支持龙架构的 gdb。gdb 在 13.1 版本引入了对龙架构调试的支持,可以通过下面的指令来在 /opt/gdb 目录下编译支持龙架构的 gdb(中途遇到报错多半是缺少某个库,可以上网搜)(执行指令的位置无所谓,不过在 root 的目录下需要加很多 sudo ……):
1 | wget https://ftp.gnu.org/gnu/gdb/gdb-13.1.tar.xz |
编译得到的 gdb 位于 /opt/gdb/bin/loongarch64-unknown-linux-gnu-gdb
程序分析
学过 mips 和 riscv 的朋友会对 LoongArch 的指令集感到比较熟悉,LoongArch 也是 risc。它的寄存器昵称和 riscv 的几乎一模一样,比如存放 return address 的 ra。
从 pwner 的视角来看,龙架构:
- 系统调用的参数依次存放在 a0, a1, a2, a3, a4, ……
- 系统调用编号存放在:a7
- 返回地址存放在 ra 寄存器中
返回指令是jirl $zero, $ra, 0
bl
用作 call,先把返回地址存到 ra 然后跳转到目标地址- syscall 指令就是 syscall
使用 cross tool 中的 objdump 可以查看 binary 的汇编,我们直接看 main 函数:
1 | 0000000120000520 <main>: |
从中可以观察到很多经典的过程调用行为,比如开始时拓展栈空间、存放返回地址等信息;结束时取回返回地址、恢复栈空间。毕竟栈这种 LIFO 的结构对于过程调用还是非常根本的。
注意到,main 函数会依次调用 puts、gets 和 printf。有 gets 不就可以直接栈溢出了吗?
使用 qemu-loongarch64 hello
,然后输入一大段 A,果然 qemu 报了 segmentation fault。
接下来的问题就是,我们已经能够控制栈了,那么 LoongArch 的栈上可以 ROP 吗?答案是可以。虽然 LoongArch 有专门用来存返回地址的 ra 寄存器,但很多过程仍然会把返回地址存到栈上,这是因为这些过程自己也需要调用其他的过程。因此,LoongArch 过程的结束既有从栈上读取返回地址,又有返回指令,可以进行 ROP。
漏洞利用
我们的目标是 get shell,但这个 hello 虽然是静态链接的,却没有 system 函数。不过,我们可以直接找到 syscall gadget:
1 | 120013e4c: 002b0000 syscall 0x0 |
至于 LoongArch Linux 的 Syscall Table,我好像只在 [6/14, LoongArch] Linux Syscall Interface - Patchwork (ozlabs. Org) 有看到,其中 execve 是 221。
只要能够控制 $a0 指向一个 “/bin/sh” 的字符串,$a1 和 $a2 控制为 0,就可以 get shell。我们需要为此找到合适的 gadget。
从本文参考的文章那边找到了一个非常牛逼的 gadget,来自 _dl_runtime_resolve 函数:
1 | 120048098: 0015008d move $t1, $a0 |
似乎不管在哪个架构中,_dl_runtime_resolve 函数的功能都是保存寄存器的值到栈中,然后调用_dl_fixup执行具体的功能,然后从栈中恢复寄存器。因此以后要是遇到了什么riscv pwn,也可以使用这个gadget。
这个 gadget 能够控制所有参数寄存器,但需要提前把返回地址存在 $a0 中。所以继续手工找 gadget:
1 | 12000bc54: 28c0a061 ld.d $ra, $sp, 40(0x28) |
这个 gadget 可以把 $s1 移到 $a0 ,那就继续找可以改 $s1 的 gadget:
1 | 12000be90: 28c06061 ld.d $ra, $sp, 24(0x18) |
有了这三个 gadget,齐活了!我们拥有了执行任意函数、任意 syscall 的能力。
接下来就是 exp 了,思路是首先把 “/bin/sh”读入到已知地址(程序关闭了 PIE),比如 bss 段,然后用 syscall gadget 来 get shell。前者我们可以通过 return to gets 来实现。
利用脚本写得不是很优雅,不过懒得改了。总之知道大概意思就行了)
1 | g1 = 0x12000bc54 |