Android APP常见安全漏洞
这篇笔记最初写于 2024 年,当时主要按漏洞类型整理。漏洞名可以帮助查漏,但真正落到一个 APK 上,还是要回答五个问题:入口在哪里、谁能调用、输入经过了什么检查、最后触发什么敏感操作、修复后怎样证明合法功能和拒绝路径都正确。
旧文里的案例、命令、四张配图和漏洞分类都保留了下来;涉及旧 Android/WebView 行为的地方加上版本条件,不再把历史默认值直接套到新项目。本文自写的 Kotlin、Java 和 XML 都是用于讲清边界的教学片段,本次没有构建 APK,也没有在 Android 真机或模拟器上运行。文末的 Python 实验是在本机实际执行的,它只验证 URL、SQLite、ZIP 与防重放模型,不代替 Android 框架测试。
概述:先把输入画出来
旧文用下面这张脑图列出了组件、网络、WebView、数据存储和动态加载等攻击面。它仍然适合做目录,但其中“默认设置”和“防护方式”要结合系统版本、targetSdkVersion、WebView 版本以及最终 APK 重新判断。

旧文配图已本地化保存;原图地址仍保留作出处记录。
原文归纳的五个入口没有变:
- 通信协议:本地 IPC、网络协议及其 C/C++ 解析代码,既可能有状态机逻辑错误,也可能出现越界读写、拒绝服务甚至代码执行。
- 四大组件:Activity、Service、BroadcastReceiver、ContentProvider 通过 Intent、Binder 或 URI 接收外部输入,常见问题是未授权调用、数据泄露和畸形输入导致崩溃。
- 开放端口:应用监听 TCP、UDP 或 Unix socket,却没有调用者认证、消息完整性和资源限制。
- IPC:Binder 调用者身份、URI grant、PendingIntent 等能力一旦在错误的时机被放大,就可能越过组件本身的导出限制。
- 文件和数据:日志、外部存储、备份、数据库、压缩包、密钥与网络传输都可能让敏感数据离开预期边界。
审计时我会先画一条最短数据流:
1 | |
例如“Activity 已导出”只是入口事实。若它只展示公开帮助页,风险可能很低;若它接受 orderId 后直接退款,且没有重新检查登录用户与订单归属,才形成可复现的越权。
| 层次 | 要留下的证据 | 常见误判 |
|---|---|---|
| 入口 | 合并后的 Manifest、动态注册代码、Deep Link、监听地址 | 只看源码中的一个 Manifest |
| 身份 | Binder UID、组件权限、URI grant、服务端会话 | 信任 extras 中自报的包名或 userId |
| 参数 | 类型、长度、允许集合、解析器结果 | try-catch 后继续执行;只做黑名单 |
| 业务授权 | 对象归属、当前状态、动作范围 | 有 HTTPS 或随机 ID 就认为不会越权 |
| sink | SQL、文件、WebView、Intent 转发、设备控制、升级安装 | 找到危险 API 就直接报漏洞 |
| 结果 | 未授权读写、崩溃、文件落点、网络请求、系统状态 | 只展示命令返回成功 |
版本条件:默认值不是常量
下面只列本文会用到的边界。这里的“必须”来自平台要求;“建议显式配置”是工程做法,两者不要混在一起。
| 行为 | 版本条件 | 审计含义 |
|---|---|---|
带 intent-filter 的 Activity、Service、Receiver |
应用 target 31+ 时必须显式声明 android:exported,否则 Android 12+ 上不能正常安装 |
仍要看合并后的 Manifest;Provider 不属于这条安装要求,但也应显式声明 |
PendingIntent 可变性 |
target 31+ 必须给出 FLAG_IMMUTABLE 或 FLAG_MUTABLE |
大多数场景选 immutable;确需可变时限制目标和可填字段 |
| 动态 Receiver 导出标志 | RECEIVER_EXPORTED/NOT_EXPORTED 在 API 33 引入;target 34+ 注册非纯系统广播时必须选择其一 |
旧文“动态注册默认导出”不能当成现代统一结论 |
WebView allowFileAccess |
target 29 及以下默认 true,target 30+ 默认 false |
查显式调用和 target,不凭经验猜默认值 |
| file URL 跨域开关 | target 15 及以下默认 true,target 16+ 默认 false;相关 setter 在 API 30 弃用 |
新代码优先 WebViewAssetLoader,旧代码仍要审计残留开关 |
| 明文网络 | Network Security Config 对 target 28+ 的平台默认是禁止明文 | 自定义网络库、native 代码和显式放行仍需单独验证 |
adb backup |
target 31+ 的应用数据默认不会被 adb backup 导出;云备份与设备迁移另有规则 |
不能再用一条旧命令概括全部备份面 |
| Intent 重定向 | Android 16 增加默认加固 | 仍需限制嵌套 Intent 的目标、flags 和 URI grant;不能依赖新系统替应用做业务授权 |
相关平台边界可对照 Android 12 行为变更、Android 14 动态 Receiver 变更、Context.registerReceiver 和 WebSettings。
一次有效复现至少记录:
1 | |
四大组件
四大组件的问题经常和 android:exported="true" 同时出现,但导出只是“可到达”。组件权限、调用者身份、业务会话、对象归属和参数校验共同决定它能不能被利用。
Intent 畸形输入与本地拒绝服务
旧文列出的典型崩溃包括 NullPointerException、ClassCastException、IndexOutOfBoundsException 和 ClassNotFoundException:攻击者向导出组件发送缺失、类型错误、过长或无法反序列化的 extras,应用在 Intent.getXXXExtra() 之后直接使用,最终崩溃。安全防护、锁屏等 App 若关键进程被反复打崩,影响会比普通页面更大。
只在最外层写一个 catch (Exception) 不够。异常前可能已经写了一半状态,异常后继续走默认分支也可能触发敏感操作。入口应先完成类型、空值、长度和枚举检查,再修改状态:
1 | |
Parcelable/Serializable 还涉及类加载和对象图大小。对外入口只接收确实需要的简单类型;API 支持时使用带目标类型的读取方法。还要测试超长字符串、大数组、重复 key 与空 Bundle,避免 Binder 事务过大或解析消耗成为另一条拒绝服务路径。
Activity:从“能打开”追到业务动作
导出与任务栈钓鱼
旧文讨论了 Activity 历史栈、AMS 调度和 FLAG_ACTIVITY_NEW_TASK:恶意 App 把伪装登录页或带任意 URL 的 WebView 放到前台,可能造成钓鱼。这个场景应保留,但现代结论不能简化成“加一个 flag 就能任意劫持”。还要结合 taskAffinity、launchMode、allowTaskReparenting、最近任务显示方式、系统版本以及用户实际看到的来源提示复现。
防护不是简单地在 App 进入后台时弹提示。更可靠的做法是:
- 内部页面明确
exported="false";对外页面只暴露必要功能。 - Deep Link 到达敏感页面后重新检查会话和对象归属,不把“从首页走过一遍”当授权。
- 登录、支付、密钥导出等页面显示可核对的账户与动作上下文,切回前台时按风险重新认证。
- WebView Activity 不接受任意
http/file/content/javascriptURL。 - 审查异常
taskAffinity和任务栈配置,不把旧系统上的 task hijacking 条件泛化到所有设备。
最小到达性验证可以这样写:
1 | |
这只能说明 adb shell 身份能够启动它。shell 的权限和普通第三方 App 不相同,涉及权限与 Binder UID 时要用一个最小测试 APK 再调用一次。随后观察未登录状态、其他账户资源 ID、畸形参数以及合法参数分别发生什么。
显式 Intent、隐式 Intent 与敏感数据
显式 Intent 直接给出组件名;隐式 Intent 描述 action、data 和 category,由系统匹配能处理它的组件。多个处理者匹配时可能出现选择器。旧文用下面的图说明了隐式 Intent 可能落到其他 App 的公开 Activity:

