typeanalysisfamilypyinstaller-pyarmor-dropperconfidencemediumcreated2026-05-30updated2026-05-30python-pyinstallerobfuscationdefense-evasionpec2persistencemitre-attck
SHA-256: d297973f8d1bb330dc7a7d7538bfbe97ea4608aee040b48122da39a2562ddf4c

pyinstaller-pyarmor-dropper: d297973f — PyInstaller single-file bootloader with PyArmor-obfuscated Python 3.13 payload

Executive Summary

A 13.8 MB PE64 that is a stock PyInstaller one-file bootloader wrapping a zlib-compressed CFFI archive. The extracted payload is a Python 3.13 runtime environment plus a PyArmor-obfuscated script (pyarmor_runtime_000000), indicating the actual malware logic is hidden behind PyArmor's runtime protection. Static evidence points to network-capable Python modules (socket, urllib, email, base64, hashlib, subprocess, ctypes, wmi) being present in the archive. No CAPE detonation was possible (no Windows guest available), so all TTPs are inferred from the static surface.

What It Is

  • SHA-256: d297973f8d1bb330dc7a7d7538bfbe97ea4608aee040b48122da39a2562ddf4c ^[file.txt]
  • Filename at source: common.dat ^[metadata.json]
  • Type: PE32+ executable (GUI) x86-64, stripped, unsigned ^[file.txt] ^[rabin2-info.txt]
  • Size: 13,842,881 bytes (13.2 MB on disk) ^[rabin2-info.txt]
  • Linker timestamp: 2026-05-21 09:51:36 UTC ^[pefile.txt]
  • Overlay: ~13.6 MB zlib-compressed CFFI archive (PyInstaller single-file mode) ^[binwalk.txt]
  • Bootloader language: C, compiled with a toolchain reporting linker version 2.45 (MinGW-w64 ld / binutils) ^[pefile.txt] ^[rabin2-info.txt]
  • No exports, no PDB path, no version info resources ^[pefile.txt]

How It Works

Build / RE

The outer PE is a standard PyInstaller bootloader — not custom and not packed with UPX. It contains the expected CRT startup code (__wgetmainargs, _initterm, malloc, memcpy), sets up a console window (CreateWindowExW, MessageBoxW), and then transitions into the PyInstaller extraction routine. The entry0 decompilation shows a classic MSVC/MinGW CRT bootstrap: parse command line, copy argv into a new wide-char buffer, call init tables, and then branch to the main payload initialization. ^[r2:entry0] ^[r2:fcn.140001010]

Key bootloader strings observed:

  • _MEIPASS — the extraction target directory variable ^[strings.txt:419]
  • base_library.zip — standard Python frozen standard library path ^[strings.txt:370]
  • lib-dynload — compiled extension module directory ^[strings.txt:371]
  • PYINSTALLER_SUPPRESS_SPLASH_SCREEN — environment flag to disable splash ^[strings.txt:329]
  • PYZ archive entry not found in the TOC! — PYZ (Python ZIP archive) error string ^[strings.txt:423]
  • PYINSTALLER_STRICT_UNPACK_MODE — new PyInstaller strict-extraction flag ^[strings.txt:338]
  • Failed to extract script from archive! — extraction failure message ^[strings.txt:312]
  • Failed to load PyInstaller's embedded PKG archive from the executable (%s) ^[strings.txt:327]

These are unmodified PyInstaller bootloader messages, confirming the outer layer is stock infrastructure. ^[strings.txt:303-365]

Payload

The overlay (~13.6 MB) is a zlib-compressed CFFI archive. binwalk identifies PNG icons (16×16 through 64×64), an XML manifest, and hundreds of zlib streams at best/default compression — the classic PyInstaller overlay signature. ^[binwalk.txt]

Standard library modules present in the TOC (extractable from strings):

  • socket, urllib, urllib.parse — HTTP/S and raw socket networking ^[strings.txt:22652,22665,22666]
  • email and email.base64mime — SMTP/mail exfil or C2 messaging ^[strings.txt:22580]
  • base64, hashlib — encoding and hashing of exfil data ^[strings.txt:22570,22604]
  • subprocess, threading — command execution and concurrency ^[strings.txt:22656,22660]
  • ctypes, ctypes._endian — direct Win32 API invocation ^[strings.txt:22575,22576]
  • wmi via _psutil_windows.pyd — system enumeration (CPU, memory, processes) ^[strings.txt:22687,22699]
  • uuid — likely for generating a victim UID ^[strings.txt:22686]
  • platform — system fingerprinting ^[strings.txt:22639]

In addition, the archive embeds a PyArmor-protected runtime:

  • pyarmor_runtime_000000 module name and pyarmor_runtime.pyd extension ^[strings.txt:22646,22700]
  • __pyarmor__ symbol name stub ^[strings.txt:883]

