【lwIP】Bug #3 | Predictable DHCP XIDs and DHCP Spoofing | The LWIP_RAND() Pitfall
Japanese version available here: 日本語版
This series covers real-world bugs, vulnerabilities, reproduction steps, minimal fixes, and debugging know-how for lwIP — the TCP/IP stack widely used in embedded systems.
▶ English article index: lwIP Troubleshooting Notes
■ Have You Seen These Symptoms?
The IP address obtained via DHCP changed unexpectedly
The default gateway or DNS server was unexpectedly changed
The device starts communicating through an unexpected gateway or DNS server
A DHCP spoofing attack succeeded during a security audit
In lwIP's DHCP client, the transaction ID can become predictable depending on the environment.
A DHCP server configures not only an IP address, but also the default gateway (the router that forwards all outbound traffic from the device) and the DNS server (the resolver that maps domain names to IP addresses). The device applies these settings without any subsequent verification. If the gateway is redirected to an attacker's machine, all traffic passes through the attacker. If the DNS server is replaced with a rogue server, the attacker can redirect connections to arbitrary addresses.
The XID (transaction ID) is an important verification mechanism that allows a DHCP client to filter out responses that do not match its own requests. If the XID is predictable, an attacker can craft a fake response with the correct XID in advance, effectively bypassing this check.
■ Affected Versions and Configurations
lwIP version: Confirmed on lwIP 2.x and the upstream code checked at the time of writing. Upgrading lwIP itself does not fix the problem if the port layer's `LWIP_RAND()` remains `rand()`
Affected configurations:
`LWIP_RAND()` is defined as `rand()` (the default in many vendor SDKs including STM32Cube, TI SDK, and AMD Vitis)
`LWIP_RAND` is undefined (falls back to sequential increment starting from `0xABCD0001`)
■ What You'll Learn
Concrete verification steps
How to observe XID regularity with Wireshark
How to observe and record XIDs using Scapy
Internal behavior traced through the source code
The complete structure of the XID generation logic and conditional branches in `dhcp.c`
What happens when `rand()` is used for `LWIP_RAND()`
Fallback behavior when `LWIP_RAND` is undefined (fixed start from `0xABCD0001`)
The actual `LWIP_RAND()` implementation in major vendor SDKs (verified from source code)
How to fix it
Correct `LWIP_RAND()` implementation using a hardware RNG
Fix methods by vendor SDK
■ Problem Overview
In DHCP, a client generates a random XID (transaction ID) when sending a DISCOVER packet, and uses it to identify responses from the legitimate DHCP server.
A DHCP server response is not just an IP address
A DHCP client applies the contents of an OFFER/ACK packet directly as its network configuration. The settings go well beyond just an IP address — they determine all communication paths for the device:
Default gateway: The router address to which the device forwards packets destined for external networks. If an attacker's machine is set here, all outbound traffic from the device passes through the attacker (man-in-the-middle attack, eavesdropping, tampering)
DNS server address: The resolver the device queries when mapping domain names to IP addresses. If an attacker's server is set here, any domain name can be redirected to an arbitrary IP address (DNS hijacking, connection redirection)
Subnet mask, lease time, DHCP server ID, and more
The client performs no additional verification after receiving these values. Once a fake DHCP response is accepted, the device starts operating with the gateway and DNS server specified by the attacker.
This class of attack is known as DHCP spoofing — using a fake DHCP server response to overwrite the client's gateway and DNS configuration and redirect traffic through attacker-controlled infrastructure.

