« Back to Blog

Running Executables on macOS From Memory

By Stephanie Archibald

As a security researcher, I'm always researching new and innovative ways that malware and attackers might exploit devices or what they might execute post-exploitation. While Windows is generally the most common target, there's no shortage of existing and burgeoning techniques against macOS (previously OS X). In this post, I will discuss some of the research I've been conducting around the execution of multi-stage payloads on macOS (up to Sierra).

A common technique for executing multi-stage payloads is to have an initial payload that can then load executables, libraries, or bundles from memory instead of a computer’s hard disk, thereby reducing the risk of detection. Historically, when attempting to load code from memory on macOS, the first goal is finding the dynamic loader, dyld, to facilitate the loading of a second stage payload. Once you find dyld in memory, you can parse its Mach-O headers to locate the functions NSCreateObjectFileImageFromMemory (to create an NSObjectFileImage) and NSLinkModule (to link the image libraries into the current process and run any constructors) to load bundles or, as shown below, even executables.

Parsing the Mach-O headers in memory is a post in itself, so if you’re not familiar with the format, I suggest reading any of these writeups [1][2][3].

Diving Deeper Into dyld

When you execve a dynamically-linked binary on macOS, the first thing the kernel will do is retrieve the location of the dynamic loader from the binary's Mach-O load commands and load it [4]. Because of this, dyld is the first Mach-O that is loaded into a process's address space after the binary. The kernel determines the candidate address for dyld by taking the binary's vmaddr and adding an ASLR slide to it. The kernel then maps the Mach-O sections into memory at the first available unallocated memory address greater-than-or-equal-to the candidate [5].

As you can see below, in versions of macOS before Sierra, dyld's vmaddr was 0x7fff5fc00000 (DYLD_BASE):

macOS 10.10.5 (Yosemite)

$ otool -l /usr/lib/dyldx
Load command 0
      cmd LC_SEGMENT_64
    cmdsize 552
    segname __TEXT
      vmaddr 0x00007fff5fc00000

This made it very easy to locate dyld in memory; the first Mach-O image found by searching from the start of DYLD_BASE was dyld. The ASLR slide for dyld could then be calculated by subtracting DYLD_BASE from the address of dyld in memory. Determining the ASLR slide was important for resolving symbols because the n_value in the nlist_64 structs of the symbol table contained a base address that needed to be offset by the slide.

Things changed in Sierra and dyld is now mapped dynamically (vmaddr is 0):

macOS 10.12.2 (Sierra)

$ otool -l /usr/lib/dyld
Mach header
    magic cputype cpusubtype caps    filetype ncmds sizeofcmds      flags
  0xfeedfacf 16777223          3  0x00           7    14       1696 0x00000085
Load command 0
    cmd LC_SEGMENT_64
  cmdsize 552
  segname __TEXT
    vmaddr 0x0000000000000000

This means that instead of being the first Mach-O image from DYLD_BASE, dyld can now be found adjacent to the loaded executable image in memory. Because there is no fixed base address, we can now no longer easily calculate the ASLR slide value. Fortunately, we no longer care about this value, as the n_value of the nlist_64 struct of the symbol table now contains an offset from the start of dyld; once you find the address of dyld in memory, you can resolve its symbols. We’ll cover this in more detail below, in the Resolving Symbols section.

Location of dyld in Memory

So how do we search for dyld in memory? The proper way to search for a particular mapping within the address space is recursively, using vm_region. The shellcode produced by this method, however, is lengthy and tedious. Luckily, there's an alternative; if we find a syscall that takes a pointer and returns different return values based on whether the address is mapped or not, we can use that instead. The chmod syscall does just this.

Looking at the chmod man(2) page [6], we see that chmod returns EFAULT if the path pointer is outside the process's allocated address space; it returns ENOENT if the path doesn't exist. Therefore, we can use the following function to find dyld:

