Site icon The Mac Security Blog

Python Applications, Active & Hidden Malware Infection Vector on macOS

Executive Summary

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.

PyInstaller, the Beloved Wizard

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.

What Is PyInstaller, and What Problems Does It Pose

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.

Key Threat Benefits:

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.

Historical Parallel: Lessons from OSX/Shlayer

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.

Case Study: Ransomware Script

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).

Code Signing and Misuse of Developer IDs

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.

Detection Considerations

Every Mach-O binary built with PyInstaller leaves behind specific indicators. Analysts can look for these markers to identify suspicious binaries.

Indicators of PyInstaller Use

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.

Inside a PyInstaller-Packed Malware on macOS

What Does It Look Like?

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:

  1. % chmod u+X Ransomware_script
  2. Then, the user may right-click the file and select: Open With > Terminal in the pop-up menu.

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.

Infection Steps

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.)

Stage 1: Dropper

f123e1513bc86ab157c5360d879fe569e54e4955eaee64be8c3232ef3f4f54e0

Stage 2: Payload

1bad4b0f42e1d2dd8aacadf0a994b82082d159ee210ebc6d5628587643d03ea1

Container file:

13a35628258d7c5d3c97db15489e66d393324a73607288a30d6c844262af1125

Stage 3: Loader

9c9611ac997d3bf2a513e0c7caa2cc94acf60921cc30d4e65710b6a479775011

Container file:

442df258ad8352966da9ba43bdb6a338ce9952e6912dbda3be5560b8dd12a1e7

Extracting Pyz Archive

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.

Package Structure

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

Payload (A) and Loader (B)

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

Decompiler

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.

Ransomware_script.pyc

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

ransomware_script.pyc_Decompiled.py

IOCs % shasum -a 256 ransomware_script.pyc_Decompiled.py

1bad4b0f42e1d2dd8aacadf0a994b82082d159ee210ebc6d5628587643d03ea1 ransomware_script.pyc_Decompiled.py

PYZ-00.pyz_extracted/ransomware.pyc

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

ransomware.pyc_Decompiled.py

IOCs % 256 ransomware.pyc_Decompiled.py

9c9611ac997d3bf2a513e0c7caa2cc94acf60921cc30d4e65710b6a479775011 ransomware.pyc_Decompiled.py

Python Application Behavior

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):

Consequences on System Target

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:

  1. Go to a Bitcoin exchange platform (e.g., Coinbase, Binance)
  2. Create an account and purchase the necessary amount of Bitcoin
  3. Send the Bitcoin to the address specified in the .txt file
  4. 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.

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.

Detection

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.

Detection Log Summary:

“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

Conclusion

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.

IOCs

Name SHA-256
ransomware_script f123e1513bc86ab157c5360d879fe569e54e4955eaee64be8c3232ef3f4f54e0
ransomware_script.pyc 13a35628258d7c5d3c97db15489e66d393324a73607288a30d6c844262af1125
ransomware.pyc 442df258ad8352966da9ba43bdb6a338ce9952e6912dbda3be5560b8dd12a1e7
ransomware_script.pyc_Decompiled.py 1bad4b0f42e1d2dd8aacadf0a994b82082d159ee210ebc6d5628587643d03ea1
ransomware.pyc_Decompiled.py 9c9611ac997d3bf2a513e0c7caa2cc94acf60921cc30d4e65710b6a479775011

Dissection Tools and Techniques for Analysts

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

Share this: