Skip Navigation
Get a Demo
 
 
 
 
 
 
 
 
 
 

T1620

Reflective Code Loading

Adversaries commonly abuse deprecated (since Mac OS X 10.5) reflective loading APIs on macOS to evade detection.

Pairs with this song
 

Analysis Icon

Analysis

 

Note: Reflective Code Loading is a broad, cross-platform technique, and we’ve chosen to focus our analysis specifically on this technique in the context of macOS.

Why do adversaries abuse reflective code loading?

The macOS file system is carefully scrutinized by endpoint detection and response (EDR) tools, commercial antivirus (AV) products, and Apple’s baked-in XProtect AV. As a result, when an adversary drops a known malicious binary on disk, the binary is very rapidly detected and often blocked. Even net new or custom payloads run the risk of being quickly signatured and prevented from successfully executing from then on.

Keenly aware of the defender’s upper hand here, adversaries leverage the concept of reflective code loading to execute their payloads directly within the memory space of a host process, specifically Mach-O files, which are commonly:

  • executables (MH_EXECUTE): a paged executable file
  • loadable bundles (MH_BUNDLE): a dynamically bound bundle file
  • dynamic library (MH_DYLIB): a dynamically bound shared library file

Note: If you’re interested in learning more about Mach-O files, Aidan Steele’s reference provides a comprehensive and easy-to-follow overview of the file format.

If the host process has Hardened Runtime enabled, which is the default in Xcode and disallows reflection, then one of two entitlements must be signed to the host binary to allow the execution of unsigned memory:

  • com.apple.security.cs.allow-unsigned-executable-memory
  • com.apple.security.cs.disable-executable-page-protection

 

In most cases, an adversary can avoid Hardened Runtime altogether by compiling their own host process on the target system. Since it’s not being distributed widely beyond that system, it won’t require notarization, and the adversary can avoid enabling Hardened Runtime and signing the above entitlements. It’s worth pointing out that these entitlements are widely used in legitimate applications (e.g., Spotify, GitHub Desktop, Hopper Disassembler, and more).

The main benefit of reflective loading is that adversaries are never writing their payload to disk (or so they believe) and thus evading EDR, AV, and XProtect’s defenses. Additionally, reflective loading effectively enables a binary to “bypass” Gatekeeper’s first launch checks, as noted by Csaba Fitzl. The advantages of correctly executing this technique are clear, but modern macOS systems present formidable roadblocks to an adversary in the form of Apple’s code-level mitigations and implementation complexity, which we’re about to explore.

How do adversaries abuse reflective code loading?

On macOS, attackers traditionally reflectively load their code using the following Dynamic Loader (Dyld) APIs: NSLinkModule or the higher-level NSCreateObjectFileImageFromMemory, which expects the mapped image to be a “loadable bundle.” This can be done at compile time with the -bundle flag in gcc or by setting the executable Mach-O’s filetype to MH_BUNDLE from MH_EXECUTE.

It used to be the case that by preparing their payload in-memory and then calling these deprecated APIs, adversaries were able to gain code execution without writing anything to disk. This has been a problem theoretically for Apple and observed in the wild since at least 2017, with the introduction of the signed Snake trojan and in the hands of red teams likely prior to 2009. Apple has been proactive in noticing that these API functions are rarely used for legitimate purposes. As such, they’ve implemented powerful code-level mitigations to the Dyld APIs. When NSLinkModule is called, the module’s image is written to disk at a randomized path following the format:

/private/var/folders/.../NSCreateObjectFileImageFromMemory-XXXXXXXX

 

Patrick Wardle and @roguesys have reported on this technique extensively. Here’s a look at the module source code:

NSLinkModule

