Simulator scenario 1: Registry reconnaissance and initial ROA creation¶
High‑level objective¶
Before attempting any prefix hijack, understand and establish legitimacy in the RPKI system. That means:
Discover existing RPKI ROAs for the target prefix
Create a legitimate ROA for a prefix you control
Document the baseline so defenders (and your SIEM) see this as “normal” activity
Why this matters:
Later stages exploit weaknesses in RPKI validation
But without first establishing legitimacy, later hijacks look suspicious
So playbook 1 is about laying the groundwork without triggering alerts or suspicion This is a control‑plane attack strategy — not merely announcing forged prefixes, but shaping the validation infrastructure that tells routers what is “valid.” ([purple.tymyrddin.dev][1])
Key narrative:
“RPKI ROAs are meant to be the truth that routers check announcements against. If we can manipulate ROAs, we redefine what ‘valid’ means.”
This is different from a normal BGP hijack. It is a control‑plane preparation that makes later hijacks look valid.
Turning playbook 1 into a simulator scenario¶
Below is a suggested scenario definition (YAML) and telemetry mapping you can drop into the simulator. This is based on the essence of Playbook 1, boiled down to what matters for your simulator:
Simulator scenario¶
id: playbook1_rpki_recon
name: "Playbook 1: RPKI Reconnaissance and ROA Creation"
description: |
Control-plane attack preparation: Registry reconnaissance, legitimate ROA creation,
and baseline establishment before ROA poisoning. This is phase 1 of a 3-part chain
that poisons RPKI validation rather than bypassing it.
Timeline simulates:
- Action 1.1: RPKI infrastructure reconnaissance for target prefix
- Action 1.2: Create legitimate ROA for our own prefix
- Action 1.3: Baseline documentation and verification
Key insight: This is preparation, not attack. No hijack occurs in phase 1.
The attack happens in playbook 3 after RPKI validation is compromised.
timeline:
# === BASELINE: Target prefix operating normally ===
- t: 0
action: baseline_announcement
prefix: "203.0.113.0/24"
as_path: [65001, 65002, 65003]
origin_as: 65003
next_hop: "198.51.100.1"
peer_ip: "198.51.100.1"
peer_as: 65001
peer_bgp_id: "198.51.100.1"
communities: ["65003:100"]
attack_step: "baseline"
note: "Victim AS65003 legitimately announces their allocated prefix"
# === ACTION 1.1: RPKI Infrastructure Reconnaissance ===
- t: 60
action: rpki_query
prefix: "203.0.113.0/24"
origin_as: 65003
query_type: "ripe_stat_rpki_validation"
query_source: "https://stat.ripe.net/data/rpki-validation/data.json"
attack_step: "reconnaissance"
note: "Query RIPE Stat to check if victim has deployed RPKI ROAs"
- t: 65
action: rpki_validation_result
prefix: "203.0.113.0/24"
origin_as: 65003
rpki_state: "not_found"
roa_exists: false
attack_step: "reconnaissance"
note: "Discovery: No ROA exists for target prefix (victim hasn't deployed RPKI)"
- t: 70
action: whois_query
prefix: "203.0.113.0/24"
registry: "ARIN"
allocated_to: "AS65003"
attack_step: "reconnaissance"
note: "WHOIS query to identify RIR managing the allocation"
- t: 120
action: validator_query
prefix: "203.0.113.0/24"
validator: "routinator"
result: "no_roa_found"
attack_step: "reconnaissance"
note: "Cross-check with local Routinator validator"
# === ACTION 1.2: Create Legitimate ROA for Our Own Prefix ===
- t: 300
action: roa_creation_request
prefix: "198.51.100.0/24"
origin_as: 64513
max_length: 25
registry: "RIPE"
actor: "operator@attacker-as64513.net"
attack_step: "establish_presence"
note: "Submit ROA creation via RIPE hosted RPKI service for our legitimate allocation"
- t: 360
action: roa_accepted
prefix: "198.51.100.0/24"
origin_as: 64513
max_length: 25
registry: "RIPE"
attack_step: "establish_presence"
note: "RIPE accepts ROA creation request (typically instant)"
# === ROA Publication Lag (15-45 minutes typical) ===
- t: 1500
action: roa_published
prefix: "198.51.100.0/24"
origin_as: 64513
max_length: 25
trust_anchor: "ripe"
repository_url: "rsync://rpki.ripe.net/repository/"
attack_step: "establish_presence"
note: "ROA appears in RIPE's RPKI repository (20-minute publication cycle)"
- t: 1800
action: validator_sync
prefix: "198.51.100.0/24"
validator: "routinator"
rpki_state: "valid"
attack_step: "establish_presence"
note: "Routinator polls RIPE repository and sees our new ROA"
- t: 1850
action: validator_sync
prefix: "198.51.100.0/24"
validator: "fort"
rpki_state: "valid"
attack_step: "establish_presence"
note: "FORT validator also sees our ROA (different polling schedule)"
- t: 1900
action: validator_sync
prefix: "198.51.100.0/24"
validator: "cloudflare"
rpki_state: "valid"
attack_step: "establish_presence"
note: "Cloudflare's validator confirms ROA visibility"
# === Our legitimate BGP announcement (now with RPKI validation) ===
- t: 2000
action: baseline_announcement
prefix: "198.51.100.0/24"
as_path: [65001, 64513]
origin_as: 64513
next_hop: "198.51.100.254"
peer_ip: "198.51.100.2"
peer_as: 65001
peer_bgp_id: "198.51.100.2"
rpki_state: "valid"
attack_step: "establish_presence"
note: "Our announcement now validates as VALID against our ROA"
# === ACTION 1.3: Baseline Documentation ===
- t: 2100
action: baseline_documented
target_prefix: "203.0.113.0/24"
target_as: 65003
target_roa_status: "not_found"
our_prefix: "198.51.100.0/24"
our_as: 64513
our_roa_status: "published_and_validated"
validators_checked: ["routinator", "fort", "cloudflare"]
publication_lag_observed: 1500
attack_step: "documentation"
note: "Baseline complete: Target has no RPKI, we have legitimate presence"
# === PHASE 1 COMPLETE - 7 DAY WAIT (compressed to 5 minutes for simulation) ===
- t: 2400
action: waiting_period_complete
days_elapsed: 7
attack_step: "operational_cover"
note: "7-day waiting period establishes us as normal long-term RPKI participant (compressed for simulation)"
# === Verification: Target status unchanged ===
- t: 2460
action: rpki_query
prefix: "203.0.113.0/24"
origin_as: 65003
rpki_state: "not_found"
attack_step: "verification"
note: "Re-verify target still has no ROA (unchanged baseline)"
- t: 2520
action: phase1_complete
attack_step: "completion"
note: "Phase 1 success criteria met. Ready for Phase 2 (ROA expansion)"
Why these three events?
rpki_recon→ a reconnaissance lookup of ROA statecreate_roa→ actually creating your own RPKI ROAbaseline_log→ produce logs that document the status quo
These map to the three phase‑1 actions described in the playbook.
Telemetry for playbook 1¶
This mapping script decides what telemetry to emit for each type of event in scenario.yaml.
"""
Telemetry mapping for Playbook 1: RPKI Reconnaissance and ROA Creation.
Control-plane attack preparation showing:
- RPKI infrastructure reconnaissance (Action 1.1)
- Legitimate ROA creation and publication (Action 1.2)
- Baseline documentation (Action 1.3)
This playbook generates realistic multi-source telemetry for each action,
demonstrating what defenders would see during the reconnaissance phase.
"""
"""
Telemetry mapping for Playbook 1: RPKI Reconnaissance and ROA Creation.
Phase 1 of a multi-stage control-plane operation:
- RPKI reconnaissance for target prefix
- Legitimate ROA creation for our allocation
- Baseline documentation
- Waiting period to establish normality
"""
from typing import Any
from simulator.engine.event_bus import EventBus
from simulator.engine.clock import SimulationClock
# from telemetry.generators.bmp_telemetry import BMPTelemetryGenerator
from telemetry.generators.router_syslog import RouterSyslogGenerator
def register(event_bus: EventBus, clock: SimulationClock, scenario_name: str) -> None:
"""Register telemetry generators for Playbook 1."""
# bmp_gen = BMPTelemetryGenerator(
# scenario_id=scenario_name,
# scenario_name="Playbook 1: RPKI Reconnaissance and ROA Creation",
# clock=clock,
# event_bus=event_bus,
# )
syslog_gen = RouterSyslogGenerator(
clock=clock,
event_bus=event_bus,
router_name="edge-router-01",
scenario_name=scenario_name,
)
def on_timeline_event(event: dict[str, Any]) -> None:
entry = event.get("entry")
if not entry:
return
action = entry.get("action")
attack_step = entry.get("attack_step", "unknown")
prefix = (
entry.get("prefix")
or entry.get("target_prefix")
or entry.get("our_prefix")
or "unknown"
)
incident_id = f"{scenario_name}-{attack_step}-{prefix}"
# === BASELINE ANNOUNCEMENTS ===
if action == "baseline_announcement":
syslog_gen.emit(
message=f"BGP announcement observed: {prefix} origin AS{entry.get('origin_as')}",
severity="info",
subsystem="bgp",
scenario={
"name": scenario_name,
"attack_step": attack_step,
"incident_id": incident_id,
},
)
# === ACTION 1.1: RPKI Reconnaissance ===
elif action == "rpki_query":
event_bus.publish({
"event_type": "rpki.query",
"timestamp": clock.now(),
"source": {"observer": entry.get("query_source")},
"attributes": {
"prefix": prefix,
"origin_as": entry.get("origin_as"),
"query_type": entry.get("query_type"),
},
"scenario": {
"name": scenario_name,
"attack_step": attack_step,
"incident_id": incident_id,
},
})
elif action == "rpki_validation_result":
event_bus.publish({
"event_type": "rpki.validation",
"timestamp": clock.now(),
"source": {"observer": entry.get("validator", "routinator")},
"attributes": {
"prefix": prefix,
"origin_as": entry.get("origin_as"),
"validation_state": entry.get("rpki_state"),
"roa_exists": entry.get("roa_exists"),
},
"scenario": {
"name": scenario_name,
"attack_step": attack_step,
"incident_id": incident_id,
},
})
elif action == "validator_query":
event_bus.publish({
"event_type": "rpki.query",
"timestamp": clock.now(),
"source": {"observer": entry.get("validator")},
"attributes": {
"prefix": prefix,
"query_type": "local_validator_check",
},
"scenario": {
"name": scenario_name,
"attack_step": attack_step,
"incident_id": incident_id,
},
})
elif action == "whois_query":
event_bus.publish({
"event_type": "registry.whois",
"timestamp": clock.now(),
"source": {"observer": "whois-client"},
"attributes": {
"prefix": prefix,
"allocated_to": entry.get("allocated_to"),
"registry": entry.get("registry"),
},
"scenario": {
"name": scenario_name,
"attack_step": attack_step,
"incident_id": incident_id,
},
})
# === ACTION 1.2: Legitimate ROA Creation ===
elif action == "roa_creation_request":
event_bus.publish({
"event_type": "rpki.roa_creation",
"timestamp": clock.now(),
"source": {"observer": "rpki-portal"},
"attributes": {
"prefix": prefix,
"origin_as": entry.get("origin_as"),
"max_length": entry.get("max_length"),
"registry": entry.get("registry"),
"actor": entry.get("actor"),
},
"scenario": {
"name": scenario_name,
"attack_step": attack_step,
"incident_id": incident_id,
},
})
elif action == "roa_accepted":
syslog_gen.emit(
message=f"ROA request accepted for {prefix} AS{entry.get('origin_as')}",
severity="notice",
subsystem="rpki",
scenario={
"name": scenario_name,
"attack_step": attack_step,
"incident_id": incident_id,
},
)
elif action == "roa_published":
event_bus.publish({
"event_type": "rpki.roa_published",
"timestamp": clock.now(),
"source": {"observer": "rpki-repository"},
"attributes": {
"prefix": prefix,
"origin_as": entry.get("origin_as"),
"trust_anchor": entry.get("trust_anchor"),
},
"scenario": {
"name": scenario_name,
"attack_step": attack_step,
"incident_id": incident_id,
},
})
elif action == "validator_sync":
syslog_gen.emit(
message=f"Validator {entry.get('validator')} sees prefix {prefix} as {entry.get('rpki_state')}",
severity="info",
subsystem="rpki",
scenario={
"name": scenario_name,
"attack_step": attack_step,
"incident_id": incident_id,
},
)
# === ACTION 1.3: Baseline Documentation ===
elif action == "baseline_documented":
event_bus.publish({
"event_type": "internal.documentation",
"timestamp": clock.now(),
"source": {"observer": "operator"},
"attributes": {
"target_prefix": entry.get("target_prefix"),
"target_roa_status": entry.get("target_roa_status"),
"our_roa_status": entry.get("our_roa_status"),
},
"scenario": {
"name": scenario_name,
"attack_step": attack_step,
"incident_id": incident_id,
},
})
# === PHASE TRANSITIONS ===
elif action == "waiting_period_complete":
event_bus.publish({
"event_type": "internal.phase_transition",
"timestamp": clock.now(),
"source": {"observer": "operator"},
"attributes": {
"phase": "phase_1_wait_complete",
"days_elapsed": entry.get("days_elapsed"),
},
"scenario": {
"name": scenario_name,
"attack_step": attack_step,
"incident_id": incident_id,
},
})
elif action == "phase1_complete":
event_bus.publish({
"event_type": "internal.phase_complete",
"timestamp": clock.now(),
"source": {"observer": "operator"},
"attributes": {"phase": "phase_1"},
"scenario": {
"name": scenario_name,
"attack_step": attack_step,
"incident_id": incident_id,
},
})
# Subscribe to all timeline events
event_bus.subscribe(on_timeline_event)
This mirrors Phase 1 actions without making anything malicious.
Expected output¶
When you run:
python -m simulator.cli simulator/scenarios/easy/playbook1/scenario.yaml --mode practice --output cli
Your SIEM could see logs like (if it is edible):
<14>Jan 01 00:00:00 edge-router-01 BGP announcement observed: 203.0.113.0/24 origin AS65003
<30>Jan 01 00:01:00 https://stat.ripe.net/data/rpki-validation/data.json RPKI query: 203.0.113.0/24 AS65003 (ripe_stat_rpki_validation)
<30>Jan 01 00:01:05 routinator RPKI validation: 203.0.113.0/24 origin AS65003 -> not_found (ROA not found)
<30>Jan 01 00:01:10 whois-client WHOIS query: 203.0.113.0/24 allocated to AS65003 via ARIN
<30>Jan 01 00:02:00 routinator RPKI query: 203.0.113.0/24 ASNone (local_validator_check)
<29>Jan 01 00:05:00 rpki-portal ROA creation request: 198.51.100.0/24 origin AS64513 maxLength /25 by operator@attacker-as64513.net via RIPE
<13>Jan 01 00:06:00 edge-router-01 ROA request accepted for 198.51.100.0/24 AS64513
<30>Jan 01 00:25:00 rpki-repository ROA published: 198.51.100.0/24 origin AS64513 in ripe repository
<14>Jan 01 00:30:00 edge-router-01 Validator routinator sees prefix 198.51.100.0/24 as valid
<14>Jan 01 00:30:50 edge-router-01 Validator fort sees prefix 198.51.100.0/24 as valid
<14>Jan 01 00:31:40 edge-router-01 Validator cloudflare sees prefix 198.51.100.0/24 as valid
<14>Jan 01 00:33:20 edge-router-01 BGP announcement observed: 198.51.100.0/24 origin AS64513
<30>Jan 01 00:41:00 None RPKI query: 203.0.113.0/24 AS65003 (None)
These events together create a believable baseline before later playbooks alter RPKI and launch hijacks.