iOS逆向(6)-從fishhook看Runtime,Hook系統函式

一縷清風揚萬里發表於2019-03-24

在上篇文章不知MachO怎敢說自己懂DYLD中已經詳細介紹了MachO,並且由MachO引出了dyld,再由dyld講述了App的啟動流程,而在App的啟動流程中又說到了一些關鍵的名稱如:LC_LOAD_DYLINKERLC_LOAD_DYLIB以及objc的回撥函式_dyld_objc_notify_register等等。並且在末尾提出了MachO中還有一些符號表,而有哪些符號表,這些符號表又有些什麼用呢?筆者在這篇文章就將一一道來。

老規矩,片頭先上福利:點選下載demo,demo中有筆者給fishhook每句程式碼加的詳細註釋!!! 這篇文章會用到的工具有:

在開始正文之前,假設面試官問了一個問題:
都知道Objective-C最大的特性就是runtime,大家可以用使用runtime對OC的方法進行hook,那麼C函式能不能hook?

有興趣回答的朋友可以先行在評論區回答,答完之後再繼續閱讀或者預先偷窺一下文末的答案,看看這被炒了無數次冷飯的runtime自己是否真的瞭然於胸。

本將從以下幾方面回答上面所提的問題:

  • Runtime的Hook原理
  • 為什麼C不能hook
  • 如何利用MachO“玩壞”系統C函式
  • fishhook原始碼分析
  • 繫結系統C函式過程驗證

一、Runtime的Hook原理

Runtime,從名稱上就知道是執行時,也是它造就了OC執行時的特性,而要想徹底明白什麼是執行時,那麼就需要將之與C語言有相比較。
今天我們們就從彙編的角度看一看OC和C在呼叫方法(函式)上有什麼區別。

注:筆者使用的是iPhone 7徵集除錯,所有一下彙編都是基於arm64,所以以下所有彙編預設為基於arm64。

新建一個工程取名為:FishhookDemo
敲入兩個OC方法mylogmylog2,掛上斷點,如圖:

OC方法.png

開啟彙編斷點,如圖:

設定彙編斷點.png

執行工程,會跳轉到如下圖的彙編斷點:

OC彙編斷點.png

從上圖可以看的出來呼叫了兩個objc_msgSend,這兩個很像是 我們的mylogmylog2,但現在還不能確定。
想一想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中存放的到底是什麼,如圖:

mylog1.png

mylog2.png

這也就驗證了我們們OC方法都是訊息轉發(objc_msgSend)。而同一個C函式的地址又都是一樣的(筆者這次執行的地址就是0x1026ce130) 。

所以在每次呼叫OC方法的時候就讓我們有了一次改變訊息轉發「目標」的機會。

這裡稍微提一下runtime的原始碼分析流程:
Step 1、方法查詢
① 彙編快速查詢快取
② C/C++慢速查詢:self->super->NSObject->找到換快取起來
Step 2、動態方法解析: _class_resolveMethod
_class_resolveInstanceMethod
_class_resolveClassMethod
Step 3、訊息轉發
_forwardingTargetForSelector
_methodSignatureForSelector
_forwardInvocation
_doesNotRecognizeSelector

二、為什麼C不能hook

同樣我們從彙編的角度切入。
敲入程式碼一些C函式,掛上斷點,如圖:

C函式.png

執行工程:
會看到斷點斷到如下彙編:

彙編斷點.png

可以看到每個NSLog對應跳轉的地址都是0x10000a010,每個printf對應跳轉的地址都是0x10000a184,也就是說每個C的函式都是一一對應著一個真實的地址空間。每次在呼叫一個C函式的時候都是執行一句彙編bl 0xXXXXXXXX

所以上面講述到的訊息轉發的機會沒有了,也就是沒有了利用runtime來Hook的機會了。

三、如何利用MachO“玩壞”系統C函式

既然如此,那麼是否C函式就真的那麼牢不可破,無法對他進行Hook呢?
答案肯定是否定的!
想要從根上理解這個問題,首先要了解:我們的C函式分為系統C函式和我們自定義的C函式。

1、自定義的C函式

在上面的步驟中我們已經瞭解到所有C函式的呼叫都是跳轉到一個「固定的地址」,那麼就可以推斷得出這個「固定的地址」其實是在編譯期已經被生成好了,所以才能快速、直接的跳轉到這個地址,實現函式呼叫。
C語言被稱之為是靜態語言也就是這麼個理。

2、系統的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函式的地址,那麼我們們就也能。

四、fishhook原始碼分析

1、fishhook的總體思路

Facebook的開源庫fishhook就可以完美的實現這個任務。
先上一張官網原理圖:

fishhook原理圖.png

