Skip to content

Commit

Permalink
update doc, refactor for code quality
Browse files Browse the repository at this point in the history
  • Loading branch information
1 committed Aug 23, 2023
1 parent 31f5d40 commit e709e43
Show file tree
Hide file tree
Showing 24 changed files with 336 additions and 109 deletions.
154 changes: 119 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,44 +2,68 @@

Python Obfuscation Framework.

Combine/chain obfuscation methods on a single Python source file.
Combine and chain obfuscation methods on a single Python source file.

## Install
## Goals

```bash
git clone https://github.com/2O4/pof
cd pof
python -m venv venv
source ./venv/bin/activate
./setup.py install
```
The goals of this project is to create a toolkit to obfuscate Python source code to create payload for offensive security:

- **Payloads**: store the code inside images, have multiple stages, use LOTL techniques
- **Stager**: easily create multi stages payloads
- **Evasion**: AV, EDR, DPI, sandbox and other analysis techniques
- **Human**: slow down human analysis of the payload
- **Automation**: automate the whole process, to produce numerous variant of the payload
- **Fun**: because it's always fun to see what's possible to do with Pythonnd

This will install POF inside a virtual env.
Python is not exactly the best language to create payloads with, especially for Windows if it's not already installed. This project was made for learning, and discovering new ways of bypassing security, of obfuscating the same code, it's a great way to test obfuscations techniques.

This project could also give you ideas to implement in other languages, such as powershell where it would make sens to obfuscate the source code. Or in C, C#, C++, Go or Rust where it would make sens to stage payloads, compress them, encrypt them and obfuscate strings.

You could also use most of the stagers to stage payload that are not built in Python.

## Usage

You can either pipe or give a file for input, same for output.

```bash
pof $SOURCE -o $OUT
echo "print('Hello, world')" | pof
```

Using the default chained obfuscator
Output:

```bash
pof in.py -o out.py
```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())]))
```

Using a specific obfuscator
And yes, this is a valid Python script, that actually runs and only output `Hello world` ! And you'll get a different output each times.

Using a specific obfuscator:

```bash
pof in.py -o out.py -f obfuscator BuiltinsObfuscator
```

Using a specific payload
Using a specific payload:

```bash
pof inf.py -o out.py -f payload GzipPayload
```

## Install

```bash
git clone https://github.com/2O4/pof
cd pof
python -m venv venv
source ./venv/bin/activate
./setup.py install
```

This will install pof inside a virtual env, so you'll need to activate it every times you want to use it.

## Examples

These are examples of obfuscators of the script `print('Hello, world')`.
Expand All @@ -58,23 +82,6 @@ To test the validity of the output you can simply pipe it to Python:
echo "print('Hello, world')" | pof -f obfuscator -k UUIDObfuscator | python
```

If no obfuscator is selected, a default chain obfuscator is used.

```bash
echo "print('Hello, world')" | pof
```

Output:

```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())]))
```

And yes, this is a valid Python script, that actually runs and only output `Hello world` !

### Obfuscator

#### BuiltinsObfuscator
Expand Down Expand Up @@ -140,6 +147,8 @@ print(b85decode( b'NM&qnZ!92pZ*pv8').decode())

#### RC4Obfuscator

Warning: the RC4 obfuscator (and other cipher obfuscators) will combine both, the cipher text and the key in the same file, this is obviously not secure, and should never be used for security purposes. The idea behind this obfuscator is to fool humans, AV, EDR, network TAP etc. not to be secured and safe.

```python
import codecs
def rc4decrypt(key,ciphertext):
Expand Down Expand Up @@ -186,6 +195,8 @@ exec("".join([chr(ord(i)-3)for i in'sulqw+*Khoor/#zruog*,\r']))

#### XORObfuscator

Warning: like for the RC4 cipher the XOR obfuscator shouldn't be used for security purposes, its main goal is to evade common security tools, not protect the information! Plus the XOR cipher is really weak and easy to crack.

```python
from base64 import b64decode

