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 the buff 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:

  • packet header
  • 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 the kbase_kcpu_command_queue and dma_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, the live_ranges pointer would be set to -0x4000 bytes from the buff object’s allocated space. The copy_from_user function would then write 0x7004 bytes, based on the calculation of update->live_ranges_count times 4. Consequently, this operation would result in user-controlled data overwriting the memory area between the live_ranges pointer and the buff allocation. It is essential, therefore, to carefully ensure that no critical system objects within that range are accidentally overwritten. Given that the operation involves a copy_from_user call, one might consider triggering an EFAULT 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 the raw_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