Skip to content

Latest commit

 

History

History
334 lines (234 loc) · 11.7 KB

ARM汇编相关的iOS逆向理论.md

File metadata and controls

334 lines (234 loc) · 11.7 KB

1. ARM汇编基础

在逆向一个功能的时候,往往需要分析大量的汇编代码,在iOS逆向中,ARM汇编是必须掌握的语言,本文总结了ARM汇编的基础知识,如果你想了解更多,请参考狗神的小黄书《iOS逆向逆向工程》或ARM官方手册.

1.1 寄存器,内存和栈

在ARM汇编里,操作对象是寄存器,内存和栈 ARM的栈遵循先进后出,是满递减的,向下增长,也就是开口向下,新的变量被存到栈底的位置;越靠近栈底,内存地址越小 一个名为stackPointer的寄存器保存栈的栈底地址,成为栈地址. 可以把一个变量给入栈(push)以保存它的值,也可以让它出栈(pop),恢复变量的原始值.在实际操作中,栈地址会不断变化;但是在执行一块代码的前后,栈地址应该是不变的,不然程序就要出问题,

1.2 特殊用途的寄存器

ARM处理器中的部分寄存器有特殊用途 如下所示:

寄存器 用途
R0-R3 传递参数与返回值
R7 帧指针,指向母函数与被调用子函数在栈中的交界
R9 在iOS3.0以前被系统保留
R12 内部过程调用存储器,dynamic linker会用到它
R13 sp寄存器
R14 LR寄存器,保存函数返回地址
R15 PC寄存器
1.3 分支跳转与条件判断

处理器名为"Program counter"(简称PC)的寄存器用于存放下一条指令的地址.一般情况下,计算机一条接一条地顺序执行指令,处理器执行完一条指令后将PC加1,让它指向下一条指令.例如处理器顺序执行指令1到指令5,但是如果把PC的值变一变,指令执行的顺序就完全不同

指令执行顺序被打乱,变成了指令1,指令5,指令4,指令2,指令3,指令6,这种乱序的学名叫做"分支",或者"跳转",它使循环和subroutime(子程序)成为可能,例如:

```
// endless() 函数
endless:
	操作  操作数1, 操作数2
	分支  endless
	返回  // 死循环,执行不到这里啦!
```

在实际情况中,满足一定条件才得以触发的分支是最实用的,这种分支成为条件分支.if else 和 while都是基于条件分支实现的,在ARM汇编中,分支的条件一般有4种:

  • □ 操作结果为0(或不为0);
  • □ 操作结果为负数;
  • □ 操作结果有进位;
  • □ 运算溢出(比如两个正数相加得到的数超过了寄存器位数).

这些条件的判断准则(flag)存放在程序状态寄存器(Program Status Register,PSR)中,数据处理相关指令会改变这些flag,分支指令再根据这些flag决定是否跳转.下面的伪代码展示了一个for循环

for:
	相加  A,#1
	比较	A,#16
	不为0则跳转到for
	/* 此循环将A和#16作比较,如果两者不相等,则将A加1,继续比较.
	 如果两者相等,则不再循环,继续往下执行. */

2. ARM/THUMB指令解读

ARM处理器用到的指令集分为ARM和THUMB两种:ARM指令长度均为32bit,THUMB指令长度为16bit.所有指令可大致分为三类,分别为,数组操作指令,内存操作指令和分支指令.

2.1 数据操作指令

数据操作指令有以下2条规则:

  • 所有的操作数均为32bit;
  • 所有的结果均为32bit,且只能存放在寄存器当中. 总的来说,数据操作指令的基本格式是:

op{cond}{s} Rd,Rn,Op2


其中,"cond"和"s"是另个可选后缀;"cond"的作用是指定指令"op"在什么条件下执行,共有17中条件: 

指令	| 条件
--- 	| 	---
EQ 		| 	结果为0(EQual to 0)
NE 		|	结果不为0(Not Equal to 0) 
CS 		| 	有进位或借位(Carry Set)
HS		|	同CS(unsigned Higer or Same)
CC		| 	没有进位或借位(Carry Clear)
LO		|  	同CC(unsigned LOwer)
MI		| 	结果小于0(MInus)
PL		| 	结果大于等于0(PLus)
VS		| 	溢出(Overflow Set)
VC		| 	无溢出(Overflow Clear)
HI		| 	无符号比较大于(unsigned HIger)
LS		|  	无符号比较小于等于(unsigned Lower or Same)
GE		| 	有符号比较大于等于(signed Greater than or Equal)
LT		| 	有符号比较小于(signed Less Than)
GT		| 	有符号比较大于(signed Greater Than)
LE		| 	有符号比较小于等于(signed Less than or Equal)
AL		| 	无条件(Always,默认)

"cond"的用法很简单,例如:

比较 R0, R1 移动 GE R2, R0 移动 LT R2, R1


