Skip to content

Instantly share code, notes, and snippets.

@mate-h
Last active April 8, 2024 13:49
Show Gist options
  • Star 10 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save mate-h/fc8f21bd3ce9e3ad1737b85f795bab07 to your computer and use it in GitHub Desktop.
Save mate-h/fc8f21bd3ce9e3ad1737b85f795bab07 to your computer and use it in GitHub Desktop.
Reverse Engineering Python executable

Reverse engineering

Obtained binaries from Discord server. The download link: https://drive.google.com/file/d/1xPP9R2VKmJ9jwNY_1xf1sVVHlxZIsLcg

Basic information about binaries. There are two main versions of the program in question: aimful-kucoin.exe and aimful-binance.exe. They are both Windows executables. From the FAQ section of the discord server, the following information is available:

In what language was this bot written?

  • Python.
  1. Extract the contents of the binary
git clone https://github.com/countercept/python-exe-unpacker.git
cd python-exe-unpacker
python pyinstxtractor.py ~/Downloads/Aimful/aimful-kucoin.exe

Install python decompiler called decompyle3 based on uncompyle6:

git clone https://github.com/rocky/python-decompile3.git
cd python-decompile3
pip install -e .
  1. Attempt to decompile the contents of the binary
cd ./aimful-kucoin.exe_extracted/PYZ-00.pyz_extracted
decompyle3 ./kucoin.client.pyc

> ...
> ImportError: Ill-formed bytecode file ./kucoin.client.pyc
> <class 'ValueError'>; bad marshal data (unknown type code)

By searching for the file header "e3000000" on google, we can find this article: https://timonpeng.com/tips-of-pyinstaller-executable-file-decompilation/

  1. Inspect the struct file header bytes
xxd < struct | head -5

Output:

00000000: 420d 0d0a 0000 0000 7079 6930 1001 0000  B.......pyi0....
00000010: e300 0000 0000 0000 0000 0000 0008 0000  ................
00000020: 0040 0000 0073 3800 0000 6400 6401 6402  .@...s8...d.d.d.
00000030: 6403 6404 6405 6406 6407 6708 5a00 6408  d.d.d.d.d.g.Z.d.
00000040: 6409 6c01 5400 6408 640a 6c01 6d02 5a02  d.l.T.d.d.l.m.Z.

Mainly we are interested in the first 16 bytes:

420d 0d0a 0000 0000 7079 6930 1001 0000

You can find that the first byte of the main program is e3, therefore, the contents before e3 in the struct file are filled to the front of the main program file.

cp ./aiumful-kucoin ./aimful-kucoin.pyc

set pyc_file ./aimful-kucoin.pyc

# pad file with extra 4 bytes at the beginning
printf '\x00\x00\x00\x00' > $pyc_file.new
cat $pyc_file >> $pyc_file.new
mv $pyc_file.new $pyc_file
# replace the binary header with the 16 bytes above
printf '\x42\x0d\x0d\x0a\x00\x00\x00\x00\x70\x79\x69\x30\x10\x01\x00\x00' | dd of=$pyc_file bs=1 seek=0 count=16 conv=notrunc
  1. Attempt to decompile the binary
decompyle3 ./aimful-kucoin.pyc

Output:

