写点什么

有隙可乘 - Android 序列化漏洞分析实战

  • 2024-05-16
    广东
  • 本文字数:7260 字

    阅读完需:约 24 分钟

作者:vivo 互联网大前端团队 - Ma Lian


本文主要描述了 FileProvider,startAnyWhere 实现,Parcel 不对称漏洞以及这三者结合产生的漏洞利用实战,另外阐述了漏洞利用的影响和修复预防措施,这个漏洞波及了几乎所有的 Android 手机,希望能带给读者提供一些经验和启发。

一、背景

大家应该看到过一篇《2022 年的十大安全漏洞与利用》的文章,文章中提到一个漏洞:

利用 Android Parcel 序列化和反序列不匹配,借助应用 FileProvider 未限制路径,可以获取系统级 startAnyWhere 能力,从而获取用户敏感信息,修改系统配置,获取系统特权等等。


这里面有三个关键词:

  • Parcel 不匹配漏洞

  • startAnyWhere

  • FileProvider 未限制路径


看到以上,大家可能会就其中涉及到的几个点有些疑问:

  1. startAnyWhere 是什么意思,是什么样的能力?

  2. Parcel 不匹配漏洞是什么原理,是如何产生的?

  3. FileProvider 的作用是什么,未限制路径又是什么问题?

  4. 这几者之间存在什么关联,又会带来哪些风险?

二、FileProvider

2.1 功能简介

首先我们来简单讲一下 FileProvider,FileProvider 其实就是用来进程间共享文件的。


上方左侧图是早期的应用间共享文件的方案,就是 A 应用把文件存在外置存储,然后把文件的物理地址给到 B 应用,B 应用去这个地址去取。


那么这样的方式存在哪些问题呢?有以下几点:

  1. 权限无法控制:文件存放的位置,要保证都能访问,这样无法精确控制权限;

  2. 权限无法回收:文件一旦共享,无法撤销;

  3. 目录结构暴露:文件共享需要公开原始的文件地址,暴露了目录结构;

  4. 隐私内容泄露:部分私有目录文件共享存在安全隐私泄露的风险。

基于以上问题,google 基于 ContentProvider 设计了 FileProvider,如上方右侧图,文件共享必须基于 FileProvider,由 AMS 来管控权限,提供的协议也是定制的 content 协议。

2.2 使用简介

了解了 FileProvider 出现的背景,下面介绍一下 FileProvider 的使用,使用 FileProvider 需要提供四个参数:

  • Uri(文件地址)

  • Action(接收方信息)

  • Type(文件类型)

  • Flags(授予权限)


如下面代码,最终通过 startActivity 来发起共享,记住这个 startActivity,很重要。

Intent intent = new Intent();intent.setAction("");Uri uri = FileProvider.getUriForFile(getContext(), "", file);intent.setType(getContext().getContentResolver().getType(uri));intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);startActivity(intent);
复制代码

2.3 URI 简介

content URI 和普通的 Http 协议一样,也拥有 scheme,authorities,path。

示例:content://authorities /XXX/xxx.txt。


Android 提供了 xml 配置,如下代码所示,把实际的路径映射成一个虚拟的名称,这样的优势就是限制了路径,可以把指定目录的路径共享出去。


看到这里,大家就可以理解未限制路径的含义了,简单讲就是把系统根目录给共享出去了,正确的做法是只共享需要使用的目录。

<provider    android:name="androidx.core.content.FileProvider"    android:authorities="${applicationId}.file"    android:exported="false"    android:grantUriPermissions="true">    <meta-data        android:name="android.support.FILE_PROVIDER_PATHS"        android:resource="@xml/******_paths" /></provider>
复制代码


<?xml version="1.0" encoding="utf-8"?><resources>    <paths>        <files-path name="test_in" path="/test/file" />        <external-path name="test_external" path="/test/file" />    </paths></resources>
复制代码

2.4 权限简介

FLAG_GRANT_READ_URI_PERMISSION:文件读权限;

FLAG_GRANT_WRITE_URI_PERMISSION:文件写权限;

