Upgrade to Pro — share decks privately, control downloads, hide ads and more …

高橋メソッドでCVE解説シリーズ 〜CVE-2026-98432〜

Avatar for Hakkadaikon Hakkadaikon
November 01, 2026
21

高橋メソッドでCVE解説シリーズ 〜CVE-2026-98432〜

高橋メソッドでCVEを解説する。(CVE-2026-98432)
claude opus5.5で作成。情報源は1次情報や実際のカーネルのソースコード。

Avatar for Hakkadaikon

Hakkadaikon

November 01, 2026

More Decks by Hakkadaikon

Transcript

  1. 深刻度の評価(2026-10-29 時点) 評価元 kernel.org CNA Ubuntu Oracle Amazon Linux Red

    Hat スコア 9.8 Critical Priority High 8.1 7.0 5.9 Moderate 備考 CVSS 3.1 AV:N/AC:L/PR:N/UI:N CVSS 4.0 では 8.3 主な影響はクラッシュと評価 差の主因は「レースに勝つ難しさ」と「任意コード実行まで行けるか」の見積もり EPSS は 1% 未満、CISA KEV には未掲載(悪用の報告は今のところ無い) 出典: cve.org / NVD / ubuntu.com/security / access.redhat.com / linux.oracle.com / alas.aws.amazon.com / OpenCVE
  2. ① おさらい: 3-way handshake クライアント サーバ(Linux) SYN 「接続待ち」を 1 個つくる

    SYN-ACK ACK 接続が完成 → accept() で取れる ここまでは TCP/IP の教科書どおり。カーネルが裏で何を作っているかを次に見る
  3. request sock と子ソケット listen ソケット SYN request sock (SYN で作る)

    ACK 子ソケット (ACK で作る) request sock = 「SYN は来た、まだ ACK は来ていない」相手のメモ 子ソケット = 普段 fd で触っているソケットの実体 今回の主役はこの 2 つが同じメモリを指す瞬間 アプリ accept()
  4. ② IP オプションとは 0 1 2 3 +-------+-------+---------------+-------------------------------+ |Version| IHL

    | TOS | Total Length | +-------+-------+---------------+-------------------------------+ | ...( 20 )... | +---------------------------------------------------------------+ | Options(0 40 ) ← | +---------------------------------------------------------------+ 固定部分 バイト 〜 バイト ここ IHL(ヘッダ長)が 5 より大きければオプション付き 例: Record Route(通ったルータのアドレスを記録) 例: Timestamp(通過時刻を記録) 普段のインターネットではまず使われない。でも誰でも付けて送れる
  5. カーネル内での IP オプションの形 /* include/net/inet_sock.h */ struct ip_options { __be32

    faddr; __be32 nexthop; unsigned char optlen; /* unsigned char srr; /* unsigned char rr; unsigned char ts; /* ... ... */ unsigned char __data[]; /* }; フラグ類 struct ip_options_rcu { struct rcu_head rcu; struct ip_options opt; }; optlen オプションの長さ */ 各オプションの位置(オフセット) */ オプションのバイト列そのもの */ /* ← 後で効いてくる */ と rr ・ ts はあとで「長さ」と「書き込み位置」として使われるのがポイント Ubuntu 24.04 (noble) カーネル 6.8 系 include/net/inet_sock.h:39-61
  6. ④ use-after-free を C で書くと struct opt *p = malloc(sizeof(*p));

    p->len = 8; struct opt *q = p; /* 同じメモリを指すポインタがもう 1 本 */ さんが捨てた */ char *x = malloc(sizeof(*p)); /* 別の誰かが同じ場所を再利用 */ free(p); /* A memset(x, 0xff, sizeof(*p)); memcpy(buf, q->data, q->len); /* B さんはまだ使っている → len は 255 */ 解放済みのメモリはすぐ別の用途に再利用される 古いポインタで読むと、他人のデータを「長さ」や「位置」として信じてしまう
  7. RCU のルール 読む人 /* */ rcu_read_lock(); p = rcu_dereference(ptr); use(p);

    rcu_read_unlock(); /* ロックは取らない。速い */ /* /* まず見えなくする */ 読んでいる人が全員抜けてから free */ 捨てる人 /* */ old = ptr; ptr = NULL; kfree_rcu(old, rcu); 読み手はロックを取らないので、書き手は「いつ読み終わるか」を知らない だから free は猶予期間(grace period)の後に回す。これが kfree_rcu() 図書館の本: 誰かが閲覧室で読んでいる間は廃棄しない
  8. 場面 1: SYN 受信 → オプションを保存 static void tcp_v4_init_req(struct request_sock

    *req, const struct sock *sk_listener, struct sk_buff *skb) { struct inet_request_sock *ireq = inet_rsk(req); /* ... */ RCU_INIT_POINTER(ireq->ireq_opt, tcp_v4_save_options(net, skb)); } が SYN の IP オプションをkmalloc した領域にコピー そのポインタを request sock の ireq_opt に入れる 攻撃者が送った Record Route / Timestamp がここに入る tcp_v4_save_options() Ubuntu noble 6.8 系 net/ipv4/tcp_ipv4.c:1664-1674
  9. 場面 2: SYN-ACK 送信 → オプションを RCU で読む rcu_read_lock(); err

    = ip_build_and_send_pkt(skb, sk, ireq->ir_loc_addr, ireq->ir_rmt_addr, rcu_dereference(ireq->ireq_opt), tos); rcu_read_unlock(); の中 ロックではなくRCU だけで ireq_opt を読む 読んだポインタ opt を、IP ヘッダを組み立てる関数へ渡す tcp_v4_send_synack() Ubuntu noble 6.8 系 net/ipv4/tcp_ipv4.c:1204-1209
  10. 場面 2 の続き: optlen を何度も信じる /* ip_build_and_send_pkt() */ /* */

    skb_push(skb, sizeof(struct iphdr) + (opt ? opt->opt.optlen : 0)); iph = ip_hdr(skb); iph->ihl = 5; /* ... */ if (opt && opt->opt.optlen) { iph->ihl += opt->opt.optlen >> 2; ip_options_build(skb, &opt->opt, daddr, rt); } ヘッダ分の場所をパケット先頭に確保 opt->opt.optlen を2 回以上読み、そのたびに値を信じている Ubuntu noble 6.8 系 net/ipv4/ip_output.c:149-186
  11. 場面 2 の続き: ip_options_build() より抜粋 /* ip_options_build() */ memcpy(iph +

    sizeof(struct iphdr), opt->__data, opt->optlen); /* ... */ if (opt->rr_needaddr) ip_rt_get_source(iph + opt->rr + iph[opt->rr + 2] - 5, skb, rt); if (opt->ts_needaddr) ip_rt_get_source(iph + opt->ts + iph[opt->ts + 2] - 9, skb, rt); if (opt->ts_needtime) { /* ... midtime = ... */ memcpy(iph + opt->ts + iph[opt->ts + 2] - 5, &midtime, 4); } 現在時刻 バイトをそのまま SYN-ACK にコピー rr ・ ts の位置にアドレスや時刻を書き込む optlen Ubuntu noble 6.8 系 net/ipv4/ip_options.c:44-66
  12. 場面 3: ACK 受信 → 子ソケットにポインタを渡す inet_opt = rcu_dereference(ireq->ireq_opt); RCU_INIT_POINTER(newinet->inet_opt,

    inet_opt); /* ... ... */ *own_req = inet_ehash_nolisten(newsk, req_to_sk(req_unhash), &found_dup_sk); if (likely(*own_req)) { ireq->ireq_opt = NULL; } 子ソケットの初期化 の中 コピーせず、同じポインタを子ソケットの inet_opt に渡す(所有権の移動) request sock 側を NULL にするのはその後 tcp_v4_syn_recv_sock() Ubuntu noble 6.8 系 net/ipv4/tcp_ipv4.c:1774-1828
  13. 場面 4: 子ソケットはいつ消えるか void tcp_done(struct sock *sk) { /* ...

    */ tcp_set_state(sk, TCP_CLOSE); /* ... */ } if (!sock_flag(sk, SOCK_DEAD)) sk->sk_state_change(sk); /* else inet_csk_destroy_sock(sk); /* 状態を変えるだけ */ ここで初めて破棄へ */ RST を受けても、アプリが accept() する前の子ソケットは消えない 子ソケットが解放されるのは、アプリが accept() して close() したとき(または listen ソケットを閉じ たとき) つまり free のタイミングを決めるのはサーバのアプリ Ubuntu noble 6.8 系 net/ipv4/tcp.c tcp_done()
  14. 場面 4: 子ソケットの破棄 → 即 free void inet_sock_destruct(struct sock *sk)

    { struct inet_sock *inet = inet_sk(sk); /* ... */ kfree(rcu_dereference_protected(inet->inet_opt, 1)); /* ... */ } 子ソケットが消えるときに呼ばれる inet_opt を普通の kfree() で即座に解放 RCU で読んでいる人がいても待たない Ubuntu noble 6.8 系 net/ipv4/af_inet.c:133-157
  15. 再送 SYN が来るとどうなるか if (!tcp_oow_rate_limited(sock_net(sk), skb, /* ... */) &&

    !inet_rtx_syn_ack(sk, req)) { /* inet_rtx_syn_ack → tcp_rtx_synack → tcp_v4_send_synack */ /* ... */ } return NULL; の中。request sock に対して SYN がもう一度来た場合 「SYN-ACK が届かなかったのかも」と判断してSYN-ACK を送り直す つまり場面 2 がもう一度走る。 ireq_opt を RCU で読む tcp_check_req() Ubuntu noble 6.8 系 net/ipv4/tcp_minisocks.c:706-719
  16. タイムライン 時刻 t0 t1 CPU 0 t2 ACK を受信 →

    子ソケット作成 SYN-ACK 組み立て中… t3 t4 t5 アプリが accept() → close() 子ソケット破棄 → kfree(opt) 空いた場所を別データが再利用 (まだ組み立て中…) newinet->inet_opt = opt CPU 1 再送 SYN を受信 rcu_read_lock() opt = ireq->ireq_opt opt->optlen ← 解放済み! ← まだ有効 を読む CPU 1 は RCU の読み取り区間にいる。だから本来は t3 で free してはいけない ただし t1〜t5 は一瞬で、その間に t2・t3 がすべて終わる必要がある(後で詳しく)
  17. 2 つ目の経路: request sock migration 有効になる条件: net.ipv4.tcp_migrate_req=1 、または BPF の

    BPF_SK_REUSEPORT_SELECT_OR_MIGRATE listen ソケットが閉じたとき、接続待ちを別の listen ソケットへ引っ越す機能 引っ越しで request sock を複製する。複製元と複製先が同じ ireq_opt を共有 複製が消えるとき tcp_v4_reqsk_destructor() が普通の kfree() static void tcp_v4_reqsk_destructor(struct request_sock *req) { kfree(rcu_dereference_protected(inet_rsk(req)->ireq_opt, 1)); } tcp_migrate_req の既定値は 0。使っていなければ主経路は 1 つ目 Ubuntu noble 6.8 系 net/ipv4/tcp_ipv4.c:1219-1222
  18. レースが成立するのに必要な出来事 1. IP オプション(Record Route / Timestamp)付きの SYN が届く →

    ireq_opt が作られる 2. 同じ SYN がもう一度届く → ある CPU で SYN-ACK の再送が始まる 3. ほぼ同時に ACK が別の CPU で処理される → 子ソケットができ、 ireq_opt を引き継ぐ 4. SYN-ACK の組み立てが終わる前に、アプリが accept() → close() → kfree 1〜3 は攻撃者がパケットで起こせる。4 はサーバのアプリの動き次第 CNA は「何度でも試せる」として攻撃難度を「低」と評価している
  19. ① 情報漏えい(機密性) memcpy(iph + sizeof(struct iphdr), opt->__data, opt->optlen); と __data

    が他人のデータになっている その中身がSYN-ACK に載って攻撃者へ送られる カーネルのヒープの中身(ポインタなど)が外に漏れる カーネルのアドレスが漏れると、他の攻撃の足がかりになる optlen
  20. ② メモリ破壊(完全性) ip_rt_get_source(iph + opt->rr + iph[opt->rr + 2] -

    5, skb, rt); memcpy(iph + opt->ts + iph[opt->ts + 2] - 5, &midtime, 4); ・ ts は「どこに書くか」のオフセット これが嘘の値になると範囲外に書き込む カーネル内の書き込みは理論上乗っ取りの入り口になりうる rr
  21. ③ サービス停止(可用性) skb_push(skb, sizeof(struct iphdr) + (opt ? opt->opt.optlen :

    0)); が大きすぎると、確保済みの先頭余白をはみ出す カーネルは skb_under_panic() で即停止 範囲外書き込みでも oops(カーネルのクラッシュ)になりうる レースに勝てば、リモートから OS ごと落とせる可能性がある optlen
  22. ① free を決めるのはアプリ 子ソケットが解放されるのは、アプリが accept() して close() したとき 攻撃者が RST

    を送っても、accept 前の子ソケットは消えない(場面 4) アプリの処理はユーザ空間を経由するので、カーネル内の処理よりずっと遅い
  23. ② 一瞬の処理の中に、遅い処理を全部収める必要がある 読む側(CPU 1) SYN-ACK を組み立てるだけ カーネル内のごく短い処理 捨てる側(CPU 0 +

    アプリ) ACK の処理 → 子ソケット作成 → アプリが起きて accept() → close() → kfree 読む側が ireq_opt を読んでから使い終わるまでの間に、右側が全部終わらないといけない 読む側が割り込みなどで偶然止まらない限り、まず重ならない 仮想マシンで vCPU が一時停止したときや、リアルタイムカーネルでは、幅が広がることはある
  24. ③ 同じ接続のパケットは同じ CPU で順番に処理されがち NIC は受信したパケットを、接続(送信元・宛先の IP とポート)ごとに受信キューへ振り分ける(RSS) 再送 SYN

    と ACK は同じ接続なので、ふつうは同じ CPU で 1 つずつ処理される 同じ CPU で順番に処理されるなら、レースは起きない ただし NIC や RPS などの設定によって変わる NIC と設定に依存する一般的な挙動。本資料では実機で検証していない
  25. 現実的な見立て シナリオ 見立て リモートから乗っ取り(RCE) ほぼ無理筋 リモートからクラッシュ(DoS) 理屈の上ではありえるが、成功率は低い 同じホストのローカルユーザ 自分で listen

    して自分でつなげば、タイミングを制御しやすくなるかもしれな から い(未検証) CVSS 9.8 は最悪のケースの評価。リモートの実態は Red Hat の 5.9 に近い 2026-10-29 時点で悪用の報告は無い(CISA KEV 未掲載、EPSS 1% 未満) 深夜の緊急対応までは要らない。通常のカーネル更新で確実に直す インターネットに公開しているホストと、他人がログインできる共用ホストを優先する 見立ては本資料の作成者の評価。公式な評価ではない
  26. 修正パッチ(Eric Dumazet, 2026-10-01) --- a/net/ipv4/af_inet.c +++ b/net/ipv4/af_inet.c @@ void inet_sock_destruct(struct

    sock *sk) - kfree(rcu_dereference_protected(inet->inet_opt, 1)); + kfree_rcu(rcu_dereference_protected(inet->inet_opt, 1), rcu); --- a/net/ipv4/tcp_ipv4.c +++ b/net/ipv4/tcp_ipv4.c @@ static void tcp_v4_reqsk_destructor(struct request_sock *req) - kfree(rcu_dereference_protected(inet_rsk(req)->ireq_opt, 1)); + kfree_rcu(rcu_dereference_protected(inet_rsk(req)->ireq_opt, 1), rcu); struct ip_options_rcu の rcu メンバは、まさにこのために最初から用意されていた mainline commit e3f33b0a1d89 "ipv4: free inet_opt and ireq_opt after an RCU grace period"
  27. 修正後のタイムライン 時刻 t1 CPU 0 t3 t5 t6 t7 アプリが

    close() → 子ソケット破棄 kfree_rcu(opt) → 予約だけ 猶予期間が終わる → ここで free CPU 1 rcu_read_lock() opt = ireq->ireq_opt SYN-ACK 組み立て中 opt->optlen を読む ← まだ有効 rcu_read_unlock() ほかの inet_opt を差し替える経路( do_ip_setsockopt() など)は、もともと kfree_rcu() を使って いた。2 か所だけ揃っていなかった
  28. 修正済みバージョン(upstream) 系列 5.10 5.15 6.1 6.6 6.12 6.18 7.2 mainline

    修正版 5.10.272 5.15.223 6.1.190 6.6.159 6.12.113 6.18.56 7.2.10 7.3 ディストリのカーネルはバックポートなので、各ディストリの告知で判断する 出典: cve.org CVE レコード(affected versions)
  29. Ubuntu の状況(2026-10-28 更新の告知) リリース 24.04 LTS noble 22.04 LTS jammy

    20.04 LTS focal 18.04 LTS bionic 14.04 LTS trusty カーネル 6.8 系 5.15 系 5.4 系 4.15 系 3.13 系 状態 Vulnerable(6.8.0-149.149 で修正作業中) Vulnerable(5.15.0-203.213 で修正作業中) Vulnerable Vulnerable Not affected(4.4 より前) Ubuntu の優先度は High noble のソースでも kfree() のままであることを確認済み(本資料のコード引用元) 出典: ubuntu.com/security/CVE-2026-98432
  30. ① 確認コマンド(Ubuntu) いま動いているカーネル # uname -r 更新があるか # sudo apt

    update && apt list --upgradable 2>/dev/null | grep linux-image 更新して再起動 カーネルは再起動しないと切り替わらない) # ( sudo apt full-upgrade && sudo reboot パッケージを入れただけでは直らない。再起動までがセット
  31. ② 暫定策: nftables で IP オプション付き SYN を落とす 例 テーブルの

    チェーンがある前提 # : inet filter input sudo nft 'insert rule inet filter input ip hdrlength != 5 tcp flags & (syn | ack) == syn drop' IHL ≠ 5(= IP オプション付き)の新規 SYN を捨てる 攻撃の入口( ireq_opt に何かが入る状態)を作らせない 正当な通信で SYN に IP オプションを付けることは、まず無い 上流の FW・ロードバランサで落とせるならそちらでも可 本番投入前に検証環境で動作確認すること
  32. ③ request sock migration の確認 sysctl net.ipv4.tcp_migrate_req # net.ipv4.tcp_migrate_req =

    0 ← 既定値。この経路は無効 1 なら 2 つ目の経路も対象。不要なら 0 に戻す SO_REUSEPORT + BPF でソケット振り分けをしているサービスも要確認
  33. 参考資料 CVE レコード: https://www.cve.org/CVERecord?id=CVE-2026-98432 NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-98432 Ubuntu: https://ubuntu.com/security/CVE-2026-98432 Red Hat:

    https://access.redhat.com/security/cve/cve-2026-98432 Oracle: https://linux.oracle.com/cve/CVE-2026-98432.html Amazon Linux: https://explore.alas.aws.amazon.com/CVE-2026-98432.html Debian: https://security-tracker.debian.org/tracker/CVE-2026-98432 修正 commit(mainline): e3f33b0a1d89d738e2b913dab519226eaf76e15b 原因 commit: 079096f103fa "tcp/dccp: install syn_recv requests into ehash table" コード引用: Ubuntu 24.04 noble カーネル(git.launchpad.net/~ubuntukernel/ubuntu/+source/linux/+git/noble)