Skip to content

BenzoXdev/BlankOBF-V3

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

64 Commits
 
 
 
 
 
 
 
 

Repository files navigation


                      ██████╗ ██╗      █████╗ ███╗   ██╗██╗  ██╗ ██████╗ ██████╗ ███████╗
                      ██╔══██╗██║     ██╔══██╗████╗  ██║██║ ██╔╝██╔═══██╗██╔══██╗██╔════╝
                    ██████╔╝██║     ███████║██╔██╗ ██║█████╔╝ ██║   ██║██████╔╝█████╗
                    ██╔══██╗██║     ██╔══██║██║╚██╗██║██╔═██╗ ██║   ██║██╔══██╗██╔══╝
                 ██████╔╝███████╗██║  ██║██║ ╚████║██║  ██╗╚██████╔╝██████╔╝██║
                     ╚═════╝ ╚══════╝╚═╝  ╚═╝╚═╝  ╚═══╝╚═╝  ╚═╝ ╚═════╝ ╚═════╝ ╚═╝  v3

v3 — Production-Grade AST Code Transformer


Version Python Stdlib Only Deterministic Transforms Pipeline Zero Breakage


A complete AST-compiler rewrite of BlankOBF v2 — engineered for provably-correct semantic preservation, multi-strategy transformation depth, and professional code hardening.



◈ What is BlankOBF v3?

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.



◈ v2 → v3: What Changed

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


◈ 12-Phase Pipeline

  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.



◈ The 11 Transforms (T1–T11)

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 _x3f9a1c2d
T2 — 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
Truenot False
Falsenot True
T6 — Comparison Inversion (40% probability)
a == bnot (a != b)
a != bnot (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:
    break

All injected nodes are flagged _injected=True and are entirely skipped by all subsequent transform passes — they are never re-processed.



◈ Safety Invariants

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)


◈ Requirements & Installation

  • 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


◈ Usage

# 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

Complete CLI Reference

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)


◈ Profile Output

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)


◈ Known Limitations

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


◈ Architecture

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


◈ Credits

  • 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.


About

BlankOBF V3 is a Python obfuscation tool designed to make Python programs harder to reverse-engineer. Built on a 12-phase AST compiler pipeline, it offers multi-strategy transforms, deterministic output, and zero-breakage semantic preservation — all with no external dependencies.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

No contributors

Languages

  • Python 100.0%