type
status
date
slug
summary
tags
category
icon
password
越狱hook的原理
越狱hook用的是cydia mobilesubstrate框架,通常直接用MSHookFunction进行内联hook。我个人的感觉和Windows常用的minhook是差不多的。修改代码段、添加了一个管道函数。用起来非常方便。
除此之外,常用的是一个叫kittymemory的框架,提供了包括特征扫描、修改代码段等等一系列功能,也会自动找到模块基地址进而去拿虚拟地址,不需要自己去算,提供了一系列这样的api,非常方便。
非越狱为什么用不了
在jailed环境下用,首先是没有substrate框架,这个可以一起注入进ipa来解决。问题是内联hook、kittymemory的patch都会改代码段,而iOS有代码完整性检查。这个非常厉害,只要代码段被改了,和签名不匹配就会马上崩溃。所以其实越狱的注入substrate后,唯一的问题就是不能动代码段了。如果只做数据读写其实是没问题的。比如下面这个崩溃日志,在codesigning崩溃了。

一种朴素的思路
很自然的我们就能想到,能不能把要hook和要patch的地方重定向到我们自己的代码,根据用户的设置,选择跳转到哪里继续执行。这里的跳转分别就是原位置继续执行和执行hook函数。
想法非常简单而且肯定可行,只是自己做起来有点难度。在我的进一步调研之下,发现了有一种外挂叫做H5GG,它就是采用这种方式进行了hook。接着我在GitHub找到了别人用C语言实现的H5GG的hook方式,只不过这个仓库不太完善,只给了一些函数。而且代码写的比较乱。接下来我梳理一下其中的逻辑,整合了越狱、非越狱hook的框架,通过宏定义操作,就可以快速实现两个版本的编译。
macho基本知识
load_command
在Mach-O文件格式中,
load_command
是文件头的一部分,用于描述与文件相关的各种信息。每个load_command
都包含一个固定的结构体,用来描述某种加载任务或数据的类型。每个命令的内容由两个部分组成:- 命令类型(cmd):这个字段是
load_command
的类型标识符,表示该命令的类型(例如,是否是代码段、库、符号表等)。
- 命令大小(cmdsize):这个字段表示命令结构体的总大小(包括命令本身的大小),它用于遍历命令列表时确定每个命令的大小。
load_command
本质上定义了可执行文件的各个部分,系统加载时需要了解这些信息。常见的命令类型有:- LC_SEGMENT:表示一个内存段,包含程序的代码或数据。
- LC_SYMTAB:符号表命令,用于描述符号表的位置。
- LC_LOAD_DYLIB:加载动态库(dylib)的命令。
- LC_LOAD_WEAK_DYLIB:加载弱引用动态库的命令,意味着该动态库是可选的。
- LC_UUID:UUID命令,描述文件的唯一标识符。
- LC_MAIN:指示程序的入口点。
每个
load_command
都紧跟在Mach-O文件头部之后,并且多个命令会按顺序排列在文件中,供加载器(loader)在加载过程中解析。具体来说,
load_command
是Mach-O格式用来告知操作系统如何加载可执行文件或共享库的重要机制。其中LC_SEGMENT给人的感觉和Windows下PE头的Section Header很像。LC相当于把所有的导入命令集合于一体了,包括自身信息和加载其他动态库的信息。LC_LOAD_DYLIB又很类似于导入表。但二者和Windows的实现方式都不一样。
所以如果需要修改macho,这个头地方肯定是需要修改的
框架原理
主要有这些函数。

前两个函数就不说了,是虚拟地址转相对再转文件中的地址。load_macho_data是加载当前应用的可执行文件。这个是可以设置的,比如说对于Unity的游戏,我们需要操作的是UnityFramework这个framework,那就用它的路径即可。它在加载之后还检测了魔数,判断一下是不是支持的架构。

主要是在staticInlineHookPatch函数中进行了寻找/使用的操作。
大概逻辑是,首先我们遍历当前的可执行文件,是不是有__HOOK_TEXT段和__HOOK_DATA段。如果有,说明这是修补过的。
首先是查看load_command,查看这个可执行文件都有哪些段。如果有__HOOK_TEXT段和__HOOK_DATA,那说明这是我们已经修改过的可执行文件了,不必再进行操作;如果没找到的话,就调用add_hook_section,给macho手动再添加上这两个段。添加上之后,再循环一次就能找到并break了。