FLAG_GRANT_PERSISTABLE_URI_PERMISSION:持久授权,直至设备重启或者主动调用 revokeUriPermission;

FLAG_GRANT_PREFIX_URI_PERMISSION:相同前缀路径统一授权。

2.5 授权方式

//第一种授权方式intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);//第二种授权方式getContext().grantUriPermission("packageName", uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
复制代码

2.5.1 第一种授权方式:

一次授权,用完即止。

2.5.2 第二种授权方式:

  • 不含持久授权 flag 的,权限依附于进程存活;

  • 包含持久授权 flag 的,重启或者主动拒绝权限才会消失。

2.6 小结

上面主要简单介绍了一下 FileProvider 的设计思想、技术方案、使用方式,由此我们可以对文章开头提出的一些疑问进行解答。


1、FileProvider 的作用

答:跨进程共享文件,一般通过 startActivity 的方式。


2、未限制路径

答:没有指定需要共享的文件目录,将系统根目录共享出去了。


3、存在什么风险,如何进行攻击

答:单针对 FileProivider 来看,风险较小,光依赖 FileProvider 这个问题还是没法进行攻击的,原因如下:

  • 文件共享需要业务主动通过 startActivity 才能发起

  • 读写权限交由系统来管理

三、startAnyWhere

接下来讲一下上文中提到的 startAnyWhere,顾名思义,就是应用想打开哪个页面就打开哪个页面,那么在 Android 系统中,谁才有这个能力呢?

3.1 实现原理

能够实现 startAnyWhere 的只有系统 SystemUid 应用,这类应用在 startActivity 进行权限校验的时候是直接放行的,无论 Activity 是否 exported,都能打开,最常见的应用比如系统设置。


下面是一个系统设置打开第三方应用的案例,通过设置可以直接打开第三方的账户登录页。

3.2 实现流程

通过设置页面的添加账号的功能,可以直接拉起对应应用的界面,这个是今天漏洞的核心,我们来看一下系统调用流程。


如下图,首先系统设置调用 AccountManager 的 addAccount,然后通过 SystemServer 中的 AccountManagerService,一直调用到目标 APP 本身的 AddAccount 实现。


由 APP 本身提供一个 BundleBundle 里面本身包含了一个 intent 的由设置进行打开。

这个里面其实存在一个风险,第三方应用可以随意提供一个恶意 Intent,系统会直接调用 startActivity,随之而来的风险很大。


上图中还存在一个第 0 步,即这个流程的发起方可以是三方应用本身,不一定需要从设置进入,那么这个整个流程就闭环了,完全无需用户介入,用户也可以完全无感知。


不过这个风险呢,google 在 Android4.4 之后已经修复了,4.4 之后增加了对 intent 内容的校验。


代码如下:

if (result != null&& (intent = result.getParcelable(AccountManager.KEY_INTENT)) != null) {    if (!checkKeyIntent(Binder.getCallingUid(),intent)) {            onError(AccountManager.ERROR_CODE_INVALID_RESPONSE,"invalid intent in bundle returned");            return;       }  }
复制代码

上面代码就是取出外部传入的 KEY-INTENT 进行校验,这里面已经出现了今天的主角 Parcel,整个攻击也是通过 Parcel 漏洞使得恶意的 KEY-INTENT 绕过系统的检查。

四、Parcel

下面我们看一下 parcel 漏洞及原理。

4.1 Parcel 简介

parcel 是专门为 Android 提供的一个序列化的类,parcel 的原理其实很简单,就是一个严格的对称读写,如下代码所示。

public void writeToParcel(Parcel dest, int flags) {   dest.writeInt(mSize);}public void readFromParcel(Parcel in) {   mSize = in.readInt();}
复制代码

同时序列化遵循基本的 TLV 格式,也就是 Tag-Length-Value,Tag 代表类型,Length 代表长度,Value 代表值,当然一些特殊情况:

  • Length 不描述:有固定长度的类型可以不描述 length,比如 long,int 等等;

  • Tag 不描述:bundle 序列化时,key 一定是 string 类型,所以不需要描述 Tag。


