Hissss (rev) – CTF Write-up
Challenge Overview
We’re given a single executable named auth. The program asks for a 12-character password and prints the flag when every character satisfies a set of arithmetic constraints.
- A 64-bit Linux executable built from Python
- PyInstaller artifacts embedded in the binary
- Python bytecode that
uncompyle6fails to recover - A small set of constraints that can be solved manually without Z3
Initial Reconnaissance
Running file identifies auth as a stripped 64-bit PIE executable:
$ file auth
auth: ELF 64-bit LSB pie executable, x86-64, dynamically linked, stripped
A string search reveals a complete Python 3.8 runtime and numerous compiled extension modules:
$ strings auth | grep python
b_asyncio.cpython-38-x86_64-linux-gnu.so
b_bz2.cpython-38-x86_64-linux-gnu.so
b_hashlib.cpython-38-x86_64-linux-gnu.so
blibpython3.8.so.1.0
xinclude/python3.8/pyconfig.h
&libpython3.8.so.1.0
This strongly indicates that the executable was packaged with PyInstaller. Instead of reversing the native bootloader, we can extract its archive and recover the original Python bytecode.
Extracting the Python Bytecode
The usual workflow is to unpack the executable with pyinstxtractor and decompile the resulting .pyc file. uncompyle6 did not support this bytecode successfully, so we use pycdc instead.
To keep the required tools isolated, the extraction can be performed inside a Docker container:
$ docker run --rm -v "$PWD":/htb -it python:3.10 bash
root@container:/# wget -q https://raw.githubusercontent.com/extremecoders-re/pyinstxtractor/master/pyinstxtractor.py
root@container:/# git clone https://github.com/zrax/pycdc
root@container:/# cd /pycdc
root@container:/pycdc# apt update && apt install -y cmake
root@container:/pycdc# cmake . && make
root@container:/pycdc# cd /htb
root@container:/htb# python3 /pyinstxtractor.py auth
The extractor identifies Python 3.8 and locates the application entry point:
[+] Processing auth
[+] Pyinstaller version: 2.1+
[+] Python version: 3.8
[+] Found 68 files in CArchive
[+] Possible entry point: auth.pyc
[+] Successfully extracted pyinstaller archive: auth
The recovered bytecode can now be passed to pycdc:
root@container:/htb# /pycdc/pycdc auth_extracted/auth.pyc
Source Code Analysis
The decompiled program reads a password, rejects inputs that are not 12 characters long and then checks relationships between individual character positions:
import sys
password = input("Enter password> ")
if len(password) != 12:
print("Sorry! You've entered the wrong password.")
sys.exit(0)
if (
ord(password[0]) != 48
and password[11] != "!"
and ord(password[7]) != ord(password[5])
and 143 - ord(password[0]) != ord(password[4])
and ord(password[1]) ^ ord(password[3]) != 30
and ord(password[2]) * ord(password[3]) != 5610
and password[1] != "p"
and ord(password[6]) - ord(password[8]) != -46
and ord(password[6]) ^ ord(password[7]) != 64
and ord(password[10]) + ord(password[5]) != 166
and ord("n") - ord(password[9]) != 1
or password[10] != "3"
):
print("Sorry, the password is incorrect.")
else:
print(f"Well Done! HTB{{{password}}}")
The decompiler preserved the conditions as inequalities. To reach the success branch, we satisfy their corresponding equalities while also setting password[10] to 3.
Recovering the Password
Four positions can be read directly from the checks:
| Position | Constraint | Character |
|---|---|---|
password[0] |
ord(password[0]) == 48 |
0 |
password[1] |
password[1] == "p" |
p |
password[10] |
password[10] == "3" |
3 |
password[11] |
password[11] == "!" |
! |
The remaining characters follow from the arithmetic relationships:
password[4] = chr(143 - ord("0")) = "_"
password[3] = chr(30 ^ ord("p")) = "n"
password[2] = chr(5610 // ord("n")) = "3"
password[5] = chr(166 - ord("3")) = "s"
password[7] = password[5] = "s"
password[6] = chr(64 ^ ord("s")) = "3"
password[8] = chr(ord("3") + 46) = "a"
password[9] = chr(ord("n") - 1) = "m"
Putting the characters back in index order gives:
0p3n_s3sam3!
Result
$ ./auth
Enter password> 0p3n_s3sam3!
Well Done! HTB{0p3n_s3sam3!}
Key Takeaways
- Python executables packaged with PyInstaller often retain recoverable bytecode inside their embedded archive.
- If one Python decompiler fails, another implementation such as
pycdcmay still support the bytecode version. - Preserve Python’s boolean precedence when reformatting decompiled conditions;
andbinds more tightly thanor. - Small character constraints are often faster to solve manually than with an SMT solver.