12 KiB
Android移动安全第七章_DeepLink安全
QIANXIN Team 来源:https://forum.butian.net/share/4874
系列目录:
- 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. 前言
用户在浏览器中点击一个链接,直接跳转到了某个 App 的特定页面——这就是 Deep Link(深度链接)。它让 Web 和 App 之间建立了连接,用户不需要手动打开 App 再找到对应功能。
Android 上实现 Deep Link 有三种方式:
- 自定义 Scheme(如
myapp://):最早的方式,任何 App 都可以注册任意 scheme - Intent URL(如
intent://):第二章讲过,通过 URL 编码一个完整的 Intent - App Links(如
https://example.com/path):Android 6.0 引入,通过域名验证确保只有域名所有者的 App 能处理对应 URL
三种方式的安全性差异很大。自定义 Scheme 没有任何归属验证,任何 App 都可以声称自己能处理 myapp://;App Links 通过域名验证建立了 URL 和 App 之间的绑定关系,安全性高得多。
本章围绕 Deep Link 的注册机制、参数处理、以及链接劫持展开。
2. 自定义 Scheme
2.1 注册方式
App 通过在 Manifest 中声明 intent-filter 来注册自定义 Scheme:
<activity
android:name=".DeepLinkActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="myapp" android:host="open" />
</intent-filter>
</activity>
BROWSABLE category 表示这个 Activity 可以从浏览器中被唤起。注册后,用户在浏览器中点击 myapp://open/path?key=value 就会启动这个 Activity。
Activity 中通过 getIntent().getData() 获取 URL:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Uri uri = getIntent().getData();
if (uri != null) {
String path = uri.getPath();
String key = uri.getQueryParameter("key");
}
}
2.2 Scheme 劫持
自定义 Scheme 没有归属验证。如果两个 App 注册了相同的 scheme,系统会弹出选择器让用户选择由哪个 App 处理。攻击者可以注册与目标 App 相同的 scheme:
<activity
android:name=".HijackActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="myapp" android:host="open" />
</intent-filter>
</activity>
如果用户选择了攻击者的 App,URL 中携带的参数(可能包含 token、回调地址等)就被攻击者获取了。
在某些 Android 版本和厂商 ROM 上,如果只有一个 App 注册了某个 scheme,系统会直接跳转而不弹选择器。攻击者安装自己的 App 后,系统开始弹选择器,用户可能不会注意到多了一个选项。
2.3 OAuth 回调劫持
很多 App 使用 OAuth(开放授权协议,允许用户授权第三方 App 访问自己在某个服务上的数据,而不需要提供密码)进行第三方登录。OAuth 流程中,授权服务器会把授权码(authorization code)通过重定向发送到 App 注册的回调 URL。
如果回调 URL 使用自定义 Scheme(如 myapp://oauth/callback?code=xxx),攻击者注册相同的 scheme 就能截获授权码:
- 用户在目标 App 中发起 OAuth 登录
- 浏览器跳转到授权服务器,用户授权
- 授权服务器重定向到
myapp://oauth/callback?code=AUTH_CODE - 系统弹出选择器,如果用户选择了攻击者的 App,授权码被截获
- 攻击者用授权码换取 access token,获得用户账户的访问权限
这是自定义 Scheme 最常见的实际攻击场景。
3. App Links
3.1 原理
App Links 是 Android 6.0(API 23)引入的,用于解决自定义 Scheme 的归属验证问题。它使用标准的 https:// URL,通过域名验证确保只有域名所有者的 App 能处理对应 URL。
注册方式:
<activity
android:name=".AppLinkActivity"
android:exported="true">
<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="example.com" android:pathPrefix="/app" />
</intent-filter>
</activity>
android:autoVerify="true" 告诉系统在安装时验证域名归属。验证方式是:系统访问 https://example.com/.well-known/assetlinks.json,检查其中是否声明了当前 App 的包名和签名证书。
assetlinks.json 的内容:
[{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.example.app",
"sha256_cert_fingerprints": ["AB:CD:EF:..."]
}
}]
验证通过后,用户点击 https://example.com/app/xxx 会直接跳转到 App,不弹选择器,其他 App 无法劫持。
3.2 验证失败的情况
App Links 的验证可能失败:
- 服务器没有部署 assetlinks.json
- assetlinks.json 中的包名或证书指纹不匹配
- 服务器返回非 200 状态码
- 网络不可用(安装时无法访问服务器)
验证失败后,App Links 退化为普通的 Deep Link——系统会弹出选择器,和自定义 Scheme 一样存在劫持风险。
3.3 验证状态检查
可以通过 ADB 检查 App Links 的验证状态:
adb shell pm get-app-links com.example.app
输出中 verified 表示验证通过,none 表示未验证。
4. Deep Link 参数注入
4.1 URL 参数直接使用
很多 App 从 Deep Link URL 中读取参数后直接使用,没有做校验。常见的危险模式:
加载到 WebView
Uri uri = getIntent().getData();
String url = uri.getQueryParameter("url");
webView.loadUrl(url);
攻击者构造 myapp://open?url=https://evil.com,App 的 WebView 加载攻击者的页面。第五章讲过,如果 WebView 注册了 JS Bridge,攻击者的页面就能调用 App 的原生方法。
跳转到内部组件
Uri uri = getIntent().getData();
String target = uri.getQueryParameter("target");
Intent intent = new Intent();
intent.setClassName(getPackageName(), target);
startActivity(intent);
攻击者构造 myapp://open?target=com.example.app.InternalActivity,绕过 exported=false 限制启动内部组件。这和第二章讲的 Intent 重定向本质相同,只是入口从 Intent extras 变成了 URL 参数。
拼接到 SQL 或文件路径
String id = uri.getQueryParameter("id");
Cursor cursor = db.rawQuery("SELECT * FROM users WHERE id = " + id, null);
和第四章讲的 ContentProvider SQL 注入类似,只是注入点从 ContentResolver 参数变成了 URL 参数。
4.2 演示
演示 App(com.demo.deeplinksecurity)的 VulnDeepLinkActivity 注册了 demoapp:// scheme,从 URL 参数中读取 action 和 url,根据 action 执行不同操作:
action=webview:把 url 参数加载到 WebViewaction=navigate:把 target 参数作为组件名跳转
adb shell am start -a android.intent.action.VIEW \
-d "demoapp://open?action=webview\&url=https://example.com"
App 的 WebView 加载了攻击者指定的 URL:
adb shell am start -a android.intent.action.VIEW \
-d "demoapp://open?action=navigate\&target=com.demo.deeplinksecurity.InternalSecretActivity"
绕过 exported=false 限制,启动了内部页面:
5. Deep Link 与 Intent 的交互
5.1 getIntent().getData() 的来源
Activity 通过 getIntent().getData() 获取 Deep Link URL。但这个 Intent 不一定来自浏览器——任何 App 都可以构造一个带 data 的 Intent 发送给导出的 Activity:
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse("myapp://open?token=stolen"));
intent.setPackage("com.target.app");
startActivity(intent);
这意味着即使 Deep Link 没有从浏览器触发,攻击者仍然可以通过 Intent 直接传入恶意 URL。所以 Deep Link 的参数校验不能依赖"URL 来自可信的浏览器"这个假设。
5.2 intent:// URL 与 Deep Link 的结合
第二章讲过 intent:// URL 可以编码一个完整的 Intent。如果目标 App 的 WebView 支持 intent:// URL 处理,攻击者可以通过 Deep Link 先让 WebView 加载一个页面,页面中再通过 intent:// URL 触发更深层的攻击:
demoapp:
exploit.html 中:
<a href="intent://dummy#Intent;component=com.target.app/.InternalActivity;end">click</a>
这是一个两步攻击链:Deep Link → WebView → intent:// → 内部组件。
6. 版本演进
6.1 Android 6.0(API 23):App Links
引入 App Links,通过 assetlinks.json 验证域名归属。验证通过后直接跳转,不弹选择器。
6.2 Android 12(API 31):App Links 行为变化
Android 12 对 App Links 做了调整:
- 未通过验证的 App Links 不再在选择器中显示,而是直接在浏览器中打开
- 用户可以在设置中手动管理每个 App 的链接处理行为
- 系统对 assetlinks.json 的验证更严格
这个改动提高了 App Links 的安全性,但也意味着如果 App 的 assetlinks.json 配置有问题,Deep Link 会完全失效(直接在浏览器打开而不是跳转到 App)。
6.3 Android 12:intent-filter 必须声明 exported
第一章提到过,Android 12 要求所有带 intent-filter 的组件必须显式声明 android:exported。Deep Link 的 Activity 必须设置 exported="true",否则无法安装。这不是安全改进(Deep Link Activity 本来就需要导出),但让开发者更明确地意识到这些组件是对外暴露的。
7. 总结
Deep Link 把 URL 变成了 App 的入口。
回顾一下:
- 自定义 Scheme 没有归属验证,任何 App 都可以注册相同的 scheme 进行劫持,OAuth 回调是常见的攻击目标
- App Links 通过域名验证解决了劫持问题,但验证失败时会退化为普通 Deep Link
- URL 参数直接传给 WebView、组件跳转、SQL 查询都是常见的注入点
- Deep Link 的参数不能假设来自可信来源,因为任何 App 都可以通过 Intent 直接传入
下一章讲 Android 广播安全。广播是 Android 的事件通知机制,第一章简单提过广播注入和劫持,下一章会深入展开——有序广播的优先级劫持、Sticky 广播的数据残留、以及 LocalBroadcastManager 的使用场景。
通过网盘分享的文件:deeplink安全演示.apk
链接: https://pan.baidu.com/s/1OhxiQyFIazUmphxxBB3svA?pwd=95yg 提取码: 95yg

