安卓生态多姿多彩,在AOSP之外各大厂商的binder service也同样各式各样。这些自行实现的service通常来说是闭源的,常常成为会被人忽略的提权攻击面。在这一系列文章中,我会先描述如何定位可能有问题的binder service进行后续研究,以及逆向中一些有意思的发现,随后会以之前发现的两个典型的CVE为例,讨论这些漏洞是如何产生的,如何发现它们,以及如何进行利用。
寻找潜在的分析目标
在Android N之前,所有的binder service都是在servicemanager
中进行注册的,client通过/dev/binder
与service进行通讯。Android N对binder服务引入了domain切分的概念,常规的服务依然使用/dev/binder,而vendor domain则转换为使用/dev/vndbinder
, hardware domain转换为使用/dev/hwbinder
。常规的untrusted_app访问被限制在了/dev/binder。
通过service list
,我们可以查看设备上注册了多少normal domain的service。AOSP设备一般会有100+,而各大厂商的设备均会达到200以上。其中大部分都是Java服务,虽说Java服务通常也会引入一些常见的逻辑问题,但暂时不属于本文的讨论范围。目前的范围内,我们只关注包含有native code,可能存在内存破坏漏洞的组件。
所以第一个问题出现了,如何确定哪些服务是通过native code处理的?根据binder服务的形式,存在如下可能:
- 该服务直接运行在native process中
- 该服务运行在JVM process中(例:注册于system_server中),但存在JNI调用
无论分析哪种形式,我们都需要先确定该服务的host进程。在进程注册或打开binder服务的时候, debugfs中会留下相应的node entry或ref entry。Android Internals的作者数年前开源的工具bindump即通过遍历这个信息来获取服务的进程关系。其工作原理如下:
- tool process打开目标服务,获取本进程新增的ref id
- 遍历procfs, 通过ref id匹配各进程的node id,匹配到的进程即为该服务host process
这个方法非常有效,不过随着Android的演进,原始的bindump工具现在遇到了如下问题:
- debugfs现在需要root权限才能打开,普通进程已经无法打开debugfs
- binder node现在具有了domain的概念,需要区分不同domain中的node
- 原始的bindump link到libbinder.so,但每个版本更新后symbol location会发生变化,导致原有的binary在新版本上无法运行,每个版本都会需要在AOSP source tree下重新编译(如果vendor改动了libbinder问题就更大了)
为了解决问题2和3,我用Java重写了bindump,将其打包成可以忽略平台版本问题单独运行的jar包,相关代码和precompiled jar已经放在了GitHub上 (https://github.com/flankerhqd/bindump4j)。
在解决了以上问题之后,我们终于可以定位到运行在native process中的服务,并进行后续分析了。
CVE-2018-9143: buffer overflow in visiond service
media.air
是一个运行在Samsung设备系统进程/system/bin/visiond
中的服务。visiond
本身加载了多个动态执行库,包括libairserviceproxy
, libairservice
, libair
等, 并以system-uid运行。 相关服务的实现端,例如 BnAIRClient::onTransact, BnEngine::onTransact, BnAIRService::onTransact
等存在于libairserviceproxy
中。
虚表指针去哪里了?
逆向C++库的关键准备之一是定位相应虚函数指针,并使用IDA脚本通过这些信息进行type reconstruction。但当我们在IDA中打开media.air
服务的动态库时,却惊讶地发现,在原来应该有vtable表项指针的地方,除了top-offset和virtual-base offset还在,其他的指针大部分神秘地消失了,如下图所示
而同样大版本的AOSP/Pixel/Nexus镜像的binary中并没有出现这样的问题。谁偷了我的虚表指针?
乍一看可能会觉得三星在故意搞事,像国内厂商一样做了某种混淆来对抗静态分析,但实际上并不是。为了理解这种现象,我们先来回忆下虚表项指针的存储方式。
首先,IDA给我们展示的rel section并不是ELF中实际的内容,而是处理过后的结果。虚表指针项并不直接存储在.data.rel.ro
section,而是linker 重定位之后的结果。它们的原始内容实际上存在于.rela.dyn
中,以R_AARCH64_RELATIVE
表项的形式存在。在library被加载时,linker会根据表项中的offset,将重定位后的实际地址写入对应的offset中,也就是vtable真正的地址。 IDA和其他分析工具会模拟linker的功能预先对这些内容进行解析并写入,但如果IDA解析relocation table失败,那么这些地址会维持其在ELF中的原始内容,也就是0。
但是什么导致了IDA解析失败?这是在N后引入的APS2
重定位特性,最先应用在chromium上,如下所述:
Packed Relocations
All flavors of lib(mono)chrome.so enable “packed relocations”, or “APS2 relocations” in order to save binary size.
Refer to this source file for an explanation of the format.
To process these relocations:
Pre-M Android: Our custom linker must be used.
M+ Android: The system linker understands the format.
To see if relocations are packed, look for LOOS+# when running: readelf -S libchrome.so
Android P+ supports an even better format known as RELR.
We'll likely switch non-Monochrome apks over to using it once it is implemented in lld.
APS2将重定向表以SLEB128的格式压缩编码,对于大型binary可以缩小ELF的体积。具体的编码解码实现可以在( http://androidxref.com/9.0.0_r3/xref/bionic/tools/relocation_packer/src/delta_encoder.h)里找到。在运行时linker解压这个section,根据大小变化调整前后section的地址,将其恢复为一个正常的ELF进行加载。IDA尚不支持APS2 encoding所以我们会看到大部分重定向信息都丢失了,可以用上述relocation_packer
工具将其解码恢复。
一个好消息: 在APS2引入两年之后,IDA 7.3终于增加了对其的支持,现在可以看到IDA已经可以正确地恢复虚表项地址了。
IDA Changelog:
File formats:
...
+ ELF: added support for packed android relocations (APS2 format)
...
AirService copies in the air
在解决了逆向的这个问题之后,我们回过头来分析下这个服务的相关结构。media.air
中的BnAirServiceProxy
提供了两个接收客户端传入的AirClient
的初始化函数,其中一个以StrongBinder的形式接受输入,并返回一个指向BnAir
服务的handle供客户端进程再次调用。当option参数为0时,该函数会创建一个FileSource线程,当option参数为1时其会创建一个CameraSourceThread线程。只有在CameraSourceThread线程中可以触发本漏洞。
在获得服务端BnAir服务的handle后,客户端将可以进一步调用其实现的transaction。libair.so
中提供的BnAIR服务实现了一个针对Frame的状态机,状态机的关键函数包括configure
, start
和enqueueFrame
。在按照顺序调用之后最终触发有漏洞的enqueueFrame
函数。
android::RefBase *__fastcall android::FrameManager::enqueueFrame(__int64 someptr, __int64 imemory)
{
//...
v4 = (android::FrameManager::Frame *)operator new(0x38uLL);
android::FrameManager::Frame::Frame(v4, v5, *(_DWORD *)(v2 + 0x88), *(_DWORD *)(v2 + 140), 17, *(_DWORD *)(v2 + 144));
v16 = v4;
android::RefBase::incStrong(v4, &v16);
(*(void (**)(void))(**(_QWORD **)v3 + 0x20LL))(); //offset and size is retrived
v6 = (*(__int64 (**)(void))(*(_QWORD *)v16 + 88LL))(); //v6 = Frame->imemory->base();
v7 = (*(__int64 (__fastcall **)(__int64))(*(_QWORD *)imemoryheap + 40LL))(imemoryheap); //v7 = imemoryheap->base();
memcpy(v6, v7 + v15, v14);//memcpy(frame->imemory->base(), imemoryheap->base() + offset, imemoryheap->size());//overflow here
//...
if ( imemoryheap )
android::RefBase::decStrong(
(android::RefBase *)(imemoryheap + *(_QWORD *)(*(_QWORD *)imemoryheap - 24LL)),
&imemoryheap);
result = v16;
if ( v16 )
result = (android::RefBase *)android::RefBase::decStrong(v16, &v16);
return result;
}
可以看到,传入的IMemory在被mmap后并没有对长度做任何的检查,直接memcpy进入了Frame的IMemory中,而后者的预定义size是2*1024*1024
,即超过2M的映射,即会引发overflow。
整体的触发步骤如下:
- 向
media.air
发送一个code=1 的transaction以获取BnAir
的handle,以下步骤的调用对象均为该handle - 发送一个code=3 的transaction以触发
android::AIRService::Client::configure(int)
。该函数会完成对应对象的参数初始化 - 发送一个code=4的transaction以创建一个AIRService Client, 并调用
android::AIRService::Client::start()
启动 - 最后一个code=7的transaction最终传入攻击者可控内容和长度的IMemory,触发
android::AIRService::Client::enqueueFrame(int, android::sp<android::IMemory> const&)
中的溢出
fpsr 00000000 fpcr 00000000
backtrace:
#00 pc 000000000001b014 /system/lib64/libc.so (memcpy+332)
#01 pc 0000000000029b5c /system/lib64/libairservice.so (_ZN7android12FrameManager12enqueueFrameERKNS_2spINS_7IMemoryEEE+188)
#02 pc 0000000000030c8c /system/lib64/libairservice.so (_ZN7android10AIRService6Client12enqueueFrameEiRKNS_2spINS_7IMemoryEEE+72)
#03 pc 000000000000fbf8 /system/lib64/libair.so (_ZN7android5BnAIR10onTransactEjRKNS_6ParcelEPS1_j+732)
#04 pc 000000000004a340 /system/lib64/libbinder.so (_ZN7android7BBinder8transactEjRKNS_6ParcelEPS1_j+132)
#05 pc 00000000000564f0 /system/lib64/libbinder.so (_ZN7android14IPCThreadState14executeCommandEi+1032)
#06 pc 000000000005602c /system/lib64/libbinder.so (_ZN7android14IPCThreadState20getAndExecuteCommandEv+156)
#07 pc 0000000000056744 /system/lib64/libbinder.so (_ZN7android14IPCThreadState14joinThreadPoolEb+128)
#08 pc 0000000000074b70 /system/lib64/libbinder.so
#09 pc 00000000000127f0 /system/lib64/libutils.so (_ZN7android6Thread11_threadLoopEPv+336)
#10 pc 00000000000770f4 /system/lib64/libc.so (_ZL15__pthread_startPv+204)
#11 pc 000000000001e7d0 /system/lib64/libc.so (__start_thread+16)
如何利用?
这是一个类似于Project Zero之前公布的bitunmap案例的漏洞,两者的溢出都发生在mmap过的区域。由于mmap分配的内存区域相对较大,位置不同于常规的堆管理器管理区域,其利用方式不同于传统的堆溢出。读者应该会回忆到Project Zero是通过特定函数分配thread,然后溢出thread的control structre的方式来实现控制流劫持。同样地,在我们的目标中,android::AIRService::Client::configure
被调用时,它会创建一个新的thread。通过风水Frame对象,我们构造内存空洞并在空洞中创建thread,触发溢出后劫持thread中的回调指针来最终控制PC。
但这还远远没有结束。虽然该进程是system-uid,但SELinux对其有严格的限制,例如no execmem, no executable file loading, 甚至无法向ServiceManager查询大部分系统服务。即使控制了PC,接下来又该何去何从,例如如何利用提升的权限来安装恶意应用,如果根本无法lookup PackageManagerService?
这里需要注意的是,虽然SELinux禁止了visiond去lookup service,但实际上并没有限制调用service自身的transaction,这依赖于service自身的实现,例如ActivityManagerService的相关函数是通过enforceNotIsolated标注来禁止isolated进程调用。所以只要能成功地将PMS的binder handle传递给visiond,攻击者依然可以以visiond的身份调用PMS来安装恶意应用,相关步骤如下:
- Attacking app (untrusted_app context) 获得PMS的StrongBinder handle
- Attacking app 将handle传递给visiond. 任何接收StrongBinder的服务端函数均可,例如
BnAirServiceProxy
中的第一个transaction - Attacking app 触发上述漏洞获取PC控制后,payload在内存中搜索上一步传入的PMS handle
- Payload通过该handle调用PMS,完成恶意应用安装
总结
以上即为CVE-2018-9143,一个典型的binder service漏洞的故事。Samsung已经发布了advisory和补丁,并通过firmeware OTA修复了该漏洞。在下一篇文章中,我会介绍CVE-2018-9139,sensorhubservice中的一个堆溢出,以及如何通过fuzzing发现的该漏洞和它的利用(包括一个控制PC的poc)。
本文所描述的相关poc和有漏洞的服务binary均可以在 https://github.com/flankerhqd/binder-cves 中找到。