using osquery to audit screenshots on macos

osquery is a very powerful tool for use on your client fleet, especially if you've already deployed ODOS (On-Demand OSquery) but there other features of osquery that require significant configuration to use. Here I will outline the steps required to automatically pull screenshots from a macOS host running osquery.

requirements

  • osquery 2.7.0 or later
  • A cloned osquery repo
  • python
  • zstd (optional)

osquery config

First we are going to use the views block of our osquery config (for the purposes of this example I will assume that you're using the filesystem config plugin, if you're not add this block however osquery gets its config).

"views" : {
    "screenshots" : "select time, trim(SUBSTR(cmdline, instr(cmdline, ' /'))) as path, euid, egid, uid, gid, auid, cmdline from process_events where path like '%screencapture%';"
}

This is going to create a new view for us to simply our scheduled query later.

osquery flags

Now we need to enable the osquery audit framework. However you pass flags to osquery you want to add:

--disable_audit=false

If you're testing with osqueryi also add --disable_events=false.

This is going to allow the OpenBSM publishers and subscribers to start up when osquery executes. You can also add --verbose for testing while you follow these steps.

Openbsm

OpenBSM is macOS' version of audit. It is enabled by default on all macOS installs and has its configuration in /etc/security. We are going to modify /etc/security/audit_control to log process executions (to be precise what we really care about is AUE_POSIX_SPAWN). You do this by adding the ex flag to your audit_flags.

#
# $P4: //depot/projects/trustedbsd/openbsm/etc/audit_control#8 $
#
dir:/var/audit
flags:aa,lo,pc
minfree:5
naflags:no
policy:cnt,argv
filesz:2M
expire-after:10M
superuser-set-sflags-mask:has_authenticated,has_console_access
superuser-clear-sflags-mask:has_authenticated,has_console_access
member-set-sflags-mask:
member-clear-sflags-mask:has_authenticated

After this you can execute audit -s as root. This should restart the audit system but in my experience a full reboot if often required. You can leave the other flags as they are but if you feel you must you can add arge to the policy line to also log the environment variables as well.

test run

At this point, everything you need should be configured and you can run a test run with osqueryi. osqueryi needs one more flag because it doesn't start the extensions framework by default. So you'll want to run something like this:

sudo osqueryi --disable_audit=false --disable_events=false --config_path=/path/to/config.conf

Depending on your infrastructure you may want to add other flags such as --disable_extensions, --verbose to make testing easier. What you should see is a line stating:

Starting event publisher run loop: openbsm

If you see this then audit should be enabled correctly and you're ready to start testing.

select time, path from screenshots;

If you haven't take any screenshots, this should return no results which is exactly what we want. Now try taking a screenshot (Command + Shift 3/4) and rerun the query.

+------------+---------------------------------------------------------------+
| time       | path                                                          |
+------------+---------------------------------------------------------------+
| 1500000000 | /Users/obelisk/Desktop/Screen Shot 2017-07-14 at 2.40.00.png  |
+------------+---------------------------------------------------------------+

Hopefully you are greeted with such a wonderful result as above. If not, you can try querying process_events directly and make sure there are entires in there (restart your computer if you didn't after enabling audit).

getting the screenshots back

Now that we have this, we need to pull the screenshots back to a server somewhere (say for compliance or leaks purposes). For this, we are going to utilize the osquery carving functionality. We'll use it to bundle up all of the paths from that the query returned and send them back to our distributed endpoint. First we need to configure osquery for a remote endpoint.

--tls_hostname=localhost:8080
--tls_server_certs=/x/y/z/osquery/tools/tests/test_server_ca.pem
--enroll_secret_path=/x/y/z/osquery/tools/tests/test_enroll_secret.txt
--enroll_tls_endpoint=/enroll
--disable_carver=false
--carver_compression=false
--carver_disable_function=false
--carver_start_endpoint=/carve_init
--carver_continue_endpoint=/carve_block

Obviously make sure that /x/y/z/osquery point to your checkout of the osquery repo. If you are using a flags file for this example, add these flags in there, otherwise add them all to your command line. If you want to start using a flag file, paste all these into document and pass osquery --flagfile /x/y/z/osquery.flags.

The observant reader may notice that --carver_compression is disabled. This is because osquery carver uses Zstandard (Zstd) as it's compression algorithm for sending carved files back. It is both more performant and offers better compression than Zlib. However, this will add an extra step to verifying the screenshots later in addition to requiring you have Zstd installed. If you wish to enable compression you will simply need to decompress the resulting file and then add the tar extension later.

For testing purposes we will use the osquery test server. The http server has many flags you can set but I've included the basic instantiation from the osquery docs below.

./osquery/tools/tests/test_http_server.py --tls --persist --cert ./tools/tests/test_server.pem --key ./tools/tests/test_server.key --use_enroll_secret --enroll_secret ./tools/tests/test_enroll_secret.txt 8080

This will start a test server on port 8080 of our localhost. The test server is capable of sending scheduled queries, receiving logs, and receiving then saving carved files.

Let's put it all together and get a proof of concept. Start the osquery test server and verify that it comes up successfully, you should see:

-- [DEBUG] Starting TLS/HTTPS server on TCP port: 8080

If you see that great, next start osquery with all its new flags and rerun the screenshot command from above. Verify you see some rows (if not, take some screenshots). Next run the query below, utilizing the carver.

select carve(path) from screenshots;

This will take all the paths returned from our screenshot query, tar them, optionally compress them, and send them to our server. Checking the output of the test server you should see:

- [DEBUG] File successfully carved to: /tmp/0cb866d9-6587-401f-a0e3-293f95933af4.tar

Note: If you left compression enabled, the extension will be zst. Simply zstd decompress it and add a tar extension.

Simply untar that file and you should have a folder with all the screenshots in it.

Next steps

You now know how to use osquery to audit screenshots from your macOS devices and collect them back on a central server automatically. However there is one notable caveat to this method: multiple screens. The query in our view does not gracefully handle multiple screens as the command line provided is of the form:

/usr/sbin/screencapture -df -tpng /Users/obelisk/Desktop/Screen Shot 2017-07-14 at 2.40.00.png /Users/obelisk/Desktop/Screen Shot 2017-07-14 at 2.40.00 (2).png

While this is easily solvable with C++, I've yet to come up with a graceful method that scales to the number of screens using pure SQL. If you have one, I will gladly update this page with credit to you for the command.

Also, you'll want to schedule this query to pull back screenshots ideally before they are erased. Luckily osquery is smart enough to know which rows have been returned so far so even if you set this query to iterate quickly, you won't pull the same screenshot twice.

I hope you found this advanced usage of osquery interesting and will help you better understand and deploy some of osquery's more advanced features to your fleet.