逆向工程:ARM64汇编基础与分析

随着 Apple Silicon 和 ARM 服务器的普及,ARM64 逆向分析变得越来越重要。本文整理 ARM64(AArch64)汇编的核心知识,以及在 IDA 中分析 ARM64 程序的要点。

一、寄存器

ARM64 有 31 个通用寄存器,加上几个特殊寄存器:

通用寄存器

  • X0 - X30:64 位通用寄存器
  • W0 - W30:对应的低 32 位(写入 W 寄存器会自动零扩展到 X 寄存器)
  • X0 - X7:函数参数和返回值
  • X8:间接返回值寄存器(结构体返回)
  • X9 - X15:临时寄存器(caller-saved)
  • X16 - X17:intra-procedure-call 临时寄存器(linker 使用)
  • X18:平台保留(iOS 上用于 TLS)
  • X19 - X28:callee-saved 寄存器
  • X29(FP):帧指针
  • X30(LR):链接寄存器,存储返回地址

特殊寄存器

  • SP:栈指针(不在 X0-X30 中,但可以被某些指令引用)
  • PC:程序计数器(不可直接读写,通过分支指令间接修改)
  • XZR/WZR:零寄存器,读取永远返回 0,写入被丢弃

条件标志(NZCV,在 PSTATE 寄存器中)

  • N(Negative):结果为负
  • Z(Zero):结果为零
  • C(Carry):产生进位/借位
  • V(oVerflow):有符号溢出

二、常用指令

数据传输

