見出し画像

【lwIP】Bug #3 | DHCPスプーフィングで通信経路を乗っ取られる?|XID予測を招くLWIP_RAND()の脆弱性

(English Version Here.)

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


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

  • DHCPで取得したIPアドレスが突然変わった

  • ゲートウェイやDNSサーバーのアドレスが意図しない値になっている

  • ネットワーク上に不正なDHCPサーバーがある環境で動作がおかしい

  • セキュリティ試験でDHCPスプーフィング攻撃が成功してしまった

lwIPのDHCPクライアントには、環境によってトランザクションIDが予測可能になる問題があります。

DHCPサーバーはIPアドレスだけでなく、デフォルトゲートウェイ(機器の全外部通信を転送する経由先)とDNSサーバー(ドメイン名をIPアドレスに変換する問い合わせ先)も設定します。機器はこれらをそのままネットワーク設定として使用し、後から検証しません。ゲートウェイを攻撃者のマシンに向けられると全通信が攻撃者を経由し、DNSサーバーを偽サーバーに向けられると接続先を任意のアドレスにすり替えられます。

DHCPクライアントが偽の応答を除外するうえで、XID(トランザクションID)の照合は重要な検証手段です。XIDが予測可能であれば、攻撃者は正しいXIDを埋め込んだ偽応答を事前に作成でき、この照合が実質的に機能しなくなります。


■ 発生条件

  • lwIPバージョン: 少なくともlwIP 2.x系および現行コードで確認。lwIP本体のバージョンを上げても、ポート層の `LWIP_RAND()` が `rand()` のままであれば問題は残る

  • 影響を受ける構成:

    • `LWIP_RAND()` に `rand()` を使用している(STM32Cube、TI SDK、AMD Vitis など多くのベンダーSDKのデフォルト)

    • `LWIP_RAND` が未定義(フォールバックで `0xABCD0001` からの連番になる)


■ この記事で分かること

  1. 実際に確認できる具体手順

    • Wireshark でXIDの規則性を観察する方法

    • Scapy を使ったXIDの観察・記録方法

  2. 内部動作を追ったコード解析

    • `dhcp.c` の XID 生成ロジックと条件分岐の全体構造

    • `LWIP_RAND()` に `rand()` を使うと何が起きるか

    • `LWIP_RAND` 未定義時のフォールバック動作(`0xABCD0001` 固定スタート)

    • 主要ベンダーSDKにおける `LWIP_RAND()` の実態(ソースコード確認済み)

  3. 回避する方法

    • ハードウェアRNGを使った `LWIP_RAND()` の正しい実装

    • ベンダーSDK別の対応方法


■ 問題の概要

DHCPでは、クライアントはDISCOVERパケットを送信する際に XID(トランザクションID) をランダムに生成し、正規のDHCPサーバーからの応答を区別します。

DHCPサーバーの応答は「IPアドレスだけ」ではない

DHCPクライアントは、サーバーから受け取ったOFFER/ACKパケットの内容をそのままネットワーク設定として使用します。設定されるのはIPアドレスだけではなく、機器のすべての通信経路を決定する情報が含まれています。

  • デフォルトゲートウェイ:機器が外部宛てのパケットを転送する先のルーターアドレス。ここを攻撃者のマシンに設定されると、機器から外部へのすべての通信が攻撃者を経由する(中間者攻撃・盗聴・改ざん)

  • DNSサーバーアドレス:機器がドメイン名をIPアドレスに変換する際の問い合わせ先。ここを攻撃者のサーバーに設定されると、任意のドメイン名を偽のIPアドレスへ誘導できる(DNSハイジャック・接続先の差し替え)

  • サブネットマスク、リース期間、DHCPサーバーIDなど

クライアントはこれらの値を受け取った後の追加検証は行いません。偽のDHCP応答が一度でも受理されると、攻撃者が指定したゲートウェイ・DNSサーバーで機器の通信が動作し始めます。

この一連の攻撃を**DHCPスプーフィング(DHCP Spoofing)**と呼びます。偽のDHCPサーバー(または同一セグメントで応答を送り込む攻撃者)を使ってクライアントのゲートウェイ・DNS設定を書き換え、通信を攻撃者の制御下に誘導する手口です。

XIDは応答対応付けの重要な検証要素

DHCPクライアントが応答を自分の要求に対応するものとして扱ううえで、XIDの照合は重要な検証要素です(後述の解析4参照)。少なくともXIDが一致しなければ応答は破棄されます。逆にXIDが予測可能になると、この重要なチェックを通過する偽応答を作りやすくなります。

