This repository contains some scripts for statically decrypting Python scripts protected with Pyarmor v8 or higher.
Blog post for further reading: https://cyber.wtf/2025/02/12/unpacking-pyarmor-v8-scripts/
Initial preparation: Go into py312
and run docker build -t pyarmor312 .
. Also create a virtual env where you install pycryptodome
.
- Open up the native Pyarmor module in IDA, find the MD5 key derivation function, adjust
ida_getkey.py
and run it in IDAPython. Adjustdecrypt_gcm.py
with the key you obtained. - Run
python decrypt_gcm.py /path/to/malware/malware.py
- Run
docker run --rm -u $(id -u):$(id -g) -v $(pwd)/analyze_crypted_code.py:/script.py:ro -v /path/to/malware:/data -it pyarmor312 /script.py /data/malware.py.dec
- Run
python decrypt_gcm.py /path/to/malware/malware.py.dec
- it will now use the json file generated by the step above to decrypt individual functions and generate adec2
file - Run
docker run --rm -u $(id -u):$(id -g) -v $(pwd)/disassemble.py:/script.py:ro -v /path/to/malware:/data -it pyarmor312 /script.py /data/malware.py.dec2
, which will fully disassemble the Python bytecode
Go to the PyInit export and scroll almost all the way down, until you see a place like this:
Inside that function you'll find the necessary details for ida_getkey.py
.
The Docker image builds a custom Python version that is able to read objects serialized by Pyarmor.
The difference to 'normal' Python is that code objects have an additional string/array at the end. Since the normal unmarshaler doesn't expect this, it runs into an "unknown type" error.
The patch introduces an armor
flag into RFILE
so that we can only apply the changed logic for explicit calls to marshal.load(file: SupportsRead[bytes])
.
Otherwise, Python breaks because it cannot unmarshal its builtin objects.
If you have a protected version that utilizes a different Python version, you need to build that specific version and possibly adjust the patch.