Skip to content

SECURE_CONNECTION//PRESS[CTRL+J]FOR ROOT ACCESS

BACK TO INTEL
CryptoMedium

Fibodream

CTF writeup for Fibodream from Vianu CTF

//Fibodream

//Fibodream — Vianu CTF — Writeup

>TL;DR

  • Flag: Vianu_CTF{16bc18d0ca25efa5e7c28aeff7f11f21d4de4de9}
  • Summary: The provided ciphers.txt contains one SHA-256 hash per byte (each hash = sha256(single-character)). After decoding the hashes locally to a character-level ciphertext (length 51), the remote service acts as a per-position Caesar-like cipher: letters are shifted mod 26 (using a per-position key), digits are shifted mod 10 (per-position key), and punctuation like {, }, _ are unchanged. We recovered the keystream by querying the service with known plaintexts ('a'*n for letters and '0'*n for digits), then subtracted the keystream to obtain the cleartext flag.

>Files in this challenge

  • ciphers.txt — the provided list of 51 SHA-256 hashes (one per character).
  • decode_hashes.py — helper script that decodes ciphers.txt into the ciphertext by reversing SHA-256 on single bytes (uses a 0..255 single-byte preimage table).
  • solve.py — end-to-end solver that decodes the hashes locally and queries the remote service to pull the keystream and decrypt the flag.
cypher.txt 44bd7ae60f478fae1061e11a7739f4b94d1daf917982d33b6fc8a01a63f89c21 454349e422f05297191ead13e21d3db520e5abef52055e4964b82fb213f593a1 4c94485e0c21ae6c41ce1dfe7b6bfaceea5ab68e40a2476f50208e526f506080 e3b98a4da31a127d4bde6e43033f66ba274cab0eb7eb1c70ec41402bf6273dd8 a1fce4363854ff888cff4b8e7875d600c2682390412a8cf79b37d0b11148b0fa d2e2adf7177b7a8afddbc12d1634cf23ea1a71020f6a1308070a16400fb68fde 86be9a55762d316a3026c2836d044f5fc76e34da10e1b45feee5f18be7edb177 44bd7ae60f478fae1061e11a7739f4b94d1daf917982d33b6fc8a01a63f89c21 8ce86a6ae65d3692e7305e2c58ac62eebd97d3d943e093f577da25c36988246b 021fb596db81e6d02bf3d2586ee3981fe519f275c0ac9ca76bbcf2ebb4097d96 4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce 5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9 cd0aa9856147b6c5b4ff2b7dfee5da20aa38253099ef1b4a64aced233c9afe29 de7d1b721a1e0632b7cf04edf5032c8ecffa9f9a08492152b926f1a5a7e765d7 d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35 7902699be42c8a8e46fbbb4501726517e86b22c56a189f7625a6da49081b2451 3f79bb7b435b05321651daefd374cdc681dc06faa65e374e38337b88ca046dea ef2d127de37b942baad06145e54b0c619a1f22327b2ebbcfbec78f5564afe39d aaa9402664f1a41f40ebbc52c9993eb66aeb366602958fdfaa283b71e64db123 189f40034be7a199f1fa9891668ee3ab6049f82d38c68be70f596eab2e1857b7 6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b 2c624232cdd221771294dfbb310aca000a0df6ac8b66b696d90ef06fdefb64a3 62c66a7a5dd70c3146618063c344e531e6d4b59e379808443ce962b3abd63c5a 454349e422f05297191ead13e21d3db520e5abef52055e4964b82fb213f593a1 0bfe935e70c321c7ca3afc75ce0d0ca2f98b5422e008bb31c00c6d7f1f1c0ad6 19581e27de7ced00ff1ce50b2047e7a567c76b1cbaebabe5ef03f7c3017bb5b7 62c66a7a5dd70c3146618063c344e531e6d4b59e379808443ce962b3abd63c5a d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35 252f10c83610ebca1a059c0bae8255eba2f95be4d1d7bcfa89d7248a82d9f111 6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b ef2d127de37b942baad06145e54b0c619a1f22327b2ebbcfbec78f5564afe39d 148de9c5a7a44d19e56cd9ae1a554bf67847afb0c58f6e12fa29ac7ddfca9940 50e721e49c013f00c62cf59f2163542a9d8df02464efeb615d31051b0fddc326 8e35c2cd3bf6641bdb0e2050b76932cbb2e6034a0ddacc1d9bea82a6ba57f7cf 65c74c15a686187bb6bbf9958f494fc6b80068034a659a9ad44991b08c58f2d2 4b227777d4dd1fc61c6f884f48641d02b4d121d3fd328cb08b5531fcacdabf8a 4c94485e0c21ae6c41ce1dfe7b6bfaceea5ab68e40a2476f50208e526f506080 4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce e7f6c011776e8db7cd330b54174fd76f7d0216b612387a5ffcfb81e6f0919683 043a718774c572bd8a25adbeb1bfcd5c0256ae11cecf9f9c3f925d0e52beaf89 5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9 ef2d127de37b942baad06145e54b0c619a1f22327b2ebbcfbec78f5564afe39d 148de9c5a7a44d19e56cd9ae1a554bf67847afb0c58f6e12fa29ac7ddfca9940 4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce 1b16b1df538ba12dc3f97edbb85caa7050d46c148134290feba80f8236c83db9 3e23e8160039594a33894f6564e1b1348bbd7a0088d42c4acb73eeaed59c009d d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35 ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb 594e519ae499312b29433b7dd8a97ff068defcba9755b6d5d00e84c524d67b06 5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9 d10b36aa74a59bcf4a88185837f658afaf3646eff2bb16c3928d0e9335e945d2