XID is a critical verification element
For a DHCP client to treat a response as corresponding to its own request, XID matching is an important verification step (see Analysis 4 below). A response with a mismatched XID is discarded. Conversely, if the XID becomes predictable, it becomes easier to craft fake responses that pass this critical check.
If the XID is sufficiently random, an attacker cannot prepare a fake response with the correct XID in advance. Brute-forcing a 32-bit random space (approximately 4.3 billion possibilities) is not practical. However, if the XID becomes predictable, crafting a fake response that passes this critical check becomes straightforward.
lwIP's `dhcp.c` calls the `LWIP_RAND()` macro for XID generation. However, lwIP core provides the hook, but the actual quality of randomness depends entirely on the platform-specific `LWIP_RAND()` implementation in the port layer (`cc.h`).
In many vendor SDKs including STM32Cube, TI SDK, and AMD Vitis, it is defined as follows:
// cc.h (each vendor SDK)
#define LWIP_RAND() ((u32_t)rand())The standard C `rand()` function is not suitable by itself for security-relevant randomness. Under certain conditions, it returns exactly the same sequence on every boot, making XIDs a repeating, predictable series of values.
Try capturing packets with Wireshark — does the XID come out the same on every boot? If so, your device is likely affected by this issue.
When the `LWIP_RAND` macro is not defined at all, the implementation falls back to a simple increment starting from `0xABCD0001`, making prediction even easier.
One important note: DHCP DISCOVER is sent as a broadcast, so an attacker on the same L2 segment can read the XID directly from the received DISCOVER without predicting it in advance. Therefore, the attack of "read XID from DISCOVER, then send a fake OFFER" works even when the XID is random.
Predictable XIDs become a problem in two specific scenarios:
① DHCP relay environments (attack from a different segment)
In architectures common to industrial IIoT and smart meters — where field networks (OT) and management networks (IT) are separated — a DHCP relay agent converts DISCOVERs to unicast and forwards them to the DHCP server on the management network. The broadcast DISCOVER sent by an IoT device never reaches the management network side, so an attacker who has penetrated the management network cannot observe the XID. If the XID is random, the attack is difficult; but if it is predictable, fake responses can be constructed without any observation.
② Fully deterministic next XID via simple increment (when `LWIP_RAND` is undefined)
The XID becomes a simple sequential increment from `0xABCD0001`, so observing just one DISCOVER is enough to predict the next XID as current+1. Even without any observation (e.g., in a relay environment), the first XID on boot is always `0xABCD0001`, making prediction possible with no prior observation. Whether a fake response is actually accepted depends on timing and competition with the legitimate DHCP server.
Conversely, an attack on the same segment succeeds even when the XID is random, so randomizing the XID alone is not a defense against it. The fundamental countermeasure for that attack is switch-level security such as DHCP snooping.
Note: the same issue exists in DHCPv6 (IPv6 environments). Details are covered in the supplemental section below.

■ Verification Steps
Note: Use these steps only in your own lab environment or on systems you are authorized to test.
Prerequisites
A target device running lwIP (DHCP client mode) is connected to the network
`cc.h` or `lwipopts.h` defines `LWIP_RAND()` as `rand()`, or `LWIP_RAND` is undefined
A PC on the same network can run Wireshark packet capture
Scapy is optional — useful when you want to continuously record XIDs (`pip install scapy`, requires administrator privileges)
Steps
Step 1: Capture DHCP traffic from the target
Launch Wireshark and set the DHCP filter:
dhcpOn older versions of Wireshark, this may appear as `bootp`.
Reboot the target and capture the DHCP request.
Step 2: Check for XID regularity
When `LWIP_RAND` is undefined, observe several fresh DHCP transactions, such as across reboots or after restarting the DHCP client — you will see the XID incrementing sequentially: `0xABCD0001`, `0xABCD0002`, `0xABCD0003`, ...
When `LWIP_RAND() = rand()` (unseeded), the same XID sequence repeats on every boot (e.g., `0x4BB5F646`, `0x47033129`, ...).
In both cases, the XID on the next boot is predictable.
Step 3: Observe and record XIDs with Scapy
Beyond viewing XIDs in the Wireshark GUI, Scapy can continuously display and record XID, MAC address, and message type in the terminal. The following is an observation-only script — it sends no packets whatsoever.
What is Scapy: A Python packet manipulation library. Install with `pip install scapy` (requires administrator privileges).
#!/usr/bin/env python3
"""
DHCP XID observation script (receive-only)
Receives DHCP DISCOVER/REQUEST and displays XID, MAC, and message type.
This script sends no packets.
"""
from scapy.all import *
IFACE = "eth0" # Network interface name (adjust to your environment)
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):
"""Safely retrieve the DHCP message-type option.
pkt[DHCP].options contains tuples like ("name", value) as well as
plain strings like "end" or "pad", so isinstance is used to distinguish."""
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"[*] Observation started (interface: {IFACE})")
sniff(
iface=IFACE,
filter="udp and port 67", # Capture all DHCP directions (DISCOVER/OFFER/REQUEST/ACK)
prn=handle_dhcp,
store=False,
promisc=True, # Enable promiscuous mode
)About promiscuous mode
`promisc=True` switches the NIC to promiscuous mode, allowing it to receive frames not addressed to itself. In a switched network, unicast traffic not forwarded to your port is not visible. DHCP DISCOVER is sent as a broadcast (`ff:ff:ff:ff:ff:ff`), so it can be observed within the same segment.
Step 4: Confirm XID reproducibility
Run the script and reboot the target several times, then compare the output.
When `LWIP_RAND` is undefined, you will see `XID=0xABCD0001`, `0xABCD0002`, ... incrementing monotonically. When `LWIP_RAND() = rand()` (unseeded), the same XID sequence repeats across reboots. If either pattern is observed, your device is affected by this issue.

