leveraging Unified Logs - a series

Foreword

Before we start, I have a super cool life update to get out of the way–I graduated in May 2024, got some traveling out of the way and have finally joined the workforce at a super cool company! :D

I have always taken an interest in macOS security since reading Patrick Wardle's "The Art of Mac Malware" and then attending the first Objective For The We conference in San Francisco (an absolutely incredible expereience, I would definitely recommend attending any Objective-See events). Now that I have experience working with macs on an enterprise scale and have gained a more comprehensive understanding of what that entails, I wanted to write a blog to really note down everything I've learned along the way and hopefully help others who are just starting out as well.

Intro

Unified logs, also known as Apple Unified Logs (AUL), is the native macOS logging utility that Apple introduced to replace and consolidate older logging mechanisms. With the unified logging system, logs are stored in binary files, which you can access and interact with using the log command. This architecture provides a vast amount of detailed, low-level data on virtually everything that occurs on your machine–valuable insight that was designed for troubleshooting, but can be just as effective for security.

For those who have experience with macOS security, you may be wondering: What's the deal with using Unified Logs? Apple has already introduced the Endpoint Security Framework, wouldn't that be more helpful? In my opinion, unified logs can be just as powerful, offering a different degree of visibility. Because these logs can be extremely verbose—try running log stream in your Terminal to see how quickly they flood in—you'll want to take advantage of "predicates," which are essentially queries. Predicates help you zero in on the specific data you need, filtering out the noise and making the logging system far more manageable.

As I am constantly learning and improving, I'd like to create this series to document and share the predicates I've found especially effective, whether I'm responding to incidents or enhancing detections to improve visibility across macOS devices.

The first few parts of this series will be focusing heavily on utilizing unified logs to gain better visibility on unpermitted actions happening on the endpoint–primarily, potential exfiltration.

Enabling private logging

In order to harness the full capabilities of unified logs, we must enable the logging of private data, which will transform your redacted logs into a helpful and detailed record of events on your system.

To create this configuration profile, I just followed this blog by Bob Gendler (thank you!!): https://boberito.medium.com/private-data-in-unified-logging-10-15-9eb2b4be5c40

At this point, you can sign it and deploy it with your MDM. However, if you want to use this on your local machine, a couple more steps are required. The steps to do so are outlined below:

  1. Create a self signed cert
    • Keychain Access
    • Certificate Assistant -> Create a Certificate
  2. Name it something that makes sense + Certificate Type: Codesigning + override defaults
  3. Most of the other things are fine
  4. Signing the cert
    • /usr/bin/security cms -S -N "<cert name>" -i <path to cert> -o <path to new cert>
    • Make sure output path is different or the profile gets corrupted

Once your configuration profile is signed, you should be able to install it onto your device.

USB/External Storage Activity

A pretty cool way that I am leveraging unified logs is for USB events. I know that sounds basic, but hear me out–through some finagling with the logs, I was able to obtain the file names of some of the contents of the USB, and even use that to deduce potentially transferred files. This method is a little reminiscent of forensic artifacts such as shellbags in my opinion. I noticed that when an external drive is plugged in to a macOS device, the system–specifically the Quicklook Thumbnail Agent–does a preliminary scan of sorts of the contents of the drive and renders thumbnails for them. This action creates a kernel-level event recorded by unified logs. By querying logs created by this thumbnail agent at a path that contains "/Volumes/", we can identify the contents of externally mounted media. I use the basic query below, but of course, there is a lot of room to adapt it to suit your use case:

            log show --predicate 'process == "kernel" and senderImagePath contains[c] "Sandbox" and eventMessage contains "Sandbox:" and eventMessage contains[c] "com.apple.quicklook" and eventMessage contains[c] "Thumbnails" and eventMessage contains[c] "\\/Volumes\\/"'
          

Events for the contents of the drive will yield logs like this:

            Sandbox: com.apple.quicklook.ThumbnailsAg(<number>) deny(<number>) file-read-xattr /Volumes/<name_of_drive>/<file_path>
          

While logs of copied files will look similar, but with 1 duplicate report for appended to the beginning of the log.

Pairing this with two more unified log events–external drive mounted and external drive removed–you get a pretty accurate reconstruction of external media usage, including the name of the drive, the partial contents of the drive, what was transferred, and how long it was attached.

Query for external media mounted:

            log show --predicate 'process == "fskitd" and subsystem == "com.apple.LiveFS" and eventMessage contains[c] "ReallyMountVolume"'
          

Query for external media removed:

            log show --predicate 'process == "fskitd" and subsystem == "com.apple.LiveFS" and eventMessage contains[c] "Unmounting"'
          

The way I am utilizing these logs is to first query on events where a drive is mounted, joining that event with a drive being removed, then finally correlating that with events of the contents on the drive based on the host and the given time range. To make this easier to implement into use cases like dashboards, for example, I parse out all the important fields with regex, as unfortunately for our purposes, these logs are more textual and do not adhere to a standard key-value format.

While these logs will not be 100% accurate in producing all the contents of an external drive, I think it is still incredibly helpful and honestly really cool that we can recover this level of information without any sort of additional monitoring or DLP tool.

If you are familiar with unified logs, you may notice that my queries are utilizing only "Default" level logs. The reason for this is currently I am forwarding these logs to a central logging server using Jamf Protect, and while that is a super cool feature of Jamf, it also means we are limited to "Default" level logs only. (However, I am currently testing a unified log forwarder/collector which would enable this capability, but I am still working on it.)

Gatekeeper User Override

Though this will definitely need some tuning, this log has been extremely helpful for identifying instances when a user installs an application from an external source, such as Github.

            log show --predicate 'process=="syspolicyd" and subsystem=="com.apple.syspolicy.exec" and category=="default" and ((eventMessage contains[c] "marked application" and eventMessage contains[c] "approved") or (eventMessage contains[c] "allowing"))'
          

From here, you can easily parse out the path of the application and the codesigning information associated with it. This can further be used to create detections where a commonly trojanized application is launched with the incorrect developer ID, etc.

Interactive Application

Another cool query that I leverage unified logs for are interactive launches of applications. These events record any time a user is actively involved in launching an application, be it through Spotlight or by double-clicking on the application in Launchpad. These events can provide helpful insight and context to the user's activity surrounding an event.

            log show --predicate 'process=="runningboardd" and subsystem=="com.apple.runningboard" and category=="process" and eventMessage contains[c] "executing launch request for app"'