MIPS汇编


前言

为了学习异架构的 pwn 和 IOT 来学习 MIPS 的汇编。

环境搭建

编译环境

  • 安装交叉编译工具

安装 MIPS 架构交叉编译工具。

1
2
3
sudo apt update
sudo apt install gcc-mips-linux-gnu binutils-mips-linux-gnu
sudo apt install gcc-mipsel-linux-gnu binutils-mipsel-linux-gnu

我们也可以通过 mars 模拟器软件来进行 MIPS 汇编的学习。

  • 编译流程

将 MIPS 汇编文件编译为可执行文件。这里我们先要确定需要编译端序的文件架构,然后选择相应的编译工具。

这里我们以 MIPS 32 位架构小端序为例。

  1. 将汇编文件编译为目标文件

因为我们需要将汇编文件编译为 MIPS 架构小端序的可执行文件,所以这里选择 mipselmips 架构小端序编译工具,默认为 32 位。

1
mipsel-linux-gnu-as demo.s -o demo.o
  1. 链接目标文件

将目标文件链接为可执行程序,同样选择相应架构的链接器进行链接。

1
mipsel-linux-gnu-ld demo.o -o demo

运行和调试环境

  • 安装 qemu

我们通过 qemu 来模拟运行

1
2
sudo apt update
sudo apt install qemu qemu-user qemu-system-mips
  • 远行流程

根据可执行文件架构,选择对应的 qemu 来运行。

例如 MIPS 32位小端序架构,我们可以选择 qemu-mipsel 来运行

这里还要区分链接方式,如果动态链接则需要指定动态链接库。

静态链接:

1
qemu-mipsel-static ./demo

动态链接:

1
qemu-mipsel -L /usr/mipsel-linux-gnu ./demo
  • 调试流程

安装gdb-multiarch来调试异架构程序

1
sudo apt install  gdb-multiarch

首先我们通过 qemu 以调试模式运行程序,-g参数表示设置为调试模式,1234表示程序映射的端口。

1
qemu-mipsel-static -g 1234 ./demo

然后运行gdb-multiarch,并根据架构和端序设置参数。

1
2
3
gdb-multiarch
set architecture mips
set endian little

然后连接映射程序的端口

1
target remote localhost:1234

MIPS 架构知识

  • MIPS 固定 4 字节指令长度

  • 栈是从内存的高地址向低地址方向增长的

  • 叶子函数:函数内部没有再调用其他函数

  • 非叶子函数:函数内部调用其他函数的函数

  • 流水线效应:在分析 MIPS 汇编代码时会发现,其跳转到函数或者分支跳转语句的下一条都是 nop (),这是因为 MIPS 采用了高度的流水线,其中最重要的是跳转指令导致的分支延迟效应。在分支跳转语句后面那条语句叫做分支延迟槽,当跳转语句刚执行的一瞬间,跳转到的地址刚填充好(填充到程序计数器),还没有执行程序计数器中存放的指令,分支延迟槽的指令已经被执行了,这就是流水线效应(几条指令被同时执行,只是处于不同的阶段, MIPS 不像其他架构那样存在流水线阻塞),为了避免出现问题,因此在分支跳转语句的下一条指令通常是 nop 指令或者其他有用的指令。

  • 缓存刷新机制:MIPS CPUs有两个独立的 cache : 指令cache 和 数据cache 。 指令和数据分别在两个不同的缓存中。当缓存满了,会触发 flush , 将数据写回到主内存。攻击者的攻击payload 通常会被应用当做数据来处理,存储在数据缓存中。当 payload 触发漏洞, 劫持程序执行流程的时候,会去执行内存中的 shellcode .如果数据缓存没有触发 flush 的话,shellcode 依然存储在缓存中,而没有写入主内存。这会导致程序执行了本该存储 shellcode 的地址处随机的代码,导致不可预知的后果。(通常执行 sleep(1) 刷新)

  • 只有特定的指令(如 lwsw)可以访问内存,所有其他操作都在寄存器之间完成。这种设计简化了指令和流水线操作。

汇编基础

寄存器

MIPS 有 32 个通用寄存器,其中每个寄存器都是 32 位,用于存储操作数、中间结果、返回地址等。

寄存器编号 寄存器名称 寄存器描述
0 zero 永远为零
1 $at 保留寄存器
2~3 $v0~$v1 函数返回值
4~7 a0-a3 函数参数寄存器,不够用堆栈处理
8~15 t0-t7 临时寄存器
16~23 $s0~$s7 保存寄存器,用于保存函数调用中的参数和返回值
24~25 $t8~$t9 临时寄存器
26~27 $k0~$k1 保留寄存器
28 $gp 全局指针
29 $sp 堆栈指针
30 $fp 保存栈指针
31 $ra 返回地址

MIPS 架构中还定义了 3 个特殊的寄存器,分别是PC(程序计数器)、HI(乘除结果高位寄存器)、LO(乘除结果低位寄存器)。

指令格式

