Loading Python Runtimes in Swift

Cedric Owens
Red Teaming with a Blue Team Mentality
7 min readAug 14, 2020

In this blog post I will walk through using a neat Swift package that imports local python libraries into Swift so that you can run python commands directly inside of Swift (without running the python binary). Note: This method does require that python libraries already be installed on the host. Also, while the methods tested may get around some python parent child relationship detections, my tests below leveraged command line utilities which are easily traceable. As of the time of this post, new macOS installs include both python2 and python3 (though python2 has been sunset). Apple has noted plans to remove all scripting runtimes from future versions of macOS, though Apple has not yet specifically identified when this will take place (Big Sur still appears to include both python2 and python3). As an offensive security engineer I enjoy trying different ways of running commands and post exploitation tasks and seeing how common logging/detection approaches work. So I will walk through my setup and tests and document everything below for you to follow along.

Setup

  • Host: macOS Catalina 10.15.6
  • XCode IDE: XCode 11.6
  • Swift: Version 5.2.4
  • Local Python Versions: 2.7.16, 3.7.3 (Note: I used python2 in my tests below, since the PythonKit library I used defaulted to that version; you can change which python runtime PythonKit uses)
  • Objective See’s Endpoint Security Framework Monitoring Tool (Process Monitor): https://bitbucket.org/objective-see/deploy/downloads/ProcessMonitor_1.3.0.zip
  • PythonKit Swift library by pvieito: https://github.com/pvieito/PythonKit. — this is the neat library that I used in this post to test parent child detections around python
  • Caveat: This technique below works well for standard libraries in python. However this technique does not allow you to import external libraries, so you are relegated to standard libraries. Therefore the examples I used below include standard libraries.

Steps to import the PythonKit:

  • From a macOS terminal, create a directory for the new project you will build
  • cd into that directory and run: “swift package init --type executable”
  • run: “swift package generate-xcodeproj”
  • Open the new .xcodeproj file in Xcode
  • Add the PythonKit package in the “Package.swift” file as below:
  • Save and Close
  • From a terminal, cd into the project directory and run the “swift package generate-xcodeproj” command again to pull in the dependecies and re-generate your xcode project file.
  • In XCode, re-open your xcodeproj file and navigate to the main.swift file. Once there add “import PythonKit” at the top of the main.swift file. It should not generate an error due to the step above. If you still see an error, run a build inside of XCode and the error should go away.
  • If you encounter an error around xctest not being found, run “xcode-select -s /Applications/XCode.app/Contents/Developer/” and then run “swift package generate-xcodeproj

Steps for Setting Up the Objective See ProcessMonitor Tool:

  • download link: https://bitbucket.org/objective-see/deploy/downloads/FileMonitor_1.2.0.zip
  • I really like the ProcessMonitor tool, since it leverages Apple’s Endpoint Security Framework, which has good visibility into process executions and handles
  • After extracting the zip, grant the Process Monitor app full disk access: System Preferences → Security & Privacy → Privacy → Full Disk Access → add ProcessMonitor
  • To Run: sudo ./ProcessMonitor (from inside of ProcessMonitor.app/Contents/MacOS/ directory)…For each test, I would start this immediately before running my code and inspect the events to see what detection logs would look like.

Running Python From Swift

For my initial basic test, I ran python’s os library in order to list out the current directory with the command “ls -al”. Swift code in Xcode:

I then built and executed the macho binary from the code above to verify that it worked:

I then ran ProcessMonitor to see what the logs for this activity would look like, since the python library is being invoked in our swift code (as opposed to the python binaries on disk):

Observations from the ProcessMonitor capture above:

  • as expected, my mach-o SwiftPy’s parent is /bin/zsh; ancestor processes include: zsh, login, and Terminal
  • no references to python anywhere in the logs though python libraries were invoked in my Swift code
  • I ran my compiled binary directly from the terminal. So the log flow: My default /bin/zsh shell forks and launches my mach-o (SwiftPy). My mach-o is then seen forking and executing /bin/sh (which is used by the python os library by default) with the arguments ls -al (directory listing command)

Next, I decided to run python locally and execute this same command in order to compare ProcessMonitor logs. From my terminal I ran python and executed the os.system(“ls -al”) command and checked the ProcessMonitor capture:

ProcessMonitor capture observations from locally running python from my terminal:

  • as expected, the parent process to my python execution was /bin/zsh; ancestor processes were identical to above (zsh, login, and terminal)
  • Python is referenced in the logs
  • overall log flow: python forks and starts /bin/sh with arguments “ls -al”

So as you can see above: my SwiftPy tool leveraging PythonKit executed the same command that I ran locally via my terminal but the logs associated with my tool had no identification of python being used (though it was used).

This same approach could be used to run a variety of macOS shell commands, such as curl, osascript, history, etc. Any detections relying on suspicious invocation of python in parent child relationships would not be triggered using this method. One common parent child detection might be:

/bin/sh (or /bin/zsh or /bin/bash) → python → a shell command (or another /bin/sh, /bin/bash, or /bin/zsh process)

I think that is a solid parent child detection to flag on. However, the example above (where python libraries are executed via Swift code) would not trigger that detection since python is not seen in the logs (though python is indeed invoked). So in essence, this simple technique can be used to bypass detections around suspicious python parent-child relationships (since python is not shown in logs using this technique).

Example with a Web Connection:

For this example, I used the urllib2 standard library. Below is the Swift code:

This is just a simple POST request to a local server running on port 80. I validated that the code works:

Next, let’s see how this looks in logs:

Similar to above…though I used the urllib2 library in my SwiftPy mach-o, the logs do not show any indication of python being used. You can see zsh being the parent of my mach-o SwiftPy. So this example would get around detections looking for python processes connecting out to command and control servers. Other methods could be used though (ex: looking in network traffic for hosts performing beaconing-like activity).

Summary

This was an interesting side project and helped identify some ways around parent child relationship detections involving python. There are lots of different use cases on the offensive side for this approach, including running shell commands, making web requests, encoding/encryption, and leveraging python for any task that Swift may not handle as well. Of course this technique will be much less useful once Apple removes all scripting runtimes from future versions of macOS, but that does not appear to be happening anytime soon.

I also plan to add code samples into my MacShellSwift project so you can play with this technique and/or test defections:

https://github.com/cedowens/MacShellSwift

In general, I recommend checking out the following post on macOS low level process hunting by Jaron Bradley:

This post gives a good overview on common execution chains on macOS and anomalies to consider.

I also wrote a post on some base level detections for macOS to consider:

Happy Hunting!

--

--

Cedric Owens
Red Teaming with a Blue Team Mentality

Red teamer with blue team roots🤓👨🏽‍💻 Twitter: @cedowens