Frida是什么?
它是原生应用程序的 Greasemonkey,或者,用更专业的术语来说,它是一个动态代码检测工具包。它允许您将 JavaScript 片段或您自己的库注入到 Windows、macOS、GNU/Linux、iOS、Android 和 QNX 上的本机应用程序中。Frida 还为您提供了一些基于 Frida API 构建的简单工具。这些可以按原样使用,根据您的需要进行调整,或者作为如何使用 API 的示例。
环境依赖
推荐 python 3.x brew install python
安装 frida-tools
pip install frida-tools
主语言
Frida 的核心是用 C 语言编写的,并将QuickJS 注入到目标进程中,您的 JS 在执行时可以完全访问内存、挂钩函数,甚至在进程内调用本机函数。有一个双向通信通道,用于在您的应用程序和目标进程内运行的 JS 之间进行通信。
使用 Python 和 JS 可以使用无风险的 API 进行快速开发。Frida 可以帮助您轻松捕获 JS 中的错误,并为您提供异常而不是崩溃。
支持多个语言绑定 例如 Node.js、 Python、 Swift、 .NET、 Qml等。为其他语言和环境构建额外的绑定非常容易.
JavaScript 与 TypeScript 的区别 TypeScript 是 JavaScript 的超集,扩展了 JavaScript 的语法,因此现有的 JavaScript 代码可与 TypeScript 一起工作无需任何修改,TypeScript 通过类型注解提供编译时的静态类型检查。
什么是TypeSctipt? TypeScript 可处理已有的 JavaScript 代码,并只对其中的 TypeScript 代码进行编译。
运行模式
注释模式
大部分情况下,我们都是附加到一个已经运行到进程,或者是在程序启动到时候进行劫持,然后再在目标进程中运行我们的代码逻辑。这种方式是Frida最常用的使用方式。注入模式的大致实现思路是这样的,带有GumJS的Frida核心引擎被打包成一个动态连接库,然后把这个动态连接库注入到目标进程中,同时提供了一个双向通信通道,这样控制端就可以和注入的模块进行通信了,在不需要的时候,还可以在目标进程中把这个注入的模块给卸载掉。
嵌入模式
针对没root过的设备Frida提供了一个动态连接库组件 frida-gadget, 可以把这个动态库集成到程序里面来使用Frida的动态执行功能。一旦集成了gadget,就可以和程序使用Frida进行交互。
预加载模式
自动加载Js文件。
主要工具
查看可用的设备列表
frida-ls-devices
用于获取可用的设备列表,在多设备交互的情况下会非常有用
获取设备的进程列表
frida-ps
用于获取进程列表信息
参数 | 描述 |
---|---|
-U | 连接到USB设备 |
-D | 如果当前有多台USB设备,可以使用该参数指定设备的UDID(frida-ls-devices 列出的那些 id) |
-R/-H | 连接到远程 frida-server,主要用于远程调试 |
-a | 仅显示正在运行的应用 |
-i | 显示所有已安装的应用(包括 AppStore安装的应用和系统应用) |
举例: 连接到USB设备查看进程列表 frida-ps -U
连接到USB设备查看正在运行的应用 frida-ps -U -a
连接到USB设备查看所有安装的应用 frida-ps -U -a -i
连接到指定的USB设备查看正在运行的应用 frida-ps -D d007dc58edd70caad950ff01b41ebf73cfa49fbe -a
杀死进程
frida-kill
用来结束设备上的指定进程
跟踪函数/方法的调用
frida-trace
参数 | 描述 |
---|---|
-i | 包含的C函数 |
-x | 排除某个C函数 |
-m | hook匹配 OC |
-M | 忽略匹配的OC |
-y | hook匹配 Swift |
-Y | 忽略匹配的Swift |
-j | 跟踪某个java函数 |
-J | 排除某个java函数 |
-h | 其他参考帮助文档 |
开发中常用的调试方式
javascript
console.log('\tBacktrace:\n\t' + Thread.backtrace(this.context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join('\n\t'));
获取目标类
javascript
var targetClass = ObjC.classes.PDNetworkAccessManager;
获取目标方法
javascript
var targetMethod =
targetClass[
"- dataAccessFromNetWithFunctionId:param:finishBlock:cancelBlock:"
];
监听目标方法
javascript
Interceptor.attach(targetMethod.implementation, {
onEnter: function (args) {
var functionId = ObjC.Object(args[2]);
var param = ObjC.Object(args[3]);
var finishBlock = new ObjC.Block(args[4]);
var cancelBlock = new ObjC.Block(args[5];
},
onLeave: function (retval) {
// 在方法返回后处理返回值
console.log("Return value: " + ObjC.Object(retval));
},
});
接受方法参数的OC对象:
javascript
var param = ObjC.Object(args[2]);
接受Int类型参数:
javascript
var value32 = args[3].toInt32();
console.log("32位整数值: " + value32);
// 示例2:使用 toInt64
var value64 = args[3].toInt64();
console.log("64位整数值: " + value64);
打印对象类型:
javascript
console.log('Param类型:' + param.$className);
接受Block参数:
javascript
var finishBlock = new ObjC.Block(args[4]);
打印Block结构签名:
javascript
console.log('blockTypes:' + finishBlock.types);
创建Block:
javascript
var onCompleteBlock = new ObjC.Block({
retType: 'void',
argTypes: ['object', 'object'], // 两个对象参数
implementation: function (dict, error) {
console.log('Block 被调用');
}
});
拦截某个网络请求的Block回调数据:
js
var blockList = new Set(); // 避免提前释放,临时存储
var C = ObjC.classes.HTTPClient;
var m = C['- sendRequest:success:fail:'];
Interceptor.attach(m.implementation, {
onEnter: function (args) {
var successBlock = new ObjC.Block(args[3]);
blockList.add(successBlock);
var blockFunc = successBlock.implementation;
// 如果不确定有block的参数,可以通过types属性打印block签名
successBlock.implementation = function (result) {
console.log('网络请求结果:', result);
blockFunc(result); // 调用原始 success block
blockList.delete(successBlock);
}
},
});
打印某个进程的基址,基址可以用来推算实际偏址:
sh
偏移 = 内存地址 - 模块基址
js
Module.findBaseAddress('企业微信');
通过内存地址找到某个对象:
js
ObjC.Object(prt(0xaabbccdd))
修改基本类型参数:
javascript
args[2] = ptr(123);
nil
赋值:
javascript
args[3] = ptr("0x0");
获取某个类的所有方法:
javascript
var TestClass = ObjC.classes.TestClass;
console.log('所有方法:' + TestClass.$methods);
调用某个类的类方法:
javascript
// 获取类
var TestClass = ObjC.classes.TestClass;
// 获取类的 + topViewController 类方法
var topViewControllerMethod = TestClass['+ topViewController'];
// 调用类方法(有返回值)
var topViewController = topViewControllerMethod.call(TestClass);
调用某个类的实例方法:
javascript
// 获取类
var TestClass = ObjC.classes.TestClass;
// 获取类的 - topViewController 实例方法,且有一个参数NSString
var printMethod = TestClass['- print:'];
// 首先初始化获取实例
var object = TestClass.alloc().init();
var urlString = ObjC.classes.NSString.stringWithString_("打印的字符串");
// 调用实例方法(无返回值)
printMethod.call(object, urlString);
打印某个方法的imp地址:
js
var method = ObjC.classes.MMPreviewWindowController["- successToParseQRCodeResult:"];
console.log(method.implementation);
// 可以通过此地址进行lldb断点调试 br set -a 0x104b746d6
将Js对象转为Object对象(字典或数组):
js
function jsonToObject(jsValue) {
const NSString = ObjC.classes.NSString;
const NSJSONSerialization = ObjC.classes.NSJSONSerialization;
let jsonString;
if (typeof jsValue === 'string') {
jsonString = jsValue;
} else {
try {
jsonString = JSON.stringify(jsValue);
} catch (e) {
throw new Error('JSON.stringify 失败: ' + e);
}
}
const cStr = Memory.allocUtf8String(String(jsonString)); // 分配并写入 UTF-8
const nsStr = NSString.stringWithUTF8String_(cStr);
const data = nsStr.dataUsingEncoding_(4); // 4 = NSUTF8StringEncoding
const errPtr = Memory.alloc(Process.pointerSize);
Memory.writePointer(errPtr, ptr(0));
const objPtr = NSJSONSerialization.JSONObjectWithData_options_error_(data, 0, errPtr);
const errObjPtr = Memory.readPointer(errPtr);
if (objPtr.isNull()) {
if (!errObjPtr.isNull()) {
const errObj = new ObjC.Object(errObjPtr);
throw new Error('NSJSONSerialization 解析失败: ' + errObj.toString());
} else {
throw new Error('NSJSONSerialization 返回空且没有 NSError');
}
}
return new ObjC.Object(objPtr);
}
ObjC.available
: 一个布尔值,指定当前进程是否加载了Objective-C运行时。ObjC除非是这种情况,否则不要调用任何其他属性或方法。
ObjC.api
: 将函数名称映射到NativeFunction实例的对象,用于直接访问大部分Objective-C运行时 API。
ObjC.classesObjC.Object
:为每个当前注册的类映射类名到 JavaScript 绑定的对象。您可以通过使用点表示法并用下划线替换冒号来与对象交互,即: [NSString stringWithString:@"Hello World"]
变成 const { NSString } = ObjC.classes; NSString.stringWithString_("Hello World");
. 注意方法名称后面的下划线。
ObjC.protocols
: 一个对象映射协议名称到ObjC.Protocol 每个当前注册协议的 JavaScript 绑定。
ObjC.mainQueue
:主线程的GCD队列
ObjC.schedule(queue, work)work
: 在 .指定的 GCD 队列上调度 JavaScript 函数queue。AnNSAutoreleasePool
在调用之前创建work,并在返回时清理。
通过IP连接Frida调试
frida 默认的frida-service启动地址为 127.0.0.1,所以直接通过 frida -H 连接上无法访问的。 1.ssh 登录越狱设备后,重新启动一个frida-server服务,如:frida-server -l 0.0.0.0:12345
,然后通过 frida -H <ip:12345>
连接。 2.直接Frida启动文件,添加 -l 参数,路径在 /var/jb/Library/LaunchDaemons
或 /Library/LaunchDaemons
,启动文件名称为:re.frida.server.plist
.
xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>re.frida.server</string>
<key>Program</key>
<string>/var/jb/usr/sbin/frida-server</string>
<key>ProgramArguments</key>
<array>
<string>/var/jb/usr/sbin/frida-server</string>
</array>
<key>UserName</key>
<string>root</string>
<key>POSIXSpawnType</key>
<string>Interactive</string>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>ThrottleInterval</key>
<integer>5</integer>
<key>ExecuteAllowed</key>
<true/>
</dict>
</plist>
记得使用root用户修改,切换root用户可以使用 sudo -s
.
Frida JS代码给Photon开放接口
在Js代码中可以将方法报漏给Python调用:
js
/// 通过短链接打开小程序
function shortLinkOpenMiniProgram(shortLink) {
if (ObjC.available) {
try {
var pathUrl = shortLink;
var MMMessageUiUtils = ObjC.classes.MMMessageUiUtils;
var innerBrowser = ObjC.classes.NSString.stringWithString_(pathUrl);
var forceWebview = 0;
var withMessage = ptr("0x0");
MMMessageUiUtils['+ openInnerBrowser:forceWebview:withMessage:'](innerBrowser, forceWebview, withMessage);
} catch (e) {
console.log('[!] 模拟调用异常:', e);
}
} else {
console.log('[!] Objective-C 运行时不可用');
}
}
// 对外开放的方法,python调用
rpc.exports = {
shortLinkOpenMiniProgram: shortLinkOpenMiniProgram,
};
在Python中调用:
py
# 读取JS脚本,假设上面的路径为 JS_SCRIPT_PATH
with open(JS_SCRIPT_PATH, 'r', encoding='utf-8') as f:
js_code = f.read()
pid = get_wechat_pid()
session = frida.attach(pid)
script = session.create_script(js_code)
script.load()
script.exports_sync.short_link_open_mini_program('#小程序://某小程序/VcwOqlN49ynLeRn')
Frida17版本大变革
变动描述
从 Frida 17.0.0 开始,在Python中调用Frida脚本代码时候,ObjC
对象不再作为全局变量自动可用,需要显式导入 frida-objc-bridge
,否则报错ObjC is not defined
,参考:github.com/frida/frida... 。
Frida 在命令行直接调试是不影响的,主要是Python调用的代码,需要打包。
解决方案
1. 使用 frida-compile(推荐)
这是新版本 Frida 的标准做法:
bash
# 安装必要的依赖
npm install --save-dev frida-compile
npm install frida-objc-bridge
npm install --save-dev @types/frida-gum
##### 或者直接运行
npx frida-compile -S -c calendar_price.ts -o _agent.js
2. 在 Python 中使用
python
import frida
#### 读取编译后的脚本
with open('_agent.js', 'r') as f:
script_content = f.read()
#### 连接设备和应用
device = frida.get_usb_device()
session = device.attach("应用名") # 或使用 bundle ID
#### 创建和加载脚本
script = session.create_script(script_content)
script.load()
#### 调用函数
script.exports.simulateCalendarPriceRequest(1397051404)
文件说明
calendar_price.ts
- TypeScript 源码_agent.js
- 编译后的 JavaScript 文件(用于 Frida)package.json
- 包含编译脚本的配置
注意事项
- 新版本 Frida 需要显式导入所需的 bridge
- 编译步骤是必须的,Python里不能直接运行 TypeScript 文件
- 如果遇到其他 bridge(如 Swift bridge)的问题,可能需要额外处理
Demo
Javascript
Python
直接见LinXunFeng大牛写好的代码:Github
更多接口文档
注意
Mac测试需要关闭安全模式 iPhone使用Frida一般需要越狱,如果不越狱使用,可以参考文档