NSModule APIs::NSLinkModule(NSObjectFileImage ofi, const char* moduleName, uint32_t options)
{
#if TARGET_OS_OSX && !TARGET_OS_EXCLAVEKIT
    if ( config.log.apis )
        log("NSLinkModule(%p, %s)\n", ofi, moduleName);
    // if this is memory based image, write to temp file, then use file based loading
    int openMode = 0;
    if ( ofi->memSource != nullptr ) {
        // make temp file with content of memory buffer
        ofi->path = nullptr;
        char        tempFileName[PATH_MAX];
        const char* tmpDir = this->libSystemHelpers->getenv("TMPDIR");
        if ( (tmpDir != nullptr) && (strlen(tmpDir) > 2) ) {
            strlcpy(tempFileName, tmpDir, PATH_MAX);
            if ( tmpDir[strlen(tmpDir) - 1] != '/' )
                strlcat(tempFileName, "/", PATH_MAX);
        }
        else
            strlcpy(tempFileName, "/tmp/", PATH_MAX);
        strlcat(tempFileName, "NSCreateObjectFileImageFromMemory-XXXXXXXX", PATH_MAX);
        int fd = this->libSystemHelpers->mkstemp(tempFileName);
  // ...
}

NSCreateObjectFileImageFromMemory

NSModule APIs::NSLinkModule(NSObjectFileImage ofi, const char* moduleName, uint32_t options)
{
#if TARGET_OS_OSX && !TARGET_OS_EXCLAVEKIT
    if ( config.log.apis )
        log("NSLinkModule(%p, %s)\n", ofi, moduleName);
    // if this is memory based image, write to temp file, then use file based loading
    int openMode = 0;
    if ( ofi->memSource != nullptr ) {
        // make temp file with content of memory buffer
        ofi->path = nullptr;
        char        tempFileName[PATH_MAX];
        const char* tmpDir = this->libSystemHelpers->getenv("TMPDIR");
        if ( (tmpDir != nullptr) && (strlen(tmpDir) > 2) ) {
            strlcpy(tempFileName, tmpDir, PATH_MAX);
            if ( tmpDir[strlen(tmpDir) - 1] != '/' )
                strlcat(tempFileName, "/", PATH_MAX);
        }
        else
            strlcpy(tempFileName, "/tmp/", PATH_MAX);
        strlcat(tempFileName, "NSCreateObjectFileImageFromMemory-XXXXXXXX", PATH_MAX);
        int fd = this->libSystemHelpers->mkstemp(tempFileName);
  // ...
}

Below is what this looks like in Mac Monitor using our POSIX AtomicTestHarness. We explore this more in the Testing section below.

Mac Monitor screenshot depicting reflective code loading visibility

Note that this is just a test, and we’d expect the adversary to fetch the code remotely (i.e., not from disk). However, by the test exercising the following we can reliably emulate the following behavior:

NSCreateObjectFileImageFromMemory → NSLinkModule → NSLookupSymbolInModule → NSAddressOfSymbol (done) → NSUnLinkModule

 

In the wild, this behavior will appear very similar in code. Patrick Wardle’s analysis of OSX.AppleJeus.C provides us direct visibility into what this looks like, as written by Lazarus Group:

int _memory_exec2(int arg0, int arg1, int arg2) {

// ...

rax = NSCreateObjectFileImageFromMemory(rdi, rsi, &var_58);

rax = NSLinkModule(var_58, "core", 0x3);

// ...

}

In-the-wild examples of reflective code loading

The following is a rough timeline of identified occurrences of adversaries using reflective loading. In each of these cases, it’s been in the form of NSCreateObjectFileImageFromMemory (and by extension NSLinkModule).

IdentifiedVariantClassificationAttribution
2017SnakeTrojan?
2019OSX.AppleJeus.C / macloaderRATLazarus
2020OSX.EvilQuest / ThiefQuestStealer / ransomware?
2020Double AgentPuPLegitimate company
2021NukeSped / AppleJeusRATLazarus
2022CovidRATRed team
2023SUGARLOADERStagerLazarus

 

Advanced tradecraft

To our knowledge, adversaries haven’t managed to circumvent these APIs for in-the-wild reflective code loading. However, red teamers have. For example, Adam Chester’s Dyld-DeNeuralyzer project aims to circumvent Apple’s code path mitigations by reminding us that we (largely) own our address space by either:

(a) utilizing Dyld but patching and hooking the following system calls: mmap (mapping a file into memory), pread (read bytes from a file descriptor), and fcntl (adding signatures to a file with F_ADDFILESIGS_RETURN and checking for Library Validation with F_CHECK_LV)

or

(b) implementing a custom in-memory loader

Chester’s work successfully accomplishes these goals, and we can verify this by again watching for the module writeback to occur. If the writeback does not occur, then we’ve largely re-gained reflective loading capabilities. Additionally, two of these variations are distinct from Dyld’s NSCreateObjectFileImageFromMemory/NSLinkModule API functions.

By running another Mac Monitor trace against this implementation, we can clearly see that the module’s image is not being written to disk, blinding file-based detection heuristics.

Mac Monitor screenshot depicting Dyld-DeNeuralyzer avoiding writing to disk

 

Basic

Modern installs of macOS will largely mitigate the opportunity for adversaries to reflectively load code on the platform. By building mitigations into new versions of macOS at a key cinch point, Apple has given defenders ample time to profile reflectively loaded code statically on disk.

Advanced

However, as we’ve cited above: Research published in January 2023 demonstrated it’s possible to successfully restore reflective loading. Beyond that single implementation, the potential for adversaries to develop their own dynamic loader always exists. Therefore, defenders cannot rely on file-based monitoring solutions alone and should opt-in to EDR-based monitoring solutions to identify suspicious process behaviors.

Visibility icon

Visibility

Note: The visibility sections in this report are mapped to MITRE ATT&CK data sources and components.

Application log monitoring

In the most basic case where the adversary leverages the NSCreateObjectFileImageFromMemory/NSLinkModule API functions, we can easily gain visibility into this behavior in two key ways:

  1. AMFI through the kernel will log this result to the Apple Unified Log (AUL). You can in-real time filter the AUL for events like this using:
log stream --predicate 'eventMessage CONTAINS "NSCreateObjectFileImageFromMemory-"' --style compact

Screenshot of NSCreateObjectFileImageFromMemory in Apple Unified Log

The file will be created on disk if the memory space was not successfully tampered with and evaded. The path will resemble:

/private/var/folders/.../NSCreateObjectFileImageFromMemory-XXXXXXXX

Process and file monitoring

In the worst cases for defenders: hooking and patching system calls or implementations of custom loaders can lead to some very noisy endpoint security (ES) telemetry:

  1. When the adversary gets the base address of Dyld using TASK_DYLD_INFO_COUNT, ES will emit an ES_EVENT_TYPE_NOTIFY_PROC_CHECK for the type of PIDINFO and flavor of 13.
  2. If in the case that the adversary leverages libffi-trampolines.dylib—as in the case of Dyld-DeNeuralyzer—then ES will also emit an ES_EVENT_TYPE_NOTIFY_OPEN for that.

Collection Icon

Collection

Note: The collection sections of this report showcase specific log sources from Windows events, Sysmon, and elsewhere that you can use to collect relevant security information.

Above we’ve mentioned: file system-based visibility, AUL, and ES. Each of these areas could be monitored in a host of ways from both a developer’s perspective and an user’s.

Endpoint security

Mac Monitor provides an excellent platform for us to test visibility here on an adhoc basis (as we’ve shown). However, any ES monitoring solution collecting: ES_EVENT_TYPE_NOTIFY_EXEC, ES_EVENT_TYPE_NOTIFY_PROC_CHECK, and ES_EVENT_TYPE_NOTIFY_OPEN will suffice for this testing.

Apple Unified Log

Console.app should collect relevant data but you can also use the command line: /usr/bin/logger. We suggest using the query:

log stream --predicate 'eventMessage CONTAINS "NSCreateObjectFileImageFromMemory-"' --style compact

However, leveraging ES again will likely yield the best results. Any file system monitoring tool could be used (e.g. Mac Monitor), FileMonitor, or  eslogger.

For a developer’s perspective on leveraging Endpoint Security as a data source, we’ll point the reader to the Mac Monitor wiki, which explores it (and adjacent technologies) in-depth.

Icon-threat detection

Detection opportunities

Mac reflected module writeback file

Adversaries leverage deprecated Dyld APIs to reflectively load malicious payloads from memory. This detector will be high fidelity in detecting normal usage of NSCreateObjectFileImageFromMemory or NSLinkModule using file system telemetry. However, under a more advanced engagement adversaries are likely aware this mitigation exists.

file_path_matching_regex ('\/private\/var\/folders\/[0-9a-zA-Z]+\/[0-9a-zA-Z_]+\/T\/NSCreateObjectFileImageFromMemory-[0-9a-zA-Z]+')

Mac reflected module writeback log 

As in the above example, this analytic will be dependent on the module writeback occuring. However, instead of monitoring the file system (potentially more noisy), you can leverage the Apple Unified Log to easily detect this activity.

apple_unified_log_predicate('eventMessage CONTAINS "NSCreateObjectFileImageFromMemory-"')

Or you can use /usr/bin/log at the command line to filter this activity.

Mac patched Dyld reflected module

This is the most advanced case we’ll be considering here. Again, this procedure has not been seen in the wild to our knowledge. Here we’re directly calling out the incredible work of the Dyld-Deneuralyzer project’s “dyld patch” variation. File open events are incredibly noisy. However, at the ES level developers can leverage the extensive path muting APIs offered by Apple. For example, a client could be created specifically to monitor these noisy file open events and apply a muting inversion mask to it via ES_MUTE_INVERSION_TYPE_TARGET_PATH.

file_open_path_matching_exactly('/usr/lib/libffi-trampolines.dylib')

ES_EVENT_TYPE_NOTIFY_CREATE event details

ES_EVENT_TYPE_NOTIFY_MODIFY event details

Testing Icon

Testing

Start testing your defenses against Reflective Code Loading using Atomic Red Team—an open source testing framework of small, highly portable detection tests mapped to MITRE ATT&CK.

Getting started

View atomic tests for T1620 Reflective Code Loading. However, since this analysis of Reflective Code Loading is focused on macOS, we recommend checking out the new POSIX AtomicTestHarness suite for T1620. To execute the test, you’ll just need to have Python 3.10+ along with Xcode Command Line Tools (for gcc) installed.

pip install posixath
python -m posixath macos -t T1620

As a quick overview our test for reflective code loading on macOS consists of:

  1. Compiling a loadable bundle (from a C source code file). The file must have a function named run_me for our example to call into. By default we provide a simple example. Optionally, you can provide your own pre-compiled bundle–again with a function called run_me.
void run_me() {
    printf("👋 Hello curious user!\n");
}

2. Compiling our reflector/stager. This is another C source file containing the calls to NSCreateObjectFileImageFromMemory/NSLinkModule. It’s this binary that will call your run_me function.

3. Execute the reflector/stager and capture the output according to our standardized model

4. Hunt through the AUL using the guidance mentioned above to record the module being written to disk.

Review and repeat

Now that you have executed one or several common tests and checked for the expected results, it’s useful to answer some immediate questions:

  • Were any of your actions detected?
  • Were any of your actions blocked or prevented?
  • Were your actions visible in logs or other defensive telemetry?

Repeat this process, performing additional tests related to this technique. You can also create and contribute tests of your own.

 
 
Back to Top