当前位置: 首页 > news >正文

不越狱给iOS App装Tweak/插件:LiveContainer环境介绍与Tweak编写

自iOS 16.5.1之后,Arm64e设备的越狱一直处于停滞状态,没有新的进展。
不少越狱开发者也逐渐脱离越狱开发,转向JIT等新方式面对iOS的新环境。

iOS App相关的Tweak/插件在越狱鼎盛时代生机勃勃,Flex3的注入和Cydia等包管理商店里的注入层出不穷,如今想起来还觉得是个好时代。

那么如今iOS环境下,还可以写Tweak,还可以注入dylib,还可以Hook吗?

可以的!

项目LiveContainer正为我们提供了这样一个平台。

这几周编写时发现很难找到类似的文章,所以打算在这里记录一下。

LiveContainer环境

LiveContainer为未越狱iOS用户提供了一个不受签名限制,可自由安装App的美妙平台。
并且LiveContainer自带TweakLoader.dylib,可以在App启动时自动加载Tweak进行注入;
同时还内置ElleKit,Tweak不必自己链接CydiaSubstrate或其他Hook框架,直接使用即可。

选择直接将Tweak打包到IPA中侧载虽然也是个选择,但是3 Apps限制还是太难受;
借助LiveContaier,可以安装任意数量的应用,并且这种Tweak载入方式可以让使用者只需要更新IPA,不需要在每次更新时都反反复复给IPA注入一次,开发者也可以省心不少。

虽然LiveContainer可能会遇到一些兼容性问题,但是其带来的利好实在是太过庞大,以至于这些兼容性问题在我看来皆可以忽略。

为LiveContainer环境开发Tweak

插件Android有,iOS没有一直是普遍存在的情况,在网上冲浪时看到LiveContainer的Tweak注入功能后,我就决心打破这一僵局。
这里为大家说明一下在LiveContainer下的Tweak需要注意的部分:

开发环境依然可以选用Theos,并且不需要使用jailed模版(或者说我没用过不知道啊)。

因为LiveContainer环境的特殊性,我们Hook所需要的函数(MSHookFunction/MSHookMessageEx)均已经由TweakLoader加载了,我们的Tweak在加载时,只需要使用dlsym找到对应的函数即可,不需要再链接库。

如果你的Tweak需要链接第三方库,尽量让LiveContainer去加载,然后找标志,而不要自己链接,否则很容易出现各种问题。

MSHookFunction是针对C/C++等无法通过别的方式劫持(fishhook提供了一种以篡改Mach-O导入表Hook函数的方案,但是该方案局限性大,只适用于动态链接库的函数,所以这里不再提及),必须通过篡改可执行文件__TEXT内存段进行Hook的函数,需要绕过Apple内存保护。(这点我们稍后会提及)

MSHookMessageEx则是针对Objective-C函数,通过劫持ObjC的方法信息Hook函数。不需要绕过内存保护,在非越狱环境不需要任何额外权限,可直接运行。

// 这里演示如何使用TweakLoader已经加载好的两个Hook函数
#import <Foundation/Foundation.h>
#import <dlfcn.h>// 定义将要用到的两个Hook函数的函数指针
typedef void (*MSHookFunction_t)(void *symbol, void *hook, void **old);
static MSHookFunction_t MSHookFunction_p = nullptr;typedef void (*MSHookMessageEx_t)(Class _class, SEL message, IMP hook, IMP *old);
static MSHookMessageEx_t MSHookMessageEx_p = nullptr;// 在初始化时通过dlsym找到函数
__attribute__((constructor))
static void tweakConstructor() {MSHookFunction_p = (MSHookFunction_t)dlsym(RTLD_DEFAULT, "MSHookFunction");if (!MSHookFunction_p) {NSLog(@"Failed to find MSHookFunction.");return;}MSHookMessageEx_p = (MSHookMessageEx_t)dlsym(RTLD_DEFAULT, "MSHookMessageEx");if (!MSHookMessageEx_p) {NSLog(@"Failed to find MSHookMessageEx.");return;}
}

也正是因为这种导入方式,我们不能使用Logos的快捷语法,否则Theos会自动链接CydiaSubstrate库,导致加载库时找不到路径。

最好是老实使用Objective-C/C++语法,手动进行Hook。(其实也不是太复杂)

