Baike.dev
All toolsAI codingTrendingOpen sourceNewsSubmit
Log in
< Back to tools
P

PyArmor-Unpacker

> 编程语言
Open source

A deobfuscator for PyArmor.

759 stars0 likes0 views
WebsiteGitHub

About

A deobfuscator for PyArmor.

PyArmor-Unpacker

NOTE: This unpacker does not support PyArmor v8+ yet

How to use it

There are 3 different methods for unpacking PyArmor, in the methods folder in this repository you will find all the files needed for each method. Method 3 is the preferred method for Python 3.9. Below you will find a detailed write-up on how I started all the way to the end product. I hope more people actually understand how it works this way rather than just using the tool.

Known issues

This is a list of all the known issues/missing features. I don't have enough time to fix them myself so I heavily rely on contributors.

Issues:

  • Unsafe way of stopping the second marshal.loads trigger (see write-up) Fixed by issue #9
  • Async code objects don't get invoked correctly -> programs like discord bots can't be unpacked Fixed by issue #19
  • From Python 3.10 and higher the absolute jump indexes have been divided by 2 to save storage, we have to add support for that. Fixed by issue #3

Missing features:

  • Unit tests
  • Static unpacking for versions below 3.9.7
  • Multi file support Fixed by issue #11
  • Better logging
  • Better prevention of accidentally executing the program for method #3 Fixed by issue #9

How to use

IMPORTANT: USE THE SAME PYTHON VERSION EVERYWHERE, LOOK AT WHAT THE PROGRAM YOU ARE UNPACKING IS COMPILED WITH. If you don't, you will face issues.

Method #1

  1. Copy all the files from the method #1 directory into the same directory as the file you want to unpack.
  2. Run the file you want to unpack
  3. Use an injector (I recommend Process Hacker 2) to inject https://github.com/call-042PE/PyInjector (Choose the x64 or x86 version based on your application)
  4. Run the method_1.py file
  5. Now you can run the partially unpacked program using run.py

Method #2

  1. Copy all the files from the method #2 directory into the same directory as the file you want to unpack.
  2. Run the file you want to unpack
  3. Use an injector (I recommend Process Hacker 2) to inject https://github.com/call-042PE/PyInjector (Choose the x64 or x86 version based on your application)
  4. In the dumps directory you can find the fully unpacked .pyc file.
  5. (Optional) Use a decompiler to get the Python source back, since currently 3.9.7 is the minimum version you will have to use: https://github.com/zrax/pycdc

Method #3

NOTE: Don't use the static unpacker for anything below version 3.9.7, The marshal.loads audit log was only added in and after 3.9.7. Any contributors are welcome to add support

  1. Copy all the files from the method #3 directory into the same directory as the file you want to unpack.
  2. In the terminal run this: python3 bypass.py filename.pyc (replace filename.pyc with the actual filename, obviously)
  3. In the dumps directory you can find the fully unpacked .pyc file.
  4. (Optional) Use a decompiler to get the Python source back, since currently 3.9.7 is the minimum version you will have to use: https://github.com/zrax/pycdc

Contribute

Contributions are really important. I don't have enough time to fix all the issues listed above. Please contribute if you can.

If you like you can also sponsor me on Github: https://github.com/sponsors/Svenskithesource/

Write-Up

This is the long-awaited write-up about the full process I went through to deobfuscate or rather unpack PyArmor, I will go through all the research I did and at the end give 3 methods for unpacking PyArmor, they are all unique and applicable in different situations. I want to mention I didn’t know a lot about Python internals so it took a lot longer for me than for other people with more experience in Python internals.

PyArmor has a very extensive documentation on how they do everything, I would recommend you read that fully. PyArmor essentially loops through every code object and encrypts it. There is a fixed header and footer though. This depends on if the “wrap mode” is enabled, it is by default.

wrap header:

        LOAD_GLOBALS    N (__armor_enter__)     N = length of co_consts
        CALL_FUNCTION   0
        POP_TOP
        SETUP_FINALLY   X (jump to wrap footer) X = size of original byte code

changed original byte code:

        Increase oparg of each absolute jump instruction by the size of wrap header

        Obfuscate original byte code

        ...

wrap footer:

        LOAD_GLOBALS    N + 1 (__armor_exit__)
        CALL_FUNCTION   0
        POP_TOP
        END_FINALLY

From the PyArmor docs

In the header there is a call to the __armor_enter__ function, which will decrypt the code object in memory. After the code object has finished the __armor_exit__ function will be called which will re-encrypt the code object again so no decrypted code objects get left behind in memory.

When we compile a PyArmor script we can see there is the entry point file, and a pytransform folder. This folder contains a dll and an __init__.py file.

dist
│ test.py
└───pytransform
    | _pytransform.dll
    | __init__.py

The __init__.py file doesn’t have to do much with decrypting the code objects. It is mostly used so that we can import the module. It does some checks like what OS you're using, if you’d like to read it, it’s open source so you can just open it like a normal Python script.

