Analyzing DPRK's SpectralBlur
The first malware of 2024 is (already) here!
by: Patrick Wardle / January 4, 2024
The Objective-See Foundation is supported by the "Friends of Objective-See" that include:
📝 👾 Want to play along?
As ‘Sharing is Caring’ I’ve uploaded a sample of the malware SpectralBlur.zip to our public macOS malware collection. The password is: infect3d
...please though, don't infect yourself! 🙈
Not three days into 2024 Greg Lesnewich tweeted the following:
#100DaysofYARA day 03 - talking SpectralBlur, a MacOS (and other OS 🤫) backdoor linked to TA444/Bluenoroff, that I suspect is a cousin of the KandyKorn family our pals at Elastic found! https://t.co/P2TGw98UR6 pic.twitter.com/Y8U3hsjNiF
— Greg Lesnewich (@greglesnewich) January 3, 2024
In both his twitter (err, X) thread and in a subsequent posting he provided a comprehensive background and triage of the malware dubbed SpectralBlur
. In terms of its capabilities he noted:
SpectralBlur is a moderately capable backdoor, that can upload/download files, run a shell, update its configuration, delete files, hibernate or sleep, based on commands issued from the C2. -Greg
He also pointed out similarities to/overlaps with the DPRK malware known as KandyKorn
(that we covered in our “Mac Malware of 2024” report), while also pointing out there was differences, leading him to conclude:
We can see some similarities ... to the KandyKorn. But these feel like families developed by different folks with the same sort of requirements. -Greg
Greg's writeup, though focusing more on the discovery and triage of the SpectralBlur, is excellent a read, and provides essential background and context for our more indepth analysis.
Have a read:
Greg ended his write up, nothing that he hadn’t had time yet to fully analyze the sample, but “if anyone in the community is keen on it, go for it!!”. As I’m rather obsessed with (new) Mac malware I decided to dig in more.
The sample of SpectralBlur mentioned in his tweets and triage has a SHA-1 hash of 06c8c84fb0a85bdf3520608b0a5c910b77e3b8c1
. Popping over to VirusTotal to grab the sample, we can see that currently it is not flagged as malicious by any of the AV engines:
Triggering a rescan of the malware on VirusTotal shows it is (now) detected by at least one AV engine (ESET).
Moreover its worth noting that the previous scan of the malware (refereneced in the screenshot above) was performed in August 2023. Thus, though at that time no AV engine flagged the item as malicous, between now and then, detections could have been added.
Looking at telemetric data we can see its name is either .macshare
or mac.jpg
, and at least on one macOS system it was found in /Users/Shared/.macshare
.
Prefixing the name with a '.' will hide the file from Finder (unless the viewing of hidden files has been enabled).
This SpectralBlur sample was initially submitted to VirusTotal on 2023-08-16
from Colombia (CO). Interestingly in VirusTotal’s telemetric data, we can also see that at least one of Objective-See’s tools (which, integrate with VirusTotal, for example to allow users to submit unrecognized files) encountered the malware in the wild too …how cool!
Using WhatsYourSign
we can see the binary is not signed:
You can also extract codesigning information (or lack thereof) from the terminal via macOS’s codesign
utility:
% codesign -dvv SpectralBlur/.macshare SpectralBlur/.macshare: code object is not signed at all
Via the terminal we can also see its a 64bit Intel (x86_64
) binary:
% file SpectralBlur/.macshare SpectralBlur/.macshare: Mach-O 64-bit executable x86_64
Next let’s pull out any embedded strings, as this can give a good idea of the malware’s capabilities and also guide continued analysis:
% strings SpectralBlur/.macshare There is NO WARRANTY, to the extent permitted by law. %s.d /dev/null SHELL /bin/sh ...
Other than some strings related to the shell, not a lot! Maybe others are obfuscated? (We also didn’t show function names or API imports in the strings
output, as these show up better via nm
).
Using nm
we can extract symbols which will include the Malware’s function names, as well as APIs that the malware calls into (“imports”). Let’s start with just function names, which will be found the __TEXT
segment/section: __text
. We can use nm
’s -s
to limit its output to just a specified segment/section:
% nm -s __TEXT __text SpectralBlur/.macshare 0000000100001540 T _hangout 00000001000034f0 T _init 0000000100001570 T _init_fcontext 0000000100001870 T _load_config 0000000100003650 T _main 0000000100002a10 T _mainprocess 0000000100003370 T _mainthread 00000001000031c0 T _openchannel 00000001000029a0 T _proc_die 0000000100001b10 T _proc_dir 0000000100002420 T _proc_download 0000000100002290 T _proc_download_content 00000001000027e0 T _proc_getcfg 00000001000028c0 T _proc_hibernate 00000001000019f0 T _proc_none 00000001000029d0 T _proc_restart 00000001000025a0 T _proc_rmfile 0000000100002860 T _proc_setcfg 0000000100001a90 T _proc_shell 0000000100002930 T _proc_sleep 0000000100001a20 T _proc_stop 00000001000026b0 T _proc_testconn 0000000100002160 T _proc_upload 0000000100002040 T _proc_upload_content 00000001000015f0 T _read_packet 0000000100001930 T _save_config 0000000100001500 T _sigchild 00000001000011f0 T _socket_close 0000000100000d10 T _socket_connect 0000000100001140 T _socket_recv 00000001000010c0 T _socket_send 0000000100000be0 T _wait_read 0000000100001730 T _write_packet 00000001000017e0 T _write_packet_value 0000000100001270 T _xcrypt
Looks like functions dealing with a config (e.g., load_config
), network communications (e.g., socket_recv
, socket_send
), and encryption (xcrypt
). But also then, standard backdoor capabilities implemented (as noted by Greg), in function prefixed with proc
.
And what about the APIs the malware imports to call into? Again we can use nm
, this time the -u
flag:
% nm -m SpectralBlur/.macshare _connect _dup2 _execve ... _fork _fread _fwrite ... _gethostbyname _getlogin _getpwuid _getsockopt _grantpt ... _ioctl _kill ... _pthread_create _rand _recv _send _socket _unlink _waitpid _write ...
From these imports we can surmise that the malware performs file I/O (fread
, fwrite
, unlink
), network I/O (socket
, recv
, send
), and spawning/managing processes (execve
, fork
, kill
).
We’ll see these APIs are invoked by the malware often in response to commands. For example, the malware’s proc_rmfile
function invokes the unlink
API to remove a file:
1int proc_rmfile(int arg0, int arg1) {
2 var_10 = arg1;
3 var_18 = var_10 + 0x10;
4 ...
5 unlink(var_18);
6 ...
Based on this triage (and Greg’s report), we’ve developed what is likely a reasonable understanding of the malware. Still, its always good to validate triages with continued static, but also dynamical analysis. So …onward!
At the start of the malware disassembly it calls into a function named init
. Here, it builds a path to its config, and then opens it. The path is built by appending .d
to the malware’s binary full path:
init(...){
_sprintf_chk(config, 0x0, 0x41a, "%s.d", malwaresPath);
...
loadConfig(...)
}
We can confirm this in a debugger, where at a call to fopen
(in the load_config
function) the malware will attempt to open the file macshare.d
, in the directory where the malware is currently running (e.g. /Users/user/Downloads/
).
% lldb /Users/user/Downloads/macshare (lldb) * thread #1, queue = 'com.apple.main-thread' macshare`load_config: -> 0x100001890 <+32>: callq 0x100003d8c ; symbol stub for: fopen Target 0: (macshare) stopped. (lldb) x/s $rdi 0x100008820: "/Users/user/Downloads/macshare.d"
We can also see this in a File Monitor:
# ./FileMonitor.app/Contents/MacOS/FileMonitor -pretty -filter macshare { "event" : "ES_EVENT_TYPE_NOTIFY_OPEN", "file" : { "destination" : "/Users/user/Downloads/macshare.d", "process" : { "pid" : 6818, "name" : "macshare", "path" : "/Users/user/Downloads/macshare" } } }
By looking at its cross-references (xrefs), we can see the xcrypt
function is invoked to encrypt/decrypt the malware’s config and network traffic:
…the xcrypt
function according to ChatGPT appears to be a custom stream cipher. While static analysis shows that the key may be stored at the start of this config (address 0x100008c3a
), and set to random 64bit value:
*qword_100008c3a = sign_extend_64(rand()) + time(0x0) + sign_extend_64(rand() * rand());
Back to the config file, unfortunately, I (currently) don’t have access an example config. Thus some of our continued analysis is based solely on static analysis.
Once the init
function returns (which loaded the config), that malware performs a myriad of actions that appear to complicate dynamic analysis and perhaps detection. This including fork
ing itself, but also setting up a pseduo-terminal via posix_openpt
(as noted by Phil Stokes):
Uses grantpt to set up a pseudo-terminal. Not seen that before. Interesting!
— Phil Stokes ⫍🐠⫎ (@philofishal) January 4, 2024
…this is followed by more forks, execs, and more. Again, if I had to guess, this simply to complicate analysis (and/or perhaps, making it a detached/“isolated” process complicated detections)? We’ll also see that the psuedo-terminal is used to execute shell commands from the attacker’s remote C&C server.
Regardless we can skip over this all, and simply continue execution (or static analysis) where a new thread (named _mainthread
) is spawned. After invoking functions such as openchannel
and socket_connect
to likely connect to its C&C server (whose address likely would be found in the malware’s config: macshare.d
), it invokes a function named mainprocess
.
The mainprocess
function (eventually) invokes the read_packet
function which appears to return the index of a command. The code in mainprocess
function then iterates over an array named _procs
in order to find the handler for the specified command (that I’ve named commandHandler
in the below disassembly). The command handler is then directly invoked:
1int mainprocess(int arg0, int arg1) {
2
3 var_558 = read_packet(...);
4 if (var_558 != 0x0) goto loc_100002dfc;
5
6loc_100002dfc:
7 var_560 = *(var_558 + 0x8);
8 commandHandler = 0x0;
9 addrOfProcs = _procs;
10 do {
11 var_5C1 = 0x0;
12 if (*addrOfProcs != 0x0) {
13 var_5C1 = (var_568 != 0x0 ? 0x1 : 0x0) ^ 0xff;
14 }
15 if ((var_5C1 & 0x1) == 0x0) {
16 break;
17 }
18 if (*addrOfProcs == var_560) {
19 commandHandler = *(addrOfProcs + 0x4);
20 }
21 addrOfProcs = addrOfProcs + 0xc;
22 } while (true);
23
24
25 var_538 = (commandHandler)(var_530, var_558);
26
27}
After creating a custom structure (procStruct
) for this array, we can see each command number and its handler:
procs:
0x0000000100008000 struct procStruct {
0x1,
_proc_none
}
0x000000010000800c struct procStruct {
0x2,
_proc_shell
}
0x0000000100008018 struct procStruct {
0x3,
_proc_dir
}
0x0000000100008024 struct procStruct {
0x4,
_proc_upload
}
0x0000000100008030 struct procStruct {
0x5,
_proc_upload_content
}
0x000000010000803c struct procStruct {
0x6,
_proc_download
}
0x0000000100008048 struct procStruct {
0x7,
_proc_rmfile
}
0x0000000100008054 struct procStruct {
0x8,
_proc_testconn
}
0x0000000100008060 struct procStruct {
0x9,
_proc_getcfg
}
0x000000010000806c struct procStruct {
0xa,
_proc_setcfg
}
0x0000000100008078 struct procStruct {
0xb,
_proc_hibernate
}
0x0000000100008084 struct procStruct {
0xc,
_proc_sleep
}
0x0000000100008090 struct procStruct {
0xd,
_proc_die
}
0x000000010000809c struct procStruct {
0xe,
_proc_stop
}
0x00000001000080a8 struct procStruct {
0xf,
_proc_restart
}
Recall we saw the names of each command handler (_proc_*
) in the output of nm
. And, though we can guess the likely capability of eahc command from its name, let’s look a few to confirm.
The proc_rmfile
will remove a file by invoking the unlink
API. However, we can also see that it first opens the file (fopen
) and overwrites its contents with zero:
1int proc_rmfile(int arg0, int arg1) {
2 var_4 = arg0;
3 var_10 = arg1;
4 var_18 = var_10 + 0x10;
5 file = fopen(var_18, "rb+");
6 if (file != 0x0) {
7 fseek(file, 0x0, 0x2);
8 var_28 = ftell(file);
9 fseek(file, 0x0, 0x0);
10 var_30 = 0x5000;
11 if (var_28 < var_30) {
12 var_30 = var_28;
13 }
14 var_38 = malloc(var_30);
15 _memset_chk(var_38, 0x0, var_30, 0xffffffffffffffff);
16 fwrite(var_38, 0x1, var_30, file);
17 free(var_38);
18 fclose(file);
19 }
20 rdx = unlink(var_18);
21 rax = 0x0;
22 if (rdx == 0x0) {
23 rax = 0x1;
24 }
25 return _write_packet_value(var_4, *var_10, rax);
26}
…each command will also report a result by invoking the malware write_packet_value
API.
The proc_restart
will terminate the child process:
1int main(...)
2
3 call fork
4 mov dword [childPID], eax
5
6int proc_restart(int arg0, int arg1)
7
8 kill(*childPID, 0x9);
9 return write_packet_value(arg0, *arg1, 0x0);
Finally, let’s look at the proc_shell
, which executed a command by write
‘ing to the pseudo-terminal that was opened (via posix_openpt
) previously:
1int main(...)
2
3 call posix_openpt
4 mov dword [pt], eax
5
6int proc_shell(...) {
7 var_8 = arg0;
8 var_10 = arg1;
9 if (write(*pt, var_10 + 0x10, strlen(var_10 + 0x10)) <= 0x0) {
10 var_4 = _write_packet_value(var_8, *var_10, 0x0);
11 }
The other commands execute actions consistent with their respective names.
Today we dug into SpectralBlur, a DPRK backdoor. Building upon Greg’s research we further detailed its capabilities.
Interested in learning more Mac Malware Analysis Techniques?
You're in luck, as I've written a book on this topic! It's 100% free online while all royalties from sale of the printed version donated to the Objective-See Foundation.
|
|
Or, come attend our macOS security conference, "Objective by the Sea" v6.0 in sunny Spain! ...where I'm teaching a class on Mac Malware Detection & Analysis
|
You can support them via my Patreon page!