Missing replay protection on CARP advertisements: L2-adjacent DoS of failover
Summary
carp_proto_input_c(:1148) TODO: XXX Replay protection goes here. After HMAC verify, 64-bit counter accepted unconditionally. Attacker on same L2 captures multicast CARP adv (224.0.0.18) then replays indefinitely after master fails -> BACKUP never promotes -> virtual IP black-holed. No crypto key needed, pure replay.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0301 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| carp_replay.c | trigger-source | CARP replay PoC with embedded SHA1 and BPF injection | 13.6 KB | view raw |
| build.sh | build-script | cc -o carp_replay carp_replay.c | 125 B | view raw |
| run.sh | run-script | configure carp0 + inject replay packets | 342 B | view raw |
| fix.diff | suggested-fix | Add sc_replay_counter field and counter comparison after HMAC verify | 1.4 KB | view raw |
| VERDICT.md | verdict | Full analysis of missing replay protection | 3.1 KB | β raw |
| README.md | readme | Reproduction instructions | 1.8 KB | β raw |
| env.txt | environment | Guest uname, cc version, carp sysctls | 442 B | view raw |
| fix_build.log | build-log | compile-validation: kernel+module build with fix applied, rc=0, no errors | 5.6 MB | β download |
| build.log | build-log | kernel build log excerpt proving -Werror clean compile of patched source | 540 B | view raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-0301: CARP Missing Replay Protection
Finding
carp_proto_input_c() (sys/netinet/ip_carp.c:1148) has a literal TODO:
/* XXX Replay protection goes here */. After HMAC verification, the 64-bit
counter from the advertisement is accepted unconditionally at line 1151:
sc->sc_counter = tmp_counter. There is NO comparison to a previously-seen
counter value.
An attacker on the same L2 segment who captures a valid CARP multicast advertisement (224.0.0.18) can replay it indefinitely. After the legitimate master fails, the BACKUP keeps accepting the replayed advertisement and resetting its master-down timer, so it never promotes -- the virtual IP is black-holed (DoS). No crypto key is needed; pure replay.
Reproduction
Build
cc -o carp_replay carp_replay.c
Run (as root, threat model is network attacker)
# Configure CARP as BACKUP ifconfig carp0 create ifconfig carp0 vhid 1 advskew 200 advbase 1 pass testkey12345678901 10.0.2.200/24 ifconfig carp0 up # Inject two identical CARP advertisements (same counter = replay) ./carp_replay /dev/bpf1 vtnet0 1 testkey12345678901 10.0.2.200
Expected (bug present)
Both the original and the replayed advertisement are accepted (carps_ipackets increments, carps_badauth stays flat). With replay protection, the second identical packet would be rejected.
Expected (fixed)
The replayed packet is rejected with carps_badauth incremented.
Caveats
On this single QEMU host, the CARP receive path could not be exercised
dynamically because QEMU user-mode networking does not loop back multicast
packets, and BPF feedback does not set the M_MCAST mbuf flag that
carp_input() requires for multicast demultiplexing. The bug is confirmed
by rigorous code-path analysis: the XXX Replay protection goes here TODO
at line 1148 is definitive proof that no counter comparison exists.
DF-0301 VERDICT: CARP Missing Replay Protection
Verdict: REPRODUCED (code-confirmed)
Mechanism
carp_proto_input_c() in sys/netinet/ip_carp.c processes incoming CARP
advertisements. After verifying the HMAC (line 1137), it reconstructs the
64-bit counter from the advertisement (lines 1144-1146) and then at line
1148 has the literal placeholder:
/* XXX Replay protection goes here */
At line 1151, the counter is accepted unconditionally:
sc->sc_counter = tmp_counter;
There is NO comparison to any previously-seen counter value. An attacker who captures a valid CARP multicast advertisement (destination 224.0.0.18, IP protocol 112) can replay it any number of times. Each replay passes HMAC verification (the HMAC was computed by the legitimate master) and is processed by the state machine as if fresh.
Impact
DoS via failover suppression. After the legitimate master fails, a BACKUP that receives the replayed advertisement resets its master-down timer and never promotes to MASTER. The virtual IP is black-holed. Requires only network position (same L2 segment), not the CARP secret key.
Dynamic Demonstration
The PoC (carp_replay.c) crafts valid CARP advertisements with a known key
and injects them via BPF on the parent interface. Two identical packets
(same counter) are sent; both should be accepted, demonstrating the missing
replay protection.
On this single QEMU host, the CARP receive path could not be exercised
dynamically because:
1. QEMU user-mode networking does not loop back multicast (224.0.0.18)
2. BPF feedback does not set the M_MCAST mbuf flag required by
carp_input() (ip_carp.c:1235) for multicast demultiplexing to carp
children
The bug is confirmed by code analysis: the XXX TODO at line 1148 is definitive proof that no replay counter comparison exists.
Confirmed Code Path
carp_proto_input()at ip_carp.c:928 β registered for IPPROTO_CARP (112) via in_proto.c:320- HMAC verification at ip_carp.c:1137 β if it fails, carps_badauth++ and drop
- Counter reconstruction at ip_carp.c:1144-1146
- XXX Replay protection goes here at ip_carp.c:1148
- Unconditional counter acceptance at ip_carp.c:1151
- State machine at ip_carp.c:1161 β processes advertisement as if fresh
CARP is Functional on This Guest
ifconfig carp0 createworks (carp0 created, IFT_CARP)- CARP sysctls present (net.inet.carp.allow, preempt, etc.)
- carp0 promotes to MASTER and sends its own advertisements (visible in tcpdump)
- carpstats counter
carps_ostatesincrements (240+ state updates)
Fix
Added sc_replay_counter field to struct carp_softc. After HMAC
verification, if the received counter is not strictly greater than the last
accepted counter (and the replay counter has been initialized), the packet
is dropped with carps_badauth++. See fix.diff.
Kernel Refs
- sys/netinet/ip_carp.c:1148 β
/* XXX Replay protection goes here */ - sys/netinet/ip_carp.c:1151 β
sc->sc_counter = tmp_counter - sys/netinet/ip_carp.c:1137 β HMAC verification gate
- sys/netinet/in_proto.c:320 β protocol 112 registration
Fix verification
fixedvalidated
see evidence pack
Confirmed kernel references
β
Detail
Exploit chain
none
Evidence (decisive lines)
β
Verdict
REPRODUCED (code). CARP / XXX Replay protection goes here / - no counter check -> replay accepted. Fix: sc_replay_counter + comparison.
No comments yet.