Summary #
Over the last few years, Apple has put more effort into addressing the growing problem of malware on their platform. One of their primary responses was the introduction of the XProtect suite. This post walks through how to extract a set of encrypted YARA rules stored within the XProtect Remediator binaries and correlate them with their public names.
Not including private helper rules, there are 75 YARA rules spread across 24 remediators. In addition to the YARA, there are also a number of other artifacts such as network IOCs and persistence paths.
You can find all the code, extracted YARA rules, and other analysis here!
Helper scripts to automate the extraction of YARA rules from XProtectRemediators
XProtect Primer #
XProtect is part of a suite of tools Apple uses to identify, block, and remediate malware on macOS devices. In its current state, there are three main components:
- XProtect regularly scans the disk using YARA rules
- XProtectRemediator (XPR) which is comprised of a number of scanners that use both YARA scans and some other detection methods to find and remove malware
- XProtectBehaviorService (XBS) is the latest addition to the gang that monitors system behavior relating to critical resources
XProtect’s YARA rule corpus can be found at:
/Library/Apple/System/Library/CoreServices/XProtect.bundle/Contents/Resources/XProtect.yara
Some rules have meaningful names like XProtect_MACOS_PIRRIT_GEN
which is a sig for the
Pirrit adware, while others are generic (XProtect_MACOS_2fc5997
) or internal to Apple (XProtect_snowdrift
).
The remediator part of XProtect has a series of executables stored at (/Library/Apple/System/Library/CoreServices/XProtect.app/Contents/MacOS
) but each target a particular malware family. Similarly to the YARA rules, some of these have meaningful names and others are internal to Apple:
- XProtectRemediatorAdload
- XProtectRemediatorDubRobber
- XProtectRemediatorKeySteal
- XProtectRemediatorCrapyrator (lol)
If you want some more comprehensive background reading on these macOS security features, I highly recommend digging through the amazing articles on The Eclectic Light Company.
Function Attributes #
Now before we dive into how can extract secret YARA rules from these binaires, I’m going to quickly cover function attributes and why they’re important in macOS malware world.
In GCC and Clang, function attributes allow you to provide extra information to the compiler about how it should treat that function. Typically this is to enforce particular behaviors or optimizations. In this case, we’re dealing with constructor attributes. A function with this attribute will run before main
. The code for this would look something like:
void __attribute__((constructor)) sneakySneaky() {
printf("i'll run before main even if u dont call me!\n");
}
Apple uses these constructors to decrypt global strings before main
executes. This way, anyone (ops) doing basic analysis of these binaries wouldn’t see any of the more sensitive strings.
Funnily enough, malware authors have also used this technique for ages1. It makes it easy to build a lazy-APTs packer, keeping your precious payload safe until it runs. But it really is just a trick as you can trivially access whatever modified content gets decrypted/w/e by using a debugger to break after the constructor.
Reverse Engineering the RedPine Remediator #
With all that background out the way, let’s open up one of the XPR scanners in Binary Ninja and actually see what this looks like. There’s no particular reason for choosing RedPine, they all look pretty much the same.
The scanners, like almost all macOS system binaries, are FAT executables so you can either break them up using lipo
or just let Binja handle it for you:
$ lipo -thin x86_64 -output RedPine_x86 XProtectRemediatorRedPine
Since we know that our sensitive strings are going to be decrypted in _mod_init_func
we just find the address of that section and jump to it (0x1000d4cc8
). If there isn’t a function pointer there by default, retype it (hotkey Y
) to a void*
and then jump to the resulting function!
The structure of this _mod_init_func
changes slightly across XPR binaries but for the most part it is just a simple XOR decryption routine that looks something like this:
if (obj_guard == 0 && ___cxa_guard_acquire(&obj_guard) != 0)
guard_flag = 1
_memcpy(&local_thread_var, &encrypted_string, 0xf2)
___cxa_atexit(func: func, arg: &local_thread_var, dso_handle: &__macho_header)
___cxa_guard_release(&obj_guard)
if (guard_flag != 0)
char* rax_1 = &local_thread_var
int64_t i = 0
// Loop over our encrypted string and xor with the key
do // Key = 1:18:03
*rax_1 = *rax_1 ^ (0x313a31383a303300 u>> (i.b & 0x38)).b
i = i + 8
rax_1 = &rax_1[1]
while (i != 0x790) // 8 * len(encrypted_string)
guard_flag = 0
data_1000ee9f0 = &local_thread_var
Automate #
The process to automate the extraction of all the encrypted strings is:
- Find the calls to string and memory related functions (
memcpy
,strncpy
, etc) - Grab the data referenced by function in 1.
- Grab the key from the do-while loop
- Return the encrypted buffer when
guard_release
is reached - Decrypt that buffer at your leisure
Recreating the encryption loop is pretty straight forward:
decrypted = []
for i, byte in enumerate(data):
decrypted_char = chr(byte ^ (key >> ((i * 8) & 0x38)) & 0xFF)
decrypted.append(decrypted_char)
And then we just need to iterate over the HLIL for the function and match on the interesting functions. Note that some of these functions, like __builtin_memcpy
, are actually abstractions that HLIL creates to make our lives easier. While it might seem tedious to account for all of them, it is much easier than dealing with the assembly IMO.
if isinstance(inst, binaryninja.highlevelil.HighLevelILCall):
match str(inst.dest):
case "_memcpy":
data_addr = inst.params[1]
data_len = inst.params[2]
data_buffer.append(
bv.read(data_addr.constant, data_len.constant).decode(
"utf-8"
)
)
case "__builtin_memcpy":
dest, src, n = inst.params
data += str(src.constant_data.data)
case "__builtin_strncpy":
dest, src, n = inst.params
data += str(src.constant_data.data)
# We have reached an exitpoint so we can dump the buffer
case "___cxa_atexit":
if len(data) > 0:
data_buffer.append(data)
data = ""
Again, Binja’s HLIL is extremely helpful here as it can identify the occurance of a do-while loop. We can match on it just like we did for the previous function calls. There is most certainly a nicer way of getting the key from the body but it works lol.
if isinstance(inst, binaryninja.highlevelil.HighLevelILDoWhile):
key_buf = [i for i in inst.body]
try:
key = (
str(key_buf[0])
.split("^")[1]
.split("u>>")[0]
.strip()
.replace("(", "")
)
except:
key = ""
And now we should be able to run this on all of the XProtectRemediators and get their associated encrypted strings! The output isn’t perfect, there is some occasional junk.
You can find the full script in the repo! It lets you either automatically detect the key, or supply your own in the event the key identification fails.
Notable Results #
Since there are 24 remediators, I won’t cover them all in detail. These are just some of the most interesting. The full list and mappings are here!
Helper scripts to automate the extraction of YARA rules from XProtectRemediators
RankStank #
This rule is one of the more obvious, as it includes the paths to the malicious executables found in the 3CX incident 2.
rule macos_rankstank
{
strings:
$injected_func = "_run_avcodec"
$xor_decrypt = { 80 b4 04 ?? ?? 00 00 7a }
$stringA = "%s/.main_storage"
$stringB = ".session-lock"
$stringC = "%s/UpdateAgent"
condition:
2 of them
}
In addition to the YARA rule, there was also a few hardcoded paths for the malicious executables:
/Applications/3CX Desktop App.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Libraries/libffmpeg.dylib
~/Library/Application Support/3CX Desktop App/.main_storage
~/Library/Application Support/3CX Desktop App/UpdateAgent
ColdSnap #
Keeping with the DPRK trend, ColdSnap is likely looking for the macOS version of the SimpleTea3 malware. This was also associated with the 3CX breach and shares traits with both the Linux and Windows variants.
rule macos_coldsnap_c_symbols {
strings:
$ = "_m_ComInfo"
$ = "_m_Config"
$ = "_m_ConnectReason"
$ = "_m_CurrentStatus"
$ = "_m_MsgStackCS"
$ = "_m_MsgStackHead"
$ = "_m_MsgStackIterBegin"
$ = "_m_MsgStackIterEnd"
$ = "_m_ProxyIndex"
$ = "_m_ProxyUrl"
$ = "_m_Session"
$ = "__isPlatformOrVariantPlatformVersionAtLeast"
$ = "__Z10msg_systemP11_MSG_STRUCT"
$ = "__Z11get_os_infoP15_COMINFO_STRUCT"
$ = "__Z12custom_sleepj"
$ = "__Z12get_com_infoP15_COMINFO_STRUCT"
$ = "__Z12msg_keep_conP11_MSG_STRUCT"
$ = "__Z12msg_set_pathP11_MSG_STRUCT"
$ = "__Z13get_file_timePcS_"
$ = "__Z13msg_hibernateP11_MSG_STRUCTa"
$ = "__Z14msg_secure_delP11_MSG_STRUCT"
$ = "__Z15get_internal_ipP15_COMINFO_STRUCT"
$ = "__Z15msg_read_configP11_MSG_STRUCT"
$ = "__Z15reset_msg_stackv"
$ = "__Z16connect_to_proxyP11_MSG_STRUCT"
$ = "__Z16msg_write_configP11_MSG_STRUCT"
$ = "__Z22generate_random_stringm"
$ = "__Z6msg_upP11_MSG_STRUCT"
$ = "__Z7msg_cmdP11_MSG_STRUCT"
$ = "__Z7msg_dirP11_MSG_STRUCT"
$ = "__Z7msg_runP11_MSG_STRUCT"
$ = "__Z7pop_msgP11_MSG_STRUCT"
$ = "__Z8msg_downP11_MSG_STRUCT"
$ = "__Z8msg_exitP11_MSG_STRUCT"
$ = "__Z8msg_testP11_MSG_STRUCT"
$ = "__Z8push_msgP11_MSG_STRUCT"
$ = "__Z9msg_sleepP11_MSG_STRUCTa"
$ = "__ZN11TransCenter13m_LastMsgTimeE"
$ = "__ZN11TransCenter18recv_at_first_connEP11_MSG_STRUCTP20_PROXY_NOTIFY_STRUCT"
$ = "__ZN11TransCenter8recv_msgEP11_MSG_STRUCTja"
$ = "__ZN11TransCenter8send_msgEjjPhjj"
$ = "__ZN8A5Stream12GetKeyStreamEv"
$ = "__ZN8A5Stream13CalcThresholdEv"
$ = "__ZN9WebStream11set_payloadEPhj"
$ = "__ZN9WebStream4postEPhPj"
$ = "__ZN9WebStream9make_bodyEPhPm"
condition:
_macho and 4 of them
}
rule macos_coldsnap_c {
strings:
$user_agent = "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.122 Safari/537.36"
$configuration_path = "%s/Library/WebKit/xpdf.conf"
$system_version_check = "/System/Library/CoreServices/SystemVersion.plist"
$fake_content_disposition = {
2e 70 6e 67 22 0d 0a 43 // .png" C
6f 6e 74 65 6e 74 2d 54 // ontent-T
79 70 65 3a 20 61 70 70 // ype: app
6c 69 63 61 74 69 6f 6e // lication
2f 6f 63 74 65 74 2d 73 // /octet-s
74 72 65 61 6d 0d 0a 0d // tream
0a
}
$encryption_routine = "8A5Stream"
condition:
_macho and 2 of them
}
rule macos_coldsnap_c_xor_config {
strings:
$xor_instructions = { 80 34 ?? 5e 4? ff c1 4? 81 f9 2e 05 00 00 75 ?? }
condition:
all of them
}
BlueTop #
BlueTop appears to be the Trojan-Proxy campaign that was covered by Kaspersky in late 20234
rule macos_bluetop {
strings:
$ = {
47455420257320485454502F312E310D0A // GET %s HTTP/1.1
486F73743A2025733A25750D0A // Host: %s:%u
436F6E6E656374696F6E3A20557067726164650D0A // Connection: Upgrade
557067726164653A20776562736F636B65740D0A // Upgrade: websocket
5365632D576562536F636B65742D56657273696F6E3A20[2]0D0A // Sec-WebSocket-Version:
5365632D576562536F636B65742D4B65793A2025730D0A0D0A // Sec-WebSocket-Key: %s
}
$ = { 66 61 69 6C 72 65 73 74 61 72 74 00 66 6C 6F 63 6B 00 6C 6F 67 2E 74 78 74 00 }
condition:
_macho and all of them and filesize < 1MB
}
rule macos_bluego {
strings:
$ = "_GoUrbanDll_StarVPNStartProxyClient"
$ = "_GoUrbanDll_StarVPNClientSSHError"
$ = "There is no information on currently connected SSH server"
condition:
_macho and all of them and filesize < 20MB
}
RedPine #
Now this one I’m not totally confident about, but it’s givinggg TriangleDB from Operation Triangulation5. I wonder where Red Pines grow? π¦ πΊπΈ
rule macos_redpine_implant {
strings:
$classA = "CRConfig"
$classD = "CRPwrInfo"
$classE = "CRGetFile"
$classF = "CRXDump"
condition:
all of them
}
Toodles! #
As I said before, there are lots more YARA rules located in the repo. I highly recommend you take a look if you are someone who works with macOS as the type of protections that exist is pretty eye opening.
If you have any feedback or questions, please feel free to shoot me a message on Keybase ( @birchboy), Discord (@aldens), or Twitter ( @birchb0y)!
ps: pls don’t be mad at me SEAR π₯° y’all are doing great teehee
Appendix #
Extraction Support #
XProtectRemediator | Supported | Explanation |
---|---|---|
Adload | β | |
BadGacha | π‘ | no yara rule! |
BlueTop | β | |
CardboardCutout | β | |
ColdSnap | β | |
DubRobber | β | |
Eicar | β | |
FloppyFlipper | π‘ | no yara, but regex found |
Genieo | π‘ | no yara, but file paths |
GreenAcre | β | |
KeySteal | β | |
MRTv3 | β | no yara, but run strings on this |
Pirrit | β | |
RankStank | β | |
RedPine | β | |
RoachFlight | β | |
SheepSwap | β | |
SnowBeagle | β | |
SnowDrift | β | |
ToyDrop | β | |
Trovi | π‘ | no yara, some filepaths, junk |
WaterNet | β |