1. 异架构Pwn基础:ARM/MIPS/RISC-V架构差异、交叉编译环境搭建、QEMU用户态模拟调试
做Pwn做了几年,x86_64下的漏洞利用已经轻车熟路了。但第一次拿到一个ARM架构的IoT固件时,我整个人是懵的——寄存器不一样、调用约定不一样、连shellcode都得重新写。说白了,异架构Pwn就是让你跳出舒适区,用另一种思维方式去理解二进制漏洞。
这一章,我们先把地基打牢。ARM、MIPS、RISC-V这三兄弟,你得知道它们各自什么脾气,才能在后续的沙箱绕过中游刃有余。
1.1 三大架构的核心差异
先看一张总览图,把三者的关键差异刻在脑子里。
一句话总结:ARM统治移动端和IoT,MIPS活在路由器和老嵌入式里,RISC-V是开源新贵,正在快速崛起。
1.2 寄存器与调用约定的坑
做异架构Pwn,第一关就是搞清楚参数怎么传、返回值放哪。我见过太多人把ARM的R0当成x86的rax来用,结果调试半天找不到bug在哪。
ARM (32位)
- 寄存器:R0-R15,其中R13(SP)、R14(LR)、R15(PC)有特殊用途
- 传参:R0-R3放前4个参数,多余的压栈
- 返回值:R0
- 特殊:支持条件执行——每条指令都可以带条件码,比如
moveq r0, #1只在相等时才执行
我的经验:ARM的Thumb模式是个双刃剑。指令长度减半能省空间,但ROP gadget的地址对齐会变得很恶心。我曾在Thumb模式下找一条pop {pc}找了两个小时——因为它的机器码是BD 00而不是E8BD 8000。
MIPS (32位)
- 寄存器:R0-R31,R0恒为0,R31是返回地址(RA)
- 传参:A0-A3放前4个参数,多余的压栈
- 返回值:V0
- 特殊:有延迟槽——分支指令后面的那条指令一定会被执行
避坑指南:MIPS的延迟槽是新手杀手。我曾经写过一个MIPS的ROP链,跳转地址算得完美,但延迟槽里放了一条nop,结果目标函数根本没执行——因为延迟槽里的指令覆盖了关键寄存器。记住:延迟槽不是摆设,它真的会执行。
RISC-V (64位)
- 寄存器:x0-x31,x0恒为0,x1(RA)存返回地址
- 传参:A0-A7放前8个参数,多余的压栈
- 返回值:A0
- 特殊:模块化设计,支持压缩指令集(RVC),指令长度可以是2字节或4字节
RISC-V是三者中最清爽的。没有条件执行、没有延迟槽、传参寄存器多达8个。但清爽不代表简单——它的ROP gadget往往更少,因为指令编码太规整了。
1.3 交叉编译环境搭建
环境搭建是异架构Pwn的第一步,也是最容易劝退的一步。我建议直接用Docker,省去所有依赖地狱。
方案一:Docker一键部署(推荐)
# 拉取我常用的异架构Pwn镜像
docker pull pwndocker/pwndocker:latest
# 或者自己搭,更灵活
docker run -it --rm \
-v $(pwd):/workspace \
ubuntu:22.04 bash
# 安装交叉编译工具链
apt update
apt install -y \
gcc-arm-linux-gnueabihf \
g++-arm-linux-gnueabihf \
gcc-mipsel-linux-gnu \
g++-mipsel-linux-gnu \
gcc-riscv64-linux-gnu \
g++-riscv64-linux-gnu \
qemu-user \
qemu-user-static \
gdb-multiarch
方案二:手动安装(适合有洁癖的)
| 架构 | 工具链包名 | QEMU模拟器 | GDB调试器 |
|---|---|---|---|
| ARM (32位) | gcc-arm-linux-gnueabihf |
qemu-arm |
gdb-multiarch |
| ARM (64位) | gcc-aarch64-linux-gnu |
qemu-aarch64 |
gdb-multiarch |
| MIPS (小端) | gcc-mipsel-linux-gnu |
qemu-mipsel |
gdb-multiarch |
| RISC-V (64位) | gcc-riscv64-linux-gnu |
qemu-riscv64 |
gdb-multiarch |
验证安装:跑一个简单的Hello World,确认工具链和QEMU都能正常工作。
# 写一个测试程序
cat > test.c << 'EOF'
#include <stdio.h>
int main() {
printf("Hello from ARM!\n");
return 0;
}
EOF
# 交叉编译
arm-linux-gnueabihf-gcc -o test_arm test.c
# QEMU用户态模拟运行
qemu-arm -L /usr/arm-linux-gnueabihf/ ./test_arm
# 输出:Hello from ARM!
1.4 QEMU用户态模拟与调试
QEMU用户态模式是我们做异架构Pwn的利器。它不需要完整模拟整个系统,只模拟CPU和系统调用,启动快、调试方便。
核心命令速查
| 操作 | 命令 | 说明 |
|---|---|---|
| 直接运行 | qemu-arm ./binary |
不加参数直接跑 |
| 指定库路径 | qemu-arm -L /path/to/sysroot ./binary |
解决动态链接问题 |
| 开启GDB调试 | qemu-arm -g 1234 ./binary |
监听1234端口等待GDB连接 |
| 单步调试 | gdb-multiarch -q ./binary(gdb) target remote :1234 |
连接QEMU的GDB stub |
| 打印寄存器 | (gdb) info registers |
查看所有寄存器状态 |
| 反汇编 | (gdb) disassemble $pc,+32 |
从当前PC反汇编32字节 |
调试一个ARM程序
# 终端1:启动QEMU并等待调试
qemu-arm -g 1234 -L /usr/arm-linux-gnueabihf/ ./test_arm
# 终端2:启动GDB并连接
gdb-multiarch -q ./test_arm
(gdb) target remote :1234
(gdb) b main
(gdb) c
(gdb) info registers r0 r1 r2 r3
r0 0x1 1
r1 0xbefffae4 3196054244
r2 0xbefffafc 3196054268
r3 0x0 0
(gdb) disassemble
Dump of assembler code for function main:
0x000104c0 <+0>: push {r11, lr}
0x000104c4 <+4>: add r11, sp, #4
0x000104c8 <+8>: ldr r0, [pc, #12]
0x000104cc <+12>: bl 0x10360 <puts>
0x000104d0 <+16>: mov r0, #0
0x000104d4 <+20>: pop {r11, pc}
我的调试习惯:我会在GDB里提前配好~/.gdbinit,加上set architecture arm和set endian little,省得每次手动敲。另外,pwndbg对ARM的支持还不错,但MIPS下经常崩——这时候我一般切回纯GDB。
1.5 第一个异架构Pwn题:ARM栈溢出
光说不练假把式。我们写一个简单的ARM栈溢出程序,感受一下异架构漏洞利用的流程。
// vuln_arm.c
#include <stdio.h>
#include <string.h>
void vuln() {
char buf[64];
printf("Enter your input: ");
gets(buf); // 经典栈溢出
printf("You entered: %s\n", buf);
}
int main() {
vuln();
return 0;
}
# 编译(关闭栈保护)
arm-linux-gnueabihf-gcc -o vuln_arm vuln_arm.c -fno-stack-protector -no-pie
# 检查保护
checksec --file=vuln_arm
# 输出:No PIE, No Canary, NX enabled
嗯,这里要注意:ARM的栈布局和x86不一样。函数入口会push {r11, lr},所以返回地址在buf + 64 + 8的位置(64字节缓冲区 + 8字节保存的r11和lr)。
# 用Python生成payload
python3 -c "
import sys
# 64字节填充 + 8字节覆盖r11 + 4字节覆盖返回地址
payload = b'A' * 64
payload += b'B' * 8 # 覆盖r11和lr
payload += b'C' * 4 # 覆盖返回地址
sys.stdout.buffer.write(payload)
" > payload.txt
# 测试
qemu-arm -L /usr/arm-linux-gnueabihf/ ./vuln_arm < payload.txt
# 预期:Segmentation fault (core dumped)
避坑指南:ARM的栈是满递减的,SP指向最后一个压入的数据。这和x86一样,但MIPS也是满递减——别搞混了。另外,ARM的返回地址存在LR里,而不是像x86那样在栈上。所以ROP时你要找pop {pc}或者bx lr这样的gadget。
1.6 本章小结
这一章我们干了三件事:搞清楚了ARM/MIPS/RISC-V的寄存器差异和调用约定、搭好了交叉编译和QEMU环境、亲手调试了一个ARM程序。这些东西看着琐碎,但它们是后续所有沙箱绕过技术的基础。
我个人建议:别急着往下看,先把环境搭好,把上面的例子跑通。调试器里多看看寄存器变化,多试试不同的payload。等你闭着眼睛都能说出ARM的传参规则时,我们再聊怎么绕过seccomp。