خطوات آمنة

تقييمات الأمن السيبراني، ورؤى معمقة، وحلول واضحة للمعالجة.

Category:

Sleepy CPU — Power-Trace Flag Recovery | NNS CTF

Sleepy CPU — Power-Trace Flag Recovery

Platform: NNS CTF 2026
Category: Miscellaneous / Hardware Forensics

1. The misleading source flag

The challenge includes a Zephyr application and a flag.h file. The header contains a decoy value, so reading the source string is not a valid solution. The real behavior is visible in the supplied Joulescope recording, sleepy_cpu.jls.

2. What the firmware leaks

The application loops through the flag one character at a time:

for (char* c = flag; *c; c++) {
    for (int i = 0; i < 100000; i++) {
        __asm volatile ("nop");
    }
    k_sleep(K_MSEC(*c));
}

Each character creates two observable phases. First, the processor performs 100,000 nop instructions, producing a repeatable high-current region. It then sleeps for a number of milliseconds equal to the ASCII value of the current character. The low-current gap is therefore a direct timing encoding of the byte.

3. Reading the trace

The trace contains a 50 kHz signal, so there are 50 samples per millisecond. A current threshold of 0.0016 A separates the high-current instruction loops from the quiet sleep intervals. The decoder marks transitions into and out of high-current regions and keeps runs longer than 250 samples to reject noise.

If end_previous is the end of one high-current run and start_current is the beginning of the next, then:

gap_samples = start_current - end_previous
character = round(gap_samples / 50)

The first high-current run has no preceding character gap. Every following run contributes one decoded character. A final high-current run after the last sleep acts as a terminator and makes the last character measurable.

4. Reproduction

import pyjls, numpy as np
r = pyjls.Reader('misc_sleepy-cpu/sleepy_cpu.jls')
x = r.fsr(2, 0, r.signals[2].length)
high = x > 0.0016
edges = np.diff(np.r_[False, high, False].astype(np.int8))
starts = np.flatnonzero(edges == 1)
ends = np.flatnonzero(edges == -1)
runs = [(a, z) for a, z in zip(starts, ends) if z-a > 250]
vals = [round((runs[i][0] - runs[i-1][1]) / 50)
        for i in range(1, len(runs))]
print(''.join(chr(v) for v in vals))

The preserved decode.py prints the number of detected runs, each gap, each candidate character, and the final decoded string. It identifies 58 regions and 57 inter-region gaps.

5. Verified result

NNS{pow3r_4n4lys15_c4n_rev3al_what_th3_cpu_i5_w0rk1ng_on}

6. Why this works

The program does not need to intentionally transmit the flag. The current draw of the CPU is correlated with its instruction and sleep state, and the sleep duration is controlled directly by the secret byte. This is a timing and power side channel: the measurement device becomes the output channel.

7. Defensive lessons

  • Do not perform secret-dependent sleeps on observable hardware.
  • Use constant-time and constant-power techniques where practical.
  • Keep sensitive operations away from externally measurable power rails.
  • Do not assume that removing serial output removes information leakage.
  • Validate suspicious “decoy” source values against runtime measurements.
ar
Secure Steps
Privacy Overview

This website uses cookies so that we can provide you with the best user experience possible. Cookie information is stored in your browser and performs functions such as recognising you when you return to our website and helping our team to understand which sections of the website you find most interesting and useful.