CTF比赛的逆向已经发展到向arm和mips等嵌入式架构发展了,国内可以看到关于mips逆向的一些基础文章,但是对于mips64却介绍比较少,这里通过jarvisoj一道mips64的题目(来自于某强网杯)来看看mips64的一些坑,以及介绍新手如何入门逆向mips64。
题目链接 Here
file一下程序,是mips64 rel2,静态编译且没有符号的。
root@kali:/mnt/hgfs/ctfsample/jarvisoj/mips64# file mips64
mips64: ELF 64-bit MSB executable, MIPS, MIPS64 rel2 version 1 (SYSV), statically linked, BuildID[sha1]=1fd09709a4c48cd14efe9454d332d16c1b096fd0, for GNU/Linux 3.2.0, stripped
拖入IDA64(7.0版本)分析,看到一堆sub函数,但是没有符号信息。
也能看到关键的字符串,但是无法交叉引用查找调用点
在linux中安装qemu
sudo apt-get install qemu qemu-system qemu-user-static
然后尝试执行mips64的程序
qemu-mips64 ./mips64
可以看到程序运行效果如下
我是从源码编译mips64版的gdb开始,环境是kali2008(如下),默认配置gdb 8.1.1,所以选择同版本的gdb源码进行编译。
Linux kali 4.17.0-kali1-amd64 #1 SMP Debian 4.17.8-1kali1 (2018-07-24) x86_64 GNU/Linux
2.将gdb-8.1.1.tar.gz 拷贝到任何你愿意的Linux目录下, 解压
tar -zxvf gdb-8.1.1.tar.gz
3.编译mips64-linux-gdb
到目录gdb-8.1.1下,编译命令
cd gdb-8.1.1
./configure --target=mips64-linux --prefix=/usr/local/mips64-gdb -v
make
make install
安装成功后,可以在 /usr/local/mips64-gdb/bin 目录中看到这两个文件
mips64-linux-gdb mips64-linux-run
4.运行mips64-linux-gdb
root@kali:/usr/local/mips64-gdb/bin# /usr/local/mips64-gdb/bin/mips64-linux-gdb
GNU gdb (GDB) 8.1.1
Copyright (C) 2018 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "--host=x86_64-pc-linux-gnu --target=mips64-linux".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word".
(gdb)
5.一些说明
启动qemu时,使用-g 9999 开启 gdbserver ,9999是调试端口号,gdb中用这个端口号链接gdbserver。
# qemu-mips64 -g 9999 ./mips64
然后在mips64-linux-gdb中链接gdbserver调试
(gdb) file mips64
Reading symbols from mips64...(no debugging symbols found)...done.
(gdb) set architecture mips:isa64r2
The target architecture is assumed to be mips:isa64r2
(gdb) target remote localhost:9999
Remote debugging using localhost:9999
0x0000000120003c50 in ?? ()
mips64-linux-gdb调试指令和gdb是一样的,常用的有:
i r #查看所有寄存器
i fl #查看所有fpu
c #继续程序到下一个断点
ni #单步执行
x /10i $pc #查看当前指令情况
可以参考附录里面各种mips汇编指令的介绍,这里重点介绍几点与x86逆向调试不同的地方,了解了这些会让逆向事半功倍。
1.函数的输入参数分别在寄存器a0,a1,a2…中,关注这几个寄存器的值,就可以知道某个函数如sub_120022504(a0,a1,a2)的输入参数
2.mips64的跳转指令时(b开头的指令),会执行跳转后一条语句之后再跳,这叫分支延迟。
如下面的代码片段,bc1f是跳转指令,满足条件跳转至 loc_120003B24 。无论是否满足跳转条件,都会先执行 ld $t9, -0x7F68($gp) 那条指令,再跳到 loc_120003B24 或者 ld $a0, -0x7F78($gp) 。gdb断点只能下到 0x120003C24 或 0x120003C2C,无法下到0x120003C28。
.text:0000000120003C20 loc_120003C20:
.text:0000000120003C20 c.lt.s $fcc6, $f1, $f0
.text:0000000120003C24 bc1f $fcc6, loc_120003B24
.text:0000000120003C28 ld $t9, -0x7F68($gp)
.text:0000000120003C2C ld $a0, -0x7F78($gp)
.text:0000000120003C30 ld $a1, -0x7F58($gp)
3.本程序涉及大量的fpu操作(浮点运算单元),可在gdb中使用i fl
(info float)指令查看fpu,下文的f0、f12等都是fpu。
4.fpu会有single(单精度)和double(双精度)表示,以上图f0为例,其单精度值(flt)为4,双精度值(dbl)为13.000001922249794。如果汇编指令是 c.lt.s (最后的s表示以单精度的计算),会判断 $f1(flt) < $f0(flt),即4是否小于0.5,而不是13是否小于122。
.text:0000000120003BA8 c.lt.s $f1, $f0
.text:0000000120003BAC bc1f loc_120003BCC
.text:0000000120003BB0 ld $v0, -0x7F78($gp)
.text:0000000120003BB4 lwc1 $f1, -0x116C($v0)
c.lt.s 意思大概是 compare less than in single ( c.lt.d 则是在double,即双精度范围计算)
bc1f : jump if compare result is false (f表示false,bc1t 表示 true才跳)
5.程序中多次出现以下片段,多次出现的-0x7f78
是程序里面一个基地址,将基地址赋值给$v0寄存器,第二句再根据这个基地址($v0),取一个常量到寄存器或fpu($f1 = [$v0-0x1164])。
ld $v0, -0x7F78($gp)
lwc1 $f1, -0x1164($v0)
qemu使用-strace参数,让程序输出更多的调试信息
可以看到系统使用了write(1,0x200b97d0,40)来输出“Welcome to QWB, Please input your flag:”这40个字符,联想到x64架构1表示stdout,0x200b97d0表示字符串地址,40表示输出长度
从write(1,0x200b97d0,40)到write(1,0x200b97d0,12)之间,有一个Linux(0,4832599008,1024,0,4832599008,4),猜测就是一个read函数系统调用了,要逆向就要知道read函数到输出Wrong Flag!之间发生什么,调用了哪些函数。
由于gdbserver调试不能用ctrl+c中断再下断点,所以从IDA将所有可能是函数的地址复制出来
编辑成为gdb断点的格式,并粘贴到gdb中,大约600多个断点
用qemu-mips64 -strace -g 9999 ./mips64启动程序,在gdb侧链接gdbserver后不停的按c,直至程序堵塞等待输入,这是看到最后一个触发的断点是:0x0000000120022404,说明输入在这里附近
然后在程序随便输入内容,如1234回车,让程序继续执行。在gdb一路c,直到看到程序输出Wrong Flag!,记录这段时间的断点。
0x0000000120014740 in ?? ()
0x0000000120014740 in ?? ()
0x000000012001f110 in ?? ()
0x000000012000d6b0 in ?? ()
0x000000012001f110 in ?? ()
0x00000001200206e0 in ?? ()
0x00000001200138a0 in ?? ()
0x0000000120012978 in ?? ()
0x0000000120012120 in ?? ()
0x000000012000ffc8 in ?? ()
0x00000001200112f0 in ?? ()
0x0000000120022504 in ?? ()
gdb中按d清理所有断点,重新设置断点为上述函数
b* 0x0000000120014740
b* 0x0000000120014740
b* 0x000000012001f110
b* 0x000000012000d6b0
b* 0x000000012001f110
b* 0x00000001200206e0
b* 0x00000001200138a0
b* 0x0000000120012978
b* 0x0000000120012120
b* 0x000000012000ffc8
b* 0x00000001200112f0
b* 0x0000000120022504
我们从后开始看,看到函数0x0000000120022504执行时,其输入参数是(1,0x1200b97d0, 0xc),查看内存,是输出Wrong Flag的函数。a1已经指向WrongFlag字符串了。
Breakpoint 618, 0x0000000120022504 in ?? ()
(gdb) i r
...
a0 a1 a2 a3
R4 0000000000000001 00000001200b97d0 000000000000000c fffffffffbad2a84
(gdb) x/s $a1
0x1200b97d0: "Wrong Flag!nWB, Please input your flag: "
用同样的方法一路往前看在进入0x12000d6b0时,a0已经是WrongFlag字符串,而进入 0x12001f110 时,a0指向用户输入的字符串,说明 0x12001f110 是关键函数,用于判断用户输入是否正确的。
在0x12001f110函数中逐行调试(ni指令),返回到了 0x120003ac0 (sub_120003AC0),有这么一段指令,这是调用0x12001f110的地方,beq $v0, $v1
是将输入长度和0x10进行比较
.text:0000000120003B10 bal sub_12001F110 # a0为用户输入
.text:0000000120003B14 ld $a0, -0x7F58($gp)
.text:0000000120003B18 li $v1, 0x10 # sub_12001f110+110时返回到这里
.text:0000000120003B1C beq $v0, $v1, key # v0=len(input),v1=0x10
.text:0000000120003B20 ld $t9, -0x7F40($gp)
如果比较不相等,则一路调用sub_12000D6B0(根据上面的回溯分析,调用时a0已经是指向WrongFlag字符串了),所以输入长度是16个字符
确定输入长度后,可以使用 qemu-mips64 -strace -g 9999 ./mips64 <1.payload
来启动程序,在同目录的1.payload文件中输入1234567890abcdef
输入16个字符,程序会走另一个分支。在0x120003B5C的代码片段中,调用了4个关键函数(sub_120003EB0、sub_120004278、sub_120004640 和 sub_120004A08)。每个函数调用返回后,都会对fpu的f24/f25/f26和f0操作,最终可以看成是f0等于4个函数执行的结果。
f0 = sub_120003EB0(...) + sub_120004278(...) + sub_120004640(...) + sub_120004A08(...)
断点设置在 0x120003BA8,f0(值为0,因为指令是c.lt.s,s表示单精度)与0.5比较
Breakpoint 619, 0x0000000120003ba8 in ?? ()
(gdb) i fl
fpu type: double-precision
reg size: 64 bits
...
f0: 0x4018000000000000 flt: 0 dbl: 6
f1: 0x404cc0003f000000 flt: 0.5 dbl: 57.500007510185242
运行至0x120003BD0,f0 与 1.5比较;
运行至0x120003bf8,f0 与 2.5比较;
运行至0x120003c20,f0 与 3.5比较,如果此时f0小于3.5,则跳转到粉红色区域,即输出WrongFlag的函数。
所以逆向的目标就是让f0大于3.5(ctf老司机可能意识到就是让f0=4.0,上面4个函数都输出1.0,加起来就是4.0了)
以第一个函数 sub_120003EB0 为例,查看其执行时输入a0为输入字符串
Breakpoint 620, 0x0000000120003eb0 in ?? ()
(gdb) i r
zero at v0 v1
R0 0000000000000000 0000000000000001 0000000000000010 0000000000000010
a0 a1 a2 a3
R4 00000001200b6140 0000000000000000 ffffffffffffffff 8080808080808080
...
(gdb) x/s $a0
0x1200b6140: "1234567812345678"
单步执行,发现程序读取了输入的前4字节(想想有4个函数,一次读取4个字节处理,正好16字节)
根据上面在“mips64基础知识”提及的调试经验,在-0x7F78($gp)
这个基础地址之上,读取了两个偏移值,为47.5和57.5,与输入比较,比较明显的在判断输入的上下界。
然后看到将输入f0 = input[0] – 48.0,48是ascii的’0’字符串。看到判断上下界和减去0操作,逆向老司机会相当熟悉,这就是字符转数字,定义为digit[0]。
然后继续跟踪看到是将 f12 = digit[0]*16+digit[2] = 19.0 (以输入1234567890abcdef为例),就是在转换16进制数。这个过程中有较多的浮点数计算和精度转换指令,需要耐心跟踪。
然后在0x120004138 进入了函数 sub_120004EB0,从函数返回后,有明显的f0和f1比较。
.text:0000000120004134 loc_120004134: # CODE XREF: sub_120003EB0:loc_120004198↓j
.text:0000000120004134 # sub_120003EB0+368↓j ...
.text:0000000120004134 ld $t9, -0x7F70($gp)
.text:0000000120004138 bal sub_120004EB0
.text:000000012000413C nop
.text:0000000120004140 ld $v0, -0x7F78($gp)
.text:0000000120004144 lwc1 $f1, -0x1190($v0)
.text:0000000120004148 c.eq.s $f0, $f1
.text:000000012000414C bc1f loc_120004178
.text:0000000120004150 ld $ra, 0x20+var_18($sp)
.text:0000000120004154 add.d $f12, $f25, $f24
.text:0000000120004158 ld $t9, -0x7F70($gp)
.text:000000012000415C bal sub_120004EB0
.text:0000000120004160 cvt.s.d $f12, $f12
.text:0000000120004164 ld $v0, -0x7F78($gp)
.text:0000000120004168 lwc1 $f1, -0x118C($v0)
.text:000000012000416C c.eq.s $fcc1, $f0, $f1
.text:0000000120004170 bc1t $fcc1, loc_1200041B0
.text:0000000120004174 ld $ra, 0x20+var_18($sp)
设置断点0x120004148观察fpu,因为比较的是用c.eq.s指令,用单精度进行比较,所以是19和83(f1的值,从全局变量中加载,是个常量)比较
Breakpoint 621, 0x0000000120004148 in ?? ()
(gdb) i fl
fpu type: double-precision
...
f0: 0x4010000041980000 flt: 19 dbl: 4.0000009774230421
f1: 0x404cc00042a60000 flt: 83 dbl: 57.500007945112884
f2: 0x0000000000000000 flt: 0 dbl: 0
直觉告诉我们需要让其相等,以继续运行
19/16 = 1 <==> 83/16 = 5
19%16 = 3 <==> 83%16 = 3
所以原来输入(1234…),要对应修改为5234..(1改成5,3改成3)
修改输入后重新执行程序,原来不执行的0x12000415C也执行了,第二次进入函数 sub_120004EB0,函数返回后在0x12000416C进行了比较
Breakpoint 622, 0x000000012000416c in ?? ()
(gdb) i fl
fpu type: double-precision
reg size: 64 bits
...
f0: 0x4010000042100000 flt: 36 dbl: 4.0000009844079614
f1: 0x404cc00042880000 flt: 68 dbl: 57.500007931143045
f2: 0x0000000000000000 flt: 0 dbl: 0
同理
36/16 = 2 <==> 68/16 = 4
36%16 = 4 <==> 68%16 = 4
所以原来输入(5234…),要对应修改为5434…(2改成4,4改成4)
在修改了前4字节后,在原来 0x120003BA8 设置断点,就是4个关键函数返回值之和与0.5比较的地方,此时我们可以看到f0已经变成1
Breakpoint 623, 0x0000000120003ba8 in ?? ()
(gdb) i fl
fpu type: double-precision
reg size: 64 bits
cond : 1 2 3 4 5 6 7
cause :
mask :
flags :
rounding: nearest
flush : no
nan2008 : no
abs2008 : no
f0: 0x401800003f800000 flt: 1 dbl: 6.0000009462237358
f1: 0x404cc0003f000000 flt: 0.5 dbl: 57.500007510185242
这时候在IDA中查找函数 sub_120004EB0 的交叉引用,函数被调用了8次,上述提及的4个关键函数都各调用了2次,说明规律是类似的。只要在这8个地方附近设置断点,用同样的规律修改输入,即可让4个关键函数之和为4。
输入正确时,程序提示正确
由于mips指令和x86指令差异较大,需要查阅网上mips指令的相关说明,结合动态调试理解。