1. 程式人生 > >Mach-O簡介及實際應用

Mach-O簡介及實際應用

一、前言

    在正題開始之前,我們先來聊聊iOS中的hook技術。一談到hook,很多人首先想到的是runtime,runtime確實強大,但是它存在很多侷限性:

1)、侵入性:一旦hook了某個類的方法,那麼只能這個類的所有物件的方法都會被hook。

2)、語言上的侷限性:runtime 的hook 只能作用於OC方法。

    開源框架Aspects很巧妙的解決了第一個問題,Aspects通過動態建立子類的方式將對當前類的hook轉換為對當前類動態生成的子類的hook,以此避免對當前類其他物件的程式碼侵入,這與KVO的實現思路是一致的。而fishhook能從一定程度上輔助runtime解決hook對語言侷限性的問題。

二、淺談fishhook

    fishhook是Facebook開源的一個C語言的hook工具,我們可以使用fishhook來hook動態連結的C函式。為什麼在這裡要強調動態連結呢?因為fishhook只能hook iOS系統的C函式,你自己編寫的C函式是無法hook的。

    fishhook使用起來很簡單,在這裡就不談了,先來簡單介紹下fishhook的實現原理。由於動態庫並不參與前期的靜態編譯連結,所以在程式的可執行檔案中,程式碼段並不包含動態庫相關函式的彙編後的指令。那麼系統是如何根據函式的呼叫符號找到真實的函式地址呢?在Mach-O檔案中存在符號表和動態符號表以及字串表,字串表中儲存了所有的字元資訊,比如程式碼int a = 100;這個變數a的名字即存在字串表中。符號表則儲存了所有符號位於字串表中的位置資訊,動態符號表儲存了動態庫符號位於符號表中的偏移資訊。動態庫的section中的reserved1儲存了該section的偏移量X,動態符號表偏移X後即是該section的符號表索引陣列Indices的首地址,以Indices陣列中的值為索引,可以在符號表中獲取到當前的符號在字串表中的偏移,從而獲取到符號字串。通過該section的addr欄位可以獲取到該section的符號繫結表,表中記錄著動態符號如:printf所對應的函式地址,修改符號繫結表的內容為指定函式地址即實現了hook。fishhook看起來非常的繞,這是由於動態連結存在複雜的索引關係,在這裡就不過多介紹了,有興趣的可以搜尋下有關fishhook的博文,優秀博文非常多。

    fishhook很厲害,但是在剛接觸時我有兩個疑問:1、fishhook能hook C++函式嗎?在我的前篇文章中也提出了hook C++函式的問題,但是在留言中貌似沒有得到有效的答案。2、fishhook 為什麼不能hook自己寫的C函式呢?下面我們來一一解答這兩個問題。

三、C++的符號修飾

在程式設計師還是使用紙帶寫程式碼的時候,人們約定在指定的某幾位代表指令,不同的0、1組合代表不同的指令。如:01000000中,0100代表跳轉指令,後面的0000代表目標地址。由於彙編的出現,0100被用jump來代替,這就是最早的符號,符號表能映根據符號對映到一個指令。C語言也是與此類似,實際上我們也是通過一個符號來代表一個函式的地址,但是隨著程式的不斷變大,符號衝突的概率逐漸增加。一個程式設計師在一個.c檔案實現了hello函式,可能另一個程式設計師在另一個.c檔案中也實現了一個同名的hello函式,在這兩個檔案進行編譯和彙編後,會在各自的目標檔案中形成同名的強符號,導致最終連結時報錯。這是由於在C語言中,函式和初始化後的全域性變數預設都是強符號,如果你想改為弱符號,那麼可以使用__attribute__((weak))修飾。在這裡提一下最近58客戶端發現的一個有意思的事情。在iOS 8.11.1版本以後,我們發現buggly上崩潰日誌都會攜帶一個來自RN的函式呼叫棧RCTFBQuickPerformanceLoggerConfigureHooks,在RN中它的宣告如下,

但是在原始碼中,這個函式沒有任何實現,完全是一個空函式。看名字這個函式是hook使用的,那麼它是怎麼實現hook的呢?將RCT__EXTERN 展開後為__attribute__((visibility("default"))),其作用為將RCTFBQuickPerformanceLoggerConfigureHooks向外界暴露,如果外界存在同名函式,那麼RCTFBQuickPerformanceLoggerConfigureHooks會報符號衝突的錯誤。那麼如何做到即能暴露符號,又不造成符號衝突呢?這就利用了__attribute__((weak)),將RCTFBQuickPerformanceLoggerConfigureHooks生命為弱符號,當外界有同名函式時,SDK內部呼叫外屆的函式,否則呼叫內部空函式。

    為了防止出現函式名衝突,在UNIX的C環境下,所有的函式會被加上”_”字首,也就是說void hello ( ),符號實際上為”_hello”,這種機制能夠避免與系統函式的衝突。C++為了解決符號衝突的問題,表現的更為徹底。與C相比,C++有名稱空間的限制,可以極大地避免函式的衝突,除了名稱空間外,C++還存在構造和解構函式,函式過載等特徵。這就導致C++的函式符號要比C函式更復雜。同樣的一個函式,在C和C++中,函式符號是完全不一樣的。假設有函式

在C環境下,它的符號為”_cleanup”,而在C++環境下它的符號為“__Z7cleanupPv”,這就表明,同樣一個函式在C和C++中,修飾機制是不一樣的。為了避免由於符號不同導致的問題,很多開原始碼會加上extern "C” {}來限定函式在C環境。但是在C環境中並不識別extern "C”標識,因此你會看到很多的開原始碼中存在以下程式碼

其意圖在於如果在C++環境中則限定為C環境。那麼究竟C++的修飾機制是怎樣的呢?我們看到一個C++函式,如何推斷出它的符號呢?很遺憾,我沒有找到明確的關於C++函式符號修飾的介紹,不同的編譯器不同的平臺簽名有所不同。不過沒有關係,辦法還是有的,假設我想知道JavaScriptCore中某個C++函式的符號,那麼我們可以建立一個cpp檔案,將C++函式名複製過去,

宣告名稱空間和列舉 建立函式

然後通過gcc -c將檔案編譯成目標檔案WBIMC++.o,然後呼叫命令nm WBIMC++.o即可檢視相應的符號。

能獲取到C++的符號,是不是也就意味著hook C++函式是可行的。我們在fishhook的符號中隨便傳入一個JavaScriptCore的C++函式符號”_ZNK3JSC11SlotVisitor18containsOpaqueRootEPv“,通過程式碼斷點除錯發現,fishhook能夠正確獲取和替換函式指標

因此hook C++是可行的。

四、Mach-O檔案簡介

    在接觸Mach-O之前,我有兩個疑問,第一個是之前提出的問題,fishhook為什麼不能hook自己寫的C函式。第二個問題是跟58正在做的技術專案相關,如何動態呼叫static 函式。弄清楚這兩個問題必須要對Mach-O有較為透徹的瞭解。

    什麼是Mach-O,按我的理解就是遵循特定結構的檔案。一般比較常見的檔案有:應用程式、目標檔案、動態庫、連結器等,其中應用程式、目標檔案.o是尤為重要的。Mach-O可以分為三個部分:

1)、Header

Header是檔案的頭部資訊,包括CPU資訊、檔案型別、Command條數及Size資訊。總體來說,作為開發者Header使用的較少,比較常用的是(uintptr_t)&_mh_execute_header獲取header地址進行計算用。

Header

2)、Commands

Commands描述的是檔案的載入資訊,載入資訊有很多,載入的段、符號表、動態庫資訊等都在Commands中取到。這個部分資訊還是比較有用的,我們可以從這裡獲取到符號表和字串表的偏移量,下文中會有詳細的解釋。

Commands

首先來說下段(Segment),上圖中可以看出共載入了4個段,__PAGEZERO是一個空段,它位於檔案起始段的位置。__TEXT和__DATA分別是文字段和資料段,分別儲存了程式碼資訊和資料資訊。__LINKEDIT是連結資訊段,可以通過__LINKEDIT進行地址計算。段又可以細分為section,每個Segment可以包含多個section。

段展開

3)、資料區

    除了Header和Commands外所有的原始資料。Commands是對資料的彙總提示,而資料區則是真實的資料。Commands與資料區的關係就像size和char*的關係。

資料展示

接下來先介紹幾個比較重要的模組:

1)、(__TEXT,__text)

這裡存放的是彙編後的程式碼,當我們進行編譯時,每個.m檔案會經過預編譯->編譯->彙編形成.o檔案,稱之為目標檔案。彙編後,所有的程式碼會形成彙編指令儲存在.o檔案的(__TEXT,__text)區((__DATA,__data)也是類似)。連結後,所有的.o檔案會合併成一個檔案,所有.o檔案的(__TEXT,__text)資料都會按連結順序存放到應用檔案的(__TEXT,__text)中。

(__TEXT,__text)

2)、(__DATA,__data)

儲存資料的section,static在進行非零賦值後會儲存在這裡,如果static 變數沒有賦值或者賦值為0,那麼它會儲存在(__DATA,__bss)中。

(__DATA,__data)

3)、Symbol Table

符號表,這個是重點中的重點,符號表是將地址和符號聯絡起來的橋樑。符號表並不能直接儲存符號,而是儲存符號位於字串表的位置。

Symbol Table

4)、String Table

字串表所有的變數名、函式名等,都以字串的形式儲存在字串表中。

String Table

5)、動態符號表

動態符號表儲存的是動態庫函式位於符號表的偏移資訊。(__DATA,__la_symbol_ptr) section 可以從動態符號表中獲取到該section位於符號表的索引陣列。

動態符號表

動態符號表並不儲存符號資訊,而是儲存其位於符號表的偏移資訊。Fishhook原始碼看起來比較複雜主要是因為hook的是動態連結的函式,索引和連結關係比較繞。但是我們自己編寫的C函式不是動態連結的,而是在編譯連結後代碼指令就儲存在檔案內部的函式,因此不會用到動態符號表。接下來我們以static 函式為例,看看如何動態的查詢自己編寫的函式地址。

五、動態呼叫static函式

在58iOS客戶端中大量存在static函式,這些static函式該如何動態呼叫呢?能否通過指令碼來呼叫static函式呢?在調研Mach-O之前,我們是一愁莫展,嘗試使用dlsym函式獲取靜態函式,但是實踐發現dlsym並不能獲取到函式地址。在瞭解Mach-O後,我們發現Mach-O檔案中存放了所有編譯過的的函式指令,static 函式也一定在檔案中。假設在檔案中下面函式

示例函式

在Mach-O檔案中,搜尋程式碼段,可以發現靜態函式存放在程式碼段中,其地址為0x1000010C0

檢視函式地址

那麼我們如何通過函式的名稱獲取到函式地址呢?所謂的函式名實際上就是函式符號,因此函式地址與函式名強關聯。

符號表與字串表、函式地址、section的關係

符號表實際上是個結構體陣列,結構體nlist_64中包括該符號位於字串表的偏移,section索引,以及對應的地址資訊。在符號表中,實際上不能直接獲取到其對應的符號,在圖中我們能看到符號為”_s_cleanup”,這實際上是工具幫我們獲取好後展示出來的,實際上我們在程式碼中只能拿到其位於字串表中的索引,在_s_cleanup的符號表中其索引為0x594,也就是說字串表+0x594即為_s_cleanup字串符號。

字串表的起始地址

從上圖中可以看出字串表的起始地址為0x6004,0x6004+0x594 = 0x6598。

計算符號位置獲取符號

獲取到字串符號後,我們可以知道這個符號是不是我們想要的符號,如果是我們想要的符號,那麼獲取其函式地址。到這裡,應該說通過Mach-O檔案獲取靜態連結函式地址已經完美解決了。需要注意的是,這個函式地址並不是真實的地址,需要計算出其相對於真實地址的偏移,再加上真實檔案地址即為真實函式地址。

函式地址計算

那麼如何獲取函式符號表、字串表呢?實際上segment和符號表在Commands中是順序存放的,_mh_execute_header.ncmds可以根據索引遍歷所有的Command。

找到LC_SYMTAB後,LC_SYMTAB會告訴我們符號表位於可執行檔案的偏移以及字串表位於可執行檔案的偏移。地址計算後即可得到符號表和字串表

獲取符號表和字串表

最終效果如下:

動態呼叫static函式演示

細心的同學可能會發現一個bug,static 函式在不同的檔案中是可以同名的,引數只有一個函式符號的話如何確定是哪個檔案中函式?實際上在符號表中,是可以存在相同符號的,即如果兩個檔案中都存在s_cleanup函式,那麼符號表中會存在兩個_s_cleanup,只不過他們的函式地址不同。那麼如何區分同名靜態函式呢?實際上在連結時,各個段可以理解為按檔案的順序存放的,也就是說符號表實際上也是存在檔案順序的。

連結過程簡圖

符號表的type可以區分出這個符號是否是檔案相關資訊,type == 0x64則是檔案相關資訊,因此在遍歷符號表時可以判斷出當前正在遍歷哪個檔案的符號。能判斷出正在遍歷哪個檔案,那麼bug就迎刃而解。

獲取檔名

    另外,如果static 函式只是在程式碼中實現了,但是並沒有任何呼叫的地方,那麼在編譯時,編譯器會將static函式優化掉,不會生成相關指令。因此符號表中不會儲存static函式相關資訊,也就無法實現動態呼叫。如果想要做到static 資料存取,那麼方式與此類似,只不過獲取到的地址不是函式地址,而是資料儲存地址,如果static 是函式內的區域性變數,那麼其符號需要加上函式符號,比如

那麼它的靜態變數s_iData符號為"_application:didFinishLaunchingWithOptions:.s_iData”。通過memset即可修改變數的值。

    關於Mach-O還有個比較有意思的是,我們可以自定義section,將資料和函式指令放入我們指定的section中,

修改函式存放section

編譯連結後,其檔案中多了個(__TEXT,__mysection),並且函式還能正常執行。這為我們進行程式碼混淆又提供了一個手段。

在瞭解Mach-O之前,我們無法動態呼叫行內函數,動態呼叫任意C函式首先需要嘗試是否能夠通過dlsym函式獲取到指標,如果獲取不到函式指標則可能說明是行內函數,因此需要根據if-else來判斷是哪個行內函數。但是現在我們可以通過Mach-O發現,所謂的行內函數在iOS程式碼中都是以static inline 修飾的,那麼在編譯時行內函數的函式符號會被寫入當前目標檔案的符號表,函式實現會被當做指令寫入程式碼段,如同普通函式一樣。在AppDelegate.m中呼叫CGSizeMake後,檢視AppDelegate的目標檔案符號表,可以看出符號表中包含行內函數的符號,如同普通函式一樣。

_CGSizeMake

六、總結

    在iOS領域hook的方式有很多種,在不是必須的情況下還是少用為妙,hook之後出現問題非常難排查。本文主要介紹瞭如何根據Mach-O檔案,獲取靜態連結的函式地址,動態連結的函式可以參考fishhook。