Since the release of macOS 12.3 (Monterey), Python has not been included with Apple’s operating system. This programming language, popular among developers and researchers for its versatility, was deemed unnecessary by Apple. Notably, even before its removal, macOS only included the deprecated Python 2.7 rather than the latest versions.
Users can only manually install Python using package managers like Homebrew and create virtual environments with tools like pyenv to manage multiple concurrent Python versions.
As macOS adoption continues to rise, threat actors are becoming increasingly creative in targeting the Apple ecosystem. One lesser-known but growing trend is the use of PyInstaller, a Python utility that packages scripts into standalone executables.
Although PyInstaller is widely used for legitimate software distribution, it is now frequently abused by malware authors to create cross-platform, self-contained malware capable of running on macOS without requiring Python to be installed.
This article explores how adversaries leverage PyInstaller to deliver sophisticated macOS malware, how these threats evade traditional detection methods, and what malware researchers can do to analyze and defend against them.
Installing Python and ensuring that all dependencies are correctly configured for a given application can be a headache. Developers often rely on complex structures when building Python applications. Following best practices, they typically include support files, such as requirements.txt, to inform deployment tools which libraries must be installed for the code to run properly.
PyInstaller is a widely used tool that compiles Python applications into standalone executables. It bundles the Python interpreter and all necessary dependencies into a single binary file. Its cross-platform compatibility and ease of use make it appealing not only to developers but also to malware authors.
When a PyInstaller-packed executable is deployed on macOS, its Mach-O format and embedded Python bytecode present unique challenges for defenders. This technique has been adopted by a wide range of threats, including file coders (a.k.a. ransomware), information stealers, and keyloggers.
Intego’s previous discovery of OSX/Shlayer, a well-known piece of Mac malware, highlights the effectiveness of multi-stage dropper techniques on macOS. Although Shlayer used shell scripts embedded in fake Flash installer .app bundles, instead of Python, the way it delivered its payloads and concealed its operations is similar to how PyInstaller-based malware behaves.
Typical characteristics include:
In one variant, Shlayer leveraged OpenSSL to decode and decrypt a bundled file, which was then dynamically executed in memory. This behavior is comparable to how a PyInstaller-packed binary can execute Python scripts contained within its .pyz archive.
This article examines an example of ransomware Python scripts found inside a PyInstaller-packed Mach-O sample. The sample is intended for educational purposes and is named Ransomware_script (SHA-256: f123e1513bc86ab157c5360d879fe569e54e4955eaee64be8c3232ef3f4f54e0).
Some malware variants, such as Shlayer A, may be signed with valid Apple Developer IDs, allowing them to appear legitimate and bypass Gatekeeper protections.
The Ransomware_script Mach-O binary is self-signed using an ad hoc signature. This means it lacks a Developer ID certificate verified by Apple’s signing authority. Despite this, it can still bypass Gatekeeper on macOS due to how ad hoc signatures are treated.
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:
Many open-source applications use self-signed code to avoid the cost of obtaining a Developer ID certificate. Self-signed components, such as embedded libraries, are common within otherwise notarized applications.
Every Mach-O binary built with PyInstaller leaves behind specific indicators. Analysts can look for these markers to identify suspicious binaries.
Using tools like Hopper Disassembler helps isolate these strings at specific addresses in the binary code.
For example:
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;
In its “modal” structure, a PyInstaller-generated Mach-O binary contains a PYZ archive, which includes everything needed to run the Python application. This is conceptually similar to a macOS executable compressed with tools like the now-deprecated UPX format, a method often used to hide malicious strings and evade antivirus detection.
A typical PyInstaller-packed malware binary on macOS is a Mach-O executable that contains:
To identify such binaries, researchers can use the following command:
stringssuspicious_file | grep -i_ pyinstaller_pyz
When downloaded via macOS applications (e.g., Apple Safari), files may be tagged with a quarantine flag. This can be verified 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 (x-man-page://xattr) tool.
% sudo xattr -d com.apple.quarantine
f123e1513bc86ab157c5360d879fe569e54e4955eaee64be8c3232ef3f4f54e0
Once the quarantine flag is removed, the Ransomware_script sample can be launched, even on macOS Sequoia 15.5 with SIP (System Integrity Protection) enabled, without requiring root privileges:
In practice, implants that deliver such Mach-O samples often do not use Gatekeeper-compliant distribution methods, meaning the quarantine flag is not applied in the first place.
We can isolate three identified stages in the infection chain. (Stage 0 is unknown, as the vector used to drop the PyInstaller-packed binary on the target system remains unidentified.)
f123e1513bc86ab157c5360d879fe569e54e4955eaee64be8c3232ef3f4f54e0
1bad4b0f42e1d2dd8aacadf0a994b82082d159ee210ebc6d5628587643d03ea1
Container file:
13a35628258d7c5d3c97db15489e66d393324a73607288a30d6c844262af1125
9c9611ac997d3bf2a513e0c7caa2cc94acf60921cc30d4e65710b6a479775011
Container file:
442df258ad8352966da9ba43bdb6a338ce9952e6912dbda3be5560b8dd12a1e7
PyInstaller provides several options for building self-extracting Python applications. During this process, malware authors must specify the target operating system. When the PyInstaller tool is executed, the Python interpreter is bundled along with other dependencies.
The collected data may include plain .py Python scripts, or compiled Python files, either stored in folders or compressed into a special archive format known as a PYZ archive.
One widely used open-source tool for extracting PYZ archives from PyInstaller executables is pyinstxtractor. However, this tool depends on having Python installed and requires compatibility with the specific Python version used when the original binary was built. As a result, users may encounter various version-related errors when attempting extraction.
To streamline the process, online services such as PyInstaller Extractor WEB (an online version of pyinstxtractor) can be used instead.
Once the PYZ archive has been extracted, its contents can reveal valuable information. For instance, the extraction logs help identify possible entry points within the payload.
In this case, one such entry appears to be the compiled script: ransomware_script.pyc
It’s worth noting that the name of the suspicious ransomware_script executable closely resembles the self-signed ad hoc identifier string used in the binary:
Identifier=ransomware_script-55554944014579c30b5e3f0e89275f47324c78a6
This naming pattern—<filename>-555<random_hash>—can serve as an additional heuristic when analyzing similarly crafted malware samples.
After unpacking the Mach-O PyInstaller sample, a quick inspection of its contents reveals several interesting elements:
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 intego staff 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 ransomware_script.pyc
-rw-rw-r–@ 1 intego staff 305 Nov 30 1979 struct.pyc
%otool-hlLsample_Macho_file
A ./ransomware_script.pyc
SHA-256: 13a35628258d7c5d3c97db15489e66d393324a73607288a30d6c844262af1125
B ./PYZ-00.pyz_extracted/ransomware.pyc
SHA-256: 442df258ad8352966da9ba43bdb6a338ce9952e6912dbda3be5560b8dd12a1e7
File system search and hash verification:
f123e1513bc86ab157c5360d879fe569e54e4955eaee64be8c3232ef3f4f54e
0_extracted%find.-nameransomware”*”
./ransomware_script.pyc
./PYZ-00.pyz_extracted/ransomware.pyc
f123e1513bc86ab157c5360d879fe569e54e4955eaee64be8c3232ef3f4f54e
0_extracted%shasum-a 256
./ransomware_script.pyc
13a35628258d7c5d3c97db15489e66d393324a73607288a30d6c844262af1125 ./ransomware_script.pyc
f123e1513bc86ab157c5360d879fe569e54e4955eaee64be8c3232ef3f4f54e
0_extracted%shasum-a 256
./PYZ-00.pyz_extracted/ransomware.pyc
442df258ad8352966da9ba43bdb6a338ce9952e6912dbda3be5560b8dd12a1e7
./PYZ-00.pyz_extracted/ransomware.pyc
PyLingual Python Decompiler is a web-based tool for decompiling .pyc Python-compiled scripts. It performs bytecode-to-source conversion and can display notifications for any bytecode or syntax errors encountered during the process.
Example string offsets:
269
QApplication)
286 Ransomware
298 __main__
308 12345678z\Your system is under attack.
Pay 2.5 Bitcoin to address O to restore your filesimmediately.z
412 76665@tor.com)
429 password
439 ransom_message
455 extensions
467 email)
485 shutil
493 PyQt5.QtWidgetsr
515 ransomwarer
532 __name__
542 argv
553 show
559 exit
565 exec_
579 ransomware_script.py
601 <module>r
IOCs % shasum -a 256 ransomware_script.pyc_Decompiled.py
1bad4b0f42e1d2dd8aacadf0a994b82082d159ee210ebc6d5628587643d03ea1 ransomware_script.pyc_Decompiled.py
Searching for ransom-related strings in this compiled file confirms multiple symbols tied to ransomware behavior:
f123e1513bc86ab157c5360d879fe569e54e4955eaee64be8c3232ef3f4f54e
0_extracted%str./PYZ-00.pyz_extracted/ransomware.pyc|grep-iransom
500 Ransomware
733 ransom_message
834 ransomware.pyr
854 Ransomware.__init__
1737 !Ransomware-EducationalUseOnly
2091 ransom_note
2140 Ransomware.initUI
2898 Ransomware.encrypt_all_drives.
3732 Ransomware.encrypt_drive4
4229 ,Ransomware.should_encrypt.<locals>.<genexpr>C
4366 Ransomware.should_encrypt@
4715 RANSOM_NOTE.txtr
4898 )Ransomware.is_excluded.<locals>.<genexpr>H
5431 Ransomware.is_excludedE
6277 Ransomware.encrypt_fileN
7370 RansomwareDecryptor
7504 Ransomware.decrypt_files^
7885 Ransomware.update_progresso
8173 !Ransomware.on_decryption_completer
9090 3. SendtheBitcointotheaddressspecifiedintheRANSOM_NOTE.txtfile.
9444 ransom_note_path
9502 Ransomware.create_instructionsv
10363 RansomwareDecryptor.__init__
11336 *RansomwareDecryptor.run.<locals>.<genexpr>
11664 RansomwareDecryptor.run
12350 !RansomwareDecryptor.decrypt_drive
13354 RansomwareDecryptor.decrypt_file
IOCs % 256 ransomware.pyc_Decompiled.py
9c9611ac997d3bf2a513e0c7caa2cc94acf60921cc30d4e65710b6a479775011 ransomware.pyc_Decompiled.py
The decompiled scripts define a Qt-based UI designed to simulate a ransomware attack. Key features include:
Class and function definitions:
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 initUI(self):
def encrypt_all_drives(self):
def encrypt_drive(self, drive):
def should_encrypt(self, file_path):
def is_excluded(self, file_path):
def encrypt_file(self, file_path, key, salt):
def decrypt_files(self):
def update_progress(self, value):
def on_decryption_complete(self):
def create_instructions(self):
def __init__(self,password, progress_bar):
def run(self):
def decrypt_drive(self, drive):
def decrypt_file(self, file_path, password):
This sample is for educational purposes only. When executed in a sandboxed environment, it does not cause any destructive impact.
Upon execution, the Qt-based UI displays a message claiming that the victim’s files have been encrypted, demanding 2.5 Bitcoin to unlock them.
Additionally, the malware drops two files on the user’s desktop:
(1) INSTRUCTIONS.txt
How to pay with Bitcoin:
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.
No encrypted files were detected during live execution.
This sample also does not implement persistence. However, other threat types, such as information stealers or keyloggers, may add Launch daemons to persist across reboots or user sessions.
Intego’s antivirus solution can detect malicious PyInstaller-packed Mach-O binaries and Python-compiled and decompiled scripts. When such threats are detected, Intego’s users are presented with a cleanup option and can choose to delete the identified files immediately.
“trojan:OSX/Ransomware.ext” found in “./IOCs/f123e1513bc86ab157c5360d879fe569e54e4955eaee64be8c3232ef3f4f54e0 by engines: antiviralLib”. Action performed: none.
Cleanup analysis:
1 item(s)to clean:
./IOCs/f123e1513bc86ab157c5360d879fe569e54e4955eaee64be8c3232ef3f4f54e0
1 action(s)to run:
Role:Destructive – When:On update – delete file at path
./IOCs/f123e1513bc86ab157c5360d879fe569e54e4955eaee64be8c3232ef3f4f54e0
“trojan:Python/Ransomware.gen” found in “./IOCs/ransomware_script.pyc_Decompiled.py by engines: antiviralLib”. Action performed: none.
Cleanup analysis:
1 item(s) to clean:
./IOCs/ransomware_script.pyc_Decompiled.py
1 action(s) to run:
Role:Destructive – When:On update – delete file at path./IOCs/ransomware_script.pyc_Decompiled.py
“trojan:Python/Ransomware.gen” found in “./IOCs/ransomware.pyc_Decompiled.py by engines: antiviralLib”. Action performed: none.
Cleanup analysis:
1 item(s) to clean:
./IOCs/ransomware.pyc_Decompiled.py
1 action(s) to run:
Role:Destructive – When:On update – delete file at path ./IOCs/ransomware.pyc_Decompiled.py
“trojan:Python/Ransomware.gen” found in “./IOCs/f123e1513bc86ab157c5360d879fe569e54e4955eaee64be8c3232ef3f4f54e0_extracted.zip/PYZ-00.pyz_extracted/ransomware.pyc by engines: antiviralLib”. Action performed: none.
Cleanup analysis:
1 item(s) to clean:
./IOCs/f123e1513bc86ab157c5360d879fe569e54e4955eaee64be8c3232ef3f4f54e0_extracted.zip
1 action(s) to run:
Role:Destructive – When:On update – delete file at path ./IOCs/f123e1513bc86ab157c5360d879fe569e54e4955eaee64be8c3232ef3f4f54e0_extracted.zip
“trojan:Python/Ransomware.gen” found in “./IOCs/f123e1513bc86ab157c5360d879fe569e54e4955eaee64be8c3232ef3f4f54e0_extracted.zip/ransomware_script.pyc by engines: antiviralLib”. Action performed: none.
Cleanup analysis:
1 item(s) to clean:
./IOCs/f123e1513bc86ab157c5360d879fe569e54e4955eaee64be8c3232ef3f4f54e0_extracted.zip
1 action(s) to run:
Role:Destructive – When:On update – delete file at path ./IOCs/f123e1513bc86ab157c5360d879fe569e54e4955eaee64be8c3232ef3f4f54e0_extracted.zip
PyInstaller may have been designed for convenience, but it becomes a formidable tool for building cross-platform, stealthy, and modular malware in attackers’ hands. Its use on macOS is a concern for the evolution of malware delivery due to its combination of Python’s flexibility with native execution capabilities.
Security professionals must stay vigilant, refine their tooling, and educate others on how these threats bypass traditional detection. As malware increasingly blurs platform boundaries, defenders must adapt, decode, and disarm — one binary at a time.
Name | SHA-256 |
ransomware_script | f123e1513bc86ab157c5360d879fe569e54e4955eaee64be8c3232ef3f4f54e0 |
ransomware_script.pyc | 13a35628258d7c5d3c97db15489e66d393324a73607288a30d6c844262af1125 |
ransomware.pyc | 442df258ad8352966da9ba43bdb6a338ce9952e6912dbda3be5560b8dd12a1e7 |
ransomware_script.pyc_Decompiled.py | 1bad4b0f42e1d2dd8aacadf0a994b82082d159ee210ebc6d5628587643d03ea1 |
ransomware.pyc_Decompiled.py | 9c9611ac997d3bf2a513e0c7caa2cc94acf60921cc30d4e65710b6a479775011 |
Tool | Use Case |
PyInstaller Extractor WEB | Online tool for extracting embedded .pyz archives from PyInstaller executables (like pyinstxtractor) |
PyLingual Python Decompiler | Web-based tool for decompiling .pyc (compiled Python) files into .py source |
Hopper | Disassembler and decompiler for inspecting and debugging Mach-O binaries |
IDA Pro | Advanced disassembler used by experienced reverse engineers |
Binary Ninja | Versatile tool for disassembly, decompilation, debugging, and binary analysis; includes cloud-based options |
otool / codesign | Command-line tools for viewing embedded code signatures and runtime libs in Mach-O binaries |
MachO-Explorer | GUI-based utility to visualize and analyze the internal structure of Mach-O files |