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官方文檔