iOS逆向实战--001:初识汇编

汇编语言(assembly language):是任何一种用于电子计算机、微处理器、微控制器,或其他可编程器件的低级语言。在不同的设备中,汇编语言对应着不同的机器语言指令集。一种汇编语言专用于某种计算机系统结构,而不像许多高级语言,可以在不同系统平台之间移植。

使用汇编语言编写的源代码,然后通过相应的汇编程序将它们转换成可执行的机器代码。这一过程被称为汇编过程。

逆向开发中,有一个非常重要的环节就是静态分析。首先我们是逆向iOS系统上面的APP,一个APP安装在手机上面的可执行文件本质上是二进制文件。因为iPhone手机上执行的指令是二进制,由手机上的CPU执行的,所以静态分析是建立在分析二进制上面,对汇编语言的掌握是必不可少的。

汇编语言的发展
机器语言

01组成的机器指令

  • 加:0100 0000
  • 减:0100 1000
  • 乘:1111 0111 1110 0000
  • 除:1111 0111 1111 0000
汇编语言(assembly language)

使用助记符代替机器语言,如:

  • 加:INC EAX通过编译器0100 0000
  • 减:DEC EAX通过编译器0100 1000
  • 乘:MUL EAX通过编译器1111 0111 1110 0000
  • 除:DIV EAX通过编译器1111 0111 1111 0000
高级语言(High-level programming language)

C\C++\Java\OC\Swift更加接近人类的自然语言,例如C语言:

  • 加:A+B通过编译器0100 0000
  • 减:A-B通过编译器0100 1000
  • 乘:A*B通过编译器1111 0111 1110 0000
  • 除:A/B通过编译器1111 0111 1111 0000

代码终端设备上的过程:

  • 汇编语言机器语言一一对应,每一条机器指令都有与之对应的汇编指令
  • 汇编语言可以通过编译得到机器语言机器语言可以通过反汇编得到汇编语言
  • 高级语言可以通过编译得到汇编语言\机器语言,但汇编语言\机器语言几乎不可能还原成高级语言
汇编语言的特点
  • 可以直接访问、控制各种硬件设备,比如存储器、CPU等,能最大限度地发挥硬件的功能
  • 能够不受编译器的限制,对生成的二进制代码进行完全的控制
  • 目标代码简短,占用内存少,执行速度快
  • 汇编指令是机器指令的助记符,同机器指令一一对应。每一种CPU都有自己的机器指令集\汇编指令集,所以汇编语言不具备可移植性
  • 知识点过多,开发者需要对CPU等硬件结构有所了解,不易于编写、调试、维护
  • 不区分大小写,比如movMOV是一样的
汇编的用途
  • 编写驱动程序、操作系统(比如Linux内核的某些关键部分)
  • 对性能要求极高的程序或者代码片段,可与高级语言混合使用(内联汇编)
  • 软件安全
    病毒分析与防治
    逆向\加壳\脱壳\破解\外挂\免杀\加密解密\漏洞\黑客
  • 理解整个计算机系统的最佳起点和最有效途径
  • 为编写高效代码打下基础
  • 弄清代码的本质
    函数的本质究竟是什么?
    ++a + ++a + ++a底层如何执行的?
    编译器到底帮我们干了什么?
    DEBUG模式和RELEASE模式有什么关键的地方被我们忽略
    ......

越底层越单纯!真正的程序员都需要了解的一门非常重要的语言,汇编!

汇编语言的种类