>Step 1 — Local analysis (decode ciphers.txt)

  1. Look at ciphers.txt. It has 51 lines — each looks like a hex SHA-256 digest.
  2. Hypothesis: each hash is sha256(s) for a small s (likely 1-character ASCII). Confirm by brute-force:
  • compute sha256('0'), sha256('1'), ..., and single ASCII letters/symbols; many matches appear immediately (digits 0..9, letters, {, } etc).
  1. There was one small gotcha: the file had a UTF‑8 BOM on the first line. Strip it before matching (or use .lstrip('\\ufeff')).

Command used to inspect (examples):

bash

# quick tests (python):

python3 - <<'PY'

import hashlib

print(hashlib.sha256(b'0').hexdigest())

print(hashlib.sha256(b'a').hexdigest())

PY

Result of decoding ciphers.txt (character-level ciphertext):

Hrvty_KHN{30gi27e5hj18mru9m2f15pwqo4v36s05p3nb2az0}

Length = 51 (which matches the hint that the flag has 51 characters).

decode_hashes.py (what it does)

  • Builds a map of SHA256(byte) -> byte for all byte values 0..255.
  • Reads ciphers.txt (strips BOM), reverses each hash using that map, and prints the result.

(See the full script included below.)


>Step 2 — Remote oracle exploration

  1. Start a quick probe with nc host port and send short strings to see how the remote encoder responds.
  2. Observations from simple queries:
  • printf 'a'*51 | nc HOST PORT returns a 51-character lowercase string; each returned character depends on position — this suggests a position-based shift (a per-position keystream applied to letters).
  • printf '0'*51 | nc HOST PORT returns a 51-digit string; digits are being shifted mod 10 position-wise.
  • Braces { and } and underscores appear to be sent through unchanged.

Reasoning: If the service applies a per-position shift (keystream) to letters and digits, then encrypting a known constant ('a'*n and '0'*n) returns the keystream mapped into letters/digits. Given ciphertext and keystream, plaintext is recovered by subtracting keystream modulo alphabet size.

How we derived decryption formulas

  • For letters: ciphertext_char = (plaintext_char + key) mod 26 (in letter positions). So plaintext = (ciphertext - key) mod 26. We get key = (oracle('a') - 'a').
  • For digits: ciphertext_digit = (plaintext_digit + key) mod 10. So subtract digit-wise with key obtained from oracle('0').

>Step 3 — Combine & retrieve the flag

  1. Use decode_hashes.py or the identical logic in solve.py to obtain cipher (the character-level ciphertext).
  2. Query remote using n = len(cipher):
  • ks_letters = oracle('a'*n) → returns lowercase letters, each representing (key + 'a') at that position.
  • ks_digits = oracle('0'*n) → returns digits representing keys for digit positions.
  1. For each position i:
  • If cipher[i] is lowercase letter: plaintext = (ord(cipher[i]) - ord('a') - (ord(ks_letters[i]) - ord('a'))) mod 26.
  • If cipher[i] is uppercase letter: same, but using ord('A').
  • If cipher[i] is digit: plaintext = (int(cipher[i]) - int(ks_digits[i])) mod 10.
  • Else (e.g., {, }, _): copy as-is.

Result: Vianu_CTF{16bc18d0ca25efa5e7c28aeff7f11f21d4de4de9}


>Scripts (full source)

decode_hashes.py

