OC的反汇编
案例1:
OC
的方法调用打开
Person.h
,写入以下代码:#import <Foundation/Foundation.h> @interface Person : NSObject @property(nonatomic,copy)NSString *name; @property(nonatomic,assign)int age; +(instancetype)person; @end
打开
Person.m
,写入以下代码:#import "Person.h" @implementation Person +(instancetype)person{ return [[Person alloc] init]; } @end
打开
ViewController.m
,写入以下代码:#import "ViewController.h" #import "Person.h" @implementation ViewController - (void)viewDidLoad { // [super viewDidLoad]; Person *p = [Person person]; } @end
真机运行项目,来到
viewDidLoad
方法
objc_msgSend
函数有两个默认参数,id
类型的self
和SEL
类型的_cmd
- 两个参数分别对应
x0
和x1
查看
x0
寄存器0x1046de46c <+20>: adrp x8, 2 0x1046de470 <+24>: add x8, x8, #0xec8 ; =0xec8 0x1046de474 <+28>: ldr x0, [x8]
- 使用
adrp + add
指令,计算地址为0x1046e0ec8
,赋值x8
- 对
x8
进行寻址,写入x0
x0
对应id
类型参数,id
类型本质上是结构体指针,占8字节
查看内存中的数据
x 0x1046e0ec8 ------------------------- 0x1046e0ec8: 38 0f 6e 04 01 00 00 00 b0 0f 6e 04 01 00 00 00 8.n.......n..... 0x1046e0ed8: 08 00 00 00 10 00 00 00 10 00 00 00 00 00 00 00 ................
- 读取
8字节
数据,小端模式,从右往左读取0x01046e0f38
打印
x0
po 0x01046e0f38 ------------------------- Person
- 打印的
Person
是类对象
查看
x1
寄存器0x1046de478 <+32>: adrp x8, 2 0x1046de47c <+36>: add x8, x8, #0xeb0 ; =0xeb0 0x1046de480 <+40>: ldr x1, [x8]
- 使用
adrp + add
指令,计算地址为0x1046e0eb0
,赋值x8
- 对
x8
进行寻址,写入x1
x1
对应SEL
类型参数,占8字节
查看内存中的数据
x 0x1046e0eb0 ------------------------- 0x1046e0eb0: 10 c8 88 e2 01 00 00 00 18 f6 15 e3 01 00 00 00 ................ 0x1046e0ec0: 98 88 fd e2 01 00 00 00 38 0f 6e 04 01 00 00 00 ........8.n.....
- 读取
8字节
数据,小端模式,从右往左读取0x01e288c810
打印
x1
po (SEL)0x01e288c810 ------------------------- "person"
- 打印的
"person"
是方法名称
当汇编代码中出现
objc_msgSend
,表示调用了一个OC
方法,读取x0
和x1
便可知道调用了哪个对象的哪个方法
案例2:
alloc
和init
函数使用
iOS12.2
系统调试,来到Person
对象的person
方法
- 调用
objc_alloc_init
函数从
iOS12.2
开始,系统优化alloc
和init
函数,不再使用消息发送(objc_msgSend
),直接调用优化后的objc_alloc_init
函数
使用
iOS12.1
系统调试
- 分别调用
objc_alloc
函数和objc_msgSend
函数调用
objc_msgSend
函数,实际上就是在调用init
函数
- 调用
objc_msgSend
函数,但这里没有对x0
进行赋值。这说明在上一个调用函数objc_alloc
中返回的x0
,正是objc_msgSend
函数将要使用的参数从
iOS10.0
开始,系统优化alloc
函数,直接调用优化后的objc_alloc
函数。但对于init
函数,依然使用objc_msgSend
进行消息发送
使用
iOS9.1
系统调试对
alloc
和init
函数,调用两次objc_msgSend
进行消息发送
案例3:
objc_storeStrong
函数当
Person
初始化完毕,x0
返回一个实例对象。之后会调用objc_storeStrong
函数
- 在
OC
中,使用strong
修饰的对象,都会调用此函数- 在
viewDidLoad
方法中,定义的局部变量p
,就是一个强引用- 调用
objc_storeStrong
函数,并不一定会让对象的引用计数+1
,也可能调用后直接销毁。例如:局部变量p
,它没有被外部代码使用,出栈后就会销毁
来到
objc
源码打开
NSObject.mm
文件,找到objc_storeStrong
函数的实现void objc_storeStrong(id *location, id obj) { id prev = *location; if (obj == prev) { return; } objc_retain(obj); *location = obj; objc_release(prev); }
- 参数
location
,二级指针,指向对象指针的指针- 参数
obj
,对象指针- 局部变量
prev
,将location
指向的对象指针赋值给prev
- 判断对象相等,直接
return
- 如果不等,当前对象
obj
引用计数+1
location
指向obj
location
指向的老对象释放
objc_storeStrong
函数的目的,对一个strong
修饰的对象retain
,对老对象release
,等价于以下代码:Person *p = p1; p = p2;
- 当
p2
赋值给p
,p2
引用计数+1
,p1
释放
回到汇编代码
查看
location
参数0x10058a3ac <+56>: add x8, sp, #0x8 ; =0x8 0x10058a3b0 <+60>: str x0, [sp, #0x8] 0x10058a3b4 <+64>: mov x0, x8
- 将
sp + #0x8
地址写入x8
- 将
x0
,即:局部变量p
,入栈到sp + #0x8
- 将
x8
写入x0
,此时x0
是指向局部变量p
的指针地址查看
obj
参数0x10058a3b8 <+68>: mov x8, #0x0 0x10058a3bc <+72>: mov x1, x8
- 将
#0x0
,即:nil
,写入x8
,- 将
x8
写入x1
,此时x1
为nil
此刻调用
objc_storeStrong
函数触发的逻辑:
prev
被赋值为局部变量p
- 判断对象不相等
- 调用
objc_retain
函数,传入nil
location
指向nil
- 调用
objc_release
函数,传入局部变量p
,将其释放
工具反汇编
真实场景下,对
Mach-O
进行分析,只能使用静态分析,无法使用lldb
动态调试。所以要学会运用工具进行反汇编
案例1:
打开
ViewController.m
,写入以下代码:#import "ViewController.h" #import "Person.h" @implementation ViewController - (void)viewDidLoad { // [super viewDidLoad]; Person *p = [Person person]; p.name = @"Zang"; p.age = 18; } @end
使用真机编译项目,将
Mach-O
文件拖到Hopper
中,找到viewDidLoad
函数
Hopper
可以解析出方法、属性等名称,作为注释标记给开发者- 对于
bl
指令,也将跳转地址解析为对应的函数名称
案例2:
在
Mach-O
中,找到Hopper
转义出的注释找到
objc_cls_ref_Person
在Mach-O
中的位置0000000100006350 adrp x8, #0x10000c000 0000000100006354 add x8, x8, #0xd58 ; >objc_cls_ref_Person
- 使用
adrp + add
指令,计算地址为0x10000cd58
,赋值x8
打开
Mach-O
,找到0x10000cd58
找到
person
等selector
,在Mach-O
中的位置000000010000635c adrp x8, #0x10000c000 0000000100006360 add x8, x8, #0xd40 ; @selector(person)
- 使用
adrp + add
指令,计算地址为0x10000cd40
,赋值x8
打开
Mach-O
,找到0x10000cd40
找到
person
等字符串,在Mach-O
中的位置
打开
Mach-O
,找到0x100006a2c
,存储了方法名称的字符串
找到
imp___stubs__objc_msgSend
在Mach-O
中的位置0000000100006368 bl imp___stubs__objc_msgSend
在
Hopper
的viewDidLoad
方法中,双击imp___stubs__objc_msgSend
imp___stubs__objc_msgSend: 000000010000685c nop ; CODE XREF=-[ViewController viewDidLoad]+44, -[ViewController viewDidLoad]+92, -[ViewController viewDidLoad]+116 0000000100006860 ldr x16, #0x10000c028 0000000100006864 br x16 ; endp
打开
Mach-O
,找到0x10000685c
,存储的正是_objc_msgSend
函数
找到
Zang
字符串,在Mach-O
中的位置000000010000638c adrp x2, #0x100008000 0000000100006390 add x2, x2, #0x8 ; @"Zang"
- 使用
adrp + add
指令,计算地址为0x100008008
,赋值x8
在
Hopper
的viewDidLoad
方法中,双击Zang
; Section __cfstring ; Range: [0x100008008; 0x100008028[ (32 bytes) ; File offset : [32776; 32808[ (32 bytes) ; S_REGULAR cfstring_Zang: 0000000100008008 dq ___CFConstantStringClassReference, 0x7c8, 0x10000755d, 0x4 ; "Zang", DATA XREF=-[ViewController viewDidLoad]+84
打开
Mach-O
,找到0x100008008
,存储字符串常量Zang
的相关信息
案例3:
使用
Hopper
查看代码的流程图打开
ViewController.m
,写入以下代码:#import "ViewController.h" @implementation ViewController void test1(bool b){ if(b){ test2(b); } else{ test3(); } } void test2(bool b){ if(b){ test3(); } else{ test4(); } } void test3(){ NSLog(@"test3"); } void test4(){ NSLog(@"test4"); } - (void)viewDidLoad { // [super viewDidLoad]; int a = 1; int b = 2; if(a == b){ test1(YES); } else{ test2(NO); } } @end
使用真机编译项目,将
Mach-O
文件拖到Hopper
中,找到viewDidLoad
函数
在
Hopper
中,点击CFG mode
按钮
切换到
viewDidLoad
函数的流程图,清晰的展示出不同的代码分支
双击
test1
函数,切换到test1
函数中的代码分支
还原高级代码时,遇到复杂的逻辑分支,可以使用代码的流程图进行分析
案例4:
使用
Hopper
查看伪码模式在
Hopper
中,点击Pseudo-code mode
按钮
查看
Hopper
帮我们还原的viewDidLoad
函数的伪码int -[ViewController viewDidLoad]() { r31 = r31 - 0x30; *(r31 + 0x20) = r29; *(r31 + 0x28) = r30; *(r31 + 0x18) = r0; *(r31 + 0x10) = r1; *(r31 + 0xc) = zero_extend_64(0x1); *(r31 + 0x8) = zero_extend_64(0x2); if (*(r31 + 0xc) == *(r31 + 0x8)) { r0 = _test1(zero_extend_64(0x1) & 0x1); } else { r0 = _test2(zero_extend_64(0x0) & 0x1); } return r0; }
Block的反汇编
日常开发中,
Block
有时作为参数传递,有时作为返回值,但大多数情况下,Block
会作为方法的参数回调在逆向分析中,需要定位
Block
的invoke
。只有找到invoke
,才能找到回调方法中的代码逻辑
案例1:
查看
GlobalBlock
的汇编代码打开
ViewController.m
,写入以下代码:#import "ViewController.h" @implementation ViewController - (void)viewDidLoad { // [super viewDidLoad]; void(^block)(void) = ^() { NSLog(@"block"); }; block(); } @end
真机运行项目,来到
viewDidLoad
方法
使用
register read x0
命令,查看x0
寄存器x0 = 0x00000001029d8028 002--OC`__block_literal_global
x0
存储的是一个GlobalBlock
- 在
Block
内部不使⽤外部变量,或者只使⽤静态变量和全局变量,会形成一个GlobalBlock
,它位于全局区Block
使用
po 0x00000001029d8028
命令,打印内存中的数据<__NSGlobalBlock__: 0x1029d8028> signature: "v8@?0" invoke : 0x1029d6384 (/private/var/containers/Bundle/Application/2B6AC788-34A7-46BB-A44B-2237AFD48A2A/002--OC.app/002--OC`__29-[ViewController viewDidLoad]_block_invoke)
NSGlobalBlock
是Block
的isa
invoke : 0x1029d6384
是代码实现的所在位置
来到
libclosure
源码打开
Block_private.h
文件,找到Block
的定义struct Block_layout { void *isa; volatile int32_t flags; int32_t reserved; BlockInvokeFunction invoke; struct Block_descriptor_1 *descriptor; };
Block
是一个结构体isa
占8字节
,flags
和reserved
共占8字节
16字节
之后,就是invoke
,最后是descriptor
描述
回到汇编代码
使用
x/8g 0x1029d8028
,查看内存中的数据
16字节
之后,即:invoke
,地址和上面打印的0x1029d6384
一致
在
Hopper
中,找到Block
的invoke
使用真机编译项目,将
Mach-O
文件拖到Hopper
中,找到viewDidLoad
函数
Hopper
直接标记出___block_literal_global
双击
___block_literal_global
,找到Block
的结构体对象
双击
invoke
,找到invoke
中的代码逻辑___29-[ViewController viewDidLoad]_block_invoke: 0000000100006384 sub sp, sp, #0x20 ; Objective C Block defined at 0x100008028, DATA XREF=0x100008038 0000000100006388 stp x29, x30, [sp, #0x10] 000000010000638c add x29, sp, #0x10 0000000100006390 str x0, [sp, #0x8] 0000000100006394 str x0, sp 0000000100006398 adrp x0, #0x100008000 ; argument #1 for method imp___stubs__NSLog 000000010000639c add x0, x0, #0x48 ; @"block" 00000001000063a0 bl imp___stubs__NSLog 00000001000063a4 ldp x29, x30, [sp, #0x10] 00000001000063a8 add sp, sp, #0x20 00000001000063ac ret ; endp
案例2:
查看
StackBlock
的汇编代码打开
ViewController.m
,写入以下代码:- (void)viewDidLoad { // [super viewDidLoad]; int a = 1; void(^block)(void) = ^() { NSLog(@"block:%i", a); }; block(); }
真机运行项目,来到
viewDidLoad
方法
标记的
第1段
代码0x1021e62dc <+28>: add x9, sp, #0x8 ; =0x8 0x1021e62e0 <+32>: adrp x10, 2 0x1021e62e4 <+36>: ldr x10, [x10] 0x1021e62e8 <+40>: str x10, [sp, #0x8]
sp + #0x8
的地址,赋值给x9
adrp
指令,计算地址为0x1021e8000
,赋值给x10
- 对
x10
寻址,将值再写入x10
- 将
x10
入栈sp + #0x8
的位置,此时x9
成为指针,指向x10
使用
x/8g 0x1021e8000
,查看内存中的数据0x1021e8000: 0x00000001f0a50830 0x000000019b84ef94 0x1021e8010: 0x0000000000000000 0x0000000000000024 0x1021e8020: 0x00000001021e6a26 0x00000001021e6ad2 0x1021e8030: 0x00000001f101a280 0x00000000000007d0
使用
po 0x00000001f0a50830
命令,打印第一个8字节
数据__NSStackBlock__
- 存储的是一个
StackBlock
- 在内部使⽤局部变量或者
OC
属性,但是不能赋值给强引⽤或者
Copy
修饰的变量,它位于栈区Block
标记的
第2段
代码0x1021e62f8 <+56>: adrp x10, 0 0x1021e62fc <+60>: add x10, x10, #0x358 ; =0x358
- 使用
adrp + add
指令,计算地址为0x1021e6358
,赋值x10
- 此处疑似是
Block
的invoke
使用
dis -s 0x1021e6358
指令,查看invoke
的汇编代码002--OC方法的本质`__29-[ViewController viewDidLoad]_block_invoke: 0x1021e6358 <+0>: sub sp, sp, #0x30 ; =0x30 0x1021e635c <+4>: stp x29, x30, [sp, #0x20] 0x1021e6360 <+8>: add x29, sp, #0x20 ; =0x20 0x1021e6364 <+12>: stur x0, [x29, #-0x8] 0x1021e6368 <+16>: str x0, [sp, #0x10] 0x1021e636c <+20>: ldr w8, [x0, #0x20] 0x1021e6370 <+24>: mov x0, x8 0x1021e6374 <+28>: adrp x9, 2
在
Hopper
中,查看StackBlock
使用真机编译项目,将
Mach-O
文件拖到Hopper
中,找到viewDidLoad
函数
Hopper
直接标记出invoke
的位置- 在
invoke
下面是descriptor
双击
_block_invoke
,找到invoke
中的代码逻辑___29-[ViewController viewDidLoad]_block_invoke: 0000000100006358 sub sp, sp, #0x30 ; DATA XREF=-[ViewController viewDidLoad]+60 000000010000635c stp x29, x30, [sp, #0x20] 0000000100006360 add x29, sp, #0x20 0000000100006364 stur x0, [x29, #-0x8] 0000000100006368 str x0, [sp, #0x10] 000000010000636c ldr w8, [x0, #0x20] 0000000100006370 mov x0, x8 0000000100006374 adrp x9, #0x100008000 0000000100006378 add x9, x9, #0x30 ; cfstring_b 000000010000637c str x0, [sp, #0x8] 0000000100006380 mov x0, x9 0000000100006384 mov x9, sp 0000000100006388 ldr x10, [sp, #0x8] 000000010000638c str x10, x9 0000000100006390 bl imp___stubs__NSLog 0000000100006394 ldp x29, x30, [sp, #0x20] 0000000100006398 add sp, sp, #0x30 000000010000639c ret ; endp
通过汇编代码和
invoke
的位置来看,StackBlock
和案例1
中的GlobalBlock
区别很大
总结
OC
的反汇编
- 出现
objc_msgSend
,表示调用了一个OC
方法,读取x0
和x1
便可知道调用了哪个对象的哪个方法- 初始化的
alloc
和init
函数,在iOS12.2
及更高版本,优化为objc_alloc_init
函数objc_storeStrong
函数,对一个strong
修饰的对象retain
,对老对象release
。有些情况下,也会直接释放对象工具反汇编
- 运用
Hopper
工具,查看汇编代码、流程图、伪码模式Hopper
可以解析出方法、属性等名称,作为注释标记给开发者,本质上也是通过Mach-O
读取相应数据
Block
的反汇编
Block
的类型:全局区、堆区、栈区。重点掌握全局区和栈区Block
的分析- 在逆向分析中,需要定位
Block
的invoke
。只有找到invoke
,才能找到回调方法中的代码逻辑了解更多汇编指令
- 参考文档:ARM官方文档