This means the raw Python source is not available in the overlay; it is obfuscated by PyArmor and decrypted/resolved at runtime by the .pyd extension. Static string extraction will therefore miss all IOCs (C2 URLs, hardcoded credentials, mutex names) because they are encrypted inside the PyArmor runtime.

Signing

Unsigned. signed: false. No Authenticode signature, no certificate table. ^[rabin2-info.txt]

Anti-Analysis

Not in the bootloader itself. The only anti-analysis mechanism visible statically is the PyArmor runtime obfuscation of the Python bytecode, which blocks decompilation and string extraction of the actual malicious logic. No anti-VM, anti-debug, or TLS callback tricks were identified in the PE header or radare2 analysis. ^[pefile.txt] ^[r2:fcn.140001010]

Decompiled Behavior

entry0 → CRT bootstrap → extraction

radare2 decompilation of entry0 at 0x1400013e0 is a short thunk that initializes a global flag and immediately calls fcn.140001010, the main CRT startup routine. ^[r2:entry0]

fcn.140001010 performs the expected MinGW-w64 startup:

  1. References __wgetmainargs to retrieve command-line arguments.
  2. Allocates a new wide-char argv buffer via malloc and copies each argument with memcpy/wcslen.
  3. Calls _initterm for constructor tables.
  4. Reads the PE header to validate the optional-header magic (0x20b for PE32+) and subsystem.
  5. Eventually dispatches to the PyInstaller main loop (the remainder of the function is not fully decompiled by radare2's pdc, but the cross-references to Python C-API strings in .rdata confirm the transition).

No suspicious Win32 API imports (e.g., VirtualAllocEx, WriteProcessMemory, CreateRemoteThread) are present in the import table — all malware logic is delegated to the embedded Python interpreter. ^[r2:list_imports]

C2 Infrastructure

None observable statically. Because the payload is protected by PyArmor, no C2 URLs, IPs, domains, mutex names, or registry keys appear in the strings. The presence of urllib, socket, and email strongly implies network C2 or exfil, but the exact endpoints are runtime-resolved inside the PyArmor-decrypted layer. ^[strings.txt:22652,22666,22580]

Interesting Tidbits

  • Massive overlay ratio: The PE code section is ~200 KB; the overlay is ~13.6 MB — a 68:1 ratio. This alone is a detection signal. ^[pefile.txt] ^[binwalk.txt]
  • No UPX, no custom packer: The threat actor relied entirely on the PyInstaller + PyArmor double-wrap for evasion; no third-party packer or shellcode loader was added.
  • Python 3.13: The presence of python313.dll indicates a very recent PyInstaller build (Python 3.13 went stable in October 2024). This is a fresh toolchain, not a recycled older PyInstaller stub. ^[strings.txt:22701,22702]
  • Missing capa/floss: Both failed due to missing signatures or command-line syntax errors, not due to packing. The sample is trivial to analyze with strings and binwalk. ^[capa.txt] ^[floss.txt]

How To Mess With It (Homelab Replication)

This is a teaching-sample for the PyInstaller + PyArmor packaging pipeline.

  1. Write a minimal Python payload

    import socket, urllib.request, base64, hashlib
    def main():
        print("Hello from the sandbox.")
    if __name__ == "__main__":
        main()
    
  2. Obfuscate with PyArmor (trial or licensed)

    pip install pyarmor
    pyarmor gen --pack onefile payload.py
    

    This produces a single PE that combines PyInstaller bootloader + the PyArmor runtime.

  3. Verify overlay

    python3 -m binwalk your_payload.exe | head -20
    

    Expect hundreds of Zlib compressed data entries and PNG icons.

  4. Static comparison Run strings your_payload.exe | grep -i pyarmor — you should see pyarmor_runtime strings similar to this sample. Compare to strings.txt of this analysis.

What you will learn: PyArmor does not hide Python's standard library import names from the PyInstaller TOC; they remain visible in the outer PE's string table. This is why socket, urllib, ctypes, etc. are still present despite the obfuscation. Defenders can use these as weak behavioral indicators even when the C2 is encrypted.

Deployable Signatures

YARA Rule — PyInstaller + PyArmor Dropper

rule pyinstaller_pyarmor_dropper : pyinstaller pyarmor python stealer
{
    meta:
        description = "PyInstaller one-file bootloader with embedded PyArmor runtime"
        author      = "wiki-auto"
        date        = "2026-05-30"
        sha256      = "d297973f8d1bb330dc7a7d7538bfbe97ea4608aee040b48122da39a2562ddf4c"
    strings:
        $pyi1 = "PYINSTALLER_SUPPRESS_SPLASH_SCREEN" ascii wide
        $pyi2 = "_MEIPASS" ascii wide
        $pyi3 = "base_library.zip" ascii wide
        $pyi4 = "Could not load PyInstaller's embedded PKG archive" ascii wide
        $pyarm1 = "pyarmor_runtime_000000" ascii wide
        $pyarm2 = "__pyarmor__" ascii wide
        $pyarm3 = "pyarmor_runtime.pyd" ascii wide
    condition:
        uint16(0) == 0x5A4D and
        filesize > 5MB and
        3 of ($pyi*) and
        1 of ($pyarm*)
}

Sigma Rule — PyInstaller Extraction Directory Activity

title: Suspicious PyInstaller Extraction Directory Execution
status: experimental
description: Detects processes executing from the _MEI temporary directory created by PyInstaller single-file binaries
logsource:
    category: process_creation
    product: windows
detection:
    selection:
        Image|contains: '\_MEI'
    filter:
        - Image|contains: 'python'
        - Image|endswith: '.exe'
    condition: selection
falsepositives:
    - Legitimate PyInstaller-based tools used by administrators
level: medium
tags:
    - attack.execution
    - attack.t1059.006

IOC List

Indicator Value Notes
SHA-256 d297973f8d1bb330dc7a7d7538bfbe97ea4608aee040b48122da39a2562ddf4c Bootloader + overlay
Filename (source) common.dat Masquerade name; no relation to content
Build timestamp 2026-05-21 09:51:36 UTC Likely near compilation time
Embedded Python python313.dll CPython 3.13.x
Obfuscation PyArmor runtime 000000 Version not further discernible
Overlay ~13.6 MB zlib-compressed CFFI archive with PNG resources
Signing Unsigned No Authenticode

Behavioral Fingerprint Statement

This binary launches as a stock Windows GUI PE with benign KERNEL32/USER32 imports. At runtime it extracts a ~13 MB zlib-compressed archive to %TEMP%\_MEI<XXXX>, loads python313.dll and pyarmor_runtime.pyd, then initializes the CPython interpreter to execute an obfuscated script. Network activity, if any, originates from the embedded Python layer (not directly from the outer PE) and typically uses urllib, socket, or smtplib modules. The outer PE itself performs no direct suspicious API calls — all malware behavior is deferred to the extracted Python runtime.

Detection Signatures (capa → ATT&CK)

capa failed on this sample due to a missing signatures directory. The following ATT&CK mapping is derived from static evidence:

Technique ID Technique Name Evidence
T1027.002 Obfuscated Files or Information: Software Packing PyArmor runtime obfuscates Python payload; PyInstaller compresses overlay with zlib
T1059.006 Command and Scripting Interpreter: Python Embedded CPython 3.13 DLL; Py_InitializeFromConfig in strings
T1071 Application Layer Protocol socket, urllib, email modules present in archive
T1083 File and Directory Discovery FindFirstFileW, FindNextFileW, GetDriveTypeW imported by bootloader (used for extraction)
T1005 Data from Local System wmi/psutil and platform modules imply system enumeration
T1041 Exfiltration Over C2 Channel email + urllib suggest staged exfil; static-only inference
T1105 Ingress Tool Transfer If the payload downloads second-stage tools via urllib (static inference)

Confidence note: All network and impact TTPs are inferred from Python module presence, not confirmed by dynamic execution. This sample requires a Windows sandbox detonation to confirm actual C2 endpoints, persistence, and payload behavior.

References

  • pyinstaller-bootloader — concept page for PyInstaller single-file bootloader
  • python-packed-payload — technique page for Python-packed malware
  • pyarmor-obfuscation — concept page for PyArmor runtime obfuscation
  • PyInstaller docs: https://pyinstaller.org/
  • PyArmor docs: https://github.com/dashingsoft/pyarmor

Provenance

  • file.txt — file(1) type identification
  • pefile.txt — pefile Python library full dump
  • rabin2-info.txt — radare2 rabin2 -I summary
  • strings.txt — GNU strings output (22,707 lines)
  • binwalk.txt — binwalk signature scan (zlib PNG overlay enumeration)
  • floss.txt — flare-floss invocation failed (bad CLI syntax)
  • capa.txt — Mandiant capa invocation failed (missing signatures)
  • dynamic-analysis.md — CAPE skipped (no Windows guest available)
  • radare2 decompilation — r2 v5.x with pdc decompiler on entry0 and fcn.140001010
  • pyghidra analysis — queued successfully; strings/code indexing incomplete at time of query due to background processing

Analyst note: The absence of dynamic execution means this report is a static-only surface analysis. Any C2, persistence, or impact claims above are hypothesis-grade and must be validated with sandbox detonation once a Windows analysis VM is available.