应用安全 --- 逆向技巧 之 逆向工程化(无加固版)
免责声明
本文档所有内容仅供安全研究、学术交流与技术学习使用,严禁用于任何未经授权的逆向破解、网络攻击、隐私窃取、恶意软件开发及其他违反《中华人民共和国网络安全法》《数据安全法》等法律法规的行为,使用者应确保已获得目标软件权利人的合法授权并自行承担因使用本文档内容所产生的一切法律责任与后果,作者不对任何直接或间接损害承担任何责任,继续阅读即视为您已知悉并同意上述全部条款。
捐助扶贫

我将逆向的大流程分为三个阶段
第一阶段:工具阶段,掌握主流工具和技术,可以进行动静态分析和简单的算法还原
第二阶段:脱壳阶段,掌握主流加固厂家的加密技术
第三阶段:还原阶段,完美还原源代码,甚至可以回编译回去
具体来讲是
1.元数据分析(了解你要分析的目标文件)
2.全函数导出,函数控制流导出,数据流导出
3.函数去噪( AI还原源码 -> AI识别和ida签名识别函数真实名称 -> 重命名函数文件 -> 去除非业务函数文件)
4.项目还原(依据函数控制流导出,数据流导出,函数重命名清单 三个资料进行全局分析代码原理并还原项目源码)
5.回编译验证
一张图

从这张图我们可以看出,我们的重点是静态分析,动态调试只是辅助,当我们对每个函数都有把握时,动态调试才不会跑飞。
我们为什么要工程化逆向流程,因为对于复杂需求适合,对于简单的算法分析可能直接分析更加高效。
预分析
为什么要预分析,有点像渗透测试阶段的信息收集,在充分收集足够多的信息后才能更好的开展行动,而不是将一个原本不可执行的文件作为可执行文件解析
〇、开始之前:明确你的目标
拿到文件后,先花 30 秒回答三个问题:
| 问题 | 示例回答 |
|---|---|
| 我要分析到什么程度? | A. 判断功能概览 / B. 提取核心算法 / C. 完整还原协议 / D. 提取密钥/配置 |
| 我的目标文件是什么? | 一个 APK / 一个 .exe / 一个 .so / 一个 .dll |
| 我有哪些约束? | 只能静态 / 可以动态调试 / 有无 root 设备 / 有无源码对照 |
写在文档开头。后续每一步都围绕这个目标做取舍,避免在无关细节上耗时间。同时完成环境工具的搭建
一、文件识别——搞清楚"它是什么"
目的
确定文件格式 → 选择正确的工具链。用错工具,后面全部白费。
操作流程
步骤 1:命令行快速识别
─────────────────────────────
# Linux / macOS
file target_file
# 输出示例:
# ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV), dynamically linked
# PE32+ executable (GUI) x86-64, for MS Windows
# Mach-O 64-bit x86_64 executable
# Java archive data (JAR/APK)
步骤 2:GUI 工具交叉验证
─────────────────────────────
→ 将文件拖入 DIE(Detect It Easy)
→ 查看左上角识别结果:PE / ELF / Mach-O / DEX / ZIP(APK)
→ 记录:位数(32/64)、架构(x86/ARM/MIPS)、字节序(LSB/MSB)
步骤 3(Windows PE 专用):
─────────────────────────────
→ 用 ExeinfoPE 打开,查看更详细的 PE 子类型(EXE/DLL/SYS/OCX)
判断结果记录表
| 字段 | 填写 |
|---|---|
| 文件格式 | PE / ELF / Mach-O / DEX / APK / 其他:___ |
| 位数 | 32 / 64 |
| 架构 | x86 / x86_64 / ARM / ARM64(AArch64) / MIPS |
| 字节序 | 小端(LSB) / 大端(MSB) |
| 子类型 | 可执行文件 / 动态库 / 静态库 / 驱动 / 其他:___ |
| 目标平台 | Windows / Linux / macOS / Android / iOS |
如果是 APK
# 解压 APK 查看内部结构
unzip -l target.apk
# 关注以下文件:
# classes.dex → Java/Kotlin 字节码
# lib/arm64-v8a/*.so → Native 库
# assets/ → 资源/配置/JS 代码(框架相关)
# AndroidManifest.xml → 权限和组件声明
二、壳检测与熵值分析——判断"它有没有被包裹"
目的
加壳 = 静态分析几乎失效。必须先判断,有壳就先脱壳再做后续步骤。
操作流程
═══════════════════════════════════════
指标 1:熵值检测(最可靠的客观指标)
═══════════════════════════════════════
方法 A:DIE
→ 打开文件 → 点击「Entropy」按钮
→ 查看每个节区的熵值柱状图
→ 判断标准:
熵值 < 6.0 → 正常代码/数据
熵值 6.0~7.0 → 可能有压缩
熵值 > 7.0 → 高度疑似加壳/加密
熵值 ≈ 8.0 → 几乎确定加密
方法 B:binwalk(Linux/macOS)
binwalk -E target_file
→ 输出熵值曲线图,观察是否有大面积高熵区间
方法 C:rabin2(radare2 套件)
rabin2 -S target_file
→ 列出所有节区及大小,配合熵值工具交叉验证
═══════════════════════════════════════
指标 2:导入表大小(快速直觉判断)
═══════════════════════════════════════
# 查看导入函数数量
rabin2 -i target_file | wc -l
# 或在 IDA/Ghidra 中查看 Imports 窗口
→ 判断标准:
一个功能复杂的程序(文件 > 500KB),导入函数 < 10 个
且集中在 LoadLibraryA / GetProcAddress / VirtualAlloc / VirtualProtect
→ 极大概率有壳——壳会在运行时动态解析所有需要的 API,所以静态看到的导入表几乎是空的。
Android SO 文件只有少量导入且包含:
mmap / mprotect / __loader_dlopen
→ 疑似自定义加载器或壳
═══════════════════════════════════════
指标 3:节区名称异常
═══════════════════════════════════════
# 查看节区名
rabin2 -S target_file
# 或 readelf -S target_file(ELF)
# 或在 DIE/CFF Explorer 中查看(PE)
→ 异常节区名速查表:
UPX0, UPX1, UPX2 → UPX 壳
.petite → Petite 壳
.themida, .winlice → Themida/WinLicense
.vmp0, .vmp1 → VMProtect
.enigma1, .enigma2 → Enigma Protector
.ndata → NSIS 安装包
.shielden → ShieldenProtector
名称为乱码或非 ASCII → 自定义壳 / 混淆
═══════════════════════════════════════
Android 专用:APK/SO 壳检测
═══════════════════════════════════════
方法 A:MT 管理器(手机端)
→ 打开 APK → 点击「查看」→ 自动识别加固厂商
→ 常见结果:腾讯乐固 / 360加固 / 梆梆 / 爱加密 / 娜迦
方法 B:NP 管理器(手机端)
→ 功能类似 MT,打开即可识别
方法 C:命令行特征判断
ls -la lib/arm64-v8a/
# 如果看到以下文件,基本确认有壳:
# libjiagu.so / libjiagu_a64.so → 360加固
# libexec.so / libexecmain.so → 腾讯乐固
# libDexHelper.so → 梆梆加固
# libdexjni.so → 爱加密
# libsecexe.so → 娜迦
# 检查 Application 入口类
aapt dump xmltree target.apk AndroidManifest.xml | grep "application"
# 如果 Application 类名包含 Stub / Wrapper / Protect 等关键词 → 有壳
壳检测结果记录
| 字段 | 填写 |
|---|---|
| 是否有壳 | 是 / 否 / 疑似 |
| 壳类型 | UPX / VMProtect / Themida / 360 / 乐固 / 梆梆 / 未知自定义壳 |
| 高熵节区 | 节区名:___ 熵值:___ |
| 导入表异常 | 是(仅 ___ 个导入)/ 否(正常) |
下一步决策
如果 → 无壳:
继续第三步
如果 → UPX 等简单壳:
upx -d target_file # 直接脱壳
→ 脱壳后从第一步重新开始验证
如果 → VMProtect / Themida 等强壳:
静态脱壳极难,标记后续需要动态 dump
→ 先完成剩余预分析步骤,脱壳留到动态分析阶段
如果 → Android 商业加固:
使用 Frida + dexdump / FART / BlackDex 脱壳
→ 脱壳后重新分析 DEX 和 SO
三、安全特性检查——了解"它有哪些防护"
目的
安全特性决定后续动态调试和 hook 的策略。提前知道,避免撞墙。
操作流程
═══════════════════════════════════════
ELF 文件(Linux / Android SO)
═══════════════════════════════════════
# 方法 1:checksec(推荐)
checksec --file=libxxx.so
# 方法 2:rabin2
rabin2 -I libxxx.so
# 方法 3:readelf 手动检查
readelf -l libxxx.so | grep GNU_STACK # NX
readelf -l libxxx.so | grep GNU_RELRO # RELRO
readelf -d libxxx.so | grep FLAGS # PIE/BIND_NOW
readelf -s libxxx.so | grep __stack_chk # Canary
═══════════════════════════════════════
PE 文件(Windows EXE/DLL)
═══════════════════════════════════════
# 方法 1:winchecksec(命令行)
winchecksec.exe target.exe
# 方法 2:PE-bear / CFF Explorer(GUI)
→ 查看 Optional Header → DLL Characteristics 字段
# 方法 3:PowerShell
Get-ProcessMitigation -Name target.exe
═══════════════════════════════════════
Mach-O 文件(macOS / iOS)
═══════════════════════════════════════
# 检查 PIE
otool -hv target_binary | grep PIE
# 检查代码签名
codesign -dvvv target_binary
# 检查加密标志(iOS App Store 加密)
otool -l target_binary | grep -A4 LC_ENCRYPTION_INFO
# cryptid = 1 → 有 FairPlay 加密,需要先砸壳
各保护机制的实际影响速查表
| 保护机制 | 状态 | 对你的影响 | 应对策略 |
|---|---|---|---|
| PIE/ASLR | 开启 | 每次加载基址不同,断点地址需要动态计算 | 调试时先获取模块基址:base + offset |
| PIE/ASLR | 关闭 | 地址固定,断点设置方便 | 直接使用 IDA 中看到的地址 |
| NX/DEP | 开启 | 栈/数据段不可执行 | 不影响常规逆向,影响漏洞利用 |
| Full RELRO | 开启 | GOT 表只读,不可覆写 | 无法用 GOT hijack 做 hook,改用 inline hook |
| Partial RELRO | 开启 | GOT 表可写 | 可以用 GOT 表覆写做 hook |
| Stack Canary | 开启 | 函数有栈溢出检测 | 看到 __stack_chk_fail 引用是正常的,别误判为恶意行为 |
| FORTIFY | 开启 | 危险函数被替换为安全版本 | 看到 __memcpy_chk、__strcpy_chk 是 FORTIFY 产生的,等价于原函数 |
安全特性记录表
| 特性 | 状态(开启/关闭) |
|---|---|
| PIE/ASLR | |
| NX/DEP | |
| RELRO | None / Partial / Full |
| Stack Canary | |
| FORTIFY | |
| 代码签名 | |
| 加密(FairPlay等) |
四、元数据提取——系统性收集信息
前置条件
如果第二步检测到有壳 → 先脱壳 → 再执行本步骤。 壳会伪造/隐藏大部分元数据。
4.1 架构与编译器识别
═══════════════════════════════════════
确认架构(决定反汇编工具配置)
═══════════════════════════════════════
# ELF
readelf -h target | grep -E "Machine|Class"
# Machine: AArch64 → ARM64
# Machine: ARM → ARM32
# Machine: X86-64 → x86_64
# Class: ELF64 → 64位
# PE
# 在 DIE 或 CFF Explorer 中查看 Machine 字段
# 0x8664 → x86_64
# 0x14C → x86_32
# 0xAA64 → ARM64
# Mach-O
lipo -info target_binary
# 或 file target_binary
═══════════════════════════════════════
识别编译器
═══════════════════════════════════════
# 方法 1:DIE 自动识别(最简单)
→ 打开文件 → 查看 Compiler/Linker 识别结果
# 方法 2:字符串特征
strings target_file | grep -iE "gcc|clang|msvc|visual|borland|mingw|rustc|go\."
# 方法 3:IDA / Ghidra 中观察
→ GCC 特征:函数以 _Z 开头的 C++ name mangling,
.init_array/.fini_array 节区
→ MSVC 特征:SEH 异常处理结构,
_security_cookie 引用
→ Clang 特征:类似 GCC 但优化模式略有不同,
可能出现 __clang_call_terminate
→ Go 特征:runtime.goexit, runtime.mstart 等符号
→ Rust 特征:core::panicking, alloc:: 等符号前缀
编译器识别结果:___________________
4.2 导入导出表分析
═══════════════════════════════════════
导入表(程序依赖了哪些外部函数)
═══════════════════════════════════════
# ELF
rabin2 -i target.so # 简洁列表
readelf -d target.so | grep NEEDED # 依赖的库
objdump -T target.so # 动态符号表
# PE
rabin2 -i target.exe
# 或 dumpbin /IMPORTS target.exe(MSVC 工具链)
# 或 CFF Explorer → Import Directory
# 将导入表保存到文件便于分析
rabin2 -i target_file > imports.txt
═══════════════════════════════════════
导出表(程序对外提供了哪些函数)
═══════════════════════════════════════
# ELF
rabin2 -E target.so
nm -D target.so | grep " T " # 只看导出的函数
# PE
rabin2 -E target.dll
# 或 dumpbin /EXPORTS target.dll
# 将导出表保存到文件
rabin2 -E target_file > exports.txt
═══════════════════════════════════════
导入表快速分类(找到关键功能线索)
═══════════════════════════════════════
在 imports.txt 中搜索以下关键词:
网络通信:
grep -iE "socket|connect|send|recv|http|url|curl|ssl|tls" imports.txt
文件操作:
grep -iE "fopen|fread|fwrite|CreateFile|ReadFile|WriteFile" imports.txt
加密解密:
grep -iE "crypt|aes|des|rsa|md5|sha|hash|hmac|EVP_|cipher" imports.txt
进程/线程:
grep -iE "CreateProcess|fork|exec|pthread|CreateThread" imports.txt
内存操作:
grep -iE "malloc|calloc|mmap|VirtualAlloc|HeapAlloc" imports.txt
注册表(Windows):
grep -iE "RegOpen|RegQuery|RegSet|RegCreate" imports.txt
反调试:
grep -iE "IsDebugger|NtQuery|ptrace|sysctl" imports.txt
Android JNI:
grep -iE "JNI_OnLoad|RegisterNatives|FindClass|GetMethodID" imports.txt
4.3 节区/段信息
═══════════════════════════════════════
查看节区信息
═══════════════════════════════════════
# ELF
readelf -S target_file
rabin2 -S target_file
# PE(使用 CFF Explorer 或 PE-bear 更直观)
rabin2 -S target.exe
# 关注项:
# 1. 各节区大小 → 特别大的节区可能包含隐藏数据
# 2. 各节区权限 → RWX(可读可写可执行)的节区很可疑
# 3. 虚拟大小 vs 文件大小差异 → 差异大说明可能有运行时解包
# 4. 非标准节区名 → 参考第二步的壳检测
# 快速查找可疑的 RWX 节区
readelf -S target_file | grep "WXA"
4.4 字符串提取与智能筛选
═══════════════════════════════════════
提取字符串
═══════════════════════════════════════
# 基础提取(ASCII + Unicode)
strings -a target_file > strings_ascii.txt
strings -el target_file > strings_unicode.txt # UTF-16LE(Windows 常用)
# 更智能的提取(使用 FLOSS,可以提取混淆后的字符串)
floss target_file > strings_floss.txt
# rabin2 提取(包含地址信息,方便后续定位)
rabin2 -zz target_file > strings_with_addr.txt
# Android DEX 字符串
# 使用 jadx 打开 APK 后在搜索窗口全局搜索
═══════════════════════════════════════
AI 辅助筛选关键字符串
═══════════════════════════════════════
# 将提取的字符串发送给 LLM(Claude/Gemini/GPT)
# 使用以下提示词模板:
"""
以下是从一个 [PE/ELF/APK] 文件中提取的字符串列表。
请帮我分类并标注以下类别的关键字符串:
1. URL / IP 地址 / 域名(网络通信线索)
2. 文件路径(访问了哪些文件)
3. 注册表路径(Windows 相关)
4. 加密相关(密钥、算法名称、IV、Salt)
5. 错误信息 / 日志信息(帮助理解逻辑)
6. SQL 语句(数据库操作)
7. API 密钥 / Token / 证书
8. 调试信息 / 函数名 / 类名(辅助代码理解)
9. 配置项 / 命令行参数
10. 可疑字符串(Base64 编码、硬编码的长字符串等)
请忽略明显无意义的短字符串和系统库路径。
[粘贴字符串列表]
"""
═══════════════════════════════════════
手动快速筛选(如果字符串太多不想全发给 AI)
═══════════════════════════════════════
# 搜索 URL 和 IP
grep -iE "https?://|[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+" strings_ascii.txt
# 搜索文件路径
grep -iE "/data/|/sdcard/|C:\\\\|\\\\Users\\\\" strings_ascii.txt
# 搜索密钥/密码相关
grep -iE "key|password|passwd|secret|token|api_key|AES|DES|RSA" strings_ascii.txt
# 搜索 Base64 编码(长度>20 的纯字母数字+/=)
grep -E "^[A-Za-z0-9+/]{20,}={0,2}$" strings_ascii.txt
# 搜索错误信息
grep -iE "error|fail|exception|invalid|denied|unauthorized" strings_ascii.txt
# 搜索 JSON 键名
grep -iE "\"[a-z_]+\":" strings_ascii.txt
元数据提取汇总表
| 维度 | 结果 |
|---|---|
| 架构 | |
| 编译器 | |
| 依赖库 | |
| 关键导入函数 | |
| 关键导出函数 | |
| 可疑节区 | |
| 关键字符串(URL) | |
| 关键字符串(密钥) | |
| 关键字符串(路径) | |
| 关键字符串(其他) |
五、识别开发框架——精准定位核心代码
目的
避免分析框架的胶水代码,直接找到业务逻辑所在的文件。
操作流程
═══════════════════════════════════════
Android APK 框架识别
═══════════════════════════════════════
# 解压 APK 后,按以下顺序检查
# ---- Unity(游戏/3D应用) ----
检查文件:
ls lib/*/libil2cpp.so # IL2CPP 模式
ls lib/*/libmono.so # Mono 模式
ls assets/bin/Data/ # Unity 资源目录
ls assets/bin/Data/Managed/ # Mono 模式的 DLL
如果存在 libil2cpp.so:
→ 核心逻辑文件:lib/arm64-v8a/libil2cpp.so
→ 辅助文件:assets/bin/Data/Managed/Metadata/global-metadata.dat
→ 分析工具:Il2CppDumper → 导出函数名/类名 → 然后用 IDA 分析 SO
→ 操作命令:Il2CppDumper libil2cpp.so global-metadata.dat output/
如果存在 libmono.so:
→ 核心逻辑文件:assets/bin/Data/Managed/Assembly-CSharp.dll
→ 分析工具:dnSpy / ILSpy / dotPeek(直接反编译 C# 代码)
# ---- Flutter ----
检查文件:
ls lib/*/libflutter.so # Flutter 引擎
ls lib/*/libapp.so # 业务逻辑(Dart AOT 编译)
如果存在 libapp.so:
→ 核心逻辑文件:lib/arm64-v8a/libapp.so
→ 分析工具:reFlutter(修改引擎抓包)/ Doldrums / blutter
→ 注意:Flutter Dart AOT 逆向难度较高,优先考虑抓包分析网络协议
# ---- React Native ----
检查文件:
ls assets/index.android.bundle # JS Bundle(旧版)
ls assets/index.android.bundle # Hermes 字节码(新版)
file assets/index.android.bundle # 判断是 JS 明文还是 Hermes 字节码
如果是 JS 明文:
→ 核心逻辑文件:assets/index.android.bundle
→ 直接用文本编辑器 / beautifier 美化后阅读
如果是 Hermes 字节码(file 显示 Hermes 或不可读):
→ 工具:hermes-dec / hbctool
→ 操作:hbctool disasm index.android.bundle output/
→ 或 hermes-dec index.android.bundle > output.js
# ---- UniApp / 小程序框架 ----
检查文件:
ls assets/apps/ # UniApp 资源目录
ls assets/www/ # Cordova/Ionic 资源目录
# 寻找 .js / .json / .nvue 文件
如果存在 assets/apps/:
→ 核心逻辑文件:assets/apps/[appid]/www/ 下的 JS 文件
→ 通常是 app-service.js 或 app.js
→ 工具:JS 美化器 → 直接阅读 JS 代码
# ---- Xamarin ----
检查文件:
ls assemblies/*.dll # .NET DLL
ls lib/*/libmonodroid.so # Mono 运行时
ls lib/*/libxamarin*.so
如果存在 assemblies/ 目录:
→ 核心逻辑文件:assemblies/ 下的 DLL 文件(特别是与 App 同名的 DLL)
→ 工具:dnSpy / ILSpy 直接反编译 C# 代码
→ 注意:DLL 可能被 AOT 编译或使用 Bundle,需要先用 XamAsmUnZ 解包
# ---- Cocos2d-x ----
检查文件:
ls lib/*/libcocos2dcpp.so # C++ 编译的引擎+业务逻辑
ls assets/src/ # Lua 脚本(如果使用 Lua 绑定)
ls assets/script/ # JS 脚本(如果使用 JS 绑定)
如果存在 Lua/JS 脚本:
→ 核心逻辑文件:assets/src/ 或 assets/script/ 下的脚本
→ 可能经过 LuaJIT 编译或 xxtea 加密
→ 工具:unluac(Lua 反编译)/ xxtea-decrypt
如果只有 libcocos2dcpp.so:
→ 核心逻辑在 SO 中,按照 Native 逆向流程分析
我们还要识别公共库,这里我建议使用libchecker快速识别app中所有公共库,一般来讲我们不去分析公共的第三方库文件
═══════════════════════════════════════
Windows / 桌面端框架识别
═══════════════════════════════════════
# ---- Electron ----
检查文件:
ls resources/app.asar # 主要代码包
ls resources/app/ # 未打包的代码目录
如果存在 app.asar:
→ 工具:npx asar extract app.asar output/
→ 核心逻辑在解包后的 JS 文件中
# ---- .NET / C# ----
# 用 DIE 识别显示 .NET
→ 工具:dnSpy(推荐,可调试)/ ILSpy / dotPeek
→ 直接反编译为 C# 源码
# ---- Qt ----
# 依赖 Qt5Core.dll / Qt6Core.dll 等
→ 核心逻辑在主 EXE 中
→ QML 文件可能在 resources 中(可用 qrc_extractor 提取)
# ---- PyInstaller / cx_Freeze(Python 打包) ----
# DIE 识别或看到 python3x.dll
→ 工具:pyinstxtractor / uncompyle6
→ 操作:python pyinstxtractor.py target.exe
→ 然后 uncompyle6 提取的 .pyc 文件 > output.py
框架识别结果
| 字段 | 填写 |
|---|---|
| 开发框架 | 原生 / Unity / Flutter / RN / UniApp / Electron / .NET / 其他:___ |
| 核心逻辑文件 | |
| 推荐分析工具 | |
| 代码类型 | Native(C/C++) / C# / Java / JavaScript / Dart / Lua / Python |
工具链速查(按平台)
| 平台 | 反汇编/反编译 | 动态调试 | 辅助工具 |
|---|---|---|---|
| Windows PE | IDA Pro / Ghidra / x64dbg(内置) | x64dbg / WinDbg / IDA | CFF Explorer / PE-bear / API Monitor |
| Linux ELF | IDA Pro / Ghidra / radare2 | GDB + GEF/pwndbg / IDA Remote | checksec / strace / ltrace |
| Android Native | IDA Pro / Ghidra | IDA Remote / GDB / Frida | Frida / Objection / MT管理器 |
| Android Java | JADX / JEB / GDA | Frida / Xposed / JEB | Apktool / dex2jar |
| iOS | IDA Pro / Ghidra / Hopper | LLDB / Frida | class-dump / Clutch(砸壳) |
| Unity IL2CPP | Il2CppDumper + IDA | Frida / GameGuardian | Il2CppDumper / Il2CppInspector |
| Unity Mono | dnSpy / ILSpy | dnSpy(带调试) | — |
| .NET | dnSpy / ILSpy / dotPeek | dnSpy | de4dot(去混淆) |
| Flutter | blutter / reFlutter | Frida | reFlutter |
| Python打包 | pyinstxtractor + uncompyle6 | — | — |
最后将结果输出到一份文档中!!!
如果你的项目复杂就需要创建一个项目,我的项目如下。
我的项目/ ├── 00_生肉/ # 原始的目标文件的存放地 比如 libdexjni.so,是一切开始的地方 ├── 01_熟肉/ # 脱壳修复后文件 ├── 02_伪码/ # 反编译输出代码结果文件,包含ida反汇编反编译和ai代码重构后的文件,比如我的文件是sub_564B8_000564B8_子进程看门狗.txt ├── 03_pwn/ # 重点函数的详细分析报告 ├── 控制流/ # 函数调用链分析 ├── 数据流/ # 数据处理分析 ├── 06_脚本/ # frida或者py脚本,用于辅助分析
├── 06_抓包/ # 分析流量
└── README.md # 项目总览
阶段二:静态分析(核心)
静态分析的主要目的是重构为接近源码的表示格式
-
脱壳处理(如需)
-
有壳子先脱壳,自动脱不行就手动脱,配合dump内存映像文件,然后修复脱壳后的程序保证可以正常运行就是脱修完成了,这一步是最难的部分,需要深入学习,这里不讨论!!!
-
自动脱壳:UPX等简单壳
-
手动脱壳:找OEP → dump内存 → 修复IAT
-
加固方案:邦邦/ai加密/063等需专门处理
-
-
导出所有函数代码(https://www.cnblogs.com/GKLBB/p/19106488)
-
清理干扰项
-
删除垃圾函数(大小保持在 1KB 以下极小函数、名称具有C库特征(如 __write_chk、__strlen_chk)、函数名以 . 或 _ 开头、仅做简单的参数转发/跳转的函数、)
-
排除无关代码:
├── FLIRT签名 → 匹配标准库函数
├── IDA Lumina → 在线识别已知函数 - 这里应该使用脚本去除杂质,还未开发出来!!!!!!!!!!!!!!!!
-
-
函数作用分析 。如果有效文件很多超过了百个,那么通过人力分析已经不可能了,需要借助ai批量分析。
单个函数的AI代码重构
分析单个文件方法如下,
初筛(便宜AI,分析函数功能,通俗解释并分析是不是公共库函数,) → Gemini / DeepSeek/glm4.5/豆包code
↓
精析(重量级AI,如果是关键代码再用claude这种重量级的ai进行分析。因为大的代码更有可能是关键函数。) → Claude opus4.6/ evol/ https://lmarena.ai/,或者是在不行就某宝买一个稳定账号。
↓
输出:功能描述 + 重构代码 + 通俗解释
分析关键函数,
利用claude技术分析关键函数的功能将重构的源码追加到文件后比如
函数名称: sub_56BD8 起始地址: 0x00056BD8 结束地址: 0x00056C98 函数大小: 192 字节 ====================================================================== 反汇编代码 (包含地址信息): ====================================================================== 0x00056BD8: STR X19, [SP,#-0x10+var_10]!
====================================================================== 反编译代码 (Hex-Rays): ====================================================================== void __fastcall __noreturn sub_56BD8(int *a1) { int v1; // w19
====================================================================== 分析报告: ======================================================================
函数功能描述:xxx
参数作用描述:xxx
加密算法:xxx
外部引用描述:xxx
第三方库猜测:xxx
数据流和控制流简明描述:xxx
重构源代码:
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/prctl.h>
#include <sys/syscall.h>
#include <android/log.h>
void __attribute__((noreturn)) pipe_watchdog_thread(int *arg)
{
int pipe_read_fd = *arg;
free(arg);
通俗解释:xxx
详细比喻:xxx
分析完成后给每个关键函数文件名称重命名后缀,比如 sub_56BD8_00056BD8_监控看门狗管道连接本身.txt
批量AI代码重构
一个一个分析效率太低,这里建议采用批量分析方法
合并发送给ai批量分析:
一般来讲一个so文件会反编译出大于2000个函数,去除无用函数后就剩下用户编写的函数,大约是800个左右,我们将这些函数按照从根函数(没有调用其他任何函数的叶子函数)开始合并为一个大文件一次性发送给claude可以极大的加速,为了加速分析,我们可以将多个小文件合并为一个大文件一次性发给大模型。
我的提示词是:
编写一个py脚本将这个目录exported_functions下的所有代码文件分批合并为每个文件,合并的方法是按照大小依次合并,注意 只提取反编译代码部分,合并后按照原始文件名称分隔,同时每次合并后的文件字数不能超出119,472个字母(这个字符数来自测试 https://arena.ai/网站得出的最大结果),如果超出就将这个函数文件放到下一个合并的文件中继续合并直到结束,如果单个文件已经超出了119,472个字母,那么将大文件分隔为多个小文件,但是要体现这是一个文件的分块,请编写代码
import os import glob # 定义常量 MAX_FILE_SIZE = 119986 INPUT_DIR = "exported_functions" OUTPUT_DIR = "merged_functions" # 创建输出目录 if not os.path.exists(OUTPUT_DIR): os.makedirs(OUTPUT_DIR) # 获取所有.txt文件 files = glob.glob(os.path.join(INPUT_DIR, "*.txt")) # 过滤掉非.txt文件 files = [f for f in files if f.endswith('.txt')] # 函数:提取文件中的反编译代码部分 def extract_decompiled_code(file_path): with open(file_path, 'r', encoding='utf-8', errors='ignore') as f: lines = f.readlines() # 找到反编译代码开始的位置 start_line = 0 for i, line in enumerate(lines): if "反编译代码 (Hex-Rays):" in line: start_line = i + 3 # 跳过标题行和分隔线 break # 提取反编译代码 if start_line < len(lines): return ''.join(lines[start_line:]) return '' # 计算每个文件的大小(只计算反编译代码部分的大小) file_info = [] for file_path in files: code = extract_decompiled_code(file_path) size = len(code) if size > 0: # 只处理有反编译代码的文件 # 计算文件标题和分隔符的大小 filename = os.path.basename(file_path) header_size = len(f"\n{'='*80}\n文件: {filename}\n{'='*80}\n\n") total_size = size + header_size file_info.append((file_path, size, total_size, code)) # 按照文件总大小排序(包括标题和分隔符) file_info.sort(key=lambda x: x[2]) # 函数:保存当前合并文件 def save_merged_file(files_to_save, output_index): output_file = os.path.join(OUTPUT_DIR, f"merged_{output_index}.txt") with open(output_file, 'w', encoding='utf-8') as f: for fp, c in files_to_save: filename = os.path.basename(fp) f.write(f"\n{'='*80}\n") f.write(f"文件: {filename}\n") f.write(f"{'='*80}\n\n") f.write(c) return output_index + 1 # 开始合并文件 current_size = 0 current_files = [] output_file_index = 1 for file_path, code_size, total_size, code in file_info: filename = os.path.basename(file_path) header_size = len(f"\n{'='*80}\n文件: {filename}\n{'='*80}\n\n") # 检查文件是否太大,需要单独处理 if total_size > MAX_FILE_SIZE: # 保存当前合并文件 if current_files: output_file_index = save_merged_file(current_files, output_file_index) current_size = 0 current_files = [] # 直接保存这个大文件 output_file = os.path.join(OUTPUT_DIR, f"merged_{output_file_index}.txt") with open(output_file, 'w', encoding='utf-8') as f: f.write(f"\n{'='*80}\n") f.write(f"文件: {filename}\n") f.write(f"{'='*80}\n\n") f.write(code) output_file_index += 1 else: # 检查是否能加入当前合并文件 if current_size + total_size <= MAX_FILE_SIZE: current_files.append((file_path, code)) current_size += total_size else: # 保存当前合并文件 if current_files: output_file_index = save_merged_file(current_files, output_file_index) current_size = 0 current_files = [] # 添加当前文件 current_files.append((file_path, code)) current_size += total_size # 保存最后一批文件 if current_files: save_merged_file(current_files, output_file_index) print(f"合并完成!生成了 {output_file_index-1} 个合并文件。") print(f"合并文件保存在: {OUTPUT_DIR}") # 验证所有合并文件的大小 print("\n验证合并文件大小:") merged_files = glob.glob(os.path.join(OUTPUT_DIR, "*.txt")) for file_path in merged_files: with open(file_path, 'r', encoding='utf-8', errors='ignore') as f: content = f.read() size = len(content) status = "✓" if size <= MAX_FILE_SIZE else "✗" print(f"{status} {os.path.basename(file_path)}: {size} 字符")
ai重构源码:
将上述得出的合并文件发送给ai,对于简单的非重点的函数不需要任何提示词,直接要结果即可,可以多开几个页面同时进行我的简单提示词
你是一个高级的逆向安全分析研究员,目标是将伪代码重构为源代码的表示
注意事项:
不要带有臆想出来的源码原来什么代码反编译后应该是什么代码,最终只保留最精确还原源码的一个版本
如果我一次发送了多个函数,请将反编译后的结果用函数原始名称分隔开来,如果函数已经有了完整的重构的反编译版本就跳过这个反编译
不要使用带有汇编代码的注释,注释应该全程使用中文,再每个关键代码处都应该具有注释
在所有代码分析完成后,单独列出所有函数重命名的列表清单
对于简单的非重点的函数不需要任何提示词,直接要结果即可
我给出的建议模板如下:
函数原始名称:sub_xxxxx
函数建议名称:函数来源(自定义函数、系统或第三方库函数、桩函数、导出函数、编译器生成函数等)_函数作用类型(加密函数、业务函数、反调试函数)_(有没有加密加固反调试代码)_如果是已知的库函数就用原有名称如果是自定义函数精简描述
sub_xxxx,重命名后的名称不要使用任何中文因为idapro不支持
函数描述: 函数来源: 安全代码:如果代码具有任何安全有关的代码,都应该这里表述,比如反调试,流量加密,签名验证等等 函数源码:
这里有个问题,对于超大函数ai无法处理,含需要单独分析,不要有任何忽略。
具体如何分析这里暂时未完成!!!!!!!!!!!!!!!!!!!!!!!!
生成函数重命名表
我们将所有的函数分析完成后,用trae写py脚本批量高效提取所有函数的重命名为单独的文件保存。
生成所有函数的元数据,并将重命名后的函数名称填入表
https://www.cnblogs.com/GKLBB/p/19618222
# -*- coding: utf-8 -*- """ IDA Python Script - Export Function Information 导出函数信息到 CSV 和 TXT 文件 包含:地址、段节名称、函数名称、大小、调用次数、被调用次数、函数类型 """ import idautils import idaapi import idc import csv import os from collections import defaultdict def get_function_type(func_flags): """ 根据函数标志判断函数类型 """ types = [] if func_flags & idaapi.FUNC_LIB: types.append("Library") if func_flags & idaapi.FUNC_THUNK: types.append("Thunk/Import") if func_flags & idaapi.FUNC_LUMINA: types.append("Lumina") if func_flags & idaapi.FUNC_STATICDEF: types.append("Static") if func_flags & idaapi.FUNC_FRAME: types.append("BP-Based Frame") if func_flags & idaapi.FUNC_BOTTOMBP: types.append("BottomBP") if func_flags & idaapi.FUNC_NORET: types.append("NoReturn") if func_flags & idaapi.FUNC_FAR: types.append("Far") # 判断是否为导入函数(在导入段中) func_ea = None # 这里不传地址,由外部补充判断 if not types: types.append("Normal") return "/".join(types) def is_import_function(ea): """ 判断函数是否为导入函数 """ seg = idaapi.getseg(ea) if seg: seg_type = seg.type seg_name = idaapi.get_segm_name(seg) if seg_type == idaapi.SEG_XTRN: return True if seg_name and seg_name.lower() in ['.idata', '.plt', '.got.plt', '__imports', 'extern', '.plt.got']: return True return False def get_callers_count(func_ea): """ 获取被调用次数(有多少处调用了该函数) """ xrefs = [] for xref in idautils.XrefsTo(func_ea, 0): # 只统计代码交叉引用(调用/跳转) if xref.type in [idaapi.fl_CF, idaapi.fl_CN, idaapi.fl_JF, idaapi.fl_JN]: xrefs.append(xref.frm) return len(xrefs) def get_callees_count(func_ea): """ 获取调用次数(该函数调用了多少个不同的函数) """ callees = set() func = idaapi.get_func(func_ea) if func is None: return 0 # 遍历函数内的所有指令 for head in idautils.FuncItems(func_ea): for xref in idautils.XrefsFrom(head, 0): if xref.type in [idaapi.fl_CF, idaapi.fl_CN]: # 确认目标是一个函数 target_func = idaapi.get_func(xref.to) if target_func: callees.add(target_func.start_ea) return len(callees) def get_detailed_function_type(ea, func_flags): """ 获取详细的函数类型描述 """ types = [] if is_import_function(ea): types.append("Import") if func_flags & idaapi.FUNC_LIB: types.append("Library") if func_flags & idaapi.FUNC_THUNK: types.append("Thunk") if func_flags & idaapi.FUNC_NORET: types.append("NoReturn") if func_flags & idaapi.FUNC_FAR: types.append("Far") if not types: types.append("Normal") return " | ".join(types) def collect_function_info(): """ 收集所有函数信息 """ func_info_list = [] total = idaapi.get_func_qty() print("[*] 开始收集函数信息,共 {} 个函数...".format(total)) for i, func_ea in enumerate(idautils.Functions()): if i % 500 == 0: print("[*] 进度: {}/{}".format(i, total)) func = idaapi.get_func(func_ea) if func is None: continue # 函数地址 address = "0x{:X}".format(func_ea) # 段节名称 seg = idaapi.getseg(func_ea) segment_name = idaapi.get_segm_name(seg) if seg else "Unknown" # 函数名称 func_name = idc.get_func_name(func_ea) if not func_name: func_name = "sub_{:X}".format(func_ea) # 函数大小 func_size = func.size() # 被调用次数(Callers - 有多少处引用了该函数) callers_count = get_callers_count(func_ea) # 调用次数(Callees - 该函数调用了多少个其他函数) callees_count = get_callees_count(func_ea) # 函数类型 func_flags = func.flags func_type = get_detailed_function_type(func_ea, func_flags) func_info_list.append({ 'address': address, 'segment': segment_name, 'name': func_name, 'size': func_size, 'callees_count': callees_count, # 该函数调用了多少函数 'callers_count': callers_count, # 该函数被调用了多少次 'type': func_type }) print("[*] 函数信息收集完成!共 {} 个函数".format(len(func_info_list))) return func_info_list def export_to_csv(func_info_list, filepath): """ 导出为 CSV 格式 """ headers = [ 'Address', 'Segment', 'Function Name', 'Size (bytes)', 'Callees Count (calls out)', 'Callers Count (called by)', 'Function Type' ] try: with open(filepath, 'w', newline='', encoding='utf-8-sig') as f: writer = csv.writer(f) writer.writerow(headers) for info in func_info_list: writer.writerow([ info['address'], info['segment'], info['name'], info['size'], info['callees_count'], info['callers_count'], info['type'] ]) print("[+] CSV 文件已导出: {}".format(filepath)) except Exception as e: print("[-] CSV 导出失败: {}".format(str(e))) def export_to_txt(func_info_list, filepath): """ 导出为格式化的 TXT 格式 """ try: # 计算各列最大宽度以对齐 col_widths = { 'address': max(len("Address"), max((len(f['address']) for f in func_info_list), default=10)), 'segment': max(len("Segment"), max((len(f['segment']) for f in func_info_list), default=10)), 'name': max(len("Function Name"), max((len(f['name']) for f in func_info_list), default=20)), 'size': max(len("Size"), max((len(str(f['size'])) for f in func_info_list), default=6)), 'callees': max(len("Callees"), max((len(str(f['callees_count'])) for f in func_info_list), default=6)), 'callers': max(len("Callers"), max((len(str(f['callers_count'])) for f in func_info_list), default=6)), 'type': max(len("Type"), max((len(f['type']) for f in func_info_list), default=10)), } # 限制函数名最大宽度,避免过长 col_widths['name'] = min(col_widths['name'], 60) col_widths['type'] = min(col_widths['type'], 30) # 格式化字符串 fmt = "| {:<{aw}} | {:<{sw}} | {:<{nw}} | {:>{szw}} | {:>{clew}} | {:>{clrw}} | {:<{tw}} |" with open(filepath, 'w', encoding='utf-8') as f: # 文件头信息 input_file = idaapi.get_input_file_path() f.write("=" * 120 + "\n") f.write(" Function Information Export Report\n") f.write(" Binary: {}\n".format(input_file if input_file else "Unknown")) f.write(" Total Functions: {}\n".format(len(func_info_list))) f.write("=" * 120 + "\n\n") # 统计信息 normal_count = sum(1 for fi in func_info_list if "Normal" in fi['type']) lib_count = sum(1 for fi in func_info_list if "Library" in fi['type']) import_count = sum(1 for fi in func_info_list if "Import" in fi['type']) thunk_count = sum(1 for fi in func_info_list if "Thunk" in fi['type']) noret_count = sum(1 for fi in func_info_list if "NoReturn" in fi['type']) f.write(" [Statistics]\n") f.write(" Normal Functions: {}\n".format(normal_count)) f.write(" Library Functions: {}\n".format(lib_count)) f.write(" Import Functions: {}\n".format(import_count)) f.write(" Thunk Functions: {}\n".format(thunk_count)) f.write(" NoReturn Functions: {}\n".format(noret_count)) f.write("\n") # 表头 header = fmt.format( "Address", "Segment", "Function Name", "Size", "Callees", "Callers", "Type", aw=col_widths['address'], sw=col_widths['segment'], nw=col_widths['name'], szw=col_widths['size'], clew=col_widths['callees'], clrw=col_widths['callers'], tw=col_widths['type'] ) separator = "+" + "-" * (col_widths['address'] + 2) + \ "+" + "-" * (col_widths['segment'] + 2) + \ "+" + "-" * (col_widths['name'] + 2) + \ "+" + "-" * (col_widths['size'] + 2) + \ "+" + "-" * (col_widths['callees'] + 2) + \ "+" + "-" * (col_widths['callers'] + 2) + \ "+" + "-" * (col_widths['type'] + 2) + "+" f.write(separator + "\n") f.write(header + "\n") f.write(separator + "\n") # 数据行 for info in func_info_list: # 截断过长的函数名 name = info['name'] if len(name) > col_widths['name']: name = name[:col_widths['name'] - 3] + "..." func_type = info['type'] if len(func_type) > col_widths['type']: func_type = func_type[:col_widths['type'] - 3] + "..." line = fmt.format( info['address'], info['segment'], name, str(info['size']), str(info['callees_count']), str(info['callers_count']), func_type, aw=col_widths['address'], sw=col_widths['segment'], nw=col_widths['name'], szw=col_widths['size'], clew=col_widths['callees'], clrw=col_widths['callers'], tw=col_widths['type'] ) f.write(line + "\n") f.write(separator + "\n") f.write("\n Total: {} functions\n".format(len(func_info_list))) print("[+] TXT 文件已导出: {}".format(filepath)) except Exception as e: print("[-] TXT 导出失败: {}".format(str(e))) def main(): """ 主函数 """ print("=" * 60) print(" IDA Function Information Exporter") print("=" * 60) # 获取当前 IDB 文件路径作为默认导出路径 idb_path = idaapi.get_path(idaapi.PATH_TYPE_IDB) if idb_path: default_dir = os.path.dirname(idb_path) base_name = os.path.splitext(os.path.basename(idb_path))[0] else: default_dir = os.getcwd() input_file = idaapi.get_input_file_path() base_name = os.path.splitext(os.path.basename(input_file))[0] if input_file else "functions" # 默认文件路径 default_csv = os.path.join(default_dir, "{}_functions.csv".format(base_name)) default_txt = os.path.join(default_dir, "{}_functions.txt".format(base_name)) # 让用户选择保存路径(CSV) csv_path = idaapi.ask_file(True, default_csv, "Save CSV file") if not csv_path: print("[-] 用户取消了 CSV 导出") csv_path = None # 让用户选择保存路径(TXT) txt_path = idaapi.ask_file(True, default_txt, "Save TXT file") if not txt_path: print("[-] 用户取消了 TXT 导出") txt_path = None if not csv_path and not txt_path: print("[-] 未选择任何导出路径,退出") return # 收集函数信息 func_info_list = collect_function_info() if not func_info_list: print("[-] 未找到任何函数") return # 导出 if csv_path: export_to_csv(func_info_list, csv_path) if txt_path: export_to_txt(func_info_list, txt_path) print("\n" + "=" * 60) print(" 导出完成!") print("=" * 60) # 执行主函数 if __name__ == "__main__": main() else: main()
找到关键函数
最后利用重命名后的函数名称定位关键函数,需要重点分析,可以将下面的话作为ai提示词
什么是关键函数,如何找到关键函数,
一般来讲,被大量调用的函数是工具函数,大函数或者大量调用其他函数的函数一般是入口函数或者核心业务函数
入口函数和导出函数 → JNI_OnLoad / .init_array / DllMain(通过导出函数就可以定位这个函数)
加解密函数 → AES/RSA/自定义算法(通过关键导入函数就可以定位)
反调试函数 → ptrace检测/完整性校验(通过关键导入函数就可以定位)
核心业务函数 → 协议解析/数据处理/通信逻辑(一个业务函数通常他的调用链接最长 或者 交叉引用很少但内部逻辑复杂的函数可能是关键函数)
回调函数 → 被注册到框架中的处理函数(通过关键字字符串就可以定位)
调度函数 → 根据输入分发到不同处理逻辑 (内部控制流存在巨大的switch语句,通常有100个以上的判断)
工具函数 → 字符串处理、内存分配等(交叉引用多,被大量调用的函数通常就是)
一般来讲,我们无法使用函数名称直接定位,因为现在函数名称就是匿名化了,都是sub_开头。其余函数也可能是关键函数,这时候就需要重命名函数我们追个分析攻破就显得尤为重要了。
最后这一步结束后我们得出,
1.重构的源码文件
2.重命名的函数文件列表,包含对应重命名列和是否是关键函数列
例如

阶段三:控制流分析
控制流分析的目的是理解这个文件的到底要干啥,控制流分为内部控制流,比如if,for这些影响函数处理流程的语句,这部分ida已经实现了,外部控制流或者叫调用交叉引用链,是谁调用了我,我调用了谁。我们知道单独分析一个函数是没有意义的。我们重点分析的是外部控制流
- 导出函数控制流文件,https://www.cnblogs.com/GKLBB/p/19605218
-
我们将调用链文件和重命名的函数文件列表发送给claude去分析,比如得出如下结论,将分析结果保存在md文件中
利用Claude还原调用链: 入口函数 ├── 初始化函数A │ ├── 反调试检测 │ │ ├── ptrace自跟踪 │ │ ├── /proc/status检查 │ │ └── 管道心跳监控 │ ├── 完整性校验 │ │ ├── CRC32校验 │ │ └── 代码段hash │ └── 环境检测 │ ├── root检测 │ ├── 模拟器检测 │ └── hook框架检测 ├── 核心业务函数B │ ├── 解密DEX │ ├── 加载类 │ └── 注册Native方法 └── 清理函数C ├── 抹除痕迹 └── 释放资源
阶段四:数据流分析
数据流分析的目的是"数据从哪来、怎么变、到哪去" 。数据流分析一般包含在控制流中,不需要重点关注。有些特殊的数据需要单独追踪。我们直接跳过,不做展开。
https://www.cnblogs.com/GKLBB/p/19603685
阶段五:动态分析
动态分析目的就是查看指令和数据在内存中运行时的表现形式。这一步是最复杂的阶段,如果静态分析足够完整,动态分析就只是辅助,我们直接跳过,不做展开。
阶段六:重复重复再重复
不断重复2到5步, 最后将你分析的的每个字节在ida中标注出来,同时将未被分析的字节进行二次分析,包括未识别的函数和数据。直到所有的都是已知的即可。
我们逆向的最终目的就是还原源码结构,将调整后的源码通过相同版本的编译器编译为目标so文件,逆向比和原版的区别,逐渐靠近原版格式!!!!!!!!!!!
阶段七:存档
存档的目的是将下一次遇见同样的问题时可以瞬间完成处理
执行的命令如下::
git clone 最后将项目上传到gitee获取上传链接
将.git文件夹复制到项目中来
cd 进入你的项目
git add .
git commit -m "第一个提交"
git push origin master
整个流程的口诀就是
导出、去杂、合并,给ai重构每个函数和函数名称
导出调用链,给ai分析处理流
最后得出结论
claude sonnet 4.6
安卓 SO 文件逆向还原指南
整体流程概览
.so文件 → 静态分析 → 反汇编/反编译 → 伪代码还原 → 接近源码
一、准备工作
1.1 基本工具清单
| 工具 | 用途 | 获取方式 |
|---|---|---|
| IDA Pro | 反汇编/反编译(最强) | 商业软件 |
| Ghidra | 反编译(免费开源) | NSA开源 |
| Binary Ninja | 反汇编/反编译 | 商业软件 |
| Radare2 | 命令行分析 | 免费开源 |
| jadx | 辅助分析Java层 | 免费开源 |
| 010 Editor | 十六进制编辑 | 商业软件 |
1.2 确认 SO 文件基本信息
# 查看文件类型和架构
file libxxx.so
# 输出示例:
# libxxx.so: ELF 64-bit LSB shared object, ARM aarch64
# libxxx.so: ELF 32-bit LSB shared object, ARM
# 查看依赖库
readelf -d libxxx.so | grep NEEDED
# 查看导出函数
readelf -s libxxx.so | grep FUNC
nm -D libxxx.so
# 查看节区信息
readelf -S libxxx.so
# 查看ELF头
readelf -h libxxx.so
二、静态分析阶段
2.1 使用 IDA Pro 分析
1. 打开IDA Pro → 拖入.so文件
2. 选择对应架构(ARM/ARM64/x86)
3. 等待自动分析完成
4. 使用Hex-Rays插件反编译(F5)
关键操作:
# IDA 快捷键
F5 - 反编译当前函数(生成伪C代码)
N - 重命名变量/函数
; - 添加注释
X - 查看交叉引用
Ctrl+F - 搜索字符串/函数
Alt+T - 搜索文本
2.2 使用 Ghidra(免费替代方案)
# 启动Ghidra
./ghidraRun
# 操作流程:
1. New Project → 导入SO文件
2. 自动分析(Analysis → Auto Analyze)
3. 在Decompiler窗口查看伪代码
4. Window → Decompiler 查看C伪代码
2.3 字符串提取分析
# 提取所有字符串
strings libxxx.so
# 过滤有意义的字符串
strings libxxx.so | grep -E "(http|key|token|secret|encrypt)"
# 使用rabin2
rabin2 -z libxxx.so # 数据段字符串
rabin2 -zz libxxx.so # 所有字符串
三、识别关键函数
3.1 JNI 函数识别
// 静态注册(名称规律)
Java_包名_类名_方法名
// 例如:
Java_com_example_app_MainActivity_stringFromJNI
// 动态注册(需找JNI_OnLoad)
JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
// 内部调用 RegisterNatives
}
# 快速找JNI函数
readelf -s libxxx.so | grep "Java_"
nm -D libxxx.so | grep "Java_"
3.2 动态注册分析
// 在IDA/Ghidra中找到 JNI_OnLoad
// 查找 RegisterNatives 调用
// 结构体形如:
static JNINativeMethod methods[] = {
{"nativeMethod", "(Ljava/lang/String;)V", (void*)real_function},
};
四、反混淆处理
4.1 常见混淆类型
1. 字符串加密 → 动态调试获取解密后内容
2. 控制流混淆(OLLVM) → 手动/脚本还原
3. 函数名混淆 → 重命名+语义分析
4. 反调试 → patch掉检测代码
4.2 OLLVM 混淆还原
# 使用 D810 插件(IDA插件,针对OLLVM)
# 或使用 deflat.py 脚本
# deflat 使用示例
python deflat.py -f libxxx.so --addr 0x1234 # 指定混淆函数地址
# 使用 Binary Ninja + OLLVM 插件
# 安装 binja-ollvm 插件
4.3 字符串解密
# IDA Python 脚本 - 批量解密字符串
import idc
import idaapi
def decrypt_string(addr, length):
data = idc.get_bytes(addr, length)
# 根据实际加密算法实现解密
result = bytes([b ^ 0x42 for b in data]) # 示例:XOR解密
return result.decode('utf-8', errors='ignore')
# 遍历数据段找加密字符串
for addr in range(start_addr, end_addr):
# 分析特征,找到加密字符串位置
pass
五、动态调试辅助
5.1 使用 Frida Hook 辅助分析
// frida hook 关键函数,获取运行时数据
// 安装:pip install frida-tools
// hook 任意函数
Interceptor.attach(Module.findExportByName("libxxx.so", "target_func"), {
onEnter: function(args) {
console.log("Called with args:");
console.log(" arg0: " + args[0]);
console.log(" arg1: " + Memory.readUtf8String(args[1]));
},
onLeave: function(retval) {
console.log("Return value: " + retval);
}
});
// 按地址hook(用于没有符号的函数)
var base = Module.findBaseAddress("libxxx.so");
var funcAddr = base.add(0x1234); // 函数偏移地址
Interceptor.attach(funcAddr, {
onEnter: function(args) {
console.log("func called");
// 打印内存
console.log(hexdump(args[0], {length: 64}));
}
});
# 运行frida
frida -U -f com.example.app -l hook.js --no-pause
# 或附加到运行中进程
frida -U com.example.app -l hook.js
5.2 GDB/LLDB 调试
# 手机端启动 gdbserver
adb push gdbserver /data/local/tmp/
adb shell chmod +x /data/local/tmp/gdbserver
adb shell /data/local/tmp/gdbserver :23456 --attach $(pidof com.example.app)
# PC端连接
adb forward tcp:23456 tcp:23456
gdb-multiarch
(gdb) target remote :23456
(gdb) set solib-search-path /path/to/libs
(gdb) b Java_com_example_app_nativeFunc
(gdb) continue
5.3 IDA 远程调试
1. 将 android_server 推送到手机
adb push android_server /data/local/tmp/
adb shell chmod 777 /data/local/tmp/android_server
adb shell /data/local/tmp/android_server
2. 端口转发
adb forward tcp:23946 tcp:23946
3. IDA → Debugger → Attach → Remote ARMLinux
输入 localhost:23946
六、还原源码实践
6.1 伪代码清理示例
IDA 生成的原始伪代码:
// 混乱的IDA伪代码
int __fastcall sub_1234(int a1, int a2, int a3)
{
int v3; // r3
int v4; // r4
int v5; // r5
char v7[64]; // [sp+0h] [bp-50h]
v3 = *(a1 + 4);
v4 = *(a1 + 8);
if ( *(a1 + 4) == 0 )
return 0;
sub_5678(v7, a2, 64);
v5 = sub_9ABC(v7, v3, v4);
return v5;
}
整理后的源码:
// 重命名后的清晰代码
int encrypt_data(Context *ctx, const char *input, int input_len)
{
char buffer[64];
int key_len;
char *key;
key = ctx->key;
key_len = ctx->key_len;
if (ctx->key == NULL)
return 0;
strncpy(buffer, input, 64);
return aes_encrypt(buffer, key, key_len);
}
6.2 数据结构还原
// 在IDA中定义结构体 (Local Types)
// 或在Ghidra中 Data Type Manager
// 示例:识别并还原结构体
struct UserInfo {
char username[32]; // offset 0x00
char password[32]; // offset 0x20
int user_id; // offset 0x40
int permission; // offset 0x44
void *callback; // offset 0x48
};
七、自动化辅助工具
7.1 使用 RetDec 自动反编译
# 安装 RetDec
pip install retdec-python
# 反编译SO文件
retdec-decompiler.py libxxx.so
# 输出 .c 文件
7.2 使用 Ghidra 脚本批量处理
# Ghidra Python 脚本
# 自动重命名JNI函数、添加注释
from ghidra.program.model.symbol import SourceType
# 获取所有函数
functionManager = currentProgram.getFunctionManager()
functions = functionManager.getFunctions(True)
for func in functions:
name = func.getName()
if name.startswith("Java_"):
# 处理JNI函数
print("JNI Function: " + name)
# 添加注释
setPlateComment(func.getEntryPoint(), "JNI Export Function")
7.3 IDA Python 批量分析
import idaapi
import idautils
import idc
# 找所有函数并输出
for func_addr in idautils.Functions():
func_name = idc.get_func_name(func_addr)
print(f"0x{func_addr:08X}: {func_name}")
# 批量查找字符串引用
for string in idautils.Strings():
print(f"0x{string.ea:08X}: {str(string)}")
# 导出所有伪代码
import ida_hexrays
for func_addr in idautils.Functions():
try:
cfunc = ida_hexrays.decompile(func_addr)
print(str(cfunc))
except:
pass
八、常见难点处理
8.1 反调试对抗
// 常见反调试手段及对策
1. ptrace检测 → patch掉 ptrace 调用
2. /proc/status → hook open/read 系统调用
3. 时间检测 → hook gettimeofday
4. 端口检测 → 修改调试端口
// Frida 对抗反调试
Interceptor.attach(Module.findExportByName(null, "ptrace"), {
onEnter: function(args) {
args[0] = ptr(-1); // 让ptrace失效
},
onLeave: function(retval) {
retval.replace(0);
}
});
8.2 加壳处理
# 检测是否加壳
# 常见壳:360加固、腾讯乐固、梆梆、爱加密
# 方法1:内存dump(运行时脱壳)
# 使用 frida-dump
frida-ps -U | grep com.example.app
python dump.py com.example.app
# 方法2:使用专用脱壳工具
# BlackDex、FART、Youpk 等
# 方法3:内存搜索ELF头
# 找到 7f 45 4c 46 (ELF魔数)
九、实战工作流程
1. 基础信息收集
├── file / readelf 查看架构
├── strings 提取字符串
└── nm/readelf 查看符号表
2. 静态分析
├── IDA Pro / Ghidra 导入
├── 识别JNI函数入口
├── 追踪关键逻辑
└── 还原数据结构
3. 动态调试
├── Frida hook 关键函数
├── 获取运行时参数
└── 解密字符串/算法
4. 代码还原
├── 清理伪代码
├── 重命名变量函数
├── 还原注释和逻辑
└── 验证还原结果
十、推荐学习资源
书籍:
- 《Android软件安全权威指南》
- 《IDA Pro权威指南》
- 《Android安全攻防实战》
在线资源:
- https://github.com/frida/frida
- https://github.com/NationalSecurityAgency/ghidra
- https://hex-rays.com/ida-pro/
练习平台:
- CTF题目(逆向方向)
- https://crackmes.one/
⚠️ 法律声明:逆向分析仅用于安全研究、漏洞挖掘、兼容性分析等合法目的。请遵守相关法律法规,不得用于侵犯他人知识产权或非法用途。
taoba0-6元 claude opus 4.6
从源码到 SO 文件的闭环编译与反编译
这是一个关于 逆向工程(反编译 .so)→ 还原源码 → 重新编译为 .so 的完整闭环流程。我来详细解释每个环节。
一、整体闭环思路
原始 .so 文件
↓ [反编译/逆向]
接近源码的 C/C++ 代码
↓ [修复/补全]
可编译的源码工程
↓ [用 NDK/GCC 编译]
新的 .so 文件
↓ [对比验证]
与原始 .so 功能一致 → 闭环完成
二、反编译阶段:从 .so 还原出接近源码的结构
1. 工具选择
IDA Pro + Hex-Rays Decompiler 是最核心的工具,它能把汇编直接还原为伪 C 代码。其他辅助工具包括 Ghidra(NSA 开源的免费替代品)、Radare2/Cutter、Binary Ninja 等。
2. 反编译的具体步骤
第一步:确认架构信息
file libtarget.so
# 输出类似: ELF 64-bit LSB shared object, ARM aarch64
readelf -h libtarget.so # 查看头信息
readelf -d libtarget.so # 查看动态段,了解依赖库
readelf --syms libtarget.so # 查看符号表(如果没有 strip)
第二步:用 IDA/Ghidra 加载并反编译
以 Ghidra 为例(免费):
- 新建项目 → 导入 .so → 选择正确的处理器架构(ARM/x86)
- 自动分析完成后,在 Function 列表中浏览所有函数
- 对每个函数按 "Decompile" 查看伪 C 代码
第三步:识别关键结构
反编译出来的代码通常是"丑陋"的,需要做以下还原工作:
| 反编译产物 | 还原目标 |
|---|---|
sub_12345() |
根据逻辑推断函数名,如 decrypt_data() |
*(int *)(a1 + 8) |
还原为结构体访问,如 ctx->key_len |
魔数 0x67452301 |
识别为 MD5/SHA 常量 |
| 大量查表操作 | 识别为 AES S-Box 等已知算法 |
3. 辅助识别技巧
识别标准库函数: 很多 .so 会静态链接 OpenSSL、zlib 等库。可以用 IDA 的 FLIRT 签名或 Ghidra 的 Function ID 自动识别这些库函数,避免重复逆向。
识别 JNI 函数: 如果是 Android .so,导出表中的 Java_com_xxx_yyy 格式函数就是 JNI 入口,参数固定为 (JNIEnv *env, jobject thiz, ...),可以直接套用 JNI 类型定义。
字符串交叉引用: 通过 .so 中残留的字符串(日志、错误信息)反向定位函数用途。
三、源码还原阶段:把伪代码整理为可编译的工程
1. 建立工程结构
project/
├── jni/
│ ├── Android.mk # 或 CMakeLists.txt
│ ├── Application.mk
│ ├── main.c # 主入口/JNI 函数
│ ├── crypto_utils.c # 还原的加密模块
│ ├── crypto_utils.h
│ ├── data_parser.c # 还原的数据解析模块
│ └── data_parser.h
2. 伪代码 → 真实源码的转换
反编译得到的伪代码(示例):
// Ghidra/IDA 输出
int FUN_00012a4c(int param_1, int param_2, int param_3) {
int iVar1;
int local_20;
local_20 = 0;
while (local_20 < *(int *)(param_1 + 4)) {
iVar1 = *(int *)(*(int *)(param_1 + 8) + local_20 * 4);
*(int *)(param_3 + local_20 * 4) = iVar1 ^ *(int *)(param_2 + local_20 * 4);
local_20 = local_20 + 1;
}
return 0;
}
还原后的源码:
// 通过分析,这是一个 XOR 加密函数
typedef struct {
int type;
int length; // offset +4
uint8_t *key; // offset +8
} CryptoContext;
int xor_encrypt(CryptoContext *ctx, const uint8_t *input, uint8_t *output) {
for (int i = 0; i < ctx->length; i++) {
output[i] = ctx->key[i] ^ input[i];
}
return 0;
}
3. 还原的关键原则
- 结构体还原: 根据指针偏移量反推结构体成员的大小和排列
- 控制流还原: 把 goto 风格的跳转恢复为 if/else、for、while、switch
- 类型还原: IDA/Ghidra 中手动设置变量类型,反编译结果会大幅改善
- 不需要100%一致: 追求的是功能等价,不是字节级一致
四、重新编译阶段:把还原的源码编译为 .so
1. 使用 Android NDK 编译(Android .so 场景)
CMakeLists.txt:
cmake_minimum_required(VERSION 3.10)
project(target_lib)
add_library(target SHARED
main.c
crypto_utils.c
data_parser.c
)
# 如果原 so 链接了其他库
target_link_libraries(target log z)
# 关键:编译选项要尽量匹配原始 so
target_compile_options(target PRIVATE
-O2 # 优化等级要匹配
-fvisibility=hidden # 符号可见性
-ffunction-sections
-fdata-sections
)
Application.mk:
APP_ABI := armeabi-v7a arm64-v8a # 匹配原始架构
APP_PLATFORM := android-21
APP_STL := c++_static # 如果用了 C++
编译命令:
# 方式一:用 ndk-build
cd project
ndk-build NDK_PROJECT_PATH=. APP_BUILD_SCRIPT=./jni/Android.mk
# 方式二:用 CMake
mkdir build && cd build
cmake -DCMAKE_TOOLCHAIN_FILE=$NDK/build/cmake/android.toolchain.cmake \
-DANDROID_ABI=arm64-v8a \
-DANDROID_PLATFORM=android-21 \
..
make
2. 通用 Linux .so 编译
gcc -shared -fPIC -O2 -o libtarget.so main.c crypto_utils.c data_parser.c -lz -lpthread
五、验证闭环:确认新 .so 与原始 .so 功能一致
1. 静态对比
# 对比导出符号是否一致
nm -D original.so | sort > sym_orig.txt
nm -D rebuilt.so | sort > sym_new.txt
diff sym_orig.txt sym_new.txt
# 对比节区结构
readelf -S original.so
readelf -S rebuilt.so
# 对比大小(量级应接近)
ls -la original.so rebuilt.so
2. 动态对比(功能验证)
# 写测试程序加载两个 so,对同样的输入对比输出
# test_compare.c
#include <dlfcn.h>
#include <stdio.h>
#include <string.h>
int main() {
void *orig = dlopen("./original.so", RTLD_LAZY);
void *rebuilt = dlopen("./rebuilt.so", RTLD_LAZY);
// 获取同名函数
typedef int (*func_t)(const char *, char *, int);
func_t f1 = dlsym(orig, "process_data");
func_t f2 = dlsym(rebuilt, "process_data");
char out1[256], out2[256];
const char *input = "test_input_data";
f1(input, out1, sizeof(out1));
f2(input, out2, sizeof(out2));
if (memcmp(out1, out2, sizeof(out1)) == 0) {
printf("闭环验证通过!两个 so 输出一致\n");
} else {
printf("输出不一致,需要继续调试\n");
}
dlclose(orig);
dlclose(rebuilt);
return 0;
}
3. 在真实环境替换测试
对于 Android .so,直接把新编译的 .so 替换进 APK:
# 解包 APK
apktool d app.apk -o app_dir
# 替换 so
cp rebuilt.so app_dir/lib/arm64-v8a/libtarget.so
# 重新打包签名
apktool b app_dir -o app_new.apk
jarsigner -keystore my.keystore app_new.apk alias
然后安装运行,验证功能是否正常。
六、常见难点与应对
混淆/加壳: 如果 .so 使用了 OLLVM 混淆(控制流平坦化、虚假控制流、字符串加密),需要先用 Unicorn/Frida 动态脱混淆,再反编译。
编译器差异: 不同编译器、不同优化等级产出的二进制差异很大。不需要追求二进制一致,只要功能等价即可。调整 -O0/-O2/-Os 等优化级别可以让结构更接近。
缺失的上下文: 反编译无法恢复注释、变量名、宏定义。需要结合运行时调试(Frida hook)来理解数据流。
浮点运算精度: 不同编译器对浮点的处理可能导致微小差异,可以设置容差比较。
总结来说,这个闭环的核心思路就是:反编译 → 理解逻辑 → 重写为结构化源码 → 编译 → 功能验证。其中最耗时的是第二步"理解逻辑",它需要逆向工程经验和对目标领域的了解。完全的字节级还原几乎不可能,但功能等价的还原在大多数场景下是可以实现的。
Android SO 文件逆向闭环全流程详解
总体流程概览
原始 .so ──► 反汇编 ──► 反编译(伪C) ──► 人工还原(真C/C++) ──► NDK重编译 ──► 对比验证 ──► 迭代修正
▲ │
└──────────────────────── 闭环校验 ◄──────────────────────────────────────────────┘
第一阶段:前期侦察与信息收集
1.1 基本信息提取
# 查看 ELF 基本信息 file libtarget.so readelf -h libtarget.so # ELF Header readelf -S libtarget.so # Section Headers readelf -l libtarget.so # Program Headers readelf -d libtarget.so # Dynamic Section (依赖库) # 查看架构 readelf -A libtarget.so # arm64-v8a / armeabi-v7a / x86 / x86_64 # 导出符号表(极其重要——未strip的so可以获得函数名) readelf -s libtarget.so # Symbol Table readelf --dyn-syms libtarget.so # Dynamic Symbols nm -D libtarget.so # 动态符号 nm -C -D libtarget.so # C++ demangle后的符号 # 查看字符串(获取线索) strings -a libtarget.so | head -100 # 查看 RELOC 表 readelf -r libtarget.so # 检查是否 stripped file libtarget.so | grep -i strip
1.2 判断编译器与编译选项
关键信息记录清单:
- 目标架构 (ARM64/ARM32/x86)
- 编译器类型与版本 (Clang/GCC)
- NDK 版本
- 优化等级推测 (
-O0/-O1/-O2/-O3/-Os)- 是否有混淆 (OLLVM/Hikari/自研)
第二阶段:反汇编与反编译
2.1 工具选择
| 工具 | 优势 | 适用场景 |
|---|---|---|
| IDA Pro 8.x+ | 反编译质量最高,Hex-Rays 伪C极佳 | 专业逆向(付费) |
| Ghidra | 免费开源,支持协作,反编译质量不错 | 通用逆向分析 |
| Binary Ninja | 中间表示(IL)强大,API友好 | 自动化分析 |
| RetDec | 开源反编译器,可独立使用 | 辅助参考 |
| Cutter (Rizin) | 基于Rizin的图形界面 | 轻量分析 |
2.2 Ghidra 反编译实战(免费方案)
# 安装 Ghidra (需要 JDK 17+) wget https://github.com/NationalSecurityAgency/ghidra/releases/download/Ghidra_11.0_build/ghidra_11.0_PUBLIC_20231222.zip unzip ghidra_11.0_PUBLIC_20231222.zip cd ghidra_11.0_PUBLIC ./ghidraRun # 命令行批量反编译(Headless模式) ./support/analyzeHeadless /path/to/project MyProject \ -import libtarget.so \ -postScript ExportCScript.java /output/decompiled.c \ -deleteProject
Ghidra 自动导出全部函数的脚本:
// ExportAllFunctions.java — Ghidra Script import ghidra.app.script.GhidraScript; import ghidra.app.decompiler.*; import ghidra.program.model.listing.*; import java.io.*; public class ExportAllFunctions extends GhidraScript { @Override public void run() throws Exception { DecompInterface decomp = new DecompInterface(); decomp.openProgram(currentProgram); File outputFile = new File("/tmp/decompiled_all.c"); PrintWriter writer = new PrintWriter(new FileWriter(outputFile)); FunctionIterator funcs = currentProgram.getFunctionManager().getFunctions(true); int count = 0; while (funcs.hasNext()) { Function func = funcs.next(); DecompileResults results = decomp.decompileFunction(func, 60, monitor); if (results.depiledFunction() != null) { writer.println("// ========== " + func.getName() + " @ 0x" + func.getEntryPoint().toString() + " =========="); writer.println(results.getDecompiledFunction().getC()); writer.println(); count++; } } writer.close(); println("Exported " + count + " functions to " + outputFile.getAbsolutePath()); } }
2.3 IDA Pro + Hex-Rays 反编译
# IDAPython 批量导出伪C代码 import idautils import ida_hexrays import ida_funcs output = open("/tmp/ida_decompiled.c", "w") for func_ea in idautils.Functions(): func = ida_funcs.get_func(func_ea) func_name = ida_funcs.get_func_name(func_ea) try: cfunc = ida_hexrays.decompile(func_ea) if cfunc: output.write(f"// ===== {func_name} @ {hex(func_ea)} =====\n") output.write(str(cfunc) + "\n\n") except ida_hexrays.DecompilationFailure: output.write(f"// FAILED: {func_name} @ {hex(func_ea)}\n\n") output.close() print("Export complete!")
第三阶段:反编译代码的深度清理与还原
这是最关键、最耗时的阶段。反编译器输出的伪C代码与真正的源码之间存在巨大差异。
3.1 反编译伪代码的典型问题
| 差异类型 | 反编译伪代码 | 真实源码 |
|---|---|---|
| 变量名 | v12, a1, uVar3 |
buffer, context, result |
| 类型 | long long *, undefined8 |
JNIEnv*, struct MyCtx* |
| 结构体访问 | *(a1 + 0x18) |
ctx->key_length |
| 虚函数调用 | (*(*(long **)a1 + 2))(a1, ...) |
obj->virtualMethod(...) |
| 循环 | goto / if + goto |
for / while |
| switch | 跳转表 *(base + idx*4) |
switch(value) { case: } |
| 字符串 | &DAT_0001a3c0 |
"Hello World" |
| 枚举 | 5, 0x10 |
MODE_CBC, FLAG_ENCRYPT |
| 宏 | 内联展开 | ARRAY_SIZE(arr), ALIGN() |
| C++ STL | 大量底层指针操作 | std::string, std::vector |
3.2 系统化还原流程
Step 1: JNI 接口还原
// ===== 反编译伪代码 ===== long FUN_00012a4c(long *a1, long a2, long a3, long a4) { long v5 = (*(*a1 + 0x538))(a1, a3, 0); long v6 = (*(*a1 + 0x540))(a1, a3); long v7 = (*(*a1 + 0x2a0))(a1, a4); // ... } // ===== 还原后的源码 ===== #include <jni.h> JNIEXPORT jbyteArray JNICALL Java_com_example_NativeLib_encrypt(JNIEnv *env, jobject thiz, jbyteArray input, jstring key) { // JNIEnv 偏移 0x538 → GetByteArrayElements jbyte *inputBytes = (*env)->GetByteArrayElements(env, input, NULL); // JNIEnv 偏移 0x540 → GetArrayLength jsize inputLen = (*env)->GetArrayLength(env, input); // JNIEnv 偏移 0x2a0 → GetStringUTFChars const char *keyStr = (*env)->GetStringUTFChars(env, key, NULL); // ... }
JNI 偏移速查(ARM64 / 指针大小8字节):
// 常用 JNI 函数偏移表 (ARM64, sizeof(void*) = 8) // 偏移 = 函数在 JNINativeInterface 中的序号 × 8 // // GetStringUTFChars = index 169 → offset 0x548 (169*8=1352=0x548) // ReleaseStringUTFChars = index 170 → offset 0x550 // GetArrayLength = index 171 → offset 0x558 // GetByteArrayElements = index 184 → offset 0x5C0 // ReleaseByteArrayElements = index 192 → offset 0x600 // NewByteArray = index 176 → offset 0x580 // SetByteArrayRegion = index 211 → offset 0x698 // FindClass = index 6 → offset 0x30 // GetMethodID = index 33 → offset 0x108 // CallObjectMethod = index 34 → offset 0x110 // 技巧:用 Ghidra/IDA 导入 jni.h 的结构体定义,自动解析偏移!
Step 2: 结构体重建
// ===== 反编译中的字段访问模式 ===== // *(a1 + 0) → 某指针 // *(a1 + 8) → 某指针 // *(int*)(a1 + 16) → 整数 // *(int*)(a1 + 20) → 整数 // *(a1 + 24) → 某指针 (大小256) // ===== 推导出的结构体 ===== typedef struct { uint8_t *key; // offset 0x00, size 8 uint8_t *iv; // offset 0x08, size 8 int key_length; // offset 0x10, size 4 int mode; // offset 0x14, size 4 uint8_t state[256]; // offset 0x18, size 256 } CryptoContext; // total size: 0x118 (280 bytes) // 验证:检查 malloc/calloc 的参数 // 如果看到 malloc(280) 或 calloc(1, 0x118),则结构体大小匹配
Step 3: 算法识别与还原
// ===== 反编译中的特征代码 ===== void FUN_00013f20(uint *param_1, uchar *param_2, int param_3) { uint s0, s1, s2, s3; // 看到以下常量 → AES! // 0x63, 0x7c, 0x77, 0x7b → AES S-Box // 0x01000000, 0x02000000 → AES Rcon // 10/12/14 轮循环 → AES-128/192/256 // Te0/Te1/Te2/Te3 查找表 → AES T-Table 实现 s0 = Te0[...] ^ Te1[...] ^ Te2[...] ^ Te3[...] ^ rk[0]; } // ===== 识别后直接用标准库替代 ===== #include <openssl/aes.h> // 或直接使用已知的 AES 实现
常见算法特征指纹:
┌─────────────────┬──────────────────────────────────────────┐
│ 算法 │ 特征常量/模式 │
├─────────────────┼──────────────────────────────────────────┤
│ AES │ S-Box: 0x63,0x7c,0x77,0x7b... │
│ MD5 │ 0x67452301, 0xefcdab89, 0x98badcfe... │
│ SHA-1 │ 0x67452301, 0xEFCDAB89, 0x98BADCFE... │
│ SHA-256 │ 0x6a09e667, 0xbb67ae85, 0x3c6ef372... │
│ CRC32 │ 0xEDB88320 (多项式) │
│ Base64 │ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef..." │
│ RC4 │ 256字节状态数组 + swap循环 │
│ DES │ IP/FP 置换表, 48→32 S-Box │
│ RSA │ 大数运算, Montgomery乘法 │
│ HMAC │ ipad(0x36)/opad(0x5c) XOR 模式 │
│ TEA/XTEA │ 0x9E3779B9 (黄金比例) │
│ ChaCha20 │ "expand 32-byte k" │
│ SM4 │ 国密 S-Box: 0xD6,0x90,0xE9,0xFE... │
└─────────────────┴──────────────────────────────────────────┘
Step 4: C++ 类还原
// ===== 反编译中的 C++ 虚表调用 ===== // a1 → this 指针 // *(long*)a1 → vtable 指针 // *((long*)*(long*)a1 + 0) → vtable[0] = ~Destructor // *((long*)*(long*)a1 + 1) → vtable[1] = method1 // *((long*)*(long*)a1 + 2) → vtable[2] = method2 // ===== 还原后的 C++ 类 ===== class CryptoEngine { public: virtual ~CryptoEngine(); // vtable[0] virtual int init(const uint8_t* key); // vtable[1] virtual int update(const uint8_t* in, // vtable[2] size_t len, uint8_t* out); virtual int finalize(uint8_t* out); // vtable[3] private: uint8_t m_key[32]; // this + 0x08 int m_keyLen; // this + 0x28 uint8_t m_state[256]; // this + 0x2C int m_mode; // this + 0x12C };
3.3 利用 FLIRT / Signature 自动识别库函数
第四阶段:源码重构与项目组织
4.1 项目结构
restore_project/
├── CMakeLists.txt
├── jni/
│ └── Android.mk # (可选,ndk-build方式)
│ └── Application.mk
├── src/
│ ├── main.c # JNI_OnLoad / JNI 函数注册
│ ├── crypto/
│ │ ├── aes.c
│ │ ├── aes.h
│ │ ├── md5.c
│ │ ├── md5.h
│ │ └── crypto_engine.cpp
│ ├── utils/
│ │ ├── base64.c
│ │ ├── base64.h
│ │ └── hex_utils.c
│ └── core/
│ ├── sign_verify.c
│ └── anti_tamper.c
├── include/
│ └── common.h
└── test/
└── test_crypto.c # 单元测试
4.2 CMakeLists.txt(关键:匹配原始编译选项)
cmake_minimum_required(VERSION 3.22.1) project("restored_lib" C CXX) # ============ 关键:匹配原始编译选项 ============ # 通过 readelf -p .comment 得知编译器版本 # 通过反编译代码特征推测优化等级 set(CMAKE_C_STANDARD 11) set(CMAKE_CXX_STANDARD 17) # 优化等级 — 对最终二进制影响极大 # -O0: 无优化(调试版),变量多,容易逆向 # -O2: 常见 release 版本优化 # -Os: 大小优化 # -O3: 激进优化(可能影响代码结构) set(CMAKE_C_FLAGS_RELEASE "-O2 -DNDEBUG") set(CMAKE_CXX_FLAGS_RELEASE "-O2 -DNDEBUG") # 如果原始so有特殊编译标志 add_compile_options( -ffunction-sections # 每个函数独立 section -fdata-sections # 每个数据独立 section -fvisibility=hidden # 隐藏符号(常见于安卓so) -fPIC # Position Independent Code # -fno-exceptions # 如果原始没有异常处理 # -fno-rtti # 如果原始没有 RTTI ) # 链接选项 set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} \ -Wl,--gc-sections \ -Wl,--version-script=${CMAKE_SOURCE_DIR}/version_script.txt") add_library(target SHARED src/main.c src/crypto/aes.c src/crypto/md5.c src/crypto/crypto_engine.cpp src/utils/base64.c src/utils/hex_utils.c src/core/sign_verify.c src/core/anti_tamper.c ) target_include_directories(target PRIVATE ${CMAKE_SOURCE_DIR}/include ) target_link_libraries(target log # Android log 库 z # zlib(如果原始依赖) android # Android native API )
4.3 完整的 JNI 注册还原示例
// main.c — JNI 入口还原 #include <jni.h> #include <string.h> #include <android/log.h> #define TAG "NativeLib" #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__) // ========== 方式一:静态注册(函数名可直接从导出表看到)========== JNIEXPORT jbyteArray JNICALL Java_com_example_app_NativeLib_encrypt(JNIEnv *env, jobject thiz, jbyteArray data, jstring key) { // ... 还原的加密逻辑 } // ========== 方式二:动态注册(函数名被隐藏,需要分析 JNI_OnLoad)========== // 原始 so 中看到的模式: // JNI_OnLoad → FindClass → RegisterNatives // RegisterNatives 的参数是 JNINativeMethod 数组 static jbyteArray native_encrypt(JNIEnv *env, jobject thiz, jbyteArray data, jstring key) { // ... 还原的加密逻辑 jbyte *dataBytes = (*env)->GetByteArrayElements(env, data, NULL); jsize dataLen = (*env)->GetArrayLength(env, data); const char *keyStr = (*env)->GetStringUTFChars(env, key, NULL); // 分配输出 uint8_t *output = (uint8_t *)malloc(dataLen); // 执行加密 aes_cbc_encrypt((uint8_t *)dataBytes, dataLen, (uint8_t *)keyStr, strlen(keyStr), output); jbyteArray result = (*env)->NewByteArray(env, dataLen); (*env)->SetByteArrayRegion(env, result, 0, dataLen, (jbyte *)output); free(output); (*env)->ReleaseByteArrayElements(env, data, dataBytes, 0); (*env)->ReleaseStringUTFChars(env, key, keyStr); return result; } static jboolean native_verify(JNIEnv *env, jobject thiz, jstring signature) { // ... 还原的验签逻辑 return JNI_TRUE; } // JNINativeMethod 表 static const JNINativeMethod gMethods[] = { {"encrypt", "([BLjava/lang/String;)[B", (void *)native_encrypt}, {"verify", "(Ljava/lang/String;)Z", (void *)native_verify}, }; JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { JNIEnv *env = NULL; if ((*vm)->GetEnv(vm, (void **)&env, JNI_VERSION_1_6) != JNI_OK) { return JNI_ERR; } jclass clazz = (*env)->FindClass(env, "com/example/app/NativeLib"); if (clazz == NULL) { return JNI_ERR; } int rc = (*env)->RegisterNatives(env, clazz, gMethods, sizeof(gMethods) / sizeof(gMethods[0])); if (rc != JNI_OK) { return JNI_ERR; } LOGI("JNI_OnLoad: registered %d native methods", (int)(sizeof(gMethods) / sizeof(gMethods[0]))); return JNI_VERSION_1_6; }
第五阶段:编译与二进制对比验证
5.1 编译
# 方式一:使用 NDK 命令行 export NDK=/path/to/android-ndk-r25c # 尽量匹配原始NDK版本 mkdir build && cd build cmake -DCMAKE_TOOLCHAIN_FILE=$NDK/build/cmake/android.toolchain.cmake \ -DANDROID_ABI=arm64-v8a \ -DANDROID_PLATFORM=android-24 \ -DANDROID_STL=c++_shared \ -DCMAKE_BUILD_TYPE=Release \ .. make -j$(nproc) # 方式二:ndk-build cd jni/ $NDK/ndk-build APP_ABI=arm64-v8a NDK_DEBUG=0 -j$(nproc)
5.2 二进制级别对比
# ========== 基本对比 ========== ls -la original.so restored.so # 文件大小 md5sum original.so restored.so # 哈希(完全一致几乎不可能) # ========== 结构对比 ========== # 对比 Section 布局 diff <(readelf -S original.so) <(readelf -S restored.so) # 对比导出符号 diff <(nm -D original.so | sort) <(nm -D restored.so | sort) # 对比动态依赖 diff <(readelf -d original.so) <(readelf -d restored.so) # ========== 函数级对比(最重要)========== # 使用 objdump 反汇编后 diff aarch64-linux-gnu-objdump -d original.so > original.asm aarch64-linux-gnu-objdump -d restored.so > restored.asm diff original.asm restored.asm | head -200 # 更智能的对比:只比较特定函数 aarch64-linux-gnu-objdump -d original.so | \ awk '/^[0-9a-f]+ <Java_com_example.*>:$/,/^$/' > orig_func.asm aarch64-linux-gnu-objdump -d restored.so | \ awk '/^[0-9a-f]+ <Java_com_example.*>:$/,/^$/' > rest_func.asm diff orig_func.asm rest_func.asm
5.3 使用 BinDiff / Diaphora 做函数级语义对比
# ========== Diaphora (IDA Pro 插件 / 独立工具) ========== # 1. 对两个 so 分别导出 SQLite 数据库 # 2. Diaphora 对比函数相似度 # 在 IDA 中: # File → Script file → diaphora.py # 分别对 original.so 和 restored.so 导出 # 然后进行 Diff # ========== BinDiff (Google) ========== # 1. 在 IDA/Ghidra 中导出 BinExport 文件 # 2. 使用 BinDiff 对比 bindiff original.BinExport restored.BinExport # 输出会显示: # - 匹配的函数数量和相似度百分比 # - 每个函数的指令级相似度 # - 未匹配的函数
5.4 功能验证(黑盒测试)
# Python 脚本通过 Frida 对比原始SO与还原SO的运行结果 import frida import sys def test_encryption(): """同时 hook 原始和还原的 so,对比加密结果""" js_code = """ // Hook encrypt 函数 var encrypt = Module.findExportByName("libtarget.so", "Java_com_example_app_NativeLib_encrypt"); Interceptor.attach(encrypt, { onEnter: function(args) { console.log("[*] encrypt called"); // 记录输入参数 }, onLeave: function(retval) { // 打印加密结果 var env = Java.vm.getEnv(); var arr = env.getByteArrayElements(retval, null); var len = env.getArrayLength(retval); console.log("[*] Result length: " + len); console.log("[*] Result hex: " + hexdump(arr, {length: len})); } }); """ # 分别注入原始APK和替换SO的APK,对比结果 device = frida.get_usb_device() pid = device.spawn(["com.example.app"]) session = device.attach(pid) script = session.create_script(js_code) script.load() device.resume(pid) sys.stdin.read() test_encryption()
第六阶段:处理混淆(OLLVM / 高级保护)
6.1 OLLVM 混淆识别
// ===== 控制流平坦化 (CFF) 的典型特征 ===== // 一个巨大的 while(true) + switch 结构 void obfuscated_function(int *param) { int state = 0xe4c3b2a1; // 初始状态 while (true) { switch (state) { case 0xe4c3b2a1: // 第一个基本块 state = 0x7f8d9e2b; break; case 0x7f8d9e2b: // 第二个基本块 if (condition) state = 0x3a5c1d4e; else state = 0x9b2e7f80; break; case 0x3a5c1d4e: // ... break; // 大量 case ... case 0xdeadbeef: return; } } }
6.2 去混淆工具链
# ========== D-810 (IDA 插件) ========== # 专门对付 OLLVM 控制流平坦化 # https://github.com/joydo/d810 # 安装后:Edit → Plugins → D-810 # 自动识别并简化 CFF 模式 # ========== deflat (基于 angr 的符号执行去平坦化) ========== pip install angr git clone https://github.com/cq674350529/deflat cd deflat # 使用方法 python deflat.py -f libtarget.so --addr 0x12345 # 指定混淆函数地址 # ========== OLLVM Deobfuscator (Binary Ninja 插件) ========== # 利用 Binary Ninja 的 MLIL/HLIL 去除 CFF # ========== 使用 Unicorn/Qiling 模拟执行辅助分析 ========== pip install unicorn qiling
# 使用 angr 符号执行还原控制流的简化示例 import angr import claripy def deobfuscate_function(binary_path, func_addr, func_size): proj = angr.Project(binary_path, auto_load_libs=False) # 创建符号化的输入 sym_arg = claripy.BVS("input", 64) state = proj.factory.call_state(func_addr, sym_arg) simgr = proj.factory.simulation_manager(state) # 探索所有可达路径 simgr.explore(find=lambda s: s.addr >= func_addr and s.addr < func_addr + func_size) # 收集所有可达的基本块和它们之间的真实控制流 real_cfg = {} for found in simgr.found: trace = found.history.bbl_addrs.hardcopy for i in range(len(trace) - 1): src = trace[i] dst = trace[i + 1] if src not in real_cfg: real_cfg[src] = set() real_cfg[src].add(dst) return real_cfg # 得到真实的控制流图后,可以据此重构代码
6.3 字符串加密还原
// 常见模式:字符串在 .init_array / JNI_OnLoad 中解密 // 或者每次使用前动态解密 // ===== 反编译中常见的字符串解密模式 ===== char * get_string_42(void) { static char buf[] = {0x6f^0x1a, 0x48^0x1a, 0x5c^0x1a, ...}; static int decoded = 0; if (!decoded) { for (int i = 0; i < sizeof(buf); i++) { buf[i] ^= 0x1a; } decoded = 1; } return buf; } // ===== 还原策略 ===== // 方法1: 用 Frida hook 运行时获取解密后的字符串 // 方法2: 模拟执行解密函数 // 方法3: 根据解密算法写脚本批量解密 # Frida 批量 dump 解密后的字符串 import frida js = """ // Hook 所有字符串解密函数(假设它们都调用某个通用解密函数) var decrypt_str = Module.findExportByName("libtarget.so", null); // 方法:hook strlen/strcmp 来捕获使用中的字符串 Interceptor.attach(Module.findExportByName(null, "strlen"), { onEnter: function(args) { var addr = args[0]; var module = Process.findModuleByAddress(addr); if (module && module.name === "libtarget.so") { try { var str = Memory.readUtf8String(addr); if (str && str.length > 3) { console.log("[STR @ " + addr.sub(module.base) + "] " + str); } } catch(e) {} } } }); """
第七阶段:迭代细化与完美闭环
7.1 迭代验证循环
┌──────────────────────────────────────────────────────────┐
│ 迭代验证循环 │
│ │
│ ① 编译还原代码 │
│ ↓ │
│ ② BinDiff 对比函数相似度 │
│ ↓ │
│ ③ 找到差异最大的函数 │
│ ↓ │
│ ④ 对比该函数的汇编指令 │
│ ↓ │
│ ⑤ 分析差异原因: │
│ - 编译器优化差异?→ 调整编译选项 │
│ - 代码逻辑差异?→ 修正源码 │
│ - 数据布局差异?→ 调整结构体/对齐 │
│ ↓ │
│ ⑥ 修正后回到 ① │
│ │
│ 目标:核心函数 BinDiff 相似度 > 95% │
│ + 功能测试 100% 通过 │
└──────────────────────────────────────────────────────────┘
7.2 编译选项微调对照
# 如果反编译看到大量内联函数 → 可能用了 -O2 或 -O3 # 如果函数都很小、没有内联 → 可能用了 -O0 或 -O1 # 如果代码极度紧凑 → 可能用了 -Os # 常见需要调整的选项: add_compile_options( -O2 # 优化等级 -fomit-frame-pointer # 是否省略帧指针(影响函数入口/出口) -finline-functions # 内联控制 -fno-inline-functions # 禁止内联(如果原始没有内联) -fno-strict-aliasing # 别名规则 -fno-stack-protector # 如果原始没有栈保护 # -fstack-protector-strong # 如果原始有栈保护 -mfloat-abi=softfp # 浮点ABI(ARM32) -mfpu=neon # NEON 指令(ARM32) ) # 对齐控制 # __attribute__((aligned(16))) # #pragma pack(push, 1) / #pragma pack(pop)
7.3 精确匹配技巧
// ===== 技巧1: 使用相同的编译器内建函数 ===== // 如果原始代码用了 __builtin_expect (likely/unlikely) #define likely(x) __builtin_expect(!!(x), 1) #define unlikely(x) __builtin_expect(!!(x), 0) // 如果看到 CLZ 指令 → __builtin_clz() // 如果看到 CTZ 指令 → __builtin_ctz() // 如果看到 BSWAP → __builtin_bswap32() // ===== 技巧2: 匹配函数属性 ===== __attribute__((noinline)) // 防止内联 __attribute__((always_inline)) // 强制内联 __attribute__((visibility("default"))) // 导出函数 __attribute__((constructor)) // .init_array __attribute__((destructor)) // .fini_array __attribute__((aligned(64))) // 对齐 // ===== 技巧3: volatile 防止优化消除 ===== // 如果反编译中看到看似"无用"的内存访问,可能原始代码用了 volatile volatile uint32_t *mmio = (volatile uint32_t *)addr; *mmio = value; // 不会被优化掉 // ===== 技巧4: 匹配寄存器分配(高级)===== // 使用 register 关键字提示(效果有限,现代编译器通常忽略) // 更有效的方式是调整变量声明顺序和作用域
第八阶段:自动化工具链
8.1 一键式还原脚本
#!/bin/bash # restore_pipeline.sh — 自动化逆向闭环 set -e SO_FILE=$1 ARCH=${2:-arm64-v8a} NDK_PATH=${ANDROID_NDK_HOME:-/opt/android-ndk-r25c} echo "========================================" echo " Android SO Reverse Engineering Pipeline" echo "========================================" # Step 1: 信息收集 echo "[*] Step 1: Collecting information..." mkdir -p analysis output readelf -h "$SO_FILE" > analysis/elf_header.txt readelf -S "$SO_FILE" > analysis/sections.txt readelf -s "$SO_FILE" > analysis/symbols.txt readelf -d "$SO_FILE" > analysis/dynamic.txt readelf -p .comment "$SO_FILE" > analysis/compiler.txt 2>/dev/null || true strings -a "$SO_FILE" > analysis/strings.txt nm -D -C "$SO_FILE" > analysis/exports.txt 2>/dev/null || true echo " Compiler: $(cat analysis/compiler.txt 2>/dev/null | grep -o 'Android.*' | head -1)" echo " Arch: $(readelf -h $SO_FILE | grep Machine | awk '{print $2}')" # Step 2: Ghidra 自动反编译 echo "[*] Step 2: Decompiling with Ghidra..." GHIDRA_HOME=${GHIDRA_HOME:-/opt/ghidra} if [ -d "$GHIDRA_HOME" ]; then $GHIDRA_HOME/support/analyzeHeadless \ $(pwd)/analysis ghidra_project \ -import "$SO_FILE" \ -postScript ExportAllFunctions.java output/decompiled.c \ -deleteProject \ -noanalysis 2>/dev/null || echo " [!] Ghidra decompilation needs manual setup" fi # Step 3: 提取函数列表 echo "[*] Step 3: Extracting function list..." nm -D "$SO_FILE" 2>/dev/null | grep " T " | awk '{print $3}' > analysis/exported_functions.txt echo " Found $(wc -l < analysis/exported_functions.txt) exported functions" # Step 4: 生成项目骨架 echo "[*] Step 4: Generating project skeleton..." mkdir -p src include cat > CMakeLists.txt << 'CMAKE_EOF' cmake_minimum_required(VERSION 3.22.1) project("restored" C CXX) set(CMAKE_C_FLAGS_RELEASE "-O2 -DNDEBUG -fvisibility=hidden") add_library(restored SHARED src/main.c) target_link_libraries(restored log) CMAKE_EOF # 为每个导出函数生成桩代码 cat > src/main.c << 'C_EOF' #include <jni.h> // ===== AUTO-GENERATED STUBS ===== // TODO: Fill in actual implementations C_EOF while IFS= read -r func; do if [[ "$func" == Java_* ]]; then echo "// TODO: Implement $func" >> src/main.c echo "JNIEXPORT void JNICALL ${func}(JNIEnv *env, jobject thiz) {}" >> src/main.c echo "" >> src/main.c fi done < analysis/exported_functions.txt echo "[*] Step 5: Build skeleton..." mkdir -p build && cd build cmake -DCMAKE_TOOLCHAIN_FILE=$NDK_PATH/build/cmake/android.toolchain.cmake \ -DANDROID_ABI=$ARCH \ -DANDROID_PLATFORM=android-24 \ -DCMAKE_BUILD_TYPE=Release \ .. 2>/dev/null && make -j$(nproc) 2>/dev/null cd .. echo "" echo "========================================" echo " Pipeline complete!" echo " Decompiled code: output/decompiled.c" echo " Analysis data: analysis/" echo " Project skeleton: src/" echo "========================================"
8.2 Python 辅助分析工具
#!/usr/bin/env python3 """so_analyzer.py — 辅助分析与对比工具""" import subprocess import re import json from pathlib import Path from collections import defaultdict class SOAnalyzer: def __init__(self, so_path): self.so_path = so_path self.info = {} def analyze(self): """全面分析 SO 文件""" self.info['sections'] = self._get_sections() self.info['exports'] = self._get_exports() self.info['imports'] = self._get_imports() self.info['strings'] = self._get_interesting_strings() self.info['compiler'] = self._get_compiler_info() self.info['dependencies'] = self._get_dependencies() return self.info def _get_sections(self): out = subprocess.check_output( ['readelf', '-S', self.so_path], text=True) sections = [] for match in re.finditer( r'\[\s*(\d+)\]\s+(\S+)\s+(\S+)\s+([0-9a-f]+)\s+([0-9a-f]+)\s+([0-9a-f]+)', out): sections.append({ 'index': int(match.group(1)), 'name': match.group(2), 'type': match.group(3), 'addr': match.group(4), 'offset': match.group(5), 'size': int(match.group(6), 16), }) return sections def _get_exports(self): out = subprocess.check_output( ['nm', '-D', '-C', self.so_path], text=True, stderr=subprocess.DEVNULL) exports = [] for line in out.strip().split('\n'): parts = line.split() if len(parts) >= 3 and parts[1] == 'T': exports.append({ 'address': parts[0], 'name': ' '.join(parts[2:]) }) return exports def _get_imports(self): out = subprocess.check_output( ['nm', '-D', '-C', self.so_path], text=True, stderr=subprocess.DEVNULL) imports = [] for line in out.strip().split('\n'): parts = line.split() if len(parts) >= 2 and parts[0] == 'U': imports.append(parts[1] if len(parts) == 2 else ' '.join(parts[1:])) return imports def _get_interesting_strings(self): out = subprocess.check_output( ['strings', '-a', self.so_path], text=True) interesting = [] patterns = [ r'(?:AES|DES|RSA|SHA|MD5|HMAC)', r'(?:encrypt|decrypt|sign|verify|hash)', r'(?:key|password|secret|token)', r'(?:http|https|ftp)://', r'(?:\.json|\.xml|\.db|\.so)', r'Java_\w+', ] combined = '|'.join(f'({p})' for p in patterns) for s in out.strip().split('\n'): if re.search(combined, s, re.IGNORECASE): interesting.append(s.strip()) return list(set(interesting))[:200] def _get_compiler_info(self): try: out = subprocess.check_output( ['readelf', '-p', '.comment', self.so_path], text=True, stderr=subprocess.DEVNULL) return out.strip() except: return "Unknown" def _get_dependencies(self): out = subprocess.check_output( ['readelf', '-d', self.so_path], text=True) deps = [] for match in re.finditer(r'NEEDED.*\[(.+?)\]', out): deps.append(match.group(1)) return deps def compare_sos(original_path, restored_path): """对比两个 SO 文件""" orig = SOAnalyzer(original_path) rest = SOAnalyzer(restored_path) orig_info = orig.analyze() rest_info = rest.analyze() print("=" * 60) print("SO Comparison Report") print("=" * 60) # 对比导出函数 orig_exports = {e['name'] for e in orig_info['exports']} rest_exports = {e['name'] for e in rest_info['exports']} print(f"\n📦 Exports:") print(f" Original: {len(orig_exports)} functions") print(f" Restored: {len(rest_exports)} functions") print(f" Matching: {len(orig_exports & rest_exports)}") print(f" Missing: {len(orig_exports - rest_exports)}") if orig_exports - rest_exports: for name in sorted(orig_exports - rest_exports)[:20]: print(f" ❌ {name}") print(f" Extra: {len(rest_exports - orig_exports)}") # 对比 Section 大小 print(f"\n📊 Section Size Comparison:") orig_sections = {s['name']: s['size'] for s in orig_info['sections']} rest_sections = {s['name']: s['size'] for s in rest_info['sections']} for name in sorted(set(orig_sections) | set(rest_sections)): os = orig_sections.get(name, 0) rs = rest_sections.get(name, 0) diff = rs - os pct = (diff / os * 100) if os > 0 else float('inf') status = "✅" if abs(pct) < 10 else "⚠️" if abs(pct) < 30 else "❌" if os > 0 or rs > 0: print(f" {status} {name:20s} orig={os:8d} rest={rs:8d} diff={diff:+8d} ({pct:+.1f}%)") # 对比依赖 print(f"\n📎 Dependencies:") orig_deps = set(orig_info['dependencies']) rest_deps = set(rest_info['dependencies']) for dep in sorted(orig_deps | rest_deps): if dep in orig_deps and dep in rest_deps: print(f" ✅ {dep}") elif dep in orig_deps: print(f" ❌ {dep} (missing in restored)") else: print(f" ⚠️ {dep} (extra in restored)") if __name__ == '__main__': import sys if len(sys.argv) == 2: analyzer = SOAnalyzer(sys.argv[1]) info = analyzer.analyze() print(json.dumps(info, indent=2, ensure_ascii=False)) elif len(sys.argv) == 3: compare_sos(sys.argv[1], sys.argv[2]) else: print(f"Usage: {sys.argv[0]} <so_file>") print(f" {sys.argv[0]} <original.so> <restored.so>")
关键经验总结
✅ 可以做到的
| 项目 | 可行度 | 说明 |
|---|---|---|
| 功能等价还原 | ⭐⭐⭐⭐⭐ | 输入输出完全一致 |
| 算法逻辑还原 | ⭐⭐⭐⭐⭐ | 标准算法可100%还原 |
| JNI 接口还原 | ⭐⭐⭐⭐⭐ | 导出表信息完整 |
| 数据结构还原 | ⭐⭐⭐⭐ | 通过偏移分析可高度还原 |
| 控制流还原 | ⭐⭐⭐⭐ | 无混淆时可高度还原 |
| 二进制级完全一致 | ⭐⭐ | 极难,但核心函数可接近 |
| 变量名/注释还原 | ⭐ | 除非有调试符号,否则不可能 |
🔑 核心原则
编译是多对一映射:多种源码 → 同一二进制
逆向是一对多映射:同一二进制 → 多种可能的源码
因此,"完美还原"的定义应该是:
✅ 功能完全等价(黑盒测试通过)
✅ 算法逻辑一致(相同的计算过程)
✅ 数据结构兼容(ABI 兼容)
⚠️ 变量名合理(根据语义自行命名)
❌ 与原始源码逐字符一致(理论上不可能)
📋 最佳实践清单
- 先全局后局部:先理解整体架构,再深入具体函数
- 先识别后分析:先用签名/特征码识别已知库和算法
- 先动态后静态:Frida / 调试器获取运行时信息,辅助静态分析
- 匹配编译环境:NDK版本、编译器版本、优化等级缺一不可
- 增量验证:每还原一个函数就编译对比一次
- 善用自动化:脚本化重复性工作,专注于真正需要人脑的部分
浙公网安备 33010602011771号