droidsaw
Health Pass
- License — License: BSD-3-Clause
- Description — Repository has a description
- Active repo — Last push 0 days ago
- Community trust — 18 GitHub stars
Code Pass
- Code scan — Scanned 12 files during light audit, no dangerous patterns found
Permissions Pass
- Permissions — No dangerous permissions requested
No AI report is available for this listing yet.
Pure-Rust Android decompiler and security-audit suite. DEX → Java, Hermes → JavaScript. Cross-layer taint across the React Native bridge. CycloneDX SBOM + OpenVEX. CLI and MCP. Bytecode is not a security layer.
droidsaw
Art by pmjv_prahou. A gift to the last mage of the Open clan.
droidsaw takes a DEX file or Hermes bundle apart and puts it back together byte-for-byte — 5,767 DEX files from F-Droid recovered bit-identically under preservation mode, Hermes bytecode round-trip verified on v84, v96, v98, and v99. That test fails loudly when the format model has a hole: a string-table offset wrong by one, an alignment requirement missed, a padding byte forgotten — the re-emitted bytes diverge and the test says where. Most Android RE tooling works per-layer; droidsaw traces a JS value through the React Native bridge into Java as a single taint path. Hand it an APK and it unpacks the container, decompiles every layer, and can pipe the output through Semgrep and TruffleHog in the same pass (audit --mode=full). CLI and MCP server share one command surface. Pure Rust, BSD-3-Clause.
Install
cargo install droidsaw # the CLI
cargo install droidsaw --features mcp # also installs the droidsaw-mcp server
cargo fetches the droidsaw-* library crates from crates.io and compiles everything locally. The
only prerequisites are a Rust toolchain (rustup) and a C compiler (cc/clang).semgrep and trufflehog are optional (for audit --mode=full, --mode=semgrep, or --mode=trufflehog); YARA is built in. No
Java/Android SDK is needed.
Installing from automation or CI? Use cargo install --locked droidsaw — it builds against the
dependency versions pinned in the publish-time lockfile, so the same release installs reproducibly.
For fleet or CI installs, prefer the cargo auditable
variant — it embeds the full crate dependency list inside the binary itself (a compressed JSON
record in a dedicated linker section), so the artifact stays auditable long after the build
environment is gone:
cargo install cargo-auditable cargo-audit # one-time tooling
cargo auditable install --locked droidsaw # binary carries its own dependency inventory
cargo audit bin $(which droidsaw) # audit the installed artifact, no manifest needed
cargo audit bin reads the embedded inventory and checks it against the RustSec advisory
database — the binary answers "what exactly is in you?" years later, with nothing but the file.
Working on droidsaw itself? Check out the
droidsawrepo and the fivedroidsaw-*library repos as
siblings, then builddroidsaw/— its dev-loop[patch.crates-io]wires in the local../droidsaw-*
crates instead of crates.io.
Quickstart
droidsaw info app.apk # layer summary: bytecode + manifest + signing
droidsaw audit app.apk --mode=basic # fast hermetic audit (no subprocesses)
droidsaw audit app.apk --mode=basic --format sarif # SARIF 2.1.0 for code scanning
droidsaw manifest app.apk | jq '.manifest.exported_components'
droidsaw decompile app.apk com.example.Foo # DEX class → Java
droidsaw decompile app.apk 42 # Hermes function index → JS
droidsaw xrefs app.apk --search 'api_secret|Bearer' # who references this string?
Every command composes with jq; worked per-audience flows are in Playbooks below.
Inputs & output
Inputs: APK, XAPK, .hbc, .dex. Hermes bundles and DEX files are extracted from an APK automatically; co-located split_config.*.apk siblings are auto-merged (--no-auto-splits to disable).
Output: stdout is one JSON object, array, or NDJSON stream — nothing else. The documented plain-text exemptions: --version and --help (clap convention), hbc disassemble (instruction stream for differential tooling), scan trufflehog (raw string feed), and decompile --all --js (concatenated JS). Progress goes to stderr prefixed droidsaw: .
Exit codes: 0 success · 1 reserved for the opt-in audit --fail-on=<severity> gate (the audit completed, stdout carries the normal audit output, and at least one emitted finding is at or above the threshold — pure exit-code CI gating, no jq required) · 2 failure. Every failure — including a mistyped flag — is a typed JSON error envelope on stdout:
{
"error": {
"code": "USER_INPUT | PERMISSION | TRANSIENT | CONFIGURATION | INTERNAL",
"operation": "audit",
"message": "no such file: app.apk",
"hint": "verify the path points to a readable APK/DEX/HBC file"
}
}
Repeated runs on the same input produce bit-identical output.
What it finds
Patterns surfaced across a corpus of production apps spanning dating, social, fintech, banking, and health:
- Key material in the JS heap — private-key operations running in Hermes with no native boundary, no memory zeroing. Visible because droidsaw reads the Hermes layer.
- Signing-chain weaknesses at the cryptographic level — ROCA fingerprint (CVE-2017-15361), Fermat-factorable close primes (returns
(p, q)), batch-GCD shared-prime recovery across a corpus (Bernstein quasilinear), Wiener-regime exponent fingerprint (e > N^0.75; full recovery). - Data crossing the React Native bridge into sinks it shouldn't reach — JS network input landing in
Runtime.exec(), analytics SDKs receiving health or financial signals. Reported as a single taint path from JS source to Java sink. - Exported components without permission guards — activities and services reachable from any app, leaking auth tokens, OAuth codes, refresh tokens.
- Staging infrastructure surviving release — internal endpoints, debug flags, hardcoded credentials across the manifest, DEX string pool, and Hermes global string table.
Cross-referencing is for understanding what an app does: trace a suspicious string to every function that references it, then decompile those functions to map the dispatch surface. A worked example on a stalkerware sample: Cerberus Anti-theft: Stalkerware RE.
Commands
| Command | Output |
|---|---|
audit |
Security audit (--mode=<basic|full|semgrep|trufflehog>) |
decompile |
Decompile DEX classes to Java or Hermes functions to JavaScript |
xrefs |
Cross-reference a string to every function that references it (DEX + HBC). "Who references this key?" is a query, not a grep. |
manifest |
AndroidManifest.xml analysis |
signing |
v1 / v2 / v3 / v4 signing block analysis |
strings |
Search strings across all layers (--layer dex|hbc|native, --search <re>) |
frida |
Generate Frida hook stubs for functions matching a string pattern |
diff |
Structural diff of two Hermes bundles (accepts APK or .hbc) |
deobf-strings |
Recover obfuscated strings by emulating a DEX method over argument sets |
info |
Container + bytecode layer summary: layers + manifest + signing |
inspect |
Container internals: entries (ZIP + anomalies), elf, resources, webview |
hbc |
Hermes subcommands (info, functions, strings, decompile, disassemble) |
dex |
DEX subcommands (classes, methods, strings) |
Cross-layer taint paths (JS → bridge → Java → sink) are produced by audit, not a standalone command — see Cross-layer taint.
Further commands are grouped under umbrellas: scan <yara|sbom|trufflehog|semgrep|export>, inspect <entries|elf|resources|webview>, corpus <ingest|scan>, triage promote. Global flags cap resources per parse: --budget-mem, --budget-time, --single-thread (deterministic), --permissive-recovery (tolerant AXML). Run droidsaw --help or droidsaw <cmd> --help for the full surface.
Playbooks
Concrete flows for the four people droidsaw is built for. All commands are verified against the binary; substitute your own APK path.
App developer — audit your build, gate CI
droidsaw info app-release.apk # what's in the build
droidsaw audit app-release.apk --mode=basic # fast hermetic audit (~10-30s, no subprocesses)
droidsaw audit app-release.apk --mode=basic --fail-on=high # CI gate: exit 1 if any High+ finding (no jq)
droidsaw audit app-release.apk --mode=basic --format sarif > droidsaw.sarif # GitHub code scanning
droidsaw scan sbom app-release.apk > sbom.json # CycloneDX 1.5 SBOM
Bug-bounty hunter — recon → decompile → trace
droidsaw info target.apk
droidsaw manifest target.apk | jq '.manifest.exported_components' # attack surface
droidsaw signing target.apk # weak keys / cert posture
droidsaw decompile target.apk com.example.AuthManager # pull a class to source
# Cross-reference a secret-looking string to every function that references it (DEX + HBC).
# Output: {xrefs:[{layer, string, functions:["name(#id)", ...]}]}; then decompile a referencing function.
droidsaw xrefs target.apk --search 'authorization|Bearer|api_secret'
droidsaw audit target.apk --mode=basic | jq '[.findings[] | select(.id | test("TAINT"))]' # JS→bridge→Java taint
droidsaw frida target.apk --search 'password|token|decrypt' # Frida hook stubs
Product Security team — fleet scan & triage
droidsaw corpus ingest /path/to/apks/ --output corpus.db --tag 2026-q2 # signing/metadata DB (idempotent)
droidsaw corpus scan /path/to/apks/ --min-severity high > fleet.ndjson # batch NDJSON for a SIEM/store
DROIDSAW_SEMGREP_RULES=./rules/android.yml droidsaw audit app.apk --mode=full # deep audit, your Semgrep rules
droidsaw audit app.apk --mode=basic --format sarif > app.sarif # SARIF into code scanning
sqlite3 corpus.db "SELECT rsa_modulus_hex, COUNT(*) c FROM signers GROUP BY rsa_modulus_hex HAVING c > 1" # shared-prime hunt
Human-rights / at-risk-user defender — examine a suspected spyware sample safely
Static-only — droidsaw never runs the sample and never touches the network. It parses and decompiles in memory; that is exactly the property you want for a sample that may target a person. (Still handle the APK on an isolated host per your operational-security practice.)
droidsaw info suspect.apk
droidsaw manifest suspect.apk | jq '{pkg:.manifest.package, perms:.manifest.permissions, exported:.manifest.exported_components}'
droidsaw scan yara suspect.apk # hooking frameworks, root/debugger evasion
droidsaw audit suspect.apk --mode=basic --stix-feed known-iocs.json # match a local STIX 2.1 IOC feed
droidsaw strings suspect.apk --search 'location|microphone|camera|sms|contacts|keylog'
droidsaw xrefs suspect.apk --search 'android.location|telephony.SmsManager|startForeground'
Look at: exported Services/Receivers without a permission guard (a remote-control surface); high-risk permission clusters (ACCESS_FINE_LOCATION, READ_SMS, RECORD_AUDIO, READ_CONTACTS, BIND_DEVICE_ADMIN); anti-analysis / hooking matches that signal evasion.
Cross-layer taint
audit --mode=basic and --mode=full run three taint passes. Results land in the taint_flows SQLite table.
- HBC pass. User-controlled inputs seeded and propagated through Hermes functions. Detects
DirectEval(CWE-95) and tainted args to NativeModuleCall*ops. Records which arg positions carried taint. - DEX pass. Follows
invoke-direct,invoke-static, and monomorphicinvoke-virtual/invoke-interfaceacross DEX boundaries via a cross-DEX class hierarchy index (CHA). Interprocedural depth: 4. - Bridge pass.
@ReactMethodparams seeded as taint sources, then run through the DEX pass. Seeding is restricted to the arg positions the HBC pass found tainted — only the JS-side values that actually carry taint become Java-side seeds. Falls back to all params when no HBC bundle is present.
15 taint sources × 15 sinks defined in droidsaw-common, including bridge edges that span the layers — sources like JS network input, React Native bridge params, and IntentExtra; sinks like Runtime.exec(), WebView.loadUrl, SQL execution, and file writes. A tainted value crossing into a JNI-native method is flagged (JNI_TAINTED_NATIVE_CALL) but not traced into native code — a documented stop, not a gap.
Per-layer scope
| Layer | Status |
|---|---|
| APK container | Signing v1–v4, crypto (ROCA / Fermat / Wiener / batch-GCD), YARA (credential / packer / crypto / anti-analysis), SBOM, ELF metadata. |
| DEX → Java | Decompiler. Byte-identical roundtrip on F-Droid corpus under preservation mode (default emit differs on a 5.4% legacy-dx subset — see Correctness). |
| Hermes → JS | Decompiler. Parses v40–v100; byte-exact roundtrip verified on v84/v96/v98/v99. OXC round-trip validated on decompile output. |
| Native ELF | Hardening flags, JNI exports, relocation counts. No disassembly. |
| Dart AOT | Not supported. |
| IL2CPP | Not supported. |
MCP server
droidsaw exposes its analysis surface over MCP. An agent can load an APK, run a cross-layer audit, query findings, decompile a class, and diff two Hermes bundles — all in one session, one schema.
The server is a second binary, droidsaw-mcp (cargo install droidsaw --features mcp). Transport is stdio only — an MCP client such as Claude Code spawns it as a child process over stdin/stdout; there is no network listener. Wire it in via .mcp.json:
{
"mcpServers": {
"droidsaw": {
"type": "stdio",
"command": "droidsaw-mcp",
"args": ["--allowed-tool-classes=all", "--tool-tier=full"],
"env": { "DROIDSAW_MCP_ROOT": "/path/to/your/analysis/root" }
}
}
}
Two dials plus a sandbox set the security posture: DROIDSAW_MCP_ROOT confines every path the server reads or writes; --allowed-tool-classes (default read-only,writes-tempfile) gates what it may do; --tool-tier (basic = 12-tool core workflow, full (default) = every tool) gates what the model sees. The class gate is per-tool — audit is classed spawns-subprocess even in basic mode, and triage needs manages-state. There is no built-in authentication, and large outputs stream to a tempfile rather than the context window. The args above (--allowed-tool-classes=all) is a full-trust local setup; on a shared or untrusted host drop them to keep the safe default classes and add --tool-tier=basic.
A session goes load(path=…) (required first) → audit(mode='basic') → query(sql='SELECT …') → investigate(rowid=…) → decompile(…) → triage(…). Every tool errors until load has been called.
Integrations
| Tool | Surface |
|---|---|
| SQLite | Each writer produces its own schema: scan export → parsed-layer tables (strings, functions, classes, edges, strings_fts); corpus ingest → apks + signers; audit --format unsigned-evidence → the findings + taint_flows schema (rev 6). The query MCP tool runs read-only SELECT against the audit findings DB. |
| Semgrep | audit --mode=semgrep / scan semgrep --persist. Decompiled DEX source extracted to disk and fed to Semgrep. No rules ship bundled — supply your own via --rules <path> or DROIDSAW_SEMGREP_RULES. |
| TruffleHog | audit --mode=trufflehog / scan trufflehog. Extracted strings from every layer piped to TruffleHog. All hits land in the credentials view, each carrying TruffleHog's own verified flag. |
| YARA-X | scan yara / bundled in audit. YARA-X (Rust port). Bundled rule packs; custom rules accepted. Provenance-aware suppression. |
| STIX 2.1 | audit --stix-feed <path>. Loads any STIX 2.1 bundle (file path; no network I/O). IOC matches against parsed APK content. |
| Frida | frida subcommand. Auto-generated hook stubs against functions that touch matched strings. |
Architecture
Both decompilers follow the same pipeline. The middle stages live in droidsaw-common (generic over an I: Instr trait); the bundle crate supplies its own Insn type and language-specific sugar.
| Stage | Module | Input → Output |
|---|---|---|
| decode | <bundle>/decode.rs, <bundle>/parser/ |
&[u8] → Vec<Insn> |
| CFG | <bundle>/cfg.rs, oracle in common/graph/ |
Vec<Insn> → basic blocks + edges |
| dominators | common/graph/dominators.rs |
basic blocks → idom map |
| SSA (Braun) | common/ssa/, <bundle>/ssa.rs |
basic blocks → SsaFunction |
| Expr IR | <bundle>/expr.rs (Hermes), common/region/ |
SsaFunction → expression tree |
| structure | common/region/, <bundle>/structure.rs |
expression tree → RegionTree |
| sugar | <bundle>/sugar.rs, hermes/decompile/ |
RegionTree → RegionTree |
| emit | dex/emit_dex.rs, hermes/emit.rs |
RegionTree → source bytes |
| validate | tests/byte_identity_smoke.rs, tests/hbc_corpus_roundtrip.rs |
source bytes ≡ input (round-trip) |
Deterministic IR — BTreeMap throughout, so output is stable across runs. Typed opcode enum. Cross-validated against reference disassemblers — DEX against dexdump, Hermes against hbcdump.
Correctness
Five gates. Each catches what the layer above it can't.
1. Round-trip disassembly
The strongest claim about a format parser is that it understands every byte. One way to test that: parse the file, regenerate the bytes from what was parsed, and check that they are identical to what you started with. A wrong rule anywhere — a string-table offset off by one, a missed alignment requirement, a padding byte misclassified — produces a divergence the test catches precisely.
DEX: 100% byte-identical on the F-Droid corpus (5,767 DEX files across 3,782 apps) under preservation mode. The 5.4% subset (309 files) that differs under default emit does so in 24 header bytes only — exclusively legacy-dx-toolchain non-canonical SHA-1 inputs; droidsaw recomputes correct checksums on default emit, or preserves verbatim under audit mode.
Hermes: byte-exact reconstruction on bytecode versions v84, v96, v98, v99 — header, global string table, function table, alignment, and metadata layout. Verified clean on public v96 corpus samples.
Verify locally:
cargo test -p droidsaw-dex byte_identity_smoke
cargo test -p droidsaw-hermes hbc_corpus_roundtrip
2. Fixture ratchets
DEX: 68 in-repo Java + Kotlin + R8 sources. COMPILE_FAIL = 1. SEMANTIC_FAIL = 0.
Hermes: v96 fixture matrix. COMPILE_FAIL = 0. SEMANTIC_FAIL = 0.
UNRECOGNIZED_REGION ratchet pinned per-fixture in tests/unrecognized_ratchet.rs. Any new region a recognizer fails to handle is a build break.
The ratchet only decreases. A fixture flip blocks merge.
3. Adversarial fuzz
libFuzzer targets: fuzz_parser, fuzz_opcode_decode, fuzz_cfg, fuzz_ssa, fuzz_emit_roundtrip (DEX), fuzz_emit_roundtrip_hbc (Hermes), fuzz_protector_recognizer, fuzz_enum_cross_class.
The parser and decoder targets for both DEX and Hermes ran for extended campaigns with zero panics, zero artifacts.
fuzz_emit_roundtrip{_hbc} runs the round-trip property under libFuzzer instrumentation on the full input space, not just the fixture set.
4. Cross-tool differential
DEX vs dexdump (Android SDK's DEX disassembler), used as a code-unit coverage oracle. Every class descriptor and every method (class, name, proto) triple dexdump -d enumerates must also appear in droidsaw-dex output. A missed class or method is a build break.
Hermes vs hbcdump (Meta's official disassembler):
- Parse-side: header, global string table, function table compared byte-for-byte.
- Instruction-level: 12,000 sampled
(opname, operand_count)tuples compared across functions. Zero opcode disagreements.
5. Formal proofs
Kani (bounded model checking). 101 harnesses across the workspace on statements with decidable input domains: MUTF-8 codec totality, signing-block padding gate, LEB128 read/write round-trip, bit-field bounds, Hermes function_get u64-overflow guard (against a u128 oracle), MANIFEST.MF base64 positional gate, recursion-depth caps, per-tag truncation guards, base64 capacity arithmetic.
Lean 4. 20 proved theorems on statements that quantify over arbitrary input length or arbitrary CFG shape — out of Kani's bounded reach. AXML parser totality and acyclicity. Dominator antisymmetry, transitivity, and unique idom. Lattice monotonicity of the dataflow fixed point. Hermes try-catch RPO ordering. No sorry, no axiom. Each .lean file names the Rust function it backs via a RUST: comment; the correspondence is asserted in source and maintained by hand, not mechanically verified. The Lean 4 proofs are maintained in a separate droidsaw-lean project (Lake build, not a Cargo crate).
OXC round-trip. Every Hermes decompile output is parsed back by OXC (a Rust-native JavaScript parser and codegen). Output OXC rejects is annotated and returned, never silently dropped.
The compile-time floor on every non-test module:
#![deny(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::unreachable,
clippy::todo,
clippy::arithmetic_side_effects,
)]
Suppressions on the panic family require a written PROOF: obligation. panic = "abort" is set workspace-wide — a stale PROOF obligation terminates the process at runtime, not just at lint.
Reporting issues
File an issue on GitHub with a reproduction — search
first, it may already be tracked:
- A crash or panic — the input that triggers it (minimized if you can), or a bundle from
droidsaw triage promote. - A decompilation gap or wrong output — the smallest input that reproduces, plus the output you got vs. expected.
- A wrong or missing finding — the finding ID and the input, plus (false positive) why the flag looks incorrect, or (false negative) a reference showing the pattern droidsaw should catch.
Issue reports with reproductions are the most useful contribution; code PRs aren't currently solicited.
License
BSD-3-Clause.
Reviews (0)
Sign in to leave a review.
Leave a reviewNo results found