2、ARM架构基础:处理器模式、寄存器组、指令集概述、Thumb与Thumb-2模式

ARM架构,说实话,是异架构Pwn里绕不开的一座山。x86那套你玩得再溜,碰到ARM也得重新学起。我当年第一次接触ARM逆向时,看着那堆寄存器编号和奇怪的指令格式,头都是大的。但摸清楚之后你会发现,ARM的设计哲学其实很清晰——精简、规整、省电。

这一章我们只聊最核心的东西:处理器模式、寄存器组、指令集概览,以及Thumb/Thumb-2这两个你写shellcode时一定会碰到的模式。不扯虚的,全是实战用得上的。

2.1 ARM处理器模式

ARM处理器有好几种运行模式,但对我们写shellcode和做Pwn来说,真正需要关心的其实就两个:User模式和SVC模式(也叫Supervisor模式)。

模式 权限等级 典型用途 Pwn场景
User (USR) PL0(最低) 普通应用程序运行 你拿到的shellcode默认跑在这里
Supervisor (SVC) PL1(内核级) 操作系统内核、异常处理 通过SVC指令陷入内核提权
System (SYS) PL1 运行特权任务,但使用User模式的寄存器组 偶尔在ROP链中见到
其他(FIQ、IRQ、Abort、Undef) PL1 中断和异常处理 shellcode一般不直接碰,但异常向量表是攻击面
我的习惯:写ARM shellcode时,我默认自己处于User模式。所有需要特权操作的事情——比如读写系统寄存器、关闭MMU——都得通过SVC指令(软中断)让内核代劳。你想想看,如果你在User模式直接去写CPSR(当前程序状态寄存器),CPU会直接给你一个Undefined Instruction异常。

2.2 寄存器组

ARM的寄存器比x86多得多,但别慌。我们只记那些写shellcode时真正会用到的。

ARM有16个通用寄存器(R0-R15),外加一个CPSR。其中R15就是PC(程序计数器),R14是LR(链接寄存器),R13是SP(栈指针)。

寄存器 别名 我的用法 Pwn中的关键点
R0-R3 参数寄存器 传参、返回值 函数调用时参数依次放入R0-R3,多余的压栈
R4-R11 普通寄存器 保存局部变量 函数调用需保存/恢复,ROP里常用
R12 IP(Intra-Procedure-call) 临时寄存器 链接器可能用它做跳板
R13 SP 栈指针 栈溢出攻击的核心目标
R14 LR 返回地址 函数返回时跳回LR指向的地址——ROP链的入口
R15 PC 程序计数器 控制PC就等于控制执行流
CPSR 状态寄存器 标志位、模式位 Thumb模式切换靠它(CPSR的T位)
我曾经踩过的坑:在ARM里,PC(R15)的值在ARM模式下是当前指令地址+8,在Thumb模式下是当前指令地址+4。这个“预取指偏移”让很多新手在计算跳转地址时算错。我第一次写ARM shellcode时,因为这个偏移量debug了整整一个下午。

2.3 指令集概述

ARM指令集是固定长度的——ARM模式下每条指令4字节,Thumb模式下每条指令2字节(Thumb-2引入了混合长度,后面说)。这个特性对shellcode编写影响很大:你没法像x86那样用变长指令做混淆,但反过来,指令对齐更简单,不容易出现奇怪的解码错误。

我挑几个写shellcode时最常用的指令说一下:

  • MOV:数据传送。注意ARM的立即数是有格式限制的(8位数值+4位循环移位),不是所有立即数都能直接写。
  • LDR/STR:内存加载/存储。ARM是加载-存储架构,所有运算都在寄存器里做,不能直接操作内存。
  • B/BL/BX:分支指令。B是跳转,BL是跳转并保存返回地址到LR,BX是跳转并切换指令集(根据目标地址的最低位判断是ARM还是Thumb)。
  • SVC:软中断。这是shellcode调用系统调用的唯一途径。R7放系统调用号,R0-R6放参数。
  • PUSH/POP:压栈出栈。ARM的PUSH/POP可以一次操作多个寄存器,非常方便。
; ARM模式下的shellcode片段:调用exit(0)
MOV R0, #0          ; 参数1:exit code = 0
MOV R7, #1          ; 系统调用号:exit (ARM Linux)
SVC 0               ; 触发软中断

关键区别:ARM Linux的系统调用号放在R7里,而x86 Linux放在EAX里。这个区别我刚开始总是记混,后来写了个小纸条贴在显示器上才改过来。

2.4 Thumb与Thumb-2模式

Thumb模式是ARM的一个“压缩”指令集。每条指令只有16位(2字节),代码密度更高,但功能也受限——比如不能直接访问所有寄存器,条件执行也基本被砍掉了。

Thumb-2是Thumb的增强版,引入了32位指令,让Thumb模式也能做ARM模式的大部分事情。现在的ARMv7以上芯片,基本都支持Thumb-2。

特性 ARM模式 Thumb模式 Thumb-2模式
指令长度 固定4字节 固定2字节 混合(2或4字节)
代码密度 低 高(约节省30%空间) 高
寄存器访问 全部16个 仅R0-R7(低寄存器) 全部16个
条件执行 几乎所有指令 仅分支指令 部分指令
shellcode适用性 功能完整,但体积大 体积小,但写起来受限 推荐:兼顾体积和功能
我的建议:写shellcode时优先用Thumb-2模式。为什么?因为很多漏洞利用场景下,你的输入长度是受限的(比如栈溢出只有几十字节的溢出空间)。Thumb-2的代码密度能让你在有限空间里塞进更多功能。而且现在主流ARM设备(手机、路由器)都支持Thumb-2,兼容性不是问题。

切换Thumb模式的方法很简单:通过BX指令跳转到一个奇数地址。ARM的PC最低位(LSB)被用来指示指令集:0表示ARM模式,1表示Thumb模式。

; 从ARM模式切换到Thumb模式
ADR R0, thumb_code + 1  ; 目标地址+1,表示Thumb模式
BX R0

; 下面这段代码在Thumb模式下执行
.thumb
thumb_code:
    MOV R0, #0
    MOV R7, #1
    SVC 0
我曾经犯过的错:在Thumb模式下写SVC指令时,系统调用号还是放在R7里,但SVC指令的立即数(那个0)在Thumb模式下只有8位,而在ARM模式下是24位。这个细节导致我写的shellcode在ARM设备上跑得好好的,换到Thumb设备上就崩了。后来查ARM手册才发现,Thumb模式的SVC指令格式和ARM模式不一样。

2.5 知识结构总览

下面这张图把本章的核心知识点串起来了。你可以把它当作一个快速索引,以后写shellcode时拿不准了,回来扫一眼就行。

ARM架构基础(Pwn视角) 处理器模式 User (PL0) SVC (PL1) 寄存器组 R0-R3 (参数) R13 SP R14 LR R15 PC 指令集 MOV/LDR B/BL/BX Thumb / Thumb-2 模式 Thumb:2字节指令 Thumb-2:混合长度 切换:BX奇数地址 Pwn实战要点 • shellcode默认跑在User模式,提权需SVC指令 • 控制PC(R15)等于控制执行流——ROP链的核心 • Thumb-2是shellcode首选:体积小、功能全 • 切换模式用BX指令,目标地址最低位=1表示Thumb

嗯,这一章的内容就这些。ARM架构的东西很多,但对我们写shellcode来说,上面这些已经够用了。处理器模式决定了你能做什么,寄存器组决定了你用什么干活,指令集决定了你怎么干活,Thumb/Thumb-2决定了你的shellcode能塞多小。