keii

Analysis of PyArmor-Obfuscated Python Malware Without Deobfuscating the Source - ITSEC CTF 2025, Operation: Baby-Step

On August 9-10, 2025, I participated as a finalist in the ITSEC CTF 2025 competition. During the final stage, I encountered something intriguing: a Python malware obfuscated with PyArmor. I quickly identified it after unpacking it using pyinstxtractor and decompiling the .pyc file with PyLingual.

PyLingual

Why was this interesting? I struggled to deobfuscate it using various methods suggested online, such as scripts and tools like Process Hacker to inject the pyinjector DLL.

Then, an idea struck me: what if I simply ran the malware, allowed PyArmor to load its obfuscated code, and dumped its memory?

But first, how does PyArmor work, and why would this approach succeed?

PyArmor Scheme

PyArmor operates as a layered protection system for Python code, combining code encryption with license-based execution control. The process begins in the obfuscation stage, where the PyArmor CLI works with its obfuscation engine to transform plain Python bytecode into an encrypted form. This encrypted code is wrapped with a stub loader, and a native binary module called pytransform is embedded to handle decryption and validation tasks at runtime. The encryption key itself is never stored in plain form; instead, it is protected and tied to a license mechanism. In the licensing stage, the license manager can generate machine-specific keys based on hardware identifiers, set expiration dates, and enforce other restrictions such as Python version compatibility or domain binding.

For someone like me, who doesn’t specialize in reverse engineering, this complexity was intimidating. However, since PyArmor validates its policies before executing the script, it means that every process in memory can be dumped.

To dump this memory, we can use Task Manager. First, open a handle to the process with permissions like PROCESS_QUERY_INFORMATION and PROCESS_VM_READ. Then, resolve the output path (typically your %TEMP%) and prepare dump options. Task Manager produces a full user-mode dump by calling the Windows dump writer (commonly via DbgHelp.dll’s MiniDumpWriteDump). This process walks through the threads, modules, and memory regions of the process and streams the snapshot to a .dmp file. The kernel/filesystem layer persists the file. On success, Task Manager shows the file location, while failures occur if access is denied, the process disappears, or disk space is insufficient.

dmp-process

I proceeded by dumping the memory from Task Manager after running the malware in a safe environment (my Windows VM).

dump memory

This process results in a .DMP file that can be analyzed further.

dmp-file

The dump contains all the processes occurring in memory, potentially leaking critical information.

For example, one of the challenges required identifying the bot token used to interact with a Discord Bot. This indicated that the malware interacted with a Discord server, allowing us to narrow our search. Based on this logic, we knew it would interact with Discord API endpoints such as gateway.discord.gg. Using HxD, we examined the dumped file and searched for this specific keyword.

discord-host

From this, we discovered that the malware interacted with a webhook using the discord.py bot module, identifying it as the user agent. Further investigation revealed the leaked Discord bot token from the dumped memory.

bot-token

In addition to leaked values, the memory dump also revealed function and variable names used in the source code. However, it did not provide access to the actual source code.

func

Upon further analysis, I attempted to decompile the dumped memory using Ghidra. From this, we discovered that the source code also persisted in memory, allowing us to extract it.

ghidra-dmp

Take a look at the screenshot. While it still appears scrambled, we can observe the program interacting with the Discord bot server through the printed addresses (or possibly request details). This opens up opportunities for further research to examine the behavior of the dumped memory in greater detail.