XIDが十分にランダムであれば、攻撃者が正しいXIDを持つ偽応答を事前に用意することはできません。32ビット乱数空間(約43億通り)への総当たりは現実的ではないためです。しかし XIDが予測可能になると、この重要なチェックを通過する偽応答を作りやすくなります。

lwIPの `dhcp.c` は、XID生成に `LWIP_RAND()` マクロを呼び出す設計です。しかし `LWIP_RAND()` の実装はプラットフォーム(`cc.h`)に完全に委ねられており、lwIP 本体は中身を関知しません。

STM32Cube、TI SDK、AMD Vitis など多くのベンダーSDKでは次のように定義されています:

// cc.h(各ベンダーSDK)
#define LWIP_RAND() ((u32_t)rand())

C標準の `rand()` のみでは不十分です。 条件によっては起動ごとに全く同じ数列が返され、XID が毎回同じ予測可能な値の列になります。

Wiresharkでパケットキャプチャして、毎回の起動でXIDが同じ値になっていませんか? もしそうであれば、この記事の問題に該当している可能性が高いです。

また `LWIP_RAND` マクロ自体が未定義の場合は `0xABCD0001` から始まる単純なインクリメントにフォールバックし、この場合はさらに予測が容易です。

ここで注意が必要な点があります。DHCP DISCOVERはブロードキャストで送信されるため、同一L2セグメントにいる攻撃者はXIDを事前に予測しなくても受信したDISCOVERからXIDを読み取れます。 そのため「同一セグメントでDISCOVERを見てからOFFERを返す」攻撃は、XIDがランダムでも成立します。

XIDが予測可能であることが問題になるのは、次の2つのシナリオです。

① DHCPリレー環境(異なるセグメントからの攻撃)
工場内IIoTやスマートメーターなど、フィールドネットワーク(OT系)と管理ネットワーク(IT系)が分離された構成では、DHCPリレーエージェントがDISCOVERをユニキャストに変換して管理ネットワーク側のDHCPサーバーへ中継します。IoT機器が送ったブロードキャストDISCOVERは管理ネットワーク側には届かないため、管理ネットワークに侵入した攻撃者はXIDを観測できません。XIDがランダムなら攻撃は困難ですが、予測可能なら観測なしで偽応答を組み立てられます。

② 単純インクリメントにより次のXIDが完全に特定可能(`LWIP_RAND` 未定義時)
`0xABCD0001` からの単純な連番インクリメントになるため、1回でもDISCOVERを観測すれば次のXIDは現在値+1と特定できます。DHCPリレー環境などで観測できない場合でも、機器の起動直後なら初回XIDは `0xABCD0001` と固定されているため、観測なしでも予測が可能です。ただし実際に偽応答が受理されるかどうかは、送信タイミングや正規DHCPサーバーとの競争にも依存します。

逆に言えば、同一セグメント内の攻撃はXIDがランダムでも成立するため、XIDのランダム化だけでは防げません。 この攻撃に対する本質的な対策はDHCPスヌーピングなどスイッチレベルのセキュリティ機能です。

なお、DHCPv6(IPv6環境)においても同様の問題があります。詳細は後半の補足セクションで解説します。


■ 確認手順

前提環境

  • lwIP 2.1.2 を搭載したターゲット機器がDHCPクライアントとして動作している

  • `cc.h` または `lwipopts.h` で `LWIP_RAND()` が `rand()` として定義されている、または `LWIP_RAND` が未定義

  • 同一ネットワーク上のPCから Wireshark でパケットキャプチャが実行できる

  • Scapy は任意。XIDを連続記録したい場合に使用する(`pip install scapy`、管理者権限が必要)

手順

Step 1: ターゲットのDHCP通信をキャプチャする

Wiresharkを起動し、DHCPフィルタを設定します:

dhcp

古いバージョンのWiresharkでは `bootp` と表示される場合があります。

ターゲットを再起動してDHCPリクエストをキャプチャします。

Step 2: XIDの規則性を確認する

`LWIP_RAND` 未定義の場合は、数回DISCOVERパケットを観察すると、XIDが `0xABCD0001`, `0xABCD0002`, `0xABCD0003` ... と順番に増加しているのが確認できます。

`LWIP_RAND() = rand()`(シード未設定)の場合は、毎回の起動で同じ XID の数列が繰り返されます(例:`0x4BB5F646`, `0x47033129`, ...)。

どちらも「次の起動でどの XID が来るか」が予測可能です。

Step 3: ScapyでXIDを観察・記録する

WiresharkのGUI上でXIDを確認するだけでなく、Scapyを使うとXID・MACアドレス・メッセージタイプをターミナルに連続表示して記録できます。以下は受信専用のXID観察スクリプトです(パケット送信は一切行いません)。

