IDA Pro 入门

目录

IDA Pro 入门

IDA Pro (Interactive Disassembler Professional) 是由 Hex-Rays 公司开发的一款业界闻名的交互式反汇编器。在逆向工程领域,IDA Pro 被广泛认为是黄金标准,以其最强大的反汇编引擎、无与伦比的处理器支持和极其成熟的生态系统,成为专业人士进行软件分析、漏洞挖掘和恶意软件研究的首选工具。

核心特性

顶级的反汇编引擎

IDA Pro 的核心竞争力在于其无与伦比的静态反汇编能力。它能够智能地、递归地遍历代码,区分代码与数据,识别函数边界,其分析结果的准确性是业界公认的最高水准。

FLIRT 技术

Fast Library Identification and Recognition Technology。这是 IDA 的标志性技术,通过对标准编译器库函数的签名进行模式匹配,能够自动识别并命名大量的库函数,极大地减少了逆向工程师的重复工作。

强大的交互性

IDA 的设计哲学鼓励用户与反汇编结果进行交互。用户可以随时重命名变量、修改类型、添加注释、转换数据格式,这些交互操作会实时地影响整个分析数据库。

Hex-Rays 反编译器

IDA Pro 的杀手级应用是其配套的 Hex-Rays 反编译器。虽然需要额外付费,但它被公认为目前市面上最强大的 C/C++ 反编译器,生成的伪代码质量极高,可读性极强。

多平台调试器

内置了强大的跨平台调试器,支持本地和远程调试,允许动态分析和修改程序行为。

极其丰富的插件生态

经过数十年的发展,IDA Pro 积累了海量的第三方插件,覆盖了从漏洞扫描、代码着色、数据解密到与其他工具联动的方方面面,极大地扩展了其功能边界。


IDA Pro vs. Ghidra vs. Radare2

特性IDA ProGhidraRadare2
价格非常昂贵完全免费完全免费
开源是 (Java)是 (C)
核心优势最强的交互式反汇编高质量的免费反编译器极致的脚本化和命令行
UIQt,业界标准,成熟稳定Java Swing,功能强大但略显笨重命令行,或通过 Cutter 提供 GUI
反编译器Hex-Rays (业界顶尖,需付费)内置免费,质量非常高内置免费 (ghidra-dec),或支持其他
生态系统极其成熟,插件海量快速发展中高度可定制,但插件较少
处理器支持最广泛广泛,但略少于 IDA极广,覆盖很多小众架构

版本与安装

版本类型

版本说明
IDA Pro完整版本,包含所有处理器模块和调试器
IDA Home针对个人爱好者的廉价版,功能受限
IDA Free免费版本,功能严重受限,仅支持 x86/x64,且不能保存数据库

购买与安装

  • 需要通过官方或授权经销商购买
  • 安装过程是标准的下一步式安装
  • 免费版仅适合非常初级的学习

基本工作流程

1. 启动 IDA

打开 IDA Pro。

2. 加载文件

在启动界面点击 New,或将二进制文件直接拖入主窗口。

3. 加载选项

IDA 会弹出一个加载对话框,让你确认文件类型、处理器类型等。通常,IDA 的自动分析非常准确,直接点击 OK 即可。

4. 自动分析

IDA 会进行长时间的自动分析。分析过程可以在底部的输出窗口看到。耐心等待分析完成是使用 IDA 的好习惯,否则很多功能无法正常使用。

5. 开始分析

分析完成后,即可开始交互式分析。


关键视图与快捷键

IDA View (反汇编视图)

这是 IDA 的主视图。按空格键可以在图形视图(流程图)和文本视图(线性反汇编)之间切换。

图形视图非常适合理解函数内的逻辑分支和循环。

Hex View (十六进制视图)

以经典的十六进制编辑器形式展示文件内容,与反汇编视图同步高亮。

Structures (结构体视图)

  • 快捷键: Shift+F9
  • 用于定义和管理 C 语言风格的结构体
  • 你可以手动创建,也可以从 C 头文件导入
  • 正确地定义数据结构是逆向工程的关键一步

Enums (枚举视图)

  • 快捷键: Shift+F10
  • 用于定义和管理枚举类型
  • 可以极大地提高代码的可读性,例如将 mov eax, 2 变为 mov eax, MODE_READ

核心快捷键

