1.本文分享了对一类历史漏洞的研究,提到的所有漏洞已经按照漏洞管理规定和业界最佳实践,通报给相关厂商,并已经在最新版本中修复。
2. 本文所述漏洞相关的技术研究应仅在授权范围内进行,并严格遵守相应的法律法规和管理要求。
上篇链接:Parcelable和Bundle的爱恨情仇(一)——读写不匹配
之前我们介绍过,各个Parcelable会自定义自己的读写方法,如果期间有差错,可能会影响到Parcel后续所有数据的读写。对其他数据产生影响会导致在A处进行了校验,校验通过后传给B、B处的内容却变成不能通过校验的非法内容的漏洞。具体到漏洞类型上,我们介绍了两种:读写调用了不同接口导致不匹配/捕捉异常提前返回导致不匹配。今天我们会介绍第三个问题。
由于Parcelable在Framework代码里应用非常广泛,很难避免出现读写不匹配的情况,重要的是不要影响其他数据的读取;也许是基于这样的理由,谷歌做了若干修复,重要且和这次内容有关的主要是这两个:
第一步,对于若干长度不定的类型,在前面加入一个长度字段:
无论在处理这个值时出现了什么差错,后一个值也永远从d开始读起/写入,避免其对其他值造成影响。
第二步,在Bundle里引入了LazyValue。
前面反序列化不匹配的漏洞,其利用涉及的功能是Account服务,期间不涉及什么特殊的Parcelable(除了Bundle自己、Intent这种基础的);但出问题的可以是任意BootClassLoader可以找到的所有Parcelable。所以,如果功能上用不到它,那就不反序列它,让它原封不动地呆在Bundle里,就不会有影响了。所以针对上述的长度不定类型,参考懒加载的设计模式,采用了LazyValue,只读取了键值对关系,在使用Bundle.get*()访问到这个值的时候再进行反序列。
可以看到,LazyValue在读取的时候会回到对应的位置然后使用Bundle正常用来读值的Parcel.readValue(),最后将对原Parcel的引用置空;而在写的时候会将对应位置之后相应长度的内容直接复制过来。
Parcel可以通过recycle()回收进池,下一次有地方调用Parcel.obtain()再取出来使用,这样可以减少对象分配和GC:
通常,主动通信的一方(即调用transact(...)方法的一方会直接使用Parcel.obtain(),就会使用sOwnedPool;而被调用onTransact(...)函数的一方,系统会传sHolderPool的Parcel过来。
结合前面说的,LazyValue本质上是读取Parcel某一位置往后一定长度的内容。如果LazyValue没有被解析,但Parcel被回收了,然后用于其他IPC之后,再次解析这个LazyValue,就会获取到其他IPC的内容。
如图所示,此时A/B访问Bundle里未被成功解析的LazyValue,就会复制到C、D之间IPC的内容。
达到这个效果需要解决两个问题。
刚才说了推行的目的就是为了懒加载,没直接访问即不会解析。不过有一个例外,就是当Parcel.hasReadWriteHelper()为true时。ReadWriteHelper可以自己设置,像readString()等方法会经由该Helper实现,从而实现类似去重的功能,例如PackageParserCacheHelper:
在有Helper的情况下,Parcel会选择马上反序列全部的值:
initializeFromParcelLocked(parcel, /*recycleParcel=*/ false, isNativeBundle),先第一次构造Map,由于recycleParcel为false,暂不回收;随后调用unparcel(true),该方法参数为true时会挨个访问Map里的值,从而将LazyValue全部解析,回收它。
这一步可以参考上篇提到的第二个问题,抛出一个异常解决;比如放一个根本找不到的类名进去。这个异常会在getValueAt()解析LazyValue时捕捉并处理:
sShouldDefuse在SystemServer里被设置为true,避免系统接受外部Bundle时抛出异常。因此在系统服务里,有异常的LazyValue会保留下来,不会被解析。
系统里什么Parcelable同时设置了ReadWriteHelper且可以传一个外部的Bundle呢,RemoteViews,通常用于给桌面传widgets。然后我们需要SystemServer读我们传过去的RemoteViews,稍后再传回来。由于有Helper,期间该Parcel会被回收,但由于抛出了异常,所以我们的LazyValue还在,还指向被回收的Parcel。传回来的时候,参考上面LazyValue写的代码,会把已被回收、甚至已被用作其他用途的那个Parcel,对应位置长度的内容复制过来。
能复制过来的内容相当有限,因为RemoteViews的Bundle位置非常下:
1. 如果不是一个直接接受RemoteViews的方法,可能会有前面的其他内容,以及它的名字“android.view.RemoteViews”;
2. 一个相对大的ApplicationInfo;
3. 其他内容,这里有4个int;
4. 然后调用readActionsFromParcel(...),里面我们需要放置ReflectionAction,然后经过两个int一个String才会到我们的Bundle。
Bundle本身是我们可以控制的,它可以很大,所以取更后的数据当然是没有问题的;但这个位置本来就很靠后了,如果想要拿到一些在Parcel里放的比较前的数据,就需要想办法回到前面的位置,也就是在读取调用setDataPosition()。基本上,大部分Parcelable自己是不会也不应该调整位置指针的,因为保不齐这个Parcel里有没有别的内容,就只有Parcel自己在读取LazyValue时有一处(前情提要最后那里贴的代码):
这个end是通过MathUtils.addOrThrow(...)计算的,会检查越界问题,但并不检查正负:
如果这里objectLength为负数,就可以在读完这个LazyValue后回到之前的位置。所以我们会需要两个LazyValue,一个正常放在RemoteViews里面,然后回退,读取到的第二个也是LazyValue即可。但受限于Bundle的读取规则,第二个LazyValue也就是第二个键值对,会需要一个String作为键,一个Int类型让它作为LazyValue被读取,一个Int长度。这几个值没有问题不会报错的话,就可以获取到后面对应长度的内容。
到这里,已经相对完整和详细地介绍了出问题的功能LazyValue其相关的机制,以及问题本身:
1. 存在一种情况,即当有ReadWriteHelper时,解析出错将导致Parcel被回收但LazyValue仍指向它,造成use-after-recycled;
2. 如果LazyValue的长度为负数,会错误地将位置指针指向前面的位置。
这两个点是关键,也是Patch所打的地方。实际上,这个问题后续的利用细节也相当复杂,从漏洞修复的角度来说,可以忽略,这里只是作为分享。
就像之前反序列不匹配的漏洞,需要找到毫不相关的Account服务来触发一样,也要先找触发点。
我们需要SystemServer读我们传过去的RemoteViews,稍后再传回来。这个场景实际上不少,不过有的需要用户交互授权(比如注册成AppWidgetHost,默认是桌面),有的会用户可见(比如发一个有contentView的通知);如果希望相对静默地做这个事情,可以选择创建一个MediaSession然后调用setQueue(List<MediaSession.QueueItem> queue),后续可以通过MediaSession.getController().getQueue()再取回来。可以看到代码上实际需要的是MediaSession.QueueItem而不是RemoteViews,但也可以用,因为Java的类型擦除:
这两个类在List中均使用的通用序列/反序列方法,并不会报错。
setQueue()期间的数据本质上是通过ParcelableListBinder来传输的,期间可能会有多次Binder传输:
1. 第一次传输会带有List的总数量;
2. List内的每个值都会带有一个1代表后面还有数据,然后通过正常读写Parcelable的方式读写(即类名+Parcelable自己的writeToParcel);
3. 如果超过了Binder传输的大小限制,会写0表示后面没有数据了,如果没有达到第一次传输的List总数量,后续数据会走下一次Binder传输。
getQueue()会有一些不同,使用ParceledListSlice来传输数据,注释也写了,与ParcelableListBinder类似只是传输方向相反;如果一次传输传不完会写一个Binder进去,通过该Binder传输后续的内容。这个区别导致了如果SystemServer接受了来自外部的ParceledListSlice,且单次传输没有完,就需要用到一个来自外部的Binder做后续传输,如果它的transact方法挂起就会挂起整个SystemServer。
已知,我们可以拿到别的IPC内容。拿什么是比较好的呢?通常,如果只是一些Int、String等基本类型的组合,一个是很难判断这些东西是用来做啥的,没有上下文;二是可能效果也不够好。我们可以仿造上篇提到的第二个问题,取IApplicaitonThread,来进行代码执行。再次简单介绍下,一个App启动时会通过attachApplication()将它自己的IApplicaitonThread传给AMS,然后等待AMS告诉他需要拉起什么组件,由于里面会传组件代码的路径,所以可以进行代码执行。传完之后是不会有类似Binder.getCallingUid()检查的,都可以直接调用。
所以我们需要在attachApplication()期间通过getQueue()取回RemoteView,读取其中的LazyValue,其实际上会包含IApplicaitonThread。
前面提到,我们在第二个LazyValue回退到前面后需要一个String作为键,一个Int类型,一个Int长度。由于在attachApplication()里,IApplicationThread的位置也相当前,而回退后需要读一个String,很容易就想到直接回到开头,用RemoteViews的类名作为这个String,但后面紧接着的Int必须是0,这样RemoteViews才能包含Bundle,0如果作为类型来读是String,不会是LazyValue,所以这样是行不通的;而再后就过了IApplicationThread的位置了。所以我们还需要一个别的Parcelable,包含这个RemoteViews,在其前面至少有两个可控的Int。这里选择会很多,比如android.os.Message:
正常填入数据,回退到对应位置读完第二个LazyValue之后,还有一个int一个long,RemoteViews结束,后续Message的数据还会继续读,不报错就行。一个是后面的Bundle,正常会读一个长度,然后读Bundle的魔数,魔数不对的话readBundle()也会报错;不过这里长度刚好会读到RemoteViews里ApplicationInfo前的int,置为0即会跳过Bundle魔数的检查。其次是readMessengerOrNullFromParcel(),实际上是readStrongBinder(),这里是写不了一个Binder进去的,不过这个报错只是会返回null,不会抛出异常,所以也可以安全度过。
实际上这个方法执行的非常快,Parcel填入IApplicationThread到被回收时间很短,所以最好寻找一个办法将其挂起。该方法的大部分代码逻辑在ActivityManagerService.attachApplicationLocked(...)里,代码链上涉及到许多锁,我们选择其中一个将其挂起即可。前面提到的ParceledListSlice可以用于挂起。比如ActivityTaskManagerService.moveTaskToFront(...):
其会使用外部传入的bOptions构造ActivityOptions,同时会触发Bundle的反序列化,解析到里面包含一个Binder的ParceledListSlice,后续让那个Binder的transact方法挂起,同时去取LeakValue就好。
顺带一提,由于内存里肯定会同时存在若干Parcel,所以可以递归若干次以拿到大多数Parcel,增加拿到IApplicationThread的成功率。也就是:
1. 递归setQueue();
2. 调用moveToFront(),卡住mGlobalLock锁;
3. 拉起Settings,让其调用attachApplicationLocked(...);
4. 调用getQueue(),拿到IApplicationThread;
https://github.com/michalbednarski/LeakValue
➤ 往期推荐
·Parcelable和Bundle的爱恨情仇(一)——读写不匹配
·Android Notification从发送到显示的流程简析
·frida入门使用介绍
·使用honggfuzz进行Android模糊测试
·蓝牙安全漏洞BLUFFS的原理解析及影响范围
原文始发于微信公众号(OPPO安珀实验室):Parcelable和Bundle的爱恨情仇(二)——LazyValue
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论