Expand Down Expand Up @@ -389,6 +400,81 @@ def quine():
exec(b64decode(esource))
```

## Python API

The true power of pof is in chaining multiple different obfuscation techniques easily, there is a prety simple Python API to do so.

For example this is a snippet of the default obfuscator:

```python
def obfuscate(source):
tokens = get_tokens(source)

# get all the names and add them to the RESERVED_WORDS for the
# generators
reserved_words_add = NameExtract.get_names(tokens)
BaseGenerator.extend_reserved(reserved_words_add)

tokens = CommentsObfuscator().obfuscate_tokens(tokens)
tokens = LoggingObfuscator().obfuscate_tokens(tokens)
tokens = PrintObfuscator().obfuscate_tokens(tokens)
ex_generator = BasicGenerator.number_name_generator()
tokens = ExceptionObfuscator(
add_codes=True,
generator=ex_generator,
).obfuscate_tokens(tokens)

# configure generator
gen_dict = {
86: AdvancedGenerator.realistic_generator(),
10: BasicGenerator.alphabet_generator(),
4: BasicGenerator.number_name_generator(length=random.randint(2, 5)),
}
generator = AdvancedGenerator.multi_generator(gen_dict)

# core obfuscation
tokens = ConstantsObfuscator(
generator=generator,
obf_number_rate=0.7,
obf_string_rate=0.1,
obf_string_rate=0.1,
obf_builtins_rate=0.3,
).obfuscate_tokens(tokens)

tokens = NamesObfuscator(generator=generator).obfuscate_tokens(tokens)

tokens = GlobalsObfuscator().obfuscate_tokens(tokens)
tokens = BuiltinsObfuscator().obfuscate_tokens(tokens)

b64decode_name = next(generator)
b85decode_name = next(generator)
string_obfuscator = StringsObfuscator(
import_b64decode=True,
import_b85decode=True,
b64decode_name=b64decode_name,
b85decode_name=b85decode_name,
)
tokens = string_obfuscator.obfuscate_tokens(tokens)
string_obfuscator.import_b64decode = False
string_obfuscator.import_b85decode = False

for _ in range(2):
tokens = NumberObfuscator().obfuscate_tokens(tokens)
tokens = BuiltinsObfuscator().obfuscate_tokens(tokens)
for _ in range(2):
tokens = string_obfuscator.obfuscate_tokens(tokens)