add_hook_section的流程如下:
1. 读取 Mach-O 文件头
struct mach_header_64* header = (struct mach_header_64*)macho.mutableBytes;
- 通过
macho.mutableBytes
获取 Mach-O 文件的内存映像,然后将其解析为mach_header_64
结构,获取 Mach-O 文件的基本信息。
2. 遍历 Load Commands(加载命令)
- 通过遍历 Mach-O 文件中的加载命令来查找各个 segment(段)和 section(节),并获取相关的内存地址和文件偏移信息。
struct load_command* lc = (struct load_command*)((UInt64)header + sizeof(*header));
这一部分获取到所有的加载命令,并逐个解析。
- 如果是
LC_SEGMENT_64
类型的命令,代码会继续查看它是否是__LINKEDIT
段,并计算出内存中的最大地址(vm_end
)和最小的 section 偏移量(min_section_offset
)。
3. 提取
__LINKEDIT
段NSRange linkedit_range = NSMakeRange(linkedit_seg->fileoff, linkedit_seg->filesize);
- 通过提取
__LINKEDIT
段的数据,暂时删除它([macho replaceBytesInRange:linkedit_range withBytes:nil length:0];
),因为接下来会修改它。
4. 构建新的段和节
- 代码接着创建了两个新的段:
__HOOK_TEXT
:这个段将存放hook
的执行代码。__HOOK_DATA
:这个段将存放与hook
相关的数据。
- 创建这两个段时,代码设置了它们的虚拟地址、文件偏移、大小、权限等信息。

5. 修改 Load Command
- 在新段和节被添加之后,代码需要更新 Mach-O 文件中的
load_command
。它通过计算linkedit_cmd_offset
来定位到__LINKEDIT
段的位置,并替换为新的段(text_seg
和data_seg
)。
- 然后,更新了加载命令中的各种字段,如
LC_SYMTAB
和LC_DYSYMTAB
中涉及符号表和动态符号表的偏移量。
6. 修改其他相关信息
- 之后,代码还会调整
dyld_info_command
、symtab_command
等结构体中的偏移量,确保新的段和节被正确地映射到它们的文件和虚拟内存地址中。
7. 向 Mach-O 文件末尾添加代码和数据
- 通过
macho appendBytes
将实际的hook
代码(全是 0xFF 填充)和数据(全是 0)附加到 Mach-O 文件的末尾。
- 注意,
text_seg.vmsize
和data_seg.vmsize
决定了附加的代码和数据段的大小。
8. 返回修改后的 Mach-O 文件
- 最终,修改后的 Mach-O 文件作为
NSMutableData
返回。
添加hook和patch
因为添加的段的大小都是一个页的大小。这里遍历一个页,找到还是空着的地方。

把整个数据段当成一个StaticInlineHookBlock数组,找到还是空着的,就是没插入过的hook。下面是这个结构体的定义。

找到第一个空着的该结构体,作为新添加hook的位置。在这里用dobby框架进行hook。如果是patch的话还要写入patch的大小和哈希。这里写哈希的作用是,后续启动patch的时候一样需要传入需要改为的指令。如果该指令哈希与我们修改可执行文件时候写入的哈希不一样,认为代码有错误,那么不会进行patch。

在全部hook/patch添加完成之后,那么应该保存一次macho。以此作为在非越狱手机上使用的文件。
启动hook

这个函数,以vaddr作为标识找到对应的hookblock,修改其中的target_replace,就可以把执行流重定向到我们的函数。返回的是原函数地址。
启动patch

启动patch的做法仍然是按照对应的地址找到对应的block,修改target_replace到patched_vaddr,重定向执行流。只不过会添加一个哈希作为校验,不一样的话不会执行,因为patch是写死在可执行文件里的,跟dylib没关系,防止造成误解。
关闭patch

找到对应的block、确定哈希正确,把target_replace设置为null即可。
关于dobby是怎么建立管道的,看这个函数即可。我们直接包含编译完成的.a文件就行了。
🤗 打包和使用
生成免越狱版可执行文件、签名、安装
对于指定的可执行文件,我们在越狱手机上注入一次刚编译的针对免越狱的dylib,他会读取整个.text段,把对应的位置修改后保存下来。
在应用的document目录会产生新的可执行文件,替换到dump下来的未加密app中

重新压缩payload文件夹,重命名为.ipa

然后,使用ipatch注入dylib,注意勾选inject substrate。这是在非越狱手机上使用的必要框架

patch完成之后需要对ipa进行签名。其中所有的dylib也都需要代码签名。问题是很多签名工具都不会自动签其中的dylib。签不好的话打开就闪退。最好用的是爱思助手,可以直接签好整个ipa。淘宝买个证书12块钱能用半年到一年。

这样拿到了签名后的ipa。可以直接用xcode安装。windows也可以用爱思助手安装。

再配合上一个imgui的菜单,就可以在非越狱的手机上随时随地开挂了。其实是添加了一个MTKView用来处理Metal渲染管线。
MTKView
的 draw(in:)
方法会调用 ImGui 的渲染函数,将 UI 渲染数据通过 Metal 命令提交到 GPU。框架写完之后,和在电脑上写挂没什么区别。
- Author:Lynnette177
- URL:https://next.lynnette.uk/article/jailed_hook
- Copyright:All articles in this blog, except for special statements, adopt BY-NC-SA agreement. Please indicate the source!
Relate Posts