Binder - Analysis and exploitation of CVE-2020-0041

Written by Jean-Baptiste Cayrou - 03/03/2020 - in Exploit - Download
In December 2019, a new Binder commit was pushed in the Linux kernel. This patch fixes the calculation of an index used to process specific types of objects in a Binder transaction.

This article studies the implication of the corrected issue, why it's a security bug and how to take advantage of it.

Reading the previous article about binder internals is strongly recommended before reading this article.

Patch and versions concerned

The CVE-2020-0041 was published in the Android Security Bulletin of March 2020.

The patch attached to this CVE is the commit 16981742 and its description is the following:

binder: fix incorrect calculation for num_valid

For BINDER_TYPE_PTR and BINDER_TYPE_FDA transactions, the
num_valid local was calculated incorrectly causing the
range check in binder_validate_ptr() to miss out-of-bounds
offsets.
Fixes: bde4a19 ("binder: use userspace pointer as base of buffer space")

The description mentions an out-of-bounds in the binder_valid_ptr() function. This seems to be a security fix!

The bug was introduced in February 2019 by a code refactoring (commit bde4a19). Actually few devices were impacted because most vendors use old kernels, and this vulnerability only affects recent kernels. To my knowledge only Pixel 4 and Pixel 3/3a XL on Android 10 were impacted:

Patch overview

diff --git a/drivers/android/binder.c b/drivers/android/binder.c
index e9bc9fc..b2dad43 100644
--- a/drivers/android/binder.c
+++ b/drivers/android/binder.c
@@ -3310,7 +3310,7 @@
            binder_size_t parent_offset;
            struct binder_fd_array_object *fda =
                to_binder_fd_array_object(hdr);
