关于 rev-frida
面向动态分析人员的 Frida 脚本生成指南,用于运行时插桩、Hook 和检查。内容涵盖现代 Frida API,并强调按模块加载时机安装 Hook,以及谨慎处理初始化阶段。
主要功能
- 01生成原生导出函数 Hook 脚本
- 02覆盖 Java 与 ObjC 方法 Hook
- 03捕获参数、返回值和运行轨迹
- 04读取与写入内存并转储数据
- 05根据模块加载事件安装 Hook
- 06提供初始化阶段 Hook 风险指引
适用场景
- 01检查应用运行时的函数调用与数据
- 02在目标原生模块加载后安装 Hook
- 03分析 Java 或 ObjC 方法行为
- 04排查构造函数阶段的反调试行为
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ p4nda0s/ reverse-skills --skill rev-frida安装后在 Claude Code 中输入/rev-frida确认已加载。
详细介绍
rev-frida - Frida 脚本生成器
生成用于动态分析、Hook 和运行时检查的 Frida 插桩脚本。
概览
使用 Frida 实现:
- 原生导出函数 Hook
- Java 或 ObjC 方法 Hook
- 运行时跟踪
- 参数或返回值捕获
- 内存转储
- 感知加载器的原生插桩
重要:现代 Frida CLI
现代 Frida CLI 不使用 --no-pause。进程启动后会在脚本加载完毕时恢复运行。
# Spawn and hook
frida -U -f com.example.app -l hook.js
# Attach to running process
frida -U com.example.app -l hook.js
# Attach by PID
frida -U -p 1234 -l hook.js
现代 API 参考
模块和符号查找
const mod = Process.getModuleByName("libssl.so");
mod.name;
mod.base;
mod.size;
mod.path;
const ptr = mod.getExportByName("SSL_read");
Process.enumerateModules();
mod.enumerateExports();
mod.enumerateImports();
const addr = Module.getExportByName(null, "open");
Interceptor
Interceptor.attach(ptr, {
onEnter(args) {
console.log("arg0:", args[0].toInt32());
console.log("arg1 str:", args[1].readUtf8String());
},
onLeave(retval) {
console.log("ret:", retval.toInt32());
}
});
Interceptor.replace(ptr, new NativeCallback(function (a0, a1) {
console.log("replaced");
return 0;
}, "int", ["pointer", "int"]));
NativeFunction 和 NativeCallback
const open = new NativeFunction(
Module.getExportByName(null, "open"),
"int",
["pointer", "int"]
);
const fd = open(Memory.allocUtf8String("/etc/hosts"), 0);
const cb = new NativeCallback(function (arg) {
console.log("called with:", arg);
return 0;
}, "int", ["int"]);
内存操作
ptr(addr).readByteArray(size);
ptr(addr).readUtf8String();
ptr(addr).readU32();
ptr(addr).readPointer();
ptr(addr).writeByteArray(bytes);
ptr(addr).writeUtf8String("hello");
ptr(addr).writeU32(0x41414141);
const buf = Memory.alloc(256);
const str = Memory.allocUtf8String("hello");
Memory.scan(mod.base, mod.size, "48 89 5C 24 ?? 48 89 6C", {
onMatch(address, size) {
console.log("found at:", address);
},
onComplete() {}
});
ObjC
if (ObjC.available) {
const hook = ObjC.classes.ClassName["- methodName:"];
Interceptor.attach(hook.implementation, {
onEnter(args) {
const selfObj = new ObjC.Object(args[0]);
const param = new ObjC.Object(args[2]);
console.log(selfObj.toString());
console.log(param.toString());
}
});
}
Java
if (Java.available) {
Java.perform(function () {
const Activity = Java.use("android.app.Activity");
Activity.onCreate.implementation = function (bundle) {
console.log("onCreate called");
return this.onCreate(bundle);
};
});
}
脚本生成指南
生成 Frida 脚本时:
- 始终使用现代 API,例如
Process.getModuleByName()和mod.getExportByName()。 - 不要使用
--no-pause。 - 优先采用由加载事件驱动的原生 Hook,而不是轮询。
- 以易读的形式打印指针和缓冲区。
- 对有风险的 Hook 使用
try/catch包装。 - 使用
hexdump()检查二进制数据。
处理原生模块加载时序
不要假设目标 .so 已经加载。
首选顺序:
- Hook
android_dlopen_ext或dlopen,并在目标库加载时安装 Hook。 - 立即使用
Process.findModuleByName()检查模块是否已加载。 - 仅在后备情况下使用轮询。
默认使用此辅助函数:
function hookModuleLoad(moduleName, callback) {
const dlopen = Module.findGlobalExportByName("android_dlopen_ext")
|| Module.findGlobalExportByName("dlopen");
if (!dlopen) {
throw new Error("dlopen/android_dlopen_ext not found");
}
const hooked = new Set();
Interceptor.attach(dlopen, {
onEnter(args) {
this.path = args[0].isNull() ? null : args[0].readCString();
this.shouldHook = this.path && this.path.indexOf(moduleName) !== -1;
},
onLeave(retval) {
if (!this.shouldHook || retval.isNull()) {
return;
}
const mod = Process.findModuleByName(moduleName);
if (!mod) {
return;
}
const key = mod.base.toString();
if (hooked.has(key)) {
return;
}
hooked.add(key);
callback(mod);
}
});
}
像这样使用:
hookModuleLoad("libtarget.so", function (mod) {
const target = mod.getExportByName("target_export");
Interceptor.attach(target, {
onEnter(args) {
console.log("target_export called");
}
});
});
如果目标可能已经加载,请将立即检查与加载 Hook 结合使用:
function hookNowOrOnLoad(moduleName, callback) {
const mod = Process.findModuleByName(moduleName);
if (mod) {
callback(mod);
return;
}
hookModuleLoad(moduleName, callback);
}
仅在后备情况下使用轮询:
function hookWhenReady(moduleName, exportName, callbacks) {
const mod = Process.findModuleByName(moduleName);
if (mod) {
Interceptor.attach(mod.getExportByName(exportName), callbacks);
return;
}
const timer = setInterval(function () {
const loaded = Process.findModuleByName(moduleName);
if (!loaded) {
return;
}
clearInterval(timer);
Interceptor.attach(loaded.getExportByName(exportName), callbacks);
}, 100);
}
说明:
- 在 Android 上,优先使用
android_dlopen_ext,然后再考虑dlopen。 - 根据模块基址去重,而不只是根据路径。
- 通常在
dlopen/android_dlopen_ext的onLeave中安装 Hook 最合适,此时构造函数已运行完毕。 - 如果原生加载由 Java 驱动,还应考虑
System.loadLibrary、Runtime.loadLibrary0、dlsym或RegisterNatives。
不要盲目 Hook init 系列函数
不要建议用户盲目 Hook .init、.init_array、构造函数或 JNI_OnLoad。
这些位置很脆弱:
- 许多库会在这些位置执行一次性设置
- 不当的 Hook 可能会在真正的目标逻辑运行前导致进程崩溃
- 过早的 Hook 可能改变时序,并掩盖用户想要研究的行为
- 构造函数代码通常会调用许多不相关的辅助函数
建议在初始化阶段 Hook 之前:
- 确定为何必须过早 Hook
- 确定具体模块和确切的初始化例程
- 确认目标符号或行为是否仅在常规导出函数可用之前存在
- 如果较晚的稳定 Hook 能提供同样的可见性,则优先使用它
优先顺序:
- 在模块加载后 Hook 稳定的导出函数
- Hook
RegisterNatives、dlsym或第一个实际业务函数 - 仅当原生注册或反调试设置发生在
JNI_OnLoad中时,才 HookJNI_OnLoad - 仅在有充分证据表明关键逻辑位于构造函数或
.init_array中时,才 Hook 它们
如果建议使用早期初始化 Hook,请说明:
- 常规导出函数 Hook 为何不够
- 应 Hook 的确切函数或地址
- 预期会出现何种故障
- 如何验证该 Hook 没有破坏初始化
错误建议:
// do not suggest this without a reason
Interceptor.attach(Module.findBaseAddress("libtarget.so").add(0x1234), ...);
更好的建议:
- 等待模块加载
- 通过符号、交叉引用、字符串或跟踪证据确认构造函数目标
- 识别出确切的初始化函数后再附加 Hook
- 使用初始化阶段 Hook 之前,向用户说明风险
优先使用构造函数分发器,而不是盲目使用 init 钩子
如果怀疑构造函数调用链中存在反调试逻辑,优先观察或钩取分发器,而不是直接盲目附加到原始 .init_array 条目。
可考虑的较高层级目标包括:
call_constructorscall_array- 链接器侧的构造函数遍历器
- 遍历构造函数表的应用侧包装函数
这样更安全的原因:
- 一个钩子就能揭示完整的构造函数执行顺序
- 可以看到哪个构造函数在进程终止或反调试设置前立即运行
- 可减少对无关初始化代码的盲目修补
- 可以先记录构造函数目标,再只修补有问题的目标
推荐的工作流:
- 钩取模块加载
- 确认进程是否在执行构造函数期间终止
- 如果是,在可用时钩取
call_constructors或call_array - 记录每个被分发的构造函数目标
- 找出执行反调试检查的确切构造函数
- 修补或钩取该构造函数,或者修补其中具体的反调试分支
仅在以下情况下建议使用 call_constructors 或 call_array:
- 目标平台或链接器构建版本提供这些函数
- 符号、跟踪记录或反汇编表明实际使用了这些函数
- 用户需要查看构造函数执行期间的反调试行为
请提醒用户注意以下限制:
- 这些函数属于加载器或链接器内部函数,其名称可能因 Android 版本、厂商构建版本或平台而异
- 它们可能未导出,可能需要恢复符号或通过偏移量附加
- 过早钩取它们可能影响每次库加载,因此应谨慎限定日志范围
- 应先用它们进行发现,而不是将其作为默认的永久绕过方法
好的建议:
- “进程在原生库初始化期间退出。首先,在存在
call_constructors或call_array时钩取相应的构造函数分发器,记录构造函数目标,然后转向确切的有问题的构造函数。”
不好的建议:
- “钩取每个
.init_array条目并逐个修补,直到不再崩溃。”
指针和缓冲区日志记录
console.log(args[0]);
console.log(args[0].toString());
console.log(hexdump(args[0], {
offset: 0,
length: 64,
header: true,
ansi: false
}));
评论
支持 Markdown