1. 简介
什么是 Frida
Frida 是一款基于 Python 和 JavaScript 的动态代码插桩工具,能够在运行时注入代码到原生应用程序中。它支持 iOS、Android、macOS、Windows、Linux 等多个平台,常用于安全研究、逆向工程和应用分析。在 iOS 平台上,Frida 特别适合用于分析无源码应用和系统框架。
什么是 frida-trace
frida-trace 是 Frida 提供的一个命令行工具,用于快速跟踪函数调用。它可以自动生成用于跟踪函数调用的 JavaScript 钩子脚本,非常适合初步分析 iOS 应用程序的行为。对于 Objective-C 方法,frida-trace 可以轻松跟踪类和方法的调用过程。
主要特点
- 跨平台支持,对 iOS 有良好适配
- 实时函数跟踪
- 自动生成钩子脚本
- 可以修改函数参数和返回值
- 支持本地连接 USB 设备和远程设备
2. 安装与配置
环境要求
- Python 3.x
- pip(Python 包管理器)
- iOS 设备(有以下两种使用方式):
- 越狱设备:可直接注入 Frida
- 非越狱设备:需使用开发者证书重签名应用,或使用 Frida-Gadget
注意:本文主要介绍在越狱设备下的使用。
安装步骤
- 安装 Frida
pip install frida-tools
- iOS 设备配置
在 Sileo App上 添加 Frida 源并安装:
# 在 Sileo 中添加源
https://build.frida.re
# 安装 Frida 包
验证安装
# 检查 Frida 版本
frida --version
# 列出连接的设备
frida-ls-devices
# 列出正在运行的进程
frida-ps -U # 针对 USB 连接的设备
frida-ps -R # 针对远程设备
3. 基础入门
基本概念
- 目标进程:要跟踪的 iOS 应用程序
- 钩子(Hook):插入到目标进程中的代码
- 处理程序(Handler):定义钩子行为的 JavaScript 函数
基本命令格式
frida-trace [选项] -[操作] <目标>
常见选项:
-U:USB 连接的 iOS 设备-R:远程设备-f:启动应用-p:附加到进程 ID-n:按名称附加到进程
常见操作:
-i:包含要跟踪的函数-m:使用模式匹配跟踪函数,特别适合 Objective-C 方法-a:添加模块-I:包含/排除导入函数
基本用法示例
- 跟踪特定函数
# 跟踪 open 系统调用
frida-trace -U -i "open" 应用名
# 跟踪多个函数
frida-trace -U -i "open" -i "close" 应用名
- 使用模式匹配跟踪 Objective-C 方法
# 跟踪UIViewController类下的所有 实例方法
frida-trace -U -m "-[UIViewController *]" 应用名
# 跟踪UIViewController类下的所有 类方法
frida-trace -U -m "+[UIViewController *]" 应用名
# 使用通配符*,来跟踪UIViewController所有的实例方法和类方法
frida-trace -U -m "*[UIViewController *]" 应用名
# 跟踪指定方法
frida-trace -U -m "-[UIViewController viewDidLoad]" 应用名
# 跟踪多个类的方法
frida-trace -U -m "-[UIViewController *]" -m "-[UINavigationController *]" 应用名
- 跟踪框架中的方法
# 跟踪 UIKit 框架中的方法
frida-trace -U -m "-[UI* *]" 应用名
# 跟踪 Foundation 框架中的方法
frida-trace -U -m "-[NS* *]" 应用名
- 根据特定动作跟踪
# 跟踪 URL 加载相关方法
frida-trace -U -m "*[* URL*]" 应用名
# 跟踪网络请求相关方法
frida-trace -U -m "*[NSURLSession *]" -m "*[NSURLConnection *]" 应用名
4. 进阶使用
自定义 JavaScript 处理程序
frida-trace 会在当前目录生成名为 __handlers__ 的文件夹,其中包含为每个跟踪的函数生成的 js 文件。你可以编辑这些文件来自定义行为。
基本结构:
// 文件名: 函数名_函数地址.js
defineHandler({
onEnter(log, args, state) {
log("函数被调用,参数:" + args[0]);
// 保存状态以便在 onLeave 中使用
state.args0 = args[0];
},
onLeave(log, retval, state) {
log("函数返回,返回值:" + retval);
log("之前保存的参数:" + state.args0);
// 可以修改返回值
// retval.replace(0);
},
});
获取和修改参数值
对于不同类型的参数,Frida 提供了不同的访问方式:
整数和指针:
onEnter(log, args, state) {
// 读取整数参数
log('参数值: ' + args[0].toInt32());
// 对于64位整数
log('64位参数: ' + args[0].toString());
}
字符串:
onEnter(log, args, state) {
// 读取字符串参数
log('字符串: ' + args[0].readUtf8String());
// 或者读取特定长度
log('固定长度字符串: ' + args[0].readUtf8String(10));
}
Objective-C 对象:
onEnter(log, args, state) {
// 将参数转换为 ObjC 对象
const objcObj = new ObjC.Object(args[0]);
// 读取属性或调用方法
log('对象描述: ' + objcObj.toString());
log('对象类: ' + objcObj.$className);
// 调用方法
log('属性值: ' + objcObj.propertyName());
// 访问 NSString
if (objcObj.$className === 'NSString') {
log('字符串值: ' + objcObj.toString());
}
// 访问 NSArray
if (objcObj.$className === 'NSArray' || objcObj.$className.indexOf('NSArray') !== -1) {
const count = objcObj.count().valueOf();
log('数组元素数量: ' + count);
for (let i = 0; i < count; i++) {
log(`数组元素[${i}]: ${objcObj.objectAtIndex_(i)}`);
}
}
// 访问 NSDictionary
if (objcObj.$className === 'NSDictionary' || objcObj.$className.indexOf('NSDictionary') !== -1) {
const keys = objcObj.allKeys();
const count = keys.count().valueOf();
log('字典元素数量: ' + count);
for (let i = 0; i < count; i++) {
const key = keys.objectAtIndex_(i);
const value = objcObj.objectForKey_(key);
log(`字典元素[${key}]: ${value}`);
}
}
}
结构体和复杂对象:
onEnter(log, args, state) {
// 读取结构体字段
log('结构体字段1: ' + args[0].add(0).readPointer().toInt32());
log('结构体字段2: ' + args[0].add(4).readPointer().toInt32());
}
修改参数和返回值
onEnter(log, args, state) {
// 修改参数
args[0] = ptr('0x12345678');
// 对于字符串
const newString = Memory.allocUtf8String('新字符串');
args[0] = newString;
// 对于 Objective-C 对象
// 例如修改 NSString 参数
const NSString = ObjC.classes.NSString;
args[1] = NSString.stringWithString_("修改后的字符串");
}
onLeave(log, retval, state) {
// 修改返回值
retval.replace(1); // 改为返回 1
// 如果返回值是 ObjC 对象,也可以替换
const NSString = ObjC.classes.NSString;
retval.replace(NSString.stringWithString_("修改后的返回值"));
}
添加额外跟踪点
在运行时,你可以在现有处理程序中添加额外的跟踪点:
onEnter(log, args, state) {
// 找到函数地址
const targetAddr = Module.findExportByName(null, 'open');
// 为其添加拦截
Interceptor.attach(targetAddr, {
onEnter(args) {
log('open called with: ' + args[0].readUtf8String());
}
});
// 拦截 Objective-C 方法
const UIApplication = ObjC.classes.UIApplication;
Interceptor.attach(UIApplication['- openURL:'].implementation, {
onEnter(args) {
const url = new ObjC.Object(args[2]);
log('打开 URL: ' + url.toString());
}
});
}
访问内存和模块
// 查找模块
const module = Process.findModuleByName("UIKit");
log("模块基址: " + module.base);
// 查找导出函数
const func = Module.findExportByName(null, "UIApplicationMain");
log("函数地址: " + func);
// 读取内存
const data = Memory.readByteArray(ptr("0x12345678"), 32);
log("内存数据: " + hexdump(data));
枚举 Objective-C 类和方法
// 获取所有已加载的类
const classes = ObjC.classes;
for (const className in classes) {
log("类名: " + className);
}
// 获取特定类的所有方法
const UIViewController = ObjC.classes.UIViewController;
const methods = UIViewController.$ownMethods;
for (const method of methods) {
log("方法: " + method);
}
// 获取类的所有属性
const properties = UIViewController.$ownProperties;
for (const property in properties) {
log("属性: " + property);
}
5. 实战案例
案例 1: 跟踪网络请求
下面的示例展示如何跟踪 iOS 应用的网络请求:
frida-trace -U -m "-[NSURLSession dataTaskWith*]" 应用名
然后编辑生成的处理程序,如 __handlers__/NSURLSession/dataTaskWithURL_.js:
defineHandler({
onEnter(log, args, state) {
const session = new ObjC.Object(args[0]);
const url = new ObjC.Object(args[2]);
log(`[+] 发现网络请求: ${url.toString()}`);
state.url = url.toString();
// 可以打印请求头
if (args[2].$className === "NSMutableURLRequest") {
const request = args[2];
const headers = request.allHTTPHeaderFields();
if (headers) {
const keys = headers.allKeys();
const count = keys.count().valueOf();
log("HTTP Headers:");
for (let i = 0; i < count; i++) {
const key = keys.objectAtIndex_(i);
const value = headers.objectForKey_(key);
log(` ${key}: ${value}`);
}
}
// 打印 HTTP 方法
log(`HTTP Method: ${request.HTTPMethod()}`);
// 打印请求体
const body = request.HTTPBody();
if (body) {
const length = body.length().valueOf();
if (length > 0) {
const data = Memory.readByteArray(body.bytes(), length);
log(`Request Body (${length} bytes):`);
log(hexdump(data, { header: false, ansi: false }));
// 尝试解析为字符串
try {
const bodyStr = body.toString();
log(`Body as string: ${bodyStr}`);
} catch (e) {
log("Body cannot be converted to string");
}
}
}
}
},
onLeave(log, retval, state) {
log(`[-] 完成请求: ${state.url}`);
// 可以在这里获取响应(需要额外的钩子)
},
});
案例 2: 跟踪用户界面事件
frida-trace -U -m "-[UIControl sendAction:to:forEvent:]" 应用名
编辑生成的处理程序:
defineHandler({
onEnter(log, args, state) {
const control = new ObjC.Object(args[0]);
const action = args[2];
const target = new ObjC.Object(args[3]);
const event = new ObjC.Object(args[4]);
log(`[+] UI 事件被触发`);
log(` 控件类型: ${control.$className}`);
log(` 动作: ${ObjC.selectorAsString(action)}`);
log(` 目标: ${target.$className}`);
// 获取控件的标题(如按钮文本)
if (control.$className === "UIButton") {
const title = control.titleForState_(0); // UIControlStateNormal
if (title) {
log(` 按钮标题: ${title}`);
}
}
// 保存状态
state.action = ObjC.selectorAsString(action);
state.target = target.$className;
},
onLeave(log, retval, state) {
log(`[-] 完成事件: ${state.action} on ${state.target}`);
},
});
案例 3: 绕过 SSL 证书锁定
以下示例展示如何绕过 iOS 应用的 SSL 证书锁定:
frida-trace -U -n 应用名 -m "*[NSURLSession *]" -m "*[NSURLConnection *]"
然后创建自定义脚本 ssl_bypass.js:
defineHandler({
onLoad() {
// 替换 SecTrustEvaluate
Interceptor.replace(
Module.findExportByName("Security", "SecTrustEvaluate"),
new NativeCallback(
(trust, result) => {
log("[+] SecTrustEvaluate 被调用");
Memory.writePointer(result, ptr("0x1")); // 始终返回成功
return 0; // errSecSuccess
},
"int",
["pointer", "pointer"]
)
);
// 处理 NSURLSession 的验证
const SSLHandshake = ObjC.classes.NSURLSession.constructor.constructor;
const originalValidation =
SSLHandshake["- URLSession:didReceiveChallenge:completionHandler:"];
SSLHandshake["- URLSession:didReceiveChallenge:completionHandler:"] =
ObjC.implement(
SSLHandshake["- URLSession:didReceiveChallenge:completionHandler:"],
function (handle, selector, session, challenge, completionHandler) {
log("[+] NSURLSession 证书验证被拦截");
// 获取证书挑战
const objcChallenge = new ObjC.Object(challenge);
log(
` 挑战类型: ${objcChallenge
.protectionSpace()
.authenticationMethod()}`
);
// 始终接受证书
const kCFStreamSSLValidateServerCertificate = 0;
const credential = ObjC.classes.NSURLCredential.credentialForTrust_(
objcChallenge.protectionSpace().serverTrust()
);
const completionHandlerBlock = new ObjC.Block(completionHandler);
completionHandlerBlock.implementation(
kCFStreamSSLValidateServerCertificate, // NSURLSessionAuthChallengeUseCredential
credential
);
}
);
},
});
案例 4: 跟踪 Keychain 访问
frida-trace -U -n 应用名 -i "SecItemAdd" -i "SecItemCopyMatching" -i "SecItemUpdate" -i "SecItemDelete"
编辑生成的处理程序,例如 __handlers__/Security/SecItemAdd.js:
defineHandler({
onEnter(log, args, state) {
log(`[+] SecItemAdd 被调用 - 尝试写入 Keychain`);
// 获取第一个参数(CFDictionaryRef)
const dict = new ObjC.Object(args[0]);
// 打印 Keychain 项目内容
const keys = dict.allKeys();
const count = keys.count().valueOf();
log(`Keychain 项目 (${count} 个属性):`);
for (let i = 0; i < count; i++) {
const key = keys.objectAtIndex_(i);
let value = dict.objectForKey_(key);
// 特殊处理密码字段
if (key.toString() === "v_Data") {
try {
// 尝试解码为字符串
const rawData = Memory.readByteArray(
value.bytes(),
value.length().valueOf()
);
log(` ${key} = [二进制数据 ${value.length().valueOf()} 字节]`);
log(` 内容: ${hexdump(rawData, { header: false, ansi: false })}`);
// 尝试转换为字符串
value = ObjC.classes.NSString.alloc().initWithData_encoding_(
value,
4
); // NSUTF8StringEncoding
log(` UTF8字符串: ${value}`);
} catch (e) {
log(` ${key} = [无法解码的二进制数据]`);
}
} else {
log(` ${key} = ${value}`);
}
}
state.dict = dict;
},
onLeave(log, retval, state) {
log(`[-] SecItemAdd 返回: ${retval.toInt32()}`);
// OSStatus 返回码 (0 表示成功)
if (retval.toInt32() === 0) {
log(` Keychain 项目添加成功`);
} else {
log(` Keychain 项目添加失败,错误码: ${retval.toInt32()}`);
}
},
});
6. 资源与参考资料
官方资源
社区资源
- Frida CodeShare - 社区共享的 Frida 脚本
- Frida Slack 频道
- Awesome Frida - Frida 资源列表
iOS 特定资源
- iOS App Security Assessment Tools - OWASP 移动安全测试指南
- iOSRE - iOS 逆向工程资源
推荐工具
- iOS 特定工具
- Frida-iOS-Dump - 基于 Frida 的 iOS 应用砸壳工具
- bagbak - iOS 应用砸壳工具,支持 App 和 Extension
- bfinject - 向运行中的 iOS 应用注入动态库
- SSL Kill Switch 2 - 禁用 iOS 应用的 SSL 证书验证
- 配套工具
- r2frida - 将 Frida 与 Radare2 结合使用
- Ghidra - 用于静态分析
- Burp Suite - 用于网络流量分析