iOS Hook原理(一)- fishhook

一后众、Hook概述

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

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


搶紅包Hook示意圖

1.1Hook的幾種方式

iOSHOOK技術(shù)的大致上分為5種:Method Swizzle谜叹、fishhookCydia Substrate搬葬、libffi荷腊、inlinehook

1.1.1 Method Swizzle (OC)

利用OCRuntime特性急凰,動態(tài)改變SEL(方法編號)和IMP(方法實(shí)現(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種方式實(shí)現(xiàn)方法交換:

  • method_exchangeImplementations:在分類中直接交換就可以了歧沪,如果不在分類需要配合class_addMethod實(shí)現(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è)計(jì)影钉,安卓一樣可以用画髓。Cydia Substrate官方

Cydia Substrate主要分為3部分:Mobile HookerMobileLoader平委、safe 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ù) MSHookFunction肉微。 Logos語法的%hook就是對這個函數(shù)做了一層封裝。

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

MobileLoader

MobileLoader用于加載第三方dylib在運(yùn)行的應(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)用原方法實(shí)現(xiàn)和切面blockAOPStingerBlockHook就是使用libbfi做的。

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

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

  • 將原函數(shù)的前 N 個字節(jié)搬運(yùn)到 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 HookDobby 是通過插入 __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)的喂柒,也就是說在編譯的時候,編譯器就知道了它的實(shí)現(xiàn)地址禾嫉,這也是為什么C函數(shù)只寫函數(shù)聲明調(diào)用時會報(bào)錯灾杰。那么為什么fishhook還能夠改變C函數(shù)的調(diào)用呢?難道函數(shù)也有動態(tài)的特性存在熙参?

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

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

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

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

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

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

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

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

2.2.2驗(yàn)證

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處打個斷點(diǎn) 運(yùn)行查看:
主程序開始0x0000000100b24000送漠,ASLR0xb24000

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

image.png

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

image.png

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

符號表指向了HP_NSLog谓媒。

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

2.3 符號綁定過程(間接)

剛才在上面NSLog第一次執(zhí)行之前我們拿到的地址0x0100b2a960實(shí)際上指向一段本地代碼何乎,加上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)用");

斷點(diǎn)斷到第一個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)上了。實(shí)際上執(zhí)行的是dyld_stub_binder酥艳。也就是說懶加載符號表里面的初始值都是執(zhí)行符號綁定的函數(shù)摊溶。

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

image.png

MachOx160x100008000:

image.png

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

image.png

所以dyld_stub_binder是通過去非懶加載表中查找更扁。
驗(yàn)證 :
image.png

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

符號綁定過程:

  • 程序一運(yùn)行浓镜,先綁定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

這個時候通過樁直接跳到了真實(shí)地址(還是虛擬的)。這個做的原因是符號表中保存地址執(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)瓤湘,程序運(yùn)行就綁定好了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

通過偏移值計(jì)算得到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
    字符串唧取。

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

  1. String Table中找到字符串計(jì)算偏移值。
  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 PostprocessingYES萍虽,Strip StyleAll Symbols睛廊。編譯查看多了一個.bcsymbolmap文件,這個文件就是bitcode杉编。

image.png

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

image.png

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

代碼中打斷點(diǎn)就斷不住了:


image.png

要斷住NSLog就要打符號斷點(diǎn)了:

image.png

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

image.png

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

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

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

2.5.2 恢復(fù)符號

前面動態(tài)調(diào)試下斷點(diǎn)比較麻煩戳寸,如果能恢復(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的實(shí)現(xiàn):

//第一次是拿dyld的回調(diào)锈拨,之后是手動拿到所有image去調(diào)用砌庄。這里因?yàn)闆]有指定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é)點(diǎn)佛纫,鏈表的頭部是:_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驗(yàn)證:
    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`不同是因?yàn)樵赻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;
    }
  }
  //有任何一項(xiàng)為空就直接返回稀拐,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不同是因?yàn)樵?code>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ù)實(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ù)的名稱是否有兩個字符,因?yàn)楹瘮?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  要替換的自己實(shí)現(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值。

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

image.png

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

demo

總結(jié)

image.png

libffi
inlinehook
http://www.reibang.com/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)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市瘦癌,隨后出現(xiàn)的幾起案子猪贪,更是在濱河造成了極大的恐慌,老刑警劉巖讯私,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件热押,死亡現(xiàn)場離奇詭異,居然都是意外死亡斤寇,警方通過查閱死者的電腦和手機(jī)桶癣,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來娘锁,“玉大人牙寞,你說我怎么就攤上這事∧眩” “怎么了间雀?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長镊屎。 經(jīng)常有香客問我惹挟,道長,這世上最難降的妖魔是什么杯道? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任匪煌,我火速辦了婚禮责蝠,結(jié)果婚禮上党巾,老公的妹妹穿的比我還像新娘萎庭。我一直安慰自己,他們只是感情好齿拂,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布驳规。 她就那樣靜靜地躺著,像睡著了一般署海。 火紅的嫁衣襯著肌膚如雪吗购。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天砸狞,我揣著相機(jī)與錄音捻勉,去河邊找鬼。 笑死刀森,一個胖子當(dāng)著我的面吹牛踱启,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播研底,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼埠偿,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了榜晦?” 一聲冷哼從身側(cè)響起冠蒋,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎乾胶,沒想到半個月后抖剿,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡识窿,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年牙躺,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片腕扶。...
    茶點(diǎn)故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡孽拷,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出半抱,到底是詐尸還是另有隱情脓恕,我是刑警寧澤,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布窿侈,位于F島的核電站炼幔,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏史简。R本人自食惡果不足惜乃秀,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧跺讯,春花似錦枢贿、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至愈污,卻和暖如春耀态,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背暂雹。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工首装, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人杭跪。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓仙逻,卻偏偏與公主長得像,于是被迫代替她去往敵國和親揍魂。 傳聞我的和親對象是個殘疾皇子桨醋,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評論 2 345

推薦閱讀更多精彩內(nèi)容

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