Scapy とは: Python で動作するパケット操作ライブラリです。`pip install scapy` でインストール可能です(管理者権限が必要です)。

#!/usr/bin/env python3
"""
DHCP XID 観察スクリプト(受信専用)
DHCP DISCOVER/REQUEST を受信して XID・MAC・メッセージタイプを表示する
パケット送信は一切行いません
"""
from scapy.all import *

IFACE = "eth0"   # ネットワークインターフェース名(環境に合わせて変更)

MSG_NAMES = {
    1: "DISCOVER", 2: "OFFER", 3: "REQUEST", 4: "DECLINE",
    5: "ACK",      6: "NAK",   7: "RELEASE", 8: "INFORM",
    "discover": "DISCOVER", "offer": "OFFER", "request": "REQUEST",
    "ack": "ACK", "nak": "NAK",
}

def get_dhcp_message_type(pkt):
    """DHCP message-type オプションを安全に取得する。
    pkt[DHCP].options には ("name", value) のタプルのほか
    "end" や "pad" のような文字列が混在するため isinstance で判定する。"""
    for opt in pkt[DHCP].options:
        if isinstance(opt, tuple) and opt[0] == "message-type":
            return opt[1]
    return None

def handle_dhcp(pkt):
    if not (pkt.haslayer(BOOTP) and pkt.haslayer(DHCP)):
        return
    xid      = pkt[BOOTP].xid
    chaddr   = pkt[BOOTP].chaddr[:6]
    mac_str  = ":".join(f"{b:02x}" for b in chaddr)
    msg_type = get_dhcp_message_type(pkt)
    msg_name = MSG_NAMES.get(msg_type, str(msg_type))
    print(f"XID=0x{xid:08X}  MAC={mac_str}  type={msg_name}")

print(f"[*] 観察開始 (インターフェース: {IFACE})")
sniff(
    iface=IFACE,
    filter="udp and port 67",   # DHCP全方向(DISCOVER/OFFER/REQUEST/ACK)を捕捉
    prn=handle_dhcp,
    store=False,
    promisc=True,               # プロミスキャスモードを有効化
)

プロミスキャスモードについて

`promisc=True` はNICをプロミスキャスモードに切り替え、自分宛て以外のフレームも受信します。ただしスイッチング環境では、自ポートに転送されないユニキャスト通信は見えません。DHCP DISCOVERはブロードキャスト(`ff:ff:ff:ff:ff:ff`)で送出されるため、同一セグメント内であれば観測できます。

Step 4: XIDの再現性を確認する

スクリプトを起動した状態でターゲットを数回再起動し、出力を比較します。

`LWIP_RAND` 未定義の場合は `XID=0xABCD0001`、`0xABCD0002`... と単調増加します。`LWIP_RAND() = rand()`(シード未設定)の場合は、再起動をまたいでも毎回同じXID列が繰り返されます。いずれかのパターンが見られる場合、この記事の問題に該当しています。


■ ソースコード解析

`dhcp.c` の XID 生成部分を追います。


【解析1】XID生成コードの全体構造

`dhcp_create_msg()` 関数内のXID生成ロジックは次の通りです(現行の全バージョン共通の構造):

// dhcp.c  dhcp_create_msg()(抜粋)

#if DHCP_CREATE_RAND_XID && defined(LWIP_RAND)
  static u32_t xid;            // LWIP_RAND() で毎回セット
#else
  static u32_t xid = 0xABCD0000;  // ← フォールバック(予測可能)
#endif

  if (dhcp->tries == 0) {
#if DHCP_CREATE_RAND_XID && defined(LWIP_RAND)
    xid = LWIP_RAND();   // ← LWIP_RAND() を呼び出す
#else
    xid++;               // ← 単純インクリメント(LWIP_RAND 未定義時)
#endif
  }
  dhcp->xid = xid;

`DHCP_CREATE_RAND_XID` のデフォルト値は `1`(有効)です。したがってコードパスは:

LWIP_RAND が定義されている?
  ├── YES → xid = LWIP_RAND()     ← 結果はLWIP_RANDの実装次第
  └── NO  → xid = 0xABCD0001、0xABCD0002  ← 予測可能(危険)

重要なのは YES の分岐に入った後の話です。`LWIP_RAND()` が安全な実装かどうかは 完全にプラットフォーム次第 です。


【解析2】`rand()` を使うと何が起きるか

`LWIP_RAND() = rand()` の場合、`srand()` が呼ばれていなければ C標準規格により `srand(1)` と同等 の動作になります。つまり起動のたびに全く同じ数列が返ります。

