图解 fishhook 原理

前言

虽然写 fishhook 原理的文章有很多,但是总觉得不够简单直观。大部分都是罗列大堆源码进行讲解,看得人云里雾里。

因此,本文将完全抛开源码,旨在简单清晰,直击要害,带你彻底弄清 fishhook 的原理。

hook 本质

函数是如何被调用的?

我们所写的代码最终都会编译为机器码。程序运行时,cpu 会顺序执行一条条的指令,根据不同类型的指令和数据,执行不同的操作。所有的符号都有自己的地址,包括数据和函数符号。

而函数调用是一条跳转指令,如下所示。在做完一些前置保存工作之后,就会跳转到函数所在地址的第一条指令开始执行。

// x86
call xxx

// arm64
br xxx

试想,如果我们想要 hook 某个函数,那是不是将其地址替换下就可以呢?没错,原理就是这么简单,将原函数地址替换为新函数地址

其实在使用 fishhook 时,我们也能看到一些影子。下面这段代码中,传入了待修改的函数名 NSLog、新函数 my_log 以及存放原函数的指针 orig_nslog

rebind_symbols((struct rebinding[1]){{"NSLog", my_log, (void *)&orig_nslog}}, 1);

从传入的几个参数来看,我们不妨先做个简单的猜想:它可能是在某个地方取到一些函数符号值,对比与传入的函数名是否相等,然后做替换。

当然,这个猜想并不完善,因为还涉及到一些未解的问题:

  • 函数符号值存在哪
  • 原函数地址又存在哪
  • 如何修改原函数地址

那,究竟是不是这个思路呢?下面,我们带着这些问题一一来解答。

Mach-O 结构

iOS 中,可执行文件是个 Mach-O 结构。国际惯例,先了解一下它的组成结构,请看下图。

image

Mach-O 大体上由如下几部分构成:

  • Header,描述 Macho-O 文件的一些元信息,比如魔数、支持的 cpu 类型等。

  • Load Commands,加载命令,也就是告诉系统如何去处理不同的加载信息。

    常用的命令有:

    • LC_SEGMENT,表示加载 segment,系统会将其映射到进程的虚拟地址空间中,比如 TEXT 代码段、DATA 数据段。
    • LC_DYLIB,表示加载动态库,会带有动态库的路径信息。
    • LC_LOAD_DYLINKER,表示动态链接器的信息,里面有 dyld 的路径。
    • ...
  • Sections,编译器对不同类型的资源在逻辑上的划分,比如 .text.data.symtab 等。

这里,我们只需要要关注几个特定的 Load CommandSection 就好,这对了解 fishhook 的原理就足够了。

如果想要详细了解 mach-o 的具体构成,可参考 osx-abi-macho-file-format-reference。

Load Command

Load Command 表示加载命令,里面包含一些数据信息,不同的加载命令中包含的数据不太一样。

今天我们需要了解的只有如下几种:

  • LC_SEGMENT (__DATA_CONST),加载常量数据段。包括一系列的 section header,也就是 section 相关的信息,比如地址、大小、section 名称等。这里我们知道 __got section header 在里面就好。对应的结构为 segment_command

  • LC_SEGMENT (__DATA),加载可写数据段。同样包括一系列的 section header,这里我们只需记住有 lazy_symbol_ptr section header

  • LC_SEGMENT (__LINK_EDIT),加载 __LINK_EDIT 段。该段中包含了符号表、字符串表、重定位符号表等,主要供 dyld 使用。

  • LC_SYMTAB,包含了符号表、字符串表的偏移量,对应结构为 symtab_command

  • LC_DYSYMTAB,包含了间接表(动态库符号表)的偏移量,对应结构为 dysymtab_command

觉得文字啰里啰嗦的同学,可以直接看下面这张图。

image

Section

section 这部分,我们只要关注数据段__got__la_symbol_ptr 两个 section 即可。这两部分的结构一样,都是一个表,里面存储了动态库符号的地址。以下我们统称为 got 表。

__got 中存储数据符号地址,__la_symbol_ptr 中存储函数符号地址,下面我们只分析一种就好。

一看到符号地址这几个词,有没有突然间精神抖擞。到这里,我们又可以猜想一下,改变符号地址,就是修改 got 表中的值。

为什么符号地址会存放在单独的表中?这就与动态库共享有关。

动态库符号地址

为了做到动态库代码段共享,符号地址不能写死。因此,程序中使用 got 中间表来存放动态库符号地址,代码段中的符号地址都从 got 中获取,从而解决了符号地址固定的问题。

got 在数据段中,可读可写。那么,我们就可以利用这个特性,通过修改 got 表中符号的地址,以达到替换的目的。

关于 got 的说明,可以参看我之前写的文章 图解 Mach-O 中的 got ,对动态库符号地址及链接有个大概的了解。

PS:如果没空看的同学,也无大碍,记住 got 表中存储的是符号地址就好。

关键 Load Command 查找

前面我们提到过,__link_edit 中存放了一些重要的信息,比如符号表、字符串表、重定位表。那么如何获取到这些表呢?

