Binder Security

Baiqin Wang
20 min readFeb 4, 2021

--

Most of the world’s authentication and authorization (auth) techniques in computer science or even in real life are either based on “who you are” (identity based) or “what you own” (possession based). The purpose of auth is to grant or deny someone access to certain resources. For the purpose of easier description, let’s call the one who requests access to certain resources “subject”, and call the one who performs the auth “authenticator”. In a “who-you-are” based auth, the authenticator performs the auth by obtaining the identity of the subject under an identification system that the subject have no control with. In a “what-you-own” based auth, the subject presents the authenticator a kind of “token” that he owns; unlike the “who-you-are” based auth, the authenticator performs the authentication by retrieving the identity of the “token”, the auth result depends on the identity of the “token” not the identity of the subject.‌

For example, face recognition and fingerprint based login are examples of identify based auth in computer science. This kind of auth methods are also widely used in real life. For example, you need to present your passport to the custom when entering a country, because the government needs to know who you are. A typical example of possession based auth is the movie tickets distributed by a cinema. As long as you hold a valid movie ticket that the cinema can identify, you are granted entrance into the cinema.‌

Some kinds of auth methods are essential for an IPC framework like Binder, because the Binder service requesters usually come from third party apps that could be malicious. Binder supports both identity based auth and possession based auth. During a Binder transaction, the source thread is the auth subject and the target BBinder is the authenticator.

Binder identity based security

During a Binder transaction, the source thread’s process id (pid) and effective user id (euid) need to be sent to remote BBinder for authentication.

We have seen this binder_transaction_data structure in the article "Binder transaction". When a source thread initiates a transaction, it needs to fill this data structure and send to Binder driver. This is the contract data structure between Binder driver and user space for Binder transactions.

As we can see from the writeTransactionData method, the source thread sets both sender_pid and sender_euid to 0. Of course they are not the source thread's real ids. The two fields are just set to zero because the tr structure is located on stack memory so they need to be cleared. Binder driver will fill in these fields when the binder_transaction_data structure is sent to kernel. If the source user space thread is responsible for filling in the fields, then this identity based security becomes strictly useless. Because the source thread can always fill in a privileged process id and root user id.

When the target thread handles the BR_TRANSACTION return code, the binder_transaction_data it reads from Binder driver already contains the sender_pid and sender_euid that Binder driver fills in. The mCallingPid and mCallingUid fields will be set to the sender's pid and euid respectively during a transaction and restored afterwards. During the time when the transaction buffer is handled to target BBinder to process (between line 31 and 40), the target BBinder can retrieve the identities of the caller to make authorization decisions.‌

The Binder framework provides several utility methods for retrieving the mCallingPid and mCallingUid:

The clearCallingIdentity and restoreCallingIdentity methods are used to temporarily set the mCallingPid and mCallingUid to the target process's ids. This mechanism is widely used in system_server process where many different system services live. Let's assume an app process is requesting a system service "S1" to perform an action, while "S1" is processing this request, it may need another system service "S2" to perform another action. Since both "S1" and "S2" live in the same system_server process, if "S1" doesn't call clearCallingIdentity before talking to "S2", "S2" will think this request comes from the app process. But in factor it is now "S1" making the request so "S2" should judge based on the identity of "S1". So this mechanism can help by temporarily setting the caller identity to that of system_server.‌

Similar utility methods also exist in Java that just call their native counterparts:

Let’s look at an example of identity based auth in Android:

The ActivityManagerService is a system service running inside the system_server process. As its name suggests, it manages the runtime activities of applications in the system. In general, an app process is allowed to perform less sensitive activities like starting an Activity. However, the action of killing other apps is more sensitive. At the beginning of killAllBackgroundProcesses, the method checks whether the caller has permission KILL_BACKGROUND_PROCESSES. The checkCallingPermission method retrieves the caller's identities and eventually calls the checkComponentPermission method in ActivityManager to make the decision:

