iOS Hook原理(一)- fishhook

一、Hook概述

HOOK中文译为挂钩钩子。在iOS逆向中是指改变程序运行流程的一种技术。通过hook可以让别人的程序执行自己所写的代码。在逆向中经常使用这种技术。只有了解其原理才能够对恶意代码进行有效的防护。

比如很久之前的微信自动抢红包插件:


抢红包Hook示意图

1.1Hook的几种方式

iOSHOOK技术的大致上分为5种:Method SwizzlefishhookCydia Substratelibffiinlinehook

1.1.1 Method Swizzle (OC)

利用OCRuntime特性,动态改变SEL(方法编号)和IMP(方法实现)的对应关系,达到OC方法调用流程改变的目的。主要用于OC方法。

可以将SELIMP 之间的关系理解为一本书的目录SEL 就像标题IMP就像页码。他们是一一对应的关系。(书的目录不一定一一对应,可能页码相同,理解就行。)。

image.png

Runtime提供了交换两个SELIMP对应关系的函数:

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

通过这个函数交换两个SELIMP对应关系的技术,称之为Method Swizzle(方法欺骗)

Method Swizzle

runtime中有3种方式实现方法交换:

  • method_exchangeImplementations:在分类中直接交换就可以了,如果不在分类需要配合class_addMethod实现原方法的回调。
  • class_replaceMethod:直接替换原方法。
  • method_setImplementation:重新赋值原方法,通过getImpsetImp配合。

runtime都比较熟悉,就不多介绍了,不是很了解使用的可以参考:第4部分代码注入

1.1.2 fishhook (外部函数)

Facebook提供的一个动态修改链接mach-O文件的工具。利用MachO文件加载原理,通过修改懒加载非懒加载两个表的指针达到C(系统C函数)函数HOOK的目的。fishhook

总结下来是:dyld 更新 Mach-O 二进制的 __DATA segment__la_symbol_str 中的指针,使用 rebind_symbol方法更新两个符号位置来进行符号的重新绑定。

1.1.3 Cydia Substrate

Cydia Substrate 原名为 Mobile Substrate ,主要作用是针对OC方法C函数以及函数地址进行HOOK操作。并不仅仅针对iOS而设计,安卓一样可以用。Cydia Substrate官方

Cydia Substrate主要分为3部分:Mobile HookerMobileLoadersafe mode

Mobile Hooker

它定义了一系列的宏和函数,底层调用objcruntimefishhook来替换系统或者目标应用的函数。其中有两个函数:

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

    void MSHookMessageEx(Class class, SEL selector, IMP replacement, IMP result) 
    
  • MSHookFunction :(inline hook)主要作用于CC++函数 MSHookFunction。 Logos语法的%hook就是对这个函数做了一层封装。

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

MobileLoader

MobileLoader用于加载第三方dylib在运行的应用程序。启动时MobileLoader会根据规则把指定目录的第三方的动态库加载进去,第三方的动态库也就是我们写的破解程序。

safe mode

破解程序本质是dylib寄生在别人进程里。 系统进程一旦出错,可能导致整个进程崩溃,崩溃后就会造成iOS瘫痪。所以CydiaSubstrate引入了安全模式,在安全模式下所有基于CydiaSubstratede的三方dylib都会被禁用,便于查错与修复。

1.1.4 libffi

基于libbfi动态调用C函数。使用libffi中的ffi_closure_alloc构造与原方法参数一致的"函数" (stingerIMP),以替换原方法函数指针;此外,生成了原方法和Block的调用的参数模板cifblockCif。方法调用时,最终会调用到void _st_ffi_function(ffi_cif *cif, void *ret, void **args, void *userdata), 在该函数内,可获取到方法调用的所有参数、返回值位置,主要通过ffi_call根据cif调用原方法实现和切面blockAOP库 Stinger和BlockHook就是使用libbfi做的。

1.1.5 inlinehook 内联钩子 (静态)

Inline Hook 就是在运行的流程中插入跳转指令来抢夺运行流程的一个方法。大体分为三步:

  • 将原函数的前 N 个字节搬运到 Hook 函数的前 N 个字节;
  • 然后将原函数的前 N 个字节填充跳转到 Hook 函数的跳转指令;
  • Hook 函数末尾几个字节填充跳转回原函数 +N 的跳转指令;
