
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.