PyInstaller Binaries¶
Stealers, RATs and loaders written in Python ship as PyInstaller EXEs: a small bootloader with the whole program appended. Look at the EXE alone and you see a generic C loader. The malware is in the archive, and PPEE opens it for you: which Python, which script runs, and everything that was bundled with it.
How PPEE detects PyInstaller¶
PyInstaller ends the file with a cookie, MEI\x0C\x0B\x0A\x0B\x0E, followed by the archive's size, where its table of contents is and the Python version. PPEE finds the cookie in the last 4 KB of the file; the Detected line gives its offset.
Everything is read from the table of contents. Nothing is decompressed, extracted or run.
Views¶
| View | Shows | Use it to |
|---|---|---|
| Summary | Python version and library, where the archive is, what it holds, the scripts run at start | Find the one script that matters |
| Entries | Every entry: name, kind, size, stored size, storage, file offset | See what was bundled; each row opens its bytes in the hex view |
Entry kinds: script (run at start), module, package, PYZ archive (the program's other modules), binary (DLL/PYD), data file, runtime option, dependency.
Real sample: a Python malware sample¶
At a glance
- Sample
e21e0977284f9eacbb69e04b132f923586837d2b405cee03bdca81d2944bf4c5.exe(18 MB)- Question
- Which Python script is the program, and what did it bundle?
- You'll use
- Analysis → PyInstaller → Summary and Entries
$ ppee-cli --analysis e21e0977….exe
PyInstaller
Detected: PyInstaller archive cookie (MEI\x0C\x0B\x0A\x0B\x0E) at offset 0x1265B3B, near the end of the file
PyInstaller > Summary (built from the archive cookie and table of contents)
Python
Version: 3.10
Library: python310.dll
Cookie: PyInstaller 2.1 or later
Archive
Location: 0x44A00, 18.1 MB [offset 0x44A00]
Entries: 170 [Analysis > PyInstaller > Entries]
Contents: 1 PYZ archive (bundled modules), 153 binary (DLL/PYD), 5 module, 1 runtime option, 10 script (run at start)
Scripts run at start
The program's: main
PyInstaller's: pyiboot01_bootstrap, pyi_rth_inspect, pyi_rth_multiprocessing, pyi_rth_setuptools, pyi_rth_pkgutil,
pyi_rth_cryptography_openssl, pyi_rth_pythoncom, pyi_rth_pkgres, pyi_rth_pywintypes
PS C:\> ppee-cli.exe --analysis C:\MalwareSamples\e21e0977284f9eacbb69e04b132f923586837d2b405cee03bdca81d2944bf4c5.exe.sample
C:\MalwareSamples\e21e0977284f9eacbb69e04b132f923586837d2b405cee03bdca81d2944bf4c5.exe.sample: 19291319 bytes, PE32+
Analysis (derived views, not PE structures):
PyInstaller
Detected: PyInstaller archive cookie (MEI\x0C\x0B\x0A\x0B\x0E) at offset 0x1265B3B, near the end of the file
PyInstaller > Summary (built from the archive cookie and table of contents)
Python
Version: 3.10
Library: python310.dll
Cookie: PyInstaller 2.1 or later
Archive
Location: 0x44A00, 18.1 MB [offset 0x44A00]
Entries: 170 [Analysis > PyInstaller > Entries]
Contents: 1 PYZ archive (bundled modules), 153 binary (DLL/PYD), 5 module, 1 runtime option, 10 script (run at start)
Scripts run at start
The program's: main
PyInstaller's: pyiboot01_bootstrap, pyi_rth_inspect, pyi_rth_multiprocessing, pyi_rth_setuptools, pyi_rth_pkgutil, pyi_rth_cryptography_openssl, pyi_rth_pythoncom, pyi_rth_pkgres, pyi_rth_pywintypes
Each script and module is stored as compiled Python bytecode (zlib-compressed when Storage says so); the PYZ archive holds the rest of the program's modules.
PyInstaller > Entries (170) (built from the archive's table of contents)
Name Kind Size Stored size Storage File offset
struct module 271 206 zlib 0x44A00 [offset 0x44A00]
pyimod01_archive module 3125 1850 zlib 0x44ACE [offset 0x44ACE]
pyimod02_importers module 22887 9689 zlib 0x45208 [offset 0x45208]
pyimod03_ctypes module 3620 1769 zlib 0x477E1 [offset 0x477E1]
pyimod04_pywin32 module 1053 634 zlib 0x47ECA [offset 0x47ECA]
pyiboot01_bootstrap script (run at start) 838 595 zlib 0x48144 [offset 0x48144]
pyi_rth_inspect script (run at start) 1503 872 zlib 0x48397 [offset 0x48397]
pyi_rth_multiprocessing script (run at start) 1069 673 zlib 0x486FF [offset 0x486FF]
pyi_rth_setuptools script (run at start) 717 468 zlib 0x489A0 [offset 0x489A0]
pyi_rth_pkgutil script (run at start) 910 591 zlib 0x48B74 [offset 0x48B74]
pyi_rth_cryptography_openssl script (run at start) 273 229 zlib 0x48DC3 [offset 0x48DC3]
pyi_rth_pythoncom script (run at start) 264 214 zlib 0x48EA8 [offset 0x48EA8]
pyi_rth_pkgres script (run at start) 4516 2100 zlib 0x48F7E [offset 0x48F7E]
pyi_rth_pywintypes script (run at start) 265 211 zlib 0x497B2 [offset 0x497B2]
main script (run at start) 19400 19416 zlib 0x49885 [offset 0x49885]
Crypto\Cipher\_ARC4.pyd binary (DLL/PYD) 10752 4768 zlib 0x4E45D [offset 0x4E45D]
Crypto\Cipher\_Salsa20.pyd binary (DLL/PYD) 13824 6270 zlib 0x4F6FD [offset 0x4F6FD]
Crypto\Cipher\_chacha20.pyd binary (DLL/PYD) 13312 6193 zlib 0x50F7B [offset 0x50F7B]
Crypto\Cipher\_pkcs1_decode.pyd binary (DLL/PYD) 13312 5774 zlib 0x527AC [offset 0x527AC]
Crypto\Cipher\_raw_aes.pyd binary (DLL/PYD) 35328 17910 zlib 0x53E3A [offset 0x53E3A]
...
Windows screenshot: PyInstaller Summary: Python 3.10, the 18.1 MB archive at 0x44A00, 170 entries, and main set apart from PyInstaller's own scripts
In the GUI, the navigator strip above the Summary already shows the shape of the file: a thin PE at the left, then one long band for the appended archive. The location (0x44A00) and every script name are links.
How to read it:
mainis the program. Ten scripts run at start, but nine are PyInstaller's own bootstrap and runtime hooks (pyiboot*,pyi_rth_*), the same in every build. PPEE sets them apart so you go straight to the program's script.- The runtime hooks tell you the libraries:
cryptography_openssl,pythoncomandpywintypesmean the program uses crypto and Windows COM. - The Entries view confirms it:
Crypto\Cipher\_raw_aes.pyd,_chacha20.pyd,_Salsa20.pyd,_ARC4.pyd(PyCryptodome) are bundled, so the program encrypts or decrypts data itself: worth checking inmain.
PyInstaller > Entries (170) (built from the archive's table of contents)
Name Kind Size Stored size Storage File offset
main script (run at start) 19400 19416 zlib 0x49885
Crypto\Cipher\_ARC4.pyd binary (DLL/PYD) 10752 4768 zlib 0x4E45D
Crypto\Cipher\_Salsa20.pyd binary (DLL/PYD) 13824 6270 zlib 0x4F6FD
Crypto\Cipher\_chacha20.pyd binary (DLL/PYD) 13312 6193 zlib 0x50F7B
Crypto\Cipher\_raw_aes.pyd binary (DLL/PYD) 35328 17910 zlib 0x53E3A
…
Windows screenshot: PyInstaller Entries view: bootstrap modules, the runtime-hook scripts, main, and the bundled PyCryptodome cipher modules
The Entries view in table order: PyInstaller's modules, then the ten scripts run at start with main last (19,400 bytes), then the bundled binaries starting with the PyCryptodome ciphers. Each row opens its bytes in the hex view.
Next step
The main row gives the script's offset and stored size. Decompile it with a Python bytecode tool of your choice (the Python version above tells you which one fits). PPEE doesn't decompile; an assistant can read the decompressed script in memory through its #entry:main layer.
In triage and MCP¶
triage_peshows the same facts underruntimeAnalysis, andoverlay.detectedAsreads PyInstaller archive (MEI cookie at the end).list_containerlists the entries with filtering and paging, says what each holds (content, andmismatchwhen the name says otherwise), and gives each apath(file.exe#entry:NAME) that any tool opens, decompressed, in memory. Ask an assistant: "Which Python script doese21e0977….exerun, and which crypto libraries does it bundle?"
References¶
- What PyInstaller does and how it does it: one-file and one-folder bundles and the bootloader.
- PyInstaller advanced topics: the CArchive and PYZ formats.

