15、Broadcast Receiver安全:动态注册、粘性广播、权限绕过
Broadcast Receiver,说白了就是Android系统里的「消息喇叭」。谁都可以发广播,谁都可以收广播。这种开放性,恰恰是安全问题的根源。
我做过不少应用安全审计,发现很多开发者在Broadcast Receiver这块栽过跟头。动态注册、粘性广播、权限绕过——这三个点,几乎每次都能挖出点东西来。今天咱们就一个一个掰开看。
15.1 动态注册 vs 静态注册:哪个更安全?
先问个问题:你平时用哪种方式注册广播?
静态注册,就是在AndroidManifest.xml里写死。动态注册,是在代码里调用registerReceiver()。两者差别不小。
| 对比项 | 静态注册 | 动态注册 |
|---|---|---|
| 生命周期 | 随应用安装常驻 | 随组件生命周期 |
| 权限控制 | 可在Manifest声明 | 需在代码中指定 |
| 接收时机 | 应用未启动也能收 | 必须运行中 |
| 安全风险 | 容易被全局广播轰炸 | 泄漏风险取决于注册时机 |
我个人习惯:能用动态注册就别用静态的。为什么?静态注册的Receiver,应用没启动也能被唤醒。攻击者只要发一条精心构造的广播,就能让你的应用在后台偷偷干活——消耗电量、窃取数据,你都不知道。
我记得有一次做渗透测试,目标应用用了静态注册的Receiver,连个权限都没加。我直接发了一条Intent,里面塞了个恶意URL,结果Receiver自动拉起WebView加载了那个URL。嗯,这就是典型的「无意识攻击面」。
15.2 动态注册的安全陷阱
动态注册虽然灵活,但坑也不少。我总结了几条最常见的:
- 未在onPause/onStop中注销:Receiver泄漏,导致内存泄漏或重复注册
- 注册时未指定权限:任何应用都能发广播给你
- 在onCreate中注册,但未在onDestroy中注销:Activity销毁后Receiver还在
来看一段典型的「问题代码」:
// 问题代码:动态注册未注销
public class MainActivity extends AppCompatActivity {
private BroadcastReceiver myReceiver;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
myReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
// 处理广播
String data = intent.getStringExtra("secret");
Log.d("RECEIVER", "收到数据: " + data);
}
};
IntentFilter filter = new IntentFilter("com.example.MY_ACTION");
registerReceiver(myReceiver, filter); // 没加权限!
}
// 忘记在onDestroy中注销
}
这段代码有两个问题。第一,没在onDestroy里注销Receiver。第二,registerReceiver没传权限参数。任何应用都能发广播过来,你的Receiver照单全收。
正确的做法是这样:
// 安全写法:动态注册+权限控制
public class MainActivity extends AppCompatActivity {
private BroadcastReceiver myReceiver;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
myReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String data = intent.getStringExtra("secret");
// 校验发送者身份
if (checkCallingPermission("com.example.MY_PERMISSION")
== PackageManager.PERMISSION_GRANTED) {
Log.d("RECEIVER", "安全数据: " + data);
} else {
Log.w("RECEIVER", "未授权发送者,丢弃");
}
}
};
IntentFilter filter = new IntentFilter("com.example.MY_ACTION");
// 指定发送者必须持有的权限
registerReceiver(myReceiver, filter, "com.example.MY_PERMISSION", null);
}
@Override
protected void onDestroy() {
super.onDestroy();
if (myReceiver != null) {
unregisterReceiver(myReceiver);
}
}
}
15.3 粘性广播:一个被遗忘的漏洞
粘性广播(Sticky Broadcast),现在用的人不多了。但老项目里还能见到。这东西有个特点:广播发送后,即使Receiver还没注册,等注册后也能收到最后一条广播。
你想想看,这意味着什么?
攻击者可以先发一条恶意广播,然后等你的Receiver注册。你的Receiver一注册,立马收到那条「过期」的广播。如果Receiver里没有做来源校验,直接就中招了。
我曾经在一个金融类App里发现过这个问题。那个App用粘性广播传递登录状态,攻击者伪造了一条「登录成功」的粘性广播。结果呢?App以为用户已经登录,直接跳过了登录页面,暴露了内部功能。
15.4 权限绕过:攻击者的最爱
权限绕过,说白了就是攻击者想办法绕过你设的权限检查。常见的绕过方式有几种:
- 利用exported属性:Receiver声明了intent-filter,系统自动把它标记为exported。攻击者可以直接调用。
- 权限提升:如果Receiver声明了signature级别的权限,但签名校验有漏洞,攻击者可以伪造签名。
- 隐式广播劫持:多个Receiver注册了同一个action,攻击者可以拦截或篡改广播。
来看一个典型的权限绕过场景:
<!-- AndroidManifest.xml -->
<receiver android:name=".SecretReceiver"
android:exported="true"> <!-- 这里暴露了 -->
<intent-filter>
<action android:name="com.example.SECRET_ACTION" />
</intent-filter>
</receiver>
这个Receiver声明了intent-filter,系统自动把它设为exported。攻击者不需要任何权限,直接发广播就能触发:
// 攻击代码
Intent intent = new Intent("com.example.SECRET_ACTION");
intent.putExtra("command", "delete_all_data");
sendBroadcast(intent);
怎么防?我建议的做法是:
- 显式设置exported="false":除非你确定需要外部调用,否则别让Receiver暴露。
- 使用自定义权限:在Manifest里声明一个protectionLevel="signature"的权限,只有同签名的应用才能发广播。
- 在onReceive里做二次校验:检查调用者的包名、签名、UID。
15.5 知识体系总览
下面这张图,我把Broadcast Receiver安全的核心知识点串起来了。你可以把它当作一个检查清单:
15.6 实战检查清单
最后,我把自己平时做Broadcast Receiver安全测试的检查清单分享给你。每次拿到一个App,我都会按这个顺序过一遍:
- 第一步:列出所有Receiver,区分静态和动态注册
- 第二步:检查exported属性,看哪些暴露了
- 第三步:检查权限声明,看有没有signature级别的保护
- 第四步:检查onReceive里的代码,有没有做发送者校验
- 第五步:检查粘性广播的使用,有没有废弃API
- 第六步:尝试发送伪造广播,看能不能触发敏感操作
我曾经用这个清单,在一个第三方SDK里挖出了三个高危漏洞。那个SDK的Receiver没加任何权限,攻击者可以远程触发文件下载、数据库操作、甚至拨打电话。嗯,从那以后,我对所有第三方SDK都格外小心。
Broadcast Receiver的安全,说白了就是一句话:别信任何人,校验每一步。动态注册要管好生命周期,粘性广播能不用就不用,权限设置要层层加码。做到这三点,你的Receiver基本就安全了。
公众号:蓝海资料掘金营,微信deep3321