结合 Frida 和 IDA Trace 分析算法
Frida 和 IDA 的各自优势与局限
在 Android Native 代码逆向分析中,Frida 和 IDA Pro 是两个最核心的工具。它们各自有不同的优势和局限,理解这些差异是制定高效分析策略的基础。
Frida 的优势与局限
优势:
- 实时动态分析,不需要修改 APK 文件
- 脚本热加载,即改即用,调试效率高
- 可以在任何时刻注入和 Hook,不受编译限制
- Stalker 提供指令级代码追踪能力
- 跨平台支持(Android、iOS、Windows、Linux、macOS)
局限:
- 只能看到运行时状态,无法直接查看静态代码结构
- 对于未执行的代码路径(如异常分支),无法获取信息
- 热加载脚本在多线程环境中可能有竞态条件
- 无法自动识别数据类型和函数签名
IDA Pro 的优势与局限
优势:
- 静态分析能力强大,反编译、反汇编、交叉引用一应俱全
- 可以查看函数的完整代码结构,包括所有分支路径
- 支持多种处理器架构的指令集
- 丰富的插件生态(Hex-Rays 反编译器、脚本支持等)
- Trace 功能提供指令级执行记录
局限:
- 对混淆代码(如 OLLVM)的静态分析效果有限
- 无法看到运行时的实际数据值
- 调试器附加速度较慢,调试体验不如 Frida 灵活
- 条件断点等高级功能的性能开销大
最佳实践:两者互补
IDA 静态分析 ←→ Frida 动态验证
IDA 负责:
- 代码结构理解
- 函数识别和命名
- 交叉引用追踪
- 伪代码阅读
Frida 负责:
- 实际数据值获取
- 执行路径记录
- 算法行为验证
- 实时调试和修改
Frida 动态分析 + IDA 静态分析的组合策略
四阶段组合分析策略
Phase 1: IDA 静态概览
→ 打开 SO 文件,浏览导出表和字符串
→ 识别关键函数,建立初步理解
→ 标记可疑函数和代码区域
Phase 2: Frida 动态定位
→ Hook 关键函数,确认输入输出
→ 使用 Stalker 记录执行路径
→ 缩小分析范围到具体函数
Phase 3: IDA 深度分析
→ 对 Phase 2 定位的函数进行详细静态分析
→ 结合 Frida 获取的运行时数据验证分析
→ 还原算法逻辑
Phase 4: Frida 验证还原结果
→ 用还原的算法实现与原始函数对比验证
→ 确保分析的完整性和正确性
先用 Frida 定位关键函数和参数
Step 1: 确定入口函数
// 使用 Frida 快速定位算法的入口
function locateAlgorithmEntry(moduleName) {
var mod = Process.findModuleByName(moduleName);
if (!mod) {
console.log("[-] 模块未加载: " + moduleName);
return;
}
console.log("[*] 模块信息:");
console.log(" 名称: " + mod.name);
console.log(" 基地址: " + mod.base);
console.log(" 大小: 0x" + mod.size.toString(16));
// 枚举导出函数,寻找可能的算法入口
var exports = mod.enumerateExports();
var candidates = [];
exports.forEach(function (exp) {
var name = exp.name.toLowerCase();
// 通过函数名关键词匹配
if (name.indexOf("encrypt") !== -1 ||
name.indexOf("decrypt") !== -1 ||
name.indexOf("sign") !== -1 ||
name.indexOf("hash") !== -1 ||
name.indexOf("hmac") !== -1 ||
name.indexOf("cipher") !== -1 ||
name.indexOf("transform") !== -1) {
candidates.push(exp);
console.log("[候选] " + exp.name + " @ " + exp.address);
}
});
return candidates;
}
var candidates = locateAlgorithmEntry("libnative.so");
Step 2: Hook 并记录参数
// 对候选函数进行 Hook,记录参数和返回值
function probeAlgorithmFunctions(moduleName, candidates) {
var mod = Process.findModuleByName(moduleName);
var results = {};
candidates.forEach(function (exp) {
var addr = exp.address;
try {
Interceptor.attach(addr, {
onEnter: function (args) {
this._hit = true;
console.log("\n[调用] " + exp.name);
console.log(" x0 = " + this.context.x0);
console.log(" x1 = " + this.context.x1);
console.log(" x2 = " + this.context.x2);
console.log(" x3 = " + this.context.x3);
// 尝试读取指针指向的数据
try {
var len = this.context.x2.toInt32();
if (len > 0 && len < 1024) {
console.log(" [数据] 长度=" + len);
console.log(hexdump(this.context.x0, {
length: Math.min(len, 64)
}));
}
} catch (e) {}
},
onLeave: function (retval) {
if (this._hit) {
console.log(" [返回] " + retval);
try {
console.log(hexdump(retval, { length: 32 }));
} catch (e) {}
}
}
});
console.log("[+] Hook: " + exp.name);
} catch (e) {
console.log("[-] Hook 失败: " + exp.name);
}
});
}
Step 3: Stalker 追踪调用链
// 使用 Stalker 追踪函数内部的调用链
function traceCallChain(moduleName, funcOffset) {
var mod = Process.findModuleByName(moduleName);
var funcAddr = mod.base.add(funcOffset);
var tid = Process.getCurrentThreadId();
var callChain = [];
Interceptor.attach(funcAddr, {
onEnter: function (args) {
callChain = [];
Stalker.follow(tid, {
events: { call: true },
onReceive: function (events) {
var parsed = Stalker.parse(events);
parsed.forEach(function (ev) {
if (ev[0] === 'call') {
var target = ev[1];
if (target >= mod.base &&
target < mod.base.add(mod.size)) {
callChain.push(
target.sub(mod.base).toInt32());
}
}
});
}
});
},
onLeave: function (retval) {
Stalker.unfollow(tid);
Stalker.flush();
// 输出调用链(去重连续重复)
var unique = [];
callChain.forEach(function (c, i) {
if (i === 0 || c !== callChain[i - 1]) {
unique.push(c);
}
});
console.log("\n[调用链] (" + unique.length + " 个函数)");
unique.forEach(function (offset, i) {
console.log(" " + (i + 1) + ". 0x" +
offset.toString(16));
});
// 将调用链数据传递给 IDA 分析
console.log("\n[IDA 分析提示]");
console.log("请在 IDA 中分析以下偏移处的函数:");
unique.forEach(function (offset) {
console.log(" - 0x" + offset.toString(16));
});
}
});
}
再用 IDA 深入分析算法逻辑
将 Frida 数据同步到 IDA
Frida 追踪得到的偏移地址可以直接在 IDA 中定位和分析:
# IDA Python - 根据 Frida 追踪结果定位函数
import idc
import ida_funcs
import idautils
# Frida 追踪到的调用链偏移
frida_call_chain = [
0x1A2B, 0x2A10, 0x2A50, 0x3A20, 0x3A50,
0x3A80, 0x2A10, 0x2A50, 0x4A20, 0x4A50
]
base_addr = idc.get_imagebase()
print("=== Frida → IDA 函数定位 ===")
for offset in frida_call_chain:
addr = base_addr + offset
func = ida_funcs.get_func(addr)
if func:
func_name = idc.get_func_name(func.start_ea)
func_size = func.end_ea - func.start_ea
print(f"0x{offset:X} → 函数 {func_name} " +
f"(0x{func.start_ea:X}-0x{func.end_ea:X}, " +
f"{func_size} 字节)")
else:
print(f"0x{offset:X} → 无所属函数")
# 为关键函数添加注释
for i, offset in enumerate(frida_call_chain):
addr = base_addr + offset
idc.set_cmt(addr, f"[Frida 调用链 #{i+1}]", 0)
Frida 数据标注到 IDA 注释
# IDA Python - 将 Frida 收集的运行时数据写入 IDA 注释
def annotate_with_frida_data(annotations):
"""
annotations: dict, {offset: comment_string}
"""
base = idc.get_imagebase()
for offset, comment in annotations.items():
addr = base + offset
idc.set_cmt(addr, comment, 0)
print(f"[标注] 0x{offset:X}: {comment}")
# 示例:Frida 追踪发现的数据
frida_annotations = {
0x1A2B: "算法入口 - 输入: 16字节明文",
0x2A10: "自定义 S-Box 查表替换",
0x2A50: "行移位变换",
0x3A20: "列混合 (非标准 GF(2^8) 运算)",
0x3A50: "轮密钥加",
0x4A20: "最终输出处理",
}
annotate_with_frida_data(frida_annotations)
IDA Remote Debug 配合 Frida 联合调试
配置 IDA Remote Debug
IDA Pro 支持通过 Android gdbserver 进行远程调试,而 Frida 可以同时在同一进程中运行。两者配合使用可以在 IDA 中设置断点的同时使用 Frida 脚本进行动态分析。
配置步骤:
1. 在 Android 设备上运行:
adb push android_server /data/local/tmp/
adb shell chmod 755 /data/local/tmp/android_server
adb shell /data/local/tmp/android_server
2. 端口转发:
adb forward tcp:23946 tcp:23946
3. IDA 中连接:
Debugger → Attach → Remote ARM Linux/Android debugger
输入 hostname: localhost, port: 23946
4. 同时运行 Frida:
frida -U -f com.example.app -l script.js
联合调试工作流
// Frida 端 - 在 IDA 设置断点的位置配合 Hook
function cooperativeDebug(moduleName) {
var mod = Process.findModuleByName(moduleName);
// 在 IDA 中也设置了断点的位置
var idaBreakpoints = [0x1A2B, 0x2A10, 0x3A20];
idaBreakpoints.forEach(function (offset) {
var addr = mod.base.add(offset);
Interceptor.attach(addr, {
onEnter: function (args) {
console.log("[Frida] 到达 IDA 断点位置 0x" +
offset.toString(16));
// 在这里可以做 IDA 不方便做的动态操作
var ctx = this.context;
// 1. 修改寄存器值
// ctx.x0 = ptr(0x12345678);
// 2. 读取和打印内存
try {
console.log(" x0 指向的数据:");
console.log(hexdump(ctx.x0, { length: 32 }));
} catch (e) {}
// 3. 调用其他函数获取信息
// ...
}
});
});
}
同步 Frida Hook 数据到 IDA 注释
实时数据同步方案
// Frida 端 - 收集数据并生成 IDA 注释脚本
function generateIdaAnnotations(moduleName, funcOffset) {
var mod = Process.findModuleByName(moduleName);
var funcAddr = mod.base.add(funcOffset);
var annotations = [];
var tid = Process.getCurrentThreadId();
Interceptor.attach(funcAddr, {
onEnter: function (args) {
this._annotations = [];
// 使用 Stalker 追踪基本块
Stalker.follow(tid, {
events: { block: true },
transform: function (iterator) {
var inst;
while ((inst = iterator.next()) !== null) {
if (inst.address >= mod.base &&
inst.address < mod.base.add(mod.size)) {
var offset = inst.address.sub(mod.base).toInt32();
// 在关键指令处记录数据
if (inst.mnemonic === 'LDRB' ||
inst.mnemonic === 'EOR') {
iterator.putCallout(function (context) {
// 记录当前寄存器值
this._annotations.push({
offset: offset,
x0: context.x0.toInt32(),
x1: context.x1.toInt32(),
mnemonic: inst.mnemonic
});
}.bind(this));
}
}
iterator.keep();
}
}.bind(this)
});
},
onLeave: function (retval) {
Stalker.unfollow(tid);
Stalker.flush();
// 生成 IDA Python 脚本
console.log("\n# ===== IDA Python 标注脚本 =====");
console.log("import idc");
console.log("base = idc.get_imagebase()");
console.log("");
var unique = [];
this._annotations.forEach(function (a) {
var key = a.offset + "_" + a.mnemonic;
if (!unique[key]) {
unique[key] = a;
}
});
Object.values(unique).forEach(function (a) {
console.log("idc.set_cmt(base + 0x" +
a.offset.toString(16) +
', "' + a.mnemonic +
" x0=0x" + a.x0.toString(16) +
" x1=0x" + a.x1.toString(16) +
'", 0)');
});
console.log("\n# 共生成 " +
Object.keys(unique).length + " 条标注");
}
});
}
完整案例:从 APK 到还原出完整算法签名的全流程
案例:还原某 APP 的请求签名算法
背景:某 APP 在发送网络请求时,会对参数进行签名。签名算法在 libsecurity.so 中实现,且经过了 OLLVM 混淆。
Phase 1: IDA 静态概览
# 1. 打开 libsecurity.so,查看导出函数
# 2. 找到 JNI_OnLoad,分析动态注册的 Native 方法
# 3. 识别签名函数: Java_com_example_app_security_SignUtil_nativeSign
# 4. 观察代码特征:大量位运算、switch-case → 确认 OLLVM 混淆
Phase 2: Frida 动态定位
// Frida 脚本 - Phase 2
Java.perform(function () {
// Hook Java 层签名方法
var SignUtil = Java.use("com.example.app.security.SignUtil");
SignUtil.sign.implementation = function (params) {
console.log("\n[签名] 参数: " + params);
// 调用原始签名方法
var result = this.sign(params);
console.log("[签名] 结果: " + result);
return result;
};
// Hook JNI Native 方法
var nativeSignAddr = Module.findExportByName("libsecurity.so",
"Java_com_example_app_security_SignUtil_nativeSign");
if (nativeSignAddr) {
Interceptor.attach(nativeSignAddr, {
onEnter: function (args) {
// args[2] = JNIEnv*, args[3] = jobject, args[4] = jstring
console.log("[Native签名] 被调用");
// 使用 Stalker 追踪内部执行
var tid = Process.getCurrentThreadId();
Stalker.follow(tid, {
events: { block: true, call: true },
onReceive: function (events) {
var parsed = Stalker.parse(events);
// 收集执行数据...
}
});
},
onLeave: function (retval) {
Stalker.unfollow(tid);
Stalker.flush();
}
});
}
});
Phase 3: IDA 深度分析
# 根据 Frida 追踪到的调用链,在 IDA 中深入分析
# 1. 定位 S-Box 地址,导出查找表
# 2. 分析轮函数的结构
# 3. 识别自定义的运算方式
# 4. 还原密钥扩展算法
Phase 4: 验证还原结果
// Frida 脚本 - Phase 4 验证
function verifyRestoredSign(moduleName, funcOffset) {
var mod = Process.findModuleByName(moduleName);
var funcAddr = mod.base.add(funcOffset);
// 测试多组数据
var testCases = [
{ params: '{"uid":"12345","time":"1700000000"}' },
{ params: '{"action":"login","token":"abc123"}' }
];
Interceptor.attach(funcAddr, {
onEnter: function (args) {
this._input = args[4].readUtf8String();
},
onLeave: function (retval) {
var originalSign = this._input; // 简化示例
// 对比 Python 还原算法的输出
console.log("[验证] 原始签名: " + originalSign);
// 如果 Python 实现的输出一致,还原成功
}
});
}
完整流程总结
APK 分析
↓
IDA 静态概览 → 识别签名函数,发现 OLLVM 混淆
↓
Frida 动态定位 → Hook JNI 入口,Stalker 追踪调用链
↓
发现调用: nativeSign → initKey → subBytes → shiftRows → mixColumns
↓
IDA 深度分析 → 逐一分析子函数,导出 S-Box,还原运算
↓
Python 还原实现 → 用还原的算法在 Python 中重写
↓
Frida 验证 → 对比原始函数和 Python 实现的输出
↓
还原完成 ✓
总结
本文介绍了 Frida 动态分析和 IDA 静态分析的组合策略。核心思想是"IDA 看结构,Frida 看数据"——先用 IDA 建立对代码的静态理解,再用 Frida 获取运行时的实际数据,两者相互印证。通过 Frida 的 Stalker 追踪获得执行路径和调用链,将数据同步到 IDA 的注释中辅助静态分析;通过 IDA Remote Debug 与 Frida 联合调试,在 IDA 断点处配合 Frida 脚本进行动态操作。完整的案例展示了从 APK 分析到算法还原的全流程,是 Android 高级逆向分析的系统性方法论。