iOS 逆向 - Hook / fishHook 原理與符號表

李斌同學發表於2019-11-12

前言 :

本篇文章較與依賴前一篇 Mach-O檔案 的先導知識 , 建議先閱讀後再探究 .

  • 由於逆向過程中程式碼注入往往會使用 hook 這種方式 , 而且在安全防護與監測方面經常使用 .

  • 另外只知道 runtime 交換 imp 的方式對於中高階開發人員 ( 想偷懶又想裝* ) 顯然是不太夠的 .

那麼我們今天就來好好探討一下 HookfishHook 原理 .

Hook 概述

HOOK,中文譯為 “掛鉤““鉤子” 。在 iOS 逆向中是指改變程式執行流程的一種技術。通過 hook 可以讓別人的程式執行自己所寫的程式碼。在逆向中經常使用這種技術。所以在學習過程中,我們重點要了解其原理,這樣能夠對惡意程式碼進行有效的防護。

大名鼎鼎的 Hook 已經不知道被多少人玩出了花 ❀ , 其用途之多我們就不說了 .

iOS 中幾種常見的 Hook

1 . Method Swizzle

利用 OCRuntime 特性,動態改變 SEL(方法編號)IMP(方法實現)的對應關係,達到 OC 方法呼叫流程改變的目的。主要用於 OC 方法。

常用的有

  • method_exchangeImplementations 交換函式 imp
  • class_replaceMethod 替換方法
  • method_getImplementationmethod_setImplementation 直接 get / set imp

關於這些 Runtime 方法的基本使用以及原理 這一點在 重籤應用除錯與程式碼修改 這篇文章最後有詳細的解釋和 demo , 感興趣的可以去閱讀一下 .

2 . fishhook

它是 Facebook 提供的一個動態修改連結 Mach-O 檔案的工具。利用 MachO 檔案載入原理,通過修改懶載入和非懶載入兩個表的指標達到 C 函式 Hook 的目的。

3. Cydia Substrate

Cydia Substrate 原名為 Mobile Substrate ,它的主要作用是針對 OC 方法、C 函式以及函式地址進行 Hook 操作。當然它並不是僅僅針對 iOS 而設計的,安卓一樣可以用。官方地址:www.cydiasubstrate.com/

它使用的是 logos 語法 , 關於這個工具的使用 , 後續文章我會詳細講述 .

fishhook 基本使用

下載

git - 地址 : fishhook - git

有需要的可以下載這個有中文註釋的版本 link 提取碼:f4f8 .

demo

#import "ViewController.h"
#import "fishhook.h"

@interface ViewController ()
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //rebinding結構體
    struct rebinding nslog;
    //需要HOOK的函式名稱,C字串
    nslog.name = "NSLog";
    //新函式的地址
    nslog.replacement = myNslog;
    //原始函式地址的指標!
    nslog.replaced = (void *)&sys_nslog;
    //rebinding結構體陣列
    struct rebinding rebs[1] = {nslog};
    /**
     * 引數1 : 存放rebinding結構體的陣列
     * 引數2 : 陣列的長度
     */
    rebind_symbols(rebs, 1);
}
//---------------------------------更改NSLog-----------
//函式指標
static void(*sys_nslog)(NSString * format,...);
//定義一個新的函式
void myNslog(NSString * format,...){
    format = [format stringByAppendingString:@"勾上了!\n"];
    //呼叫原始的
    sys_nslog(format);
}

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSLog(@"點選了螢幕!!");
}
@end
複製程式碼

點選螢幕 , 列印結果 :

001--fishHookDemo[15776:645816] 點選了螢幕!!勾上了!
複製程式碼

關鍵函式

rebind_symbols , 原始碼如下 :

int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel) {
    //prepend_rebindings的函式會將整個 rebindings 陣列新增到 _rebindings_head 這個連結串列的頭部
    //Fishhook採用連結串列的方式來儲存每一次呼叫rebind_symbols傳入的引數,每次呼叫,就會在連結串列的頭部插入一個節點,連結串列的頭部是:_rebindings_head
    int retval = prepend_rebindings(&_rebindings_head, rebindings, rebindings_nel);
    //根據上面的prepend_rebinding來做判斷,如果小於0的話,直接返回一個錯誤碼回去
    if (retval < 0) {
    return retval;
  }
    //根據_rebindings_head->next是否為空判斷是不是第一次呼叫。
  if (!_rebindings_head->next) {
      //第一次呼叫的話,呼叫_dyld_register_func_for_add_image註冊監聽方法.
      //已經被dyld載入的image會立刻進入回撥。
      //之後的image會在dyld裝載的時候觸發回撥。
    _dyld_register_func_for_add_image(_rebind_symbols_for_image);
  } else {
      //遍歷已經載入的image,進行hook
    uint32_t c = _dyld_image_count();
    for (uint32_t i = 0; i < c; i++) {
      _rebind_symbols_for_image(_dyld_get_image_header(i), _dyld_get_image_vmaddr_slide(i));
    }
  }
  return retval;
}
複製程式碼

