OC的反汇编
案例1:
OC
的方法调用打开
Person.h
,写入以下代码:```
import
@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`方法<br />

> - `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`方法<br />

> - 调用`objc_alloc_init`函数
> 从`iOS12.2`开始,系统优化`alloc`和`init`函数,不再使用消息发送(`objc_msgSend`),直接调用优化后的`objc_alloc_init`函数
>
---
> 使用`iOS12.1`系统调试<br />

> - 分别调用`objc_alloc`函数和`objc_msgSend`函数
> 调用`objc_msgSend`函数,实际上就是在调用`init`函数<br />

> - 调用`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`函数<br />

> - 在`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`函数<br />

> - `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`<br />

>
---
> 找到`person`等`selector`,在`Mach-O`中的位置
>
000000010000635c adrp x8, #0x10000c000 0000000100006360 add x8, x8, #0xd40 ; @selector(person)
> - 使用`adrp + add`指令,计算地址为`0x10000cd40`,赋值`x8`
> 打开`Mach-O`,找到`0x10000cd40`<br />

>
---
> 找到`person`等字符串,在`Mach-O`中的位置<br />

> 打开`Mach-O`,找到`0x100006a2c`,存储了方法名称的字符串<br />

>
---
> 找到`imp___stubs__objc_msgSend`在`Mach-O`中的位置
>
0000000100006368 bl imp_stubsobjc_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`函数<br />

>
---
> 找到`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`的相关信息<br />

> 案例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`函数<br />

> 在`Hopper`中,点击`CFG mode`按钮<br />

> 切换到`viewDidLoad`函数的流程图,清晰的展示出不同的代码分支<br />

> 双击`test1`函数,切换到`test1`函数中的代码分支<br />

> 还原高级代码时,遇到复杂的逻辑分支,可以使用代码的流程图进行分析
> 案例4:
> 使用`Hopper`查看伪码模式
> 在`Hopper`中,点击`Pseudo-code mode`按钮<br />

> 查看`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`方法<br />

> 使用`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`,查看内存中的数据<br />

> - `16字节`之后,即:`invoke`,地址和上面打印的`0x1029d6384`一致
>
---
> 在`Hopper`中,找到`Block`的`invoke`
> 使用真机编译项目,将`Mach-O`文件拖到`Hopper`中,找到`viewDidLoad`函数<br />

> - `Hopper`直接标记出`___block_literal_global`
> 双击`___block_literal_global`,找到`Block`的结构体对象<br />

> 双击`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_stubsNSLog 000000010000639c add x0, x0, #0x48 ; @”block” 00000001000063a0 bl imp_stubsNSLog 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官方文档