Analyzing What Appears to be GNNCRY's macOS Test Build
macOS “GNNCRYS” ransomware PoC is a Linux port with predictable Blowfish keys, harmless but instructive.
GonnaCry is a ransomware variant primarily targeting Linux systems. It emerged around 2017 as an open-source proof-of-concept project on GitHub, created by developer Tarcísio Marinho for educational purposes to demonstrate ransomware mechanics, and encryption techniques. However, by 2021, malicious linux variants had appeared in the wild, encrypting user files and demanding cryptocurrency ransoms, leading to detections by antivirus firms.
A review of VirusTotal submissions confirms that the Linux variants still circulating are consistently flagged as ELF-ransomware.

While hunting for benign files, I came across a sample that exhibited properties closely matching those of GonnaCry, yet it had been compiled specifically for macOS.

Like any other researcher, checking the open-source project, I could not find any recent commits or information indicating the development of a macOS variant. This seems interesting, if the build was not officially planned, then who created it?

Initial Triage and Sample Overview
Getting a copy of the sample, we perform standard triage using file and otool to inspect the headers. The results confirmed it as a Mach-O arm64 executable.

Running strings revealed a hardcoded list of target file extensions covering various document types, images, archives, and more, alongside a cleartext ransom note.

Static Analysis
We then moved onto static analysis and found the flow to be straightforward, almost exactly like the original Linux PoC, just wearing a macOS coat.
Main execution flow
At first glance, the sample reveals a clear-text path variable exposing the developer's environment, indicating where the malware was either created or tested:
/home/tarcisio/tests/This path suggests the original author of GonnaCry may have been developing or testing a macOS variant.

The ransomware begins with environment path discovery:
_get_home_enviroment()→ Gets user's home directory_get_desktop_enviroment()→ Finds Desktop folder_get_username()→ Gets current username_get_trash_path()→ Constructs trash directory path_get_media_path()→ Constructs media directory path

The sample then performs targeted file enumeration by invoking _find_files() three times, scanning the home, trash, and media directories. Discovered files are stored in a linked list for processing.
Once enumeration is complete:
- Files are encrypted using
_encrypt_files() - A ransom note is generated on the Desktop via
_create_files_desktop()
After encryption, the malware performs resource cleanup, freeing all dynamically allocated memory to minimize forensic traces.
File discover
The _find_files() function performs a recursive directory traversal, hunting for files with specific extensions that have been hard coded into the binary. The full list of targeted extensions is extensive and spans common document, image, archive, and database formats:
doc docx xls xlsx ppt pptx pst ost msg eml vsd vsdx txt csv rtf wks wk1 pdf dwg onetoc2 snt jpeg jpg docb docm dot dotm dotx xlsm xlsb xlw xlt xlm xlc xltx xltm pptm pot pps ppsm ppsx ppam potx potm edb hwp 602 sxi sti sldx sldm vdi vmdk vmx gpg aes ARC PAQ bz2 tbk bak tar tgz gz 7z rar zip ba
Upon closer inspection, the function begins by parsing this space-delimited string using strtok() to isolate individual extensions. For each file encountered during traversal, it extracts the extension via _get_filename_ext() and compares it against the tokenized list using _strcmp(). When a match is found, the full file path is appended to a linked list through _append().

The traversal logic is straightforward yet robust. Regular files (type 8) trigger the extension check, while directories (type 4) prompt a recursive call into the sub-directory. To avoid infinite loops, entries named "." and ".." are explicitly skipped.
Notably, the sample includes several hardcoded paths such as, .local/share/Trash/ and /media/, which indicate that this binary was cross-compiled from a Linux-originating codebase.
Despite the developer path suggesting macOS testing (/home/tarcisio/tests/), these internal path references align closely with the original Linux variant, implying that a dedicated macOS port was never fully implemented.


Encryption
The ransomware sample then generates a 16-byte initialization vector (IV) using _generate_key and a 32-byte key via _generate_key. Even though the underlying Blowfish algorithm only consumes the first 16 bytes of the key. The actual encryption is performed by the _encrypt() function, which processes the file content in place.
For each encrypted file, the malware records the generated key, IV, and full file path into a linked list using _append(). This data is later serialized into a file named enc_files.gc, presumably to enable decryption in exchange for ransom.

Finally, the original plaintext file is securely overwritten and deleted using _shred() to prevent recovery.

Key generation
The ransomware relies on the standard C library function rand() for key and IV generation, rather than employing a cryptographically secure pseudorandom number generator (CSPRNG).
This is a known weakness: rand() produces entirely predictable output once the seed is known, and in this sample, no call to srand() appears anywhere in the code. As a result, the RNG initializes with its default seed, typically 1 on most systems, causing every execution of the malware to generate identical keys and IVs.

A further flaw to the existing weakness (rand()), the generated material is not raw binary but instead drawn from a restricted 78-character set:
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789,.-#@$%&(){};'?!Each byte is thus limited to one of 78 possible values (0x4E in hexadecimal), far short of the full 256-value range (0x00–0xFF) expected in proper cryptographic keys. This huge drop in randomness makes guessing the key easy, even without knowing the starting seed, and totally breaks the Blowfish encryption.
Secure deletion
The _shred() function securely wipes the original plaintext file by first retrieving its size with stat(). It then opens the file in write-only mode (O_WRONLY) and allocates a 4 KB buffer filled entirely with zeros.
The function proceeds to overwrite the entire file in 4 KB chunks, ensuring all original data is destroyed. Once overwriting is complete, it closes the file descriptor and finally deletes the file using remove().

