不做偽學習者
上一篇我們一起分析了 fishhook的實現原理,但很多東西如果我們僅僅知道原理,其實距離真正吸收它並將其轉化成自己的生產力還有很長的路。你得弄清楚別人是怎麼利用這個原理去解決問題的,還要借鑑別人的設計思想,再結合我們自己的思考不斷地實踐和總結,才能真正讓知識成為自己的生產力。
話不多說,進入今天的第一個正題。
fishhook 使用場景
在 上一篇 裡已經為大家演示了它的基本用法,使用很簡單,這裡就不展開了。它的使用場景正如其名: fishhook,主要用在安全防護領域。當然,大神級的逆向與安全防護專家我們們先不談,那個級別的高手我相信也不會看到這篇文章,天下沒有絕對的安全,黑與白永遠都在博弈,所以希望大家不要鑽牛角尖,至少我們們不能寫出讓菜鳥逆向就能輕鬆搞定的應用對吧?當然,後面我們們也會學習靜態分析和彙編的知識,掌握更高階的逆向和防護技能,那都是打好基礎的後話了。今天我們們的重點是原始碼分析,順便溫習下 c 的資料結構。
下面先來了解一下用 fishhook 防 HOOK 的基本思路:
- 在基礎的動態除錯逆向中,最常見的就是定位到目標方法後,通過 runtime 中的幾種方法交換方式,實現程式碼注入的目的。為你準備好了相關的文章:iOS程式碼注入+HOOK微信登入
- 既然 fishhook 可以攔截系統外部的 C 函式,那自然就可以 HOOK 到 runtime 庫中的所有方法。
- 那我們就將所有可能用來篡改我們 OC 方法實現的 runtime API,都用 fishhook 攔截掉,使其無法用程式碼注入的方式成功 HOOK。
思路理清了,擼起袖子開始幹。
- 為了方便演示,這裡直接搞了一個分類,將ViewController的 例項方法
viewDidAppear:
用method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2)
的方式與my_viewDidAppear:
交換了實現,上程式碼: 這時我們程式跑起來就可以看到如下輸出: - 為了阻止其完成方法交換,我們要 hook
method_exchangeImplementations
方法,拖入 fishhook 原始檔,再新增一個分類並寫好 hookmethod_exchangeImplementations
的程式碼:(如果成功 hook 了method_exchangeImplementations
,那別人呼叫該方法時會進入我們的myExchange
,然後順便又把NSLog
hook 了一下,不要被這個繞暈了 ?) 再次 Run 起來,咦?腫麼肥四?你會發現method_exchangeImplementations
並沒有 HOOK 成功,viewDidAppear:
依然被篡改了實現,問題出在哪了呢?
對,聰明的你一定發現了問題所在:是程式碼執行順序的問題
經過實踐,我發現專案裡參與編譯的檔案順序就是其編譯後被載入時的載入順序(暫未找到官方的編譯順序說明,還請有研究的大佬指點),即此時 ViewController+HOOKTest
的 load
方法會早於 ViewController+FishHook
的 load
呼叫,所以 method_exchangeImplementations
的實現被我們 HOOK 發生在 viewDidAppear:
被別人交換之後,從而導致防護的失敗:
如上圖所示,在調整了編譯檔案的順序之後成功 HOOK 到了 method_exchangeImplementations
的呼叫,但實際開發中我們不可能採用這麼笨的方法,也不可能通過這種方式決定檔案的載入順序,因此我們要想辦法保證 fishhook 的程式碼必須最先執行才行。
那如何做到呢?由此前的 dyld背後的故事&原始碼分析 可以得知,本地的Framework中的類一定會早於後注入的庫(動態庫例外,非越獄裝置是沒有插入動態庫的許可權的)和可執行檔案中的類進行初始化。所以我們將 fishhook 的 HOOK 操作程式碼移到自建的 Framework 中即可:
至此,我們已經知道了 fishhook 反除錯的基本思路,當然,上面的程式碼只是思路演示,實際開發中,像method_getImplementation
、method_setImplementation
等函式都需要用同樣的方式一一 HOOK,同時,如果自己的專案中已經用到了這些函式,還需要設計相應的白名單方案,並且在檢測到是被三方非法 HOOK 時通常直接呼叫 exit(0)
這類介面終止掉程式。這些細節以後還會詳細講,這裡算是拋磚引玉吧。
那我們們進入第二個正題,原始碼分析。
fishhook 原始碼分析
(一): 在寫 fishhook 的程式碼時,第一件事就是宣告一個 rebinding
型別的結構體變數,其原始碼如下:
void **replaced
是指向指標的指標,可以理解為一個存著另一個指標地址的指標,在上述示例中, *replaced
取出的就是一個指向共享庫中 method_exchangeImplementations
函式實現的指標,再對其取值,**replaced
得到的就是共享庫中 method_exchangeImplementations
函式實現的首地址,還不清楚的同學要自己去補補基礎了?。
(二): 按結構體成員的型別寫好宣告和實現之後,一一賦值給結構體對應的成員,再把這些結構體放到一個陣列中,然後呼叫重繫結符號函式 rebind_symbols
(如果繫結成功返回 0,否則返回 -1),並將結構體陣列和陣列長度作為引數傳入:
- 第一件事,呼叫了這個函式--
prepend_rebindings
,其具體實現如下: 咦?傳入的第一個引數&_rebindings_head
是個啥東東? 看原始碼:_rebindings_head
被宣告為一個指向rebindings_entry
型別結構體的靜態指標變數,那&_rebindings_head
就是取出這個指標的地址,再看該函式的引數宣告struct rebindings_entry **
,沒錯,這又是一個指向指標的指標。
結構體rebindings_entry
的三個成員分別是:指向rebinding
型別結構體的指標(用來指向傳入結構體陣列的首元素地址)、rebindings_nel
:記錄此次要重繫結的數量(用於開闢對應大小的空間)、指向下一個rebindings_entry
型別的結構體(記錄下一次需要重繫結的資料),這就是典型的資料結構——連結串列的一種實現。_rebindings_head
就是指向該連結串列的指標。
為了加深理解,我為你畫了一張 prepend_rebindings
函式的作用示意圖:
prepend_rebindings
函式的目的:將新加入的 rebindings 陣列不斷的新增到 _rebindings_head 這個連結串列的頭部成為新的頭節點。
- 第二件事,對已經載入的映象檔案(也就是庫)逐一查詢目標符號進行 hook。前面我們已經知道 fishhook 的程式碼執行時間非常早,所以第一次執行時要 hook 的庫可能還沒完成裝載,因此這裡如果是第一次呼叫會通過一個函式對庫的裝載完成註冊監聽和回撥的方法:
extern void _dyld_register_func_for_add_image(void (*func)(const struct mach_header* mh, intptr_t vmaddr_slide)) __OSX_AVAILABLE_STARTING(__MAC_10_1, __IPHONE_2_0);
複製程式碼
原始碼註釋如下圖:
當回撥到_rebind_symbols_for_image
時,會將存著待繫結函式資訊的連結串列作為引數傳入,用於符號查詢和函式指標的交換,第二個引數 header
是 當前 image
的頭資訊,第三個引數 slide
是 ASLR 的偏移:
-
第三件事,根據 fishhook 是如何根據字串對應在符號表中的指標,找到其在共享庫的函式實現 中的幾個步驟,去找到目標符號對應指標所指向的函式實現地址:
這個過程比較枯燥,無非就是按照規則計算各種表的地址和指標在表中的偏移量。 -
最後一件事,根據算好的符號表地址和偏移量,找到在符號表中用於指向共享庫目標函式的指標,然後將該指標的值(即目標函式的地址)賦值給我們的
*replaced
,最後修改該指標的值為我們的replacement
(新的函式地址),perform_rebinding_with_section
的原始碼實現:
fishhook 原始碼之旅,告一段落
如果不算註釋,fishhook 的原始碼實現一共 180+ 行,通過對其原始碼的分析,如果做到讀懂它的每一行,我相信不管是對指標的理解和使用,還是對連結串列的資料結構和實現方式,你都會有更好的理解。當然,你對 MachO 的檔案結構和載入機制,也更加了然於胸,同時還 get 了基本的安全防護技巧。總之,願你不虛此行。
下篇速遞:LLDB 知多少