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 uncompyle6 fails 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 pycdc may still support the bytecode version.
  • Preserve Python’s boolean precedence when reformatting decompiled conditions; and binds more tightly than or.
  • Small character constraints are often faster to solve manually than with an SMT solver.