iOS應用安全6 -- fishhook破解系統C函數

前言

在上上篇文章iOS應用安全4 -- 代碼注入,竊取微信登陸密碼中咱們知道了如何hook App中的OC方法,即便用OC的運行時機制,在運行期間替換相應方法的實現。
那麼有沒有想過咱們如何才能hook C語言寫的函數呢?
這就要使用到咱們今天所要講的fishhook了,下載地址git

fishhook簡介

fishhook是facebook提供的一個動態修改連接mach-O文件的工具。利用MachO文件加載原理,經過修改懶加載和非懶加載兩個表的指針達到C函數hook的目的。github

話很少說,先把它下載下來添加到工程。 算法

file
下載完fishhook以後能夠發現裏面有這5個文件,咱們只須要使用這倆.h和.c就能夠了,新建工程,將這倆文件拖如工程目錄下。
project

點擊fishhook.h文件,咱們能夠查看到這個頭文件裏面主要有如下一些信息:

  1. 有一個結構體,添加了一些註釋進行解釋,以下:
// rebinding--->從新綁定
struct rebinding {
  const char *name;         // 要hook的函數名稱,char* 是C語言的字符串
  void *replacement;        // 用來 替換原始函數 的函數的地址
  void **replaced;          // 被替換掉的原始函數的地址,注意是void**
};
複製代碼
  1. 還有兩個函數,以下:
FISHHOOK_VISIBILITY int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel);

FISHHOOK_VISIBILITY int rebind_symbols_image(void *header, intptr_t slide, struct rebinding rebindings[], size_t rebindings_nel);
複製代碼

很明顯,第二個函數是對第一個函數的擴展,那麼就先來分析一下第一個函數如何使用吧。數組

fishhook使用

爲了簡單起見,就再也不重簽名一個App再測試了,直接在新建的項目的ViewController類中hook系統的C函數NSLog。代碼不多,就下面這些xcode

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    // 就以hook系統的NSLog函數爲例
    // 建立一個結構體變量
    struct rebinding rebind;
    rebind.name = "NSLog";                  // 注意是C語言的字符串不用加@
    rebind.replacement = cus_log;           // C語言函數名 即 函數指針
    // 這裏傳的是函數地址的地址,爲的是在rebind_symbols函數內部附上NSLog的地址
    rebind.replaced = (void *)&sys_log;
    
    // 定義rebinding數組,裏面只有一個元素。固然,也能夠存多個
    struct rebinding rbs[] = {rebind};
    rebind_symbols(rbs, 1);
    
}
// 定義函數指針用來`接收`系統的NSLog函數的地址
static void (*sys_log)(NSString *format, ...);
// 自定義打印函數,用來替換NSLog
void cus_log(NSString *format, ...) {
    // 給打印的字符串後面添加一個標記
    format = [format stringByAppendingString:@"-------->cus_log的標記"];
    // 調用系統NSLog函數
    sys_log(format);
}

// 測試
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"點擊屏幕了😂😂😂");
}
複製代碼

我認爲上面的代碼註釋的很清晰了,這裏再也不重複解釋。但有一點仍是須要說明一下,rebinding結構體的replaced變量是void** 類型,若是對此不太瞭解的能夠本身搜索一下C語言傳值和傳地址的區別,也能夠看我前幾天寫的文章數據結構與算法2 -- 鏈表的最下面總結部分,有對這一塊的詳細解釋。緩存

運行效果以下: 安全

運行gif

hook自定義的C函數?

上面咱們實現了使用fishhook替換系統C函數NSLog的實現,那麼在App中由開發者本身寫的C函數能不能使用fishhook替換呢?
試試就知道了,測試代碼以下:微信

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];

    struct rebinding rb;
    rb.name = "printHelloWorld";
    rb.replacement = hook_printHelloWorld;
    rb.replaced = (void *)&app_printHelloWorld;
    
    struct rebinding rbs[] = {rb};
    rebind_symbols(rbs, 1);
}

// 一個簡單的C語言函數, 假設這個函數是別人App中實現的C語言函數
void printHelloWorld() {
    NSLog(@"Hello World!");
}
// 接收printHelloWorld函數的實現地址
static void (*app_printHelloWorld)(void);

