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* forgeryObjective
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
- AUSF log: authentication verification failure
- AMF sends NAS
Authentication Reject(cause 0x14 MAC failure) - UE not registered; no IP assigned
- No PDU session created
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-emergencyObjective
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)
- AMF does not select NIA0
- UE registration rejected or falls back to NIA1/NIA2
- No
Security Mode Commandwith integrity algorithm = NIA0
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:#fffObjective
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
- AMF:
Registration Rejectwith 5GMM cause #11 or #3 - UDM returns HTTP 404 to AMF
- SMF never called; no session created
- No IP assigned to ghost UE
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 checkObjective
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
- Each new registration challenge uses a fresh RAND (cryptographically random)
- Old RES* cannot satisfy new HXRES* (bound to new RAND)
- SQN in AUTN increments in MongoDB after each successful auth
- Replayed AUTN rejected by UE if SQN ≤ stored SQN (Synchronization Failure)
Pass Criteria
Authentication vector is single-use; replay produces auth failure. SQN verified incrementing in DB.