2
月
在社区成长 2 月
LEVEL 1
1 XP
一、背景
Android 应用由 Activity、BroadcastReceiver、Service、ContentProvider 等组件构成。为了支持网页跳转、跨进程调用、系统广播或数据共享,部分组件需要对其他应用开放。问题在于,一旦组件被错误导出,或者虽然设置了访问权限但组件内部缺少参数校验,任意本地应用就可能绕过正常业务流程调用它。
这类风险常见于登录状态绕过、内部页面暴露、敏感广播伪造、后台任务滥用和数据越权读取。本文只针对自己创建的本地实验应用或已获得书面授权的测试环境,演示如何系统识别导出组件、验证实际可达性,并给出对应修复方法。请勿将相关命令用于未授权设备或真实第三方应用。
二、核心原理
Android 组件能否被外部应用调用,主要受以下因素影响:
1. android:exported:决定组件是否允许其他应用直接访问。
2. intent-filter:在旧版 Android 行为中,声明过滤器可能影响组件默认导出状态。面向 Android 12 及以上版本时,包含 intent-filter 的组件必须显式声明 android:exported。
3. android
ermission:可在组件入口配置调用权限,但权限保护级别必须合理。
4. 运行时校验:即使组件已经导出,也应校验调用参数、用户身份、当前登录状态和业务状态。
5. URI 授权:Provider 或文件共享还会受到 grantUriPermissions、路径规则及临时读写授权影响。
需要注意,导出并不等于漏洞。例如供浏览器唤起的深链 Activity 本来就需要导出;真正的安全问题是外部输入能够触发未授权行为,或者访问本不应公开的数据。
三、前置条件
建议准备以下本地环境:
1. Android Studio 与 Android SDK Platform Tools。
2. 一台模拟器或专用测试机,建议不要使用存有私人数据的日常手机。
3. 自己编译的实验 APK,包名假设为 com.example.exportlab。
4. adb 已加入 PATH,执行 adb devices 能看到状态为 device 的测试设备。
5. 测试前保留源码与日志,便于区分设计行为、配置缺陷和系统版本差异。
可先确认基础环境:
adb devices
adb shell getprop ro.build.version.release
adb shell getprop ro.build.version.sdk
adb shell pm path com.example.exportlab
本文示例组件均属于虚构的本地实验应用,不对应任何真实线上目标。
四、搭建最小实验场景
在实验应用的 AndroidManifest.xml 中加入以下组件。为了观察风险,这里故意保留不安全配置:
<activity
android:name=".DebugPanelActivity"
android:exported="true" />
<activity
android:name=".DeepLinkActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="exportlab" android:host="open" />
</intent-filter>
</activity>
<receiver
android:name=".SyncReceiver"
android:exported="true">
<intent-filter>
<action android:name="com.example.exportlab.SYNC" />
</intent-filter>
</receiver>
<service
android:name=".MaintenanceService"
android:exported="true" />
<provider
android:name=".NotesProvider"
android:authorities="com.example.exportlab.notes"
android:exported="true" />
DebugPanelActivity 可以显示一段虚构的调试状态;SyncReceiver 接收 mode 参数后写入日志;MaintenanceService 执行一次无害的缓存统计;NotesProvider 只保存测试文本。不要在实验应用中放入真实账号、令牌、联系人或生产接口地址。
五、分步骤审计与验证
步骤 1:建立组件清单
首先从源码中的 AndroidManifest.xml 检查所有 activity、activity-alias、receiver、service 和 provider。若只有 APK,可在自己的授权样本上使用 Android SDK 工具分析:
aapt2 dump xmltree app-debug.apk AndroidManifest.xml
也可以查看系统安装后的包信息:
adb shell dumpsys package com.example.exportlab
重点记录组件全名、exported 状态、intent-filter、permission、authorities、grantUriPermissions 和 path-permission。仅看到 exported=true 还不能直接定性,应继续检查组件功能和调用结果。
步骤 2:验证导出 Activity
在本地设备上尝试显式启动实验 Activity:
adb shell am start -W -n com.example.exportlab/.DebugPanelActivity
观察输出中的 Status、Activity 和 TotalTime,同时查看应用界面。如果未经过主界面登录流程就能进入仅供内部使用的调试页,说明导航入口缺少保护。
防御上不能只依赖“正常用户看不到按钮”。若该页面完全不需要外部访问,应修改为:
<activity
android:name=".DebugPanelActivity"
android:exported="false" />
如果确实需要跨应用调用,则应添加签名级权限,并在 Activity 内再次校验调用目的与会话状态。对于敏感操作,还应在执行前要求用户确认,而不是在 onCreate 中根据 Intent 参数直接执行。
步骤 3:检查深链参数
调用本地实验深链:
adb shell am start -W -a android.intent.action.VIEW -d "exportlab://open?screen=profile&next=home"
在代码中重点检查 getIntent().getData()、getQueryParameter() 以及后续页面跳转逻辑。常见问题是应用直接信任 next、url 或 target 参数,将其传给 WebView、文件接口或内部路由。
推荐做法是使用明确的允许列表,例如只接受 profile、help、order-detail 等固定路由;拒绝未知 scheme 和 host;对订单号等业务对象进行服务端归属校验;不要根据外部参数直接打开任意类名。若深链涉及网页登录回跳,应校验随机 state,并避免把长期令牌放在 URL 查询参数中。
步骤 4:验证广播接收器边界
向实验 Receiver 发送自定义广播:
adb shell am broadcast -a com.example.exportlab.SYNC --es mode demo -n com.example.exportlab/.SyncReceiver
随后仅查看自己应用的日志:
adb logcat --pid=$(adb shell pidof com.example.exportlab)
如果任意应用都能触发同步、清理、切换环境或修改配置,可能形成未授权业务操作。无外部调用需求时,应设置 android:exported="false"。确需开放时,可声明自定义签名权限:
<permission
android:name="com.example.exportlab.permission.INTERNAL_SYNC"
android
rotectionLevel="signature" />
并在 Receiver 上配置:
android
ermission="com.example.exportlab.permission.INTERNAL_SYNC"
签名级权限适合同一开发方、使用可信签名发布的应用间通信。不要把普通级权限误当作安全边界,因为其他应用可能申请普通权限。
步骤 5:验证 Service 是否可被滥用
对实验 Service 执行一次启动测试:
adb shell am startservice -n com.example.exportlab/.MaintenanceService --es task stats
较新 Android 版本会限制后台启动 Service,因此结果可能因系统版本和前后台状态不同。测试重点不是绕过系统限制,而是确认服务是否存在外部可达入口,以及传入参数能否触发敏感行为。
Service 不应根据外部字符串直接执行文件删除、账号切换、环境切换或动态代码加载。对于耗时任务,应结合 JobScheduler 或 WorkManager;对于必须导出的绑定服务,应配置权限,并在 Binder 方法中进行调用身份和参数校验。仅在 Manifest 中加权限,而 Binder 接口内部完全信任参数,仍可能留下业务越权问题。
步骤 6:检查 ContentProvider 数据暴露
在仅含虚构测试数据的 Provider 上查询:
adb shell content query --uri content://com.example.exportlab.notes/items
若无需跨应用共享,最直接的修复是设置 android:exported="false"。确需共享时,应分别配置 readPermission 与 writePermission,并尽量缩小 URI 范围。还要检查 selection、sortOrder 和 projection 是否被直接拼接进 SQL。
Provider 查询应使用参数化条件。例如不要把外部 selection 直接连接成 SQL 字符串,应通过 SQLiteDatabase.query 的 selectionArgs 传递变量。对于 write、delete 和 update 操作,还要验证调用者是否有权操作对应记录,避免只验证“拥有写权限”,却不验证“是否拥有该条数据”。
步骤 7:回归验证修复效果
完成修改后重新编译和安装:
adb install -r app-debug.apk
再次执行前述 am start、am broadcast、am startservice 和 content query 命令。预期结果应是:内部 Activity 无法由 shell 代表的外部调用者直接打开;受保护 Receiver 和 Service 返回权限拒绝;非公开 Provider 无法查询;合法深链仍能打开允许的页面。
同时执行正常业务回归,避免安全加固破坏应用内导航、通知点击、Widget、快捷方式或合作应用调用。安全修复不能只看攻击用例,还要覆盖合法调用链。
六、建议的系统化检查表
1. 所有组件是否显式声明 android:exported,而不是依赖默认行为。
2. 每个导出组件是否都有明确业务理由和负责人。
3. 导出组件是否配置了合适的权限,权限保护级别是否足够。
4. 外部 Intent 的 action、data、extras、ClipData 和 flags 是否经过校验。
5. 深链是否采用 scheme、host、path 和参数允许列表。
6. 是否把登录状态等同于对象访问权限;敏感对象是否做归属校验。
7. Provider 是否限制读写权限、URI 路径和可访问列。
8. WebView 是否可能接收外部 URL,是否限制协议和可信域名。
9. 敏感操作是否要求用户可感知确认,是否具备审计日志。
10. release 构建是否移除了调试组件、测试接口和环境切换入口。
七、风险与注意事项
第一,adb shell 的调用身份不完全等同于普通第三方应用,某些组件可能对 shell、系统 UID 或调试构建表现不同。最终结论应结合一个自建的最小调用端应用进行验证。
第二,Android 版本、targetSdkVersion 和厂商系统会影响后台服务、隐式广播和包可见性行为。报告中应记录设备型号、系统版本、目标 SDK、应用版本和复现时间。
第三,不要为了验证风险而触发删除数据、支付、发信、上传隐私或高频后台任务。优先使用无害测试数据和可回滚操作。
第四,组件权限只能解决入口访问控制,不能替代业务鉴权。例如一个合法合作应用拥有调用权限,也不代表它能读取所有用户的记录。
第五,日志中可能出现 Intent 参数、账号标识和临时令牌。测试完成后应清理日志、测试账号与抓取文件,报告中也应脱敏。
八、常见问题
问题 1:exported=true 是否一定是高危漏洞?
不是。浏览器深链、系统回调或跨应用服务可能必须导出。风险等级取决于是否可访问敏感信息、能否触发敏感操作、是否需要交互、是否存在额外权限和业务校验。
问题 2:设置 android:exported=false 后是否还需要代码校验?
内部组件仍应校验关键参数,避免应用自身其他入口传入异常数据。对于可导出组件,代码级校验更是必需。Manifest 是第一道边界,不是全部边界。
问题 3:为什么 adb 命令返回 Permission Denial?
可能是组件未导出、设置了调用权限、组件名称错误、用户空间不一致,或者系统限制了该类后台行为。应结合 dumpsys package 和 logcat 判断,而不是直接认定组件不存在。
问题 4:签名级权限能解决所有跨应用调用问题吗?
不能。它可以限制调用方必须由相同或受信任签名签署,但仍需检查参数、业务状态和对象权限。此外,签名轮换、多应用发布流程和密钥管理也要纳入设计。
问题 5:应用已经要求登录,为什么还要检查组件?
因为外部调用可能绕过正常页面顺序,直接到达登录后的 Activity。即便页面能打开,真正敏感的数据读取和操作仍应在接口层验证会话与对象权限,不能只依赖前端页面状态。
九、总结
Android 导出组件审计的关键不是机械统计 exported=true,而是建立“入口、参数、身份、业务动作、数据对象”之间的完整调用链。实践时先盘点 Manifest,再通过本地 adb 命令验证可达性,随后检查运行时参数校验和业务授权,最后对修复结果及正常功能同时回归。
优先级上,建议先关闭无必要的导出组件,再为确需开放的入口增加签名级权限或细粒度读写权限,并对深链、广播、Binder 和 Provider 参数实施允许列表与对象级鉴权。这样既能缩小攻击面,也能避免只靠隐藏入口或页面流程形成脆弱的伪安全边界。
Android 应用由 Activity、BroadcastReceiver、Service、ContentProvider 等组件构成。为了支持网页跳转、跨进程调用、系统广播或数据共享,部分组件需要对其他应用开放。问题在于,一旦组件被错误导出,或者虽然设置了访问权限但组件内部缺少参数校验,任意本地应用就可能绕过正常业务流程调用它。
这类风险常见于登录状态绕过、内部页面暴露、敏感广播伪造、后台任务滥用和数据越权读取。本文只针对自己创建的本地实验应用或已获得书面授权的测试环境,演示如何系统识别导出组件、验证实际可达性,并给出对应修复方法。请勿将相关命令用于未授权设备或真实第三方应用。
二、核心原理
Android 组件能否被外部应用调用,主要受以下因素影响:
1. android:exported:决定组件是否允许其他应用直接访问。
2. intent-filter:在旧版 Android 行为中,声明过滤器可能影响组件默认导出状态。面向 Android 12 及以上版本时,包含 intent-filter 的组件必须显式声明 android:exported。
3. android
4. 运行时校验:即使组件已经导出,也应校验调用参数、用户身份、当前登录状态和业务状态。
5. URI 授权:Provider 或文件共享还会受到 grantUriPermissions、路径规则及临时读写授权影响。
需要注意,导出并不等于漏洞。例如供浏览器唤起的深链 Activity 本来就需要导出;真正的安全问题是外部输入能够触发未授权行为,或者访问本不应公开的数据。
三、前置条件
建议准备以下本地环境:
1. Android Studio 与 Android SDK Platform Tools。
2. 一台模拟器或专用测试机,建议不要使用存有私人数据的日常手机。
3. 自己编译的实验 APK,包名假设为 com.example.exportlab。
4. adb 已加入 PATH,执行 adb devices 能看到状态为 device 的测试设备。
5. 测试前保留源码与日志,便于区分设计行为、配置缺陷和系统版本差异。
可先确认基础环境:
adb devices
adb shell getprop ro.build.version.release
adb shell getprop ro.build.version.sdk
adb shell pm path com.example.exportlab
本文示例组件均属于虚构的本地实验应用,不对应任何真实线上目标。
四、搭建最小实验场景
在实验应用的 AndroidManifest.xml 中加入以下组件。为了观察风险,这里故意保留不安全配置:
<activity
android:name=".DebugPanelActivity"
android:exported="true" />
<activity
android:name=".DeepLinkActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="exportlab" android:host="open" />
</intent-filter>
</activity>
<receiver
android:name=".SyncReceiver"
android:exported="true">
<intent-filter>
<action android:name="com.example.exportlab.SYNC" />
</intent-filter>
</receiver>
<service
android:name=".MaintenanceService"
android:exported="true" />
<provider
android:name=".NotesProvider"
android:authorities="com.example.exportlab.notes"
android:exported="true" />
DebugPanelActivity 可以显示一段虚构的调试状态;SyncReceiver 接收 mode 参数后写入日志;MaintenanceService 执行一次无害的缓存统计;NotesProvider 只保存测试文本。不要在实验应用中放入真实账号、令牌、联系人或生产接口地址。
五、分步骤审计与验证
步骤 1:建立组件清单
首先从源码中的 AndroidManifest.xml 检查所有 activity、activity-alias、receiver、service 和 provider。若只有 APK,可在自己的授权样本上使用 Android SDK 工具分析:
aapt2 dump xmltree app-debug.apk AndroidManifest.xml
也可以查看系统安装后的包信息:
adb shell dumpsys package com.example.exportlab
重点记录组件全名、exported 状态、intent-filter、permission、authorities、grantUriPermissions 和 path-permission。仅看到 exported=true 还不能直接定性,应继续检查组件功能和调用结果。
步骤 2:验证导出 Activity
在本地设备上尝试显式启动实验 Activity:
adb shell am start -W -n com.example.exportlab/.DebugPanelActivity
观察输出中的 Status、Activity 和 TotalTime,同时查看应用界面。如果未经过主界面登录流程就能进入仅供内部使用的调试页,说明导航入口缺少保护。
防御上不能只依赖“正常用户看不到按钮”。若该页面完全不需要外部访问,应修改为:
<activity
android:name=".DebugPanelActivity"
android:exported="false" />
如果确实需要跨应用调用,则应添加签名级权限,并在 Activity 内再次校验调用目的与会话状态。对于敏感操作,还应在执行前要求用户确认,而不是在 onCreate 中根据 Intent 参数直接执行。
步骤 3:检查深链参数
调用本地实验深链:
adb shell am start -W -a android.intent.action.VIEW -d "exportlab://open?screen=profile&next=home"
在代码中重点检查 getIntent().getData()、getQueryParameter() 以及后续页面跳转逻辑。常见问题是应用直接信任 next、url 或 target 参数,将其传给 WebView、文件接口或内部路由。
推荐做法是使用明确的允许列表,例如只接受 profile、help、order-detail 等固定路由;拒绝未知 scheme 和 host;对订单号等业务对象进行服务端归属校验;不要根据外部参数直接打开任意类名。若深链涉及网页登录回跳,应校验随机 state,并避免把长期令牌放在 URL 查询参数中。
步骤 4:验证广播接收器边界
向实验 Receiver 发送自定义广播:
adb shell am broadcast -a com.example.exportlab.SYNC --es mode demo -n com.example.exportlab/.SyncReceiver
随后仅查看自己应用的日志:
adb logcat --pid=$(adb shell pidof com.example.exportlab)
如果任意应用都能触发同步、清理、切换环境或修改配置,可能形成未授权业务操作。无外部调用需求时,应设置 android:exported="false"。确需开放时,可声明自定义签名权限:
<permission
android:name="com.example.exportlab.permission.INTERNAL_SYNC"
android
并在 Receiver 上配置:
android
签名级权限适合同一开发方、使用可信签名发布的应用间通信。不要把普通级权限误当作安全边界,因为其他应用可能申请普通权限。
步骤 5:验证 Service 是否可被滥用
对实验 Service 执行一次启动测试:
adb shell am startservice -n com.example.exportlab/.MaintenanceService --es task stats
较新 Android 版本会限制后台启动 Service,因此结果可能因系统版本和前后台状态不同。测试重点不是绕过系统限制,而是确认服务是否存在外部可达入口,以及传入参数能否触发敏感行为。
Service 不应根据外部字符串直接执行文件删除、账号切换、环境切换或动态代码加载。对于耗时任务,应结合 JobScheduler 或 WorkManager;对于必须导出的绑定服务,应配置权限,并在 Binder 方法中进行调用身份和参数校验。仅在 Manifest 中加权限,而 Binder 接口内部完全信任参数,仍可能留下业务越权问题。
步骤 6:检查 ContentProvider 数据暴露
在仅含虚构测试数据的 Provider 上查询:
adb shell content query --uri content://com.example.exportlab.notes/items
若无需跨应用共享,最直接的修复是设置 android:exported="false"。确需共享时,应分别配置 readPermission 与 writePermission,并尽量缩小 URI 范围。还要检查 selection、sortOrder 和 projection 是否被直接拼接进 SQL。
Provider 查询应使用参数化条件。例如不要把外部 selection 直接连接成 SQL 字符串,应通过 SQLiteDatabase.query 的 selectionArgs 传递变量。对于 write、delete 和 update 操作,还要验证调用者是否有权操作对应记录,避免只验证“拥有写权限”,却不验证“是否拥有该条数据”。
步骤 7:回归验证修复效果
完成修改后重新编译和安装:
adb install -r app-debug.apk
再次执行前述 am start、am broadcast、am startservice 和 content query 命令。预期结果应是:内部 Activity 无法由 shell 代表的外部调用者直接打开;受保护 Receiver 和 Service 返回权限拒绝;非公开 Provider 无法查询;合法深链仍能打开允许的页面。
同时执行正常业务回归,避免安全加固破坏应用内导航、通知点击、Widget、快捷方式或合作应用调用。安全修复不能只看攻击用例,还要覆盖合法调用链。
六、建议的系统化检查表
1. 所有组件是否显式声明 android:exported,而不是依赖默认行为。
2. 每个导出组件是否都有明确业务理由和负责人。
3. 导出组件是否配置了合适的权限,权限保护级别是否足够。
4. 外部 Intent 的 action、data、extras、ClipData 和 flags 是否经过校验。
5. 深链是否采用 scheme、host、path 和参数允许列表。
6. 是否把登录状态等同于对象访问权限;敏感对象是否做归属校验。
7. Provider 是否限制读写权限、URI 路径和可访问列。
8. WebView 是否可能接收外部 URL,是否限制协议和可信域名。
9. 敏感操作是否要求用户可感知确认,是否具备审计日志。
10. release 构建是否移除了调试组件、测试接口和环境切换入口。
七、风险与注意事项
第一,adb shell 的调用身份不完全等同于普通第三方应用,某些组件可能对 shell、系统 UID 或调试构建表现不同。最终结论应结合一个自建的最小调用端应用进行验证。
第二,Android 版本、targetSdkVersion 和厂商系统会影响后台服务、隐式广播和包可见性行为。报告中应记录设备型号、系统版本、目标 SDK、应用版本和复现时间。
第三,不要为了验证风险而触发删除数据、支付、发信、上传隐私或高频后台任务。优先使用无害测试数据和可回滚操作。
第四,组件权限只能解决入口访问控制,不能替代业务鉴权。例如一个合法合作应用拥有调用权限,也不代表它能读取所有用户的记录。
第五,日志中可能出现 Intent 参数、账号标识和临时令牌。测试完成后应清理日志、测试账号与抓取文件,报告中也应脱敏。
八、常见问题
问题 1:exported=true 是否一定是高危漏洞?
不是。浏览器深链、系统回调或跨应用服务可能必须导出。风险等级取决于是否可访问敏感信息、能否触发敏感操作、是否需要交互、是否存在额外权限和业务校验。
问题 2:设置 android:exported=false 后是否还需要代码校验?
内部组件仍应校验关键参数,避免应用自身其他入口传入异常数据。对于可导出组件,代码级校验更是必需。Manifest 是第一道边界,不是全部边界。
问题 3:为什么 adb 命令返回 Permission Denial?
可能是组件未导出、设置了调用权限、组件名称错误、用户空间不一致,或者系统限制了该类后台行为。应结合 dumpsys package 和 logcat 判断,而不是直接认定组件不存在。
问题 4:签名级权限能解决所有跨应用调用问题吗?
不能。它可以限制调用方必须由相同或受信任签名签署,但仍需检查参数、业务状态和对象权限。此外,签名轮换、多应用发布流程和密钥管理也要纳入设计。
问题 5:应用已经要求登录,为什么还要检查组件?
因为外部调用可能绕过正常页面顺序,直接到达登录后的 Activity。即便页面能打开,真正敏感的数据读取和操作仍应在接口层验证会话与对象权限,不能只依赖前端页面状态。
九、总结
Android 导出组件审计的关键不是机械统计 exported=true,而是建立“入口、参数、身份、业务动作、数据对象”之间的完整调用链。实践时先盘点 Manifest,再通过本地 adb 命令验证可达性,随后检查运行时参数校验和业务授权,最后对修复结果及正常功能同时回归。
优先级上,建议先关闭无必要的导出组件,再为确需开放的入口增加签名级权限或细粒度读写权限,并对深链、广播、Binder 和 Provider 参数实施允许列表与对象级鉴权。这样既能缩小攻击面,也能避免只靠隐藏入口或页面流程形成脆弱的伪安全边界。