// 使用MSHookMessageEx钩取一个ObjC函数(以[AppController didFinishLaunchingWithOptions:]为例)// 先定义函数指针和Hook后函数
static BOOL (*original_didFinishLaunchingWithOptions)(id self, SEL _cmd, UIApplication *application, NSDictionary *launchOptions) = nullptr;
BOOL hooked_didFinishLaunchingWithOptions(id self, SEL _cmd, UIApplication *application, NSDictionary *launchOptions) {// 你自己的实现...// 如果要调用原函数:original_didFinishLaunchingWithOptions(self, _cmd, application, launchOptions);
}// 初始化函数或你自己的某个函数:
void hookAppControllerFunc() {Class appController = NSClassFromString(@"AppController");// 找不到Class的时候应当通过NSLog等方式输出错误信息,并避免下一步执行if (!appController) return;MSHookMessageEx_p(targetClass,@selector(application:didFinishLaunchingWithOptions:),(IMP)hooked_didFinishLaunchingWithOptions,(IMP *)&original_didFinishLaunchingWithOptions);
}

绕过内存保护做Inline Hook MSHookFunction

上面我们提到,对于使用C/C++及其他语言编写的程序,并没有ObjC这样不需要高权限的Hook方案,需要直接对内存打补丁。

以往越狱环境下,Tweak一般有权限将R-X内存页设置为RW-再设置回R-X
Inline Hook需要这一过程去写入跳转。

但别忘了,我们现在是在非越狱环境,可没有那个权限。
那么我的Unity IL2CPP应用和其他那些非ObjC应用怎么办?

当然是有办法滴!

iOS 15 ~ iOS 18:JIT

在iOS 26以前的版本中,通过启用JIT就能让应用拿到这个权限,允许程序修改内存后再将内存设置为可执行。

即在LiveContainer中,为你的应用开启“带JIT启动”即可破解这个难关!

如果是侧载应用,在SideStore里对应用启用JIT即可!

虽然要多折腾那么一点,但是除了iOS 17.0 ~ iOS 17.3.1的JIT启用有些烦人,都蛮简单的,在设备上就可以完成。

iOS 26+:JIT+SkitDebug(挂载Geode.js)

iOS 26 RC版本开始,Apple加强了其保护策略,即使应用启用了JIT,也没有权限将已经改为RW的内存改回RX。
导致程序在运行到对应Hook片段时,会因没有执行权限而直接崩溃。

寻找破局之法

我想到SkitDebug对于UTM等虚拟机应用的支持,是通过JS脚本实现的。
那我是否可以借助这个机制,让我的程序可以做Inline Hook呢?

于是我到SkitDebug的源代码中寻找,发现了下面这个文件Geode.js的这个片段:

... else if (brkImmediate === 0x70) { // patching instructslog(`Received command to patch instructions (0x70)`);let x2Match = /02:(?<reg>[0-9a-f]{16});/.exec(brkResponse);let x2 = x2Match ? x2Match.groups['reg'] : null;if (!x1 || !x2) {log(`Missing x1 or x2 for function patching`);continue;}let destAddr = x0Num;let srcAddr = x1Num;let size = x2 ? littleEndianHexStringToNumber(x2) : 10n;log(`Patching: dest=0x${destAddr.toString(16)}, src=0x${srcAddr.toString(16)}, size=0x${size.toString(16)}`);// Unsure exactly why, but anything over 4 MB freezes the app for some reason, so we will set a soft limitif (size > 0x400000n) {log(`Size too large (0x${size.toString(16)}), skipping`);let pcPlus4 = numberToLittleEndianHexString(pcNum + 4n);let pcPlus4Response = send_command(`P20=${pcPlus4};thread:${tid};`);log(`pcPlus4Response = ${pcPlus4Response}`);validBreakpoints++;continue;}try {// m (read) = `m${curPointer.toString(16)},<size>`// M (write) = `M${curPointer.toString(16)},<size>:<your hex instructions goes here>`const CHUNK_SIZE = 0x4000n; // 16 KBfor (let i = 0n; i < size; i += CHUNK_SIZE) {let chunkSize = i + CHUNK_SIZE <= size ? CHUNK_SIZE : size - i;let readAddr = srcAddr + i;let writeAddr = destAddr + i;let readRes = send_command(`m${readAddr.toString(16)},${chunkSize.toString(16)}`);if (readRes && readRes.length > 0) {let writeResponse = send_command(`M${writeAddr.toString(16)},${chunkSize.toString(16)}:${readRes}`);if (writeResponse !== "OK") {log(`Write failed at offset ${i.toString(16)}`);break;}}if (Number(i / CHUNK_SIZE) % 10 === 0) {log(`Progress: 0x${i.toString(16)}/0x${size.toString(16)}`);}}log(`Memory write completed!`);} catch (e) {log(`Memory write failed: ${e}`);}let pcPlus4 = numberToLittleEndianHexString(pcNum + 4n);let pcPlus4Response = send_command(`P20=${pcPlus4};thread:${tid};`);log(`pcPlus4Response = ${pcPlus4Response}`);validBreakpoints++;
}

