iOS逆向工程(十):ARM64汇编

前言

我们使用Hopper Disassembler等反编译工具查看Mach-O可执行文件时,看到的都是汇编代码,所以只有我们学会了汇编后,才能更好的去调试,去分析App的逻辑
学会了汇编之后,甚至可以直接修改汇编代码,导出可执行文件,再经过签名安装使用了,都不用通过之前的tweak方式hook了,当然,这需要对汇编掌握的及其深入,并且经过调试分析清楚了App整体逻辑之后了。
一、汇编
    1. CPU的内部有寄存器、运算器和控制器,它们之间由总线连接。其中运算器负责信息处理,由CPU硬编码指令完成;控制器负责协调控制计算机的其他器件进行工作;寄存器进行数据的临时存储,程序员只需关心寄存器的数据存取即可改变运行CPU运行结果。
    1. 在iOS领域,根据CPU架构不同,汇编主要分为两种:模拟器上的x86汇编真机上的arm64汇编,这里我们重点介绍真机上的arm64汇编
    1. 想要学好arm64汇编,主要学三个方面:寄存器、指令、堆栈
二、寄存器
寄存器就是CPU内部临时存储数据的地方,分为有很多种,如下图所示,我们下面依次讲解:
image
    1. 我们把x0~x28称为通用寄存器,通用寄存器通常用来存放一般性的数据,通用寄存器分为64位和32位

      • 64位(8字节)的通用寄存器有:x0 ~ x28,29个寄存器,8个字节代表这个寄存器最多能放8个字节的数据

      • 32位(4字节)的通用寄存器有:w0 ~ w28,这29个4字节的寄存器其实是8字节寄存器x0 ~ x28的低32位

      • 通用寄存器的x0与w0之间的关系,如下图所示,其他通用寄存器也是如此

        image
      • 通过LLDB命令,我们也可以验证出他们的关系,如下图所示:

        image
        image
      • x0~x7寄存器,一般会存储函数的参数,大于8个的会通过堆栈传参

      • x0:一般会存储函数的返回值

    1. pc寄存器,也叫做程序计数器,记录着当前CPU执行的是哪一条指令,存储着当前CPU正在执行指令的地址,类似于8086汇编的ip寄存器
    1. 堆栈寄存器,有两个,分别是:栈顶指针寄存器sp栈低指针寄存器fp栈低指针寄存器fp也叫做x29寄存器,堆栈寄存器是用来控制函数分配栈空间的
    1. 链接寄存器lr,也叫做x30寄存器存储着函数的返回地址
    1. 程序状态寄存器,有两种,分别是cpsr(current Program Status Register)spsr(Saved Program Status Register),cpsr是当前程序的运行状态,spsr是在异常状态下使用的,CPSR寄存器和其他寄存器不一样(其他寄存器是用来存放数据的,都是整个寄存器具有一个含义),是按起作用的,也就是说,它的每一位都有专门的含义,记录特定的信息,如下图所示:
    image
      - CPSR的低8位(包括I、F、T和M[4~0])称为控制位,程序无法修改,除非CPU运行于特权模式下,程序才能修改控制位!
      - N、Z、C、V均为条件码标志位。它们的内容可被算术或逻辑运算的结果所改变,并且可以决定某条指令是否被执行,意义重大!
    
    
    1. xzr零寄存器,表示zero register,里面存储的值都是0,xzr代表8字节,wzr代表4字节
    1. 总结:
      • x0 ~ x28通用寄存器,有8个字节的空间,其中较低的4个字节的空间,是w0~w28寄存器x代表8个字节,w代表4个字节

      • x29就是fp寄存器,也就是栈底寄存器,代表着函数的栈底

      • x30就是lr寄存器,也就是链接寄存器,存储着函数的返回地址,使用bl指令跳转时,会把bl的下面的一条指令的地址,存放到lr寄存器中,如果bl跳转之后,遇到了ret指令ret指令会把lr寄存器的值给pc寄存器,这样CPU就会执行lr寄存器存放的指令了,就相当于函数返回了

