MIPS汇编
前言
为了学习异架构的 pwn 和 IOT 来学习 MIPS 的汇编。
环境搭建
编译环境
- 安装交叉编译工具
安装 MIPS 架构交叉编译工具。
1 |
|
我们也可以通过 mars 模拟器软件来进行 MIPS 汇编的学习。
- 编译流程
将 MIPS 汇编文件编译为可执行文件。这里我们先要确定需要编译端序的文件架构,然后选择相应的编译工具。
这里我们以 MIPS 32 位架构小端序为例。
- 将汇编文件编译为目标文件
因为我们需要将汇编文件编译为 MIPS 架构小端序的可执行文件,所以这里选择 mipsel
即 mips
架构小端序编译工具,默认为 32 位。
1 |
|
- 链接目标文件
将目标文件链接为可执行程序,同样选择相应架构的链接器进行链接。
1 |
|
运行和调试环境
- 安装 qemu
我们通过 qemu 来模拟运行
1 |
|
- 远行流程
根据可执行文件架构,选择对应的 qemu 来运行。
例如 MIPS 32位小端序架构,我们可以选择 qemu-mipsel
来运行
这里还要区分链接方式,如果动态链接则需要指定动态链接库。
静态链接:
1 |
|
动态链接:
1 |
|
- 调试流程
安装gdb-multiarch
来调试异架构程序
1 |
|
首先我们通过 qemu 以调试模式运行程序,-g
参数表示设置为调试模式,1234
表示程序映射的端口。
1 |
|
然后运行gdb-multiarch
,并根据架构和端序设置参数。
1 |
|
然后连接映射程序的端口
1 |
|
MIPS 架构知识
MIPS 固定
4
字节指令长度栈是从内存的高地址向低地址方向增长的
叶子函数:函数内部没有再调用其他函数
非叶子函数:函数内部调用其他函数的函数
流水线效应:在分析 MIPS 汇编代码时会发现,其跳转到函数或者分支跳转语句的下一条都是
nop
(),这是因为 MIPS 采用了高度的流水线,其中最重要的是跳转指令导致的分支延迟效应。在分支跳转语句后面那条语句叫做分支延迟槽,当跳转语句刚执行的一瞬间,跳转到的地址刚填充好(填充到程序计数器),还没有执行程序计数器中存放的指令,分支延迟槽的指令已经被执行了,这就是流水线效应(几条指令被同时执行,只是处于不同的阶段, MIPS 不像其他架构那样存在流水线阻塞),为了避免出现问题,因此在分支跳转语句的下一条指令通常是nop
指令或者其他有用的指令。缓存刷新机制:
MIPS CPUs
有两个独立的cache
:指令cache
和数据cache
。 指令和数据分别在两个不同的缓存中。当缓存满了,会触发flush
, 将数据写回到主内存。攻击者的攻击payload
通常会被应用当做数据来处理,存储在数据缓存中。当payload
触发漏洞, 劫持程序执行流程的时候,会去执行内存中的shellcode
.如果数据缓存没有触发flush
的话,shellcode
依然存储在缓存中,而没有写入主内存。这会导致程序执行了本该存储shellcode
的地址处随机的代码,导致不可预知的后果。(通常执行sleep(1)
刷新)只有特定的指令(如
lw
和sw
)可以访问内存,所有其他操作都在寄存器之间完成。这种设计简化了指令和流水线操作。
汇编基础
寄存器
MIPS 有 32 个通用寄存器,其中每个寄存器都是 32 位,用于存储操作数、中间结果、返回地址等。
寄存器编号 | 寄存器名称 | 寄存器描述 |
---|---|---|
0 | zero |
永远为零 |
1 | $at |
保留寄存器 |
2~3 | $v0~$v1 |
函数返回值 |
4~7 | a0-a3 |
函数参数寄存器,不够用堆栈处理 |
8~15 | t0-t7 |
临时寄存器 |
16~23 | $s0~$s7 |
保存寄存器,用于保存函数调用中的参数和返回值 |
24~25 | $t8~$t9 |
临时寄存器 |
26~27 | $k0~$k1 |
保留寄存器 |
28 | $gp |
全局指针 |
29 | $sp |
堆栈指针 |
30 | $fp |
保存栈指针 |
31 | $ra |
返回地址 |
MIPS 架构中还定义了 3 个特殊的寄存器,分别是PC
(程序计数器)、HI
(乘除结果高位寄存器)、LO
(乘除结果低位寄存器)。
指令格式
MIPS 的指令集采用 3 种主要格式:
R 型指令(Register)
- 这种指令格式用于寄存器之间的操作。
- 格式:
opcode | rs | rt | rd | shamt | funct
- opcode:操作码,指令该指令的操作类型。
- rs,rt:源寄存器。
- rd:目标寄存器。
- shamt:移位操作的位数。
- funct:指定具体操作的附加操作码。
I 型指令(Immediate)
- 这种指令用于处理立即数(常量)和寄存器的操作。
- 格式:
opcode | rs | rt | immediate
opcode
:操作码,指定该指令的操作类型。rs
:源寄存器。rt
:目标寄存器。immediate
:立即数,用于与寄存器中的值进行运算。
J 型指令(Jump)
- 这种指令用于跳转到其他地址。
- 格式:
opcode | address
opcode
:操作码。address
:跳转目标地址。
寻址方式
- 立即数寻址:操作数是位于指令自身中的常数。
将 $t1
中的值加上常数 10,并存储在 $t0
中
1 |
|
- 寄存器寻址:操作数是寄存器。
将 $t1
和 $t2
的值相加,结果存储在 $t0
中
1 |
|
- 基址寻址或偏移寻址:操作数在内存中,其地址是指令中基址寄存器和常数的和,如lw与sw指令。
从 $t1
中的基址加上偏移量 4 的内存地址处加载数据到 $t0
中,然后将 $t0
的值存储到 $sp
基址减去 8 的内存地址中。
1 |
|
PC
相对寻址:地址是PC
和指令中常数的和。
如果 $t0
和 $t1
相等,则跳转到偏移地址处执行
1 |
|
- 伪直接寻址:跳转地址由指令中26字段和PC高位相连而成。
跳转到目标地址,由 PC
高位与指令中的 26 位目标字段拼接而成
1 |
|
常用指令
类型 | 指令 | 功能描述 | 示例 | |
---|---|---|---|---|
R 型 | add |
加法,$rd = $rs + $rt | add $t0, $t1, $t2 |
|
R 型 | sub |
减法,$rd = $rs - $rt | sub $t0, $t1, $t2 |
|
R 型 | and |
按位与,$rd = $rs & $rt | and $t0, $t1, $t2 |
|
R 型 | or |
按位或,$rd = $rs | $rt | or $t0, $t1, $t2 |
R 型 | xor |
按位异或,$rd = $rs ^ $rt | xor $t0, $t1, $t2 |
|
R 型 | nor |
按位取反或,$rd = ~($rs | $rt) | nor $t0, $t1, $t2 |
R 型 | sll |
逻辑左移,$rd = $rt << sa | sll $t0, $t1, 2 |
|
R 型 | srl |
逻辑右移,$rd = $rt >> sa | srl $t0, $t1, 2 |
|
R 型 | sra |
算术右移,$rd = $rt >> sa | sra $t0, $t1, 2 |
|
R 型 | slt |
小于,$rd = ($rs < $rt) ? 1 : 0 | slt $t0, $t1, $t2 |
|
R 型 | sltu |
无符号小于,$rd = ($rs < $rt unsigned) ? 1 : 0 | sltu $t0, $t1, $t2 |
|
I 型 | addi |
加法立即数,$rt = $rs + imm | addi $t0, $t1, 10 |
|
I 型 | addiu |
无符号加法立即数,$rt = $rs + imm | addiu $t0, $t1, 10 |
|
I 型 | andi |
按位与立即数,$rt = $rs & imm | andi $t0, $t1, 0xFF |
|
I 型 | ori |
按位或立即数,$rt = $rs | imm | ori $t0, $t1, 0xFF |
I 型 | xori |
按位异或立即数,$rt = $rs ^ imm | xori $t0, $t1, 0xFF |
|
I 型 | slti |
小于立即数,$rt = ($rs < imm) ? 1 : 0 | slti $t0, $t1, 10 |
|
I 型 | sltiu |
无符号小于立即数,$rt = ($rs < imm unsigned) ? 1 : 0 | sltiu $t0, $t1, 10 |
|
I 型 | lui |
加载上半字,$rt = imm << 16 | lui $t0, 0x1000 |
|
I 型 | lw |
从内存加载字(32位)到寄存器 $rt | lw $t0, 0($t1) |
|
I 型 | sw |
将寄存器 $rt 的值存储到内存中的地址 $t1 + offset | sw $t0, 0($t1) |
|
I 型 | lb |
从内存加载字节(8位)到寄存器 $rt | lb $t0, 0($t1) |
|
I 型 | lbu |
从内存加载无符号字节(8位)到寄存器 $rt | lbu $t0, 0($t1) |
|
I 型 | sb |
将寄存器 $rt 的低8位存储到内存中的地址 $t1 + offset | sb $t0, 0($t1) |
|
I 型 | lh |
从内存加载半字(16位)到寄存器 $rt | lh $t0, 0($t1) |
|
I 型 | lhu |
从内存加载无符号半字(16位)到寄存器 $rt | lhu $t0, 0($t1) |
|
I 型 | sh |
将寄存器 $rt 的低16位存储到内存中的地址 $t1 + offset | sh $t0, 0($t1) |
|
I 型 | lwc1 |
从内存加载单精度浮点数到浮点寄存器 $f0 | lwc1 $f0, 0($t1) |
|
I 型 | swc1 |
将浮点寄存器 $f0 中的单精度浮点数存储到内存中的地址 $t1 + offset | swc1 $f0, 0($t1) |
|
I 型 | ld |
从内存加载双字(64位)到寄存器 $rt | ld $t0, 0($t1) |
|
I 型 | sd |
将寄存器 $rt 的64位值存储到内存中的地址 $t1 + offset | sd $t0, 0($t1) |
|
J 型 | j |
无条件跳转,跳转到目标地址 | j target |
|
J 型 | jal |
跳转并链接,将返回地址存储到 $ra | jal target |
|
J 型 | jr |
跳转到寄存器指定的地址 | jr $ra |
|
J 型 | jalr |
跳转并链接到寄存器指定的地址,返回地址存储在 $ra | jalr $t0 |
|
特殊 | syscall |
系统调用,执行指定的操作 | syscall |
|
特殊 | break |
生成断点,通常用于调试 | break |
|
特殊 | eret |
从异常中返回到用户态 | eret |
|
特殊 | nop |
无操作,通常用于填充 | nop |
函数调用约定
如果函数的参数小于等于四个,那么会使用 $a0-$a3
寄存器来存放参数。
如果参数多于四个,那么多出来的参数会使用栈进行传参。
函数 A
调用函数 B
。若 B
是叶子函数(即 B
不调用其他函数),调用 B
时会将返回地址直接存入 $ra
寄存器;若 B
是非叶子函数(即 B
调用了函数 C
),则在跳转到 B
时,返回地址会先存入 $ra
,然后在 B
内部将 $ra
的值保存到栈中。
当 B
调用 C
时,C
的返回地址会覆盖 $ra
的值;返回时,jr $ra
指令会跳回 B
的调用点。待 B
执行完毕准备返回 A
时,它会从栈中恢复原始的返回地址到 $ra
,再通过 jr $ra
跳回 A
。
系统调用
在 x86 的 Linux 系统中,功能函数是由系统调用来实现的。我们可以直接使用功能函数,也可以使用系统调用来使用相应功能。同样,在 MIPS 架构中,系统调用机制也类似,通过 syscall
指令触发。MIPS 的系统调用实现方式不同于 x86,但基本原理相同:通过触发中断与内核进行交互。
- 系统调用号:在 MIPS 中
$v0
寄存器用于存放系统调用号。 - 参数:系统调用的参数通常存储在
$a0
到$a3
寄存器中,最多可以有 4 个参数。 - 返回值:系统调用的返回值通常存储在
$v0
和v1
寄存器中。
注:在 MIPS 架构下,系统调用号可能因操作系统而异。
以下是基于 Linux MIPS ABI 的一些常用系统调用及其功能:
系统调用号 | 名称 | 功能说明 |
---|---|---|
4001 | exit |
退出程序,参数是退出状态码 |
4003 | read |
从文件描述符读取数据 |
4004 | write |
向文件描述符写入数据 |
4005 | open |
打开文件 |
4006 | close |
关闭文件描述符 |
4009 | link |
创建硬链接 |
4010 | unlink |
删除文件 |
4011 | execve |
执行新程序 |
4012 | chdir |
改变当前工作目录 |
4013 | time |
获取当前时间 |
4014 | mknod |
创建特殊文件 |
4015 | chmod |
修改文件权限 |
4016 | lseek |
文件指针定位 |
4017 | getpid |
获取当前进程 ID |
4020 | getuid |
获取用户 ID |
4021 | geteuid |
获取有效用户 ID |
4022 | getgid |
获取组 ID |
4023 | getegid |
获取有效组 ID |
4024 | ptrace |
调试工具接口 |
4037 | kill |
向进程发送信号 |
4038 | uname |
获取系统信息 |
4040 | mkdir |
创建目录 |
4041 | rmdir |
删除目录 |
4042 | dup |
复制文件描述符 |
4045 | brk |
调整进程数据段的大小(分配堆内存) |
4057 | socket |
创建套接字 |
4066 | gettimeofday |
获取时间和时区 |
4083 | bind |
绑定套接字 |
4085 | listen |
监听连接 |
4086 | accept |
接受连接 |
Lab
Hello World
我们通过编写经典程序 Hello World
作为我们 MIPS 汇编 Lab 的开始。
代码中 .section
用于定义段,我们用它定义了 .text
段和 .data
段。
我们声明了一个标签 msg
,它是 Hello World!
的地址。
.asciiz
用于定义一个以空字符结尾的字符串。
.set noreorder
用于禁止指令重排序。
.global __start
用于将 __start
定义为全局符号,是程序的入口点。我们通过 __start
告诉编译器,从这里开始执行代码。
然后我们通过设置 write
系统调用的参数最后触发中断输出 Hello World!
。
然后通过执行 exit
系统调用让程序正常退出。
1 |
|
shellcode
shellcode 即一段可以获取到 shell 的汇编代码。
这里我们通过 execve
系统调用执行 /bin/sh
获取一个 shell。
由于我们执行之后会直接弹出一个 shell,所以我们不需要使用 exit
让程序正常退出。
1 |
|
后言
参考:IOT安全入门学习–MIPS汇编基础 | ZIKH26’s Blog
参考:MIPS 指令集 Shellcode 编写入门-安全客 - 安全资讯平台