发布于 

结合 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 高级逆向分析的系统性方法论。