三、指令
    1. mov指令,传送指令,格式是mov {条件}{S}目标寄存器 ,源操作数 ,mov指令可以完成从另一个寄存器或者是将一个立即数,加载到目的寄存器中,其中S选项决定执行的操作是否影响CPSR中条件标志位的值,当没有S时指令不更新CPSR中的条件标志位的值。

      • mov指令实例如下:

      • mov x1,x0,意思是将寄存器x0的值传送到寄存器x1

      • mov pc,x14,意思是将寄存器x14的值传送到寄存器pc中,pc寄存器通常用来存放CPU正在执行的指令的地址,所以常用于子程序返回

      • mov x1,x0,#0x3,意思是将寄存器x0的值加上0x3,然后传送到寄存器x1

    1. ret指令,返回指令,作用是函数返回,本质上是将lr(x30)寄存器的值赋值给pc寄存器pc寄存器存储CPU当前执行的指令的地址,lr寄存器存储着函数的返回地址,将lr寄存器的值,给了pc寄存器,相当于实现了函数返回
    1. add指令,加法指令,格式是add {条件}{S}目标寄存器 ,操作数1,操作数2 ,add指令用于将 两个操作数相加,并将结果存放到目的寄存器中,操作数1必须是一个寄存器,操作数2可以是寄存器,也可以是立即数

      • add指令实例如下:

      • add x0,x1,x3,意思是将寄存器x1的值加上寄存器x3的值,赋值给寄存器x0

      • add x0,x1,#0x77,意思是将寄存器x1的值加上0x77,然后赋值给寄存器x0

      • add x0,x1,x3,LSL0x1,意思是将寄存器x1的值加上寄存器x3左移0x1,然后赋值给寄存器x0,也就是:x0 = x1 + (x3 << 1)

    1. sub指令,减法指令,格式是sub {条件}{S}目标寄存器 ,操作数1,操作数2 ,sub指令用于把操作数1减去操作数2,并将结果存放到目的寄存器中,操作数1必须是一个寄存器,操作数2可以是寄存器,也可以是立即数

      • sub指令实例如下:

      • sub x0,x1,x3,意思是将寄存器x1的值减去寄存器x3的值,赋值给寄存器x0

      • sub x0,x1,#0x88,意思是将寄存器x1的值减去0x88,然后赋值给寄存器x0

      • sub x0,x1,x3,LSL0x1,意思是将寄存器x1的值减去寄存器x3左移0x1,然后赋值给寄存器x0,也就是:x0 = x1 - (x3 << 1)

    1. cmp指令,比较指令,格式是cmp {条件}{S}目标寄存器 ,操作数1,操作数2 ,cmp指令用于把一个寄存器的内容和另一个寄存器的内容或者立即数,进行比较,同时更新CPSR中的条件标志位,cmp指令进行的是一次减法运算,不会存储结果,只会更改条件标志位

      • cmp指令实例如下:

      • cmp x0,x1,意思是将寄存器x0的值与寄存器x1的值相减,并根据结果设置CPSR的标志位

      • cmp x0,#0x88,意思是将寄存器x0的值与立即数0x88相减,并 根据结果设置CPSR的标志位

    1. b指令,不带返回的跳转指令,常与cmp指令配合使用,格式是b {条件} 目的地址 ,一旦遇到b指令,ARM处理器将立即跳转到目的地址,从那里继续执行,注意存储在跳转指令中的是相对于当前pc值的一个偏移量,而不是一个绝对地址,偏移量是由汇编器来计算的。
    1. bl 指令,带返回的跳转指令,常与cmp指令配合使用,格式是bl {条件} 目的地址,bl 指令的格式为:bl{条件} 目标地址,bl是另一个跳转指令,在跳转之前,会把下一条指令的地址,存储到lr寄存器中,等到子函数执行完毕,执行ret指令时,ret指令会把lr寄存器的值给了pc寄存器pc寄存器存储的是CPU当前执行的指令的地址,这样就实现了子函数返回。

      例如:cmp与b配合使用,如下所示

0x100432624 <+88>:  cmp    x1, #0x1                 ; =0x1 
0x100432628 <+92>:  b.le   0x100432630               ; 