// 替換printHelloWorld函數的函數
void hook_printHelloWorld() {
    NSLog(@"在Hello World!以前打印一句話,表明了hook成功");
    // 調用原始的printHelloWorld函數
    app_printHelloWorld();
}
// 測試
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    printHelloWorld();
}
複製代碼

運行效果以下,能夠看到hook失敗了
由於在Hello World!以前打印一句話,表明了hook成功這句話並無在Hello World!打印前打印。 數據結構

hook函數失敗

分析失敗緣由

P一、爲何會失敗呢?爲何不能hook這個自定義的C函數呢?app

仔細想一想咱們會發現如下兩件事!

  1. OC是動態語言,這個動態就 體如今 OC的方法 是 在運行時 才能肯定 真正要執行的方法實現 是哪一個?(這句話有點長,筆者加了幾個空格斷句)
    也由於OC是動態語言,咱們才能使用Method Swizzling等方法來實現hook OC的方法。
  2. C是靜態語言,靜態 體如今 使用C語言寫的函數,在程序編譯期間 就可以肯定 調用函數時要執行的代碼 在哪裏?
    也正是所以,咱們沒法像OC那樣去hook C語言寫的函數。

P二、爲何fishhook能夠hook NSLog函數呢?NSLog難道不是C函數嗎?

首先說明NSLog確實是C語言函數,這一點沒問題。
之因此fishhook可以hook NSLog函數,這個我默認你已經看過上一篇文章iOS應用安全5 -- main函數調用以前作了些什麼?
上一篇文章主要介紹了MachO文件dyld是如何加載應用程序的?這兩部分,那麼固然不會無緣無故的來說這兩塊內容,確定是有用的。

共享緩存

在dyld加載應用程序的過程當中,有一個步驟叫加載共享緩存,相信你們應該都還記得這一步是作什麼的?

咱們都知道NSLog是系統庫Foundation框架中的一個C函數,而這個Foundation框架自己就是一個真正意義上的動態庫,即它具備在多個進程中共享的特性。而咱們平時說的共享緩存就是指這一類系統庫。

自定義的C函數和系統共享庫中的C函數的區別

系統C函數

系統定義的C函數,因爲具體的函數實現是在系統共享庫中,所以在程序編譯期間是沒法獲取到這個C函數的實現地址,只能經過一種叫符號綁定的方法動態連接到函數名。
這樣直接說有點抽象,下面我列舉個場景:

一、新建一個工程,在ViewController.mviewDidLoad方法中寫一句NSLog(@"哈哈哈");
二、而後command+B編譯程序,那麼在編譯到NSLog(@"哈哈哈");這一句代碼時,xcode會判斷NSLog函數是否是系統庫裏面的函數。
三、發現NSLogFoundation框架裏的函數,那麼就會將NSLog(@"哈哈哈");這句代碼和符號表中的一個char*類型的C字符串"NSLog"進行綁定。
四、當command+R運行應用程序時,就會在dyld加載應用程序時將iOS系統共享緩存區Foundation庫加載到內存中。
五、程序運行過程當中,當代碼第一次執行到NSLog函數調用時,就會到Foundation庫的MachO文件中查詢NSLog函數的實現地址。而後將函數的實現地址和符號表中的"NSLog"字符串進行綁定,這樣後面再調用到NSLog函數時就可以直接找到函數的實現地址。(有點懶加載的意思,因此這種方法也被稱爲懶綁定)

自定義的C函數

到這,想必不用我說,也都知道了自定義的C函數是怎麼回事了吧?
自定義的C函數,因爲函數的實現和函數的調用是在同一個MachO文件(App自己的MachO文件)中,所以在編譯連接期間,xcode就直接將函數調用語句和函數的實現地址進行了連接,也就不會有系統C函數的那些步驟了。

fishhook原理

理解了上面系統C函數的工做原理,咱們應該也能猜到一些fishhook hook系統C函數的原理了。下面仍然以NSLog函數爲例:

