ARM-PWN
ARMPWN 环境搭建与题目学习过程:
使用 qemu-user
1 | sudo apt update |
安装 32 位和 64 位 ARM 的基础运行库
1 | # 1. 安装 QEMU 用户模式模拟器 |
直接运行即可
1 | # 运行 ARM32 |

gdb 调试
1 | qemu-arm -g 1234 -L /usr/arm-linux-gnueabi ./arm32_pwn # 32位 |
打开一个新终端:
1 | gdb-multiarch ./文件名 |
- 连接:pwndbg> target remote :1234,之后就可以进行调试了:


python 脚本
1 | from pwn import * |

架构特定设置
- 32位专用:
pwndbg> set architecture arm - 64位专用:
pwndbg> set architecture aarch64
ARM 架构基础知识
基础的 ARM 架构环境和做题流程都基本了解了,需要补充的就一点一点学习,下面来了解 ARM 架构的基础知识
与x86/x64架构大量依赖栈来传递参数不同,ARM架构高度依赖寄存器。以下是ARM 32位(AArch32)和64位(AArch64)架构中常用寄存器的详细解析:
一、 ARM 32位 (AArch32 / ARMv7) 寄存器
在32位模式下,ARM拥有16个可见的通用寄存器(R0 - R15)以及一个状态寄存器。所有的寄存器都是32位宽。
| 寄存器 | 别名 | 在函数调用与Pwn中的核心作用 |
|---|---|---|
| R0 - R3 | A1 - A4 | 参数传递与返回值。函数的前4个参数通过这四个寄存器传递,多余的参数压入栈中。R0 通常也用于存放函数执行后的返回值。 |
| R4 - R10 | V1- V7 | 局部变量寄存器。被调用的函数如果要使用这些寄存器,必须先保存它们的值,并在返回前恢复。 |
| R11 | FP | 栈帧指针 Frame Pointer。类似于x86的ebp ,用于定位当前函数的局部变量和参数。 |
| R12 | IP | 内部过程调用寄存器 Intra-Procedure-call通常在动态链接和位置无关代码(PIC)中用作临时计算,链接器插入的veneer代码(如PLT/GOT跳转)经常使用它。 |
| R13 | SP | 栈指针 Stack Pointer。类似于x86的esp ,指向当前栈顶。 |
| R14 | LR | 链接寄存器 (Link Register)。 当使用BL指令进行函数调用时,硬件会自动将返回地址存入LR寄存器。函数返回时,通常会执行 MOV PC, LR或从栈中弹出值到PC |
| R15 | PC | 程序计数器 (Program Counter)。类似于x86的eip。在32位ARM中,PC可以作为通用寄存器直接读写,这意味着你可以直接通过指令(如 MOV PC, R0)来劫持控制流。 |
| CPSR | - | 当前程序状态寄存器。保存条件标志位(如N, Z, C, V)和控制位(如当前处理器模式、中断屏蔽位以及最重要的T位:决定当前是ARM指令集还是Thumb指令集)。 |
二、 ARM 64位 (AArch64 / ARMv8) 寄存器
升级到64位后,寄存器数量大幅增加到31个通用寄存器。每个寄存器有64位宽(用 X0-X30 表示)和32位宽的访问方式(用 W0-W30 表示,访问时只会操作低32位,高32位清零)。
| 寄存器 | 在函数调用与Pwn中的核心作用 |
|---|---|
| X0 - X7 | 参数传递与返回值。支持多达8个参数通过寄存器传递。X0用于存放返回值。这使得64位ARM函数在不触碰栈的情况下能做更多事情。 |
| X8 | 间接结果位置寄存器。当函数返回一个非常大的结构体时,X8 用来保存该结构体在内存中的目标地址。 |
| X9 - X15 | 临时寄存器。调用者负责保存,被调用函数可以随意覆盖。 |
| X16, X17 | IP0, IP1。类似于32位的R12,主要用于链接器的跳板代码,PLT表解析等 |
| X18 | 平台保留寄存器(有时用作TLS指针)。通常在用户态应用中避免使用。 |
| X19 - X28 | 局部变量寄存器。被调用的函数必须在修改前将其压栈并在返回前恢复。在构造复杂的ROP链时,如ret2csu,这些寄存器经常被用来控制执行流 |
| X29 | FP (Frame Pointer)。指向当前栈帧的底部。 |
| X30 | LR (Link Register)。保存函数调用的返回地址。执行RET指令时,CPU会将X30的值赋给PC |
| SP | 栈指针。在64位中,SP拥有专门的硬件寄存器,不再与通用寄存器(如X31)混用。 |
| PC | 程序计数器。重大改变: 在AArch64中,PC不再是通用寄存器,不能直接对其进行算术运算或赋值。你必须使用跳转指令如 RET``BR``BLR来修改它 |
| PSTATE | 替代了32位的CPSR,包含状态标志位等信息。 |
x86 对应 ARM 的指令集:
对于习惯了 x86 汇编的安全研究者来说,初看 ARM 汇编可能会觉得有些啰嗦,尤其是 AArch64 完全移除了专门的 push 和 pop 指令。
以下是这几个经典 x86 指令在 ARM32 和 ARM64 中的等效实现及底层逻辑:
1. push (压栈)
x86 的 push 会自动递减栈指针并存入数据。ARM 通过多寄存器存取或带有回写机制的寻址来实现。
- ARM32:
- 常规写法:
PUSH {R0, R1, R2}(可以一次性压入多个寄存器)。 - 底层等效:
STMDB SP!, {R0, R1, R2}(Store Multiple Decrement Before)。意思是先把 SP 减小,然后再把寄存器的值存进去,最后的!表示将计算后的新地址写回 SP。
- 常规写法:
- ARM64: (没有 PUSH 指令)
- ARM64 强制要求栈指针在访问内存时必须保持16字节对齐。因此,通常使用
STP(Store Pair) 指令一次性压栈两个 64 位寄存器。 - 写法:
STP X0, X1, [SP, #-16]!。含义:将 SP 减 16,然后把 X0 和 X1 存入该地址,最后的!表示将新地址写回 SP。如果只想压入一个寄存器,可以使用STR X0, [SP, #-16]!。
- ARM64 强制要求栈指针在访问内存时必须保持16字节对齐。因此,通常使用
2. pop (出栈)
与 push 对应,操作方向相反。
- ARM32:
- 常规写法:
POP {R0, R1, R2}。 - 底层等效:
LDMIA SP!, {R0, R1, R2}(Load Multiple Increment After)。先从 SP 指向的地址读取数据到寄存器,然后将 SP 增加,!负责更新 SP。
- 常规写法:
- ARM64: (没有 POP 指令)
- 使用 LDP (Load Pair) 配合后索引寻址。
- 写法:
LDP X0, X1, [SP], #16。含义:先从 SP 当前指向的位置读取数据到 X0 和 X1,然后再把 SP 加上 16。如果是单个寄存器:LDR X0, [SP], #16。
3. cmp (比较)
原理与 x86 非常类似,都是执行减法但不保存结果,只更新状态寄存器CPSR / PSTATE中的标志位(N, Z, C, V)
- ARM32:
CMP R0, R1:计算 R0 - R1,更新 CPSR。 - ARM64:
CMP X0, X1。在底层,这其实是SUBS XZR, X0, X1的别名,将减法结果丢弃到零寄存器 XZR,并设置状态标志S。
4. jmp (无条件跳转)
ARM 中称为分支 (Branch)。
- ARM32:
B <label>:相对跳转(通常用于函数内部的短跳)。BX Rm(Branch and Exchange):跳转到寄存器 Rm 中的地址。非常重要:如果 Rm 的最低位(LSB)为 1,处理器会切换到 Thumb 模式;如果为 0,则保持 ARM 模式。
- ARM64:
B <label>:相对跳转。BR Xm(Branch to Register):绝对跳转到 Xm 中的地址。AArch64 取消了 Thumb 状态,所以不再有BX指令的状态切换概念。
5. call (函数调用)
x86 的 call 会自动将下一条指令的地址(返回地址)压入栈中,然后跳转。ARM 则是将返回地址保存到链接寄存器(LR)中。
- ARM32:
BL <label>(Branch with Link):将下一条指令的地址放入 R14 (LR),然后跳转到 label。BLX Rm:将返回地址放入 LR,然后跳转到 Rm,并根据 Rm 的最低位决定是否切换 Thumb 状态。
- ARM64:
BL <label>:将返回地址存入 X30 (LR),跳转。BLR Xm(Branch with Link to Register):将返回地址存入 X30,跳转到寄存器 Xm 的地址。
6. leave (销毁栈帧)
x86 的 leave 等效于 mov esp, ebp; pop ebp。ARM 的函数尾声通常直接通过恢复寄存器来实现。
- ARM32: 通常写为
MOV SP, FP然后配合POP {FP}(或者直接恢复更多局部变量)。 - ARM64: 通常没有专用的宏指令,而是通过算术运算恢复栈空间,然后恢复 FP 和 LR:
1 | MOV SP, X29 // 恢复栈顶指针 |
7. ret (函数返回)
x86 的 ret 会从栈顶弹出一个地址并赋给 eip/rip。ARM 则是将之前保存在 LR 中的地址赋值给 PC。
- ARM32:
- 可以直接把 LR 挪给 PC:
MOV PC, LR或者BX LR。 - Pwn中最常见的形式:如果函数开头把 LR 压栈了
PUSH {..., LR},结尾通常会直接弹出到 PC:POP {..., PC}。这条指令是 ARM32 ROP 链的灵魂
- 可以直接把 LR 挪给 PC:
- ARM64:
- 使用专门的 RET 指令。默认情况下 RET 会将 X30 (LR) 的值赋给 PC 从而实现返回。如果你想返回到其他寄存器指定的地址,可以写
RET Xm。
- 使用专门的 RET 指令。默认情况下 RET 会将 X30 (LR) 的值赋给 PC 从而实现返回。如果你想返回到其他寄存器指定的地址,可以写
基础知识的学习就先到这里吧 ^_^
更新: 2026-05-07 19:56:53
原文: https://www.yuque.com/idcm/wnemg9/ncbly2ipgy0qrz8g