<1>. cmp:  将寄存器 x1 的值与立即数 0x1 相减,并根据结果设置 CPSR 的标志位
<2>. b.le 0x100432630:表示如果x1 <= 0x1那么就执行0x100432630

    1. ldr 指令,从内存加载数据到寄存器,格式是:ldr{条件} 目的寄存器,<存储器地址>,ldr指令用于从存储器中将读取相应大小的字节数,传送到目的寄存器中

      • ldr指令实例如下:

      • ldr x0,[x1],意思是从x1寄存器存储的地址取出8个字节,然后存到x0寄存器

      • ldr w0,[x1],意思是从x1寄存器存储的地址取出4个字节,然后存到w0寄存器

      • ldr x0,[x1,#0x888],意思是从x1寄存器存储的值加上0x888得到地址处,取出8个字节,然后存到x0寄存器

      • ldr x0,[x1,#0x888]!,意思是从x1寄存器存储的值加上0x888得到地址处,取出8个字节,然后存到x0寄存器中,并且将新地址x1+0x888,存入到x1寄存器

    1. str 指令,将寄存器的数据存储到内存中,格式是:str{条件} 源寄存器,<存储器地址>

      • str指令实例如下:

      • str x0,[x1],意思是从x0寄存器存储的地址取出8个字节,然后存到x1寄存器的内存中

      • ldr w0,[x1,#0x888],意思是从w0寄存器存储的地址取出4个字节,然后存到x1寄存器的值加上0x888的地址中

三、叶子函数的堆栈
    1. 函数分为:叶子函数和非叶子函数,这两种函数的堆栈分配情况略有不同,叶子函数就是像叶子一样没有分支了,函数内部不会调用其他函数了;非叶子函数,就是函数内部会调用其他函数。
    1. 我们先来看一个叶子函数,C代码和汇编代码如下:(w代表4个字节,x代表8个字节,;代表注释)
C代码:

void test(){
    int a = 2;
    int b = 3;
}

test的函数汇编代码如下:

sub sp, sp, #16             ; =16          ;sp = sp - 16,开辟16个字节的栈空间

orr w8, wzr, #0x2                          ;设置w8寄存器为2
str w8, [sp, #12]                          ;在sp地址偏移12个字节的地方开始存储4个字节的数据,也就是w8寄存器的值,也就是存储了2
orr w8, wzr, #0x3                          ;设置w8寄存器为3
str w8, [sp, #8]                           ;在sp指针+8的地址开始,存储4个字节的数据,也就是将w8的值存储到sp偏移8字节的位置

add sp, sp, #16             ; =16          ;sp = sp + 16,回收16个字节的空间,保持堆栈平衡
ret

    1. 叶子函数的堆栈开辟,可以用下面一幅图表示:

      image
四、非叶子函数的堆栈
    1. 我们再来看一个非叶子函数,C代码和汇编代码如下:(w代表4个字节,x代表8个字节,;代表注释)
C代码如下,我们在good函数里,调用了test函数
void test(){
    int a = 2;
    int b = 3;
}

void good(){
    int a = 7;
    int b = 8;
    test();
}

good函数的汇编代码如下:

sub sp, sp, #32             ; =32                     ;sp = sp-32,开辟函数栈空间,也就是将sp指针往低字节移动32字节
stp x29, x30, [sp, #16]     ; 16-byte Folded Spill    ; 在sp+16的地方,往高字节的方向,依次存放x29、x30的数据,x29的数据占8个字节,x30的数据也占8个字节
add x29, sp, #16            ; =16                     ;x29 = sp + 16,x29也就是fp栈底指针

orr w8, wzr, #0x7                                     ;将w8置为7
stur w8, [x29, #-4]                                   ;将w8的值存放到x29-4的位置
orr w8, wzr, #0x8                                     ;将w8置为8
str w8, [sp, #8]                                      ;将w8的值存放到sp+8的位置
bl  _test                                             ;调用test函数,执行test函数的汇编代码

ldp x29, x30, [sp, #16]     ; 16-byte Folded Reload   ;调用完test,会返回到这里,继续执行,从sp+16的地址,加载8个字节到x29,然后再加载8个字节的数据到x30
add sp, sp, #32             ; =32                     ;sp = sp + 32,回收函数的栈空间,将sp指针复原
ret                                                   ;good函数调用完毕,将x30的值给了pc寄存器,CPU会执行pc寄存器中存放的地址所对应的指令的

    1. 我们可以用一副图,清晰的表示good函数的调用过程,的如下所示:

      image

你可能感兴趣的:(iOS逆向工程(十):ARM64汇编)