
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_MSG_DELRULE操作:nft_ruleを削除します。lookup式と匿名nft_setも暗黙的に削除されることに注意してください。NFT_MSG_DELSETELEM操作:削除された匿名nft_setのいずれかの要素を削除します。現在のバージョンのNetfilter nf_tablesは、上記のバッチリクエストを受け入れます。次に、nf_tables_commit_release()を呼び出し、解放されたリソースをnf_tables_destroy_listに追加します。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() は nft_lookup_destroy() を指す
nf_tables_destroy_set()
nft_set_destroy()
kvfree() で `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()との競合に勝つ必要があります。これは、既存の緩和策(カーネルスラブアロケーターの強化、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()の1回の呼び出し内で呼び出されます。さらに、解放されたメモリチャンクは通常、同じ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コアから新しい構造体を割り当てようと試みます。目標は、異なるタイプの新しいnft_setを割り当て、nf_tables_rule_destroy()で解放されたnft_setのメモリ位置を再利用することです。
新しいnft_setのタイプは、set->ops->elemsizeに異なる値を使用するように選択されます。これにより、バックグラウンドワーカースレッドが最終的にnf_tables_set_elem_destroy()を呼び出してNFT_MSG_DELSETELEM操作を処理する際に、その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_object->udataを割り当ててnft_log->prefixのメモリチャンクを再利用し、nft_log式を破棄します。これによりnft_object->udataメモリが解放されますが、今度はnft_object->udataのダングリングポインタを使用して、NULLバイトの制限なしにメモリ内容を漏洩させることができます。
次のステップに適した構造体を探した結果、nft_dynset_new()から割り当てられるnft_exprを選択しました。これらはnft_log->prefixやnft_object->udataと同じスラブに存在します。また、割り当てサイズを適切に制御できるため、必要に応じて後で異なるサイズのスラブ間を簡単に切り替えることができます。
これらの構造体を使用するために、nft_dynset式を含むパケットフィルターを作成します。そして、ループバックインターフェースを介してパケットを送信すると、nft_dynset式はnft_dynset_new()を呼び出し、関連するnft_setの新しい要素を作成します。作成される要素は、以下のタイプのステートフル式です。
nft_counter:カーネルメモリ内のnf_tables.koの位置を取得するため。
この構造体には、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緩和策を備えたシステムに対して実際に機能するかどうかをテストで確認することに興味があります。
これによりnft_quota->consumedが変更されます。