FlawOpen

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

● CVE-2024-0582 · CVSS 7.8 · Hoch
Sicherheitsforschung · FlawOpen

CVE-2024-0582: Use-after-free des gemappten Buffer-Rings von io_uring in Linux

CVE-2024-0582 (CVSS 7.8, hoch): Ein mit IORING_REGISTER_PBUF_RING und IOU_PBUF_RING_MMAP registrierter Buffer-Ring wurde beim Abbau freigegeben, während der Userspace ihn noch per mmap() gemappt hatte. Zurück blieb eine Seite, die der Angreifer lesen und schreiben konnte, nachdem der Kernel sie wiederverwendet hatte – eine lokale Rechteausweitung.

💡 Einfache Erklärung (ELI5)

Eine Bibliothek lässt dich einen besonderen Leseraum reservieren und gibt dir einen Schlüssel zum Kommen und Gehen. Meldest du den Raum ab, gibt ihn der Bibliothekar sofort zurück in den Pool, nimmt dir aber nie den Schlüssel ab. Dann vermietet die Bibliothek genau diesen Raum an jemand anderen, und du kannst mit deinem alten Schlüssel weiterhin hinein und überschreiben, was er auf dem Tisch liegen ließ. Die Lösung: jeden solchen Raum reserviert halten und nur einmal freigeben, beim Schließen, nachdem alle Schlüssel zurückgegeben sind.

Kernkonzepte & Begriffe

io_uring
Die asynchrone I/O-Schnittstelle von Linux. Anwendungen und Kernel teilen sich Submission- und Completion-Ringe und sparen so einen Syscall pro Operation.
Bereitgestellter Buffer-Ring (IORING_REGISTER_PBUF_RING)
Ein Ring von Puffern, den die Anwendung registriert, damit der Kernel je Operation einen auswählt. Mit IOU_PBUF_RING_MMAP alloziert der Kernel den Ring und die Anwendung mappt ihn per mmap(), statt eigenen Speicher zu liefern.
Use-after-free (UAF)
Speicher nach seiner Freigabe nutzen. Sobald der Kernel diesen Speicher einem anderen Objekt zuteilt, kann die veraltete Referenz es lesen oder beschädigen.
remap_pfn_range / folio_put
Der Ring wird per remap_pfn_range() in den Userspace gemappt, daher gibt ihn die normale munmap-Buchführung nicht frei. Der Aufruf von folio_put() beim Abbau gab die Seite frei, während das Mapping noch bestand.

Framework-Zuordnungen

  • CWE-Referenz für die in diesem Leitfaden beschriebene Schwachstelle: CWE-416

Ursachenanalyse

Für einen mit IOU_PBUF_RING_MMAP erzeugten Buffer-Ring allozierte io_alloc_pbuf_ring() die Seiten, und der Userspace mappte sie per mmap() über remap_pfn_range(). Da dieser Pfad die normale munmap/release-Buchführung umgeht, gab __io_remove_buffers() mit dem Aufruf von folio_put() die Seiten frei, obwohl noch ein User-Mapping bestehen konnte – ein Use-after-free (CWE-416): Der Userspace behielt ein gültiges Mapping auf eine Seite, die der Kernel dem Allocator zurückgegeben hatte und neu vergeben konnte. Jann Horn meldete den Fehler. Commit c392cbecd8ec vermerkt jeden per mmap allozierten Ring in einer neuen io_buf_list und gibt ihn erst in io_kbuf_mmap_list_free() beim ->release() frei, nachdem das Mapping weg ist.

Schritt-für-Schritt Angriffsablauf

Schritt 1

Einen mmap-gestützten Ring registrieren

Ein unprivilegierter Prozess ruft io_uring_register() mit IORING_REGISTER_PBUF_RING und dem Flag IOU_PBUF_RING_MMAP auf, sodass der Kernel den Buffer-Ring alloziert.

Schritt 2

In den Userspace mappen

Der Prozess mappt den Ring per mmap(). Der Kernel mappt die Seiten mit remap_pfn_range(), und der Prozess behält das Mapping.

Schritt 3

Den noch gemappten Ring freigeben

Das Deregistrieren der Puffer (oder der Abbau des Rings) erreicht __io_remove_buffers(), dessen folio_put() die Seiten freigibt, obwohl das User-Mapping noch besteht.

Schritt 4

Über das veraltete Mapping schreiben

Der Kernel vergibt die freigegebene Seite an ein anderes Objekt; der Prozess mit noch bestehendem Mapping liest und überschreibt sie, um Kernel-Zustand zu korrumpieren und root zu werden.

Quellcode: Verwundbar vs. Sicher

VERWUNDBARE IMPLEMENTIERUNG
/* io_uring/kbuf.c vor der Korrektur (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) {
			/* FEHLER: Der Ring wurde über mmap() und
			 * remap_pfn_range() an den Userspace übergeben. folio_put() gibt hier die Seite frei, obwohl noch
			 * eine Zuordnung bestehen kann; der Userspace schreibt dadurch weiterhin in eine
			 * freigegebene und erneut zugewiesene Kernel-Seite. */
			folio_put(virt_to_folio(bl->buf_ring));
			bl->buf_ring = NULL;
			bl->is_mmap = 0;
		}
	}
	/* ... */
}
GEHÄRTETER SICHERHEITS-PATCH
/* io_uring/kbuf.c nach 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;

	/* BEHEBUNG: Jeden mmap-fähigen Ring speichern, damit seine Seiten erst bei
	 * ->release() freigegeben werden, nachdem die Zuordnung entfernt ist, niemals beim Abbau. */
	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() gibt die Seite(n) bei ->release() frei,
			 * damit hier nichts freigegeben wird, solange es noch zugeordnet sein könnte. */
			bl->buf_ring = NULL;
			bl->is_mmap = 0;
		}
	}
	/* ... */
}

/* Wird bei oder nach ->release() aufgerufen: Die mmap-Ringe können jetzt sicher freigegeben werden. */
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);
	}
}

Checkliste für Engineering & Systemsicherheit

Quellen