跳转至

静态 Android APK 分析

REA 通过 src/android/ 包装了一个现成的无头 JADX 引擎。其源代码以未修改、提交固定的 Git 子模块形式保留;请参阅上游来源。CLI 和 MCP 使用相同的工作流,并以内联方式返回绑定工件的 Evidence。

该系列在仓库 main 分支和 npm 4.1.0 中可用。在选择它之前,请检查已发布包边界和所连接服务器的工具列表。

在 Linux 上,JADX 会启动一个 REA 拥有的进程,并且需要在 REA 的 PATH 中存在兼容 procps 的 ps;请参阅共享的 Linux 进程前置要求。

可选包交付

MCP 的 inspect_android_package 请求在省略 detail 或设置为 "complete" 时保留完整输出。显式选择 detail: "summary" 可接收所有包、SDK 和权限声明、引擎标识以及类/资源计数,而不含已解码的 manifest 文本或重复的原始 provider 回复。保留型会话会保留完整的原始 Evidence,并返回一个不同的、关联的 inspect_analysis_view 摘要。这会改变交付方式,而非 JADX 分析本身。

使用返回的 normalized_result.parent_evidence_id 配合 inspect_analysis_view、来源类型 retained-evidence 和 view: {kind: "android", facet: "manifest", offset: 0, limit: 4096},即可在不重新打开 APK 的情况下读取精确解码的 manifest。偏移量和限制以 UTF-16 代码单元计数;请遵循 coverage.next_offset。该页面会保留 source_status 和 reported_total_bytes:耗尽保留文本并不会使部分解码的 manifest 变得完整。完整的源 Evidence 在现有会话生命周期内仍可导出。不支持保留的服务器会在启动 JADX 之前拒绝 summary。一次性 CLI 输出保持完整。较旧的服务器在 schema 发现后仍需要原有的完整请求。APK 的真实性和运行时行为仍未验证。

Manifest 组件投影

inspect_android_package 还会从解码后的 manifest 中返回所声明 activities、aliases、services、receivers 和 providers 的静态清单。declared_name 保留源属性;仅当 manifest 包已知且解析后的副本符合 UTF-8 字节预算时,name 才会解析相对名称。未解析的包或已耗尽的名称预算会保留声明名称,并将清单标记为 partial。空的组件名称以及包含被禁止 DEX 字符的名称会保留其原始拼写,并且不会被当作标识。有效的 DEX 空格(包括边界空格)在解析组件名称和匹配别名目标时会得到保留。Manifest 包中的空白由其独立的包语法拒绝。当没有声明组件权限时,组件权限会继承应用权限,但没有任何权限的 activity alias 除外:Android 将该情况说明为通过该别名无需任何权限。permission_source 区分本地声明、继承值、无要求(包括显式空覆盖)以及因资源/manifest 占位符令牌未解析或已达到总权限字节预算而省略的值。资源和占位符引用不会在此解析器边界展开。重复的继承值共享一个整个清单范围的字节预算。Provider 的 android:readPermission 和 android:writePermission 覆盖不会作为单独字段投影;它们的存在会使清单标记为 partial,并在 limitations 中明确指出。带有周围空白的 manifest 包不会用于解析相对组件名称。类名段遵循 DEX SimpleName 字符(包括前导数字或连字符),而不是 Java 源标识符规则。相对名称展开并不证明类存在、DEX 版本兼容性或运行时可加载性。SDK 默认值接受 JADX 的十进制和十六进制整数拼写;原始 SDK 声明字符串保持不变。

exported 是声明的字面量 true 或 false,否则为 null。 exported_default 仅在可以确定时才是文档记录的默认值;对于未知的 provider 目标 SDK、非字面量声明,或包含针对 API 31 或更高版本过滤器却省略了现在必需的显式声明的组件,它保持为 null。一条 limitation 会记录后一种验证错误。 Provider authorities 和所有清单维度都有界。status: "partial" 和 limitations 标识被省略的声明或值;容量限制仅在达到实际收集限制时出现。该投影不会合并 manifest、展开资源引用,也不会观察运行时 enabled/export 状态。

service 的 foreground_service_type 字段保留声明的属性,包括空值或周围空格;null 表示该属性不存在或该组件不是 service。此字段不验证 service 类型令牌,也不确定运行时资格。

供应工具

