Binder - Analysis and exploitation of CVE-2020-0041
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:
- Pixel 4 - Kernel msm-coral-4.14-android10
- Pixel 3/3a XL - Kernel msm-bonito-4.9-android10
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.
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).
The function binder_validate_ptr()
uses num_valid
and checks two things:
- If the given index (here
off_start_offset
) is less thannum_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.
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 beforenum_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.
This example is valid and respects all rules.
Example of rule checking (invalid 1)
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.
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:
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.
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.
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.
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.
At this point hierarchy of parents changes!
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.
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).