TP-02_authentication

TP-02 — Authentication & Key Agreement Tests

Domain: Authentication and Key Agreement
Standards: 3GPP TS 33.501 §6.1 · 3GPP TS 33.401 §6.1 · NIST SP 800-187 §4.3
Prerequisites: TP-00 complete; 5G SA lab running; TC-REG-01 passed


TC-AUTH-01: 5G-AKA XRES* Verification (Tampered RES*)

Threat Model

sequenceDiagram
    participant ATK as Attacker
    participant UE as UE (UERANSIM)
    participant AMF as AMF
    participant AUSF as AUSF
    participant UDM as UDM

    Note over ATK,UDM: THREAT VECTOR: MitM intercepts and modifies RES*
    AMF->>UE: Authentication Request (RAND, AUTN)
    UE->>UE: Compute RES* = f(K, RAND, AUTN)

    ATK->>ATK: Intercept NAS AuthResponse
Flip 1 bit in RES* ATK->>AMF: Modified AuthResponse (RES*_tampered) AMF->>AUSF: POST Nausf_UEAuthentication_Authenticate\n{RES*_tampered} AUSF->>AUSF: Compute HXRES* from stored XRES*\nCompare with RES*_tampered Note over AUSF: HXRES* ≠ hash(RES*_tampered)
Authentication FAILS AUSF-->>AMF: 403 Forbidden (MAC failure) AMF->>UE: Authentication Reject (cause: MAC failure 0x14) Note over ATK: ❌ Attack BLOCKED\nNo session allocated Note over UE,UDM: ✅ MITIGATION: XRES* binding
prevents RES* forgery

Objective

Verify AMF/AUSF rejects a tampered RES* and issues Authentication Reject.

Steps

# Method A: Use pycrate to craft tampered NAS response
python3 << 'PYEOF'
from pycrate.mobile.NAS5G import *
from pycrate.core.utils import *

# Build a fake Authentication Response with wrong RES* (all zeros)
msg = FGMMAuthenticationResponse()
msg['5G-GUTI'].set_val({'type': 'SUCI'})
msg['AuthenticationResponseParameter'].set_val(b'\x00' * 16)  # wrong RES*
print("Crafted tampered AuthResponse:", msg.hex())
PYEOF

# Method B: Modify UERANSIM source to inject wrong RES*
# In UERANSIM UE config, change auth key by one hex digit (wrong K)
# This causes derived RES* to be wrong

# 1. Create ue-wrongkey.yaml with invalid OPc
cp ~/open5gs-lab/5g/config/ue.yaml /tmp/ue-wrongkey.yaml
sed -i 's/E8ED289DEBA952E4283B54E88E6183CA/E8ED289DEBA952E4283B54E88E6183CB/' /tmp/ue-wrongkey.yaml

# 2. Start UE with wrong key
docker run --rm --cap-add=NET_ADMIN \
  --network open5gs-lab_ran \
  -v /tmp/ue-wrongkey.yaml:/etc/ueransim/ue.yaml \
  louisroyer/ueransim-ue:latest &

# 3. Monitor AMF for rejection
sleep 8
docker logs open5gs-amf 2>&1 | grep -E "Authentication|Reject|MAC"
docker logs open5gs-ausf 2>&1 | grep -E "failed|reject|error"

# Expected: "Authentication failed" or "MAC failure"

Expected Results

Pass Criteria

Tampered RES* always rejected. Zero false accepts observed across 5 runs.


TC-AUTH-02: NAS Null-Cipher Downgrade Attempt (EEA0/EIA0)

Threat Model

sequenceDiagram
    participant ATK as Attacker (Rogue gNB)
    participant UE as UE
    participant AMF as AMF (Open5GS)

    Note over ATK,AMF: THREAT VECTOR: Downgrade to null algorithms
