环境配置
sudo apt-get install gcc-mips-linux-gnu gcc-mipsel-linux-gnu gcc-mips64-linux-gnuabi64 gcc-mips64el-linux-gnuabi64 |
编译及运行
MIPS架构 其也包括大小端序 32位和64位
32位小端序 mipsel
32位大端序 mips
64位小端序 mips64el
64位大端序 mips64
用如下所示代码编译一个32位小端序的二进制文件 并尝试使用qemu模拟运行
#include <stdio.h> |
编译指令
mipsel-linux-gnu-gcc -o test test.c |
qemu模拟运行指令
qemu-mipsel-static -L /usr/mipsel-linux-gnu/ ./test |
mips
寄存器
这里以o32 abi接口标准为主
其约定的寄存器如下
此外 MIPS架构还强制要求协处理器 最多可以拥有4个
固定拥有协处理器cp0
其功能包括CPU配置 Cache控制 异常、中断控制 中断或异常发生时的行为和处理的定义 内存管理单元控制等等
协处理器cp0一共包括32个寄存器 这里挑选几个比较重要的寄存器来记忆
$sr 状态寄存器 可以反应cpu的状态以及控制cpu |
读懂简单的程序汇编
这里通过上面编译出来的32位小端序程序来逐行分析汇编
以main函数为例
addiu和addi功能相同 为左侧操作数加上右侧的立即数 不过addiu并不会检测溢出
$sp指向栈顶 这里抬高栈顶 为后续操作腾出栈空间
sw(store word)将寄存器的值保存到某地址 这里将返回地址保存到$sp+0x40+0x4处
下一行将栈底指针保存到$sp+0x40处
move指令用于寄存器值之间的传递 这里使$fp=$sp 将栈底指针也抬高到栈顶处
前面这些操作类似于x64架构中的初始化栈帧空间以及保存返回地址以便返回到上一个执行语句
li(load immediate)将立即数赋值给寄存器 这里把0x419010传给$gp 顺带下一句把$gp的值保存到了$sp+0x40-0x30处
la(load address)将地址赋值给寄存器
lw(load word)将某地址内的值赋值给寄存器
__stack_chk_guard的地址也为0x4110a0 这两句组合起来就相当于将canary的值赋值给v0寄存器
接着用sw 将v0的值存储到$fp+0x40-0x4中
红框圈起来的是该语句写入的canary 而你会发现 这里实际上是往$s8+0x3c处写入 而不是$fp
查阅了cyberangle师傅的博客后 得知对于gdb来说 对于$fp的操作就等于对于$s8操作
上面没提及的是 $fp是30号寄存器 根据编译器的不同 30号寄存器也可以看作是$s8
在介绍jal汇编指令之前 需要引入两个概念 叶子函数和非叶子函数
叶子函数: 该函数中不会再调用其他函数
非叶子函数: 该函数中会调用其他函数
这里以另外一个程序为例来观察二者的区别
#include <stdio.h> |
为了排除开启canary保护加入的stack_fail_check函数带来的影响 这里选择关闭canary保护
此时按照上面的理念 可以判断出main函数调用了vuln和printf属于非叶子函数 vuln函数为叶子函数
先来看非叶子函数main
首先 抬栈腾出栈帧空间 随后往栈上保存返回地址和栈底指针 迁移sp指针至fp指针
随后跳转执行vuln函数
再来看叶子函数vuln
可以明显看到 叶子函数并没有将$ra寄存器的值存放到栈上 这是因为非叶子函数需要调用到其他函数 所以将返回地址暂存到栈上 而叶子函数不必考虑这一点
那么说回到jal指令 其将对应函数的地址载入ra寄存器 随后nop滑动执行函数
接着以test程序的main函数来分析 看一下puts函数是如何调用的
先把之前存到栈上的gp寄存器值重新赋值给寄存器
lui(load upper immediate) 取立即数存到寄存器的高16位 低16位用0填充
此时v0寄存器为0x00400000
aTest指向存放于rodata段的test字符串 此时的addiu相当于 a0=v0+(aTest-0x400000)
随后将puts函数的地址存放到t9寄存器中
jarl和jal指令的区别在于 前者会多一个存放返回地址的功能
其有两种格式 jalr opt1 opt2 和jalr opt1
当为前者时 返回地址存入opt2 为后者时 返回地址存入ra寄存器
分析一下 像read这类需要多参数的函数如何处理
可以看到三个参数是用寄存器$a0-$a2存储 不同于i386架构的32位是利用栈来传参
最后来看开启了canary保护的栈帧是如何结束的
重点在于beq指令 如果$v1(也就是栈上的canary)和$v0相等
那么就跳转到loc_400920 否则调用stack_chk_fail函数来输出报错以及终止程序
栈帧末尾 清空了v0寄存器 使其为0 和i386架构类似 后者把eax寄存器用于存储函数返回值
随后把$sp挪到$fp所指向的地址 也就是最开始$sp抬高栈帧后的地址
随后取回放在栈上的$ra和$fp
一开始把$sp往低地址移动了0x48字节 在结束后将$sp放回去
随后跳转回到$ra中存储的返回地址