-           size_t num_valid = (buffer_offset - off_start_offset) *
+           size_t num_valid = (buffer_offset - off_start_offset) /
                        sizeof(binder_size_t);
            struct binder_buffer_object *parent =
                binder_validate_ptr(target_proc, t->buffer,
@@ -3384,7 +3384,7 @@
                t->buffer->user_data + sg_buf_offset;
            sg_buf_offset += ALIGN(bp->length, sizeof(u64));

-           num_valid = (buffer_offset - off_start_offset) *
+           num_valid = (buffer_offset - off_start_offset) /
                    sizeof(binder_size_t);
            ret = binder_fixup_parent(t, thread, bp,
                          off_start_offset,

The patch fixes the computation of a num_valid index which is used as parameter in the call of binder_fixup_parent(). The multiplication * is replaced by a division /.

When a binder transaction contains binder objects, a list of offsets gives the position of the different binder objects in the transaction buffer.

offsets_index
Offsets buffer of a binder transaction

Let's take an example, if the object is at offset 0x10 (object BINDER_TYPE_PTR C on the diagram above), the correct value of the index should be 0x2:

    size_t num_valid = (buffer_offset - off_start_offset) / sizeof(binder_size_t);
    /*
    If (buffer_offset - off_start_offset) = 0x10

    num_valid = 0x10 / 0x8
    num_valid = 0x2
    */

The computed value is 0x80 in the vulnerable version.

    // Incorrect version
    size_t num_valid = (buffer_offset - off_start_offset) * sizeof(binder_size_t);
    /*
    If (buffer_offset - off_start_offset) = 0x10

    num_valid = 0x10 * 0x8
    num_valid = 0x80
    */

That's quite the difference! The buggy version allows to send a binder object with a parent index out of the offsets buffer (in blue).

offsets_out_of_bound
Offsets buffer out-of-bound

The function binder_validate_ptr() uses num_valid and checks two things:

  • If the given index (here off_start_offset) is less than num_valid, the function only trusts the objects that have already been processed.
  • If there is a valid binder_buffer_object (checked using the magic number) at the offset found in index (off_start_offset).
//drivers/android/binder.c

static int binder_fixup_parent(struct binder_transaction *t,
                   struct binder_thread *thread,
                   struct binder_buffer_object *bp,
                   binder_size_t off_start_offset,
                   binder_size_t num_valid,
                   binder_size_t last_fixup_obj_off,
                   binder_size_t last_fixup_min_off)
{
    // [...]
    if (!(bp->flags & BINDER_BUFFER_FLAG_HAS_PARENT))
        return 0;

    parent = binder_validate_ptr(target_proc, b, &object, bp->parent,
                     off_start_offset, &parent_offset,
                     num_valid);
//drivers/android/binder.c

static struct binder_buffer_object *binder_validate_ptr(
                        struct binder_proc *proc,
                        struct binder_buffer *b,
                        struct binder_object *object,
                        binder_size_t index,
                        binder_size_t start_offset,
                        binder_size_t *object_offsetp,
                        binder_size_t num_valid)
{
    // [...]
    if (index >= num_valid)
        return NULL;

    buffer_offset = start_offset + sizeof(binder_size_t) * index;
    binder_alloc_copy_from_buffer(&proc->alloc, &object_offset,
                      b, buffer_offset, sizeof(object_offset));
    object_size = binder_get_object(proc, b, object_offset, object);
    if (!object_size || object->hdr.type != BINDER_TYPE_PTR)
        return NULL;
    // [...]

Even though there is an out-of-bound (read access) with the parent index, it is only possible to access a limited part of the memory because the kernel verifies that the memory is in the recipient transaction buffer. Moreover, a magic number is needed at the start of the object, so the offset must point on this magic. Additionally, the kernel does not leak the value if the magic is not correct.

For the moment, the impact of the bug is hard to see. To understand a possible exploitation, we need a better understanding of the object parent system in Binder.

Parents with binder objectsl

Binder objects BINDER_TYPE_PTR and BINDER_TYPE_FDA have a field parent and parent_offset which allows to patch a pointer inside the parent buffer. This feature is used by the HIDL language (Hardware Service) and explained in the previous article about binder internals.

hidl_sting Example

A good example of usage of BINDER_TYPE_PTR parents is the hidl_string structure.

    // Extract from system/libhidl/base/include/hidl/HidlSupport.h
    struct hidl_memory {
        // ...
        private:
            hidl_handle mHandle     __attribute__ ((aligned(8)));
            uint64_t    mSize       __attribute__ ((aligned(8)));
            hidl_string mName       __attribute__ ((aligned(8)));
    };

When a process A wants to send a hidl_string to process B, the structure contains a pointer which belongs to process A. To make the structure valid in the receiver process memory, the Binder driver must change it by a pointer which belongs to the memory space of process B. This is done by the fields parent and parent_offset of BINDER_TYPE_PTR.

// structure of BINDER_TYPE_PTR
struct binder_object_header {
    __u32        type;
};

struct binder_buffer_object {
    struct binder_object_header hdr;
    __u32 flags;
    binder_uintptr_t buffer;
    binder_size_t length;
    binder_size_t parent; // Index to parent object (in offsets buffer)
    binder_size_t parent_offset; // Offset to patch in the parent buffer
};

To send a hidl_string, a first buffer (A) is used to send the hidl_memory C structure and a second buffer (B) is used to store the real string ("My demo string", in this case). The buffer A is the parent of B and the offset 0 of A is patched using the address of the string in the targeted process memory.

binder_buffer_object_example2
Binder parent example with hidl_string

parents fixup rules

Some rules restrict the usage of parents in binder objects. Of course, before calling this check, the binder kernel had already checked that the pointer to the buffer and its size is valid (points in the memory of the caller).

These rules on parents binder objects hierarchy are checked by calling binder_fixup_parent().

Rules applied by the kernel are the following :

Rules checked by binder_validate_ptr()

  • [1] The parent index must be smaller or equal to num_valid. All objects before num_valid are already verified by the kernel.

Rules checked by binder_validate_fixup()

  • [2] Only allow fixup on the last buffer object that was verified, or one of its parents
  • [3] We only allow fixups inside a buffer to happen at increasing offsets

For the next of the article, these previous rules have been identified by a number from [1] to [3].

Example of rule checking (valid)

To validate all binder objects of a transaction, the kernel checks them in the order they are registered in the offsets buffer. Remember that this buffer contains a list of offsets where binder object are stored in the transaction data buffer.

valid_parents
Valid binder parents

This example is valid and respects all rules.

Example of rule checking (invalid 1)

invalid
Invalid parent offset

This example is not valid because it breaks the rule [3]:

We only allow fixups inside a buffer to happen at increasing offsets

Example of rule checking (invalid 2)

In the diagram below, the validation failed while the kernel was checking the offset corresponding to the object D.

invalid_2
Invalid parent

This example is not valid because it breaks the rule [2]:

Only allow fixup on the last buffer object that was verified, or one of its parents

In our case, the last verified object is C however the parent of object D is B. But B is not C or A (C's parent). The hierarchy is not valid.

How to exploit the bug ?

An interesting way to exploit this bug could be to have a parent buffer with arbitrary value for its fields buffer and length.

The bug allows to easily bypass the rule [1] however the rule [3] is harder to bypass.

Exploitation Idea

The trick is to change the parents hierarchy during the validation process. This can be done using the extra buffer ! Indeed this part of the buffer is used by the kernel to store the data related to BINDER_TYPE_PTR objects. If a binder object has a parent index which points to the extra part, its parent will be changed when the kernel will copy data at this place.

Kernel validation - Initial configuration

To perform the exploit, three buffer objects are needed (add one buffer not registered in the offsets list) with the following hierarchy:

exploit_parent1
The object D contains arbitrary data

We use the vulnerability on the calculation of num_valid to setup a same parent for objects B and C and set their parent index which refers the extra data part (purple area). Before the validation, the extra part is uninitialized and contains data of previous binder transactions. These data can be controlled by sending transactions and spraying the buffer with the wanted offset A value.

exploit1

Kernel validation - Pause when C is checked

The kernel checks the objects contained in the offsets list by starting at offset A. All objects until C are valids. Each time an object is processed, its related buffer is copied in the extra part as described in the diagram below.

exploit2

The diagram shows the state of the algorithm. The kernel already checked the buffers A and B, but the object C was not checked. At this time, the offset value of parent_index was not modified yet (set with the value corresponding to offset A) and the last verified object is B.

exploit_parent2

Kernel validation - Object C

When the Binder driver processes the object C, it first copies its buffer in the extra part. However this copy overwrites the previous value of the parent_index. The data of buffer C were prepared to change the value with a new value corresponding to the object D.

exploit3

 

At this point hierarchy of parents changes!

exploit_parent3

 

All rules are respected ! Indeed the parent of C (here D) must be the last verified object or one of its parents.

With this configuration we have bypassed checks done by binder_validate_ptr() and binder_validate_fixup()

Kernel validation - Parent patching

Once the object C passed all checks, the kernel patches the parent buffer by calling the function binder_alloc_copy_to_buffer()

// Extract of binder.c
static int binder_fixup_parent(...){
    // Check of rules here [...]

    buffer_offset = bp->parent_offset +
            (uintptr_t)parent->buffer - (uintptr_t)b->user_data;
    binder_alloc_copy_to_buffer(&target_proc->alloc, b, buffer_offset,
                    &bp->buffer, sizeof(bp->buffer));

Remember that when this code is executed, the targeted process is not mapped.

All /dev/binder devices are accessible in the kernel memory. When the binder file descriptor is mapped by an userland process. The kernel allocates pages (with kzalloc here) and maps these pages in the process memory.

During a binder transaction, the kernel can retrieve the kernel address of this allocation by applying an offset on the memory address of the receiver process. In a normal call, the value of parent->buffer belongs to the /dev/binder memory of the targeted process because the value was previously patched by the kernel when the parent object was processed. The driver can obtain the corresponding kernel address with the following calculation:

kernel_proc_buffer = parent->buffer - b->user_data

Using our exploit, we are able to control partially kernel_proc_buffer because b->user_data is not known.

The value written in the parent buffer (plus the offset) is the address of the current object (in our case, the address of object C in targeted process memory : in extra part)

Unfortunately for our exploit, the function binder_alloc_copy_to_buffer() performs additional checks on the address to patch.

// in drivers/android/binder_alloc.c

int binder_alloc_copy_to_buffer(struct binder_alloc *alloc,
                struct binder_buffer *buffer,
                binder_size_t buffer_offset,
                void *src,
                size_t bytes)
{
    return binder_alloc_do_buffer_copy(alloc, true, buffer, buffer_offset,
                       src, bytes);
}

static int binder_alloc_do_buffer_copy(struct binder_alloc *alloc,
                       bool to_buffer,
                       struct binder_buffer *buffer,
                       binder_size_t buffer_offset,
                       void *ptr,
                       size_t bytes)
{
    /* All copies must be 32-bit aligned and 32-bit size */
    BUG_ON(!check_buffer(alloc, buffer, buffer_offset, bytes));

    // [...]

The function binder_alloc_do_buffer_copy() checks that the buffer to patch is inside the current reception buffer for the current binder transaction.

This exploit does not allow to target the kernel memory.

It can be noticed that if the address is not valid, the kernel calls BUG_ON which stops the kernel execution.

By bypassing all checks, we are able to set an arbitrary value to parent->buffer however we only have one try else the kernel will stop! We need a memory leak to know the address where is mapped /dev/binder in the targeted process.

In order to validate this theory, let's see with the described exploit works. Because we do not have a leak (yet), we run a modified kernel in the Android emulator.

static void binder_alloc_do_buffer_copy(struct binder_alloc *alloc,
                    bool to_buffer,
                    struct binder_buffer *buffer,
                    binder_size_t buffer_offset,
                    void *ptr,
                    size_t bytes)
{

    if (!check_buffer(alloc, buffer, buffer_offset, bytes)){
        size_t buffer_size = binder_alloc_buffer_size(alloc, buffer);
        pr_info("[JB] check_buffer buffer_size : 0x%lx bytes =  0x%lx  offset = 0x%lx\n", buffer_size, bytes, buffer_offset);
    }
    /* All copies must be 32-bit aligned and 32-bit size */
    BUG_ON(!check_buffer(alloc, buffer, buffer_offset, bytes));

A debug print has been added (call to pr_info(), to check if we have an invalid value for buffer_offset)

POC

The custom kernel (based on msm-bonito-4.9-android10) is launched with a Pixel 3a XL firmware. The PoC sends a binder transaction to the servicemanager using the parent hierarchy described in the previous diagrams.

./emulator -avd Pixel_3a_XL_API_29_64b -kernel custom_bzImage -show-kernel -no-window -verbose -ranchu -no-snapshot
[  148.291702] binder: 3410:3410 ioctl c0306201 7fff98cb5f20 returned -22
[  148.295022] binder_alloc: [JB] check_buffer buffer_size : 0x10e0 bytes =  0x8  offset = 0x71829fdc8b8
[  148.299460] ------------[ cut here ]------------
[  148.301159] kernel BUG at drivers/android/binder_alloc.c:1133!
[  148.303042] invalid opcode: 0000 [#1] PREEMPT SMP NOPTI
[  148.304537] Modules linked in:
[  148.305422] CPU: 0 PID: 3410 Comm: poc Not tainted 4.14.150HELLO+ #28
[  148.307397] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS rel-1.11.1-0-g0551a4be2c-prebuilt.qemu-project.org 04/01/2014
[  148.311690] task: 0000000086b3eedc task.stack: 0000000000a1c204
[  148.313730] RIP: 0010:binder_alloc_do_buffer_copy+0x8d/0x15e
[  148.315692] RSP: 0018:ffffa11501effa48 EFLAGS: 00010246
[  148.317540] RAX: 0000000000000000 RBX: ffff9e98a62079c0 RCX: 0000000000000008
[  148.320403] RDX: ffff9e98aa0e5dd8 RSI: 0000000000000000 RDI: ffff9e98aa0e5da0
[  148.323268] RBP: ffffa11501effaa0 R08: 0000000000000ff4 R09: 0000000000000000
[  148.325435] R10: 0000000000000000 R11: 0000000000000000 R12: 0000000000000008
[  148.328290] R13: 0000071829fdc8b8 R14: ffff9e98aa0e5da0 R15: ffff9e98a62079c0
[  148.330194] FS:  000000000048d648(0000) GS:ffff9e98bfc00000(0000) knlGS:0000000000000000
[  148.331780] CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[  148.332740] CR2: 00007435311239a0 CR3: 0000000010ee2000 CR4: 00000000000006b0
[  148.333848] Call Trace:
[  148.334207]  binder_alloc_copy_to_buffer+0x1a/0x1c
[  148.334895]  binder_fixup_parent+0x186/0x1ac

The debug string proves the PoC works because the offset value is invalid (0x71829fdc8b8 is quite a large offset!)

binder_alloc: [JB] check_buffer buffer_size : 0x10e0 bytes =  0x8  offset = 0x71829fdc8b8

Memory Mapping leak of /dev/binder

Without knowing the memory mapping of the targeted process, this PoC is quite useless :(. However, nothing is lost!

Android Java applications have a specificity, they are all a fork of Zygote or Zygote64 (depending if 32/64 bits).

Zygote is a process with a pre-initialized Java Virtual Machine. When the system needs to start a new Java application, Zygote is forked and starts the execution of this application. This design allows to reduce the initialization step; Java applications can be launched as fast as possible. However, when the call to fork() is performed, the virtual memory is cloned and so its memory mapping too. Thus all children of Zygote share the same mapping.

Let's check on the emulator:

root          1612     1 4758476 190144 poll_schedule_timeout 0 S zygote64
...
u0_a103       3891  1612 4927284 124964 SyS_epoll_wait      0 S com.foo.mypoc
cat /proc/$(pidof com.foo.mypoc)/maps | grep "/dev/binder"
7a6242192000-7a6242290000 r--p 00000000 00:12 7315                       /dev

Let's say we can execute arbitrary code as the com.foo.mypoc package, by checking process memory maps it is possible to find where /dev/binder is mapped. In our case, it is mapped at 0x7a6242192000.

The process com.foo.mypoc was forked from zygote64. Others process with the same mother are the following:

generic_x86_64:/ # ps -e  | grep $(pidof zygote64)
root          1612     1 ... zygote64
system        1845  1612 ... system_server
u0_a89        1996  1612 ... com.android.systemui
network_stack 2118  1612 ... com.android.networkstack
radio         2199  1612 ... com.android.phone
system        2210  1612 ... com.android.settings
u0_a55        2261  1612 ... android.ext.services
u0_a84        2296  1612 ... com.android.launcher3
u0_a102       2321  1612 ... com.android.inputmethod.latin
u0_a87        2436  1612 ... com.android.dialer
u0_a37        2465  1612 ... android.process.acore
secure_element 2553 1612 ... com.android.se
radio         2586  1612 ... com.android.ims.rcsservice
system        2626  1612 ... com.android.emulator.multidisplay
u0_a77        2686  1612 ... com.android.smspush
u0_a67        2705  1612 ... com.android.printspooler
u0_a40        2787  1612 ... android.process.media
u0_a97        2884  1612 ... com.android.email
u0_a78        2947  1612 ... com.android.messaging
u0_a81        2971  1612 ... com.android.onetimeinitializer
u0_a52        3005  1612 ... com.android.packageinstaller
u0_a54        3027  1612 ... com.android.permissioncontroller
u0_a39        3050  1612 ... com.android.providers.calendar
u0_a62        3075  1612 ... com.android.traceur
u0_a41        3097  1612 ... com.android.externalstorage
system        3134  1612 ... com.android.localtransport
system        3230  1612 ... com.android.keychain
u0_a103       3891  1612 ... com.foo.mypoc

The package com.android.settings seems quite interesting because it runs as system.

generic_x86_64:/ # cat /proc/$(pidof com.android.settings)/maps | grep "/dev/binder"
7a6242192000-7a6242290000 r--p 00000000 00:12 7315

Actually, the binder device is mapped at the same position than our application com.foo.mypoc!

Exploitation Ideas

With the previous PoC and the memory mapping of a targeted process it is possible to overwrite data related to already verified binder objects.

Userland binder libraries (libbinder.so and libhwbinder.so) trust in the binder driver processing and consider all binder objects are correctly patched. In the case where patching is not correctly done, applications can be vulnerable during the Parcel unserialization step.

File descriptor

Overwrite a BINDER_TYPE_FDA patched object to make it point to controlled and unchecked file descriptors list. We could imagine closing an arbitrary file descriptor in a targeted process to replace it with a controlled one.

Binder buffers

Overwrite the size of a BINDER_TYPE_PTR object. If the size field of an embedded buffer structure (like hidl_string) is changed using the vulnerability, the new value will be a pointer and won't be valid regarding the correct buffer.

    details::hidl_pointer<const char> mBuffer;
    uint32_t mSize; // Try overwrite the size
    bool mOwnsBuffer;

Binder/handle objects

Overwrite pointers of BINDER_TYPE_HANDLE/BINDER_TYPE_WEAK_HANDLE objects. When the binder kernel module processes these objects it will replace handlers by their original pointers value in the remote process. The kernel keeps in its memory a mapping between handlers/pointers and fixes BINDER_TYPE_HANDLE or BINDER_TYPE_BINDER using this table. Sometimes, a remote service needs to instantiate an object to execute a command. To use this object, clients send an object handle (BINDER_TYPE_HANDLE). The kernel replaces it by a BINDER_TYPE_BINDER which contains the real pointer in the targeted receiving buffer.

If an attacker replaces the BINDER_TYPE_BINDER object pointer using the vulnerability, he could control all fields of the object type in order to gain code execution.

binder_type_binder_1
Normal transaction
 
binder_type_binder_2
The object points to controlled data

Conclusion

The analysis of this bug and the way to exploit it is quite fun and original. It allows to play with binder parents.

Even though this vulnerability does not allow to target the kernel memory, it could lead to the exploitation of a more privileged application.

The analysis done in this article is just the first step required to exploit this vulnerability. Quite a lot of work would be required to transform these primitives into an actual privilege escalation exploit.

In my opinion, this bug could have been found during an attentive code review before adding it in the Linux kernel source code. My statement is the same than for my previous patch analysis on secctx patch , several 'simple' bugs have been recently inserted in the kernel source code.

Luckily this recent bug only affected a few devices (Pixel 4 and 3a on Android 10).