Using ESF to Detect In-Memory Execution

In the macOS security world, new attack techniques are few and far between. Patrick Wardle recently analyzed malware from the Lazarus APT Group which demonstrated the ability to execute payloads from memory. Luckily, proof of concept code was published utilizing the same technique. I modified the PoC code (specifically bundle.c) slightly to the following which will also spawn Calculator:

The Makefile, upon compilation, generates an executable named main and a bundle named test.bundle.

all-check: all check

check: ./main

all: main test.bundle

main : main.c ${CC} ${CCFLAGS} -o main main.c

test.bundle : bundle.c ${CC} ${CCFLAGS} -bundle -o test.bundle bundle.c

clean: ${RM} ${RMFLAGS} *~ main test.bundle

Let’s take a look at the main.c code to understand what is happening at a lower level. First, the open system call opens test.bundle with read-only permissions.

int fd = open("test.bundle", O_RDONLY, 0);

Next, the mmap system call maps memory pages from the object specified by fd, pointing to test.bundle.

void* codeAddr = mmap(NULL, stat_buf.st_size, PROT_READ, MAP_FILE | MAP_PRIVATE, fd, 0);

Next, NSCreateObjectFileImageFromMemory is called to create an object file image from the pointer to the memory region containing test.bundle. NSLinkModule links the object file image as a module into the main executable. NSLookupSymbolInModule returns a reference to the symbol _execute given the module representing test.bundle (bundle.c). function() in the last line executes the payload at the address of _execute in the module.

NSCreateObjectFileImageFromMemory(codeAddr, stat_buf.st_size, &fileImage); module = NSLinkModule(fileImage, "module",NSLINKMODULE_OPTION_NONE); symbol = NSLookupSymbolInModule(module, "_execute"); function = NSAddressOfSymbol(symbol); function();

Execution of the main executable.

Now that we have an understanding of how the technique works, let’s shift gears to detection. The following section will summarize what is outlined in the following Juypter notebook: https://gist.github.com/richiercyrus/449f37765595e53a54b3b9ec94a353c. Because we know Calculator was spawned, we’ll look at the process execution event and walk backwards to see what we can uncover. From the collection capture with Appmon, the following events were recorded:

|ES_EVENT_TYPE_NOTIFY_GET_TASK |1 |

|ES_EVENT_TYPE_NOTIFY_OPEN |641 |

|ES_EVENT_NOTIFY_FORK |168 |

|ES_EVENT_TYPE_NOTIFY_MMAP |89 |

|ES_EVENT_NOTIFY_EXIT |164 |

|ES_EVENT_TYPE_NOTIFY_WRITE |8909 |

|ES_EVENT_TYPE_NOTIFY_IOKIT_OPEN|2 |

|ES_EVENT_TYPE_NOTIFY_CLOSE |751 |

|ES_EVENT_TYPE_NOTIFY_CREATE |15 |

|ES_EVENT_TYPE_NOTIFY_SETOWNER |20 |

|ES_EVENT_NOTIFY_EXEC |65 |

|ES_EVENT_TYPE_NOTIFY_SETEXTATTR|196 |

|ES_EVENT_TYPE_NOTIFY_RENAME |11 |

Let’s start by looking into the schema of ES_EVENT_NOTIFY_EXEC (which are process execution events) to identify fields of interest.

root

|-- ProcessArgs: string (nullable = true)

|-- binarypath: string (nullable = true)

|-- destinationfilepath: string (nullable = true)

|-- env_variables: array (nullable = true)

| |-- element: string (containsNull = true)

|-- extendedattr: string (nullable = true)

|-- fileoffset: long (nullable = true)

|-- filepath: string (nullable = true)

|-- filesize: long (nullable = true)

|-- max_protection: long (nullable = true)

|-- mmapflags: array (nullable = true)

| |-- element: string (containsNull = true)

|-- mmapprotection: array (nullable = true)

