iOS Hook框架:Cydia Substrate与Frida
说实话,iOS逆向工程里最核心的能力,就是Hook。没有Hook,你只能看看二进制文件发呆,有了Hook,你就能在运行时修改任何东西。今天我们来聊聊两个主流框架:Cydia Substrate和Frida。
我个人习惯把Substrate叫做「老派硬核」,把Frida叫做「现代瑞士军刀」。两者各有千秋,但都值得你掌握。
Cydia Substrate原理与Tweak开发
Cydia Substrate,以前叫MobileSubstrate,是越狱iOS上最经典的Hook框架。它的核心原理其实不复杂:
- MSHookMessageEx:Hook Objective-C方法,替换IMP指针
- MSHookFunction:Hook C/C++函数,通过跳板修改指令
- MSFindSymbol:在内存中查找符号地址
说白了,Substrate就是在运行时把目标方法的实现指针,偷偷换成你的函数地址。等原方法被调用时,实际上跑的是你的代码。
核心流程:
- Substrate通过dyld加载到进程空间
- 扫描所有Objective-C类和方法列表
- 替换目标方法的IMP为你写的C函数
- 你的函数里可以调用%orig执行原逻辑
我在项目中遇到过一个问题:Substrate的Hook顺序很重要。如果你和另一个Tweak同时Hook同一个方法,后加载的会覆盖先加载的。嗯,这里要注意,依赖注入的顺序由Cydia的依赖系统控制。
Logos语法:%hook、%log、%orig
Logos是Substrate的预处理语法,它让你用更简洁的方式写Tweak。我刚开始用的时候觉得这玩意儿有点玄学,后来发现它其实就是宏展开。
%hook ClassName
- (void)methodName:(id)arg {
%log; // 打印调用日志
NSLog(@"Hook到了!参数: %@", arg);
%orig; // 调用原始实现
}
%end
几个关键指令:
| 指令 | 作用 |
|---|---|
| %hook | 开始Hook一个类,后面跟类名 |
| %log | 自动打印方法名、参数、返回值到syslog |
| %orig | 调用原始方法实现,可以传参 |
| %new | 给类添加新方法 |
| %c | 获取Class对象,等价于objc_getClass |
| %end | 结束Hook块 |
避坑指南:我曾经在%orig里传错了参数类型,导致App直接闪退。记住,%orig的参数必须和原方法签名完全一致,少一个都不行。
Frida on iOS:Frida-Gadget注入与ObjC Bridge
Frida是后来崛起的跨平台Hook框架。它不需要越狱,通过Frida-Gadget动态库注入就能工作。你想想看,这对非越狱设备来说简直是福音。
Frida在iOS上的工作方式有两种:
- Frida-Server:越狱设备上运行守护进程,通过USB/TCP通信
- Frida-Gadget:把动态库注入到App中,无需越狱
我个人更常用Gadget方式,因为不需要越狱设备。操作步骤很简单:
- 下载Frida-Gadget.dylib
- 用optool或insert_dylib注入到App二进制
- 重签名并安装到设备
- 用frida-trace或Python脚本连接
Frida的ObjC Bridge非常强大,可以直接在JavaScript里操作Objective-C对象:
// Frida脚本示例:Hook Objective-C方法
if (ObjC.available) {
var className = "SKPaymentQueue";
var methodName = "- addPayment:";
var hook = ObjC.classes[className][methodName];
Interceptor.attach(hook.implementation, {
onEnter: function(args) {
console.log("[*] 拦截到 addPayment:");
console.log("[*] 参数: " + ObjC.Object(args[2]));
// 这里可以修改参数或阻止调用
},
onLeave: function(retval) {
console.log("[*] 原始方法返回");
}
});
}
为什么会这样?因为Frida在底层做了ObjC Runtime的桥接,让你用JS就能调用objc_msgSend。说实话,我第一次看到这个的时候觉得太神奇了。
实战:编写Tweak绕过应用内购买
好了,理论讲完了,我们来点实际的。绕过IAP(应用内购买)是逆向工程的经典案例。你想想看,很多App的付费功能其实只是客户端校验,服务端根本不验证。
核心思路:Hook SKPaymentQueue 的 addPayment: 方法,让它什么都不做,然后手动调用 finishTransaction: 并伪造购买成功回调。
用Logos写Tweak:
%hook SKPaymentQueue
- (void)addPayment:(SKPayment *)payment {
%log;
NSLog(@"[IAP Bypass] 拦截到购买请求: %@", payment.productIdentifier);
// 不调用%orig,直接返回
}
- (void)finishTransaction:(SKPaymentTransaction *)transaction {
%log;
NSLog(@"[IAP Bypass] 完成交易: %@", transaction.payment.productIdentifier);
%orig;
}
%end
%hook SKPaymentTransactionObserver
- (void)paymentQueue:(SKPaymentQueue *)queue
updatedTransactions:(NSArray *)transactions {
%log;
// 伪造交易成功状态
for (SKPaymentTransaction *t in transactions) {
if (t.transactionState == SKPaymentTransactionStatePurchasing) {
// 这里可以伪造一个成功的交易对象
// 实际项目中需要更复杂的处理
}
}
%orig;
}
%end
重要提醒:绕过IAP仅供学习和安全研究使用。实际App的支付验证通常有服务端签名校验,单纯客户端Hook不一定有效。我曾经见过一个App,客户端Hook后显示购买成功,但服务端返回403,因为票据验证失败了。
用Frida实现同样的功能:
// Frida IAP Bypass脚本
if (ObjC.available) {
var SKPaymentQueue = ObjC.classes.SKPaymentQueue;
// Hook addPayment:
var addPayment = SKPaymentQueue['- addPayment:'];
Interceptor.attach(addPayment.implementation, {
onEnter: function(args) {
console.log("[IAP] 拦截购买: " +
ObjC.Object(args[2]).productIdentifier());
// 阻止实际购买
}
});
// Hook updatedTransactions
var observerProto = ObjC.protocols.SKPaymentTransactionObserver;
// 实际需要找到实现了该协议的类
console.log("[IAP] Frida脚本已加载");
}
嗯,这里要注意,实际项目中你还需要处理restoreCompletedTransactions和transactionReceipt的伪造。不过核心思路就是:让App以为购买成功了,但实际上你一分钱都没花。
我的经验:真正难搞的不是客户端Hook,而是服务端验证。很多App会把receipt data发到Apple服务器验证,这时候你需要在客户端Hook NSData的base64EncodedStringWithOptions:,把真实的receipt替换成你从越狱设备上抓取的有效receipt。这招我用了好多次,成功率很高。
知识体系总览
下面这张图总结了本章的核心知识点和它们之间的关系:
这张图把两大框架的组件和关系梳理清楚了。Substrate走的是越狱路线,依赖Logos语法;Frida更灵活,支持非越狱设备,通过ObjC Bridge用JS操作原生代码。最终都汇聚到同一个实战目标:绕过IAP。
好了,这一章的内容就到这里。记住,Hook框架只是工具,真正重要的是你分析App逻辑的能力。多动手,多踩坑,你也能成为逆向高手。