In Pythonic Malware Part-1, I demonstrated how Python executables can be used to bypass Windows Defender and successfully launch Meterpreter shells on a fully patched system. However, this raised an interesting question, why don’t more APT’s and threat groups use Python for malware development?
While highly effective, one reason is because compiled Python is easily reversed. Without manual intervention or converting to lower-level languages, the default ways of compiling Python could allow blue teams to recover the clear-text source.
This post demonstrates how to decompile the
shellcode_loader.exe file created in Part-1 and recover the source code being executed — even when employing PyInstaller’s bytecode obfuscation with AES256 encryption.
The first step in decompiling the
shellcode_loader.exe file is to unpack the compiled binary using pyinstxtractor. This will create a new directory containing the original Python bytecode files and packaged resources:
1 2 3 4 5 6 7 8 >> python pyinstxtractor.py shellcode_loader.exe [+] Processing shellcode_loader.exe [+] Pyinstaller version: 2.1+ [+] Python version: 307 ... [+] Possible entry point: shellcode_loader.pyc [+] Found 179 files in PYZ archive [+] Successfully extracted pyinstaller archive: shellcode_loader.exe
Extracted files located in the newly created “shellcode_loader.exe_extracted” directory.
Now that we have the bytecode (
.pyc) version of our source, we can use uncompyle6 to convert our
shellcode_loader.pyc back to human readable code:
Converting .pyc files back to .py with uncompyle6.
After successfully reversing the shellcode loader script, I wanted to dig deeper and explore PyInstaller’s bytecode obfuscation with AES encryption. This can be implemented by adding the
--key argument during compilation, as shown below:
1 pyinstaller -F --key MySecretKey12345 shellcode_loader.py
When going back and unpacking the newly created executable, several errors were displayed that indicated encryption may be applied:
Unpacking the encrypted executable with pyinstxtractor.
However, it was still possible to convert the
shellcode_loader.pyc file back to its original source — without any decryption methods applied:
Code snippet from original shellcode_loader.py file
Reviewing the terminal messages and unpacked files, it appeared only resource files were encrypted and placed in the
PYZ-00.pyz_extracted directory. This means the scripts entry-point and primary file was NOT protected.
Given only script resources are encrypted, I restructured the shellcode loader script to import the primary code as a resource. The final directory structure looked something like:
1 2 3 |_shellcode_loader.py |_scloader |_ __init__.py
Once recompiling and unpacking, the same encryption error messages were shown. However, this time, I was unable to recover the source:
That’s when I found the
pyimod00_crypto_key.pyc file in the unpacked directory, which contained the static key used to decrypt the executable at runtime:
PyInstaller’s encryption key found in clear-text at “pyimod00_crypto_key.pyc”.
Using the script below, this key can be leveraged to decrypt the Python bytecode and recover the original file before using uncompyle6:
Reversing Python executables without additional protection mechanisms can be trivial. In fact, PyInstaller’s documentation even mentions their AES256 encryption only prevents “casual” tampering. This is just one reason Python malware is not more common in modern enterprise environments.