總體來說,步驟是這樣的:

  • 先找到四張表Lazy Symbol Pointer Table、Indirect Symbol Table、Symbol Table、String Table。
  • MachO有個規律:Lazy Symbol Pointer Table中第index行代表的函式和Indirect Symbol Table中第index行代表的函式是一樣的。
  • Indirect Symbol Table中value值表示Symbol Table的index。
  • 找到Symbol Table的中對應index的物件,其data代表String Table的偏移值。
  • 用String Table的基值,也就是第一行的pFile值,加上Symbol Table的中取到的偏移值,就能得到Indirect Symbol Table中value(這個value代表函式的偏移值)代表的函式名了。

2、驗證NSLog地址

下面就來驗證一下在NSLog的地址是不是真的就存在Indirect Symbol Table中。 同樣在NSLog處下好斷點,開啟彙編斷點,執行程式碼。會發現斷點斷在如下入位置:

NSLog斷點.png
注:筆者的工程重新build了,MachO也重新生成,所以此處的截圖和上文中斷住NSLog的截圖的地址不一樣,這是正常情況。

可以發現NSLog的地址是0x104d36010,先記住這個值。

然後檢視我們APP在記憶體中的偏移值。
利用image list命令列出所有image,第一個image就是我們APP的偏移值,也就是記憶體地址。

APP在記憶體中的偏移值.png

可以看到APP在記憶體中的偏移值為0x104d30000
接著開啟MachOView檢視MachO中的Indirect Symbol Table中的value,如圖:

函式偏移地址.png

其值為0x100006010,去除最高位得到的0x6010就是NSLog在MachO中的偏移值。 最後將NSLog在MachO中的偏移值於APP在記憶體中的偏移值相加就得到NSLog真實的記憶體地址:
0x6010+0x104d30000=0x104d36010

最終證明,在Indirect Symbol Table的value中的值就是其對應的函式的地址!!!

3、根據MachO的表查詢對應的函式名和函式地址

我們們還是用NSLog來距離查詢。

Step1、Indirect Symbol Table

取出其data值0000010A,用10進製表示,結果為266,如圖:

Indirect Symbols Table.png

Step2、Symbol Table

在Symbol Table中找到下標(offset)為266的的物件,取出其data0x124,如圖:

Symbols Table.png

Step3、String Table

將在Symbols中得到的偏移值0x124加上String Table的首個地址DC6C,得到值DD90,然後找到pFile為DD90的值,如下兩圖:

String Table 1.png

String Table 2.png

上述就是根據MachO的表查詢對應的函式名和函式地址全過程了。

4、原始碼分析

fishhook的原始碼總共只有250行左右,所以結合MachO慢慢看,其實一點也不費勁,在筆者的demo中有對其每一句函式的詳細註釋。當然也有對fishhook使用的demo。

所以筆者就不在此處對fishhook做太過詳細的介紹了。只對其中一些關鍵引數和關鍵函式做介紹。

  • fishhook為維護一個連結串列,用來儲存需要hook的所有函式
// 給需要rebinding的方法結構體開闢出對應的空間
// 生成對應的連結串列結構(rebindings_entry),並將新的entry插入頭部
static int prepend_rebindings(struct rebindings_entry **rebindings_head,
                              struct rebinding rebindings[],
                              size_t nel)
複製程式碼
  • 根據linkedit的基值,找到對應的三張表:symbol_table、string_table和indirect_symtab :
// 找到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函式過程驗證

上面說了這麼多,那麼我們們來驗證一下系統C函式是不是真的會這樣被繫結起來,並且看一看,是在什麼時候繫結的。

同樣,在第一次敲入NSLog函式的地方加上斷點,在第二個NSLog處也加上斷點:

兩個NSLog斷點.png

執行工程後,使用dis -s命令檢視該函式的彙編程式碼,並且繼續檢視其中第一次b指令,也就是函式呼叫的彙編,如圖:

第一次NSLog彙編斷點+dis -s.png

從上圖就可以看到,在我們第一次呼叫NSLog的時候,系統確實會預設的呼叫dyld_stub_binder函式對NSLog進行繫結。

繼續跳過這個斷點,進入下一個NSLog的彙編斷點處,同樣利用dis -s命令檢視該彙編:

第二次NSLog彙編斷點+dis -s.png

得到答案:
系統確實會在第一次呼叫系統C函式的時候對其進行繫結!

還記得正文開始的時候的那個問題嗎?
那麼是不是系統C函式可以hook,而自定義的C函式就絕對不能hook了呢?
很顯然,國內外大神那麼多,肯定是能做到的,有興趣的讀者可以自行查閱Cydia Substrate。

這篇文章利用了一些LLDB命令列看了許多我們想看的內容,如image listregister read還有dis -s,在我們正向開發中,LLDB就是一把利器,而在我們玩逆向的時候,LLDB就成為了我們某些是後的唯一途徑了!所以,在下一篇文章中,筆者將會對LLDB進行更加詳細的講解,讓大家看到LLBD的偉大。

相關文章