| |-- element: string (containsNull = true)

|-- origin_binarypath: string (nullable = true)

|-- origin_cdhash: string (nullable = true)

|-- origin_codesigningflags: array (nullable = true)

| |-- element: string (containsNull = true)

|-- origin_pid: long (nullable = true)

|-- origin_platform_binary: boolean (nullable = true)

|-- origin_ppid: long (nullable = true)

|-- origin_signingid: string (nullable = true)

|-- origin_teamid: string (nullable = true)

|-- origin_uid: long (nullable = true)

|-- path_truncated: boolean (nullable = true)

|-- pid: long (nullable = true)

|-- ppid: long (nullable = true)

|-- size: long (nullable = true)

|-- sourcefilepath: string (nullable = true)

|-- sourcepath: string (nullable = true)

|-- uid: long (nullable = true)

|-- gid: long (nullable = true)

|-- user_class: string (nullable = true)

|-- user_client: long (nullable = true)

The fields binarypath, processArgs, pid, origin_pid, origin_binarypath appear to be useful. Calculator has a pid of 1376, origin_binarypath of /Users/johnappleseed/Downloads/macos_execute_from_memory-master/main and an origin_pid of 1376.

Using the origin_binarypath and origin_pid fields as a pivot, there are nine events with five unique event types.

Of the event types, ES_EVENT_TYPE_NOTIFY_MMAP stands out as there was a call to mmap in the PoC code which generated the Calculator execution. The schema for mmap shows the following fields of interest: mmapflags, mmapprotection, origin_binarypath and sourcepath.

root

|-- ProcessArgs: string (nullable = true)

|-- binarypath: string (nullable = true)

|-- destinationfilepath: string (nullable = true)

|-- env_variables: array (nullable = true)

| |-- element: string (containsNull = true)

|-- extendedattr: string (nullable = true)

|-- fileoffset: long (nullable = true)

|-- filepath: string (nullable = true)

|-- filesize: long (nullable = true)

|-- max_protection: long (nullable = true)

|-- mmapflags: array (nullable = true)

| |-- element: string (containsNull = true)

|-- mmapprotection: array (nullable = true)

| |-- element: string (containsNull = true)

|-- origin_binarypath: string (nullable = true)

|-- origin_cdhash: string (nullable = true)

|-- origin_codesigningflags: array (nullable = true)

| |-- element: string (containsNull = true)

|-- origin_pid: long (nullable = true)

|-- origin_platform_binary: boolean (nullable = true)

|-- origin_ppid: long (nullable = true)

|-- origin_signingid: string (nullable = true)

|-- origin_teamid: string (nullable = true)

|-- origin_uid: long (nullable = true)

|-- path_truncated: boolean (nullable = true)

|-- pid: long (nullable = true)

|-- ppid: long (nullable = true)

|-- size: long (nullable = true)

|-- sourcefilepath: string (nullable = true)

|-- sourcepath: string (nullable = true)

|-- uid: long (nullable = true)

|-- gid: long (nullable = true)

|-- user_class: string (nullable = true)

|-- user_client: long (nullable = true)

Sourcepath in this instance refers to “the file to map memory into.” Looking into event, we identify the sourcepath of /Users/johnappleseed/Downloads/macos_execute_from_memory-master/test.bundle which was used to spawn calculator and mapped into memory. This aligns with the activity we expect given the PoC code. In addition, the mmapflag value of MAP_PRIVATE and mmapproctection value of PROT_READ matches what we see in the code sample.

At this point, equipped with an understanding of the technique, we can build a query with the ESF dataset to look for process creation events in which a process parent links to one or more mmap events.

The more likely scenario would be to build a query using the data accessible to you internally, using the ESF data as a reference. Running Appmon at scale isn’t feasible and should be used in a lab/testing environment.

*Disclaimer: I don’t know of any EDR tools that currently provide mmap events for macOS.

Happy Hunting!