return untokenize(tokens)
```

In this example we can see that first we remove comments, logging, print statements, and change the content of exceptions, and then we start to obfuscate constants, names, globals, builtins, strings, then strings and numbers multiple times, and we finnaly convert the tokens back to code.

By chaining multiple ofuscations techniques we can create very complexe and custom output.

Pof also provide evasions methods, detailed below, they are useful for quick and easy evasions, and can be used and customized to fit the need.

For more example of how to use the pof Python API check the [examples/](./examples) directory.

## Yara

Yara rules can be used to detect malware, they can also be used to find interesting strings in Python source code. To check rules against source files and/or obfuscated files run:
Expand Down Expand Up @@ -454,8 +540,6 @@ ruff .
- Resolve every ruff errors
- Increase test coverage
- Add pre-commit hooks
- Setup .gitignore
- Create repository on GitHub
- Setup package
- Publish package on pypi
- Write a doc
Expand Down
7 changes: 7 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Pof examples

In this directory you'll find 2 files, the source and then generator file. The source file `source.py` contains the Python source file to obfuscate, and the generator file `gen.py` contain the obfuscator code. All outputs are produce to the `out/` directory.

This is useful to test pof, get a feel for the Python API, and how to use it, but also with some outputs and test the result.

Note that you'll need to install pof first.
Empty file added examples/__init__.py
Empty file.
32 changes: 32 additions & 0 deletions examples/gen.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import pathlib

import pof


class ExampleObfuscator(pof.BaseObfuscator):
def constant_obf(self, source):
tokens = self._get_tokens(source)
tokens = pof.obfuscator.ConstantsObfuscator().obfuscate_tokens(tokens)
return self._untokenize(tokens)


def obfuscate_to_file(obf_class, func_name, source):
out = getattr(obf_class, func_name)(source)
file_name = func_name + ".py"
file = pathlib.Path(__file__).parent / "out" / file_name
with file.open("w") as f:
f.write(out)


def run_all():
obf = ExampleObfuscator()

file = pathlib.Path(__file__).parent / "source.py"
with file.open() as f:
source = f.read()

obfuscate_to_file(obf, "constant_obf", source)


if __name__ == "__main__":
run_all()
30 changes: 30 additions & 0 deletions examples/out/constant_obf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
TB_="My pet is name: "
LoIC1lyQ=1
Mm9IyM=print
tkBdI=8
B6iSi8_=range
# source file that will be obfuscated
import random
import string


def get_random_letter():
"""This is a docstring."""
return random.choice(string.ascii_lowercase)


def get_random_name(name_len):
# this is a comment
name=get_random_letter().upper()
for _ in B6iSi8_(name_len-LoIC1lyQ):
name+=get_random_letter()
return name


def present_my_pet():
pet_name=get_random_name(tkBdI)
message=TB_+pet_name
Mm9IyM(message)


present_my_pet()
25 changes: 25 additions & 0 deletions examples/source.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# source file that will be obfuscated
import random
import string


def get_random_letter():
"""This is a docstring."""
return random.choice(string.ascii_lowercase)


def get_random_name(name_len):
# this is a comment
name = get_random_letter().upper()
for _ in range(name_len - 1):
name += get_random_letter()
return name


def present_my_pet():
pet_name = get_random_name(8)
message = "My pet is name: " + pet_name
print(message)


present_my_pet()
6 changes: 3 additions & 3 deletions pof/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

from pof.main import Obfuscator
from pof.main import BaseObfuscator, Obfuscator

__version__ = "1.3.6"
__version__ = "1.3.7"

__all__ = ("Obfuscator", "__version__")
__all__ = ("Obfuscator", "BaseObfuscator", "__version__")
2 changes: 1 addition & 1 deletion pof/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ def _cli():
)
parser.add_argument(
"--logging",
help="obfuscation function",
help="logging level, INFO, DEBUG, ERROR, CRITICAL",
default="INFO",
)
parser.add_argument(
Expand Down
18 changes: 9 additions & 9 deletions pof/evasion/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@

## TODO

- file size evasion
- file content evasion
- screen resolution size
- integrity, check the hash of the current script to see if it has been tampered with
- screen resolution
- check if at least one monitor is plugged in
- check hard drive and free space
- check uptime
- make multiple versions, for different OS and move them into specific folders, or have 2 classes in the same file when it's different evasion
- File size evasion
- File content evasion
- Screen resolution size
- Integrity, check the hash of the current script to see if it has been tampered with
- Screen resolution
- Check if at least one monitor is plugged in
- Check hard drive and free space
- Check uptime
- Make multiple versions, for different OS and move them into specific folders, or have 2 classes in the same file when it's different evasion
8 changes: 5 additions & 3 deletions pof/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,9 @@ def obfuscate(
# generators
reserved_words_add = NameExtract.get_names(tokens)
BaseGenerator.extend_reserved(reserved_words_add)
logging.info("reserved {lt} names", extra={"lt": len(tokens)})

msg = f"reserved {len(tokens)} names"
logging.info(msg)

# clean input
tokens = CommentsObfuscator().obfuscate_tokens(tokens)
Expand Down Expand Up @@ -207,7 +209,7 @@ def basic(self, source):
tokens = NewlineObfuscator().obfuscate_tokens(tokens)
return self._untokenize(tokens)

def full_evasion(
def full_evasion( # noqa: C901 PLR0913 PLR0912
self,
source,
hostname: str | None = None,
Expand Down Expand Up @@ -312,7 +314,7 @@ def image(self, source):
return self._untokenize(tokens)

def advanced_image(self, source):
"""Encrypt and store the payload code inside an image."""
"""Obfuscate, encrypt, and store the payload code inside an image."""
tokens = self._get_tokens(source)
generator = BasicGenerator.alphabet_generator()

Expand Down
Loading

0 comments on commit e709e43

Please sign in to comment.