比较R0和R1的值,如果R0大于等于R1,则R2 = R0;否则R2 = R1.
"s"的作用是指定指令"op"是否设置了flag,共有下面**4种flag**:

* ***N(Negative)***如果结果小于0则置1,否则置0;

* ***Z(zero)***如果结果是0则置1,否则置0;

* ***C(Carry)***对于加操作(包括CMN)来说,如果产生进位则置1,否则置0;对于减操作(包括CMP来说),Carry相当于Not-Borrow,如果产生借位则置0,否则置1;对于有移位的非加/减操作来说,C置移出值得最后一位;对于其他的非加/减操作来说,C的值一般不变;

* ***V(overflow)***如果操作导致溢出,则置1,否则置0

>需要注意一点的是,**C flag**表示**无符号数**运算结果是否**溢出**;**V flag**表示**有符号数**运算结果是否**溢出**.

算数操作指令可以大致分为4类:

* 1.算数操作

> ADD R0,R1,R2; ------------------> R0 = R1 + R2

> ADC R0,R1,R2; ------------------> R0 = R1 + R2 + C(array)

> SUB R0,R1,R2; ------------------> R0 = R1 - R2

> SBC R0,R1,R2; ------------------> R0 = R1 - R2 - !C

> RSB R0,R1,R2; ------------------> R0 = R2 - R1

> RSC R0,R1,R2; ------------------> R0 = R2 - R1 - !C

>算数操作中,ADD和SUB为基础操作,其他均为两者的变种.RSB是"Reverse Sub"的缩写,仅仅是把SUB的两个操作数调换了位置而已;以"C"结尾的变种代表没有进位和借位的加减法,当产生进位或者借位时,将Carrry flag 置为1.

* 2.逻辑操作


> AND R0,R1,R2; ------------------> R0 = R1 & R2

> ORR R0,R1,R2; ------------------> R0 = R1 | R2

> EOR R0,R1,R2; ------------------> R0 = R1 ^ R2

> BIC R0,R1,R2; ------------------> R0 = R1 &~ R2

> MOV RO,R2;    ------------------> R0 = R2

> MVN R0,R2;    ------------------> R0 = ~R2

>逻辑操作指令都已经用C操作符说明了作用,但是C操作符里的移位操作并没有对位的逻辑操作指令,ARM采用了桶式移位,共有四种指令:

>LSL 逻辑左移 ![逻辑左移](http://o7pqb42yk.bkt.clouddn.com/LSL.jpg)

>LSR 逻辑右移 ![逻辑右移](http://o7pqb42yk.bkt.clouddn.com/lsr.jpg)

>ASR 算术右移 ![算术右移](http://o7pqb42yk.bkt.clouddn.com/asr.jpg)

>ROR 循环右移 ![循环右移](http://o7pqb42yk.bkt.clouddn.com/ROR.jpg)

* 3.比较操作
>CMP R1,R2;  ------------------> 执行R1 - R2并依结果设置flag

>CMN R1,R2;  ------------------> 执行R1 + R2并依结果设置flag

>TST R1,R2;  ------------------> 执行R1 & R2并依结果设置flag

>TEQ R1,R2;  ------------------> 执行R1 ^ R2并依结果设置flag

> 比较操作其实就是改变flag的算术操作或逻辑操作,只是操作结果不保留在寄存器里而已.

* 4.乘法操作

> MUL R4,R3,R2    ------------------> R4 = R3 * R2

> MLA R4,R3,R2,R1 ------------------> R4 = R3 * R2 + R1
 
>乘法操作的操作数必须来自寄存器


##### 2.2 内存操作指令
内存操作指令的基本格式是:

op{cond}{type} Rd,[Rn,Op2]


其中Rn是基址寄存器,用于存放基地址;"cond"的作用与数据操作指令相同;"type"指定指令"op"操作的数据类型,共有四种:

B(unsigned Byte) 无符号byte(执行时扩展到32bit,以0填充);

SB(signed Byte) 有符号byte(仅用于LDR指令;执行时扩展到32bit,以符号位填充);

H(unsigned Halfword) 无符号halfword(执行时扩展到32bit,以0填充);

SH(Signed Halfword) 有符号halfword(仅用于LDR指令;执行时扩展到32bit,以符号位填充).


>如果不指定"type",则默认是word
ARM内存操作基础指令只有2个,LDR(loaD Register)将数据从内存中读出来,存到寄存器中;STR(STore Register)将数组从寄存中读出来,存到内存中.两个指令的使用情况如下:

* LDR

LDR Rt,[Rn {,#offset}] ; Rt = *(Rn {+ offset}),{}代表可选 LDR Rt,[Rn, #offset]! ; Rt = *(Rn + offset);Rn = Rn + offset LDR Rt,[Rn], #offset ; Rt = *Rn;Rn = Rn + offset


* STR

STR Rt,[Rn {,#offset}] ; *(Rn {+ offset}) = Rt STR Rt,[Rn, #offset]! ; *(Rn + offset) = Rt; Rn = Rn + offset STR Rt,[Rn], #offset ; *Rn = Rt; Rn = Rn + offset


此外,LDR和STR的变种LDRD和STRD还可以操作双字(DoubleWord),即一次性操作两个寄存器,其基本格式如下:

op{cond} Rt,Rt2, [Rn {, #offset}]


其用法与原型类似,如下:

* STRD

SRTD R4,R5, [R9,#offset] ; (R9 + offset) = R4;(R9 + offset + 4) = R5


* LDRD

LDRD R4,R5,[R9,#offset] ; R4 = *(R9 + offset); R5 = *(R9+offset+4)


除LDR和STR外,还可以通过LDM(LoaD Multiple)和STM(STore Multipe)进行块传输,一次性操作多个寄存器.块传输指令的基本格式是

op{cond}{}mode] Rd{!},reglist


其中Rd是基址寄存器,可选的"!"制定Rd变化后的值是否写会Rd, reglist是一系列寄存器,用大括号括起来,它们之间可以用","分割,也可以用"-"表示一个范围,比如,{R4-R6,R8}表示寄存器,R4,R5,R6,R8;这些寄存器的顺序是按照自身的编号由小到大排列的,与大括号内的排列顺序无关.
	
需要特别注意的是,**LDM和STM的操作方向与LDR和STR完全相反:LDM是把从Rd开始,地址连续的内存数据存入reglist中,STM是把reglist中的值存入从Rd开始,地址连续的内存中.此处特别容易混淆**
	
"cond" 的作用与数据操作指令相同."mode"指定R4值得变化的4中规律,如下所示:

IA(Increament After)每次传输后增加Rd的值;

IB(Increament Before)每次传输前增加Rd的值

DA(Decrement After) 每次传输后减少Rd的值;

DB(Decreament Before)每次传输前减少Rd的值.


>这是什么意思呢?下面以LDM为代表,举一个简单的例子,相信大家一看就明白了.在下图(块传输指令模拟环境)中,R0指向的值是5.

![块传输指令模拟环境](http://o7pqb42yk.bkt.clouddn.com/%E5%9D%97%E4%BC%A0%E8%BE%93%E6%8C%87%E4%BB%A4%E6%A8%A1%E6%8B%9F%E7%8E%AF%E5%A2%83.png)

>在执行以下命令后,R4,R5,R6的值分别变成:

foo(): LDMIA R0, {R4 - R6}; R4 = 5, R5 = 6, R6 = 7 LDMIB R0, {R4 - R6}; R4 = 6, R5 = 7, R6 = 8 LDMDA R0, {R4 - R6}; R4 = 5, R5 = 4, R6 = 2 LDMDB R0, {R4 - R6}; R4 = 4, R5 = 3, R6 = 3

>STM指令的作用方式与此类似,不再赘述.**LDM和STM的操作与LDR和STR完全相反**

##### 2.3 分支指令
分支指令可以分为无条件分支和条件分支两种.

* 无条件分支

B Label;PC = Label BL Label;LR = PC - 4;PC = Label BX Rd ;PC = Rd并切换指令集 eg: foo(): B Label ; 跳转到Label处并往下执行 ...... ; 得不到执行 Label: ......


* 无条件分支

跳转分支的cond是依照前面的flag来判断的,它们的对应关系如下:

cond	| flag
--- 	| 	---
EQ 		| 	Z = 1
NE 		|	Z = 0 
CS 		| 	C = 1
HS		|	C = 1
CC		| 	C = 0
LO		|  	C = 0
MI		| 	N = 1
PL		| 	N = 0
VS		| 	V = 1
VC		| 	V = 0
HI		| 	C = 1 & Z = 0
LS		|  C = 0 | Z = 1
GE		| 	N = V
LT		| 	N != V
GT		| 	Z = 0 & N = V
LE		| 	Z = 1 | N != V

在条件分支指令钱会有一条数据操作指令来设置flag,分支指令根据flag的值来决定代码走向,举例如下:

Label: Lable1: LDR R0, [R1], #4 CMP R0, 0; 如果R0 == 0,Z =1 ; 否则Z = 0 BNE Label ; Z == 0则跳转


##### 2.4 THUMB指令
THUMB指令集是ARM指令集的一个子集,每条THUMB指令均为16bit;因此THUMB指令比ARM指令更节省空间,且在16位数据总线上的传输效率更高.有得必有失,除了"b"之外,所有的THUMB指令均无法条件执行;桶式移位无法结合其他指令执行;大多数THUMB指令只能使用R0-R7这8个寄存器等.相对于ARM指令,THUMB指令的特点如下:

* 指令数量减少
* 没有条件执行
* 所有指令默认附带*
* 桶式移位无法结合其他指令执行
* 寄存器使用受限
* 立即数和第二操作数使用有限
* 不支持数据写回