neven@dimitri-dev:~$
cd ../hackultet-2026

$cat waste-of-time.mdReversingHackultet 2026

Waste of Time

A binary computes the flag through a deterministic billion-iteration loop that would take decades to actually run, so the fix is to simulate the same arithmetic without the sleeps.

  • #simulation
  • #anti-debugging
  • #red-herring

$man waste-of-time

What this one teaches:

  • Spotting when a program's real output doesn't actually depend on the slow or random-looking parts of what it does.
  • Reimplementing a loop's pure arithmetic in a faster language once its logic is understood, instead of waiting out the original.

Task description

A binary that computes and prints a 12-digit flag.

Why it’s called Waste of Time

main calls ptrace(PTRACE_TRACEME) first, a common way a binary makes itself exit immediately if a debugger is already attached, then runs its real computation loop one billion times, calling sleep(1) once or twice per iteration. At that rate the program takes on the order of 31 years to finish on its own; the name is literal.

What the loop actually does

Stepping through the disassembled loop by hand shows 12 single-byte state variables (g[0] through g[11], all starting at 0x41, the ASCII code for A) and two 32-bit integers (int1 = 5, int2 = 1), updated every iteration through a fixed sequence of additions, multiplications, XORs and a conditional branch every tenth iteration. One of the called functions reads ten kilobytes from /dev/urandom and writes part of it straight to standard output, visible noise while the program runs, but that random data is never read back into any of the twelve state bytes or the two integers; it only ever goes out, never back in.

Why that makes it solvable instantly

Since the only inputs that affect the twelve bytes are the fixed starting values and the loop counter itself, the entire billion-iteration computation is fully deterministic and depends on nothing external. Reimplementing the same arithmetic in C, with the sleep() calls and the /dev/urandom reads stripped out entirely, reproduces the exact same final state in about two seconds instead of three decades:

uint8_t g[12];
for (int i = 0; i < 12; i++) g[i] = 0x41;
uint32_t int1 = 5, int2 = 1;

for (uint32_t i = 0; i <= 999999999u; i++) {
    g[6] += 1;
    g[1] = g[4];
    uint32_t wc = (int1 * int2 + g[3]) % 100;
    g[8] += (uint8_t)wc;
    g[4] = g[9];
    g[9] += (uint8_t)int2;
    g[7] += 0x55;
    g[11] = (uint8_t)((i * int1) % 6);
    if (i % 10 == 0) {
        g[3] += 1;
        g[10] = g[5] & (uint8_t)int1;
        int2 = int1 | int2;
        g[2] = (uint8_t)((uint32_t)g[3] * int2);
        g[7] = g[8] + (uint8_t)i;
    }
    g[8] -= 1;
    int1 += int2 * (int2 + 1);
    int2 = int1 ^ (int2 * 2);
    g[0] -= (uint8_t)int2;
    g[5] = g[2];
}

The binary’s print routine has one more quirk worth replicating exactly: right before printing, a loop that’s meant to copy each byte into a scratch slot ends up leaving g[10] overwritten with g[11]’s value, which is why the last two printed digits always come out identical. Printing g[i] % 10 for each of the twelve bytes, in order, gives the flag.

Result

771561545533

What this one was testing

Everything slow or seemingly random in this binary, the billion sleeps, the /dev/urandom noise, the ptrace self-check, was a deliberate obstacle to running it as intended, not to understanding it. Once the loop’s actual dependencies were traced out by hand, none of those obstacles mattered: the real computation was small, pure, and fast to reproduce from scratch.

$cat flag.txt revealCTF2026[771561545533]

Written by Neven Marić. This writeup was written with the help of Claude Code (Anthropic).