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,它必须先把原来的值存到栈里,用完再恢复(即“谁用谁负责还原”)。
常用指令
| 指令 | 功能 | 语法 | 示例 | 解释 |
|---|---|---|---|---|
add |
加法(有符号) | add $rd, $rs, $rt |
add $t0, $t1, $t2 |
将 $t1 和 $t2 的值相加,结果存储在 $t0 中。 |
addu |
加法(无符号) | addu $rd, $rs, $rt |
addu $t0, $t1, $t2 |
将 $t1 和 $t2 的值相加(无符号),结果存储在 $t0 中。 |
sub |
减法(有符号) | sub $rd, $rs, $rt |
sub $t0, $t1, $t2 |
将 $t1 的值减去 $t2 的值,结果存储在 $t0 中。 |
subu |
减法(无符号) | subu $rd, $rs, $rt |
subu $t0, $t1, $t2 |
将 $t1 的值减去 $t2 的值(无符号),结果存储在 $t0 中。 |
and |
按位与 | and $rd, $rs, $rt |
and $t0, $t1, $t2 |
将 $t1 和 $t2 的值按位与,结果存储在 $t0 中。 |
or |
按位或 | or $rd, $rs, $rt |
or $t0, $t1, $t2 |
将 $t1 和 $t2 的值按位或,结果存储在 $t0 中。 |
xor |
按位异或 | xor $rd, $rs, $rt |
xor $t0, $t1, $t2 |
将 $t1 和 $t2 的值按位异或,结果存储在 $t0 中。 |
nor |
按位与非 | nor $rd, $rs, $rt |
nor $t0, $t1, $t2 |
将 $t1 和 $t2 的值按位与非,结果存储在 $t0 中。 |
sll |
左移 | sll $rd, $rt, shamt |
sll $t0, $t1, 2 |
将 $t1 的值左移 2 位,结果存储在 $t0 中。 |
srl |
逻辑右移 | srl $rd, $rt, shamt |
srl $t0, $t1, 2 |
将 $t1 的值右移 2 位,结果存储在 $t0 中。 |
sra |
算术右移 | sra $rd, $rt, shamt |
sra $t0, $t1, 2 |
将 $t1 的值算术右移 2 位,结果存储在 $t0 中 |
lw |
加载字(32 位) | lw $rt, offset($rs) |
lw $t0, 4($a0) |
从地址 $a0 + 4 处加载 4 字节数据到 $t0。 |
sw |
存储字(32 位) | sw $rt, offset($rs) |
sw $t0, 4($a0) |
将 $t0 中的数据存储到地址 $a0 + 4 处。 |
lb |
加载字节(8 位) | lb $rt, offset($rs) |
lb $t0, 0($a0) |
从地址 $a0 处加载 1 字节数据到 $t0。 |
sb |
存储字节(8 位) | sb $rt, offset($rs) |
sb $t0, 0($a0) |
将 $t0 中的 1 字节数据存储到地址 $a0 处。 |
lui |
加载上半字(立即数) | lui $rt, imm |
lui $t0, 0x1234 |
将立即数 0x1234 加载到 $t0 的高 16 位(低 16 位为 0)。 |
ori |
立即数按位或 | ori $rt, $rs, imm |
ori $t0, $t1, 0xFF |
将 $t1 和立即数 0xFF 按位或,结果存储在 $t0 中。 |
beq |
等于分支 | beq $rs, $rt, offset |
beq $t0, $t1, label |
如果 $t0 等于 $t1,则跳转到 label。 |
bne |
不等于分支 | bne $rs, $rt, offset |
bne $t0, $t1, label |
如果 $t0 不等于 $t1,则跳转到 label。 |
j |
无条件跳转 | j target |
j label |
跳转到 label 处。 |
jal |
跳转并链接 | jal target |
jal subroutine |
跳转到 subroutine,并将返回地址存储在 $ra 寄存器中。 |
jr |
跳转寄存器 | jr $rs |
jr $ra |
跳转到 $ra 寄存器中存储的地址。 |
nop |
空操作 | nop |
nop |
什么也不做,通常用于填充延迟槽。 |
总结
参数寄存器 (
$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。常用于清零或赋值操作。
延迟绑定
这是 MIPS 的特殊之处。当 beq 或 bne 条件成立需要跳转时,紧跟在分支指令后面的那一条指令仍然会被执行,然后才跳转到目标位置。
Pwn 贴士: 在分析漏洞函数时,如果你看到一条
bne指令,别忘了看它下面那一行做了什么。有时候关键的寄存器赋值就在延迟槽里完成。
demo
1 | # ---------------------------------------------------------------- |

如何在find_max:内部调用函数要通过sw把返回地址存在栈上
通过lw取出(相当于在sp+4的位置上临时储存返回地址)
1 | find_max: |
XYCTF2024_EZ1.0?例题
gdb调试流程
启动 QEMU (Server 端)
1 | qemu-mipsel-static -g 1234 ./mips |
再开一个终端进行gdb调试
1 | gdb-multiarch ./mips#启动gdb并连接 |
杀死所有QEMU进程
1 | killall -9 qemu-mipsel-static |
分析

输入地址是在sp+0x18处

lw $ra, 0x58+var_s4($sp)看到返回地址是在sp+0x5c处
所以垃圾字节应该填充0x44个,也可以通过cyclic 0x100去看
1 | .text:0040088C lw $ra, 0x58+var_s4($sp) |
找到偏移之后通过read写shellcode到bss,然后让它跳转过去执行
写shellcode
覆盖fp,并改变逻辑让它去再次执行read
为了保持fp和sp一样,fp要减去0x60
1 | payload = b'a'*0x40 + p32(bss + 0x200 - 0x60) + p32(read_addr) |
exp
1 | from pwn import * |
ARM架构拓展
工具下载
1 | sudo apt update |
ARM 的运行库下载
1 | sudo apt install libc6-armel-cross |
调试环境搭建
qemu-arm -g 1234 -L /usr/arm-linux-gnueabi ./pwn 程序会暂停,等待 gdb 连接
-L 指定 ARM 的运行库路径
另一终端启动 gdbgdb-multiarch ./pwn
设置架构 set architecture arm
连接 target remote :1234
脚本
1 | from pwn import * |
调试模式
1 | p = process(["qemu-arm", "-g", "1234", "-L", "/usr/arm-linux-gnueabi", "./pwn"]) |
然后
gdb-multiarch ./pwntarget remote :1234
AARCH64(64位 ARM)
1 | sudo apt install libc6-arm64-cross |
运行:
1 | qemu-aarch64 -L /usr/aarch64-linux-gnu ./pwn |
调试:
1 | qemu-aarch64 -g 1234 -L /usr/aarch64-linux-gnu ./pwn |
基础操作
ARM32:16 个通用寄存器 R0~R12、SP、LR、PC
ARM64:31 个通用寄存器 X0~X30,另有 SP、PC
| 寄存器 | 核心用途 |
|---|---|
| R0~R3 | 前 4 个函数参数、R0 存函数返回值 |
| R4~R10 | 被调用者保存,函数调用不会被随便改,存局部变量 |
| R11(FP) | 栈帧指针,用来标记函数栈起始位置,回溯调用栈 |
| R12(IP) | 临时中转寄存器,函数跳转、动态链接用 |
三大特殊寄存器(重中之重)
- SP 栈指针:永远指向栈顶,压栈减 SP、出栈加 SP
- LR 链接寄存器:调用函数时,自动存返回地址
- PC 程序计数器:存当前执行指令地址,改 PC 就能劫持流程(Pwn 核心)
XYCTF2024_EZ2.0?例题


开了NX保护,可以调用mprotect函数机进行
exp
1 |
|