// srand() 未呼び出し時の rand() の動作(典型的な実装例)
// 毎回の起動で同じ値列が返る
// 例: 0x4BB5F646, 0x47033129, 0x30705B04, 0x20FD5DB4, ...

攻撃者がターゲットのCランタイム実装とシード条件を把握していれば、起動後のXID列を再現できる可能性があります(`rand()` のアルゴリズムはC標準で規定されておらず、ライブラリ実装が異なれば値列も変わります)。把握していない場合でも、何度かDHCPパケットを観測することで、同一機器・同一ファームウェアにおけるXID列の再現性や規則性を確認できる場合があります。


【解析3】主要ベンダーSDKにおける LWIP_RAND() の実態

各ベンダーSDKのcc.hを実際のソースコードで確認した結果です(SDKのバージョンや同梱lwIPの更新により定義が変わる可能性があります):

STM32CubeF7(STM32Cube_FW_F7_V1.17.0 / lwIP 2.1.2)

確認ファイル:`Middlewares/Third_Party/LwIP/system/arch/cc.h`

#define LWIP_RAND() ((u32_t)rand())   // ← rand() のみ

TI AM243x(MCU+ SDK 10_00_00_20 / lwIP 2.2.0)

確認ファイル:`source/networking/lwip/lwip-port/include/arch/cc.h`

#define LWIP_RAND() ((u32_t)rand())   // ← rand() のみ

AMD Vitis(Vitis 2024.1 lwip220_v1_0 / lwIP 2.2.0)

確認ファイル:`contrib/ports/xilinx/include/arch/cc.h`(Zynq-7000 / ZCU102 共通)

#define LWIP_RAND() ((u32_t)rand())   // ← rand() のみ

Renesas RX65N(r_lwip_driver_rx)

確認ファイル:`r_lwip_driver_rx/src/arch/cc.h`

#define LWIP_RAND() ((u32_t)r_lwip_driver_get_rand())
// → TSIPモジュール搭載時: R_TSIP_GenerateRandomNumber()  ← ハードウェアRNG
// → TSIP非搭載時: rand()  ← フォールバック

少なくとも確認した複数の主要ベンダーSDKでは、`LWIP_RAND()` が `rand()` のままになっている例が見られます。`rand()` は `srand()` で適切にシードしない限り、毎回同じ数列を返します。


【解析4】XID検証の仕組みはあるが、予測可能なために突破される

DHCPレスポンス受信時のXID検証(`dhcp_recv()` 内):

// dhcp.c  dhcp_recv()
if (lwip_ntohl(reply_msg->xid) != dhcp->xid) {
    LWIP_DEBUGF(DHCP_DEBUG | LWIP_DBG_TRACE,
                ("transaction id mismatch ...\n"));
    goto free_pbuf_and_return;   // XID不一致は拒否
}

検証ロジック自体は正しく実装されています。しかし XID が予測可能であるため、攻撃者は正しい XID を持つ偽応答を作れてしまい、検証が無効化されます。


【解析5】攻撃が成立するタイミング

XIDの予測可能性と攻撃者の位置によって、攻撃のパターンは3つに分かれます。


パターンA:同一セグメント内の攻撃(XID予測は不要)

DHCP DISCOVERはブロードキャストで送信されるため、同一L2セグメントにいる攻撃者はXIDを事前に予測しなくても攻撃できます。XIDがランダムな場合でもこの攻撃は成立します。

[ターゲット lwIP]                    [攻撃者(同一セグメント)]

DHCP DISCOVER 送信
  dst: 255.255.255.255(broadcast)
  xid: 0x4BB5F646(ランダムでも可)  ← broadcast なので読み取れる

                                     XID をコピーして即座に偽OFFER 送信
                                     xid: 0x4BB5F646
                                     yiaddr: 192.168.1.200
                                     router: 攻撃者IP
                                     dns:    攻撃者IP
                                     ↓ 正規サーバーより先に届けば成立

偽 OFFER 受信(XID一致 → 検証通過)
→ 偽サーバーへの REQUEST 送信 → 偽 ACK 受信
→ 攻撃者設定の IP・GW・DNS で動作開始

この攻撃はXIDがランダムでも成立します。 XIDの書き換えは4バイトのコピーに過ぎず、観測してから送信するまでの追加コストは事実上ゼロです。同一セグメント内での攻撃に対してXIDのランダム化は有効な対策ではなく、DHCPスヌーピングが正しい対策です。

DHCPスヌーピング(DHCP Snooping)とは

