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 Parcels, 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;
      }
    // ...
    }
  }
}
Link to source

(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.VolumeInfos) 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.

Binder IPC

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;
}
Link to source

This is great for us, since we can parse this file and easily determine the structure of the Parcels 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;
}
Link to source

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;
}
Link to source

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();
    }
}
Link to source

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;
    }
}

Link to source

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.