回到对称读写这一块,如果这个代码不对称了会出现什么情况呢,google 曾经在 android 源码中出现了很多类似不对称的错误,看一下下面几个案例。

4.2 Parcel 不对称读写案例

4.2.1 案例 1

如下图,这是一个典型且明显的不对称,writeLog&readInt,为什么不对称,很简单,int 和 long 对应的长度不一样。

4.2.2 案例 2

这是一个比较隐晦的不对称案例,是 Android 原生的 WorkSource 类,这个不对称一眼无法看出,以致于最近的 Android 版本这个问题一直存在,这个类也是此次漏洞攻击真正被利用的一个类。


下面简单看一下 WorkSource 序列化和反序列化的流程。


  • 序列化

如下述代码,WorkSource 序列化时,如果 mChains 是一个长度为 0 的空 list,那么就会走 else 分支,此时序列化会连续写两个 0。

序列化:    public void writeToParcel(Parcel dest, int flags) {        dest.writeInt(mNum);        dest.writeIntArray(mUids);        dest.writeStringArray(mNames);         if (mChains == null) {            dest.writeInt(-1);        } else { // 当mChains不为空的时候,这时候写了两个0            dest.writeInt(mChains.size());// 写第一个0            dest.writeParcelableList(mChains, flags);// 写第二个0        }    }
复制代码


  • 反序列化

如下述代码,WorkSource 反序列化时,当读到第一个 0 也就是 numChains=0 的时候,这个对应 mChains 长度为 0,同样也会走 else 分支,此时 mChains 直接被置为 null,但是序列化其实是写了两个 0,这时候后面还有一个 0 没有读,这样序列化和反序列化就造成了不对称。

反序列化:WorkSource(Parcel in) {        mNum = in.readInt();        mUids = in.createIntArray();        mNames = in.createStringArray();         int numChains = in.readInt(); // 读第一个0        if (numChains > 0) {            mChains = new ArrayList<>(numChains);            in.readParcelableList(mChains, WorkChain.class.getClassLoader());        } else { // 当读到numChains=0的时候,这时候直接就将mChains置为null,第二个0还没有读            mChains = null;        }    }
复制代码

当然实际上不对称的类还有很多,大家可以看下网上泄露出来的漏洞利用源码,有很多这样的类,这里就不列出来了,知道了漏洞的本质是因为 Parcel 读写不对称,我们接下来看一下其中的原理。

4.3 parcel 漏洞原理

了解 parcel 漏洞真正的原理之前,首先来看一下系统校验 intent 的序列化流程。

4.3.1 系统校验序列化流程

首先攻击者手动会序列化一次需要传给系统的 bundle,然后系统会反序列化一次进行校验,校验完之后又会重新序列化交给设置,然后设置真正去打开页面的时候会再次反序列化,这样就经历了两次序列化与反序列化,因为其中读写不对称,所以给了攻击者有机可趁的机会。

4.3.2 漏洞原理简介

这个漏洞核心就是前后一共经历了两次序列化和反序列化。我们以上面 4.2.1 案例 1 的不对称举例(readInt()对应 writeLong()),当出现不对称读写之后,两次序列化与反序列化会有什么后果?如下图所示可以看到:

第一次序列化:输入两个 int 1;

第二次反序列化:读的时候是 readInt(),读出两个 int 1;

第三次序列化:写的时候是 writeLong(),这是分别写了 long 1 和 int 1,long 的长度是 int 长度的双倍;

第四次反序列化:读的时候是 readInt(),第一个 long 1 会被分成两个 int 来读,所以就一次读成了 101。

而攻击者也正是借助这个不对称,导致实际输入和输出不一样,隐藏了恶意的 KEY-INTENT,从而绕过了系统的校验,以此打开任意一个页面,实现 startAnyWhere。

4.3.3 漏洞原理实践

因为案例 1 比较明显,google 早已经修复该漏洞,而 WorkSource 因为比较隐晦,所以该漏洞一直存在,我们接下来看一下如何利用 WorkSource 来构造攻击实现。


下面一张图带你搞明白如何通过两次序列化和反序列化达到我们的目的:


由上述文章可知,最终给到系统校验的是一个 bundle 类型的数据结构,bundle 是存储 key-value 类型的,而我们目的就是要将恶意的 KEY-INTENT 隐藏起来然后绕过系统的校验。接下来详细讲一下实现步骤:


1、手动序列化:

如上图左侧第一列,手动序列化这个 bundle,这个 bundle 序列化时携带了三个 key-value:

  • 第一个 key-value:WorkSource 相关的;

  • 第二个 key-value:经过精心构造;

  • 第三个 key-value:隐藏恶意的 KEY-INTENT。

第一次序列化后的 bundle 通过 16 进制打印出来如下图所示:


2、系统进行反序列化

经过系统第一次反序列化,没有触发不对称,系统是读不到这个恶意的 KEY-INTENT 的,所以自然校验通过。


3、系统重新序列化

系统校验完需要重新序列化,这时候由于读写不对称,最终红色区域【1,-1】两个值变成了【0,0】。


4、setting 反序列化

setting 再次反序列化,上面也讲到了,由于不对称,原本两个 0 只读了 1 个 0。


5、解析最终的 key-value

  1. 读第一个 key-value:由于上述 WorkSource 的不对称,原本两个 0 只读了 1 个 0;

  2. 读第二个 key-value:由于读第一个时少读了一个 0,剩余的 0 变成了第二个 key-value 的内容,整体内容错位,由于遵循 TLV 的格式,错位之后,0 和 13 变成了第二个 key-value 的 key,恶意 KEY-INTENT 前的所有值都变成了第二个 key-value 的 value;

  3. 读第三个 key-value:此时真正恶意的 KEY-INTENT 变成我们需要的第三个 key-value。

五、漏洞攻击实战

通过上面两节,我们可以看到,借助 startAnyWhere 和 parcel 漏洞,可以绕过系统校验任意打开一个页面,下面来看两个真实案例:

5.1 实战案例 1

可以看到在虚拟机上,通过这个漏洞直接就打开了锁屏密码的设置页面,然后可以直接绕过密码校验将锁屏密码改掉。

5.2 实战案例 2

案例 1 已经足以反应出问题和风险,但是实际上国内的手机经过改造,基本不会存在这个问题,那么我们来看一下真机上的使用案例:


在讲这个案例之前,我们要先额外讲一下 XXSDK 中存在的一个 AsistActivity,里面存在一段代码,如下所示。


这个代码很简单,就是接受外部的 intent 的然后直接 startActivity 了,这里面又提到了 startActivity,上面文件共享也是这样调用的,正好符合了 FileProvider 的使用逻辑。

@Overrideprotected void onCreate(Bundle savedInstanceState) {    super.onCreate(savedInstanceState);    //此处省略部分代码    Intent intent = getIntent().getParcelableExtra(Intent.EXTRA_INTENT);    int intExtra = intent == null ? 0 : intent.getIntExtra("", 0);    //此处省略部分代码    startActivityForResult(intent, intExtra);    }
复制代码


借助这个类,我们便可以模拟一个完整的攻击流程,如下图所示:


  • 第一步:攻击 APP 构造一个 intent1,这个 intent1 的意图是打开上述 AssistActivity;intent1 中携带了恶意的 intent2,这个 intent2 的意图打开攻击 APP 的指定页面,然后让应用共享指定文件了;

  • 第二步:调用 andorid 系统添加账号页面;

  • 第三步:业务 APP 中由于集成了 AssistActivity,接受恶意的 intent2 会直接 startActivity 进行共享文件;

经过以上三步,直接就把 APP 的一些隐私文件共享给攻击 APP,同时攻击 APP 可以在恶意 intent 中授权直接修改文件。

5.3 恶意 intent 的代码

下面看一下恶意 intent 的代码:

private Intent makeFileIntent() {        Intent intent1 = new Intent().setComponent(new ComponentName("XXX", "xxx.xxx.AssistActivity")); // 打开AssistActivity        Uri uri = Uri.parse("content://xxxx/xxx_info");        Intent intent2 = new Intent(mContext, SecondActivity.class); // 打开攻击者的页面并且共享指定URI的文件        intent2.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);        intent2.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);        intent2.setType(mContext.getContentResolver().getType(uri));        intent2.setData(uri);        intent1.putExtra("key", intent2);// 恶意intent2放入intent1中        return intent;    }
复制代码

5.4 恶意序列化的代码

目前漏洞均已修复,为避免风险,不展示所有代码。

private static Bundle makeEvilIntent(Intent intent) {        Bundle bundle = new Bundle();        Parcel obtain = Parcel.obtain();        Parcel obtain2 = Parcel.obtain();        Parcel obtain3 = Parcel.obtain();        obtain2.writeInt(3);// bundle中key-va长度        obtain2.writeString("firstKey");        obtain2.writeInt(4); //VAL_PARCELABLE        obtain2.writeString("android.os.WorkSource");        obtain2.writeInt(-1);//mNum        obtain2.writeInt(-1);//mUids        obtain2.writeInt(-1);//mNames        obtain2.writeInt(1);//mChains.length        obtain2.writeInt(-1);        ...此处省略一些构造代码        bundle.readFromParcel(obtain);        return bundle;    }
复制代码


以下是视频演示,通过上面这一段攻击代码,拿到了手机上某个 APP 存在应用私有目录下的账号信息, 同样为了隐私,此处部分脱敏。


六、漏洞利用影响

通过上文的介绍,我们知道借助这个漏洞可以实现对系统任意文件的修改,下面列出了漏洞带来的影响:

  1. 读取用户隐私信息;

  2. 安装恶意应用;

  3. 改写动态加载的代码;

  4. 改写系统配置;

  5. 获取特殊权限。

七、漏洞修复措施

除了发现问题更重要的是解决问题,下面列出了修复这个漏洞对应的一些方案:

系统层:

  • 修复 pacel 漏洞的不对称;

  • 系统校验的时候,做两次序列化与反序列化;


应用层:

  • FileProvider 增加路径限制;

  • 接受 intent 的 Activity 要着重注意校验,设置黑白名单。

八、漏洞预防措施

漏洞其实是不可避免的,下面是面对层出不穷漏洞的一些预防措施:

  1. 组件能不导出就不导出;

  2. 可导出的组件建议增加签名或者包名校验;

  3. 接受 intent 或者 url 参数务必校验;

  4. 文件共享务必遵循最小化原则;

  5. 敏感内容需要进行加密。

九、总结

接下来简单回顾一下,本文主要讲了 5 方面内容:

  1. 第 1 方面:主要描述了 FileProvider,阐述了其出现背景、设计原理、使用方式、优缺点等;

  2. 第 2 方面:主要描述了 startAnyWhere,阐述了其实现原理、实现方式;

  3. 第 3 方面:主要描述了 Parcel 不对称漏洞,阐述了 Parcel 的设计原理、不对称漏洞、漏洞案例、漏洞原理以及漏洞利用方案;

  4. 第 4 方面:主要描述了漏洞攻击实战,从模拟器到真机,从原理到代码,演示了通过漏洞攻击手机、获取用户隐私信息的流程;

  5. 第 5 方面:主要是讲了漏洞带来的影响、漏洞的修复和预防措施。


整体来讲,这个漏洞波及了所有的 Android 手机,无论是对用户,对企业都造成了巨大的损失。

作为开发者的我们需要从自身做起,守护好每一个环节,避免让攻击者有隙可乘。

发布于: 刚刚阅读数: 7
用户头像

官方公众号:vivo互联网技术,ID:vivoVMIC 2020-07-10 加入

分享 vivo 互联网技术干货与沙龙活动,推荐最新行业动态与热门会议。

评论

发布
暂无评论
有隙可乘 - Android 序列化漏洞分析实战_Android序列化漏洞_vivo互联网技术_InfoQ写作社区