enables plaintext NAS interception UE->>ATK: Registration Request (UE Security Capability:\nNIA0, NIA1, NIA2 / NEA0, NEA1, NEA2) ATK->>AMF: Forward Registration Request AMF->>AMF: Select algorithms from policy Note over AMF: Policy check: Is NIA0 allowed? alt AMF MISCONFIGURED (allows NIA0) AMF-->>UE: Security Mode Command\n(Selected: NIA0=NULL integrity, NEA0=NULL cipher) UE-->>AMF: Security Mode Complete (unprotected) Note over ATK: ❌ CRITICAL: NAS traffic\ncompletely unprotected Note over ATK: Attacker intercepts all NAS messages\nin plaintext else AMF CORRECTLY CONFIGURED (rejects NIA0) AMF-->>UE: Registration Reject\n(cause: #9 UE identity not derived) Note over UE: ✅ Connection refused\nNo null-integrity session end Note over ATK,AMF: MITIGATION: Set integrity_order in amf.yaml\nto exclude NIA0 for non-emergency

Objective

Verify AMF rejects null-integrity algorithm negotiation in non-emergency mode.

Steps

# 1. Check current AMF security policy
docker exec open5gs-amf cat /etc/open5gs/amf.yaml | grep -A10 "security"

# 2. Modify UERANSIM UE to advertise ONLY null algorithms
cp ~/open5gs-lab/5g/config/ue.yaml /tmp/ue-null-algo.yaml
# Add to ue.yaml under 'integrity:' section:
cat >> /tmp/ue-null-algo.yaml << 'EOF'
# Override security algorithms — null only
integrity:
  - NIA0
ciphering:
  - NEA0
EOF

# 3. Attempt registration with null-only capability
docker run --rm --cap-add=NET_ADMIN \
  --network open5gs-lab_ran \
  -v /tmp/ue-null-algo.yaml:/etc/ueransim/ue.yaml \
  louisroyer/ueransim-ue:latest &
sleep 8

# 4. Check what algorithm was selected
docker logs open5gs-amf 2>&1 | grep -E "NIA|NEA|Security Mode|algorithm"

# 5. Check if UE registered (should NOT if AMF policy is correct)
docker logs open5gs-amf 2>&1 | grep "is registered"

# 6. HARDENING: Ensure NIA0 excluded from AMF policy
# In amf.yaml:
#   security:
#     integrity_order: [ NIA2, NIA1 ]   # NIA0 NOT listed
#     ciphering_order: [ NEA0, NEA2, NEA1 ]

Expected Results (secure config)

Finding

If AMF accepts NIA0: CRITICAL FINDING — all NAS messages unprotected, trivially intercepted.

Pass Criteria

NIA0 never selected; any Registration with only NIA0 capability rejected.


TC-AUTH-03: Unprovisioned Subscriber Rejection

Threat Model

flowchart TD
    A["UE sends Registration Request\nIMSI: 001010000099999\n(NOT in database)"] --> B["AMF: NGAP InitialUEMessage"]
    B --> C["AMF calls UDM\nGET /nudm-sdm/v1/imsi-001010000099999/am-data"]
    C --> D{"UDM checks MongoDB"}
    D -->|Subscriber found| E["✅ Proceed with AKA"]
    D -->|NOT FOUND| F["UDM returns HTTP 404"]
    F --> G["AMF: 5GMM Registration Reject\ncause #11 PLMN not allowed\nor #3 Illegal UE"]

    G --> H{"Does any resource leak?"}
    H -->|Session partially created| I["❌ FINDING: Partial session\nresource exhaustion possible"]
    H -->|No leak| J["✅ Clean rejection\nNo resource allocated"]

    subgraph THREAT["Threat: Resource Exhaustion via Ghost Subscribers"]
        K["Attacker floods with\nnon-existent IMSIs\n→ UDM DB lookup storm"]
        K --> L["⚠️ UDM CPU spike\nMongoDB query flood"]
    end

    style I fill:#c0392b,color:#fff
    style J fill:#27ae60,color:#fff
    style L fill:#e67e22,color:#fff

Objective

Verify core cleanly rejects registration from a subscriber not in the database with no resource leakage.

Steps

# 1. Confirm IMSI 001010000099999 does NOT exist
docker exec open5gs-mongodb mongosh open5gs --eval \
  "db.subscribers.findOne({imsi: '001010000099999'})"
# Must return: null

# 2. Attempt registration with ghost IMSI
cp ~/open5gs-lab/5g/config/ue.yaml /tmp/ue-ghost.yaml
sed -i 's/imsi: .*/imsi: 001010000099999/' /tmp/ue-ghost.yaml

docker run --rm --cap-add=NET_ADMIN \
  --network open5gs-lab_ran \
  --name ueransim-ghost \
  -v /tmp/ue-ghost.yaml:/etc/ueransim/ue.yaml \
  louisroyer/ueransim-ue:latest &
sleep 10

# 3. Check rejection in AMF
docker logs open5gs-amf 2>&1 | grep -E "Reject|99999|not found"

# 4. Verify no session in SMF
docker logs open5gs-smf 2>&1 | grep "99999"
# Expected: nothing — SMF never called for unprovisioned IMSI

# 5. Verify no IP allocated
docker exec ueransim-ghost ip addr show uesimtun0 2>&1
# Expected: "Device uesimtun0 does not exist"

# 6. Check UDM HTTP response
docker logs open5gs-udm 2>&1 | grep "404"

Expected Results

Pass Criteria

Ghost IMSI cleanly rejected in < 5 seconds. No partial session, no IP leak.


TC-AUTH-04: Authentication Vector Replay Prevention

Threat Model

sequenceDiagram
    participant ATK as Attacker
    participant UE as UE
    participant AMF as AMF
    participant AUSF as AUSF
    participant UDM as UDM (SQN state)

    Note over ATK,UDM: THREAT VECTOR: Attacker captures and replays AV
to authenticate as UE without knowing K Note over ATK: Step 1: Record legitimate AV exchange AMF->>UE: Auth Request (RAND₁, AUTN₁) — captured by attacker UE-->>AMF: Auth Response (RES*₁) — captured by attacker Note over ATK: Step 2: Later, replay captured RAND₁/AUTN₁ ATK->>AMF: New Registration (same SUCI) AMF->>AUSF: Request new AV (SUCI) AUSF->>UDM: Get auth vectors UDM->>UDM: Generate new AV with SQN₂ (SQN₁ + 1) UDM-->>AUSF: AV with RAND₂, AUTN₂ (SQN₂) Note over ATK: Attacker sends old RES*₁ for new challenge ATK->>AMF: Auth Response (RES*₁ — from captured session) AMF->>AUSF: Verify (RES*₁) AUSF->>AUSF: HXRES*₂ ≠ hash(RES*₁)\nDifferent RAND was used Note over AUSF: ✅ Replay BLOCKED\nRAND per-session randomness prevents replay AUSF-->>AMF: Auth Failure AMF-->>ATK: Registration Reject Note over UDM: SQN monotonically increases\nAUTN contains SQN for UE-side replay check

Objective

Verify AUSF detects replayed authentication responses from prior sessions.

Steps

# 1. Complete a successful registration and capture the session
sudo tshark -i br-$(docker network ls --filter name=sbi -q) \
  -Y "ngap || nas-5gs" \
  -T fields -e frame.time -e nas-5gs.mm.message_type \
  -e nas-5gs.mm.auth_param_rand -e nas-5gs.mm.auth_param_autn \
  -e nas-5gs.mm.auth_param_res 2>/dev/null \
  -w ~/open5gs-lab/captures/tc-auth-04-session1.pcap &
CAPID=$!

# Start UE, let it register, then stop
docker compose -f ~/open5gs-lab/5g/docker-compose.yml restart ue
sleep 15
kill $CAPID

# 2. Extract RAND and RES* from first session
tshark -r ~/open5gs-lab/captures/tc-auth-04-session1.pcap \
  -Y "nas-5gs.mm.message_type == 0x56" \
  -T fields -e nas-5gs.mm.auth_param_rand -e nas-5gs.mm.auth_param_autn

# 3. Attempt to reuse the old RES* in a new registration
# Using pycrate to replay:
python3 << 'PYEOF'
# Conceptual replay — demonstrates the defense mechanism
# In a real attack, attacker injects old RES* in response to new RAND
# Defense: AUSF computes HXRES* from its own stored XRES* (tied to new RAND)
# Old RES* (tied to old RAND) will never match new HXRES*

# The SQN in AUTN also differs — UE would detect SQN out of sync
# and send Synchronization Failure back to network

print("Replay defense mechanism: per-session RAND ensures RES* is session-bound")
print("SQN in AUTN prevents AUTN replay: UE checks SQN > stored SQN")
PYEOF

# 4. Verify SQN increments in MongoDB after each auth
docker exec open5gs-mongodb mongosh open5gs --eval \
  "db.subscribers.findOne({imsi:'001010000000001'}, {security:1})" | grep sqn

Expected Results

Pass Criteria

Authentication vector is single-use; replay produces auth failure. SQN verified incrementing in DB.