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);
        }
    }
}
我的习惯:每次写registerReceiver,我都会问自己三个问题——1. 这个Receiver真的需要动态注册吗?2. 我指定权限了吗?3. 我在合适的生命周期注销了吗?三个问题都答「是」,才算过关。

15.3 粘性广播:一个被遗忘的漏洞

粘性广播(Sticky Broadcast),现在用的人不多了。但老项目里还能见到。这东西有个特点:广播发送后,即使Receiver还没注册,等注册后也能收到最后一条广播。

你想想看,这意味着什么?

攻击者可以先发一条恶意广播,然后等你的Receiver注册。你的Receiver一注册,立马收到那条「过期」的广播。如果Receiver里没有做来源校验,直接就中招了。

我曾经在一个金融类App里发现过这个问题。那个App用粘性广播传递登录状态,攻击者伪造了一条「登录成功」的粘性广播。结果呢?App以为用户已经登录,直接跳过了登录页面,暴露了内部功能。

警告:Android 14(API 34)已经废弃了sendStickyBroadcast()。如果你还在用,赶紧迁移。粘性广播的安全模型太脆弱,根本不适合现代应用。

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);

怎么防?我建议的做法是:

  1. 显式设置exported="false":除非你确定需要外部调用,否则别让Receiver暴露。
  2. 使用自定义权限:在Manifest里声明一个protectionLevel="signature"的权限,只有同签名的应用才能发广播。
  3. 在onReceive里做二次校验:检查调用者的包名、签名、UID。
核心原则:永远不要信任广播的发送者。即使你设了权限,也要在Receiver内部再做一次校验。这叫「纵深防御」。

15.5 知识体系总览

下面这张图,我把Broadcast Receiver安全的核心知识点串起来了。你可以把它当作一个检查清单:

Broadcast Receiver 安全知识体系 动态注册 • 生命周期管理 • 权限参数传递 • 注销时机 • 发送者校验 粘性广播 • 延迟接收机制 • 状态伪造风险 • API废弃警告 • 迁移方案 权限绕过 • exported属性 • 签名伪造 • 隐式广播劫持 • 二次校验 防御核心:最小暴露 + 权限控制 + 二次校验 每个Receiver上线前,对照这张图做一次安全检查

15.6 实战检查清单

最后,我把自己平时做Broadcast Receiver安全测试的检查清单分享给你。每次拿到一个App,我都会按这个顺序过一遍:

  • 第一步:列出所有Receiver,区分静态和动态注册
  • 第二步:检查exported属性,看哪些暴露了
  • 第三步:检查权限声明,看有没有signature级别的保护
  • 第四步:检查onReceive里的代码,有没有做发送者校验
  • 第五步:检查粘性广播的使用,有没有废弃API
  • 第六步:尝试发送伪造广播,看能不能触发敏感操作

我曾经用这个清单,在一个第三方SDK里挖出了三个高危漏洞。那个SDK的Receiver没加任何权限,攻击者可以远程触发文件下载、数据库操作、甚至拨打电话。嗯,从那以后,我对所有第三方SDK都格外小心。

避坑指南:我曾经接手过一个项目,Receiver里直接调了startActivity。攻击者发一条广播,就能让App跳转到任意页面。这种问题,用上面的清单第一步就能发现。别嫌麻烦,安全测试就是靠这些「笨办法」堆出来的。

Broadcast Receiver的安全,说白了就是一句话:别信任何人,校验每一步。动态注册要管好生命周期,粘性广播能不用就不用,权限设置要层层加码。做到这三点,你的Receiver基本就安全了。


公众号:蓝海资料掘金营,微信deep3321