見出し画像

【lwIP】Bug #5|切断したはずのRSTが効かず接続が残る|CVE-2004-0230対策の副作用

このnoteでは、lwIPで実際に起こり得る不具合・脆弱性・再現方法・最小修正の考え方やデバッグのノウハウをシリーズで解説しています。
▶ 日本語記事一覧はこちら:lwIPトラブル対策室|日本語記事一覧


■ lwIPでこんな現象に遭遇していませんか?

  • 相手機器がRST(接続を即座に切断するリセット)を送ったのに、lwIP側の接続がなかなか閉じない

  • ファイアウォール越しの通信で、切断したはずの接続が長時間残り続ける

  • Windows環境のクライアントと通信したときだけ、ソケットを閉じても(socket close しても)十数分間リソースが解放されない

  • 同じコードがLinux相手では問題なく切断できるのに、特定環境でだけ接続が残る

lwIPがCVE-2004-0230(RSTスプーフィング)対策として導入した「チャレンジACK」の挙動が関係している可能性があります。

TCPの「チャレンジACK(Challenge ACK / RFC 5961)」という仕組みをご存じでしょうか。本記事は、この一見地味な防御機構が、lwIPで“切れないTCP接続”を生む実例を、ソースコードと再現手順を交えて解説します。


■ 発生条件

  • lwIPバージョン: 2.0.0〜2.2.1(CVE-2004-0230対策が導入されているバージョン)

  • 影響を受ける状況: 接続を切断するRSTパケットのシーケンス番号が、受信ウィンドウ内ではあるものの、lwIPが次に期待する値(`rcv_nxt`)と完全には一致しない場合

    • 経路上のファイアウォールが、`rcv_nxt` と一致する正規RSTの再送を遮断してしまう環境(報告例ではWindowsファイアウォール有効時)

    • シーケンス番号がずれた状態でRSTが到達し、その後も一致するRSTが届かないネットワーク構成

参考までに、CVE-2004-0230対策が導入される前の lwIP 1.4.1 以前では、この副作用は発生しません。ただしその場合、CVE-2004-0230(RSTスプーフィング)自体には対策されていない状態になります。


■ この記事で分かること

  1. なぜ「正規のRSTなのに接続が閉じない」状況が起きるのか

    • CVE-2004-0230対策(チャレンジACK方式)の動作の仕組み

    • シーケンス番号が完全一致しないRSTがどう扱われるか

  2. lwIPのRST受信処理がバージョンでどう変わったか(ソースコード解析)

  3. 自分の環境でRSTの挙動を確認する方法

    • Scapyで意図的にシーケンス番号をずらしたRSTを送り、応答を観察する手順

    • Wiresharkで「チャレンジACKが返る」様子を確認するポイント

  4. 接続が残る場合に検討できる緩和策とそのトレードオフ

    • CVE-2004-0230対策を無効化してはいけない理由

    • タイムアウト側でリソースを回収する考え方


■ 問題の概要

TCPでは、接続を即座に切断したいときにRST(リセット)というフラグを立てたパケットを送ります。受け取った側は接続を破棄します。

ここで関係するTCPの状態をいくつか整理します。

  • ESTABLISHED:接続が確立し、データをやり取りしている通常の状態

  • SYN_SENT:接続要求(SYN)を送って、相手の応答を待っている状態

かつてのTCP仕様(RFC 793)では、ESTABLISHED状態の接続に届いたRSTは、シーケンス番号が「受信ウィンドウの範囲内」にありさえすれば受け入れて接続を切断していました。

この緩い判定が問題になったのが CVE-2004-0230(RSTスプーフィング攻撃) です。攻撃者は接続の正確なシーケンス番号を知らなくても、ウィンドウの範囲内に収まる番号を総当たりで送るだけで、第三者のTCP接続を強制切断できてしまいます。長時間維持される重要な接続(例:ルーター間のBGP)が標的になりました。

チャレンジACK(Challenge ACK)とは?

これを受けて、後のTCP仕様(RFC 5961)では「チャレンジACK(Challenge ACK)」と呼ばれる方式が導入されました。lwIPも 2.0.0 以降でこの方式を採用しています。動作は次の通りです。

  • 届いたRSTのシーケンス番号が、次に期待する値(`rcv_nxt`)と完全に一致する場合 → 正規のRSTとみなして接続を切断する

  • シーケンス番号がウィンドウ内ではあるが完全一致しない場合 → すぐには切断せず、確認応答(ACK)を1つ返す(この確認用のACKをチャレンジACKと呼びます)。そして正しいシーケンス番号でのRST再送を待つ

正規の通信相手であれば、チャレンジACKを受け取った後に正しいシーケンス番号でRSTを送り直すため、最終的に接続は切断されます。攻撃者は正しいシーケンス番号を知らないため、再送できず切断は成立しません。CVE-2004-0230対策として、この仕組みは正しく機能します。