The most important thing it does is loading the _pytransform.dll and exposing its functions to the Python interpreter’s globals. In all the scripts we can see that from pytransform it imports pyarmor_runtime.

from pytransform import pyarmor_runtime
pyarmor_runtime()
__pyarmor__(__name__, __file__, b'\x50\x59\x41\\x5...')

This function will create all the functions necessary to run PyArmor scripts, like the __armor_enter__ and __armor_exit__ function.

The first resource I found was this thread on the forum tuts4you, here the user extremecoders wrote a few posts on how he unpacked the PyArmor protected file. He edited the CPython source code to dump the marshal of every code object that gets executed.

While this method is great for exposing all the constants, it’s less ideal if you want to get the bytecode, this is because:

  1. You will have to find the main module code object, since all code objects get dumped, even the ones from modules, etc. it will be more difficult to find the main one.
  2. The code object won’t have been decrypted yet by PyArmor since that only happens when the __armor_enter__ function gets called, which is at the start of the code object. Since the __armor_enter__ function decrypts it in memory it won’t get dumped by CPython.

There are some people that have experimented with dumping the decrypted code objects from memory by injecting Python code.

In this video someone demonstrates how he disassembles all the decrypted functions in memory.

However, he hasn’t found out yet how to dump the main module, only functions. Thankfully he published his code he used to inject Python code. On the GitHub repository we can see that he creates a dll in which he calls an exported function from the Python dll to execute simple Python code. Currently he has only added support for finding the Python dlls for versions 3.7 to 3.9 but you can easily add more versions by modifying the source and recompiling it. He made it so it executes the code found in code.py, this way it’s easy to edit the Python code without having to rebuild the project every time.

In the repository he includes a Python file which will dump all the function’s names to a file with their corresponding address in memory, if there is no memory found it means it hasn’t been called yet so it also hasn’t been decrypted yet.

…

From call-042PE's repository

In the code you can see he added a comment saying the problem he has is that he can’t access the main module code object.

After a lot of Googling I was stumped, unable to find anything about how to get the current running code's object. Sometime later in an unrelated project I saw a function call to sys._getframe(). I did some research on what it does, it gets the current running frame.

You can give an integer as an argument which will walk up the call stack and get the frame at a specific index.

sys._getframe(1) # get the caller's frame

Now the reason that this is important is because a frame in Python is basically just a code object but with more information about its state in memory. To get the code object from a frame we can use the .f_code attribute, you will also be familiar with this if you have created a custom CPython version which dumps the code objects that get executed as we also get the code object from a frame there.

...
1443    tstate->frame = frame;
1444    co = frame->f_code;
...

From my custom CPython version

So now we have figured out how to get the current running code object, we can simply walk up the call stack until we find the main module, which will be decrypted.

Now we’ve pretty much figured out the main idea of how to unpack PyArmor. I’ll now show 3 methods of unpacking that I have personally found useful in different situations.

Method #1

The first one requires you to inject Python code, so you’ll have to run the PyArmor script. When we dump the main code object like I explained above the main problem will be that some functions will still be encrypted, thus the first method invokes the PyArmor runtime function so that all the functions needed to decrypt the code objects are loaded, like __armor_enter__ and __armor_exit__.

This seems like a pretty simple thing to do but PyArmor did think of this, they implemented a restrict mode. You can specify this when compiling a PyArmor script, by default the restrict mode is 1.

I haven’t tested every restrict mode out but it works for the default one.

When we try to run this code in our REPL you will get the following error:

>>> from pytransform import pyarmor_runtime
>>> pyarmor_runtime()
Check bootstrap restrict mode failed

This prevents us from being able to use the __armor_enter__ and __armor_exit__.

So the next step I took was contacting extremecoders on tuts4you. He helped me by mentioning that I could natively patch the _pytransform.dll. I also want to thank him for giving me the solution on doing this solely in Python.

If we open the _pytransform.dll in a native debugger, I chose x64dbg, we will look for all the strings in the current module.

If we filter this now by searching for "bootstrap", we will get the following.

When we watch the disassembly on the first search result you see that there is a reference of _errno indicating that there could be some error raised, a few lines below that we can see the error that we get in Python.

When we just NOP everything from the point of the jump that jumps over the code that triggers the error to the return there is no way that the error could be raised.

Now if we save this and replace the `_pyt

Issues· 0 open

View all issuesOpen on GitHub

No open issues yet, or sync has not completed.

> Tags

Pythondeobfuscatorpyarmorpythonreverse-engineering

No comments yet. Be the first to share.

> Details

PublishedAug 1, 2026
UpdatedSep 17, 2026
Category编程语言
PricingOpen source

> Related tools

T
TypeScript
JavaScript 的超集,为前端与全栈提供静态类型
P
Python
通用编程语言,广泛用于 Web、数据与 AI
G
Go
Google 推出的简洁高效系统语言