iOS Hook原理(一)- fishhook

一、Hook概述

HOOK中文譯為掛鉤鉤子。在iOS逆向中是指改變程序運行流程的一種技術(shù)。通過hook可以讓別人的程序執(zhí)行自己所寫的代碼。在逆向中經(jīng)常使用這種技術(shù)。只有了解其原理才能夠?qū)阂獯a進(jìn)行有效的防護(hù)。

比如很久之前的微信自動搶紅包插件:


搶紅包Hook示意圖

1.1Hook的幾種方式

iOSHOOK技術(shù)的大致上分為5種:Method Swizzle、fishhook、Cydia Substrate、libffi、inlinehook。

1.1.1 Method Swizzle (OC)

利用OCRuntime特性,動態(tài)改變SEL(方法編號)和IMP(方法實現(xiàn))的對應(yīng)關(guān)系,達(dá)到OC方法調(diào)用流程改變的目的。主要用于OC方法。

可以將SELIMP 之間的關(guān)系理解為一本書的目錄SEL 就像標(biāo)題,IMP就像頁碼。他們是一一對應(yīng)的關(guān)系。(書的目錄不一定一一對應(yīng),可能頁碼相同,理解就行。)。

image.png

Runtime提供了交換兩個SELIMP對應(yīng)關(guān)系的函數(shù):

OBJC_EXPORT void
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2) 
   OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

通過這個函數(shù)交換兩個SELIMP對應(yīng)關(guān)系的技術(shù),稱之為Method Swizzle(方法欺騙)

Method Swizzle

runtime中有3種方式實現(xiàn)方法交換:

  • method_exchangeImplementations:在分類中直接交換就可以了,如果不在分類需要配合class_addMethod實現(xiàn)原方法的回調(diào)。
  • class_replaceMethod:直接替換原方法。
  • method_setImplementation:重新賦值原方法,通過getImpsetImp配合。

runtime都比較熟悉,就不多介紹了,不是很了解使用的可以參考:第4部分代碼注入

1.1.2 fishhook (外部函數(shù))

Facebook提供的一個動態(tài)修改鏈接mach-O文件的工具。利用MachO文件加載原理,通過修改懶加載非懶加載兩個表的指針達(dá)到C(系統(tǒng)C函數(shù))函數(shù)HOOK的目的。fishhook

總結(jié)下來是:dyld 更新 Mach-O 二進(jìn)制的 __DATA segment__la_symbol_str 中的指針,使用 rebind_symbol方法更新兩個符號位置來進(jìn)行符號的重新綁定。

1.1.3 Cydia Substrate

Cydia Substrate 原名為 Mobile Substrate ,主要作用是針對OC方法、C函數(shù)以及函數(shù)地址進(jìn)行HOOK操作。并不僅僅針對iOS而設(shè)計,安卓一樣可以用。Cydia Substrate官方

Cydia Substrate主要分為3部分:Mobile Hooker、MobileLoadersafe mode。

Mobile Hooker

它定義了一系列的宏和函數(shù),底層調(diào)用objcruntimefishhook來替換系統(tǒng)或者目標(biāo)應(yīng)用的函數(shù)。其中有兩個函數(shù):

  • MSHookMessageEx:主要作用于OC方法 MSHookMessageEx

    void MSHookMessageEx(Class class, SEL selector, IMP replacement, IMP result) 
    
  • MSHookFunction :(inline hook)主要作用于CC++函數(shù) MSHookFunctionLogos語法的%hook就是對這個函數(shù)做了一層封裝。

    void MSHookFunction(voidfunction,void* replacement,void** p_original)
    

MobileLoader

MobileLoader用于加載第三方dylib在運行的應(yīng)用程序。啟動時MobileLoader會根據(jù)規(guī)則把指定目錄的第三方的動態(tài)庫加載進(jìn)去,第三方的動態(tài)庫也就是我們寫的破解程序。

safe mode

破解程序本質(zhì)是dylib寄生在別人進(jìn)程里。 系統(tǒng)進(jìn)程一旦出錯,可能導(dǎo)致整個進(jìn)程崩潰,崩潰后就會造成iOS癱瘓。所以CydiaSubstrate引入了安全模式,在安全模式下所有基于CydiaSubstratede的三方dylib都會被禁用,便于查錯與修復(fù)。

1.1.4 libffi

基于libbfi動態(tài)調(diào)用C函數(shù)。使用libffi中的ffi_closure_alloc構(gòu)造與原方法參數(shù)一致的"函數(shù)" (stingerIMP),以替換原方法函數(shù)指針;此外,生成了原方法和Block的調(diào)用的參數(shù)模板cifblockCif。方法調(diào)用時,最終會調(diào)用到void _st_ffi_function(ffi_cif *cif, void *ret, void **args, void *userdata), 在該函數(shù)內(nèi),可獲取到方法調(diào)用的所有參數(shù)、返回值位置,主要通過ffi_call根據(jù)cif調(diào)用原方法實現(xiàn)和切面block。AOPStingerBlockHook就是使用libbfi做的。

1.1.5 inlinehook 內(nèi)聯(lián)鉤子 (靜態(tài))