MIPS 的指令集采用 3 种主要格式:

  1. R 型指令(Register)

    • 这种指令格式用于寄存器之间的操作。
    • 格式:opcode | rs | rt | rd | shamt | funct
      • opcode:操作码,指令该指令的操作类型。
      • rs,rt:源寄存器。
      • rd:目标寄存器。
      • shamt:移位操作的位数。
      • funct:指定具体操作的附加操作码。
  2. I 型指令(Immediate)

    • 这种指令用于处理立即数(常量)和寄存器的操作。
    • 格式:opcode | rs | rt | immediate
      • opcode:操作码,指定该指令的操作类型。
      • rs:源寄存器。
      • rt:目标寄存器。
      • immediate:立即数,用于与寄存器中的值进行运算。
  3. J 型指令(Jump)

    • 这种指令用于跳转到其他地址。
    • 格式:opcode | address
      • opcode:操作码。
      • address:跳转目标地址。

寻址方式

  1. 立即数寻址:操作数是位于指令自身中的常数。

$t1 中的值加上常数 10,并存储在 $t0

1
addi $t0, $t1, 10
  1. 寄存器寻址:操作数是寄存器。

$t1$t2 的值相加,结果存储在 $t0

1
add $t0, $t1, $t2
  1. 基址寻址或偏移寻址:操作数在内存中,其地址是指令中基址寄存器和常数的和,如lw与sw指令。

$t1 中的基址加上偏移量 4 的内存地址处加载数据到 $t0 中,然后将 $t0 的值存储到 $sp 基址减去 8 的内存地址中。

1
2
lw $t0, 4($t1)
sw $t0, -8($sp)
  1. PC相对寻址:地址是PC和指令中常数的和。

如果 $t0$t1 相等,则跳转到偏移地址处执行

1
beq $t0, $t1, offset
  1. 伪直接寻址:跳转地址由指令中26字段和PC高位相连而成。

跳转到目标地址,由 PC 高位与指令中的 26 位目标字段拼接而成

1
j target

常用指令

类型 指令 功能描述 示例
R 型 add 加法,$rd = $rs + $rt add $t0, $t1, $t2
R 型 sub 减法,$rd = $rs - $rt sub $t0, $t1, $t2
R 型 and 按位与,$rd = $rs & $rt and $t0, $t1, $t2
R 型 or 按位或,$rd = $rs $rt or $t0, $t1, $t2
R 型 xor 按位异或,$rd = $rs ^ $rt xor $t0, $t1, $t2
R 型 nor 按位取反或,$rd = ~($rs $rt) nor $t0, $t1, $t2
R 型 sll 逻辑左移,$rd = $rt << sa sll $t0, $t1, 2
R 型 srl 逻辑右移,$rd = $rt >> sa srl $t0, $t1, 2
R 型 sra 算术右移,$rd = $rt >> sa sra $t0, $t1, 2
R 型 slt 小于,$rd = ($rs < $rt) ? 1 : 0 slt $t0, $t1, $t2
R 型 sltu 无符号小于,$rd = ($rs < $rt unsigned) ? 1 : 0 sltu $t0, $t1, $t2
I 型 addi 加法立即数,$rt = $rs + imm addi $t0, $t1, 10
I 型 addiu 无符号加法立即数,$rt = $rs + imm addiu $t0, $t1, 10
I 型 andi 按位与立即数,$rt = $rs & imm andi $t0, $t1, 0xFF
I 型 ori 按位或立即数,$rt = $rs imm ori $t0, $t1, 0xFF
I 型 xori 按位异或立即数,$rt = $rs ^ imm xori $t0, $t1, 0xFF
I 型 slti 小于立即数,$rt = ($rs < imm) ? 1 : 0 slti $t0, $t1, 10
I 型 sltiu 无符号小于立即数,$rt = ($rs < imm unsigned) ? 1 : 0 sltiu $t0, $t1, 10
I 型 lui 加载上半字,$rt = imm << 16 lui $t0, 0x1000
I 型 lw 从内存加载字(32位)到寄存器 $rt lw $t0, 0($t1)
I 型 sw 将寄存器 $rt 的值存储到内存中的地址 $t1 + offset sw $t0, 0($t1)
I 型 lb 从内存加载字节(8位)到寄存器 $rt lb $t0, 0($t1)
I 型 lbu 从内存加载无符号字节(8位)到寄存器 $rt lbu $t0, 0($t1)
I 型 sb 将寄存器 $rt 的低8位存储到内存中的地址 $t1 + offset sb $t0, 0($t1)
I 型 lh 从内存加载半字(16位)到寄存器 $rt lh $t0, 0($t1)
I 型 lhu 从内存加载无符号半字(16位)到寄存器 $rt lhu $t0, 0($t1)
I 型 sh 将寄存器 $rt 的低16位存储到内存中的地址 $t1 + offset sh $t0, 0($t1)
I 型 lwc1 从内存加载单精度浮点数到浮点寄存器 $f0 lwc1 $f0, 0($t1)
I 型 swc1 将浮点寄存器 $f0 中的单精度浮点数存储到内存中的地址 $t1 + offset swc1 $f0, 0($t1)
I 型 ld 从内存加载双字(64位)到寄存器 $rt ld $t0, 0($t1)
I 型 sd 将寄存器 $rt 的64位值存储到内存中的地址 $t1 + offset sd $t0, 0($t1)
J 型 j 无条件跳转,跳转到目标地址 j target
J 型 jal 跳转并链接,将返回地址存储到 $ra jal target
J 型 jr 跳转到寄存器指定的地址 jr $ra
J 型 jalr 跳转并链接到寄存器指定的地址,返回地址存储在 $ra jalr $t0
特殊 syscall 系统调用,执行指定的操作 syscall
特殊 break 生成断点,通常用于调试 break
特殊 eret 从异常中返回到用户态 eret
特殊 nop 无操作,通常用于填充 nop

