一次性脚本的痛点
逆向工程师的电脑里都有一堆 test1.js、hook_final.js、hook_final_真的最终版.js——一次性脚本越攒越多,换个 App 又要重写 80%。Frida 脚本需要工程化:模块化、可配置、可复用。
模块化:按功能拆分
把常用能力拆成独立模块,用 require 组合:
// lib/utils.js
function bytesToHex(bytes) {
return Array.from(new Uint8Array(bytes))
.map(b => b.toString(16).padStart(2, '0')).join('');
}
function bytesToStr(bytes) {
return String.fromCharCode.apply(null, new Uint8Array(bytes));
}
module.exports = { bytesToHex, bytesToStr };
// lib/okhttp.js —— OkHttp 抓包模块
var { bytesToStr } = require('./utils.js');
function hookOkHttp(onRequest) {
Java.perform(function() {
var Chain = Java.use('okhttp3.internal.http.RealInterceptorChain');
Chain.proceed.overload('okhttp3.Request').implementation = function(req) {
onRequest(req.url().toString(), req.method());
return this.proceed(req);
};
});
}
module.exports = { hookOkHttp };
// main.js —— 具体项目的组装
var { hookOkHttp } = require('./lib/okhttp.js');
hookOkHttp(function(url, method) {
if (url.includes('api.target.com')) {
console.log(method + ' ' + url);
}
});Frida 17+ 原生支持 require(frida-compile 打包),老版本用 frida-compile 工具链。
可配置:目标信息外置
把 App 相关的信息抽成配置,脚本主体不变:
// config.js
module.exports = {
packageName: 'com.target.app',
signClass: 'com.target.app.SignUtils',
signMethod: 'getSign',
apiFilter: 'api.target.com',
logFile: '/sdcard/target_hook.log',
};
// main.js
var config = require('./config.js');
// 所有硬编码换成 config.xxx换目标 App 时只改 config.js——脚本主体零改动。
RPC 服务化:签名能力的标准封装
逆向项目的终极产物大多是"签名服务"。标准封装模式:
// sign_server.js
Java.perform(function() {
var SignUtils = Java.use(config.signClass);
rpc.exports = {
sign: function(params) {
return SignUtils[config.signMethod](params);
},
// 健康检查接口,监控必备
ping: function() { return 'pong'; },
// 版本信息,排查 App 更新导致的问题
appVersion: function() {
var ctx = Java.use('android.app.ActivityThread')
.currentApplication().getApplicationContext();
var pm = ctx.getPackageManager();
return pm.getPackageInfo(ctx.getPackageName(), 0).versionName.getInt();
}
};
});Python 侧的标准客户端:
import frida
class SignService:
def __init__(self, device_id=None):
self.device = frida.get_device(device_id) if device_id else frida.get_usb_device()
self.session = self.device.attach(config['packageName'])
self.script = self.session.create_script(open('sign_server.js').read())
self.script.load()
def sign(self, params):
return self.script.exports.sign(params)
def health(self):
try:
return self.script.exports.ping() == 'pong'
except:
return False精髓:三个工程实践
- 日志分级与落盘。hook 脚本里
console.log在 App 高频调用时会刷屏卡死。分级:ERROR必打、INFO关键节点、DEBUG调试期。生产环境日志写文件(frida 的FileAPI 或 send 到 Python 侧落盘),别刷终端 - 异常隔离。每个 hook 点的回调里包 try-catch——一个 hook 点抛异常不能搞挂整个脚本(Frida 的默认行为是异常后该 hook 失效,静默失效最难排查):
try {
// hook 逻辑
} catch(e) {
console.log('[ERROR] hook点名称: ' + e);
}- 版本适配层。App 更新后类名/方法名可能变,把"定位目标类"的逻辑做成特征搜索而非硬编码:
function findSignClass() {
// 按特征找:有 getSign 方法且引用了特定字符串
var classes = Java.enumerateLoadedClassesSync();
for (var name of classes) {
try {
var cls = Java.use(name);
if (cls.class.getDeclaredMethods().some(m => m.getName() === 'getSign')) {
return cls;
}
} catch(e) {}
}
}总结
- 模块化:utils/抓包/签名拆库,require 组合
- 可配置:App 信息外置 config,换目标零改动
- RPC 服务化:sign/ping/version 标准三接口
- 日志分级、异常隔离、特征定位,生产三件套
交流微信:run1255