在上篇文章不知MachO怎敢說本身懂DYLD中已經詳細介紹了MachO,而且由MachO引出了dyld
,再由dyld
講述了App的啓動流程,而在App的啓動流程中又說到了一些關鍵的名稱如:LC_LOAD_DYLINKER
、LC_LOAD_DYLIB
以及objc
的回調函數_dyld_objc_notify_register
等等。而且在末尾提出了MachO中還有一些符號表,而有哪些符號表,這些符號表又有些什麼用呢?筆者在這篇文章就將一一道來。git
老規矩,片頭先上福利:點擊下載demo,demo中有筆者給fishhook每句代碼加的詳細註釋!!! 這篇文章會用到的工具備:github
在開始正文以前,假設面試官問了一個問題:
都知道Objective-C最大的特性就是runtime,你們能夠用使用runtime對OC的方法進行hook,那麼C函數能不能hook?面試
有興趣回答的朋友能夠先行在評論區回答,答完以後再繼續閱讀或者預先偷窺一下文末的答案,看看這被炒了無數次冷飯的runtime本身是否真的瞭然於胸。緩存
本將從如下幾方面回答上面所提的問題:bash
Runtime,從名稱上就知道是運行時,也是它造就了OC運行時的特性,而要想完全明白什麼是運行時,那麼就須要將之與C語言有相比較。
今天我們就從彙編的角度看一看OC和C在調用方法(函數)上有什麼區別。app
注:筆者使用的是iPhone 7徵集調試,全部一下彙編都是基於arm64,因此如下全部彙編默認爲基於arm64。框架
新建一個工程取名爲:FishhookDemo
敲入兩個OC方法mylog
和mylog2
,掛上斷點,如圖:
ide
開啓彙編斷點,如圖:
函數
運行工程,會跳轉到以下圖的彙編斷點: 工具
從上圖能夠看的出來調用了兩個objc_msgSend
,這兩個很像是 咱們的mylog
和mylog2
,但如今還不能肯定。
想想objc_msgSend
的定義:
OBJC_EXPORT void
objc_msgSend(void /* id self, SEL op, ... */ )
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
複製代碼
第一個參數是self
,第二個參數是SEL
,因此能夠知道SEL是放在x1的寄存器裏面(什麼是x1?繼續關注做者,以後的文章會有相關的彙編的專門篇章)。
快馬加鞭,掛上兩個彙編斷點,查看一下兩個x1中存放的究竟是什麼,如圖:
這也就驗證了我們OC方法都是消息轉發(objc_msgSend)。而同一個C函數的地址又都是同樣的(筆者此次運行的地址就是0x1026ce130
) 。
因此在每次調用OC方法的時候就讓咱們有了一次改變消息轉發「目標」的機會。
這裏稍微提一下runtime的源碼分析流程:
Step 一、方法查找
① 彙編快速查找緩存
② C/C++慢速查找:self
->super
->NSObject
->找到換緩存起來
Step 二、動態方法解析: _class_resolveMethod
① _class_resolveInstanceMethod
② _class_resolveClassMethod
Step 三、消息轉發
① _forwardingTargetForSelector
② _methodSignatureForSelector
③ _forwardInvocation
④ _doesNotRecognizeSelector
一樣咱們從彙編的角度切入。
敲入代碼一些C函數,掛上斷點,如圖:
運行工程:
會看到斷點斷到以下彙編:
能夠看到每一個NSLog
對應跳轉的地址都是0x10000a010
,每一個printf
對應跳轉的地址都是0x10000a184
,也就是說每一個C的函數都是一一對應着一個真實的地址空間。每次在調用一個C函數的時候都是執行一句彙編bl 0xXXXXXXXX
。
因此上面講述到的消息轉發的機會沒有了,也就是沒有了利用runtime來Hook的機會了。
既然如此,那麼是否C函數就真的那麼牢不可破,沒法對他進行Hook呢?
答案確定是否認的!
想要從根上理解這個問題,首先要了解:咱們的C函數分爲系統C函數和咱們自定義的C函數。
在上面的步驟中咱們已經瞭解到全部C函數的調用都是跳轉到一個「固定的地址」,那麼就能夠推斷得出這個「固定的地址」實際上是在編譯期已經被生成好了,因此才能快速、直接的跳轉到這個地址,實現函數調用。
C語言被稱之爲是靜態語言也就是這麼個理。
在上篇文章不知MachO怎敢說本身懂DYLD已經提到了在dyld啓動app的第二個步驟就是加載共享緩存庫,共享緩存庫包括Foundation框架,NSLog
是被包含在Foundation框架的。那麼就能夠肯定一件事情,在咱們將本身工程打包出的MachO文件中是不可能預先肯定NSLog
的地址的。
可是又由於C語言是靜態的特性,無法在運行的時候實時獲取共享緩存庫中NSLog
的地址。而共享緩存庫的存在好處太大,既能節省大量內存,又能加快啓動速度提高性能,不能棄之而不用。
爲了解決這個問題,Apple使用了PIC(Position-independent code)技術,在第一次使用對應函數(NSLog
)的時候,從系統內存中將對函數(NSLog
)的內存地址取出,綁定到APP中對應函數(NSLog
)上,就能夠實現正常的C函數(NSLog
)調用了。
既然有這麼個過程,iOS系統能夠動態的綁定系統C函數的地址,那麼我們就也能。
Facebook的開源庫fishhook就能夠完美的實現這個任務。
先上一張官網原理圖:
整體來講,步驟是這樣的:
下面就來驗證一下在NSLog的地址是否是真的就存在Indirect Symbol Table中。 一樣在NSLog處下好斷點,打開彙編斷點,運行代碼。會發現斷點斷在以下入位置:
能夠發現NSLog的地址是0x104d36010
,先記住這個值。
而後查看咱們APP在內存中的偏移值。
利用image list
命令列出全部image,第一個image就是咱們APP的偏移值,也就是內存地址。
能夠看到APP在內存中的偏移值爲0x104d30000
。
接着打開MachOView查看MachO中的Indirect Symbol Table中的value,如圖:
其值爲0x100006010
,去除最高位獲得的0x6010
就是NSLog
在MachO中的偏移值。 最後將NSLog
在MachO中的偏移值於APP在內存中的偏移值相加就獲得NSLog
真實的內存地址:
0x6010
+0x104d30000
=0x104d36010
最終證實,在Indirect Symbol Table的value中的值就是其對應的函數的地址!!!
我們仍是用NSLog
來距離查找。
取出其data值0000010A
,用10進製表示,結果爲266
,如圖:
在Symbol Table中找到下標(offset)爲266的的對象,取出其data0x124
,如圖:
將在Symbols中獲得的偏移值0x124
加上String Table的首個地址DC6C
,獲得值DD90
,而後找到pFile爲DD90
的值,以下兩圖:
上述就是根據MachO的表查找對應的函數名和函數地址全過程了。
fishhook的源碼總共只有250行左右,因此結合MachO慢慢看,其實一點也不費勁,在筆者的demo中有對其每一句函數的詳細註釋。固然也有對fishhook使用的demo。
因此筆者就不在此處對fishhook作太過詳細的介紹了。只對其中一些關鍵參數和關鍵函數作介紹。
// 給須要rebinding的方法結構體開闢出對應的空間
// 生成對應的鏈表結構(rebindings_entry),並將新的entry插入頭部
static int prepend_rebindings(struct rebindings_entry **rebindings_head,
struct rebinding rebindings[],
size_t nel)
複製代碼
// 找到linkedit的頭地址
uintptr_t linkedit_base = (uintptr_t)slide + linkedit_segment->vmaddr - linkedit_segment->fileoff;
// 獲取symbol_table的真實地址
nlist_t *symtab = (nlist_t *)(linkedit_base + symtab_cmd->symoff);
// 獲取string_table的真實地址
char *strtab = (char *)(linkedit_base + symtab_cmd->stroff);
// Get indirect symbol table (array of uint32_t indices into symbol table)
// 獲取indirect_symtab的真實地址
uint32_t *indirect_symtab = (uint32_t *)(linkedit_base + dysymtab_cmd->indirectsymoff);
複製代碼
// 在四張表(section,symtab,strtab,indirect_symtab)中循環查找
// 直到找到對應的rebindings->name,將原先的函數複製給新的地址,將新的函數地址賦值給原先的函數
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)
複製代碼
上面說了這麼多,那麼我們來驗證一下系統C函數是否是真的會這樣被綁定起來,而且看一看,是在何時綁定的。
一樣,在第一次敲入NSLog
函數的地方加上斷點,在第二個NSLog
處也加上斷點:
運行工程後,使用dis -s
命令查看該函數的彙編代碼,而且繼續查看其中第一次b
指令,也就是函數調用的彙編,如圖:
從上圖就能夠看到,在咱們第一次調用NSLog
的時候,系統確實會默認的調用dyld_stub_binder
函數對NSLog
進行綁定。
繼續跳過這個斷點,進入下一個NSLog
的彙編斷點處,一樣利用dis -s
命令查看該彙編:
獲得答案:
系統確實會在第一次調用系統C函數的時候對其進行綁定!
還記得正文開始的時候的那個問題嗎?
那麼是否是系統C函數能夠hook,而自定義的C函數就絕對不能hook了呢?
很顯然,國內外大神那麼多,確定是能作到的,有興趣的讀者能夠自行查閱Cydia Substrate。
這篇文章利用了一些LLDB命令行看了許多咱們想看的內容,如image list
,register read
還有dis -s
,在咱們正向開發中,LLDB就是一把利器,而在咱們玩逆向的時候,LLDB就成爲了咱們某些是後的惟一途徑了!因此,在下一篇文章中,筆者將會對LLDB進行更加詳細的講解,讓你們看到LLBD的偉大。