【lwIP】Bug #2|SYNスキャンで既存TCP接続が切断される原因|TCP_LISTEN_BACKLOG未設定の落とし穴
(English Version Here.)
このnoteでは、lwIPで実際に起こり得る不具合・脆弱性・再現方法・最小修正の考え方やデバッグのノウハウをシリーズで解説しています。
▶ 日本語記事一覧はこちら:lwIPトラブル対策室|日本語記事一覧
■ lwIPでこんな現象に遭遇していませんか?
ポートスキャンを実行すると確立済みのTCP接続が突然切断される
通信中にもかかわらず接続が切られ、再接続が必要になる
ポートスキャンやネットワーク診断ツールの実行後に機器の挙動がおかしくなる
lwIPに存在する既知の不具合が原因の可能性があります。
■ 発生条件
lwIPバージョン: 2.0.3(またはそれ以前のバージョン)
影響を受ける構成: `TCP_LISTEN_BACKLOG = 0`設定でTCPサーバーを起動している
注意: 2.1.0 以降でも `TCP_LISTEN_BACKLOG = 0` は新規接続のブロックを引き起こす可能性があります(後述)
■ この記事で分かること
実際に再現できる具体手順
hping3 コマンドを使った再現方法とTCPパケットの条件
Python スクリプトを使った代替再現方法
内部動作を追ったコード解析
SYN_RCVD が20秒間残留してプールを枯渇させる仕組み
`TCP_LISTEN_BACKLOG = 0` の問題点
`tcp_kill_prio()` が ESTABLISHED を誤って切断する理由
回避する方法
最小限の回避設定と修正パッチ

