Skip to main content
The Secrets of XProtectRemediator

The Secrets of XProtectRemediator

Binary Ninja Malware RE CTI YARA macOS


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:

  1. XProtect regularly scans the disk using YARA rules
  2. 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
  3. 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:


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/ 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!

Fig 1. Screenshot of Binary Ninja header section
Fig 2. Dramatic rendition of changing type to a pointer and binja discovering a 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)

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


The process to automate the extraction of all the encrypted strings is:

  1. Find the calls to string and memory related functions (memcpy, strncpy, etc)
  2. Grab the data referenced by function in 1.
  3. Grab the key from the do-while loop
  4. Return the encrypted buffer when guard_release is reached
  5. 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)

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_len.constant).decode(
        case "__builtin_memcpy":
            dest, src, n = inst.params
            data += str(
        case "__builtin_strncpy":
            dest, src, n = inst.params
            data += str(
        # We have reached an exitpoint so we can dump the buffer
        case "___cxa_atexit":
            if len(data) > 0:
                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]
        key = (
            .replace("(", "")
        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



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
        $injected_func = "_run_avcodec"
        $xor_decrypt = { 80 b4 04 ?? ?? 00 00 7a }
        $stringA = "%s/.main_storage"
        $stringB = ".session-lock"
        $stringC = "%s/UpdateAgent"
        2 of them

In addition to the YARA rule, there was also a few hardcoded paths for the malicious executables:

/Applications/3CX Desktop Framework.framework/Versions/A/Libraries/libffmpeg.dylib
~/Library/Application Support/3CX Desktop App/.main_storage
~/Library/Application Support/3CX Desktop App/UpdateAgent


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 {
        $ = "_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"
        _macho and 4 of them
rule macos_coldsnap_c {
       $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
       $encryption_routine = "8A5Stream"
        _macho and 2 of them
rule macos_coldsnap_c_xor_config {
        $xor_instructions = { 80 34 ?? 5e 4? ff c1 4? 81 f9 2e 05 00 00 75 ??  }
        all of them


BlueTop appears to be the Trojan-Proxy campaign that was covered by Kaspersky in late 20234

rule macos_bluetop {
        $ = {
            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 }
        _macho and all of them and filesize < 1MB
rule macos_bluego {
        $ = "_GoUrbanDll_StarVPNStartProxyClient"
        $ = "_GoUrbanDll_StarVPNClientSSHError"
        $ = "There is no information on currently connected SSH server"
        _macho and all of them and filesize < 20MB


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 {
        $classA = "CRConfig"
        $classD = "CRPwrInfo"
        $classE = "CRGetFile"
        $classF = "CRXDump"
        all of them


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


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 βœ…