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 songThreat sounds
Adversaries leverage reflective code loading to avoid writing their payload to disk (or so they believe) and thus evading defensive controls like Justin Timberlake avoids Britney Spears fans.
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.
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
).
Identified | Variant | Classification | Attribution |
---|---|---|---|
2017 | Snake | Trojan | ? |
2019 | OSX.AppleJeus.C / macloader | RAT | Lazarus |
2020 | OSX.EvilQuest / ThiefQuest | Stealer / ransomware | ? |
2020 | Double Agent | PuP | Legitimate company |
2021 | NukeSped / AppleJeus | RAT | Lazarus |
2022 | Covid | RAT | Red team |
2023 | SUGARLOADER | Stager | Lazarus |
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.
Take Action
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
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:
- 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
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:
- When the adversary gets the base address of Dyld using
TASK_DYLD_INFO_COUNT
, ES will emit anES_EVENT_TYPE_NOTIFY_PROC_CHECK
for the type ofPIDINFO
and flavor of 13. - If in the case that the adversary leverages
libffi-trampolines.dylib
—as in the case of Dyld-DeNeuralyzer—then ES will also emit anES_EVENT_TYPE_NOTIFY_OPEN
for that.
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.
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')
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:
- 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 calledrun_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.