mips架构初探(一)

基础知识及简单的编译

在常规的ctf的pwn方向当中,我们常见的汇编都是基于Intel的8086汇编,但是对于很多物联网设备,其实并不都是采取这一种架构,常见如mips架构和arrch64之类,而在pwn题中,这类题型统一称作异架构,这篇文章会介绍mips架构的一些基础知识,所涉及不深,只是起到入门的效果

首先我们来介绍一下什么是mips,MIPS架构是一种采取 精简指令集(RISC)的处理器架构,1981年出现,由MIPS科技公司开发并授权,它是基于一种固定长度的定期编码指令集,并采用 导入/存储(Load/Store)数据模型。经改进,这种架构可支持高级语言的优化执行。其算术和逻辑运算采用三个操作数的形式,允许编译器优化复杂的表达式。

如今基于该架构的芯片广泛被使用在许多电子产品、网络设备、个人娱乐装置与商业装置上。最早的MIPS架构是32位,最新的版本已经变成64位。

我们的虚拟机基本都是ubuntu,也就是基于8086架构,所以这里我们学习mips需要理由qemu来模拟这个架构,而QEMU代表快速模拟器,是虚拟化领域的一个重要工具,能够在单个硬件平台上同时运行多个操作系统。以其能够仿真广泛的客户系统和架构而闻名,QEMU是创建和管理虚拟环境的多功能解决方案。它作为一个类型1虚拟机运行,直接与物理硬件接口,这与其他虚拟化技术有显著的不同。通过整合像Intel VT和AMD-V这样的硬件虚拟化技术,QEMU优化了虚拟机的性能,为开发人员和IT专业人员提供了一个强大的平台,用于模拟各种计算环境,无需为每个系统提供专用硬件。

#!/bin/sh

# 安装QEMU及相关工具
sudo apt install qemu
sudo apt install qemu-system qemu-user-static binfmt-support

# 安装ARM架构编译器和依赖
sudo apt install libncurses5-dev gcc-arm-linux-gnueabi build-essential

# 安装MIPS架构编译器
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

# 安装多架构gdb调试依赖
sudo apt install gdb-multiarch

这里有一个自动化脚本,运行就可以完整配置qemu和mips,那么我本地使用的是ubuntu22.04

这里我们可以编译一下自己的第一个mips架构的题目

#include <stdio.h>

int main() {
    printf("Hello, MIPS!\n");
    return 0;
}

这是一个最简单的hello world,我们来学习怎么编译

不同于8086的小端序,mips有大端和小端格式两种格式,分别为 mipsmipsel 系列,且支持32位和64位的指令集,这两种格式的编译语句也是不同的,那么我本地的这个c文件叫做test.c,下面的语句,真正决定编译情况的是前面利用的gcc,而最后的名字大家可以自行决定,这里只是为了区分

#32位小端序:
mipsel-linux-gnu-gcc -g test.c -o test_mipsel_32
#使用小端 MIPS 架构的编译器生成 32 位小端格式的可执行文件。

#32位大端序:
mips-linux-gnu-gcc -g test.c -o test_mips_32
#使用大端 MIPS 架构的编译器生成 32 位大端格式的可执行文件。

#64位小端序:
mips64el-linux-gnuabi64-gcc -g test.c -o test_mipsel_64
#使用小端 MIPS 64 位架构的编译器生成小端 64 位的可执行文件。

#64位大端序:
mips64-linux-gnuabi64-gcc -g test.c -o test_mips_64
#使用大端 MIPS 64 位架构的编译器生成大端 64 位的可执行文件。

我们任选其一进行编译看看,我这里就选择第一个了

那么这里就编译好了,怎么去运行编译好的程序呢

这里直接运行当然会报错,因为本地的架构不同,所需的依赖也没有,所以这里是依靠qemu来进行模拟运行

qemu-mipsel -L /usr/mips-linux-gnu/ ./test_mipsel_32

这里的-L /usr/mips-linux-gnu/指向MIPS库的位置,根据具体环境可能需要调整。这里是默认的位置,当然如果你的并不可以,那可以选择使用如下的语句来寻找

dpkg -L libc6-mipsel-cross

那么我本地就不是上面写的语句,而是要使用下面的语句才可以运行

qemu-mipsel -L /usr/mipsel-linux-gnu/ ./test_mipsel_32

mips指令集

1. MIPS 指令集结构

MIPS 指令集是精简指令集(RISC)架构的典型代表,所有指令长度为 32 位。指令分为三种类型:R 型I 型J 型,每种类型有不同的操作码格式。

  • R 型指令(寄存器-寄存器操作):适用于寄存器之间的运算,通常包括算术和逻辑操作。
  • I 型指令(立即数操作):适用于需要立即数或地址的操作。
  • J 型指令(跳转操作):用于跳转指令。

2. 指令分类

MIPS 指令集主要分为以下几类:

(1)算术运算指令

指令 功能 格式
add 有符号整数加法 add rd, rs, rt
addi 有符号整数加法(立即数) addi rt, rs, imm
sub 有符号整数减法 sub rd, rs, rt
mult 有符号乘法 mult rs, rt
div 有符号除法 div rs, rt