L2スイッチに実装されるセキュリティ機能で、スイッチのポートを「信頼済み(trusted)」と「未信頼(untrusted)」に分類し、未信頼ポートから送出されたDHCPサーバー応答パケット(OFFER・ACK)をスイッチが破棄します。

         [DHCP Server]
               |
        (trusted port)
               |
          [L2 Switch]
      /                 \
 (untrusted)       (untrusted)
      |                 |
  [Client]          [Attacker]

  trusted port  : OFFER/ACK(Server 発)→ 転送する
  untrusted port: OFFER/ACK(Attacker 発)→ 破棄する(DISCOVER は転送)

正規サーバーの OFFER/ACK -> trusted port  -> Client へ届く   ✓
攻撃者の偽OFFER/ACK      -> untrusted port -> スイッチで破棄  ✗

通常、DHCPサーバーや上流ルーターが接続されるポートだけを trusted に設定し、クライアント機器が接続される一般ポートはすべて untrusted にします。攻撃者が同一セグメントに接続していても、スイッチが偽OFFERをクライアントへ転送しないため、XIDの値に関係なく攻撃が成立しません。

DHCPスヌーピングはCisco・Juniper・Arubaなど業務用L2スイッチに標準搭載されていますが、家庭用ルーターや安価なL2スイッチには搭載されていないことが多く、また搭載機種でもデフォルトで無効になっているため管理者による明示的な設定が必要です。


パターンB:DHCPリレー環境での攻撃(XID予測が必要)

IoT機器が実際に接続される環境では、フィールド側ネットワーク(センサーや機器が置かれるセグメント)と管理ネットワーク(DHCPサーバーが置かれるセグメント)が分離されているケースが多くあります。

たとえば次のような構成です。

工場内IIoT(産業用IoT)
製造ラインに設置されたSTM32やTIマイコン搭載のセンサー・制御機器が、フィールドネットワーク(OT系)に接続します。DHCPサーバーは管理ネットワーク(IT系)に置かれ、フィールドとの境界にある産業用ゲートウェイがDHCPリレーエージェントとして機能します。

スマートメーター・ビル管理システム
電力メーターや空調・照明コントローラーなどのIoT機器が建物内の専用ネットワークセグメントに接続し、管理システム側のDHCPサーバーからアドレスを取得します。

これらの環境では、フィールド側ネットワークからのブロードキャストDISCOVERは管理ネットワーク側には届きません。

[フィールドネットワーク(OT/IoT系)]   [リレー]   [管理ネットワーク(IT系)]

IoT機器(lwIPマイコン)
  │
  │ DISCOVER(broadcast)
  │ xid: 0x4BB5F646
  ↓
産業用ゲートウェイ ──────── unicast ─────────→  DHCPサーバー
(リレーエージェント)                              ↑
                                            攻撃者が管理ネットワークに侵入
                                            ★ フィールド側の
                                              broadcast は届かない
                                            ★ XIDをパケットから
                                              読み取れない

管理ネットワークに不正アクセスした攻撃者は、フィールド側のDISCOVERを観測できないためXIDを知る手段がありません。XIDがランダムであれば偽応答を組み立てられませんが、XIDが予測可能なら、DISCOVERを一切観測せずとも偽応答を送信できます。

XIDが予測可能な場合、攻撃者は事前に偽応答を準備して送信できる:

攻撃者(管理ネットワーク内)
  │
  │ 偽OFFER を送信(unicast または broadcast)
  │   xid:    0xABCD0001  ← 起動直後なら観測不要で確定
  │   router: 攻撃者のIP  ← 全通信が攻撃者を経由
  │   dns:    攻撃者のIP  ← 名前解決が攻撃者のサーバーへ
  ↓
リレーエージェント(産業用GW)──→ IoT機器(lwIPマイコン)

正規DHCPサーバーの応答より先に届けば偽OFFERが採用される

IoT機器のゲートウェイやDNSサーバー設定が書き換えられると、クラウドへの接続先やファームウェア更新サーバーが攻撃者の制御下に向けられる可能性があります。

実際に成立するかどうかは、DHCPリレーの実装、管理ネットワーク側のACL、DHCPスヌーピング、リレーが受理する送信元制限などに依存します。


パターンC:`LWIP_RAND` 未定義時の予測可能なXID(単純インクリメント)

`LWIP_RAND` が未定義の場合、XIDは `0xABCD0001` から始まる単純なインクリメントになります。インクリメント幅が1固定のため、現在のXIDが分かれば次のXIDは現在値+1と完全に特定できます。さらに機器の起動直後であれば初回XIDは必ず `0xABCD0001` になるため、DISCOVERを一度も観測せずとも予測が可能です。これはランダムなXIDが持つ予測困難性を完全に失っており、セキュリティ上望ましくありません。

`rand()` でシードが固定の場合も同様で、XID列の再現性が高く予測が容易になります。