fishhook 的基礎使用非常簡單.

  • 就是需要我們定義一個指標從而 fishhook 可以幫我們儲存原系統函式的實現地址 , 另外將需要替換的函式名稱自定義函式地址寫成結構體呼叫 rebind_symbols 就可以了 ,
  • 另外可以一次往陣列中寫入多個結構體進行多個函式的 hook.

fishhook 分析

基礎 OC 函式的 hook 原理我們不在多贅述了 , 其實簡單來說就是替換掉方法實現的 imp , 這個基於 OC 語言的動態執行時機制是很好理解的 .

但是 C 呢 ?

我們知道 C 函式是靜態的,也就是說在編譯的時候,編譯器就知道了它的實現地址,這也是為什麼 C 函式只寫函式宣告呼叫時會報錯。那麼為什麼 fishhook 還能夠改變 C 函式的呼叫呢?難道函式也有動態的特性存在?我們一起來探究它的原理

注意 :

fishhook 是可以 hook 系統的函式 , 並非所有的 C 函式 , 也就是說 fishhook 也只能對帶有符號表的系統函式進行重繫結 , 而對自己實現的 C 函式同樣是沒有辦法的.

我們大可以自己寫一個 C 函式實驗一下 .

#import "ViewController.h"
#import "fishhook.h"

@interface ViewController ()
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // hook 自定義C函式
    struct rebinding Cfunction;
    Cfunction.name = "func";
    Cfunction.replacement = newfunc;
    Cfunction.replaced = (void *)&funcOri;
    struct rebinding resbs[1] = {Cfunction};
    rebind_symbols(resbs, 1);
}

// 要hook 的c函式
void func(const char * str){
    NSLog(@"%s",str);
}

//原始的函式指標記錄
static void(*funcOri)(const char *);

void newfunc(const char * str){
    NSLog(@"勾住了!");
    funcOri(str);
}
@end
複製程式碼

執行 , 列印結果

2019-11-12 14:54:19.001680+0800 fishhookDemo[35238:1563336] 點選了螢幕
2019-11-12 14:54:19.706819+0800 fishhookDemo[35238:1563336] 點選了螢幕
2019-11-12 14:54:19.861428+0800 fishhookDemo[35238:1563336] 點選了螢幕
複製程式碼

無法 hook 自定義 C 函式 , 接下來我們通過 fishhook 原理來解析一下原因 .

fishhook 原理

首先 :

C OC
靜態 動態
編譯時確定函式地址 執行時確定函式地址

而系統的 C 函式有動態的部分 , 就是我們經常提到的符號表 , 用到的技術叫做 Position Independent Code ( 位置程式碼獨立 ) , fishhook 也就是在此處做了文章 .

fishhook 原文講述 :

iOS 逆向 - Hook / fishHook 原理與符號表

iOS 逆向 - Hook / fishHook 原理與符號表

原理概述

由於 iOS 系統中 UIKit / Foundation 庫每個應用都會通過 dyld 載入到記憶體中 , 因此 , 為了節約空間 , 蘋果將這些系統庫放在了一個地方 : 動態庫共享快取區 (dyld shared cache) . ( Mac OS 一樣有 ) .

因此 , 類似 NSLog 的函式實現地址 , 並不會也不可能會在我們自己的工程的 Mach-O 中 , 那麼我們的工程想要呼叫 NSLog 方法 , 如何能找到其真實的實現地址呢 ?

其流程如下 :

  • 在工程編譯時 , 所產生的 Mach-O 可執行檔案中會預留出一段空間 , 這個空間其實就是符號表 , 存放在 _DATA 資料段中 ( 因為 _DATA 段在執行時是可讀可寫的 )

  • 編譯時 : 工程中所有引用了共享快取區中的系統庫方法 , 其指向的地址設定成符號地址 , ( 例如工程中有一個 NSLog , 那麼編譯時就會在 Mach-O 中建立一個 NSLog 的符號 , 工程中的 NSLog 就指向這個符號 )

  • 執行時 : dyld將應用載入到記憶體中時 , 根據 load commands 中列出的需要載入哪些庫檔案 , 去做繫結的操作 ( 以 NSLog 為例 , dyld 就會去找到 FoundationNSLog 的真實地址寫到 _DATA 段的符號表中 NSLog 的符號上面 )

這個過程被稱為 PIC 技術 . ( Position Independent Code : 位置程式碼獨立 )

那麼瞭解了系統函式的整個載入過程 , 我們再來看 fishhook 的函式名稱 :

rebind_symbols :: 重繫結符號 也就簡單明瞭了.

