
CVE-2023-32233
Der betroffene Code stammt vom offiziellen Linux-Kernel von https://kernel.org/ und ist Teil der Netfilter nf_tables-Komponente (net/netfilter/nf_tables_api.c).
Netfilter nf_tables erlaubt es, seine Konfiguration als atomare Operation zu aktualisieren. Bei Verwendung dieser Funktion senden die Benutzermodus-Clients Batch-Anfragen, die eine Liste von Basisoperationen enthalten. Netfilter nf_tables verarbeitet dann alle Operationen innerhalb des Batches als einzelne Transaktion. Bei der Verarbeitung des Batches prüft Netfilter nf_tables die Konfigurationszustandsänderungen, um sicherzustellen, dass jede aufeinanderfolgende Basisoperation gültig ist, und dies berücksichtigt auch die Zustandsänderungen aller vorherigen Operationen innerhalb des Batches. Allerdings ist die derzeit implementierte Prüfung unzureichend.
In unserem spezifischen Szenario starten wir mit einer Netfilter nf_tables-Konfiguration,
die eine nft_rule mit lookup-Ausdruck auf einem anonymen nft_set hat,
und wobei das anonyme nft_set einige Elemente enthält. Als nächstes senden wir eine Batch-Anfrage,
die die folgenden zwei Basisoperationen enthält:
NFT_MSG_DELRULE-Operation zum Löschen der nft_rule.lookup-Ausdruck und das
anonyme nft_set löscht.NFT_MSG_DELSETELEM-Operation zum Löschen eines der Elemente des
gelöschten anonymen nft_set.Die aktuelle Version von Netfilter nf_tables akzeptiert die obige Batch-Anfrage.
Sie ruft dann nf_tables_commit_release() auf, das freigegebene Ressourcen an
nf_tables_destroy_list anhängt. Die nf_tables_destroy_list wird dann von
nf_tables_trans_destroy_work() verarbeitet, das zunächst Ressourcen freigibt, die mit der
NFT_MSG_DELRULE-Operation zusammenhängen, indem es aufruft:
nft_commit_release()
nf_tables_rule_destroy()
nf_tables_expr_destroy()
expr->ops->destroy() zeigt auf nft_lookup_destroy()
nf_tables_destroy_set()
nft_set_destroy()
kvfree() gibt den von `nft_set` verwendeten Speicher frei
bevor die NFT_MSG_DELSETELEM-Operation verarbeitet wird, bei der eine Referenz auf das
freigegebene nft_set über nft_trans_elem_set() während der folgenden
Aufrufe zugegriffen wird:
nft_commit_release()
nf_tables_set_elem_destroy()
nft_set_elem_ext()
Innerhalb von nft_set_elem_ext() oben wird auf die Speicherstelle des freigegebenen
nft_set zugegriffen, um die Position von nft_set_ext zu bestimmen:
static inline struct nft_set_ext *nft_set_elem_ext(const struct nft_set *set,
void *elem)
{
return elem + set->ops->elemsize;
}
für die folgenden Operationen. Sobald also der Wert von set->ops->elemsize
korrumpiert wird, könnte eine bestimmte unerwartete Speicherstelle als
Liste von zu zerstörenden nft_expr interpretiert werden:
static void nf_tables_set_elem_destroy(const struct nft_ctx *ctx,
const struct nft_set *set, void *elem)
{
struct nft_set_ext *ext = nft_set_elem_ext(set, elem);
if (nft_set_ext_exists(ext, NFT_SET_EXT_EXPRESSIONS))
nft_set_elem_expr_destroy(ctx, nft_set_ext_expr(ext));
Die Ausnutzung der obigen Schwachstelle erfordert, ein Rennen mit nf_tables_trans_destroy_work() zu gewinnen, das im Hintergrund-Worker-Thread des Linux-Kernels ausgeführt wird. Dies scheint die praktische Ausnutzung selbst dann zu erschweren, wenn wir bestehende Schutzmaßnahmen berücksichtigen, wie die Härtung des Kernel-Slab-Allokators, Kernel Address Space Layout Randomization (KASLR) und insbesondere Control-Flow Integrity. Der beigefügte PoC beweist jedoch, dass es dennoch möglich ist, eine einigermaßen zuverlässige Ausnutzung in der Praxis zu erreichen.
Um die Schwachstelle auszunutzen, müssen wir den Inhalt des Speichers
von nft_set ändern, nachdem es unter nf_tables_rule_destroy() freigegeben wurde, aber
bevor es unter nf_tables_set_elem_destroy() verwendet wird. Beide
nf_tables_rule_destroy() und nf_tables_set_elem_destroy() werden innerhalb eines
einzelnen Aufrufs von nf_tables_trans_destroy_work() aufgerufen, das im
Hintergrund-Worker-Thread des Linux-Kernels ausgeführt wird. Darüber hinaus ist der freigegebene
Speicherblock normalerweise nur vom selben CPU-Kern aus zur Wiederverwendung verfügbar.
Wenn wir mit nf_tables_trans_destroy_work() um die Wette laufen, verbessern wir unsere Chancen, indem wir
eine kontrollierte Verzögerung für den Hintergrund-Worker-Thread zwischen seinen Aufrufen
nf_tables_rule_destroy() und nf_tables_set_elem_destroy() hinzufügen. Dazu fügen wir
eine zusätzliche Operation ein, um ein weiteres nft_set mit einer großen Anzahl von
Elementen zu zerstören. Zusätzlich halten wir alle anderen CPU-Kerne beschäftigt, sodass
der Hintergrund-Worker-Thread wahrscheinlich auf einem bestimmten CPU-Kern
geplant wird, sodass wir versuchen können, eine neue Struktur vom selben CPU-Kern
zu allozieren, direkt nachdem er nft_set unter nf_tables_rule_destroy() freigegeben hat. Unser
Ziel ist es, ein neues nft_set eines anderen Typs zu allozieren, um die Speicherstelle des
unter nf_tables_rule_destroy() freigegebenen nft_set wiederzuverwenden.
Der neue nft_set-Typ wird so gewählt, dass er einen anderen Wert für
set->ops->elemsize verwendet. Wenn der Hintergrund-Worker-Thread schließlich
nf_tables_set_elem_destroy() aufruft, um die NFT_MSG_DELSETELEM-Operation zu verarbeiten, interpretiert
er sein elem-Argument falsch, sodass das korrumpierte
nft_set_ext *ext einige Bytes nach der korrekten Position liegt. Dies bedeutet, dass
bestimmte benutzergesteuerte Datenfelder des ursprünglichen nft_set_ext nun als
Header interpretiert werden, was zu einer Typverwirrung führt.
Eine Möglichkeit, diese Typverwirrung auszunutzen, besteht darin, die korrumpierten
nft_set_ext-Header mit Offsetwerten so zu präparieren, dass
nf_tables_set_elem_destroy() den Inhalt beliebiger benachbarter Speicherblöcke
als die Liste der zu zerstörenden nft_expr interpretiert, und zwar über die folgenden Aufrufe:
nft_set_elem_expr_destroy()
__nft_set_elem_expr_destroy()
nf_tables_expr_destroy()
expr->ops->destroy()
An diesem Punkt der Ausnutzung haben wir noch keine Details zum Kernel-Speicherlayout.
Daher ist es nicht möglich, absolute Zeigeradressen zu präparieren.
Bei der Präparierung der korrumpierten nft_set_ext-Header können wir jedoch immer noch
außerhalb des gültigen Bereichs liegende Offsets verwenden, sodass expr->ops->destroy() auf
bestimmten gültigen nft_expr in den benachbarten Speicherblöcken aufgerufen wird.
Dazu spammen wir nft_log-Ausdrücke mit kontrolliertem NFTA_LOG_PREFIX.
Dieses nft_log->prefix wird dann durch nft_log_destroy() freigegeben, sobald
expr->ops->destroy() aufgerufen wird:
static void nft_log_destroy(const struct nft_ctx *ctx,
const struct nft_expr *expr)
{
struct nft_log *priv = nft_expr_priv(expr);
struct nf_loginfo *li = &priv->loginfo;
if (priv->prefix != nft_log_null_prefix)
kfree(priv->prefix);
Beachten Sie, dass wir auf diesen Speicher über die andere Referenz des gespammten
nft_log-Ausdrucks immer noch zugreifen und ihn sogar erneut freigeben können.
Darüber hinaus können wir auch die Größe von nft_log->prefix steuern, sodass
er aus einem der Slabs kmalloc-{8, ..., 192} alloziert werden kann. Schließlich wird der
referenzierte Speicher vom Kernel als Zeichenkette interpretiert, sodass
wir uns keine Gedanken über Korruption machen müssen, wenn wir verschiedene Objekte darüber legen.
Dies ist im Wesentlichen "Game Over".
Ein Nachteil ist, dass NULL-Zeichen nft_log->prefix terminieren, sodass
wir beim Auslesen von Speicherinhalten nicht über NULL-Bytes hinauslesen können. Dies wird
im nächsten Schritt adressiert, bei dem wir nft_object->udata allozieren, um den
nft_log->prefix-Speicherblock wiederzuverwenden und den nft_log-Ausdruck zu zerstören. Dies
gibt den nft_object->udata-Speicher frei, aber jetzt können wir den
hängenden Zeiger nft_object->udata immer noch verwenden, um Speicherinhalte ohne
Einschränkungen bezüglich NULL-Bytes auszulesen.
Auf der Suche nach geeigneten Strukturen für die folgenden Schritte haben wir uns für
nft_expr entschieden, das aus nft_dynset_new() alloziert wird. Diese leben in denselben Slabs wie
nft_log->prefix und nft_object->udata. Außerdem haben wir eine angemessene
Kontrolle über die Allokationsgröße, sodass wir später bei Bedarf problemlos zwischen
Slabs unterschiedlicher Größe wechseln könnten.
Um diese Strukturen zu verwenden, erstellen wir einen Paketfilter mit nft_dynset-Ausdruck.
Und wenn wir Pakete über die Loopback-Schnittstelle senden, ruft der
nft_dynset-Ausdruck nft_dynset_new() auf, um neue Elemente für das
zugehörige nft_set zu erstellen. Die erstellten Elemente sind zustandsbehaftete Ausdrücke der
folgenden Typen:
nft_counter, um die Adresse von nf_tables.ko im Kernel-Speicher zu erhalten.
Die Struktur enthält einen Zeiger auf nft_counter_ops im Kernel-Modul
nf_tables.ko. Wir leaken diesen Zeiger, indem wir nft_object->udata lesen.
nft_quota für beliebiges Speicherlesen und -schreiben.
Wir können nft_object->udata wiederholt freigeben und neu allozieren, um den
nft_quota->consumed-Zeiger zu modifizieren. Als nächstes führen wir eine
NFT_MSG_GETSETELEM-Operation durch, die nft_quota_do_dump() aufruft, um den Inhalt des
referenzierten Speichers zu lesen und das Ergebnis als NFTA_QUOTA_CONSUMED-Attribut
im Ergebnis zurückzugeben. Für das Schreiben senden wir einfach Pakete über die
Loopback-Schnittstelle, wobei nft_quota_do_eval() aufruft:
static inline bool nft_overquota(struct nft_quota *priv,
const struct sk_buff *skb)
{
return atomic64_add_return(skb->len, priv->consumed) >=
Wir verwenden das obige beliebige Speicherlesen, um die Basisadresse des Kernel-Kerns zu erhalten. Und dann fahren wir fort, die Teilzeichenkette "sbin" des Pfadnamens "/sbin/modprobe" zu modifizieren, sodass sie durch "/tmp" ersetzt wird. Der resultierende Pfadname "//tmp/modprobe" wird dann vom Kernel verwendet, um einen Prozess mit Root-Rechten zu starten, wobei wir den Dateiinhalt kontrollieren.
Beachten Sie, dass wir keine bewussten Anstrengungen unternommen haben, um die Control-Flow Integrity zu umgehen. Allerdings haben wir für jeden der Ausbeutungsschritte bewusst die flexibelsten und robustesten Primitive gewählt. Es stellte sich heraus, dass unsere Auswahl irgendwie alle Primitive vermied, die möglicherweise durch Control-Flow Integrity blockiert werden könnten. Wir sind nun gespannt, durch Tests zu bestätigen, ob der resultierende Exploit wirklich gegen Systeme mit Control-Flow-Integrity-Schutzmaßnahmen funktioniert.
um nft_quota->consumed zu modifizieren.