從上面咱們知道,當第一次調用NSLog函數的時候,系統就會到Foundation框架中找NSLog的函數實現地址,與App的MachO文件中的符號表中的"NSLog"字符串進行綁定。

fishhook就是利用這一點,回過頭看fishhook中的那個函數名rebind_symbols,翻譯爲從新綁定符號,再看看那個rebinding結構體裏面有哪些東西?

// rebinding--->從新綁定
struct rebinding {
  const char *name;         // 要hook的函數名稱,char* 是C語言的字符串
  void *replacement;        // 用來 替換原始函數 的函數的地址
  void **replaced;          // 被替換掉的原始函數的地址,注意是void**
};
複製代碼

是否是一切都是那麼的恰到好處?
咱們給rebind_symbols函數傳進去一個rebinding結構體,包括了符號表中對應NSLog函數的符號"NSLog",而後rebind_symbols函數經過符號"NSLog"找到Foundation框架中NSLog函數的實現地址,將這個函數地址賦值給replaced變量返回給咱們,再將符號"NSLog"綁定的函數地址替換爲咱們傳進去的函數地址replacement。(注意:fishhook爲了保證返回給咱們的replaced是正確有值的,必要時它會在rebind_symbols函數內部先默認調用一次對應的函數)

空口無憑?

說了那麼大一堆東西,誰知道是真的仍是框個人?

  1. 雖然fishhook爲了保證返回給咱們的原函數地址是正確的,必要時會在重綁定以前先默認調用一次對於的函數,可是爲了更清晰的測試,咱們能夠在rebind_symbols函數調用以前手動先調用一次NSLog函數。如圖,添加一句NSLog(@"123456");,並在調用前打上斷點。
    斷點
  2. 運行代碼,斷點斷到20行。使用lldbimage list命令查看主程序MachO在內存中的偏移值(即ASLR)。
    ASLR
  3. 使用MachOView工具打開App的MachO文件。
    綁定地址
  4. 經過上圖中說的計算方法,計算符號NSLog當前綁定的地址在內存中的真實地址。
    即真實地址 = 0x00000001005b0000 + 0xc000 = 0x00000001005bc000。
  5. 經過lldb命令memory read查看咱們計算出來的那塊內存。
    內存
  6. 此時能夠看到符號NSLog綁定的內存地址保存的值,這個值是一個指針,由於iOS默認是小端模式,高位字節保存在低位地址中,因此這個指針指向的真實地址應該是0x00000001005b6904。
  7. 經過lldbdis -s指令反彙編這個地址。能夠看到這個地址如今就是一塊無心義的地址,也就是符號NSLog默認綁定的地址。
    反彙編
  8. 斷點往下走,執行完NSLog(@"123456");斷點停在rebind_symbols函數以前。
    走斷點
  9. 此時重複步驟5,經過lldb命令memory read查看內存,能夠發現,同一塊內存地址,裏面保存的地址已經發生了改變。
    查看內存
  10. 一樣,使用lldbdis -s指令反彙編這個地址(注意小端)。
    NSLog地址
  11. 此時能夠發現,符號NSLog綁定的那個指針保存的地址已經由最開始的那個無心義的地址變成了Foundation框架裏面的NSLog函數的地址了。
  12. 別急,尚未結束,斷點繼續往下走一步,讓rebind_symbols函數執行。再次使用lldb命令memory read查看那塊內存。
    再次查看
  13. 發現綁定的指針指向的地址又變了,老規矩,反彙編這個地址。
    自定義的log函數
  14. 能夠發現符號NSLog綁定指針保存的Foundation裏面的NSLog函數地址已經被fishhook修改爲了fishhook-test裏面的cus_log函數

整個流程就是這樣,沒有框你吧?

如何經過C字符串"NSLog"找到符號NSLog?

從上面咱們知道了如何經過MachO文件中懶加載符號表中的符號找到函數在內存中的地址,進而hook這個函數。而這整個過程都有一個前提,那就是要找到符號表中的NSLog,那麼如何找呢?

