无机可乘
一、背景
大家应该看到过一篇《2022年的十大安保破绽与应用》的文章,文章中提到一个破绽:
应用Android Parcel序列化和反序列不婚配,借助运行FileProvider未限度门路,可以失掉系统级startAnyWhere才干,从而失掉用户敏感消息,修正系统性能,失掉系统特权等等。
这外面有三个关键词:
看到以上,大家或许会就其中触及到的几个点有些不懂:
二、FileProvider
首先咱们来便捷讲一下FileProvider,FileProvider其实就是用来进程间共享文件的。
上方左侧图是早期的运行间共享文件的打算,就是A运行把文件存在外置存储,而后把文件的物理地址给到B运行,B运行去这个地址去取。
那么这样的方式存在哪些疑问呢?有以下几点:
基于以上疑问,google基于ContentProvider设计了FileProvider,如上方右侧图,文件共享必定基于FileProvider,由AMS来管控权限,提供的协定也是定制的content协定。
了解了FileProvider发生的背景,上方引见一下FileProvider的经常使用,经常使用FileProvider须要提供四个参数:
如上方代码,最终经过 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);
content URI和普通的Http协定一样,也领有scheme,authorities,path。
示例:content://authorities /XXX/xxx.txt。
Android提供了xml性能,如下代码所示,把实践的门路映射成一个虚构的称号,这样的长处就是限度了门路,可以把指定目录的门路共享进来。
看到这里,大家就可以了解未限度门路的含意了,便捷讲就是把系统根目录给共享进来了,正确的做法是只共享须要经常使用的目录。
FLAG_GRANT_READ_URI_PERMISSION :文件读权限;
FLAG_GRANT_WRITE_URI_PERMISSION :文件写权限;
FLAG_GRANT_PERSISTABLE_URI_PERMISSION :耐久授权,直至设施重启或许被动调用revokeUriPermission;
FLAG_GRANT_PREFIX_URI_PERMISSION :相反前缀门路一致授权。
//第一种授权方式intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);//第二种授权方式getContext().grantUriPermission("packageName", uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
2.5.1 第一种授权方式:
一次性授权,用完即止。
2.5.2 第二种授权方式:
上方关键便捷引见了一下FileProvider的设计思维、技术打算、经常使用方式,由此咱们可以对文章扫尾提出的一些不懂启动解答。
1、FileProvider的作用
答:跨进程共享文件,普统统过startActivity的方式。
2、未限度门路
答:没有指定须要共享的文件目录,将系统根目录共享进来了。
3、存在什么危险,如何启动攻打
答:单针对FileProivider来看,危险较小,光依赖FileProvider这个疑问还是没法启动攻打的,要素如下:
三、startAnyWhere
接上去讲一下上文中提到的startAnyWhere,望文生义,就是运行想关上哪个页面就关上哪个页面,那么在Android系统中,谁才有这个才干呢?
能够成功startAnyWhere的只要系统SystemUid运行,这类运行在startActivity启动权限校验的时刻是间接放行的,无论Activity能否exported,都能关上,最经常出现的运行比如系统设置。
上方是一个系统设置关上第三方运行的案例,经过设置可以间接关上第三方的账户登录页。
经过设置页面的参与账号的性能,可以间接拉起对应运行的界面,这个是当天破绽的外围,咱们来看一下系统调用流程。
如下图,首先系统设置调用AccountManager的addAccount,而后经过SystemServer中的AccountManagerService,不时调用到指标APP自身的AddAccount成功。
由APP自身提供一个,外面自身蕴含了一个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破绽及原理。
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代表值,当然一些不凡状况:
回到对称读写这一块,假设这个代码不对称了会发生什么状况呢,google曾经在android源码中发生了很多相似不对称的失误,看一下上方几个案例。
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不为空的时刻,这时刻写了两个0dest.writeInt(mChains.size());// 写第一个0dest.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(); // 读第一个0if (numChains > 0) {mChains = new ArrayList<>(numChains);in.readParcelableList(mChains, WorkChain.class.getClassLoader());} else { // 当读到numChains=0的时刻,这时刻间接就将mChains置为null,第二个0还没有读mChains = null;}}
当然实践上不对称的类还有很多,大家可以看下网上泄显露来的破绽应用源码,有很多这样的类,这里就不列进去了,知道了破绽的实质是由于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:
第一秩序列化后的bundle经过16进制打印进去如下图所示:
2、系统启动反序列化
经过系统第一次性反序列化,没有触发不对称,系统是读不到这个恶意的KEY-INTENT的,所以人造校验经过。
3、系统从新序列化
系统校验完须要从新序列化,这时刻由于读写不对称,最终白色区域【1,-1】两个值变成了【0,0】。
4、setting反序列化
setting再次反序列化,上方也讲到了,由于不对称,原本两个0只读了1个0。
5、解析最终的key-value
五、破绽攻打实战
经过上方两节,咱们可以看到,借助startAnyWhere和parcel破绽,可以绕过系统校验恣意关上一个页面,上方来看两个实在案例:
可以看到在虚构机上,经过这个破绽间接就关上了锁屏明码的设置页面,而后可以间接绕过明码校验将锁屏明码改掉。
案例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的一些隐衷文件共享给攻打APP,同时攻打APP可以在恶意intent中授权间接修正文件。
上方看一下恶意intent的代码:
private Intent makeFileIntent() {Intent intent1 = new Intent().setComponent(new ComponentName("XXX", "xxx.xxx.AssistActivity")); // 关上AssistActivityUri 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;}
目前破绽均已修复,为防止危险,不展现一切代码。
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_PARCELABLEobtain2.writeString("android.os.WorkSource");obtain2.writeInt(-1);//mNumobtain2.writeInt(-1);//mUidsobtain2.writeInt(-1);//mNamesobtain2.writeInt(1);//mChains.lengthobtain2.writeInt(-1);...此处省略一些结构代码bundle.readFromParcel(obtain);return bundle;}
以下是视频演示,经过上方这一段攻打代码,拿到了手机上某个APP存在运行私有目录下的账号消息, 雷同为了隐衷,此处局部脱敏。
六、破绽应用影响
经过上文的引见,咱们知道借助这个破绽可以成功对系统恣意文件的修正,上方列出了破绽带来的影响:
七、破绽修复措施
除了发现疑问更关键的是处置疑问,上方列出了修复这个破绽对应的一些打算:
系统层:
运行层:
八、破绽预防措施
破绽其实是无法防止的,上方是面对层出不穷破绽的一些预防措施:
九、总结
接上去便捷回忆一下,本文关键讲了5方面内容:
全体来讲,这个破绽涉及了一切的Android手机,无论是对用户,对企业都形成了渺小的损失。
作为开发者的咱们须要从自身做起,守护好每一个环节,防止让攻打者无机可乘。