快捷键功能
G跳转到指定地址
N重命名变量、函数、标签
Y修改变量类型
X查看交叉引用 (cross-references),即哪些地方调用/引用了当前符号
P创建一个函数
U取消定义(如将代码变为未定义数据)
;添加行注释
:添加可重复注释
F5(如果已购买) 启动 Hex-Rays 反编译器
D在 byte / word / dword / qword 之间切换数据类型
A将数据转换为 ASCII 字符串
C将数据转换为代码
H十进制/十六进制切换
Alt+T文本搜索
Alt+B二进制字节搜索

Android SO 分析实战

💡 思路一句话: 加载 SO → 等自动分析完成 → Exports 找 JNI 函数入口 → F5 反编译 → Strings 窗口搜索关键字符串 → 交叉引用追踪调用链。IDA 的核心价值是「从字符串和导出函数出发,逆向追踪代码逻辑」。

这是 IDA Pro 在 Android 逆向中最核心的使用场景。我们将以一个典型的 .so 文件为例,完整演示从加载到分析的全过程。

第一步:从 APK 中提取 SO 文件

APK 本质上是一个 ZIP 文件。原生 SO 库位于 lib/ 目录下,按 ABI 分类:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
app.apk
  ├── lib/
  │   ├── armeabi-v7a/
  │   │   └── libnative.so      <-- 32-bit ARM
  │   ├── arm64-v8a/
  │   │   └── libnative.so      <-- 64-bit ARM (推荐分析此版本)
  │   └── x86_64/
  │       └── libnative.so      <-- x86_64 (模拟器用)
  ├── classes.dex
  └── AndroidManifest.xml

推荐优先分析 arm64-v8a 版本,原因如下:

  • ARM64 寄存器更多(X0-X30),反编译结果更清晰
  • 现代 Android 设备几乎全部是 64 位
  • Hex-Rays 对 ARM64 的反编译支持最好

提取方法:直接 unzip app.apk -d app_extracted 解压,或使用 apktool d app.apk 保留更多元信息。

第二步:加载 SO 到 IDA

启动 IDA Pro,选择 New,打开提取出的 .so 文件。IDA 会自动识别为 ELF 格式并选择正确的处理器类型(ARM / AArch64)。通常保持默认设置点击 OK 即可。等待底部状态栏显示 AU: idle 表示自动分析完成。

第三步:定位关键函数

加载完成后,我们需要找到有意义的分析入口。以下是几种常用策略:

策略一:从导出函数入手

打开 Exports 窗口(View -> Open subviews -> Exports),查找 JNI 函数:

1
2
3
4
5
6
JNI 函数命名规则:Java_包名_类名_方法名

示例:
Java_com_example_app_NativeLib_encrypt
Java_com_example_app_NativeLib_verify
Java_com_example_app_NativeLib_getSign

这些函数是 Java 层调用 Native 层的直接入口,通常是分析的最佳起点。

策略二:从字符串搜索入手

Shift+F12 打开 Strings 窗口,搜索关键词:

1
2
3
4
5
6
7
常见有价值的字符串:
- "AES", "RSA", "MD5", "SHA"        -> 加密算法线索
- "key", "secret", "token"          -> 密钥相关
- "verify", "sign", "check"         -> 签名验证
- "http", "https", "api"            -> 网络请求
- "/proc/self/maps"                 -> 反调试检测
- "su", "magisk", "frida"           -> Root/Hook 检测

找到感兴趣的字符串后,双击跳转到字符串定义位置,然后按 X 查看交叉引用,即可找到使用该字符串的函数。

策略三:从 init_array 入手

.init_array 段中的函数会在 SO 被 System.loadLibrary() 加载时自动执行,常用于反调试检测初始化、字符串解密和动态注册 JNI 函数。在 IDA 中通过 View -> Open subviews -> Segments 找到 .init_array 段即可查看。

策略四:从 JNI_OnLoad 入手

JNI_OnLoad 是 SO 加载时被 JVM 调用的特殊导出函数。许多应用使用它来进行动态 JNI 注册RegisterNatives)。典型流程为:JNI_OnLoad -> GetEnv / FindClass -> RegisterNatives

重点关注 RegisterNatives 的第三个参数,它是一个 JNINativeMethod 结构体数组:

1
2
3
4
5
typedef struct {
    const char* name;       // Java 方法名
    const char* signature;  // 方法签名
    void*       fnPtr;      // Native 函数指针 <-- 真正的实现函数
} JNINativeMethod;

第四步:分析函数逻辑

