I am looking for how to hide my Python source code.
print "Hello World!"
How can I encode this example so that it isn't human-readable? I've been told to use base64 but I'm not sure how.
I am looking for how to hide my Python source code.
print "Hello World!"
How can I encode this example so that it isn't human-readable? I've been told to use base64 but I'm not sure how.
Cython
It seems that the goto answer for this is Cython. I'm really surprised no one else mentioned this yet? Here's the home page: https://cython.org
In a nutshell, this transforms your python into C and compiles it, thus making it as well protected as any "normal" compiled distributable C program.
There are limitations though. I haven't explored them in depth myself, because as I started to read about them, I dropped the idea for my own purposes. But it might still work for yours. Essentially, you can't use Python to the fullest, with the dynamic awesomeness it offers. One major issue that jumped out at me, was that keyword parameters are not usable :( You must write function calls using positional parameters only. I didn't confirm this, but I doubt you can use conditional imports, or evals. I'm not sure how polymorphism is handled...
Anyway, if you aren't trying to obfuscate a huge code base after the fact, or ideally if you have the use of Cython in mind to begin with, this is a very notable option.
Check out these tools for obfuscation and minification of python code:
Example .py output from pyminifier when run with --obfuscate and --gzip:
$ pyminifier --obfuscate --gzip /tmp/tumult.py
#!/usr/bin/env python3
import zlib, base64
exec(zlib.decompress(base64.b64decode('eJx1kcFOwzAMhu95ClMO66apu0/KAQEbE5eJC+IUpa27haVJ5Ljb+vakLYJx4JAoiT/7/+3c3626SKvSuBW6M4Sej96Jq9y1wRM/E3kSexnIOBZObrSNKI7Sl59YsWDq1wLMiEKNrenoYCqB1woDwzXF9nn2rskZd1jDh+9mhOD8DVvAQ8WdtrZfwg74aNwp7ZpnMXHUaltk878ybR/ZNKbSjP8JPWk6wdn72ntodQ8lQucIrdGlxaHgq3QgKqtjhCY/zlN6jQ0oZZxhpfKItlkuNB3icrE4XYbDwEBICRP6NjG1rri3YyzK356CtsGwZuNd/o0kYitvrBd18qgmj3kcwoTckYPtJPAyCVzSKPCMNErs85+rMINdp1tUSspMqVYbp1Q2DWKTJpcGURRDr9DIJs8wJFlKq+qzZRaQ4lAnVRuJgjFynj36Ol7SX/iQXr8ANfezCw==')))
# Created by pyminifier.py (https://github.com/liftoff/pyminifier)
This output corresponds to a 40-line original input script as shown here.
I would really recommend Nuitka over Cython. Nuitka also compiles Python to native platform code providing a similar level of obfuscation like compiled C code.
python -m pip install nuitka
python -m nuitka --follow-imports --include-package urllib3.util.ssl_ myprogram.py
./myprogram.bin
--follow-imports does a great job of including all imported modules.--include-package if some imports are hidden and are missing when starting the compiled program, it can be helpful to pass additional packages.Add the flags --onefile or -standalone if this works to get a package for distribution.
I also used pyarmor referenced here, but the pytransform.so or pytransform.dll shared object which is the core of pyarmor is closed source, which was a blocker in my project.
The best way to do this is to first generate a .c file, and then compile it with tcc to a .pyd file
Note: Windows-onlyRequirements
Install:
sudo pip install -U cython
To obfuscate your .py file:
pyobfuscate.py myfile.py >obfuscated.py
To generate a .c file,
Add an Optionalinit<filename>() function to your .py file
cython --embed file.py
cp Python.h tcc\include
tcc file.c -o file.pyd -shared -I\path\to\Python\include -L\path\to\Python\lib
import .pyd file into app.exe
I know it is an old question. Just want to add my funny obfuscated "Hello world!" in Python 3 and some tips ;)
#//'written in c++'
#include <iostream.h>
#define true false
import os
n = int(input())
_STACK_CALS= [ ];
_i_CountCals__= (0x00)
while os.urandom(0x00 >> 0x01) or (1 & True):
_i_CountCals__+= 0o0;break;# call shell command echo "hello world" > text.txt
""#print'hello'
__cal__= getattr( __builtins__ ,'c_DATATYPE_hFILE_radnom'[ 0x00 ]+'.h'[-1]+'getRndint'[3].lower() )
_o0wiXSysRdrct =eval ( __cal__(0x63) + __cal__(104) + 'r_RUN_CALLER'[0] );
_i1CLS_NATIVE= getattr (__builtins__ ,__cal__(101)+__cal__(118 )+_o0wiXSysRdrct ( 0b1100001 )+'LINE 2'[0].lower( ))#line 2 kernel call
__executeMAIN_0x07453320abef =_i1CLS_NATIVE ( 'map');
def _Main():
raise 0x06;return 0 # exit program with exit code 0
def _0o7af():_i1CLS_NATIVE('_int'.replace('_', 'programMain'[:2]))(''.join( __executeMAIN_0x07453320abef( _o0wiXSysRdrct ,_STACK_CALS)));return;_Main()
for _INCREAMENT in [0]*1024:
_STACK_CALS= [0x000 >> 0x001 ,True&False&True&False ,'c++', 'h', 'e', 'l', 'o',' ', 'w', 'o', 'r', 'l', 'd']
#if
for _INCREAMENT in [0]*1024:
_STACK_CALS= [40, 111, 41, 46, 46] * n
""""""#print'word'
while True:
break;
_0o7af();
while os.urandom(0x00 >> 0xfa) or (1 & True): # print "Hello, world!"
_i_CountCals__-= 0o0;break;
while os.urandom(0x00 >> 0x01) or (1 & True):
_i_CountCals__ += 0o0;
break;
It is possible to do manually, my tips are:
use eval and/or exec with encrypted strings
use [ord(i) for i in s] / ''.join(map(chr, [list of chars goes here])) as simple encryption/decryption
use obscure variable names
make it unreadable
Don't write just 1 or True, write 1&True&0x00000001 ;)
use different number systems
add confusing comments like "line 2" on line 10 or "it returns 0" on while loop.
use __builtins__
use getattr and setattr
Opy
Opy will obfuscate your extensive, real world, multi module Python source code for free! And YOU choose per project what to obfuscate and what not, by editing the config file:
You can recursively exclude all identifiers of certain modules from obfuscation. You can exclude human readable configuration files containing Python code. You can use getattr, setattr, exec and eval by excluding the identifiers they use. You can even obfuscate module file names and string literals. You can run your obfuscated code from any platform.
Unlike some of the other options posted, this works for both Python 2 and 3! It is also free / opensource, and it is not an online only tool (unless you pay) like some of the others out there.
I am admittedly still evaluating this myself, but all of initial tests of it worked perfectly. It appears this is exactly what I was looking for!
The official version runs as a standalone utility, with the original intended design being that you drop a script into the root of the directory you want to obfuscate, along with a config file to define the details/options you want to employ. I wasn't in love with that plan, so I added a fork from project, allowing you to import and utilize the tool from a library instead. That way, you can roll this directly into a more encompassing packaging script. (You could of course wrap multiple py scripts in bash/batch, but I think a pure python solution is ideal). I requested my fork be merged into the original work, but in case that never happens, here's the url to my revised version:
Here's my very noob approach for something I'm doing in CircuitPython. It's currently partially tested. I've posted in this state because I thought it might be useful.
There are two arguments:
Here's what it does:
It turns code like this
degreeIncrement = 90
durationIncrement = 0.25
def GetEditGlyphParams(self, waveform, editIndex):
segments = waveform.leftSegments
waveformFunctionCount = len(self.waveformFunctions)
totalParameterCount = 0
segmentIndex = 0
while segmentIndex < len(segments):
segment = segments[segmentIndex]
segmentParameterCount = len(self.sineFunctions)
if segment.type == "line":
segmentParameterCount = len(self.lineFunctions)
...into code like this:
a6 = 90 # degreeIncrement = 90
a7 = 0.25 # durationIncrement = 0.25
def a8(a9, a10, a11): # def GetEditGlyphParams(self, waveform, editIndex):
a12 = a10.leftSegments # segments = waveform.leftSegments
a13 = len(a9.a5) # waveformFunctionCount = len(self.waveformFunctions)
a14 = 0 # totalParameterCount = 0
a15 = 0 # segmentIndex = 0
while a15 < len(a12): # while segmentIndex < len(segments):
a16 = a12[a15] # segment = segments[segmentIndex]
a17 = len(a9.a3) # segmentParameterCount = len(self.sineFunctions)
if a16.a332 == "line": # if segment.type == "line":
a17 = len(a9.a4) # segmentParameterCount = len(self.lineFunctions)
The comments can be omitted if necessary.
Here's the code that does it:
import sys, re
sourceDirectory = sys.argv[1]
print("sourceDirectory", sourceDirectory)
sourceFiles = sys.argv[2].split(",")
targetFiles = sys.argv[3].split(",")
if len(sourceFiles) != len(targetFiles):
raise Exception("Source file count must match target file count. Use comma to separate.")
print("uglify", sys.argv[1])
names = []
translations = []
class Analyser:
def AnalyseLines(self, lines):
for line in lines:
self._AnalyseLine(line)
def _AnalyseLine(self, line):
parts = self._GetParts(line)
if len(parts) > 1 and parts[0] == "import":
self._AnalyseImport(parts)
if len(parts) > 1 and parts[0] == "class":
self._AnalyseClass(parts)
if len(parts) > 1 and parts[1] == "=":
self._AnalyseAssignment(parts)
if len(parts) > 1 and parts[0] == "def":
self._AnalyseDef(parts)
def _GetParts(self, line):
minusTabs = line.strip().replace("\t", " ")
minusOpenSquare = minusTabs.replace("[", " ")
minusCloseSquare = minusOpenSquare.replace("]", " ")
minusDoubleSpace = minusCloseSquare.replace(" ", " ")
parts = minusDoubleSpace.split(" ")
while "#" in parts:
del parts[-1]
while len(parts) > 0 and parts[0] == "":
del parts[0]
nonEmptyParts = []
for part in parts:
if len(part) > 0:
nonEmptyParts.append(part)
return nonEmptyParts
def _AddName(self, name, elementType):
nameToAppend = name # + " " + elementType
if nameToAppend in names:
return
if nameToAppend == "sin" or nameToAppend == "value":
print("--> adding", nameToAppend, "as", elementType)
names.append(nameToAppend)
translation = "a" + str(len(names))
translations.append((name, translation))
def _AnalyseImport(self, parts):
if len(parts) == 4 and parts[0] == "import" and parts[2] == "as":
self._AddName(parts[3], "import")
def _AnalyseClass(self, parts):
p1 = parts[1].split(":")
p2 = p1[0].split("(")
self._AddName(p2[0], "class")
def _AnalyseAssignment(self, parts):
mutableName = parts[0].split(".")[0]
self._AddName(mutableName, "assignment")
def _AnalyseDef(self, parts):
methodNameParts = parts[1].split("(")
if methodNameParts[0] == "__init__":
return
self._AddName(methodNameParts[0], "method")
if len(methodNameParts) > 1:
self._AddName(methodNameParts[1].replace(",", "").replace("):", ""), "param1")
for part in parts[2:]:
params = part.split(",")
for param in params:
if param != "":
if param.replace(":", "").replace(")", "") == "value":
print("found value amongst", parts)
self._AddName(param.replace(":", "").replace(")", ""), "paramN")
class Translator:
def TranslateLines(self, content):
oldLines = content.split("\n")
content = content.replace('"', "_QUOTE_").replace("\\", "_BACKSLASH_")
for (oldWord, newWord) in translations:
content = re.sub(r"\b%s\b" % oldWord, newWord, content)
content = content.replace("_QUOTE_", '"').replace("_BACKSLASH_", "\\")
newLines = content.split("\n")
for i in range(len(newLines) - 1):
if newLines[i] != "":
newLines[i] += " # " + oldLines[i].strip()
return "\n".join(newLines)
def TranslateLines2(self, content):
oldLines = content.split("\n")
newLines = []
for lineNumber, oldLine in enumerate(oldLines):
# print("translating line of length", len(oldLine), ":", oldLine)
content = oldLine.split(" # ")[0]
if len(content.strip(" \t")) > 0:
content = content.replace('"', "_QUOTE_").replace("\\", "_BACKSLASH_")
for (oldWord, newWord) in translations:
try:
content = re.sub(r"\b%s\b" % oldWord, newWord, content)
except:
print("problem translating", oldWord, "into", newWord)
raise Exception("error in translation")
content = content.replace("_QUOTE_", '"').replace("_BACKSLASH_", "\\")
newLines.append(content + " # " + oldLine.strip())
return "\n".join(newLines)
lines = []
for i, sourceFileName in enumerate(sourceFiles):
names.append(sourceFileName)
targetFileName = targetFiles[i]
translations.append((sourceFileName, targetFileName))
for sourceFileName in sourceFiles:
fullFileName = sourceDirectory + sourceFileName + ".py"
sourceFile = open(fullFileName, 'r')
content = sourceFile.read()
fileLines = content.split("\n")
lines.extend(fileLines)
print("found", len(fileLines), "lines in", sourceFileName)
print("----------------")
print("found a total of", len(lines), "lines")
print("----------------")
analyser = Analyser()
analyser.AnalyseLines(lines)
for i, name in enumerate(names):
if len(name) < 1:
print("deleting name", i, "because it is zero length")
names.remove(name)
translation = translations[i]
translations.remove(translation)
# print(names)
# raise Exception("Not implemented beyond here.")
translator = Translator()
for i, sourceFileName in enumerate(sourceFiles):
print("translating", sourceFileName, "into", targetFiles[i])
fullFileName = sourceDirectory + sourceFileName + ".py"
targetFileName = sourceDirectory + targetFiles[i] + ".py"
sourceFile = open(fullFileName, 'r')
content = sourceFile.read()
targetFile = open(targetFileName, 'w')
fileLines = content.split("\n")
newContent = translator.TranslateLines2(content)
targetFile.write(newContent)
sourceFile.close()
targetFile.close()
# print(len(lines), "lines, starting with", lines[0])
# print(names)
# print(translations)