Vendor binder services proved to be an interesting part of android devices nature. They usually remains close-source, but sometimes open new attack surface for privilege escalation. In these articles I will first describe how to locate interesting binder service and a reversing quirk, then two typical CVEs will be discussed about their nature and exploitation, and how to find them.
Locating interesting binder services
Before Android N, all binder services were registered to servicemanager, and communicated with each other under /dev/binder. After Android N, binder domains are splitted to normal domain under /dev/binder, vendor domain under /dev/vndbinder, and hardware domain under /dev/hwbinder. Normal untrusted_app access is restricted to /dev/binder.
There’re possibly more than 200 binder services on some vendor devices, and most of them are Java services. Java code may introduce common android logic problems, which we will not cover in this blog post. Currently we are only interested in memory corruption bugs lies in native code.
So first question arises: how do we deduce which services may have interesting native code? Where are these services running?J previously released a tool named bindump, by reading binder data in debugfs, the tool can iterate which process owns a process and which processes are using this service. However days have passed and android has involved pretty much, major problems including
- debugfs is no longer readable by normal process so you will need root to run
- Binder node now have context so you have to distinguish between different domain context
- Libbinder.so linking symbols changes in every version so one may not be able to reuse the binary and need to recompile the source on top of corresponding AOSP source branch
To solve problem 2 and 3, I rewrite the tool in Java and encapsulated it into a standalone jar, its source and binary can be found here.
With the tool to find hosting process for binder service, we move to look binder services in native processes.
CVE-2018-9143: buffer overflow in visiond service
There’s an arbitrary length/value overflow in the platform’s visiond
daemon, which runs under system uid. visiond
exposes a service named media.air, multiple dynamic libraries are used by this service, including visiond, libairserviceproxy, libairservice, libair. libairserviceproxy is the library with server-side method implementations, namely BnAIRClient::onTransact, BnEngine::onTransact, BnAIRService::onTransact
.
Missing vtable entries?
An interesting fact worth noticing when reversing these libraries is that, in IDA at the address where we should find vtables, except top-offset and virtual-base offset, all other virtual method pointer items are all zero. However at runtime, the corresponding memory locations become normal. This does not appear on Nexus or Pixel image libraries when opened with IDA.
At first glance it may seem like a binary protection for anti-reversing. However after some digging in we found it’s acutally a new Android linker feature that was not supported by IDA. To understand this symposym, one should first be clear that the vtable function addresses does not naturally resides in .data.rel.ro
. Their offsets are actually stored in relocation entries at .rela.dyn
with type R_AARCH64_RELATIVE
or others. IDA and other disassembler tools do the job of linker, read, calculate and store the value into their respective locations. If IDA failed to parse the relocation entries, the target addresses will be left untouched, as seen in the screenshot.
So what’s the offending linker feature? Quoting from chromium
docs:
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.
This feature encodes binary data in SLEB128 format in order to save space for large binaries. The detailed implementation, including encoding and decoding algorithm can be found here( http://androidxref.com/9.0.0_r3/xref/bionic/tools/relocation_packer/src/delta_encoder.h). At runtime, the linker decodes this segment, rearranging other segment addresses and extracts normal relocation entries. Then everything goes back to normal. IDA at that time does not supported this encoding so all relocation infos are lost in IDA. One can use the relocation_packer
tool itself to recover the addresses.
Update: Two years after Android introduced this feature IDA 7.3 has finally supported APS2 relocation, which can be seen in the following screenshot.
+ ELF: added support for packed android relocations (APS2 format)
AirService copies in the air
BnAirServiceProxy provides two methods for managing AirClient passed from client process. One accepts a StrongBinder (AirClient) and an int option, returns a binder handle (pointing to BnAIR) for client process to invoke.If the int option is 0, it will create a FileSource thread. If the option is 1, it will create a CameraSourceThread(only this can trigger this vulnerability)
BnAIR::transact (in libair.so) has many functions, the functions relating to this exp is android::AIRService::Client::configure, start and enqueueFrame. We must call these functions in order of configure->start->enqueueFrame to do a property setup.The specific vulnerable function is enqueueFrame. enqueueFrame
receives an int and IMemory, and the IMemory content is copied out to a previously allocated fixed IMemory in Frame object.
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;
}
The detailed steps are as follows:
- We use first transact with code=3 from untrusted_app to /system/bin/visond service, to trigger android::AIRService::Client::configure(int) in libairservice.so. This transact is needed to init some parameters
- The second transact with code=4, which starts an AIRService Client, android::AIRService::Client::start() start the Client to accept the final transaction
- The final transact, with code=7, actually passes an IMemory with attacker controlled length/content, to trigger android::AIRService::Client::enqueueFrame(int, android::spandroid::IMemory const&).
The mmaped area in Frame itself is usually 2M, so any passed in Imemory with size > 2M will trigger overflow.
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)
Exploitation?
This may be similar to Project Zero’s bitunmap bug, since overflow occurs in mmaped area. When android::AIRService::Client::configure is called, a new thread is created in libairservice.so. By allocating Frame, and removing Frame we can create a hole in highmem area. Allocate Frame again, trigger overflow and we can override the thread mmaped area to get PC control.
However, SElinux still enforces strict limitation (no execmem, no executable file loading, cannot lookup PackageManagerService in servicemanager), on visiond, although it’s a system-uid process. How can we utilize this elevated privilege to do things malicious like installing arbitrary app, if we even cannot access PackageManagerService?
Note that although SELinux prohibits visiond from looking up the PMS service, neither it nor the PMS itself has additional restrictions or checks when PMS’s transactions are actually invoked, if we have the service handle. So we may use the following steps to bypass this restriction:
- Attacking app (untrusted_app context) obtains StrongBinder handle of PMS
- Attacking app passes the handle to visiond. Any transaction method accepting and storing StrongBinder will work.
- Attacking app triggers the vulnerability, gains code execution. Payload searches the specific memory location in step 2 to find the handle
- Payload calls PMS’s installPackage method with the handle
Conclusion
So this’s CVE-2018-9143. Samsung has already posted advisory and pushed out FIX via firmware OTA. In next article I will describe CVE-2018-9139, a heap overflow in sensorhubservice, and how we examined and fuzzed the target to find this vulnerability, with a PC-controlled POC.
Bonus: POC for this CVE and vulnerable binary has been posted at https://github.com/flankerhqd/binder-cves for your reference.