静态代码分析:从Manifest看组件安全

做Android安全这么多年,我有个习惯——拿到一个APK,第一件事不是反编译代码,而是先打开AndroidManifest.xml。为什么?因为Manifest就像房子的设计图纸,哪里开了门、哪里装了锁、哪里留了窗户,一眼就能看出来。很多安全漏洞,其实在Manifest阶段就能发现。

今天我们就聊聊,怎么通过静态分析Manifest文件,找出Activity劫持、Provider数据泄露这些风险点。

一、组件导出属性:门到底开没开?

组件导出,说白了就是「这个组件能不能被其他App调用」。你想想看,一个Activity如果被标记为exported="true",那任何App都能启动它。这就像你家大门没上锁,谁都能推门进来。

核心判断逻辑:

  • exported="true" → 组件对外暴露,任何App可调用
  • exported="false" → 组件仅内部使用
  • 未显式设置 → 取决于是否有Intent Filter

这里有个坑,我踩过好几次。如果组件声明了Intent Filter,系统默认会把exported设为true。很多开发者以为不写exported就是安全的,其实恰恰相反。

<!-- 危险写法:隐式导出 -->
<activity android:name=".LoginActivity">
    <intent-filter>
        <action android:name="com.example.LOGIN" />
    </intent-filter>
</activity>

<!-- 安全写法:显式声明 -->
<activity 
    android:name=".LoginActivity"
    android:exported="false">
    <intent-filter>
        <action android:name="com.example.LOGIN" />
    </intent-filter>
</activity>

我个人建议,所有组件都显式声明exported属性。别偷懒,别依赖默认行为。默认行为这东西,不同厂商、不同Android版本可能不一样,你赌不起。

二、权限声明:谁有钥匙?

权限声明是第二道防线。就算组件导出了,如果设置了权限,那只有持有对应权限的App才能调用。这就像门虽然开着,但门口有个保安查证件。

我在项目中遇到过一种情况:某个ContentProvider声明了权限,但权限的保护级别是normal。normal级别意味着什么?任何App只要在Manifest里声明一下就能获取,系统不会弹窗询问用户。说白了,形同虚设。

保护级别 含义 风险等级
normal App声明即可获取,无需用户确认 低(但容易被滥用)
dangerous 运行时需用户授权
signature 仅同签名App可获取 高(推荐)
signatureOrSystem 系统应用或同签名App

我的经验:对于敏感组件,尽量用signature级别的权限。这样只有你自己签名的App才能调用,第三方恶意App根本拿不到钥匙。

三、Intent Filter配置:暴露的攻击面

Intent Filter定义了组件能响应哪些隐式Intent。你想想看,如果一个Activity声明了,那系统就会把它注册到系统中,其他App可以通过匹配的Intent来启动它。

这里有个经典攻击场景——Activity劫持。恶意App可以注册一个与目标App相同Action的Activity,然后通过Intent匹配机制,劫持用户的点击操作。

<!-- 容易被劫持的配置 -->
<activity android:name=".PaymentActivity">
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <data android:scheme="https" android:host="pay.example.com" />
    </intent-filter>
</activity>

这段配置有什么问题?它声明了VIEW Action,并且没有设置exported="false"。这意味着任何App都可以通过一个https://pay.example.com的链接来启动这个Activity。如果恶意App也注册了同样的Intent Filter,系统可能会弹出选择框,用户一不小心就点错了。

避坑指南:我曾经审计过一个金融App,它的支付确认Activity就用了上面的配置。攻击者只需要写一个简单的App,注册同样的Intent Filter,就能在用户支付时弹出自己的伪造界面。嗯,后来我们强制要求所有涉及敏感操作的Activity必须使用显式Intent调用,并且exported设为false。

四、ContentProvider:数据泄露的重灾区

ContentProvider是Android组件安全的重灾区。为什么?因为它直接操作数据。一个导出的ContentProvider,如果没有做好权限控制,就等于把数据库直接暴露给了全世界。

我见过最离谱的一个案例:某个社交App的ContentProvider导出了,而且没有设置任何权限。攻击者只需要知道URI路径,就能通过ContentResolver.query()读取用户的聊天记录、通讯录、甚至密码。

<!-- 高危配置 -->
<provider
    android:name=".DataProvider"
    android:authorities="com.example.data"
    android:exported="true" />

<!-- 安全配置 -->
<provider
    android:name=".DataProvider"
    android:authorities="com.example.data"
    android:exported="false" />

如果确实需要导出Provider,那必须做好三件事:

  1. 设置readPermission和writePermission
  2. 使用signature级别的权限
  3. 对URI进行路径校验,防止目录遍历

五、知识体系总览

说了这么多,我们来梳理一下。Manifest静态分析的核心逻辑,其实就是回答三个问题:

  • 门开了吗?(exported属性)
  • 谁有钥匙?(权限声明)
  • 门牌号写了什么?(Intent Filter)

下面这张图,是我自己总结的Manifest安全分析流程,你可以对照着用。

Manifest 组件安全分析流程 获取 AndroidManifest.xml 遍历所有组件(Activity/Service/Provider/Receiver) 检查 exported 是否显式声明? 默认值是否安全? 检查权限 保护级别是否足够? 是否使用signature? 检查 Intent Filter 是否暴露敏感Action? 是否存在劫持风险? 输出风险报告 + 修复建议

这张图的核心逻辑很简单:拿到Manifest后,逐个检查每个组件的exported、权限和Intent Filter。三个维度都安全,组件才算安全。任何一个维度出问题,都可能成为攻击入口。

六、实战建议

最后,分享几个我在实际项目中用到的检查清单:

Manifest安全检查清单:

  • 所有组件是否显式声明了exported?
  • 导出的组件是否设置了合理的权限?
  • 权限的保护级别是否至少为signature?
  • Intent Filter是否暴露了敏感Action?
  • ContentProvider的URI路径是否做了校验?
  • 是否有不必要的声明?

嗯,其实这些检查点都不复杂。但就是这些看似简单的地方,最容易出问题。我见过太多App,功能做得花里胡哨,结果Manifest里一堆漏洞。安全这东西,往往就藏在细节里。


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