python

#!/usr/bin/env python3

import hashlib

from pathlib import Path

def build_sha256_single_byte_reverse_map() -> dict[str, bytes]:

    out = {}

    for i in range(256):

        b = bytes([i])

        out[hashlib.sha256(b).hexdigest()] = b

    return out

def decode_hash_lines(path: Path) -> bytes:

    lines = [ln.strip().lstrip("\\ufeff") for ln in path.read_text().splitlines() if ln.strip()]

    rev = build_sha256_single_byte_reverse_map()

    decoded = bytearray()

    for h in lines:

        b = rev.get(h)

        if b is None:

            raise SystemExit(f"Unknown hash: {h}")

        decoded.extend(b)

    return bytes(decoded)

def main() -> None:

    msg = decode_hash_lines(Path("ciphers.txt"))

    print(msg.decode("latin-1"))

    print(f"length={len(msg)}")

if __name__ == "__main__":

    main()

solve.py (end-to-end)

python

#!/usr/bin/env python3

import argparse

import hashlib

import socket

from pathlib import Path

def build_sha256_single_byte_reverse_map() -> dict[str, bytes]:

    return {hashlib.sha256(bytes([i])).hexdigest(): bytes([i]) for i in range(256)}

def decode_hash_lines(path: Path) -> str:

    rev = build_sha256_single_byte_reverse_map()

    out = bytearray()

    for ln in path.read_text().splitlines():

        h = ln.strip().lstrip("\\ufeff")

        if not h:

            continue

        b = rev.get(h)

        if b is None:

            raise SystemExit(f"Unknown hash: {h}")

        out.extend(b)

    return out.decode("latin-1")

def oracle_encrypt(host: str, port: int, s: str, timeout: float) -> str:

    with socket.create_connection((host, port), timeout=timeout) as sock:

        sock.sendall((s + "\\n").encode())

        data = bytearray()

        while True:

            chunk = sock.recv(4096)

            if not chunk:

                break

            data.extend(chunk)

    return data.decode(errors="replace").strip()

def decrypt_with_keystream(cipher: str, ks_letters: str, ks_digits: str) -> str:

    if not (len(cipher) == len(ks_letters) == len(ks_digits)):

        raise ValueError("cipher/keystream length mismatch")

    kL = [ord(ch) - ord('a') for ch in ks_letters]

    kD = [int(ch) for ch in ks_digits]

    out = []

    for i, ch in enumerate(cipher):

        if 'a' <= ch <= 'z':

            out.append(chr((ord(ch) - 97 - kL[i]) % 26 + 97))

        elif 'A' <= ch <= 'Z':

            out.append(chr((ord(ch) - 65 - kL[i]) % 26 + 65))

        elif '0' <= ch <= '9':

            out.append(str((int(ch) - kD[i]) % 10))

        else:

            out.append(ch)

    return ''.join(out)

def main() -> None:

    ap = argparse.ArgumentParser(description="Solve VianuCTF fibodream")

    ap.add_argument("--host", default="185.213.240.231")

    ap.add_argument("--port", default=5123, type=int)

    ap.add_argument("--timeout", default=5.0, type=float)

    ap.add_argument("--file", default="ciphers.txt")

    args = ap.parse_args()

    cipher = decode_hash_lines(Path(args.file))

    n = len(cipher)

    ks_letters = oracle_encrypt(args.host, args.port, 'a' * n, args.timeout)

    ks_digits = oracle_encrypt(args.host, args.port, '0' * n, args.timeout)

    plain = decrypt_with_keystream(cipher, ks_letters, ks_digits)

    print(plain)

if __name__ == "__main__":

    main()

>Reproduction (quick)

  1. Local decode only:
bash

python3 decode_hashes.py

# prints: Hrvty_KHN{30gi27e5...}
  1. Full solve (queries remote):
bash

python3 solve.py  # default host/port set in script

# prints: Vianu_CTF{16bc18d0...}

If the remote host/port differ, run:

bash

python3 solve.py --host <host> --port <port>

>Why this worked / how the idea came to be

  • Seeing that every line was exactly 64 hex characters hinted strongly at SHA-256 digests.
  • Many digests matched the SHA-256 of simple single characters (e.g., digits and letters), which suggested these were per-byte SHA-256 preimages (sha256 of a single byte).
  • The next step was to look for how the service takes plaintext and turns it into ciphertext. Small probes ('a'*n and '0'*n) revealed a deterministic, position-wise shift for letters and digits, respectively. That led directly to computing the keystream and subtracting it.

>References