花了几天的时间来学习MIPS,先从调试一段简单的代码开始,看看MIPS汇编指令是怎么进行一系列操作的,然后去了解各个指令的含义和寄存器的用途,遇到不易理解的就在gdb中调试一下,接着就是其函数调用约定及相关特性,最后是编写MIPS汇编的shellcode。学完这些紧接着就去复现漏洞了,也没好好总结一下,复现完漏洞后还是觉得要梳理一下MIPS的相关知识,想了想写篇文章总结是再好不过了,因此又花了不少时间写了下面这些内容。
学习过程中参考了不少师傅写的文章,大佬们写的都非常好非常感谢,链接放在最后了。本人菜鸟一枚,如有不对请指出,谢谢包容^_^
环境搭建
最开始的开始当然是先搭建好环境,搭建交叉编译环境和qemu,调试还要安装gdb-multiarch,这一部分就不细说了,内容不多就下面几条命令。(其实在这一步我已经踩了无数坑😀👍
安装qemu、mips依赖库、gdb-multiarch
sudo apt install qemu |
安装buildroot交叉编译环境
去buildroot.org下载buildroot-2024.02.11.tar.gz
tar -zxvf buildroot-2019.02.4.tar.gz |
在target options->target arch选项里面选择自己要编译的架构,这里选择MIPS(Little endian),代表MIPS小端序。toolchain中还要选择C语言库,这里选uGlibc。
设置环境变量:
echo "export PATH=/home/wen/Desktop/buildroot-2024.02.11/output/host/bin:\$PATH" >> ~/.bashrc |
程序启动调试
demo.c:
|
以上面demo.c程序为例,执行下面指令来启动程序:
注:mips和mipsel的区别是前者为大端序,后者为小端序,readelf -h demo可以查看程序的字节序。
#编译 |
如果要直接调试程序,先执行qemu-mips -g 6666 ./demo,然后在另一个终端,执行下面这几条命令。
gdb-multiarch |
如下:
上面这种情况是调试静态链接的程序,如果程序是动态链接的,先执行qemu-mips -g 6666 ./demo,然后执行这几条命令即可。
gdb-mutiarch |
寄存器
通用寄存器
MIPS架构中有32个通用寄存器,在汇编程序中可以用编号表示,也可以用寄存器的名称来表示,各个通用寄存器的详细信息如下表所示:
| 编号 | 名称 | 描述 | 备注 |
|---|---|---|---|
| $0 | $zero | 常量寄存器,值为0 | |
| $1 | $at | 为汇编器保留的寄存器,主要用于处理伪指令和加载大常数。 | Assembler Temporary |
| $2~$3 | $v0~$v1 | 存储表达式或函数返回值 | Values |
| $4~$7 | $a0~$a3 | 函数调用时,用来存储前四个参数 | Arguments |
| $8~$15 | $t0~$t7 | 临时寄存器,存放临时变量 | Temporaries |
| $16~$23 | $s0~$s7 | 保存寄存器,用于保存函数调用之间的状态(即寄存器的值)(与$t0~$t9相反) | Saved Values |
| $24~$25 | $t8~$t9 | 临时寄存器 | Temporaries |
| $26~$27 | $k0~$k1 | 用于保存异常处理和中断的返回值,为操作系统Keep使用 |
Kernel reserved |
| $28 | $gp | 全局指针 | Global Pointer |
| $29 | $sp | 堆栈指针,会指向栈顶 | Stack Pointer |
| $30 | $s8/$fp | 可以作为第九个保存寄存器($s8),也可以作为栈帧指针($fp)保存栈指针 | Saved value / Frame Pointer |
| $31 | $ra | 存储函数的返回地址 | Return Address |
特殊寄存器
除了上面32个通用寄存器,MIPS架构还定义了一些特殊用途的寄存器,下面介绍一些特殊寄存器:
| 名称 | 描述 |
|---|---|
| $pc | 程序计数器,指向当前执行的指令地址 |
| $hi | 高位寄存器,存储乘除操作的高位结果 |
| $lo | 低位寄存器,存储乘除操作的低位结果 |
| $status | 状态寄存器,控制处理器模式和中断状态 |
| $cause | 原因寄存器,存储异常和中断的原因 |
指令
MIPS架构固定4字节指令长度,其汇编指令与x86还是不太一样的,但区别也不大,下面就只介绍一些常见的汇编指令。
基础指令
| 指令 | 备注 | 描述 | 举例分析 |
|---|---|---|---|
li |
Load Immediate | 将立即数存入寄存器 | li $a2,2,$a2的值为2 |
lui |
将立即数(以二进制形式)左移16位后,存入寄存器 | lui $t0,0xF,$t0的值为0xF0000 |
|
la |
Load Address | 将地址存入寄存器 | la $t9,memset,$t9中为memset函数的地址 |
lw |
Load Word | 从内存中加载一个word类型的值到寄存器中 |
lw $gp,0x170+var_158($sp),从栈指针$sp偏移 0x170+var_158 的位置,加载一个32位的值到$gp中 |
sw |
Store Word | 将一个 32 位的值从寄存器存储到内存中 | |
addi |
Add Immediate | 将立即数与寄存器的值相加后,把结果写入另一个寄存器 | addi $t,$s,0xF,$t中为$s的值与0xF的和 |
addu |
Add Unsigned | 将寄存器的值相加(无符号加法)后,把结果写到寄存器中 | addu $v0,$v1,$v0 |
add |
同addu,区别是该指令是有符号加法 | ||
addiu |
同addi,区别是该指令加无符号立即数 |
跳转指令
| 指令 | 备注 | 描述 | 举例分析 |
|---|---|---|---|
jr |
Jump Register | 无条件跳转到某个寄存器指定的地址 | |
jal |
Jump and Link | 跳转到某个地址,并将返回地址存入$ra寄存器 |
|
jalr |
Jump and Link Register | 跳转到某个寄存器指定的地址,并将返回地址存入另一个寄存器 | jalr $t9跳转到$t9存储的地址,并将返回地址存入$ra |
b |
Branch | 无条件跳转指令(标签或地址) | |
bnez |
Branch if Not Equal to Zero | 指定寄存器的值不为0,才跳转 | bnez $v0,loc_402B24若$v0的值为零则跳转到loc_402B24标签处 |
beqz |
Branch if Equal to Zero | 指定寄存器的值为0,才跳转 |
函数调用
叶子与非叶子函数
定义:现有3个函数分别为A、B、C,其中函数A调用函数B,函数B调用函数C,因此A和B为非叶子函数,C为叶子函数。
调用函数C时会将其返回地址直接存入$ra寄存器,执行完函数C后,程序流会直接执行jr $ra指令跳到返回地址(返回函数B);而对于函数B,在跳转到B时会将其返回地址先存入$ra寄存器,然后在执行函数B的过程中再将$ra的值存入栈中,执行完后,程序流则会先从堆栈中取出被保存在堆栈上的返回地址,放入$ra寄存器中,然后再执行jr $ra指令。
函数传参
当参数小于等于4个时,使用$a0 ~ $a3寄存器存储;超过4个的部分被放到了栈里。且前4个参数在使用前也会被放入之前在栈中预留的空间中。
栈帧开辟
调用函数时,MIPS架构下开辟栈帧的方式与x86架构不同,但最后的栈帧结构是相同的。MIPS下的栈帧开辟方式如下所示:
► 0x400614 <main+16> jal A <0x4005a4> |
等价的x86指令如下所示:
call main #这里的作用同上面的(2):先将返回地址push到栈上,然后跳到目标函数的第一条指令地址 |
mips架构特性
MIPS架构存在“流水线效应”和“缓存不一致性”这两个特性。
“流水线效应”指的是本应该顺序执行的几条指令会同时执行,这样在执行跳转指令的时候,当刚要跳转到指定地址时,跳转指令的下一条指令也已经执行了,这样的现象称为分支延迟效应,跳转指令的下一条指令称为分支延迟槽。也因此,MIPS架构下的分支延迟槽通常都是nop指令,当然也不全是。
“缓存不一致性”指的是指令缓存区和数据缓存区两者的同步需要一个时间来同步,比如我们将shellcode写入栈上后,我们需要这块区域已经是指令缓存区,但此时其还属于数据缓存区,若直接跳转过去执行shellcode,就会出现问题,因此,我们需要调用sleep函数,先停顿一段时间,给它时间从数据缓存区转成指令缓存区,然后再跳转过去,才能成功执行。
shellcode编写
$v0寄存器存储系统调用号和系统调用的返回值,$a0 ~ $a3寄存器用来存储前4个参数,syscall指令触发系统调用。
先试着自己写一个write的系统调用,如下所示:
.data |
注意系统调用完write后,还要再执行一个exit系统调用,以防止程序的执行流继续执行后面的指令,而导致出现一系列错误。
execve系统调用
.data |
上面这段代码需要开辟栈帧,借助栈来传递字符串/bin//sh的地址给$a0寄存器,也可以通过.byte指令将/bin//sh字符串放在自定义的标签中(注意字符串后面要00截断,不然后面可能会带上其他字符),然后直接la标签的地址给$a0寄存器,如下所示:
.data |
可以借助这个网站Online Assembler and Disassembler,查看汇编代码对应的机器码。
参考文章
《IoT从入门到入土》(1)–MIPS交叉编译环境搭建及其32位指令集
IOT安全入门学习–MIPS汇编基础 | ZIKH26’s Blog
[原创]IDA及插件MIPSROP安装——《揭秘家用路由器0day漏洞挖掘技术》学习笔记-安全工具-看雪-安全社区|安全招聘|kanxue.com