ところが、経路上のファイアウォールが介在する環境では、何らかの理由で正規のRSTがlwIPから見て「完全一致しない」状態になることがあります。lwIPはチャレンジACKを返しますが、ファイアウォールがそのチャレンジACKへの応答や、`rcv_nxt` と一致する正規RSTの再送を遮断してしまうと、RSTがいつまでも受け入れられません。結果として、本来すぐ閉じるはずの接続が、TCP再送タイムアウト(最長で数十分程度・設定依存)まで解放されずに残ります。

この現象は、Savannahに Bug #66366 として報告されています(報告環境ではWindowsファイアウォール有効時に約18分の遅延)。原因は、lwIPの実装ミスではなく、CVE-2004-0230対策(チャレンジACK方式)と、`rcv_nxt` と一致する正規RSTの再送を遮断してしまうファイアウォールの組み合わせによる副作用です。


■ そもそも、そんな「ずれたRST」を誰が送るのか?

ここで当然の疑問が生まれます。「シーケンス番号がずれたRSTなんて、攻撃者かバグのある機器しか送らないのでは? うちの製品には関係ないのでは?」

実はそうではありません。攻撃でもバグでもない、ごく普通のネットワークの動作として、シーケンス番号が一致しないRSTは発生します。 ここを理解しておくと、この問題が「特殊な状況の絵空事」ではなく、現実に踏みやすい落とし穴だと分かります。

チャレンジACKが返るのは「RSTが `rcv_nxt` より 前方 にある」とき

まず条件を正確にしておきます。lwIPがチャレンジACKを返すのは、届いたRSTのシーケンス番号が、

  • lwIPが次に期待する値(`rcv_nxt`)と完全一致はしないが、

  • `rcv_nxt` **より前方(大きい側)**にあって、受信ウィンドウの内側に収まっている

ときです。逆に `rcv_nxt` より後方(小さい側)のRSTは、そもそも受け入れ対象外として黙って捨てられます。

つまり問題が起きるのは、「lwIPがまだ受け取れていない、少し先のシーケンス番号でRSTが届く」 状況です。言い換えると、lwIPの受信位置(`rcv_nxt`)が、相手の実際の送信位置より 少し遅れている(シーケンスに“穴”があいている) ときに起こります。

なぜ「穴」があくのか(正常系での代表例)

相手の送信ストリームの一部がlwIPに届かず、シーケンスに穴があくのは、次のような正常な場面で普通に起こります。

  • 接続終了パケット(FIN)の取りこぼし+その後のRST送出: 相手が接続を閉じるためFINを送ったのに、経路上の機器がそれを落とすと、lwIPは`rcv_nxt`をFINの分だけ進められず、相手の送信位置より手前に取り残されます。この状態で、相手側がFINの再送を待たずに(アプリ層のタイムアウトや強制クローズなどにより)RSTを送ってきた場合、そのRSTはlwIPから見て「少し前方」になります。ただしFIN取りこぼし後の標準的な回復動作はFINの再送であり、RSTが出るには別途アプリ層側の事情が必要になるため、他の2つの例より発生条件はやや限定的です。

  • 接続・切断を何度も繰り返した後の“遅延RST”: 同じ相手・同じポートの組み合わせ(4タプル)を再利用すると、前の接続で送られたRSTが遅れて現在の接続のウィンドウに紛れ込むことがあります(TCPの古典的なTIME-WAITハザード)。Bug #66366の報告者も「接続と切断を数回繰り返した後に発生した」と述べており、この筋書きと符合します。

  • パケットの順序入れ替わり(リオーダリング): RSTが先行データを追い越して先に届くと、lwIPの`rcv_nxt`が追いつく前にRSTが到着し、「前方のRST」として扱われます。

いずれも攻撃でもバグでもなく、ルーターやファイアウォール、経路の揺らぎがあれば起こり得る、TCPの正常な振る舞いの範囲です。

RFC 5961のチャレンジACKは「攻撃対策専用」ではない

チャレンジACKは「RSTスプーフィング攻撃対策」として語られがちですが、RFC 5961 §3.2には、正規の通信相手が送った“ウィンドウ内・非一致”のRSTについても触れられています。この場合、チャレンジACKを受け取った正規の相手はTCB(接続管理情報)を失っているため、RFC 793の規則に従って2度目のRSTを正しいシーケンス番号で送り返し、最終的に接続は問題なく切断される、という趣旨の記述です。つまりこの仕組みは、攻撃対策を主目的としながらも、シーケンス番号の一致・不一致だけで機械的に判定するため、攻撃RSTと正規RSTを区別せず同じ経路で処理します。だからこそ、攻撃を受けていない普通の製品でも、経路条件が揃えばこの副作用に踏み込む可能性があるのです。

補足(本記事の立場): Bug #66366の報告には詳細なパケットキャプチャ(pcap)や `lwipopts.h` の設定が添付されていないため、報告環境で「穴」が生じた正確な経路までは公開情報からは断定できません。上記は、報告本文(「シーケンス番号が一致しない」「ファイアウォール有効時のみ発生」「RSTが再送されない」)と最も整合する説明として提示するものです。筆者のWindows 11環境では、ファイアウォールによる書き換えのような決定論的な再現はできておらず、この“穴”の発生がパケットロスのタイミングに依存することとも符合します。


■ 再現手順