Key storage
The _save_into_file_encrypted_list() function writes all encryption keys and initialization vectors to a file named enc_files.gc on the victim's Desktop. Each entry follows a simple plaintext format:
KEY:IV:ENCRYPTED_FILE_PATHAn example entry might appear as:
- aB3dE5fG7hI9jK1l:mN2oP4qR6sT8uV0w:/Users/victim/Documents/report.docx.GNNCRY
- xY9zA1bC3dE5fG7h:iJ1kL3mN5oP7qR9s:/Users/victim/Pictures/photo.jpg.GNNCRY
- tU8vW0xY2zA4bC6d:eF8gH0iJ2kL4mN6o:/Users/victim/Desktop/presentation.pptx.GNNCRYHere, the 32-byte key comes first, followed by the 16-byte IV, and then the full path to the encrypted file (ending in .GNNCRY). The original plaintext path is not stored, only the encrypted version.

The ransomware saves all the decryption keys and IVs in plain text inside enc_files.gc. If you still have this file, you can recover every encrypted file for free, no ransom needed.
Knowing this as an open-source ransomware PoC this behavior is expected, but I can't say the same for other samples which may be tampered with. This further shows a clear sign that this is either a test version or beginner-level malware, not the work of threat actors.
Dynamic analysis
Testing the sample to confirm if it works, I tried running it on a macOS system (Tahoe), and you get the usual warning which shows that the program is not verified and may harm the machine.

We ignore the message and allow the file to run, but I was surprised to see the process terminating immediately with another warning since the file fails verification.

As a third attempt we explicitly give the sample permission to run, but Apple once again kills the process, this time with a 'trace trap' message. Given that I got 52108 killed, macOS security is actively killing the process. This is either:
- Gatekeeper: Quarantine attribute blocking
- XProtect: macOS malware detection
- System Integrity Protection (SIP): Blocking certain operations
I quickly check for quarantine flags using xattr and find the quarantine flag which I remove.


Unfortunately, this approach also fails, requiring us to disable additional protections to run the sample. This is beneficial for macOS, as these safeguards effectively prevent regular users from executing the file without modifying system settings.

Conclusion
The sample depends on the Homebrew package openssl@3 (specifically the library at homebrew/opt/openssl@3/lib/libcrypto.3.dylib) to perform encryption and will fail entirely if this dependency is missing.
In summary, GonnaCry is not a live threat targeting macOS users in the wild, it is clearly a test proof-of-concept. Its behavior is consistent and observable:
It performs mass file reads across common user directories, followed by sequential encryption (read original → write encrypted → delete original). Encrypted files are renamed with the .GNNCRY extension, and the original files are zero-overwritten before deletion. Finally, it creates two key files on the Desktop: enc_files.gc (containing plaintext keys and IVs) and your_encrypted_files.txt (the ransom note).
These traits confirm its experimental nature.
IOC
SHA256
4379a2f58d3088233e7f051d882a9ce934960488aaaba640c26785645bb711c2
Yara
rule GNNCRY_Ransomware_macOS {
meta:
description = "Detects GNNCRY macOS ransomware"
author = "Tonmoy Jitu"
date = "2025-11-15"
malware_family = "GNNCRY"
strings:
// Ransom note text
$ransom_note = "Sup brother, all your files below have been encrypted, cheers!" ascii wide
// File artifacts
$extension = ".GNNCRY" ascii wide
$key_file = "enc_files.gc" ascii wide
$ransom_file = "your_encrypted_files.txt" ascii wide
// Extension list (unique to this malware)
$ext_list1 = "doc docx xls xlsx ppt pptx pst ost msg eml vsd vsdx" ascii
$ext_list2 = "vdi vmdk vmx gpg aes ARC PAQ bz2 tbk bak tar tgz" ascii
// Linux paths on macOS (suspicious)
$linux_path1 = ".local/share/Trash/" ascii
$linux_path2 = "/media/" ascii
// Portuguese debug strings
$debug_pt1 = "nao abriu" ascii
$debug_pt2 = "abriu" ascii
$debug_pt3 = "Arquivos ainda n" ascii
// Blowfish encryption
$crypto1 = "EVP_bf_cbc" ascii
$crypto2 = "EVP_CipherInit_ex" ascii
$crypto3 = "EVP_CipherUpdate" ascii
$crypto4 = "EVP_CipherFinal_ex" ascii
// Key generation charset
$keygen_charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789,.-#@$%&(){};'?!" ascii
condition:
// Must be Mach-O executable
uint32(0) == 0xfeedfacf or uint32(0) == 0xcefaedfe or
uint32(0) == 0xfeedface or uint32(0) == 0xcffaedfe
and filesize < 5MB
and (
($ransom_note and $extension and $key_file) or
(2 of ($ext_list*) and 2 of ($crypto*)) or
(1 of ($linux_path*) and 1 of ($debug_pt*) and 2 of ($crypto*)) or
($keygen_charset and 3 of ($crypto*) and $extension)
)
}