(2)逻辑运算指令

指令 功能 格式
and 按位与 and rd, rs, rt
andi 按位与(立即数) andi rt, rs, imm
or 按位或 or rd, rs, rt
ori 按位或(立即数) ori rt, rs, imm
xor 按位异或 xor rd, rs, rt
nor 按位取反或 nor rd, rs, rt

(3)移位指令

指令 功能 格式
sll 左移 sll rd, rt, shamt
srl 逻辑右移 srl rd, rt, shamt
sra 算术右移 sra rd, rt, shamt

(4)数据传输指令

指令 功能 格式
lw 加载字 lw rt, offset(rs)
sw 存储字 sw rt, offset(rs)
lb 加载字节 lb rt, offset(rs)
sb 存储字节 sb rt, offset(rs)

(5)条件分支指令

指令 功能 格式
beq 等于则分支跳转 beq rs, rt, label
bne 不等则分支跳转 bne rs, rt, label
bgtz 大于零则跳转 bgtz rs, label
blez 小于等于零则跳转 blez rs, label

(6)跳转指令

指令 功能 格式
j 无条件跳转 j label
jal 跳转并链接 jal label
jr 寄存器跳转 jr rs

3. 特殊指令

  • syscall:用于系统调用,通常用于程序和操作系统之间的交互。
  • nop:空操作指令,执行时不产生任何效果,通常用于指令延迟槽。

4. 常用寄存器

MIPS 有 32 个通用寄存器,每个寄存器在约定上有特定用途,这里借用别的师傅的图片

  • MIPS 固定 4 字节指令长度
  • 栈是从内存的高地址向低地址方向增长的
  • 叶子函数:函数内部没有再调用其他函数
  • 非叶子函数:函数内部调用其他函数的函数
  • 流水线效应:在分析 MIPS 汇编代码时会发现,其跳转到函数或者分支跳转语句的下一条都是 nop (如下图),这是因为 MIPS 采用了高度的流水线,其中最重要的是跳转指令导致的分支延迟效应。在分支跳转语句后面那条语句叫做分支延迟槽,当跳转语句刚执行的一瞬间,跳转到的地址刚填充好(填充到程序计数器),还没有执行程序计数器中存放的指令,分支延迟槽的指令已经被执行了,这就是流水线效应(几条指令被同时执行,只是处于不同的阶段, MIPS 不像其他架构那样存在流水线阻塞),为了避免出现问题,因此在分支跳转语句的下一条指令通常是 nop 指令或者其他有用的指令。
  • 缓存刷新机制:MIPS CPUs有两个独立的 cache : 指令cache数据cache 。 指令和数据分别在两个不同的缓存中。当缓存满了,会触发 flush , 将数据写回到主内存。攻击者的攻击payload 通常会被应用当做数据来处理,存储在数据缓存中。当 payload 触发漏洞, 劫持程序执行流程的时候,会去执行内存中的 shellcode .如果数据缓存没有触发 flush 的话,shellcode 依然存储在缓存中,而没有写入主内存。这会导致程序执行了本该存储 shellcode 的地址处随机的代码,导致不可预知的后果。(通常执行 sleep(1) 刷新)

函数调用

函数调用时传参:如果函数的参数小于等于四个,那么会使用 $a0 ~ $a3 寄存器来存放参数。如果参数多于四个,那么多于的参数则存放到栈里(同时也会预留出前四个参数的内存空间,因为被调用者使用前四个参数时,会统一将参数放到保留的栈空间),具体情况是函数 A 调用函数 B ,调用者函数(函数A )会在自己的栈顶预留一部分空间来保存被调用者(函数 B )的参数,称之为调用参数空间

我们可以调试看看

gdb调试

#!/bin/bash

# Ensure the script exits on error
set -e

# Define variables for file name, GDB port, and architecture
target_file="test_mipsel_32"
gdb_port="1234"

# Check if the target file exists
if [ ! -f "$target_file" ]; then
  echo "Error: Target file '$target_file' does not exist."
  exit 1
fi

# Step 1: Start QEMU with GDB server enabled
echo "Starting QEMU with GDB server on port $gdb_port..."
qemu-mipsel -L /usr/mipsel-linux-gnu/ -g $gdb_port  "$target_file" &
qemu_pid=$!

# Wait for QEMU to be ready
sleep 2

echo "QEMU started with PID $qemu_pid."

# Step 2: Start GDB and connect to QEMU GDB server
mips_gdb="gdb-multiarch"

# Check if GDB is installed
if ! command -v $mips_gdb &> /dev/null; then
  echo "Error: GDB ($mips_gdb) not found. Please install it and try again."
  exit 1
fi

echo "Starting GDB and connecting to QEMU..."
$mips_gdb -ex "target remote :$gdb_port" "$target_file"

# Cleanup after GDB session ends
echo "Stopping QEMU..."
kill $qemu_pid

# End of script