As we can see, root and system user ids are always granted permission but other user ids need more security checks. (In Android, every installed application is assigned a unique Linux user id by the system.) Basically the caller needs to have the KILL_BACKGROUND_PROCESSES permission granted. After the caller passes the permission check, the killBackgroundProcesses method is done with its security checks. So it calls clearCallingIdentity method to temporarily set the transaction identities to that of system_server process itself before calling other methods, so that if there are other security checks in these methods, it will be based on the system_server identities not the original caller's identity.

Binder token

Binder tokens provide a widely used possession based authorization method during Binder transactions. Binder tokens are just normal BBinder instances so the term "Binder token" refers to a special usage pattern of Binder objects, not another kind of objects in Binder system. Now think of a process in Android as a big cinema, then Binder tokens are the movie tickets this cinema produces. A key feature that makes BBinder instances to work like a movie ticket is that they are unforgeable under Binder's identity system. The process who creates these BBinder instances can uniquely identify them. Besides, a remote process that holds references to BpBinder instances can also recognize the identities of the underlying BBinders; if you compare it to a cinema in real world, it basically means cinema customers should also be able identify the movie tickets. Since a BBinder is unforgeable under Binder's identity system, it is usually used as a security token in Android system. In most common cases, a Binder token is just a plain BBinder object in C++ or Binder object in Java. However, instead of using these raw Binder objects, under some use cases you want the Binder tokens to implement an interface by creating a class that inherits both BBinder and IInterface (or both Binder and IInterface in Java), then this class inherits both the unforgeable feature of BBinder and the remote service interface described in IInterface. In addition to performing security checks, the Binder tokens are often used to manage system resources because being uniquely identifiable means they can be used as keys to system resources.‌

Now let’s look into how the unforgeable feature of BBinder instances is achieved in Binder system. I will use the notation BN_x to denote native Binder instances where the subscript "x" is a number; the notation ID_x denotes the unique identifier of BN_x under a certain identification system F_x in Binder; the notation EQ(ID_x, ID_y) denotes a procedure that takes in two unique identifiers and outputs a boolean. What we want is that when EQ(ID_x, ID_y) returns true, the two native Binders that generates ID_x and ID_y are actually the same native Binder instance; otherwise, the two native Binders that generates ID_x and ID_y are two different native Binder instances. If this property holds, then the unforgeable feature of native Binders is achieved. We will see how Binder achieves this kind of behavior soon. By the way, you probably won't see this kind of notations elsewhere and you won't find code in Android Binder framework that corresponds to the notations. I make these abstractions in this article merely for you to better understand and for myself to easier describe the concepts.‌

When we talk about native and proxy Binders, usually we are referring to the BBinder and BpBinder instances. However these utility classes are provided by Android native framework to present an object oriented view of Binder system, but user space programs don't need to use Android frameworks to work with Binder. For example, you can side load a native program to an Android device and talk to the Binder driver directly. The unforgeable feature of native Binders should hold even if a user space program is not using Android framework.‌

So first let’s understand how bare metal Binder driver makes native Binders unforgeable. As we have learned from previous articles, the driver maintains a set of binder_node structures for the native Binders created in a process. The keys of these binder_node structures in Binder driver is the user space memory addresses of the native Binders. For a remote process that is referencing the remote Binder, a unique handle value is allocated for the binder_ref structure and the kernel maintains the mapping between the binder_ref and binder_node. So the ID_x that the F_x generates is simple: a value pair (id, type) where the numerical value id is either the registered address in binder_node or the handle value in binder_ref and the type is either BINDER_TYPE_BINDER or BINDER_TYPE_HANDLE from the flat_binder_object structure. The EQ() procedure is also simple: it only outputs true when both the id and type are equal. I will explain the correctness of this EQ(ID_1, ID_2) in three cases:‌

(1) If the type part of ID_1 and ID_2 are different (one BINDER_TYPE_BINDER and one BINDER_TYPE_HANDLE), then EQ() always yields false. This is correct because in this case the BN_1 and BN_2 that ID_1 and ID_2 are generated from must be allocated in different processes, they cannot be the same instance.‌

(2) If both ID_1 and ID_2 have type equal to BINDER_TYPE_BINDER, then both BN_1 and BN_2 are created in local process. The EQ() yields true iff the two objects have the same memory address registered to kernel. They must be the same Binder object instance if EQ() yields true since you cannot allocate two objects in a process that have the same address. So EQ() behavior is correct intuitively for this case.‌

