Winstaller
Winstaller
Platform: spbctf | Category: Reversing | Type: Challenge | Difficulty: Easy | OS: Windows | Author: XESXOR | Date: 2026-07-02 | Status: Solved Techniques: base64_block_inversion, pyc_decompilation, pyinstaller_extraction
Summary
Task: a ~9.7 MB PyInstaller-bundled Windows exe (Python 3.8) that validates a flag. Solution: extract with pyinstxtractor, decompile the entry pyc with decompyle3, and invert the check flag == res[::-1] by reversing the blob and base64-decoding it in 4-byte per-character chunks.
Winstaller — SPbCTF (rev-kids20)
Description
The task provides a Windows executable
pyinstallerwin.exe(downloaded fromhttps://rev-kids20.forkbomb.ru/files/rev/new9/pyinstallerwin.exe).
English summary: A PE32+ (console) x86-64 Windows binary produced by PyInstaller, bundling Python 3.8. The program asks for a flag and validates it. The goal is to recover the correct flag.
Analysis
The file is large (~9.7 MB) and file reports a Windows PE — a strong signature of
a PyInstaller-bundled Python program:
bash read-only • copy allowed$ file pyinstallerwin.exe pyinstallerwin.exe: PE32+ executable (console) x86-64, for MS Windows
PyInstaller EXEs embed a CArchive containing the frozen Python program. The entry
script is compiled to a .pyc we can extract and decompile.
Solution
1. Extract the PyInstaller archive
bash read-only • copy allowedgit clone https://github.com/extremecoders-re/pyinstxtractor python3 pyinstxtractor/pyinstxtractor.py pyinstallerwin.exe
Output reports:
[+] Pyinstaller version: 2.1+
[+] Python version: 3.8
[+] Length of package: ... (954 files in CArchive)
[+] Possible entry point: pyinstallerwin.pyc
[+] Successfully extracted pyinstaller archive: pyinstallerwin.exe
This creates pyinstallerwin.exe_extracted/ containing the entry pyinstallerwin.pyc
(only 548 bytes). Because it was extracted from a 2.1+ archive on Python 3.8, it
already carries a valid Python 3.8 magic header (55 0d 0d 0a), so no header fixup
is needed.
2. Decompile the entry pyc
The local interpreter was Python 3.14, so a naive marshal.load on 3.8 bytecode
fails with bad marshal data (unknown type code) — unmarshalling requires the
matching interpreter version. A dedicated decompiler bypasses this:
bash read-only • copy allowed# decompyle3 emits a large verbose grammar dump; the recovered source is at the # very end of stdout, so filter stderr and take the tail: decompyle3 pyinstallerwin.pyc 2>/dev/null | tail
The embedded original filename was
E:\AetherEternity\PycharmProjects\spbctfpytask\pyinstallerwin.py.
Recovered source:
python read-only • copy allowedimport base64 flag = b'==Qf==QT==AN==gU==QO==AM==gU==AU==wX==AT==AN==Qb==gU==AM==gT==wX==AN==wX==wM==wS==QM==AT==wX==wN==wU==QV==gS==wX==wd==AM==wV==we==gR==AV==wQ==gY==AU==wU' res = b'' text = input("Please enter the flag:\n") for i in text: res += base64.b64encode(i.encode()) if flag == res[::-1]: print("Flag is corect") else: print("Flag is incorect")
3. Understand and invert the logic
- For each input character, the program computes
base64.b64encode(char), which is always exactly 4 bytes per single ASCII character, and concatenates them intores. - The check is
flag == res[::-1], i.e.res == flag[::-1]. - Therefore: reverse
flag, split into consecutive 4-byte base64 blocks, and base64-decode each block to recover the original characters in order.
4. Solver
python read-only • copy allowedimport base64 flag = b'==Qf==QT==AN==gU==QO==AM==gU==AU==wX==AT==AN==Qb==gU==AM==gT==wX==AN==wX==wM==wS==QM==AT==wX==wN==wU==QV==gS==wX==wd==AM==wV==we==gR==AV==wQ==gY==AU==wU' target = flag[::-1] # res == flag[::-1] out = '' for i in range(0, len(target), 4): # each char -> 4-byte base64 block out += base64.b64decode(target[i:i+4]).decode() print(out)
Output:
SPbCTF{REDACTED}