其原理就是 :

將編譯後系統庫函式所指向的符號 , 在執行時重繫結到使用者指定的函式地址 , 然後將原系統函式的真實地址賦值到使用者指定的指標上.

那麼再回頭看自定義的C函式為什麼 hook 不了 ?

那答案就很簡單了 :

  • 自定義 C 函式實際地址就在自己的 Mach-O 內 , 也並沒有符號和繫結的過程 .
  • 編譯時就已經確定了 , 並沒有辦法操作 .

Mach-O 中檢視符號表

利用 MachOView 直接檢視.

iOS 逆向 - Hook / fishHook 原理與符號表

有同學說了 , 說是這麼說 , 怎麼驗證呢 ?

既然講到了這兒 , 那我們就順道提一點符號表的知識 , 畢竟一些 bug 收集工具也經常用到符號表還原 , 順便我們來實際操作一下 , 一邊驗證理論 , 一邊加深記憶 .

符號表與實際操作驗證理論

MachOView 中我們看到 , 符號表分為兩種

  • Lazy Symbol Pointers
  • Non-Lazy Symbol Pointers

就是字面意思 , 懶載入和非懶載入 .

因此我們在使用 fishhook 的時候 , 最好是呼叫一下原函式 , 以防止可能會出現沒使用並沒有繫結的問題 .

那麼接下來 , 我們來玩一下 ?

廢話不多說 , 來到我們剛剛寫的 hookNSLogdemo , 在 viewdidload 中先加一句 NSLog(@"123");

開始玩

1 準備好程式碼和斷點

iOS 逆向 - Hook / fishHook 原理與符號表

2 MachOView 檢視

cmd + r 執行執行工程來到斷點 , 找到 Mach-O , 使用 MachOView 檢視.

iOS 逆向 - Hook / fishHook 原理與符號表

3 計算符號地址

iOS 逆向 - Hook / fishHook 原理與符號表

  • 首先我們看到 這個符號基於 Mach-O 檔案的首地址偏移量是 3028 , ( 每個人的都不一樣 , 你用你自己的 ) .

    那麼 Mach-O 的地址在哪呢 ?

    來到工程 LLDB 輸入指令 : image list

    第一個就是我們工程的 Mach-O 實際記憶體地址

    iOS 逆向 - Hook / fishHook 原理與符號表

  • 開啟計算器 cmd + 3 , 選擇十六進位制 , cmd + vMach-O 實際記憶體地址貼上進去 , 加上 MachOView 的符號偏移地址 3028.

    iOS 逆向 - Hook / fishHook 原理與符號表

    cmd + c 拷貝計算結果.

4 lldb 檢視記憶體與彙編程式碼

x + 0x1042C8028 ( 你的計算結果 )

( memory read 也可以 , x 就是 memory read 的簡寫 )

iOS 逆向 - Hook / fishHook 原理與符號表

5 檢視前八個位元組的內容

注意 : iOS 小端模式 , 從右往左讀 .

那麼我上圖中對應的實際地址就是 0x01042c69c0 .

檢視彙編 : dis -s 0x01042c69c0 ( 你自己的地址 )

iOS 逆向 - Hook / fishHook 原理與符號表

那麼我們看到裡面並沒有什麼內容 , 也就是說在此時這個斷點這裡 , 符號並沒有被繫結內容 .

過掉斷點 , 來到第二處斷點 .

( 有對於彙編不熟悉的同學不用著急 , 對比一下第二個斷點的結果來看 . 另外後續筆者會考慮繼續更彙編部分內容 )

重新檢視符號

x + 0x1042C8028 ( 你的計算結果 )

iOS 逆向 - Hook / fishHook 原理與符號表

可以看到明顯內容已經改變了 .

再次檢視彙編 : dis -s 0x01042c69c0 ( 你自己的地址 )

iOS 逆向 - Hook / fishHook 原理與符號表

大功告成 , 再回想一下我們之前的原理探究部分. 完全驗證 !

彆著急 , 這只是驗證了 iOSPIC 部分 , 那麼我們 fishhook 呢 ?

  • touchesBegan 加個斷點 ( 不一定非要在 touchesBegan 加 , 我這裡只是 fishhookrebind_symbols 後面就沒有程式碼可以過斷點了. )
  • 過掉當前斷點 ( rebind_symbols ) , 點選螢幕 來到下一個斷點.
  • 再次檢視記憶體和彙編

結果如下 :

iOS 逆向 - Hook / fishHook 原理與符號表

fishhook 重繫結後符號指向了我們自定義的函式地址 . 完全驗證之前假設 .

最後

fishhook 在逆向中非常重要 , 很多工具也內建了 fishhook , 因此希望大家能認真理解並掌握原理 .

感謝關注 , 我們下期見 .

相關文章