原图地址。图里的旧式选择器界面仅作机制示意,实际 UI 随系统版本变化。
对明确合作方发送敏感信息时,优先设置具体 component 或至少 setPackage(),并确认接收方签名/权限模型。系统 chooser 只让用户选处理者,不保证每个候选都可信。能不放进 Intent 的 token、明文凭据和完整个人信息就不要放;确需共享文件时使用窄范围 content:// URI grant,并及时撤销。
接收 Activity 也不能把 getCallingActivity()!=null、Referer 或某个可伪造 extra 当身份认证。对外 Deep Link 的 orderId、redirect、next 之类参数,都要在当前会话下重新解析和授权。Android Deep Link 风险说明也强调了入口参数与登录/授权检查。
Service:线程和权限是两回事
Service 可以由组件启动,也可以通过 bindService() 暴露客户端—服务端接口。旧文中“启动后可无限期后台运行”的描述适用于早期的简化模型;现代 Android 还有后台执行与前台服务限制。Service 默认也运行在应用主线程,长时间网络或文件操作必须自行调度。
导出的 Service 若没有限制,其他 App 可能启动任务、绑定 Binder、读取返回值或污染内部状态。审计要分别走:
onStartCommand()收到的 Intent;onBind()返回了哪个 Binder;- 每个 Binder 方法在什么时机鉴权;
- 是否调用
clearCallingIdentity(); - 是否把任务转给线程池后才尝试读取原始调用者身份。
下面的核心是先在 Binder 调用语境中取 UID 并完成授权,之后才清理身份或排队:
1 | |
不要信任参数里自报的包名。一个 UID 可能对应需要特殊处理的包集合,旧项目还可能使用 shared UID。准确的版本边界是:Manifest 的 android:sharedUserId 在 API 29 弃用;API 33 又引入 android:sharedUserMaxSdkVersion,让应用可以阻止较新系统上的新安装继续加入 shared UID。两者不是同一项变更,历史 App 的既有安装也仍需兼容分析。Manifest <manifest> 元素列出了这两个属性的 API 条件。signature 级自定义权限适合同一签名合作 App:
1 | |
旧文示例里的 android:permission:"android.perrmission.SEND SMS" 既有 XML 语法/拼写问题,也把 dangerous 权限和合作方信任混在了一起,因此这里改成完整的 signature 权限声明。组件权限控制调用入口,不替代用户登录和对象授权。导出组件权限控制给出了同一边界的官方说明。
BroadcastReceiver:发送面和接收面分开看
广播发送者产生 Intent,系统按显式目标或 IntentFilter 分发给接收者。Manifest 静态注册和 Context.registerReceiver() 动态注册都可能出现两类问题:
- 发送不受限:恶意接收者嗅探敏感 extras,或在有序广播中截断/修改结果。
- 接收不受限:恶意发送者伪造状态、触发任务、传入畸形数据造成崩溃。
旧文说“动态注册默认导出”,这在现代版本上过于笼统。API 33 引入 RECEIVER_EXPORTED/RECEIVER_NOT_EXPORTED;target 34+ 注册非纯系统广播时必须显式选择。仅在进程内部传播状态时,直接使用回调、Flow/LiveData 等进程内机制。LocalBroadcastManager 已经弃用,因此保留为旧项目识别点,不再推荐给新代码。弃用说明
1 | |
需要接收合作 App 广播时,可以要求发送者持有 signature 权限,同时对 action、extras 和业务状态做校验。发送给明确合作方时使用显式 component 或 setPackage();敏感数据尽量走受权限保护的 Binder/API,而不是全局广播。
ContentProvider:URI、SQL 与文件是三道门
ContentProvider 负责跨进程共享结构化数据或文件。旧文示意图准确表达了“调用进程—Provider—数据源”的关系:

原图地址。
原文列出的三类问题都要保留,并且要分别复现:
- 信息泄露:Provider 导出或 URI grant 过宽,外部调用者能读取敏感行/文件。
- SQL 注入:调用者控制的
selection、列、排序等被拼进 SQL,越过表或权限边界。 - 目录遍历:
openFile()将 URI 片段直接拼成文件路径,第三方 App 读取或覆盖允许目录之外的文件。
入口与权限
先看合并后的 Provider 声明:
1 | |
readPermission 和 writePermission 应按能力拆开;若只需要临时共享单个文件,通常让内部 Provider 保持不导出,通过精确 URI grant 或 FileProvider 分享更容易收口。还要检查 <path-permission>、grantUriPermissions、FLAG_GRANT_PREFIX_URI_PERMISSION、持久 grant 以及撤销时机。
旧文提醒“API level 低于 8 时即使写 exported=false 仍可能被访问”。这条保留作历史线索,但现代审计不能从它推出当前设备上 exported=false 无效。实际支持范围应由 APK 的 min/target、Manifest 合并结果和目标设备验证;对现代项目直接显式声明,避免依赖不同年代的默认值。
SQL:绑定值、限制结构、追加归属
危险实现通常类似:
1 | |
只把 selectionArgs 分开,不代表任意 selection 就安全。对不可信调用者,Provider 自己定义 URI 语义和 WHERE 结构;外部只提供值。列名、表名、排序方向无法作为普通值参数绑定,要映射到允许集合:
1 | |
这里的 owner_id = ? 才是对象级授权。参数化查询阻止值变成 SQL 语法,却不会自动判断 Alice 能否读 Bob 的记录。官方 SQL 注入说明也把 replaceable parameter 作为值绑定方案;Provider 还要根据自身共享模型收紧 projection、selection 与表边界。
本机实验用内存 SQLite 实际跑了同一差异:
1 | |
实验脚本见 android_boundary_lab.py。这是 SQLite 后端模型,不是 Android ContentResolver 实机结果。
openFile():不要让 URI 直接变成路径
更稳妥的设计是把 URI 中的业务 ID 查询成应用自己保存的相对路径,而不是接受外部文件名。即便如此,也要限制打开模式并验证最终规范路径:
1 | |
目录前缀必须带分隔符,否则 /safe-other 可能被误认成 /safe 的子目录。目标目录还应由应用独占,避免符号链接和并发替换;调用权限、URI grant、行级归属、文件模式和路径边界缺一不可。
可以在授权测试设备上用下面的命令观察 Provider 行为:
1 | |
adb shell 仍不是普通第三方 App;权限拒绝、临时 grant 和 Binder UID 的结论必须用对应身份复测。
原文四大组件防护清单,按现代边界校正
原文的要点没有删,整理后可以作为 review checklist:
- Activity:谨慎处理 Intent 和返回数据;私有页面不导出;目标明确时使用显式 Intent;不要发送不必要的敏感 extras;合作方访问用 signature 权限或可验证的信任关系;对外页面仍做会话、对象归属和参数检查。
- Service:内部 Service 不设无用的 intent-filter 并显式不导出;显式启动;合作 Binder 校验权限/UID;在每次
onStartCommand、onBind和 Binder 方法入口鉴权;限制返回数据;不要只在onCreate检查一次。 - ContentProvider:无需共享就不导出;需要共享时拆分读写权限;所有外部参数均视为不可信;SQL 值绑定、结构白名单、行级授权分别落实;避免把外部文本交给
execSQL();文件接口单独检查 URI grant、模式和规范路径。 - Receiver:私有 Receiver 设为不导出;动态注册选择导出标志;公开 Receiver 限制发送者并验证消息;敏感广播指定接收者;不再把已弃用的 LocalBroadcastManager 当成新项目方案。
Intent 转发和 PendingIntent
嵌套 Intent 转发
常见危险入口是导出的 Activity/Service 从 extras 取出另一个 Intent,然后直接 startActivity()、startService() 或 sendBroadcast():
1 | |
攻击者可能借应用身份访问本来不导出的组件,或把读写 URI 权限带到非预期目标。修复时先判断业务是否真的需要通用转发;如果只允许两个页面,直接把外部枚举映射到内部显式 Intent 最清楚:
1 | |
确需接收嵌套 Intent 时,用固定 component/package 允许集合,清理 FLAG_GRANT_READ_URI_PERMISSION、FLAG_GRANT_WRITE_URI_PERMISSION、FLAG_GRANT_PERSISTABLE_URI_PERMISSION、FLAG_GRANT_PREFIX_URI_PERMISSION,限制 data/type/ClipData/extras,并考虑 AndroidX IntentSanitizer。Android 16 的默认加固降低部分风险,但不能替代应用自己的目标允许集合和业务授权。Intent Redirection列出了 flags 和 IntentSanitizer 的处理方式。
修复验证至少包含:
- 允许目标仍能正常打开;
- 指向私有 Activity 的嵌套 Intent 被拒绝;
- 带 URI grant、ClipData 和异常 flags 的输入被清理/拒绝;
- Android 16 与较低测试版本都执行应用自身策略。
PendingIntent:受托能力、可变字段和重放
PendingIntent 是系统维护的 token,接收者可以让创建者身份执行预定义动作。风险通常来自三处:
- 内层 Intent 隐式或字段未填完整,同时 PendingIntent 可变,接收方可以
fillIn()改目标或 data。 - 多个语义不同的 PendingIntent 因 requestCode/action/data 相同而被系统视为同一份,旧 extras 被复用或覆盖。
- 本应一次性的能力可被重复触发。
1 | |
FLAG_IMMUTABLE 阻止接收方填充字段,但创建者仍可通过 FLAG_UPDATE_CURRENT 更新自己创建 token 的 extras;它不是“对象永远不变”。FLAG_ONE_SHOT 适合只允许触发一次的能力,但最终页面仍应检查 payment 当前状态、账户归属和服务端幂等,不能把 token 本身当成完成支付的唯一授权。官方 PendingIntent 风险说明也单独列出了 mutable 与 replay 两类问题。
默认设置:以发布 APK 为准
合并后的 Manifest
库 Manifest、product flavor、构建占位符和 manifest merger 都可能改变最终声明。审计源码时同步检查 release APK:
1 | |
重点搜 debuggable、testOnly、allowBackup、dataExtractionRules、usesCleartextTraffic、networkSecurityConfig、组件 exported、组件权限、Provider authority 与 URI grant。android:debuggable="true" 会扩大调试与数据观察面;结论应来自最终发布包,而不是 Gradle 文件中的期望值。
备份不是一条 allowBackup 就结束
旧文记录了 Android 2.1 以后 allowBackup 默认风险,并用 adb backup/restore 解释数据导出。这是早期 Android 上重要的检查点,但现在需要拆成:
- 旧设备上的
adb backup; - Auto Backup 云备份;
- 设备到设备迁移(D2D);
- 自定义
BackupAgent; - 恢复后的 token、设备密钥和账户状态是否仍然有效。
Android 官方说明:Auto Backup 的 allowBackup 默认值仍是 true;target 31+ 在 Android 12+ 上使用 data-extraction-rules,同时还要为 Android 11 及以下保留旧规则。部分厂商设备上,allowBackup="false" 可能关闭云备份却不关闭 D2D,因此敏感数据应通过明确的 include/exclude 规则和恢复后失效策略一起处理。Auto Backup 文档
1 | |
1 | |
是否允许备份是产品决策。更重要的是,长期 refresh token、设备绑定私钥和防重放状态不要随着普通偏好一起迁移;恢复后服务端应能识别新设备状态并要求重新认证。
网络:TLS、调用认证和对象授权逐层验证
TLS 证书链与主机名校验
旧文列出的风险仍然成立:
- 自定义
X509TrustManager.checkServerTrusted()为空,或无条件接受证书; HostnameVerifier.verify()总返回true,或使用历史上的ALLOW_ALL_HOSTNAME_VERIFIER;- WebView 的
onReceivedSslError()直接调用proceed(); - 明文 HTTP 或调试 CA 配置进入 release;
- 证书颁发机构或应用自管密钥失陷。
这些问题会让中间人读取或修改账号密码、聊天内容、地址、电话、支付信息,也可能把升级地址和页面内容换成恶意版本。正常业务优先使用平台/成熟网络库的默认验证,不要为了抓包长期保留空 TrustManager。WebView 遇到证书错误应取消加载,而不是静默继续。
Network Security Config 能把系统 CA、调试 CA 和明文策略分开:
1 | |
1 | |
官方 Network Security Configuration说明了三个容易忽略的条件:target 28+ 的平台默认禁止明文;target 23 及以下默认还信任用户安装 CA;debug-overrides 只在 debuggable=true 时生效。使用 native socket、自带 TLS 栈或忽略平台策略的第三方库时,还要验证具体实现是否遵守配置。
证书 pinning 不是默认必选项。若业务确实需要,至少准备 backup pin、密钥轮换和失效恢复方案;否则一次证书/CA 变更可能让旧客户端永久离线。pinning 也不能替代主机名校验、业务登录和服务端授权。
怎样复测修复
在授权测试环境中准备 release 等价构建和 debug 构建:
- 正常证书、正确主机名应成功;
- 自签证书、错误主机名、过期证书应失败;
- debug CA 仅 debug 构建成功,release 构建失败;
- HTTP URL 和 HTTPS→HTTP 重定向应按策略拒绝;
- WebView SSL 错误不应出现
proceed()后继续展示的页面。
不要只根据代理“抓不到包”判断安全;应用可能使用 pinning、QUIC、native 库,也可能根本没有发出请求。记录 Logcat、网络错误、目标地址和服务端日志,说明实际发生了什么。
HTTPS 之后仍有对象级越权
旧文“业务接口是否存在任意权限调用”指的就是这一层:敏感接口如果只相信请求里的 user_id、订单号或设备号,攻击者即使无法窃听 TLS,也可以用自己的合法会话请求别人的资源。
最清楚的验证方法是准备两个测试账户 A、B:
- A、B 分别创建一条对象并记录 ID;
- 带 A 的认证信息读取、修改、删除、导出 B 的对象;
- 再测批量接口、子资源、分享链接和历史版本;
- 把 ID 换成随机 UUID 后重复;
- 修复后确认 A→B 全部拒绝,A→A 仍正常。
服务端查询把认证主体放进条件:
1 | |
:authenticated_user_id 来自验证过的会话或 token,而不是请求体自报字段。随机 ID 降低枚举概率,HTTPS 保护传输;两者都不是授权。
Socket 远程连接与本地监听
旧文的结论是:App 开放端口但没有发送者认证或权限控制时,攻击者可能获得该端口背后的全部功能。这个检查仍然有价值,但“开放”要精确到监听地址和命名空间:
1 | |
不同 Android 版本、SELinux 策略和 shell 权限可能隐藏进程信息。127.0.0.1:port 不对局域网开放,但设备上的其他 App 通常仍能访问 loopback;0.0.0.0:port 还要结合网络路由、防火墙与权限验证。Unix domain socket 要看文件权限或 abstract namespace 名称,不要只检查 TCP。
沿服务端代码继续确认:
- 每条连接如何认证,token 是否可重放;
- 长度字段、帧边界、字符编码和状态机是否有上限;
- 并发连接、空闲超时、单连接内存和文件写入是否受限;
- 管理命令是否与普通数据通道隔离;
- 身份校验失败后是否仍执行了一部分操作;
- C/C++ 解析器是否对整数溢出、越界和生命周期做了防护。
“系统版本不影响此漏洞”的旧说法太绝对。应用协议缺陷由 App 引入,但网络命名空间、后台限制、SELinux 与平台 API 会影响可达性和利用条件,报告中要写实测环境。
APK 升级、签名与动态加载
原升级链路的三个风险点
旧文的表格保留,并补上最终验证:
| 阶段 | 原文隐患 | 可能结果 | 现在还要验证 |
|---|---|---|---|
| 升级 API | API 未加密 | 返回恶意下载地址 | TLS、服务端认证、元数据签名、版本与通道 |
| 下载 API | 下载未加密 | APK 内容或路径被替换 | 私有临时文件、大小、可信 SHA-256、完整写入 |
| 程序安装 | 本地路径可篡改 | 安装错误 APK | 包名、签名证书沿革、versionCode、防回滚、PackageInstaller 结果 |
如果 APK 和“正确 hash”来自同一个可篡改 HTTP 响应,攻击者可以同时替换两者。hash 必须由已经认证的元数据绑定,或者依赖平台安装时对包签名身份的校验。下载完成后先在应用私有目录验证,安装成功前不要覆盖当前可用包或插件。
从 v1 的 META-INF 到现代签名方案
旧文详细记录了 v1/JAR 签名结构,这部分对分析老 APK 仍有用:
META-INF/MANIFEST.MF保存各文件的 SHA-1 或 SHA-256 摘要及 Base64 编码;CERT.SF保存 Manifest 整体和各 section 的摘要;CERT.RSA、CERT.DSA或CERT.EC是 PKCS#7 数据,包含对CERT.SF的签名以及证书、公钥、算法、有效期等信息;- ZIP 中可以出现多个签名文件,因此“看到了一个 CERT.RSA”不能单独证明 APK 身份。
这只描述 v1。Android 7.0 引入 v2,Android 9 引入 v3;v2/v3 把签名块放在 APK 中并覆盖更完整的文件内容,不能再只盯着 META-INF。AOSP 的 APK Signing说明了 v1/v2/v3 的版本与覆盖范围。v4 用于增量安装,签名存放在 APK 旁边的 .idsig 文件;它要求 APK 已有相同 key 的 v2/v3 签名,不能替代包内签名。
1 | |
记录每个包内 scheme 的验证结果、Signer certificate DN、SHA-256 digest、包名和 versionCode;交付链使用 v4 时再连同 .idsig 核对。apksigner 的 命令文档还区分了 v3.1 key rotation:默认的轮换 signing key 面向 Android 13/API 33+,较低版本继续使用旧 key,除非构建时明确调整 rotation min SDK。知道证书或公钥指纹不等于拥有私钥;真正的升级关系由安装系统根据已有包的签名身份/证书沿革判断。Google Play App Signing 的 signing key、upload key 和 key upgrade 也不能混为一谈。App signing 文档说明:系统升级时会比较新旧包证书;Play 的 signing key 升级在不同 Android 版本上还可能使用不同签名密钥。
对自定义插件、DEX/JAR/SO 热更新,系统 PackageInstaller 的包级校验不再自动保护每个文件。应用必须把插件版本、允许的宿主版本、hash/签名、来源和防回滚状态放进同一可信策略;校验完成前不得加载。仅检查文件后缀或下载 URL 白名单没有意义。
加密、密钥和重放
原文中的弱算法清单如何使用
原文列出 MD5、DES、RC2、TripleDES、SHA-1、RIPEMD-160。保留这些名字,但判断时要先问“算法用来做什么”:
- MD5、SHA-1 的碰撞弱点使其不适合安全完整性和数字签名;用于非对抗性的缓存键,与用于验证更新包不是同一风险。
- DES 密钥空间过小;RC2、TripleDES 都不应继续用于新设计。
- 普通 SHA-256 不能替代密码哈希/KDF,也不能提供消息来源认证;认证消息用 HMAC 或数字签名。
- Base64 是编码,不是加密;把固定 AES key 写进 APK 再混淆,也不等于密钥安全。
Android 官方 Cryptography推荐的现代原语包括 SHA-2、HMAC-SHA-2 和 AES/GCM;具体协议仍要处理 nonce、密钥轮换、错误返回与数据格式。
AES-GCM 与 Android Keystore
下面演示本地加密记录的基本形状:密钥生成在 Android Keystore,GCM IV 由 Cipher.init(ENCRYPT_MODE, key) 生成并和密文一起保存,解密时验证 tag。代码是教学片段,本次未构建或在 Android 设备运行:
1 | |
真实格式要限制 IV 长度、版本化 envelope,并在解密前验证长度;AAD 可以绑定记录类型、账户或 schema 版本。不要重用同一密钥下的 GCM nonce。Keystore 能让 key material 保持不可导出,并可限制密钥用途;进程被控制时,攻击者仍可能调用应用已有的解密功能,因此业务授权和最小数据暴露依然必要。Android Keystore
加密不阻止重放
攻击者不需要解密,只需再次发送一份先前合法的密文或签名请求。单独加时间戳也挡不住时间窗口内重放。一个可审计的请求认证格式至少绑定:
1 | |
服务端先验证认证标签与时间窗口,再把 (subject, nonce) 以原子唯一约束写入带 TTL 的共享存储,最后在事务中检查对象归属和状态并执行一次。多实例服务只在单机内存放 Set,切换节点就能绕过。幂等键可以让客户端安全重试,但必须绑定账户、操作和响应,不能成为跨账户通用通行证。
本机实验实际执行的结果:
1 | |
脚本以 HMAC-SHA256 模拟请求认证,不代表特定生产协议;测试 key 是固定 fixture,nonce 存储也只是内存模型。完整代码和运行方法见 android_boundary_lab.py 与 android-labs-README.md。
WebView:URL、origin 和 Native Bridge
旧文把 WebView 风险分成远程代码执行、UXSS、设置错误、忽略证书错误和 file 域同源绕过。这五类都保留,但分析顺序改成“谁能控制 URL—最终加载哪个 origin—页面能执行什么脚本—脚本能调用什么原生能力”。
设置项与准确的历史条件
| 设置 | 旧文关注点 | 现在的判断方式 |
|---|---|---|
setJavaScriptEnabled() |
JS 使页面能执行脚本,默认 false | 仅业务需要时开启;同时审计内容来源和 bridge |
setPluginState() |
ON / ON_DEMAND / OFF,旧 WebView 默认 OFF | API 18 已弃用,作为老项目线索,不用于现代修复建议 |
setAllowFileAccess() |
允许 file 访问 | target ≤29 默认 true,target ≥30 默认 false;仍建议按业务显式配置 |
setAllowContentAccess() |
允许 content:// |
默认 true;不需要就关闭,需使用时约束 URI 来源 |
setAllowFileAccessFromFileURLs() |
file 页面读其他 file | target ≤15 默认 true,target ≥16 默认 false;API 30 弃用 |
setAllowUniversalAccessFromFileURLs() |
file 页面跨到任意 origin | 同上;API 30 弃用,官方建议 WebViewAssetLoader |
setSavePassword() |
早期 WebView 将密码保存到 webview.db |
API 18 已弃用;旧设备/旧引擎仍可作为历史检查点,不能当现代统一默认 |
| mixed content | HTTPS 页面加载 HTTP 子资源 | target Lollipop+ 默认 MIXED_CONTENT_NEVER_ALLOW,检查代码是否放宽 |
旧文提到密码可能明文保存在 /data/data/<package>/databases/webview.db,root 后被读取。这是早期 WebView 的典型风险,因此保留并标明历史范围;现代审计应直接查看实际 WebView 版本、应用自己的 credential storage 和页面 autofill 行为,而不是只搜索一个旧数据库名。
一个只展示静态可信内容且不需要脚本的页面可以明确收紧:
1 | |
业务确实依赖 JS 时,不应为了通过扫描器直接关闭功能;要继续限制内容来源、重定向和 bridge 权限。
File 域与同源策略绕过
旧文描述的历史利用链是:WebView 启用 JavaScript 和 file 访问,外部可控 HTML 借延时与符号链接切换读取应用私有文件,再把结果送出。早期版本里 setAllowFileAccessFromFileURLs(true) 或 setAllowUniversalAccessFromFileURLs(true) 会显著扩大后果。
修复时同时处理入口和能力:
- 含 WebView 的 Activity 不必要就不导出;
- 不加载外部可写目录中的 HTML;
- 不需要 file/content scheme 就显式关闭;
- 本地静态资源使用
WebViewAssetLoader映射到受控 HTTPS origin; - 不可信内容加载前移除 JavaScript interface;
- 更新系统 WebView,并验证厂商定制接口。
旧文只写 setAllowFileAccess(false) 或关闭 JS 是必要但不总是充分的:应用若把私有内容先复制到共享目录,或 bridge 本身提供通用 readFile(path),攻击面仍然存在。
URL 白名单:解析完整 origin
旧文保留了一个历史解析差异案例:
1 | |
当时某些 Android URL 解析/WebView 组合可能对反斜杠作不同解释,导致 getHost() 判断结果与实际导航目标不一致。原文还给过基于站内跳转参数的示例:
1 | |
这些地址只作为旧案例保留,不表示今天仍可访问或所有 Android/WebView 都有相同解析结果。现代测试要对当前应用实际使用的解析器和 WebView 版本构造用例,不能把“getHost 本身有漏洞”当作普遍结论。
策略上至少解析并完整比较 scheme、host、port 和 userinfo,拒绝控制字符与反斜杠,对每次重定向重新判断:
1 | |
| 输入 | 该策略结果 | 原因 |
|---|---|---|
https://docs.example.com/help |
允许 | origin 精确匹配 |
https://DOCS.EXAMPLE.COM:443/help |
允许 | host 大小写不敏感、标准 TLS 端口 |
https://docs.example.com.evil.test/ |
拒绝 | host 不相等 |
https://docs.example.com@evil.test/ |
拒绝 | 实际 host 是 evil.test,且有 userinfo |
http://docs.example.com/ |
拒绝 | scheme 错误 |
https://docs.example.com:444/ |
拒绝 | 端口不在允许集合 |
file:///data/user/0/... |
拒绝 | scheme 错误 |
本机 Python fixture 对上述 host、userinfo、端口、反斜杠和逐跳重定向实际执行了 13 个相关断言,其中显式端口 0 和越界端口 65536 都必须拒绝;这验证策略模型,不证明 android.net.Uri 与 Python 对所有畸形输入解析一致。官方 Unsafe URI Loading要求同时校验 scheme 与完整 host,并提醒不正确的 startsWith/endsWith/contains 会扩大信任范围。
Native Bridge:网页进入 App 权限范围的门
addJavascriptInterface、postWebMessage 和 message channel 都可能让页面调用原生代码。API 17 以前还存在著名的反射调用面;现代系统即便使用 @JavascriptInterface,如果不可信页面能到达读取 token、文件、联系人或设备控制方法,仍会以宿主 App 权限执行。
检查时画出三件事:
1 | |
不要假定“顶层 URL 在白名单”就保证所有 frame 都可信。把接口缩成具体业务动作,不暴露通用 readFile(path)、exec(cmd)、getToken();原生方法里仍校验当前用户、对象和状态。加载不可信内容前调用 removeJavascriptInterface(),或更直接地让不可信内容进入没有 bridge 的独立 WebView/外部浏览器。官方 WebView Native Bridge 风险说明明确建议不需要 JS 时关闭,并在不可信内容加载前移除接口。
WebView 修复回归矩阵
| 用例 | 预期 |
|---|---|
| 允许 HTTPS 页面 | 正常加载,合法业务动作可用 |
| 允许页 302 到非允许 host | WebView 停止或转系统浏览器 |
http/file/content/javascript URL |
拒绝或按明确外部策略处理 |
| 错误证书/主机名 | cancel,页面不继续 |
| 非允许页面尝试 bridge | 接口不可见或动作拒绝 |
| 允许页面的恶意 iframe/脚本 | 不能借 bridge 取得通用 App 权限 |
| 旧 target 与新 target 构建 | 显式设置使结果一致,差异有记录 |
手机厂商、浏览器厂商、App 开发者和用户都需要及时更新 WebView/浏览器;这是旧文的防护建议之一。对 App 团队而言,更新不能替代自身 URL 和 bridge 边界,但能缩短已知 Chromium/WebView 漏洞暴露时间。
敏感数据泄露
原文列出的六类泄露全部保留:
- Logcat 输出 token、手机号、VIN、定位、请求/响应正文;
- 敏感数据明文写入 sdcard/共享外部存储;
- 数据库敏感字段明文保存;
- SharedPreferences 被错误设为全局可读写,或通过备份/调试环境泄露;
- API key、私钥、固定加密 key 等硬编码进 APK;
- HTTP 明文传输敏感信息。
现代 Android 的应用沙箱、scoped storage 和 SELinux 降低了部分默认暴露,但不能把“私有目录”写成绝对安全:debuggable 构建、root/设备失陷、备份、导出 Provider、WebView bridge 和应用自身越权接口都可能把数据带出来。
逐项审计生命周期比只看“是否加密”更有用:
| 数据 | 产生 | 存储 | 使用 | 清理/失效 |
|---|---|---|---|---|
| access token | 登录响应 | 内存/受控持久层 | Authorization header | 过期、登出、设备解绑 |
| refresh token | 服务端签发 | 不进入普通备份 | 换取短期 token | 服务端撤销、轮换 |
| 业务导出文件 | 用户操作 | 私有临时目录 | FileProvider/分享 | 超时删除、撤销 URI grant |
| 日志事件 | 各模块 | Logcat/本地文件/SDK | 排障与审计 | 脱敏、采样、保留期限 |
硬编码的公开 API identifier 不一定是秘密;真正高风险的是能代表服务端身份的 secret、可签名私钥、长期通用 token。把它们加密后连同可解密 key 一起放进 APK,只增加逆向步骤,没有建立服务器侧信任边界。
还应检查通知、最近任务缩略图、剪贴板、截图/录屏、键盘与第三方 SDK。FLAG_SECURE 可以降低特定敏感页面被普通截图/投屏捕获的概率,但会影响可用性,也不能防住被控制的进程或所有外部相机/系统场景。
ZIP 解压缩漏洞
ZIP entry 名称可以包含 ../、绝对路径等特殊形式。若应用简单执行 File(destination, entry.name),攻击者可能把文件写到目标目录之外,覆盖配置、数据库、DEX 或 SO,造成拒绝服务,特定条件下甚至代码执行。旧文提到“寄生兽”、海豚浏览器和三星输入法相关历史事件,保留作这一漏洞影响的背景;具体成因与版本应分别查对应事件资料,不能把它们当作同一利用链。
最小路径检查如下:
1 | |
这段只解决规范路径边界,完整实现还要做:
- 目标目录由应用独占,解压前为空或采用明确的覆盖策略;
- 拒绝符号链接 entry,并防止检查后到打开前的 symlink/rename 竞态;
- 限制 entry 数、单文件大小、总解压大小和压缩比,防 ZIP bomb;
- 拒绝 NUL、绝对路径、盘符、反斜杠歧义与异常 Unicode 规范化;
- 使用独占创建,不静默覆盖现有文件;
- 写入失败时清理临时目录,验证完成后再原子移动;
- 对 TAR/RAR/7z 和第三方库执行同类检查。
Android 官方 Zip Path Traversal明确指出 java.util.zip 默认不会替调用者过滤 ../,并要求在每个 entry 写入前确认目标是 destination 的子路径。不要依赖“新 Android 会自动拦”这类没有覆盖到自定义/第三方解压器的假设。
本机实验构造了真实内存 ZIP,实际验证:
1 | |
实验脚本还要求目标目录为空并使用独占创建。它运行在 Windows/Python 3.11,不是 Android Java 解压器的实机结果;价值在于让路径和资源策略有可重复的正反断言。
Android APP 审计系统:从清单变成证据
旧文最后保存了一张静态/动态扫描脑图,覆盖文件信息、证书、权限、SO、第三方 SDK、组件、WebView、SQLite、网络、加密、敏感数据、动态 Hook、拒绝服务与注入等检查项:

