{"id":13570273,"url":"https://github.com/Svenskithesource/PyArmor-Unpacker","last_synced_at":"2025-04-04T06:32:13.338Z","repository":{"id":57149687,"uuid":"514646939","full_name":"Svenskithesource/PyArmor-Unpacker","owner":"Svenskithesource","description":"A deobfuscator for PyArmor.","archived":false,"fork":false,"pushed_at":"2025-01-05T18:48:34.000Z","size":143,"stargazers_count":561,"open_issues_count":12,"forks_count":78,"subscribers_count":9,"default_branch":"main","last_synced_at":"2025-01-05T19:33:10.781Z","etag":null,"topics":["deobfuscator","pyarmor","python","reverse-engineering"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Svenskithesource.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2022-07-16T17:44:56.000Z","updated_at":"2025-01-05T18:48:38.000Z","dependencies_parsed_at":"2024-01-14T06:49:23.704Z","dependency_job_id":"8884eb92-2ab5-44b9-ae56-e754bad17cfa","html_url":"https://github.com/Svenskithesource/PyArmor-Unpacker","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Svenskithesource%2FPyArmor-Unpacker","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Svenskithesource%2FPyArmor-Unpacker/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Svenskithesource%2FPyArmor-Unpacker/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Svenskithesource%2FPyArmor-Unpacker/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Svenskithesource","download_url":"https://codeload.github.com/Svenskithesource/PyArmor-Unpacker/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247134916,"owners_count":20889410,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["deobfuscator","pyarmor","python","reverse-engineering"],"created_at":"2024-08-01T14:00:50.449Z","updated_at":"2025-04-04T06:32:08.328Z","avatar_url":"https://github.com/Svenskithesource.png","language":"Python","funding_links":[],"categories":["Deobfuscators","Python"],"sub_categories":["Manual analysis"],"readme":"# PyArmor-Unpacker\n## NOTE: This unpacker does not support PyArmor v8+ yet\nI decided it was time that there was a proper PyArmor unpacker released. All the ones that currently are public are either outdated, not working at all or only giving partial output. I plan on making this one support the latest version of PyArmor.\n\nPlease star the repository if you found it helpful. I'd really appreciate it.\n\n## How to use it\nThere are 3 different methods for unpacking PyArmor, in the methods folder in this repository you will find all the files needed for each method. 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.\n\n## Known issues\nThis is a list of all the known issues/missing features.\nI don't have enough time to fix them myself so I am heavily relying on contributors.\n\nIssues:\n- ~Unsafe way of stopping the second marshal.loads trigger (see write-up)~ Fixed by [issue #9](https://github.com/Svenskithesource/PyArmor-Unpacker/issues/9)\n- ~Async code objects don't get invoked correctly -\u003e programs like discord bots can't be unpacked~ Fixed by [issue #19](https://github.com/Svenskithesource/PyArmor-Unpacker/issues/19)\n- ~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](https://github.com/Svenskithesource/PyArmor-Unpacker/issues/3)\n\nMissing features:\n- Unit tests\n- Static unpacking for versions below 3.9.7\n- ~Multi file support~ Fixed by [issue #11](https://github.com/Svenskithesource/PyArmor-Unpacker/issues/11)\n- Better logging\n- ~Better prevention of accidentally executing the program for method #3~ Fixed by [issue #9](https://github.com/Svenskithesource/PyArmor-Unpacker/issues/9)\n\n## How to use\n**IMPORTANT: USE THE SAME PYTHON VERSION EVERYWHERE, LOOK AT WHAT THE PROGRAM YOU ARE UNPACKING IS COMPILED WITH.**\nIf you don't you will face issues.\n\n### Method #1\n1. Copy all the files from the method #1 directory into the same directory as the file you want to unpack.\n2. Run the file you want to unpack\n3. Use an injector (I recommend [Process Hacker 2](https://processhacker.sourceforge.io/)) to inject https://github.com/call-042PE/PyInjector (Choose the x64 or x86 version based on your application)\n4. Run the `method_1.py` file\n5. Now you can run the partially unpacked program using `run.py`\n\n### Method #2\n1. Copy all the files from the method #2 directory into the same directory as the file you want to unpack.\n2. Run the file you want to unpack\n3. Use an injector (I recommend [Process Hacker 2](https://processhacker.sourceforge.io/)) to inject https://github.com/call-042PE/PyInjector (Choose the x64 or x86 version based on your application)\n4. In the `dumps` directory you can find the fully unpacked `.pyc` file.\n5. (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\n\n### Method #3\n**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**\n1. Copy all the files from the method #3 directory into the same directory as the file you want to unpack.\n2. In the terminal run this: `python3 bypass.py filename.pyc` (replace `filename.pyc` with the actual filename, obviously)\n3. In the `dumps` directory you can find the fully unpacked `.pyc` file.\n4. (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\n\n## Contribute\nContributions are really important. I don't have enough time to fix all the issues listed above. Please contribute if you can.\n\nDonations are also really welcome:\u003cbr/\u003e\nBTC - 37rq1xeB5q8ScmMKK3MVmD4RBe5FV7eMmh\u003cbr/\u003e\nETH - 0x28152666867856fa48b3924c185d7e1fb36f3b9a\u003cbr/\u003e\nLTC - MFhdLRdzAqYGZxuvXQfM4RwVGbmrzmdzao\u003cbr/\u003e\n\n## Write-Up\nThis 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.\u003cbr/\u003e\n\nPyArmor has a very extensive [documentation](https://pyarmor.readthedocs.io/en/latest/) 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.\u003cbr/\u003e\n```\nwrap header:\n\n        LOAD_GLOBALS    N (__armor_enter__)     N = length of co_consts\n        CALL_FUNCTION   0\n        POP_TOP\n        SETUP_FINALLY   X (jump to wrap footer) X = size of original byte code\n\nchanged original byte code:\n\n        Increase oparg of each absolute jump instruction by the size of wrap header\n\n        Obfuscate original byte code\n\n        ...\n\nwrap footer:\n\n        LOAD_GLOBALS    N + 1 (__armor_exit__)\n        CALL_FUNCTION   0\n        POP_TOP\n        END_FINALLY\n```\n\u003csup\u003eFrom the [PyArmor docs](https://pyarmor.readthedocs.io/en/latest/how-to-do.html#how-to-obfuscate-python-scripts)\u003c/sup\u003e\u003cbr/\u003e\nIn 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.\n\nWhen 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.\u003cbr/\u003e\n```\ndist\n│ test.py\n└───pytransform\n    | _pytransform.dll\n    | __init__.py\n```\nThe `__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.\nIt 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.\u003cbr/\u003e\nThe 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.\n```py\nfrom pytransform import pyarmor_runtime\npyarmor_runtime()\n__pyarmor__(__name__, __file__, b'\\x50\\x59\\x41\\x5...')\n```\nThis function will create all the functions necessary to run PyArmor scripts, like the `__armor_enter__` and `__armor_exit__` function.\n\nThe first resource I found was [this](https://forum.tuts4you.com/topic/41945-python-pyarmor-my-protector/) 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.\u003cbr/\u003e\nWhile this method is great for exposing all the constants, it’s less ideal if you want to get the bytecode, this is because:\n1. 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.\n2. 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.\n\nThere are some people that have experimented with dumping the decrypted code objects from memory by injecting Python code.\u003cbr/\u003e\nIn [this](https://www.youtube.com/watch?v=P9zhLuKOqT8) video someone demonstrates how he disassembles all the decrypted functions in memory.\u003cbr/\u003e\nHowever, 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](https://github.com/call-042PE/PyInjector) 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.\u003cbr/\u003e\nIn 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. \n```py\n# Copyright holder: https://github.com/call-042PE\n# License: GNU GPL v3.0 (https://github.com/call-042PE/PyInjector/blob/main/LICENSE)\nimport os,sys,inspect,re,dis,json,types\n\nhexaPattern = re.compile(r'\\b0x[0-9A-F]+\\b')\ndef GetAllFunctions(): # get all function in a script\n    functionFile = open(\"dumpedMembers.txt\",\"w+\")\n    members = inspect.getmembers(sys.modules[__name__]) # the code will take all the members in the __main__ module, the main problem is that it can't dump main code function\n    for member in members:\n        match = re.search(hexaPattern,str(member[1]))\n        if(match):\n            functionFile.write(\"{\\\"functionName\\\":\\\"\"+str(member[0])+\"\\\",\\\"functionAddr\\\":\\\"\"+match.group(0)+\"\\\"}\\n\")\n        else:\n            functionFile.write(\"{\\\"functionName\\\":\\\"\"+str(member[0])+\"\\\",\\\"functionAddr\\\":null}\\n\")\n    functionFile.close()\n\nGetAllFunctions()\n```\n\u003csup\u003eFrom [call-042PE's repository](https://github.com/call-042PE/PyInjector/blob/main/src/GetAllFunctions.py)\u003c/sup\u003e\u003cbr/\u003e\nIn 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.\u003cbr/\u003e\nAfter 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](https://docs.python.org/3/library/sys.html#sys._getframe), it gets the current running frame.\u003cbr/\u003e\nYou can give an integer as an argument which will walk up the call stack and get the frame at a specific index.\n```py\nsys._getframe(1) # get the caller's frame\n```\nNow 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.\n```c\n...\n1443    tstate-\u003eframe = frame;\n1444    co = frame-\u003ef_code;\n...\n```\n\u003csup\u003eFrom [my custom CPython version](https://github.com/Svenskithesource/cpython/blob/main/Python/ceval.c#L1443)\u003c/sup\u003e\u003cbr/\u003e\nSo 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.\u003cbr/\u003e\nNow 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.\n\n#### Method #1\nThe 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__`.\u003cbr/\u003e\nThis seems like a pretty simple thing to do but PyArmor did think of this, they implemented [a restrict mode](https://pyarmor.readthedocs.io/en/latest/mode.html?highlight=restrict#restrict-mode). You can specify this when compiling a PyArmor script, by default the restrict mode is 1.\u003cbr/\u003e\nI haven’t tested every restrict mode out but it works for the default one.\u003cbr/\u003e\nWhen we try to run this code in our REPL you will get the following error:\n```py\n\u003e\u003e\u003e from pytransform import pyarmor_runtime\n\u003e\u003e\u003e pyarmor_runtime()\nCheck bootstrap restrict mode failed\n```\nThis prevents us from being able to use the `__armor_enter__` and `__armor_exit__`.\u003cbr/\u003e\nSo the next step I took was contacting [`extremecoders`](https://forum.tuts4you.com/profile/79240-extreme-coders/) 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.\u003cbr/\u003e\nIf we open the `_pytransform.dll` in a native debugger, I chose x64dbg, we will look for all the strings in the current module.\u003cbr/\u003e\n\u003cimg src=\"https://user-images.githubusercontent.com/40274381/179605917-a3f3544f-f8f4-4852-9735-ffdc248d244c.png\" width=\"50%\"/\u003e\u003cbr/\u003e\nIf we filter this now by searching for \"bootstrap\", we will get the following.\u003cbr/\u003e\n\u003cimg src=\"https://user-images.githubusercontent.com/40274381/179606874-c01c4817-793f-4162-9180-db698ae5a607.png\" width=\"50%\"/\u003e\u003cbr/\u003e\nWhen 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.\u003cbr/\u003e\n\u003cimg src=\"https://user-images.githubusercontent.com/40274381/179607650-65743a16-0cea-4460-9679-646757061535.png\"/\u003e\u003cbr/\u003e\nWhen 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.\u003cbr/\u003e\n\u003cimg src=\"https://user-images.githubusercontent.com/40274381/179608107-6a864a9e-b79d-41ea-b1f4-e19c1dd68e84.png\"/\u003e\u003cbr/\u003e\nNow if we save this and replace the `_pytransform.dll` you will see that when we try the same code again the error won't happen and we have access to the `__armor_enter__` and `__armor_exit__` functions.\n```py\n\u003e\u003e\u003e from pytransform import pyarmor_runtime\n\u003e\u003e\u003e pyarmor_runtime()\n\u003e\u003e\u003e __armor_enter__\n\u003cbuilt-in function __armor_enter__\u003e\n\u003e\u003e\u003e __armor_exit__\n\u003cbuilt-in function __armor_exit__\u003e\n```\nNow this is quite tiring if we have to do this for every PyArmor script that we want to unpack, so `extremecoders` made a script that NOPS the specific addresses in memory in Python.\n```py\n# Credit to extremecoders (https://forum.tuts4you.com/profile/79240-extreme-coders/) for writing the script\n# Credit to me for adding the comments explaining it\nimport ctypes\nfrom ctypes.wintypes import *\n\nVirtualProtect = ctypes.windll.kernel32.VirtualProtect\n\nVirtualProtect.argtypes = [LPVOID, ctypes.c_size_t, DWORD, PDWORD]\nVirtualProtect.restype = BOOL\n\n# Load the dll in memory, this is useful because once it's loaded in memory it won't need to get loaded again so all the changes we make will be kept, including the bootstrap bypass\nh_pytransform = ctypes.cdll.LoadLibrary(\"pytransform\\\\_pytransform.dll\")\npytransform_base = h_pytransform._handle # Get the memory address where the dll is loaded\n\nprint(\"[+] _pytransform.dll loaded at\", hex(pytransform_base))\n\n# We got this offset like I showed above with x64dbg, it's the first address where we start the NOP\npatch_offset = 0x70A18F80 - pytransform_base\nnum_nops = 0x70A18FD5 - 0x70A18F80 # Minus the end address, this is the size that the NOP will be. The result will be 0x55\n\noldprotect = DWORD(0)\nPAGE_EXECUTE_READWRITE = DWORD(0x40)\n\nprint(\"[+] Setting memory permissions\")\nVirtualProtect(pytransform_base+patch_offset, num_nops, PAGE_EXECUTE_READWRITE, ctypes.byref(oldprotect))\n\nprint(\"[+] Patching bootstrap restrict mode\")\nctypes.memset(pytransform_base+patch_offset, 0x90, num_nops) # 0x90 is NOP\n\nprint(\"[+] Restoring memory permission\")\nVirtualProtect(pytransform_base+patch_offset, num_nops, oldprotect, ctypes.byref(oldprotect))\n\n\nprint(\"[+] All done! Pyarmor bootstrap restrict mode disabled\")\n```\nIf we put this code a in a file called `restrict_bypass.py` we can use it like the following, using the original `_pytransform.dll`\n```py\n\u003e\u003e\u003e import restrict_bypass\n[+] _pytransform.dll loaded at 0x70a00000\n[+] Setting memory permissions\n[+] Patching bootstrap restrict mode\n[+] Restoring memory permission\n[+] All done! Pyarmor bootstrap restrict mode disabled\n\u003e\u003e\u003e from pytransform import pyarmor_runtime\n\u003e\u003e\u003e pyarmor_runtime()\n\u003e\u003e\u003e __armor_enter__\n\u003cbuilt-in function __armor_enter__\u003e\n\u003e\u003e\u003e __armor_exit__\n\u003cbuilt-in function __armor_exit__\u003e\n```\n\n#### Method #2\nThe second method starts off the same as the first method, we inject the script which gets the current running code object.\u003cbr/\u003e\nOnly now the difference is that we won't just dump it, we will \"fix\" it. By that I mean removing PyArmor from it completely so that we get the original code object.\u003cbr/\u003e\nSince PyArmor has multiple options when obfuscating I decided to add support for all the common ones.\u003cbr/\u003e\nWhen it detects a script has `__armor_enter__` inside it it will modify it so that the code object returns right after the  `__armor_enter__` has been called.\u003cbr/\u003e\nThere is a `POP_TOP` opcode following the function call, this is used so that the return value of the function is removed from the stack, we just replace it with the `RETURN_VALUE` opcode so that we can get the return value of the `__armor_enter__` function and so that we have the decrypted code object in memory without actually running the original bytecode. See the example below\u003cbr/\u003e\n```py\n  1           0 JUMP_ABSOLUTE           18\n              2 NOP\n              4 NOP\n        \u003e\u003e    6 POP_BLOCK\n\n  3           8 \u003c53\u003e\n             10 NOP\n             12 NOP\n             14 NOP\n\n  7          16 JUMP_ABSOLUTE           82\n        \u003e\u003e   18 LOAD_GLOBAL              5 (__armor_enter__)\n             20 CALL_FUNCTION            0\n             22 POP_TOP  # we change this to RETURN_VALUE\n\n  9          24 NOP\n             26 NOP\n             28 NOP\n             30 SETUP_FINALLY           50 (to 82)\n```\nBecause PyArmor edits the code object in memory the changes will stay even after we exit the code object.\u003cbr/\u003e\nNow we can invoke (exec) the code object. We now have access to the decrypted code object. All that's left now is to remove the [PyArmor modifications to the code object](https://pyarmor.readthedocs.io/en/latest/how-to-do.html#how-to-obfuscate-python-scripts), that being the wrap header and footer. \u003cbr/\u003e\nAfter that has been cleaned we have to remove the `__armor_enter__` and `__armor_exit__` from the `co_names`.\u003cbr/\u003e\nWe repeat this recursively for all code objects.\u003cbr/\u003e\nThe output will be the original code object. It'll be like pyarmor was never applied.\u003cbr/\u003e\nBecause of this we can use all our favorite tools, for example [decompyle3](https://github.com/rocky/python-decompile3) to get the original source code.\u003cbr/\u003e\n\n#### Method #3\nThe third method fixes the last issue with method #2.\u003cbr/\u003e\nIn method #2 we still have to actually run the program and inject it.\u003cbr/\u003e\nThis can be an issue because:\u003cbr/\u003e\n- it's malware\n- the program exits instantly because of some anti debugging\n- any other case where you don't have enough time to inject \n\nThe third method attempts to statically unpack PyArmor, with which I mean without running anything of the obfuscated program.\u003cbr/\u003e\nThere are a few ways you could go about statically unpacking it but the method I will explain looks the easiest to implement without having to use other tools and/or languages.\u003cbr/\u003e\nWe will be using [audit logs](https://docs.python.org/3/library/sys.html#sys.audit), audit logs were implemented in Python for security reasons. Now ironically we will be exploiting the audit logs to remove security.\u003cbr/\u003e\nAudit logs essentially log internal CPython functions. Including `exec` and `marshal.loads`, both of which we can use to get the main obfuscated code object without having to inject/run the code. A full list of audit logs can be found [here](https://docs.python.org/3/library/audit_events.html#audit-events)\u003cbr/\u003e\nCPython added something neat called [audit hooks](https://docs.python.org/3/library/sys.html#sys.addaudithook), every time an audit log is triggered it will do a callback to the hook we installed. The hook will simply be a function taking 2 arguments, `event`, `arg`.\u003cbr/\u003e\nExample of an audit hook:\u003cbr/\u003e\n```py\nimport sys\n\ndef hook(event, arg):\n    print(event, arg)\n\nsys.addaudithook(hook)\n```\nThe only way to save code objects to disk is by marshalling it. This means PyArmor has to encrypt the marshalled code objects, so naturally they have to decrypt it when they want to access it in Python.\u003cbr/\u003e\nThey, like most other people, use the built-in marshaller. The package is called `marshal` and it's a built-in package, written in C. It's one of the packages that has audit logs, so when PyArmor calls it we can see the arguments.\u003cbr/\u003e\nThe code object will still have encrypted bytecode, but we already managed to get past the first \"layer\", we can basically re-use our method #2 from this stage as it also has to deal with encrypted code objects. The only difference now is that every code object will be encrypted instead of the ones that would normally already have been ran, like the main code object.\u003cbr/\u003e\nBecause in method #2 we inject the code we already have access to all the PyArmor functions like `__armor_enter__` and `__armor_exit__`. Since we try to unpack it statically we don't have that luxury.\u003cbr/\u003e\nAs I mentioned above PyArmor has restrict modes, I already showed how to bypass the bootstrap restrict mode since that only gets triggered when we run the `pyarmor_runtime()` function.\u003cbr/\u003e\nNow we need to run the whole obfuscated file, which includes the `__pyarmor__` call. That function triggers another restrict mode so we have to bypass that. First I was thinking that we use a similar method by patching it natively.\u003cbr/\u003e\nA friend helped with that, these are the steps you can do to repeat it. Keep in mind I found a better and easier method.\nPyArmor checks if the PYARMOR string is present at a specific memory address in `__main__`. We need to patch this check. See the image below\n\u003cimg src=\"https://media.discordapp.net/attachments/984728907666300948/997534104264396890/unknown.png\"/\u003e\u003cbr/\u003e\nNow the better method I found is that PyArmor's restrict mode doesn't check if the main file is directly ran by Python or if it was invoked, so we can simply do this:\n```py\nexec(open(filename))\n```\nOf course after we installed the audit hook.\u003cbr/\u003e\nThe problem I had was that the audit hook triggered on `marshal.loads`, but obviously after it had triggered I needed to load the code object myself but that would just trigger it again so I added a check to see if the `dumps` directory exists. This is dangerous because if there is still a `dumps` folder left over from before it would just result in executing the protected script without stopping it. We have to find a better way to do that.\u003cbr/\u003e\n\n**EDIT**: I recently discovered that I forgot about the part where we need to edit the absolute jumps. This part will cover that.\u003cbr/\u003e\nWhen need to do this in both method #2 and method #3. When we remove the footer there will be no collions with the indexes.\nWhen we remove the header however it will cause the indexes to shift by the size of the header so we need to loop over all the absolute jumps and subtract the size of the header. That part is fairly easy.\n```py\n    for i in range(0, len(raw_code), 2):\n        opcode = raw_code[i]\n        if opcode == JUMP_ABSOLUTE:\n            argument = calculate_arg(raw_code, i)\n\n            new_arg = argument - (try_start+2)\n            extended_args, new_arg = calculate_extended_args(new_arg)\n            for extended_arg in extended_args:\n                raw_code.insert(i, EXTENDED_ARG)\n                raw_code.insert(i+1, extended_arg)\n                i += 2\n\n            raw_code[i+1] = new_arg\n```\n\u003csup\u003eFrom [method #3]([https://github.com/call-042PE/PyInjector/blob/main/src/GetAllFunctions.py](https://github.com/Svenskithesource/PyArmor-Unpacker/blob/main/methods/method%203/bypass.py#L187))\u003c/sup\u003e\u003cbr/\u003e\nWe loop through the bytecode and check if the opcode is the `JUMP_ABSOLUTE` opcode. If it is we will calculate the argument (keeping the `EXTENDED_ARG` in mind). Then we take the `try_start` which is the size of the header (it's actually the index of the last opcode from the header, that's why we add 2) and subtract it from the argument of the `JUMP_ABSOLUTE` opcode.\u003cbr/\u003e\nThe hardest part of implementing this was taking care of the `EXTENDED_ARG` opcodes that we potentially have to add when the argument goes over the maximum size of 1 byte (255). We handle that in `calculate_extended_args`.\u003cbr/\u003e\n```py\ndef calculate_extended_args(arg: int): # This function will calculate the necessary extended_args needed\n    extended_args = []\n    new_arg = arg\n    if arg \u003e 255:\n        extended_arg = arg \u003e\u003e 8\n        while True:\n            if extended_arg \u003e 255:\n                extended_arg -= 255\n                extended_args.append(255)\n            else:\n                extended_args.append(extended_arg)\n                break\n\n        new_arg = arg % 256\n    return extended_args, new_arg\n```\n\u003csup\u003eFrom [method #3]([https://github.com/call-042PE/PyInjector/blob/main/src/GetAllFunctions.py](https://github.com/Svenskithesource/PyArmor-Unpacker/blob/main/methods/method%203/bypass.py#L107))\u003c/sup\u003e\u003cbr/\u003e\nTo write this code I first had to understand how the `EXTENDED_ARG` worked exactly.\u003cbr/\u003e\n[This article](https://towardsdatascience.com/understanding-python-bytecode-e7edaae8734d) helped a lot for understanding this opcode.\u003cbr/\u003e\nAn instruction in Python is 2 bytes in the more recent versions (3.6+). One byte is used for the opcode and one byte is for the argument. When we need to exceed one byte we use the `EXTENDED_ARG`. It basically works like this:\n```py\narg = 300 # Let's say this is the size of our argument\n```\nWe know the maximum allowed is 255 so we need to use EXTENDED_ARG, you would think it'd be like this:\n```py\nextended_arg = 255\narg = 45\n```\nThat's what I first assumed but after looking at the code that Python generated I noticed it was like this:\n```py\nextended_arg = 1\narg = 44\n```\nI was very confused why it was like that since I saw no correlation between what I expected and the reality.The article linked above explained everything.\u003cbr/\u003e\nPython handles the EXTENDED_ARG like following:\n```py\nextended_arg = extended_arg * 256\n```\nAfter seeing this everything was clear since it would mean that\n```py\nextended_arg = 1 * 256\narg = 44\n\nprint(extended_arg + arg)\n```\nWould output `300`.\u003cbr/\u003e\nI applied that logic to the function so that it returns a list of the necessary EXTENDED_ARG opcodes and the new argument value (which would be under or equal to 255).\u003cbr/\u003e\nI then just insert the EXTENDED_ARG at the correct index's.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FSvenskithesource%2FPyArmor-Unpacker","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FSvenskithesource%2FPyArmor-Unpacker","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FSvenskithesource%2FPyArmor-Unpacker/lists"}