Detection Engineering using Apple’s Endpoint Security Framework

Richie Cyrus
Posts By SpecterOps Team Members
7 min readJan 30, 2020

--

Referencing the Funnel of Fidelity, this post will cover both collection and detection stages.

As detection engineers, our main goal is to create detections for techniques of interest. This requires the following:

  1. Identify the technique to “hunt” for.
  2. Research how the technique works from an operating system internals perspective.
  3. Create or find proof of concept code/”malware” that will perform the technique.
  4. Identify data sources mapped to the technique, and build the detection.

Endpoint Security Framework Introduction & Use Case

For Windows techniques, our team’s tool of choice for building detections is Sysmon. It is free, easy to install in a lab environment, provides decent coverage of various actions that could take place on a Windows system, in addition to the ability to ingest its data into a centralized analytic platform such as an ELK stack.

For macOS, I have not found a solution comparable to what Sysmon provides in relation to the data contained in events. Osquery is excellent for querying the state of an endpoint at a given point in time however, it lacks real-time contextual data. At Apple’s Worldwide Developers Conference (WWDC) last year, the Endpoint Security Framework (ESF) was introduced, which “monitors system events for potentially malicious activity.” The Endpoint Security Framework is a C API which is part of the broader System Extensions Framework. My excitement rose when I discovered the types of events ESF covers. Below are a few I believe are useful for detection engineering purposes.

ES_EVENT_TYPE_NOTIFY_CREATE
A type that represents events for notification of the creation of a file.
ES_EVENT_TYPE_NOTIFY_DELETEEXTATTR
A type that represents events for notification of the deletion of an extended attribute from a file.
ES_EVENT_TYPE_NOTIFY_EXEC
A type that represents events for notification of the execution of a process.
ES_EVENT_TYPE_NOTIFY_GET_TASK
A type that represents events for notification of the retrieval of a task’s port.
ES_EVENT_TYPE_NOTIFY_MMAP
A type that represents events for notification of the mapping of memory to a file.
ES_EVENT_TYPE_NOTIFY_OPEN
A type that represents events for notification of the opening of a file.
ES_EVENT_TYPE_NOTIFY_RENAME
A type that represents events for notification of the renaming of a file.

My colleague, Chris Ross, built a macOS application using ESF called Appmon that captures various notification events on a macOS system running Catalina (10.15.x). Appmon was used to generate the data used for this post. For those interested in the details around how ESF works under the hood, refer to Patrick Wardle’s post which demonstrates how he used ESF to build a process monitor. Equipped with Appmon and the data it provides, I stood up a lab environment which included a macOS VM running Catalina, and HELK. I modified HELK to add the “esf” topic to the Kafka config, and Logstash Kafka input filter. The following command is used to send ESF data via Appmon to the Kafka topic “esf”, which is read by Logstash, and sent to an Elasticsearch instance:

sudo ./appmon.app/Contents/MacOS/appmon | kafka-console-producer.sh --broker-list <kafka IP address>:9092 --topic esf

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!

--

--