int find_macho(unsigned long addr, unsigned long *base) {
    *base = 0;

    while(1) {
        chmod((char *)addr, 0777);
        if(errno == 2 && /*ENOENT*/
          ((int *)addr)[0] == 0xfeedfacf /*MH_MAGIC_64*/) {

            *base = addr;
            return 0;

        addr += 0x1000;
    return 1;

In versions of macOS before Sierra, you call it as so:

unsigned long dyld;
if(find_macho(DYLD_START, &dyld)) return 1;

In Sierra, we have to call find_macho twice: once to find the loaded binary and once to find dyld:

unsigned long binary, dyld;
if(find_macho(EXECUTABLE_BASE_ADDR, &binary)) return 1;
if(find_macho(binary + 0x1000, &dyld)) return 1;

Resolving Symbols

Finding the strings table for dyld can be done by parsing its load commands and finding the symbol table and the linkedit and text segments in memory with the following code (base is the address of dyld in memory):

lc = (struct load_command *)(base + sizeof(struct mach_header_64));
for(int i=0; i<((struct mach_header_64 *)base)->ncmds; i++) {
    if(lc->cmd == 0x2/*LC_SYMTAB*/) {
        symtab = (struct symtab_command *)lc;
    } else if(lc->cmd == 0x19/*LC_SEGMENT_64*/) {
        switch(*((unsigned int *)&sc->segname[2])) { //skip __
        case 0x4b4e494c:    //LINK
            linkedit = (struct segment_command_64 *)lc;
        case 0x54584554:    //TEXT
            text = (struct segment_command_64 *)lc;
lc = (struct load_command *)((unsigned long)lc + lc->cmdsize);                      

The code above skips past the mach_header_64 struct in memory then iterates over the various load command structs. We store the addresses for the LC_SYMTAB and the two LC_SEGMENT_64 commands pertaining to the __LINKEDIT and __TEXT segments. With pointers to these structs, we can now calculate the address of the string table in memory:

unsigned long file_slide = linkedit->vmaddr - text->vmaddr - linkedit->fileoff;
strtab = (char *)(base + file_slide + symtab->stroff);

To walk the string table, we need to iterate over the nlist_64 structs of the symbol table. Each nlist_64 struct contains an offset into the string table (n_un.n_strx):

char *name = strtab + nl[i].n_un.n_strx;

Instead of using a traditional hashing algorithm to match the symbol names in the strings table, I've written a helper script (symbolyzer.py) to generate unique offset / int pairs as identifiers in the following manner:

$ nm /usr/lib/dyld | cut -d" " -f3 | sort | uniq | python symbolyzer.py
_NSCreateObjectFileImageFromFile[25] = 0x466d6f72

The code for symbolyzer.py can be found on GitHub here.

As you can see above, the offset / int pair for NSCreateFileImageFromMemory is 25 & 0x466d6f72. This means that if we index into a given string in the strings table by 25 and it equals 0x466d6f72, we have found a match. It's possible to match all symbol strings in dyld in this manner by including the NULL terminating character in our pair generation logic.

Loading Executables

The most commonly used technique on macOS for loading code from memory is by calling NSCreateObjectFileImageFromMemory on a macOS bundle followed by a call to NSLinkModule and then NSAddressOfSymbol to find a known function ala “The Mac Hacker's Handbook”[7]. The man page [8] for NSLinkModule states "Currently the implementation is limited to only Mach-O MH_BUNDLE types which are used for plugins."  The header file for dyld further states "NSObjectFileImage can only be used with MH_BUNDLE files". A quick test confirms that this is true; if the filetype of the mach_header_64 struct is not MH_BUNDLE (0x8), NSCreateObjectFileImageFromMemory fails. Luckily, this is easy to correct with a couple of lines of code to change the struct’s filetype:

int type = ((int *)binbuf)[3];
if(type != 0x8) ((int *)binbuf)[3] = 0x8; //change to mh_bundle type

We can then find the entry point by parsing the Mach-O image within the NSModule object returned by NSLinkModule; in Sierra the Mach-O image changed from offset 0x48 to 0x50. By iterating over the load commands of this image, we can find the LC_MAIN command and obtain the entry offset. Simply adding this offset to the Mach-O base gives us the entry point:

unsigned long execute_base = *(unsigned long *)((unsigned long)nm + 0x50);
struct entry_point_command *epc; 

if(find_epc(execute_base, &epc)) {  //loops over commands and finds LC_MAIN
fprintf(stderr, "Could not find entry point command.\n");
goto err;

int(*main)(int, char**, char**, char**) = (int(*)(int, char**, char**, char**))(execute_base
    + epc->entryoff);

char *argv[]={"test", NULL};
int argc = 1;
char *env[] = {NULL}; 
char *apple[] = {NULL};
return main(argc, argv, env, apple);

All of the POC sources for this post can be found on GitHub here. 

What's the benefit of running an executable instead of running a bundle? I'll be presenting some ideas at this year's Infiltrate in a talk titled "Sierra Had a Little Lamb: A Userland Kit for MacOS". Come see and find out!

For organizations interested in preventing attacks and malware on macOS endpoints, Cylance has you covered there, too. Contact us to learn how our AI-driven solution can predict and prevent unknown and emerging threats.


1. http://phrack.org/issues/64/11.html

2. http://www.blackhat.com/presentations/bh-dc-09/Iozzo/BlackHat-DC-09-Iozzo-let-your-mach0-fly-whitepaper.pdf

3. https://lowlevelbits.org/parsing-mach-o-files/

4. https://www.mikeash.com/pyblog/friday-qa-2012-11-09-dyld-dynamic-linking-on-os-x.html

5. https://opensource.apple.com/source/xnu/xnu-2782.1.97/bsd/kern/kern_exec.c

6. https://developer.apple.com/legacy/library/documentation/Darwin/Reference/ManPages/man2/chmod.2.html

7. https://www.amazon.com/Mac-Hackers-Handbook-Charlie-Miller/dp/0470395362

8. https://opensource.apple.com/source/cctools/cctools-384.1/man/NSModule.3.auto.html

Tags: macOS, OS X