AndroidManifest保护:清单文件混淆、权限保护、组件保护
AndroidManifest.xml 这个文件,说白了就是应用的「身份证」加「通行证」。
我见过太多开发者,把精力全花在代码混淆上,却忽略了清单文件。结果呢?反编译一看,四大组件、权限声明、暴露的 Activity 一览无余。嗯,这就像你把家门锁得死死的,却在门口贴了张纸条:「钥匙在花盆底下」。
核心观点:清单文件是攻击者的第一份情报。保护它,等于切断了对方 30% 的信息来源。
1. 清单文件泄露了什么?
我们先看看,一个没保护的 AndroidManifest.xml 会暴露哪些信息:
- 组件列表:所有 Activity、Service、Receiver、Provider 的完整类名
- 权限声明:应用申请了哪些敏感权限
- 导出状态:哪些组件是 exported="true" 的
- Intent Filter:组件能接收哪些隐式意图
- 元数据:API Key、渠道号、配置信息
我曾在一次渗透测试中,仅凭清单文件就找到了三个可被外部调用的导出组件。其中一个 Provider 居然没有做权限校验,直接可以读取数据库。你想想看,这有多危险?
2. 清单文件混淆:不只是改个名字
很多人以为清单文件混淆就是把包名改掉。其实远不止这些。
我个人习惯的做法是:
- 组件类名混淆:利用 ProGuard 的
-keep规则,把清单中引用的类名也一并混淆。但要注意,清单中声明的类名必须和混淆后的保持一致。 - 字符串加密:把清单中的敏感字符串(如 API Key、自定义权限名)在构建时加密,运行时再解密。
- 动态注册组件:能不写在清单里的组件,尽量用代码动态注册。这样反编译后看不到组件列表。
// ProGuard 配置示例:保留清单中引用的类,但允许重命名
-keep,allowobfuscation class com.example.app.MainActivity
-keep,allowobfuscation class com.example.app.SecondActivity
// 注意:清单中声明的类必须显式 keep,否则混淆后找不到
小技巧:我习惯在构建脚本中写一个 Gradle Task,自动解析 AndroidManifest.xml,提取所有组件类名,然后生成对应的 ProGuard keep 规则。这样既不会漏掉,也不会多 keep 不该 keep 的类。
3. 权限保护:别让你的权限被滥用
权限保护这块,我踩过不少坑。
先看一个常见问题:自定义权限的 protectionLevel 设置不当。
| protectionLevel | 含义 | 风险等级 |
|---|---|---|
| normal | 安装时自动授予,无需用户确认 | 低 |
| dangerous | 运行时需用户确认 | 中 |
| signature | 仅同签名应用可获取 | 高(推荐) |
| signatureOrSystem | 同签名或系统应用可获取 | 高 |
我曾经接手过一个项目,自定义权限用的是 normal 级别。结果呢?任何第三方应用只要声明了这个权限,就能调用我们暴露的接口。这等于把大门钥匙复制了无数份发出去。
避坑指南:自定义权限一定要用 signature 级别。除非你有特殊需求,否则别碰 normal 和 dangerous。我曾经因为偷懒用了 normal,上线后被人利用,紧急发版修复,那叫一个狼狈。
另外,对于系统权限,我建议做「最小权限原则」:
- 只申请真正需要的权限
- 能用
uses-permission-sdk-23的,别用uses-permission - 敏感权限(如定位、相机)尽量在运行时申请,别写在清单里
4. 组件保护:把门锁好
组件保护是清单文件保护的重头戏。说白了,就是控制谁能调用你的组件。
4.1 导出控制
这是最基本也最容易出问题的地方。我见过太多 exported="true" 的组件,其实根本不需要对外暴露。
<!-- 错误示范:不必要地导出 -->
<activity android:name=".InternalActivity" android:exported="true" />
<!-- 正确做法:不导出,仅内部使用 -->
<activity android:name=".InternalActivity" android:exported="false" />
<!-- 如果有 Intent Filter,默认就是导出的,要小心 -->
<activity android:name=".ShareActivity">
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
// 注意:有 intent-filter 的组件,exported 默认为 true
4.2 权限校验
对于必须导出的组件,一定要加权限校验。我常用的做法是:
- 在清单中声明自定义权限
- 在组件上添加
android:permission属性 - 在代码中二次校验调用方的身份
<!-- 声明自定义权限 -->
<permission android:name="com.example.app.ACCESS_INTERNAL"
android:protectionLevel="signature" />
<!-- 在组件上应用权限 -->
<provider android:name=".InternalProvider"
android:exported="true"
android:permission="com.example.app.ACCESS_INTERNAL" />
重要:清单中的权限声明只是第一道防线。我建议在代码中再做一次 checkCallingPermission() 校验。为什么?因为清单权限可以被绕过(比如通过反射调用)。双重校验才保险。
4.3 Intent 过滤保护
对于有 Intent Filter 的组件,攻击者可以通过构造隐式 Intent 来调用。我建议:
- 尽量使用显式 Intent 调用内部组件
- 对于必须使用隐式 Intent 的场景,在代码中校验 Intent 的来源和内容
- 使用
setPackage()限制 Intent 只能发给特定包名
// 安全地发送隐式 Intent
Intent intent = new Intent("com.example.action.CUSTOM");
intent.setPackage("com.example.app"); // 限制目标包名
startActivity(intent);
5. 知识体系总览
下面这张图,是我对 AndroidManifest 保护的整体理解。你可以把它当作一个检查清单:
6. 实战建议
最后,我总结几条实战中验证过的建议:
- 清单文件要纳入版本管理:每次修改都要 review,别偷偷加权限
- 用工具扫描:我常用
androguard和qark自动分析清单文件的安全问题 - 测试导出组件:写自动化测试,验证每个导出组件是否做了权限校验
- 别信默认值:Android 不同版本的默认 exported 值不一样,显式声明最安全
再次提醒:我曾经因为相信「默认就是安全的」,把一个 Provider 的 exported 留空。结果在 Android 12 上,默认值变成了 true。那一次线上事故,让我养成了「所有组件都显式声明 exported」的习惯。
清单文件保护,说白了就是「别让攻击者轻易拿到你的家底」。做好这三件事——混淆、权限、组件保护——你的应用就已经比市面上 80% 的应用安全了。
公众号:蓝海资料掘金营,微信deep3321