← Back to Writeups
CUSTOMEasyReversing

Winstaller

XESXOR9/2/20262 min read
#reversing#custom#easy#base64_block_inversion#pyc_decompilation#pyinstaller_extraction#windows

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 from https://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
$ 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
git 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
# 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
import 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 into res.
  • 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
import 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}

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