IOT入门之MIPS架构基础知识学习

花了几天的时间来学习MIPS,先从调试一段简单的代码开始,看看MIPS汇编指令是怎么进行一系列操作的,然后去了解各个指令的含义和寄存器的用途,遇到不易理解的就在gdb中调试一下,接着就是其函数调用约定及相关特性,最后是编写MIPS汇编的shellcode。学完这些紧接着就去复现漏洞了,也没好好总结一下,复现完漏洞后还是觉得要梳理一下MIPS的相关知识,想了想写篇文章总结是再好不过了,因此又花了不少时间写了下面这些内容。

学习过程中参考了不少师傅写的文章,大佬们写的都非常好非常感谢,链接放在最后了。本人菜鸟一枚,如有不对请指出,谢谢包容^_^

环境搭建

最开始的开始当然是先搭建好环境,搭建交叉编译环境和qemu,调试还要安装gdb-multiarch,这一部分就不细说了,内容不多就下面几条命令。(其实在这一步我已经踩了无数坑😀👍

安装qemu、mips依赖库、gdb-multiarch

sudo apt install qemu
sudo apt install qemu-user-static binfmt-support qemu-user qemu-system
sudo apt-get install gcc-mips-linux-gnu
sudo apt-get install gcc-mipsel-linux-gnu
sudo apt-get install gcc-mips64-linux-gnuabi64
sudo apt-get install gcc-mips64el-linux-gnuabi64
sudo apt install gdb-multiarch

安装buildroot交叉编译环境

buildroot.org下载buildroot-2024.02.11.tar.gz

tar -zxvf buildroot-2019.02.4.tar.gz
make clean
make menuconfig

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
source ~/.bashrc

程序启动调试

demo.c:

#include <stdio.h>
int main()
{
int age = 7;
printf("My age is %d.\n",age);
return 0;
}

以上面demo.c程序为例,执行下面指令来启动程序:
注:mips和mipsel的区别是前者为大端序,后者为小端序,readelf -h demo可以查看程序的字节序。

#编译
mips-linux-gnu-gcc demo.c -o demo -static -g #-static是静态链接
#运行
qemu-mips ./demo #(大端序

如果要直接调试程序,先执行qemu-mips -g 6666 ./demo,然后在另一个终端,执行下面这几条命令。

gdb-multiarch
set architecture mips #设置架构为mips
set endian big #设置端序为大端序
symbol-file ./demo #添加符号表
target remote localhost:6666

如下:

image-20250310092641519

上面这种情况是调试静态链接的程序,如果程序是动态链接的,先执行qemu-mips -g 6666 ./demo,然后执行这几条命令即可。

gdb-mutiarch
file ./demo
target remote localhost:6666

寄存器

通用寄存器

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>

0x4005a4 <A> addiu $sp, $sp, -0x20 #(1)开辟栈帧空间
0x4005a8 <A+4> sw $ra, 0x1c($sp) #(2)存储返回地址(调用完函数A后返回到的地址
0x4005ac <A+8> sw $fp, 0x18($sp) #(3)将$fp放入栈中
0x4005b0 <A+12> move $fp, $sp #(4)将$sp放入$fp中

等价的x86指令如下所示:

call 	main			#这里的作用同上面的(2):先将返回地址push到栈上,然后跳到目标函数的第一条指令地址
push ebp #(3)
mov ebp, esp #(4)
sub esp, 14h #(1)

mips架构特性

MIPS架构存在“流水线效应”和“缓存不一致性”这两个特性。

“流水线效应”指的是本应该顺序执行的几条指令会同时执行,这样在执行跳转指令的时候,当刚要跳转到指定地址时,跳转指令的下一条指令也已经执行了,这样的现象称为分支延迟效应,跳转指令的下一条指令称为分支延迟槽。也因此,MIPS架构下的分支延迟槽通常都是nop指令,当然也不全是。

“缓存不一致性”指的是指令缓存区和数据缓存区两者的同步需要一个时间来同步,比如我们将shellcode写入栈上后,我们需要这块区域已经是指令缓存区,但此时其还属于数据缓存区,若直接跳转过去执行shellcode,就会出现问题,因此,我们需要调用sleep函数,先停顿一段时间,给它时间从数据缓存区转成指令缓存区,然后再跳转过去,才能成功执行。

shellcode编写

$v0寄存器存储系统调用号系统调用的返回值$a0 ~ $a3寄存器用来存储前4个参数,syscall指令触发系统调用。

先试着自己写一个write的系统调用,如下所示:

.data
some_label: .word 0x61626364
.text
.globl __start
__start:
.set noreorder
li $a0,1
la $a1,some_label
li $a2,4
li $v0,4004
syscall
li $v0,4001
syscall

注意系统调用完write后,还要再执行一个exit系统调用,以防止程序的执行流继续执行后面的指令,而导致出现一系列错误。

execve系统调用

.data
.text
.globl __start
__start:
.set noreorder
addiu $sp,$sp,-0x10
li $t0,0x2f62696e #/bin
li $t1,0x2f2f7368 #//sh
sw $t0,0x8($sp)
sw $t1,0xc($sp)
la $a0,0x8($sp)
addi $a1,$zero,0
addi $a2,$zero,0
addi $v0,$zero,4011
syscall

上面这段代码需要开辟栈帧,借助栈来传递字符串/bin//sh的地址给$a0寄存器,也可以通过.byte指令将/bin//sh字符串放在自定义的标签中(注意字符串后面要00截断,不然后面可能会带上其他字符),然后直接la标签的地址给$a0寄存器,如下所示:

.data
# /bin//sh\x00
some_label: .byte 0x2f,0x62,0x69,0x6e,0x2f,0x2f,0x73,0x68,0x00
.text
.globl __start
__start:
.set noreorder
la $a0,some_label
addi $a1,$zero,0
addi $a2,$zero,0
addi $v0,$zero,4011
syscall

可以借助这个网站Online Assembler and Disassembler,查看汇编代码对应的机器码。

参考文章

《IoT从入门到入土》(1)–MIPS交叉编译环境搭建及其32位指令集

路由器漏洞分析环境搭建 | Prowes5’s Blog

IOT安全入门学习–MIPS汇编基础 | ZIKH26’s Blog

[原创]IDA及插件MIPSROP安装——《揭秘家用路由器0day漏洞挖掘技术》学习笔记-安全工具-看雪-安全社区|安全招聘|kanxue.com

ida插件安装踩坑经历 - V1ct0r的博客