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是开源新贵,正在快速崛起。

ARM / MIPS / RISC-V 核心差异对比图 ARM (32/64-bit) R0-R15 通用寄存器 PC = R15, LR = R14, SP = R13 ATPCS: R0-R3 传参, R0 返回值 Thumb/Thumb-2 变长指令 MIPS (32/64-bit) R0-R31 通用寄存器 R0 恒为 0, R31 = RA O32: A0-A3 传参, V0 返回值 固定 4 字节指令长度 RISC-V (32/64-bit) x0-x31 通用寄存器 x0 恒为 0, x1 = RA A0-A7 传参, A0 返回值 压缩指令集 (RVC) 关键差异速查表 ARM MIPS RISC-V 指令长度 条件执行 延迟槽 栈布局 Pwn难度 4/2 字节混合 支持 (条件码) 无 满递减 中等 固定 4 字节 不支持 有 (分支延迟槽) 满递减 较难 4/2 字节混合 不支持 无 满递减 中等

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。