ただし、実際に偽応答が受理されるかどうかは、送信タイミング、chaddr(クライアントMACアドレス)、ネットワーク構成、正規DHCPサーバーとの競争、DHCPスヌーピング等の有無に依存します。


■ 【補足】DHCPv6 の場合

IPv6を有効にしている環境(`LWIP_IPV6=1`)では、DHCPv6 クライアントでも同様の問題が発生します。

DHCPv4 との違い

`dhcp6.c` の XID 生成は次の通りです:

// dhcp6.c  dhcp6_create_msg()
if (dhcp6->tries == 0) {
    dhcp6->xid = LWIP_RAND() & 0xFFFFFF;   // ← 無条件で LWIP_RAND() を呼ぶ
}

DHCPv4 との主な違いは2点あります。

① フォールバックがない

DHCPv4 は `LWIP_RAND` 未定義時に `0xABCD0001` 連番へのフォールバックがありますが、DHCPv6 は `LWIP_RAND()` を無条件に呼び出します。DHCPv6クライアントを有効にして `dhcp6.c` がビルド対象になる構成では `LWIP_RAND()` が必要になります。逆に言えば、その構成でビルドが通っているならば `LWIP_RAND()` は必ず定義済みです。

② XIDが24ビット

DHCPv4 の XID は32ビットですが、DHCPv6 の XID は `& 0xFFFFFF` で24ビットに制限されています。`rand()` の値列から予測する難易度はほぼ同等です。

DHCPv6 の攻撃シナリオ

DHCPv6 では SOLICIT → ADVERTISE → REQUEST → REPLY のシーケンスで設定を取得します。XID が予測可能であれば、攻撃者は正規の DHCPv6 サーバーより先に偽の ADVERTISE を送ることで、DHCPv6 の DNS Recursive Name Server オプションなどを書き換えられる可能性があります。

修正方法

DHCPv4 の修正(`LWIP_RAND()` をハードウェアRNGに変更)と全く同じ対応で解決します。 `LWIP_RAND()` はプロセス全体で共有されるマクロのため、DHCPv4 向けの修正が DHCPv6 にも自動的に適用されます。


■ 修正方法

方法①:ハードウェアRNGを使った LWIP_RAND の直接実装(品質重視)

STM32F7 には内蔵の真乱数生成器(RNG)があります。これを使うことで毎回異なる予測不可能な XID を生成できます。

`lwipopts.h` に以下を追加します:

/* ハードウェアRNG を LWIP_RAND に使用する */
extern uint32_t lwip_rand_hw(void);
#define LWIP_RAND() lwip_rand_hw()

`lwip_rand_hw()` の実装例:

// lwip_rand.c(新規作成)
#include "stm32f7xx_hal.h"

extern RNG_HandleTypeDef hrng;  // CubeMX で生成された RNG ハンドル

uint32_t lwip_rand_hw(void)
{
    uint32_t random_value = 0;
    if (HAL_RNG_GenerateRandomNumber(&hrng, &random_value) != HAL_OK) {
        /* RNG失敗時は時刻ベースのフォールバック(本番環境では要検討) */
        random_value = HAL_GetTick() ^ (HAL_GetTick() << 16);
    }
    return random_value;
}

RNGペリフェラルの有効化は CubeMX で「RNG」を有効化することで設定できます。

ただしこの方法は `LWIP_RAND()` が呼ばれるたびにハードウェアRNGを呼び出します。多くのマイコンでは生成は高速ですが、ハードウェアの実装によってはエントロピーの蓄積を待つ場合があり、DHCP以外でも `LWIP_RAND()` が頻繁に呼ばれる構成(TCP ISN生成など)では方法②の方が適している場合があります。

方法②:ハードウェアRNGをシードとして srand() を初期化する(実装容易性重視)

方法①の代替として、起動時に一度だけハードウェアRNGからシード値を取得して `srand()` に渡す方法があります。以降の `rand()` 呼び出しはソフトウェアPRNGで行われるため毎回の待ち時間がなく、かつシードが真乱数であるため方法③(UID+Tick)より強固です。

// システム初期化時に一度だけ呼び出す
uint32_t seed = 0;
if (HAL_RNG_GenerateRandomNumber(&hrng, &seed) != HAL_OK) {
    /* RNG失敗時は UID+Tick によるフォールバック */
    seed = HAL_GetTick() ^ (uint32_t)HAL_GetUIDw0() ^
           (uint32_t)HAL_GetUIDw1() ^ (uint32_t)HAL_GetUIDw2();
}
srand(seed);

`LWIP_RAND()` の定義は変更不要です。`cc.h` の `rand()` 定義のままで構いません:

// cc.h(変更不要)
#define LWIP_RAND() ((u32_t)rand())

`rand()` はソフトウェアPRNGであるため同じシードからは常に同じ数列が生成されますが、シードに真乱数を使うことで起動ごとに異なる値列になります。

方法③:srand() でシードを設定する(ハードウェアRNGが使えない場合)

ハードウェアRNGが使えない場合、少なくとも `srand()` に予測困難な値を与えます:

// システム初期化時に一度だけ呼び出す
// HAL_GetTick() だけでは予測可能なので複数の値を組み合わせる
srand(HAL_GetTick() ^ (uint32_t)HAL_GetUIDw0() ^
      (uint32_t)HAL_GetUIDw1() ^ (uint32_t)HAL_GetUIDw2());

STM32 には96ビットのユニークID(UID)があります。これを含めることで、製品ごとに異なるシードになります。ただし起動時刻が毎回ほぼ同じになる場合(組み込み機器ではよくある)は弱さが残ります。ハードウェアRNGが利用できる場合は方法①または方法②を優先してください。

エントロピー源としては、UIDのほかにMACアドレスを加える方法もあります。MACアドレスは製品ごとに一意であるため、UID同様にシードのばらつきに寄与します。ただしMACアドレスは通信パケットから読み取れる公開情報であるため、単独で使用するのではなくUIDや起動時刻と組み合わせるとより効果的です。

// MACアドレスを加えたシード例
extern uint8_t MACAddr[6];   // アプリケーション側で定義済みのMACアドレス
uint32_t mac_val = ((uint32_t)MACAddr[2] << 24) | ((uint32_t)MACAddr[3] << 16)
                 | ((uint32_t)MACAddr[4] <<  8) |  (uint32_t)MACAddr[5];
srand(HAL_GetTick() ^ (uint32_t)HAL_GetUIDw0() ^
      (uint32_t)HAL_GetUIDw1() ^ (uint32_t)HAL_GetUIDw2() ^ mac_val);

シードの品質に関する注意

`srand()` に渡す値が起動のたびに同じになる場合、XIDの数列も毎回同じになり、対策の意味がなくなります。次のようなケースは特に注意してください:

  • RTCの初期値が常に0になる(電源投入直後やバッテリー切れ後)

  • `HAL_GetTick()` のみを使用する(起動直後は値のばらつきが小さい)

  • 固定定数をそのまま渡している

エントロピー源を増やしたい場合、ADCの未接続ピン(フローティング入力)から読み取る熱雑音をシードに加算する方法もあります。ただしADCノイズの品質はボードの設計・配線に依存するため、品質を重視する場合は方法①または方法②(ハードウェアRNG)を使用してください。


■ 修正後の確認方法

修正後、Wiresharkで複数回の再起動・DHCP DISCOVERをキャプチャし、XIDが各回でランダムな値になっていることを確認します:

Wiresharkフィルタ: dhcp
確認項目: "Transaction ID" フィールド

古いバージョンのWiresharkでは `bootp` と表示される場合があります。

連続した再起動間で `0xABCD0001`, `0xABCD0002` のような連番パターンや、毎回同一の値列が見られなければ修正が有効です。


■ 発見の経緯

Bug #30302 と「修正」の経緯

2010年6月、lwIP 開発者メーリングリスト(lwip-devel)にて Bug #30302「DHCP should use LWIP_RAND」として問題が報告されました。

当時の lwIP では XID が `0xABCD0001` から単純に連番インクリメントされる実装になっており、2011年に `DHCP_CREATE_RAND_XID` マクロを追加する変更が行われました。

なぜ今も問題なのか

この変更は「`LWIP_RAND()` を呼び出す仕組みを追加した」だけです。`LWIP_RAND()` の中身はプラットフォーム(`cc.h`)に完全に委ねられており、lwIP 本体は関知しません。

STM32Cube・TI SDK・AMD Vitis といった主要ベンダーSDKは、この変更後も `LWIP_RAND() = rand()` のままでリリースし続けています。`rand()` は `srand()` で適切にシードされない限り起動ごとに同じ数列を返すため、バージョンがいくら新しくても状況は変わりません。

ベンダーSDKのバージョンアップに伴い lwIP が新しくなっても、`cc.h` の `LWIP_RAND()` 定義が `rand()` のままである限り、この問題は継続します。

また、この問題には専用のCVE番号が振られていないため、SBOMスキャンツールでも検出されにくく、見落とされやすい問題の一つです。`LWIP_RAND()` の実装はlwIPのコード自体ではなくポート層(`cc.h`)の問題であるため、自動化されたツールでは検知されにくい問題です。開発者自身がベンダーSDKの `cc.h` 実装を確認する必要があります。