请使用完整的 JDK 17 或更新版本(包括其编译器模块)以及 jadx-headless-mcp 0.7.1。使用前,请根据 REA 固定的发布元数据验证下载的 JAR 的 SHA-256。以下下载和验证命令适用于 npm 包使用者以及仓库检出;仅限仓库的 npm run fixtures:android 辅助命令不包含在 npm 包中。

在 macOS 或 Linux 上,选择一个目标位置并验证固定的摘要:

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"

在 Windows 上,请在 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 以源文件模式针对未修改的引擎 JAR 运行其打包的 Java 元数据桥。REA 不会安装 Java、Android SDK、JADX、设备服务或模拟器。请在 shell 或 MCP 服务器环境中设置路径:

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

在 Windows x64 上,请使用 REA 包及其配套捆绑的 Windows 原生控件,包括自有协议 stdin 支持。此 Windows 边界是仓库 main 分支上的新内容;旧版本发布的包仍可能拒绝此主机。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 会选择 JAVA_HOME\bin\java.exe,或在所选 Windows PATH 上解析 java.exe。若原生构件缺失或不匹配,会报告包前置条件,而不会尝试发起不属于自身的 Java 启动。Windows arm64 不在该提供方当前的原生边界之内。

就绪检查会在所选 JAR 的 ZIP 目录中,检查 REA 元数据桥所消费的类以及 Java 的编译器和版本模块。它不会加载 APK 或执行引擎代码。无效归档和缺失的桥接类会报告针对 JAR 的恢复方式;仅凭类路径清单并不能证明每个类或引擎操作都能成功加载。发现过程在分配前将每次 ZIP 元数据读取限制为 8 MiB;目录更大时会报告读取预算已耗尽和所选 JAR 路径。

经过筛选的类页面桥(协议 2)已在 Linux x86-64 上使用 Temurin 21、公开的 Appium ApiDemos 6.0.18 和 NewPipe 0.29.1 APK 验证。早期元数据桥记录覆盖了 macOS arm64/OpenJDK 21 和 Windows x64/JDK 21;此协议修订版尚未在这些主机或 Linux/JDK 25 上重新运行。Windows x64 使用自有的作业对象,并具备双向 stdio、原子作业分配和所有者关闭即终止。真实 Windows 验证使用 JDK 21 和固定的 JADX 0.7.1 JAR;请参阅下面的真实验证通道。

回答分析师问题

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

等效的 MCP 方法是 inspect_android_package、search_android_classes、inspect_android_class、inspect_android_method 和 trace_android_references。每个请求都包含 path。类/方法操作视情况使用 class_name、method_name 和可选的 overload_index。例如:

{
  "name": "inspect_android_method",
  "arguments": {
    "path": "/path/Example.apk",
    "class_name": "example.MainActivity",
    "method_name": "select",
    "overload_index": 1
  }
}

使用 inspect_android_class 来选择重载。REA 会拒绝有歧义的方法请求,而不是悄悄选择第一个匹配项。方法引用查询接受相同的 overload_index;桥接器解析所选方法节点,结果会报告其索引和显示的签名。这些索引与当前产物和引擎绑定,不会跨构建保持稳定。

Smali 回退会先使用原始方法名和 DEX 描述符选择一个完整代码块,然后再应用源代码字节限制。这会在 Java 代码生成重命名方法时保留所选重载。如果所选 Java 方法体被省略(包括方法被合并到合成桥中的情况),REA 会返回其原始 smali,并带有 java_body_unavailable 标记。该标记仅说明缺少 Java 代码,并不推断方法被省略的原因,也不指明替代方法。无法解析或重复的反汇编标识会失败,而不会将另一个方法的指令归属于该选择。

native 和 abstract 方法通过代码生成之前捕获的访问标志进行识别。它们报告 body_status: "not_available"、空源代码以及 native_method 或 abstract_method 标记;其声明不会经过引擎的源代码切片器。其他缺失的反编译方法体不会确立任一访问标志。

静态初始化器(<clinit>)返回带有 static_initializer_disassembly 标记的 smali。Java 反编译可能将其指令移入字段声明,留下空或不完整的方法体。所选方法的反汇编会保留这些指令,并保持相同的源代码字节限制和明确的截断报告。

