Hamza's random blogposts

Spreading hopefully useful knowledge

24 April 2019

Frida Android libbinder

Tags: frida - android - libbinder

Introduction

While doing some security research on the Android operating system, I stumbled upon the following blackhat presentation. It turns out that Android has a unique inter-process communication (IPC) mechanism. Although the internal workings of this mechanism is quite complex, it is abstracted away for Android app developers. The gist of the story is that Android uses Binder for inter-process communications and that it might be a good place for malware to eavesdrop for sensitive information.

Sounds like a cool thing to hook using Frida! In this blogpost I’ll describe my journey on how it’s made. I will be using the Google Keep note app for demo purposes.

Finding suitable function to hook

Most of the times there are several functions that reside on different layers that can be hooked in order to achieve a certain goal. The following resources were a great help into understanding the Android Binder architecture and the layers it contains 123. The following are a rough list of entries that can be hooked, sorted from high to low level:

Native layer: this layer is hidden from app developers and provided transparently by Android. It is implemented as a shared library libbinder.so. Particular files of interest are Binder.cpp and IPCThreadState.cpp. This layer communicates with the Kernel layer using ioctl syscalls in order to communicate binder messages.

Kernel layer: ioctl syscalls are handled here.

I chose to hook on the native layer. The ioctl call seemed like an interesting function to hook as it requires some manual parsing of messages in Frida. Note that it might be easier to hook higher level functions such as C++ functions or Java functions depending on your goal.

libbinder in Android apps

Apps make use of a shared library called libbinder.so to interact with the Binder IPC framework. In Frida we can show the loaded modules of a particular app as follows:

We can grab the binary using adb pull /system/lib64/libbinder.so and start analysing the library but we could also grab the source and start reading from there! I prefer to use a combination of both static and dynamic analysis.

Next we need to figure out the arguments passed to ioctl. Looking at the code in IPCThreadState.cpp#905 we can deduce that there are three arguments:

args[0]: An integer representing a file descriptor.

args[1]: An integer representing a certain command. We need to target a specific one BINDER_WRITE_READ. In binder.h#106 this is defined as #define BINDER_WRITE_READ _IOWR('b', 1, struct binder_write_read). I decided to create a sample C++ file to calculate this value: 0xc0306201.

args[2]: A pointer pointing to a binder_write_read struct (data). We will need to parse this struct.

Meaning that the size is 8 bytes for each entry on 64-bit devices. Since we’ll be using modern devices, we’ll stick to the x64 architecture. The following JS function parses the struct correctly using the Frida API:

The binder_write_read parameter is a Frida NativePointer object, which is essentially a bridge to interact with a real pointer in memory on the device. Since structs in memory are adjacent to each other, we can use basic pointer arithmetic to read the values out of this struct.

The data structure contains a read and write section each with a pointer to a buffer, buffer size and the amount of bytes that are consumed. binder_thread_write() and binder_thread_read() are used to handle these sections.

Lost in the data structures

Next I thought I just need to dump the buffer and I will be seeing juicy data. It turns out I was wrong and that I had to parse yet another data structure. Loading the sourcecode in CLion might speed up the process of getting insight on how everything is connected. A neat feature is the call hierarchy:

So far we’ve handled the logic until binder_ioctl_write_read(), next we need to figure out what binder_thread_write() does:

The first 4 bytes in the buffer represents a command cmd, different actions can be performed depending on this value. From the sources I linked previously, the values BC_TRANSACTION and BC_REPLY are of particular interest to us as they contain data transmitted through Binder. I’ve decided to create a JavaScript dictionary to emulate the enum:

Technically I need to compute the values such as _IOW('c', 1, struct binder_transaction_data) which gives 0x40406301 but I decided to simply discard the first 3 bytes by using & 0xff which will then result into 0x1. Let’s add a new function which parses the command:

Parsing binder_transaction_data struct

Whenever there’s a BC_TRANSACTION or BC_REPLY, a binder_transaction_data struct is allocated and filled with the rest of the write buffer. Then the function binder_transaction is called with this struct as one of its parameters:

// http://androidxref.com/kernel_3.18/xref/drivers/staging/android/uapi/binder.h#129
structbinder_transaction_data{/* The first two are only used for bcTRANSACTION and brTRANSACTION,
* identifying the target and contents of the transaction.
*/union{/* target descriptor of command transaction */__u32handle;/* target descriptor of return transaction */binder_uintptr_tptr;}target;binder_uintptr_tcookie;/* target object cookie */__u32code;/* transaction command *//* General information about the transaction. */__u32flags;pid_tsender_pid;uid_tsender_euid;binder_size_tdata_size;/* number of bytes of data */binder_size_toffsets_size;/* number of bytes of offsets *//* If this transaction is inline, the data immediately
* follows here; otherwise, it ends with a pointer to
* the data buffer.
*/union{struct{/* transaction data */binder_uintptr_tbuffer;/* offsets from buffer to flat_binder_object structs */binder_uintptr_toffsets;}ptr;__u8buf[8];}data;};

A union can store different data types in the same memory location. This means that only one value can reside in such memory location. Memory will be allocated according to the biggest value. To emulate this in JS land, I’ve opted for a dictionary. The offsets are calculated manually with x64 architecture in mind:

functionparse_binder_transaction_data(binder_transaction_data){return{"target":{// can either be u32 (handle) or 64b ptr"handle":binder_transaction_data.readU32(),"ptr":binder_transaction_data.readPointer()},"cookie":binder_transaction_data.add(8).readPointer(),"code":binder_transaction_data.add(16).readU32(),"flags":binder_transaction_data.add(20).readU32(),"sender_pid":binder_transaction_data.add(24).readS32(),"sender_euid":binder_transaction_data.add(28).readU32(),"data_size":binder_transaction_data.add(32).readU64(),"offsets_size":binder_transaction_data.add(40).readU64(),"data":{"ptr":{"buffer":binder_transaction_data.add(48).readPointer(),"offsets":binder_transaction_data.add(56).readPointer()},"buf":binder_transaction_data.add(48).readByteArray(8)}}}

Glueing it all together

Now that we’ve got the binder_transaction_data struct, we can finally dump data as we got a pointer to the data buffer and a length. The final script looks as follows: