>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.gz→mini_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
-
index.htmlshowed a placeholder “Advent Calendar 2025” React app; real logic is in the page chunk JS. -
grepforflag,SECCONfound nothing meaningful; needed to read the app bundle. -
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
ungzipcall 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) andmaskExprs(3 expressions). Each expression is a JavaScript boolean that must hold for an integerxin[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:
-
Parse: Wrote a small Pratt parser to handle the operators with JavaScript precedence (
, >>>, >>, <<, |, ^, &, +, -, *, unary~ - +). -
Model: Translate to z3
BitVec(32)expressions; enforce results of each expr to betrue. -
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):
-
Build a 100-byte buffer: for each day, write
xas 4 bytes big-endian; sandwich with pepperOnat start and end. -
key = SHA-256(buffer). -
Generate keystream: for counter = 0.., hash
key || counter_be32, concatenate hashes until reaching ciphertext length. -
XOR keystream with base64-decoded
Mnto recover plaintext.
Python snippet used:
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}