找到目标函数后,按 F5 进入反编译视图,开始逐步分析逻辑。详细的反编译器使用方法见下一节。


Hex-Rays 反编译器实战

💡 思路一句话: 反编译器输出不完美时,用 Y 键修改变量类型、N 键重命名变量、T 键指定结构体 — 这三个快捷键能让反编译结果从「不可读」变成「基本可读」。

Hex-Rays 反编译器(俗称 F5)是 IDA Pro 的核心卖点。它可以将汇编代码转换为类似 C 语言的伪代码,极大地提升分析效率。

基本使用

  1. 在反汇编视图中,将光标定位到目标函数内部
  2. F5 键,即可在新窗口中看到反编译后的伪代码
  3. 反编译视图与反汇编视图是双向同步的:在伪代码中点击某一行,反汇编视图会同步跳转

提升伪代码可读性的关键操作

重命名变量和函数

这是最重要的操作。反编译器生成的默认变量名(如 v1, v2, a1)毫无意义,需要根据分析结果进行重命名:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 重命名前(几乎不可读)
int __fastcall sub_12A0(int a1, int a2, int a3)
{
    int v4 = *(_DWORD *)(a1 + 16);
    void *v5 = malloc(a3 + 1);
    memcpy(v5, *(void **)(a1 + 24), a3);
    return sub_1400(v5, a3, v4);
}

// 重命名后(逻辑清晰)
int __fastcall decrypt_data(JNIEnv *env, jbyteArray input, int length)
{
    int key_len = *(_DWORD *)(env + 16);
    void *buffer = malloc(length + 1);
    memcpy(buffer, *(void **)(env + 24), length);
    return aes_decrypt(buffer, length, key_len);
}

操作方法:将光标放在变量/函数名上,按 N 键重命名。

修改变量类型

IDA 经常无法正确推断变量类型,尤其是 JNI 函数的参数。手动修正类型可以显著提升可读性:

1
2
3
4
5
// 修正前:IDA 将 JNIEnv* 识别为 int
int __fastcall Java_com_example_encrypt(int a1, int a2, int a3)

// 修正后:设置正确的 JNI 类型
jbyteArray __fastcall Java_com_example_encrypt(JNIEnv *env, jobject thiz, jstring input)

操作方法:将光标放在变量上,按 Y 键修改类型。

提示:IDA 内置了 JNI 类型库。加载 SO 后,可以通过 View -> Open subviews -> Type Libraries 加载 jni_all 类型库,之后即可直接使用 JNIEnv*, jstring, jbyteArray 等类型。

定义结构体

当代码中存在大量的指针偏移访问时(如 *(a1 + 8)),应定义结构体:按 Shift+F9 打开结构体窗口,Insert 创建,D 添加字段,N 命名字段。然后将伪代码中的指针变量类型改为结构体指针,偏移量会自动变为字段名:

1
2
3
4
// 定义前                           // 定义后
v3 = *(_DWORD *)(a1 + 0);          v3 = ctx->buffer;
v4 = *(_DWORD *)(a1 + 4);          v4 = ctx->buffer_size;
v5 = *(_DWORD *)(a1 + 8);          v5 = ctx->key;

常用反编译器快捷键

快捷键功能
F5反编译当前函数
N重命名变量/函数
Y修改变量/函数类型
T选择结构体成员
/添加注释
\隐藏/显示类型转换 (casts)
Tab在伪代码和反汇编视图之间切换
Ctrl+Shift+W复制伪代码到剪贴板
右键 -> Set number format改变数值的显示格式

常见的伪代码模式识别

在分析 Android SO 时,需要熟悉 JNI 调用模式。例如 GetStringUTFChars / ReleaseStringUTFChars 配对出现时说明在操作 Java 字符串,GetByteArrayElements / ReleaseByteArrayElements 配对说明在操作字节数组。

常见加密算法的伪代码特征:

算法伪代码特征
AES16 字节块操作,S-Box 查表,MixColumns 矩阵运算
MD54 个 32 位状态变量,64 轮循环,特征常量 0x67452301
SHA-2568 个 32 位状态变量,64 轮循环,特征常量 0x6a09e667
RC4256 字节 S-Box 初始化 + KSA + PRGA 两阶段
Base64包含 “ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef…” 字符表
HMAC内层和外层填充 (ipad=0x36, opad=0x5C)

IDAPython 实战脚本