image.png
image.png

MSHookFunction就是inline hook

基于 DobbyInline HookDobby 是通过插入 __zDATA 段和__zTEXT 段到 Mach-O 中。

  • __zDATA 用来记录 Hook 信息(Hook 数量、每个 Hook 方法的地址)、每个 Hook 方法的信息(函数地址、跳转指令地址、写 Hook 函数的接口地址)、每个 Hook 的接口(指针)。
  • __zText 用来记录每个 Hook 函数的跳转指令。

dobby
Dobby(原名:HOOKZz)是一个全平台的inlineHook框架,它用起来就和fishhook一样。
Dobby 通过 mmap 把整个 Mach-O 文件映射到用户的内存空间,写入完成保存本地。所以 Dobby 并不是在原 Mach-O 上进行操作,而是重新生成并替换。
Doddy

二 fishHook

2.1 fishhook的使用

fishhook源码.h文件中只提供了两个函数和一个结构体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结构体的数组(可以同时交换多个函数)。
  • rebindings_nelrebindings数组的长度。
  • slideASLR
  • headerimageHeader

只有两个函数重新绑定符号,两个函数的区别是一个指定image一个不指定。按照我们一般的理解放在前面的接口更常用,参数少的更简单。

rebinding

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

2.1.1 Hook NSLog

现在有个需求,Hook系统的NSLog函数。
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);
}

//原函数,函数指针
static void (*sys_NSLog)(NSString *format, ...);

//新函数
void HP_NSLog(NSString *format, ...) {
    format = [format stringByAppendingFormat:@"\n Hook"];
    //调用系统NSLog
    sys_NSLog(format);
}

调用:

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

输出:

hook_NSLog
Hook

这个时候就已经HookNSLog,走到了HP_NSLog中。
Hook代码调用完毕,sys_NSLog保存系统NSLog原地址,NSLog指向HP_NSLog

2.1.2 Hook 自定义 C 函数

Hook一下自己的C函数:

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);
}
//原函数,函数指针
static void (*original_func)(const char * str);

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

调用:

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

输出:

HotPotCat

这个时候可以看到没有Hookfunc

结论:自定义的函数fishhook hook 不了,系统的可以hook

2.2 fishhook原理

fishHOOK可以HOOK C函数,但是我们知道函数是静态的,也就是说在编译的时候,编译器就知道了它的实现地址,这也是为什么C函数只写函数声明调用时会报错。那么为什么fishhook还能够改变C函数的调用呢?难道函数也有动态的特性存在?

是否意味着C Hook就必须修改调用地址?那意味着要修改二进制。(原理上使用汇编可以实现。fishhook不是这么处理的)

那么系统函数和本地函数区别到底在哪里?

2.2.1 符号 & 符号绑定 & 符号表 & 重绑定符号

NSLog函数的地址在编译的那一刻并不知道NSLog的真实地址。NSLogFoundation框架中。在运行时NSLog的地址在 共享缓存 中。在整个手机中只有dyld知道NSLog的真实地址。

LLVM编译器生成MachO文件时,如果让我们做就先空着系统函数的地址,等运行起来再替换。我们知道MachO中分为Text(只读)和Data(可读可写),那么显然这种方式行不通。

