13、Intent安全:隐式Intent、Intent劫持、Activity劫持

聊到Android安全,Intent绝对是个绕不开的话题。我个人习惯把Intent比作Android系统里的「信使」——它负责在不同组件之间传递消息、启动Activity、发送广播等等。但问题来了,这个信使如果没管好,很容易被坏人利用。

今天我们就来聊聊Intent安全的三个核心问题:隐式Intent的风险、Intent劫持、以及Activity劫持。嗯,这几个坑我在项目里都踩过,有些甚至是在线上环境发现的,想想都后怕。

13.1 隐式Intent:方便背后的隐患

隐式Intent,说白了就是不指定具体目标组件,只告诉系统「我想干什么」。比如你想打开一个网页,系统会自己去找能处理这个Intent的浏览器。

听起来很方便对吧?但方便往往伴随着风险。

核心问题:隐式Intent可能被恶意应用拦截,导致数据泄露或功能被劫持。

举个例子,你的应用发送了一个隐式Intent去打开某个文件:

Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.parse("file:///sdcard/secret.pdf"), "application/pdf");
startActivity(intent);

这段代码看起来没什么问题。但你想过没有——如果用户手机上装了多个PDF阅读器,系统会弹出一个选择框。用户可能随手点了一个恶意应用,这个应用就能读取你的secret.pdf文件内容。

我在项目中遇到过类似的情况。有一次做安全审计,发现某个金融类App用隐式Intent分享交易截图。你想想看,截图里包含银行卡号、交易金额这些敏感信息。如果被恶意应用截获,后果不堪设想。

我的建议:能用显式Intent就别用隐式的。如果非要用,至少加上包名限制。

Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(uri, "application/pdf");
intent.setPackage("com.android.pdfviewer"); // 指定包名
startActivity(intent);

13.2 Intent劫持:恶意应用如何「截胡」

Intent劫持,本质上就是恶意应用注册了和你应用相同的Intent Filter,然后系统在解析隐式Intent时,把请求发给了恶意应用。

为什么会这样?因为Android系统在匹配隐式Intent时,会找所有能处理这个Intent的组件。如果有多个匹配,系统会让用户选择。但用户往往不会仔细看,随手一点就中招了。

我曾经在测试中发现一个有意思的现象:很多开发者以为设置了setPackage()就万事大吉了。但你知道吗?如果目标应用没有安装,系统会抛出ActivityNotFoundException。有些开发者为了「用户体验」,会捕获这个异常然后改用隐式Intent——这恰恰给了攻击者可乘之机。

避坑指南:我曾经见过一个App,在捕获异常后直接调用了startActivity(Intent.createChooser(intent, "选择应用"))。这等于把选择权完全交给了用户,而用户很可能选错。

正确的做法应该是:

try {
    startActivity(intent);
} catch (ActivityNotFoundException e) {
    // 提示用户安装必要的应用,而不是fallback到隐式Intent
    Toast.makeText(this, "请安装PDF阅读器", Toast.LENGTH_SHORT).show();
}

13.3 Activity劫持:更隐蔽的攻击方式

Activity劫持比Intent劫持更隐蔽。攻击者会在你的Activity启动之前,抢先启动一个伪造的Activity覆盖在上面。用户看到的界面和你的App一模一样,但输入的信息全部被恶意应用窃取。

我记得有一次帮客户做渗透测试,发现他们的登录页面存在Activity劫持风险。攻击流程大概是这样的:

  1. 用户打开银行App,进入登录界面
  2. 恶意应用通过监听系统日志或使用AccessibilityService,检测到银行App的登录Activity启动了
  3. 恶意应用立即启动一个伪造的登录界面,覆盖在真实界面之上
  4. 用户输入账号密码,点击登录
  5. 恶意应用窃取信息后,关闭伪造界面,让用户看到真实的登录成功页面

整个过程用户毫无察觉。你想想看,这有多可怕?

防御思路:Activity劫持很难完全防御,但我们可以增加攻击成本。

13.4 实战防御方案

说了这么多风险,我们来聊聊怎么防御。我个人总结了几个实用的方法:

13.4.1 使用显式Intent

这是最基本也最有效的防御手段。能指定包名和类名的,就别偷懒。

// 好的做法
Intent intent = new Intent(this, TargetActivity.class);
startActivity(intent);

// 不好的做法
Intent intent = new Intent("com.example.action.VIEW");
startActivity(intent);

13.4.2 验证Intent来源

如果你的Activity需要接收外部Intent,一定要验证来源:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    
    Intent intent = getIntent();
    String callingPackage = getCallingPackage();
    
    // 验证调用者是否在白名单中
    if (!isTrustedCaller(callingPackage)) {
        finish();
        return;
    }
    
    // 处理Intent
    handleIntent(intent);
}

13.4.3 使用FLAG_ACTIVITY_NEW_TASK时小心

这个Flag容易导致Activity被恶意应用覆盖。我建议在敏感操作时检查当前Activity是否还在前台:

// 检查当前应用是否在前台
ActivityManager am = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
List<ActivityManager.RunningTaskInfo> tasks = am.getRunningTasks(1);
if (tasks != null && !tasks.isEmpty()) {
    String topPackage = tasks.get(0).topActivity.getPackageName();
    if (!topPackage.equals(getPackageName())) {
        // 当前不在前台,可能存在劫持风险
        finish();
        return;
    }
}

小技巧:在Android 5.0以上,可以使用ActivityManager.getAppTasks()来获取当前应用的任务栈,比getRunningTasks()更安全。

13.4.4 使用Android App Links

对于Web链接的处理,我强烈建议使用Android App Links而不是隐式Intent。App Links会验证域名所有权,防止被恶意应用劫持。

<intent-filter android:autoVerify="true">
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />
    <data
        android:scheme="https"
        android:host="www.example.com"
        android:pathPrefix="/content" />
</intent-filter>

13.5 知识体系图

下面这张图总结了Intent安全的核心知识点和防御策略:

Intent安全知识体系 三大风险 隐式Intent风险 数据泄露 组件被恶意调用 Intent劫持 恶意应用截获Intent 用户选择被欺骗 Activity劫持 伪造界面覆盖 窃取用户输入 核心防御策略 显式Intent 指定包名+类名 来源验证 getCallingPackage() 前台检测 防止Activity覆盖 App Links 域名验证 安全无小事,Intent安全从每一行代码做起

13.6 总结

Intent安全这块,说白了就是「信任管理」的问题。你信任了不该信任的组件,或者把选择权交给了用户,就容易出问题。

我个人习惯在做安全设计时遵循三个原则:

  • 最小权限原则:Intent能指定多精确就指定多精确
  • 默认拒绝原则:对于外部传入的Intent,默认不信任
  • 纵深防御原则:不要依赖单一防御手段,多层防护更可靠

嗯,今天就聊到这里。这些内容都是我在实际项目中踩过的坑总结出来的,希望能帮你少走一些弯路。


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