droidsaw/droidsaw

GitHub: droidsaw/droidsaw

纯 Rust 实现的 Android 反编译与安全审计工具,支持 DEX→Java、Hermes→JS 反编译及跨 React Native bridge 的污点追踪。

Stars: 19 | Forks: 2

# droidsaw

Scientist and worker dissecting an Android robot with a chainsaw

插画由 pmjv_prahou 创作。献给 Open 家族最后一位 法师 的礼物。

droidsaw 能够将 DEX 文件或 Hermes bundle 逐字节地拆解并重新组装——在保留模式下,F-Droid 的 5,767 个 DEX 文件实现了逐位恢复,并且在 v84、v96、v98 和 v99 版本上验证了 Hermes 字节码的往返一致性。当格式模型存在漏洞时,该测试会立即报错:字符串表偏移量错位一位、未满足对齐要求、遗漏了填充字节——重新输出的字节会产生偏差,测试会准确指出问题所在。大多数 Android 逆向工程 (RE) 工具按层工作;而 droidsaw 可以将 JS 值通过 React Native bridge 追踪到 Java 中,作为一条完整的污点路径。只需提供一个 APK,它就会解包容器,反编译每一层,并能在同一次执行中将输出通过 Semgrep 和 TruffleHog 进行管道处理(`audit --mode=full`)。CLI 和 MCP server 共享同一个命令操作面。纯 Rust 编写,BSD-3-Clause 许可证。 ## 安装说明 ``` cargo install droidsaw # the CLI cargo install droidsaw --features mcp # also installs the droidsaw-mcp server ``` `cargo` 会从 crates.io 获取 `droidsaw-*` 库 crate 并在本地编译所有内容。唯一的 先决条件是 Rust 工具链 ([rustup](https://rustup.rs)) 和 C 编译器 (`cc`/`clang`)。 `semgrep` 和 `trufflehog` 是可选的(用于 `audit --mode=full`、`--mode=semgrep` 或 `--mode=trufflehog`);YARA 已内置。 不需要 Java/Android SDK。 在自动化或 CI 环境中安装?请使用 `cargo install --locked droidsaw`——它会根据发布时锁定的 lockfile 中的依赖版本进行构建, 从而确保同一版本的安装具有可复现性。 对于集群或 CI 安装,建议使用 [`cargo auditable`](https://github.com/rust-secure-code/cargo-auditable) 变体——它将完整的 crate 依赖列表嵌入到二进制文件本身中(位于专用 linker section 中的压缩 JSON 记录), 因此即使在构建环境消失多年以后,该产物仍然保持可审计性: ``` 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` 会读取嵌入的清单,并将其与 RustSec 建议数据库进行核对—— 多年后,仅凭该文件,二进制文件就能回答“你里面到底有什么?”这个问题。 ## 快速入门 ``` 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? ``` 每条命令都可以与 [`jq`](https://jqlang.github.io/jq/) 组合使用;针对不同受众的详细操作流程请参见下方的 [操作指南](#playbooks)。 ## 输入与输出 **输入:** APK、XAPK、`.hbc`、`.dex`。Hermes bundle 和 DEX 文件会自动从 APK 中提取;同级的 `split_config.*.apk` 会自动合并(使用 `--no-auto-splits` 可禁用)。 **输出:** stdout 为一个 JSON 对象、数组或 NDJSON 流——没有其他内容。已声明的纯文本豁免项包括:`--version` 和 `--help`(遵循 clap 约定)、`hbc disassemble`(用于差异化工具的指令流)、`scan trufflehog`(原始字符串流)以及 `decompile --all --js`(拼接的 JS)。进度信息会输出到 stderr,带有 `droidsaw: ` 前缀。 **退出码:** `0` 成功 · `1` 保留给可选的 `audit --fail-on=` 门禁(审计完成,stdout 输出正常的审计结果,并且至少有一条发现达到或超过阈值——纯退出码 CI 门禁,无需 `jq`)· `2` 失败。每次失败——包括拼写错误的 flag——在 stdout 上都是一个带类型的 JSON 错误封装: ``` { "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" } } ``` 对同一输入重复运行会产生逐位相同的输出。 ## 发现阶段 涵盖约会、社交、金融科技、银行和健康等领域的生产级应用语料库中提取出的模式: - **JS 堆中的密钥材料**——在 Hermes 中执行私钥操作,没有原生边界,也没有内存清零。由于 droidsaw 能够读取 Hermes 层,因此这些内容清晰可见。 - **加密级别的签名链弱点**——ROCA 指纹 (CVE-2017-15361)、可被 Fermat 分解的相近素数(返回 `(p, q)`)、跨语料库的批量 GCD 共享素数恢复(Bernstein 拟线性)、Wiener 体制的指数指纹(`e > N^0.75`;完全恢复)。 - **数据通过 React Native bridge 传递到不应到达的 sink**——JS 网络输入进入了 `Runtime.exec()`,分析 SDK 接收到了健康或财务信号。以从 JS 源到 Java sink 的单一污点路径形式报告。 - **没有权限保护的导出组件**——任何应用都可以访问 activity 和 service,泄露 auth token、OAuth code 和 refresh token。 - **发布后依然存在的预发布基础设施**——内部 endpoint、调试 flag、分布在 manifest、DEX 字符串池和 Hermes 全局字符串表中的硬编码凭据。 交叉引用有助于了解应用的具体行为:将可疑字符串追踪到每一个引用它的函数,然后反编译这些函数以映射调度面。针对一个跟踪软件样本的详细示例:[Cerberus Anti-theft: Stalkerware RE](https://hexproof.dev/datagrams/cerberus-stalkerware-re/)。 ## 命令 | 命令 | 输出 | |---|---| | `audit` | 安全审计 (`--mode=`) | | `decompile` | 将 DEX 类反编译为 Java 或将 Hermes 函数反编译为 JavaScript | | `xrefs` | 将字符串交叉引用到每一个引用它的函数 (DEX + HBC)。“谁引用了这个密钥?”是一个查询,而不是一次 grep。 | | `manifest` | AndroidManifest.xml 分析 | | `signing` | v1 / v2 / v3 / v4 签名块分析 | | `strings` | 跨所有层搜索字符串 (`--layer dex\|hbc\|native`, `--search `) | | `frida` | 为匹配字符串模式的函数生成 Frida hook stub | | `diff` | 两个 Hermes bundle 的结构化 diff(接受 APK 或 `.hbc`) | | `deobf-strings` | 通过在参数集上模拟 DEX 方法来恢复混淆字符串 | | `info` | 容器 + 字节码层摘要:层 + manifest + 签名 | | `inspect` | 容器内部结构:`entries` (ZIP + 异常)、`elf`、`resources`、`webview` | | `hbc` | Hermes 子命令 (`info`、`functions`、`strings`、`decompile`、`disassemble`) | | `dex` | DEX 子命令 (`classes`、`methods`、`strings`) | 跨层污点路径(JS → bridge → Java → sink)由 `audit` 生成,而不是独立命令——参见 [跨层污点](#cross-layer-taint)。 更多命令归类于以下总称之下:`scan `、`inspect `、`corpus `、`triage promote`。全局 flag 限制了每次解析的资源消耗:`--budget-mem`、`--budget-time`、`--single-thread`(确定性)、`--permissive-recovery`(容错 AXML)。运行 `droidsaw --help` 或 `droidsaw --help` 查看完整操作面。 ## 操作指南 为 droidsaw 服务的四类人群提供的具体操作流程。所有命令均已通过二进制文件验证;请替换为您自己的 APK 路径。 **应用开发者——审计您的构建,控制 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 ``` **赏金猎人——侦察 → 反编译 → 追踪** ``` 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 # 将疑似密钥的字符串交叉引用到每个引用它的函数 (DEX + HBC)。 # 输出:{xrefs:[{layer, string, functions:["name(#id)", ...]}]};然后反编译引用该字符串的函数。 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 ``` **产品安全团队——集群扫描与分类** ``` 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 ``` **人权/高风险用户捍卫者——安全地检查可疑的间谍软件样本** ``` 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' ``` 重点关注:没有权限保护的导出 Service/Receiver(一种远程控制面);高风险权限集群(`ACCESS_FINE_LOCATION`、`READ_SMS`、`RECORD_AUDIO`、`READ_CONTACTS`、`BIND_DEVICE_ADMIN`);暗示存在规避行为的反分析/hooking 匹配项。 ## 跨层污点 `audit --mode=basic` 和 `--mode=full` 会运行三次污点传递。结果将存入 `taint_flows` SQLite 表中。 - **HBC 传递。** 注入用户控制的输入,并在 Hermes 函数中进行传播。检测 `DirectEval` (CWE-95) 以及传递给 NativeModule `Call*` 操作的污点参数。记录哪些参数位置携带了污点。 - **DEX 传递。** 通过跨 DEX 类层次结构索引 (CHA),跨越 DEX 边界跟踪 `invoke-direct`、`invoke-static` 和单态 `invoke-virtual` / `invoke-interface`。过程间深度:4。 - **Bridge 传递。** 将 `@ReactMethod` 参数作为污点源注入,然后执行 DEX 传递。注入仅限于 HBC 传递发现被污染的参数位置——只有实际携带污点的 JS 端值才会成为 Java 端的种子。当不存在 HBC bundle 时,回退为使用所有参数。 在 `droidsaw-common` 中定义了 15 个污点源 × 15 个 sink,包括跨越各层的 bridge 边——源例如 JS 网络输入、React Native bridge 参数和 `IntentExtra`;sink 例如 `Runtime.exec()`、`WebView.loadUrl`、SQL 执行和文件写入。进入 JNI 原生方法的被污染值会被标记 (`JNI_TAINTED_NATIVE_CALL`),但不会继续追踪到原生代码中——这是一个文档说明的停止点,而不是缺口。 ## 各层范围 | 层 | 状态 | |---|---| | APK 容器 | 签名 v1–v4,加密 (ROCA / Fermat / Wiener / 批量 GCD),YARA (凭据 / 加壳 / 加密 / 反分析),SBOM,ELF 元数据。 | | DEX → Java | 反编译器。在保留模式下对 F-Droid 语料库实现字节一致的往返(默认输出在 5.4% 的旧版 `dx` 子集上存在差异——参见 [正确性](#correctness))。 | | Hermes → JS | 反编译器。解析 v40–v100;在 v84/v96/v98/v99 上验证了字节精确的往返。OXC 往返已在反编译输出上验证。 | | 原生 ELF | 加固 flag,JNI 导出,重定位计数。无反汇编。 | | Dart AOT | 不支持。 | | IL2CPP | 不支持。 | ## MCP server droidsaw 通过 MCP 暴露其分析面。Agent 可以在一个会话、一个 schema 中加载 APK、运行跨层审计、查询发现结果、反编译类,并对两个 Hermes bundle 进行 diff。 该 server 是第二个二进制文件,即 `droidsaw-mcp`(`cargo install droidsaw --features mcp`)。**传输方式仅限 stdio**——诸如 Claude Code 之类的 MCP 客户端会通过 stdin/stdout 将其作为子进程启动;没有网络监听器。通过 `.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" } } } } ``` 两个配置项加上一个沙箱构成了安全态势:**`DROIDSAW_MCP_ROOT`** 限制了 server 读取或写入的每个路径;**`--allowed-tool-classes`**(默认为 `read-only,writes-tempfile`)控制其行为权限;**`--tool-tier`**(`basic` = 12 个工具的核心工作流,`full` *(默认)* = 所有工具)控制模型可见的操作。类门禁是针对每个工具的——即使在 `basic` 模式下,`audit` 也被归类为 `spawns-subprocess`,而 `triage` 需要 `manages-state`。没有内置身份验证,大型输出将流式传输到 tempfile,而不是发送到上下文窗口。上述的 `args` (`--allowed-tool-classes=all`) 是完全信任的本地设置;在共享或不受信任的主机上,请移除它们以保持安全的默认类,并添加 `--tool-tier=basic`。 一个会话的流程为:`load(path=…)`(必须首先执行)→ `audit(mode='basic')` → `query(sql='SELECT …')` → `investigate(rowid=…)` → `decompile(…)` → `triage(…)`。在调用 `load` 之前,每个工具都会报错。 ## 集成 工具 | 操作面 | |---|---| | SQLite | 每个写入器生成自己的 schema:`scan export` → 解析层表 (`strings`、`functions`、`classes`、`edges`、`strings_fts`);`corpus ingest` → `apks` + `signers`;`audit --format unsigned-evidence` → `findings` + `taint_flows` schema (rev 6)。`query` MCP 工具针对审计结果数据库运行只读 `SELECT`。 | | Semgrep | `audit --mode=semgrep` / `scan semgrep --persist`。将反编译的 DEX 源码提取到磁盘并输入给 Semgrep。**不内置打包规则**——通过 `--rules ` 或 `DROIDSAW_SEMGREP_RULES` 提供您自己的规则。 | | TruffleHog | `audit --mode=trufflehog` / `scan trufflehog`。将每一层提取的字符串通过管道传递给 TruffleHog。所有匹配结果都会存入 `credentials` 视图,每一条都带有 TruffleHog 自身的 `verified` 标记。 | | YARA-X | `scan yara` / 内置于 `audit` 中。YARA-X (Rust 移植版)。内置规则包;接受自定义规则。支持来源感知抑制。 | | STIX 2.1 | `audit --stix-feed `。加载任何 STIX 2.1 bundle(文件路径;无网络 I/O)。IOC 针对解析的 APK 内容进行匹配。 | | Frida | `frida` 子命令。针对触及匹配字符串的函数自动生成 hook stub。 | ## 架构 两个反编译器遵循相同的 pipeline。中间阶段位于 `droidsaw-common` 中(对 `I: Instr` trait 进行泛化);bundle crate 提供自己的 `Insn` 类型和特定于语言的语法糖。 | 阶段 | 模块 | 输入 → 输出 | |---|---|---| | decode | `/decode.rs`, `/parser/` | `&[u8]` → `Vec` | | CFG | `/cfg.rs`, 位于 `common/graph/` 的 oracle | `Vec` → 基本块 + 边 | | dominators | `common/graph/dominators.rs` | 基本块 → idom 映射 | | SSA (Braun) | `common/ssa/`, `/ssa.rs` | 基本块 → `SsaFunction` | | Expr IR | `/expr.rs` (Hermes), `common/region/` | `SsaFunction` → 表达式树 | | structure | `common/region/`, `/structure.rs` | 表达式树 → `RegionTree` | | sugar | `/sugar.rs`, `hermes/decompile/` | `RegionTree` → `RegionTree` | | emit | `dex/emit_dex.rs`, `hermes/emit.rs` | `RegionTree` → 源码字节 | | validate | `tests/byte_identity_smoke.rs`, `tests/hbc_corpus_roundtrip.rs` | 源码字节 ≡ 输入 (往返) | 确定性 IR——全程使用 BTreeMap,因此输出在多次运行中保持稳定。带类型的 opcode enum。与参考反汇编器交叉验证——DEX 对比 `dexdump`,Hermes 对比 `hbcdump`。 ## 正确性 五个关卡。每个关卡捕获其上一层无法捕获的问题。 ### 1. 往返反汇编 关于格式解析器最强有力的声明是,它理解每一个字节。测试这一点的一种方法是:解析文件,根据解析的内容重新生成字节,并检查它们是否与起始文件完全一致。任何错误的规则——字符串表偏移错位一位、遗漏对齐要求、错误分类的填充字节——都会导致测试能够精确捕捉到的差异。 DEX:在保留模式下,F-Droid 语料库(涵盖 3,782 个应用的 5,767 个 DEX 文件)达到 100% 的字节一致性。在默认输出下存在差异的 5.4% 子集(309 个文件)仅在于 24 个头字节不同——专门针对旧版 `dx` 工具链的非规范 SHA-1 输入;droidsaw 在默认输出下会重新计算正确的校验和,或在审计模式下逐字保留。 Hermes:在字节码版本 v84、v96、v98、v99 上实现字节精确重建——包括头、全局字符串表、函数表、对齐和元数据布局。已在公开的 v96 语料库样本上验证无误。 本地验证: ``` cargo test -p droidsaw-dex byte_identity_smoke cargo test -p droidsaw-hermes hbc_corpus_roundtrip ``` ### 2. 基准测试棘轮 DEX:库内包含的 68 个 Java + Kotlin + R8 源码。`COMPILE_FAIL = 1`。`SEMANTIC_FAIL = 0`。 Hermes:v96 基准测试矩阵。`COMPILE_FAIL = 0`。`SEMANTIC_FAIL = 0`。 `UNRECOGNIZED_REGION` 棘轮在 `tests/unrecognized_ratchet.rs` 中按测试用例锁定。识别器未能处理的任何新 region 都会导致构建中断。 该棘轮只会减小。测试基准状态的翻转将阻止合并。 ### 3. 对抗性模糊测试 libFuzzer 目标:`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`。 针对 DEX 和 Hermes 的解析器与解码器目标进行了长期的活动测试,未发生任何 panic,没有产生任何异常工件。 `fuzz_emit_roundtrip{_hbc}` 在 libFuzzer 插桩下对整个输入空间(而不仅仅是测试用例集)运行往返属性测试。 ### 4. 跨工具差异化 DEX 对比 `dexdump`(Android SDK 的 DEX 反汇编器),用作代码单元覆盖率的 oracle。`dexdump -d` 枚举出的每个类描述符和每个方法 `(class, name, proto)` 三元组也必须出现在 droidsaw-dex 输出中。遗漏类或方法会导致构建中断。 Hermes 对比 `hbcdump`(Meta 官方反汇编器): - 解析端:逐字节比较头、全局字符串表、函数表。 - 指令级别:跨函数比较了 12,000 个采样的 `(opname, operand_count)` 元组。零 opcode 分歧。 ### 5. 形式化验证 **Kani**(有界模型检查)。在整个 workspace 中针对具有可判定输入域的语句使用了 101 个测试套件:MUTF-8 编解码器的完备性、签名块填充门禁、LEB128 读/写往返、位域边界、Hermes `function_get` u64 溢出防护(针对 u128 oracle)、MANIFEST.MF base64 位置门禁、递归深度上限、按 tag 的截断防护、base64 容量算术。 **Lean 4**。在对任意输入长度或任意 CFG 形状进行量化的语句上证明了 20 个定理——这超出了 Kani 的有界范围。AXML 解析器的完备性和无环性。支配者的反对称性、传递性和唯一性。数据流不动点的格单调性。Hermes try-catch 的 RPO 排序。没有 `sorry`,没有 `axiom`。每个 `.lean` 文件都通过 `RUST:` 注释指明其支持的 Rust 函数;这种对应关系在源码中声明并由人工维护,并非机械验证。Lean 4 证明在单独的 `droidsaw-lean` 项目中维护(Lake 构建,而非 Cargo crate)。 **OXC** 往返。每一个 Hermes 反编译输出都会被 OXC(Rust 原生的 JavaScript 解析器和 codegen)重新解析。OXC 拒绝的输出会被标记并返回,绝不会静默丢弃。 每个非测试模块的编译时底线: ``` #![deny( clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unreachable, clippy::todo, clippy::arithmetic_side_effects, )] ``` 对 panic 家族的抑制需要书面的 `PROOF:` 义务。`panic = "abort"` 在全 workspace 范围内设置——过时的 PROOF 义务在运行时就会终止进程,而不仅仅是在 lint 阶段。 ## 报告问题 在 [GitHub](https://github.com/droidsaw/droidsaw/issues) 上提交带有复现步骤的 issue——请先 搜索,因为该问题可能已被记录: - **崩溃或 panic**——触发它的输入(如果可以,请尽量精简),或者来自 `droidsaw triage promote` 的 bundle。 - **反编译缺口或输出错误**——能够复现问题的最小输入,以及您实际得到的输出与预期输出的对比。 - **发现结果错误或缺失**——发现结果的 ID 和输入,如果是误报,请说明为何该 flag 看起来不正确;如果是漏报,请提供参考,展示 droidsaw 应捕获的模式。 带有复现步骤的 issue 报告是最有用的贡献;目前暂不接受代码 PR。 ## 许可证 BSD-3-Clause。
标签:Android逆向, Rust, SBOM, 反编译器, 可视化界面, 图数据库, 硬件无关, 网络流量审计, 通知系统