██████╗ ██╗ █████╗ ███╗ ██╗██╗ ██╗ ██████╗ ██████╗ ███████╗
██╔══██╗██║ ██╔══██╗████╗ ██║██║ ██╔╝██╔═══██╗██╔══██╗██╔════╝
██████╔╝██║ ███████║██╔██╗ ██║█████╔╝ ██║ ██║██████╔╝█████╗
██╔══██╗██║ ██╔══██║██║╚██╗██║██╔═██╗ ██║ ██║██╔══██╗██╔══╝
██████╔╝███████╗██║ ██║██║ ╚████║██║ ██╗╚██████╔╝██████╔╝██║
╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ v3
A complete AST-compiler rewrite of BlankOBF v2 — engineered for provably-correct semantic preservation, multi-strategy transformation depth, and professional code hardening.
BlankOBF v3 is not an upgrade — it is a ground-up architectural reinvention.
Where v2 relied on fragile regex-based string manipulation and layer-stacking with zlib/base64 wrappers, v3 operates directly on Python's Abstract Syntax Tree (AST) — the same internal representation the interpreter itself uses before execution.
The result: transformations that are structurally sound, impossible to accidentally corrupt, and effective against both automated and manual analysis — regardless of how complex your source code is.
Semantic preservation is not a goal. It is a hard invariant, enforced by two independent
compile()checkpoints on every single run.
| Capability | v2 | v3 |
|---|---|---|
| Transformation layer | String/regex + zlib wrappers | AST compiler |
| Semantic correctness guarantee | ✗ | ✓ |
match/case statement safety |
✗ | ✓ |
| f-string internal safety | ✗ | ✓ |
| Type annotation safety | ✗ | ✓ |
__future__ import safety |
✗ | ✓ |
__all__ export protection |
✗ | ✓ |
Deterministic output (--seed) |
✗ | ✓ |
| Integer encoding strategies | 2 | 5 |
| String encoding strategies | 1 (reversed bytes) | 2 + splitting |
| Inner function / class renaming | ✗ | ✓ |
| Dead code & opaque predicates | ✗ | ✓ |
Double compile() validation |
✗ | ✓ |
Transform statistics (--profile) |
✗ | ✓ |
| Self-transformation safe | ✗ | ✓ |
Input Source Code
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ │
│ Phase 1 ── Input Validation ast.parse + compile() │
│ Phase 2 ── AST Enrichment parent refs + depth │
│ Phase 3 ── __all__ Extraction public API protection │
│ Phase 4 ── Scope Analysis safe locals + imports │
│ Phase 5 ── Injection Building decoders + noise vars │
│ Phase 6 ── Safe Insertion after ALL imports │
│ Phase 7 ── Re-enrichment rebuild parent graph │
│ Phase 8 ── Transform Engine T1–T11 composable │
│ Phase 9 ── Dead Code Injection opaque predicates │
│ Phase 10 ── Unparse + Validate compile() gate ① │
│ Phase 11 ── Post-processing comment noise │
│ Phase 12 ── Final Validation compile() gate ② │
│ │
└─────────────────────────────────────────────────────────────────┘
│
▼
Obfuscated Output ✓ (runtime-identical to source)
Phases 10 and 12 are independent hard abort gates. If either compile() call fails, the process stops immediately and nothing is written. It is architecturally impossible to silently produce broken output.
T1 — Local Variable & Inner Function Renaming
All local variables, inner functions, and inner class definitions are renamed using SHA-256 deterministic hashing. Five different name prefixes (_, _v, _x, _q, _z) are selected per-identifier based on the hash value, eliminating the uniform mass-rename signature that most obfuscators produce.
Scope analysis ensures only truly safe locals are touched. The following are never renamed: global/nonlocal names, function arguments, import bindings, __all__ exports, dunder names, and any function whose body contains eval(), locals(), or similar introspection calls.
# Before
def process(data):
result = []
count = 0
for item in data:
count += 1
result.append(item * 2)
return result
# After
def process(data):
_x3f9a1c2d = []
_va4b72e1f = 0
for item in data:
_va4b72e1f += 1
_x3f9a1c2d.append(item * 2)
return _x3f9a1c2dT2 — Builtin Aliasing
All referenced builtins (print, len, range, isinstance, type, etc.) are aliased through __import__('builtins') with hashed names, injected once at module level. Every call site in the source is then replaced with the alias.
# Injected at module top
_m7a2f3b1e = __import__('builtins')
_b4c9d2a1 = _m7a2f3b1e.print
_b1f8e3c7 = _m7a2f3b1e.len
# All call sites replaced
_b4c9d2a1("hello") # was: print("hello")
_b1f8e3c7(items) # was: len(items)T3 & T10 — Integer Masking (5 Strategies)
Every integer literal is replaced by one of five obfuscation expressions, selected randomly per constant. Strategy selection is seeded, ensuring full determinism.
| ID | Strategy | Form | Example for 42 |
|---|---|---|---|
| A | XOR masking | (val ^ k1 ^ k2) ^ k1 ^ k2 |
(0x9A3F ^ 0xF24B ^ 0x3C1E) ^ 0xF24B ^ 0x3C1E |
| B | Additive split | (val + offset) - offset |
52337 - 52295 |
| C | Multiply/divide | (val * n) // n |
546 // 13 |
| D | Bit shift | (val << n) >> n |
672 >> 4 |
| E (T10) | Deep composite | Two strategies + outer wrapper | ((val ^ k1) + n) - n |
T4 & T11 — String Encoding (2 Strategies + Splitting)
Two independent decoder functions are injected at module level. Each string is assigned a strategy at random.
Strategy 1 — Rotating-key XOR byte array:
# Decoder injected once at module level
def _d3a9f1b2c4(_d, _k):
return bytes([(_b ^ (_k + _i) % 256) for _i, _b in enumerate(_d)]).decode()
# Each string becomes a call with a unique key
_d3a9f1b2c4([142, 60, 118, 113, 109], 87) # → "hello"Strategy 2 — Base85 + rotating XOR:
# Second decoder, different hashed name
def _e7f2a4c1d8(_s, _k):
_r = __import__('base64').b85decode(_s)
return bytes([(_b ^ (_k + _i) % 256) for _i, _b in enumerate(_r)]).decode()
# Compact representation for longer strings
_e7f2a4c1d8('Gz7!z', 43) # → "hello"T11 — String Splitting: Strings longer than --split-threshold (default: 8) are split into separately-encoded chunks joined with +, each with its own strategy and key:
# was: "hello world from python"
_d3a9f1b2c4([...], 87) + _e7f2a4c1d8('...', 43) + _d3a9f1b2c4([...], 12)T5 — Boolean Rewriting
True → not False
False → not TrueT6 — Comparison Inversion (40% probability)
a == b → not (a != b)
a != b → not (a == b)Applied stochastically at 40% probability per site, preserving unpredictability.
T7 — Double Negation (40% probability)
if condition: → if not (not condition):
while condition: → while not (not condition):T8 — Docstring Stripping
Function and class docstrings are replaced with pass. Module-level docstrings are deliberately preserved — stripping them would break __future__ import positioning requirements.
T9 — Opaque Predicates & Dead Branches
Dead control flow is injected directly inside function bodies. Three forms of opaque predicates are used, selected randomly — each involves a runtime call that cannot be trivially resolved by static analysis:
# Form A — id(0) >= 0 is always True (runtime call)
if id(0) >= 0:
_z482910 = type('', (), {})
# Form B — isinstance([], list) is always True (runtime call)
if isinstance([], list):
_z917364 = type('', (), {})
# Form C — len(()) == 0 is always True (runtime call)
if len(()) == 0:
_z203847 = type('', (), {})
# Dead branch — (k ^ k) != 0 is always False, body never runs
if (4821 ^ 4821) != 0:
_z591028 = None
# Dead loop — (0 ^ 0) > 1 is always False, never iterates
while (0 ^ 0) > 1:
breakAll injected nodes are flagged _injected=True and are entirely skipped by all subsequent transform passes — they are never re-processed.
The following constructs are never modified, enforced at the AST level via parent-chain analysis on every node:
match/case patterns protected via _MATCH_PATTERN_TYPES parent-chain walk
f-string internals protected via JoinedStr ancestry detection
type annotations protected on arg.annotation, AnnAssign, returns
decorator expressions protected via decorator_list membership check
__future__ imports always remain as first module-level statement
__all__ exports extracted at phase 3, excluded from all renaming
injected nodes flagged _injected=True, skipped on every re-visit
global / nonlocal vars excluded from scope renaming entirely
function arguments never renamed (would break external callers)
dunder names excluded at every level (__init__, __str__, etc.)
dynamic scopes full local renaming disabled when eval/locals detected
module-level variables not renamed (public API surface safety)
- Python 3.9+ — no external dependencies, stdlib only
- Single file:
BlankOBFv3.py— download and run, nothing else needed
git clone https://github.com/BenzoXdev/BlankOBF-V3.git
cd BlankOBF-V3
python BlankOBFv3.py --help# Basic — outputs obf_script.py in the same directory
python BlankOBFv3.py script.py
# Specify output path
python BlankOBFv3.py script.py -o hardened.py
# Deterministic output — same seed always produces byte-identical result
python BlankOBFv3.py script.py -o hardened.py --seed 1337
# Validate that the output compiles cleanly, without writing anything
python BlankOBFv3.py script.py --check
# Print obfuscated result directly to stdout
python BlankOBFv3.py script.py --dry-run
# Show per-transform statistics after the run
python BlankOBFv3.py script.py --profile
# Aggressive string splitting — split all strings regardless of length
python BlankOBFv3.py script.py --split-threshold 0
# Disable splitting entirely
python BlankOBFv3.py script.py --split-threshold 999
# Minimal mode — integers and booleans only, no encoding or dead code
python BlankOBFv3.py script.py --no-strings --no-builtins --no-dead-code --no-comments| Flag | Description | Default |
|---|---|---|
input |
Python source file to transform | (required) |
-o, --output PATH |
Output file path | obf_<input>.py |
--seed N |
Deterministic seed — same seed → byte-identical output | random |
--check |
Validate output only, write nothing | — |
--dry-run |
Print result to stdout, do not write | — |
--verbose |
Enable per-phase debug logging | — |
--profile |
Display per-transform statistics after run | — |
--split-threshold N |
Minimum string length before splitting into chunks | 8 |
--no-strings |
Disable all string encoding (T4 + T11) | — |
--no-builtins |
Disable builtin aliasing (T2) | — |
--no-bool-rewrite |
Disable boolean/comparison transforms (T5, T6, T7) | — |
--no-dead-code |
Disable opaque predicates and dead branch injection (T9) | — |
--no-comments |
Disable fake engineering comment noise | — |
--no-strip-docs |
Keep function and class docstrings intact (T8) | — |
Running with --profile displays a full per-transform breakdown after the run:
[INFO] Processing script.py (seed=1337)
[INFO] Transformation successful ✓
[INFO] ── Profile ──
[INFO] booleans_rewritten 12
[INFO] comparisons_inverted 7
[INFO] dead_branches 9
[INFO] docstrings_stripped 4
[INFO] functions_renamed 6
[INFO] integers_masked 83
[INFO] opaque_predicates 11
[INFO] strings_encoded 31
[INFO] strings_split 8
[INFO] variables_renamed 47
[INFO] Written to obf_script.py (18 432 bytes)
| Limitation | Detail |
|---|---|
| Dynamic introspection | Functions using eval(), locals(), vars(), getattr() etc. have all local renaming disabled — names left intact |
| f-string string literals | String constants inside f-expressions are not encoded — ast.unparse cannot reconstruct them after transformation |
| Function arguments | Never renamed — would silently break any external caller using keyword arguments |
sys._getframe() |
Will see hashed names instead of original local names |
| Complex number literals | 10j literals pass through unchanged |
| Bytes literals | b"..." literals are not currently encoded |
BlankOBFv3.py (single file, ~900 lines)
│
├── _h() Deterministic SHA-256 name generator
│
├── ASTEnricher Phase 2 — assigns .parent + ._depth to all nodes
├── ScopeAnalyzer Phase 4 — safe locals, builtins, __all__ guard
├── InjectionBuilder Phase 5 — decoder functions, aliases, noise vars
├── TransformEngine Phase 8 — T1–T11 NodeTransformer with stats
├── DeadCodeInjector Phase 9 — opaque predicates, dead branches
├── PostProcessor Phase 11 — text-level comment noise injection
└── Pipeline Orchestrates all 12 phases + stats aggregation
- BlankOBF v2 — original concept and implementation by Blank-c
- BlankOBF v3 — complete AST-compiler rewrite, architecture, and engineering
BlankOBF v3 — because your code deserves better than regex.
◈