目前讨论比较多的汇编语言有:

  • 8086汇编(8086处理器是16bitCPU
  • Win32汇编
  • Win64汇编
  • ARM汇编(嵌入式、MaciOS
  • ......

iPhone里面用到的是ARM汇编,但是不同的设备也有差异,因CPU的架构不同

架构 设备
armv6 iPhoneiPhone2iPhone3GiPod Touch第一代、第二代
armv7 iPhone3GSiPhone4iPhone4SiPadiPad2iPad3The New iPad),iPad miniiPod Touch 3GiPod Touch4
armv7s iPhone5iPhone5CiPad4iPad with Retina Display
arm64 iPhone5S以后,iPhoneXiPad AiriPad mini2以后
必要常识

要想学好汇编,首先需要了解CPU等硬件结构

APP/程序的执行过程:

  • 硬件相关最为重要是CPU/内存
  • 在汇编中,大部分指令都是和CPU与内存相关的
总线
  • 每一个CPU芯片都有许多管脚,这些管脚和总线相连,CPU通过总线跟外部器件进行交互
  • 总线:一根根导线的集合
  • 总线的分类
    地址总线
    数据总线
    控制总线

例如:CPU从内存的3号单元读取数据

数量单位 & 容量单位

  • 地址单元:字节
  • 字节:ByteB
  • 1个字节等于8个二进制位
  • 数量单位:1M = 1024K1K = 1024
  • 容量单位:1MB = 1024KB1KB = 1024B

例如:100M的带宽,运营商所指100MbpsMbps是一种传输速率单位,指每秒传输的位(bit)数量。所以100M的带宽理论下载速度是100M/8 = 12.5MB/s

地址总线

  • 它的宽度决定了CPU的寻址能力
  • 8086的地址总线宽度是20,所以寻址能力是1M2 ^ 20

数据总线

  • 它的宽度决定了CPU的单次数据传送量,也就是数据传送速度
  • 8086的数据总线宽度是16,所以单次最大传递2个字节的数据

日常所说CPU32位64位,就是指数据吞吐量。32位一次可传送4字节64位可传送8字节

控制总线

  • 它的宽度决定了CPU对其他器件的控制能力、能有多少种控制

案例:

一个CPU的寻址能力为8KB,那么它的地址总线的宽度为13

  • 8*1024 = 2^13

808080888028680386的地址总线宽度分别为16根20根24根32根。那么他们的寻址能力分别为64KB1MB16MB4GB

  • 2^16 = 65536 = 64KB
  • 2^20 = 1048576 = 1MB
  • 2^24 = 16777216 = 16MB
  • 2^32 = 4294967296 = 4GB

8080808880868028680386的数据总线宽度分别为8根8根16根16根32根。那么它们一次可以传输的数据为1B1B2B2B4B

  • 8/8 = 1B
  • 8/8 = 1B
  • 16/8 = 2B
  • 16/8 = 2B
  • 32/8 = 4B

从内存中读取1024字节的数据,8086至少要读512次80386至少要读取256次

  • 1024*8/16 = 512
  • 1024*8/32 = 256
内存
  • 内存地址空间的大小受CPU地址总线宽度的限制。8086的地址总线宽度为20,可以定位2 ^ 20个不同的内存单元(内存地址范围0x00000~0xFFFFF),所以8086的内存空间大小为1MB
  • 0x00000~0x9FFFF:主存储器。可读可写
  • 0xA0000~0xBFFFF:向显存中写入数据,这些数据会被显卡输出到显示器。可读可写
  • 0xC0000~0xFFFFF:存储各种硬件\系统信息。只读
进制
学习进制的障碍
  • 很多人学不好进制,原因是总以十进制为依托去考虑其他进制,需要运算的时候也总是先转换成十进制,这种学习方法是错误的
  • 我们为什么一定要转换十进制呢?仅仅是因为我们对十进制最熟悉,所以才转换
  • 每一种进制都是完美的,想学好进制首先要忘掉十进制,也要忘掉进制间的转换
进制的定义
  • 八进制8个符号组成:0 1 2 3 4 5 6 7,逢八进一
  • 十进制10个符号组成:0 1 2 3 4 5 6 7 8 9,逢十进一
  • N进制就是由N个符号组成:逢N进一

例如:

1 + 1在什么情况下等于3

  • 十进制10个符号组成:0 1 3 2 8 A B E S 7,逢十进一
  • 如果这样定义十进制1 + 1 = 3

这样的目的何在?

  • 传统定义的十进制自定义十进制不一样。这10个符号构成的符号表如果保密,别人是没办法拿到具体的数据,可以用于加密
进制的运算

八进制加法表

八进制乘法表

二进制的简写形式
      二进制:1 0 1 1 1 0 1 1 1 1 0 0
三个二进制一组:101  110  111  100
      八进制: 5    6    7    4
四个二进制一组:1011 1011 1100
    十六进制:  b    b    c

二进制:从0写到1111

  • 0000 0001 0010 0011 0100 0101 0110 0111 1000 1001 1010 1011 1100 1101 1110 1111

这种二进制使用起来太麻烦,改成更简单一点的符号:

  • 0 1 2 3 4 5 6 7 8 9 A B C D E F,这就是十六进制
自定义进制符号

现在有十进制10个符号分别是:2,9,1,7,6,5,4, 8,3 , A,逢十进一

十进制:  0  1  2  3  4  5  6  7  8  9
自定义:  2  9  1  7  6  5  4  8  3  A
       92 99 91 97 96 95 94 98 93 9A
       12 19 11 17 16 15 14 18 13 1A
       72 79 71 77 76 75 74 78 73 7A
       62 69 61 67 66 65 64 68 63 6A
       52 59 51 57 56 55 54 58 53 5A
       42 49 41 47 46 45 44 48 43 4A
       82 89 81 87 86 85 84 88 83 8A
       32 39 31 37 36 35 34 38 33 3A
       922...
  • 123 + 234 = 1A6

上述案例,可以转化常规十进制运算,然后查表。但如果是其他进制,就不能转换,要直接学会查表

现在有九进制9个符号分别是:2,9,1,7,6,5,4, 8,3,逢九进一

十进制:  0  1  2  3  4  5  6  7  8  
自定义:  2  9  1  7  6  5  4  8  3  
       92 99 91 97 96 95 94 98 93 
       12 19 11 17 16 15 14 18 13 
       72 79 71 77 76 75 74 78 73 
       62 69 61 67 66 65 64 68 63 
       52 59 51 57 56 55 54 58 53 
       42 49 41 47 46 45 44 48 43 
       82 89 81 87 86 85 84 88 83 
       32 39 31 37 36 35 34 38 33 
       922...
  • 123 + 234 = 725
数据的宽度

数学上的数字,是没有大小限制的,可以无限的大。但在计算机中,由于受硬件的制约,数据都是有长度限制的(我们称为数据宽度),超过最多宽度的数据会被丢弃

计算机中常见的数据宽度
  • 位(Bit):1个位就是1个二进制位,0或者1
  • 字节(Byte):1个字节由8Bit组成(8位),内存中的最小单元Byte
  • 字(Word):1个字由2个字节组成(16位),这2个字节分别称为高字节低字节
  • 双字(Doubleword):1个双字由两个字组成(32位

计算机存储数据,它会分为有符号数无符号数

  • 无符号数,直接换算。例如:F表示15
  • 有符号数:
正数:0    1    2    3    4    5    6    7 
负数:F    E    D    B    C    A    9    8
    -1   -2   -3   -4   -5   -6   -7   -8

案例:

搭建Demo项目

打开ViewController.m文件,写入以下代码:

#import "ViewController.h"

@implementation ViewController

int test(){
  int cTemp = 0x1FFFFFFFF;
  return cTemp;
}

- (void)viewDidLoad {
   [super viewDidLoad];
   printf("%x\n",test());
}

@end

test函数return时设置断点,使用真机运行项目

  • cTemp的值为-1
  • 使用p &cTemp获取cTemp地址
  • 使用x 0x000000016d4f382c读取内存地址,int类型占4字节,所以只保留FFFFFFFF,前面的01溢出
  • 寄存器对于溢出会有单独的记录
  • iOS为小端模式,内存从右往左读

另一种查看方式,选择Debug->Debug Workflow->View Memory

Address中输入查看的地址:0x16b389b6c

int类型是有符号整型,占4字节,也就是32个二进制位。此刻全部为1。第一位的符号位为1,所以是负数

有符号数和无符号数是读取时区分的,对于数据本身的内容没有任何变化。分别使用%d%u进行打印:

CPU & 寄存器

CPU内部部件之间,由总线连接

CPU除了有控制器、运算器还有寄存器。其中寄存器的作用就是进行数据的临时存储

  • CPU的运算速度是非常快的。为了性能,CPU在内部开辟一小块临时存储区域,并在进行运算时先将数据从内存复制到这一小块临时存储区域中,运算时就在这一小快临时存储区域内进行。我们称这一小块临时存储区域为寄存器

对于arm64系的CPU来说, 如果寄存器以x开头,则表明是一个64位的寄存器,如果以w开头则表明是一个32位的寄存器,在系统中没有提供16位8位的寄存器供访问和使用。其中32位的寄存器是64位寄存器的低32位部分,并不是独立存在的

  • 对程序员来说,CPU中最主要部件是寄存器,可以通过改变寄存器的内容来实现对CPU的控制
  • 不同的CPU,寄存器的个数、结构是不相同的
浮点和向量寄存器

因为浮点数的存储以及其运算的特殊性,CPU中专门提供浮点数寄存器来处理浮点数

浮点寄存器
64位D0 - D31
32位S0 - S31

现在的CPU支持向量运算(向量运算在图形处理相关的领域用得非常多),为了支持向量计算系统,也提供了众多的向量寄存器

向量寄存器

  • 128位V0 - V31
通用寄存器

通用寄存器也称数据地址寄存器,通常用来做数据计算的临时存储、做累加、计数、地址保存等功能。定义这些寄存器的作用主要是用于在CPU指令中保存操作数,在CPU中当做一些常规变量来使用

  • 当真机调试时,读取的是手机设备的寄存器
  • 列出的是CPU上的数据

ARM64拥有有3264位的通用寄存器x0x30,以及XZR零寄存器),这些通用寄存器有时也有特定用途

  • w0w28这些是32位的,因为64位CPU可以兼容32位,所以可以只使用64位寄存器的低32位
  • 比如w0就是x0低32位

注意:了解过8086汇编的同学知道,有一种特殊的寄存器段寄存器:CSDSSSES四个寄存器来保存这些段的基地址,这个属于Intel架构CPU中,在ARM中并没有

通常,CPU会先将内存中的数据存储到通用寄存器中,然后再对通用寄存器中的数据进行运算

假设内存中有块红色内存空间的值是3,现在想把它的值加1,并将结果存储到蓝色内存空间

  • CPU首先会将红色内存空间的值放到X0寄存器中:mov X0,红色内存空间
  • 然后让X0寄存器与1相加:add X0,1
  • 最后将值赋值给内存空间:mov 蓝色内存空间,X0
pc寄存器(program counter)

为指令指针寄存器,它指示了CPU当前要读取指令的地址

在内存或者磁盘上,指令和数据没有任何区别,都是二进制信息

CPU在工作的时候,把有的信息看做指令,有的信息看做数据,为同样的信息赋予了不同的意义

  • 例如:1110 0000 0000 0011 0000 1000 1010 1010
  • 可以当做数据0xE003008AA
  • 也可以当做指令mov x0, x8

CPU根据什么将内存中的信息看做指令?

  • CPUpc指向的内存单元的内容看做指令
  • 如果内存中的某段内容曾被CPU执行过,那么它所在的内存单元必然被pc指向过

案例:

延用上述Demo案例

test函数上设置断点,查看汇编代码

  • 断点所在位置,是CPU即将执行的指令
  • pc寄存器中,存储的就是CPU即将执行的指令

pc寄存器的值进行修改,写入0x104ec5ea0

断点继续执行,CPU会直接将pc寄存器中存储的0x100b31ea0指令读取出并执行,然后pc寄存器指向下一条即将执行的0x100b31ea4指令

  • 此时0x100b31ea0是当前执行的指令
  • 执行后,断点将断到0x100b31ea4指令
  • pc寄存器存储的也是0x100b31ea0的下一条指令地址:0x100b31ea4
高速缓存

iPhoneX上搭载的ARM处理器A11,它的1级缓存的容量是64KB2级缓存的容量8M

  • CPU每执行一条指令前,都需要从内存中将指令读取到CPU内并执行。而寄存器的运行速度相比内存读写要快很多。为了性能,CPU还集成了一个高速缓存存储区域
  • 当程序在运行时,先将要执行的指令代码以及数据复制到高速缓存中去(由操作系统完成),CPU直接从高速缓存依次读取指令来执行
bl指令
  • CPU从何处执行指令是由pc中的内容决定的,我们可以通过改变pc的内容来控制CPU执行目标指令
  • ARM64提供了一个mov指令(传送指令),可以用来修改大部分寄存器的值
    例如:mov x0,#10、mov x1,#20
  • 但是,mov指令不能用于设置pc的值,ARM64没有提供这样的功能
  • ARM64提供了另外的指令来修改PC的值,这些指令统称为转移指令,最简单的是bl指令

案例:

延用上述Demo案例

创建Empty文件

命名asm.s

创建.s文件,它默认会参与编译

打开asm.s文件,写入以下代码:

.text
.global _A,_B

_A:
   mov x0,#0xa0
   mov x1,#0x00
   add x1, x0, #0x14
   mov x0,x1
   bl _B
   mov x0,#0x0
   ret

_B:
   add x0, x0, #0x10
   ret

打开ViewController.m文件,写入以下代码:

#import "ViewController.h"

@implementation ViewController

int A();

- (void)viewDidLoad {
   [super viewDidLoad];
   A();
}

@end
  • .text:声明为代码段
  • .global:暴露全局函数

viewDidLoad方法调用A函数时设置断点,使用真机运行项目

查看汇编代码,选择Debug->Debug Workflow->Always Show Disassembly

查看寄存器,点击调试区底部Auto,菜单中选择All Variables, Registers, Globals and Statics

汇编代码应该如何查看?一共划分4列:

  • 1列:内存地址
  • 2列:偏移地址,0,4,8,12...,由此可见每条指令占4字节
  • 3列:指令
  • 4列:注释,以;开头

按住control,点击Setp info,进行单步调试。此时进入A函数

  • mov x0, #0xa0:将#0xa0赋值给x0寄存器
  • mov x1, #0x0:将#0x0赋值给x1寄存器
  • add x1, x0, #0x14x0寄存器 + #0x14,赋值给x1寄存器
  • 此时x1寄存器保存的是#0xb4
  • x1寄存器赋值给x0寄存器
  • bl 0x100009f14:通过bl指令跳转到B函数,跳转前lr寄存器保存lb指令的下一条指令地址

通过bl指令进入B函数,x0x1寄存器都为#0xb4

  • add x0, x0, #0x10:将x0寄存器 + #0x10,赋值给x0寄存器
  • 此时x0寄存器保存的是#0xc4
  • ret:通过ret指令返回,跳转到lr寄存器保存的指令地址

通过ret指令回到A函数,x0寄存器为#0xc4x1寄存器为#0xb4

  • mov x0, #0x0:将#0x0赋值给x0寄存器
  • ret:通过ret指令返回,跳转到lr寄存器保存的指令地址

案例中,由于没有对lr寄存器进行保护,继续执行会形成递归。此问题待解决,先看一下x0x1寄存器的值

  • 最终x0寄存器为#0x0x1寄存器为#0xb4
总结

汇编概述:

  • 使用助记符代替机器指令的一种编程语言
  • 汇编和机器指令是一一对应的关系,拿到二进制就可以反汇编
  • 由于汇编和CPU的指令集是对应的,所以汇编不具备移植性

总线:

  • 总线:一堆导线的集合
  • 地址总线的宽度决定了寻址能力
  • 数据总线的宽度决定了CPU数据的吞吐量

进制

  • 任意进制,都是由对应个数的符号组成的,符号可以自定义
  • 2\8\16是相对完美的进制,它们之间的关系:
    32进制位,使用一个8进制标识
    42进制位,使用一个16进制标识
    216进制位,可以标识一个字节
  • 数量单位
    1024 = 1K1024K = 1M1024M = 1G
  • 容量单位
    1024B = 1KB1024KB = 1MB1024MB = 1GB
    B:字节(Byte);1B = 8bit
    bit:比特;一个2进制位
  • 数据宽度
    计算机中的数据是有宽度的,超过了就会溢出

寄存器:

  • 寄存器:CPU为了性能,在内存开屏了一小块临时存储区域
  • 浮点向量寄存器
  • 异常状态寄存器
  • 通用寄存器
    通用寄存器:除了存放数据有时候也有特殊的用途
    ARM64拥有3264位的通用寄存器x0 - x30,以及XZR(零寄存器)
    为了兼容32位,所以ARM64拥有w0 - w28\WZR3032位寄存器
    32位寄存器并不是独立存在的,比如w0x0低32位
  • pc寄存器
    pc寄存器:指令指针寄存器
    pc寄存器里面的值,保存的是CPU接下来需要执行的指令地址
    改变pc的值可以改变程序的执行流程

你可能感兴趣的:(iOS逆向实战--001:初识汇编)