(3) If both ID_1 and ID_2 have type equal to BINDER_TYPE_HANDLE, then both BN_1 and BN_2 are created in remote processes. EQ() yields true iff the handle value in ID_1 and ID_2 are the same. As we learned from previous articles, the Binder driver allocates a unique handle value for the binder_ref structure in Binder driver. In a certain process, at most one instance ofbinder_ref will be allocated for a binder_node instance. So basically the binder_ref and binder_node has a one to one mapping in the scope of a certain process. So the behavior of EQ() is also correct for this case.‌

But there is an important caveat for the third case. The Binder driver only keeps the binder_ref structure alive when the weak and strong fields in it are not both 0. In another word, the handle value that the user space receives becomes invalid after the last user space reference to the binder_ref is lost. As we learned from the article "Binder lifecycle management", a process uses BC_ACQUIRE, BC_INCREFS, BC_DECREFS and BC_RELEASE to change the reference counts on a binder_ref structure. In order for EQ() to behave correctly for the third case, we need to make sure that user space never lost all references to the binder_ref before the EQ() is applied.‌

Let’s say a process receives the handle value V1 for a certain native Binder BN_1 and then it loses all reference count on the corresponding binder_ref. Later it regains reference to the same BN_1 through another transaction, but since the previous binder_ref is recycled, Binder driver will allocate a new binder_ref instance and the new handle value V2 is likely to be different than V1. The EQ(ID_1, ID_2) yields false incorrectly even though they both refer to BN_1. Similarly, after this process loses all reference on binder_ref, the process can obtain reference to a different BN_2 through another transaction, but the allocated handle value V2 for BN_2 can be the same as V1 because the binder_ref for BN_1 is already recycled; in this case EQ(ID_1, ID_2) yields true incorrectly because BN_1 and BN_2 are different Binder objects.‌

As we can see, the unforgeable nature of Binders holds even working with Binder driver directly. But in reality programs seldom directly work with Binder driver. They use BpBinder, BBinder in C++ code and BinderProxy, Binder in Java code. But remember Android code always use the IBinder polymorphic type to point to these objects, so the ID_x is actually the memory addresses of IBinders. It would be good if EQ(ID_1, ID_2) is as simple as ID_1 == ID_2. As we will see, this is indeed the case in Android framework. In another word, in both C++ and Java, you can just compare the memory address of the two IBinder instances to determine whether the underlying BBinder instances are the same. Let's see why this is correct in Android framework by discussing it under the similar three cases. I will discuss the cases under C++ first. Let's assume there are two instances of sp<IBinder> and we refer to them as ID_1 and ID_2.‌

(1) If ID_1 actually points to a BpBinder and ID_2 actually points to a BBinder or visa versa, EQ(ID_1, ID_2) (ID_1 == ID_2) always yields false. This is because they are two different types of objects so they cannot have the same memory address. This behavior is correct because BN_1 and BN_2 are two BBinder instances created in two different processes so they cannot be the same object.‌

(2) If both ID_1 and ID_2 actually point to a BBinder, then EQ(ID_1, ID_2) yields true iff they have the same memory address. This is correct behavior because in this case both Binder objects are created in local process, the two BBinder objects can be the same instance iff they have the same memory address. This is quite a trivial case.‌

(3) If both ID_1 and ID_2 actually point to a BpBinder, then EQ(ID_1, ID_2) yields true iff they point to the same BpBinder instance. On initial thought, this behavior cannot be correct because if there are two BpBinder instances holding the same handle value, EQ(ID_1, ID_2) yields false but they point to the same remote BBinder instance. In order to make EQ() behave correctly, we need to make sure that there will never be two BpBinder instances holding the same handle value in Android framework. Thanks to the BpBinder cache in ProcessState, this requirement is satisfied.

All BpBinder instances in Android framework are generated from this getStrongProxyForHandle method. The mHandleToObject field is a vector in ProcessState containing a per-process cache for BpBinder objects. The elements in this cache are handle_entry structures and the binder field stores the memory address of the cached BpBinder and the refs fields is the memory address of the weak pointer type in the BpBinder. The index of a handle_entry structure inside the vector cache is the handle value of the BpBinder. The lookupHandleLocked method is called to look up for an existing BpBinder in the cache with a specific handle value, if not found, the method will return an uninitialized handle_entry. Line 11 checks whether there is a BpBinder in the cache, if not found then line 12 to 15 will create a new instance of BpBinder, put it in the cache and return to caller. Otherwise, line 17 just returns the BpBinder instance to the caller. So in short, this method makes sure that only one instance of BpBinder exists for a certain handle value.‌

Now we know what the getStrongProxyForHandle does in general, there are some details worth looking at. First of all, let's take a closer look at the constructor and destructor of BpBinder:

We want a BpBinder to mirror the lifetime of the corresponding binder_ref in Binder driver; as we talked about earlier, user space needs to use the four BC_* commands to keep the binder_ref alive and BpBinder does just that. It uses BC_INCREFS in it's constructor to keep the binder_ref alive and uses BC_DECREFS in its destructor to free the weak reference on the binder_ref to let the driver free up the binder_ref. BC_ACQUIRE and BC_RELEASE will be sent to the driver in onFirstRef and onLastStrongRef to increment and decrement the strong reference count on the binder_ref structure. However, in regular object lifetime, an object will be destructed when last strong reference on it is removed; but we don't want that to happen on BpBinder because the binder_ref structure can get recycled while there are still weak references on the user space BpBinder. So a extendObjectLifetime method is designed to handle this case. By setting the lifetime to OBJECT_LIFETIME_WEAK, a BpBinder and binder_ref won't get prematurely deallocated when the last strong reference on the BpBinder is gone. So in short, a BpBinder will only gets deallocated when all references on it are gone and it has the same lifetime as binder_ref. The smart pointer interface extendObjectLifetime is specially designed for BpBinder. (BpRefBase also uses this feature but it's just because BpBinder uses it.)‌

And because of the special lifetime of BpBinder, a force_set interface in sp and a forceIncStrong method in RefBase need to be implemented so that onFirstRef of BpBinder will be called each time the BpBinder gets a first strong reference and send a BC_ACQUIRE to Binder driver. (Otherwise, an object will be deallocated when the last strong reference on it is gone, it cannot "get the first strong reference" for a second time.) This force_set feature is only used by BpBinder, specifically only in the getStrongProxyForHandle method.‌

The attemptIncWeak is another smart pointer interface that's specially designed for getStrongProxyForHandle. The method returns true only when the current weak reference count on the object is greater than 0. This handles a race condition in getStrongProxyForHandle: Note that in the destructor of BpBinder, expungeHandle is called to remove the cache entry in ProcessState. Let's say there are two Binder thread T1 and T2 and T1 releases the last reference on a BpBinder so it is calling the destructor of the BpBinder to remove the cache entry; at the same time T2 receives the same handle value in a transaction and needs to get an instance of BpBinder. In this race condition the BpBinder instance is being released but its entry in the cache hasn't been cleared. T2 shouldn't get back an instance of BpBinder that's about to be released so it calls attemptIncWeak to detect such races. Under such race conditions, a new instance of BpBinder needs to allocated.‌

All these code are here for the purpose to make EQ(ID_1, ID_2) behave correctly under the third case and now we have seen why it works in C++ code under Android frameworks. So basically as long as the code has references to two IBinder instances, you can use the equality operator to check the uniqueness of the underlying BBinder objects. Note that you need to hold and compare the references not the memory address values, so the following comparison is wrong and may yield incorrect results:

The code doesn’t hold a reference to sp1 so it is possible that the after line 7 the original BpBinder that sp1 points to is destructed and removed from the cache. In this case, even if sp2 actually references the same remote BBinder as sp1, the BpBinder that sp2 points to is a different BpBinder instance, so raw_ptr1 == raw_ptr2 will probably be false even if sp1 and sp2 actually reference the same remote BBinder. Similarly, raw_ptr1 and raw_ptr2 might happen to be equal when sp1 and sp2 reference different remote BBinder instances.‌

