先看结论与判断条件

  • 扫描对象必须是准备发布的最终 APK,并绑定文件摘要、包名、版本、变体和构建来源,不能只查源码目录。
  • 资产清单要覆盖 assets、res/raw、未知根目录条目、原生库旁文件和 DEX 文本线索,同时保留类型与大小边界。
  • 规则命中表示需要复核,不等于凭据泄露或个人信息泄露;报告不得回显疑似秘密、令牌和用户数据。
  • 允许规则应精确到路径、风险类别、业务用途和到期条件,禁止用整个目录或文件后缀长期豁免。
  • 扫描器要防止路径穿越、异常压缩比和超大条目造成资源耗尽,解析失败时必须失败关闭。
  • 静态盘点不能替代运行日志、远端配置、网络流量、动态下载资产和服务端存储的验证。

先固定最终候选,再谈有没有残留

调试文件残留的判断必须面对最终 APK。源码仓库中的 debug 目录被删除,并不证明构建产物没有测试配置;相反,仓库中存在测试样本,也不代表它进入了发布包。资源合并、构建变体、第三方 AAR、生成任务和复制脚本都会改变 ZIP 条目。门禁应在候选生成后执行,读取实际条目并记录 APK 摘要,让每一项发现都能回到唯一文件。

候选身份至少包括 SHA-256 摘要、package、versionCode、构建变体和对应构建记录。若发布过程从 AAB 派生设备 APK,还要说明分析的是通用 APK、特定设备 APK 还是 APK set 中的某个 split。Android App Bundle format 说明 AAB 包含 base、feature、配置和资产模块,而设备安装的是按配置派生的 APK 集,因此单独查看 base 模块可能遗漏动态特性或资产包中的材料。

门禁的结论要用准确语言。扫描器能证明的是在给定 APK、给定工具版本和给定规则下看到了哪些路径或文本类别。它不能仅凭文件名证明内容真实可用,不能判断疑似令牌是否仍有效,也不能证明所有运行时数据都不存在。发现项应写成“需复核线索”,经人工核对与项目证据后再决定删除、脱敏、保留或阻断。

资产盘点的输入身份
输入最低记录用途缺失时决定
最终 APK摘要、包名、版本、大小固定被检查对象不得输出通过结论
构建变体build type、flavor、渠道解释资源组合差异标记来源不可追溯
AAB 或 APK set摘要、生成命令、设备规格覆盖派生与 split 关系限制结论到已检查文件
允许规则版本、路径、类别、责任人区分已批准材料命中项默认未批准
提取工具名称、版本、参数保证结果可重复保留工具证据缺口
构建证明主体摘要、构建者、材料连接产物与来源不能完成供应链归因

建立覆盖完整 ZIP 的资产地图

APK 本质上是带有 Android 特定结构的 ZIP 容器。盘点不应只列 assets,因为调试材料可能出现在 res/raw、根目录、META-INF 附加文件、嵌套压缩包或库自带资源中。第一层扫描应枚举所有非目录条目,记录规范化路径、未压缩大小、压缩大小、CRC、文件后缀和所属区域。这样既能发现未知位置,也能为不同版本的资产差分提供稳定基线。

目录分类需要服务于决定,而不是制造假精确。assets 与 res/raw 通常由应用直接读取,lib 包含 ABI 对应的本地库,classes*.dex 承载字节码,META-INF 主要承载签名或元数据,其他根目录条目需要解释来源。文件位于正常目录并不自动安全,未知目录也不自动恶意。分类只负责把检查责任路由给正确的人,最终判断仍基于内容、业务用途和实际加载路径。

枚举器必须处理不可信容器。条目名含绝对路径、父目录跳转或异常分隔符时应立即阻断;条目声明大小、累计解压大小和压缩比需要上限,避免恶意或损坏文件耗尽内存。对于超过文本检查上限的文件,只记录元数据并要求专门工具处理,不能截取一小段后声称整文件安全。任何 CRC、读取或解码错误都应出现在报告中并使门禁失败。

APK 条目区域与主要复核问题
区域常见对象重点线索判断边界
assets模型、网页、配置、证书测试环境、备份、可读配置文件存在不等于被运行时使用
res/raw原始资源与内置数据诊断样本、调试响应、内部说明资源 ID 不说明业务敏感度
根目录未知条目构建输出与复制产物dump、trace、backup、临时包未知位置需要来源解释
classes DEX应用与依赖字节码环境名、日志标签、敏感键名文本线索不能还原完整数据流
lib按 ABI 打包的本地库旁置配置、符号或诊断材料库文件本身需另用二进制工具
META-INF签名与组件元数据非预期清单或许可证外文件正常元数据不应被误报为秘密

文件名规则只做分流,不能代替内容判断

debug、staging、mock、testdata、trace、dump、backup 和 sample 等词适合作为低成本分流规则。它们能快速指出可能来自测试流程的材料,但误报很常见:正式功能可能包含 sample 音频,崩溃分析 SDK 可能合法使用 trace 名称,开源许可证也可能带 test 字样。因此路径命中应记录 category=name-review,并进入人工核对,不应直接登记凭据泄露。

真正需要优先处理的是与业务上下文组合后的线索。例如 assets/staging/payment.json 同时出现环境词、业务域和可读配置,比单独的 sample.png 更值得阻断;res/raw/server_response_dump.txt 若包含真实用户字段名称或会话头,则需要确认是否为合成数据、是否仍被代码引用。报告应显示路径、类别和规则,不回显内容片段,避免扫描系统成为第二个敏感信息存储点。

允许规则必须精确。若某个公开演示 JSON 确实是产品功能,应批准完整路径、内容摘要、业务用途、维护人和到期条件;不要批准所有 *.json 或整个 assets 目录。文件内容发生变化时摘要不匹配,旧批准自动失效。临时保留的诊断资产需要明确删除版本,到期后恢复阻断,避免一次例外永久掩盖后续新增文件。

文本扫描要找类别,不要收集秘密

对体积受控的文本资产,可以检测敏感字段类别与环境线索,例如 api key、client secret、authorization、password、localhost、staging 和 internal host。扫描器只需报告匹配规则与文件路径,无需保存匹配值。即使规则命中,也可能只是配置 schema、文档样例或无效占位;只有结合文件来源、实际值形态、引用路径和服务端状态,才能判断是否构成可利用的泄露。

二进制文件不能简单按 UTF-8 文本读取。DEX 和原生库中可能包含可打印字符串,但压缩、编码、分段和运行时拼接都会造成漏报,随机字节也会带来误报。对 DEX 可以做受限的关键字存在性检查,作为进一步静态分析的线索;对图片、模型、数据库和本地库,应使用对应格式解析器。一次通用正则扫描不应被包装成全量敏感数据检测。

日志材料需要单独关注。OWASP MASWE-0001 指出生产日志可能暴露凭据、令牌、个人信息和内部诊断数据,缓解方向包括删除、降级或脱敏。APK 内的旧 log、trace、dump 或抓包样本即使不会在运行时继续写入,也可能已经固化了历史数据。命中这类文件时应先隔离候选、限制报告可见范围,再由责任人确认数据来源和删除方式。

文本线索的证据等级
线索扫描器可报告仍需确认禁止直接声称
敏感键名路径与规则类别是否有真实值及是否被引用有效凭据已泄露
环境名称staging、mock 或 internal 类别是否为文档、功能或构建残留应用连接了内部系统
日志头字段authorization 或 cookie 类别是否含真实会话材料用户账户已受影响
个人信息字段email、phone 等 schema 类别是否包含真实个人数据存在隐私事件
DEX 关键字DEX 条目与规则类别所属类、引用路径和运行条件代码中保留了完整秘密
证书或密钥文件名扩展名与路径类别文件格式、用途和公私属性私钥一定可用

构建优化会改变可见内容,但不是清理证明

Enable app optimization with R8 说明 R8 会执行代码和资源缩减、优化与名称混淆。发布构建启用优化后,未使用代码和资源可能被移除,字符串与符号布局也可能变化,因此调试资产清单应以优化后的最终 APK 为准。检查未优化中间包得到的命中数量,不能代表商店候选;反过来,R8 输出成功也不能证明自定义复制任务或依赖资产已经被清理。

keep 规则、反射、资源动态查找和 JNI 引用会影响哪些对象能够被安全删除。团队不应为通过扫描而直接删除所有带 debug 名称的资源,否则可能破坏合法诊断或离线功能。正确顺序是先确认文件的生成来源和运行引用,再从源头构建任务、依赖配置或资源目录中移除;随后重建同一变体并验证功能。对 DEX 线索,则应定位日志调用或常量来源,而不是依赖名称混淆遮住文本。

优化前后对比可以帮助定位材料来自何处,但只是一种工程诊断。若候选中出现源码仓库搜索不到的配置,优先检查生成目录、依赖 AAR、打包任务输入和缓存;若源码中有文件而候选没有,也需确认是预期缩减还是错误变体。门禁关注的是最终是否携带不应发布的材料,不把体积减少、名称变化或规则通过扩张为任何抗分析能力结论。

发现残留后的来源排查路径
证据优先检查修复位置重新验证
assets 中测试配置source set 与复制任务变体资源或生成脚本重建并核对条目摘要
res/raw 中诊断样本资源引用与依赖合并源模块或 AAR 配置执行对应功能回归
未知根目录文件自定义打包任务输入任务声明与过滤规则检查完整 ZIP 地图
DEX 日志线索日志调用、常量与 keep 规则代码与日志策略重新扫描并做运行日志测试
依赖带入的模板依赖图与组件版本升级、替换或排除依赖锁定解析结果后重建
动态特性资产feature 模块与设备 split模块构建配置生成目标设备 APK set 再查

AAB 和设备拆分需要独立覆盖

Android App Bundle format 描述 base、feature、配置和资产模块的组织方式。团队上传 AAB 时,用户设备拿到的通常不是单一通用 APK。某个调试文件可能只存在于动态特性模块、语言 split、密度 split 或资产包中,检查 base APK 通过不能覆盖其他派生文件。资产门禁应先检查 AAB 模块清单,再按发布设备范围生成 APK set,枚举其中每个实际交付文件。

bundletool 可以构建、检查和部署 APK set,也能使用设备规格选择 split。门禁回执应记录 bundletool 版本、AAB 摘要、生成参数、设备规格摘要和每个 APK 摘要。如果只生成 universal APK,应明确它与真实商店分发路径的差异。工具在未提供发布签名信息时可能产生用于本地测试的签名结果,因此这类派生文件只能用于内容核对,不能自动冒充商店发布候选。

覆盖策略应由交付模型决定。仅发布单 APK 的项目可以对最终 APK 执行一次完整扫描;使用动态特性和资产交付的项目,需要覆盖 base 与所有计划交付模块;依赖设备配置的项目,则应选取能触发不同资产组合的代表规格。这里的“覆盖”是文件集合覆盖,不是设备兼容通过。真实安装、模块下载和运行时加载仍需另行验证。

用只读扫描器生成可复核发现清单

下面的 Python 脚本读取一个 APK 与审批 JSON,只枚举 ZIP 元数据并对受控大小的条目做有限文本分类。审批文件包含 allowedFindings,每项使用 path 与 category 精确匹配;还可设置 maxEntryBytes 与 maxTotalBytes,防止异常容器耗尽资源。脚本不打印匹配内容、不提取文件到磁盘,也不修改 APK,因此适合在隔离的发布检查环境中生成第一层清单。

规则覆盖可疑文件名、文本中的凭据键名、环境标记、日志字段和 DEX 线索。相同规则在资产文本与 DEX 中使用不同 category,便于安排后续责任人。ZIP 路径异常、重复条目、读取错误、超大条目和超出累计上限都会失败关闭。报告包含 APK 文件名、条目数、发现项与未批准项,不保存疑似秘密值,减少回执本身成为敏感材料的概率。

这段代码是门禁骨架,不是取证工具。实际项目应增加 APK 摘要、签名和包名绑定,并对证书、数据库、模型、本地库和嵌套压缩包使用专用解析器。允许项的批准还应绑定内容摘要与到期条件。脚本未命中只能表示当前规则没有发现线索,不能证明 APK 不含任何敏感资产;规则或输入变化后必须重新运行。

只读扫描 APK 条目与有限敏感文本类别
from pathlib import Path, PurePosixPath
import json
import re
import sys
import zipfile

if len(sys.argv) != 3:
    raise SystemExit(2)
apk_path = Path(sys.argv[1])
policy_path = Path(sys.argv[2])
if not apk_path.is_file() or not policy_path.is_file():
    raise SystemExit(2)
try:
    policy = json.loads(policy_path.read_text(encoding="utf-8"))
except (OSError, UnicodeError, json.JSONDecodeError):
    raise SystemExit(2)
allowed = policy.get("allowedFindings")
max_entry = policy.get("maxEntryBytes", 1048576)
max_total = policy.get("maxTotalBytes", 33554432)
if not isinstance(allowed, list) or not isinstance(max_entry, int) or not isinstance(max_total, int):
    raise SystemExit(2)