解读结果

  • 包检查会保留解码后的清单 XML、摘要声明、权限以及类/资源计数。REA 的桥接器按原始归档名称和清单类型选择确切的根 AndroidManifest.xml 资源,与资源顺序无关。一次支持命名空间的 JDK XML 解析即可提供摘要属性和直接的权限/SDK 声明;命名空间前缀和 XML 实体的拼写不会改变它们的值。原始解码后的 XML 会被原样保留,只有其显示的文本受现有源代码字节上限约束。摘要使用该上限之前的完整选定 XML。不支持文档类型;解析不会加载外部 DTD、schema 或 XInclude 文件。签名验证明确为 not_performed。
  • 类搜索会在传输页面前过滤引擎缓存的顶级名称,然后沿连续的清单位置进行,而不会重新扫描较早的行。在报告完整覆盖范围之前,它会检查选择、范围、已检查区间和计数。完整的生产者回复保留在 Evidence 中;空的 query 会列出所有类名行;类名是反编译器的表示形式。重复名称在 matches 中仍然重复出现,以便清单行数和生产者证据保持可见。duplicate_names 标识具有多行的匹配名称及其出现次数;仅凭名称无法区分后续查询选择的是哪个提供方类节点。
  • 类清单读取已解析的元数据而不生成源代码,并返回完整报告的显示类型、字段和内部类名称。代码生成隐藏的合成成员可能出现在清单中;在保留会话中,它在方法反编译后保持一致。类和方法的检查及引用跟踪接受报告的确切类名或其原始二进制名称,包括嵌套类和匿名类中的 $。桥接器保留 JADX 中的两种身份;它不会替换分隔符或猜测后缀匹配。有歧义的选择器会被拒绝。后续调用保留所选名称,而结果报告冻结的 JADX 显示名称。在 shell 中引用含 $ 的名称,例如 'com.example.Manager$WorkerThread'。精确 DEX 原型从冻结的 JADX 方法元数据中作为 dex_descriptor 报告,例如 (I)V。显示名称可能是 JADX 别名;仅凭描述符并不是完整的原始 DEX 方法 ID。重载索引与当前产物和引擎绑定,而不是跨构建的稳定身份。
  • 方法检查报告 Java 或 smali、回退标记以及完整/部分返回文本。反编译源代码是一种派生表示。完整文本结果并不表示完整语义或逐字节一致的重建。
  • 引用结果是提供方节点之间的传入静态关系。reference_unit=using_node 表示 total 计数的是包含 JADX 节点的数量,而不是单个 invoke 指令;同一方法中的两次调用可能显示为一个条目。coverage=complete 仅涵盖此节点清单。对于使用节点具有原始代码的方法引用,direct_invokes 会列出匹配的 DEX invoke 指令及其 DEX 条目、原始源方法 ID、代码单元偏移量、方法索引和操作码。空数组表示在该节点中未匹配到直接 invoke;null 表示原始代码不可用或使用节点不是方法。此列表并不建立 JADX 使用节点清单之外、或通过反射和动态加载的完整调用点覆盖。虚拟或接口 invoke 也不能确立其运行时实现。源代码行和包含节点的 dex_offset 仍然未知。方法引用结果还带有选定的 dex_descriptor;类引用结果报告 null。这些关系不是观察到的运行时调用。
  • 原始请求、APK SHA-256、实际引擎 JAR SHA-256 和原始生产者响应保留在 Evidence 中。仅当字节与经过审计的发布 JAR 匹配时,才会填充源代码修订版本。

如果后续步骤失败,错误的 details.partial_observation 会保留已选择并解析的 APK 路径、摘要、观察到的提供方身份和版本,以及失败前收集到的每个上游请求和回复。即使回复负载无法解读,失败的回复仍然可用。取消操作还会在提供方准入之前停止目标哈希计算。

这些操作不会执行 APK,也不提供拆分 APK/AAB 处理、签名验证、完整资源表语义、原生库分析或 Android 运行时捕获。带有 .aab 后缀的文件是 ZIP 归档,而不是 APK,因此 Android 工具和应用图不会将其视为独立 APK。现有的制品清单/提取工具可以提供归档证据。清单仅对名称以 .apk 结尾的 ZIP 字节记录 APK 根。当未观察到根级 AndroidManifest.xml 时,投影会添加一条限制,因为此类 ZIP 可能不是 APK。嵌套成员根据其字节内容确定属于 ZIP 或 ASAR 家族;无法读取的成员保持格式未知。清单会保留仅大小写不同的 ZIP 名称,并记录不区分大小写的目标位置无法同时存储这两种拼写。仅当目标目录无法同时保留这两个名称时,提取才会失败。

