MIPS 学习
MIPS 汇编
学习工具:MARS MIPS 模拟器 (需要配置好 java 环境)
Compiler Explorer (godbolt.org)
先看看几个简单的例子(整理自 b 站 up 主正月点灯笼的 MIPS 科普系列教学):
- 加法
1 | li $t1,1 |
- hello world
1 | .data |
- if 分支
1 | .data |
- 循环
1 | # 1 + 2 + 3 + .. + 100 |
其实基本上和 x86/x64 是一样的。
寄存器
仅介绍用户态可以使用的寄存器。
32 个通用寄存器
寄存器编号 | 别名 | 用途 |
---|---|---|
$0 |
$zero |
常量0(constant value 0) |
$1 |
$at |
保留给汇编器(Reserved for assembler) |
$2-$3 |
$v0-$v1 |
函数调用返回值(values for results and expression evaluation) |
$4-$7 |
$a0-$a3 |
函数调用参数(arguments) |
$8-$15 |
$t0-$t7 |
暂时的(或随便用的) |
$16-$23 |
$s0-$s7 |
保存的(或如果用,需要SAVE/RESTORE的)(saved) |
$24-$25 |
$t8-$t9 |
暂时的(或随便用的) |
$28 |
$gp |
全局指针(Global Pointer) |
$29 |
$sp |
堆栈指针(Stack Pointer) |
$30 |
$fp |
帧指针(Frame Pointer) |
$31 |
$ra |
返回地址(return address) |
两个整数乘法寄存器
HI 和 LO 寄存器。
在64位处理器上,对于两个32位做乘法运算后,可以产生64位结果。可以临时将结果的低32位放在 LO 寄存器,高32位放在 HI 寄存器。例如下面的指令:
1 | mult $t0,$t1 # t0和t1的乘法操作,结果的低32位存放在寄存器LO,结果的高32位存放在寄存器HI |
HI和LO是特殊的寄存器,不允许程序直接使用,里面的结果要通过指令mflo和mfhi获取。
HI和LO寄存器也用于除法运算结果的临时保存。除法运算结果商临时存放在寄存器LO,余数存放在寄存器HI。实例如下:
1 | div $t0,$t1 #t0和t1的除法运算,运算结果的商存放在寄存器LO,余数存放在寄存器HI |
和 8086 中的 ax 和 dx 的作用极为相似。
32 个浮点寄存器
浮点寄存器编号 | 功能 |
---|---|
$f0,$f2 |
用作函数返回值 |
$f12−$f19 |
用作传递参数 |
$f24−$f31 |
寄存器变量,发生函数调用时要保存 |
$f1、$f3−$f11 、$f20−$f23 |
用作临时变量 |
功能的分配基本上和通用寄存器相同。比如一个函数返回值为整数或指针时,使用通用寄存器的v0,返回值为浮点类型时,就使用浮点寄存器 f0。函数调用时的参数是整型就用通用寄存器 a0-a7 传递,如果是浮点类型就用 f12−f19 传递。对于函数返回地址、栈指针等还是使用通用寄存器的ra、sp。
常用指令
addiu
1 | addiu $dst, $src, immediate |
无符号立即数相加,这条指令在执行时不会引发整数溢出异常。
$dst
是目标寄存器,存储相加的结果。$src
是源寄存器,其当前值将与立即数相加。immediate
是一个无符号的16位立即数,用于相加。
addi
1 | addi $dst, $src, immediate |
用于将一个寄存器的值与一个16位的有符号的立即数相加,并将结果存储回该寄存器。
sw
1 | sw $source, offset($base) |
一个存储字(Store Word)的指令。用于将一个32位的字(通常是一个整数)存储到内存中的指定位置。
$source
是要存储的寄存器,其中包含要存储的32位数据。offset
是一个16位的立即数偏移量,用于计算存储地址。$base
是基址寄存器,包含存储目标的基本内存地址。
指令的执行过程是将 $source
寄存器中的值存储到内存地址 $base + offset
处。通常用于将数据写入数组或其他数据结构中的特定位置。
move(伪指令)
1 | move $t1, $t0 |
将一个寄存器的值移动到另一个寄存器,类似于赋值操作。
li(伪指令)
1 | li $dst, immediate |
用于将一个立即数加载到一个寄存器中。
$dst
是目标寄存器,用于存储加载的立即数。immediate
是一个立即数。
la(伪指令)
1 | la $dst, label |
将一个数据标签(通常是全局变量或标签)的地址加载到一个寄存器中。
$dst
是目标寄存器,用于存储加载的地址。label
是数据标签的名称。
lw
1 | lw $dst, offset($base) |
从内存中加载一个32位字(word)到寄存器中。
$dst
是目标寄存器,用于存储加载的32位字。offset
是一个16位的立即数偏移量,用于计算内存地址。$base
是基址寄存器,包含计算出的内存地址的基本值。
指令的执行过程是从内存中地址 $base + offset
处读取一个32位字,并将其存储到目标寄存器 $dst
中。
jalr
1 | jalr $dst, $src |
用于实现子程序调用和保存返回地址。
$dst
是目标寄存器,用于存储返回地址。$src
是源寄存器,包含子程序的地址。
jalr
指令会将当前指令的地址(即返回地址)存储到 $dst
寄存器中,并跳转到 $src
寄存器中存储的地址。
jr
1 | jr $src |
无条件跳转到一个寄存器中存储的地址。
jal
1 | jal target_address |
用于进行无条件跳转,并将返回地址保存到 $ra
寄存器中,以便在后续的程序中进行子程序调用的返回。
ori
1 | ori $dst, $src, immediate |
用于将一个寄存器的值与一个立即数进行按位或操作,并将结果存储回该寄存器。
$dst
是目标寄存器,用于存储按位或操作的结果。$src
是源寄存器,其当前值将与立即数进行按位或操作。immediate
是一个16位的立即数,用于按位或操作。
slti
1 | slti $dst, $src, immediate |
将一个寄存器的值与一个立即数进行有符号比较,如果寄存器的值小于立即数,则将目标寄存器的值设置为 1,否则设置为 0。
syscall
在 MARS MIPS 模拟器中编程可使用的:
功能 | 调用号$v0 | 参数 |
---|---|---|
print integer | 1 | $a0 = integer to print |
print float | 2 | $f12 = float to print |
print double | 3 | $f12 = double to print |
print string | 4 | $a0 = address of null-terminated string to print |
read integer | 5 | |
read float | 6 | |
read double | 7 | |
read string | 8 | $a0 = 输入缓冲区的地址 $a1 = 读取的最大字符数 |
sbrk (allocate heap memory) | 9 | $a0 = number of bytes to allocate |
exit (terminate execution) | 10 | |
print character | 11 | $a0 = character to print |
read character | 12 | |
open file | 13 | $a0 = address of null-terminated string containing filename $a1 = flags $a2 = mode |
read from file | 14 | $a0 = file descriptor $a1 = address of input buffer $a2 = maximum number of characters to read |
write to file | 15 | $a0 = file descriptor $a1 = address of output buffer $a2 = number of characters to write |
close file | 16 | $a0 = file descriptor |
sleep | 32 | $a0 = the length of time to sleep in milliseconds. |
print integer in hexadecimal | 34 | $a0 = integer to print |
print integer in binary | 35 | $a0 = integer to print |
print integer as unsigned | 36 | $a0 = integer to print |
真实环境下系统调用号以及调用方式参考:
[源码中规定的系统调用号][https://github.com/spotify/linux/blob/master/arch/mips/include/asm/unistd.h]
[调用方式][https://www.linux-mips.org/wiki/Syscall]
https://syscalls.w3challs.com/?arch=mips_o32
shellcode 编写
参考:https://fireshellsecurity.team/writing-a-shellcode-for-mips32/
做了一些 mips pwn,主要还是以 32 位小端为主。
先利用 pwntools 生成的 shellcode 作为参考:
1 | /* execve(path='//bin/sh', argv=['sh'], envp={}) */ |
来做一些修改,首先把 shellcode 的长度尽可能缩减,其次,为了增加 shellcode 的兼容性,尽可能不使用伪指令。
MIPS 的 shellcode 和 x86/x64 本质上是一样的,首先需要构造 /bin/sh\x00 字符串并将其地址传给相应寄存器,接着设置系统调用号,执行 execve 系统调用。
1 | # $a0 存放 /bin/sh 的地址 |
1.解决 /bin/sh\x00 的问题
MIPS 的通用寄存器都是 32 位的,一次只能存储 /bin/sh\x00 的一半。
因此将 /bin/sh\x00 分为两半存放在两个临时寄存器中:
1 | lui $t7, 0x2f2f |
多一个 ‘/‘ 用作填充。
同样的,把它们依次放到栈上。
1 | sw $t7, -12($sp) |
2.将 a1 和 a2 归零
1 | slti $a1, $zero, -1 |
3.传递系统调用号,进行系统调用
1 | li $v0, 4011 |
按照正常的流程,到这里就该结束了,但是这里 syscall 的默认机器码为 \x00\x00\x00\x0c,出现了空字节。为了避免传输不可见字符,最后的 syscall 要改成 syscall + (0x40404 ~ 0x40407) 的一个数(这样 \x0c 之前正好没有空字节),这点在 pwntools 生成的 shellcode 中也出现了。
1 | from pwn import * |
可以 getshell。而且 syscall 后面只要跟的是范围内的数字,编译得到的机器码都是 \x01\x01\x01\xcc ,这是为什么?暂时不想探究了。
可以 getshell :
1 | teriteri@ubuntu:~/Desktop/MIPS$ python shellcode32.py |
qemu 调试方法
在用户模式下,有几种可行的方法可以实现调试,但都没法同时利用 pwntools,而且非常卡。
如果通过系统模式直接使用 qemu 虚拟机,则需要虚拟机配置及文件上传的问题,非常麻烦。
这块等校赛之后再做补充了。
例题
axb_2019_mips
经典什么保护也没开的 32 位小端。
main 函数里没什么,主要的东西在 vuln 里:
1 | ssize_t vuln() |
mips 是没有 bp 指针的,所以值得注意的是调用函数的返回地址是如何存储的,是否会被压入栈,要怎么控制?
看一下汇编,首先是 main 函数:

在 jal vuln (以及一些函数调用)之前,lw $gp,xxx 指令出现地很频繁。
$gp 是一个全局指针(是访问全局变量的关键), $fp 为帧指针,lw 指令从当前栈帧(使用帧指针 $fp)的局部变量中加载一个值,然后加上一个常数偏移量,并将结果存储到全局指针 $gp 中。确保函数能够正确地访问全局数据。
紧接着 jal 指令,跳转到 vuln 地址处执行 vuln。同时将下一条指令的地址存储在 $ra 寄存器中。
看看 vuln:

这条指令:
1 | addiu $v0, $fp, 0x38+var_20 |
0x38 + var_20 是不可更改的,但是 $fp 的值可以通过第一次 read 栈溢出进行覆盖(类似于覆盖 ebp)。
直接把 $fp 位置处的值修改为想要注入 shellcode 的地址 + offset 即可。这里不知道栈地址,所以选择在 bss 段写 shellcode。
返回地址就覆盖为 read 函数开始的地址,即 addiu 之前的 0x4007E0。
第二次 read 覆盖返回地址为 shellcode 地址,劫持程序执行 shellcode。
exp:
1 | from pwn import* |