Now I will briefly touch on why the simple equality comparison works in Java code as well. I will not go into detail about it since the idea that make this possible in Java is similar to that in C++.

We talked about the relationship between Java Binder and C++ BBinder in the article "Binder architecture and core components", basically a Java BBinder has a peer JavaBBinderHolder and it will be deallocated at the same time the Java Binder is garbage collected. JavaBBinderHolder lazily initializes a JavaBBinder class which inherits BBinder and it is the kind of BBinder peered by Java Binder. So basically a Java Binder and its peer BBinder have the same lifetime.‌

The Java class BinderProxy peers a native BpBinder class. There is a special kind of BinderProxy cache in this class called ProxyMap that serves a similar purpose as the BpBinder cache in ProcessState. What this ProxyMap does is that for a given handle value, the same BinderProxy instance will be used as long as the BinderProxy is still strongly referenced in Java code. So in Java code you can also compare two instances of IBinder under any of the three cases discussed above.‌

As a conclusion, BBinder instances are unforgeable objects under Binder frameworks. The way you can check their identities is by simply using the intuitive equality operator ==. You can take this simplicity for granted but the Binder framework tried hard to make that simplicity possible. It will helpful to understand how that kind of simplicity is achieve by Binder framework.‌

For example, in the HamKing sample project [1], the unforgeable feature of Binder objects are exploited in a lot of places:

The mOrders field is a list of pending orders and the elements in the list are OrderRecord objects which extend from an auto-generated Stub so the OrderRecord class is a local Binder object. It might not be that obvious, but you can use a list to manage the orders simply because Binder objects are unforgeable. These OrderRecord instances will be sent to client app in requestOrder and sent back from client app in pickupOrder later, even though it passes process boundaries it can still maintain its identity so the pickupOrder method can just traverse the list and find out the corresponding order.‌

Besides, the cancelOrder method uses the == operator to find out all orders to remove based on the input session, and the removeDeadOrders method filters all orders based on the mCreditCard field. These operation are possible all because these Binder objects are unforgeable.

Window token

Window token is one of the best examples in Android to study the usages of Binder tokens. A window refers to a rectangle layer on the display that has certain properties like size, z-order, transparency, focusability, etc. The WindowManagerService (WMS) orchestrates all windows on a display based on the properties of these windows. Note that WMS doesn't manage the content rendering inside the windows, a window to WMS is just a rectangle in a 3-dimensional coordinate system. Content rendering is managed by another system service called SurfaceFlinger. A window token is a kind of Binder token used by WMS to identify a window. When a process wants to add, remove or change a window in WMS, it needs to provide a window token in the Binder request. The WMS will identify the window token and check the validity and permissions of this token before performing the actions requested.‌

There are many kinds of windows in Android, probably the most common type of window is the one used by Activitys. Before an application launches an Activity, it needs to make a request to ActivityTaskManagerService (ATMS) and it will receives a Binder token in the response that ATMS created and this Binder token is basically an identifier of the Activity instance. The ATMS will keep a record of this token itself of course. After the application receives the token, it needs to use this token to add a window of type TYPE_BASE_APPLICATION to WMS. When WMS receives the request, it checks the validity of this token with the help of ATMS and adds an application window if everything looks fine. This token is only possessed by this application, so a malicious app cannot mess with this window since it cannot duplicate a token to pass the security check of WMS.‌

Besides, WMS also uses these tokens as identifiers of windows to group and manage them. For example, there are some kinds of sub-windows that must reside within parent windows. For example, the window type of PopupWindow is TYPE_APPLICATION_PANEL which is one kind of sub-window. Unlike most other kinds of widgets, you cannot inflate a PopupWindow in an Activity's view tree because it floats on top of everything else, so it needs to create a separate window instance. However, by definition a PopupWindow needs an anchor in the Activity's view tree to display. (Otherwise you would rather just use a Dialog.) So the window of PopupWindow is a child of the Activity's window so it needs to use the Activity's window token to add the child window to WMS. WMS groups the two windows so that when the Activity's window is removed, the PopupWindow's window also gets removed.‌