使用不执行代码的清单工具投影该 Evidence:

rea project-android-application-graph '{"inventory_evidence":[<inventory_artifact Evidence>]}'
{
  "name": "project_android_application_graph",
  "arguments": {
    "inventory_evidence": ["<inventory_artifact Evidence>"]
  }
}

投影会报告精确的组件路径和哈希、运行时家族提示以及基于路径的桥接假设。文件和切片出现项提供组件端点;目录树和符号链接在源清单中保留其身份,而不会被归类为代码文件。它不会解码 DEX,也不会声称观察到运行时调用。DEX/JVM 运行时提示总结已分类的字节码组件列表。框架提示匹配完整的原生库名称,例如 libreactnativejni.so、libreactnative.so、libhermes.so、libhermes_executor.so、libflutter.so 和 libunity.so;不相关的名称(如 libcommunity.so 和 libreactivity.so)不能识别这些家族。签名列表保留 Android v1 验证器选择的 META-INF/MANIFEST.MF、.SF、.RSA、.DSA 和 .EC 路径,包括嵌套路径。这些是清单元数据候选,而不是证明 v1 签名验证通过或 APK 已签名;请使用 inspect-apk-signature 进行签名分析。桥接基础是从原生路径推断的,并为每个受管组件重复生成,因此它不能识别特定的 JNI 绑定。桥接假设构成 DEX/JVM 类与原生库的笛卡尔积。投影在创建候选对象之前计算该乘积和精确的序列化候选数组大小,然后在 2 MiB UTF-8 JSON 预算内保留确定性前缀。如果省略了候选,bridge_candidate_coverage 会报告总数、已输出数和已省略数;在组件数组仍包含来自所提供清单页面的每个组件时,整体覆盖率为 partial。路径对并不能证明受管声明命名、加载或调用了该原生库。

资源与生命周期限制

REA 保留一个序列化会话,其中包含私有且不可变的 APK 副本、引擎 JAR 副本和元数据桥接副本。匹配的请求会复用已加载的引擎。APK/引擎摘要和 JVM 配置在复用前进行检查;输入变更会使先前会话失效。会话在空闲 60 秒后,或在目标/配置变更、失败、超时、取消、MCP 断开连接或服务器关闭时被清理。一次性 CLI 命令总是在返回前参与清理。REA 从不写入原始 APK,也不启动目标代码。

在 Windows 上,标准输入写入保留流背压,并在 Node 事件循环之外运行。取消会在工作区清理之前终止保留的作业,包括后代进程。在 Windows 上,分离的 REA 辅助进程拥有私有工作区的原生目录句柄。CLI/服务器所有者突然终止会关闭 Java 作业和辅助进程的输入管道;辅助进程随后通过句柄移除工作区。真实的 Windows 通道会检查这两种效果。如果辅助进程也死亡,或主机在完成前断电,则私有文件可能会残留。

如果 Java 无法启动桥接,REA 会报告所选可执行文件和保留的启动诊断信息。请通过 JAVA_HOME 或 PATH 选择完整 JDK,并使用 java --list-modules 确认 jdk.compiler 存在。仅靠可运行的 JRE 无法编译桥接。缺失或模糊的类和方法选择属于输入失败;请先使用 search_android_classes 和 inspect_android_class 发现名称,再更正所选参数。

如果无法确认进程清理,REA 会保留工作区并报告其位置。该提供方实例会阻止后续引擎启动,直到调用方处理所报告的资源并启动新实例。

该提供方使用一个 JADX 分析工作器,与 JVM 处理器和垃圾回收器设置无关。REA 不提供默认堆或可见 CPU 覆盖;Java 使用其正常的自适应行为,并继承调用方的 JVM 设置。可选的提供方设置可添加显式 JVM 参数:

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

