【iOS】iOS逆向系列九 - ARM

当我们使用Xcode断点调试APP时,手动开启汇编模式下是会进入汇编模式的(异常情况下也会自动跳到汇编代码错误位置),如果了解汇编会提高我们调试APP的效率。

以iPhone5s(含)为分界点,之前是armv7、armv7s,之后就是arm64,会向下兼容。iOS汇编分为真机和模拟器,真机采用的是arm64架构(GNU汇编),模拟器采用的是x86架构(AT&T汇编),由于汇编是依赖机器的,机器不一样(不同架构)汇编指令可能不一样,因此我们主要研究真机的汇编代码。

一、ARM

ARM(Advanced RISC Machines),既可以认为是一个公司的名字,也可以认为是对一类微处理器的通称,还可以认为是一种技术的名字。

精简指令集计算机(英语:reduced instruction set computer,缩写:RISC)或简译为精简指令集,是计算机中央处理器的一种设计模式。

采用 RISC 架构的 ARM 微处理器一般具有如下特点:

  1. 体积小、低功耗、低成本、高性能;
  2. 支持 Thumb(16 位)/ARM(32 位)双指令集,能很好的兼容 8 位/16 位器件;
  3. 大量使用寄存器,指令执行速度更快;
  4. 大多数数据操作都在寄存器中完成;
  5. 寻址方式灵活简单,执行效率高;
  6. 指令长度固定;

ARM体系结构可以用两种方法存储字数据,称之为大端格式和小端格式。

1.1. ARM 微处理器的寄存器结构

ARM微处理器共有37个32位寄存器,其中31个为通用寄存器(包括程序计数器–PC 指针),6个为状态寄存器(用以标识 CPU 的工作状态及程序的运行状态)。但是这些寄存器不能被同时访问,具体哪些寄存器是可编程访问的,取决微处理器的工作状态及具体的运行模 式。但在任何时候,通用寄存器R14~R0、程序计数器PC、一个或两个状态寄存器都是可访问的。

64位的ARM有x0x28个通用寄存器,32位的ARM有w0w28个通用寄存器(属于x0x28的低32位)。x0x7通常用来存放函数的参数,更多的参数使用堆栈来传递。x0通常用来存放函数的返回值。

程序计数器(pc:Program Counter)其实就是记录CPU当前指令的是哪⼀条指令,存储着当前CPU正在执⾏的指令的地址。

链接寄存器(lr:Link Register),也就是x30,存储着函数的返回地址。例:执行bl指令前会将下一条指令的地址存储到lr寄存器中,然后会跳转到标记处开始执⾏代码。标记处的代码执行完成后(ret),会把lr寄存器存储的值取出来赋值给pc。

1.2. 程序状态寄存器

ARM体系结构包含一个当前程序状态寄存器(Current Program Status Register,缩写:CPSR)和五个备份的程序状态寄存器(Saved Program Status Register,缩写:SPSRs)。 备份的程序状态寄存器用来进行异常处理,其功能包括:保存ALU中的当前操作信息、控制允许和禁止中断、设置处理器的运行模式。

程序状态寄存器的每一位的安排如图所示:

条件码标志(Condition Code Flags):N、Z、C、V均为条件码标志位。它们的内容可被算术或逻辑运算的结果所改变,并且可以决定某条指令是否被执行。 在ARM状态下,绝大多数的指令都是有条件执行的。

条件码标志各位的具体含义如表所示:

控制位:PSR的低8位(包括I、F、T和M[4:0])称为控制位,当发生异常时这些位可以被改变。如果处理器运行特权模式,这些位也可以由程序修改。

保留位:PSR中的其余位为保留位,当改变PSR中的条件码标志位或者控制位时,保留位不要被改变,在程序中也不要使用保留位来存储数据。保留位将用于ARM版本的扩展。

1.3. ARM 微处理器的指令的分类与格式

ARM微处理器的指令集是加载/存储型的,也即指令集仅能处理寄存器中的数据,而且处理结果都要放回寄存器中,而对系统存储器的访问则需要通过专门的加载/存储指令来完成。

ARM微处理器的指令集可以分为跳转指令、数据处理指令、程序状态寄存器(PSR)处理指令、 加载/存储指令、协处理器指令和异常产生指令六大类,具体的指令及功能如表所示(表中指令为基本ARM指令,不包括派生的ARM指令)。

1.4. 指令的条件域

当处理器工作在ARM状态时,几乎所有的指令均根据CPSR中条件码的状态和指令的条件域有条件的执行。当指令的执行条件满足时,指令被执行,否则指令被忽略。

