← Back to Writeups
CUSTOMEasyReversing

Elfstaller

XESXOR9/10/20263 min read
#reversing#custom#easy#known_key_from_flag_prefix#pyc_decompilation#pyinstaller_extraction#repeating_key_xor#linux

Elfstaller

Platform: spbctf | Category: Reversing | Type: Challenge | Difficulty: Easy | OS: Linux | Author: XESXOR | Date: 2026-07-02 | Status: Solved Techniques: known_key_from_flag_prefix, pyc_decompilation, pyinstaller_extraction, repeating_key_xor

Summary

Task: 12MB stripped PyInstaller ELF (Python 3.8) validating a flag via repeating-key XOR. Solution: extract with pyinstxtractor, decompile the 448-byte .pyc with decompyle3, then decrypt the hardcoded ciphertext using the flag prefix 'SPbCTF' which is itself the 6-byte XOR key.

Elfstaller — spbctf

Description

Elfstaller — reverse challenge. A single ELF binary pyinstallerelf.elf is provided.

The challenge name "Elfstaller" is a pun on ELF + (Py)Installer, hinting directly at the technique: a PyInstaller-bundled Python application shipped as an ELF executable. Goal: recover the flag (SPbCTF{...}) accepted by the binary.

Challenge file: https://rev-kids20.forkbomb.ru/files/rev/new9/pyinstallerelf.elf

Analysis

Initial recon on the binary:

bash
file pyinstallerelf.elf
# ELF 64-bit LSB executable, x86-64, dynamically linked, stripped, ~12 MB
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

strings pyinstallerelf.elf | grep -i python
# libpython3.8.so.1.0
# python3.8 ... paths

A ~12 MB stripped ELF that references libpython3.8.so.1.0 is the classic signature of a PyInstaller bundle built with Python 3.8. The real logic is Python bytecode packed inside the executable, not native code.

Solution

1. Extract the PyInstaller archive

bash
curl -s -o pyinstxtractor.py https://raw.githubusercontent.com/extremecoders-re/pyinstxtractor/master/pyinstxtractor.py
python3 pyinstxtractor.py pyinstallerelf.elf

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

Output reports PyInstaller 2.1+, Python 3.8, 696 files, extracted to pyinstallerelf.elf_extracted/. The entry-point candidate is pyinstallerelf.pyc — only 448 bytes, so it holds the flag check.

2. Decompile the Python 3.8 bytecode

On a modern host (Python 3.14) decompyle3 handles 3.8 bytecode:

bash
# installed via homebrew: /opt/homebrew/bin/decompyle3
decompyle3 pyinstallerelf.elf_extracted/pyinstallerelf.pyc

It prints a lot of parser grammar debug noise, but the reconstructed source appears at the end: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

python
import os
flag = b'(d=\x01eq\x0c8V1\x10\x03!\x0f 6c\x19fgS\x0f8*?<=%1rfa\x00\x0f\x11;'
res = b''
c = 0
text = input("Insert flag to continue playing:\n").encode()
flagd = text[:6]
for i in text[6:]:
    res += bytes([i ^ flagd[c % 6]])
    c += 1

if res == flag:
    print("Enjoy!")
else:
    print("That is definitely not a flag.")

3. Understand the check

  • The user's input is the flag.
  • flagd = text[:6] takes the first 6 bytes of the flag as a repeating XOR key.
  • Everything after position 6 (text[6:]) is XOR'd byte-by-byte against the cycled 6-byte key (c % 6) and must equal the hardcoded flag ciphertext. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

The key is self-referential: it is the flag prefix itself. But the flag format is known — SPbCTF{...} — so the first 6 bytes are exactly SPbCTF. That gives us both the key and the prefix for free.

4. Decrypt

python
#!/usr/bin/env python3
flag = b'(d=\x01eq\x0c8V1\x10\x03!\x0f 6c\x19fgS\x0f8*?<=%1rfa\x00\x0f\x11;'
key = b'SPbCTF'
res = ''.join(chr(b ^ key[i % 6]) for i, b in enumerate(flag))
print('SPbCTF' + res)
# SPbCTF{REDACTED}

Lessons / Takeaways

The whole check hinges on a self-referential trick: the XOR key was the first 6 bytes of the flag, and those bytes are dictated by the known flag format SPbCTF{...}. Whenever a validator uses part of the secret as the key to transform the rest of the secret, and the format gives you that part, the scheme collapses instantly — recognizing the known flag prefix as the key turns "reverse engineering" into a one-line XOR decrypt. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