前言 :
本篇文章較與依賴前一篇 Mach-O檔案 的先導知識 , 建議先閱讀後再探究 .
由於逆向過程中程式碼注入往往會使用
hook
這種方式 , 而且在安全防護與監測方面經常使用 .另外只知道
runtime
交換imp
的方式對於中高階開發人員 ( 想偷懶又想裝* ) 顯然是不太夠的 .那麼我們今天就來好好探討一下
Hook
與fishHook
原理 .
Hook 概述
HOOK,中文譯為 “掛鉤“
或 “鉤子”
。在 iOS 逆向中是指改變程式執行流程的一種技術。通過 hook
可以讓別人的程式執行自己所寫的程式碼。在逆向中經常使用這種技術。所以在學習過程中,我們重點要了解其原理,這樣能夠對惡意程式碼進行有效的防護。
大名鼎鼎的 Hook 已經不知道被多少人玩出了花 ❀ , 其用途之多我們就不說了 .
iOS 中幾種常見的 Hook
1 . Method Swizzle
利用 OC 的 Runtime
特性,動態改變 SEL
(方法編號)和 IMP
(方法實現)的對應關係,達到 OC 方法呼叫流程改變的目的。主要用於 OC 方法。
常用的有
method_exchangeImplementations
交換函式 impclass_replaceMethod
替換方法method_getImplementation
與method_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
系統中 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
就會去找到Foundation
中NSLog
的真實地址寫到_DATA
段的符號表中NSLog
的符號上面 )
這個過程被稱為 PIC
技術 . ( Position Independent Code : 位置程式碼獨立 )
那麼瞭解了系統函式的整個載入過程 , 我們再來看 fishhook
的函式名稱 :
rebind_symbols :: 重繫結符號
也就簡單明瞭了.
其原理就是 :
將編譯後系統庫函式所指向的符號 , 在執行時重繫結到使用者指定的函式地址 , 然後將原系統函式的真實地址賦值到使用者指定的指標上.
那麼再回頭看自定義的C函式為什麼 hook
不了 ?
那答案就很簡單了 :
- 自定義
C
函式實際地址就在自己的Mach-O
內 , 也並沒有符號和繫結的過程 . - 編譯時就已經確定了 , 並沒有辦法操作 .
Mach-O 中檢視符號表
利用 MachOView
直接檢視.
有同學說了 , 說是這麼說 , 怎麼驗證呢 ?
既然講到了這兒 , 那我們就順道提一點符號表的知識 , 畢竟一些 bug 收集工具也經常用到符號表還原 , 順便我們來實際操作一下 , 一邊驗證理論 , 一邊加深記憶 .
符號表與實際操作驗證理論
從 MachOView
中我們看到 , 符號表分為兩種
Lazy Symbol Pointers
Non-Lazy Symbol Pointers
就是字面意思 , 懶載入和非懶載入 .
因此我們在使用 fishhook
的時候 , 最好是呼叫一下原函式 , 以防止可能會出現沒使用並沒有繫結的問題 .
那麼接下來 , 我們來玩一下 ?
廢話不多說 , 來到我們剛剛寫的 hook
了 NSLog
的 demo
, 在 viewdidload
中先加一句 NSLog(@"123")
;
開始玩
1 準備好程式碼和斷點
2 MachOView 檢視
cmd + r
執行執行工程來到斷點 , 找到 Mach-O
, 使用 MachOView
檢視.
3 計算符號地址
-
首先我們看到 這個符號基於
Mach-O
檔案的首地址偏移量是3028
, ( 每個人的都不一樣 , 你用你自己的 ) .那麼
Mach-O
的地址在哪呢 ?來到工程
LLDB
輸入指令 :image list
第一個就是我們工程的
Mach-O
實際記憶體地址 -
開啟計算器
cmd + 3
, 選擇十六進位制 ,cmd + v
把Mach-O
實際記憶體地址貼上進去 , 加上MachOView
的符號偏移地址3028
.cmd
+c
拷貝計算結果.
4 lldb 檢視記憶體與彙編程式碼
x
+ 0x1042C8028
( 你的計算結果 )
( memory read
也可以 , x
就是 memory read
的簡寫 )
5 檢視前八個位元組的內容
注意 : iOS
小端模式 , 從右往左讀 .
那麼我上圖中對應的實際地址就是 0x01042c69c0
.
檢視彙編 : dis -s 0x01042c69c0 ( 你自己的地址 )
那麼我們看到裡面並沒有什麼內容 , 也就是說在此時這個斷點這裡 , 符號並沒有被繫結內容 .
過掉斷點 , 來到第二處斷點 .
( 有對於彙編不熟悉的同學不用著急 , 對比一下第二個斷點的結果來看 . 另外後續筆者會考慮繼續更彙編部分內容 )
重新檢視符號
x
+ 0x1042C8028
( 你的計算結果 )
可以看到明顯內容已經改變了 .
再次檢視彙編 : dis -s 0x01042c69c0 ( 你自己的地址 )
大功告成 , 再回想一下我們之前的原理探究部分. 完全驗證 !
彆著急 , 這只是驗證了 iOS
的 PIC
部分 , 那麼我們 fishhook
呢 ?
- 在
touchesBegan
加個斷點 ( 不一定非要在touchesBegan
加 , 我這裡只是fishhook
的rebind_symbols
後面就沒有程式碼可以過斷點了. ) - 過掉當前斷點 (
rebind_symbols
) , 點選螢幕 來到下一個斷點. - 再次檢視記憶體和彙編
結果如下 :
fishhook
重繫結後符號指向了我們自定義的函式地址 . 完全驗證之前假設 .
最後
fishhook
在逆向中非常重要 , 很多工具也內建了 fishhook
, 因此希望大家能認真理解並掌握原理 .
感謝關注 , 我們下期見 .