Inline Hook 就是在運行的流程中插入跳轉(zhuǎn)指令來搶奪運行流程的一個方法。大體分為三步:

  • 將原函數(shù)的前 N 個字節(jié)搬運到 Hook 函數(shù)的前 N 個字節(jié);
  • 然后將原函數(shù)的前 N 個字節(jié)填充跳轉(zhuǎn)到 Hook 函數(shù)的跳轉(zhuǎn)指令;
  • Hook 函數(shù)末尾幾個字節(jié)填充跳轉(zhuǎn)回原函數(shù) +N 的跳轉(zhuǎn)指令;
image.png
image.png

MSHookFunction就是inline hook。

基于 DobbyInline Hook。Dobby 是通過插入 __zDATA 段和__zTEXT 段到 Mach-O 中。

  • __zDATA 用來記錄 Hook 信息(Hook 數(shù)量、每個 Hook 方法的地址)、每個 Hook 方法的信息(函數(shù)地址、跳轉(zhuǎn)指令地址、寫 Hook 函數(shù)的接口地址)、每個 Hook 的接口(指針)。
  • __zText 用來記錄每個 Hook 函數(shù)的跳轉(zhuǎn)指令。

dobby
Dobby(原名:HOOKZz)是一個全平臺的inlineHook框架,它用起來就和fishhook一樣。
Dobby 通過 mmap 把整個 Mach-O 文件映射到用戶的內(nèi)存空間,寫入完成保存本地。所以 Dobby 并不是在原 Mach-O 上進(jìn)行操作,而是重新生成并替換。
Doddy

二 fishHook

2.1 fishhook的使用

fishhook源碼.h文件中只提供了兩個函數(shù)和一個結(jié)構(gòu)體rebinding。

rebind_symbols、rebind_symbols_image

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);

  • rebindings[]:存放rebinding結(jié)構(gòu)體的數(shù)組(可以同時交換多個函數(shù))。
  • rebindings_nelrebindings數(shù)組的長度。
  • slideASLR。
  • headerimageHeader。

只有兩個函數(shù)重新綁定符號,兩個函數(shù)的區(qū)別是一個指定image一個不指定。按照我們一般的理解放在前面的接口更常用,參數(shù)少的更簡單。

rebinding

struct rebinding {
  const char *name;//需要HOOK的函數(shù)名稱,C字符串
  void *replacement;//新函數(shù)的地址
  void **replaced;//原始函數(shù)地址的指針!
};
  • name:要HOOK的函數(shù)名稱,C字符串。
  • replacement:新函數(shù)的地址。(函數(shù)指針,也就是函數(shù)名稱)。
  • replaced:原始函數(shù)地址的指針。(二級指針)。

2.1.1 Hook NSLog

現(xiàn)在有個需求,Hook系統(tǒng)的NSLog函數(shù)。
Hook代碼:

- (void)hook_NSLog {
    struct rebinding rebindNSLog;
    rebindNSLog.name = "NSLog";
    rebindNSLog.replacement = HP_NSLog;
    rebindNSLog.replaced = (void *)&sys_NSLog;
    
    struct rebinding rebinds[] = {rebindNSLog};
    
    rebind_symbols(rebinds, 1);
}

//原函數(shù),函數(shù)指針
static void (*sys_NSLog)(NSString *format, ...);

//新函數(shù)
void HP_NSLog(NSString *format, ...) {
    format = [format stringByAppendingFormat:@"\n Hook"];
    //調(diào)用系統(tǒng)NSLog
    sys_NSLog(format);
}

調(diào)用:

    [self hook_NSLog];
    NSLog(@"hook_NSLog");

輸出:

hook_NSLog
Hook

這個時候就已經(jīng)HookNSLog,走到了HP_NSLog中。
Hook代碼調(diào)用完畢,sys_NSLog保存系統(tǒng)NSLog原地址,NSLog指向HP_NSLog。

2.1.2 Hook 自定義 C 函數(shù)

Hook一下自己的C函數(shù):

void func(const char * str) {
    NSLog(@"%s",str);
}

- (void)hook_func {
    struct rebinding rebindFunc;
    rebindFunc.name = "func";
    rebindFunc.replacement = HP_func;
    rebindFunc.replaced = (void *)&original_func;
    
    struct rebinding rebinds[] = {rebindFunc};
    
    rebind_symbols(rebinds, 1);
}
//原函數(shù),函數(shù)指針
static void (*original_func)(const char * str);

//新函數(shù)
void HP_func(const char * str) {
    NSLog(@"Hook func");
    original_func(str);
}

調(diào)用:

 [self hook_func];
func("HotPotCat");

輸出:

HotPotCat

這個時候可以看到?jīng)]有Hookfunc。

結(jié)論:自定義的函數(shù)fishhook hook 不了,系統(tǒng)的可以hook。

2.2 fishhook原理

fishHOOK可以HOOK C函數(shù),但是我們知道函數(shù)是靜態(tài)的,也就是說在編譯的時候,編譯器就知道了它的實現(xiàn)地址,這也是為什么C函數(shù)只寫函數(shù)聲明調(diào)用時會報錯。那么為什么fishhook還能夠改變C函數(shù)的調(diào)用呢?難道函數(shù)也有動態(tài)的特性存在?