这个片段展示了SkitDebug接收到应用程序发出的BRK #0x70断点,从x0x1寄存器中取出地址,x2寄存器中取出长度,依赖调试器实现高权限内存写入的过程。

这一过程可以完全绕开原本的实现途径,由具有高权限的调试器进行内存写入,不需要进行权限修改

通过这种方式,即使修改内存,也不会引起可感知的内存段权限变化!
(即RX内存写入后仍然为RX)

这不就把权限问题绕开了吗?真巧妙!

随后我查看了这段代码的源头Geode(即Geode SDK iOS),找到了对应的代码片段
dyld_bypass_validation_txm.m

...// ldr x8, value; br x8; value: .ascii "\x41\x42\x43\x44\x45\x46\x47\x48"
static char patch[] = { 0x88, 0x00, 0x00, 0x58, 0x00, 0x01, 0x1f, 0xd6, 0x1f, 0x20, 0x03, 0xd5, 0x1f, 0x20, 0x03, 0xd5, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41 };// x0 (dest), x1 (src), x2 (bytes)
__attribute__((noinline,optnone,naked))
void BreakJITWrite(void* dest, void* src, size_t bytes) {asm("brk #0x70 \n""ret");
}static bool redirectFunction(char* name, void* patchAddr, void* target) {if (has_txm()) {BreakJITWrite(patchAddr, patch, sizeof(patch));}...
}

看来Geode SDK似乎利用BreakJITWrite写入了一个无条件跳转,进行函数重定向。
(下面的代码段显示Hook了dyld_mmapdyld_fcntl

这不正是我们要做的吗!?

有了方法,要怎么实现呢?

改写Dobby实现Inline Hook

我也尝试过自己造一个Hook框架,但是太过复杂,我的知识水平和时间都不足以实现这样一个东西。

那——有没有现成的轮子呢?有的!

先后考察了祖师爷CydiaSubstrateSubstituteElleKitlitehook之后,我认为改写起来最简便、功能最全的还是跨平台框架Dobby

那就动手吧!

直接搜索mach_vm寻找内存操作函数,可以发现最底层进行内存操作的是code-patch-tool-darwin.cc文件中的DobbyCodePatch函数

由于代码比较长,原始代码这里就不再粘贴了,总之修改起来非常简单:
保留跨页处理,后面所有的mach_vm操作都可以不要,直接用BreakJITWrite即可

如果彻底不要兼容性(毕竟我只是拿来给iOS 26以上用,以下的有内置的ElleKit)
修改后大概是这个样子:

// code-patch-tool-darwin.cc => DobbyCodePatch
// ...这里是 Cross over page 跨页逻辑部分...// 采用分块写入方式,每次写page_size以免卡死const size_t CHUNK_SIZE = page_size;size_t remaining = buffer_size;uint8_t *src_ptr = buffer;addr_t dest_addr_base = (addr_t)address;while (remaining > 0) {size_t this_chunk = remaining > CHUNK_SIZE ? CHUNK_SIZE : remaining;void *dest_chunk = (void *)(dest_addr_base + (buffer_size - remaining));void *src_chunk = (void *)src_ptr;BreakJITWrite(dest_chunk, src_chunk, this_chunk);src_ptr += this_chunk;remaining -= this_chunk;}ClearCache(address, (void *)((addr_t)address + buffer_size));return 0;
}

虽然可能线程不安全,但也没权限做成线程安全啊。

为iOS平台编译之后,导入LiveContainer,成了!

由于DobbyHook的声明和MSHookFunction一模一样,我们可以直接:

