Home > Writeups > WHAMazon! Rev 2 - Armor

WHAMazon! Rev 2 - Armor

Identifying a PyArmor v9.x protected Python script, generating the correct runtime to execute it, and extracting a base64-encoded flag from the crash dump it writes to disk.

Armor

Challenge Description

These WHAMazon bots are armored up? WTF made these things?

Flag: Raptor{Wh0S3_GREAT_id34_w4S_thiS?}

Provided: hidden.ziphidden.py


Identifying the Protection

Opening hidden.py:

from pyarmor_runtime_000000 import __pyarmor__
__pyarmor__(__name__, __file__, b'PY000000\x00\x03\r\x00......')

This is PyArmor: a Python obfuscation tool that encrypts the bytecode and wraps it in a runtime that decrypts and executes it on the fly. The script can't run without its matching pyarmor_runtime_000000 package present.

The blob identifier PY000000 and the import naming convention pyarmor_runtime_000000 are characteristic of PyArmor v9.x. Earlier versions used different naming schemes and trying to run this with a v8.x runtime (or no runtime at all) just throws an import error. Several attempts with wrong versions confirmed this before landing on v9.x.


The Approach

Rather than trying to unpack the encrypted bytecode directly (which would require reversing PyArmor's encryption), the cleaner path is: give the script what it needs to run, execute it, and observe what it does. If the flag is produced as output or written somewhere, we catch it.


The Script

import subprocess
import sys
import os
import base64

# Step 1: Install PyArmor 9.x (uninstall any existing version first)
subprocess.run([sys.executable, '-m', 'pip', 'uninstall', 'pyarmor', '-y', '-q'], capture_output=True)
subprocess.run([sys.executable, '-m', 'pip', 'install', 'pyarmor', '-q'], capture_output=True)

# Step 2: Generate the matching runtime for this platform
# PyArmor 9.x requires generating a platform-specific runtime package
# before any protected script can be imported or executed
subprocess.run(
    ['pyarmor', 'gen', 'runtime', '--output', '.', '--platform', 'linux.x86_64'],
    capture_output=True, text=True
)
# This creates ./pyarmor_runtime_000000/ in the current directory

# Step 3: Remove stale crash dump if present, then run the protected script
if os.path.exists('.whamazon_crashdump'):
    os.remove('.whamazon_crashdump')

result = subprocess.run(
    [sys.executable, 'hidden.py'],
    capture_output=True, text=True, timeout=10
)
print(result.stdout)

# Step 4: Read the crash dump the script wrote to disk
with open('.whamazon_crashdump', 'r') as f:
    content = f.read()

# Step 5: Extract and decode the base64 chunks
# The file contains base64 split across multiple lines interspersed with headers
lines = content.split('\n')
b64_lines = [
    line.strip() for line in lines
    if line.strip() and all(
        c in 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='
        for c in line.strip()
    )
]

b64_joined = ''.join(b64_lines)
decoded = base64.b64decode(b64_joined).decode('utf-8')
print(f"FLAG: {decoded}")

Step by step:

1. Uninstall and reinstall PyArmor at v9.x. PyArmor is version-sensitive, a runtime generated by v8.x won't satisfy a v9.x protected script. Uninstalling first ensures we get a clean v9.x install from pip.

2. Generate the runtime. PyArmor 9.x doesn't ship the runtime as a static package, it generates a platform-specific one on demand with pyarmor gen runtime. The --output . flag places pyarmor_runtime_000000/ in the current directory where hidden.py can find it via the from pyarmor_runtime_000000 import __pyarmor__ line.

3. Execute hidden.py. With the runtime present, the script can now decrypt and run its protected bytecode. We capture stdout but the real output turns out to be a file.

4. Read .whamazon_crashdump. The script's theme: morale daemon crash, kernel panic, was a hint that something gets written to disk. The file appears in the working directory after execution.

5. Filter and decode base64. The crash dump contains a header line, separator, and the flag encoded as base64 split across multiple short lines. Filtering for lines containing only valid base64 characters, joining them, and decoding produces the flag.


Script Output

🤖 WHAMazon Autonomous Diagnostic Terminal
Loading compliance modules...
Reconnecting to HR Bot-42...
ERROR: Employee morale buffer overflow detected.
Generating emergency crash dump...
Crash dump written to internal storage.
Incident ID: INTERN-0xDEADBEEF
🤖 Please forward diagnostics to Corporate.
WHAMazon Kernel Panic: morale daemon exited unexpectedly

The crash dump file contents:

WHAMazon Internal Memory Snapshot
================================

UmFwdG9y
e1doMFMz
X0dSRUFU
X2lkMzRf
dzRTX3Ro
aVM/fQ==

Joined and decoded: Raptor{Wh0S3_GREAT_id34_w4S_thiS?}


A Note on Approach

The tempting first move with any obfuscated binary is to attack the obfuscation directly, unpack, decrypt, decompile. With PyArmor that's genuinely hard; it's designed to resist static analysis and the encryption keys are tied to the runtime. The faster path here is dynamic analysis: give the program what it needs to execute, run it in a sandbox, and watch what it does. If the program writes output, reads files, makes network calls, or crashes in an interesting way, that's where the flag lives.

The crash dump pattern also appeared in the James Smith JS challenge (the string array contained crashdump, .whamazon_, and writeFileSync fragments). In that case it was flavor text; here it was the actual mechanism.


Key Takeaways

PyArmor version matching is the first hurdle with any PyArmor challenge, the runtime must match the version used to protect the script. The PY000000 blob prefix and pyarmor_runtime_000000 import are v9.x signatures; other versions use different identifiers.

When static analysis is blocked by obfuscation, pivot to dynamic analysis. Reconstruct the execution environment, run the target, and observe its behaviour. Files written to disk, network traffic, and stdout/stderr are all valid flag delivery mechanisms, check all of them.

< Back to All Writeups