Skip to content

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

BACK TO INTEL
ReverseMedium

Mini_Bloat

CTF writeup for Mini_Bloat from SECCON

>Mini_Bloat

>TL;DR

Extracted the hidden puzzle data from the bundled Next.js app, parsed the bitwise/arith constraint expressions, solved all 25 daily integers with z3 (32-bit semantics), then reproduced the in-app SHA-256 + keystream XOR to decrypt the final blob. Flag: SECCON{b00l34n_4dv3nt_2025_fl4g}.

>Challenge layout

  • Archive: mini_bloat.tar.gzmini_bloat/dist/ (static Next.js export).

  • Interesting assets: index.html, dist/_next/static/chunks/app/page-fdcc665989738875.js (main page bundle), plus CSS/boilerplate.

>Recon

  1. index.html showed a placeholder “Advent Calendar 2025” React app; real logic is in the page chunk JS.

  2. grep for flag, SECCON found nothing meaningful; needed to read the app bundle.

  3. Opening the bundle (214 KB) revealed a function zn (the main React component) and constants:

   - Mn – base64 blob

   - On – pepper string "boolean-advent-2025-pepper"

   - LocalStorage key advent2025_solutions

   - Message string "Solve all 25 days to reveal the final flag".

>Finding the hidden puzzle data

  • Near a ungzip call on a big base64 string, the code did:

  ```js

  const data = JSON.parse(new TextDecoder().decode(ungzip(atob_payload)))

  ```

  • That payload decompressed to a JSON array of 25 entries: each has day, eqExprs (3 expressions) and maskExprs (3 expressions). Each expression is a JavaScript boolean that must hold for an integer x in [0, 2^32-1].

Example (day 1, shortened)

((((((x >>> 0) | 1384569162) + ... ) >>> 31) ^ 1) === 1)

>Approach to solve the constraints

The expressions are pure 32-bit integer/bitwise arithmetic with shifts, adds, subs, xors, and equality-to-1 checks. Strategy:

  1. Parse: Wrote a small Pratt parser to handle the operators with JavaScript precedence (, >>>, >>, <<, |, ^, &, +, -, *, unary ~ - +).

  2. Model: Translate to z3 BitVec(32) expressions; enforce results of each expr to be true.

  3. Solve per day: For each of the 25 days, run z3 to get one satisfying 32-bit value. Verified by re-evaluating with the concrete interpreter (mirroring JS semantics). All were SAT and unique.

Solver outputs (decimal):

[1559119409, 2281820615, 3413531028, 3436485615, 2829004470,  1389400533, 1070462966, 2534665693, 305368212, 4270731763,  1024060755, 3944557506, 493359155, 2601114477, 2712755675,  3463169353, 1603909851, 3354656626, 2291380519, 3228661065,  4045939578, 2428467629, 3990651856, 2239715624, 3534079978]

>Reproducing the flag derivation

The app’s effect for “final flag” (simplified):

  1. Build a 100-byte buffer: for each day, write x as 4 bytes big-endian; sandwich with pepper On at start and end.

  2. key = SHA-256(buffer).

  3. Generate keystream: for counter = 0.., hash key || counter_be32, concatenate hashes until reaching ciphertext length.

  4. XOR keystream with base64-decoded Mn to recover plaintext.

Python snippet used:

python

import hashlib, base64

solutions = [...]  # as above

pepper = b"boolean-advent-2025-pepper"

sol_bytes = b"".join(int.to_bytes(v,4,'big') for v in solutions)

key = hashlib.sha256(pepper + sol_bytes + pepper).digest()

ct = base64.b64decode("uJVY4mJFB6T9yppuCdGFmTW1O5GZ06yw4OTVml4VNOw=")

ks = b""; c = 0

while len(ks) < len(ct):

    ks += hashlib.sha256(key + c.to_bytes(4,'big')).digest(); c += 1

pt = bytes(a ^ b for a,b in zip(ct, ks))

print(pt.decode())

Output: SECCON{b00l34n_4dv3nt_2025_fl4g}

>Takeaways

  • Minified React/Next static exports can hide structured challenge data; look for large base64 blobs and decompression calls.

  • Maintain JS operator precedence when re-parsing expressions; a quick Pratt parser is enough for bitwise puzzles.

  • Re-implement the client-side crypto exactly: hash peppers + solved values, derive keystream via SHA-256(counter), XOR with ciphertext.

>Flag

SECCON{b00l34n_4dv3nt_2025_fl4g}