JuneLeGency/dexfinder
GitHub: JuneLeGency/dexfinder
一款用 Go 实现的跨平台 Android DEX 字节码静态分析工具,提供方法引用查找、调用链追踪、ProGuard 反混淆和 Hidden API 检测能力,帮助安全研究人员和合规审计人员高效定位敏感 API 调用。
Stars: 38 | Forks: 4
# dexfinder
[English](#english) | [中文](#中文)
跨平台 APK/DEX 方法与字段引用查找器,支持调用链追踪、ProGuard/R8 反混淆以及 Android Hidden API 检测。
受 Android [veridex](https://android.googlesource.com/platform/art/+/refs/heads/master/tools/veridex/) 工具启发,使用 Go 重新实现并增强了功能:更快的反射检测、调用链追踪(veridex 仅展示一层)以及灵活的输出格式。
## 特性
- **APK/DEX/JAR 扫描** — 解析 DEX 字节码,提取所有方法/字段/字符串引用
- **多格式查询** — 支持通过 Java 名称、DEX/JNI 签名或简单关键字进行搜索
- **调用链追踪** — 向上追溯最多 N 层调用者,支持合并树或平铺列表展示,并带有环路检测
- **ProGuard/R8 反混淆** — 加载 mapping.txt,将原始名称与混淆名称一并显示
- **Hidden API 检测** — 加载 hiddenapi-flags.csv,检测被限制/不支持的 API
- **反射检测** — 通过交叉匹配类名与字符串,发现基于反射的 Hidden API 使用情况
- **灵活输出** — text / json / model 格式,tree / list 布局,java / dex 命名风格——皆可自由组合
- **零外部依赖** — 纯 Go 实现,内置自包含的 DEX 解析器
- **跨平台** — macOS (Intel / Apple Silicon)、Linux (amd64 / arm64)、Windows
## 安装
**Homebrew** (macOS / Linux):
```
brew tap JuneLeGency/tap
brew install dexfinder
```
**脚本** (自动检测操作系统/架构):
```
curl -sSL https://raw.githubusercontent.com/JuneLeGency/dexfinder/main/install.sh | bash
```
**Go install**:
```
go install github.com/JuneLeGency/dexfinder/cmd/dexfinder@latest
```
**二进制文件**: 从 [Releases](https://github.com/JuneLeGency/dexfinder/releases) 下载。
## 快速开始
```
# 显示 APK 概览
dexfinder --dex-file app.apk --stats
# 查找所有调用 getDeviceId (IMEI) 的位置
dexfinder --dex-file app.apk --query "getDeviceId"
# 将调用链追踪为合并树
dexfinder --dex-file app.apk --query "getDeviceId" --trace
# 追踪为扁平调用栈 (Java crash 样式)
dexfinder --dex-file app.apk --query "getDeviceId" --trace --layout list
# 精确的 JNI 签名查询
dexfinder --dex-file app.apk \
--query "Landroid/telephony/TelephonyManager;->getDeviceId()Ljava/lang/String;" \
--trace --depth 8
# 隐藏 API 检测
dexfinder --dex-file app.apk --api-flags hiddenapi-flags.csv
```
## 查询格式
`--query` 参数接受多种输入格式。dexfinder 会自动检测并在它们之间进行转换。
| 格式 | 示例 | 行为 |
|---|---|---|
| 简单名称 | `getDeviceId` | 跨所有 API 的模糊子串匹配 |
| Java 类名 | `android.telephony.TelephonyManager` | 该类的所有方法/字段 |
| Java 类名#方法 | `android.telephony.TelephonyManager#getDeviceId` | 该方法的所有重载 |
| Java 完整签名 | `...TelephonyManager#getDeviceId()` | 精确匹配 + 重载回退 |
| DEX/JNI 签名 | `Landroid/telephony/TelephonyManager;->getDeviceId()Ljava/lang/String;` | 仅精确匹配 |
```
# 所有等效方式 — 在 LocationManager 中查找 requestLocationUpdates:
dexfinder --dex-file app.apk --query "requestLocationUpdates"
dexfinder --dex-file app.apk --query "android.location.LocationManager#requestLocationUpdates"
dexfinder --dex-file app.apk --query "Landroid/location/LocationManager;->requestLocationUpdates(Ljava/lang/String;JFLandroid/location/LocationListener;)V"
```
## 输出控制
三个独立的控制维度,可自由组合:
```
--format (text / json / model) what to output
--layout (tree / list) how to arrange traces
--style (java / dex) how to display names
```
### `--format`
| 值 | 描述 |
|---|---|
| `text` | 纯文本输出(默认) |
| `json` | JSON — 扫描结果或采用 tree/list 布局的追踪结果 |
| `model` | 包含完整 MethodInfo/FieldInfo 类型的结构化 JSON(适用于 IDE/CI) |
### `--layout` (与 `--trace` 配合使用)
| 值 | 描述 |
|---|---|
| `tree` | 合并树 — 将共享调用路径折叠成一棵树(默认) |
| `list` | 平铺列表 — 每条唯一调用链作为独立的堆栈展示 |
### `--style`
| 值 | 示例 | 用例 |
|---|---|---|
| `java` | `com.example.Foo.method(Foo.java)` | 人类可读(默认) |
| `dex` | `Foo.method(Ljava/lang/String;)V` | 精确签名分析 |
### `--scope` (搜索范围)
控制查询匹配**哪种引用类型**。这对于理解结果至关重要。
| 值 | 搜索内容 | 回答的问题 | 输出标签 |
|---|---|---|---|
| `all` | 被调用的 API + 字段 + 代码字符串 | “谁调用了这个 API?”(默认) | `[METHOD]` `[FIELD]` `[STRING]` |
| `callee` | 仅 `invoke-*` / `get/put` 指令中的目标 API 签名 | “谁调用了这个具体的方法/字段?” | `[METHOD]` `[FIELD]` |
| `caller` | 仅调用方方法的签名 | “这个方法内部调用了什么?” | `[CALLER→]` |
| `string` | `const-string` 指令中的字符串常量 | “这个字符串在代码中哪里被使用了?” | `[STRING]` |
| `string-table` | 代码字符串 + 完整的 DEX 字符串表 | “这个字符串是否存在于 DEX 中的任何位置?”(包括注解、死代码) | `[STRING]` `[STRING_TABLE]` |
| `everything` | 以上所有组合 | 全局视图 | 所有标签 |
**理解 callee 与 caller 的区别:**
```
scope=callee: "Who calls finish()?"
onCreate ──calls──→ finish() ← these callers are shown
onResume ──calls──→ finish()
scope=caller: "What does finish() call internally?"
finish() ──calls──→ Log.i() ← these callees are shown
finish() ──calls──→ super.finish()
```
`--scope=all`(默认)= `callee` + `string`。`caller` 方向被有意排除在默认值之外,因为它回答的是一个根本不同的问题。当你需要时,请显式使用 `--scope=caller` 或 `--scope=everything`。
**理解输出标签:**
| 标签 | 含义 |
|---|---|
| `[METHOD]` | **被调用的**方法与你的查询匹配(被调用方匹配)。缩进行是调用方。 |
| `[FIELD]` | **被访问的**字段与你的查询匹配。缩进行是访问者。 |
| `[CALLER→]` | **调用方方法**与你的查询匹配。缩进行显示它正在调用的 API。 |
| `[STRING]` | 代码中的字符串常量与你的查询匹配。缩进行是使用它的位置。 |
| `[STRING_TABLE]` | 字符串存在于 DEX 字符串表中,但在代码中没有 `const-string` 引用(可能在注解中,或被 R8 优化掉等) |
## 示例
### 1. 扫描 APK 统计信息
```
dexfinder --dex-file app.apk --stats
```
```
Loaded 31 DEX file(s): 183913 classes, 1250566 method refs
Method references: 680610
Field references: 625572
String constants: 654353
Referenced types: 192586
Time: 3.9s
```
### 2. 查找所有位置追踪调用
```
dexfinder --dex-file app.apk --query "requestLocationUpdates"
```
```
[METHOD] Landroid/location/LocationManager;->requestLocationUpdates(Ljava/lang/String;JFLandroid/location/LocationListener;)V (3 ref)
Lcom/example/TestEntry;->init(Landroid/content/Context;)V (2 occurrences)
Lcom/example/service/LocationService;->onStartCommand(Landroid/content/Intent;II)I
```
### 3. 追踪调用链 — 树状视图
```
dexfinder --dex-file app.apk \
--query "Landroid/telephony/TelephonyManager;->getDeviceId()Ljava/lang/String;" \
--trace --depth 5
```
```
android.telephony.TelephonyManager.getDeviceId()
└── com.example.aopsdk.TelephonyManager.getDeviceId(TelephonyManager.java)
├── com.example.session.PhoneInfo.getImei(PhoneInfo.java)
├── com.example.logging.ClientIdHelper.initClientId(ClientIdHelper.java)
│ └── com.example.logging.ContextInfo.(ContextInfo.java)
│ ├── com.example.logging.LogStrategyManager.getInstance(LogStrategyManager.java)
│ └── com.example.logging.LogContextImpl.(LogContextImpl.java)
├── com.example.msp.DeviceInfo.k(DeviceInfo.java)
│ └── com.example.msp.DeviceInfo.(DeviceInfo.java)
│ └── com.example.msp.DeviceInfo.getInstance(DeviceInfo.java)
│ ├── com.example.msp.TidHelper.getIMEI(TidHelper.java)
│ ├── com.example.msp.TidHelper.getIMSI(TidHelper.java)
│ └── com.example.msp.DeviceCollector.collectData(DeviceCollector.java)
└── com.example.weex.WXEnvironment.getDevId(WXEnvironment.java)
└── com.example.weex.WXEnvironment.(WXEnvironment.java)
```
### 4. 追踪调用链 — 列表视图(Java crash 风格)
```
dexfinder --dex-file app.apk \
--query "Landroid/telephony/TelephonyManager;->getDeviceId()Ljava/lang/String;" \
--trace --depth 5 --layout list
```
```
--- Call chain #1 for android.telephony.TelephonyManager.getDeviceId() ---
at com.example.session.PhoneInfo.getImei(PhoneInfo.java)
at com.example.aopsdk.TelephonyManager.getDeviceId(TelephonyManager.java)
at android.telephony.TelephonyManager.getDeviceId(TelephonyManager.java)
--- Call chain #2 for android.telephony.TelephonyManager.getDeviceId() ---
at com.example.logging.LogStrategyManager.getInstance(LogStrategyManager.java)
at com.example.logging.ContextInfo.(ContextInfo.java)
at com.example.logging.ClientIdHelper.initClientId(ClientIdHelper.java)
at com.example.aopsdk.TelephonyManager.getDeviceId(TelephonyManager.java)
at android.telephony.TelephonyManager.getDeviceId(TelephonyManager.java)
```
### 5. 以 DEX 签名风格追踪
```
dexfinder --dex-file app.apk --query "getDeviceId" --trace --depth 3 --style dex
```
```
Landroid/telephony/TelephonyManager;->getDeviceId()Ljava/lang/String;
└── TelephonyManager.getDeviceId(Landroid/telephony/TelephonyManager;)Ljava/lang/String;
├── PhoneInfo.getImei(Landroid/content/Context;)Ljava/lang/String;
├── ClientIdHelper.initClientId(Landroid/content/Context;)Ljava/lang/String;
└── DeviceInfo.k(Landroid/content/Context;)V
```
### 6. JSON 输出 — 树状
```
dexfinder --dex-file app.apk --query "getDeviceId" --trace --depth 2 --format json
```
```
{
"targets": [{
"api": "android.telephony.TelephonyManager.getDeviceId()",
"tree": {
"method": "android.telephony.TelephonyManager.getDeviceId(TelephonyManager.java)",
"callers": [
{ "method": "com.example.aopsdk.TelephonyManager.getDeviceId(TelephonyManager.java)",
"callers": [
{ "method": "com.example.session.PhoneInfo.getImei(PhoneInfo.java)" },
{ "method": "com.example.logging.ClientIdHelper.initClientId(ClientIdHelper.java)" }
]}
]
}
}]
}
```
### 7. JSON 输出 — 列表
```
dexfinder --dex-file app.apk --query "getDeviceId" --trace --depth 2 --format json --layout list
```
```
{
"targets": [{
"api": "android.telephony.TelephonyManager.getDeviceId()",
"chains": [
["com.example.session.PhoneInfo.getImei(PhoneInfo.java)",
"com.example.aopsdk.TelephonyManager.getDeviceId(TelephonyManager.java)",
"android.telephony.TelephonyManager.getDeviceId(TelephonyManager.java)"],
["com.example.logging.ClientIdHelper.initClientId(ClientIdHelper.java)",
"com.example.aopsdk.TelephonyManager.getDeviceId(TelephonyManager.java)",
"android.telephony.TelephonyManager.getDeviceId(TelephonyManager.java)"]
]
}]
}
```
### 8. 结构化模型输出(适用于 CI/IDE)
```
dexfinder --dex-file app.apk --query "getDeviceId" --trace --format model | jq '.call_chains[0]'
```
```
{
"target": "Landroid/telephony/TelephonyManager;->getDeviceId()Ljava/lang/String;",
"chain": [
{ "method": { "dex_signature": "...", "class": "...", "name": "getImei",
"param_types": ["Landroid/content/Context;"], "return_type": "Ljava/lang/String;",
"java_readable": "com.example.session.PhoneInfo.getImei(...)" }},
{ "method": { "dex_signature": "...", "java_readable": "...TelephonyManager.getDeviceId(...)" }},
{ "method": { "dex_signature": "...", "java_readable": "...TelephonyManager.getDeviceId(...)" }}
],
"depth": 2
}
```
### 9. ProGuard/R8 映射 — 查询与展示
使用 `--mapping` 后,**输入**和**输出**均支持原始(未混淆的)名称。
**通过原始名称查询 → 自动转换为混淆名称以进行 DEX 搜索:**
```
# 使用原始简单类名进行查询 (mapping 在内部会将 "KotlinCases" 转换为 "LJ7;")
dexfinder --dex-file app.apk --query "KotlinCases" --mapping mapping.txt
# 使用原始 Java 全名进行查询
dexfinder --dex-file app.apk --query "com.example.app.utils.Helper" --mapping mapping.txt
# 使用混淆名称进行查询仍然有效
dexfinder --dex-file app.apk --query "LJ7;" --mapping mapping.txt
```
**在追踪结果中输出反混淆名称:**
```
# 带有反混淆名称的树形追踪
dexfinder --dex-file app.apk --query "KotlinCases" --mapping mapping.txt --trace --depth 3
```
```
com.example.kotlin.KotlinCases$$ExternalSyntheticLambda1.(int)
└── com.example.TestEntry.runAllTests(TestEntry.java)
└── com.example.MainActivity.onCreate(MainActivity.java)
```
**同时显示混淆名称与原始名称:**
```
dexfinder --dex-file app.apk --query "KotlinCases" --mapping mapping.txt --show-obf --trace
```
```
com.example.kotlin.KotlinCases.fetchLocationAsync(KotlinCases.java)
└── com.example.kotlin.KotlinCases$testCoroutines$3.invokeSuspend(KotlinCases.java) [obf: G7.e]
└── com.example.kotlin.KotlinCases$testCoroutines$3.create(KotlinCases.java) [obf: G7.b]
```
**与其他参数的所有组合:**
```
# 原始名称 + 以扁平列表形式追踪
dexfinder --dex-file app.apk --query "KotlinCases" --mapping mapping.txt --trace --layout list
# 原始名称 + DEX 签名样式
dexfinder --dex-file app.apk --query "KotlinCases" --mapping mapping.txt --trace --style dex
# 原始名称 + JSON 树 + show-obf
dexfinder --dex-file app.apk --query "KotlinCases" --mapping mapping.txt --show-obf --trace --format json
# 原始名称 + 反向追踪 (这个类调用了什么?)
dexfinder --dex-file app.apk --query "com.example.kotlin.KotlinCases" --mapping mapping.txt --scope caller
```
**输入 × 输出矩阵:**
| 查询输入 | 无 mapping | `--mapping` | `--mapping --show-obf` |
|---|---|---|---|
| 混淆名: `LJ7;` | ✓ 混淆输出 | ✓ 反混淆输出 | ✓ 两种名称 |
| 原始简名: `KotlinCases` | ✗ 未找到 | ✓ 自动转换,反混淆输出 | ✓ 自动转换,两种名称 |
| 原始全名: `com.example...KotlinCases` | ✗ 未找到 | ✓ 自动转换,反混淆输出 | ✓ 自动转换,两种名称 |
### 10. Hidden API 检测
```
# 下载 CSV (单次)
curl -o hiddenapi-flags.csv \
https://dl.google.com/developers/android/baklava/non-sdk/hiddenapi-flags.csv
# 完整扫描 — 链接 + 反射检测
dexfinder --dex-file app.apk --api-flags hiddenapi-flags.csv
```
```
#1: Linking unsupported Lsun/misc/Unsafe;->allocateInstance(Ljava/lang/Class;)Ljava/lang/Object; use(s):
Lcom/google/gson/internal/UnsafeAllocator;->create()Lcom/google/gson/internal/UnsafeAllocator;
#2: Reflection blocked Landroid/location/ILocationManager;->getCurrentLocation potential use(s):
Lcom/example/monitor/LocationMonitor;->hookSystemLocationManager(Landroid/content/Context;)V
```
### 11. 搜索字符串常量(content:// URI、API 密钥等)
```
# 在代码中查找 content:// URI
dexfinder --dex-file app.apk --query "content://com.android.contacts" --scope string
# 仅包含 DEX 表中的字符串 (被 R8、annotations 等优化掉的内容)
dexfinder --dex-file app.apk --query "content://com.android.contacts" --scope everything
```
```
[STRING] "content://com.android.contacts/" (1 ref)
Lcom/example/imageloader/BaseImageDownloader;->getStreamFromContent(Ljava/lang/String;)Ljava/io/InputStream;
[STRING_TABLE] "content://com.android.contacts" (in DEX string table, no code reference found)
```
### 12. 按类名前缀过滤
```
# 仅扫描您自己包中的类
dexfinder --dex-file app.apk --query "getDeviceId" --class-filter "Lcom/mycompany/"
# 扫描多个包
dexfinder --dex-file app.apk --query "getDeviceId" --class-filter "Lcom/mycompany/,Lcom/mylib/"
```
### 13. 组合所有参数
```
# 位置 API 使用情况的反混淆 JSON 树,已过滤至您的代码
dexfinder --dex-file app.apk \
--query "android.location.LocationManager#requestLocationUpdates" \
--trace --depth 8 \
--format json --layout tree --style java \
--mapping mapping.txt --show-obf \
--class-filter "Lcom/mycompany/"
```
## 性能
在 Apple M 系列芯片上基准测试,单线程:
| APK 大小 | DEX 文件数 | 类数量 | 方法引用数 | 扫描 | Hidden API |
|---|---|---|---|---|---|
| ~1 MB | 1 | ~2K | ~18K | **24ms** | — |
| ~10 MB | 2 | ~25K | ~100K | **335ms** | — |
| ~300 MB | 30+ | ~180K | ~1.2M | **3.9s** | **5.4s** |
在同一个约 300MB 的 APK 上与 veridex (C++, 不精确模式) 相比:
- veridex precise: **27s**(无法通过 Binder/AIDL 检测反射)
- veridex imprecise: **>32 分钟**(被 kill,笛卡尔积爆炸)
- **dexfinder: 5.4s**(反向索引优化)
## 所有选项
| 参数 | 描述 | 默认值 |
|---|---|---|
| `--dex-file` | 要分析的 APK/DEX/JAR 文件 **(必需)** | — |
| `--query` | 搜索关键字(Java、DEX/JNI 或简单名称) | — |
| `--trace` | 启用调用链追踪(需要 `--query`) | `false` |
| `--depth` | 最大调用链深度 | `5` |
| `--layout` | 追踪布局: `tree` 或 `list` | `tree` |
| `--style` | 命名风格: `java` 或 `dex` | `java` |
| `--format` | 输出格式: `text`、`json`、`model` | `text` |
| `--mapping` | ProGuard/R8 mapping.txt 路径 | — |
| `--show-obf` | 在反混淆名称旁显示混淆名称 | `false` |
| `--api-flags` | hiddenapi-flags.csv 路径 | — |
| `--class-filter` | 逗号分隔的类描述符前缀 | — |
| `--exclude-api-lists` | 要从报告中排除的 API 列表 | — |
| `--scope` | 搜索范围: `all`、`callee`、`caller`、`string`、`string-table`、`everything` | `all` |
| `--stats` | 仅显示统计摘要 | `false` |
| `--version` | 显示版本 | `false` |
## 从源码构建
```
git clone https://github.com/JuneLeGency/dexfinder.git
cd dexfinder
go build -o dexfinder ./cmd/dexfinder/
go test ./...
```
## 许可证
Apache License 2.0
# dexfinder
跨平台 APK/DEX 方法与字段引用查找器,支持调用链追踪、ProGuard/R8 反混淆以及 Android Hidden API 检测。
基于 Android [veridex](https://android.googlesource.com/platform/art/+/refs/heads/master/tools/veridex/) 原理,使用 Go 重新实现并增强:更快的反射检测、调用链追踪(veridex 仅展示一层)以及灵活的输出格式。
## 特性
- **APK/DEX/JAR 扫描** — 解析 DEX 字节码,提取所有方法/字段/字符串引用
- **多格式查询** — 支持通过 Java 名称、DEX/JNI 签名或简单关键字进行搜索
- **调用链追踪** — 向上追溯最多 N 层调用者,支持合并树或平铺列表展示,并自动检测环路
- **ProGuard/R8 反混淆** — 加载 mapping.txt,显示原始名称
- **Hidden API 检测** — 加载 hiddenapi-flags.csv,检测被限制/不支持的 API
- **反射检测** — 类名与字符串交叉匹配,发现基于反射调用的 Hidden API(兼容 veridex)
- **灵活输出** — text / json / model 格式,tree / list 布局,java / dex 命名风格——可自由组合
- **零外部依赖** — 纯 Go 实现,自包含 DEX 解析器
- **跨平台** — macOS (Intel / Apple Silicon)、Linux (amd64 / arm64)、Windows
## 安装
**Homebrew** (macOS / Linux):
```
brew tap JuneLeGency/tap
brew install dexfinder
```
**脚本** (自动检测操作系统/架构):
```
curl -sSL https://raw.githubusercontent.com/JuneLeGency/dexfinder/main/install.sh | bash
```
**Go install**:
```
go install github.com/JuneLeGency/dexfinder/cmd/dexfinder@latest
```
**二进制文件**: 从 [Releases](https://github.com/JuneLeGency/dexfinder/releases) 下载。
## 快速开始
```
# 查看 APK 概况
dexfinder --dex-file app.apk --stats
# 查找所有 getDeviceId 调用(获取 IMEI)
dexfinder --dex-file app.apk --query "getDeviceId"
# 追踪调用链(合并树形视图)
dexfinder --dex-file app.apk --query "getDeviceId" --trace
# 追踪调用链(展开为独立调用栈)
dexfinder --dex-file app.apk --query "getDeviceId" --trace --layout list
# 用精确 JNI 签名查询
dexfinder --dex-file app.apk \
--query "Landroid/telephony/TelephonyManager;->getDeviceId()Ljava/lang/String;" \
--trace --depth 8
```
##查询格式 (`--query`)
| 格式 | 示例 | 行为 |
|---|---|---|
| 简单名称 | `getDeviceId` | 模糊子串匹配 |
| Java 类名 | `android.telephony.TelephonyManager` | 匹配该类的所有方法 |
| Java 类名#方法名 | `...TelephonyManager#getDeviceId` | 匹配该方法的所有重载 |
| Java 完整签名 | `...#getDeviceId()` | 精确匹配 + 重载回退 |
| DEX/JNI 签名 | `Landroid/telephony/TelephonyManager;->getDeviceId()Ljava/lang/String;` | 仅精确匹配 |
## 输出控制
三个独立的维度,可自由组合:
```
--format (text / json / model) 输出什么
--layout (tree / list) 怎么排列调用链
--style (java / dex) 怎么显示名称
```
### `--layout` 对比(配合 `--trace` 使用)
**tree** — 合并共同路径,用一棵树展示全貌:
```
android.telephony.TelephonyManager.getDeviceId()
└── ...aopsdk...TelephonyManager.getDeviceId(TelephonyManager.java)
├── PhoneInfo.getImei(PhoneInfo.java)
├── ClientIdHelper.initClientId(ClientIdHelper.java)
│ └── ContextInfo.(ContextInfo.java)
└── DeviceInfo.k(DeviceInfo.java)
└── DeviceInfo.getInstance(DeviceInfo.java)
├── TidHelper.getIMEI(TidHelper.java)
└── DeviceCollector.collectData(DeviceCollector.java)
```
**list** — 每条链独立展示(Java crash 风格):
```
--- Call chain #1 ---
at PhoneInfo.getImei(PhoneInfo.java)
at ...aopsdk...TelephonyManager.getDeviceId(TelephonyManager.java)
at android.telephony.TelephonyManager.getDeviceId(TelephonyManager.java)
--- Call chain #2 ---
at ContextInfo.(ContextInfo.java)
at ClientIdHelper.initClientId(ClientIdHelper.java)
at ...aopsdk...TelephonyManager.getDeviceId(TelephonyManager.java)
at android.telephony.TelephonyManager.getDeviceId(TelephonyManager.java)
```
### `--style` 对比
**java** (默认): `com.example.Foo.method(Foo.java)`
**dex**: `Foo.method(Ljava/lang/String;)V`
### JSON 输出
```
# JSON 树
dexfinder --dex-file app.apk --query "getDeviceId" --trace --format json
# JSON 列表
dexfinder --dex-file app.apk --query "getDeviceId" --trace --format json --layout list
```
### `--scope` 搜索范围
控制查询匹配**哪种引用类型**。理解此参数对于正确解读结果至关重要。
| 值 | 搜索内容 | 回答的问题 | 输出标签 |
|---|---|---|---|
| `all` | 被调用的 API + 字段 + 代码字符串 | “谁调用了这个方法?”(默认) | `[METHOD]` `[FIELD]` `[STRING]` |
| `callee` | 仅 `invoke-*` / `get/put` 指令中的目标签名 | “谁调用了这个具体的方法/字段?” | `[METHOD]` `[FIELD]` |
| `caller` | 仅调用方方法的签名 | “这个方法内部调用了什么?” | `[CALLER→]` |
| `string` | `const-string` 指令中的字符串常量 | “这个字符串在代码中哪里被使用了?” | `[STRING]` |
| `string-table` | 代码字符串 + 完整的 DEX 字符串表 | “这个字符串是否存在于 DEX 中?”(包含注解、死代码) | `[STRING]` `[STRING_TABLE]` |
| `everything` | 以上所有组合 | 完整视图 | 所有标签 |
**callee 与 caller 的区别:**
```
scope=callee: "谁调了 finish()?"
onCreate ──调用──→ finish() ← 显示这些调用者
onResume ──调用──→ finish()
scope=caller: "finish() 内部调了什么?"
finish() ──调用──→ Log.i() ← 显示这些被调用者
finish() ──调用──→ super.finish()
```
`--scope=all`(默认)= `callee` + `string`。`caller` 方向被故意排除在默认值之外,因为它回答的是一个完全不同的问题。需要时请显式使用 `--scope=caller` 或 `--scope=everything`。
**输出标签含义:**
| 标签 | 含义 |
|---|---|
| `[METHOD]` | 你搜索的方法**被调用了**。缩进行是调用方。 |
| `[FIELD]` | 你搜索的字段**被访问了**。缩进行是访问者。 |
| `[CALLER→]` | 你搜索的方法名出现在某个**调用方**中,缩进行显示它调用了什么 API。 |
| `[STRING]` | 代码中的字符串常量匹配。缩进行是使用该字符串的方法。 |
| `[STRING_TABLE]` | 字符串仅存在于 DEX 字符串表中,代码里没有 `const-string` 引用(可能在注解中,或被 R8 优化掉等)。 |
## 更多用法
### 反混淆(--mapping)
加载 `--mapping` 后,**输入和输出**均支持原始(未混淆的)名称。
**使用原始名称查询 → 自动转换为混淆名称搜索 DEX:**
```
# 用原始简短类名查(mapping 内部将 "KotlinCases" 转为 "LJ7;")
dexfinder --dex-file app.apk --query "KotlinCases" --mapping mapping.txt
# 用原始 Java 全名查
dexfinder --dex-file app.apk --query "com.example.app.utils.Helper" --mapping mapping.txt
# 用混淆名查也正常工作
dexfinder --dex-file app.apk --query "LJ7;" --mapping mapping.txt
```
**输出反混淆名称:**
```
# trace 树形 + 反混淆
dexfinder --dex-file app.apk --query "KotlinCases" --mapping mapping.txt --trace
```
**同时显示混淆名称与原始名称:**
```
dexfinder --dex-file app.apk --query "KotlinCases" --mapping mapping.txt --show-obf --trace
```
```
com.example.KotlinCases.fetchLocationAsync(KotlinCases.java)
└── com.example.KotlinCases$testCoroutines$3.invokeSuspend(KotlinCases.java) [obf: G7.e]
```
**与其他参数自由组合:**
```
# 原始名 + 展开列表
dexfinder --dex-file app.apk --query "KotlinCases" --mapping mapping.txt --trace --layout list
# 原始名 + DEX 签名风格
dexfinder --dex-file app.apk --query "KotlinCases" --mapping mapping.txt --trace --style dex
# 原始名 + JSON 树 + 显示混淆名
dexfinder --dex-file app.apk --query "KotlinCases" --mapping mapping.txt --show-obf --trace --format json
# 原始名 + 反向查看(这个类内部调了什么)
dexfinder --dex-file app.apk --query "com.example.KotlinCases" --mapping mapping.txt --scope caller
```
**输入×输出矩阵:**
| 查询输入 | 无 mapping | `--mapping` | `--mapping --show-obf` |
|---|---|---|---|
| 混淆名 `LJ7;` | ✓ 混淆输出 | ✓ 反混淆输出 | ✓ 两者并列 |
| 原始简名 `KotlinCases` | ✗ 未找到 | ✓ 自动转换 + 反混淆输出 | ✓ 自动转换 + 两者并列 |
| 原始全名 `com.example...` | ✗ 未找到 | ✓ 自动转换 + 反混淆输出 | ✓ 自动转换 + 两者并列 |
### Hidden API 检测
```
# 下载 CSV(一次性)
curl -o hiddenapi-flags.csv \
https://dl.google.com/developers/android/baklava/non-sdk/hiddenapi-flags.csv
# 全量检测(直接链接 + 反射检测)
dexfinder --dex-file app.apk --api-flags hiddenapi-flags.csv
```
### 字符串搜索
```
# 搜索代码中的 content:// URI
dexfinder --dex-file app.apk --query "content://com.android.contacts" --scope string
# 包含被 R8 优化掉的字符串(注解、死代码等)
dexfinder --dex-file app.apk --query "content://com.android.contacts" --scope everything
```
### 按类前缀过滤
```
# 只扫描自己的代码
dexfinder --dex-file app.apk --query "getDeviceId" --class-filter "Lcom/mycompany/"
```
### 组合使用
```
# 反混淆 + JSON 树形输出 + 定位 API 调用 + 过滤自己的代码
dexfinder --dex-file app.apk \
--query "android.location.LocationManager#requestLocationUpdates" \
--trace --depth 8 \
--format json --layout tree --style java \
--mapping mapping.txt --show-obf \
--class-filter "Lcom/mycompany/"
```
## 性能
Apple M 系列芯片,单线程:
| APK 大小 | DEX 文件数 | 类数 | 方法引用数 | 扫描 | Hidden API |
|---|---|---|---|---|---|
| ~1 MB | 1 | ~2K | ~18K | **24ms** | — |
| ~10 MB | 2 | ~25K | ~100K | **335ms** | — |
| ~300 MB | 30+ | ~180K | ~1.2M | **3.9s** | **5.4s** |
与 veridex (C++) 在同一约 300MB APK 上对比:
- veridex precise: **27s**(无法追踪 Binder/AIDL 反射)
- veridex imprecise: **>32 分钟**(笛卡尔积爆炸,被 kill)
- **dexfinder: 5.4s**(反向索引优化)
## 所有参数
| 参数 | 说明 | 默认值 |
|---|---|---|
| `--dex-file` | APK/DEX/JAR 文件路径 **(必需)** | — |
| `--query` | 搜索关键字(Java / DEX/JNI / 简单名称) | — |
| `--trace` | 启用调用链追踪(需配合 `--query`) | `false` |
| `--depth` | 调用链最大深度 | `5` |
| `--layout` | 追踪布局: `tree`(合并树)或 `list`(平铺列表) | `tree` |
| `--style` | 命名风格: `java`(易读)或 `dex`(JNI 签名) | `java` |
| `--format` | 输出格式: `text`、`json`、`model` | `text` |
| `--mapping` | ProGuard/R8 mapping.txt 路径 | — |
| `--show-obf` | 同时显示混淆名称与反混淆名称 | `false` |
| `--api-flags` | hiddenapi-flags.csv 路径 | — |
| `--class-filter` | 类描述符前缀过滤(逗号分隔) | — |
| `--exclude-api-lists` | 要排除的 API 级别列表 | — |
| `--scope` | 搜索范围: `all`、`callee`、`caller`、`string`、`string-table`、`everything` | `all` |
| `--stats` | 仅显示统计摘要 | `false` |
| `--version` | 显示版本号 | `false` |
## 从源码构建
```
git clone https://github.com/JuneLeGency/dexfinder.git
cd dexfinder
go build -o dexfinder ./cmd/dexfinder/
go test ./...
```
## 许可证
Apache License 2.0
跨平台 APK/DEX 方法与字段引用查找器,支持调用链追踪、ProGuard/R8 反混淆以及 Android Hidden API 检测。
受 Android [veridex](https://android.googlesource.com/platform/art/+/refs/heads/master/tools/veridex/) 工具启发,使用 Go 重新实现并增强了功能:更快的反射检测、调用链追踪(veridex 仅展示一层)以及灵活的输出格式。
## 特性
- **APK/DEX/JAR 扫描** — 解析 DEX 字节码,提取所有方法/字段/字符串引用
- **多格式查询** — 支持通过 Java 名称、DEX/JNI 签名或简单关键字进行搜索
- **调用链追踪** — 向上追溯最多 N 层调用者,支持合并树或平铺列表展示,并带有环路检测
- **ProGuard/R8 反混淆** — 加载 mapping.txt,将原始名称与混淆名称一并显示
- **Hidden API 检测** — 加载 hiddenapi-flags.csv,检测被限制/不支持的 API
- **反射检测** — 通过交叉匹配类名与字符串,发现基于反射的 Hidden API 使用情况
- **灵活输出** — text / json / model 格式,tree / list 布局,java / dex 命名风格——皆可自由组合
- **零外部依赖** — 纯 Go 实现,内置自包含的 DEX 解析器
- **跨平台** — macOS (Intel / Apple Silicon)、Linux (amd64 / arm64)、Windows
## 安装
**Homebrew** (macOS / Linux):
```
brew tap JuneLeGency/tap
brew install dexfinder
```
**脚本** (自动检测操作系统/架构):
```
curl -sSL https://raw.githubusercontent.com/JuneLeGency/dexfinder/main/install.sh | bash
```
**Go install**:
```
go install github.com/JuneLeGency/dexfinder/cmd/dexfinder@latest
```
**二进制文件**: 从 [Releases](https://github.com/JuneLeGency/dexfinder/releases) 下载。
## 快速开始
```
# 显示 APK 概览
dexfinder --dex-file app.apk --stats
# 查找所有调用 getDeviceId (IMEI) 的位置
dexfinder --dex-file app.apk --query "getDeviceId"
# 将调用链追踪为合并树
dexfinder --dex-file app.apk --query "getDeviceId" --trace
# 追踪为扁平调用栈 (Java crash 样式)
dexfinder --dex-file app.apk --query "getDeviceId" --trace --layout list
# 精确的 JNI 签名查询
dexfinder --dex-file app.apk \
--query "Landroid/telephony/TelephonyManager;->getDeviceId()Ljava/lang/String;" \
--trace --depth 8
# 隐藏 API 检测
dexfinder --dex-file app.apk --api-flags hiddenapi-flags.csv
```
## 查询格式
`--query` 参数接受多种输入格式。dexfinder 会自动检测并在它们之间进行转换。
| 格式 | 示例 | 行为 |
|---|---|---|
| 简单名称 | `getDeviceId` | 跨所有 API 的模糊子串匹配 |
| Java 类名 | `android.telephony.TelephonyManager` | 该类的所有方法/字段 |
| Java 类名#方法 | `android.telephony.TelephonyManager#getDeviceId` | 该方法的所有重载 |
| Java 完整签名 | `...TelephonyManager#getDeviceId()` | 精确匹配 + 重载回退 |
| DEX/JNI 签名 | `Landroid/telephony/TelephonyManager;->getDeviceId()Ljava/lang/String;` | 仅精确匹配 |
```
# 所有等效方式 — 在 LocationManager 中查找 requestLocationUpdates:
dexfinder --dex-file app.apk --query "requestLocationUpdates"
dexfinder --dex-file app.apk --query "android.location.LocationManager#requestLocationUpdates"
dexfinder --dex-file app.apk --query "Landroid/location/LocationManager;->requestLocationUpdates(Ljava/lang/String;JFLandroid/location/LocationListener;)V"
```
## 输出控制
三个独立的控制维度,可自由组合:
```
--format (text / json / model) what to output
--layout (tree / list) how to arrange traces
--style (java / dex) how to display names
```
### `--format`
| 值 | 描述 |
|---|---|
| `text` | 纯文本输出(默认) |
| `json` | JSON — 扫描结果或采用 tree/list 布局的追踪结果 |
| `model` | 包含完整 MethodInfo/FieldInfo 类型的结构化 JSON(适用于 IDE/CI) |
### `--layout` (与 `--trace` 配合使用)
| 值 | 描述 |
|---|---|
| `tree` | 合并树 — 将共享调用路径折叠成一棵树(默认) |
| `list` | 平铺列表 — 每条唯一调用链作为独立的堆栈展示 |
### `--style`
| 值 | 示例 | 用例 |
|---|---|---|
| `java` | `com.example.Foo.method(Foo.java)` | 人类可读(默认) |
| `dex` | `Foo.method(Ljava/lang/String;)V` | 精确签名分析 |
### `--scope` (搜索范围)
控制查询匹配**哪种引用类型**。这对于理解结果至关重要。
| 值 | 搜索内容 | 回答的问题 | 输出标签 |
|---|---|---|---|
| `all` | 被调用的 API + 字段 + 代码字符串 | “谁调用了这个 API?”(默认) | `[METHOD]` `[FIELD]` `[STRING]` |
| `callee` | 仅 `invoke-*` / `get/put` 指令中的目标 API 签名 | “谁调用了这个具体的方法/字段?” | `[METHOD]` `[FIELD]` |
| `caller` | 仅调用方方法的签名 | “这个方法内部调用了什么?” | `[CALLER→]` |
| `string` | `const-string` 指令中的字符串常量 | “这个字符串在代码中哪里被使用了?” | `[STRING]` |
| `string-table` | 代码字符串 + 完整的 DEX 字符串表 | “这个字符串是否存在于 DEX 中的任何位置?”(包括注解、死代码) | `[STRING]` `[STRING_TABLE]` |
| `everything` | 以上所有组合 | 全局视图 | 所有标签 |
**理解 callee 与 caller 的区别:**
```
scope=callee: "Who calls finish()?"
onCreate ──calls──→ finish() ← these callers are shown
onResume ──calls──→ finish()
scope=caller: "What does finish() call internally?"
finish() ──calls──→ Log.i() ← these callees are shown
finish() ──calls──→ super.finish()
```
`--scope=all`(默认)= `callee` + `string`。`caller` 方向被有意排除在默认值之外,因为它回答的是一个根本不同的问题。当你需要时,请显式使用 `--scope=caller` 或 `--scope=everything`。
**理解输出标签:**
| 标签 | 含义 |
|---|---|
| `[METHOD]` | **被调用的**方法与你的查询匹配(被调用方匹配)。缩进行是调用方。 |
| `[FIELD]` | **被访问的**字段与你的查询匹配。缩进行是访问者。 |
| `[CALLER→]` | **调用方方法**与你的查询匹配。缩进行显示它正在调用的 API。 |
| `[STRING]` | 代码中的字符串常量与你的查询匹配。缩进行是使用它的位置。 |
| `[STRING_TABLE]` | 字符串存在于 DEX 字符串表中,但在代码中没有 `const-string` 引用(可能在注解中,或被 R8 优化掉等) |
## 示例
### 1. 扫描 APK 统计信息
```
dexfinder --dex-file app.apk --stats
```
```
Loaded 31 DEX file(s): 183913 classes, 1250566 method refs
Method references: 680610
Field references: 625572
String constants: 654353
Referenced types: 192586
Time: 3.9s
```
### 2. 查找所有位置追踪调用
```
dexfinder --dex-file app.apk --query "requestLocationUpdates"
```
```
[METHOD] Landroid/location/LocationManager;->requestLocationUpdates(Ljava/lang/String;JFLandroid/location/LocationListener;)V (3 ref)
Lcom/example/TestEntry;->init(Landroid/content/Context;)V (2 occurrences)
Lcom/example/service/LocationService;->onStartCommand(Landroid/content/Intent;II)I
```
### 3. 追踪调用链 — 树状视图
```
dexfinder --dex-file app.apk \
--query "Landroid/telephony/TelephonyManager;->getDeviceId()Ljava/lang/String;" \
--trace --depth 5
```
```
android.telephony.TelephonyManager.getDeviceId()
└── com.example.aopsdk.TelephonyManager.getDeviceId(TelephonyManager.java)
├── com.example.session.PhoneInfo.getImei(PhoneInfo.java)
├── com.example.logging.ClientIdHelper.initClientId(ClientIdHelper.java)
│ └── com.example.logging.ContextInfo.标签:Android安全, Android逆向, APK分析, DEX文件解析, EVTX分析, Go语言, HTTP 参数枚举, Linux安全, ProGuard去混淆, R8去混淆, URL发现, 云安全监控, 云资产清单, 代码混淆, 反射检测, 安全检测, 方法查找, 日志审计, 目录枚举, 移动安全, 程序破解, 调用链追踪, 软件开发, 逆向工程, 隐藏API检测, 静态分析