之前我们介绍过,各个Parcelable会自定义自己的读写方法,如果期间有差错,可能会影响到Parcel后续所有数据的读写。对其他数据产生影响会导致在A处进行了校验,校验通过后传给B、B处的内容却变成不能通过校验的非法内容的漏洞。
至今我们已经介绍过了:
1. 经典款。由于读取和写入的方法实现不一样,比如读取int写入long,导致二次读取时内容发生变化;
2. 由于读取时捕捉了异常并返回null,若二次读取时才报错,会导致读取提前结束,内容发生变化;
3. LazyValue保存了Parcel指针,解析后释放;正常所有LazyValue解析后Parcel才会回收。存在一种情况导致LazyValue未解析完成,指针未释放,但Parcel已认为可回收。此时再次访问该LazyValue,有机会访问到已经回收后再次使用的,其他IPC的内容。
这次会简单介绍(可能是最后的)两种。
Android 接口定义语言 (AIDL) 是一款可供用户用来抽象化 IPC 的工具。以在 .aidl 文件中指定的接口为例,各种构建系统都会使用 aidl 二进制文件构造 C++ 或 Java 绑定,以便跨进程使用该接口(无论其运行时环境或位数如何)。放在编译器里实际上就是自动生成Binder transact的基础代码的工具。
比如:
然后实现该接口即可:
至于什么onTransact的部分,均自动生成,源码里是看不到的,反编译可以看到类似以下的内容:
可以看到,实际上他会自动生成通信双方(Stub.onTransact()以及Stub$Proxy.transact())Parcel读写相关的代码。正常来说,基础类型都很难出幺蛾子的,读写的实现各方面都比较固定,但是涉及到Parcelable这个自定义接口,以及各种父类子类,实现就五花八门了起来。对于指定类型的Parcelable类,会直接调用对应的读接口。直接来看问题代码:
可以看到,在读取的时候,由于传入的就是Intent的Creator,就会按照Intent的实现去读取数据,就不需要传输类名然后反射了,节省空间和效率;但由于写入时直接调用目标的writeToParcel方法,如果它不是"Intent"呢?比如说,<T extends Intent>这样的呢?此时,一旦writeToParcel实现不同,那么就会导致错位。
我们回到第一个问题的利用点:
由于数据传输过去的时候是Bundle,Bundle里对于Parcelable的键值对采用先读类名然后反射的方式读取,并不像AIDL一样指定了Intent的Creator,因此可以很方便的放置一个Intent的子类过去。
Intent有两个子类,ReferrerIntent和LabeledIntent。他们两个都是先将Intent的部分进行读写,再添加新的字段。相对来说,前者只是多了一个String,利用空间比较小,后者要方便一点。
而ChooseTypeAndAccountActivity拉起页面的时候调用的是startActivityForResult方法。该方法的Intent参数后的部分都有机会被我们篡改。具体的利用构造可以看参考链接,比较麻烦的一个坑来自以前讲过的一个谷歌的缓解措施:
因为许多不匹配的问题经过二次读取之后,Parcel会提前结束,即有内容剩下。如果一切正常,Parcel应该是正好被读完的。放到这次的场景中,一开始写是labeledIntent加上startActivityForResult方法的剩余参数。读的时候在原本Intent的内容被读取完毕之后,后面labeledIntent的额外参数会被当作方法的剩余参数读取,需要想办法利用一些不定长的数据(比如Bundle、String、各种Array等),让它一直读到Parcel的结尾,把原来的剩余参数也读走。
实际上,Parcelable相关的所有漏洞,介绍至今,原理本身都很简单;但原理本身就只是bug,想要利用来让它成为漏洞,就会面临很多需要解决的问题。比如说,会需要一些可以放置任意数据的入口,让你把有问题的Parcelable丢进去不会报错。像前面介绍的第二个问题,我们需要在一个在读时捕获异常后提前返回的Parcelable里,放入一个第一次读写不报错,第二次读写会报错的类,这时候我们就需要利用到它调用了一个readList()这种可以任意指定数据类型的接口。这个入口可能还有要求,比如说我们的第三个问题,我们需要使用一个Parcelable,包含着有问题的目标类RemoteViews,并在前面有至少两个可控的int,以符合利用后Bundle读取数据的格式,这里我们就会需要用一个读了若干位后又读了一个任意Parcelable的Parcelable。这些问题本来也可以通过开发人员更规范地编写代码、执行类型检查来避免;谷歌为了推进这个事情,提高利用难度,弃用了许多原来不指定类型的接口,希望大家用到新的、指定类的接口去读写数据,保证类型的准确性。
但是,我们最开始的那个利用点,并没有改:
对于getParcelable这个场景下,指定类型与否的区别就在于,是否确实调用了它的构造方法。以前讲过,读取Parcelable实际上是先读取类名,然后反射获取它的类和Creator,然后调用对应的createFromParcel函数。如果指定了类型,在反射拿到类之后就会看类型是否符合;没有的话就会读取出来之后做强转。也就是说,实际上是:
也就是说,在没有指定类型的情况下,只要你是Parcelable,就会执行一次createFromParcel函数。这个事情实际上解决了之前我们讨论第三个问题Lazy Value时提到的防护措施。我们回忆一下,推行Lazy Value是因为Bundle本身当然是支持放任意数据的,出问题的可以是任意BootClassLoader可以找到的Parcelable,所以让这些和功能没有任何关系的类原封不动待在Bundle里,不要产生影响。但是Intent这个是没有办法避免的,所以我们依旧可以执行任意Parcelable的读方法。
我们都知道,Parcel在读写期间,不应该有额外的、不匹配的操作;谷歌最开始面对第一个问题、最基础的那个读写实现不匹配问题,就是来一个修一个。即使只是方便辅助漏洞利用的Parcelable,也渐渐被改成了上面提到的指定类型的接口以增强安全性。那么,我们还有其他有问题的Parcelable可以用来利用吗?刚好,有一个这样的代码:
这段代码执行了相当危险的操作,没有限制地尝试反射类,可以让我们扩充下寻找的范围:只要是构造函数里接收单个Parcel参数的类,如果构造函数里有对Parcel执行读写,就有机会造成不匹配的情况,比如这个类:
然而这里会遇到一个和上面提到的类似的问题,强转。也就是说:
因此PooledStringWriter强转时必然会抛出异常。我们会需要使用一个带有try-catch的Parcelable去把这个异常吃掉,类似当初第二个问题那样;但那个已经修了。三星那边有一个满足条件的类:com.samsung.android.content.clipboard.data.SemImageClipData,它在捕捉到异常时,仅仅只是日志记录一下,并没有做任何其他的处理。
但也因为他没有做任何其他的处理,所以和当初第二个问题返回为空不同,这里不为空。按照上面所说的,在getParcelable方法返回后,将自动强转成Intent。显然强转会报错,所以需要做一些额外的处理。本身他有一个try-catch:
问题是强转发生在这个方法返回之后,所以强转的报错依旧会在try-catch以外抛出。可以想方设法让它在里面抛出,这样返回NULL就不会强转了。一个方式是换一个数据类型,比如Parcelable[],它在挨个读取元素时依旧会像刚才所说的执行createFromParcel方法,并且会在上面这个方法返回前强转成Parcelable时报错。
这里介绍了也许是另外两种Parcel相关的问题:
1. 在AIDL的实现中,读取是代码中设定的类别,写入却为当前类别,所以如果传入的是Intent的子类,可能会导致读写后内容发生变化;
2. 不使用类型限制的接口依旧会执行任意Parcelable的构造方法,存在一个类会任意执行反射,导致一些非Parcelable,但是构造时会对Parcel执行操作的系统类也会被构造,导致读写后内容发生变化。
从设计的角度来说,开发人员自行处理Parcelable的读写方法,可能有点过于自由了。如果能考虑像Gson库那样,要求按某种格式去执行,然后转换过程对开发人员不可见,或许会更容易避免这种问题。不过类似createIntentsList这种不限制反射的操作就难以执行了。
反序列化相关的这一系列漏洞,逻辑相对底层,并且会涉及很多源码上看不到的机制内容,挖掘难度相对较大;IPC使用广泛、Parcelable接口“历史悠久”等问题也导致了设计上难以根治。在这里也感谢相关安全研究人员的分享。
https://konata.github.io/posts/creator-mismatch/
https://github.com/michalbednarski/TheLastBundleMismatch
原文始发于微信公众号(OPPO安珀实验室):Parcelable和Bundle的爱恨情仇(三)——AIDL和类型限制
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论