fishhook--终于被我悟透了

fishhook 作为一个 hook 工具在 iOS 开发中有着高频应用,理解 fishhook 的基本原理对于一个高级开发应该是必备技能。很遗憾,在此之前虽然对 fishhook 的基本原理有过多次探索但总是一知半解,最近在整理相关知识点对 fishhook 又有了全新的认识。如果你跟我一样对 fishhook 的原理不甚了解那这篇文章会适合你。

需要强调的是本文不会从 fishhook 的使用基础讲起,也会不对照源码逐行讲解,只会着重对一些比较迷惑知识点进行重点阐述,建议先找一些相关系列文章进行阅读,补充一些基本知识再回过头来阅读本文。

注1:所有代码均以 64 位 CPU 架构为例,后文不再进行特别说明

注2:请下载 MachOView 打开任意 Mach-O 文件同步进行验证

注3:Mach-O 结构头文件地址

MachO 文件结构

0x01

Mach-O 文件结构有三部分,第一部分是 header,描述 Mach-O 文件的关键信息。其数据结构如下:

C 复制代码
struct mach_header_64 {
	uint32_t	magic;		/* mach magic number identifier */
	cpu_type_t	cputype;	/* cpu specifier */
	cpu_subtype_t	cpusubtype;	/* machine specifier */
	uint32_t	filetype;	/* type of file */
	uint32_t	ncmds;		/* number of load commands */
	uint32_t	sizeofcmds;	/* the size of all the load commands */
	uint32_t	flags;		/* flags */
	uint32_t	reserved;	/* reserved */
};

如上面结构体所示,Mach-O 文件 header 的关键信息包括:

  • cputype:当前文件支持的 CPU 类型
  • filetype:当前 MachO 的文件类型
  • ncmds: Load Command 的数量
  • sizeofcmds:所有 Command 的总大小

每个 iOS 的可执行文件、动态库都会从 header 开始加载到内存中。

0x02

第二部分是 Load Commands ,Load Commands 有不同的类型,有的用于描述不同类型数据结构(在文件中的位置、大小、类型、限权等),有的单纯用来记录信息,比如记录:dyld 的路径、main 函数的地址、UUID 等,用于记录信息的 Command 一般不会出现在数据区(Data)内。

不同的类型的 Load Command 对应着不同的结构体,但所有 Load Command 的前两个字段(cmd/cmdsize)都是相同的。所以,所有的 Load Command 都可以通过类型强转为 load_command 结构体类型。

有了 load_command 就可以通过每一个 Load Command 的 cmdsize 计算出下一个 Load Command 的位置。