# decompyle3 version 3.7.6
# Python bytecode 3.7 (3394)
# Decompiled from: Python 3.7.12 (default, Oct  9 2021, 17:28:41)
# [Clang 12.0.0 (clang-1200.0.32.29)]
# Embedded file name: dist\obf\aimful-kucoin.py
# Compiled at: 1995-09-27 10:18:56
# Size of source mod 2**32: 272 bytes
from pytransform import pyarmor_runtime
pyarmor_runtime()
__pyarmor__(__name__, __file__, b'PYARMOR\x00\x00\x03\x07\x00B\r\r\n\x06*\xa0\x01\x00\x00\x00\x00\x01\x00\x00\x00@\x00\x00\x00\x94\xa5\x01\x00\x00\x00\x00\x18>\x11
... binary obfuscated ...

From here we can deduce that the binary is obfuscated with the pytransform library using pyarmor_runtime(). Quick search reveals there are some tools that can de-obfuscate the binary data.

git clone https://github.com/u0pattern/PyArmor-Deobfuscator.git
# copy PyArmorDeobfuscator.py to the working directory
# ...

decompyle3 ./aimful-kucoin.pyc > ./aimful-kucoin-obf.py
pip install uncompyle6
python ./PyArmorDeobfuscator.py -f ./pyimful-kucoin-obf.py -o ./aimful-kucoin.py

Output is:

[-] _pytransform.dll file not found [-]

At this point we need to start looking at the source code of the decompiler script. From the comment in the source code we can see that we need the following:

# please make sure you have _pytransform.dll and __init__.py in /dist/pytransform/ directory !!!

https://forum.tuts4you.com/topic/41945-python-pyarmor-my-protector/?tab=comments#comment-203008 From another article online, I found that using the extracted files, we can create the following directory structure:

.
|-- some-python-bytecode.pyc
`-- pytransform
    |-- __init__.py
    |-- _pytransform.dll
    |-- _pytransform.dylib

1 directory, 5 files

The DLL was already included in the directory. For running on OSX I downloaded the pytransform library and placed it in the same directory as the script. https://pyarmor.dashingsoft.com/platforms.html

The contents of __init__.py are:

from pytransform import pyarmor_runtime
pyarmor_runtime('/path/to/runtime')

as per docs at https://pyarmor.readthedocs.io/en/latest/understand-obfuscated-scripts.html

  1. Another attempt
python ./PyArmorDeobfuscator.py -f ./pyimful-kucoin-obf.py -o ./aimful-kucoin.py

Output is:

ImportError: File name: './aimful-kucoin-obf.pyc' doesn't exist

Rename aimful-kucoin.pyc to aimful-kucoin-obf.pyc and re-run the script. The output is the same as the previous one, with a slight difference because this time we are using uncompyle6 to decompile the file.

# uncompyle6 version 3.7.4
# Python bytecode 3.7 (3394)
# Decompiled from: Python 3.7.12 (default, Oct  9 2021, 17:28:41) 
# [Clang 12.0.0 (clang-1200.0.32.29)]
# Embedded file name: dist\obf\aimful-kucoin.py
# Compiled at: 1995-09-27 10:18:56
# Size of source mod 2**32: 272 bytes
from pytransform import pyarmor_runtime
pyarmor_runtime()
__pyarmor__(__name__, __file__, b'PYARMOR\x00\x00\x03\x07\x00B\r\r\n\x06*\xa0\x01\x00\x00\x00\x00\x01\x00\x00\x00@\x00\x00\x00\x94\xa5\x01\x00\x00\x00\x00\x18>\x11\xb8\n\x00\x0
... binary obfuscated ...
@mate-h
Copy link
Author

mate-h commented Oct 11, 2021

@mate-h
Copy link
Author

mate-h commented Oct 11, 2021

__init__.pyc is the same file as PYZ-00.pyz_extracted/pytransform.pyc after renaming it. It's possible to further decompile the pyc to py but that's not strictly necessary.

@mate-h
Copy link
Author

mate-h commented Oct 12, 2021

Compile Python 3.7 from source. Modify the _PyEval_EvalFrameDefault function such that it dumps the code object to disk. By doing so we do not need to bother about all the anti-debugging and encrypted stuff. This is because pyarmor decrypts the code object in memory before it hands it to the Python VM for execution.

https://github.com/Svenskithesource/cpython/commits/main

@oswinkil-git
Copy link

Compile Python 3.7 from source. Modify the _PyEval_EvalFrameDefault function such that it dumps the code object to disk. By doing so we do not need to bother about all the anti-debugging and encrypted stuff. This is because pyarmor decrypts the code object in memory before it hands it to the Python VM for execution.

https://github.com/Svenskithesource/cpython/commits/main

Do we have to modify the _PyEval_EvalFrameDefault function or can we use the built-in python dumper that they showcase in the first few lines of the cpython readme?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment