Cross-platform APK/DEX method & field reference finder with call chain tracing, ProGuard/R8 deobfuscation, and Android hidden API detection.
Inspired by Android's veridex tool, reimplemented in Go with enhanced capabilities: faster reflection detection, call chain tracing (veridex only shows one level), and flexible output formats.
--fail-on blocked exits non-zero when restricted APIs are found.dexfinder.yaml for project defaults, CLI flags overrideHomebrew (macOS / Linux):
brew tap JuneLeGency/tap
brew install dexfinder
Script (auto-detects OS/arch):
curl -sSL https://raw.githubusercontent.com/JuneLeGency/dexfinder/main/install.sh | bash
Go install:
go install github.com/JuneLeGency/dexfinder/cmd/dexfinder@latest
Binary: download from Releases.
# Show APK overview
dexfinder --dex-file app.apk --stats
# Find all calls to getDeviceId (IMEI)
dexfinder --dex-file app.apk --query "getDeviceId"
# Trace call chains as merged tree
dexfinder --dex-file app.apk --query "getDeviceId" --trace
# Trace as flat call stacks (Java crash style)
dexfinder --dex-file app.apk --query "getDeviceId" --trace --layout list
# Exact JNI signature query
dexfinder --dex-file app.apk \
--query "Landroid/telephony/TelephonyManager;->getDeviceId()Ljava/lang/String;" \
--trace --depth 8
# Hidden API detection
dexfinder --dex-file app.apk --api-flags hiddenapi-flags.csv
The --query flag accepts multiple input styles. dexfinder auto-detects and converts between them.
| Format | Example | Behavior |
|---|---|---|
| Simple name | getDeviceId |
Fuzzy substring match across all APIs |
| Java class | android.telephony.TelephonyManager |
All methods/fields of that class |
| Java class#method | android.telephony.TelephonyManager#getDeviceId |
All overloads of that method |
| Java full signature | ...TelephonyManager#getDeviceId() |
Exact + overload fallback |
| DEX/JNI signature | Landroid/telephony/TelephonyManager;->getDeviceId()Ljava/lang/String; |
Exact match only |
# All equivalent — find requestLocationUpdates in LocationManager:
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"
Three independent axes, freely combinable:
--format (text / json / model / html / sarif) what to output
--layout (tree / list) how to arrange traces
--style (java / dex) how to display names
--color (auto / always / never) terminal colors
--format| Value | Description |
|---|---|
text |
Plain text output with colored tags (default) |
json |
JSON — scan results or trace with tree/list layout |
model |
Structured JSON with full MethodInfo/FieldInfo types (for IDE/CI) |
html |
Self-contained HTML report with collapsible trees and search |
sarif |
SARIF 2.1.0 static analysis format (GitHub / VS Code) |
--layout (used with --trace)| Value | Description |
|---|---|
tree |
Merged tree — shared call paths collapsed into one tree (default) |
list |
Flat list — each unique call chain shown as independent stack |
--style| Value | Example | Use case |
|---|---|---|
java |
com.example.Foo.method(Foo.java) |
Human-readable (default) |
dex |
Foo.method(Ljava/lang/String;)V |
Precise signature analysis |
--scope (search scope)Controls what kind of references the query matches against. This is critical for understanding results.
| Value | What it searches | Question it answers | Output tag |
|---|---|---|---|
all |
Callee APIs + fields + code strings | "Who calls this API?" (default) | [METHOD] [FIELD] [STRING] |
callee |
Only target API signatures in invoke-* / get/put instructions |
"Who calls this specific method/field?" | [METHOD] [FIELD] |
caller |
Only the calling method's signature | "What does this method call internally?" | [CALLER→] |
string |
String constants in const-string instructions |
"Where is this string used in code?" | [STRING] |
string-table |
Code strings + full DEX string table | "Does this string exist anywhere in DEX?" (includes annotations, dead code) | [STRING] [STRING_TABLE] |
everything |
All of the above combined | Full picture | all tags |
Understanding callee vs 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 (default) = callee + string. The caller direction is intentionally excluded from default because it answers a fundamentally different question. Use --scope=caller or --scope=everything explicitly when you need it.
Understanding output tags:
| Tag | Meaning |
|---|---|
[METHOD] |
A method being called matches your query (callee match). Indented lines are the callers. |
[FIELD] |
A field being accessed matches your query. Indented lines are the accessors. |
[CALLER→] |
A calling method matches your query. The indented line shows what API it's calling. |
[STRING] |
A string constant in code matches your query. Indented lines are where it's used. |
[STRING_TABLE] |
String exists in DEX string table but has no const-string reference in code (may be in annotations, optimized out by R8, etc.) |
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
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
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.<init>(ContextInfo.java)
│ ├── com.example.logging.LogStrategyManager.getInstance(LogStrategyManager.java)
│ └── com.example.logging.LogContextImpl.<init>(LogContextImpl.java)
├── com.example.msp.DeviceInfo.k(DeviceInfo.java)
│ └── com.example.msp.DeviceInfo.<init>(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.<clinit>(WXEnvironment.java)
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.<init>(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)
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
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)" }
]}
]
}
}]
}
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)"]
]
}]
}
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
}
With --mapping, both input and output support original (unobfuscated) names.
Query by original name → auto-converts to obfuscated name for DEX search:
# Query with original simple class name (mapping converts "KotlinCases" → "LJ7;" internally)
dexfinder --dex-file app.apk --query "KotlinCases" --mapping mapping.txt
# Query with original Java full name
dexfinder --dex-file app.apk --query "com.example.app.utils.Helper" --mapping mapping.txt
# Query with obfuscated name still works
dexfinder --dex-file app.apk --query "LJ7;" --mapping mapping.txt
Output deobfuscated names in trace:
# Tree trace with deobfuscated names
dexfinder --dex-file app.apk --query "KotlinCases" --mapping mapping.txt --trace --depth 3
com.example.kotlin.KotlinCases$$ExternalSyntheticLambda1.<init>(int)
└── com.example.TestEntry.runAllTests(TestEntry.java)
└── com.example.MainActivity.onCreate(MainActivity.java)
**
$ claude mcp add dexfinder \
-- python -m otcore.mcp_server <graph>