Unpacking Pyinstaller Packed Python Malware

I do not consider these next steps complex but I still wanted to document it because I didn't really find much online except a few Stack Overflow comments here and there and I had it in my draft posts for a few days. So this is my effort to consolidate some information for others to use.

So what's Pyinstaller? According to their website, "PyInstaller is a program that converts (packages) Python programs into stand-alone executables, under Windows, Linux, Mac OS X, Solaris and AIX."

Simply put for those running Windows it takes your .py file and turns it into an EXE file so you can run Python files on your system without needing Python installed.

Unfortunately (or fortunately for job security purposes) malware takes advantage of Python's ease of use and it's ability to be packaged into an exe easily and then subsequently used to pwn people and organizations.

The sample I will look at now is: MD5 (test.exe) = 43adebf0983a5fc74d5b696bcbfd5f93, which isn't anything special. It just happened to be a sample that triggered on one of my VT Yara rules so I decided to take a look at it.

First you will need a couple tools for this little demo.

archive_viewer.py - This will allow you to "expand" the .exe file and see into it similar to how you would with an archive tool to look into a .zip, .7z, etc.

pyinstxtractor.py - This one works more often than archive_viewer.py, so it's good to have a couple to choose from.

I haven't run into any pieces of malware where either one of these didn't do their job. I'm sure there are samples, but I haven't found them yet. Please let me know if you do.

So now, let's take a look.

python pyinstxtractor.py test.exe

Successfully extracted Pyinstaller archive : test.exe

Now in the directory where you ran pyinstxtractor you should have some files. Let's ignore all of them except "test". Open it up in a text editor.

It looks like a blob of junk, yeah? Something similar to this. I've removed some code to conserve space.

from Crypto.Cipher import AES as DvMfw;from datetime import date;from base64 import b64decode as fBPSR;import struct, socket, binascii, ctypes, random, time;from datetime import datetime
exec(fBPSR("ZXhlYyhEdk=="))

Let's make it a bit more readable:

from Crypto.Cipher import AES as DvMfw;
from datetime import date;
from base64 import b64decode as fBPSR;
import struct, socket, binascii, ctypes, random, time;
from datetime import datetime
exec(fBPSR("ZXhlYyhEdk=="))

So we can see that fBPSR is actually base64decode, which makes since considering there is a big base64 blob in the code. Let's clean it up a bit more and then run it.

import struct, socket, binascii, ctypes, random, time
from Crypto.Cipher import AES as DvMfw
from datetime import date
from base64 import b64decode
from datetime import datetime
print(b64decode("ZXhlYyhEdk=="))

Now we have some more obfuscated code, but we are getting somewhere.

exec(DvMfw.new("@ZKWogab)$o8Gi1)cA6zo3(.P244jRdn").decrypt(fBPSR("oBpFHK")).rstrip('{'))

Let's fix it up again. I removed some of the base64 blob to preserve space.

from Crypto.Cipher import AES
from datetime import date
from base64 import b64decode
import struct, socket, binascii, ctypes, random, time
from datetime import datetime
print(AES.new("@ZKWogab)$o8Gi1)cA6zo3(.P244jRdn").decrypt(b64decode("oBpFHK")).rstrip('{'))

And now you have some readable code that you can continue to analyze. I actually watched this person upload this code to VT about 10 times. Ultimately they set the IP to some no-ip domain, but it appeared down/not responding when I attempted to infect my lab machine(s).

Silvrback blog image

So yeah, in case you're curious how this works that's pretty much it. It's pretty simple. This is of course if the attackers aren't using some custom packaging tool and they are relying on stock pyinstaller to build their EXEs.

If you find these posts useful and educational you can donate via PayPal.Me here: $1, $2, $3, $4, $5 or Custom.

Enjoy!