两个 REA 设置都必须是正安全整数;处理器数量还必须适合 JVM 的有符号 32 位范围。REA 保留 JAVA_TOOL_OPTIONS、JDK_JAVA_OPTIONS 和 _JAVA_OPTIONS,而不是替换它们。Java 的标准优先级适用:显式命令行堆设置覆盖 JAVA_TOOL_OPTIONS,而 _JAVA_OPTIONS 可以覆盖命令行设置。Evidence 报告工作器观察到的 Runtime.maxMemory()(向下取整到 MiB),并在其原始运行时响应中保留精确字节、Java 版本和可见处理器。观察到的可分配堆可能与配置的 -Xmx 不同,尤其是在 Serial GC 下。堆不是总 RSS 上限:JVM 还使用原生内存。APK 加载期间的类型化 JVM 内存分配失败会报告 resource_constraint 及观察到的最大堆字节数。已知的堆失败会在修复建议中指明 REA_JADX_HEAP_MIB;其他内存区域仍然未知。健康的 doctor 配置检查并不能确定任何特定 APK 一定适合。桥接之前的失败可以报告类型化结果,其他操作的上游错误保留其现有诊断信息;仅凭错误文本不能确定内存耗尽。

独立的 REA 实例具有独立的队列。默认情况下,整个操作有一个 120 秒 的执行截止时间,从它们到达队列前端后开始计时。对于已知的缓慢冷加载(例如大型 multidex APK),请选择另一个有限截止时间:

export REA_JADX_OPERATION_TIMEOUT_MS=600000

该值必须是从 1 到 2147483647 的十进制毫秒计数;其他任何值都会在 Java 启动之前被拒绝。REA 从进程启动时捕获的环境中读取该值。更改在下次进程启动时生效;已在运行的服务器会保留其启动时的截止时间。同一截止时间约束每次上游调用。超时信息会指明 REA_JADX_OPERATION_TIMEOUT_MS、实际生效的毫秒数以及该范围。rea doctor 中通过的 JADX 配置检查会报告实际生效的 operation_timeout_ms。显式取消仍会立即结束操作,部分观察结果和所属进程清理保持不变。提高截止时间并不会延长桥接的 30 秒启动握手或上游 90 秒 反编译限制。MCP 客户端会执行自身的工具调用超时,且安装过程中生成的客户端工具超时不包含此设置,因此在选择更长的截止时间时,也应提高客户端超时。

解码后的 manifest 和方法文本具有 1 MiB 的上游字节预算,并会报告截断。类名页面使用最多 4,096 个名称的索引切片,且不改变完整的、区分大小写的搜索契约。超过 8 MiB 的协议帧,或每次操作合并的 stdout/stderr 超过 32 MiB,都会失败并给出诊断信息,而不会将不完整的清单当作完整结果返回。这些是内存安全边界,而非未成文的结果上限。

真实验证

下载一次指定的公共测试样本;此过程不会安装任何工具:

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

主测试线(main lane)和 Windows Java 选择检查通过 validateVerifierJadxConfiguration 验证所选 JDK 和引擎归档。这个共享验证器边界为生产验证器提供资源所有者,并等待保留的清理完成后再继续。失败的关闭操作会保留其确切的所有者以便重试;前置条件失败绝不能误认为已完成的桥接测试。恢复检查会匹配带类型的 Java 或归档失败及其适用的补救措施。

该测试线还使用所选完整 JDK 的 jlink 创建一个不包含编译器模块的一次性 JRE。它检查真实的 Java 启动失败、缺失的选择器、失败后的恢复,以及带所属进程清理的 CLI 取消。原始 APK 和宿主机 Java 配置保持不变。

同一测试线在 Windows x64 上运行,使用捆绑的原生控件和完整 JDK。它检查 CLI/MCP 结果、真实的 MCP 取消和断连,以及 CLI 所有者进程被突然终止后的 Java 退出和工作区移除。Windows 强制终止与 POSIX CLI SIGTERM 处理程序分开检查。一个合成的双所有者测试确认一个所有者的退出不会移除另一个所有者的工作区。该测试线并不宣称具备结构化取消结果或断电恢复能力。

下载脚本验证固定的 SHA-256 值,复用匹配的文件,并拒绝覆盖不同的现有文件。APK/JAR 文件位于被忽略的 _reference/ 目录下,并被排除在包内容之外。该测试线使用真实引擎演练实际的 REA CLI 和 MCP 结果,与合成协议回归测试分开。它不需要 Gradle 构建、模拟器、Android SDK 或 Ghidra。

direct invoke 调用点保留精确的 DEX 代码单元偏移量,与每个包含节点的未知 dex_offset 分开存放。direct_invokes_exhaustive: false 表示指令行受本地桥接帧预算限制;null 表示生产者未建立指令覆盖。使用节点的总数和覆盖度并不表示完整的指令证据。

本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 rea2/rea