Load Command 一节,我们介绍了几个需要留意的加载命令。现在再来回顾一下:

  • LC_SEGMENT (__LINK_EDIT)
  • LC_SYMTAB,存放符号表和字符串表的偏移。
  • LC_DYSYMTAB,存放间接符号表的偏移。

这里,出现了符号和字符串字眼,那么一定跟符号查找有关系。现在知道的信息是几张表的偏移量。那如果取到了真实的基址(加上 ASLR 随机偏移后),再加上偏移量不就可以获取到这几张表了吗?

所以,第一步做的事情就是从 Load Commands 中找到这些个命令,计算出 Mach-O 在虚拟地址空间的实际基址,然后得到以下三张表:

  • 符号表
  • 字符串表
  • 间接符号表

如下图所示,最右边的三张表。

image

此时已经知道了符号表信息,那么如何将 got 表中的地址跟它所对应的符号关联起来呢?

间接符号表

在说间接符号表之前,我们先了解一下符号表中存的是啥。

是具体符号值吗?‍♂️,不不不,其实是符号在字符串表中的下标。由于字符串表存储了所有的符号值,那么自然的,符号表中只需存下标就可以,省时省心又省力。

间接符号表,也就是动态库符号表,存放了动态库符号的信息。间接,顾名思义,说明表中的数据不是字符串表的下标,而是该符号在符号表中的下标,中间多了一层。

如下图所示:

image

got 中的符号是动态库符号,肯定会涉及到间接符号表。那么它们究竟是如何对应上的呢?下面就请 got section header 闪亮登场。

got section header

由于 __got section headerlazy_symbol_ptr secton header 结构是完全一样的,下面以 lazy_symbol_ptr secton header 举例说明。

其结构如下图所示:

image

它里面包含了两个非常关键的字段:

  • address,表示它所属的 section 地址。由于 section 中存放是的 got 表,所以它也就是 got 表地址。
  • reserved1,表示 got 表中第一个符号在间接表中的下标。

第二点有点绕,举个。

假设 got 中总共有 3 个符号,reserved1 = 2,那么说明第一个符号在间接表中的下标为 2,第二个符号的下标为 3,第三个符号的下标为 4。如下图所示:

image

这样,如果有了在间接表的下标 → 得到在符号表的下标 → 得到在字符串表的下标 → 得到字符串。

完整的动态符号查找路径如下图所示:

image

由于我们在使用 fishhook 的时候会传入函数名称,假设为 A,那么当查找到某项 got 符号字符串值 B 后,那么只需对比一下 A、B 是否相等即可。如果相等,更新该项符号在 got 中的地址即可。

注意 got 中的数据项实际上是个间接指针,也就是指向指针的指针,指针的值才是符号地址。

根据上图,我们可以推断出一个初始的替换雏形:

  • 根据 address 字段,获取 got 表地址。
  • 遍历 got 表,根据当前是第几项,查找当前符号在间接表中的下标
  • 间接表中获取到符号表下标
  • 符号表中获取到字符串表下标
  • 得到符号字符串,与待替换函数名称进行比较。若相等,则更新 got 表中的值。

举个,假设将 NSLog 替换为 my_log 自定义实现。整个处理过程如下图所示:

image

注意:更新 got 第一项符号的地址时,是 *got[0] = my_log,因为表中每一项是二级指针。

至此,如果你能理解上述所讲的知识点,那么恭喜你,已经搞懂了 fishhook 的原理。

加点东西

rebinding 结构

以上只是一个简易模型。在具体实现中,fishhook 将所要替换的函数封装成了一个链表结构,每个节点中有个数组,存放了待修改的函数们。如下图所示:

image
  • rebindings_entry 表示链表节点。
  • 节点字段 rebindings 表示待替换函数数组。
  • struct rebinding 表示函数替换信息。

那么相应的,我们的雏形也得稍微改进一下:

  • 根据 address 字段,获取 got 表地址。
  • 遍历 got 表,根据当前是第几项,查找当前符号在间接表中的下标
  • 间接表中获取到符号表下标
  • 符号表中获取到字符串表下标
  • 得到符号字符串,与待替换函数名称进行比较。
    1. 遍历链表节点
    2. 遍历当前节点中的函数替换数组
    3. 若找到了相同的函数符号值,则更新 got

全局替换

由于动态库中的符号很可能被可执行文件或多个动态库使用。因此,对于每个加载的 image,都要做如上的操作。也就是说,这是一项全局性的替换。

打个比方,假设我们将 NSLog 替换为自己的实现。在 A、B、C 三个动态库(不论是系统库还是自己编写的)中都用到了 NSLog,那么,它们 got 表中的值都要被更新,以达到全局替换的目的。

限制

由于 fishhook 修改的是 got 中的符号地址,那么它也只能对动态库中的 c 函数进行替换。

总结

这篇文章中,我主要讲述了 fishhook 所涉及到的最小知识点,比如动态库符号地址在哪,如何查找符号值,以及如何匹配修改符号地址等。以图代码,期望以最简单直接的方式探究其本质原理。希望对你有帮助~

PS:如果你看懂了原理,再去看源码,应当是非常轻松了。

你可能感兴趣的:(图解 fishhook 原理)