Executive Summary
Since the release of macOS 12.3 (Monterey), Apple ceased bundling the Python interpreter with its operating system. This versatile programming language, while popular among developers, was deemed superfluous by Apple. It is worth noting that even before its removal, macOS only included the outdated Python 2.7 instead of newer versions.
Users must now manually install Python using package managers like Homebrew or create virtual environments with tools such as pyenv to manage different Python versions concurrently.
As macOS market share continues to rise, threat actors are becoming increasingly sophisticated in targeting the Apple ecosystem. A notable and growing trend is the misuse of PyInstaller, a legitimate Python utility that packages scripts into standalone executables.
Although PyInstaller is widely used for lawful software distribution, it is frequently abused by malware authors to create self-contained and cross-platform malware. Critically, this packed payload can execute flawlessly on macOS without Python needing to be installed on the target system.
This comprehensive analysis details how adversaries leverage PyInstaller to deliver advanced macOS malware, explains how these threats evade signature-based detection, and outlines advanced strategies for reverse engineering and defense.
PyInstaller, The Developer's Tool Turned Threat
Installing Python and ensuring all dependencies are correctly configured for a given application is often a complex task. Developers typically manage Python applications with intricate structures, including support files like requirements.txt to guide deployment tools on which libraries must be installed.
What PyInstaller Is, and the Security Problems It Poses
PyInstaller is a tool that compiles Python applications into truly standalone executables. It bundles the required Python interpreter and all dependencies into a single binary file. Its excellent cross-platform compatibility and ease of use make it appealing not only to developers but, unfortunately, also to malware authors.
Key Threat Advantages:
- Zero Host Dependency: No requirement for Python or specific libraries on the target machine, guaranteeing execution.
- Full Packaging: Produces a fully packed binary that significantly complicates static analysis and inspection.
- Cross-Platform Efficiency: A single malicious Python script can be quickly packaged for both Windows and macOS with minimal code modification.
When a PyInstaller-packed executable is deployed on macOS, its Mach-O format combined with embedded Python bytecode poses unique challenges for security tools. This methodology is now favored by creators of various threats, including file coders (ransomware), information stealers, and persistent keyloggers.
Historical Context: The Shadow of OSX/Shlayer
Previous analysis of OSX/Shlayer, a notorious piece of Mac malware, established the effectiveness of multi-stage dropper techniques on macOS. While Shlayer used shell scripts embedded in fake Flash installer application bundles, the mechanism of payload delivery and operational concealment is conceptually analogous to how PyInstaller-based malware operates.
Common characteristics include:
- A primary macOS executable acting as a dropper.
- Obfuscation methods to conceal the final infection code.
- An extracted ZIP or Mach-O binary containing secondary components (e.g., adware, spyware, or persistence files).
In one Shlayer variant, the malware leveraged OpenSSL to decode and decrypt a bundled file, which was then dynamically executed in memory. This parallels how a PyInstaller-packed binary runs Python scripts from within its embedded .pyz archive.
Case Study: Ransomware Script Analysis
This section examines an illustrative example of Python ransomware scripts found inside a PyInstaller-packed Mach-O sample. The sample is used for educational security analysis and is named Ransomware\_script (SHA-256: f123e1513bc86ab157c5360d879fe569e54e4955eaee64be8c3232ef3f4f54e0).
Code Signing and Ad Hoc Signatures
Some malware, like certain Shlayer strains, may use valid Apple Developer IDs to bypass Gatekeeper. However, this sample employs a more common evasion tactic.
The Ransomware\_script Mach-O binary is self-signed using an ad hoc signature. This means it lacks the verification of an Apple Developer ID certificate. Crucially, due to how macOS treats ad hoc signatures, it can still bypass basic Gatekeeper checks upon execution.
Example terminal output:
% codesign -dvv f123e1513bc86ab157c5360d879fe569e54e4955eaee64be8c3232ef3f4f54e0
Executable=
f123e1513bc86ab157c5360d879fe569e54e4955eaee64be8c3232ef3f4f54e0
Identifier=ransomware_script-55554944014579c30b5e3f0e89275f47324c78a6
Format=Mach-Othin (arm64)
CodeDirectoryv=20400size=219827 flags=0x2(adhoc)hashes=6863+2 location=embedded
Signature=adhoc
Info.plist=notbound
TeamIdentifier=not set
SealedResources=none
Internalrequirements count=0 size=12
Mitigation Note (SEO Enhancement - Prevention):
While ad hoc signatures are common in legitimate, smaller open-source applications (to avoid developer fees), security teams must treat all ad hoc signed files with heightened suspicion. This technique is consistently leveraged by threat actors for simple security evasion.
Technical Detection Indicators
Every Mach-O binary constructed with PyInstaller leaves behind specific, observable forensic indicators. Analysts should prioritize searching for these markers.
Markers of PyInstaller Packing
- Presence of the .pyz archive embedded within the Mach-O structure.
- Specific internal strings, including \_MEIXXXXXX and \_pyinstaller\_pyz.
Using reverse engineering tools like Hopper Disassembler helps isolate these unique strings at specific binary offsets, providing immediate evidence of the packing method.
Example (Disassembler Output):
aMeixxxxxx:
000000010000be38 db “_MEIXXXXXX” , 0 ; DATAXREF=sub_100007b1c+260, sub_100007b1c+264, sub_100007b1c+456, sub_100007b1c+460, sub_100007b1c+652, sub_100007b1c+656, sub_100007b1c+784, sub_100007b1c+788, sub_100007b1c+920, sub_100007b1c+924, sub_100007b1c+1056
r0=strlen(r19);
*(int32_t*)(0x7 + r19 + r0) = 0x585858;
*(r19 + r0) = *“_MEIXXXXXX”;
aPyinstallerpyz:
000000010000b128 db “_pyinstaller_pyz”,0 ; DATAXREF=sub_100006074+232
loc_10000614c:
(*0x1000114e8)(“_pyinstaller_pyz”,r19);
(*0x1000113d0)(r19);
if (r21==0x0) goto loc_10000619c;
This PyInstaller-generated Mach-O binary contains a PYZ archive, encapsulating all code needed for execution. This mechanism shares similarities with older macOS executables compressed using tools like the obsolete UPX format, which was used to hide malicious strings and frustrate early antivirus scanners.
Dissecting the PyInstaller-Packed Malware on macOS
Core Structure and Dependencies
A typical PyInstaller-packed malicious binary on macOS is a Mach-O executable composed of:
- The embedded .pyz archive (Python bytecode compressed in ZIP format).
- The self-contained Python interpreter (as a shared object).
- A small C-based loader responsible for unpacking and executing the internal scripts.
To quickly identify such binaries, researchers can use this command:
stringssuspicious_file | grep -i_ pyinstaller_pyz
When downloaded via web browsers (e.g., Safari), files may be flagged with a quarantine flag. This can be confirmed using the xattr tool:
% xattr f123e1513bc86ab157c5360d879fe569e54e4955eaee64be8c3232ef3f4f54e0
com.apple.lastuseddate#PS
com.apple.macl
com.apple.quarantine
This flag can be easily removed with root user privileges (%sudo) using the xattr tool, demonstrating a simple bypass for initial user warnings:
% sudo xattr -d com.apple.quarantine
f123e1513bc86ab157c5360d879fe569e54e4955eaee64be8c3232ef3f4f54e0
Once the quarantine flag is removed, the Ransomware\_script sample can be successfully launched, even on macOS Sequoia 15.5 with SIP (System Integrity Protection) enabled, without requiring root privileges:
- % chmod u+X Ransomware\_script
- Then, the user may right-click the file and select: Open With > Terminal in the pop-up menu.
In real-world attacks, the initial vector (Stage 0) often circumvents Gatekeeper-compliant distribution, meaning the quarantine flag is never applied, leading to direct execution risk.
Infection Stages
Our analysis isolates three identified stages in the infection chain. (Stage 0, the initial delivery vector of the PyInstaller binary, remains unidentified.)
Stage 1: Dropper
f123e1513bc86ab157c5360d879fe569e54e4955eaee64be8c3232ef3f4f54e0
- File Name: ransomware_script
- File Type: Mach-O
- Size: 28,346,128 bytes (approx. 4 MB)
- MD5:13737a54b5b6d94b8780df5c519980aa
- Tags: arm, macho, 64-bit
- VirusTotal
Stage 2: Payload
1bad4b0f42e1d2dd8aacadf0a994b82082d159ee210ebc6d5628587643d03ea1
- File Name: ransomware_script.pyc_Decompiled.py
- File Type: Python script (ASCII text, executable)
- VirusTotal
Container file:
13a35628258d7c5d3c97db15489e66d393324a73607288a30d6c844262af1125
- Path: ./f123e1513bc86ab157c5360d879fe569e54e4955eaee64be8c3232ef3f4f54e0_extracted/ransomware_script.pyc
- File Type: data
- VirusTotal
Stage 3: Loader
9c9611ac997d3bf2a513e0c7caa2cc94acf60921cc30d4e65710b6a479775011
- File Name: ransomware.pyc_Decompiled.py
- File Type: Python script (ASCII text executable, 483-character lines)
- VirusTotal
Container file:
442df258ad8352966da9ba43bdb6a338ce9952e6912dbda3be5560b8dd12a1e7
- Path: ./f123e1513bc86ab157c5360d879fe569e54e4955eaee64be8c3232ef3f4f54e0_extracted/PYZ-00.pyz_extracted/ransomware.pyc
- File Type: data
- VirusTotal
Extracting the PYZ Archive
PyInstaller offers multiple options for building self-extracting Python applications. The process bundles the Python interpreter along with all specified dependencies.
The collected code can be standard .py Python scripts or compiled Python files, often compressed into a special format known as a PYZ archive.
A popular open-source tool for extracting PYZ archives is pyinstxtractor. However, compatibility issues with the specific Python version used during the binary's original compilation can often lead to errors. To simplify this, online services such as PyInstaller Extractor WEB (an online version of pyinstxtractor) provide an easier alternative.
Once the PYZ archive is extracted, its contents become available for inspection, helping to identify key entry points within the malicious payload.
In this sample, one such entry point appears to be the compiled script: ransomware\_script.pyc
It is worth noting that the name of the suspicious ransomware\_script executable closely matches the self-signed ad hoc identifier string used in the binary:
Identifier=ransomware_script-55554944014579c30b5e3f0e89275f47324c78a6
This pattern—<filename>-555<random_hash>—serves as an additional heuristic for researchers when analyzing similar malware samples, aiding in rapid classification.
Analysis of the Package Structure
After successfully unpacking the Mach-O PyInstaller sample, inspection of the filesystem reveals several key elements designed to ensure self-sufficiency and smooth operation:
f123e1513bc86ab157c5360d879fe569e54e4955eaee64be8c3232ef3f4f54e
0_extracted%ls-la
total 11336
drwx——@ 32 intego staff 1024 Jun 10 21:18 .
drwxr-xr-x 10 intego wheel 320 Jun 10 21:18 ..
-rw-r–r–@ 1 intego staff 6148 Jun 10 21:26 .DS_Store
drwxr-xr-x@ 9 intego staff 288 Jun 10 21:18 Crypto
drwxr-xr-x@ 85 intego staff 2720 Jun 10 21:18 PYZ-00.pyz_extracted
drwxr-xr-x@ 7 intego staff 224 Jun 10 21:18 PyQt5
-rw-rw-r–@ 1 integstaff 38 Nov 30 1979 Python
drwxr-xr-x@ 5 intego staff 160 Jun 10 21:18 Python.framework
-rw-rw-r–@ 1 integstaff 49 Nov 30 1979 QtCore
-rw-rw-r–@ 1 intego staff 49 Nov 30 1979 QtDBus
-rw-rw-r–@ 1 intego staff 47 Nov 30 1979 QtGui
-rw-rw-r–@ 1 intego staff 55 Nov 30 1979 QtNetwork
-rw-rw-r–@ 1 intego staff 65 Nov 30 1979 QtPrintSupport
-rw-rw-r–@ 1 intego staff 47 Nov 30 1979 QtQml
-rw-rw-r–@ 1 intego staff 59 Nov 30 1979 QtQmlModels
-rw-rw-r–@ 1 intego staff 51 Nov 30 1979 QtQuick
-rw-rw-r–@ 1 intego staff 47 Nov 30 1979 QtSvg
-rw-rw-r–@ 1 intego staff 61 Nov 30 1979 QtWebSockets
-rw-rw-r–@ 1 intego staff 55 Nov 30 1979 QtWidgets
-rw-rw-r–@ 1 intego staff 1396821 Nov 30 1979 base_library.zip
drwxr-xr-x@ 45 intego staff 1440 Jun 10 21:18 lib-dynload
-rw-rw-r–@ 1 intego staff 3619168 Nov 30 1979 libcrypto.3.dylib
-rw-rw-r–@ 1 intego staff 650768 Nov 30 1979 libssl.3.dylib
-rw-rw-r–@ 1 intego staff 2849 Nov 30 1979 pyi_rth_inspect.pyc
-rw-rw-r–@ 1 intego staff 1585 Nov 30 1979 pyi_rth_pkgutil.pyc
-rw-rw-r–@ 1 intego staff 2040 Nov 30 1979 pyi_rth_pyqt5.pyc
-rw-rw-r–@ 1 intego staff 1916 Nov 30 1979 pyiboot01_bootstrap.pyc
-rw-rw-r–@ 1 intego staff 4813 Nov 30 1979 pyimod01_archive.pyc
-rw-rw-r–@ 1 intego staff 31848 Nov 30 1979 pyimod02_importers.pyc
-rw-rw-r–@ 1 intego staff 6469 Nov 30 1979 pyimod03_ctypes.pyc
-rw-rw-r–@ 1 intego staff 727 Nov 30 1979 pyimod04_i64.pyc
-rw-rw-r–@ 1 intego staff 727 Nov 30 1979 pyimod05_os.pyc
-rw-rw-r–@ 1 intego staff 727 Nov 30 1979 ransomware_script.pyc
-rw-rw-r–@ 1 intego staff 305 Nov 30 1979 struct.pyc
- The PYZ-00.pyz\_extracted directory holds the unpacked Python application files.
- The application utilizes Qt, specifically QtWidgets, to construct a graphical interface for displaying the ransom demand.
- Two self-signed OpenSSL dynamic libraries are included:
- libcrypto.3.dylib
- Libssl.3.dylib
- These libraries are essential for cryptographic functions and TLS communication, ensuring the malware can encrypt files and communicate with its C2 infrastructure independent of the host system's OpenSSL version.
Payload (A) and Loader (B) Hash Verification
Filesystem search and hash verification confirm the two core compiled Python files:
f123e1513bc86ab157c5360d879fe569e54e4955eaee64be8c3232ef3f4f54e0_extracted%find.-nameransomware”*”
./ransomware_script.pyc
./PYZ-00.pyz_extracted/ransomware.pyc
f123e1513bc86ab157c5360d879fe569e54e4955eaee64be8c3232ef3f4f54e0_extracted%shasum-a 256 ./ransomware_script.pyc
13a35628258d7c5d3c97db15489e66d393324a73607288a30d6c844262af1125 ./ransomware_script.pyc
f123e1513bc86ab157c5360d879fe569e54e4955eaee64be8c3232ef3f4f54e0_extracted%shasum-a 256 ./PYZ-00.pyz_extracted/ransomware.pyc
442df258ad8352966da9ba43bdb6a338ce9952e6912dbda3be5560b8dd12a1e7 ./PYZ-00.pyz_extracted/ransomware.pyc
Decompiler Analysis
PyLingual Python Decompiler is a valuable web-based utility for decompiling .pyc Python-compiled scripts. It facilitates the bytecode-to-source conversion and assists analysts by highlighting potential errors, offering a clearer view of the malicious logic.
Ransomware\_script.pyc Analysis
- SHA-256: 13a35628258d7c5d3c97db15489e66d393324a73607288a30d6c844262af1125
- Review of the strings extracted from this compiled Python payload reveals explicit references to:
- The original source filename
ransomware_script.py - Qt-based UI components
- The ransom message displayed to the user
- The original source filename
Example string offsets:
269
QApplication)
286 Ransomware
308 12345678z\Your system is under attack.
Pay 2.5 Bitcoin to address O to restore your filesimmediately.z
412 76665@tor.com)
579 ransomware_script.py
ransomware\_script.pyc\_Decompiled.py (Stage 2)
- SHA-256: 1bad4b0f42e1d2dd8aacadf0a994b82082d159ee210ebc6d5628587643d03ea1
- Decompilation status: No significant syntax errors were reported.
PYZ-00.pyz\_extracted/ransomware.pyc Analysis
- SHA-256: 442df258ad8352966da9ba43bdb6a338ce9952e6912dbda3be5560b8dd12a1e7
Searching for ransom-related strings in this deeper compiled file confirms multiple symbols tied directly to the ransomware functionality:
f123e1513bc86ab157c5360d879fe569e54e4955eaee64be8c3232ef3f4f54e
0_extracted%str./PYZ-00.pyz_extracted/ransomware.pyc|grep-iransom
500 Ransomware
733 ransom_message
2898 Ransomware.encrypt_all_drives.
7370 RansomwareDecryptor
9090 3. SendtheBitcointotheaddressspecifiedintheRANSOM_NOTE.txtfile.
12350 !RansomwareDecryptor.decrypt_drive
ransomware.pyc\_Decompiled.py (Stage 3)
- SHA-256: 9c9611ac997d3bf2a513e0c7caa2cc94acf60921cc30d4e65710b6a479775011
- Decompilation status: Contains several syntax errors, which is typical for code that has been lightly obfuscated or manipulated.
Python Application Behavior & Consequences
The decompiled scripts define a Qt-based UI designed to mimic a real ransomware attack. Key class and function definitions explicitly confirm the destructive intent:
IOCs % cat ransomware.pyc_Decompiled.py | grep -i class
class Ransomware(QMainWindow):
class RansomwareDecryptor(QThread):
IOCs % catransomware.pyc_Decompiled.py | grep -i def
def __init__(self, password, ransom_message, extensions, email):
def encrypt_all_drives(self):
def decrypt_files(self):
Upon execution (in a controlled, sandboxed environment), the UI displays a message claiming files have been encrypted, demanding 2.5 Bitcoin. Additionally, the malware drops two files on the user’s desktop to convey the ransom message:
(1) INSTRUCTIONS.txt
How to pay with Bitcoin:
- Go to a Bitcoin exchange platform (e.g., Coinbase, Binance)
- Create an account and purchase the necessary amount of Bitcoin
- Send the Bitcoin to the address specified in the .txt file
- After the payment, email the transaction ID to 76665@tor.com
Note: Ensure that you follow the instructions carefully to recover your files.
(2) RANSOM\_NOTE.txt
Your system is under attack. Pay 2.5 Bitcoin to the address to restore your files immediately.
While this educational sample lacks persistence, other PyInstaller-packed threats, particularly information stealers, commonly include mechanisms like *Launch daemons* to ensure they persist across system reboots.
5. Advanced Mitigation and Prevention Strategies (Workflow Enhancement)
The rise of PyInstaller-packed malware demonstrates the persistent evolution of the macOS threat landscape. Effective protection requires moving beyond static signature analysis towards sophisticated, behavioral detection methods that monitor file and process activity at runtime. Defense must be multi-layered, targeting both the packaging and the execution behavior.
Defense for End-Users and System Administrators
- Enhanced Antivirus: Use security solutions that employ behavioral-based detection (Heuristics) rather than relying solely on simple signatures. These advanced tools can spot and stop the suspicious process of rapid, unauthorized file encryption or the internal archive unpacking—key hallmarks of PyInstaller malware—before the malicious payload executes fully.
- Application Control & Whitelisting (Admin Focus): Implement strict application control policies (e.g., via MDM or Endpoint Protection Platforms) that only permit execution of applications that are signed by verified, trusted Developer IDs. Block the execution of ad hoc signed or unsigned Mach-O binaries unless a business need is absolutely proven.
- Offline Backup Rule: Maintain rigorous adherence to the offline backup rule. Ensure Time Machine or external drives are physically disconnected when they are not actively performing a backup session.
Limitations of Apple’s Native Security
While Apple's built-in defenses are robust, PyInstaller packing exploits key limitations:
- Gatekeeper/Notarization: Gatekeeper primarily validates the signature and integrity of the main Mach-O executable. Since the malicious Python scripts are deeply nested within the .pyz archive and executed dynamically post-check, they bypass Gatekeeper's initial analysis.
- XProtect: XProtect relies on known signatures or simple heuristics. PyInstaller's ability to quickly repackage the payload means threat actors can generate new, unique binary variants rapidly, often outpacing XProtect's signature update schedule.
Detection
Intego’s antivirus solution is specifically designed to detect malicious PyInstaller-packed Mach-O binaries as well as the contained Python compiled and decompiled scripts. When these threats are identified, users are provided with a cleanup option to immediately delete the malicious files..
Detection Log Summary:
“trojan:OSX/Ransomware.ext” found in “./IOCs/f123e1513bc86ab157c5360d879fe569e54e4955eaee64be8c3232ef3f4f54e0 by engines: antiviralLib”. Action performed:
Frequently Asked Questions (FAQ) 🧐
Q: How does PyInstaller help malware bypass Gatekeeper?
PyInstaller bundles the malicious script and the interpreter into a single Mach-O file. Gatekeeper checks the signature of this outer executable, but the malicious Python code inside the compressed .pyz archive is generally not analyzed during the initial Gatekeeper check, allowing the internal threat to run, which is a significant security loophole.
Q: What is the significance of the _MEIXXXXXX string in a binary?
The _MEIXXXXXX string is a signature marker left by the PyInstaller bootloader. It typically refers to the temporary directory where the bundled contents (including the Python interpreter and the .pyz archive) are extracted for execution. Its presence is a strong indicator of a PyInstaller-packed binary, whether legitimate or malicious, and is a key forensic marker.
Q: Why is PyInstaller a growing threat compared to traditional Mac malware?
PyInstaller offers unique advantages: cross-platform portability (easier for threat actors to target Mac and Windows simultaneously) and the critical ability to run Python malware even though Apple removed Python from the operating system, making it a very versatile and low-friction attack vector.