FlawOpen

flawopen.com/Teardowns/cve-2024-0582-linux-io-uring-privilege-escalation

● CVE-2024-0582 · CVSS 7.8 · Tinggi
Riset Keamanan · FlawOpen

CVE-2024-0582: Use-After-Free Ring Buffer Ter-mmap di io_uring Linux

CVE-2024-0582 (CVSS 7.8, tinggi): ring buffer yang didaftarkan dengan IORING_REGISTER_PBUF_RING dan IOU_PBUF_RING_MMAP dibebaskan saat pembongkaran padahal ruang pengguna masih memetakannya dengan mmap(), menyisakan satu halaman yang bisa dibaca dan ditulis penyerang setelah kernel memakainya kembali, sehingga terjadi peningkatan hak istimewa lokal.

💡 Penjelasan Sederhana (ELI5)

Sebuah perpustakaan membiarkan Anda mendaftarkan ruang baca khusus dan memberi Anda kunci untuk keluar-masuk. Saat Anda membatalkan pendaftaran ruang itu, pustakawan langsung mengembalikannya ke daftar tersedia, tetapi tidak pernah menarik kunci Anda. Perpustakaan lalu menyewakan ruang yang sama kepada orang lain, dan Anda masih bisa masuk dengan kunci lama dan menulis ulang apa pun yang ditinggalkannya di meja. Perbaikannya adalah menjaga setiap ruang semacam itu tetap dipesan dan hanya melepasnya sekali, saat tutup, setelah semua kunci dikembalikan.

Konsep Kunci & Istilah

io_uring
Antarmuka I/O asinkron Linux. Aplikasi dan kernel berbagi ring pengiriman dan penyelesaian, sehingga tidak perlu satu syscall per operasi.
Ring buffer yang disediakan (IORING_REGISTER_PBUF_RING)
Ring buffer yang didaftarkan aplikasi agar kernel memilih satu per operasi. Dengan IOU_PBUF_RING_MMAP, kernel mengalokasikan ring dan aplikasi mem-mmap()-nya alih-alih menyediakan memorinya sendiri.
Use-after-free (UAF)
Memakai memori setelah dibebaskan. Begitu kernel mengalokasikan ulang memori itu untuk objek lain, referensi usang dapat membaca atau merusaknya.
remap_pfn_range / folio_put
Ring dipetakan ke ruang pengguna dengan remap_pfn_range(), jadi pembukuan munmap biasa tidak membebaskannya. Memanggil folio_put() saat pembongkaran membebaskan halaman padahal pemetaan masih aktif.

Pemetaan kerangka kerja

  • Referensi CWE untuk kelemahan yang dijelaskan dalam panduan ini: CWE-416

Analisis Akar Masalah (Root Cause)

Untuk ring buffer yang dibuat dengan IOU_PBUF_RING_MMAP, io_alloc_pbuf_ring() mengalokasikan halaman dan ruang pengguna memetakannya dengan mmap() lewat remap_pfn_range(). Karena jalur itu melewati pembukuan munmap/release biasa, __io_remove_buffers() yang memanggil folio_put() membebaskan halaman padahal pemetaan pengguna masih mungkin ada — sebuah use-after-free (CWE-416): ruang pengguna tetap memegang pemetaan yang sah ke halaman yang sudah dikembalikan kernel ke alokator dan bisa dialokasikan ulang. Jann Horn melaporkannya. Commit c392cbecd8ec mencatat setiap ring yang dialokasikan mmap ke io_buf_list baru dan hanya membebaskannya di io_kbuf_mmap_list_free() saat ->release(), setelah pemetaan hilang.

Alur Serangan Langkah demi Langkah

Langkah 1

Mendaftarkan ring berbasis mmap

Proses tanpa hak istimewa memanggil io_uring_register() dengan IORING_REGISTER_PBUF_RING dan flag IOU_PBUF_RING_MMAP, sehingga kernel mengalokasikan ring buffer.

Langkah 2

Memetakannya ke ruang pengguna

Proses mem-mmap() ring itu. Kernel memetakan halaman dengan remap_pfn_range(), dan proses mempertahankan pemetaan.

Langkah 3

Membebaskan ring saat masih dipetakan

Membatalkan pendaftaran buffer (atau membongkar ring) mencapai __io_remove_buffers(), yang folio_put()-nya membebaskan halaman meski pemetaan pengguna masih aktif.

