基础知识及简单的编译
在常规的ctf的pwn方向当中,我们常见的汇编都是基于Intel的8086汇编,但是对于很多物联网设备,其实并不都是采取这一种架构,常见如mips架构和arrch64之类,而在pwn题中,这类题型统一称作异架构,这篇文章会介绍mips架构的一些基础知识,所涉及不深,只是起到入门的效果
首先我们来介绍一下什么是mips,MIPS架构是一种采取 精简指令集(RISC)的处理器架构,1981年出现,由MIPS科技公司开发并授权,它是基于一种固定长度的定期编码指令集,并采用 导入/存储(Load/Store)数据模型。经改进,这种架构可支持高级语言的优化执行。其算术和逻辑运算采用三个操作数的形式,允许编译器优化复杂的表达式。
如今基于该架构的芯片广泛被使用在许多电子产品、网络设备、个人娱乐装置与商业装置上。最早的MIPS架构是32位,最新的版本已经变成64位。
我们的虚拟机基本都是ubuntu,也就是基于8086架构,所以这里我们学习mips需要理由qemu来模拟这个架构,而QEMU代表快速模拟器,是虚拟化领域的一个重要工具,能够在单个硬件平台上同时运行多个操作系统。以其能够仿真广泛的客户系统和架构而闻名,QEMU是创建和管理虚拟环境的多功能解决方案。它作为一个类型1虚拟机运行,直接与物理硬件接口,这与其他虚拟化技术有显著的不同。通过整合像Intel VT和AMD-V这样的硬件虚拟化技术,QEMU优化了虚拟机的性能,为开发人员和IT专业人员提供了一个强大的平台,用于模拟各种计算环境,无需为每个系统提供专用硬件。
#!/bin/sh
# 安装QEMU及相关工具
sudo apt install qemu
sudo apt install qemu-system qemu-user-static binfmt-support
# 安装ARM架构编译器和依赖
sudo apt install libncurses5-dev gcc-arm-linux-gnueabi build-essential
# 安装MIPS架构编译器
sudo apt-get install gcc-mips-linux-gnu
sudo apt-get install gcc-mipsel-linux-gnu
sudo apt-get install gcc-mips64-linux-gnuabi64
sudo apt-get install gcc-mips64el-linux-gnuabi64
# 安装多架构gdb调试依赖
sudo apt install gdb-multiarch
这里有一个自动化脚本,运行就可以完整配置qemu和mips,那么我本地使用的是ubuntu22.04
这里我们可以编译一下自己的第一个mips架构的题目
#include <stdio.h>
int main() {
printf("Hello, MIPS!\n");
return 0;
}
这是一个最简单的hello world,我们来学习怎么编译
不同于8086的小端序,mips有大端和小端格式两种格式,分别为 mips
和 mipsel
系列,且支持32位和64位的指令集,这两种格式的编译语句也是不同的,那么我本地的这个c文件叫做test.c,下面的语句,真正决定编译情况的是前面利用的gcc,而最后的名字大家可以自行决定,这里只是为了区分
#32位小端序:
mipsel-linux-gnu-gcc -g test.c -o test_mipsel_32
#使用小端 MIPS 架构的编译器生成 32 位小端格式的可执行文件。
#32位大端序:
mips-linux-gnu-gcc -g test.c -o test_mips_32
#使用大端 MIPS 架构的编译器生成 32 位大端格式的可执行文件。
#64位小端序:
mips64el-linux-gnuabi64-gcc -g test.c -o test_mipsel_64
#使用小端 MIPS 64 位架构的编译器生成小端 64 位的可执行文件。
#64位大端序:
mips64-linux-gnuabi64-gcc -g test.c -o test_mips_64
#使用大端 MIPS 64 位架构的编译器生成大端 64 位的可执行文件。
我们任选其一进行编译看看,我这里就选择第一个了
那么这里就编译好了,怎么去运行编译好的程序呢
这里直接运行当然会报错,因为本地的架构不同,所需的依赖也没有,所以这里是依靠qemu来进行模拟运行
qemu-mipsel -L /usr/mips-linux-gnu/ ./test_mipsel_32
这里的-L /usr/mips-linux-gnu/
指向MIPS库的位置,根据具体环境可能需要调整。这里是默认的位置,当然如果你的并不可以,那可以选择使用如下的语句来寻找
dpkg -L libc6-mipsel-cross
那么我本地就不是上面写的语句,而是要使用下面的语句才可以运行
qemu-mipsel -L /usr/mipsel-linux-gnu/ ./test_mipsel_32
mips指令集
1. MIPS 指令集结构
MIPS 指令集是精简指令集(RISC)架构的典型代表,所有指令长度为 32 位。指令分为三种类型:R 型、I 型 和 J 型,每种类型有不同的操作码格式。
- R 型指令(寄存器-寄存器操作):适用于寄存器之间的运算,通常包括算术和逻辑操作。
- I 型指令(立即数操作):适用于需要立即数或地址的操作。
- J 型指令(跳转操作):用于跳转指令。
2. 指令分类
MIPS 指令集主要分为以下几类:
(1)算术运算指令
指令 | 功能 | 格式 |
---|---|---|
add |
有符号整数加法 | add rd, rs, rt |
addi |
有符号整数加法(立即数) | addi rt, rs, imm |
sub |
有符号整数减法 | sub rd, rs, rt |
mult |
有符号乘法 | mult rs, rt |
div |
有符号除法 | div rs, rt |
(2)逻辑运算指令
指令 | 功能 | 格式 |
---|---|---|
and |
按位与 | and rd, rs, rt |
andi |
按位与(立即数) | andi rt, rs, imm |
or |
按位或 | or rd, rs, rt |
ori |
按位或(立即数) | ori rt, rs, imm |
xor |
按位异或 | xor rd, rs, rt |
nor |
按位取反或 | nor rd, rs, rt |
(3)移位指令
指令 | 功能 | 格式 |
---|---|---|
sll |
左移 | sll rd, rt, shamt |
srl |
逻辑右移 | srl rd, rt, shamt |
sra |
算术右移 | sra rd, rt, shamt |
(4)数据传输指令
指令 | 功能 | 格式 |
---|---|---|
lw |
加载字 | lw rt, offset(rs) |
sw |
存储字 | sw rt, offset(rs) |
lb |
加载字节 | lb rt, offset(rs) |
sb |
存储字节 | sb rt, offset(rs) |
(5)条件分支指令
指令 | 功能 | 格式 |
---|---|---|
beq |
等于则分支跳转 | beq rs, rt, label |
bne |
不等则分支跳转 | bne rs, rt, label |
bgtz |
大于零则跳转 | bgtz rs, label |
blez |
小于等于零则跳转 | blez rs, label |
(6)跳转指令
指令 | 功能 | 格式 |
---|---|---|
j |
无条件跳转 | j label |
jal |
跳转并链接 | jal label |
jr |
寄存器跳转 | jr rs |
3. 特殊指令
-
syscall
:用于系统调用,通常用于程序和操作系统之间的交互。 -
nop
:空操作指令,执行时不产生任何效果,通常用于指令延迟槽。
4. 常用寄存器
MIPS 有 32 个通用寄存器,每个寄存器在约定上有特定用途,这里借用别的师傅的图片
-
MIPS
固定4
字节指令长度 - 栈是从内存的高地址向低地址方向增长的
- 叶子函数:函数内部没有再调用其他函数
- 非叶子函数:函数内部调用其他函数的函数
- 流水线效应:在分析
MIPS
汇编代码时会发现,其跳转到函数或者分支跳转语句的下一条都是nop
(如下图),这是因为MIPS
采用了高度的流水线,其中最重要的是跳转指令导致的分支延迟效应。在分支跳转语句后面那条语句叫做分支延迟槽,当跳转语句刚执行的一瞬间,跳转到的地址刚填充好(填充到程序计数器),还没有执行程序计数器中存放的指令,分支延迟槽的指令已经被执行了,这就是流水线效应(几条指令被同时执行,只是处于不同的阶段,MIPS
不像其他架构那样存在流水线阻塞),为了避免出现问题,因此在分支跳转语句的下一条指令通常是nop
指令或者其他有用的指令。 - 缓存刷新机制:
MIPS CPUs
有两个独立的cache
:指令cache
和数据cache
。 指令和数据分别在两个不同的缓存中。当缓存满了,会触发flush
, 将数据写回到主内存。攻击者的攻击payload
通常会被应用当做数据来处理,存储在数据缓存中。当payload
触发漏洞, 劫持程序执行流程的时候,会去执行内存中的shellcode
.如果数据缓存没有触发flush
的话,shellcode
依然存储在缓存中,而没有写入主内存。这会导致程序执行了本该存储shellcode
的地址处随机的代码,导致不可预知的后果。(通常执行sleep(1)
刷新)
函数调用
函数调用时传参:如果函数的参数小于等于四个,那么会使用 $a0 ~ $a3
寄存器来存放参数。如果参数多于四个,那么多于的参数则存放到栈里(同时也会预留出前四个参数的内存空间,因为被调用者使用前四个参数时,会统一将参数放到保留的栈空间),具体情况是函数 A
调用函数 B
,调用者函数(函数A
)会在自己的栈顶预留一部分空间来保存被调用者(函数 B
)的参数,称之为调用参数空间
我们可以调试看看
gdb调试
#!/bin/bash
# Ensure the script exits on error
set -e
# Define variables for file name, GDB port, and architecture
target_file="test_mipsel_32"
gdb_port="1234"
# Check if the target file exists
if [ ! -f "$target_file" ]; then
echo "Error: Target file '$target_file' does not exist."
exit 1
fi
# Step 1: Start QEMU with GDB server enabled
echo "Starting QEMU with GDB server on port $gdb_port..."
qemu-mipsel -L /usr/mipsel-linux-gnu/ -g $gdb_port "$target_file" &
qemu_pid=$!
# Wait for QEMU to be ready
sleep 2
echo "QEMU started with PID $qemu_pid."
# Step 2: Start GDB and connect to QEMU GDB server
mips_gdb="gdb-multiarch"
# Check if GDB is installed
if ! command -v $mips_gdb &> /dev/null; then
echo "Error: GDB ($mips_gdb) not found. Please install it and try again."
exit 1
fi
echo "Starting GDB and connecting to QEMU..."
$mips_gdb -ex "target remote :$gdb_port" "$target_file"
# Cleanup after GDB session ends
echo "Stopping QEMU..."
kill $qemu_pid
# End of script
这是一个脚本,用于自动启动 QEMU 并使用 GDB 连接调试 MIPS 程序。将其保存为一个 .sh
文件,并运行就可以使用gdb调试了
-
-
如果需要调试其他 MIPS 可执行文件,只需修改以下行,将
target_file
改为需要调试的新文件名:
target_file="your_new_mips_file"
-
例如:
target_file="new_test_mipsel_32"
-
-
QEMU 库路径 (
-L
参数):-
如果新目标文件需要不同的库路径,例如可能有不同的工具链和根文件系统,可以修改 QEMU 的 -L参数路径:
qemu-mipsel -L /path/to/new/lib/ -g $gdb_port "$target_file" &
-
确保路径中包含与目标文件相对应的共享库和
ld.so.1
动态链接器。
-
添加和扩展调试功能
-
GDB 启动脚本 (
.gdbinit
):-
如果有一些常用的 GDB 命令,可以将这些命令添加到 GDB 启动脚本中,例如设置断点、打印特定变量、查看寄存器等。
-
修改 GDB 启动参数,添加更多 GDB 命令:
$mips_gdb -ex "target remote :$gdb_port" -ex "break main" "$target_file"
这会在 main函数上自动设置一个断点。
-
-
环境变量:
-
如果的目标程序依赖特定的环境变量,可以使用 QEMU 的 -E 选项设置环境变量:
qemu-mipsel -L /usr/mipsel-linux-gnu/ -g $gdb_port -E VAR_NAME=VALUE "$target_file" &
-
我们可以看到这样就可以调试了
1.函数调用机制
在 MIPS 架构中,函数调用是通过 jal
(Jump and Link)指令完成的。jal
指令会跳转到目标地址,同时将返回地址保存在 $ra
寄存器中。
在截图中,我们可以看到以下指令:
-
jalx 0x43fdc070
:这条指令调用了一个函数,并将当前地址存入$ra
(返回地址寄存器)中,以便函数执行结束后可以返回到原来的调用点。
2. 函数返回机制
函数的返回是通过 jr $ra
指令完成的,它会跳转到 $ra
寄存器中存储的地址,从而返回到调用函数的位置。
3. 栈指针和帧指针
在函数调用过程中,MIPS 使用堆栈指针 $sp
和帧指针 $fp
来管理函数的局部变量和保存调用环境。截图中的指令说明了函数进入时对堆栈的操作:
-
addiu $sp, $sp, -0x20
:这条指令将栈指针$sp
向下移动 32 个字节,以为当前函数调用创建新的栈帧。这是典型的函数进入操作,用于为局部变量和保存寄存器分配空间。 - 随后,我们看到一些 sw指令,例如:
-
sw $ra, 0x1c($sp)
:将返回地址$ra
存储到栈帧中,以便在函数结束时恢复。 -
sw $fp, 0x18($sp)
:将帧指针$fp
保存到栈中。
-
在这些指令之后,函数会更新帧指针:
-
move $fp, $sp
:将当前栈指针的值赋给帧指针$fp
,这样$fp
可以作为当前栈帧的基准。
4. 参数传递
在 MIPS 架构中,函数参数通常通过寄存器 $a0
到 $a3
传递。这些寄存器可以存储最多 4 个参数,如果参数超过 4 个,则剩余的参数会通过栈进行传递。
在截图中可以看到:
-
$a0
和$a1
等寄存器在函数调用过程中会被用来存储传递给被调用函数的参数。
就像这里的v0里面就存的是puts函数的第一个参数
5. 局部变量和保存寄存器
MIPS 使用 $t0
到 $t9
作为临时寄存器,这些寄存器的值在函数调用过程中不需要被保留。而 $s0
到 $s7
则是保存寄存器,在函数调用过程中需要保存并在返回时恢复。
在截图中可以看到多个 sw
和 lw
指令:
-
sw $s0, 0x10($sp)
:保存寄存器$s0
到栈中,以便在函数返回时恢复其值。 -
lw $s0, 0x10($sp)
:在函数返回前,将栈中保存的值重新加载到$s0
。
6. 内存访问和全局指针
在截图中的汇编代码,我们还看到涉及 $gp
(全局指针)的操作,例如加载全局变量或函数指针:
-
lw $t9, <offset>($gp)
:从全局指针$gp
基础地址加偏移量的位置加载一个值到$t9
,通常用于访问全局变量或调用全局函数。
7. 函数调用中的延迟槽
MIPS 架构使用延迟槽(delay slot)机制,在跳转或调用指令之后的下一条指令会在跳转前执行。这是为了充分利用流水线并减少处理器的停顿。在截图中,并未直接看到延迟槽的指令,但在 MIPS 汇编中,需要注意这种指令紧跟 jal
或 j
的情况。
总结
-
函数调用和返回:通过
jal
和jr $ra
完成,返回地址存储在$ra
。 -
栈帧管理:使用
$sp
(栈指针)和$fp
(帧指针)管理函数的局部变量和返回地址。 -
参数传递:通过
$a0
到$a3
传递参数,超过 4 个参数时通过栈传递。 -
保存寄存器和局部变量:使用
sw
和lw
将保存寄存器(如$s0
)存入栈中,在函数结束前恢复。 -
全局指针:使用
$gp
访问全局变量或调用全局函数。 - 延迟槽:MIPS 指令有延迟槽机制,需要注意跳转指令后的执行顺序。