← Back to Writeups
CUSTOMEasyReversing

Randomness

XESXOR9/10/20264 min read
#reversing#custom#easy#docker_amd64_emulation#dynamic_analysis#prng_prediction#time_based_seed_reproduction#linux

Randomness

Platform: spbctf | Category: Reversing | Type: Challenge | Difficulty: Easy | OS: Linux | Author: XESXOR | Date: 2026-06-24 | Status: Solved Techniques: docker_amd64_emulation, dynamic_analysis, prng_prediction, time_based_seed_reproduction

Summary

Task: an x86-64 C binary gates the flag behind a 7-char check where each expected char is random() % 26 + 'a' from a time-seeded glibc PRNG. Solution: reproduce srandom(time(0)) in the same second, pipe predicted chars to pass the gate, and let the binary print the flag itself instead of reversing its bytecode VM.

Randomness — spbctf (rev-kids20)

Description

Flag is the input that makes the app not say 'Wrong' :)

We are given randomness.elf, an x86-64 ELF binary. It prompts for 7 letters; if all of them match an internally generated sequence, it prints the flag instead of saying "Wrong".

Analysis

Binary overview

  • randomness.elf: ELF 64-bit LSB executable, x86-64, dynamically linked, not stripped, GNU/Linux.
  • Imports: srandom, random, time, getchar, printf, puts.
  • Notable symbols: main @ 0x400f2c, genflag @ 0x4007c6, data _1_genflag_$array @ 0x602080, _1_genflag_$strings @ 0x6026c0.
  • Strings in .rodata: "Guess random english letter: ", "Wrong", "%s\n". ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

main logic (@ 0x400f2c)

c
srandom(time(0));
for (i = 0; i <= 6; i++) {              // 7 iterations
    long r = random();
    char expected = (r % 26) + 'a';     // compiled as multiply by 0x4ec4ec4ec4ec4ec5, sar 3 (division by 26)
    printf("Guess random english letter: ");
    int guess = getchar();              // read the guess char
    getchar();                          // consume the trailing newline
    if (expected != (char)guess) {
        puts("Wrong");
        return 1;
    }
}
// only reached if all 7 chars are correct:
for (i = 0;  i <= 6;  i++)  genflag(i);   // first VM pass
for (i = 7;  i <= 26; i++)  genflag(i);   // second VM pass
printf("%s\n", 0x6026e0);                 // prints the generated flag buffer
return 0;

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

The % 26 appears in the disassembly as a signed multiply by the magic constant 0x4ec4ec4ec4ec4ec5 followed by sar 3 — the standard compiler idiom for division/modulo by a constant (26).

genflag (@ 0x4007c6) — the red herring

genflag is a small stack-based bytecode VM. It reads opcodes from _1_genflag_$array (@ 0x602080) and dispatches through a jump table at 0x4010d0:

asm
mov rax, [8*opcode + 0x4010d0]
jmp rax

It maintains a value stack (sp in [rbp-0x160], base near [rbp-0x140]) and supports push/pop/dup/swap, arithmetic (add, imul, xor, shl), comparisons (setl, sete), conditional jumps, and load/store of various widths. It writes characters one at a time into an output buffer at 0x6026e0. This VM is pure obfuscation: it constructs the flag string REDACTED into 0x6026e0, which main then prints. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Key insight

The 7-character "password" gate is entirely PRNG-driven. Each expected char is random() % 26 + 'a', and the PRNG is seeded with srandom(time(0)). Because the seed is the current Unix time in seconds, the expected sequence is fully predictable: reproduce the same srandom(time(0)) with the same glibc inside the same wall-clock second, and you know all 7 answers.

You never have to reverse the VM — once the gate is passed, the program prints the real flag itself.

Solution

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

  1. Write a tiny C helper that reproduces the gate sequence using the same glibc PRNG.
  2. Each guess needs a char plus a newline (main calls getchar() twice per iteration — one for the guess, one to eat the newline).
  3. Pipe the helper's output into the binary within the same wall-clock second so the time(0) seed matches.

solve.c:

c
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main() {
    time_t t = time(0);
    srandom(t);
    for (int i = 0; i < 7; i++)
        printf("%c\n", (char)(random() % 26 + 'a'));  // char + newline per guess
    return 0;
}

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

Run on Linux x86-64. On Apple Silicon, force --platform linux/amd64 (Rosetta otherwise fails with failed to open elf at /lib64/ld-linux-x86-64.so.2):

bash
docker run --rm --platform linux/amd64 -v "$PWD":/w -w /w gcc:12 bash -c '
  gcc -O0 solve.c -o solve
  chmod +x randomness.elf
  ./solve | ./randomness.elf
'

Output:

Guess random english letter: Guess random english letter: ... (x7)
REDACTED

The helper and the binary share the same time-seeded glibc PRNG, so all 7 predicted characters match, the gate passes, and the binary prints the flag. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