■ 公式対応状況

lwIP本体(Bug #30302

  • 状況:`DHCP_CREATE_RAND_XID` マクロを追加済み(仕組みのみ)

  • `LWIP_RAND()` の中身はプラットフォーム次第であり、lwIP 本体としての対応はここまで

`LWIP_RAND` 未定義時のフォールバック

  • 状況:未修正(全バージョン)

  • `LWIP_RAND` が未定義の場合、`0xABCD0001` からの連番インクリメントになる動作は現在も維持されている

**主要ベンダーSDKのデフォルト対応状況 **

  • STM32Cube(F4/F7/H7):`rand()` のまま → シード未設定の場合は脆弱

  • TI AM243x SDK:`rand()` のまま → シード未設定の場合は脆弱

  • AMD Vitis:`rand()` のまま → シード未設定の場合は脆弱

  • Renesas RX(TSIP搭載):ハードウェアRNG使用 → 対応済み

  • Renesas RX(TSIP非搭載):`rand()` にフォールバック → シード未設定の場合は脆弱


■ まとめ

  • 関連バグ: Bug #30302(lwip-devel、2010年)

  • 症状: DHCPで取得したIPアドレス・ゲートウェイ・DNSが攻撃者により書き換えられる可能性

  • 根本原因: `LWIP_RAND()` に予測可能な `rand()`(シードなし)が使われており、XIDが予測可能

  • 発生条件: `LWIP_RAND` 未定義、または `rand()` をシードなしで使用。実際の影響は同一L2セグメント・DHCPリレー環境・DHCPスヌーピングの有無などネットワーク構成に依存する

  • 修正: ①`LWIP_RAND()` をハードウェアRNGで直接実装、②起動時にハードウェアRNGで `srand()` を初期化(`LWIP_RAND()` の変更不要)、③ハードウェアRNGが使えない場合はUID等を組み合わせて `srand()` にエントロピーを与える

  • 公式対応: lwIP 本体は `LWIP_RAND()` を呼び出す仕組みを追加済み。しかし `LWIP_RAND()` の実装はプラットフォーム次第であり、多くのベンダーSDKが `rand()` のまま未対応。バージョンを上げても問題は解消しない

  • CVE: なし(SBOMスキャンでは自動検出できない。開発者自身による `cc.h` 実装の確認が必要)

  • 関連ファイル: dhcp.c(dhcp_create_msg 関数)、dhcp6.c(IPv6環境)、arch/cc.h


■ 本記事の取り扱いについて(免責事項)

  • 目的: 本記事は、lwIPおよび各ベンダーSDKにおけるDHCP XID生成の挙動解析と、問題の理解・対策方法の共有を目的としています。

  • 検証: 提示する内容はlwIPソースコード解析に基づくものですが、すべてのハードウェアやコンパイル条件での動作を保証するものではありません。

  • 自己責任: 修正の適用にあたっては、各プロジェクトの要件に基づき、利用者自身の責任において十分な検証・回帰テストを行ってください。

  • 免責: 万一、本記事の情報に基づいて生じた損害やトラブルについて、筆者は一切の責任を負いかねます。


■ 最後に

この記事では、lwIPの移植層(`cc.h`)における `LWIP_RAND()` の実装が不十分な場合に、DHCPのトランザクションIDが予測可能になる問題を解説しました。

このように、lwIPでは本体コードだけでなく、ベンダーSDKや移植層の実装によって挙動が大きく変わる場合があります。他の記事では、実際の不具合を題材に、再現方法・ソースコード解析・最小修正の考え方を詳しく解説しています。

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


lwIPは無料で手軽に製品に組み込める反面、利用する際にはバージョンや設定、既知の問題を把握しておくことが重要です。
また、商用製品のような保証付きサポートが前提ではないOSSであるため、最終的にはメーカー自身で不具合や脆弱性に対応しなければなりません。
そのため対応コストが製品出荷後に増大するリスクについても考慮しておく必要があります。

OSSであるlwIPには多くの利点がありますが、長期保守やサポート体制が重要な製品では、商用スタックを選択するケースもあります。

ただし、すでにlwIPで開発中の製品や出荷済みの製品を今から置き換えることは現実的ではありません。
そういった方に向けて、本記事の情報が、

  • 不具合や脆弱性の早期発見

  • 原因特定の時間短縮

  • 対応コストの低減

の参考になれば幸いです。

今後も、lwIPの問題に対して実用的な情報を公開します。

▶ 日本語記事一覧はこちら:lwIPトラブル対策室|日本語記事一覧

いいなと思ったら応援しよう!