Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,10 @@ test@test:python python_exe_unpack.py -p unpacked/malware_3.exe/archive

If you weren't able to extract the python "original" code following the previous steps, then you can try to **extract** the **assembly** (but i**t isn't very descriptive**, so **try** to extract **again** the original code).In [here](https://bits.theorem.co/protecting-a-python-codebase/) I found a very simple code to **disassemble** the _.pyc_ binary (good luck understanding the code flow). If the _.pyc_ is from python2, use python2:

```bash
<details>
<summary>Disassemble a .pyc</summary>

```python
>>> import dis
>>> import marshal
>>> import struct
Expand Down Expand Up @@ -158,6 +161,19 @@ True
17 RETURN_VALUE
```

</details>

## PyInstaller raw marshal & Pyarmor v9 static unpack workflow

- **Extract embedded marshal blobs**: `pyi-archive_viewer sample.exe` and export raw objects (e.g., a file named `vvs`). PyInstaller stores bare marshal streams that start with `0xe3` (TYPE_CODE with FLAG_REF) instead of full `.pyc` files. Prepend the correct **16-byte `.pyc` header** (magic for the embedded interpreter version + zeroed timestamp/size) so decompilers accept it. For Python 3.11.5 you can grab the magic via `imp.get_magic().hex()` and patch it with `dd`/`printf` before the marshal payload.
- **Decompile with version-aware tools**: `pycdc -c -v 3.11.5 vvs.pyc > vvs.py` or PyLingual. If only partial code is needed, you can walk the AST (e.g., `ast.NodeVisitor`) to pull specific arguments/constants.
- **Parse the Pyarmor v9 header** to recover crypto parameters: signature `PY<license>` at `0x00`, Python major/minor at `0x09/0x0a`, protection type `0x09` when **BCC** is enabled (`0x08` otherwise), ELF start/end offsets at `0x1c/0x38`, and the 12-byte AES-CTR nonce split across `0x24..0x27` and `0x2c..0x33`. The same pattern repeats after the embedded ELF.
- **Account for Pyarmor-modified code objects**: `co_flags` has bit `0x20000000` set and an extra length-prefixed field. Disable CPython `deopt_code()` during parsing to avoid decryption failures.
- **Identify encrypted code regions**: bytecode is wrapped by `LOAD_CONST __pyarmor_enter_*__` … `LOAD_CONST __pyarmor_exit_*__`. Decrypt the enclosed blob with AES-128-CTR using the runtime key (e.g., `273b1b1373cf25e054a61e2cb8a947b8`). Derive the per-region nonce by XORing the payload-specific 12-byte XOR key (from the Pyarmor runtime) with the 12 bytes in the `__pyarmor_exit_*__` marker. After decryption, you may also see `__pyarmor_assert_*__` (encrypted strings) and `__pyarmor_bcc_*__` (compiled dispatch targets).
- **Decrypt Pyarmor “mixed” strings**: constants prefixed with `0x81` are AES-128-CTR encrypted (plaintext uses `0x01`). Use the same key and the runtime-derived string nonce (e.g., `692e767673e95c45a1e6876d`) to recover long string constants.
- **Handle BCC mode**: Pyarmor `--enable-bcc` compiles many functions to a companion ELF and leaves Python stubs that call `__pyarmor_bcc_*__`. Map those constants to ELF symbols with tooling such as `bcc_info.py`, then decompile/analyze the ELF at the reported offsets (e.g., `__pyarmor_bcc_58580__` → `bcc_180` at offset `0x4e70`).


## Python to Executable

To start, we’re going to show you how payloads can be compiled in py2exe and PyInstaller.
Expand Down Expand Up @@ -217,7 +233,7 @@ C:\Users\test\Desktop\test>pyinstaller --onefile hello.py
## References

- [https://blog.f-secure.com/how-to-decompile-any-python-binary/](https://blog.f-secure.com/how-to-decompile-any-python-binary/)

- [VVS Discord Stealer Using Pyarmor for Obfuscation and Detection Evasion](https://unit42.paloaltonetworks.com/vvs-stealer/)

{{#include ../../../banners/hacktricks-training.md}}

Expand Down