How to extract values from programs protected with PyArmor
What is PyArmor?
PyArmor is an obfuscation tool that aims to be a drop-in solution for protecting your Python source code while preserving names.
The output after obfuscation will be something similar to what you see below. The original program is encrypted and stored inside the bytes on line 3.
from pytransform import pyarmor_runtime
pyarmor_runtime()
__pyarmor__(__name__, __file__, b'\x50\x59\...', 2)Intro
Here is some sample code from which we will attempt to extract values. It makes a request to example.com with a secret value that we will extract.
import requests
headers = {
"SECRET-API-KEY": "1d03c416ad67b6a9846557aeb8103473"
}
response = requests.get('https://example.com', headers=headers)
print(response.status_code)Obfuscation
$ pyarmor obfuscate main.py
INFO PyArmor Trial Version 7.2.4
INFO Python 3.10.0
INFO Obfuscate 1 scripts OK.
$ cd dist
$ python main.py
200The obfuscated code works as it is, but it requires the user to have the Python runtime, which is not ideal.
Packing
To recreate a real-world situation, we will package the obfuscated program into an executable that contains the Python runtime.
$ pyarmor pack --clean -e "--onefile " main.py
INFO PyArmor Trial Version 7.2.4
INFO Python 3.10.0
INFO Prepare to pack obfuscated scripts with PyInstaller
INFO Final output path: dist
INFO Pack obfuscated scripts successfully.
$ cd dist
$ main.exe
200Now we have a self-contained executable that can be distributed.
Extracting
Now we can start the reversing process. Using pyinstxtractor, a tool to unpack executable files built with pyinstaller, we get a directory with many files and the Python runtime (python310.dll).
$ python pyinstxtractor.py main.exe
[+] Processing main.exe
[+] Pyinstaller version: 2.1+
[+] Python version: 310
[+] Length of package: 7480506 bytes
[+] Found 33 files in CArchive
[+] Beginning extraction...please standby
[+] Possible entry point: pyiboot01_bootstrap.pyc
[+] Possible entry point: pyi_rth_inspect.pyc
[+] Possible entry point: pyi_rth_pkgutil.pyc
[+] Possible entry point: pyi_rth_multiprocessing.pyc
[+] Possible entry point: main.pyc
[+] Found 301 files in PYZ archive
[+] Successfully extracted pyinstaller archive: main.exe
You can now use a python decompiler on the pyc files within the extracted directoryFixing Imports
Our main focus is on the main.pyc file. This file is compiled bytecode of our obfuscated program. Running this by itself, we get a ModuleNotFoundError. To resolve this, we have to make some changes to the structure of the directory.
$ python main.pyc
Traceback (most recent call last):
File "dist\obf\main.py", line 1, in <module>
ModuleNotFoundError: No module named 'pytransform'Located inside the directory, PYZ-00.pyz_extracted, are all the imported modules needed for the main entry point (main.pyc). Most of these modules are pre-installed libraries, but the only ones we need (for this example) are requests and pytransform.pyc. Move these two modules to the parent directory.
$ python main.pyc
200Hooking
Now that the program is running, we can create a hook into main.pyc by replacing the files of the requests library with some of our own modified files.
Looking through the requests module, we can see the ideal file to extract data from is api.py. We will replace api.pyc with api.py, which we have from the original repository. From here, extracting data is a trivial task.
def request(method, url, **kwargs):
print(method,url,kwargs)
with sessions.Session() as session:
return session.request(method=method, url=url, **kwargs)And success!
$ python main.pyc
get https://example.com {'params': None, 'headers': {'SECRET-API-KEY': '1d03c416ad67b6a9846557aeb8103473'}}
200