ReezS#
First wrong turn#
My first read on this binary was completely wrong. It looked like a normal flag checker, so I did what I usually do for that kind of challenge: identify the comparison logic, lift the constants, and script the inverse.


That script only gave me:
| |
That should have been a clue immediately, but I still lost a lot of time staring at the control flow.
The behavior that finally forced me to rethink the challenge was this:
- under a debugger,
sorry_this_is_fake_flag!!!!!!!!!was accepted - running the same input normally, it failed
Same input, same binary, different result. That is not a math mistake. That is environment-sensitive behavior.
The actual pivot#
After the contest I came back to the import table, and the answer was sitting there:

IsDebuggerPresent is imported, and the program checks it very early.


That explained the split behavior perfectly. The fake string was not a failed inversion of the real checker. It was bait. The binary was selecting different encoded data depending on whether a debugger was attached.
Once I knew that, I stopped trying to model every branch. I just took the two real encoded blocks from the debugger-only path, XORed them with the constant mask, swapped the halves into the right order, and reversed the decoded string.
This is the cleaned-up version of the script:
| |
That recovered:
| |
The whole solve really came down to noticing that the checker was lying differently depending on whether it saw a debugger.
Chatbot#
First pass#
This executable looked different right away. Opening it in IDA showed PyInstaller-style markers, so instead of treating it like a normal native binary, I treated it like a packaged Python app with a native helper library.

That meant the first useful step was extraction, not decompilation. I used pyinstxtractor.py to unpack the bundled files:

I did not have decompyle3 available, so I used an online decompiler to get a readable main.py. The high-level flow was enough:
- load
libnative.so - optionally run an integrity check
- verify a token for
role == VIP - if that passes, call
decrypt_flag_file("flag.enc")
That last point was the real clue. The program pretends the hard part is token validation, but the Python side already tells us the flag is sitting in a local encrypted file and the decryption routine lives in the native library we already have.
So I stopped caring about forging a VIP token and moved straight to libnative.so.

Native side#
Inside the library, decrypt_flag_file calls recover_key:

And recover_key is much simpler than the name makes it sound. It just rebuilds the original AES key from an obfuscated byte array and a short repeating mask:

Back in decrypt_flag_file, the logic is straightforward:
- read the first 16 bytes of
flag.encas the IV, - treat the rest as ciphertext,
- choose the AES branch based on key length,
- decrypt.
Because the recovered key is 32 bytes long, the branch used here is AES-256-CBC.
That means the whole solve can be reproduced locally without ever passing the token check.
I reimplemented the key recovery and decryption in Python:
| |
That decrypted the bundled file and printed:
| |
The nice part of this challenge is that the intended story is “become VIP,” but the cleaner reversing route is just to follow the local decryption path and ignore the access-control theater entirely.