Angry Opaq
Angry Opaq
Platform: spbctf | Category: Reversing | Type: Challenge | Difficulty: Easy | OS: NA | Author: XESXOR | Date: 2026-07-02 | Status: Solved Techniques: opaque_predicate_resolution, dead_code_elimination, xor_decryption, rotate_left_keystream, gate_value_identification
Summary
Task: x86-64 ELF crackme whose main is bloated with opaque predicates that conditionally XOR single bits into a scanf'd integer before a final compare. Solution: recognize the maze is dead code, read the gate value 0x7f89409, and seed it into the rol32/XOR keystream decrypt loop to recover the flag directly.
Angry Opaq — spbctf
Description
Make Angry Opaq less angry and more transparent. He'll give you the flag.
Provided file: angryopaq.elf (from https://rev-kids20.forkbomb.ru/files/rev/new8/angryopaq.elf).
English summary: a single x86-64 ELF crackme reads an integer, runs it through a long chain of confusing branches, and — if a hidden condition is met — prints the flag. The goal is to recover that flag.
Semantic Clue
The title and description are the whole hint:
- "Opaq" → opaque predicates, a classic code-obfuscation technique where a branch condition always evaluates to a constant (always-true or always-false) but is written so that a static analyzer cannot easily see it.
- "make transparent" → resolve the opaque predicates: once you evaluate them, the control flow collapses and the real logic becomes visible.
So before touching a disassembler seriously, the plan is: "find the branches that only look data-dependent, prove they are constant, and delete them from the picture."
Recon
$ file angryopaq.elf
ELF 64-bit LSB executable, x86-64, dynamically linked, not stripped,
BuildID[sha1]=dbf36c23c5a1926813acd0d54be611b08e6b3290
$ nm angryopaq.elf | grep main # main @ 0x401150
$ strings angryopaq.elf
...
Your flag is: spbctf{%s}
exit 1
exit 0
YES
no
Not stripped, main is at 0x401150, and the format string Your flag is: spbctf{%s} tells us the flag body is computed at runtime and printed between literal spbctf{ }.
Analysis — the opaque-predicate maze
main does:
scanf("%d", &n)— reads a signed int into[rbp-0x24].- A long chain of opaque predicates, each of which conditionally XORs one distinct bit into
n(0x1, 0x2, 0x4, 0x8, 0x10, ...up to0x40000000). The branch conditions are deliberately obfuscated, e.g.:((n + n) & 1) == 0—2nis always even, so this predicate is constant-true.(~n) == n— impossible for any integer, so this predicate is constant-false.- Several conditions gate on the return value of
system("exit 1")(child exits 1 →systemreturns256, soeax != 0) andsystem("exit 0")(returns0, soeax == 0). The"exit 1"/"exit 0"strings live at0x402036/0x40203d. These real runtime side-effects are woven in purely to make static reasoning look scary.
- The real gate at
0x401543:
i.e. the final, post-XOR value ofasm read-only • copy allowedcmp dword ptr [rbp-0x24], 0x7f89409 jne <print "no">nmust equal0x7f89409; otherwise the program printsnoand exits. - On success it prints
YES, runs a decryption loop over 16 encrypted bytes at0x402010, and printsYour flag is: spbctf{%s}.
Encrypted data (16 bytes @ 0x402010)
cd 4b e2 b0 3c 23 57 30 04 99 4c 8a 5f df dc c8
Decryption loop (@ 0x40156b..0x4015b4)
c read-only • copy allowedfor (int i = 0; i < 16; i++) { n = (n * 0x876d3acd) & 0xffffffff; // imul n = ((n << 3) | (n >> 29)) & 0xffffffff; // rol32(n, 3): shl 3 | shr 29 data[i] ^= n & 0xff; // low byte is the keystream byte }
Key insight — the maze is irrelevant
The decrypt loop consumes n as it is at the gate. The gate forces n == 0x7f89409. Therefore the flag output depends only on that single seed value — not on the input you type, not on which opaque predicates fired, not on the system() side effects.
There is no need to:
- emulate the predicate chain,
- recover the correct
scanfinput that XORs its way to0x7f89409, or - run the binary at all.
You just seed the keystream generator with 0x7f89409 and decrypt.
This is the whole point of "make Opaq transparent": once the opaque predicates are recognized as dead/constant control flow, the reachable-state at the gate is a single known constant, and everything before it collapses to nothing.
Solution
python read-only • copy allowed#!/usr/bin/env python3 # angryopaq.elf solver — no emulation needed data = bytes.fromhex('cd4be2b03c2357300 4994c8a5fdfdcc8'.replace(' ', '')) n = 0x7f89409 # value forced by the gate at 0x401543 out = bytearray(data) for i in range(16): n = (n * 0x876d3acd) & 0xffffffff # imul n = ((n << 3) | (n >> 29)) & 0xffffffff # rol32(n, 3) out[i] ^= n & 0xff print('spbctf{' + out.decode('latin1') + '}') # -> spbctf{REDACTED}
The decrypted inner string is REDACTED — leetspeak for "first time angry" — confirming the routine and the seed are correct.