安卓生态多姿多彩,在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中的服务,并进行后续分析了。
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.0r3/xref/bionic/tools/relocationpacker/src/delta\_encoder.h)里找到。在运行时linker解压这个section,根据大小变化调整前后section的地址,将其恢复为一个正常的ELF进行加载。Samsung在编译platform binary时启用了APS2 encoding, 而IDA尚不支持,所以我们会看到大部分重定向信息都丢失了,可以用上述 relocation_packer
工具将其解码恢复。
一个好消息: 在APS2引入两年之后,IDA 7.3终于增加了对其的支持,现在可以看到IDA已经可以正确地恢复虚表项地址了。
IDA Changelog:
File formats:
...
+ ELF: added support for packed android relocations (APS2 format)
...
在解决了逆向的这个问题之后,我们回过头来分析下这个服务的相关结构。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&)
中的溢出
11-08 16:49:05.731 15966 15966 F DEBUG : pid: 15743, tid: 15753, name: Binder:15743_1 >>> /system/bin/visiond <<<
11-08 16:49:05.731 15966 15966 F DEBUG : signal 11 (SIGSEGV), code 2 (SEGV_ACCERR), fault addr 0x7f9d51b000
11-08 16:49:05.731 15966 15966 F DEBUG : x0 0000007f9d2e1000 x1 0000007f9cb1b030 x2 00000000007c5f80 x3 0000000000a00000
11-08 16:49:05.731 15966 15966 F DEBUG : x4 0000000000000000 x5 bea17f9958545531 x6 0000007f9d51aff0 x7 0706050403020100
11-08 16:49:05.731 15966 15966 F DEBUG : x8 0f0e0d0c0b0a0908 x9 1716151413121110 x10 1f1e1d1c1b1a1918 x11 2726252423222120
11-08 16:49:05.731 15966 15966 F DEBUG : x12 2f2e2d2c2b2a2928 x13 3736353433323130 x14 3f3e3d3c3b3a3938 x15 0000007fa5f06c40
11-08 16:49:05.731 15966 15966 F DEBUG : x16 0000007f9ed61560 x17 0000007fa5db6ec8 x18 0000000000000000 x19 0000007fa5651400
11-08 16:49:05.731 15966 15966 F DEBUG : x20 0000007f9d2e1000 x21 0000007fa55010e0 x22 bea17f9958545531 x23 0000000000000007
11-08 16:49:05.731 15966 15966 F DEBUG : x24 bea17f9958545531 x25 0000000000000000 x26 0000000000000000 x27 bea17f9958545531
11-08 16:49:05.731 15966 15966 F DEBUG : x28 0000000000000000 x29 0000007fa5501070 x30 0000007f9ed3f654
11-08 16:49:05.731 15966 15966 F DEBUG : sp 0000007fa5501010 pc 0000007fa5db7014 pstate 0000000020000000
11-08 16:49:05.736 15966 15966 F DEBUG :
11-08 16:49:05.736 15966 15966 F DEBUG : backtrace:
11-08 16:49:05.736 15966 15966 F DEBUG : #00 pc 000000000001b014 /system/lib64/libc.so (memcpy+332)
11-08 16:49:05.736 15966 15966 F DEBUG : #01 pc 000000000002a650 /system/lib64/libairservice.so (_ZN7android12FrameManager12enqueueFrameERKNS_2spINS_7IMemoryEEE+188)
11-08 16:49:05.736 15966 15966 F DEBUG : #02 pc 0000000000031870 /system/lib64/libairservice.so (_ZN7android10AIRService6Client12enqueueFrameEiRKNS_2spINS_7IMemoryEEE+72)
11-08 16:49:05.736 15966 15966 F DEBUG : #03 pc 000000000000fbf8 /system/lib64/libair.so (_ZN7android5BnAIR10onTransactEjRKNS_6ParcelEPS1_j+732)
11-08 16:49:05.736 15966 15966 F DEBUG : #04 pc 000000000004a340 /system/lib64/libbinder.so (_ZN7android7BBinder8transactEjRKNS_6ParcelEPS1_j+132)
11-08 16:49:05.736 15966 15966 F DEBUG : #05 pc 00000000000564f0 /system/lib64/libbinder.so (_ZN7android14IPCThreadState14executeCommandEi+1032)
11-08 16:49:05.736 15966 15966 F DEBUG : #06 pc 000000000005602c /system/lib64/libbinder.so (_ZN7android14IPCThreadState20getAndExecuteCommandEv+156)
11-08 16:49:05.736 15966 15966 F DEBUG : #07 pc 0000000000056710 /system/lib64/libbinder.so (_ZN7android14IPCThreadState14joinThreadPoolEb+76)
11-08 16:49:05.736 15966 15966 F DEBUG : #08 pc 0000000000074b70 /system/lib64/libbinder.so
11-08 16:49:05.736 15966 15966 F DEBUG : #09 pc 00000000000127f0 /system/lib64/libutils.so (_ZN7android6Thread11_threadLoopEPv+336)
11-08 16:49:05.736 15966 15966 F DEBUG : #10 pc 00000000000770f4 /system/lib64/libc.so (_ZL15__pthread_startPv+204)
11-08 16:49:05.736 15966 15966 F DEBUG : #11 pc 000000000001e7d0 /system/lib64/libc.so (__start_thread+16)
11-08 16:49:05.798 15966 15966 E : ro.debug_level = 0x4f4c
11-08 16:49:05.798 15966 15966 E : sys.mobilecare.preload = false
这是一个类似于Project Zero之前公布的bitunmap案例的漏洞,两者的溢出都发生在mmap过的区域。由于mmap分配的内存区域相对较大,位置不同于常规的堆管理器管理区域,其利用方式不同于传统的堆溢出。读者应该会回忆到Project Zero是通过特定函数分配thread,然后溢出thread的control structure的方式来实现控制流劫持。同样地,在我们的目标中, 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 中找到。