
Netfilter nf_tablesにおけるバッチリクエスト処理時のUse-After-Free CVE-2023-32233

影響を受けるコードは、https://kernel.org/ の公式 Linux カーネルに由来し、Netfilter nf_tables コンポーネント (net/netfilter/nf_tables_api.c) の一部です。
Netfilter nf_tables は、その構成を原子操作として更新することを可能にします。この機能を使用する場合、ユーザーモードのクライアントは基本操作のリストを含むバッチリクエストを送信します。Netfilter nf_tables は、バッチ内のすべての操作を単一のトランザクションとして処理します。バッチを処理するとき、Netfilter nf_tables は構成状態の更新をチェックして、後続の各基本操作が有効であることを確認し、これにはバッチ内の以前のすべての操作による状態更新も考慮されます。しかし、現在実装されているチェックは不十分です。
私たちの具体的なシナリオでは、匿名の nft_set 上に lookup 式を持つ nft_rule があり、その匿名の nft_set がいくつかの要素を含む Netfilter nf_tables 構成から始めます。次に、以下の2つの基本操作を含むバッチリクエストを送信します。
nft_rule を削除するための NFT_MSG_DELRULE 操作。lookup 式と匿名の nft_set も暗黙的に削除されることに注意してください。nft_set の任意の要素を削除するための NFT_MSG_DELSETELEM 操作。現在のバージョンの Netfilter nf_tables は、上記のバッチリクエストを受け入れます。次に、解放されたリソースを nf_tables_destroy_list に追加する nf_tables_commit_release() を呼び出します。nf_tables_destroy_list は、nf_tables_trans_destroy_work() によって処理され、最初に NFT_MSG_DELRULE 操作に関連するリソースを次のように呼び出して解放します:
nft_commit_release()
nf_tables_rule_destroy()
nf_tables_expr_destroy()
expr->ops->destroy() that points to nft_lookup_destroy()
nf_tables_destroy_set()
nft_set_destroy()
kvfree() that deallocates memory used by `nft_set`
NFT_MSG_DELSETELEM 操作を処理する前に、次の呼び出し中に nft_trans_elem_set() を介して解放された nft_set への参照がアクセスされます:
nft_commit_release()
nf_tables_set_elem_destroy()
nft_set_elem_ext()
上記の nft_set_elem_ext() 内で、解放された nft_set のメモリ位置にアクセスして、nft_set_ext の位置を決定します:
static inline struct nft_set_ext *nft_set_elem_ext(const struct nft_set *set,
void *elem)
{
return elem + set->ops->elemsize;
}
後続の操作のために。したがって、set->ops->elemsize の値が破損すると、予期しないメモリ位置が破棄される nft_expr のリストとして解釈される可能性があります:
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));
上記の脆弱性を悪用するには、Linux カーネルのバックグラウンドワーカースレッドから実行される nf_tables_trans_destroy_work() との競合に勝つ必要があります。これは、カーネルのスラブアロケータのハードニング、Kernel Address Space Layout Randomization (KASLR)、特に Control-Flow Integrity などの既存の緩和策を考慮する前から、実際の悪用を複雑にすると思われます。しかし、添付の PoC は、実際には合理的に信頼できる悪用が依然として可能であることを証明しています。
脆弱性を悪用するには、nf_tables_rule_destroy() によって解放された後、nf_tables_set_elem_destroy() によって使用される前に、nft_set のメモリ内容を変更する必要があります。nf_tables_rule_destroy() と nf_tables_set_elem_destroy() はどちらも、Linux カーネルのバックグラウンドワーカースレッドから実行される nf_tables_trans_destroy_work() の単一の呼び出し内で呼び出されます。さらに、解放されたメモリチャンクは、通常、同じ CPU コアからのみ再利用可能です。
nf_tables_trans_destroy_work() との競合では、バックグラウンドワーカースレッドが nf_tables_rule_destroy() と nf_tables_set_elem_destroy() を呼び出す間に制御された遅延を追加することで、可能性を高めます。そのために、多数の要素を含む別の nft_set を破棄する追加の操作を挿入します。さらに、他のすべての CPU コアをビジー状態に保ち、バックグラウンドワーカースレッドが特定の CPU コアにスケジュールされる可能性を高めます。これにより、nf_tables_rule_destroy() の下で nft_set を解放した直後に、同じ CPU コアから新しい構造を割り当てようと試みることができます。私たちの目標は、nf_tables_rule_destroy() によって解放された nft_set のメモリ位置を再利用するために、異なるタイプの新しい nft_set を割り当てることです。
新しい nft_set タイプは、set->ops->elemsize に異なる値を使用するように選択されます。したがって、バックグラウンドワーカースレッドが最終的に NFT_MSG_DELSETELEM 操作を処理するために nf_tables_set_elem_destroy() を呼び出すと、その elem 引数が誤って解釈され、破損した nft_set_ext *ext が正しい位置より数バイト後ろになります。これは、元の nft_set_ext の特定のユーザー制御データフィールドがヘッダーとして解釈され、型混乱が生じることを意味します。
この型混乱を悪用する1つの方法は、破損した nft_set_ext ヘッダーをオフセット値で作成し、nf_tables_set_elem_destroy() が隣接するメモリブロックの内容を破棄する nft_expr のリストとして解釈するようにすることです。これは、次の呼び出しによって行われます:
nft_set_elem_expr_destroy()
__nft_set_elem_expr_destroy()
nf_tables_expr_destroy()
expr->ops->destroy()
この悪用の時点では、カーネルのメモリレイアウトの詳細はまだわかりません。したがって、絶対ポインタアドレスを作成することはできません。しかし、破損した nft_set_ext ヘッダーを作成する際に、範囲外のオフセットを使用して、隣接するメモリチャンク内の特定の有効な nft_expr に対して expr->ops->destroy() を呼び出させることは可能です。
このために、制御された NFTA_LOG_PREFIX を持つ nft_log 式をスプレーします。その nft_log->prefix は、expr->ops->destroy() が呼び出されると、nft_log_destroy() によって解放されます:
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);
スプレーされた nft_log 式からの他の参照を介して、このメモリにアクセスしたり、再度解放したりできることに注意してください。
さらに、nft_log->prefix のサイズも制御できるため、kmalloc-{8, ..., 192} の任意のスラブから割り当てることができます。最後に、この参照先メモリはカーネルによって文字列として解釈されるため、その上に異なるオブジェクトを重ねても破損を心配する必要はありません。これは事実上ゲームオーバーです。
1つの不便な点は、NULL 文字が nft_log->prefix を終了させるため、メモリ内容をリークする際に NULL バイトを超えて読み取ることができないことです。これは次のステップで対処されます。そこでは、nft_log->prefix メモリチャンクを再利用するために nft_object->udata を割り当て、nft_log 式を破棄します。これにより nft_object->udata メモリが解放されますが、NULL バイトの制限なしに nft_object->udata のダングリングポインタを使用してメモリ内容をリークすることができます。
次のステップに適した構造を探した結果、nft_dynset_new() から割り当てられる nft_expr を選択しました。これらは nft_log->prefix や nft_object->udata と同じスラブに存在します。また、割り当てサイズをある程度制御できるため、必要に応じて後で異なるサイズのスラブ間で簡単に切り替えることができます。
これらの構造を使用するために、nft_dynset 式を持つパケットフィルタを作成します。ループバックインターフェースを介してパケットを送信すると、nft_dynset 式は nft_dynset_new() を呼び出して、関連する nft_set の新しい要素を作成します。作成された要素は、次のタイプのステートフル式です:
カーネルメモリ内の nf_tables.ko の位置を取得するための nft_counter。
この構造には、nf_tables.ko カーネルモジュール内の nft_counter_ops へのポインタが含まれています。nft_object->udata を読み取ることで、このポインタをリークします。
任意のメモリ読み取りと書き込みのための nft_quota。
nft_object->udata を繰り返し解放および再割り当てして、nft_quota->consumed ポインタを変更できます。次に、NFT_MSG_GETSETELEM 操作を実行して、nft_quota_do_dump() を呼び出し、参照されたメモリの内容を読み取り、結果を NFTA_QUOTA_CONSUMED 属性として結果に渡します。書き込みについては、ループバックインターフェースを介してパケットを送信するだけで、nft_quota_do_eval() が次のように呼び出します:
static inline bool nft_overquota(struct nft_quota *priv,
const struct sk_buff *skb)
{
return atomic64_add_return(skb->len, priv->consumed) >=
上記の任意のメモリ読み取りを使用して、カーネルコアのベースアドレスを取得します。次に、"/sbin/modprobe" パス名の "sbin" 部分文字列を変更して "/tmp" に置き換えます。結果のパス名 "//tmp/modprobe" は、カーネルがファイル内容を制御できる root 権限でプロセスを開始するために使用されます。
Control-Flow Integrity をバイパスするための意図的な努力はしていないことに注意してください。ただし、悪用の各ステップについて、最も柔軟で最も堅牢なプリミティブを意識的に選択しました。結果として、私たちの選択は、Control-Flow Integrity によってブロックされる可能性のあるプリミティブを何らかの形で回避していました。現在、結果として得られるエクスプロイトが Control-Flow Integrity 緩和策を備えたシステムに対して実際に機能するかどうかをテストで確認することに関心があります。