原图地址。图中包含 Wormhole、XG SDK、旧 WebView/File 域、
adb backup等当年的检查项,适合作历史索引;工具名称和红绿状态不代表当前版本仍适用。
一套可复核的流程可以这样走。
1. 固定样本与运行条件
1 | |
记录 APK 来源、版本、签名摘要、设备/API/补丁、target SDK、WebView provider、账户和测试网络。只有这样,同事才能解释“我这里复现不了”到底是代码、版本还是身份差异。
2. 枚举真正入口
- 合并 Manifest 中的 exported Activity/Service/Receiver/Provider;
- 动态 Receiver、Deep Link/App Link、WebView
loadUrl; - Binder/AIDL、ContentProvider URI、FileProvider paths;
- 本地 TCP/UDP/Unix socket;
- 通知/PendingIntent、分享与文件导入;
- 网络 API、升级/插件下载和压缩包解析。
每个入口写明调用者身份。adb shell、root、同签名 App、普通第三方 App 和远程未登录用户的影响不能混写。
3. 从入口追到 sink
用数据流而不是关键字作结论:
1 | |
SQL、文件路径、反射、动态加载、系统 Intent、设备控制、日志和网络请求都可以是 sink。遇到“已校验”继续问:校验的是字符串格式、调用身份、对象归属,还是全部?校验发生在 clearCallingIdentity 或异步切换之前吗?
4. 写最小复现和对照组
一个好的复现同时有正例与反例:
| 组别 | 输入 | 调用者 | 预期/实际 |
|---|---|---|---|
| 合法 | A 读取 A 的 report | 普通合作 App A | 成功 |
| 越权 | A 读取 B 的 report | 普通合作 App A | 修复前成功、修复后拒绝 |
| 无权限 | 无 signature 权限调用 Service | 普通第三方 App | SecurityException |
| 畸形 | 错类型/超长 extras | 普通第三方 App | 拒绝且进程不崩溃 |
| 重放 | 同 nonce 再发一次 | 已认证账户 A | 第二次拒绝/返回同一幂等结果 |
截图用于展示 UI、代理请求和系统行为;关键参数、命令、日志和哈希仍要以文本保存,避免证据只剩一张难搜索的图。
5. 修复验证要回到敏感操作
“页面打不开了”不一定等于漏洞修复,可能只是测试路径失效。确认最终 sink 没有发生:数据库无越权行、文件未写出允许目录、WebView 未加载恶意 origin、服务端状态只变化一次。同时确认合法调用仍可用,避免以破坏功能换取扫描器不报警。
本机可复现实验
下载:
运行:
1 | |
本次在 Python 3.11.5 上实际执行 13 个 unittest,结果全部通过,覆盖:
- 9 个 URL 输入和两条 redirect chain;
- SQL 字符串拼接、参数绑定、行归属和排序字段允许集合;
- 正常 ZIP、4 种路径逃逸、symlink 与大小限制;
- 请求首次接受、重复 nonce、篡改 path 与过期时间戳。
这些 fixture 是可重复的主机侧模型。本文没有声称以下内容已经实测:Android 组件导出、Binder UID、URI grant、PendingIntent、WebView bridge、Network Security Config、Keystore、PackageInstaller。把代码带进真实项目时,应按项目的 minSdk/targetSdk、设备矩阵和网络栈补上 instrumentation/集成测试。
参考资料
Android 官方资料
- Permission-based access control to exported components
- Intent redirection
- Pending intents
- Create a content provider
- SQL injection
- Unsafe URI loading
- WebView native bridges
- WebSettings API reference
- Network Security Configuration
- Back up user data with Auto Backup
- Cryptography
- Android Keystore
- Zip Path Traversal
- APK Signing (AOSP)
- apksigner
旧文参考链接
以下链接是原文的资料来源,继续保留;其中部分文章较早,涉及默认值和防护方式时以上面的官方版本条件为准。