allowed_keys = {(item.get("path"), item.get("category")) for item in allowed if isinstance(item, dict)}
name_rules = {
    "debug-name": re.compile(r"(?i)(debug|staging|mock|testdata|trace|dump|backup)"),
    "key-file-name": re.compile(r"(?i)\.(pem|p12|pfx|jks|keystore)$")
}
text_rules = {
    "credential-key": re.compile(rb"(?i)(api[_-]?key|client[_-]?secret|password|authorization)\s*[:=]"),
    "environment-marker": re.compile(rb"(?i)(localhost|staging|internal[_-]?host|mock[_-]?server)"),
    "log-header": re.compile(rb"(?i)(set-cookie|bearer\s+[a-z0-9._-]+|session[_-]?id)")
}
text_suffixes = {".json", ".xml", ".txt", ".properties", ".yaml", ".yml", ".log", ".csv"}
findings = []
seen_paths = set()
total_size = 0

def add(path, category):
    findings.append({"path": path, "category": category})

try:
    with zipfile.ZipFile(apk_path) as archive:
        for info in archive.infolist():
            if info.is_dir():
                continue
            path = info.filename
            parts = PurePosixPath(path).parts
            if path.startswith("/") or ".." in parts or path in seen_paths:
                raise ValueError("unsafe or duplicate path")
            seen_paths.add(path)
            total_size += info.file_size
            if info.file_size < 0 or total_size > max_total:
                raise ValueError("archive size limit")
            for category, pattern in name_rules.items():
                if pattern.search(path):
                    add(path, category)
            suffix = PurePosixPath(path).suffix.lower()
            inspect_text = suffix in text_suffixes or re.fullmatch(r"classes(?:[2-9][0-9]*)?\.dex", PurePosixPath(path).name)
            if inspect_text and info.file_size > max_entry:
                add(path, "oversize-uninspected")
                continue
            if not inspect_text:
                continue
            data = archive.read(info)
            for category, pattern in text_rules.items():
                if pattern.search(data):
                    prefix = "dex-" if suffix == ".dex" else "asset-"
                    add(path, prefix + category)
except (OSError, zipfile.BadZipFile, RuntimeError, ValueError):
    raise SystemExit(2)
unique_findings = sorted({(item["path"], item["category"]) for item in findings})
unapproved = [{"path": path, "category": category} for path, category in unique_findings if (path, category) not in allowed_keys]
report = {"apk": apk_path.name, "entryCount": len(seen_paths), "findings": [{"path": path, "category": category} for path, category in unique_findings], "unapproved": unapproved}
print(json.dumps(report, ensure_ascii=False, indent=2))
if unapproved:
    raise SystemExit(3)

把修复放回生成源,而不是在 APK 上擦除

确认残留后,应在生成源修复。自有资源从对应 source set 删除或改为发布安全版本,生成任务增加明确输入与过滤规则,第三方依赖通过升级、排除或供应商配置处理,日志调用在代码层降级或脱敏。不要解包最终 APK、手工删除条目后再重新压缩,因为这种做法会改变产物身份、破坏原有签名关系,并让构建记录与发布文件失去对应。

修复完成后要重新走完整构建并生成新摘要,再执行资产扫描、功能回归和发布审批。若删除的是模型、网页、证书、规则库或离线数据,还需验证真实加载路径与失败回退;若移除的是日志或诊断配置,则要观察发布模式下是否仍输出同类信息。只看到条目消失,不足以证明功能没有退化,也不证明运行时不会重新下载相同材料。

保留项要有可辩护理由。公开证书、开源许可证、产品内置样例和离线帮助文件可能合理存在,但应记录路径、内容摘要、用途、维护人和审查日期。NIST SP 800-218 SSDF 提倡将安全验证与变更管理纳入开发流程,适合用来约束责任和证据;它不替某个具体文件做决定。没有项目事实时,应保持待复核,而不是根据文件类型猜测安全。

发现项的处置决策
核实结果处置必须保留的证据发布条件
真实凭据或会话材料撤销、删除并调查来源范围、处置与新候选摘要完成凭据处置和重建
含真实个人或诊断数据隔离、删除并按流程评估数据来源和删除回执责任人确认影响范围
仅为 schema 或无值模板可保留或改名内容摘要与引用路径精确规则批准
正式功能公开资产保留业务用途、维护人与摘要功能回归通过
来源不明的二进制或压缩包阻断并追踪生成任务或依赖来源完成专用格式审查
工具无法读取或超限阻断错误回执与文件元数据专用工具复核完成

报告要区分事实、判断和未覆盖范围

发布报告先列事实:APK 摘要、条目总数、扫描区域、规则版本、工具版本、命中路径与类别。随后列工程判断:为什么某项可公开、为何某项必须删除、审批适用于哪个候选。最后列未覆盖范围:超大文件、专用二进制格式、动态特性、运行时下载、网络日志和服务端数据。把三类内容分开,审核者才能知道哪些可重算,哪些需要承担决定责任。

SLSA Provenance v1.1 可用于把候选主体与构建者、构建类型、外部参数和依赖材料关联。资产回执应引用候选主体摘要与构建证明摘要,使报告不会被挪用到重新构建的文件。provenance 只能证明声明的生成关系,不能证明条目内容合理,更不能替代人工核实与运行验证。构建来源、静态资产和动态行为应保留为不同证据类别。

准备评估时,可在御盾中央平台提交最终 APK 或 AAB、文件摘要、目标变体、APK set 生成参数、完整条目清单、依赖解析结果、构建证明及允许规则。登录、注册、申请、价格与控制台动作统一由中央平台承接。若尚无同候选的真实扫描回执,应明确标记未验证,不写成已清除敏感资产、已阻断攻击或已通过商店审核。

事实依据与适用边界

以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。

本文判断事实或工程依据适用限制
AAB 可以包含 base、feature、配置和资产模块,设备安装内容由派生 APK 集组成。Android App Bundle format 描述 App Bundle 的模块结构与分发时 APK 生成关系。AAB 结构说明不代表只检查 base APK 就覆盖了所有设备与动态模块资产。
bundletool 可以从 App Bundle 构建、检查和部署 APK set,并根据设备规格选择 APK。bundletool 描述 build-apks、设备规格、检查和安装相关工作流。工具生成成功不证明派生文件等同于商店候选,也不证明其中资产内容安全。
R8 的发布优化职责包括代码和资源缩减、优化与名称混淆。Enable app optimization with R8 描述 R8 在优化发布构建中的主要职责与输出。R8 成功不证明自定义复制任务、依赖资产和所有敏感文本已经被删除。
生产日志可能暴露凭据、令牌、个人信息和内部诊断数据。OWASP MASWE-0001 描述敏感数据被写入应用日志的风险与删除、降级、脱敏原则。该页面处于 Beta,本文只采用其风险与缓解原则,文件名命中仍需项目核实。
安全发布应把验证、变更管理与供应链风险处理纳入软件开发流程。NIST SP 800-218 SSDF 给出组织级安全软件开发实践框架。SSDF 不定义本扫描器规则,也不证明具体候选、文件或产品已经通过验证。
构建 provenance 可以绑定产物主体、构建者、构建类型、外部参数与依赖材料。SLSA Provenance v1.1 定义 provenance 中相关字段及其关系。来源证明不能单独判断调试材料、敏感资产或运行时数据是否安全。
扫描器应只报告路径和风险类别,不回显疑似秘密值。工程判断:减少回执中的原始内容,可以避免扫描系统复制并扩散待核实的敏感材料。不回显内容会限制远程审核,必要时仍需在受控环境由授权人员查看原文件。
文件名、键名或环境词命中只能作为复核线索,不能直接登记泄露事件。工程判断:schema、公开样例、无效占位与真实数据可能出现相同文本模式。最终结论需要结合内容、来源、引用路径、有效性和项目处置证据。

工程常见问题

只扫描 assets 目录是否足够?

不够。还应覆盖 res/raw、未知 ZIP 根目录条目、动态特性与资产模块、DEX 文本线索及本地库旁文件。每类对象需使用适合其格式的检查方法。

规则命中 api key 字样是否说明密钥已经泄露?

不能。命中可能来自字段 schema、文档或无效样例。应在受控环境确认是否存在真实值、是否仍有效、是否被代码引用,并避免把匹配值写入普通报告。

R8 已启用,是否还需要做资产盘点?

需要。R8 会处理代码和资源缩减、优化与混淆,但自定义复制任务、依赖 AAR、未知条目和仍被引用的资产可能继续进入最终包。门禁要检查优化后的候选。

AAB 项目检查一个 universal APK 就能覆盖所有用户吗?

不一定。动态特性、配置 split 和资产交付可能形成不同文件集合。应先盘点 AAB 模块,再根据发布范围生成并检查代表设备的 APK set。

发现残留后可以直接修改 APK 删除文件吗?

不应这样处理。应在 source set、生成任务、依赖配置或代码源头修复,然后重建、重新绑定摘要并执行扫描和回归,避免产物身份与签名证据失配。

提交敏感资产评估前要准备哪些材料?

准备最终 APK 或 AAB、文件摘要、构建变体、APK set 生成参数、完整条目清单、依赖解析结果、构建证明、允许规则和现有发现回执,再通过御盾中央平台提交。

想用自己的 App 验证?

提交候选包、目标系统和关键业务路径,申请御盾 PoC 与兼容性评估。

继续阅读: APK、DEX 与签名完整性如何一起验收