C 复制代码
struct load_command {
	uint32_t cmd;		/* type of load command */
	uint32_t cmdsize;`	/* total size of command in bytes */
};

struct segment_command_64 { /* for 64-bit architectures */
	uint32_t	cmd;		/* LC_SEGMENT_64 */
	uint32_t	cmdsize;	/* includes sizeof section_64 structs */
	char		segname[16];	/* segment name */
	uint64_t	vmaddr;		/* memory address of this segment */
	uint64_t	vmsize;		/* memory size of this segment */
	uint64_t	fileoff;	/* file offset of this segment */
	uint64_t	filesize;	/* amount to map from the file */
	vm_prot_t	maxprot;	/* maximum VM protection */
	vm_prot_t	initprot;	/* initial VM protection */
	uint32_t	nsects;		/* number of sections in segment */
	uint32_t	flags;		/* flags */
};

有文章说 load_command 是所有 Command 的基类,你也可以这样理解(虽然在代码语法层面不是)。

segment_command_64 作为一个 Load Command 重点类型,一般用来描述 __PAGEZERO、__TEXT、__DATA、__DATA_CONST 、__LINKEDIT 等包含实际代码数据的段(位于 Data 部分)。

因此对于 segment_command_64 类型的 Load Command 也称之为: segment

segment 内部还包含一个重要的类型:section ,section 用于描述一组相同类型的数据。例如:所有代码逻辑都位于名为 __text 的 section 内,所有 OC 类名称都位于名为 __objc_classname 的 section 内,而这两个 section 均位于 __TEXT 段(segment)。

segment_command_64 关键字段介绍:

  • segname: 当前 segment 名称,可为 __PAGEZERO、__TEXT、__DATA、__DATA_CONST 、__LINKEDIT 之一

  • vmaddr: 当前 segment 加载到内存后的虚拟地址(实际还要加上 ALSR 偏移才是真实的虚拟地址)

  • vmsize: 当前 segment 占用的虚拟内存大小

  • fileoff: 当前 segment 在 Mach-O 文件中的偏移量,实际位置 = header 开始的地址 + fileoff

  • filesize: 当前 segment 在 Mach-O 文件中的实际大小,考虑到内存对齐 vmsize >= filesize

  • nsects: 当前 segment_command_64 下面包含的 section 个数

关于随机地址偏移(ALSR) 的相关容内可自行查找相关资料进行了解,这里不再贅述

section 只有一种类型,其结构体定义如下:

C 复制代码
struct section_64 { /* for 64-bit architectures */
	char		sectname[16];	/* name of this section */
	char		segname[16];	/* segment this section goes in */
	uint64_t	addr;		/* memory address of this section */
	uint64_t	size;		/* size in bytes of this section */
	uint32_t	offset;		/* file offset of this section */
	uint32_t	align;		/* section alignment (power of 2) */
	uint32_t	reloff;		/* file offset of relocation entries */
	uint32_t	nreloc;		/* number of relocation entries */
	uint32_t	flags;		/* flags (section type and attributes)*/
	uint32_t	reserved1;	/* reserved (for offset or index) */
	uint32_t	reserved2;	/* reserved (for count or sizeof) */
	uint32_t	reserved3;	/* reserved */
};

section 关键字段介绍:

  • sectname: section 的名称,可以为 __text,__const,__bss 等

  • segname: 当前 section 所在 segment 名称

  • addr: 当前 section 在虚拟内存中的位置(实际还要加上 ALSR 偏移才是真实的虚拟地址)

  • size: 当前 section 所占据的大小(磁盘大小与内存大小)

  • reserved1: 不同 section 类型有不同的意义,一般代表偏移量与索引值

  • flags: 类型&属性标记位,fishhook 使用此标记查找懒加载表&非懒加载表

需要注意:有且仅有 segment_command_64 类型的 Command 包含 section。

0x03

最后是数据区(Data),就是 Mach-O 文件所包含的代码或者数据;所有代码或者数据都根据 Load Command 的描述进行组织、排列。其中由segment_command_64 描述的数据或代码在 Data 部分中均以 section 为最小单位进行组织,并且这部分内容占大头。segment 再加上其它类型 Load Command (其实就是 __LINKEDIT segement)描述的数据共同组成了数据区。

注意:虽然名称为 __LINKEDIT (类型为:segment_command_64) 的 segment 下面所包含的 section 数量为 0,但根据其 fileoff,filesize 计算发现:

__LINKEDIT 的 segement 所指向的文件范围其实包含其它 Load Command (包含但不限于:LC_DYLD_INFO_ONLY、LC_FUNCTION_STARTS、LC_SYMTAB、LC_DYSYMTAB、LC_CODE_SIGNATURE)所指向的位置范围。

推导过程如下:

如上图所示在 Load Commands 中 __LINKEDIT 在 Mach-O 文件的偏移:0x394000 大小为:0x5B510。而 Mach-O header 的开始地址为 0x41C000。所以 __LINKEDIT 在 Mach-O 文件中的地址范围是:{header + fileoffset, header + fileoffset + filesize},代入上式就是 {0x41C000+0x394000, 0x41C000+0x394000+0x5B510},最终得到 {0x7B0000,0x80B510} 的地址范围。

从下图看,segment 最后一个 section 结束后的第一个地址就是上面的开始的范围,文件的结束地址也是上面计算结果的结束范围(最后一个数据地址占 16 字节)。

所以可以这样理解:名称为 __LINKEDIT Load Command 是一个虚拟 Command。它用来指示LC_DYLD_INFO_ONLY、LC_FUNCTION_STARTS、LC_SYMTAB、LC_DYSYMTAB、LC_CODE_SIGNATURE 等这些 Command 描述的数据在「文件与内存」中的总范围,而这些 Command 自己本身又描述了自各的范围,从地址范围来看 __LINKEDIT 是这些 Command 在数据部分的父级,尽管它本身并没有 section。

fishhook 的四个关键表

fishhook 的实现原理涉及到四个「表」,理解这四个表之间的关系便能理解 fishhook 的原理,且保证过目不忘。

  • 符号表(Symbol Table)
  • 间接符号表(Indirect Symbol Table)
  • 字符表(String Table)
  • 懒加载和非懒加载表(__la_symbol_ptr/__non_la_symbol_ptr)

符号表&字符表

其中符号表(Symbol Table)与字符表(String Table)在 LC_SYMTAB 类型的 Load Command 中描述。

C 复制代码
struct symtab_command {
	uint32_t	cmd;		/* LC_SYMTAB */
	uint32_t	cmdsize;	/* sizeof(struct symtab_command) */

	uint32_t	symoff;		/* 符号表(Symbol Table)在文件中相对 header 的偏移 */
	uint32_t	nsyms;		/* 符号表(Symbol Table)数量 */

	uint32_t	stroff;		/* 字符表(String Table)在文件中相对 header 的偏移 */
	uint32_t	strsize;	/* 字符串(String Table)表总大小*/
};

符号表(Symbol Table)内容的数据结构用 nlist_64 表示:

C 复制代码
struct nlist_64 {
    union {
        uint32_t  n_strx; /* index into the string table */
    } n_un;
    uint8_t n_type;        /* type flag, see below */
    uint8_t n_sect;        /* section number or NO_SECT */
    uint16_t n_desc;       /* see <mach-o/stab.h> */
    uint64_t n_value;      /* value of this symbol (or stab offset) */
};

nlist_64 的第一个成员 n_un 代表当前符号的名称在字符表(String Table)中的相对位置,其它成员变量这里不需关注。

字符表(String Table)是一连串的字符 ASCII 码数据,每个字符串之间用 '\0' 进行分隔。

间接符号表

而间接符号表(Indirect Symbol Table)在 dysymtab_command 结构体的 Load Command(类型为LC_DYSYMTAB)中描述。

arduino 复制代码
struct dysymtab_command {
    uint32_t cmd;	/* LC_DYSYMTAB */
    uint32_t cmdsize;	/* sizeof(struct dysymtab_command) */

    /* 省略部分字段 */
    
    uint32_t indirectsymoff; /* 间接符号表相对 header 的偏移 */
    uint32_t nindirectsyms;  /* 间接符号表中符号数量 */

    /* 省略部分字段 */
};	

间接符号表本质是由 int32 为元素组成的数组,元素中存储的数值代表当前符号在符号表(Symbol Table)中的相对位置。

懒加载和非懒加载表

懒加载与非懒加载表位于 __DATA/__DATA_CONST segment 下面的 section 中。

懒加载与非懒加载表有如下特点:

  • 当前可执行文件或动态库引用外部动态库符号时,调用到对应符号时会跳转到懒加载与非懒加载表指定的地址执行

  • 懒加载表在符号第一次被调用时绑定,绑定之前指向桩函数,由桩函数完成符号绑定,绑定完成之后即为对应符号真实代码地址

  • 非懒加载表在当前 Mach-O 被加载到内存时由 dyld 完成绑定,绑定之前非懒加载表内的值为 0x00,绑定之后同样也为对应符号的真实代码地址

敲黑板知识点:fishhook 的作用就是改变懒加载和非懒加载表内保存的函数地址

由于懒加载和非懒加载表没有包含任何符号字符信息,我们并不能直接通过懒加载表和非懒加载表找到目标函数在表中对应的位置,也就无法进行替换。因此,需要借助间接符号表(Indirect Symbol Table)、符号表(Symbol Table)、字符表(String Table)三个表之间的关系找到表中与之对应的符号名称来确认它的位置。

如何找到目标函数地址

引用外部函数时需要通过符号名称来确定函数地址在懒加载和非懒加载表的位置,具体过程如下:

  1. 懒加载表与非懒加载表中函数地址的索引与间接符号表(Indirect Symbol Table)中的位置对应;

    以表中第 i 个函数地址为例,对应关系可以用伪公式来表述:

    间接符号表的偏移 = 间接符号表开始地址 + 懒加载表或非懒加载表指定的偏移(所在 section 的 reserved1 字段)+ i

  2. 间接符号表中保存的 int32 类型的数组,以上一步计算到的「间接符号表的偏移」为索引取数组内的值得到符到号中的位置

    同样得到一个等效伪公式:符号表的偏移 = 间接符号表开始地址 + 间接符号表的偏移

  3. 符号表中保存的数据是 nlist_64 类型,该第一个字段(n_un.n_strx)的值就是当前符号名称在字符表中的偏移

    等效伪公式:符号名称在字符表中的偏移 = (符号表的开始地址 + 符号表的偏移).n_un.n_strx

  4. 按照上面得到的偏移,去字符表中取出对应字符串(以 \0)结尾

    等效伪公式:懒加载表与非懒加载表中第 i 个函数名 = 字符表的开始地址 + 符号名称在字符表中的偏移

到这里我们从下至上进行公式代入,合并三个伪公式得到:

懒加载表或非懒加载表中第 i 个函数名 = 字符表的开始地址 + (符号表的开始地址 + 间接符号表开始位置 + 懒加载表或非懒加载表指定的偏移(所在 section 的 reserved1 字段)+ i).n_un.n_strx

现在,上面这个公式里还不知道的是三个开始地址:

  • 字符表(String Table)的开始地址
  • 符号表(Symbol)的开始地址
  • 间接符号表(Indirect Symbol Table)开始地址

而懒加载表或非懒加载表中函数地址个数也可以通过对应 section 的 size 字段(详情查看上文 section_64 结构体中的描述)计算而得到,公式:(section->size / sizeof(void *))。

到这里 fishhook 四个表的关系应该非常清楚了,fishhook 所做的无非是通过这个公式在懒加载表与非懒加载表中找到与目标函数名匹配的外部函数,一旦发现匹配则将其地址改为自定义的函数地址。

何为 linkedit_base

如果不考虑其它因素,实际上面三个表的开始地址可以直接通过 Mach-O 的 header 地址 + 对应的偏移就可以直接得到。以符号表(Symbol Table)为例:

Mach-O header 的开始地址如上文所述为:0x41C000,计算 0x41C000 + 0x3BECD8 = 0x7DACD8;再用 MachOView 查看这个地址,确实是符号表在文件中的位置:

同时上面的的推导也证明了 symtab_command->symoff symtab_command->stroff 是相对 Mach-O header 的偏移,并不是相对 __LINKEDIT 的偏移;

而 fishhook 源码中计算符号表开始地址的方式是:

C 复制代码
nlist_t *symtab = (nlist_t *)(linkedit_base + symtab_cmd->symoff);

导致不少博文说 linkedit_base 是 __LINKEDIT 段的基地址,这完全是错误的,在此可以明确的是:

  • linkedit_base 并不是 __LINKEDIT segment 在内存中的开始地址
  • linkedit_base 并不是 __LINKEDIT segment 在内存中的开始地址
  • linkedit_base 并不是 __LINKEDIT segment 在内存中的开始地址

fishhook 中计算 linkedit_base 的计算方式如下:

C 复制代码
uintptr_t linkedit_base = (uintptr_t)slide + linkedit_segment->vmaddr - linkedit_segment->fileoff;

忽略掉随机地址偏移(ALSR)值: slide 后:

linkedit_base = linkedit_segment->vmaddr - linkedit_segment->fileoff;

linkedit_segment->vmaddr:代表 __LINKEDIT segment 在「虚拟内存」中的相对开始位置 linkedit_segment->fileoff:代表 __LINKEDIT segment 在「文件」中的相对开始位置

那这两个相减有什么意义呢?

要解答这个问题先来看 MachOView 给出的信息:

如上图,在 __LINKEDIT segment 之前的几个 segment (红框标记)可以解析出几个事实:

  • 每个 segment 的在「 Mach-O 文件」中的开始地址都等于上一个 segment 的 File Offset + File Size,第一个 segment 从 0 开始

  • 同理,每个 segment 在「虚拟内存」中的位置都等于上一个 segment 的 VM Address + VM Size,第一个 segment 从 0 开始

  • __PAGEZERO_DATAVM Size > File Size,而其它 segment 中这两个值相等,意味着两个 segment 加载到虚拟内存中后有一部分「空位」(因内存对齐而出现)

  • __PAGEZERO 不占 Mach-O 文件的存储空间,但在虚拟内存在占 16K 的空间

用图形表示即为:

故而 linkedit_base = linkedit_segment->vmaddr - linkedit_segment->fileoff 的意义为:

  • Mach-O 加载到内存后 __LINKEDIT 前面的 segment 因内存对齐后多出来的空间(空位)
  • Mach-O 加载到内存后 __LINKEDIT 前面的 segment 因内存对齐后多出来的空间(空位)
  • Mach-O 加载到内存后 __LINKEDIT 前面的 segment 因内存对齐后多出来的空间(空位)

这才是 linkedit_base 在物理上的真正意义,任何其它的定义都是错误的。

__LINKEDIT 本身的 VM Size == File Size 说明它包含的符号表、字符表与间接符号表三个表本身是内存对齐的,它们之间没有空位,所以它们本身在文件中的偏移 + linkedit_base 即为在内存中的实际位置。

C 复制代码
  // 符号表在内存中的开始位置
  nlist_t *symtab = (nlist_t *)(linkedit_base + symtab_cmd->symoff);
  // 字符表在内存中的开始位置
  char *strtab = (char *)(linkedit_base + symtab_cmd->stroff);
  // 间接符号表在内存中的开始位置
  uint32_t *indirect_symtab = (uint32_t *)(linkedit_base + dysymtab_cmd->indirectsymoff);

最后

fishhook 在 APM、防逆向、性能优化等方向均有较多的应用,从本质上来看 fishhook 是对 Mach-O 文件结构的深度应用。相信在了解完原理之后再去看 Mach-O 文件的结构就比较简单了,与 Mach-O 文件结构相关的应用还有符号表的还原。下篇文章再与大家共同学习符号表还原的具体过程(虽然文件夹还没有创建 😂)。

如对本文有任何疑问,我们评论区交流 😀

相关推荐
救救孩子把6 小时前
mac中git操作账号的删除
git·macos
键盘敲没电6 小时前
【iOS】KVC
ios·objective-c·xcode
吾吾伊伊,野鸭惊啼6 小时前
2024最新!!!iOS高级面试题,全!(二)
ios
吾吾伊伊,野鸭惊啼6 小时前
2024最新!!!iOS高级面试题,全!(一)
ios
sysin.org7 小时前
VMware ESXi 8.0U3b macOS Unlocker & OEM BIOS 2.7 集成网卡驱动和 NVMe 驱动 (集成驱动版)
macos·esxi·bios·unlocker·oem·2.7
yanling20237 小时前
Parallels Desktop 20 for Mac中文版发布了?会哪些新功能
macos·虚拟机·pd
不会敲代码的VanGogh8 小时前
【iOS】——应用启动流程
macos·ios·objective-c·cocoa
sysin.org9 小时前
VMware ESXi 7.0U3q macOS Unlocker 集成驱动版更新 OEM BIOS 2.7 支持 Windows Server 2025
windows·macos·esxi·bios·oem·网卡驱动·nvme驱动
GEEKVIP10 小时前
恢复已删除文件的可行方法,如何恢复已删除的文件
macos·智能手机·电脑·笔记本电脑·cocoa·iphone·ipad
tekin10 小时前
macos清理垃圾桶时提示 “操作无法完成,因为该项目正在使用中” 解决方法 , 强制清理mac废纸篓 方法
macos·强制删除·强制清理·mac废纸篓