函数调用约定

如果函数的参数小于等于四个,那么会使用 $a0-$a3 寄存器来存放参数。

如果参数多于四个,那么多出来的参数会使用栈进行传参。

函数 A 调用函数 B。若 B 是叶子函数(即 B 不调用其他函数),调用 B 时会将返回地址直接存入 $ra 寄存器;若 B 是非叶子函数(即 B 调用了函数 C),则在跳转到 B 时,返回地址会先存入 $ra,然后在 B 内部将 $ra 的值保存到栈中。

B 调用 C 时,C 的返回地址会覆盖 $ra 的值;返回时,jr $ra 指令会跳回 B 的调用点。待 B 执行完毕准备返回 A 时,它会从栈中恢复原始的返回地址到 $ra,再通过 jr $ra 跳回 A

系统调用

在 x86 的 Linux 系统中,功能函数是由系统调用来实现的。我们可以直接使用功能函数,也可以使用系统调用来使用相应功能。同样,在 MIPS 架构中,系统调用机制也类似,通过 syscall 指令触发。MIPS 的系统调用实现方式不同于 x86,但基本原理相同:通过触发中断与内核进行交互。

  • 系统调用号:在 MIPS 中 $v0 寄存器用于存放系统调用号。
  • 参数:系统调用的参数通常存储在 $a0$a3 寄存器中,最多可以有 4 个参数。
  • 返回值:系统调用的返回值通常存储在 $v0v1 寄存器中。

注:在 MIPS 架构下,系统调用号可能因操作系统而异。

以下是基于 Linux MIPS ABI 的一些常用系统调用及其功能:

系统调用号 名称 功能说明
4001 exit 退出程序,参数是退出状态码
4003 read 从文件描述符读取数据
4004 write 向文件描述符写入数据
4005 open 打开文件
4006 close 关闭文件描述符
4009 link 创建硬链接
4010 unlink 删除文件
4011 execve 执行新程序
4012 chdir 改变当前工作目录
4013 time 获取当前时间
4014 mknod 创建特殊文件
4015 chmod 修改文件权限
4016 lseek 文件指针定位
4017 getpid 获取当前进程 ID
4020 getuid 获取用户 ID
4021 geteuid 获取有效用户 ID
4022 getgid 获取组 ID
4023 getegid 获取有效组 ID
4024 ptrace 调试工具接口
4037 kill 向进程发送信号
4038 uname 获取系统信息
4040 mkdir 创建目录
4041 rmdir 删除目录
4042 dup 复制文件描述符
4045 brk 调整进程数据段的大小(分配堆内存)
4057 socket 创建套接字
4066 gettimeofday 获取时间和时区
4083 bind 绑定套接字
4085 listen 监听连接
4086 accept 接受连接

Lab

Hello World

我们通过编写经典程序 Hello World 作为我们 MIPS 汇编 Lab 的开始。

代码中 .section 用于定义段,我们用它定义了 .text 段和 .data 段。

我们声明了一个标签 msg,它是 Hello World! 的地址。

.asciiz 用于定义一个以空字符结尾的字符串。

.set noreorder 用于禁止指令重排序。

.global __start 用于将 __start 定义为全局符号,是程序的入口点。我们通过 __start 告诉编译器,从这里开始执行代码。

然后我们通过设置 write 系统调用的参数最后触发中断输出 Hello World!

然后通过执行 exit 系统调用让程序正常退出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.section .data

msg: .asciiz "Hello World!"

.section .text
.set noreorder
.global __start

__start:

la $a0,0
la $a1,msg
la $a2,13
li $v0,4004
syscall

li $v0,4001
syscall

shellcode

shellcode 即一段可以获取到 shell 的汇编代码。

这里我们通过 execve 系统调用执行 /bin/sh 获取一个 shell。

由于我们执行之后会直接弹出一个 shell,所以我们不需要使用 exit 让程序正常退出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.section .data
sh: .asciiz "/bin/sh"

.section .text
.global __start

.set noreorder

__start:
la $a0,sh
li $a1,0
li $a2,0
li $v0,4011
syscall

后言

参考:IOT安全入门学习–MIPS汇编基础 | ZIKH26’s Blog
参考:MIPS 指令集 Shellcode 编写入门-安全客 - 安全资讯平台