指令 说明 示例
MOV 寄存器间传值 MOV X0, X1
MOVZ 立即数移入(零扩展) MOVZ X0, #0x1234
MOVK 立即数移入(保留其他位) MOVK X0, #0x5678, LSL #16
LDR 从内存加载 LDR X0, [X1] / LDR X0, [X1, #8]
STR 写入内存 STR X0, [SP, #-16]!
LDP 加载一对寄存器 LDP X29, X30, [SP], #16
STP 存储一对寄存器 STP X29, X30, [SP, #-16]!

LDR/STR 的寻址模式是 ARM64 的一个重点:

  • [X1]:基址寻址
  • [X1, #8]:基址 + 偏移
  • [X1, #8]!:前索引(先加偏移再访问,X1 更新)
  • [X1], #8:后索引(先访问再加偏移,X1 更新)
  • [X1, X2]:寄存器偏移
  • [X1, X2, LSL #3]:带移位的寄存器偏移

IDA 中看到的函数入口通常是:

STP X29, X30, [SP, #-0x20]!   ; 保存 FP 和 LR,栈减 0x20
MOV X29, SP                     ; 设置新的帧指针

函数出口:

LDP X29, X30, [SP], #0x20     ; 恢复 FP 和 LR,栈加 0x20
RET                             ; 返回(跳转到 LR)

算术与逻辑

指令 说明 示例
ADD 加法 ADD X0, X1, X2 / ADD X0, X0, #1
SUB 减法 SUB SP, SP, #0x40
MUL 乘法 MUL X0, X1, X2
SDIV 有符号除法 SDIV X0, X1, X2
AND 按位与 AND X0, X1, #0xFF
ORR 按位或 ORR X0, X1, X2
EOR 按位异或 EOR X0, X0, X0 (清零)
LSL 逻辑左移 LSL X0, X1, #3
LSR 逻辑右移 LSR X0, X1, #4
ASR 算术右移 ASR X0, X1, #1 (有符号除2)

ADDSSUBS 是带标志位更新的版本。CMP 实际上是 SUBS 的别名(结果被丢弃),TSTANDS 的别名。

分支

指令 说明
B target 无条件跳转
BL target 跳转并链接(函数调用,返回地址存入 LR)
BR Xn 寄存器间接跳转
BLR Xn 寄存器间接调用
RET 返回(等价于 BR X30)
B.cond target 条件跳转(EQ/NE/GT/LT/GE/LE/CS/CC 等)
CBZ Xn, target 比较并跳转:Xn == 0 则跳转
CBNZ Xn, target 比较并跳转:Xn != 0 则跳转
TBZ Xn, #bit, target 测试位并跳转:第 bit 位为 0 则跳转
TBNZ Xn, #bit, target 测试位并跳转:第 bit 位为 1 则跳转

CBZ/CBNZ 非常常见,它把比较和跳转合并为一条指令。在 IDA 中经常看到:

CBZ X0, loc_dead     ; if (ptr == NULL) goto error

TBZ/TBNZ 用于测试特定位,常见于标志位检查:

TBZ W8, #0, loc_skip   ; if ((flags & 1) == 0) goto skip

三、函数调用约定(AAPCS64)

ARM64 的标准调用约定是 AAPCS64(ARM Architecture Procedure Call Standard for 64-bit):

参数传递

  • 前 8 个整型/指针参数:X0 - X7
  • 前 8 个浮点参数:D0 - D7(SIMD/FP 寄存器)
  • 多出的参数通过栈传递

返回值

  • 整型/指针返回值:X0(128 位用 X0 + X1
  • 浮点返回值:D0
  • 大结构体:通过 X8 指向的内存返回

寄存器保存规则

  • Caller-saved(调用者保存):X0-X15X16-X17——被调用函数可以随意修改
  • Callee-saved(被调用者保存):X19-X28X29(FP),X30(LR)——如果函数使用了这些寄存器,必须在返回前恢复

IDA 中识别函数参数的关键就是追踪 X0-X7 的赋值:

MOV     X0, #0x10          ; size = 16
BL      _malloc             ; ptr = malloc(16)
MOV     X19, X0             ; 保存返回值到 callee-saved 寄存器
MOV     X0, X19             ; arg0 = ptr
MOV     W1, #0              ; arg1 = 0
MOV     X2, #0x10           ; arg2 = 16
BL      _memset             ; memset(ptr, 0, 16)

四、在 IDA 中分析 ARM64

识别函数序言/尾声

IDA 通常能自动识别 ARM64 函数边界,但有时需要手动干预。标准序言模式:

; 典型的函数序言
STP     X29, X30, [SP, #var_s0]!
MOV     X29, SP
SUB     SP, SP, #local_size    ; 分配局部变量空间
STP     X19, X20, [SP, #...]   ; 保存需要的 callee-saved 寄存器

如果看到 PACIASP / AUTIASP 指令,说明启用了指针认证(PAC),这是 ARM64e(iOS/macOS)的安全特性:

PACIASP                         ; 对 LR 做签名
STP     X29, X30, [SP, #-0x10]!
MOV     X29, SP
; ... 函数体 ...
LDP     X29, X30, [SP], #0x10
AUTIASP                         ; 验证 LR 签名
RET

switch-case 的识别

ARM64 的 switch 通常编译为跳转表。IDA 中的典型模式:

CMP     W8, #5              ; 检查 case 数量
B.HI    default_case         ; 超出范围走 default
ADRP    X9, #jump_table@PAGE
ADD     X9, X9, #jump_table@PAGEOFF
LDRSW   X10, [X9, X8, LSL #2]  ; 从跳转表加载偏移
ADD     X9, X9, X10
BR      X9                   ; 间接跳转

如果 IDA 没有自动识别跳转表,可以在跳转表地址处手动指定为 dword 数组,然后在 BR 指令上右键设置跳转目标。

Objective-C 消息发送

在分析 iOS/macOS 程序时,Objective-C 的方法调用通过 objc_msgSend 进行:

ADRP    X8, #selRef_init@PAGE
LDR     X1, [X8, #selRef_init@PAGEOFF]   ; SEL = @selector(init)
MOV     X0, X19                            ; self
BL      _objc_msgSend                      ; [self init]

X0 是 self(对象),X1 是 selector(方法名),X2 起是方法参数。IDA 的 Objective-C 分析插件通常能自动标注这些调用。

ADRP + ADD 模式

这是 ARM64 中加载全局地址最常见的模式。由于 ARM64 指令固定 4 字节,不能直接编码 64 位地址,所以用两条指令组合:

ADRP    X8, #aHelloWorld@PAGE         ; 加载页基址(4KB 对齐)
ADD     X8, X8, #aHelloWorld@PAGEOFF  ; 加上页内偏移
; X8 现在指向 "Hello World" 字符串

IDA 通常会自动将这对指令合并显示为对符号的引用。

总结

ARM64 汇编相比 x86-64 更规整——固定长度指令、清晰的寄存器分配约定、简洁的寻址模式。在 IDA 中分析时,重点关注函数调用约定(参数在 X0-X7 中传递)、STP/LDP 的序言/尾声模式、ADRP+ADD 的地址加载,以及 CBZ/CBNZ/TBZ/TBNZ 这些带比较的条件跳转。掌握这些模式后,阅读 ARM64 反汇编的效率会大幅提升。