Binder Tracing Part 1 - Understanding Binder structures
30 Aug 2022 - A Cyber First Summer Placement
We challenged one of our Cyber First summer placement students to build a tool for live-tracing calls through Binder, Android’s IPC mechanism. In this post they explain what’s necessary to understand and extract the data flowing through Binder.
Understanding Binder structures
When an Android app wants to call on another app or service, it uses the Binder subsystem to perform IPC (Interprocess Communication). This is a (relatively) lightweight IPC mechanism implemented by Android for calling functions on remote interfaces that other processes provide. There’s a lot of interesting data passed through Binder, and it can give us a relatively good picture of what an application is doing. However, the data passed through is completely unstructured - it’s up to the sending or recieving process to check the data is in the correct format and read it back correctly. The aim of this project was to intercept and automatically parse these Binder IPC calls to create something akin to Wireshark for Binder. For a project of Android’s size, manually writing parsers for every possible message type would be impractical, so an automated system is needed.
My project has two components - the generator, which generates .struct
files that describe the layout of either an interface or data structure,
and the extractor, which hooks into a running Android instance and
extracts and parses the data sent across Binder, using the data from the
generator.
Binder
A Binder itself is an object that represents one end of an IPC pipe. The
pipe is managed by the kernel binder.h
driver which takes care of
actually copying the data between processes address spaces. To users of
the system, data flows in and out of the pipes in Parcel
s, which are a
relatively lightweight structure that consists of a data buffer and some
supplementary data to ensure ‘live objects’ such as file
descriptors are transferred correctly. (File descriptors are
process-specific and must be recreated in the other process to be valid)
When an Android app requests a service, for example from a
ServiceManager
, what’s actually returned (at least in native code) is
a IBinder
which is just a pipe to that service. For example, here is
an except from the generated IStorageManager
service interface, and
the getVolumes
method on it:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
public interface IStorageManager extends android.os.IInterface {
// ...
public static abstract class Stub
extends android.os.Binder implements android.os.storage.IStorageManager {
@Override public boolean onTransact(
int code, android.os.Parcel data, android.os.Parcel reply, int flags)
throws android.os.RemoteException {
switch(code) {
case TRANSACTION_getVolumes:
{
int _arg0;
_arg0 = data.readInt();
data.enforceNoDataAvail();
android.os.storage.VolumeInfo[] _result = this.getVolumes(_arg0);
reply.writeNoException();
reply.writeTypedArray(_result, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
break;
}
// ...
}
}
public static android.os.storage.IStorageManager asInterface(android.os.IBinder obj)
{
if ((obj==null)) {
return null;
}
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
if (((iin!=null)&&(iin instanceof android.os.storage.IStorageManager))) {
return ((android.os.storage.IStorageManager)iin);
}
return new android.os.storage.IStorageManager.Stub.Proxy(obj);
}
private static class Proxy implements android.os.storage.IStorageManager {
// ...
@Override public android.os.storage.VolumeInfo[] getVolumes(int flags)
throws android.os.RemoteException
{
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
android.os.storage.VolumeInfo[] _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
_data.writeInt(flags);
boolean _status = mRemote.transact(Stub.TRANSACTION_getVolumes, _data, _reply, 0);
_reply.readException();
_result = _reply.createTypedArray(android.os.storage.VolumeInfo.CREATOR);
}
finally {
_reply.recycle();
_data.recycle();
}
return _result;
}
// ...
}
}
}
(Note: throughout this article I have edited out extraneous parts of AOSP code, and left the links to the original source file in image caption.
To use the interface, we cast the IBinder
to an
IStorageManager.Stub.Proxy
using
IStorageManager.Stub::asInterface()
. This Proxy
object has methods
for each call in the interface, which takes the arguments and wraps them
in a Parcel
, then calls transact
on mRemote
, the wrapped IBinder
(line 48 above). Internally this function calls
IPCThreadState::transact()
which then performs an ioctl
to send the
data to the kernel. Then once the transaction has been completed, we
read the return value (in this case an array of
android.os.storage.VolumeInfo
s) and then return that from the proxy
function. When mRemote.transact()
is called, then the parcels are sent
down the IPC pipe to the remote service, which is a (subclass of)
IStorageManager.Stub
. They are recieved in
IStorageManager.Stub::onTransact()
, which is a big switch statement
that unparcels the packed data and then (finally) calls the method to
perform the call (line 16). In the Stub
itself, these methods do
nothing, but are overridden to implement the service’s behavior.
This is quite a lot of code for a single IPC call, and to make writing services less cumbersome, Android uses a tool to generate all of this from a simple format that defines the methods involved, Android Interface Declaration Language (AIDL). The above code was generated from just this:
1
2
3
4
5
6
7
package android.os.storage;
import android.os.storage.VolumeInfo;
interface IStorageManager {
VolumeInfo[] getVolumes(int flags) = 45;
}
This is great for us, since we can parse this file and easily determine
the structure of the Parcel
s as they’re sent across the IPC pipe.
However, there is a problem - what actually is this VolumeInfo
object?
It’s read from the written parcel using
1
_result = _reply.createTypedArray(android.os.storage.VolumeInfo.CREATOR);
which sadly doesn’t give us any information about it’s structure. It
turns out that as well as just primitives, a Parcel
can serialise
special objects called Parcelables
, which have a field CREATOR
that
has methods for reading and writing to a Parcel
… and these can be
implemented in AIDL, Java, or C++! This is a big problem - now we’re
having to go from parsing the relatively simple and well-defined AIDL
format to full Java and C++.
Parcelables
After looking at how Parcelable
objects were actually implemented, I decided
to ignore the C++ ones entirely. They’re mostly related to high
performance events - things like communicating with graphics drivers,
and are extremely high frequency - in fact, in some situations, Chrome
on Android 11 will make a Binder call for every vsync frame, often 60
times a second. If there are any that aren’t just used in unimportant
graphics events, then they can be manually implemented by reading the
C++ code itself.
AIDL defined Parcelable
objects are just as simple as AIDL interfaces:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package aaudio;
import aaudio.SharedRegion;
parcelable RingBuffer {
SharedRegion readCounterParcelable;
SharedRegion writeCounterParcelable;
SharedRegion dataParcelable;
int bytesPerFrame; // index is in frames
int framesPerBurst; // for ISOCHRONOUS queues
int capacityInFrames; // zero if unused
int /* RingbufferFlags */ flags; // = RingbufferFlags::NONE;
int sharedMemoryIndex;
}
This is essentially written sequentially as a struct. Note that here
that each SharedRegion
is itself a Parcelable
:
1
2
3
4
5
parcelable SharedRegion {
int sharedMemoryIndex;
int offsetInBytes;
int sizeInBytes;
}
This leaves us now with Java defined Parcelable
objects. These are
implemented as classes that implement the Parcelable
interface. This
defines two key things: a writeToParcel(Parcel out)
method, which
writes the Parcelable
to a parcel, and a CREATOR
object that has a
readFromParcel(Parcel in)
method. This is the same CREATOR
that was
used in our first example to read the VolumeInfo
objects from the Parcel
:
1
_result = _reply.createTypedArray(android.os.storage.VolumeInfo.CREATOR);
Inside VolumeInfo
, these functions are implemented as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public final class VolumeInfo implements Parcelable {
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
parcel.writeString8(id);
parcel.writeInt(type);
if (disk != null) {
parcel.writeInt(1);
disk.writeToParcel(parcel, flags);
} else {
parcel.writeInt(0);
}
parcel.writeString8(partGuid);
parcel.writeInt(mountFlags);
parcel.writeInt(mountUserId);
parcel.writeInt(state);
parcel.writeString8(fsType);
parcel.writeString8(fsUuid);
parcel.writeString8(fsLabel);
parcel.writeString8(path);
parcel.writeString8(internalPath);
}
public static final @NonNull Parcelable.Creator<VolumeInfo> CREATOR =
new Parcelable.Creator<VolumeInfo>() {
/**
* Rebuilds a VolumeInfo previously stored with writeToParcel().
* @param p Parcel object to read the VolumeInfo from
* @return a new VolumeInfo created from the data in the parcel
*/
public VolumeInfo createFromParcel(Parcel in) {
return new VolumeInfo(in);
}
};
public VolumeInfo(Parcel parcel) {
id = parcel.readString8();
type = parcel.readInt();
if (parcel.readInt() != 0) {
disk = DiskInfo.CREATOR.createFromParcel(parcel);
} else {
disk = null;
}
partGuid = parcel.readString8();
mountFlags = parcel.readInt();
mountUserId = parcel.readInt();
state = parcel.readInt();
fsType = parcel.readString8();
fsUuid = parcel.readString8();
fsLabel = parcel.readString8();
path = parcel.readString8();
internalPath = parcel.readString8();
}
}
Internally Parcel::createTypedArray()
calls
CREATOR.createFromParcel()
to create the objects themselves for each
element in the array. In this case, parsing the structure of the parcel
looks fairly simple! We use a java parsing framework, JavaParser
, to
generate an abstract syntax tree (AST) from the source, and look for the
createFromParcel
function, and look for assignments inside it.
However, this is not always so simple - unlike the interfaces,
Parcelables are human-written, which means they don’t follow a
consistent structure and can use the full breadth of java language
features. In some cases the parsing logic is split into many seperate
functions, and the parcel being read travels through complex superclass
constructor chains that need to be resolved. Thankfully JavaParser
also includes a symbol solver that allows us to resolve these, but the
AST is still very complex.
This is further confounded by the fact that there are a few different
ways to actually read a Parcelable
! There's
CREATOR.readFromParcel()
as previously mentioned, but some
unparcelling methods read sub-parcelables using their constructor
directly, or by using Parcel::readTypedObject()
, which similarly takes
a CREATOR
but prepends a int32 field which acts as a nullcheck:
1
2
3
4
5
6
7
8
9
@Nullable
public final <T> T readTypedObject(@NonNull Parcelable.Creator<T> c) {
if (readInt() != 0) {
return c.createFromParcel(this);
} else {
return null;
}
}
There’s also Parcel::readParcelable()
which dynamically reads a
String16
which is the type of the Parcelable
before the data itself,
or even Parcel::readValue()
which reads an int32
field that
determines the type of object to read, and then a type string followed
by the data if the int32
is equal to VAL_PARCELABLE
. In some
cases, the developers appear to not trust the automatic nullchecks given
by the latter two methods and have even implemented their own, second
ones on top of these methods!
Finally, Parcelable
objects can be put into a list. One way of doing that is
by using an typed array, as above, which has a nullcheck for each
element, and is prepended by an int32
size of the array. There are
also parcelable arrays, which use Parcel::readParcelable()
for each
item, and raw arrays that use Parcel::readValue()
for each item!
There’s even a commonly used Parcelable
, ParceledListSlice
, which
can split up a list over multiple transactions if it’s too long.
To help reduce the complexity, I developed the concept of an ‘interesting function call’. An interesting call is one which either:
- The function is called on the Parcel itself
- The function is not a constructor and has the Parcel as an argument
- The function is a constructor and has exactly one argument, which is the Parcel
For example, the call new VolumeInfo(p)
in
CREATOR.createFromParcel()
above is interesting, since it is a
constructor with just the Parcel passed in. Ideally every call that
somehow reads data from the Parcel
should be classed as interesting,
which means we either add it to the generated model of the parcelable
that it is building (in the first case or third cases) or recursively
examine the function for further calls (in the second case). This will
not catch everything (for instance, if a reference to the parcel is
present inside some wrapped structure) but in most cases it gets
everything and similar to above, the cases where it does not work are
obscure and can be implemented manually if needed.
Sometimes a particular field’s presence is dependent on another field - in the majority of cases this is a simple boolean check in an if statement. Here we look for either interesting calls in the condition of the if statement, or variables that have been read from the parcel previously, and then try to evaluate if they’re true or not. As above, this is far from perfect but given the time constraints for this project it works in the majority of cases. A similar solution exists for loops in parcel read methods.
However the data is read, once it has been read to memory it is written into a simple json format:
VolumeInfo.struct
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
{
"produced_on": "2022-08-02T11:10:35.184595399Z",
"components": [
{"id": "readString8"},
{"type": "readInt32"},
{"nullcheck": "readInt32"},
{
"__backreference": "nullcheck",
"__conditional": [{
"disk": "readParcelable",
"__parcelType": "android.os.storage.DiskInfo"
}]
},
{"partGuid": "readString8"},
{"mountFlags": "readInt32"},
{"mountUserId": "readInt32"},
{"state": "readInt32"},
{"fsType": "readString8"},
{"fsUuid": "readString8"},
{"fsLabel": "readString8"},
{"path": "readString8"},
{"internalPath": "readString8"}
],
"full_name": "android.os.storage.VolumeInfo",
"name": "VolumeInfo",
"producer": "java-aidl-generator",
"type": "Parcelable"
}
This has everything needed to parse that specific Parcelable
, for the
specific AOSP version.
In part 2 of this article we’ll see how with knowledge of these structures we can intercept and display Binder calls on a live system.