Langkah 4

Menulis lewat pemetaan usang

Kernel mengalokasikan ulang halaman yang dibebaskan untuk objek lain; proses yang masih memegang pemetaan membaca dan menimpanya untuk merusak keadaan kernel dan naik ke root.

Kode Sumber: Rentan vs Aman

IMPLEMENTASI RENTAN
/* io_uring/kbuf.c sebelum perbaikan (Linux 6.6.4) */
static int io_alloc_pbuf_ring(struct io_uring_buf_reg *reg,
			      struct io_buffer_list *bl)
{
	gfp_t gfp = GFP_KERNEL_ACCOUNT | __GFP_ZERO | __GFP_NOWARN | __GFP_COMP;
	size_t ring_size;
	void *ptr;

	ring_size = reg->ring_entries * sizeof(struct io_uring_buf_ring);
	ptr = (void *) __get_free_pages(gfp, get_order(ring_size));
	if (!ptr)
		return -ENOMEM;

	bl->buf_ring = ptr;
	bl->is_mapped = 1;
	bl->is_mmap = 1;
	return 0;
}

static int __io_remove_buffers(struct io_ring_ctx *ctx,
			       struct io_buffer_list *bl, unsigned nbufs)
{
	if (bl->is_mapped) {
		i = bl->buf_ring->tail - bl->head;
		if (bl->is_mmap) {
			/* BUG: ring diberikan ke ruang pengguna melalui mmap() dan
			 * remap_pfn_range(). Di sini folio_put() melepas halaman saat
			 * pemetaan mungkin masih ada, sehingga ruang pengguna terus menulis ke
			 * halaman kernel yang telah dilepas lalu dialokasikan kembali. */
			folio_put(virt_to_folio(bl->buf_ring));
			bl->buf_ring = NULL;
			bl->is_mmap = 0;
		}
	}
	/* ... */
}
PERBAIKAN AMAN & KUAT
/* io_uring/kbuf.c setelah commit c392cbecd8ec (Linux 6.6.5 / 6.7) */
struct io_buf_free {
	struct hlist_node		list;
	void				*mem;
};

static int io_alloc_pbuf_ring(struct io_ring_ctx *ctx,
			      struct io_uring_buf_reg *reg,
			      struct io_buffer_list *bl)
{
	struct io_buf_free *ibf;
	size_t ring_size;
	void *ptr;

	ring_size = reg->ring_entries * sizeof(struct io_uring_buf_ring);
	ptr = io_mem_alloc(ring_size);
	if (!ptr)
		return -ENOMEM;

	/* FIX: catat setiap ring yang dapat di-mmap agar halamannya hanya dilepas saat
	 * ->release(), setelah pemetaan hilang, bukan selama teardown. */
	ibf = kmalloc(sizeof(*ibf), GFP_KERNEL_ACCOUNT);
	if (!ibf) {
		io_mem_free(ptr);
		return -ENOMEM;
	}
	ibf->mem = ptr;
	hlist_add_head(&ibf->list, &ctx->io_buf_list);

	bl->buf_ring = ptr;
	bl->is_mapped = 1;
	bl->is_mmap = 1;
	return 0;
}

static int __io_remove_buffers(struct io_ring_ctx *ctx,
			       struct io_buffer_list *bl, unsigned nbufs)
{
	if (bl->is_mapped) {
		i = bl->buf_ring->tail - bl->head;
		if (bl->is_mmap) {
			/* io_kbuf_mmap_list_free() melepas halaman pada ->release(),
			 * jadi tidak ada halaman yang dilepas di sini selama masih mungkin dipetakan. */
			bl->buf_ring = NULL;
			bl->is_mmap = 0;
		}
	}
	/* ... */
}

/* Dipanggil saat atau setelah ->release(): ring yang di-mmap kini aman untuk dilepas. */
void io_kbuf_mmap_list_free(struct io_ring_ctx *ctx)
{
	struct io_buf_free *ibf;
	struct hlist_node *tmp;

	hlist_for_each_entry_safe(ibf, tmp, &ctx->io_buf_list, list) {
		hlist_del(&ibf->list);
		io_mem_free(ibf->mem);
		kfree(ibf);
	}
}

Daftar Periksa Penguatan Sistem Rekayasa

Sumber