每一条ARM指令包含4位的条件码,位于指令的最高4位[31:28]。条件码共有16种,每种条件码可用两个字符表示,这两个字符可以添加在指令助记符的后面和指令同时使用。例如,跳转指令B 可以加上后缀EQ变为BEQ表示“相等则跳转”,即当CPSR中的Z标志置位时发生跳转。

在16种条件标志码中,只有15种可以使用,如表所示,第16种(1111)为系统保留,暂时不能使用。

1.5. ARM 指令的寻址方式

所谓寻址方式就是处理器根据指令中给出的地址信息来寻找物理地址的方式。目前ARM指令系统支持如下几种常见的寻址方式。

1.5.1. 立即寻址

1.5.2. 寄存器寻址

1.5.3. 寄存器间接寻址

1.5.4. 基址变址寻址

1.5.5. 多寄存器寻址

1.5.6. 相对寻址

1.5.7. 堆栈寻址

二、常见的指令

  • MOV

  • ADD

  • SUB

  • CMP

  • B

  • BL

  • LDR

  • STR

  • LDR

LDR用在正数寻址,LDUR用在负数寻址。

1
2
ldr w0, [x1, #0x10] ; 把(x1 + 0x10)的内存地址给寄存器w0
ldur w0, [x1, #-0x20] ; 把(x1 + (-0x20) == x1 - 0x20)的内存地址给寄存器w0

LDP是从内存中读取数据,放到一对寄存器中。

1
ldp w0, w1, [x2, #0x10] ; 把(x2 + 0x10)的内存地址前4个字节放到寄存器w0中,再往后4个高地址字节给w1
  • STR

STR用在正数写入,STUR用在负数写入。

1
2
str w0, [x1, #0x10] ; 把寄存器w0的数据写入到(x1 + 0x10)的内存地址
stur w0, [x1, #-0x20] ; 把寄存器w0的数据写入到(x1 + (-0x20) == x1 - 0x20)的内存地址

STP是把一对寄存器的地址放到内存地址中。

1
sdp w0, w1, [x2, #0x10] ; 把寄存器w0的数据写入到(x2 + 0x10)内存地址的前4个字节,寄存器w1的数据写入到(x2 + 0x10)内存地址的后4个字节
  • 补充

在Xcode调试的时候可能在汇编模式下看到wzr和xzr,这两个代表的是零寄存器(里面存储的值是0),在LLDB控制台是无法直接访问的。wzr(word zero register)是32位,xzr是64位。

2.1. 编写汇编代码

使用Xcode创建一个arm.s文件(.s是汇编(Assembly)语言源程序的扩展名,全称是asm)。

在汇编中,使用;表示注释。.text表示汇编代码存放在代码段中。.global可以指定函数访问权限是公开的(函数默认是私有访问),否则编译时会提示找不到对应的函数。函数使用下划线开头冒号结尾的形式,ret表示函数的返回,如果不写会穿透执行。立即数建议使用#前缀的十六进制进行表示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
.text ; 代码段

.global _test ; 扩大_test的访问权限

_test: ; 指令标记(汇编中没有函数的概念,但是可以按照函数进行理解)

mov x0, #0x8 ; 把立即数8赋值给x0
mov x1, #0x2 ; 把立即数2赋值给x1
add x2, x0, x1 ; x0 + x1的结果给x2

b code ; 跳转到code标签
mov x0, #0x3 ; 由于上面的b指令,导致此处不会执行,直接往下顺序执行
code: ; 标签code
mov x1, #0x1

cmp x0, x1
beq code2 ; 如果x0 == x1,就跳转到code2,否则继续往下顺序执行
mov x0, #0x1
code2:
mov x1, #0x2

ret ; 返回

创建一个arm.h头文件,用来做函数声明使用,并作为C函数调用。

1
void test();

2.2. LLDB寄存器读写

在Xcode的LLDB控制台,可以通过register命令进行单个寄存器的读写操作。

写:

1
2
格式:register write 寄存器 地址
例子:register write x0 0x000000016fd1fa4c(把地址0x000000016fd1fa4c放到寄存器x0)

读:

1
2
格式:register read 寄存器(如果不写寄存器,代表读取所有通用寄存器)
例子:register read x0(读取寄存器x0存放的内容)

OC的方法调用本质是消息转发机制,所以寄存器x0、x1并不是存储方法参数的。

1
void objc_msgSend(void /* id self, SEL op, ... */ )
  • 第一个参数是消息接收的对象实例
  • 第二个参数是执行的方法
  • 后面都是方法的参数

1
objc_msgSend(obj, @selector(show:at:), superView, indexPath);

po $x0:打印方法调用者
x/s $x1:打印方法名
po $x2:打印参数(以此类推,x3、x4也可能是参数)

如果是非arm64,寄存器就是r0、r1、r2。