那么可行的方案是在Data段放一个 占位符(8字节)让代码编译的时候直接bl 占位符。在运行的时候dyld加载应用的时候将Data段的地址修改为NSLog真实地址,代码bl 占位符没有变 。这个技术就叫做 PIC(position independent code`)位置无关代码。(实际不是这么简单)

  • 占位符 就叫做 符号
  • dylddata段符号进行修改的这个过程叫做 符号绑定
  • 一个又一个的符号放在一起形成了一个列表,叫做 符号表

对于外部的C函数通过 符号地址 也就给了我们机会动态的Hook外部C函数。OC是修改SELIMP对应的关系,符号 也是修改符号所对应的地址。这个动作叫做 重新绑定符号表。这也就是fishhook``hook的原理。

2.2.2验证

Hook NSLog前后分别调用NSLog:

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

image.png

MachO中我们能看到懒加载和非懒加载符号表,dyld绑定过程中绑定的是非懒加载符号和弱符号的。NSLog是懒加载符号,只有调用的时候才去绑定。

MachO中可以看到_NSLogData(值)是0000000100006960offset为:0x8010
在第一个NSLog处打个断点 运行查看:
主程序开始0x0000000100b24000ASLR0xb24000

0x0000000100b24000 + 0x8010中存储的内容为0x0100b2a960
0000000100006960 + 0xb24000 (ASLR) = 0x100B2A960
所以这里就对应上了。0x0100b2a960这个地址就是(符号表中的值其实是一个代码的地址,指向本地代码。)。

image.png

执行完第一个NSLog后(hook前):

image.png

符号表指向了NSLog
执行完hook后:
image.png

符号表指向了HP_NSLog

这也就是fishhook能够Hook的真正原因(修改懒加载符号表)。

2.3 符号绑定过程(间接)

刚才在上面NSLog第一次执行之前我们拿到的地址0x0100b2a960实际上指向一段本地代码,加上ASLR后执行对应地址的代码然后就修改了懒加载符号表。

那么这个过程究竟是怎么做的呢?

先说明一些符号的情况:

  • 本地符号:只能本MachO用。
  • 全局符号:暴露给外面用。
  • 间接符号:当我们要调用外部函数/方法时,在编译时期地址是不知道的。比如系统的NSLog

间接符号专门有个符号表Indirect Symbols

image.png

Symbols包含了所有的符号。

有以下代码:

    NSLog(@"外部函数第一次调用");
    NSLog(@"外部函数第二次调用");

断点断到第一个NSLog,可以看到两次调用NSLog是同一个地址0x100e12998

image.png

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

6998MachOSymbol Stubs中:

image.png

这个就是NSLog的桩(外部符号的桩),值为1F2003D570B3005800021FD6(代码),这个代码是:
image.png

这个时候就对应上了:


image.png

执行桩中的代码:


image.png

这段代码的意思是执行桩中的代码找到符号表中代码跳转执行(0000000100006A28)。

6A28这段代码在__stub_helper中:

image.png

这里执行的是符号绑定。

继续动态调试:

image.png

这块是刚好对应上。
继续进去:
image.png

继续进去:
image.png

对应上了。实际上执行的是dyld_stub_binder。也就是说懒加载符号表里面的初始值都是执行符号绑定的函数

dyld_stub_binder是外部函数,那么怎么得到的dyld_stub_binder函数呢?

image.png

MachOx160x100008000:

image.png

这个符号在非懒加载表中(一运行就绑定):

image.png

所以dyld_stub_binder是通过去非懒加载表中查找。
验证 :
image.png

验证确认,No-Lazy Symbol Pointers表中默认值是0

符号绑定过程:

  • 程序一运行,先绑定No-Lazy Symbol Pointers表中dyld_stub_binder的值。
  • 调用NSLog先找桩,执行桩中的代码。桩中的代码是找懒加载符号表中的代码去执行。
  • 懒加载符号表中的初始值是本地的源代码,这个代码去NoLazy表中找绑定函数地址。
  • 进入dyldbinder函数进行绑定。

binder函数执行完毕后就调用第一次的NSLog了。这个时候再看一下懒加载符号表中的符号:

image.png

符号已经变了。这个时候符号就已经绑定成功了。

接着执行第二次NSLog,这个时候依然是去找桩中的代码执行:

image.png

这个继续执行就执行到Foundation框架的NSLog了(已经绑定过了,不需要继续绑定):

image.png

这个时候通过桩直接跳到了真实地址(还是虚拟的)。这个做的原因是符号表中保存地址执行代码,代码是保存在代码段的(桩)。

间接符号绑定流程
  • 外部函数调用时执行桩中的代码(__TEXT,__stubs)。
  • 桩中的代码去懒加载符号表中找地址执行(__DATA,__la_symbo_ptrl)。
    • 通过懒加载符号表中的地址去执行。要么直接调用函数地址(绑定过了),要么去__TEXT,__stubhelper中找绑定函数进行绑定。懒加载符号表中默认保存的是寻找binder的代码。
  • 懒加载中的代码去__TEXT,__stubhelper中执行绑定代码(binder函数)。
  • 绑定函数在非懒加载符号表中(__DATA._got),程序运行就绑定好了dyld

2.4 通过符号找字符串

上面使用fishhook的时候我们是通过rebindNSLog.name = "NSLog";告诉fishhookhook NSLog。那么fishhook通过NSLog怎么找到的符号的呢?

首先,我们清楚在绑定的时候是去Lazy Symbol中去找的NSLog对应的绑定代码:

image.png

找的是0x00008008这个地址,在Lazy SymbolNSLog排在第一个。

Indirect Symbols中可以看到顺序和Lazy Symbols中相同,也就是要找Lazy Symbols中的符号,只要找到Indirect Symbols中对应的第几个就可以了。

image.png

那么怎么确认Indirect Symbols中的第几个呢?
Indirect Symbolsdata对应值(十六进制)这里NSLog101,这个代表着NSLog在总的符号表(Symbols)中的角标:

image.png

在这里我们可以看到NSLogString Table中偏移为0x98(十六进制)。

image.png

通过偏移值计算得到0xCC38就确认到了_NSLog(长度+首地址)。

这里通过.隔开,函数名前面有_

这样我们就从Lazy Symbols -> Indirect Symbols -> Symbols - > String Table 通过符号找到了字符串。那么fishhook的过程就是这么处理的,通过遍历所有符号和要hook的数组中的字符串做对比。

fishhook中有一张图说明这个关系:

fishhook close函数查找流程

这里是通过符号查找close字符串。

  1. Lazy Symbol Pointer Tableclose index1061
  2. Indirect Symbol Table 1061 对应的角标为0X00003fd7(十进制16343)。
  3. Symbol Table找角标16343对应的字符串表中的偏移值70026
  4. String Table中找首地址+偏移值(70026)就找到了close
    字符串。

实际的原理还是通过传递的字符串找到符号进行替换:通过字符串找符号过程:

  1. String Table中找到字符串计算偏移值。
  2. 通过偏移值在Symbols中找到角标。
  3. 通过角标在Indirect Symbols中找到对应的符号。这个时候就能拿到这个符号的index了。
  4. 通过找到的indexLazy Symbols中找到对应index的符号。

2.5 去掉符号&恢复符号

符号本身在MachO文件中,占用包体积大小 ,在我们分析别人的App时符号是去掉的。

2.5.1 去除符号

符号基本分为:全局符号、间接符号(导出&导入)、本地符号。
对于App来说会去掉所有符号(间接符号除外)。对于动态库来说要保留全局符号(外部要调用)。

去掉符号在Build setting中设置:

image.png

  • Deployment Postprocessing:设置为YES则在编译阶段去符号,否则在打包阶段去符号。
  • Strip StyleAll Symbols去掉所有符号(间接除外),Non-Global Symbols去掉除全局符号外的符号。Debugging Symbols去掉调试符号。

设置Deployment PostprocessingYESStrip StyleAll Symbols。编译查看多了一个.bcsymbolmap文件,这个文件就是bitcode

image.png

这个时候的MachO文件中Symbols就只剩下间接符号表中的符号了:

image.png

其中value为函数的实现地址(imp)。间接符号不会找到符号表中地址执行,是找Lazy Symbol Table中的地址。

代码中打断点就断不住了:


image.png

要断住NSLog就要打符号断点了:

image.png

bt看下调用栈:

image.png

发现自定义方法全是unnamed,这个很明显就是去掉符号的。这种情况下就不好分析代码了。
如果是oc方法调用则直接读取x0,x1就能获取selfcmd:
image.png

在这里我们就要下断点在方法调用之前,可以通过下地址断点。
image.png

先计算出偏移值,下次直接ASLR+偏移值直接断点。这个也就是动态调试常用的方法。

2.5.2 恢复符号

前面动态调试下断点比较麻烦,如果能恢复符号的话就方便很多了。
在上面的例子中去掉所有符号后Symbol Table中只有间接符号了。虽然符号表中没有了,但是类列表和方法列表中依然存在。

image.png

这也就为我们提供了创建Symbol Table的机会。
可以通过restore-symbol工具恢复符号(只能恢复oc的,runtime机制导致):./restore-symbol 原始Macho文件 -o 恢复后文件

./restore-symbol FishHookDemo -o recoverDemo

image.png

这个时候就恢复了,查看MachO(恢复的符号在Symbol Table后面):
image.png

这个时候就可以重签名后进行动态调试了。
restore-symbol地址

2.6 fishhook源码解析

rebind_symbols
rebind_symbols的实现:

//第一次是拿dyld的回调,之后是手动拿到所有image去调用。这里因为没有指定image所以需要拿到所有的。
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装载的时候触发回调。这里相当于注册了一个回调到 _rebind_symbols_for_image 函数。
    _dyld_register_func_for_add_image(_rebind_symbols_for_image);
  } else {
    //不是第一次调用,遍历已经加载的image,进行的hook
    uint32_t c = _dyld_image_count();//这个相当于 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函数生成链表,存放所有要Hook的函数。
  • 根据_rebindings_head->next是否为空判断是不是第一次调用,第一次调用走系统的回调,第二次则自己获取所有的image list进行遍历。
  • 最后都会走_rebind_symbols_for_image函数。
    image list验证:
    image.png

_rebind_symbols_for_image

//两个参数 header  和 ASLR
static void _rebind_symbols_for_image(const struct mach_header *header,
                                      intptr_t slide) {
    //_rebindings_head 参数是要交换的数据,head的头
    rebind_symbols_for_image(_rebindings_head, header, slide);
}

这里直接调用了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就直接调用了 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都调用到了rebind_symbols_for_image,由于给定了image所以不需要循环遍历。

rebind_symbols_for_image

//回调的最终就是这个函数! 三个参数:要交换的数组  、 image的头 、 ASLR的偏移
static void rebind_symbols_for_image(struct rebindings_entry *rebindings,
                                     const struct mach_header *header,
                                     intptr_t slide) {
    
    /*dladdr() 可确定指定的address 是否位于构成进程的进址空间的其中一个加载模块(可执行库或共享库)内,如果某个地址位于在其上面映射加载模块的基址和为该加载模块映射的最高虚拟地址之间(包括两端),则认为该地址在加载模块的范围内。如果某个加载模块符合这个条件,则会搜索其动态符号表,以查找与指定的address 最接近的符号。最接近的符号是指其值等于,或最为接近但小于指定的address 的符号。
     */
    /*
     如果指定的address 不在其中一个加载模块的范围内,则返回0 ;且不修改Dl_info 结构的内容。否则,将返回一个非零值,同时设置Dl_info 结构的字段。
     如果在包含address 的加载模块内,找不到其值小于或等于address 的符号,则dli_sname 、dli_saddr 和dli_size字段将设置为0 ; dli_bind 字段设置为STB_LOCAL , dli_type 字段设置为STT_NOTYPE 。
     */
    
//    typedef struct dl_info {
//            const char      *dli_fname; //image 镜像路径
//            void            *dli_fbase; //镜像基地址
//            const char      *dli_sname; //函数名字
//            void            *dli_saddr; //函数地址
//    } Dl_info;
    
  Dl_info info;//拿到image的信息
  //dladdr函数就是在程序里面找header
  if (dladdr(header, &info) == 0) {
    return;
  }
  //准备从MachO里面去找!
  segment_command_t *cur_seg_cmd;//临时变量
  //这里与MachOView中看到的对应
  segment_command_t *linkedit_segment = NULL;//SEG_LINKEDIT
  struct symtab_command* symtab_cmd = NULL;//LC_SYMTAB 符号表地址
  struct dysymtab_command* dysymtab_cmd = NULL;//LC_DYSYMTAB 动态符号表地址
  //cur为了跳过header的大小,找loadCommands cur = 首地址 + mach_header大小
  uintptr_t cur = (uintptr_t)header + sizeof(mach_header_t);
  //循环load commands找对应的 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`中分别对应`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表示间接符号表中符号数量,没有则直接返回
  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)
  //动态(间接)符号表地址 = 基址 + 动态符号表偏移量
  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)】,相当于拿到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) {
          //找到直接调用函数 perform_rebinding_with_section,这里4张表就都已经找到了。传入要hook的数组、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_LINKEDITLC_SYMTABLC_DYSYMTABload commans

SEG_LINKEDIT获取和LC_SYMTABLC_DYSYMTAB不同是因为在Load Commands中本来就不同,我们解析其它字段也要做类似操作。具体如下:

image.png

  • 根据linkedit和偏移值分别找到符号表的地址字符串表的地址以及间接符号表地址
  • 遍历load commandsdata段找到懒加载符号表非懒加载符号表
  • 找到表的同时就直接调用perform_rebinding_with_section进行hook替换函数符号。

perform_rebinding_with_section

//rebindings:要hook的函数链表,可以理解为数组
//section:懒加载/非懒加载符号表地址
//slide:ASLR
//symtab:符号表地址
//strtab:字符串标地址
//indirect_symtab:动态(间接)符号表地址
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字段指明对应的indirect symbol table起始的index。也就是第几个这里是和间接符号表中相对应的
  //这里就拿到了index
  uint32_t *indirect_symbol_indices = indirect_symtab + section->reserved1;
  //slide+section->addr 就是符号对应的存放函数实现的数组也就是我相应的__nl_symbol_ptr和__la_symbol_ptr相应的函数指针都在这里面了,所以可以去寻找到函数的地址。
  //indirect_symbol_bindings中是数组,数组中是函数指针。相当于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中的数据
    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作为下标,访问symbol table,拿到string table 的偏移值
      uint32_t strtab_offset = symtab[symtab_index].n_un.n_strx;
      //获取到symbol_name 首地址 + 偏移值。(char* 字符的地址)
      char *symbol_name = strtab + strtab_offset;
      //判断是否函数的名称是否有两个字符,因为函数前面有个_,所以方法的名称最少要1个
      bool symbol_name_longer_than_1 = symbol_name[0] && symbol_name[1];
      //遍历最初的链表,来判断名字进行hook
      struct rebindings_entry *cur = rebindings;
      while (cur) {
          for (uint j = 0; j < cur->rebindings_nel; j++) {
              //这里if的条件就是判断从symbol_name[1]两个函数的名字是否都是一致的,以及判断字符长度是否大于1
              if (symbol_name_longer_than_1 &&
                  strcmp(&symbol_name[1], cur->rebindings[j].name) == 0) {
                  //判断replaced的地址不为NULL  要替换的自己实现的方法和rebindings[j].replacement的方法不一致。
                  if (cur->rebindings[j].replaced != NULL &&
                      indirect_symbol_bindings[i] != cur->rebindings[j].replacement) {
                      //让rebindings[j].replaced保存indirect_symbol_bindings[i]的函数地址,相当于将原函数地址给到你定义的指针的指针。
                      *(cur->rebindings[j].replaced) = indirect_symbol_bindings[i];
                  }
                  //替换内容为自己自定义函数地址,这里就相当于替换了内存中的地址,下次桩直接找到lazy/non-lazy表的时候直接就走这个替换的地址了。
                  indirect_symbol_bindings[i] = cur->rebindings[j].replacement;
                  //替换完成跳转外层循环,到(懒加载/非懒加载)数组的下一个数据。
                  goto symbol_loop;
              }
          }
      //没有找到就找自己要替换的函数数组的下一个函数。
      cur = cur->next;
    }
    symbol_loop:;
  }
}
  • 首先通过懒加载/非懒加载符号表和间接符号表找到所有的index
  • 将懒加载/非懒加载符号表的data放入indirect_symbol_bindings数组中。

indirect_symbol_bindings就是存放lazynon-lazy表中的data数组:

image.png

  • 遍历懒加载/非懒加载符号表。
    • 读取indirect_symbol_indices找到符号在Indrect Symbol Table表中的值放入symtab_index
    • symtab_index作为下标,访问symbol table,拿到string table的偏移值。
    • 根据 strtab_offset偏移值获取字符地址symbol_name,也就相当于字符名称。
    • 循环遍历rebindings也就是链表(自定义的Hook数据)
    • 判断&symbol_name[1]rebindings[j].name两个函数的名字是否都是一致的,以及判断字符长度是否大于1
    • 相同则先保存原地址到自定义函数指针(如果replaced传值的话,没有传则不保存)。并且用要Hook的目标函数replacement替换indirect_symbol_bindings,这里就完成了Hook
  • reserved1确认了懒加载和非懒加载符号在间接符号表中的index值。

疑问点:懒加载和非懒加载怎么和间接符号表index对应的呢?
直接Hook dyld_stub_binder以及NSLog看下index对应的值:

image.png

在间接符号表中非懒加载符号从20开始供两个,懒加载从22开始,这也就对应上了。这也就验证了懒加载和非懒加载符号都在间接符号表中能对应上。

demo

总结

image.png

libffi
inlinehook
https://www.jianshu.com/p/7954e6cde245
越狱和防护相关内容
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

你可能感兴趣的:(iOS Hook原理(一)- fishhook)