💡 思路一句话: IDAPython 的价值在于「批量自动化」— 手动分析找到规律后,写脚本对所有同类模式一次性处理(批量重命名、批量 patch、批量提取数据)。

IDAPython 是 IDA 最强大的自动化接口。以下是在 Android SO 分析中非常实用的脚本集合。

基础:打印所有函数名和地址

1
2
3
4
5
6
import idautils
import idc

for func_ea in idautils.Functions():
    func_name = idc.get_func_name(func_ea)
    print(f"{func_name} at {hex(func_ea)}")

批量重命名 sub_ 函数

当你通过其他工具(如 Frida trace 日志)获得了函数名映射时,可以批量导入:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import idc

# 格式:{ 地址: "新函数名" }
rename_map = {
    0x12A0: "aes_encrypt",
    0x1400: "aes_decrypt",
    0x1580: "md5_init",
    0x16C0: "md5_update",
    0x1800: "md5_final",
    0x1A00: "base64_encode",
    0x1B40: "hmac_sha256",
}

for addr, name in rename_map.items():
    if idc.set_name(addr, name, idc.SN_FORCE):
        print(f"[+] Renamed {hex(addr)} -> {name}")
    else:
        print(f"[-] Failed to rename {hex(addr)}")

查找加密算法常量

利用已知的加密算法初始化常量来自动识别算法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import idautils, idc, ida_bytes

CRYPTO_CONSTANTS = {
    "MD5":      [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476],
    "SHA-256":  [0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a],
    "AES-Te0":  [0xc66363a5, 0xf87c7c84, 0xee777799, 0xf67b7b8d],
    "CRC32":    [0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA],
    "Blowfish": [0x243F6A88, 0x85A308D3, 0x13198A2E, 0x03707344],
}

for algo_name, constants in CRYPTO_CONSTANTS.items():
    for const in constants:
        ea = 0
        while True:
            ea = ida_bytes.find_dword(ea, idc.BADADDR, const)
            if ea == idc.BADADDR:
                break
            func_ea = idc.get_func_attr(ea, idc.FUNCATTR_START)
            func_name = idc.get_func_name(func_ea) if func_ea != idc.BADADDR else "N/A"
            print(f"[{algo_name}] {hex(const)} at {hex(ea)} (func: {func_name})")
            ea += 4

解析 JNI 动态注册 (RegisterNatives)

这是 Android SO 分析中最实用的脚本之一。当应用使用 RegisterNatives 动态注册 JNI 函数时,导出表中看不到 Java_xxx 命名的函数。此脚本帮你从 JNI_OnLoad 中提取注册信息:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import idc, ida_bytes

def parse_jni_native_methods(methods_ptr, count):
    """解析 JNINativeMethod 数组 (64-bit: 每项 24 字节)"""
    for i in range(count):
        base = methods_ptr + i * 24
        name_ptr = ida_bytes.get_qword(base)
        sig_ptr  = ida_bytes.get_qword(base + 8)
        fn_ptr   = ida_bytes.get_qword(base + 16)

        name = idc.get_strlit_contents(name_ptr, -1, idc.STRTYPE_C)
        sig  = idc.get_strlit_contents(sig_ptr, -1, idc.STRTYPE_C)
        if name and sig:
            name = name.decode() if isinstance(name, bytes) else name
            sig  = sig.decode() if isinstance(sig, bytes) else sig
            idc.set_name(fn_ptr, f"jni_{name}", idc.SN_FORCE)
            print(f"  {name} {sig} -> {hex(fn_ptr)}")

# 用法: 在 RegisterNatives 调用处确定 methods 地址和 count
# parse_jni_native_methods(0xABCD1234, 5)

批量导出字符串

将 SO 中所有可识别的字符串导出为文本文件,便于离线分析和搜索:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import idc, idautils

def dump_all_strings(min_length=4):
    strings = idautils.Strings()
    with open("/tmp/ida_strings.txt", "w") as f:
        for s in strings:
            if s.length >= min_length:
                content = idc.get_strlit_contents(s.ea, -1, s.strtype)
                if content:
                    text = content.decode('utf-8', errors='replace')
                    f.write(f"{hex(s.ea)}: {text}\n")
    print("[+] 字符串已导出到 /tmp/ida_strings.txt")

dump_all_strings()

XOR 字符串解密辅助

很多加固 SO 会对字符串进行 XOR 加密。找到解密函数后,可以追踪其所有调用点并批量解密:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import idc, idautils

def xor_decrypt(data, key):
    if isinstance(key, int):
        return bytes([b ^ key for b in data])
    return bytes([b ^ key[i % len(key)] for i, b in enumerate(data)])

def find_decrypt_calls(decrypt_func_addr):
    for xref in idautils.XrefsTo(decrypt_func_addr):
        print(f"[*] 解密函数被调用于: {hex(xref.frm)}")

远程调试 Android 应用

IDA Pro 内置了强大的远程调试功能,可以通过 android_server 组件对 Android 设备上运行的 Native 代码进行实时调试。

第一步:推送 android_server 到设备

IDA 安装目录下的 dbgsrv/ 文件夹包含各架构的调试服务端(android_server 为 ARM32,android_server64 为 ARM64):

1
2
3
4
5
6
7
8
9
# 推送到设备(以 arm64 为例)
adb push /path/to/ida/dbgsrv/android_server64 /data/local/tmp/
adb shell chmod 755 /data/local/tmp/android_server64

# 启动 android_server(需要 root 权限)
adb shell su -c "/data/local/tmp/android_server64 -p 23946"

# 设置端口转发
adb forward tcp:23946 tcp:23946

第二步:在 IDA 中配置远程调试

  1. 在 IDA 中打开目标 SO 文件
  2. Debugger -> Select debugger... -> 选择 Remote ARM Linux/Android debugger
  3. 配置 Hostname 为 127.0.0.1,Port 为 23946

第三步:附加到目标进程

方式一:附加到已运行的进程 - Debugger -> Attach to process...,在进程列表中选择目标应用。

方式二:从启动阶段调试 (推荐) - 如果需要调试 JNI_OnLoadinit_array 中的代码:

1
2
# 以调试模式启动应用,应用会暂停等待调试器连接
adb shell am start -D -n com.example.app/.MainActivity

此时在 IDA 中附加进程,在目标函数处下断点即可。

第四步:设置断点与调试

常用调试快捷键:F2 设置断点、F9 继续运行、F7 单步步入、F8 单步步过、F4 运行到光标。通过 Debugger 菜单可查看寄存器、栈和内存。

常见问题

  • 无法连接:确认 adb forward 已执行,检查端口占用
  • 进程列表为空android_server 需要 root 权限运行
  • 断点不生效:ASLR 导致 SO 加载地址不同,需在 Module list 中确认实际基址
  • 需要在 SO 加载时断点Debugger -> Debugger options -> Events 勾选 Library load
  • 应用检测到调试器:需先绕过反调试(参考 Frida 反调试章节)

处理 ASLR(地址空间随机化)

Android 默认启用 ASLR,SO 文件每次加载的基地址不同。调试时通过 Debugger -> Module list 查看 SO 的实际基地址,或使用 IDAPython:

1
2
3
4
import idautils
for mod in idautils.Modules():
    if "libnative.so" in mod.name:
        print(f"Base: {hex(mod.base)}, Size: {hex(mod.size)}")

常用插件推荐

IDA Pro 拥有极其丰富的插件生态。以下是在 Android 逆向中最常用的插件:

FindCrypt

属性说明
功能自动识别二进制中的加密算法常量
原理通过匹配已知加密算法的特征常量(S-Box、初始化向量等)
用途快速定位 AES、DES、MD5、SHA、RC4、Blowfish 等算法的实现
地址https://github.com/polymorf/findcrypt-yara

使用方法:安装后,通过 Edit -> Plugins -> FindCrypt 运行,结果会在 Output 窗口显示,并自动在发现位置添加书签。

Keypatch

属性说明
功能在 IDA 中直接进行汇编级别的 Patch
原理集成 Keystone 汇编引擎,支持多种架构的指令汇编
用途修改指令(如 NOP 掉检测代码)、修复被混淆的跳转
地址https://github.com/keystone-engine/keypatch

常用场景:NOP 掉反调试检测的 BL anti_debug_check,或反转条件跳转(CBNZ 改为 CBZ)跳过签名验证。

HexRaysPyTools

属性说明
功能增强 Hex-Rays 反编译器的类型恢复能力
原理通过分析数据流自动推断结构体字段和虚函数表
用途快速重建 C++ 类的虚函数表、自动创建结构体
地址https://github.com/igogo-x86/HexRaysPyTools

核心功能:

  • Scan Variable: 分析指针的所有使用方式,自动生成对应的结构体
  • Reconstruct vtable: 从虚函数表指针自动重建 C++ 虚函数表

LazyIDA

属性说明
功能提供一系列便捷操作,提升日常分析效率
用途批量复制数据、快速搜索、格式化输出
地址https://github.com/L4ys/LazyIDA

主要功能:

  • 将选中的数据复制为 Python 列表 / C 数组 / 十六进制字符串
  • 快速扫描格式化字符串漏洞
  • 一键将立即数转换为不同进制

Diaphora

属性说明
功能二进制文件差异对比(BinDiff 的开源替代)
用途对比同一 SO 的不同版本,找出新增/修改的函数
地址https://github.com/joxeankoret/diaphora

IDA FLIRT Signature Database

属性说明
功能社区维护的 FLIRT 签名数据库
用途识别各种编译器和库的标准函数,减少需要手动分析的函数数量
地址https://github.com/push0ebp/sig-database

插件安装方法

大多数 Python 插件只需将 .py 文件复制到 IDA 安装目录下的 plugins/ 文件夹,重启 IDA 即可。FLIRT 签名文件 (.sig) 则放入 sig/arm/sig/arm64/ 目录。


IDA 与 Frida 联动

在实际的 Android 逆向分析中,最高效的工作流是将 IDA 的静态分析能力与 Frida 的动态插桩能力结合起来。两者互补,形成完整的分析闭环。

联动工作流

典型的 IDA + Frida 分析闭环:

  1. IDA 静态定位 -> 在 IDA 中找到可疑函数(如 sub_1A2C),分析参数和调用关系
  2. Frida 动态验证 -> 编写 Frida Hook 脚本,观察运行时的参数值、返回值和调用栈
  3. 回填 IDA -> 根据动态结果回到 IDA 重命名函数、修正类型
  4. 深入分析 -> 在 Frida 中修改参数测试、dump 内存数据,进一步理解算法逻辑
  5. 形成完整理解 -> 反复迭代,直到完全理解目标逻辑

场景一:用 IDA 定位函数,用 Frida 验证

在 IDA 中通过静态分析发现了一个疑似加密函数 sub_1A2C,需要验证其功能:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Frida 脚本:Hook IDA 中发现的函数
var baseAddr = Module.findBaseAddress("libnative.so");
var targetFunc = baseAddr.add(0x1A2C);  // IDA 中看到的函数偏移

Interceptor.attach(targetFunc, {
    onEnter: function(args) {
        console.log("[*] sub_1A2C called!");
        console.log("    arg0 (buffer): " + args[0]);
        console.log("    arg1 (length): " + args[1].toInt32());
        if (args[1].toInt32() > 0 && args[1].toInt32() < 1024) {
            console.log("    input: " + hexdump(args[0], { length: args[1].toInt32() }));
        }
        this.buf = args[0];
        this.len = args[1].toInt32();
    },
    onLeave: function(retval) {
        console.log("    return: " + retval);
        if (this.len > 0 && this.len < 1024) {
            console.log("    output: " + hexdump(this.buf, { length: this.len }));
        }
    }
});

观察 Frida 的输出后,确认该函数是 AES-CBC 加密,回到 IDA 中将 sub_1A2C 重命名为 aes_cbc_encrypt,并修正参数类型。

场景二:从 IDA 导出信息供 Frida 使用

编写 IDAPython 脚本,将分析成果导出为 Frida 可用的 Hook 模板:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# IDAPython: 为所有已命名的非库函数生成 Frida Hook 模板
import idautils, idc, ida_funcs

output = ['var base = Module.findBaseAddress("libnative.so");', ""]

for func_ea in idautils.Functions():
    name = idc.get_func_name(func_ea)
    if name.startswith("sub_") or name.startswith("."):
        continue
    func = ida_funcs.get_func(func_ea)
    if not func:
        continue
    offset = hex(func_ea)
    output.append(f'// {name}')
    output.append(f'Interceptor.attach(base.add({offset}), {{')
    output.append(f'    onEnter: function(args) {{ console.log("[{name}] called"); }},')
    output.append(f'    onLeave: function(retval) {{ console.log("[{name}] ret: " + retval); }}')
    output.append(f'}});')

with open("/tmp/frida_hooks.js", "w") as f:
    f.write("\n".join(output))
print("[+] Frida Hook 模板已导出到 /tmp/frida_hooks.js")

高级技巧

处理混淆代码

