Android广播接收器安全审计:从组件暴露到权限绕过的完整挖掘链路 1. 从一个容易被忽视的组件说起移动安全学习这条路上很多人一上来就扎进逆向、脱壳、Hook 框架这些硬核方向结果学了大半年连一个 App 的四大组件都没摸清楚。我刚开始也是这样觉得 BroadcastReceiver 这种发广播、收广播的东西太简单了能有什么安全问题直到有一次在分析一个本地应用时随手用adb发了一条广播直接把一个本该隐藏的页面拉了起来才意识到这个组件的水比想象中深得多。BroadcastReceiver中文叫广播接收器是 Android 四大组件之一。它的核心职责就一件事接收并处理广播消息。系统开机、网络变化、电量低、屏幕点亮这些系统级事件都会以广播的形式发出来应用之间也可以自定义广播来传递消息。听起来很朴素但问题恰恰出在谁都能发、谁都能收这个特性上。如果开发者没有做好权限控制和数据校验攻击者就能通过构造恶意广播触发应用内部的敏感逻辑甚至实现越权操作、信息泄露、拒绝服务。这篇笔记面向的是正在学习移动安全、想系统掌握组件漏洞挖掘方法的人。不管你是刚入门的新手还是已经会写简单 PoC 但不知道如何系统排查的老手我都会从原理讲到实操把 BroadcastReceiver 的漏洞挖掘链路完整拆一遍。核心关键词就几个广播接收器、组件暴露、权限绕过、Intent 注入、动态注册。搞懂这几个词背后的机制你就能独立完成一次完整的广播组件安全审计。需要提前说明的是下面所有案例和代码都是基于常见开发实践构造的模拟场景不针对任何真实应用。工具使用、命令操作都经过脱敏处理你可以直接在自己的测试环境里复现。2. BroadcastReceiver 的两种注册方式与暴露面差异2.1 静态注册Manifest 里的常驻哨兵静态注册是在AndroidManifest.xml里通过receiver标签声明的系统在应用安装时就会解析并注册这个接收器。它的特点是生命周期跟随应用进程即使应用没启动只要匹配到广播系统也能把进程拉起来处理。receiver android:name.MyReceiver android:exportedtrue intent-filter action android:namecom.example.MY_ACTION / /intent-filter /receiver这里最关键的一个属性是android:exported。它决定了这个接收器是否允许其他应用发送广播过来。在 Android 12API 31之前如果声明了intent-filterexported默认就是true从 Android 12 开始必须显式声明这个属性否则安装时直接报错。很多老应用在升级 targetSdk 时忘了改这个结果要么编译不过要么被迫加上exportedtrue反而把攻击面暴露得更彻底。我实际审计时第一步永远是全局搜索exportedtrue的 receiver。这一步能快速筛出所有对外暴露的入口效率比逐个文件看高得多。2.2 动态注册代码里的临时接线动态注册是在代码中调用registerReceiver()完成的通常在 Activity 或 Service 的onCreate/onResume里注册在onDestroy/onPause里注销。IntentFilter filter new IntentFilter(com.example.DYNAMIC_ACTION); registerReceiver(myReceiver, filter);动态注册的暴露面取决于注册时是否指定了权限以及IntentFilter的匹配规则。如果没加权限限制任何应用都能发送匹配的广播。更隐蔽的是动态注册的接收器只在组件存活期间有效静态扫描 Manifest 根本发现不了必须结合代码审计或运行时 Hook 才能定位。提示Android 14API 34对动态注册的 receiver 增加了强制要求——如果应用 targetSdk 升级到 34注册时必须显式指定RECEIVER_EXPORTED或RECEIVER_NOT_EXPORTED否则会抛异常。这个变化让很多裸奔的动态注册被迫显式声明反而方便了审计人员快速定位。两种注册方式的暴露面差异可以用一张表来对比维度静态注册动态注册声明位置AndroidManifest.xml代码中 registerReceiver生命周期随应用进程随注册组件静态扫描可见性高低默认 exported旧版本有 filter 则为 true取决于权限参数常见漏洞越权调用、Intent 注入权限缺失、生命周期泄漏理解这张表你就知道为什么审计时要静态扫 Manifest 动态看代码两条腿走路。3. 广播漏洞的四种典型形态与成因3.1 未授权访问谁都能敲的门这是最常见也最容易被低估的问题。一个 receiver 声明了exportedtrue却没有在onReceive里做任何调用方校验那么任何应用都能发送对应 action 的广播触发里面的逻辑。假设某个应用有个 receiver 收到com.example.RESET_PASSWORD广播后就把密码重置为默认值且不校验发送方。攻击者只需要几行代码Intent intent new Intent(com.example.RESET_PASSWORD); intent.putExtra(user, admin); sendBroadcast(intent);密码就被重置了。这类漏洞的本质是把敏感操作暴露给了不可信输入源。开发者往往觉得这个 action 名字只有我知道但 action 字符串在反编译后一览无余根本算不上秘密。3.2 权限校验形同虚设有些开发者意识到了风险于是加了权限检查但检查方式有问题。常见错误有三种第一种是只在onReceive里检查getCallingUid()但广播的发送方 UID 在接收时并不总是可靠可获取的尤其是通过sendBroadcast发送时接收方拿到的 calling Uid 可能是系统或 null。第二种是自定义权限的 protectionLevel 设置不当。比如声明了一个normal级别的权限任何应用只要在 Manifest 里申请就能获得等于没设防。正确的做法是用signature级别只有相同签名的应用才能持有。permission android:namecom.example.PRIVATE_PERM android:protectionLevelsignature / receiver android:name.SecureReceiver android:permissioncom.example.PRIVATE_PERM android:exportedtrue ... /receiver第三种是权限声明了但发送方没带接收方也不强制校验导致权限成了摆设。3.3 Intent 注入与数据污染广播携带的 Intent 里可以放各种 extra 数据。如果接收方直接把这些数据拼接到 SQL 语句、文件路径、URL 或者反射调用的类名里就可能引发注入类漏洞。举个模拟场景某 receiver 收到广播后取出extra里的filename然后openFileOutput(filename)。攻击者传入../../databases/secret.db就可能读写到非预期目录。再比如把 extra 里的类名通过反射实例化攻击者就能加载任意类后果更严重。这类漏洞的挖掘要点是追踪 onReceive 里所有来自 Intent 的数据流看它们最终流向了哪里。流向文件、数据库、网络、反射、命令执行的地方都是高危汇聚点。3.4 拒绝服务与逻辑轰炸即使没有敏感操作一个 exported 的 receiver 也可能被滥用。攻击者可以高频发送广播触发接收方做耗时操作比如大量 IO、网络请求导致应用卡顿甚至 ANR。如果onReceive里还有空指针解引用、数组越界等隐患一条精心构造的广播就能让应用崩溃。这类问题单独看危害不大但在某些场景下可以作为组合攻击的一环比如配合其他漏洞制造竞态条件。4. 手工挖掘的完整操作链路4.1 第一步静态定位所有 receiver拿到一个 APK先反编译。我习惯用apktool解出 Manifest 和 smali再用jadx看 Java 伪代码两者互补。apktool d target.apk -o target_dir打开target_dir/AndroidManifest.xml搜索receiver。重点记录三个信息android:name、android:exported、android:permission以及intent-filter里的 action。如果 exported 是 true 且没有 permission直接标记为高优先级目标。如果 exported 没写根据 targetSdk 判断默认值小于 31 且有 filter 的默认 true大于等于 31 的必须显式声明。4.2 第二步定位 onReceive 实现并做数据流分析用 jadx 打开 APK根据 receiver 的类名找到对应的 Java 文件看onReceive方法体。这一步的核心是回答三个问题这个方法里做了什么敏感操作写文件、发网络、改配置、启动组件这些操作的输入参数来自哪里是否直接取自 Intent有没有对调用方做校验校验逻辑是否可绕过我一般会在 jadx 里用CtrlF搜索getStringExtra、getParcelableExtra、getIntExtra这些关键词快速定位数据入口然后顺着变量往下追。4.3 第三步构造 PoC 验证定位到可疑点后写一个简单的测试应用或者用adb直接发广播验证。用 adb 发送带 extra 的广播adb shell am broadcast -a com.example.MY_ACTION \ --es filename ../../databases/secret.db \ --ei user_id 1如果要指定发送方包名模拟特定应用发送可以加-p参数。如果要发送有序广播用am broadcast默认就是有序的接收方可以通过setResult回传数据。对于需要动态注册才能触发的 receiveradb 直接发可能无效因为接收器还没注册。这时候需要先启动对应组件或者用 Frida 在运行时 HookregisterReceiver来观察注册的 filter 和权限。4.4 第四步验证权限绕过路径如果 receiver 声明了权限不要急着放弃。先确认权限的 protectionLevel。用aapt或apkanalyzer查看权限定义aapt dump permissions target.apk如果看到protectionLevelnormal或者dangerous说明任何应用都能申请到这个权限防护基本无效。只有signature级别才真正限制了调用方必须是同签名应用。还有一种情况是权限声明在 receiver 上但发送方用的是sendBroadcast(intent, receiverPermission)这种带权限参数的重载。如果接收方没在 Manifest 里声明对应权限或者声明了但没生效也可能绕过。5. 动态注册接收器的运行时排查技巧静态扫描搞不定的动态注册必须上运行时手段。我常用的组合是 Frida adb logcat。5.1 用 Frida Hook registerReceiver写一个简单的 Frida 脚本Hookandroid.content.ContextWrapper.registerReceiver打印出每次注册的 filter、action 和权限参数Java.perform(function () { var ContextWrapper Java.use(android.content.ContextWrapper); ContextWrapper.registerReceiver.overload( android.content.BroadcastReceiver, android.content.IntentFilter ).implementation function (receiver, filter) { console.log([registerReceiver] filter actions:); var actions filter.actionsIterator(); while (actions.hasNext()) { console.log( action: actions.next()); } return this.registerReceiver(receiver, filter); }; });跑起来之后操作应用触发注册逻辑logcat 或 Frida 控制台就会打印出所有动态注册的 action。拿到 action 后再用 adb 发广播验证。5.2 观察广播的实际处理路径有时候一个 action 被多个 receiver 接收或者被系统拦截。可以用dumpsys activity broadcasts查看当前系统里的广播状态虽然信息有限但能确认广播是否被发出。更直接的办法是在onReceive上下断点。如果应用可调试用 jdb 或 Android Studio 附加调试如果不可调试用 Frida Hook 目标类的onReceive方法打印调用栈和 Intent 内容var TargetReceiver Java.use(com.example.MyReceiver); TargetReceiver.onReceive.implementation function (context, intent) { console.log([onReceive] action: intent.getAction()); console.log([onReceive] extras: intent.getExtras()); console.log(Java.use(android.util.Log).getStackTraceString( Java.use(java.lang.Exception).$new())); return this.onReceive(context, intent); };调用栈能告诉你这个广播是从哪里发出来的是系统发的还是某个应用发的对判断攻击面很有帮助。5.3 动态注册的生命周期陷阱动态注册的 receiver 如果忘记在onDestroy里unregisterReceiver会导致内存泄漏同时接收器在组件销毁后仍然存活扩大了攻击窗口。审计时可以重点看注册和注销是否成对出现尤其是那些在onCreate注册但只在异常分支注销的代码。6. 那些文档里不会写的踩坑经验6.1 Android 版本差异能把人坑哭同一个 APK在 Android 10 上能触发的广播漏洞在 Android 13 上可能完全没反应。原因通常是高版本对隐式广播做了限制。Android 8.0API 26之后大部分隐式广播不能再静态注册接收必须动态注册。这意味着你按 Manifest 里的 action 发广播系统可能直接丢弃。解决办法是确认目标 action 是否在隐式广播限制名单里。如果在就得想办法触发动态注册路径或者用-p指定包名发送显式广播如果 receiver 支持。6.2 exported 的默认值随 targetSdk 变化我见过太多人因为没注意 targetSdk 而误判。一个 receiver 没写 exportedtargetSdk 是 30那有 filter 就是 truetargetSdk 是 31那必须显式写没写的话应用根本装不上。审计前先看AndroidManifest.xml里的targetSdkVersion这个数字决定了默认行为。6.3 权限校验的调用方 UID 不可靠前面提过广播接收时getCallingUid()经常拿不到发送方。真正可靠的做法是发送方在 Intent 里带上自己的标识接收方通过签名校验或 token 验证。如果只依赖 UID很容易被伪造或拿到 null 后逻辑走偏。6.4 有序广播的 result 回传可能泄露数据有序广播允许接收方通过setResult把数据回传给发送方。如果接收方把敏感信息塞进 result而发送方是攻击者数据就泄露了。审计时看到setResult、setResultData、setResultExtras要格外留意。6.5 别忽略系统广播的滥用有些应用监听BOOT_COMPLETED、CONNECTIVITY_CHANGE这类系统广播来做初始化。如果这些 receiver 是 exported 的攻击者可以手动发送同名广播提前触发初始化逻辑可能绕过某些状态检查。虽然高版本对系统广播的伪造有限制但在低版本或特定条件下仍然可行。7. 从挖掘到修复的闭环思路挖到漏洞只是第一步能给出可落地的修复方案才算完整。针对 BroadcastReceiver 的安全问题修复思路可以归纳为几条最小暴露原则。不需要对外接收广播的 receiver一律exportedfalse。动态注册时Android 14 以上显式用RECEIVER_NOT_EXPORTED。权限收紧。确实需要跨应用通信的用signature级别的自定义权限并在 receiver 上声明android:permission。发送方也要用带权限参数的sendBroadcast重载双向校验。输入校验。onReceive里所有来自 Intent 的数据在使用前都要做白名单校验。文件路径要规范化后检查是否在允许目录内类名要限制在预期集合里SQL 参数要参数化。敏感操作二次确认。重置密码、修改配置这类操作不能只靠一条广播触发应该加上 token 或用户交互确认。生命周期管理。动态注册的 receiver 确保在对应生命周期回调里注销避免泄漏和窗口扩大。我在实际审计中通常会把发现的问题按可利用性和影响面两个维度排优先级。一个 exported 且无权限、onReceive里直接写文件的 receiver优先级最高一个 exported 但只打日志的优先级最低。这样输出的报告才有重点修复建议也能按轻重缓急排。最后分享一个我自己的习惯每审计完一个应用都会把遇到的 receiver 模式整理成一个小清单比如哪些 action 对应哪些敏感操作哪些权限配置是无效的。积累多了之后再看到新的 APK扫一眼 Manifest 就能大致判断出哪些地方值得深挖。这种模式识别能力比记住某个具体漏洞更有价值。