注意(実施前に必ずご確認ください): 以降の再現手順は、自身が管理する検証環境・隔離ネットワーク内でのみ実施してください。第三者が管理する機器やネットワークへの無断パケット送信は、実害の有無にかかわらず不正アクセス禁止法に抵触するおそれがあります。必ず対象機器の管理権限を持つ立場で、許可された範囲内でのみ実施してください。

この問題の核心は「シーケンス番号がずれたRSTは、すぐには接続を切断せず、チャレンジACKを返す」という挙動です。報告環境のようにファイアウォールが正規RSTの再送を遮断する状況を用意しなくても、Scapyで意図的にシーケンス番号をずらしたRSTを送れば、同じコード経路を通して挙動を確認できます。

なお、本記事ではこのメカニズムをソースコードから解説します。後述するScapyの手順は、読者が自分の管理する環境でlwIPのRST処理の挙動を確認するための方法として提示するものです。

前提環境

  • lwIP 2.0.0〜2.2.1 のいずれか(CVE-2004-0230対策が入っているバージョン)

  • lwIPデバイス:任意のTCPサーバーまたはクライアントとして接続を確立できる構成

  • 確認用PC:Linux + Python 3 + Scapy、Wireshark

手順

Step 1: lwIPデバイスとPCの間でTCP接続をESTABLISHEDにする

lwIPデバイス側で待ち受けるTCPサーバー、またはPCへ接続するTCPクライアントを用意し、データを1往復させて接続が確立した状態にします。このときWiresharkでキャプチャを開始し、双方のシーケンス番号・確認応答番号を把握しておきます。

Step 2: シーケンス番号をずらしたRSTを送る

確立済みの接続に対して、Scapyから「受信ウィンドウ内ではあるが、lwIPが次に期待する `rcv_nxt` と一致しない」シーケンス番号のRSTを送ります。

from scapy.all import IP, TCP, send

LWIP_IP   = "192.168.1.200"   # lwIPデバイスのIPアドレス
LWIP_PORT = 5001              # lwIPデバイス側のTCPポート
PC_PORT   = 50000             # この接続でPC側が使っているポート

# rcv_nxt(lwIPが次に期待するシーケンス番号)に小さなオフセットを加える。
# 受信ウィンドウ内に収まり、かつ完全一致しない値にするのがポイント。
expected_seq = 0x12345678     # 実際の接続のシーケンス番号に置き換える
offset       = 100            # ウィンドウ内の小さなずれ

pkt = IP(dst=LWIP_IP) / TCP(
    sport=PC_PORT,
    dport=LWIP_PORT,
    flags="R",                # RST
    seq=expected_seq + offset # わざとずらす
)
send(pkt)

`expected_seq` には、Step 1でWiresharkから読み取った「lwIPが次に期待するシーケンス番号」を入れます。`offset` は受信ウィンドウのサイズより小さい正の値にします(ウィンドウからはみ出すと別の経路で処理されるため)。

Step 3: lwIPの応答を観察する

ソースコード上、lwIPはこのRSTを受け取ると `tcp_ack_now()` を呼び、確認応答(チャレンジACK)を1つ返します。接続は切断されません。Wiresharkでは次の点を確認します。

  • ずらしたRSTに対して、lwIPが接続を切断せず、確認応答(チャレンジACK)を返していること

  • lwIP側の接続がESTABLISHEDのまま維持されていること(後続のデータ送信が継続している場合)

Step 4: 正しいシーケンス番号のRSTと比較する

今度は `offset = 0`(`seq = expected_seq`)で、シーケンス番号が完全一致するRSTを送ります。この場合は `acceptable = 1` となり、lwIPは接続を即座に切断します。Step 2との挙動の違いが、本問題の核心です。

この2つの違い(ずれたRSTはチャレンジACKで保留、一致したRSTは即切断)が、CVE-2004-0230対策の動作そのものです。報告環境では、ファイアウォールが `rcv_nxt` と一致する正規RSTの再送を遮断するためStep 2の状態が継続し、接続が長時間残ります。


■ 再現・原因・対策

ここまでで、発生条件・問題の概要・再現手順(Scapyでチャレンジ ACK を確認する方法)を公開しました。
ここから先の有料パートでは、この挙動をlwIPのソースコードから追い、なぜ接続が長時間(実測で約38分)残るのかを解明したうえで、緩和策と公式対応状況まで解説します。

特定環境でだけ接続が残る現象に困っている方、ファイアウォール越しの通信でリソースが解放されない問題を調査している方に向けて、原因の理解と対応の考え方まで解説しています。

またlwIPの全体像や、各ベンダーSDKごとの差異、なぜlwIPの不具合情報が重要なのか、といった背景については、導入記事で整理しています。

【lwIP】よくある不具合と対策まとめ|組み込みTCP/IPトラブル事例集


■ ソースコード解析

`src/core/tcp_in.c` の `tcp_process()` にあるRST受信処理を中心に追います。


【解析1】RST受信時の「受け入れ判定」

ここから先は

10,142字 / 1画像

¥ 800

Amazon Payで支払うと最大2%還元のチャンス! 9/30まで

この記事が気に入ったらチップで応援してみませんか?