if (@available(iOS 26.0, *)) {MSHookFunction_p = (MSHookFunction_t)dlsym(RTLD_DEFAULT, "DobbyHook");
} else {MSHookFunction_p = (MSHookFunction_t)dlsym(RTLD_DEFAULT, "MSHookFunction");
}

LiveContainer是递归加载Dylib的,可以利用这一点让libdobby.dylib始终早于你的Tweak加载。

LiveContainer的Nightly Release版本已经支持为应用单独指定JIT启动脚本,只要选择Geode.js,配合上面魔改的libdobby.dylib,即可让你的Tweak在iOS 26正常Hook!

至此从iOS 15(LiveContainer最低支持版本)至iOS 26均可以进行Inline Hook。

写在最后

这些内容是我两周的从零开始开发所遇到的,关于Hook的所有问题的集合。

我这方面水平真的还是有所欠缺,如果有什么地方说得不对,还请各位见谅。

开发过程中,真的感受到iOS作为一个封闭系统,社区却有着震撼人心的蓬勃生机,有着各种奇思妙想,想尽办法将iOS给出的限制逐一打破。

写着、思考着,我也感受到了这种快乐,震撼于前辈开发者们的惊人智慧,为自己绞尽脑汁终于成功运行的东西而心花怒放。

只要愿意再深入那么一点点,庞大iOS社区所开拓的一个辽阔的世界就在你的面前。

http://www.jsqmd.com/news/33446/

相关文章:

  • 课后作业2(异常处理)
  • Bigtop 从零开始搭建大数据集群
  • chatgpt-to-md优化并重新复习
  • 从零开始制作 MyOS(六)
  • 2025年11月介电常数测试仪推荐厂家排行:应该如何选择靠谱供应商
  • 2025年11月电阻率测试仪工厂推荐:北京冠测精电——信誉、口碑与售后的三重保障
  • SaaS版MES系统PC端后台特性清单与设计说明
  • 【2025臻选指南】酸角糕十大品牌深度解析:传承古法与现代创新的完美融合
  • 2025年水上游乐及泳池设备标杆厂家推荐:山东汇川,室内水上乐园/儿童水上乐园/大型水上乐园/室内泳池/定制化服务引领行业新标​
  • 深入解析:开源 C++ QT QML 开发(十四)进程用途
  • 各种扩展模块
  • 2025年冷冻式干燥机标杆厂家最新推荐:凌宇机械,冷冻式压缩空气干燥机/风冷高温冷冻式干燥机/水冷高温冷冻式干燥机/吸附式干燥机/以高效节能与全场景方案树立行业新标准
  • 2025全日制艺考生文化课机构推荐榜:全日制艺考生文化课,小众优质机构精准适配需求
  • 2025艺术涂料厂家推荐榜:进口艺术涂料、意大利进口艺术涂料、意大利艺术涂料厂家,装修选品不踩坑
  • 2025济南艺考文化课培训推荐榜:艺考文化课培训,艺考文化课培训机构适配不同艺考生需求
  • For xinye666
  • 从手写周报到智能生成:PandaCoder如何让你的工作汇报效率提升10倍
  • 2025优质媒体服务商推荐榜:媒体邀约靠谱平台助力品牌高效传播
  • 2025大连汽车凹陷修复厂家推荐榜:震城汽车领衔,汽车数据修复厂家靠谱机构守护原厂漆质感
  • 详细介绍:iBizModel 实体主状态(PSDEMAINSTATE)及主状态逻辑(PSDELOGIC`MAINSTATELOGIC`)模型详解
  • 2025氮化硼陶瓷推荐榜:福维科(山东)五星领跑,氮化硼陶瓷高温绝缘体/坩埚/套管/基板/高温构件/耐腐蚀构件优质厂家赋能产业升级
  • Maui 实践:JavaScript 动态生成集合属性的 get/set 代理
  • 2025小王子亚麻籽薯片工厂推荐榜:小王子亚麻籽薯片厂家实力派承包休闲时光
  • 2025大连养老院机构推荐榜:养老院中心、社区养老院守护舒心晚年
  • Apache是干嘛用的?Apache服务器搭建教程
  • ewomail docker搭建
  • Playwright为什么老是跑不稳?12个坑踩完我终于懂了!
  • 阿里云微服务引擎 MSE 及 API 网关 2025 年 10 月产品动态
  • 2025年厂房降温设备厂家新推荐排行榜白皮书,厂房降温设备哪个厂家好
  • 无畏级战列舰(HMS Dreadnought)