343 lines
19 KiB
Markdown
343 lines
19 KiB
Markdown
# Static Android APK analysis
|
|
|
|
REA wraps an existing headless JADX engine through `src/android/`. Its source is
|
|
kept as an unmodified, commit-pinned Git submodule; see
|
|
[upstream provenance](https://github.com/morluto/rea/blob/main/third_party/README.md). CLI and MCP use the same
|
|
application workflow and return artifact-bound Evidence inline.
|
|
|
|
This family is present on repository main and in npm 4.1.0. Check the
|
|
[released package boundary](installation.md#released-package-and-main) and the
|
|
connected server's tool list before selecting it.
|
|
|
|
## Optional package delivery
|
|
|
|
The MCP `inspect_android_package` request keeps complete output when `detail` is
|
|
omitted or `"complete"`. Select `detail: "summary"` explicitly to receive all
|
|
package, SDK and permission declarations, engine identity and class/resource
|
|
counts without decoded manifest text or duplicate raw provider replies. A
|
|
retaining session keeps the complete original Evidence and returns a distinct
|
|
linked `inspect_analysis_view` summary. This changes delivery, not JADX analysis.
|
|
|
|
Use the returned `normalized_result.parent_evidence_id` with
|
|
`inspect_analysis_view`, source kind `retained-evidence`, and
|
|
`view: {kind: "android", facet: "manifest", offset: 0, limit: 4096}` to read the
|
|
exact decoded manifest without reopening the APK. Offsets and limits count UTF-16
|
|
code units; follow `coverage.next_offset`. The page preserves `source_status` and
|
|
`reported_total_bytes`: exhausting retained text does not make a partial decoded
|
|
manifest complete. Complete source Evidence remains exportable under the existing
|
|
session lifetime. Servers without retention reject summary before starting JADX.
|
|
One-shot CLI output remains complete. Older servers require the existing complete
|
|
request after schema discovery. APK authenticity and runtime behavior remain
|
|
unverified.
|
|
|
|
## Supply tools
|
|
|
|
Use a **full JDK 17 or newer** (including its compiler module) and
|
|
[**jadx-headless-mcp 0.7.1**](https://github.com/1013503897/jadx-headless-mcp/releases/download/v0.7.1/jadx-headless-mcp-0.7.1-all.jar).
|
|
Verify the downloaded JAR's SHA-256 against
|
|
[REA's pinned release metadata](https://github.com/morluto/rea/blob/main/src/android/JadxRelease.ts)
|
|
before using it. The following download and verification commands work for npm
|
|
package consumers as well as repository checkouts; the repository-only
|
|
`npm run fixtures:android` helper is not included in the npm package.
|
|
|
|
On macOS or Linux, choose a destination and verify the pinned digest:
|
|
|
|
```sh
|
|
mkdir -p "$HOME/.local/share/rea/android" &&
|
|
cd "$HOME/.local/share/rea/android" &&
|
|
curl -fL -o jadx-headless-mcp-0.7.1-all.jar https://github.com/1013503897/jadx-headless-mcp/releases/download/v0.7.1/jadx-headless-mcp-0.7.1-all.jar &&
|
|
if command -v sha256sum >/dev/null; then
|
|
echo '6e5eacf500b64292bfb73c49797c1958f6ee44646e43e868039ae7feb573ff75 jadx-headless-mcp-0.7.1-all.jar' | sha256sum -c -
|
|
else
|
|
echo '6e5eacf500b64292bfb73c49797c1958f6ee44646e43e868039ae7feb573ff75 jadx-headless-mcp-0.7.1-all.jar' | shasum -a 256 -c -
|
|
fi &&
|
|
export REA_JADX_MCP_JAR="$PWD/jadx-headless-mcp-0.7.1-all.jar"
|
|
```
|
|
|
|
On Windows, run the equivalent in PowerShell:
|
|
|
|
```powershell
|
|
$jar = Join-Path $HOME '.local\share\rea\android\jadx-headless-mcp-0.7.1-all.jar'
|
|
New-Item -ItemType Directory -Force (Split-Path $jar) | Out-Null
|
|
Invoke-WebRequest -Uri 'https://github.com/1013503897/jadx-headless-mcp/releases/download/v0.7.1/jadx-headless-mcp-0.7.1-all.jar' -OutFile $jar
|
|
if ((Get-FileHash -Algorithm SHA256 $jar).Hash.ToLowerInvariant() -ne '6e5eacf500b64292bfb73c49797c1958f6ee44646e43e868039ae7feb573ff75') { throw 'JADX JAR SHA-256 does not match REA pinned metadata' }
|
|
$env:REA_JADX_MCP_JAR = $jar
|
|
```
|
|
|
|
REA runs its packaged Java metadata bridge in source-file mode against the
|
|
unmodified engine JAR. REA does not install Java, an Android SDK, JADX, a device
|
|
service or an emulator. Set the path in the shell or MCP server environment:
|
|
|
|
```sh
|
|
export REA_JADX_MCP_JAR=/absolute/path/jadx-headless-mcp-0.7.1-all.jar
|
|
# Optional: select an existing JDK rather than java on PATH.
|
|
export JAVA_HOME=/absolute/path/existing-jdk
|
|
```
|
|
|
|
On Windows x64, use a REA package with its matching bundled Windows native
|
|
controls **including owned protocol stdin support**. This Windows boundary is
|
|
new on repository main; an older released package can still reject this host.
|
|
PowerShell accepts paths containing spaces without additional escaping
|
|
inside these environment values:
|
|
|
|
```powershell
|
|
$env:REA_JADX_MCP_JAR = 'D:\tools\jadx-headless-mcp-0.7.1-all.jar'
|
|
$env:JAVA_HOME = 'D:\tools\jdk-21'
|
|
rea inspect-android-package 'D:\targets\Example.apk'
|
|
```
|
|
|
|
REA selects `JAVA_HOME\bin\java.exe`, or resolves `java.exe` on the selected
|
|
Windows `PATH`. A missing or mismatched native artifact reports the package
|
|
prerequisite rather than attempting an unowned Java launch. Windows arm64 is
|
|
outside this provider's current native boundary.
|
|
|
|
Readiness checks the selected JAR's ZIP directory for the classes consumed by
|
|
REA's metadata bridge, as well as Java's compiler and version modules. It does
|
|
not load an APK or execute engine code. Invalid archives and missing bridge
|
|
classes report JAR-specific recovery; classpath inventory alone does not prove
|
|
that every class or engine operation will load successfully. Discovery limits
|
|
each ZIP metadata read to 8 MiB before allocation; a larger directory reports
|
|
the exhausted read budget and the selected JAR path.
|
|
|
|
The current metadata bridge is verified on macOS arm64 with OpenJDK 21 and the
|
|
public Appium ApiDemos fixture. The POSIX adapter also supports Linux; the new
|
|
bridge has not yet undergone real verification on Linux/JDK 25.
|
|
Windows x64 uses an owned Job Object with bidirectional stdio, atomic job
|
|
assignment and kill-on-owner-close. Real Windows verification uses JDK 21 and
|
|
the pinned JADX 0.7.1 JAR; see the real verification lane below.
|
|
|
|
## Answer an analyst question
|
|
|
|
```sh
|
|
rea inspect-android-package /path/Example.apk
|
|
rea search-android-classes /path/Example.apk MainActivity
|
|
rea inspect-android-class /path/Example.apk example.MainActivity
|
|
rea inspect-android-method /path/Example.apk example.MainActivity onCreate
|
|
rea inspect-android-method /path/Example.apk example.MainActivity select --overload-index 1
|
|
rea trace-android-references /path/Example.apk example.MainActivity
|
|
rea trace-android-references /path/Example.apk example.MainActivity --method-name onCreate
|
|
rea trace-android-references /path/Example.apk example.MainActivity --method-name select --overload-index 1
|
|
```
|
|
|
|
Equivalent MCP methods are `inspect_android_package`, `search_android_classes`,
|
|
`inspect_android_class`, `inspect_android_method` and `trace_android_references`.
|
|
Every request includes `path`. Class/method operations use `class_name`,
|
|
`method_name` and optional `overload_index` as applicable. For example:
|
|
|
|
```json
|
|
{
|
|
"name": "inspect_android_method",
|
|
"arguments": {
|
|
"path": "/path/Example.apk",
|
|
"class_name": "example.MainActivity",
|
|
"method_name": "select",
|
|
"overload_index": 1
|
|
}
|
|
}
|
|
```
|
|
|
|
Use `inspect_android_class` to choose an overload. REA rejects an ambiguous
|
|
method request rather than silently selecting the first match. Method reference
|
|
queries accept the same `overload_index`; the bridge resolves the selected
|
|
method node and the result reports its index and displayed signature. These
|
|
indices are tied to the current artifact and engine, not stable across builds.
|
|
|
|
The engine's smali fallback also joins same-name overloads; REA rejects that
|
|
fallback for an overloaded method instead of attributing all bodies to one
|
|
selected overload. Native/abstract methods with no decompiled body report
|
|
`body_status: "not_available"` without inferring which implementation flag caused it.
|
|
|
|
Static initializers (`<clinit>`) return smali with the
|
|
`static_initializer_disassembly` marker. Java decompilation can move their
|
|
instructions into field declarations, leaving an empty or incomplete method
|
|
body. The selected method's disassembly preserves those instructions and retains
|
|
the same source byte limit and explicit truncation reporting.
|
|
|
|
## Interpret the result
|
|
|
|
- Package inspection retains decoded manifest XML, summary declarations,
|
|
permissions and class/resource counts. Summary fields are extracted by the
|
|
upstream engine and should be checked against the retained XML when needed.
|
|
Signature verification is explicitly `not_performed`.
|
|
- Class search consumes all pages and checks their counts. An empty `query`
|
|
inventories all class-name rows; class names are decompiler representations.
|
|
Repeated names remain repeated in `matches` so the inventory row count and
|
|
producer evidence stay visible. `duplicate_names` identifies matching names
|
|
with multiple rows and their occurrence counts; a name alone does not
|
|
distinguish which provider class node a later lookup selects.
|
|
- Class inventories read parsed metadata without generating source and return
|
|
full reported display types, fields and inner class names. Synthetic members
|
|
that code generation hides can appear in the inventory; it stays consistent
|
|
after method decompilation within the retained session.
|
|
Exact DEX prototypes are reported from frozen JADX method metadata as
|
|
`dex_descriptor`, for example `(I)V`. Display names can be JADX aliases;
|
|
the descriptor alone is not a complete raw DEX method ID. Overload indices
|
|
are tied to this artifact and engine, not stable identities across builds.
|
|
- Method inspection reports Java or smali, fallback markers and complete/partial
|
|
returned text. Decompiled source is a derived representation. A complete text
|
|
result does not establish complete semantics or a byte-identical reconstruction.
|
|
- Reference results are incoming static relationships between provider nodes.
|
|
`reference_unit=using_node` means `total` counts containing JADX nodes, not
|
|
individual invoke instructions; two calls from one method can appear as one
|
|
entry. `coverage=complete` covers this node inventory only. For a method
|
|
reference whose using node has raw code, `direct_invokes` lists matching DEX
|
|
invoke instructions with their DEX entry, raw source method ID, code-unit
|
|
offset, method index and opcode. An empty array means no direct invoke was
|
|
matched in that node; `null` means raw code was unavailable or the using node
|
|
was not a method. This list does not establish complete call-site coverage
|
|
outside JADX's using-node inventory or through reflection and dynamic loading.
|
|
A virtual or interface invoke also does not establish its runtime implementation.
|
|
Source lines and the containing node's own `dex_offset` remain unknown.
|
|
Method-reference results also carry the selected `dex_descriptor`;
|
|
class-reference results report `null`.
|
|
These relationships are not observed runtime calls.
|
|
- Original requests, APK SHA-256, actual engine JAR SHA-256 and raw producer
|
|
responses remain in Evidence. Source revision is populated only for bytes
|
|
matching the audited release JAR.
|
|
|
|
If a later step fails, the error's `details.partial_observation` retains the
|
|
selected and resolved APK paths, digest, observed provider identity and versions,
|
|
and each upstream request and reply collected before the failure. Failed replies
|
|
remain available even when their payload cannot be interpreted. Cancellation
|
|
also stops target hashing before provider admission.
|
|
|
|
These operations do not execute the APK or provide split APK/AAB handling,
|
|
signature validation, full resource-table semantics, native-library analysis or
|
|
Android runtime capture. An `.aab` suffix is a ZIP archive, not an APK, so
|
|
Android tools and the application graph do not treat it as a standalone APK.
|
|
Existing artifact inventory/extraction tools can supply archive evidence.
|
|
Inventory records an APK root only for ZIP bytes whose name ends in `.apk`.
|
|
The projection adds a limitation when no root `AndroidManifest.xml` was
|
|
observed, because such a ZIP may not be an APK. Nested members take a ZIP or
|
|
ASAR family from their bytes; an unread member keeps an unknown format.
|
|
Inventory keeps ZIP names that differ only in case and records that a
|
|
case-insensitive destination cannot store both spellings. Extraction fails
|
|
only when that destination directory cannot keep both names. Project that
|
|
Evidence with the execution-free inventory tool:
|
|
|
|
```sh
|
|
rea project-android-application-graph '{"inventory_evidence":[<inventory_artifact Evidence>]}'
|
|
```
|
|
|
|
```json
|
|
{
|
|
"name": "project_android_application_graph",
|
|
"arguments": {
|
|
"inventory_evidence": ["<inventory_artifact Evidence>"]
|
|
}
|
|
}
|
|
```
|
|
|
|
The projection reports exact component paths and hashes, runtime-family hints,
|
|
and path-based bridge hypotheses. It does not decode DEX or claim observed
|
|
runtime calls. Framework hints match complete native library names, such as
|
|
`libreactnativejni.so`, `libreactnative.so`, `libhermes.so`,
|
|
`libhermes_executor.so`, `libflutter.so`, and `libunity.so`; unrelated names
|
|
such as `libcommunity.so` and `libreactivity.so` do not identify those families.
|
|
The bridge basis is inferred from the native path and repeated for each managed
|
|
component, so it does not identify a specific JNI binding. Bridge hypotheses form a DEX/JVM-class by native-library
|
|
Cartesian product. The projection computes the product and exact serialized
|
|
candidate-array size before creating candidate objects, then retains a
|
|
deterministic prefix within a 2 MiB UTF-8 JSON budget. If candidates are omitted,
|
|
`bridge_candidate_coverage` reports total, emitted, and omitted counts; overall
|
|
coverage is `partial` while component arrays still include every component
|
|
from the supplied inventory pages. A path pair does not establish that a
|
|
managed declaration names, loads, or calls the native library.
|
|
|
|
## Resource and lifecycle limits
|
|
|
|
REA retains one serialized session with private, immutable APK, engine JAR and
|
|
metadata-bridge copies. Matching requests reuse the loaded engine. APK/engine
|
|
digests and JVM configuration are checked before reuse; changed inputs retire
|
|
the previous session. Sessions are cleaned after 60 seconds of inactivity, on
|
|
target/configuration changes, failure, timeout, cancellation, MCP disconnect or
|
|
server shutdown. One-shot CLI commands always join cleanup before returning.
|
|
REA never writes to the original APK or launches target code.
|
|
|
|
On Windows, stdin writes preserve stream backpressure and run outside the Node
|
|
event loop. Cancellation terminates the retained job, including descendants,
|
|
before workspace cleanup. On Windows, a detached REA helper owns the private
|
|
workspace's native directory handle. An abrupt CLI/server owner termination
|
|
closes the Java job and the helper's input pipe; the helper then removes the
|
|
workspace by handle. The real Windows lane checks both effects. If the helper
|
|
also dies or the host loses power before it finishes, private files can remain.
|
|
|
|
If Java cannot start the bridge, REA reports the selected executable and retained
|
|
startup diagnostics. Select a full JDK through `JAVA_HOME` or `PATH`, and use
|
|
`java --list-modules` to confirm `jdk.compiler` is present. A runnable JRE alone
|
|
cannot compile the bridge. Missing or ambiguous class and method selections are
|
|
input failures; discover names with `search_android_classes` and
|
|
`inspect_android_class` before correcting the selected argument.
|
|
|
|
If process cleanup cannot be confirmed, REA retains the workspace and reports its
|
|
location. That provider instance blocks subsequent engine launches until the
|
|
caller resolves the reported resources and starts a fresh instance.
|
|
|
|
The provider uses one JADX analysis worker independently of JVM processor and
|
|
garbage-collector settings. REA supplies no default heap or visible-CPU override;
|
|
Java uses its normal ergonomics and inherits caller JVM settings. Optional
|
|
provider settings add explicit JVM arguments:
|
|
|
|
```sh
|
|
export REA_JADX_HEAP_MIB=8192
|
|
# Optional, independent of the single JADX worker:
|
|
export REA_JADX_ACTIVE_PROCESSOR_COUNT=4
|
|
# Or select settings through Java's standard environment options:
|
|
export JAVA_TOOL_OPTIONS=-Xmx8g
|
|
```
|
|
|
|
Both REA settings must be positive safe integers; processor counts must also fit
|
|
the JVM's signed 32-bit range. REA preserves
|
|
`JAVA_TOOL_OPTIONS`, `JDK_JAVA_OPTIONS` and `_JAVA_OPTIONS` rather than replacing
|
|
them. Java's standard precedence applies: explicit command-line heap settings
|
|
override `JAVA_TOOL_OPTIONS`, while `_JAVA_OPTIONS` can override command-line
|
|
settings. Evidence reports the worker's observed `Runtime.maxMemory()` rounded
|
|
down to MiB and retains exact bytes, Java version and visible processors in its
|
|
raw runtime response. The observed allocatable heap can differ from configured
|
|
`-Xmx`, especially with Serial GC. Heap is not a total RSS cap: the JVM also
|
|
uses native memory. Independent REA instances have independent queues. Whole
|
|
operations have a **120-second** execution deadline after reaching the queue's
|
|
front; upstream decompilation is limited to **90 seconds**.
|
|
|
|
Decoded manifest and method text have a **1 MiB** upstream byte budget and report
|
|
truncation. Class-name pages use indexed slices of up to 4,096 names without changing the
|
|
complete, case-sensitive search contract. A protocol frame above **8 MiB**, or combined stdout/stderr above
|
|
**32 MiB per operation**, fails with a diagnostic instead of returning an incomplete inventory
|
|
as complete. These are memory-safety boundaries, not undocumented result caps.
|
|
|
|
## Real verification
|
|
|
|
Download the explicit public fixtures once; this installs no tools:
|
|
|
|
```sh
|
|
npm run fixtures:android
|
|
export REA_JADX_MCP_JAR="$PWD/_reference/apk-integration/jadx-headless-mcp-0.7.1-all.jar"
|
|
export REA_ANDROID_TEST_APK="$PWD/_reference/apk-integration/ApiDemos-debug.apk"
|
|
npm run verify:android
|
|
```
|
|
|
|
The lane also uses the selected full JDK's `jlink` to create a disposable JRE
|
|
without the compiler module. It checks real Java startup failures, missing
|
|
selectors, recovery after failures, and CLI cancellation with owned cleanup.
|
|
The original APK and host Java configuration remain unchanged.
|
|
|
|
The same lane runs on Windows x64 with the bundled native controls and a full
|
|
JDK. It checks CLI/MCP results, real MCP cancellation and disconnect, and Java
|
|
exit and workspace removal after abrupt CLI owner termination. Windows forced
|
|
termination is checked separately from the POSIX CLI SIGTERM handler. A
|
|
synthetic two-owner test confirms that one owner's exit does not remove the
|
|
other owner's workspace. The lane does not claim a structured cancellation
|
|
result or power-loss recovery.
|
|
|
|
The download script verifies fixed SHA-256 values, reuses matching files and
|
|
refuses to overwrite a different existing file. APK/JAR files live under ignored
|
|
`_reference/` and are excluded from package contents. The lane exercises actual
|
|
REA CLI and MCP results with the real engine, separately from synthetic protocol
|
|
regressions. It needs no Gradle build, emulator, Android SDK or Ghidra.
|
|
|
|
Direct invoke sites retain exact DEX code-unit offsets separately from each
|
|
containing node's unknown `dex_offset`. `direct_invokes_exhaustive: false`
|
|
means instruction rows were bounded by the local bridge frame budget; `null`
|
|
means the producer did not establish instruction coverage. Using-node totals
|
|
and coverage do not imply complete instruction evidence.
|