■ 問題の概要
lwIP 2.0.3 以前の `tcp_alloc()` には、PCBプールが枯渇した際に ESTABLISHED(確立済み接続)を強制切断してしまう設計上の不具合があります。
SYNスキャンで大量の半開き接続を作り出すと、プールが SYN_RCVD で埋まり、正常に通信中の接続が巻き添えで強制切断されます。
原因のポイントは3つです:
デフォルト設定ではバックログ制限が無効(`TCP_LISTEN_BACKLOG = 0`)
バックログ制限がないとSYNを全て受け入れてPCBプールを枯渇させる
PCBプールが枯渇すると、`tcp_kill_prio()` が ESTABLISHED を削除する(2.0.3 以前)
■ 再現手順
注意(実施前に必ずご確認ください): 以降の再現手順は、自身が管理する検証環境・隔離ネットワーク内でのみ実施してください。第三者が管理する機器やネットワークへの無断パケット送信は、被害の有無にかかわらず不正アクセス禁止法に抵触するおそれがあります。必ず対象機器の管理権限を持ち、許可された範囲内でのみ実施してください。
前提環境
lwIP 2.0.3(またはそれ以前のバージョン) を搭載したターゲット機器がネットワークに接続されている
ターゲットが少なくとも1つのTCPポートをLISTEN中(例:HTTPサーバーとしてポート80)
`lwipopts.h` に `TCP_LISTEN_BACKLOG` の設定がない(デフォルト状態)
高速にSYNスキャンを送れる環境(hping3 等)
手順
Step 1: ターゲットが正常動作していることを確認する
ping <ターゲットのIPアドレス>pingが返ってくることを確認します。
Step 2: 既存のTCP接続を張ったままにする
別のターミナルから HTTP 等で接続を維持しておきます。この既存接続が影響を確認する対象になります。
Step 3: 固定の未使用IPを送信元にSYNを連続送信する
hping3 とは: TCP/IPパケットを自由に組み立てて送信できるコマンドラインツールです。主にLinuxで使用します。通常のpingと異なりTCP・UDP・ICMPなど任意のプロトコルでパケットを生成でき、送信元IPやフラグを自由に指定できます。多くのLinuxディストリビューションでパッケージとして提供されており、`sudo apt install hping3` などでインストールできます。
# hping3 を使用(送信元IPに RFC 2544 ベンチマーク用予約アドレスを指定)
hping3 -S --fast -p 80 -c 100 -a 198.18.0.1 <ターゲットのIPアドレス>`-S`:SYNフラグのみ(3ウェイハンドシェイクを完了させない)
`-p 80`:ターゲットのポート番号を指定(サーバーがLISTENしているポートに合わせて変更)
`--fast`:100msごとに送信
`-c 100`:100パケット送信
`-a 198.18.0.1`:送信元IPをRFC 2544でベンチマーク試験用として扱われるアドレス帯(198.18.0.0/15)に設定 → 通常の外部ネットワーク宛ての送信元として使うものではないため、誤送信リスクが小さい → RSTが戻らない → SYN_RCVD PCBが20秒間残留し、プールが埋まる
送信元IPに 198.18.0.0/15 を使う理由: 198.18.0.0/15 はRFC 2544でベンチマーク試験用として扱われるアドレス帯です。通常の外部ネットワーク宛ての送信元として使うものではないため、ここでは誤送信リスクを下げる目的で固定送信元IPとして使用しています。`--rand-source` オプションでランダムIPを使用する手段もありますが、利用規約やセキュリティポリシー違反になる場合があるため使用していません。
Step 4: 既存接続の切断を確認する
lwIP 2.0.3 以前では、PCBプールが SYN_RCVD で満杯になると、既存のESTABLISHED接続がRSTで強制切断されます。Step 2 で維持していた接続が切れることを確認します。
Step 5: 復帰確認
攻撃を停止すると、SYN_RCVD PCBが20秒でタイムアウトして解放されるため自然に回復しますが、一度切断された Step 2 の接続は再接続が必要です。
hping3 が使えない環境向けに、Scapy を用いたPythonスクリプトによる代替再現手順も有料パート(修正方法の後)に記載しています。
■ 原因・対策
ここまでで、発生条件・問題の概要・再現手順を公開しました。
次に、ソースコード解析の入口として、問題の全体像が分かる最小限の部分だけ紹介します。
詳細なコード追跡・修正方法・修正後の確認方法・公式対応状況は以降の有料パートで解説します。
同様の現象で困っている方、出荷済み製品の修正対応が必要な方に向けて、すぐに使用できる具体的な内容まで解説しています。
またlwIPの全体像や、各ベンダーSDKごとの差異、なぜlwIPの不具合情報が重要なのか、といった背景については、導入記事で整理しています。
【lwIP】よくある不具合と対策まとめ|組み込みTCP/IPトラブル事例集
■ ソースコード解析
lwIP 2.0.3 のソースコードを使って、何が起きているかを順を追って解説します。
【解析1】コールスタック全体図
SYNパケットを受信してからPCBが作成されるまでの関数呼び出しの全体像です:
[Ethernetフレーム受信]
ethernetif_input()
└─ tcpip_input() ← メッセージキューに投入
└─ [TCPIPスレッドが処理]
└─ ip4_input() → tcp_input() ← tcp.c: 既存PCBを4タプルで検索
├─ [既存PCBなし → LISTEN PCBを宛先ポートで検索 → 一致]
└─ tcp_listen_input() ← tcp_in.c
│
├─ [TCP_LISTEN_BACKLOG == 0]
│ バックログチェックのコードがコンパイルされない
│ → SYN数の制限なし
│
├─ tcp_alloc(pcb->prio) ← 新規PCBを確保
│ └─ memp_malloc(MEMP_TCP_PCB) ← プールから1スロット消費
│ [満杯の場合 → tcp_kill_timewait/kill_state/kill_prio を試みる]
│
├─ npcb->state = SYN_RCVD ← 状態を SYN_RCVD に設定
├─ npcb->tmr = tcp_ticks ← タイムアウト計測開始
├─ TCP_REG_ACTIVE(npcb) ← tcp_active_pcbs リストに追加
├─ tcp_enqueue_flags(npcb, TCP_SYN | TCP_ACK)
└─ tcp_output(npcb) ← SYN-ACK 送信試行
[SYN-ACKは存在しない送信元IPへ飛ぶ → RSTが戻らない]【解析2】SYNパケットを受信したときの処理
SYNが届いたとき、lwIPは `tcp_in.c` の `tcp_listen_input()` を呼び出します。
// tcp_in.c (抜粋)
} else if (flags & TCP_SYN) {
#if TCP_LISTEN_BACKLOG
if (pcb->accepts_pending >= pcb->backlog) {
// バックログ超過 → このSYNを無視(PCBは作らない)
return;
}
#endif /* TCP_LISTEN_BACKLOG */
npcb = tcp_alloc(pcb->prio); // PCBを1つ確保
// ...
npcb->state = SYN_RCVD; // 状態を SYN_RCVD に設定
// ...
rc = tcp_enqueue_flags(npcb, TCP_SYN | TCP_ACK); // SYN-ACKを送信キューに積む
tcp_output(npcb); // 送信
}注目すべき点:
ここから先は
¥ 1,000
この記事が気に入ったらチップで応援してみませんか?
