轉(zhuǎn)載:原文地址
在上篇文章不知MachO怎敢說自己懂DYLD中已經(jīng)詳細(xì)介紹了MachO,并且由MachO引出了dyld
,再由dyld
講述了App的啟動流程,而在App的啟動流程中又說到了一些關(guān)鍵的名稱如:LC_LOAD_DYLINKER
、LC_LOAD_DYLIB
以及objc
的回調(diào)函數(shù)_dyld_objc_notify_register
等等。并且在末尾提出了MachO中還有一些符號表,而有哪些符號表,這些符號表又有些什么用呢?筆者在這篇文章就將一一道來。
老規(guī)矩,片頭先上福利:點(diǎn)擊下載demo,demo中有筆者給fishhook每句代碼加的詳細(xì)注釋!!!
這篇文章會用到的工具有:
在開始正文之前,假設(shè)面試官問了一個問題:
都知道Objective-C最大的特性就是runtime,大家可以用使用runtime對OC的方法進(jìn)行hook,那么C函數(shù)能不能hook?
有興趣回答的朋友可以先行在評論區(qū)回答,答完之后再繼續(xù)閱讀或者預(yù)先偷窺一下文末的答案,看看這被炒了無數(shù)次冷飯的runtime自己是否真的了然于胸。
本將從以下幾方面回答上面所提的問題:
- Runtime的Hook原理
- 為什么C不能hook
- 如何利用MachO“玩壞”系統(tǒng)C函數(shù)
- fishhook源碼分析
- 綁定系統(tǒng)C函數(shù)過程驗(yàn)證
一、Runtime的Hook原理
Runtime,從名稱上就知道是運(yùn)行時,也是它造就了OC運(yùn)行時的特性,而要想徹底明白什么是運(yùn)行時,那么就需要將之與C語言有相比較。
今天咱們就從匯編的角度看一看OC和C在調(diào)用方法(函數(shù))上有什么區(qū)別。
注:筆者使用的是iPhone 7征集調(diào)試,所有一下匯編都是基于arm64,所以以下所有匯編默認(rèn)為基于arm64。
新建一個工程取名為:FishhookDemo
敲入兩個OC方法mylog
和mylog2
,掛上斷點(diǎn),如圖:
開啟匯編斷點(diǎn),如圖:
運(yùn)行工程,會跳轉(zhuǎn)到如下圖的匯編斷點(diǎn):
從上圖可以看的出來調(diào)用了兩個objc_msgSend
,這兩個很像是
我們的mylog
和mylog2
,但現(xiàn)在還不能確定。
想一想objc_msgSend
的定義:
OBJC_EXPORT void
objc_msgSend(void /* id self, SEL op, ... */ )
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
第一個參數(shù)是self
,第二個參數(shù)是SEL
,所以可以知道SEL是放在x1的寄存器里面(什么是x1?繼續(xù)關(guān)注作者,之后的文章會有相關(guān)的匯編的專門篇章)。
馬不停蹄,掛上兩個匯編斷點(diǎn),查看一下兩個x1中存放的到底是什么,如圖:
這也就驗(yàn)證了咱們OC方法都是消息轉(zhuǎn)發(fā)(objc_msgSend)。而同一個C函數(shù)的地址又都是一樣的(筆者這次運(yùn)行的地址就是0x1026ce130
) 。
所以在每次調(diào)用OC方法的時候就讓我們有了一次改變消息轉(zhuǎn)發(fā)「目標(biāo)」的機(jī)會。
這里稍微提一下runtime的源碼分析流程:
Step 1、方法查找
① 匯編快速查找緩存
② C/C++慢速查找:self
->super
->NSObject
->找到換緩存起來
Step 2、動態(tài)方法解析: _class_resolveMethod
① _class_resolveInstanceMethod
② _class_resolveClassMethod
Step 3、消息轉(zhuǎn)發(fā)
① _forwardingTargetForSelector
② _methodSignatureForSelector
③ _forwardInvocation
④ _doesNotRecognizeSelector
二、為什么C不能hook
同樣我們從匯編的角度切入。
敲入代碼一些C函數(shù),掛上斷點(diǎn),如圖:
運(yùn)行工程:
會看到斷點(diǎn)斷到如下匯編:
可以看到每個NSLog
對應(yīng)跳轉(zhuǎn)的地址都是0x10000a010
,每個printf
對應(yīng)跳轉(zhuǎn)的地址都是0x10000a184
,也就是說每個C的函數(shù)都是一一對應(yīng)著一個真實(shí)的地址空間。每次在調(diào)用一個C函數(shù)的時候都是執(zhí)行一句匯編bl 0xXXXXXXXX
。
所以上面講述到的消息轉(zhuǎn)發(fā)的機(jī)會沒有了,也就是沒有了利用runtime來Hook的機(jī)會了。
三、如何利用MachO“玩壞”系統(tǒng)C函數(shù)
既然如此,那么是否C函數(shù)就真的那么牢不可破,無法對他進(jìn)行Hook呢?
答案肯定是否定的!
想要從根上理解這個問題,首先要了解:我們的C函數(shù)分為系統(tǒng)C函數(shù)和我們自定義的C函數(shù)。
1、自定義的C函數(shù)
在上面的步驟中我們已經(jīng)了解到所有C函數(shù)的調(diào)用都是跳轉(zhuǎn)到一個「固定的地址」,那么就可以推斷得出這個「固定的地址」其實(shí)是在編譯期已經(jīng)被生成好了,所以才能快速、直接的跳轉(zhuǎn)到這個地址,實(shí)現(xiàn)函數(shù)調(diào)用。
C語言被稱之為是靜態(tài)語言也就是這么個理。
2、系統(tǒng)的C函數(shù)
在上篇文章不知MachO怎敢說自己懂DYLD已經(jīng)提到了在dyld啟動app的第二個步驟就是加載共享緩存庫,共享緩存庫包括Foundation框架,NSLog
是被包含在Foundation框架的。那么就可以確定一件事情,在我們將自己工程打包出的MachO文件中是不可能預(yù)先確定NSLog
的地址的。
但是又因?yàn)镃語言是靜態(tài)的特性,沒法在運(yùn)行的時候?qū)崟r獲取共享緩存庫中NSLog
的地址。而共享緩存庫的存在好處太大,既能節(jié)省大量內(nèi)存,又能加快啟動速度提升性能,不能棄之而不用。
為了解決這個問題,Apple使用了PIC(Position-independent code)技術(shù),在第一次使用對應(yīng)函數(shù)(NSLog
)的時候,從系統(tǒng)內(nèi)存中將對函數(shù)(NSLog
)的內(nèi)存地址取出,綁定到APP中對應(yīng)函數(shù)(NSLog
)上,就可以實(shí)現(xiàn)正常的C函數(shù)(NSLog
)調(diào)用了。
既然有這么個過程,iOS系統(tǒng)可以動態(tài)的綁定系統(tǒng)C函數(shù)的地址,那么咱們就也能。
四、fishhook源碼分析
1、fishhook的總體思路
Facebook的開源庫fishhook就可以完美的實(shí)現(xiàn)這個任務(wù)。
先上一張官網(wǎng)原理圖:
總體來說,步驟是這樣的:
- 先找到四張表Lazy Symbol Pointer Table、Indirect Symbol Table、Symbol Table、String Table。
- MachO有個規(guī)律:Lazy Symbol Pointer Table中第index行代表的函數(shù)和Indirect Symbol Table中第index行代表的函數(shù)是一樣的。
- Indirect Symbol Table中value值表示Symbol Table的index。
- 找到Symbol Table的中對應(yīng)index的對象,其data代表String Table的偏移值。
- 用String Table的基值,也就是第一行的pFile值,加上Symbol Table的中取到的偏移值,就能得到Indirect Symbol Table中value(這個value代表函數(shù)的偏移值)代表的函數(shù)名了。
2、驗(yàn)證NSLog地址
下面就來驗(yàn)證一下在NSLog的地址是不是真的就存在Indirect Symbol Table中。
同樣在NSLog處下好斷點(diǎn),打開匯編斷點(diǎn),運(yùn)行代碼。會發(fā)現(xiàn)斷點(diǎn)斷在如下入位置:
注:筆者的工程重新build了,MachO也重新生成,所以此處的截圖和上文中斷住NSLog的截圖的地址不一樣,這是正常情況。
可以發(fā)現(xiàn)NSLog的地址是0x104d36010
,先記住這個值。
然后查看我們APP在內(nèi)存中的偏移值。
利用image list
命令列出所有image,第一個image就是我們APP的偏移值,也就是內(nèi)存地址。
可以看到APP在內(nèi)存中的偏移值為0x104d30000
。
接著打開MachOView查看MachO中的Indirect Symbol Table中的value,如圖:
其值為0x100006010
,去除最高位得到的0x6010
就是NSLog
在MachO中的偏移值。
最后將NSLog
在MachO中的偏移值于APP在內(nèi)存中的偏移值相加就得到NSLog
真實(shí)的內(nèi)存地址:
0x6010
+0x104d30000
=0x104d36010
最終證明,在Indirect Symbol Table的value中的值就是其對應(yīng)的函數(shù)的地址!!!
3、根據(jù)MachO的表查找對應(yīng)的函數(shù)名和函數(shù)地址
咱們還是用NSLog
來距離查找。
1、Indirect Symbol Table
取出其data值0000010A
,用10進(jìn)制表示,結(jié)果為266
,如圖:
2、Symbol Table
在Symbol Table中找到下標(biāo)(offset)為266的的對象,取出其data0x124
,如圖:
3、String Table
將在Symbols中得到的偏移值0x124
加上String Table的首個地址DC6C
,得到值DD90
,然后找到pFile為DD90
的值,如下兩圖:
上述就是根據(jù)MachO的表查找對應(yīng)的函數(shù)名和函數(shù)地址全過程了。
4、源碼分析
fishhook的源碼總共只有250行左右,所以結(jié)合MachO慢慢看,其實(shí)一點(diǎn)也不費(fèi)勁,在筆者的demo中有對其每一句函數(shù)的詳細(xì)注釋。當(dāng)然也有對fishhook使用的demo。
所以筆者就不在此處對fishhook做太過詳細(xì)的介紹了。只對其中一些關(guān)鍵參數(shù)和關(guān)鍵函數(shù)做介紹。
- fishhook為維護(hù)一個鏈表,用來儲存需要hook的所有函數(shù)
// 給需要rebinding的方法結(jié)構(gòu)體開辟出對應(yīng)的空間
// 生成對應(yīng)的鏈表結(jié)構(gòu)(rebindings_entry),并將新的entry插入頭部
static int prepend_rebindings(struct rebindings_entry **rebindings_head,
struct rebinding rebindings[],
size_t nel)
- 根據(jù)linkedit的基值,找到對應(yīng)的三張表:symbol_table、string_table和indirect_symtab :
// 找到linkedit的頭地址
uintptr_t linkedit_base = (uintptr_t)slide + linkedit_segment->vmaddr - linkedit_segment->fileoff;
// 獲取symbol_table的真實(shí)地址
nlist_t *symtab = (nlist_t *)(linkedit_base + symtab_cmd->symoff);
// 獲取string_table的真實(shí)地址
char *strtab = (char *)(linkedit_base + symtab_cmd->stroff);
// Get indirect symbol table (array of uint32_t indices into symbol table)
// 獲取indirect_symtab的真實(shí)地址
uint32_t *indirect_symtab = (uint32_t *)(linkedit_base + dysymtab_cmd->indirectsymoff);
- 最核心的一個步驟,查找并且替換目標(biāo)函數(shù):
// 在四張表(section,symtab,strtab,indirect_symtab)中循環(huán)查找
// 直到找到對應(yīng)的rebindings->name,將原先的函數(shù)復(fù)制給新的地址,將新的函數(shù)地址賦值給原先的函數(shù)
static void perform_rebinding_with_section(struct rebindings_entry *rebindings,
section_t *section,
intptr_t slide,
nlist_t *symtab,
char *strtab,
uint32_t *indirect_symtab)
五、綁定系統(tǒng)C函數(shù)過程驗(yàn)證
上面說了這么多,那么咱們來驗(yàn)證一下系統(tǒng)C函數(shù)是不是真的會這樣被綁定起來,并且看一看,是在什么時候綁定的。
同樣,在第一次敲入NSLog
函數(shù)的地方加上斷點(diǎn),在第二個NSLog
處也加上斷點(diǎn):
運(yùn)行工程后,使用dis -s
命令查看該函數(shù)的匯編代碼,并且繼續(xù)查看其中第一次b
指令,也就是函數(shù)調(diào)用的匯編,如圖:
從上圖就可以看到,在我們第一次調(diào)用NSLog
的時候,系統(tǒng)確實(shí)會默認(rèn)的調(diào)用dyld_stub_binder
函數(shù)對NSLog
進(jìn)行綁定。
繼續(xù)跳過這個斷點(diǎn),進(jìn)入下一個NSLog
的匯編斷點(diǎn)處,同樣利用dis -s
命令查看該匯編:
得到答案:
系統(tǒng)確實(shí)會在第一次調(diào)用系統(tǒng)C函數(shù)的時候?qū)ζ溥M(jìn)行綁定!
還記得正文開始的時候的那個問題嗎?
那么是不是系統(tǒng)C函數(shù)可以hook,而自定義的C函數(shù)就絕對不能hook了呢?
很顯然,國內(nèi)外大神那么多,肯定是能做到的,有興趣的讀者可以自行查閱Cydia Substrate。
這篇文章利用了一些LLDB命令行看了許多我們想看的內(nèi)容,如image list
,register read
還有dis -s
,在我們正向開發(fā)中,LLDB就是一把利器,而在我們玩逆向的時候,LLDB就成為了我們某些是后的唯一途徑了!所以,在下一篇文章中,筆者將會對LLDB進(jìn)行更加詳細(xì)的講解,讓大家看到LLBD的偉大。
轉(zhuǎn)載:原文地址
經(jīng)原作者同意:想交流iOS開發(fā),逆向等技術(shù),可加iOS技術(shù)交流群:624212887,進(jìn)行交流!