■ Source Code Analysis
We trace the XID generation section of `dhcp.c`.
【Analysis 1】Overall structure of the XID generation code
The XID generation logic inside `dhcp_create_msg()` is as follows (based on lwIP 2.x and the upstream code checked at the time of writing):
// dhcp.c dhcp_create_msg() (excerpt)
#if DHCP_CREATE_RAND_XID && defined(LWIP_RAND)
static u32_t xid; // set each time via LWIP_RAND()
#else
static u32_t xid = 0xABCD0000; // ← fallback (predictable)
#endif
if (dhcp->tries == 0) {
#if DHCP_CREATE_RAND_XID && defined(LWIP_RAND)
xid = LWIP_RAND(); // ← calls LWIP_RAND()
#else
xid++; // ← simple increment (when LWIP_RAND is undefined)
#endif
}
dhcp->xid = xid;The default value of `DHCP_CREATE_RAND_XID` is `1` (enabled). The code path is therefore:
Is LWIP_RAND defined?
├── YES → xid = LWIP_RAND() ← result depends entirely on LWIP_RAND's implementation
└── NO → xid = 0xABCD0001, 0xABCD0002, ... ← predictable (dangerous)The key issue is what happens inside the YES branch. Whether `LWIP_RAND()` is a secure implementation is entirely up to the platform.
【Analysis 2】What happens when `rand()` is used
When `LWIP_RAND() = rand()` and `srand()` has not been called, the C standard specifies behavior equivalent to `srand(1)`. This means the exact same sequence is returned on every boot.
// Typical behavior of rand() when srand() has not been called
// The same sequence is returned on every boot
// Example: 0x4BB5F646, 0x47033129, 0x30705B04, 0x20FD5DB4, ...If an attacker knows the target's C runtime implementation and seed conditions, they may be able to reproduce the XID sequence after boot (note: the `rand()` algorithm is not standardized in C; different library implementations produce different sequences). Even without that knowledge, observing a few DHCP packets may be enough to confirm the reproducibility and regularity of the XID sequence for a given device and firmware build.
【Analysis 3】Actual `LWIP_RAND()` implementations in major vendor SDKs
Results from verifying `cc.h` in each vendor SDK against actual source code (definitions may change with SDK version or lwIP updates):
STM32CubeF7 (STM32Cube_FW_F7_V1.17.0 / lwIP 2.1.2)
File: `Middlewares/Third_Party/LwIP/system/arch/cc.h`
#define LWIP_RAND() ((u32_t)rand()) // ← rand() onlyTI AM243x (MCU+ SDK 10_00_00_20 / lwIP 2.2.0)
File: `source/networking/lwip/lwip-port/include/arch/cc.h`
#define LWIP_RAND() ((u32_t)rand()) // ← rand() onlyAMD Vitis (Vitis 2024.1 lwip220_v1_0 / lwIP 2.2.0)
File: `contrib/ports/xilinx/include/arch/cc.h` (common to Zynq-7000 and ZCU102)
#define LWIP_RAND() ((u32_t)rand()) // ← rand() onlyRenesas RX65N (r_lwip_driver_rx)
File: `r_lwip_driver_rx/src/arch/cc.h`
#define LWIP_RAND() ((u32_t)r_lwip_driver_get_rand())
// → With TSIP module: R_TSIP_GenerateRandomNumber() ← hardware RNG
// → Without TSIP: rand() ← fallbackAt least several major vendor SDKs confirmed have `LWIP_RAND()` set to `rand()`. Without a proper `srand()` call, `rand()` returns the same sequence on every boot.
【Analysis 4】XID validation exists, but is bypassed when XID is predictable
XID validation on DHCP response reception (inside `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; // mismatch → discard
}The validation logic itself is correctly implemented. However, because the XID is predictable, an attacker can craft a fake response with the correct XID, rendering the validation ineffective.
【Analysis 5】When the attack succeeds — three patterns
The attack pattern depends on XID predictability and the attacker's position.
Pattern A: Attack on the same segment (no XID prediction required)
DHCP DISCOVER is sent as a broadcast, so an attacker on the same L2 segment can attack without predicting the XID in advance. This attack works even when the XID is random.
[Target lwIP] [Attacker (same segment)]
DHCP DISCOVER sent
dst: 255.255.255.255 (broadcast)
xid: 0x4BB5F646 (can be random) ← readable from broadcast
Copy XID and immediately send fake OFFER
xid: 0x4BB5F646
yiaddr: 192.168.1.200
router: attacker's IP
dns: attacker's IP
↓ succeeds if it arrives before the real server
Fake OFFER received (XID match → passes validation)
→ REQUEST sent to fake server → fake ACK received
→ Device starts operating with attacker-specified IP/GW/DNSThis attack works even when the XID is random. Copying the XID is just a 4-byte copy operation; the additional cost of observing then sending is effectively zero. Randomizing the XID is not an effective countermeasure against attacks on the same segment — DHCP snooping is the correct defense.

What is DHCP Snooping?
A security feature implemented in L2 switches that classifies switch ports as "trusted" or "untrusted" and drops DHCP server response packets (OFFER, ACK) originating from untrusted ports.
[DHCP Server]
|
(trusted port)
|
[L2 Switch]
/ \
(untrusted) (untrusted)
| |
[Client] [Attacker]
trusted port : OFFER/ACK (from Server) → forwarded
untrusted port: OFFER/ACK (from Attacker) → dropped (DISCOVER is forwarded)
Legitimate server OFFER/ACK → trusted port → reaches Client ✓
Attacker's fake OFFER/ACK → untrusted port → dropped by switch ✗Normally, only the ports connecting DHCP servers and upstream routers are set to trusted; general client ports are all untrusted. Even if the attacker is connected to the same segment, the switch does not forward the fake OFFER to the client, so the attack fails regardless of the XID value.
DHCP snooping is standard on enterprise L2 switches from Cisco, Juniper, Aruba, and others, but is often absent from consumer routers and low-cost switches, and is disabled by default even on capable devices — requiring explicit configuration by an administrator.
Pattern B: Attack in a DHCP relay environment (XID prediction required)
In environments where IoT devices are actually deployed, field-side networks (where sensors and devices reside) and management networks (where the DHCP server resides) are often separated.
For example:
Industrial IIoT (factory automation)
STM32 or TI microcontroller-based sensors and controllers on a production line connect to a field network (OT side). The DHCP server is on the management network (IT side), and an industrial gateway at the boundary acts as the DHCP relay agent.
Smart meters and building management systems
Power meters and HVAC/lighting controllers connect to a dedicated building network segment and obtain their addresses from a DHCP server on the management system side.
In these environments, broadcast DISCOVERs from the field side never reach the management network.
[Field network (OT/IoT)] [Relay] [Management network (IT)]
IoT device (lwIP MCU)
│
│ DISCOVER (broadcast)
│ xid: 0x4BB5F646
↓
Industrial gateway ──── unicast ────→ DHCP server
(relay agent) ↑
Attacker has penetrated management network
★ Field-side broadcast does not arrive
★ Cannot read XID from packetAn attacker who has gained unauthorized access to the management network cannot observe the field-side DISCOVERs and has no way to know the XID. If the XID is random, the attack is difficult; but if the XID is predictable, fake responses can be sent without ever observing a DISCOVER.
With a predictable XID, the attacker can prepare and send fake responses in advance:
Attacker (on management network)
│
│ Send fake OFFER (unicast or broadcast)
│ xid: 0xABCD0001 ← determined without observation right after boot
│ router: attacker's IP ← all traffic routes through attacker
│ dns: attacker's IP ← name resolution goes to attacker's server
↓
Relay agent (industrial GW) ──→ IoT device (lwIP MCU)
The fake OFFER is accepted if it arrives before the legitimate DHCP server's responseOnce the IoT device's gateway and DNS settings are replaced, the device's cloud connection or firmware update server may be directed to infrastructure under the attacker's control.
Whether this succeeds in practice depends on the DHCP relay implementation, management-side ACLs, DHCP snooping, and source filtering by the relay.

Pattern C: Fully predictable XID via simple increment (when `LWIP_RAND` is undefined)
When `LWIP_RAND` is undefined, the XID becomes a simple increment starting from `0xABCD0001`. Since the increment is always 1, knowing the current XID determines the next XID as current+1. Furthermore, immediately after boot, the first XID is always `0xABCD0001`, making prediction possible with zero observation. This completely eliminates the unpredictability that a random XID should provide, and is undesirable from a security standpoint.
The same applies when `rand()` is used with a fixed seed — the XID sequence is highly reproducible and easy to predict.
Note that whether a fake response is actually accepted depends on timing, the client MAC address (`chaddr`), network topology, competition with the legitimate DHCP server, and whether DHCP snooping is in place.

■ [Supplement] DHCPv6
In environments with IPv6 enabled (`LWIP_IPV6=1`), the same issue occurs in the DHCPv6 client.
Differences from DHCPv4
The XID generation in `dhcp6.c` is:
// dhcp6.c dhcp6_create_msg()
if (dhcp6->tries == 0) {
dhcp6->xid = LWIP_RAND() & 0xFFFFFF; // ← calls LWIP_RAND() unconditionally
}There are two key differences from DHCPv4:
① No fallback
DHCPv4 falls back to `0xABCD0001` sequential increment when `LWIP_RAND` is undefined; DHCPv6 calls `LWIP_RAND()` unconditionally. In a build that enables the DHCPv6 client and compiles `dhcp6.c`, `LWIP_RAND()` is required — which means if the build succeeds, `LWIP_RAND()` is necessarily defined.
② 24-bit XID
DHCPv4's XID is 32 bits; DHCPv6's XID is limited to 24 bits via `& 0xFFFFFF`. If `rand()` is predictable or seeded with a fixed value, the same fundamental issue applies. Note that DHCPv6 uses a 24-bit transaction ID, so the brute-force space is smaller than DHCPv4's 32-bit XID.
DHCPv6 Attack Scenario
In DHCPv6, configuration is obtained through the SOLICIT → ADVERTISE → REQUEST → REPLY sequence. If the XID is predictable, an attacker may be able to send a fake ADVERTISE before the legitimate DHCPv6 server, potentially overwriting the DHCPv6 DNS Recursive Name Server option and other settings.
Fix
Exactly the same fix as for DHCPv4 (change `LWIP_RAND()` to use a hardware RNG) resolves the DHCPv6 issue as well. Since `LWIP_RAND()` is a globally shared macro, fixing it for DHCPv4 automatically applies to DHCPv6.
■ Fix Methods
Method ①: Direct implementation of `LWIP_RAND` using hardware RNG (quality-focused)
STM32F7 has a built-in True Random Number Generator (RNG). Using it generates a different, unpredictable XID on every call.
Add the following to `lwipopts.h`:
/* Use hardware RNG for LWIP_RAND */
extern uint32_t lwip_rand_hw(void);
#define LWIP_RAND() lwip_rand_hw()Example implementation of `lwip_rand_hw()`:
// lwip_rand.c (new file)
#include "stm32f7xx_hal.h"
extern RNG_HandleTypeDef hrng; // RNG handle generated by CubeMX
uint32_t lwip_rand_hw(void)
{
uint32_t random_value = 0;
if (HAL_RNG_GenerateRandomNumber(&hrng, &random_value) != HAL_OK) {
/* Fallback on RNG failure — review for production use */
random_value = HAL_GetTick() ^ (HAL_GetTick() << 16);
}
return random_value;
}Enable the RNG peripheral in CubeMX by enabling "RNG" in the peripheral configuration.
Note: this approach calls the hardware RNG every time `LWIP_RAND()` is invoked. Generation is fast on most MCUs, but depending on hardware implementation, entropy accumulation may introduce latency. In builds where `LWIP_RAND()` is called frequently beyond DHCP (e.g., TCP ISN generation), Method ② may be more appropriate.

Method ②: Initialize `srand()` with a hardware RNG seed at boot (ease of implementation)
As an alternative to Method ①, obtain a seed value from the hardware RNG once at startup and pass it to `srand()`. Subsequent `rand()` calls use the software PRNG, so there is no per-call latency, and since the seed is a true random number, this is more robust than Method ③ (UID + Tick).
// Call once during system initialization
uint32_t seed = 0;
if (HAL_RNG_GenerateRandomNumber(&hrng, &seed) != HAL_OK) {
/* Fallback using UID + Tick if RNG fails */
seed = HAL_GetTick() ^ (uint32_t)HAL_GetUIDw0() ^
(uint32_t)HAL_GetUIDw1() ^ (uint32_t)HAL_GetUIDw2();
}
srand(seed);No change to the `LWIP_RAND()` definition is required. The existing `rand()` definition in `cc.h` can remain as-is:
// cc.h (no change required)
#define LWIP_RAND() ((u32_t)rand())Since `rand()` is a software PRNG, the same seed always produces the same sequence — but using a true random seed ensures a different sequence on every boot.

Method ③: Seed `srand()` with entropy when hardware RNG is unavailable
When a hardware RNG is not available, at minimum provide `srand()` with a hard-to-predict value:
// Call once during system initialization
// Using only HAL_GetTick() is predictable — combine multiple sources
srand(HAL_GetTick() ^ (uint32_t)HAL_GetUIDw0() ^
(uint32_t)HAL_GetUIDw1() ^ (uint32_t)HAL_GetUIDw2());STM32 has a 96-bit unique ID (UID). Including it ensures a different seed per device. However, if the boot time is nearly identical on every power-on (common in embedded systems), some weakness remains. Use Method ① or ② (hardware RNG) when available.
An additional entropy source is the MAC address. Since MAC addresses are device-unique, they contribute to seed variation just like the UID. However, the MAC address is a publicly visible value readable from network packets, so it should be combined with the UID and boot time rather than used alone.
// Example seed with MAC address included
extern uint8_t MACAddr[6]; // MAC address defined in the application
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);Notes on seed quality
If the value passed to `srand()` is the same on every boot, the XID sequence is also the same, defeating the fix entirely. Pay particular attention to:
RTC initial value is always 0 (on first power-on or after battery depletion)
Using only `HAL_GetTick()` (low variation immediately after boot)
Passing a fixed constant directly
To increase entropy, some designs read thermal noise from an unconnected ADC pin (floating input) and add it to the seed. However, ADC noise quality depends on board design and routing, so for high-assurance use, prefer Method ① or ② (hardware RNG).
■ How to Verify the Fix
After applying the fix, capture multiple DHCP DISCOVERs across several reboots using Wireshark and confirm that the XID is a random value each time:
Wireshark filter: dhcp
Check: "Transaction ID" fieldOn older versions of Wireshark, this may appear as `bootp`.
If you no longer see sequential patterns like `0xABCD0001`, `0xABCD0002`, or identical sequences across reboots, the fix is working.
■ Discovery History
Bug #30302 and the "fix" history
In June 2010, the issue was reported on the lwIP developer mailing list (lwip-devel) as Bug #30302: "DHCP should use LWIP_RAND."
At the time, lwIP implemented the XID as a simple sequential increment starting from `0xABCD0001`. In 2011, a change was made to add the `DHCP_CREATE_RAND_XID` macro.
Why this is still a problem today
That change only added the mechanism to call `LWIP_RAND()`. lwIP core provides the hook, but the actual quality of randomness depends entirely on the platform-specific `LWIP_RAND()` implementation in the port layer (`cc.h`).
Major vendor SDKs including STM32Cube, TI SDK, and AMD Vitis have continued to ship with `LWIP_RAND() = rand()` even after this change. Updating lwIP alone does not resolve the issue if the port layer still defines `LWIP_RAND()` as an unseeded `rand()`.
Even when a vendor SDK update brings a newer lwIP, the problem persists as long as `LWIP_RAND()` in `cc.h` remains `rand()`.
Additionally, this issue has not been assigned a dedicated CVE number, making it easy to miss in SBOM scanning tools. Because the problem lies in the port layer (`cc.h`) rather than in lwIP's code itself, automated tools cannot detect it. This is an implementation-dependent design risk that requires developers to manually inspect the `LWIP_RAND()` implementation in their vendor SDK's `cc.h`.
■ Official Status
lwIP core (Bug #30302)
Status: `DHCP_CREATE_RAND_XID` macro added
The lwIP core provides the mechanism for randomizing XIDs
The actual randomness quality depends on the platform-specific `LWIP_RAND()` implementation in the port layer
Fallback when `LWIP_RAND` is undefined
Status: Unfixed (all versions)
The behavior of falling back to sequential increment from `0xABCD0001` when `LWIP_RAND` is undefined remains in the upstream code checked at the time of writing
Default status in major vendor SDKs
STM32Cube (F4/F7/H7): `rand()` — potentially vulnerable if unseeded
TI AM243x SDK: `rand()` — potentially vulnerable if unseeded
AMD Vitis: `rand()` — potentially vulnerable if unseeded
Renesas RX (with TSIP): hardware RNG — fixed
Renesas RX (without TSIP): falls back to `rand()` — potentially vulnerable if unseeded
■ Summary
Related bug: Bug #30302 (lwip-devel, 2010)
Symptom: The IP address, gateway, and DNS server obtained via DHCP can potentially be overwritten by an attacker
Root cause: The DHCP XID source is predictable when `LWIP_RAND()` uses an unseeded `rand()`, or when `LWIP_RAND` is undefined and lwIP falls back to sequential XID generation.
Trigger conditions: `LWIP_RAND` undefined, or `rand()` used without seeding. Actual impact depends on network topology: same L2 segment, DHCP relay environment, presence of DHCP snooping, etc.
Fix: ① Implement `LWIP_RAND()` directly with hardware RNG; ② Initialize `srand()` with hardware RNG at boot (no change to `LWIP_RAND()` needed); ③ If hardware RNG is unavailable, combine UID and other sources to provide entropy to `srand()`
Official response: lwIP core has added the mechanism to call `LWIP_RAND()`. However, the implementation of `LWIP_RAND()` is platform-dependent, and multiple major vendor SDKs still define it as `rand()`. Upgrading the lwIP version alone does not resolve the issue
CVE: No dedicated CVE is known to the author as of this writing (an implementation-dependent design risk not detectable by SBOM scanners; developers must manually verify the `cc.h` implementation)
Related files: `dhcp.c` (`dhcp_create_msg` function), `dhcp6.c` (IPv6 environments), `arch/cc.h`
■ Disclaimer
Purpose: This article is intended to analyze the DHCP XID generation behavior in lwIP and major vendor SDKs, and to share an understanding of the issue and how to address it.
Verification: The content presented is based on lwIP source code analysis, but does not guarantee behavior under all hardware configurations or compilation conditions.
Verification required: When applying fixes, conduct sufficient verification and regression testing based on your project's requirements before using them in production.
Limitation of liability: The author assumes no responsibility for any damages or issues arising from the information in this article.
■ Closing Note
This article covered the issue of predictable DHCP transaction IDs (XIDs) that arises when the `LWIP_RAND()` implementation in lwIP's port layer (`cc.h`) is insufficient.
As shown here, lwIP's behavior can vary significantly depending not only on the core code, but also on the vendor SDK and port layer implementation. Other articles in this series analyze real-world bugs in detail, covering reproduction steps, source code analysis, and minimal fix strategies.
【lwIP】Common Bugs, Vulnerabilities and Fixes | Embedded Engineer's Field Notes
lwIP is free and easy to integrate into products, but using it responsibly requires understanding the version in use, its configuration, and known issues.
As an OSS project without commercial support guarantees, the product manufacturer ultimately bears the responsibility for handling bugs and vulnerabilities.
The risk of support costs escalating after product shipment is something worth planning for in advance.
lwIP offers many advantages as an open-source stack, but for products where long-term maintenance and formal support are critical, a commercial TCP/IP stack may be the right choice.
That said, replacing lwIP in a product already in development or already shipped is rarely practical.
For those engineers, this article's information may be useful for:
Early detection of bugs and vulnerabilities
Reducing root cause investigation time
Lowering the cost of remediation
More articles coming. Stay tuned.
▶ English article index: lwIP Troubleshooting Notes
