Add files via upload

This commit is contained in:
bikini
2026-04-29 00:17:18 -05:00
committed by GitHub
parent 474492bce4
commit 42ccb9ed69
39 changed files with 1440 additions and 1 deletions
+3
View File
@@ -0,0 +1,3 @@
from .core import Obfuscator, obfuscate, obfuscate_file
__version__ = '0.1.0'
__all__ = ['Obfuscator', 'obfuscate', 'obfuscate_file']
+3
View File
@@ -0,0 +1,3 @@
from .cli import main
if __name__ == '__main__':
raise SystemExit(main())
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+44
View File
@@ -0,0 +1,44 @@
from __future__ import annotations
import argparse
import sys
from pathlib import Path
from .core import Obfuscator
def _parser() -> argparse.ArgumentParser:
p = argparse.ArgumentParser(prog='patchwork')
p.add_argument('input')
p.add_argument('-o', '--output')
p.add_argument('--seed', type=int, default=None)
p.add_argument('--layers', type=int, default=3)
p.add_argument('--stage2-layers', type=int, default=3, dest='stage2_layers')
p.add_argument('--keep', action='append', default=[], metavar='NAME')
p.add_argument('--no-rename', action='store_true')
p.add_argument('--no-encrypt-strings', action='store_true')
p.add_argument('--no-obfuscate-numbers', action='store_true')
p.add_argument('--no-opaque', action='store_true')
p.add_argument('--no-mba', action='store_true')
p.add_argument('--no-junk', action='store_true')
p.add_argument('--no-lazy', action='store_true')
p.add_argument('--no-anti-debug', action='store_true')
p.add_argument('--quiet', '-q', action='store_true')
return p
def main(argv: list[str] | None=None) -> int:
args = _parser().parse_args(argv)
inp = Path(args.input)
if not inp.is_file():
print(f'patchwork: input file not found: {inp}', file=sys.stderr)
return 2
src = inp.read_text(encoding='utf-8')
obf = Obfuscator(seed=args.seed, rename=not args.no_rename, encrypt_strings=not args.no_encrypt_strings, obfuscate_numbers=not args.no_obfuscate_numbers, opaque_predicates=not args.no_opaque, mba=not args.no_mba, junk_branches=not args.no_junk, lazy_funcs=not args.no_lazy, anti_debug=not args.no_anti_debug, layers=args.layers, stage2_layers=args.stage2_layers, keep=set(args.keep))
out_src = obf.obfuscate(src)
out_path = Path(args.output) if args.output else inp.with_name(inp.stem + '_obf.py')
out_path.write_text(out_src, encoding='utf-8')
if not args.quiet:
in_size = len(src)
out_size = len(out_src)
ratio = out_size / max(in_size, 1)
print(f'patchwork: {inp} -> {out_path}\n input: {in_size:,} bytes\n output: {out_size:,} bytes ({ratio:.1f}x)\n seed: {obf.seed}\n layers: {obf.layers}', file=sys.stderr)
return 0
if __name__ == '__main__':
raise SystemExit(main())
+79
View File
@@ -0,0 +1,79 @@
from __future__ import annotations
import ast
from pathlib import Path
from .lazy import encrypt_user_functions
from .loader import build_loader
from .packer import pack
from .transforms import IdentifierRenamer, NumberObfuscator, OpaquePredicateInjector, StringEncryptor, build_decrypt_helper, collect_skip_names
from .transforms.junk import JunkBranchInjector
from .transforms.mba import MBATransformer
from .transforms.opaque import build_seed_stmt
from .util import make_rng
_SEED_NAME = '_pw_seed_value'
_DEC_S_NAME = '_pw_dec_str'
_DEC_B_NAME = '_pw_dec_bytes'
_RESOLVE_NAME = '__pw_resolve_lazy__'
class Obfuscator:
def __init__(self, *, seed: int | None=None, rename: bool=True, encrypt_strings: bool=True, obfuscate_numbers: bool=True, opaque_predicates: bool=True, mba: bool=True, junk_branches: bool=True, lazy_funcs: bool=True, anti_debug: bool=True, layers: int=3, stage2_layers: int=3, keep: set[str] | None=None):
self.rng, self.seed = make_rng(seed)
self.rename = rename
self.encrypt_strings = encrypt_strings
self.obfuscate_numbers = obfuscate_numbers
self.opaque_predicates = opaque_predicates
self.mba = mba
self.junk_branches = junk_branches
self.lazy_funcs = lazy_funcs
self.anti_debug = anti_debug
self.layers = max(1, int(layers))
self.stage2_layers = max(1, int(stage2_layers))
self.keep_extra = set(keep or ())
def obfuscate(self, source: str) -> str:
tree = ast.parse(source)
skip = collect_skip_names(tree)
seed_used = False
if self.opaque_predicates:
opi = OpaquePredicateInjector(self.rng, _SEED_NAME)
tree = opi.visit(tree)
seed_used = seed_used or opi.injected
if self.junk_branches:
jbi = JunkBranchInjector(self.rng, _SEED_NAME)
tree = jbi.visit(tree)
seed_used = seed_used or jbi.injected
if seed_used:
tree.body.insert(0, build_seed_stmt(_SEED_NAME))
if self.mba:
tree = MBATransformer(self.rng).visit(tree)
if self.obfuscate_numbers:
tree = NumberObfuscator(self.rng).visit(tree)
if self.encrypt_strings:
enc = StringEncryptor(self.rng, _DEC_S_NAME, _DEC_B_NAME)
tree = enc.visit(tree)
tree.body[:0] = build_decrypt_helper(_DEC_S_NAME, _DEC_B_NAME)
if self.rename:
renamer = IdentifierRenamer(self.rng, keep=skip | self.keep_extra | {_RESOLVE_NAME})
tree = renamer.visit(tree)
ast.fix_missing_locations(tree)
user_code = compile(tree, '<patchwork>', 'exec')
if self.lazy_funcs:
user_code, lazy_blobs = encrypt_user_functions(user_code, self.rng, _RESOLVE_NAME)
else:
lazy_blobs = []
user_payload, user_keys = pack(user_code, self.rng, layers=self.layers)
return build_loader(user_payload, user_keys, lazy_blobs, resolver_name=_RESOLVE_NAME, rng=self.rng, anti_debug=self.anti_debug, stage2_layers=self.stage2_layers)
def obfuscate(source: str, **kwargs) -> str:
return Obfuscator(**kwargs).obfuscate(source)
def obfuscate_file(input_path: str | Path, output_path: str | Path | None=None, **kwargs) -> Path:
inp = Path(input_path)
src = inp.read_text(encoding='utf-8')
out = obfuscate(src, **kwargs)
if output_path is None:
outp = inp.with_name(inp.stem + '_obf.py')
else:
outp = Path(output_path)
outp.write_text(out, encoding='utf-8')
return outp
+48
View File
@@ -0,0 +1,48 @@
from __future__ import annotations
import hashlib
import random
def keystream(key: bytes, length: int) -> bytes:
out = bytearray()
ctr = 0
while len(out) < length:
out.extend(hashlib.sha256(key + ctr.to_bytes(8, 'big')).digest())
ctr += 1
return bytes(out[:length])
def stream_xor(data: bytes, key: bytes) -> bytes:
ks = keystream(key, len(data))
return bytes((a ^ b for a, b in zip(data, ks)))
def make_perm(key: bytes) -> bytes:
seed = int.from_bytes(hashlib.sha256(key).digest(), 'big')
rng = random.Random(seed)
perm = list(range(256))
rng.shuffle(perm)
return bytes(perm)
def invert_perm(perm: bytes) -> bytes:
inv = bytearray(256)
for i, p in enumerate(perm):
inv[p] = i
return bytes(inv)
def perm_apply(data: bytes, key: bytes) -> bytes:
perm = make_perm(key)
return bytes((perm[b] for b in data))
def perm_invert(data: bytes, key: bytes) -> bytes:
inv = invert_perm(make_perm(key))
return bytes((inv[b] for b in data))
def _rot_left(b: int, n: int) -> int:
n &= 7
if not n:
return b
return (b << n | b >> 8 - n) & 255
def rot_apply(data: bytes, key: bytes) -> bytes:
return bytes((_rot_left(b, key[i % len(key)]) for i, b in enumerate(data)))
def rot_invert(data: bytes, key: bytes) -> bytes:
return bytes((_rot_left(b, -key[i % len(key)] & 7) for i, b in enumerate(data)))
+88
View File
@@ -0,0 +1,88 @@
from __future__ import annotations
import marshal
import random
import types
from .crypto import stream_xor
from .util import gen_bytes
LAZY_PREFIX = '<pw'
LAZY_SUFFIX = '>'
_CO_GENERATOR = 0x20
_CO_COROUTINE = 0x100
_CO_ASYNC_GENERATOR = 0x200
def _classify_code(code: types.CodeType) -> str:
if not code.co_flags & 1:
return 'class'
if code.co_name.startswith('<') and code.co_name.endswith('>'):
return 'special'
return 'function'
def _stub_source(kind: str, idx: int, resolver_name: str) -> str:
if kind == 'async_gen':
return (
f'async def _stub(*_a, **_k):\n'
f' async for _v in {resolver_name}({idx}, _a, _k):\n'
f' yield _v\n'
)
if kind == 'coroutine':
return (
f'async def _stub(*_a, **_k):\n'
f' return await {resolver_name}({idx}, _a, _k)\n'
)
if kind == 'generator':
return (
f'def _stub(*_a, **_k):\n'
f' yield from {resolver_name}({idx}, _a, _k)\n'
)
return (
f'def _stub(*_a, **_k):\n'
f' return {resolver_name}({idx}, _a, _k)\n'
)
def _kind_for(code: types.CodeType) -> str:
f = code.co_flags
if f & _CO_ASYNC_GENERATOR:
return 'async_gen'
if f & _CO_COROUTINE:
return 'coroutine'
if f & _CO_GENERATOR:
return 'generator'
return 'function'
def _make_stub_code(idx: int, original: types.CodeType, resolver_name: str) -> types.CodeType:
kind = _kind_for(original)
src = _stub_source(kind, idx, resolver_name)
mod = compile(src, '<pwstub>', 'exec')
inner = next((c for c in mod.co_consts if isinstance(c, types.CodeType)))
return inner.replace(
co_name=f'{LAZY_PREFIX}{idx:x}{LAZY_SUFFIX}',
co_freevars=original.co_freevars,
co_cellvars=original.co_cellvars,
)
def encrypt_user_functions(top: types.CodeType, rng: random.Random, resolver_name: str, key_size: int=24) -> tuple[types.CodeType, list[tuple[bytes, bytes]]]:
blobs: list[tuple[bytes, bytes]] = []
def transform(code: types.CodeType, parent_kind: str) -> types.CodeType:
new_consts: list = []
for c in code.co_consts:
if isinstance(c, types.CodeType):
ck = _classify_code(c)
if parent_kind in ('module', 'class') and ck == 'function':
idx = len(blobs)
key = gen_bytes(rng, key_size)
enc = stream_xor(marshal.dumps(c), key)
blobs.append((key, enc))
new_consts.append(_make_stub_code(idx, c, resolver_name))
else:
new_consts.append(transform(c, ck))
else:
new_consts.append(c)
return code.replace(co_consts=tuple(new_consts))
new_top = transform(top, 'module')
return (new_top, blobs)
+143
View File
@@ -0,0 +1,143 @@
from __future__ import annotations
import base64
import marshal
import random
import zlib
from . import crypto
from .packer import pack
from .util import gen_bytes, gen_name
_MOD_KEYS = ('sys', 'os', 'marshal', 'zlib', 'base64', 'hashlib', 'rand_mod')
_FUNC_KEYS = ('kstream', 'xor_dec', 'perm_inv', 'perm_dec', 'rot_dec')
_VAR_KEYS = ('data', 'code_obj', 'ns', 'blobs', 'tmp', 'i', 'k', 'p', 'g')
def _bytes_repr(b: bytes) -> str:
return f'bytes.fromhex({b.hex()!r})'
def _chunked_bytes(b: bytes, rng: random.Random) -> str:
if len(b) < 64:
return _bytes_repr(b)
nchunks = rng.randint(4, 9)
chunk_size = (len(b) + nchunks - 1) // nchunks
chunks: list[bytes] = []
pos = 0
while pos < len(b):
chunks.append(b[pos:pos + chunk_size])
pos += chunk_size
order = list(range(len(chunks)))
rng.shuffle(order)
shuffled = [chunks[i] for i in order]
inv = [0] * len(order)
for i, o in enumerate(order):
inv[o] = i
parts_src = ', '.join((_bytes_repr(c) for c in shuffled))
perm_src = ', '.join((str(x) for x in inv))
return f"b''.join((({parts_src},)[__pwi] for __pwi in ({perm_src},)))"
def _junk(rng: random.Random) -> str:
target = gen_name(rng)
n = rng.randint(0, 1 << 16)
forms = [f'{target} = (lambda _x: _x ^ _x)({n})', f'{target} = sum(({n},))', f'{target} = ({n} & {n}) | 0', f'{target} = pow({n} & 0xFF, 1, 1) + 0', f'{target} = len(()) + {n} - {n}', f'{target} = ({n} * 0) ^ 0', f'{target} = (~{n} ^ ~{n}) & 0xFF']
return rng.choice(forms)
def _name_pool(rng: random.Random, keys: tuple[str, ...]) -> dict[str, str]:
used: set[str] = set()
out: dict[str, str] = {}
for k in keys:
n = gen_name(rng, used=used)
used.add(n)
out[k] = n
return out
def _decrypt_call(N: dict[str, str], op: str, var: str, key: bytes) -> str:
kbytes = _bytes_repr(key)
if op == 'xor':
return f"{var} = {N['xor_dec']}({var}, {kbytes})"
if op == 'perm':
return f"{var} = {N['perm_dec']}({var}, {kbytes})"
if op == 'rot':
return f"{var} = {N['rot_dec']}({var}, {kbytes})"
raise ValueError(op)
def _emit_helpers(N: dict[str, str]) -> list[str]:
return [f"def {N['kstream']}(k, n):\n r = bytearray()\n c = 0\n while len(r) < n:\n r += {N['hashlib']}.sha256(k + c.to_bytes(8, 'big')).digest()\n c += 1\n return bytes(r[:n])", f"def {N['xor_dec']}(d, k):\n s = {N['kstream']}(k, len(d))\n return bytes(a ^ b for a, b in zip(d, s))", f"def {N['perm_inv']}(k):\n g = {N['rand_mod']}.Random(int.from_bytes({N['hashlib']}.sha256(k).digest(), 'big'))\n p = list(range(256))\n g.shuffle(p)\n inv = bytearray(256)\n for i, v in enumerate(p):\n inv[v] = i\n return bytes(inv)", f"def {N['perm_dec']}(d, k):\n inv = {N['perm_inv']}(k)\n return bytes(inv[b] for b in d)", f"def {N['rot_dec']}(d, k):\n out = bytearray()\n L = len(k)\n for i, b in enumerate(d):\n n = (-k[i % L]) & 7\n if n:\n out.append(((b << n) | (b >> (8 - n))) & 0xFF)\n else:\n out.append(b)\n return bytes(out)"]
def _emit_imports(N: dict[str, str], rng: random.Random) -> list[str]:
lines = [f"{N['sys']} = __import__('sys')", f"{N['os']} = __import__('os')", f"{N['marshal']} = __import__('marshal')", f"{N['zlib']} = __import__('zlib')", f"{N['base64']} = __import__('base64')", f"{N['hashlib']} = __import__('hashlib')", f"{N['rand_mod']} = __import__('random')"]
rng.shuffle(lines)
return lines
def _emit_anti_debug(N: dict[str, str], rng: random.Random) -> list[str]:
checks = [f"({N['sys']}.gettrace() is None) or {N['os']}._exit(1)", f"({N['sys']}.getprofile() is None) or {N['os']}._exit(1)", f"all(_m not in {N['sys']}.modules for _m in ('pdb','bdb','pydevd','debugpy','pydevd_bundle','pydevd_bundle.pydevd_resolver','ptvsd','rpdb','wdb')) or {N['os']}._exit(1)", f"({N['os']}.environ.get('PYTHONBREAKPOINT', '') in ('', '0', 'pdb.set_trace')) or {N['os']}._exit(1)"]
rng.shuffle(checks)
return checks
def _emit_audit_hook(N: dict[str, str]) -> list[str]:
return [f"def __pw_audit(event, args):\n if event == 'import':\n _n = args[0] if args else ''\n if _n in ('pdb','bdb','pydevd','debugpy','pydevd_bundle','ptvsd','rpdb','wdb'):\n {N['os']}._exit(1)\n elif event in ('sys.settrace','sys.setprofile'):\n {N['os']}._exit(1)", f"{N['sys']}.addaudithook(__pw_audit)"]
def _emit_frame_check(N: dict[str, str]) -> list[str]:
return [f"_f = {N['sys']}._getframe(0)\nwhile _f is not None:\n _gn = _f.f_globals.get('__name__', '')\n if _gn.startswith(('pdb', 'bdb', 'pydevd', 'debugpy', '_pydev')):\n {N['os']}._exit(1)\n _f = _f.f_back"]
def _build_stage2_source(user_payload: bytes, user_layer_keys: list[tuple[str, bytes]], lazy_blobs: list[tuple[bytes, bytes]], resolver_name: str, mod_names: dict[str, str], rng: random.Random, *, anti_debug: bool=True) -> str:
N = mod_names
F = _name_pool(rng, _FUNC_KEYS)
V = _name_pool(rng, _VAR_KEYS)
parts: list[str] = []
parts.append("_pw_types = __import__('types')")
parts.extend(_emit_helpers({**N, **F}))
if anti_debug:
parts.extend(_emit_anti_debug({**N, **F}, rng))
parts.append(_junk(rng))
if lazy_blobs:
blob_lines = []
for k, e in lazy_blobs:
blob_lines.append(f'({_chunked_bytes(k, rng)}, {_chunked_bytes(e, rng)})')
parts.append(f"__pw_blobs = [{', '.join(blob_lines)}]")
else:
parts.append('__pw_blobs = []')
parts.append(f"_pw_code_cache = {{}}\n_pw_done = set()\ndef _pw_find_func(g, target_code):\n seen = set()\n stack = list(g.values())\n while stack:\n v = stack.pop()\n oid = id(v)\n if oid in seen:\n continue\n seen.add(oid)\n if isinstance(v, _pw_types.FunctionType) and v.__code__ is target_code:\n return v\n if isinstance(v, (classmethod, staticmethod)):\n inner = v.__func__\n if isinstance(inner, _pw_types.FunctionType) and inner.__code__ is target_code:\n return inner\n stack.append(inner)\n continue\n if isinstance(v, property):\n for fn in (v.fget, v.fset, v.fdel):\n if fn is not None:\n stack.append(fn)\n continue\n if isinstance(v, type):\n for vv in v.__dict__.values():\n stack.append(vv)\n return None\ndef {resolver_name}(idx, args, kw):\n fr = {N['sys']}._getframe(1)\n g = fr.f_globals\n stub_code = fr.f_code\n if idx not in _pw_code_cache:\n key, enc = __pw_blobs[idx]\n _pw_code_cache[idx] = {N['marshal']}.loads({F['xor_dec']}(enc, key))\n real = _pw_code_cache[idx]\n target = _pw_find_func(g, stub_code)\n if target is not None and id(target) not in _pw_done:\n target.__code__ = real\n _pw_done.add(id(target))\n return target(*args, **kw)\n fn = _pw_types.FunctionType(real, g)\n return fn(*args, **kw)")
user_encoded = base64.b85encode(user_payload)
parts.append(f"{V['data']} = {N['base64']}.b85decode({_chunked_bytes(user_encoded, rng)})")
for op, key in reversed(user_layer_keys):
parts.append(_decrypt_call({**N, **F}, op, V['data'], key))
parts.append(f"{V['data']} = {N['zlib']}.decompress({V['data']})")
parts.append(f"{V['code_obj']} = {N['marshal']}.loads({V['data']})")
parts.append(_junk(rng))
if anti_debug:
parts.append(f"({N['sys']}.gettrace() is None) or {N['os']}._exit(1)")
parts.append(f"{V['ns']} = {{'__name__': '__main__', '__builtins__': __builtins__, '__file__': __file__, '__package__': None, '__doc__': None, '{resolver_name}': {resolver_name}}}")
parts.append(f"exec({V['code_obj']}, {V['ns']})")
return '\n'.join(parts) + '\n'
def build_loader(user_payload: bytes, user_layer_keys: list[tuple[str, bytes]], lazy_blobs: list[tuple[bytes, bytes]], resolver_name: str, rng: random.Random, *, anti_debug: bool=True, stage2_layers: int=3, junk_count: int=8) -> str:
N = _name_pool(rng, _MOD_KEYS)
F = _name_pool(rng, _FUNC_KEYS)
V = _name_pool(rng, _VAR_KEYS)
stage2_src = _build_stage2_source(user_payload, user_layer_keys, lazy_blobs, resolver_name=resolver_name, mod_names=N, rng=rng, anti_debug=anti_debug)
stage2_code = compile(stage2_src, '<patchwork:stage2>', 'exec')
stage2_payload, stage2_keys = pack(stage2_code, rng, layers=stage2_layers)
parts: list[str] = []
parts.extend(_emit_imports(N, rng))
parts.append(_junk(rng))
if anti_debug:
parts.extend(_emit_anti_debug({**N, **F}, rng))
parts.extend(_emit_audit_hook(N))
parts.extend(_emit_frame_check(N))
parts.append(_junk(rng))
parts.extend(_emit_helpers({**N, **F}))
parts.append(_junk(rng))
stage2_encoded = base64.b85encode(stage2_payload)
parts.append(f"{V['data']} = {N['base64']}.b85decode({_chunked_bytes(stage2_encoded, rng)})")
for op, key in reversed(stage2_keys):
parts.append(_decrypt_call({**N, **F}, op, V['data'], key))
parts.append(f"{V['data']} = {N['zlib']}.decompress({V['data']})")
if anti_debug:
parts.append(f"({N['sys']}.gettrace() is None) or {N['os']}._exit(1)")
parts.append(f"{V['code_obj']} = {N['marshal']}.loads({V['data']})")
parts.append(_junk(rng))
parts.append(f"{V['ns']} = {{'__builtins__': __builtins__, '__file__': __file__, '__name__': __name__, '{N['sys']}': {N['sys']}, '{N['os']}': {N['os']}, '{N['marshal']}': {N['marshal']}, '{N['zlib']}': {N['zlib']}, '{N['base64']}': {N['base64']}, '{N['hashlib']}': {N['hashlib']}, '{N['rand_mod']}': {N['rand_mod']}}}")
parts.append(f"exec({V['code_obj']}, {V['ns']})")
body = list(parts)
for _ in range(junk_count):
idx = rng.randint(0, len(body))
body.insert(idx, _junk(rng))
return '\n'.join(body) + '\n'
+23
View File
@@ -0,0 +1,23 @@
from __future__ import annotations
import marshal
import random
import zlib
from . import crypto
from .util import gen_bytes
CIPHER_OPS = ('xor', 'perm', 'rot')
def pack(code_obj, rng: random.Random, layers: int=3) -> tuple[bytes, list[tuple[str, bytes]]]:
data = marshal.dumps(code_obj)
data = zlib.compress(data, 9)
layer_keys: list[tuple[str, bytes]] = []
for _ in range(layers):
op = rng.choice(CIPHER_OPS)
key = gen_bytes(rng, rng.randint(20, 40))
if op == 'xor':
data = crypto.stream_xor(data, key)
elif op == 'perm':
data = crypto.perm_apply(data, key)
elif op == 'rot':
data = crypto.rot_apply(data, key)
layer_keys.append((op, key))
return (data, layer_keys)
+5
View File
@@ -0,0 +1,5 @@
from .identifiers import IdentifierRenamer, collect_skip_names
from .strings import StringEncryptor, build_decrypt_helper
from .numbers import NumberObfuscator
from .opaque import OpaquePredicateInjector
__all__ = ['IdentifierRenamer', 'collect_skip_names', 'StringEncryptor', 'build_decrypt_helper', 'NumberObfuscator', 'OpaquePredicateInjector']
Binary file not shown.
+167
View File
@@ -0,0 +1,167 @@
from __future__ import annotations
import ast
import builtins
import keyword
import random
from ..util import gen_name
_DUNDERS = {'__name__', '__main__', '__file__', '__doc__', '__builtins__', '__package__', '__path__', '__loader__', '__spec__', '__cached__', '__all__', '__init__', '__new__', '__del__', '__init_subclass__', '__subclasshook__', '__set_name__', '__class_getitem__', '__enter__', '__exit__', '__aenter__', '__aexit__', '__await__', '__aiter__', '__anext__', '__call__', '__repr__', '__str__', '__bytes__', '__format__', '__hash__', '__bool__', '__len__', '__length_hint__', '__sizeof__', '__dir__', '__eq__', '__ne__', '__lt__', '__le__', '__gt__', '__ge__', '__add__', '__sub__', '__mul__', '__truediv__', '__floordiv__', '__mod__', '__divmod__', '__pow__', '__lshift__', '__rshift__', '__and__', '__or__', '__xor__', '__radd__', '__rsub__', '__rmul__', '__rtruediv__', '__rfloordiv__', '__rmod__', '__rdivmod__', '__rpow__', '__rlshift__', '__rrshift__', '__rand__', '__ror__', '__rxor__', '__iadd__', '__isub__', '__imul__', '__itruediv__', '__ifloordiv__', '__imod__', '__ipow__', '__ilshift__', '__irshift__', '__iand__', '__ior__', '__ixor__', '__matmul__', '__rmatmul__', '__imatmul__', '__neg__', '__pos__', '__abs__', '__invert__', '__int__', '__float__', '__complex__', '__round__', '__index__', '__trunc__', '__floor__', '__ceil__', '__getattr__', '__setattr__', '__delattr__', '__getattribute__', '__getitem__', '__setitem__', '__delitem__', '__missing__', '__iter__', '__next__', '__reversed__', '__contains__', '__copy__', '__deepcopy__', '__reduce__', '__reduce_ex__', '__getstate__', '__setstate__', '__getnewargs__', '__getnewargs_ex__', '__class__', '__instancecheck__', '__subclasscheck__', '__slots__', '__dict__', '__weakref__', '__module__', '__qualname__', '__defaults__', '__kwdefaults__', '__annotations__', '__wrapped__', '__signature__', '__match_args__', '__post_init__', '__fspath__', '__index__', '__buffer__', '__release_buffer__'}
RESERVED: set[str] = set(dir(builtins)) | set(keyword.kwlist) | _DUNDERS | {'self', 'cls', 'metaclass'}
def collect_skip_names(tree: ast.AST) -> set[str]:
skip: set[str] = set()
for node in ast.walk(tree):
if isinstance(node, ast.Assign):
for target in node.targets:
if isinstance(target, ast.Name) and target.id == '__all__':
if isinstance(node.value, (ast.List, ast.Tuple, ast.Set)):
for elt in node.value.elts:
if isinstance(elt, ast.Constant) and isinstance(elt.value, str):
skip.add(elt.value)
elif isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and (node.func.id in ('getattr', 'setattr', 'hasattr', 'delattr')):
if len(node.args) >= 2:
arg = node.args[1]
if isinstance(arg, ast.Constant) and isinstance(arg.value, str):
skip.add(arg.value)
elif isinstance(node, ast.ClassDef):
for stmt in node.body:
if isinstance(stmt, (ast.FunctionDef, ast.AsyncFunctionDef)):
skip.add(stmt.name)
elif isinstance(stmt, ast.Assign):
for tgt in stmt.targets:
if isinstance(tgt, ast.Name):
skip.add(tgt.id)
elif isinstance(tgt, (ast.Tuple, ast.List)):
for elt in tgt.elts:
if isinstance(elt, ast.Name):
skip.add(elt.id)
elif isinstance(stmt, ast.AnnAssign) and isinstance(stmt.target, ast.Name):
skip.add(stmt.target.id)
elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.Lambda)):
args = node.args
for a in args.args + args.posonlyargs + args.kwonlyargs:
skip.add(a.arg)
if args.vararg:
skip.add(args.vararg.arg)
if args.kwarg:
skip.add(args.kwarg.arg)
return skip
class IdentifierRenamer(ast.NodeTransformer):
def __init__(self, rng: random.Random, keep: set[str] | None=None):
self.rng = rng
self.mapping: dict[str, str] = {}
self.reserved = RESERVED | (keep or set())
self._used_outputs: set[str] = set()
def _new(self) -> str:
n = gen_name(self.rng, used=self._used_outputs)
self._used_outputs.add(n)
return n
def _rename(self, name: str) -> str:
if not name or name in self.reserved:
return name
if name.startswith('__') and name.endswith('__') and (len(name) >= 4):
return name
if name not in self.mapping:
self.mapping[name] = self._new()
return self.mapping[name]
def visit_Name(self, node: ast.Name) -> ast.AST:
node.id = self._rename(node.id)
return node
def visit_FunctionDef(self, node: ast.FunctionDef) -> ast.AST:
node.name = self._rename(node.name)
node.decorator_list = [self.visit(d) for d in node.decorator_list]
if node.returns is not None:
node.returns = self.visit(node.returns)
node.args = self.visit(node.args)
node.body = [self.visit(stmt) for stmt in node.body]
return node
visit_AsyncFunctionDef = visit_FunctionDef
def visit_ClassDef(self, node: ast.ClassDef) -> ast.AST:
node.name = self._rename(node.name)
node.bases = [self.visit(b) for b in node.bases]
for kw in node.keywords:
kw.value = self.visit(kw.value)
node.decorator_list = [self.visit(d) for d in node.decorator_list]
node.body = [self.visit(stmt) for stmt in node.body]
return node
def visit_Lambda(self, node: ast.Lambda) -> ast.AST:
node.args = self.visit(node.args)
node.body = self.visit(node.body)
return node
def visit_arg(self, node: ast.arg) -> ast.AST:
node.arg = self._rename(node.arg)
if node.annotation is not None:
node.annotation = self.visit(node.annotation)
return node
def visit_arguments(self, node: ast.arguments) -> ast.AST:
node.args = [self.visit(a) for a in node.args]
node.posonlyargs = [self.visit(a) for a in node.posonlyargs]
node.kwonlyargs = [self.visit(a) for a in node.kwonlyargs]
if node.vararg is not None:
node.vararg = self.visit(node.vararg)
if node.kwarg is not None:
node.kwarg = self.visit(node.kwarg)
node.defaults = [self.visit(d) for d in node.defaults]
node.kw_defaults = [self.visit(d) if d is not None else None for d in node.kw_defaults]
return node
def visit_Attribute(self, node: ast.Attribute) -> ast.AST:
node.value = self.visit(node.value)
return node
def visit_keyword(self, node: ast.keyword) -> ast.AST:
if node.value is not None:
node.value = self.visit(node.value)
return node
def visit_Global(self, node: ast.Global) -> ast.AST:
node.names = [self._rename(n) for n in node.names]
return node
def visit_Nonlocal(self, node: ast.Nonlocal) -> ast.AST:
node.names = [self._rename(n) for n in node.names]
return node
def visit_ExceptHandler(self, node: ast.ExceptHandler) -> ast.AST:
if node.type is not None:
node.type = self.visit(node.type)
if node.name:
node.name = self._rename(node.name)
node.body = [self.visit(s) for s in node.body]
return node
def visit_Import(self, node: ast.Import) -> ast.AST:
for alias in node.names:
if '.' in alias.name:
if alias.asname:
new = self._rename(alias.asname)
alias.asname = new
continue
if alias.asname:
alias.asname = self._rename(alias.asname)
else:
if alias.name not in self.mapping:
self.mapping[alias.name] = self._new()
alias.asname = self.mapping[alias.name]
return node
def visit_ImportFrom(self, node: ast.ImportFrom) -> ast.AST:
for alias in node.names:
if alias.name == '*':
continue
if alias.asname:
alias.asname = self._rename(alias.asname)
else:
if alias.name not in self.mapping:
self.mapping[alias.name] = self._new()
alias.asname = self.mapping[alias.name]
return node
+64
View File
@@ -0,0 +1,64 @@
from __future__ import annotations
import ast
import random
_JUNK_NAMES = ('_v', '_t', '_acc', '_state', '_buf', '_h', '_z', '_w', '_q', '_n')
_JUNK_STMT_TEMPLATES = ('{v} = ({a} ^ {b}) + ({c} & {d})', '{v} = ((({a} * 0x01000193) ^ {b}) & 0xFFFFFFFF)', '{v} = sum(((({a} >> _k) & 0xFF) for _k in range(0, 32, 8)))', '{v} = ({a} | {b}) - (({c} & {d}) >> 1)', '{v} = pow({a} | 1, 1, ({b} | 0x10001))', '{v} = (({a} + {b}) ^ ({c} - {d})) & 0x7FFFFFFF', '{v} = bytes(((_i * {a}) ^ {b}) & 0xFF for _i in range(8))', '{v} = (~({a}) | ({b} & {c})) & 0xFFFFFF')
def _rint(rng: random.Random) -> int:
return rng.randrange(1, 1 << 30)
class JunkBranchInjector(ast.NodeTransformer):
def __init__(self, rng: random.Random, seed_name: str, prob: float=0.4, max_depth: int=0):
self.rng = rng
self.seed_name = seed_name
self.prob = prob
self.injected = False
self._depth = 0
self._max_depth = max_depth
def visit_FunctionDef(self, node: ast.FunctionDef) -> ast.AST:
return self._inject_into_body(node)
visit_AsyncFunctionDef = visit_FunctionDef
def visit_Module(self, node: ast.Module) -> ast.AST:
return self._inject_into_body(node)
def _inject_into_body(self, node):
for child in ast.iter_child_nodes(node):
if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, ast.Module)):
self.visit(child)
if not getattr(node, 'body', None):
return node
if self.rng.random() > self.prob:
return node
block = self._build_block()
idx = self.rng.randint(0, len(node.body))
node.body.insert(idx, block)
self.injected = True
return node
def visit_ClassDef(self, node: ast.ClassDef) -> ast.AST:
for child in node.body:
if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)):
self.visit(child)
return node
def _build_block(self) -> ast.stmt:
s = self.seed_name
kind = self.rng.choice(('parity', 'flt3', 'flt5'))
if kind == 'parity':
cond_src = f'({s} * ({s} + 1) + 1) % 2 == 0'
elif kind == 'flt3':
cond_src = f'({s} * {s} * {s} - {s} + 1) % 3 == 0'
else:
cond_src = f'({s} * {s} * {s} * {s} * {s} - {s} + 1) % 5 == 0'
cond = ast.parse(cond_src, mode='eval').body
body = [self._random_stmt() for _ in range(self.rng.randint(2, 4))]
return ast.If(test=cond, body=body, orelse=[])
def _random_stmt(self) -> ast.stmt:
v = self.rng.choice(_JUNK_NAMES)
tmpl = self.rng.choice(_JUNK_STMT_TEMPLATES)
src = tmpl.format(v=v, a=_rint(self.rng), b=_rint(self.rng), c=_rint(self.rng), d=_rint(self.rng))
return ast.parse(src).body[0]
+93
View File
@@ -0,0 +1,93 @@
from __future__ import annotations
import ast
import copy
import random
def _is_pure(node: ast.AST) -> bool:
if isinstance(node, ast.Name):
return True
if isinstance(node, ast.Constant):
return isinstance(node.value, (int, bool))
if isinstance(node, ast.Attribute):
return _is_pure(node.value)
if isinstance(node, ast.Subscript):
return _is_pure(node.value) and _is_pure(node.slice)
if isinstance(node, ast.UnaryOp) and isinstance(node.op, (ast.USub, ast.UAdd, ast.Invert)):
return _is_pure(node.operand)
return False
def _dup(node: ast.AST) -> ast.AST:
return copy.deepcopy(node)
class MBATransformer(ast.NodeTransformer):
def __init__(self, rng: random.Random, prob: float=0.7):
self.rng = rng
self.prob = prob
def visit_BinOp(self, node: ast.BinOp) -> ast.AST:
self.generic_visit(node)
if not (_is_pure(node.left) and _is_pure(node.right)):
return node
if self.rng.random() > self.prob:
return node
a, b = (node.left, node.right)
op = node.op
if isinstance(op, ast.BitXor):
return self._xor(a, b)
if isinstance(op, ast.BitOr):
return self._or(a, b)
if isinstance(op, ast.BitAnd):
return self._and(a, b)
if isinstance(op, ast.LShift) and isinstance(b, ast.Constant) and isinstance(b.value, int):
return self._lshift(a, b.value)
if isinstance(op, ast.RShift) and isinstance(b, ast.Constant) and isinstance(b.value, int):
return self._rshift(a, b.value)
return node
def visit_UnaryOp(self, node: ast.UnaryOp) -> ast.AST:
self.generic_visit(node)
if not _is_pure(node.operand):
return node
if self.rng.random() > self.prob:
return node
if isinstance(node.op, ast.Invert):
return self._invert(node.operand)
return node
def _xor(self, a: ast.AST, b: ast.AST) -> ast.AST:
choice = self.rng.choice(('subor', 'ornotand', 'addsub'))
if choice == 'subor':
return ast.BinOp(left=ast.BinOp(left=_dup(a), op=ast.BitOr(), right=_dup(b)), op=ast.Sub(), right=ast.BinOp(left=_dup(a), op=ast.BitAnd(), right=_dup(b)))
if choice == 'ornotand':
return ast.BinOp(left=ast.BinOp(left=_dup(a), op=ast.BitOr(), right=_dup(b)), op=ast.BitAnd(), right=ast.UnaryOp(op=ast.Invert(), operand=ast.BinOp(left=_dup(a), op=ast.BitAnd(), right=_dup(b))))
return ast.BinOp(left=ast.BinOp(left=_dup(a), op=ast.Add(), right=_dup(b)), op=ast.Sub(), right=ast.BinOp(left=ast.Constant(value=2), op=ast.Mult(), right=ast.BinOp(left=_dup(a), op=ast.BitAnd(), right=_dup(b))))
def _or(self, a: ast.AST, b: ast.AST) -> ast.AST:
if self.rng.random() < 0.5:
return ast.BinOp(left=ast.BinOp(left=_dup(a), op=ast.Add(), right=_dup(b)), op=ast.Sub(), right=ast.BinOp(left=_dup(a), op=ast.BitAnd(), right=_dup(b)))
return ast.BinOp(left=ast.BinOp(left=_dup(a), op=ast.BitXor(), right=_dup(b)), op=ast.BitXor(), right=ast.BinOp(left=_dup(a), op=ast.BitAnd(), right=_dup(b)))
def _and(self, a: ast.AST, b: ast.AST) -> ast.AST:
if self.rng.random() < 0.5:
return ast.BinOp(left=ast.BinOp(left=_dup(a), op=ast.Add(), right=_dup(b)), op=ast.Sub(), right=ast.BinOp(left=_dup(a), op=ast.BitOr(), right=_dup(b)))
return ast.UnaryOp(op=ast.Invert(), operand=ast.BinOp(left=ast.UnaryOp(op=ast.Invert(), operand=_dup(a)), op=ast.BitOr(), right=ast.UnaryOp(op=ast.Invert(), operand=_dup(b))))
def _invert(self, a: ast.AST) -> ast.AST:
return ast.BinOp(left=ast.UnaryOp(op=ast.USub(), operand=_dup(a)), op=ast.Sub(), right=ast.Constant(value=1))
def _lshift(self, a: ast.AST, n: int) -> ast.AST:
if n < 0 or n > 60:
return ast.BinOp(left=_dup(a), op=ast.LShift(), right=ast.Constant(value=n))
return ast.BinOp(left=_dup(a), op=ast.Mult(), right=ast.Constant(value=1 << n))
def _rshift(self, a: ast.AST, n: int) -> ast.AST:
if n < 0 or n > 60:
return ast.BinOp(left=_dup(a), op=ast.RShift(), right=ast.Constant(value=n))
return ast.BinOp(left=_dup(a), op=ast.FloorDiv(), right=ast.Constant(value=1 << n))
def visit_match_case(self, node: ast.match_case) -> ast.AST:
if node.guard is not None:
node.guard = self.visit(node.guard)
node.body = [self.visit(stmt) for stmt in node.body]
return node
+70
View File
@@ -0,0 +1,70 @@
from __future__ import annotations
import ast
import random
class NumberObfuscator(ast.NodeTransformer):
def __init__(self, rng: random.Random, prob: float=0.85):
self.rng = rng
self.prob = prob
def visit_Constant(self, node: ast.Constant) -> ast.AST:
v = node.value
if isinstance(v, bool):
return node
if isinstance(v, int):
if self.rng.random() <= self.prob:
return self._obfuscate(v)
return node
def visit_match_case(self, node: ast.match_case) -> ast.AST:
if node.guard is not None:
node.guard = self.visit(node.guard)
node.body = [self.visit(stmt) for stmt in node.body]
return node
def _obfuscate(self, n: int) -> ast.AST:
choices = ['xor', 'sum', 'shift', 'frombytes', 'affine', 'split']
kind = self.rng.choice(choices)
try:
return getattr(self, f'_t_{kind}')(n)
except Exception:
return ast.Constant(value=n)
def _t_xor(self, n: int) -> ast.AST:
k = self.rng.randrange(1, 1 << 32)
return self._parse_expr(f'(({n ^ k}) ^ {k})')
def _t_sum(self, n: int) -> ast.AST:
a = self.rng.randint(-(1 << 16), 1 << 16)
return self._parse_expr(f'(({n - a}) + ({a}))')
def _t_shift(self, n: int) -> ast.AST:
if abs(n) >= 1 << 60:
return self._t_xor(n)
s = self.rng.randint(1, 8)
return self._parse_expr(f'(({n} << {s}) >> {s})')
def _t_frombytes(self, n: int) -> ast.AST:
signed = n < 0
bit_len = max(1, n.bit_length() + (1 if signed else 0))
byte_len = (bit_len + 7) // 8
try:
data = n.to_bytes(byte_len, 'big', signed=signed)
except OverflowError:
return self._t_xor(n)
signed_arg = 'True' if signed else 'False'
return self._parse_expr(f"int.from_bytes({data!r}, 'big', signed={signed_arg})")
def _t_affine(self, n: int) -> ast.AST:
a = self.rng.randint(2, 64)
b = self.rng.randint(-(1 << 12), 1 << 12)
return self._parse_expr(f'((({n * a + b}) - ({b})) // {a})')
def _t_split(self, n: int) -> ast.AST:
p = self.rng.randint(-(1 << 16), 1 << 16)
q = n - p
return self._parse_expr(f'({p} + ({q}))')
def _parse_expr(self, src: str) -> ast.AST:
return ast.parse(src, mode='eval').body
+42
View File
@@ -0,0 +1,42 @@
from __future__ import annotations
import ast
import random
class OpaquePredicateInjector(ast.NodeTransformer):
def __init__(self, rng: random.Random, seed_name: str, prob: float=0.5):
self.rng = rng
self.seed_name = seed_name
self.prob = prob
self.injected = False
def visit_If(self, node: ast.If) -> ast.AST:
self.generic_visit(node)
if self.rng.random() < self.prob:
node.test = self._wrap(node.test)
self.injected = True
return node
def visit_While(self, node: ast.While) -> ast.AST:
self.generic_visit(node)
if self.rng.random() < self.prob * 0.4:
node.test = self._wrap(node.test)
self.injected = True
return node
def _wrap(self, test: ast.expr) -> ast.expr:
return ast.BoolOp(op=ast.And(), values=[test, self._tautology()])
def _tautology(self) -> ast.expr:
s = self.seed_name
kind = self.rng.choice(('parity', 'flt3', 'flt5'))
if kind == 'parity':
src = f'({s} * ({s} + 1)) % 2 == 0'
elif kind == 'flt3':
src = f'({s} * {s} * {s} - {s}) % 3 == 0'
else:
src = f'({s} * {s} * {s} * {s} * {s} - {s}) % 5 == 0'
return ast.parse(src, mode='eval').body
def build_seed_stmt(seed_name: str) -> ast.stmt:
return ast.parse(f'{seed_name} = id(int) & 0xFFFFFFFF').body[0]
+84
View File
@@ -0,0 +1,84 @@
from __future__ import annotations
import ast
import random
class StringEncryptor(ast.NodeTransformer):
def __init__(self, rng: random.Random, decrypt_str_name: str, decrypt_bytes_name: str):
self.rng = rng
self.dec_s = decrypt_str_name
self.dec_b = decrypt_bytes_name
def _strip_docstring(self, body: list[ast.stmt]) -> list[ast.stmt]:
if body and isinstance(body[0], ast.Expr) and isinstance(body[0].value, ast.Constant) and isinstance(body[0].value.value, str):
return body[1:] or [ast.Pass()]
return body
def visit_Module(self, node: ast.Module) -> ast.AST:
node.body = self._strip_docstring(node.body)
self.generic_visit(node)
return node
def visit_FunctionDef(self, node: ast.FunctionDef) -> ast.AST:
node.body = self._strip_docstring(node.body)
self.generic_visit(node)
return node
visit_AsyncFunctionDef = visit_FunctionDef
def visit_ClassDef(self, node: ast.ClassDef) -> ast.AST:
node.body = self._strip_docstring(node.body)
self.generic_visit(node)
return node
def visit_JoinedStr(self, node: ast.JoinedStr) -> ast.AST:
new_values: list[ast.AST] = []
for v in node.values:
if isinstance(v, ast.FormattedValue):
v.value = self.visit(v.value)
if v.format_spec is not None:
v.format_spec = self.visit(v.format_spec)
new_values.append(v)
else:
new_values.append(v)
node.values = new_values
return node
def visit_match_case(self, node: ast.match_case) -> ast.AST:
if node.guard is not None:
node.guard = self.visit(node.guard)
node.body = [self.visit(stmt) for stmt in node.body]
return node
def visit_Constant(self, node: ast.Constant) -> ast.AST:
v = node.value
if isinstance(v, bool):
return node
if isinstance(v, str):
return self._encrypt_str(v) if v else node
if isinstance(v, bytes):
return self._encrypt_bytes(v) if v else node
return node
def _make_key(self, n_min: int=4, n_max: int=24) -> bytes:
return bytes((self.rng.randrange(256) for _ in range(self.rng.randint(n_min, n_max))))
def _xor(self, data: bytes, key: bytes) -> bytes:
return bytes((b ^ key[i % len(key)] for i, b in enumerate(data)))
def _encrypt_str(self, s: str) -> ast.AST:
try:
data = s.encode('utf-8')
except UnicodeEncodeError:
return ast.Constant(value=s)
key = self._make_key()
enc = self._xor(data, key)
return ast.Call(func=ast.Name(id=self.dec_s, ctx=ast.Load()), args=[ast.Constant(value=enc), ast.Constant(value=key)], keywords=[])
def _encrypt_bytes(self, b: bytes) -> ast.AST:
key = self._make_key()
enc = self._xor(b, key)
return ast.Call(func=ast.Name(id=self.dec_b, ctx=ast.Load()), args=[ast.Constant(value=enc), ast.Constant(value=key)], keywords=[])
def build_decrypt_helper(decrypt_str_name: str, decrypt_bytes_name: str) -> list[ast.stmt]:
src = f'def {decrypt_str_name}(d, k):\n return bytes(b ^ k[i % len(k)] for i, b in enumerate(d)).decode()\ndef {decrypt_bytes_name}(d, k):\n return bytes(b ^ k[i % len(k)] for i, b in enumerate(d))\n'
return ast.parse(src).body
+25
View File
@@ -0,0 +1,25 @@
from __future__ import annotations
import random
import secrets
CONFUSE_CHARS = 'OoIl01'
def make_rng(seed: int | None=None) -> tuple[random.Random, int]:
if seed is None:
seed = secrets.randbits(63)
return (random.Random(seed), seed)
def gen_name(rng: random.Random, length: int | None=None, used: set[str] | None=None) -> str:
if length is None:
length = rng.randint(12, 20)
used = used or set()
for _ in range(64):
name = '_' + ''.join((rng.choice(CONFUSE_CHARS) for _ in range(length)))
if name not in used:
return name
while True:
name = '_' + ''.join((rng.choice(CONFUSE_CHARS) for _ in range(length + rng.randint(1, 4))))
if name not in used:
return name
def gen_bytes(rng: random.Random, length: int) -> bytes:
return bytes((rng.randrange(256) for _ in range(length)))