How Kolide Built Its macOS Screenlock Check

Thu, 23 Jul 2020 00:00:00 +0000

Years ago, we published an article detailing changes made in macOS 10.13 which prevented Mac sysadmins from checking the state of their user’s screenlock settings. It is my pleasure to announce that after dozens of attempts and dogged effort we have closed the loop on one of the most critical (and frequently requested) security features available in macOS.

If you are a Kolide customer, you can find it today as one of our Checks:

Kolide Checks: macOS Screenlock Disabled / Insecure

There is also an Inventory item, which you can use to see all of your user’s Screenlock configurations at once:

Kolide Inventory: Screenlock Configurations

If you prefer to query it in Live Query and are using the latest version of Kolide Launcher you can run the following query, which is based on the requirements of the CIS Catalina 10.15 Level 1 Benchmark:

WITH screenlock_users AS (
  SELECT user,
         CAST(enabled AS int) AS enabled,
         CAST(grace_period AS int) AS grace_period
  FROM kolide_screenlock
  WHERE user = (
    SELECT user FROM logged_in_users WHERE tty = 'console'))
SELECT * FROM screenlock_users
WHERE enabled != 1
OR grace_period > 300;

How Did We Do It?

So simple right? Wrong.

The following is a recounting of our seemingly Sisyphean undertaking to inspect the screenlock setting on macOS using osquery. The solution was the result of hard-work from Victor Vrantchan (groob), Jason Meller (terracatta), Joseph Sokol-Margolis (directionless) and myself.

Before we get into the solution, it’s important to understand how we got started down this path. With the release of macOS 10.13 (High Sierra), Apple changed the way it managed screenlock settings on the Mac. In the process they created a problem for us (and other MacAdmins) that dogged us for years.

Unifying an Ecosystem – macOS Meets iOS

As we saw at the 2020 Apple WWDC keynote, Apple has been quietly toiling towards a consolidation of its mobile and desktop devices in both hardware and software. With the announcement of Big Sur (macOS 11.0) and ARM processors they have finally tipped their hand. Macs running iOS apps natively is no small task and the Herculean effort could not take place overnight.

The rumblings of this intent have loomed steadily on the horizon. One of the earliest signs of the intended change was the inclusion of previous iOS-only private frameworks on macOS, specifically in our case, the MobileKeyBag framework

MobileKeyBag is a framework which provides storage of keys for both file and Keychain Data Protection classes iOS and iPadOS use the following keybags: user, device, backup, escrow, and iCloud Backup.

In his original article, Victor correctly identified along with Michael Lynn (frogor), that screenlock preferences had been moved from the plist com.apple.screensaver.plist, to the user’s keychain. This was likely intentional; by moving the screenlock preferences out of a plist, Apple could make the setting less vulnerable to malware, and simultaneously work towards their eventual goal of consolidating operating systems.

Unfortunately, this change meant there was no longer a way for osquery or other common MacAdmin tools to read the preference in a reliable manner.

When plists disappear, you must reverse engineer

In my time at Kolide there has been no single feature-request as common as the ability to check screenlock on macOS. Every few months–emboldened by the release of a new OS revision or just pure hubris– we would revisit the issue with renewed vigor. And each time we were rebuffed in equal measure. It wasn’t until we stumbled upon the binary below and felt a glimmer of hope:

Hidden inside, like an uncut gem, was a CLI command for the following:

➜ sysadminctl
2020-07-06 12:24:11.248 sysadminctl[78730:19253610] Usage: sysadminctl -screenLock <immediate || off> -password <password>

We suddenly had a thread to pull for determining the status of the screenlock setting we just had to do some digging.

So we fired up Hopper Disassembler to see if we couldn’t determine what was responsible for making these changes. If you unfamiliar with Hopper, I would suggest checking out Michael Lynn’s terrific QueryCon presentation where he demonstrates osquery table development using similar reverse engineering methods:

https://www.youtube.com/watch?v=BUClsxF1t3o

