Operation Deadlock
Operation Deadlock
Platform: duckerz | Category: Hardware | Type: Challenge | Difficulty: Medium | OS: Windows | Author: XESXOR | Date: 2026-04-19 | Status: Solved Techniques: firmware_register_manipulation, simple_power_analysis, spi_fault_injection, trace_window_comparison
Summary
Task: recover a 4-digit PIN from SPI power traces, then modify slave firmware to crash the master device. Solution: use simple power analysis to map each PIN digit by windowed current spikes, then flash 0xFFFFFFFF into CORE_TEMP to trigger thermal shutdown and reveal the flag.
Operation Deadlock — duckerz
Description
Операция "тупик" Вам поручено проникнуть в закрытую встроенную систему, защищенную нераскрытым PIN-кодом. Кряк-кряк. Целевое устройство взаимодействует по шине SPI. Ваша задача состоит из двух частей: извлечь PIN-код с помощью анализа боковых каналов питания, а затем изменить встроенное ПО ведомого устройства, чтобы вызвать критическую ошибку в главном устройстве. Флажок находится на другой стороне. Кряк.
The challenge is a two-stage embedded task exposed through a browser UI and JSON API. First, we must recover a hidden 4-digit PIN from power traces. After that, we gain access to a 64-byte SPI slave firmware image and must modify it so the master device crashes.
Challenge URL: http://tasks.duckerz.ru:30043
Analysis
Stage 1: Power side-channel leak
The interesting endpoint is /api/stage1/probe. It accepts a JSON body with a guessed PIN:
json read-only • copy allowed{"guess": [0, 0, 0, 0]}
The response contains a CSV trace with 800 current samples. Looking at the frontend code shows the trace is visually split into four equal windows, which strongly suggests one 200-sample region per PIN digit.
After a few probes, the leak becomes obvious:
- non-matching windows peak around
~40mA - the matching digit window spikes to about
~100mA
That means we do not need to brute-force the full PIN. We can send ten probes of the form [d, d, d, d] for d = 0..9 and inspect each 200-sample window separately. If window i has a large spike, then digit i is equal to d.
So the full PIN can be reconstructed with only 10 exploratory requests plus 1 final correct submission.
Stage 2: SPI firmware fault injection
Once Stage 1 is solved, /api/stage2/init returns a 64-byte firmware image as an array of integers. The UI also reveals the firmware map:
0x00..0x03— read-only magic0x04..0x07— version info0x08..0x0B—CORE_TEMP0x0C..0x0F— SPI config0x10..0x1F— watchdog and reserved0x20..0x3F— identifier string
Polling /api/stage2/tick makes the master read CORE_TEMP. The default value is 00 00 00 00. If we overwrite bytes 0x08..0x0B with FF FF FF FF, flash the modified firmware through /api/stage2/flash, and tick once more, the master interprets that as a critical temperature and halts with 0xDEADBEEF.
The same response includes the flag.
Solution
Step 1: Recover the PIN from traces
For each digit d from 0 to 9:
- Send
guess=[d,d,d,d]to/api/stage1/probe - Parse the returned CSV
- Split the 800 samples into four windows of 200 samples each
- Check
max(window) - If the maximum exceeds a threshold such as
70, that position contains digitd
After the ten probes, submit the recovered PIN once to unlock Stage 2.
Step 2: Force thermal shutdown
- Call
/api/stage2/init - Take the returned firmware array
- Replace bytes
8..11with[0xFF, 0xFF, 0xFF, 0xFF] - Flash it with
/api/stage2/flash - Trigger the master logic with
/api/stage2/tick
The tick response returns the flag directly.
Solve Script
python read-only • copy allowed#!/usr/bin/env python3 import csv import io import requests URL = "http://tasks.duckerz.ru:30043" def solve_stage1(session): session.post(f"{URL}/api/session/init") pin = [None] * 4 for digit in range(10): resp = session.post(f"{URL}/api/stage1/probe", json={"guess": [digit] * 4}) data = resp.json() values = [float(row["current_mA"]) for row in csv.DictReader(io.StringIO(data["csv"]))] for idx in range(4): window = values[idx * 200:(idx + 1) * 200] if max(window) > 70: pin[idx] = digit resp = session.post(f"{URL}/api/stage1/probe", json={"guess": pin}) data = resp.json() return pin, data["pin"] def solve_stage2(session): firmware = session.post(f"{URL}/api/stage2/init").json()["firmware"] firmware[8:12] = [0xFF, 0xFF, 0xFF, 0xFF] session.post(f"{URL}/api/stage2/flash", json={"firmware": firmware}) return session.post(f"{URL}/api/stage2/tick").json()["flag"] def main(): session = requests.Session() pin, revealed = solve_stage1(session) flag = solve_stage2(session) print("PIN:", "".join(map(str, pin)), f"(server revealed: {revealed})") print("FLAG:", flag) if __name__ == "__main__": main()