在实际的 Android 逆向中,大量的商业应用会使用 OLLVM(Obfuscator-LLVM)或自研混淆工具对 SO 进行保护。常见的混淆手段及 IDA 中的应对策略:

控制流平坦化 (Control Flow Flattening)

这是 OLLVM 最常用的混淆手段。原本顺序执行的基本块 A->B->C 被替换为一个大的 switch-case 分发器,所有基本块通过一个状态变量 (state variable) 控制执行顺序,在 IDA 图形视图中表现为一个中心节点连接大量分支。

IDA 应对方法:

  • 使用 D-810 插件(基于 Hex-Rays microcode API 的去混淆框架)
  • 使用 HexRaysDeob 插件自动去除平坦化
  • 手动分析:识别分发变量(state variable),追踪每个 case 块的真实逻辑

虚假控制流 (Bogus Control Flow)

插入恒真/恒假的虚假分支增加复杂度。在伪代码中识别不可能成立的条件(如 (x*x) % 2 == 1),用 Keypatch NOP 掉虚假分支即可。

指令替换 (Instruction Substitution)

将简单指令替换为等价复杂序列(如 ADD 变为 SUB + NEG)。Hex-Rays 反编译器通常能自动优化,复杂情况可使用 D-810 的 microcode 规则处理。

FLIRT 签名应用于 Android NDK

对于使用 NDK 编译的 SO 文件,IDA 默认可能无法识别 C/C++ 标准库函数。手动应用 FLIRT 签名可以显著减少需要分析的函数数量。

制作与应用 NDK 签名

1
2
3
4
# 使用 IDA FLAIR 工具集:pelf 生成 PAT,sigmake 生成 SIG
./pelf $ANDROID_NDK/.../sysroot/usr/lib/aarch64-linux-android/libc.a libc_ndk.pat
./sigmake -n"Android NDK r25 libc (arm64)" libc_ndk.pat libc_ndk.sig
cp libc_ndk.sig /path/to/ida/sig/arm64/

在 IDA 中通过 File -> Load file -> FLIRT signature file... 加载签名文件,IDA 会自动匹配并重命名已识别的库函数。

应用签名后,sub_A000 等大量未知函数会被自动识别为 memcpymemsetstrlenmalloc 等标准库函数,逆向工程师可以专注于真正的业务逻辑代码。

类型库 (Type Libraries) 的使用

类型库包含预定义的结构体、枚举和函数原型,能让 IDA 正确解析复杂的数据结构。

通过 File -> Load file -> Load type library... 加载类型库。常用的有 jni_all(JNI 接口类型)和 gnulnx_arm64(Linux ARM64 类型)。

如果 IDA 没有内置所需的类型库,可以通过 File -> Load file -> Parse C header file... 从自定义头文件导入:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// my_types.h
struct aes_context {
    unsigned int round_key[60];
    int nr;                  // 轮数
    unsigned char iv[16];    // 初始化向量
    int mode;                // 0=ECB, 1=CBC, 2=CTR
};

struct JNINativeMethod {
    const char* name;
    const char* signature;
    void* fnPtr;
};

处理 stripped 的 SO 文件

大多数发布版 SO 会被 strip。分析策略:先应用 FLIRT 签名识别库函数,再从字符串和导出函数入手,利用交叉引用追踪调用链,结合 Frida 动态分析回填信息。如果能获取旧版本或调试版本,使用 Diaphora/BinDiff 进行对比分析。

IDA 数据库管理

IDA 分析结果保存在数据库文件中:32 位分析生成 .idb,64 位分析生成 .i64。建议定期使用 File -> Save database 保存进度,对于重要分析使用 File -> Save database as 创建快照,以便回退到之前的状态。


优缺点分析

优点

优点说明
最强的反汇编质量业界公认的、最可靠的静态分析结果
FLIRT 和类型系统极大地自动化了库函数和数据结构的识别过程
成熟和稳定经过数十年打磨,软件本身极为稳定,用户体验流畅
强大的生态海量的插件、教程和社区支持,遇到任何问题几乎都能找到解决方案
顶级的反编译器Hex-Rays 反编译器是其最强大的护城河

缺点

缺点说明
价格昂贵对于个人开发者或小型团队来说,价格是最大的门槛
闭源核心功能是黑盒,无法审查或修改
协作不便原生不支持多人协作,需要依赖第三方插件

相关内容

如果这篇文章对你有帮助,请我喝杯咖啡吧~
+5 支付宝支付宝
+5 微信微信

目录