This article provides an in-depth analysis of two kernel vulnerabilities within the Mali GPU, reachable from the default application sandbox, which I independently identified and reported to Google.
It includes a kernel exploit that achieves arbitrary kernel r/w capabilities. Consequently, it disables SELinux and elevates privileges to root on Google Pixel 7 and 8 Pro models running the following Android 14 versions:
- Pixel 8 Pro:
google/husky/husky:14/UD1A.231105.004/11010374:user/release-keys
- Pixel 7 Pro:
google/cheetah/cheetah:14/UP1A.231105.003/11010452:user/release-keys
- Pixel 7 Pro:
google/cheetah/cheetah:14/UP1A.231005.007/10754064:user/release-keys
Vulnerabilities
This exploit leverages two vulnerabilities: an integer overflow resulting from an incomplete patch in the gpu_pixel_handle_buffer_liveness_update_ioctl
ioctl command, and an information leak within the timeline stream message buffers.
Buffer Underflow In gpu_pixel_handle_buffer_liveness_update_ioctl() Due To Incorrect Integer Overflow Fix
Google addressed an integer overflow in the gpu_pixel_handle_buffer_liveness_update_ioctl
ioctl command in this commit.
At first, when I reported this issue, I thought the bug was caused by an issue in the patch described earlier.
After reviewing the report, I came to the realization that my analysis of the a vulnerability was inaccurate. Despite my first assumption of the patch being incomplete, it effectively resolves and prevents an underflow in the calculation.
This lead me to suspect that the change wasn’t applied in the production builds. However, although I can cause an underflow in the calculation, it is not possible to cause an overflow.
This suggests that the ioctl command has been partially fixed, although not with the above patch shown above.
Looking at IDA revealed that another incomplete patch was shipped in the production releases, and this patch is not present in any git branch of the mali gpu kernel module.
This vulnerability was first discovered in the latest Android version and reported on November 19, 2023.
Google later informed me that they had already internally identified it and had assigned it CVE-2023-48409 in the December Android Security Bulletin, labeling it as a duplicate issue.
Although I was able to verify that the bug had been internally identified months prior to my report, (based on the commit date around August 30) there remains confusion.
Specifically, it’s strange that the Security Patch Levels (SPL) for October and November of the most recent devices were still affected by this vulnerability —I haven’t investigated versions prior to these.
Therefore, I am unable to conclusively determine whether this was truly a duplicate issue and if the appropriate patch was indeed scheduled for December prior to my submission or if there was an oversight in addressing this vulnerability.
Anyway, what makes this bug powerful is the following:
- The buffer
info.live_ranges
is fully user-controlled. - The overflowing values are user-controlled input, thereby, we can overflow the calculation so the
info.live_ranges
pointer can be at an arbitrary offset prior to the start of thebuff
kernel address. - The allocation size is also user controlled input, which gives the ability to request a memory allocation from any general-purpose slab allocator.
This vulnerability shares similarities with the DeCxt::RasterizeScaleBiasData() Buffer underflow vulnerability I found and exploited in the iOS 15 kernel back in 2022.
Leakage Of Kernel Pointers In Timeline Stream Message Buffers
The GPU Mali implements a custom timeline stream
designed to gather information, serialize it, and subsequently write it to a ring buffer following a specific format.
Users can invoke the ioctl command kbase_api_tlstream_acquire
to obtain a file descriptor, enabling them to read from this ring buffer. The format of the messages is as follows:
- A packet header
- A message id
- A serialized message buffer, where the specific content is contingent upon the message ID. For example, the
__kbase_tlstream_tl_kbase_kcpuqueue_enqueue_fence_wait
function serializes thekbase_kcpu_command_queue
anddma_fence
kernel pointers into the message buffer, resulting in leaking kernel pointers to user space process.
void __kbase_tlstream_tl_kbase_kcpuqueue_enqueue_fence_wait(
struct kbase_tlstream *stream,
const void *kcpu_queue,
const void *fence
)
{
const u32 msg_id = KBASE_TL_KBASE_KCPUQUEUE_ENQUEUE_FENCE_WAIT;
const size_t msg_size = sizeof(msg_id) + sizeof(u64)
+ sizeof(kcpu_queue)
+ sizeof(fence)
;
char *buffer;
unsigned long acq_flags;
size_t pos = 0;
buffer = kbase_tlstream_msgbuf_acquire(stream, msg_size, &acq_flags);
pos = kbasep_serialize_bytes(buffer, pos, &msg_id, sizeof(msg_id));
pos = kbasep_serialize_timestamp(buffer, pos);
pos = kbasep_serialize_bytes(buffer,
pos, &kcpu_queue, sizeof(kcpu_queue));
pos = kbasep_serialize_bytes(buffer,
pos, &fence, sizeof(fence));
kbase_tlstream_msgbuf_release(stream, acq_flags);
}
The proof of concept exploit leaks the kbase_kcpu_command_queue
object address by monitoring to the message id KBASE_TL_KBASE_NEW_KCPUQUEUE
which is dispatched by the kbasep_kcpu_queue_new
function whenever a new kcpu queue object is allocated.
Google informed me that the vulnerability was reported in March 2023 and was assigned CVE-2023-26083 in their security bulletin.
Nonetheless, I was able to replicate the issue on the latest Pixel devices shipped with the Security Patch Levels (SPL) for October and November, indicating that the fix had not been applied correctly or at all.
Subsequently, Google quickly addressed the issue in the December Security Update Bulletin without offering credit, and later informed me that the issue was considered a duplicate.
The rationale behind labeling this issue as a duplicate, however, remains questionable.
Exploitation
So I have two interesting vulnerabilities. The first one offers a powerful capability to modify the content of any 16-byte aligned kernel address that comes before the allocated address.
The second vulnerability provides hints into the potential locations of objects within the kernel memory.
Notes On buffer_count and live_ranges_count Values
With total control over the buffer_count
and live_ranges_count
fields, I have the flexibility to select the target slab and the precise offset I intend to write to.
However, selecting values for buffer_count
and live_ranges_count
requires careful consideration due to several constraints and factors:
- Both values are related, and the overflow will occur only if all the newly introduced checks are bypassed.
- The requirement for the negative offset to be 16-bytes aligned restricts the ability to write to any chosen location. However, this is generally not a significant hindrance.
- Opting for a larger offset leads to a large amount of data being written to areas of memory that may not be intended targets. For instance, if the allocation size overflows to
0x3004
, thelive_ranges
pointer would be set to-0x4000
bytes from thebuff
object’s allocated space. Thecopy_from_user
function would then write0x7004
bytes, based on the calculation ofupdate->live_ranges_count
times 4. Consequently, this operation would result in user-controlled data overwriting the memory area between thelive_ranges
pointer and thebuff
allocation. It is essential, therefore, to carefully ensure that no critical system objects within that range are accidentally overwritten. Given that the operation involves acopy_from_user
call, one might consider triggering anEFAULT
by deliberately un-mapping the undesired memory region following the user source buffer to prevent data from being written to sensitive locations. However, this approach is ineffective, that’s because if theraw_copy_from_user
function fails, it will zero out the remaining bytes in the destination kernel buffer. This behavior is implemented to ensure that in case of a partial copy due to an error, the rest of the kernel buffer does not contain uninitialized data.
static inline __must_check unsigned long
_copy_from_user(void *to, const void __user *from, unsigned long n)
{
unsigned long res = n;
might_fault();
if (!should_fail_usercopy() && likely(access_ok(from, n))) {
instrument_copy_from_user(to, from, n);
res = raw_copy_from_user(to, from, n);
}
if (unlikely(res))
memset(to + (n - res), 0, res);
return res;
}
For more information click here