这是一个脚本,用于自动启动 QEMU 并使用 GDB 连接调试 MIPS 程序。将其保存为一个 .sh 文件,并运行就可以使用gdb调试了

    • 如果需要调试其他 MIPS 可执行文件,只需修改以下行,将

      target_file

      改为需要调试的新文件名:

      target_file="your_new_mips_file"
    • 例如:

      target_file="new_test_mipsel_32"
  1. QEMU 库路径 (-L 参数)

    • 如果新目标文件需要不同的库路径,例如可能有不同的工具链和根文件系统,可以修改 QEMU 的 -L参数路径:

      qemu-mipsel -L /path/to/new/lib/ -g $gdb_port "$target_file" &
    • 确保路径中包含与目标文件相对应的共享库和 ld.so.1 动态链接器。

添加和扩展调试功能

  • GDB 启动脚本 (.gdbinit)

    • 如果有一些常用的 GDB 命令,可以将这些命令添加到 GDB 启动脚本中,例如设置断点、打印特定变量、查看寄存器等。

    • 修改 GDB 启动参数,添加更多 GDB 命令:

      $mips_gdb -ex "target remote :$gdb_port" -ex "break main" "$target_file"

      这会在 main函数上自动设置一个断点。

  • 环境变量

    • 如果的目标程序依赖特定的环境变量,可以使用 QEMU 的 -E 选项设置环境变量:

      qemu-mipsel -L /usr/mipsel-linux-gnu/ -g $gdb_port -E VAR_NAME=VALUE "$target_file" &

我们可以看到这样就可以调试了

1.函数调用机制

在 MIPS 架构中,函数调用是通过 jal(Jump and Link)指令完成的。jal 指令会跳转到目标地址,同时将返回地址保存在 $ra 寄存器中。

在截图中,我们可以看到以下指令:

  • jalx 0x43fdc070:这条指令调用了一个函数,并将当前地址存入 $ra(返回地址寄存器)中,以便函数执行结束后可以返回到原来的调用点。

2. 函数返回机制

函数的返回是通过 jr $ra 指令完成的,它会跳转到 $ra 寄存器中存储的地址,从而返回到调用函数的位置。

3. 栈指针和帧指针

在函数调用过程中,MIPS 使用堆栈指针 $sp 和帧指针 $fp 来管理函数的局部变量和保存调用环境。截图中的指令说明了函数进入时对堆栈的操作:

  • addiu $sp, $sp, -0x20:这条指令将栈指针 $sp 向下移动 32 个字节,以为当前函数调用创建新的栈帧。这是典型的函数进入操作,用于为局部变量和保存寄存器分配空间。
  • 随后,我们看到一些 sw指令,例如:
    • sw $ra, 0x1c($sp):将返回地址 $ra 存储到栈帧中,以便在函数结束时恢复。
    • sw $fp, 0x18($sp):将帧指针 $fp 保存到栈中。

在这些指令之后,函数会更新帧指针:

  • move $fp, $sp:将当前栈指针的值赋给帧指针 $fp,这样 $fp 可以作为当前栈帧的基准。

4. 参数传递

在 MIPS 架构中,函数参数通常通过寄存器 $a0$a3 传递。这些寄存器可以存储最多 4 个参数,如果参数超过 4 个,则剩余的参数会通过栈进行传递。

在截图中可以看到:

  • $a0$a1 等寄存器在函数调用过程中会被用来存储传递给被调用函数的参数。

就像这里的v0里面就存的是puts函数的第一个参数

5. 局部变量和保存寄存器

MIPS 使用 $t0$t9 作为临时寄存器,这些寄存器的值在函数调用过程中不需要被保留。而 $s0$s7 则是保存寄存器,在函数调用过程中需要保存并在返回时恢复。

在截图中可以看到多个 swlw 指令:

  • sw $s0, 0x10($sp):保存寄存器 $s0 到栈中,以便在函数返回时恢复其值。
  • lw $s0, 0x10($sp):在函数返回前,将栈中保存的值重新加载到 $s0

6. 内存访问和全局指针

在截图中的汇编代码,我们还看到涉及 $gp(全局指针)的操作,例如加载全局变量或函数指针:

  • lw $t9, <offset>($gp):从全局指针 $gp 基础地址加偏移量的位置加载一个值到 $t9,通常用于访问全局变量或调用全局函数。

7. 函数调用中的延迟槽

MIPS 架构使用延迟槽(delay slot)机制,在跳转或调用指令之后的下一条指令会在跳转前执行。这是为了充分利用流水线并减少处理器的停顿。在截图中,并未直接看到延迟槽的指令,但在 MIPS 汇编中,需要注意这种指令紧跟 jalj 的情况。

总结

  • 函数调用和返回:通过 jaljr $ra 完成,返回地址存储在 $ra
  • 栈帧管理:使用 $sp(栈指针)和 $fp(帧指针)管理函数的局部变量和返回地址。
  • 参数传递:通过 $a0$a3 传递参数,超过 4 个参数时通过栈传递。
  • 保存寄存器和局部变量:使用 swlw 将保存寄存器(如 $s0)存入栈中,在函数结束前恢复。
  • 全局指针:使用 $gp 访问全局变量或调用全局函数。
  • 延迟槽:MIPS 指令有延迟槽机制,需要注意跳转指令后的执行顺序。

参考资料

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

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

0 条评论
某人
表情
可输入 255