Also, when an application crashes, all its windows in WMS need to be removed. Since both ATMS and WMS manages the tokens as keys, it would be very easy for WMS to identify all the windows under this application and have them removed at once.‌

Let’s now look into the source code of how window token is used in Android:

When ATMS handles a request to launch an Activity, it will create an instance of ActivityRecord for book keeping. Inside the ActivityRecord definition there is an appToken field which will be used as a unique identifier of an Activity by ATMS. ATMS will send it to the application where this Activity lives in. The application will then use this token as the window token to work with WMS. The class Token is almost a raw Binder: the IApplicationToken interface only has a trivial getName interface method. Like we talked about, any Binder in Java can be used as a Binder token but you can piggyback some interface methods with it if you want.‌

The ActivityThread is the place where an application process handles incoming Binder transactions. The inner class ApplicationThread inside ActivityThread is a Binder service that implements the IApplicationThread Binder interface. While Android launches an application, an instance of ApplicationThread will be created in the application process and ATMS will always hold a reference to it. When ATMS wants an application to do something, it will call some service methods on this IApplicationThread. The ApplicationThread class handles these incoming requests on Binder threads, so it needs to post messages to the application UI thread's message queue.

After ATMS receives a request to launch an Activity, it creates a ClientTransaction object and sends it to the application process via the scheduleTransaction interface on mClient. Note that ATMS processes the request to launch an Activity but it cannot create the actual Activity instance, because that's the job of the application. The mActivityToken in it is the same token we have seen in ActivityRecord. The mActivityCallbacks and mLifecycleStateRequest contains a list of actions the ATMS wants the application to do and the desired final state of the new Activity. For example, ATMS may tell the application process to launch a new instance of the Activity and the desired final state of the Activity should be ON_RESUME. After the application process receives the request in ApplicationThread, a message will be posted to UI thread message queue and the message will be handled in the Handler named mH. The ActivityThread uses the class ActivityClientRecord to represent an instance of Activity created in current application; the token field is the Binder token created and returned from ATMS and it is used as the key in mActivities map.‌

If ATMS wants this new Activity to be in a final state ON_RESUME, then the handleResumeActivity method will be called to move the Activity to that state. Inside this method the application will add a window to WMS for this Activity, because the semantic of ON_RESUME is that an Activity's UI should become visible:

Line 22 calls the addView method in WindowManagerImpl to add a window to WMS. WindowManagerImpl manages an app's interaction with WMS. Note that the window type of an Activity is set to TYPE_BASE_APPLICATION. Eventually ViewRootImpl will add the Activity's window to WMS:

The mWindowSession points an IWindowSession that the application gets from WMS. An application needs to first open a session before interacting with WMS. If you compare WMS with the RemoteService in HamKing project, then the responsibility of IWindowSession is similar to that of IOrderSession.

Eventually IWindowSession will call WMS to perform the action of adding a window. The security checks in addWindow is quite comprehensive but let's just look at a few of them.‌

Line 14 checks whether the window to add is a sub-window, if so it’s parent window must have already been added so line 15 tries to get parent window’s WindowState using the incoming window token as the key, otherwise an error code will return at line 17. The window type of a PopupWindow is TYPE_APPLICATION_PANEL which is one kind of sub-window, the parent Activity's window must have been added to WMS before you add a PopupWindow's window. For example, when a new Activity is launched and you try to show a PopupWindow in the onCreate callback of the Activity, you will receive a BadTokenException due to this error.‌

Line 22 tries to get the WindowToken instance using the incoming window token as the key. The class WindowToken, despite its name, is just a class that WMS uses to manage and group window tokens. During the process that ATMS handles the request of launching an Activity, it has sent the window token for this Activity to WMS so this method will find the WindowToken for the Activity. Otherwise it means a process tries to display an application window without the consent of ATMS, so the request will be rejected.‌

If everything passes, a WindowState instance will be created for this new window instance. The WindowState class is what WMS uses to do book keeping for an added window. After that this WindowState instance will be added to the global mWindowMap using the IWindow input as the key. The IWindow is a Binder interface that an application uses to receive callbacks from WMS about that window instance.

External links

[1] https://github.com/androidonekb/HamKing

--

--