是否意味著C Hook就必須修改調(diào)用地址?那意味著要修改二進(jìn)制。(原理上使用匯編可以實現(xiàn)。fishhook不是這么處理的)

那么系統(tǒng)函數(shù)和本地函數(shù)區(qū)別到底在哪里?

2.2.1 符號 & 符號綁定 & 符號表 & 重綁定符號

NSLog函數(shù)的地址在編譯的那一刻并不知道NSLog的真實地址。NSLogFoundation框架中。在運行時NSLog的地址在 共享緩存 中。在整個手機(jī)中只有dyld知道NSLog的真實地址。

LLVM編譯器生成MachO文件時,如果讓我們做就先空著系統(tǒng)函數(shù)的地址,等運行起來再替換。我們知道MachO中分為Text(只讀)和Data(可讀可寫),那么顯然這種方式行不通。

那么可行的方案是在Data段放一個 占位符(8字節(jié))讓代碼編譯的時候直接bl 占位符。在運行的時候dyld加載應(yīng)用的時候?qū)?code>Data段的地址修改為NSLog真實地址,代碼bl 占位符沒有變 。這個技術(shù)就叫做 PIC(position independent code`)位置無關(guān)代碼。(實際不是這么簡單)

  • 占位符 就叫做 符號。
  • dylddata段符號進(jìn)行修改的這個過程叫做 符號綁定
  • 一個又一個的符號放在一起形成了一個列表,叫做 符號表。

對于外部的C函數(shù)通過 符號地址 也就給了我們機(jī)會動態(tài)的Hook外部C函數(shù)。OC是修改SELIMP對應(yīng)的關(guān)系,符號 也是修改符號所對應(yīng)的地址。這個動作叫做 重新綁定符號表。這也就是fishhook``hook的原理。

2.2.2驗證

Hook NSLog前后分別調(diào)用NSLog:

    NSLog(@"before");
    [self hook_NSLog];
    NSLog(@"after");

image.png

MachO中我們能看到懶加載和非懶加載符號表,dyld綁定過程中綁定的是非懶加載符號和弱符號的。NSLog是懶加載符號,只有調(diào)用的時候才去綁定。

MachO中可以看到_NSLogData(值)是0000000100006960。offset為:0x8010
在第一個NSLog處打個斷點 運行查看:
主程序開始0x0000000100b24000ASLR0xb24000

0x0000000100b24000 + 0x8010中存儲的內(nèi)容為0x0100b2a960
0000000100006960 + 0xb24000 (ASLR) = 0x100B2A960。
所以這里就對應(yīng)上了。0x0100b2a960這個地址就是(符號表中的值其實是一個代碼的地址,指向本地代碼。)。

image.png

執(zhí)行完第一個NSLog后(hook前):

image.png

符號表指向了NSLog。
執(zhí)行完hook后:
image.png

符號表指向了HP_NSLog

這也就是fishhook能夠Hook的真正原因(修改懶加載符號表)。

2.3 符號綁定過程(間接)

剛才在上面NSLog第一次執(zhí)行之前我們拿到的地址0x0100b2a960實際上指向一段本地代碼,加上ASLR后執(zhí)行對應(yīng)地址的代碼然后就修改了懶加載符號表。

那么這個過程究竟是怎么做的呢?

先說明一些符號的情況:

  • 本地符號:只能本MachO用。
  • 全局符號:暴露給外面用。
  • 間接符號:當(dāng)我們要調(diào)用外部函數(shù)/方法時,在編譯時期地址是不知道的。比如系統(tǒng)的NSLog。

間接符號專門有個符號表Indirect Symbols

image.png

Symbols包含了所有的符號。

有以下代碼:

    NSLog(@"外部函數(shù)第一次調(diào)用");
    NSLog(@"外部函數(shù)第二次調(diào)用");

斷點斷到第一個NSLog,可以看到兩次調(diào)用NSLog是同一個地址0x100e12998

image.png

比首地址大0x0000000100e0c000,所以這個地址在本MachO中。
0x100e12998 - 0x0000000100e0c000 = 0x6998。

6998MachOSymbol Stubs中:

image.png

這個就是NSLog的樁(外部符號的樁),值為1F2003D570B3005800021FD6(代碼),這個代碼是:
image.png

這個時候就對應(yīng)上了:


image.png

執(zhí)行樁中的代碼:


image.png

這段代碼的意思是執(zhí)行樁中的代碼找到符號表中代碼跳轉(zhuǎn)執(zhí)行(0000000100006A28)。

6A28這段代碼在__stub_helper中:

image.png

這里執(zhí)行的是符號綁定。

繼續(xù)動態(tài)調(diào)試:

image.png

這塊是剛好對應(yīng)上。
繼續(xù)進(jìn)去:
image.png

繼續(xù)進(jìn)去:
image.png

對應(yīng)上了。實際上執(zhí)行的是dyld_stub_binder。也就是說懶加載符號表里面的初始值都是執(zhí)行符號綁定的函數(shù)。

dyld_stub_binder是外部函數(shù),那么怎么得到的dyld_stub_binder函數(shù)呢?

image.png

MachOx160x100008000:

image.png

這個符號在非懶加載表中(一運行就綁定):

image.png

所以dyld_stub_binder是通過去非懶加載表中查找。
驗證 :
image.png

驗證確認(rèn),No-Lazy Symbol Pointers表中默認(rèn)值是0。

符號綁定過程:

  • 程序一運行,先綁定No-Lazy Symbol Pointers表中dyld_stub_binder的值。
  • 調(diào)用NSLog先找樁,執(zhí)行樁中的代碼。樁中的代碼是找懶加載符號表中的代碼去執(zhí)行。
  • 懶加載符號表中的初始值是本地的源代碼,這個代碼去NoLazy表中找綁定函數(shù)地址。
  • 進(jìn)入dyldbinder函數(shù)進(jìn)行綁定。

binder函數(shù)執(zhí)行完畢后就調(diào)用第一次的NSLog了。這個時候再看一下懶加載符號表中的符號:

image.png

符號已經(jīng)變了。這個時候符號就已經(jīng)綁定成功了。

接著執(zhí)行第二次NSLog,這個時候依然是去找樁中的代碼執(zhí)行:

image.png

這個繼續(xù)執(zhí)行就執(zhí)行到Foundation框架的NSLog了(已經(jīng)綁定過了,不需要繼續(xù)綁定):

image.png

這個時候通過樁直接跳到了真實地址(還是虛擬的)。這個做的原因是符號表中保存地址執(zhí)行代碼,代碼是保存在代碼段的(樁)。

間接符號綁定流程
  • 外部函數(shù)調(diào)用時執(zhí)行樁中的代碼(__TEXT,__stubs)。
  • 樁中的代碼去懶加載符號表中找地址執(zhí)行(__DATA,__la_symbo_ptrl)。
    • 通過懶加載符號表中的地址去執(zhí)行。要么直接調(diào)用函數(shù)地址(綁定過了),要么去__TEXT,__stubhelper中找綁定函數(shù)進(jìn)行綁定。懶加載符號表中默認(rèn)保存的是尋找binder的代碼。
  • 懶加載中的代碼去__TEXT,__stubhelper中執(zhí)行綁定代碼(binder函數(shù))。
  • 綁定函數(shù)在非懶加載符號表中(__DATA._got),程序運行就綁定好了dyld

2.4 通過符號找字符串

上面使用fishhook的時候我們是通過rebindNSLog.name = "NSLog";告訴fishhookhook NSLog。那么fishhook通過NSLog怎么找到的符號的呢?

首先,我們清楚在綁定的時候是去Lazy Symbol中去找的NSLog對應(yīng)的綁定代碼:

image.png

找的是0x00008008這個地址,在Lazy SymbolNSLog排在第一個。

Indirect Symbols中可以看到順序和Lazy Symbols中相同,也就是要找Lazy Symbols中的符號,只要找到Indirect Symbols中對應(yīng)的第幾個就可以了。

image.png

那么怎么確認(rèn)Indirect Symbols中的第幾個呢?
Indirect Symbolsdata對應(yīng)值(十六進(jìn)制)這里NSLog101,這個代表著NSLog在總的符號表(Symbols)中的角標(biāo):

image.png

在這里我們可以看到NSLogString Table中偏移為0x98(十六進(jìn)制)。

image.png

通過偏移值計算得到0xCC38就確認(rèn)到了_NSLog(長度+首地址)。

這里通過.隔開,函數(shù)名前面有_。

這樣我們就從Lazy Symbols -> Indirect Symbols -> Symbols - > String Table 通過符號找到了字符串。那么fishhook的過程就是這么處理的,通過遍歷所有符號和要hook的數(shù)組中的字符串做對比。

fishhook中有一張圖說明這個關(guān)系:

fishhook close函數(shù)查找流程

這里是通過符號查找close字符串。

  1. Lazy Symbol Pointer Tableclose index1061。
  2. Indirect Symbol Table 1061 對應(yīng)的角標(biāo)為0X00003fd7(十進(jìn)制16343)。
  3. Symbol Table找角標(biāo)16343對應(yīng)的字符串表中的偏移值70026。
  4. String Table中找首地址+偏移值(70026)就找到了close
    字符串。

實際的原理還是通過傳遞的字符串找到符號進(jìn)行替換:通過字符串找符號過程:

  1. String Table中找到字符串計算偏移值。
  2. 通過偏移值在Symbols中找到角標(biāo)。
  3. 通過角標(biāo)在Indirect Symbols中找到對應(yīng)的符號。這個時候就能拿到這個符號的index了。
  4. 通過找到的indexLazy Symbols中找到對應(yīng)index的符號。

2.5 去掉符號&恢復(fù)符號

符號本身在MachO文件中,占用包體積大小 ,在我們分析別人的App時符號是去掉的。

2.5.1 去除符號

符號基本分為:全局符號、間接符號(導(dǎo)出&導(dǎo)入)、本地符號。
對于App來說會去掉所有符號(間接符號除外)。對于動態(tài)庫來說要保留全局符號(外部要調(diào)用)。

去掉符號在Build setting中設(shè)置:

image.png

  • Deployment Postprocessing:設(shè)置為YES則在編譯階段去符號,否則在打包階段去符號。
  • Strip StyleAll Symbols去掉所有符號(間接除外),Non-Global Symbols去掉除全局符號外的符號。Debugging Symbols去掉調(diào)試符號。

設(shè)置Deployment PostprocessingYESStrip StyleAll Symbols。編譯查看多了一個.bcsymbolmap文件,這個文件就是bitcode

image.png

這個時候的MachO文件中Symbols就只剩下間接符號表中的符號了:

image.png

其中value為函數(shù)的實現(xiàn)地址(imp)。間接符號不會找到符號表中地址執(zhí)行,是找Lazy Symbol Table中的地址。

代碼中打斷點就斷不住了:


image.png

要斷住NSLog就要打符號斷點了:

image.png

bt看下調(diào)用棧:

image.png

發(fā)現(xiàn)自定義方法全是unnamed,這個很明顯就是去掉符號的。這種情況下就不好分析代碼了。
如果是oc方法調(diào)用則直接讀取x0,x1就能獲取selfcmd:
image.png

在這里我們就要下斷點在方法調(diào)用之前,可以通過下地址斷點。
image.png

先計算出偏移值,下次直接ASLR+偏移值直接斷點。這個也就是動態(tài)調(diào)試常用的方法。

2.5.2 恢復(fù)符號

前面動態(tài)調(diào)試下斷點比較麻煩,如果能恢復(fù)符號的話就方便很多了。
在上面的例子中去掉所有符號后Symbol Table中只有間接符號了。雖然符號表中沒有了,但是類列表和方法列表中依然存在。

image.png

這也就為我們提供了創(chuàng)建Symbol Table的機(jī)會。
可以通過restore-symbol工具恢復(fù)符號(只能恢復(fù)oc的,runtime機(jī)制導(dǎo)致):./restore-symbol 原始Macho文件 -o 恢復(fù)后文件

./restore-symbol FishHookDemo -o recoverDemo

image.png

這個時候就恢復(fù)了,查看MachO(恢復(fù)的符號在Symbol Table后面):
image.png

這個時候就可以重簽名后進(jìn)行動態(tài)調(diào)試了。
restore-symbol地址

2.6 fishhook源碼解析

rebind_symbols
rebind_symbols的實現(xiàn):

//第一次是拿dyld的回調(diào),之后是手動拿到所有image去調(diào)用。這里因為沒有指定image所以需要拿到所有的。
int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel) {
    //prepend_rebindings的函數(shù)會將整個 rebindings 數(shù)組添加到 _rebindings_head 這個鏈表的頭部
    //Fishhook采用鏈表的方式來存儲每一次調(diào)用rebind_symbols傳入的參數(shù),每次調(diào)用,就會在鏈表的頭部插入一個節(jié)點,鏈表的頭部是:_rebindings_head
    int retval = prepend_rebindings(&_rebindings_head, rebindings, rebindings_nel);
    //根據(jù)上面的prepend_rebinding來做判斷,如果小于0的話,直接返回一個錯誤碼回去
    if (retval < 0) {
    return retval;
  }
  //根據(jù)_rebindings_head->next是否為空判斷是不是第一次調(diào)用。
  if (!_rebindings_head->next) {
      //第一次調(diào)用的話,調(diào)用_dyld_register_func_for_add_image注冊監(jiān)聽方法.
      //已經(jīng)被dyld加載的image會立刻進(jìn)入回調(diào)。之后的image會在dyld裝載的時候觸發(fā)回調(diào)。這里相當(dāng)于注冊了一個回調(diào)到 _rebind_symbols_for_image 函數(shù)。
    _dyld_register_func_for_add_image(_rebind_symbols_for_image);
  } else {
    //不是第一次調(diào)用,遍歷已經(jīng)加載的image,進(jìn)行的hook
    uint32_t c = _dyld_image_count();//這個相當(dāng)于 image list count
    for (uint32_t i = 0; i < c; i++) {
        //遍歷重新綁定image header  aslr
      _rebind_symbols_for_image(_dyld_get_image_header(i), _dyld_get_image_vmaddr_slide(i));
    }
  }
  return retval;
}
  • 首先通過prepend_rebindings函數(shù)生成鏈表,存放所有要Hook的函數(shù)。
  • 根據(jù)_rebindings_head->next是否為空判斷是不是第一次調(diào)用,第一次調(diào)用走系統(tǒng)的回調(diào),第二次則自己獲取所有的image list進(jìn)行遍歷。
  • 最后都會走_rebind_symbols_for_image函數(shù)。
    image list驗證:
    image.png

_rebind_symbols_for_image

//兩個參數(shù) header  和 ASLR
static void _rebind_symbols_for_image(const struct mach_header *header,
                                      intptr_t slide) {
    //_rebindings_head 參數(shù)是要交換的數(shù)據(jù),head的頭
    rebind_symbols_for_image(_rebindings_head, header, slide);
}

這里直接調(diào)用了rebind_symbols_for_image,傳遞了head鏈表地址。

rebind_symbols_image

int rebind_symbols_image(void *header,
                         intptr_t slide,
                         struct rebinding rebindings[],
                         size_t rebindings_nel) {
    struct rebindings_entry *rebindings_head = NULL;
    int retval = prepend_rebindings(&rebindings_head, rebindings, rebindings_nel);
    //如果指定image就直接調(diào)用了 rebind_symbols_for_image,沒有遍歷了。
    rebind_symbols_for_image(rebindings_head, (const struct mach_header *) header, slide);
    if (rebindings_head) {
      free(rebindings_head->rebindings);
    }
    free(rebindings_head);
    return retval;
}

底層和rebind_symbols都調(diào)用到了rebind_symbols_for_image,由于給定了image所以不需要循環(huán)遍歷。

rebind_symbols_for_image

//回調(diào)的最終就是這個函數(shù)! 三個參數(shù):要交換的數(shù)組  、 image的頭 、 ASLR的偏移
static void rebind_symbols_for_image(struct rebindings_entry *rebindings,
                                     const struct mach_header *header,
                                     intptr_t slide) {
    
    /*dladdr() 可確定指定的address 是否位于構(gòu)成進(jìn)程的進(jìn)址空間的其中一個加載模塊(可執(zhí)行庫或共享庫)內(nèi),如果某個地址位于在其上面映射加載模塊的基址和為該加載模塊映射的最高虛擬地址之間(包括兩端),則認(rèn)為該地址在加載模塊的范圍內(nèi)。如果某個加載模塊符合這個條件,則會搜索其動態(tài)符號表,以查找與指定的address 最接近的符號。最接近的符號是指其值等于,或最為接近但小于指定的address 的符號。
     */
    /*
     如果指定的address 不在其中一個加載模塊的范圍內(nèi),則返回0 ;且不修改Dl_info 結(jié)構(gòu)的內(nèi)容。否則,將返回一個非零值,同時設(shè)置Dl_info 結(jié)構(gòu)的字段。
     如果在包含address 的加載模塊內(nèi),找不到其值小于或等于address 的符號,則dli_sname 、dli_saddr 和dli_size字段將設(shè)置為0 ; dli_bind 字段設(shè)置為STB_LOCAL , dli_type 字段設(shè)置為STT_NOTYPE 。
     */
    
//    typedef struct dl_info {
//            const char      *dli_fname; //image 鏡像路徑
//            void            *dli_fbase; //鏡像基地址
//            const char      *dli_sname; //函數(shù)名字
//            void            *dli_saddr; //函數(shù)地址
//    } Dl_info;
    
  Dl_info info;//拿到image的信息
  //dladdr函數(shù)就是在程序里面找header
  if (dladdr(header, &info) == 0) {
    return;
  }
  //準(zhǔn)備從MachO里面去找!
  segment_command_t *cur_seg_cmd;//臨時變量
  //這里與MachOView中看到的對應(yīng)
  segment_command_t *linkedit_segment = NULL;//SEG_LINKEDIT
  struct symtab_command* symtab_cmd = NULL;//LC_SYMTAB 符號表地址
  struct dysymtab_command* dysymtab_cmd = NULL;//LC_DYSYMTAB 動態(tài)符號表地址
  //cur為了跳過header的大小,找loadCommands cur = 首地址 + mach_header大小
  uintptr_t cur = (uintptr_t)header + sizeof(mach_header_t);
  //循環(huán)load commands找對應(yīng)的 SEG_LINKEDIT LC_SYMTAB LC_DYSYMTAB
  for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) {
    cur_seg_cmd = (segment_command_t *)cur;
    //這里`SEG_LINKEDIT`獲取和`LC_SYMTAB`與`LC_DYSYMTAB`不同是因為在`MachO`中分別對應(yīng)`LC_SEGMENT_64(__LINKEDIT)`、`LC_SYMTAB`、`LC_DYSYMTAB`
    if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) {
      if (strcmp(cur_seg_cmd->segname, SEG_LINKEDIT) == 0) {
        linkedit_segment = cur_seg_cmd;
      }
    } else if (cur_seg_cmd->cmd == LC_SYMTAB) {
      symtab_cmd = (struct symtab_command*)cur_seg_cmd;
    } else if (cur_seg_cmd->cmd == LC_DYSYMTAB) {
      dysymtab_cmd = (struct dysymtab_command*)cur_seg_cmd;
    }
  }
  //有任何一項為空就直接返回,nindirectsyms表示間接符號表中符號數(shù)量,沒有則直接返回
  if (!symtab_cmd || !dysymtab_cmd || !linkedit_segment ||
      !dysymtab_cmd->nindirectsyms) {
    return;
  }

  // Find base symbol/string table addresses
  //符號表和字符串表都屬于data段中的linkedit,所以以linkedit基址+偏移量去獲取地址(這里的偏移量不是整個macho的偏移量,是相對基址的偏移量)
  //鏈接時程序的基址 = __LINKEDIT.VM_Address -__LINKEDIT.File_Offset + silde的改變值
  uintptr_t linkedit_base = (uintptr_t)slide + linkedit_segment->vmaddr - linkedit_segment->fileoff;
  //printf("地址:%p\n",linkedit_base);
  //符號表的地址 = 基址 + 符號表偏移量
  nlist_t *symtab = (nlist_t *)(linkedit_base + symtab_cmd->symoff);
  //字符串表的地址 = 基址 + 字符串表偏移量
  char *strtab = (char *)(linkedit_base + symtab_cmd->stroff);

  // Get indirect symbol table (array of uint32_t indices into symbol table)
  //動態(tài)(間接)符號表地址 = 基址 + 動態(tài)符號表偏移量
  uint32_t *indirect_symtab = (uint32_t *)(linkedit_base + dysymtab_cmd->indirectsymoff);

  cur = (uintptr_t)header + sizeof(mach_header_t);
  for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) {
    cur_seg_cmd = (segment_command_t *)cur;
    if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) {
        //尋找到load command 中的data【LC_SEGEMNT_64(__DATA)】,相當(dāng)于拿到data段的首地址
      if (strcmp(cur_seg_cmd->segname, SEG_DATA) != 0 &&
          strcmp(cur_seg_cmd->segname, SEG_DATA_CONST) != 0) {
        continue;
      }
        
      for (uint j = 0; j < cur_seg_cmd->nsects; j++) {
        section_t *sect =
          (section_t *)(cur + sizeof(segment_command_t)) + j;
          //找懶加載表(lazy symbol table)
        if ((sect->flags & SECTION_TYPE) == S_LAZY_SYMBOL_POINTERS) {
          //找到直接調(diào)用函數(shù) perform_rebinding_with_section,這里4張表就都已經(jīng)找到了。傳入要hook的數(shù)組、ASLR、以及4張表
          perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab);
        }
          //非懶加載表(Non-Lazy symbol table)
        if ((sect->flags & SECTION_TYPE) == S_NON_LAZY_SYMBOL_POINTERS) {
          perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab);
        }
      }
    }
  }
}
  • 找到SEG_LINKEDIT、LC_SYMTAB、LC_DYSYMTABload commans。

SEG_LINKEDIT獲取和LC_SYMTABLC_DYSYMTAB不同是因為在Load Commands中本來就不同,我們解析其它字段也要做類似操作。具體如下:

image.png

  • 根據(jù)linkedit和偏移值分別找到符號表的地址字符串表的地址以及間接符號表地址。
  • 遍歷load commandsdata段找到懶加載符號表非懶加載符號表。
  • 找到表的同時就直接調(diào)用perform_rebinding_with_section進(jìn)行hook替換函數(shù)符號。

perform_rebinding_with_section

//rebindings:要hook的函數(shù)鏈表,可以理解為數(shù)組
//section:懶加載/非懶加載符號表地址
//slide:ASLR
//symtab:符號表地址
//strtab:字符串標(biāo)地址
//indirect_symtab:動態(tài)(間接)符號表地址
static void perform_rebinding_with_section(struct rebindings_entry *rebindings,
                                           section_t *section,
                                           intptr_t slide,
                                           nlist_t *symtab,
                                           char *strtab,
                                           uint32_t *indirect_symtab) {
  //nl_symbol_ptr和la_symbol_ptrsection中的reserved1字段指明對應(yīng)的indirect symbol table起始的index。也就是第幾個這里是和間接符號表中相對應(yīng)的
  //這里就拿到了index
  uint32_t *indirect_symbol_indices = indirect_symtab + section->reserved1;
  //slide+section->addr 就是符號對應(yīng)的存放函數(shù)實現(xiàn)的數(shù)組也就是我相應(yīng)的__nl_symbol_ptr和__la_symbol_ptr相應(yīng)的函數(shù)指針都在這里面了,所以可以去尋找到函數(shù)的地址。
  //indirect_symbol_bindings中是數(shù)組,數(shù)組中是函數(shù)指針。相當(dāng)于lazy和non-lazy中的data
  void **indirect_symbol_bindings = (void **)((uintptr_t)slide + section->addr);
  //遍歷section里面的每一個符號(懶加載/非懶加載)
  for (uint i = 0; i < section->size / sizeof(void *); i++) {
    //找到符號在Indrect Symbol Table表中的值
    //讀取indirect table中的數(shù)據(jù)
    uint32_t symtab_index = indirect_symbol_indices[i];
    if (symtab_index == INDIRECT_SYMBOL_ABS || symtab_index == INDIRECT_SYMBOL_LOCAL ||
        symtab_index == (INDIRECT_SYMBOL_LOCAL   | INDIRECT_SYMBOL_ABS)) {
      continue;
    }
      //以symtab_index作為下標(biāo),訪問symbol table,拿到string table 的偏移值
      uint32_t strtab_offset = symtab[symtab_index].n_un.n_strx;
      //獲取到symbol_name 首地址 + 偏移值。(char* 字符的地址)
      char *symbol_name = strtab + strtab_offset;
      //判斷是否函數(shù)的名稱是否有兩個字符,因為函數(shù)前面有個_,所以方法的名稱最少要1個
      bool symbol_name_longer_than_1 = symbol_name[0] && symbol_name[1];
      //遍歷最初的鏈表,來判斷名字進(jìn)行hook
      struct rebindings_entry *cur = rebindings;
      while (cur) {
          for (uint j = 0; j < cur->rebindings_nel; j++) {
              //這里if的條件就是判斷從symbol_name[1]兩個函數(shù)的名字是否都是一致的,以及判斷字符長度是否大于1
              if (symbol_name_longer_than_1 &&
                  strcmp(&symbol_name[1], cur->rebindings[j].name) == 0) {
                  //判斷replaced的地址不為NULL  要替換的自己實現(xiàn)的方法和rebindings[j].replacement的方法不一致。
                  if (cur->rebindings[j].replaced != NULL &&
                      indirect_symbol_bindings[i] != cur->rebindings[j].replacement) {
                      //讓rebindings[j].replaced保存indirect_symbol_bindings[i]的函數(shù)地址,相當(dāng)于將原函數(shù)地址給到你定義的指針的指針。
                      *(cur->rebindings[j].replaced) = indirect_symbol_bindings[i];
                  }
                  //替換內(nèi)容為自己自定義函數(shù)地址,這里就相當(dāng)于替換了內(nèi)存中的地址,下次樁直接找到lazy/non-lazy表的時候直接就走這個替換的地址了。
                  indirect_symbol_bindings[i] = cur->rebindings[j].replacement;
                  //替換完成跳轉(zhuǎn)外層循環(huán),到(懶加載/非懶加載)數(shù)組的下一個數(shù)據(jù)。
                  goto symbol_loop;
              }
          }
      //沒有找到就找自己要替換的函數(shù)數(shù)組的下一個函數(shù)。
      cur = cur->next;
    }
    symbol_loop:;
  }
}
  • 首先通過懶加載/非懶加載符號表和間接符號表找到所有的index
  • 將懶加載/非懶加載符號表的data放入indirect_symbol_bindings數(shù)組中。

indirect_symbol_bindings就是存放lazynon-lazy表中的data數(shù)組:

image.png

  • 遍歷懶加載/非懶加載符號表。
    • 讀取indirect_symbol_indices找到符號在Indrect Symbol Table表中的值放入symtab_index。
    • symtab_index作為下標(biāo),訪問symbol table,拿到string table的偏移值。
    • 根據(jù) strtab_offset偏移值獲取字符地址symbol_name,也就相當(dāng)于字符名稱。
    • 循環(huán)遍歷rebindings也就是鏈表(自定義的Hook數(shù)據(jù))
    • 判斷&symbol_name[1]rebindings[j].name兩個函數(shù)的名字是否都是一致的,以及判斷字符長度是否大于1。
    • 相同則先保存原地址到自定義函數(shù)指針(如果replaced傳值的話,沒有傳則不保存)。并且用要Hook的目標(biāo)函數(shù)replacement替換indirect_symbol_bindings,這里就完成了Hook
  • reserved1確認(rèn)了懶加載和非懶加載符號在間接符號表中的index值。

疑問點:懶加載和非懶加載怎么和間接符號表index對應(yīng)的呢?
直接Hook dyld_stub_binder以及NSLog看下index對應(yīng)的值:

image.png

在間接符號表中非懶加載符號從20開始供兩個,懶加載從22開始,這也就對應(yīng)上了。這也就驗證了懶加載和非懶加載符號都在間接符號表中能對應(yīng)上。

demo

總結(jié)

image.png

libffi
inlinehook
http://www.itdecent.cn/p/7954e6cde245
越獄和防護(hù)相關(guān)內(nèi)容
https://www.desgard.com/2020/08/05/why-hook-msg_objc-can-use-asm-2.html
https://www.die.lu/core/index.php/2020/05/30/299/
dobby

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • HOOK概述 HOOK,中文譯為“掛鉤”或“鉤子”。在iOS逆向中是指改變程序運行流程的一種技術(shù)。通過HOOK可以...
    帥駝駝閱讀 1,524評論 0 3
  • 注入小結(jié) 通過之前的學(xué)習(xí),我們知道了利用動態(tài)庫注入的兩種方式: 注入 App 后,使得 項目和動態(tài)庫產(chǎn)生關(guān)聯(lián)關(guān)系。...
    Superman168閱讀 14,416評論 4 5
  • iOS 開發(fā)中幾種常見的Hook 方式 Method Swizzle fishhook Cydia Substra...
    一川煙草i蓑衣閱讀 2,502評論 1 5
  • 本篇是iOS開發(fā)高手課讀書筆記第一篇 fishoook fishoook[https://github.com/f...
    forping閱讀 1,515評論 0 4
  • HOOK,中文譯為“掛鉤”或“鉤子”。在iOS逆向中是指改變程序運行流程的一種技術(shù)。通過hook可以讓別人的程序執(zhí)...
    鼬殿閱讀 1,641評論 0 2

友情鏈接更多精彩內(nèi)容