We searched for strings related to the Screenlock commands and found two that looked promising:

SACScreenLockPreferencesChanged

MKBDeviceSetGracePeriod

We took those and ran to both Google and GitHub’s search to see what prior art existed, which might in turn lend further clues.

Attentive readers will no doubt guess that MKB stood for MobileKeyBag, the private iOS framework we referenced earlier. As a result of its relatively new inclusion in macOS, there was little to no published documentation on how to interact with it. After many fruitless searches to find some useful kernel of code based on MKBDeviceSetGracePeriod, we started getting desperate.

We guessed that if there was an MKBDeviceSetGracePeriod, there might in turn also be a MKBDeviceGetGracePeriod. Lo and behold! When we ran that through GitHub we got a solitary hit from a Japanese blog post written in 2015:

Armed with the knowledge that we could not only Set but Get the GracePeriod, we went to work in osquery importing the private frameworks and set to tinkering.

The next challenge was trying to understand how we actually use this private API. Normally with Objective-C you can simply use a third-party utility called class-dump on the binary or the framework and get the precise definition of the desired function. Unfortunately, in our case, running class-dump on the /System/Library/PrivateFrameworks/MobileKeyBag.framework/MobileKeyBag binary did not dump the definition we needed. Instead of wasting time troubleshooting class-dump, we decided to simply get our hands dirty with basic guessing and checking and see if we got lucky!

Ultimately, we only needed to try a few combinations until we stumbled upon the correct incantation; a function that takes and returns an NSDictionary. It was ultimately pure luck that caused us to stumble upon this.

Typically, Mac APIs will return nothing and instead mutate a dictionary or any array passed to them. While tinkering, we flipped from our original assumption–

the code takes no arguments and returns a dictionary

to the opposite–

the code mutates a passed dictionary and returns nothing.

While doing so, we carelessly forgot to change the function to return void, and left it as stated below.

// We load the framework dynamically earlier so we can still compile Osquery on older Macs that don't have MobileKeyBag.
auto MKBDeviceGetGracePeriod =
  (NSDictionary * (*)(NSDictionary*))
    CFBundleGetFunctionPointerForName(
     bundle,
     CFSTR("MKBDeviceGetGracePeriod")
    );
// MKBDeviceGetGracePeriod requires an empty dictionary as the sole argument  NSDictionary* durationDict = MKBDeviceGetGracePeriod(@{});

Much to our surprise, the function worked and inspecting the returned dictionary included all of the data we needed for our use-case!

Later, we determined there are arguments that you can pass in this configuration dictionary, but ultimately, they did not net us any new capabilities and were left on the cutting room floor.

The new table was PR'ed and then merged to much celebration, but there were still hurdles to overcome in order to utilize this new table remotely.

Using the new Screenlock table in osquery

While testing our table, we discovered quickly that while it worked running Osquery from the Terminal, the code did not behave the same when osquery was launched from launchd. Since almost all osquery setups run osquery as a daemon most organizations running the agent could not accurately query the data we worked so hard to collect.

User vs Root — An osquery conundrum:

In a previous article I detailed the idiosyncratic behavior of osqueryi when invoked as a user vs with sudo. New osquery users will commonly stumble on this invocation nuance when running a query like this:

osquery> SELECT * FROM chrome_extensions LIMIT 1;
         browser_type = chrome
                  uid = 502
                 name = 1Password extension (desktop app required)
              profile = Fritz_Personal
         profile_path = /Users/fritz-imac/Library/Application Support/Google/Chrome/Default
