Checking macOS Screenlock Remotely
They (we) said it couldn’t be done
—
Three 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 K2 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?
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 nearly 3 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:
We searched for strings related to the Screenlock commands and found two that looked promising:
SACScreenLockPreferencesChangedMKBDeviceSetGracePeriod
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;uid = 502
name = 1Password extension (desktop app required)
profile = Person 1
identifier = aomjjhallfgjeglblehebfpbcfeobpgk
version = 4.7.5.90
description = Extends the 1Password app on your Mac or Windows PC,
so you can fill and save passwords in your browser.
locale = en
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 =
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 tableW0706 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”
You can read more about this strategy on Rich's excellent blog post:
Running processes in OS X as the logged-in user from outside the user’s account
Et voila!
➜ launcher git:(master) make sudo-osqueryi-tablesUsing 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.
Interested in using Kolide to check Screenlock?
Sign up now for a free trial! (no payment info required) and trial it across unlimited devices.
Kolide gives you unprecedented visibility into Windows, macOS and Linux devices while respecting your end-users privacy. Automatically resolve IT compliance issues with the interactive Kolide Slack App which will reach out and guide your users when problems are found.