別急,仔細看fishhook的rebinding結構體,咱們出來要傳入hook函數的地址以外還須要提供一個和系統C函數同名的C字符串。那麼確定就是經過這個C字符串來找對應函數的符號了。

  1. 使用MachOView打開App的MachO文件,裏面有一項叫作String Table,聽名字就知道這是一個字符串表,既然要經過字符串"NSLog"找函數符號,那麼固然就要先從字符串表開始了。String Table以下:
    string table
  2. 這些字符串使用.進行分割,使用_表示後面的是一個函數,仔細看上圖中的"._NSLog"對應的二進制值,能夠發現二進制值其實就是每一個字符的ascii碼。
  3. 這也就是說,咱們能夠將C字符串"NSLog"中的字符取出來,轉換成對應的ascii碼,再到MachOString Table表中查詢,就能夠找到"NSLog"字符串在String Table表中的偏移量。
    偏移量
  4. 上圖的偏移量有點問題,應該計算"_NSLog"的偏移量而不是"NSLog"的偏移量(因此上圖計算的結果應該是0x00010F5B,而非0x00010F5C)。
  5. 用計算出的"_NSLog"相對於MachO文件的偏移量 減去 String Table相對於MachO文件的偏移量便可獲得"_NSLog"相對於String Table的偏移量。
    計算結果 = 0x00010F5B - 0x00010EC0 = 0x9B。
  6. 接下來查看符號表Symbol Table能夠發現,每個符號都對應的有一個String Table Index的東西,這個東西就是上一步計算出來的。
    符號表
  7. 能夠查找String Table Index等於0x9B的,發現它對應的字符串值Value就是_NSLog,說明沒有找錯。
  8. 接下來能夠發現,這個"_NSLog"在符號中對應的位置是105,換算成16進制是0x69。再換到Dynamic Symbol Table目錄下,查找值是0x69的行。
    Dynamic Symbol Table
  9. 能夠發現,值0x69對應的Symbol符號也正是_NSLog。再次印證了沒錯。而且關鍵點就在於下面的那個Indirect Address行,0x10000C000。
  10. 0x10000C000中0x100000000表示的是Load Commands裏面的_PAGEZERO的大小,這個大小實際上是一個虛擬大小
    pagezero
  11. 也就是說Indirect Address真正的偏移量是0xC000,這個0xC000有沒有以爲很熟悉?好,再看懶加載符號表--->Lazy Symbol Pointers
    懶加載符號表
  12. 其中偏移量0xC000對應的符號就是_NSLog了,至此終於和上一個標題接上了。

總結

經過這篇文章,咱們學到了如下幾點:

  • fishhook是什麼?
    • 它是用來hook系統C函數的一種工具。
  • fishhook怎麼用?
    • 代碼很簡單,上面有。
  • fishhook爲何能hook系統C函數?
    • 由於系統的C函數的函數實現地址在共享庫裏面,沒法在編譯期間直接獲取到。
  • fishhook實現的原理?
    • 經過函數符號字符串在MachO文件中通過一系列的查找,最終找到函數的實現地址,將函數的實現地址替換爲咱們自定義的函數地址。

題外篇---簡單防禦

以前就講過,hook OC的方法,能夠經過3種方式交換方法的實現地址,使得App調用方法1的時候執行交換後的方法2,咱們再調用方法2的時候執行咱們添加的代碼和方法1本來的實現。具體能夠回頭再看看iOS應用安全4 -- 代碼注入,竊取微信登陸密碼這篇文章。

那麼面對這種破解,咱們做爲開發者要如何防禦呢?
很簡單,能夠利用本篇文章講述的fishhook,把runtime裏面的那幾個方法交換的C函數所有替換成沒法使用的C函數,這樣,別人再使用方法交換方法的時候就會調用到咱們替換以後的沒法使用的C函數。哈哈😄。

可是這樣就徹底能夠保證安全了嗎?
還差得遠呢,別人仍然可使用其餘方法來破壞或者繞過你的保護。

攻防永遠是對立的,沒有絕對的安全,只有相對的安全。黑客若真的鐵了心的要破解你的App,你再怎麼防禦也是沒用的。咱們要作的就是要讓黑客花費很長很長的時間才能破解出來,讓他知難而退便可。

本文地址

相關文章
相關標籤/搜索