referenced_identifier = aomjjhallfgjeglblehebfpbcfeobpgk
           identifier = aomjjhallfgjeglblehebfpbcfeobpgk
              version = 4.7.5.90
          description = Extends the 1Password app on your Mac.
       default_locale = en
       current_locale =
           update_url = https://clients2.google.com/service/update2/crx
               author = AgileBits
           persistent = 0
                 path = /Users/fritz-imac/Library/Application Support/Google/Chrome/Default/Extensions/aomjjhallfgjeglblehebfpbcfeobpgk/4.7.5.90_0
          permissions = contextMenus, nativeMessaging, storage, tabs, webRequest, webRequestBlocking, http://*/*, https://*/*
 optional_permissions =
        manifest_hash = 54f65a527859ecaa03a011af21e2a29e4ef216dd22bc92439809666ff34a4729
           referenced = 1
        from_webstore = true
                state = 0
         install_time = 13207841264717527
    install_timestamp = 1563367664

Content with their output they will run to Kolide’s Live Query interface, type the query in and hit the Run button only to see an error message like below:

osquery> SELECT * FROM chrome_extensions;

W0706 11:43:09.013208 288959936 virtual_table.cpp:959] The chrome_extensions table returns data based on the current user by default, consider JOINing against the users table

W0706 11:43:09.013224 288959936 virtual_table.cpp:974] Please see the table documentation: https://osquery.io/schema/#chrome_extensions

The reason this occurs is because when run as the user, osqueryi makes the assumption that you are querying the chrome_extensions table for the current user. When run as sudo, the required WHERE clause of chrome_extensions.uid must be supplied for the query to return results.

Unfortunately for us, a similar hurdle exists for the macOS Screenlock setting. Due to changes made in 10.13, when the configuration of screenlock was moved from a plist to the user’s keychain, we can only access the data when queries are run as the current logged in user.

Kolide Launcher to the rescue!

One of the most helpful resources we have leaned on at Kolide is the ability to improve osquery’s capabilities using our own extension: Launcher. When a table doesn’t quite make sense to be pushed upstream to the core osquery project, we frequently turn to Launcher as our own data collection playground.

A number of useful tables have been written to overcome limitations (eg. kolide_plist) inherent to osquery. This screenlock table would likewise require some osquery-external execution to be successful.

Getting meta: Osquery running osquery

To work around the user/root limitation present in the core osquery table, we used Launcher to instantiate an osquery shell using launchctl asuser. Empirically, if the user has logged in recently, their results will return successfully.

For those of you unfamiliar withlaunchctl asuser, it:

“…executes the given command in as similar an execution context as possible to that of the target user’s bootstrap”

Running processes in OS X as the logged-in user from outside the user’s account

Et voila!

➜  launcher git:(master) make sudo-osqueryi-tables
Using a virtual database. Need help, type '.help'
osquery> WITH screenlock_users AS (
      SELECT user,
              CAST(enabled AS int) AS enabled,
              CAST(grace_period AS int) AS grace_period
      FROM kolide_screenlock
      WHERE user = (
        SELECT user FROM logged_in_users WHERE tty = 'console'))
    SELECT * FROM screenlock_users;
+------------+---------+--------------+
| user       | enabled | grace_period |
+------------+---------+--------------+
| fritz-imac | 1       | 5            |
+------------+---------+--------------+

Using a Kolide Launcher built instance of osqueryi I am able to test the query on my local device.

The data is returned and the people rejoiced!

The Future of macOS Accessibility Is Uncertain

Though ultimately, this was a story of success, it presages a darker horizon. Increasingly, Apple has worked to tighten the screws on macOS, most recently with the pitiless execution of 3rd-party Kernel Extensions, rendering many existing security products dead in the water. Likewise, with the imminent arrival of ARM-based Macs, it stands to reason, more and more of these settings will move from traditional plists to the iOS-like keychain-entangled storage method. Gotchas like this screenlock setting may become more of the norm than the exception and the incumbent security/it compliance providers are going to have to stay on their toes.

Interest in Using Kolide to Check Screenlock?

Kolide gives you unprecedented visibility into Windows, macOS and Linux devices while respecting your end-users privacy. Automatically resolve IT compliance issues with our Device Trust solution which will notify and guide your users when problems are found.

Watch our on-demand demo to see how our Device Trust solution can help solve your device security issues.