├── .gitignore ├── Makefile.template ├── README.md ├── app ├── __init__.py ├── app.py ├── manage.py ├── models │ └── __init__.py ├── requirements.txt ├── routes │ ├── __init__.py │ ├── main.py │ └── utils.py ├── static │ └── js │ │ └── calc.js └── templates │ ├── base.html │ └── main.html ├── ast ├── hiding_strings │ ├── Makefile │ ├── README.md │ ├── encode.py │ ├── tree_parser.py │ └── unparse.py └── scrambling_code │ ├── Makefile │ ├── README.md │ ├── ihook.py │ ├── manage.py │ ├── scramble.py │ ├── scrambler.py │ ├── unparse.py │ └── unscramble.py ├── bytecode ├── pyc │ ├── Makefile │ └── README.md └── pyo │ ├── Makefile │ └── README.md ├── custom_interpreter └── opcodes │ ├── .python-version │ ├── 2.7.9-custom │ ├── Makefile │ ├── README.md │ ├── patches │ ├── .empty │ └── 001_scrambled-opcodes.diff │ └── scramble-opcodes.py ├── cython ├── Makefile └── README.md └── import_hook ├── compiled ├── Makefile ├── README.md ├── ihook.py ├── m.py └── requirements.txt └── python ├── Makefile ├── README.md ├── ihook.py ├── m.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | 43 | # Translations 44 | *.mo 45 | *.pot 46 | 47 | # Django stuff: 48 | *.log 49 | 50 | # Sphinx documentation 51 | docs/_build/ 52 | 53 | # PyBuilder 54 | target/ 55 | 56 | */app 57 | */*/app 58 | custom_interpreter/opcodes/Python-2.7.9* 59 | custom_interpreter/opcodes/scrambled-opcodes.diff 60 | # WIP, temp ignore 61 | custom_interpreter/hardened/ 62 | -------------------------------------------------------------------------------- /Makefile.template: -------------------------------------------------------------------------------- 1 | PYTHON_FILES := $(shell find . -name "*.py" ! -name "__init__.py" ! -name "manage.py") 2 | 3 | all: $(PYTHON_FILES) 4 | 5 | clean-build: 6 | @ echo "Clean build files here" 7 | 8 | clean: 9 | @ rm -rf ./app 10 | 11 | run: clean 12 | @ cp -R ../app . 13 | @ find . -iname "*.py[co]" -delete 14 | @ $(MAKE) && $(MAKE) clean-build 15 | 16 | .DEFAULT: all 17 | .PHONY: all clean clean-build run 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python code Protection Mechanisms 2 | 3 | Playground for python based project obfuscation techniques. 4 | 5 | ## Description 6 | 7 | In this repository I'll demo and try the different options available at the 8 | moment to protect a codebase based on python in order to avoid snooping from 9 | third-parties, options like source obfuscation, byte-code distribution, etc 10 | will be implemented and reviewed, later combinations of several techniques will 11 | be tried too. 12 | 13 | ## Organization 14 | 15 | The target is the project at the [app](app) directory, then each method will 16 | reside in a directory with a ``Makefile`` that will setup and run the 17 | particular technique. Also a ``README.md`` should be available in each 18 | directory explaining the current option, highlighting its pro and cons. 19 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FormulaMonks/python-obfuscation/8fb9897702dc51af7fb164f3e6906dd3d656ec68/app/__init__.py -------------------------------------------------------------------------------- /app/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | 3 | from routes.main import main 4 | 5 | 6 | app = Flask(__name__) 7 | app.debug = True 8 | app.register_blueprint(main) 9 | 10 | 11 | if __name__ == '__main__': 12 | app.run() 13 | -------------------------------------------------------------------------------- /app/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from flask_script import Server, Manager, Shell 4 | 5 | from app import app 6 | 7 | 8 | manager = Manager(app) 9 | manager.add_command('runserver', Server()) 10 | manager.add_command('shell', Shell(make_context=lambda: { 11 | 'app': app, 12 | })) 13 | 14 | 15 | if __name__ == '__main__': 16 | manager.run() 17 | -------------------------------------------------------------------------------- /app/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FormulaMonks/python-obfuscation/8fb9897702dc51af7fb164f3e6906dd3d656ec68/app/models/__init__.py -------------------------------------------------------------------------------- /app/requirements.txt: -------------------------------------------------------------------------------- 1 | flask 2 | flask-script 3 | sqlalchemy 4 | cython 5 | numpy 6 | -------------------------------------------------------------------------------- /app/routes/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FormulaMonks/python-obfuscation/8fb9897702dc51af7fb164f3e6906dd3d656ec68/app/routes/__init__.py -------------------------------------------------------------------------------- /app/routes/main.py: -------------------------------------------------------------------------------- 1 | from numpy import matrix 2 | from flask import Blueprint, jsonify, request 3 | 4 | from utils import render_to 5 | 6 | 7 | main = Blueprint('calc', __name__) 8 | 9 | 10 | @main.route('/') 11 | @render_to('main.html') 12 | def calc_form(): 13 | pass 14 | 15 | 16 | @main.route('/calc') 17 | def calc(): 18 | matrix1 = matrix(request.args['m1'].encode('utf8')) 19 | matrix2 = matrix(request.args['m2'].encode('utf8')) 20 | out = {'result': str(matrix1 * matrix2)} 21 | return jsonify(out) 22 | -------------------------------------------------------------------------------- /app/routes/utils.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | 3 | from flask import render_template 4 | 5 | 6 | def render_to(tpl): 7 | def decorator(func): 8 | @wraps(func) 9 | def wrapper(*args, **kwargs): 10 | out = func(*args, **kwargs) or {} 11 | if isinstance(out, dict): 12 | out = render_template(tpl, **out) 13 | return out 14 | return wrapper 15 | return decorator 16 | -------------------------------------------------------------------------------- /app/static/js/calc.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | $(document).on('submit', '#mult', function (event) { 3 | event.preventDefault(); 4 | 5 | var $form = $(this); 6 | 7 | $.get('/calc', { 8 | m1: $form.find('#m1').val(), 9 | m2: $form.find('#m2').val() 10 | }, 'json').done(function (data, response, xhr) { 11 | console.log('data: ', data); 12 | $form.find('#result').html("
" + data.result + "
"); 13 | }); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /app/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Matrix multiplication 5 | 6 | 7 | 8 | 9 | {% block content %} 10 | {% endblock %} 11 | 12 | 13 | {% block scripts %} 14 | {% endblock %} 15 | 16 | 17 | -------------------------------------------------------------------------------- /app/templates/main.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |
5 |
6 | 7 | 8 |
9 | 10 |
11 | 12 | 13 |
14 | 15 |
16 | 17 |
18 | {% endblock %} 19 | 20 | {% block scripts %} 21 | 22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /ast/hiding_strings/Makefile: -------------------------------------------------------------------------------- 1 | INPUT_DIR := ../app 2 | OUTPUT_DIR := ./app 3 | 4 | all: encode 5 | 6 | encode: copy 7 | @ python encode.py $(OUTPUT_DIR) 8 | 9 | copy: clean 10 | @ cp -R $(INPUT_DIR) $(OUTPUT_DIR) 11 | 12 | clean: 13 | @ rm -rf $(OUTPUT_DIR) 14 | 15 | .DEFAULT: all 16 | .PHONY: encode copy clean 17 | -------------------------------------------------------------------------------- /ast/hiding_strings/README.md: -------------------------------------------------------------------------------- 1 | # Python code Protection Mechanisms - Hidding Strings 2 | 3 | Process an python application and encode strings by processing the walking 4 | through the Abstract Syntax Trees. 5 | 6 | 7 | ## Description 8 | 9 | In this example, we will be customizing the processing a python application by 10 | walking the [Abstract Syntax Trees](https://docs.python.org/2.7/library/ast.html) 11 | and replacing strings occurrences with encoded versions that automatically 12 | decode on runtime. This comes handy when using [Cython](http://cython.org/) to 13 | compile the application code to Python extensions, that way a simple 14 | ``strings`` command won't be very useful to extract strings in the lib. 15 | 16 | This example uses [unparse.py](http://svn.python.org/view/python/trunk/Demo/parser/unparse.py?view=markup). 17 | 18 | ## Pros 19 | 20 | 1. Walking the [AST](https://docs.python.org/2.7/library/ast.html) is a very 21 | powerful transformation tool, this example just do string replacement, but 22 | more advanced transformations are possible. 23 | 24 | ## Cons 25 | 26 | 1. This just encodes strings (the example uses ``rot13``, but a custom encoder 27 | or translation table can be implemented). This is just a hiding technique. 28 | -------------------------------------------------------------------------------- /ast/hiding_strings/encode.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import ast 4 | 5 | from tree_parser import EncodeStrings 6 | from unparse import Unparser 7 | 8 | 9 | for root, dirs, files in os.walk(sys.argv[1]): 10 | for file in files: 11 | if file.endswith('.py'): 12 | with open('{0}/{1}'.format(root, file), 'r') as f: 13 | code = ''.join(f.readlines()) 14 | with open('{0}/{1}'.format(root, file), 'w') as f: 15 | node = ast.parse(code) 16 | encoded = EncodeStrings().visit(node) 17 | Unparser(encoded, f) 18 | f.write('\n') 19 | -------------------------------------------------------------------------------- /ast/hiding_strings/tree_parser.py: -------------------------------------------------------------------------------- 1 | from ast import NodeTransformer, copy_location, Call, Str, Attribute 2 | 3 | 4 | class EncodeStrings(NodeTransformer): 5 | def visit_Str(self, node): 6 | return copy_location( 7 | Call( 8 | func=Attribute(attr='decode', 9 | value=Str(s=node.s.encode('rot13'))), 10 | args=[Str(s='rot13')], 11 | keywords=(), 12 | starargs=None, 13 | kwargs=None 14 | ), 15 | node 16 | ) 17 | -------------------------------------------------------------------------------- /ast/hiding_strings/unparse.py: -------------------------------------------------------------------------------- 1 | "Usage: unparse.py " 2 | import sys 3 | import ast 4 | import cStringIO 5 | import os 6 | 7 | # Large float and imaginary literals get turned into infinities in the AST. 8 | # We unparse those infinities to INFSTR. 9 | INFSTR = "1e" + repr(sys.float_info.max_10_exp + 1) 10 | 11 | def interleave(inter, f, seq): 12 | """Call f on each item in seq, calling inter() in between. 13 | """ 14 | seq = iter(seq) 15 | try: 16 | f(next(seq)) 17 | except StopIteration: 18 | pass 19 | else: 20 | for x in seq: 21 | inter() 22 | f(x) 23 | 24 | class Unparser: 25 | """Methods in this class recursively traverse an AST and 26 | output source code for the abstract syntax; original formatting 27 | is disregarded. """ 28 | 29 | def __init__(self, tree, file = sys.stdout): 30 | """Unparser(tree, file=sys.stdout) -> None. 31 | Print the source for tree to file.""" 32 | self.f = file 33 | self.future_imports = [] 34 | self._indent = 0 35 | self.dispatch(tree) 36 | self.f.write("") 37 | self.f.flush() 38 | 39 | def fill(self, text = ""): 40 | "Indent a piece of text, according to the current indentation level" 41 | self.f.write("\n"+" "*self._indent + text) 42 | 43 | def write(self, text): 44 | "Append a piece of text to the current line." 45 | self.f.write(text) 46 | 47 | def enter(self): 48 | "Print ':', and increase the indentation." 49 | self.write(":") 50 | self._indent += 1 51 | 52 | def leave(self): 53 | "Decrease the indentation level." 54 | self._indent -= 1 55 | 56 | def dispatch(self, tree): 57 | "Dispatcher function, dispatching tree type T to method _T." 58 | if isinstance(tree, list): 59 | for t in tree: 60 | self.dispatch(t) 61 | return 62 | meth = getattr(self, "_"+tree.__class__.__name__) 63 | meth(tree) 64 | 65 | 66 | ############### Unparsing methods ###################### 67 | # There should be one method per concrete grammar type # 68 | # Constructors should be grouped by sum type. Ideally, # 69 | # this would follow the order in the grammar, but # 70 | # currently doesn't. # 71 | ######################################################## 72 | 73 | def _Module(self, tree): 74 | for stmt in tree.body: 75 | self.dispatch(stmt) 76 | 77 | # stmt 78 | def _Expr(self, tree): 79 | self.fill() 80 | self.dispatch(tree.value) 81 | 82 | def _Import(self, t): 83 | self.fill("import ") 84 | interleave(lambda: self.write(", "), self.dispatch, t.names) 85 | 86 | def _ImportFrom(self, t): 87 | # A from __future__ import may affect unparsing, so record it. 88 | if t.module and t.module == '__future__': 89 | self.future_imports.extend(n.name for n in t.names) 90 | 91 | self.fill("from ") 92 | self.write("." * t.level) 93 | if t.module: 94 | self.write(t.module) 95 | self.write(" import ") 96 | interleave(lambda: self.write(", "), self.dispatch, t.names) 97 | 98 | def _Assign(self, t): 99 | self.fill() 100 | for target in t.targets: 101 | self.dispatch(target) 102 | self.write(" = ") 103 | self.dispatch(t.value) 104 | 105 | def _AugAssign(self, t): 106 | self.fill() 107 | self.dispatch(t.target) 108 | self.write(" "+self.binop[t.op.__class__.__name__]+"= ") 109 | self.dispatch(t.value) 110 | 111 | def _Return(self, t): 112 | self.fill("return") 113 | if t.value: 114 | self.write(" ") 115 | self.dispatch(t.value) 116 | 117 | def _Pass(self, t): 118 | self.fill("pass") 119 | 120 | def _Break(self, t): 121 | self.fill("break") 122 | 123 | def _Continue(self, t): 124 | self.fill("continue") 125 | 126 | def _Delete(self, t): 127 | self.fill("del ") 128 | interleave(lambda: self.write(", "), self.dispatch, t.targets) 129 | 130 | def _Assert(self, t): 131 | self.fill("assert ") 132 | self.dispatch(t.test) 133 | if t.msg: 134 | self.write(", ") 135 | self.dispatch(t.msg) 136 | 137 | def _Exec(self, t): 138 | self.fill("exec ") 139 | self.dispatch(t.body) 140 | if t.globals: 141 | self.write(" in ") 142 | self.dispatch(t.globals) 143 | if t.locals: 144 | self.write(", ") 145 | self.dispatch(t.locals) 146 | 147 | def _Print(self, t): 148 | self.fill("print ") 149 | do_comma = False 150 | if t.dest: 151 | self.write(">>") 152 | self.dispatch(t.dest) 153 | do_comma = True 154 | for e in t.values: 155 | if do_comma:self.write(", ") 156 | else:do_comma=True 157 | self.dispatch(e) 158 | if not t.nl: 159 | self.write(",") 160 | 161 | def _Global(self, t): 162 | self.fill("global ") 163 | interleave(lambda: self.write(", "), self.write, t.names) 164 | 165 | def _Yield(self, t): 166 | self.write("(") 167 | self.write("yield") 168 | if t.value: 169 | self.write(" ") 170 | self.dispatch(t.value) 171 | self.write(")") 172 | 173 | def _Raise(self, t): 174 | self.fill('raise ') 175 | if t.type: 176 | self.dispatch(t.type) 177 | if t.inst: 178 | self.write(", ") 179 | self.dispatch(t.inst) 180 | if t.tback: 181 | self.write(", ") 182 | self.dispatch(t.tback) 183 | 184 | def _TryExcept(self, t): 185 | self.fill("try") 186 | self.enter() 187 | self.dispatch(t.body) 188 | self.leave() 189 | 190 | for ex in t.handlers: 191 | self.dispatch(ex) 192 | if t.orelse: 193 | self.fill("else") 194 | self.enter() 195 | self.dispatch(t.orelse) 196 | self.leave() 197 | 198 | def _TryFinally(self, t): 199 | if len(t.body) == 1 and isinstance(t.body[0], ast.TryExcept): 200 | # try-except-finally 201 | self.dispatch(t.body) 202 | else: 203 | self.fill("try") 204 | self.enter() 205 | self.dispatch(t.body) 206 | self.leave() 207 | 208 | self.fill("finally") 209 | self.enter() 210 | self.dispatch(t.finalbody) 211 | self.leave() 212 | 213 | def _ExceptHandler(self, t): 214 | self.fill("except") 215 | if t.type: 216 | self.write(" ") 217 | self.dispatch(t.type) 218 | if t.name: 219 | self.write(" as ") 220 | self.dispatch(t.name) 221 | self.enter() 222 | self.dispatch(t.body) 223 | self.leave() 224 | 225 | def _ClassDef(self, t): 226 | self.write("\n") 227 | for deco in t.decorator_list: 228 | self.fill("@") 229 | self.dispatch(deco) 230 | self.fill("class "+t.name) 231 | if t.bases: 232 | self.write("(") 233 | for a in t.bases: 234 | self.dispatch(a) 235 | self.write(", ") 236 | self.write(")") 237 | self.enter() 238 | self.dispatch(t.body) 239 | self.leave() 240 | 241 | def _FunctionDef(self, t): 242 | self.write("\n") 243 | for deco in t.decorator_list: 244 | self.fill("@") 245 | self.dispatch(deco) 246 | self.fill("def "+t.name + "(") 247 | self.dispatch(t.args) 248 | self.write(")") 249 | self.enter() 250 | self.dispatch(t.body) 251 | self.leave() 252 | 253 | def _For(self, t): 254 | self.fill("for ") 255 | self.dispatch(t.target) 256 | self.write(" in ") 257 | self.dispatch(t.iter) 258 | self.enter() 259 | self.dispatch(t.body) 260 | self.leave() 261 | if t.orelse: 262 | self.fill("else") 263 | self.enter() 264 | self.dispatch(t.orelse) 265 | self.leave() 266 | 267 | def _If(self, t): 268 | self.fill("if ") 269 | self.dispatch(t.test) 270 | self.enter() 271 | self.dispatch(t.body) 272 | self.leave() 273 | # collapse nested ifs into equivalent elifs. 274 | while (t.orelse and len(t.orelse) == 1 and 275 | isinstance(t.orelse[0], ast.If)): 276 | t = t.orelse[0] 277 | self.fill("elif ") 278 | self.dispatch(t.test) 279 | self.enter() 280 | self.dispatch(t.body) 281 | self.leave() 282 | # final else 283 | if t.orelse: 284 | self.fill("else") 285 | self.enter() 286 | self.dispatch(t.orelse) 287 | self.leave() 288 | 289 | def _While(self, t): 290 | self.fill("while ") 291 | self.dispatch(t.test) 292 | self.enter() 293 | self.dispatch(t.body) 294 | self.leave() 295 | if t.orelse: 296 | self.fill("else") 297 | self.enter() 298 | self.dispatch(t.orelse) 299 | self.leave() 300 | 301 | def _With(self, t): 302 | self.fill("with ") 303 | self.dispatch(t.context_expr) 304 | if t.optional_vars: 305 | self.write(" as ") 306 | self.dispatch(t.optional_vars) 307 | self.enter() 308 | self.dispatch(t.body) 309 | self.leave() 310 | 311 | # expr 312 | def _Str(self, tree): 313 | # if from __future__ import unicode_literals is in effect, 314 | # then we want to output string literals using a 'b' prefix 315 | # and unicode literals with no prefix. 316 | if "unicode_literals" not in self.future_imports: 317 | self.write(repr(tree.s)) 318 | elif isinstance(tree.s, str): 319 | self.write("b" + repr(tree.s)) 320 | elif isinstance(tree.s, unicode): 321 | self.write(repr(tree.s).lstrip("u")) 322 | else: 323 | assert False, "shouldn't get here" 324 | 325 | def _Name(self, t): 326 | self.write(t.id) 327 | 328 | def _Repr(self, t): 329 | self.write("`") 330 | self.dispatch(t.value) 331 | self.write("`") 332 | 333 | def _Num(self, t): 334 | repr_n = repr(t.n) 335 | # Parenthesize negative numbers, to avoid turning (-1)**2 into -1**2. 336 | if repr_n.startswith("-"): 337 | self.write("(") 338 | # Substitute overflowing decimal literal for AST infinities. 339 | self.write(repr_n.replace("inf", INFSTR)) 340 | if repr_n.startswith("-"): 341 | self.write(")") 342 | 343 | def _List(self, t): 344 | self.write("[") 345 | interleave(lambda: self.write(", "), self.dispatch, t.elts) 346 | self.write("]") 347 | 348 | def _ListComp(self, t): 349 | self.write("[") 350 | self.dispatch(t.elt) 351 | for gen in t.generators: 352 | self.dispatch(gen) 353 | self.write("]") 354 | 355 | def _GeneratorExp(self, t): 356 | self.write("(") 357 | self.dispatch(t.elt) 358 | for gen in t.generators: 359 | self.dispatch(gen) 360 | self.write(")") 361 | 362 | def _SetComp(self, t): 363 | self.write("{") 364 | self.dispatch(t.elt) 365 | for gen in t.generators: 366 | self.dispatch(gen) 367 | self.write("}") 368 | 369 | def _DictComp(self, t): 370 | self.write("{") 371 | self.dispatch(t.key) 372 | self.write(": ") 373 | self.dispatch(t.value) 374 | for gen in t.generators: 375 | self.dispatch(gen) 376 | self.write("}") 377 | 378 | def _comprehension(self, t): 379 | self.write(" for ") 380 | self.dispatch(t.target) 381 | self.write(" in ") 382 | self.dispatch(t.iter) 383 | for if_clause in t.ifs: 384 | self.write(" if ") 385 | self.dispatch(if_clause) 386 | 387 | def _IfExp(self, t): 388 | self.write("(") 389 | self.dispatch(t.body) 390 | self.write(" if ") 391 | self.dispatch(t.test) 392 | self.write(" else ") 393 | self.dispatch(t.orelse) 394 | self.write(")") 395 | 396 | def _Set(self, t): 397 | assert(t.elts) # should be at least one element 398 | self.write("{") 399 | interleave(lambda: self.write(", "), self.dispatch, t.elts) 400 | self.write("}") 401 | 402 | def _Dict(self, t): 403 | self.write("{") 404 | def write_pair(pair): 405 | (k, v) = pair 406 | self.dispatch(k) 407 | self.write(": ") 408 | self.dispatch(v) 409 | interleave(lambda: self.write(", "), write_pair, zip(t.keys, t.values)) 410 | self.write("}") 411 | 412 | def _Tuple(self, t): 413 | self.write("(") 414 | if len(t.elts) == 1: 415 | (elt,) = t.elts 416 | self.dispatch(elt) 417 | self.write(",") 418 | else: 419 | interleave(lambda: self.write(", "), self.dispatch, t.elts) 420 | self.write(")") 421 | 422 | unop = {"Invert":"~", "Not": "not", "UAdd":"+", "USub":"-"} 423 | def _UnaryOp(self, t): 424 | self.write("(") 425 | self.write(self.unop[t.op.__class__.__name__]) 426 | self.write(" ") 427 | # If we're applying unary minus to a number, parenthesize the number. 428 | # This is necessary: -2147483648 is different from -(2147483648) on 429 | # a 32-bit machine (the first is an int, the second a long), and 430 | # -7j is different from -(7j). (The first has real part 0.0, the second 431 | # has real part -0.0.) 432 | if isinstance(t.op, ast.USub) and isinstance(t.operand, ast.Num): 433 | self.write("(") 434 | self.dispatch(t.operand) 435 | self.write(")") 436 | else: 437 | self.dispatch(t.operand) 438 | self.write(")") 439 | 440 | binop = { "Add":"+", "Sub":"-", "Mult":"*", "Div":"/", "Mod":"%", 441 | "LShift":"<<", "RShift":">>", "BitOr":"|", "BitXor":"^", "BitAnd":"&", 442 | "FloorDiv":"//", "Pow": "**"} 443 | def _BinOp(self, t): 444 | self.write("(") 445 | self.dispatch(t.left) 446 | self.write(" " + self.binop[t.op.__class__.__name__] + " ") 447 | self.dispatch(t.right) 448 | self.write(")") 449 | 450 | cmpops = {"Eq":"==", "NotEq":"!=", "Lt":"<", "LtE":"<=", "Gt":">", "GtE":">=", 451 | "Is":"is", "IsNot":"is not", "In":"in", "NotIn":"not in"} 452 | def _Compare(self, t): 453 | self.write("(") 454 | self.dispatch(t.left) 455 | for o, e in zip(t.ops, t.comparators): 456 | self.write(" " + self.cmpops[o.__class__.__name__] + " ") 457 | self.dispatch(e) 458 | self.write(")") 459 | 460 | boolops = {ast.And: 'and', ast.Or: 'or'} 461 | def _BoolOp(self, t): 462 | self.write("(") 463 | s = " %s " % self.boolops[t.op.__class__] 464 | interleave(lambda: self.write(s), self.dispatch, t.values) 465 | self.write(")") 466 | 467 | def _Attribute(self,t): 468 | self.dispatch(t.value) 469 | # Special case: 3.__abs__() is a syntax error, so if t.value 470 | # is an integer literal then we need to either parenthesize 471 | # it or add an extra space to get 3 .__abs__(). 472 | if isinstance(t.value, ast.Num) and isinstance(t.value.n, int): 473 | self.write(" ") 474 | self.write(".") 475 | self.write(t.attr) 476 | 477 | def _Call(self, t): 478 | self.dispatch(t.func) 479 | self.write("(") 480 | comma = False 481 | for e in t.args: 482 | if comma: self.write(", ") 483 | else: comma = True 484 | self.dispatch(e) 485 | for e in t.keywords: 486 | if comma: self.write(", ") 487 | else: comma = True 488 | self.dispatch(e) 489 | if t.starargs: 490 | if comma: self.write(", ") 491 | else: comma = True 492 | self.write("*") 493 | self.dispatch(t.starargs) 494 | if t.kwargs: 495 | if comma: self.write(", ") 496 | else: comma = True 497 | self.write("**") 498 | self.dispatch(t.kwargs) 499 | self.write(")") 500 | 501 | def _Subscript(self, t): 502 | self.dispatch(t.value) 503 | self.write("[") 504 | self.dispatch(t.slice) 505 | self.write("]") 506 | 507 | # slice 508 | def _Ellipsis(self, t): 509 | self.write("...") 510 | 511 | def _Index(self, t): 512 | self.dispatch(t.value) 513 | 514 | def _Slice(self, t): 515 | if t.lower: 516 | self.dispatch(t.lower) 517 | self.write(":") 518 | if t.upper: 519 | self.dispatch(t.upper) 520 | if t.step: 521 | self.write(":") 522 | self.dispatch(t.step) 523 | 524 | def _ExtSlice(self, t): 525 | interleave(lambda: self.write(', '), self.dispatch, t.dims) 526 | 527 | # others 528 | def _arguments(self, t): 529 | first = True 530 | # normal arguments 531 | defaults = [None] * (len(t.args) - len(t.defaults)) + t.defaults 532 | for a,d in zip(t.args, defaults): 533 | if first:first = False 534 | else: self.write(", ") 535 | self.dispatch(a), 536 | if d: 537 | self.write("=") 538 | self.dispatch(d) 539 | 540 | # varargs 541 | if t.vararg: 542 | if first:first = False 543 | else: self.write(", ") 544 | self.write("*") 545 | self.write(t.vararg) 546 | 547 | # kwargs 548 | if t.kwarg: 549 | if first:first = False 550 | else: self.write(", ") 551 | self.write("**"+t.kwarg) 552 | 553 | def _keyword(self, t): 554 | self.write(t.arg) 555 | self.write("=") 556 | self.dispatch(t.value) 557 | 558 | def _Lambda(self, t): 559 | self.write("(") 560 | self.write("lambda ") 561 | self.dispatch(t.args) 562 | self.write(": ") 563 | self.dispatch(t.body) 564 | self.write(")") 565 | 566 | def _alias(self, t): 567 | self.write(t.name) 568 | if t.asname: 569 | self.write(" as "+t.asname) 570 | 571 | def roundtrip(filename, output=sys.stdout): 572 | with open(filename, "r") as pyfile: 573 | source = pyfile.read() 574 | tree = compile(source, filename, "exec", ast.PyCF_ONLY_AST) 575 | Unparser(tree, output) 576 | 577 | 578 | 579 | def testdir(a): 580 | try: 581 | names = [n for n in os.listdir(a) if n.endswith('.py')] 582 | except OSError: 583 | sys.stderr.write("Directory not readable: %s" % a) 584 | else: 585 | for n in names: 586 | fullname = os.path.join(a, n) 587 | if os.path.isfile(fullname): 588 | output = cStringIO.StringIO() 589 | print 'Testing %s' % fullname 590 | try: 591 | roundtrip(fullname, output) 592 | except Exception as e: 593 | print ' Failed to compile, exception is %s' % repr(e) 594 | elif os.path.isdir(fullname): 595 | testdir(fullname) 596 | 597 | def main(args): 598 | if args[0] == '--testdir': 599 | for a in args[1:]: 600 | testdir(a) 601 | else: 602 | for a in args: 603 | roundtrip(a) 604 | 605 | if __name__=='__main__': 606 | main(sys.argv[1:]) 607 | -------------------------------------------------------------------------------- /ast/scrambling_code/Makefile: -------------------------------------------------------------------------------- 1 | INPUT_DIR := ../app 2 | OUTPUT_DIR := ./app 3 | 4 | all: scramble 5 | 6 | scramble: copy 7 | @ python scramble.py $(OUTPUT_DIR) 8 | @ cp ihook.py scrambler.py manage.py $(OUTPUT_DIR) 9 | 10 | unscramble: 11 | @ python unscramble.py $(OUTPUT_DIR) 12 | 13 | copy: clean 14 | @ cp -R $(INPUT_DIR) $(OUTPUT_DIR) 15 | 16 | clean: 17 | @ rm -rf $(OUTPUT_DIR) 18 | 19 | .DEFAULT: all 20 | .PHONY: unscramble scramble copy clean 21 | -------------------------------------------------------------------------------- /ast/scrambling_code/README.md: -------------------------------------------------------------------------------- 1 | # Python code Protection Mechanisms - Code Scrambling 2 | 3 | This mechanism will make use of Python [Abstract Syntax Trees](https://docs.python.org/2/library/ast.html) 4 | and an [import hook](https://www.python.org/dev/peps/pep-0302/) in order to 5 | scramble code and unscramble it on import time. 6 | 7 | 8 | ## Description 9 | 10 | Python [Abstract Syntax Trees](https://docs.python.org/2/library/ast.html) is 11 | a tool to process of Python abstract syntax grammar, which can be modified on 12 | any way before the code is evaluated by the interpreter. 13 | 14 | In this case the code will be scrambled and written back to the file system. 15 | Later the application will configure an import hook to detect scrambled code 16 | and unscramble it on import time. 17 | 18 | The scrambling algorithm is fair simple, but good enough for this example. 19 | 20 | ## Pros 21 | 22 | 1. Simple to implement, scrambling algorithms can vary to make them more 23 | complicated to crack. 24 | 25 | ## Cons 26 | 27 | 1. The unscrambling code must be available in the server in order to run the 28 | application (the module can be compiled with Cython or implemented as python 29 | extension). 30 | 31 | 2. The decrypted code in memory can be inspected and dumped and decompiled from 32 | byte-code back to python. 33 | -------------------------------------------------------------------------------- /ast/scrambling_code/ihook.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import imp 4 | import ast 5 | 6 | from scrambler import Scrambler 7 | 8 | 9 | class BaseLoader(object): 10 | def modinfo(self, name, path): 11 | try: 12 | modinfo = imp.find_module(name.rsplit('.', 1)[-1], path) 13 | except ImportError: 14 | if '.' not in name: 15 | raise 16 | clean_path, clean_name = name.rsplit('.', 1) 17 | clean_path = [clean_path.replace('.', '/')] 18 | modinfo = imp.find_module(clean_name, clean_path) 19 | 20 | file, pathname, (suffix, mode, type_) = modinfo 21 | if type_ == imp.PY_SOURCE: 22 | filename = pathname 23 | elif type_ == imp.PY_COMPILED: 24 | filename = pathname[:-1] 25 | elif type_ == imp.PKG_DIRECTORY: 26 | filename = os.path.join(pathname, '__init__.py') 27 | else: 28 | return (None, None) 29 | return (filename, modinfo) 30 | 31 | 32 | class Loader(BaseLoader): 33 | def __init__(self, name, path): 34 | self.name = name 35 | self.path = path 36 | 37 | def is_package(self, val): 38 | """Flask requirement""" 39 | return None 40 | 41 | def load_module(self, name): 42 | if name in sys.modules: 43 | return sys.modules[name] 44 | 45 | filename, modinfo = self.modinfo(self.name, self.path) 46 | if filename is None and modinfo is None: 47 | return None 48 | 49 | file = modinfo[0] or open(filename, 'r') 50 | src = ''.join(file.readlines()) 51 | src = self.unscramble(src) 52 | src = ast.fix_missing_locations(src) 53 | 54 | module = imp.new_module(name) 55 | module.__file__ = filename 56 | module.__path__ = [os.path.dirname(os.path.abspath(file.name))] 57 | module.__loader__ = self 58 | sys.modules[name] = module 59 | codeobj = compile(src, name, 'exec') 60 | eval(codeobj, module.__dict__, module.__dict__) 61 | print 'scrambled module loaded: {0}'.format(name) 62 | return module 63 | 64 | def unscramble(self, code): 65 | node = ast.parse(code) 66 | code = Scrambler(scramble=False).visit(node) 67 | return code 68 | 69 | 70 | class Finder(BaseLoader): 71 | def find_module(self, name, path=None): 72 | filename, modinfo = self.modinfo(name, path) 73 | if filename is None and modinfo is None: 74 | return None 75 | 76 | file = modinfo[0] or open(filename, 'r') 77 | if file.read(len(Scrambler.HEADER)) == Scrambler.HEADER: 78 | print 'scrambled module found: {0} (at {1})'.format(name, path) 79 | file.seek(0) 80 | return Loader(name, path) 81 | 82 | 83 | def install_hook(): 84 | sys.meta_path.insert(0, Finder()) 85 | -------------------------------------------------------------------------------- /ast/scrambling_code/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Install import hook 4 | import ihook 5 | ihook.install_hook() 6 | 7 | from flask_script import Server, Manager, Shell 8 | 9 | from app import app 10 | 11 | 12 | manager = Manager(app) 13 | manager.add_command('runserver', Server()) 14 | manager.add_command('shell', Shell(make_context=lambda: { 15 | 'app': app, 16 | })) 17 | 18 | 19 | if __name__ == '__main__': 20 | manager.run() 21 | -------------------------------------------------------------------------------- /ast/scrambling_code/scramble.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import ast 4 | 5 | from scrambler import Scrambler 6 | from unparse import Unparser 7 | 8 | 9 | for root, dirs, files in os.walk(sys.argv[1]): 10 | for file in files: 11 | if file.endswith('.py'): 12 | with open('{0}/{1}'.format(root, file), 'r') as f: 13 | code = ''.join(f.readlines()) 14 | if code: 15 | with open('{0}/{1}'.format(root, file), 'w') as f: 16 | node = ast.parse(code) 17 | encoded = Scrambler(scramble=True).visit(node) 18 | f.write('{0}\n'.format(Scrambler.HEADER)) 19 | Unparser(encoded, f) 20 | f.write('\n') 21 | -------------------------------------------------------------------------------- /ast/scrambling_code/scrambler.py: -------------------------------------------------------------------------------- 1 | from ast import NodeTransformer 2 | 3 | 4 | class Scrambler(NodeTransformer): 5 | HEADER = '# scrambled' 6 | 7 | def __init__(self, scramble=True): 8 | self.do_scramble = scramble 9 | 10 | def visit(self, node): 11 | node_out = super(Scrambler, self).visit(node) 12 | if hasattr(node_out, 'body') and isinstance(node_out.body, list): 13 | if self.do_scramble: 14 | node_out.body = self.scramble(node_out.body) 15 | else: 16 | node_out.body = self.unscramble(node_out.body) 17 | return node_out 18 | 19 | def scramble(self, items): 20 | return self._step2(self._step1(items[:])) 21 | 22 | def unscramble(self, items): 23 | return self._step1(self._step2(items[:])) 24 | 25 | def _step1(self, items): 26 | i = 0 27 | length = len(items) 28 | while (i + 1) < length: 29 | items[i], items[i + 1] = items[i + 1], items[i] 30 | i += 2 31 | return items 32 | 33 | def _step2(self, items): 34 | length = len(items) 35 | if length % 2 == 0: 36 | items[:length / 2], items[length / 2:] = \ 37 | items[length / 2:], items[:length / 2] 38 | else: 39 | items[:(length - 1) / 2], items[(length + 1) / 2:] = \ 40 | items[(length + 1) / 2:], items[:(length - 1) / 2] 41 | return items 42 | -------------------------------------------------------------------------------- /ast/scrambling_code/unparse.py: -------------------------------------------------------------------------------- 1 | "Usage: unparse.py " 2 | import sys 3 | import ast 4 | import cStringIO 5 | import os 6 | 7 | # Large float and imaginary literals get turned into infinities in the AST. 8 | # We unparse those infinities to INFSTR. 9 | INFSTR = "1e" + repr(sys.float_info.max_10_exp + 1) 10 | 11 | def interleave(inter, f, seq): 12 | """Call f on each item in seq, calling inter() in between. 13 | """ 14 | seq = iter(seq) 15 | try: 16 | f(next(seq)) 17 | except StopIteration: 18 | pass 19 | else: 20 | for x in seq: 21 | inter() 22 | f(x) 23 | 24 | class Unparser: 25 | """Methods in this class recursively traverse an AST and 26 | output source code for the abstract syntax; original formatting 27 | is disregarded. """ 28 | 29 | def __init__(self, tree, file = sys.stdout): 30 | """Unparser(tree, file=sys.stdout) -> None. 31 | Print the source for tree to file.""" 32 | self.f = file 33 | self.future_imports = [] 34 | self._indent = 0 35 | self.dispatch(tree) 36 | self.f.write("") 37 | self.f.flush() 38 | 39 | def fill(self, text = ""): 40 | "Indent a piece of text, according to the current indentation level" 41 | self.f.write("\n"+" "*self._indent + text) 42 | 43 | def write(self, text): 44 | "Append a piece of text to the current line." 45 | self.f.write(text) 46 | 47 | def enter(self): 48 | "Print ':', and increase the indentation." 49 | self.write(":") 50 | self._indent += 1 51 | 52 | def leave(self): 53 | "Decrease the indentation level." 54 | self._indent -= 1 55 | 56 | def dispatch(self, tree): 57 | "Dispatcher function, dispatching tree type T to method _T." 58 | if isinstance(tree, list): 59 | for t in tree: 60 | self.dispatch(t) 61 | return 62 | meth = getattr(self, "_"+tree.__class__.__name__) 63 | meth(tree) 64 | 65 | 66 | ############### Unparsing methods ###################### 67 | # There should be one method per concrete grammar type # 68 | # Constructors should be grouped by sum type. Ideally, # 69 | # this would follow the order in the grammar, but # 70 | # currently doesn't. # 71 | ######################################################## 72 | 73 | def _Module(self, tree): 74 | for stmt in tree.body: 75 | self.dispatch(stmt) 76 | 77 | # stmt 78 | def _Expr(self, tree): 79 | self.fill() 80 | self.dispatch(tree.value) 81 | 82 | def _Import(self, t): 83 | self.fill("import ") 84 | interleave(lambda: self.write(", "), self.dispatch, t.names) 85 | 86 | def _ImportFrom(self, t): 87 | # A from __future__ import may affect unparsing, so record it. 88 | if t.module and t.module == '__future__': 89 | self.future_imports.extend(n.name for n in t.names) 90 | 91 | self.fill("from ") 92 | self.write("." * t.level) 93 | if t.module: 94 | self.write(t.module) 95 | self.write(" import ") 96 | interleave(lambda: self.write(", "), self.dispatch, t.names) 97 | 98 | def _Assign(self, t): 99 | self.fill() 100 | for target in t.targets: 101 | self.dispatch(target) 102 | self.write(" = ") 103 | self.dispatch(t.value) 104 | 105 | def _AugAssign(self, t): 106 | self.fill() 107 | self.dispatch(t.target) 108 | self.write(" "+self.binop[t.op.__class__.__name__]+"= ") 109 | self.dispatch(t.value) 110 | 111 | def _Return(self, t): 112 | self.fill("return") 113 | if t.value: 114 | self.write(" ") 115 | self.dispatch(t.value) 116 | 117 | def _Pass(self, t): 118 | self.fill("pass") 119 | 120 | def _Break(self, t): 121 | self.fill("break") 122 | 123 | def _Continue(self, t): 124 | self.fill("continue") 125 | 126 | def _Delete(self, t): 127 | self.fill("del ") 128 | interleave(lambda: self.write(", "), self.dispatch, t.targets) 129 | 130 | def _Assert(self, t): 131 | self.fill("assert ") 132 | self.dispatch(t.test) 133 | if t.msg: 134 | self.write(", ") 135 | self.dispatch(t.msg) 136 | 137 | def _Exec(self, t): 138 | self.fill("exec ") 139 | self.dispatch(t.body) 140 | if t.globals: 141 | self.write(" in ") 142 | self.dispatch(t.globals) 143 | if t.locals: 144 | self.write(", ") 145 | self.dispatch(t.locals) 146 | 147 | def _Print(self, t): 148 | self.fill("print ") 149 | do_comma = False 150 | if t.dest: 151 | self.write(">>") 152 | self.dispatch(t.dest) 153 | do_comma = True 154 | for e in t.values: 155 | if do_comma:self.write(", ") 156 | else:do_comma=True 157 | self.dispatch(e) 158 | if not t.nl: 159 | self.write(",") 160 | 161 | def _Global(self, t): 162 | self.fill("global ") 163 | interleave(lambda: self.write(", "), self.write, t.names) 164 | 165 | def _Yield(self, t): 166 | self.write("(") 167 | self.write("yield") 168 | if t.value: 169 | self.write(" ") 170 | self.dispatch(t.value) 171 | self.write(")") 172 | 173 | def _Raise(self, t): 174 | self.fill('raise ') 175 | if t.type: 176 | self.dispatch(t.type) 177 | if t.inst: 178 | self.write(", ") 179 | self.dispatch(t.inst) 180 | if t.tback: 181 | self.write(", ") 182 | self.dispatch(t.tback) 183 | 184 | def _TryExcept(self, t): 185 | self.fill("try") 186 | self.enter() 187 | self.dispatch(t.body) 188 | self.leave() 189 | 190 | for ex in t.handlers: 191 | self.dispatch(ex) 192 | if t.orelse: 193 | self.fill("else") 194 | self.enter() 195 | self.dispatch(t.orelse) 196 | self.leave() 197 | 198 | def _TryFinally(self, t): 199 | if len(t.body) == 1 and isinstance(t.body[0], ast.TryExcept): 200 | # try-except-finally 201 | self.dispatch(t.body) 202 | else: 203 | self.fill("try") 204 | self.enter() 205 | self.dispatch(t.body) 206 | self.leave() 207 | 208 | self.fill("finally") 209 | self.enter() 210 | self.dispatch(t.finalbody) 211 | self.leave() 212 | 213 | def _ExceptHandler(self, t): 214 | self.fill("except") 215 | if t.type: 216 | self.write(" ") 217 | self.dispatch(t.type) 218 | if t.name: 219 | self.write(" as ") 220 | self.dispatch(t.name) 221 | self.enter() 222 | self.dispatch(t.body) 223 | self.leave() 224 | 225 | def _ClassDef(self, t): 226 | self.write("\n") 227 | for deco in t.decorator_list: 228 | self.fill("@") 229 | self.dispatch(deco) 230 | self.fill("class "+t.name) 231 | if t.bases: 232 | self.write("(") 233 | for a in t.bases: 234 | self.dispatch(a) 235 | self.write(", ") 236 | self.write(")") 237 | self.enter() 238 | self.dispatch(t.body) 239 | self.leave() 240 | 241 | def _FunctionDef(self, t): 242 | self.write("\n") 243 | for deco in t.decorator_list: 244 | self.fill("@") 245 | self.dispatch(deco) 246 | self.fill("def "+t.name + "(") 247 | self.dispatch(t.args) 248 | self.write(")") 249 | self.enter() 250 | self.dispatch(t.body) 251 | self.leave() 252 | 253 | def _For(self, t): 254 | self.fill("for ") 255 | self.dispatch(t.target) 256 | self.write(" in ") 257 | self.dispatch(t.iter) 258 | self.enter() 259 | self.dispatch(t.body) 260 | self.leave() 261 | if t.orelse: 262 | self.fill("else") 263 | self.enter() 264 | self.dispatch(t.orelse) 265 | self.leave() 266 | 267 | def _If(self, t): 268 | self.fill("if ") 269 | self.dispatch(t.test) 270 | self.enter() 271 | self.dispatch(t.body) 272 | self.leave() 273 | # collapse nested ifs into equivalent elifs. 274 | while (t.orelse and len(t.orelse) == 1 and 275 | isinstance(t.orelse[0], ast.If)): 276 | t = t.orelse[0] 277 | self.fill("elif ") 278 | self.dispatch(t.test) 279 | self.enter() 280 | self.dispatch(t.body) 281 | self.leave() 282 | # final else 283 | if t.orelse: 284 | self.fill("else") 285 | self.enter() 286 | self.dispatch(t.orelse) 287 | self.leave() 288 | 289 | def _While(self, t): 290 | self.fill("while ") 291 | self.dispatch(t.test) 292 | self.enter() 293 | self.dispatch(t.body) 294 | self.leave() 295 | if t.orelse: 296 | self.fill("else") 297 | self.enter() 298 | self.dispatch(t.orelse) 299 | self.leave() 300 | 301 | def _With(self, t): 302 | self.fill("with ") 303 | self.dispatch(t.context_expr) 304 | if t.optional_vars: 305 | self.write(" as ") 306 | self.dispatch(t.optional_vars) 307 | self.enter() 308 | self.dispatch(t.body) 309 | self.leave() 310 | 311 | # expr 312 | def _Str(self, tree): 313 | # if from __future__ import unicode_literals is in effect, 314 | # then we want to output string literals using a 'b' prefix 315 | # and unicode literals with no prefix. 316 | if "unicode_literals" not in self.future_imports: 317 | self.write(repr(tree.s)) 318 | elif isinstance(tree.s, str): 319 | self.write("b" + repr(tree.s)) 320 | elif isinstance(tree.s, unicode): 321 | self.write(repr(tree.s).lstrip("u")) 322 | else: 323 | assert False, "shouldn't get here" 324 | 325 | def _Name(self, t): 326 | self.write(t.id) 327 | 328 | def _Repr(self, t): 329 | self.write("`") 330 | self.dispatch(t.value) 331 | self.write("`") 332 | 333 | def _Num(self, t): 334 | repr_n = repr(t.n) 335 | # Parenthesize negative numbers, to avoid turning (-1)**2 into -1**2. 336 | if repr_n.startswith("-"): 337 | self.write("(") 338 | # Substitute overflowing decimal literal for AST infinities. 339 | self.write(repr_n.replace("inf", INFSTR)) 340 | if repr_n.startswith("-"): 341 | self.write(")") 342 | 343 | def _List(self, t): 344 | self.write("[") 345 | interleave(lambda: self.write(", "), self.dispatch, t.elts) 346 | self.write("]") 347 | 348 | def _ListComp(self, t): 349 | self.write("[") 350 | self.dispatch(t.elt) 351 | for gen in t.generators: 352 | self.dispatch(gen) 353 | self.write("]") 354 | 355 | def _GeneratorExp(self, t): 356 | self.write("(") 357 | self.dispatch(t.elt) 358 | for gen in t.generators: 359 | self.dispatch(gen) 360 | self.write(")") 361 | 362 | def _SetComp(self, t): 363 | self.write("{") 364 | self.dispatch(t.elt) 365 | for gen in t.generators: 366 | self.dispatch(gen) 367 | self.write("}") 368 | 369 | def _DictComp(self, t): 370 | self.write("{") 371 | self.dispatch(t.key) 372 | self.write(": ") 373 | self.dispatch(t.value) 374 | for gen in t.generators: 375 | self.dispatch(gen) 376 | self.write("}") 377 | 378 | def _comprehension(self, t): 379 | self.write(" for ") 380 | self.dispatch(t.target) 381 | self.write(" in ") 382 | self.dispatch(t.iter) 383 | for if_clause in t.ifs: 384 | self.write(" if ") 385 | self.dispatch(if_clause) 386 | 387 | def _IfExp(self, t): 388 | self.write("(") 389 | self.dispatch(t.body) 390 | self.write(" if ") 391 | self.dispatch(t.test) 392 | self.write(" else ") 393 | self.dispatch(t.orelse) 394 | self.write(")") 395 | 396 | def _Set(self, t): 397 | assert(t.elts) # should be at least one element 398 | self.write("{") 399 | interleave(lambda: self.write(", "), self.dispatch, t.elts) 400 | self.write("}") 401 | 402 | def _Dict(self, t): 403 | self.write("{") 404 | def write_pair(pair): 405 | (k, v) = pair 406 | self.dispatch(k) 407 | self.write(": ") 408 | self.dispatch(v) 409 | interleave(lambda: self.write(", "), write_pair, zip(t.keys, t.values)) 410 | self.write("}") 411 | 412 | def _Tuple(self, t): 413 | self.write("(") 414 | if len(t.elts) == 1: 415 | (elt,) = t.elts 416 | self.dispatch(elt) 417 | self.write(",") 418 | else: 419 | interleave(lambda: self.write(", "), self.dispatch, t.elts) 420 | self.write(")") 421 | 422 | unop = {"Invert":"~", "Not": "not", "UAdd":"+", "USub":"-"} 423 | def _UnaryOp(self, t): 424 | self.write("(") 425 | self.write(self.unop[t.op.__class__.__name__]) 426 | self.write(" ") 427 | # If we're applying unary minus to a number, parenthesize the number. 428 | # This is necessary: -2147483648 is different from -(2147483648) on 429 | # a 32-bit machine (the first is an int, the second a long), and 430 | # -7j is different from -(7j). (The first has real part 0.0, the second 431 | # has real part -0.0.) 432 | if isinstance(t.op, ast.USub) and isinstance(t.operand, ast.Num): 433 | self.write("(") 434 | self.dispatch(t.operand) 435 | self.write(")") 436 | else: 437 | self.dispatch(t.operand) 438 | self.write(")") 439 | 440 | binop = { "Add":"+", "Sub":"-", "Mult":"*", "Div":"/", "Mod":"%", 441 | "LShift":"<<", "RShift":">>", "BitOr":"|", "BitXor":"^", "BitAnd":"&", 442 | "FloorDiv":"//", "Pow": "**"} 443 | def _BinOp(self, t): 444 | self.write("(") 445 | self.dispatch(t.left) 446 | self.write(" " + self.binop[t.op.__class__.__name__] + " ") 447 | self.dispatch(t.right) 448 | self.write(")") 449 | 450 | cmpops = {"Eq":"==", "NotEq":"!=", "Lt":"<", "LtE":"<=", "Gt":">", "GtE":">=", 451 | "Is":"is", "IsNot":"is not", "In":"in", "NotIn":"not in"} 452 | def _Compare(self, t): 453 | self.write("(") 454 | self.dispatch(t.left) 455 | for o, e in zip(t.ops, t.comparators): 456 | self.write(" " + self.cmpops[o.__class__.__name__] + " ") 457 | self.dispatch(e) 458 | self.write(")") 459 | 460 | boolops = {ast.And: 'and', ast.Or: 'or'} 461 | def _BoolOp(self, t): 462 | self.write("(") 463 | s = " %s " % self.boolops[t.op.__class__] 464 | interleave(lambda: self.write(s), self.dispatch, t.values) 465 | self.write(")") 466 | 467 | def _Attribute(self,t): 468 | self.dispatch(t.value) 469 | # Special case: 3.__abs__() is a syntax error, so if t.value 470 | # is an integer literal then we need to either parenthesize 471 | # it or add an extra space to get 3 .__abs__(). 472 | if isinstance(t.value, ast.Num) and isinstance(t.value.n, int): 473 | self.write(" ") 474 | self.write(".") 475 | self.write(t.attr) 476 | 477 | def _Call(self, t): 478 | self.dispatch(t.func) 479 | self.write("(") 480 | comma = False 481 | for e in t.args: 482 | if comma: self.write(", ") 483 | else: comma = True 484 | self.dispatch(e) 485 | for e in t.keywords: 486 | if comma: self.write(", ") 487 | else: comma = True 488 | self.dispatch(e) 489 | if t.starargs: 490 | if comma: self.write(", ") 491 | else: comma = True 492 | self.write("*") 493 | self.dispatch(t.starargs) 494 | if t.kwargs: 495 | if comma: self.write(", ") 496 | else: comma = True 497 | self.write("**") 498 | self.dispatch(t.kwargs) 499 | self.write(")") 500 | 501 | def _Subscript(self, t): 502 | self.dispatch(t.value) 503 | self.write("[") 504 | self.dispatch(t.slice) 505 | self.write("]") 506 | 507 | # slice 508 | def _Ellipsis(self, t): 509 | self.write("...") 510 | 511 | def _Index(self, t): 512 | self.dispatch(t.value) 513 | 514 | def _Slice(self, t): 515 | if t.lower: 516 | self.dispatch(t.lower) 517 | self.write(":") 518 | if t.upper: 519 | self.dispatch(t.upper) 520 | if t.step: 521 | self.write(":") 522 | self.dispatch(t.step) 523 | 524 | def _ExtSlice(self, t): 525 | interleave(lambda: self.write(', '), self.dispatch, t.dims) 526 | 527 | # others 528 | def _arguments(self, t): 529 | first = True 530 | # normal arguments 531 | defaults = [None] * (len(t.args) - len(t.defaults)) + t.defaults 532 | for a,d in zip(t.args, defaults): 533 | if first:first = False 534 | else: self.write(", ") 535 | self.dispatch(a), 536 | if d: 537 | self.write("=") 538 | self.dispatch(d) 539 | 540 | # varargs 541 | if t.vararg: 542 | if first:first = False 543 | else: self.write(", ") 544 | self.write("*") 545 | self.write(t.vararg) 546 | 547 | # kwargs 548 | if t.kwarg: 549 | if first:first = False 550 | else: self.write(", ") 551 | self.write("**"+t.kwarg) 552 | 553 | def _keyword(self, t): 554 | self.write(t.arg) 555 | self.write("=") 556 | self.dispatch(t.value) 557 | 558 | def _Lambda(self, t): 559 | self.write("(") 560 | self.write("lambda ") 561 | self.dispatch(t.args) 562 | self.write(": ") 563 | self.dispatch(t.body) 564 | self.write(")") 565 | 566 | def _alias(self, t): 567 | self.write(t.name) 568 | if t.asname: 569 | self.write(" as "+t.asname) 570 | 571 | def roundtrip(filename, output=sys.stdout): 572 | with open(filename, "r") as pyfile: 573 | source = pyfile.read() 574 | tree = compile(source, filename, "exec", ast.PyCF_ONLY_AST) 575 | Unparser(tree, output) 576 | 577 | 578 | 579 | def testdir(a): 580 | try: 581 | names = [n for n in os.listdir(a) if n.endswith('.py')] 582 | except OSError: 583 | sys.stderr.write("Directory not readable: %s" % a) 584 | else: 585 | for n in names: 586 | fullname = os.path.join(a, n) 587 | if os.path.isfile(fullname): 588 | output = cStringIO.StringIO() 589 | print 'Testing %s' % fullname 590 | try: 591 | roundtrip(fullname, output) 592 | except Exception as e: 593 | print ' Failed to compile, exception is %s' % repr(e) 594 | elif os.path.isdir(fullname): 595 | testdir(fullname) 596 | 597 | def main(args): 598 | if args[0] == '--testdir': 599 | for a in args[1:]: 600 | testdir(a) 601 | else: 602 | for a in args: 603 | roundtrip(a) 604 | 605 | if __name__=='__main__': 606 | main(sys.argv[1:]) 607 | -------------------------------------------------------------------------------- /ast/scrambling_code/unscramble.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import ast 4 | 5 | from scrambler import Scrambler 6 | from unparse import Unparser 7 | 8 | 9 | for root, dirs, files in os.walk(sys.argv[1]): 10 | for file in files: 11 | if file.endswith('.py'): 12 | with open('{0}/{1}'.format(root, file), 'r') as f: 13 | lines = f.readlines() 14 | if lines[0].strip() != Scrambler.HEADER: 15 | continue 16 | code = ''.join(lines[1:]) 17 | with open('{0}/{1}'.format(root, file), 'w') as f: 18 | node = ast.parse(code) 19 | decoded = Scrambler(scramble=False).visit(node) 20 | Unparser(decoded, f) 21 | f.write('\n') 22 | -------------------------------------------------------------------------------- /bytecode/pyc/Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | @ python -m compileall app/ 3 | 4 | clean-build: 5 | @ find . ! -name "manage.py" -name "*.py" -delete 6 | 7 | clean: 8 | @ rm -rf ./app 9 | 10 | run: clean 11 | @ cp -R ../../app . 12 | @ find . -iname "*.py[co]" -delete 13 | @ $(MAKE) && $(MAKE) clean-build 14 | 15 | .DEFAULT: all 16 | .PHONY: all clean clean-build 17 | -------------------------------------------------------------------------------- /bytecode/pyc/README.md: -------------------------------------------------------------------------------- 1 | # Python code Protection Mechanisms - Byte-code distribution 2 | 3 | Compile ``.py`` into ``.pyc`` to distribute. 4 | 5 | ## Description 6 | 7 | In this example, we will be using [compileall](https://docs.python.org/2/library/compileall.html) 8 | to compile the different ``.py`` files into ``.pyc``, then remove the ``.py`` 9 | keeping the application functionality. 10 | 11 | ## Pros 12 | 13 | 1. Simple to implement (just a command call is needed) 14 | 15 | ## Cons 16 | 17 | 1. Byte-code is easily converted back to python code, [uncompyle2](https://pypi.python.org/pypi/uncompyle2) 18 | does the work to build back the ``.py`` files very well. 19 | -------------------------------------------------------------------------------- /bytecode/pyo/Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | @ python -OO -m compileall app/ 3 | 4 | clean-build: 5 | @ find . ! -name "__init__.py" ! -name "manage.py" -name "*.py" -delete 6 | 7 | clean: 8 | @ rm -rf ./app 9 | 10 | run: clean 11 | @ cp -R ../../app . 12 | @ find . -iname "*.py[co]" -delete 13 | @ $(MAKE) && $(MAKE) clean-build 14 | 15 | .DEFAULT: all 16 | .PHONY: all clean clean-build run 17 | -------------------------------------------------------------------------------- /bytecode/pyo/README.md: -------------------------------------------------------------------------------- 1 | # Python code Protection Mechanisms - Optimized Byte-code distribution 2 | 3 | Compile ``.py`` into ``.pyo`` to distribute, there's not much gain compared 4 | with the usual ``.pyc`` files since optimized byte-code mostly disables 5 | ``assert`` calls and removes docstrings. 6 | 7 | ## Description 8 | 9 | In this example, we will be using [compileall](https://docs.python.org/2/library/compileall.html) 10 | to compile the different ``.py`` files into ``.pyo``, then remove the ``.py`` 11 | keeping the application functionality. 12 | 13 | ## Pros 14 | 15 | 1. Simple to implement (just a command call is needed). 16 | 17 | ## Cons 18 | 19 | 1. Optimized byte-code is easily converted back to python code, 20 | [uncompyle2](https://pypi.python.org/pypi/uncompyle2) does the work to build 21 | back the ``.py`` files very well. 22 | -------------------------------------------------------------------------------- /custom_interpreter/opcodes/.python-version: -------------------------------------------------------------------------------- 1 | 2.7.9-custom 2 | -------------------------------------------------------------------------------- /custom_interpreter/opcodes/2.7.9-custom: -------------------------------------------------------------------------------- 1 | #require_gcc 2 | install_package "readline-6.3" "http://ftpmirror.gnu.org/readline/readline-6.3.tar.gz#56ba6071b9462f980c5a72ab0023893b65ba6debb4eeb475d7a563dc65cafd43" standard --if has_broken_mac_readline 3 | install_package "Python-2.7.9" "https://www.python.org/ftp/python/2.7.9/Python-2.7.9.tgz#c8bba33e66ac3201dabdc556f0ea7cfe6ac11946ec32d357c4c6f9b018c12c5b" ldflags_dirs standard verify_py27 ensurepip 4 | -------------------------------------------------------------------------------- /custom_interpreter/opcodes/Makefile: -------------------------------------------------------------------------------- 1 | PYTHON_DIR := Python-2.7.9 2 | PYTHON_CUSTOM_DIR := $(PYTHON_DIR)-customized 3 | PYTHON_XZ := Python-2.7.9.tar.xz 4 | 5 | all: pyenv 6 | @ cp -R ../../app . 7 | @ pip install -r app/requirements.txt 8 | 9 | clean: 10 | @ rm -rf app $(PYTHON_DIR) $(PYTHON_CUSTOM_DIR) *.diff 11 | 12 | $(PYTHON_XZ): 13 | @ wget -c https://www.python.org/ftp/python/2.7.9/Python-2.7.9.tar.xz 14 | 15 | $(PYTHON_DIR): $(PYTHON_XZ) 16 | @ tar xJf Python-2.7.9.tar.xz 17 | 18 | $(PYTHON_CUSTOM_DIR): $(PYTHON_DIR) 19 | @ cp -R $(PYTHON_DIR) $(PYTHON_CUSTOM_DIR) 20 | 21 | # Do "diff .. || true" because diff exits status is based on if there 22 | # are differences between the files or not (like cmp) 23 | scramble: $(PYTHON_CUSTOM_DIR) 24 | @ python scramble-opcodes.py --python-source=$(PYTHON_CUSTOM_DIR) 25 | @ diff -u $(PYTHON_DIR)/Include/opcode.h $(PYTHON_CUSTOM_DIR)/Include/opcode.h \ 26 | > patches/001_scrambled-opcodes.diff || true 27 | 28 | patches: scramble 29 | 30 | pyenv: patches 31 | @ cat patches/*.diff | filterdiff --strip=1 | pyenv install -sp 2.7.9-custom 32 | @ pyenv local 2.7.9-custom 33 | 34 | run: clean $(PYTHON_CUSTOM_DIR) patches 35 | 36 | .DEFAULT: all 37 | .PHONY: all clean clean-build run 38 | -------------------------------------------------------------------------------- /custom_interpreter/opcodes/README.md: -------------------------------------------------------------------------------- 1 | # Python code Protection Mechanisms - Custom Interpreter 2 | 3 | Customize python interpreter to protect a python codebase. 4 | 5 | ## Description 6 | 7 | In this example, we will be customizing the CPython interpreter in order to 8 | introduce protection measures to avoid the reverse engineering of the codebase. 9 | 10 | So far the following protections were introduced: 11 | 12 | 1. Opcode scrambling to protect against tools like [uncompyle2](https://pypi.python.org/pypi/uncompyle2) 13 | 14 | The following protections will be investigated too: 15 | 16 | 1. Dynamic ``opcode`` tables (use a different ``opcode`` table for each 17 | ``.py`` file to make decompiling technics even harder) 18 | 2. Removal of properties that leak code (``co_code``, etc) 19 | 3. Removal of functions that allow code injection (``PyRun_SimpleString``, 20 | ``compile``, etc) 21 | 4. Built-in encryption support 22 | 23 | ## Pros 24 | 25 | 1. Customizing the python interpreter opens the door to several protection 26 | mechanisms available for C applications (like anti-debugging techniques) 27 | 28 | 2. Conventional tools to reverse-engineer python application won't work, 29 | [pyREtic](https://github.com/MyNameIsMeerkat/pyREtic) might still work for 30 | if scrambled-opcodes is the only technique used. 31 | 32 | ## Cons 33 | 34 | 1. The whole python platform needs to be distributed when deployed to the 35 | servers (cross-compilation is needed for this) 36 | 37 | 2. Only ``.pyc`` files must be deployed (check [bytecode](../bytecode) example) 38 | 39 | 3. The application **must** be run with this interpreter, ``pyenv`` comes handy 40 | when deploying the application. 41 | 42 | 4. Demands a very good knowledge of the python interpreter internals 43 | -------------------------------------------------------------------------------- /custom_interpreter/opcodes/patches/.empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FormulaMonks/python-obfuscation/8fb9897702dc51af7fb164f3e6906dd3d656ec68/custom_interpreter/opcodes/patches/.empty -------------------------------------------------------------------------------- /custom_interpreter/opcodes/patches/001_scrambled-opcodes.diff: -------------------------------------------------------------------------------- 1 | --- Python-2.7.9/Include/opcode.h 2014-12-10 13:59:32.000000000 -0200 2 | +++ Python-2.7.9-customized/Include/opcode.h 2015-02-09 16:36:50.748148442 -0200 3 | @@ -7,148 +7,148 @@ 4 | 5 | /* Instruction opcodes for compiled code */ 6 | 7 | -#define STOP_CODE 0 8 | -#define POP_TOP 1 9 | -#define ROT_TWO 2 10 | -#define ROT_THREE 3 11 | -#define DUP_TOP 4 12 | -#define ROT_FOUR 5 13 | -#define NOP 9 14 | - 15 | -#define UNARY_POSITIVE 10 16 | -#define UNARY_NEGATIVE 11 17 | -#define UNARY_NOT 12 18 | -#define UNARY_CONVERT 13 19 | - 20 | -#define UNARY_INVERT 15 21 | - 22 | -#define BINARY_POWER 19 23 | - 24 | -#define BINARY_MULTIPLY 20 25 | -#define BINARY_DIVIDE 21 26 | -#define BINARY_MODULO 22 27 | -#define BINARY_ADD 23 28 | -#define BINARY_SUBTRACT 24 29 | -#define BINARY_SUBSCR 25 30 | -#define BINARY_FLOOR_DIVIDE 26 31 | -#define BINARY_TRUE_DIVIDE 27 32 | -#define INPLACE_FLOOR_DIVIDE 28 33 | -#define INPLACE_TRUE_DIVIDE 29 34 | +#define STOP_CODE 91 35 | +#define POP_TOP 38 36 | +#define ROT_TWO 100 37 | +#define ROT_THREE 1 38 | +#define DUP_TOP 19 39 | +#define ROT_FOUR 75 40 | +#define NOP 9 41 | + 42 | +#define UNARY_POSITIVE 73 43 | +#define UNARY_NEGATIVE 12 44 | +#define UNARY_NOT 11 45 | +#define UNARY_CONVERT 98 46 | + 47 | +#define UNARY_INVERT 5 48 | + 49 | +#define BINARY_POWER 66 50 | + 51 | +#define BINARY_MULTIPLY 74 52 | +#define BINARY_DIVIDE 82 53 | +#define BINARY_MODULO 25 54 | +#define BINARY_ADD 86 55 | +#define BINARY_SUBTRACT 80 56 | +#define BINARY_SUBSCR 81 57 | +#define BINARY_FLOOR_DIVIDE 62 58 | +#define BINARY_TRUE_DIVIDE 87 59 | +#define INPLACE_FLOOR_DIVIDE 0 60 | +#define INPLACE_TRUE_DIVIDE 101 61 | 62 | -#define SLICE 30 63 | +#define SLICE 33 64 | /* Also uses 31-33 */ 65 | 66 | -#define STORE_SLICE 40 67 | +#define STORE_SLICE 93 68 | /* Also uses 41-43 */ 69 | 70 | -#define DELETE_SLICE 50 71 | +#define DELETE_SLICE 27 72 | /* Also uses 51-53 */ 73 | 74 | -#define STORE_MAP 54 75 | -#define INPLACE_ADD 55 76 | -#define INPLACE_SUBTRACT 56 77 | -#define INPLACE_MULTIPLY 57 78 | -#define INPLACE_DIVIDE 58 79 | -#define INPLACE_MODULO 59 80 | -#define STORE_SUBSCR 60 81 | -#define DELETE_SUBSCR 61 82 | - 83 | -#define BINARY_LSHIFT 62 84 | -#define BINARY_RSHIFT 63 85 | -#define BINARY_AND 64 86 | -#define BINARY_XOR 65 87 | -#define BINARY_OR 66 88 | -#define INPLACE_POWER 67 89 | -#define GET_ITER 68 90 | - 91 | -#define PRINT_EXPR 70 92 | -#define PRINT_ITEM 71 93 | -#define PRINT_NEWLINE 72 94 | -#define PRINT_ITEM_TO 73 95 | -#define PRINT_NEWLINE_TO 74 96 | -#define INPLACE_LSHIFT 75 97 | -#define INPLACE_RSHIFT 76 98 | -#define INPLACE_AND 77 99 | -#define INPLACE_XOR 78 100 | -#define INPLACE_OR 79 101 | -#define BREAK_LOOP 80 102 | -#define WITH_CLEANUP 81 103 | -#define LOAD_LOCALS 82 104 | -#define RETURN_VALUE 83 105 | -#define IMPORT_STAR 84 106 | -#define EXEC_STMT 85 107 | -#define YIELD_VALUE 86 108 | -#define POP_BLOCK 87 109 | -#define END_FINALLY 88 110 | -#define BUILD_CLASS 89 111 | - 112 | -#define HAVE_ARGUMENT 90 /* Opcodes from here have an argument: */ 113 | - 114 | -#define STORE_NAME 90 /* Index in name list */ 115 | -#define DELETE_NAME 91 /* "" */ 116 | -#define UNPACK_SEQUENCE 92 /* Number of sequence items */ 117 | -#define FOR_ITER 93 118 | -#define LIST_APPEND 94 119 | - 120 | -#define STORE_ATTR 95 /* Index in name list */ 121 | -#define DELETE_ATTR 96 /* "" */ 122 | -#define STORE_GLOBAL 97 /* "" */ 123 | -#define DELETE_GLOBAL 98 /* "" */ 124 | -#define DUP_TOPX 99 /* number of items to duplicate */ 125 | -#define LOAD_CONST 100 /* Index in const list */ 126 | -#define LOAD_NAME 101 /* Index in name list */ 127 | -#define BUILD_TUPLE 102 /* Number of tuple items */ 128 | -#define BUILD_LIST 103 /* Number of list items */ 129 | -#define BUILD_SET 104 /* Number of set items */ 130 | -#define BUILD_MAP 105 /* Always zero for now */ 131 | -#define LOAD_ATTR 106 /* Index in name list */ 132 | -#define COMPARE_OP 107 /* Comparison operator */ 133 | -#define IMPORT_NAME 108 /* Index in name list */ 134 | -#define IMPORT_FROM 109 /* Index in name list */ 135 | -#define JUMP_FORWARD 110 /* Number of bytes to skip */ 136 | +#define STORE_MAP 26 137 | +#define INPLACE_ADD 24 138 | +#define INPLACE_SUBTRACT 63 139 | +#define INPLACE_MULTIPLY 89 140 | +#define INPLACE_DIVIDE 90 141 | +#define INPLACE_MODULO 2 142 | +#define STORE_SUBSCR 71 143 | +#define DELETE_SUBSCR 92 144 | + 145 | +#define BINARY_LSHIFT 76 146 | +#define BINARY_RSHIFT 83 147 | +#define BINARY_AND 13 148 | +#define BINARY_XOR 78 149 | +#define BINARY_OR 3 150 | +#define INPLACE_POWER 79 151 | +#define GET_ITER 58 152 | + 153 | +#define PRINT_EXPR 68 154 | +#define PRINT_ITEM 99 155 | +#define PRINT_NEWLINE 4 156 | +#define PRINT_ITEM_TO 88 157 | +#define PRINT_NEWLINE_TO 69 158 | +#define INPLACE_LSHIFT 21 159 | +#define INPLACE_RSHIFT 67 160 | +#define INPLACE_AND 64 161 | +#define INPLACE_XOR 20 162 | +#define INPLACE_OR 23 163 | +#define BREAK_LOOP 48 164 | +#define WITH_CLEANUP 22 165 | +#define LOAD_LOCALS 72 166 | +#define RETURN_VALUE 32 167 | +#define IMPORT_STAR 10 168 | +#define EXEC_STMT 65 169 | +#define YIELD_VALUE 85 170 | +#define POP_BLOCK 84 171 | +#define END_FINALLY 15 172 | +#define BUILD_CLASS 70 173 | + 174 | +#define HAVE_ARGUMENT 102 /* Opcodes from here have an argument: */ 175 | + 176 | +#define STORE_NAME 107 /* Index in name list */ 177 | +#define DELETE_NAME 119 /* "" */ 178 | +#define UNPACK_SEQUENCE 144 /* Number of sequence items */ 179 | +#define FOR_ITER 127 180 | +#define LIST_APPEND 126 181 | + 182 | +#define STORE_ATTR 122 /* Index in name list */ 183 | +#define DELETE_ATTR 103 /* "" */ 184 | +#define STORE_GLOBAL 142 /* "" */ 185 | +#define DELETE_GLOBAL 105 /* "" */ 186 | +#define DUP_TOPX 108 /* number of items to duplicate */ 187 | +#define LOAD_CONST 159 /* Index in const list */ 188 | +#define LOAD_NAME 125 /* Index in name list */ 189 | +#define BUILD_TUPLE 153 /* Number of tuple items */ 190 | +#define BUILD_LIST 102 /* Number of list items */ 191 | +#define BUILD_SET 133 /* Number of set items */ 192 | +#define BUILD_MAP 123 /* Always zero for now */ 193 | +#define LOAD_ATTR 128 /* Index in name list */ 194 | +#define COMPARE_OP 120 /* Comparison operator */ 195 | +#define IMPORT_NAME 148 /* Index in name list */ 196 | +#define IMPORT_FROM 115 /* Index in name list */ 197 | +#define JUMP_FORWARD 117 /* Number of bytes to skip */ 198 | 199 | -#define JUMP_IF_FALSE_OR_POP 111 /* Target byte offset from beginning 200 | +#define JUMP_IF_FALSE_OR_POP 106 /* Target byte offset from beginning 201 | of code */ 202 | -#define JUMP_IF_TRUE_OR_POP 112 /* "" */ 203 | -#define JUMP_ABSOLUTE 113 /* "" */ 204 | -#define POP_JUMP_IF_FALSE 114 /* "" */ 205 | -#define POP_JUMP_IF_TRUE 115 /* "" */ 206 | - 207 | -#define LOAD_GLOBAL 116 /* Index in name list */ 208 | - 209 | -#define CONTINUE_LOOP 119 /* Start of loop (absolute) */ 210 | -#define SETUP_LOOP 120 /* Target address (relative) */ 211 | -#define SETUP_EXCEPT 121 /* "" */ 212 | -#define SETUP_FINALLY 122 /* "" */ 213 | - 214 | -#define LOAD_FAST 124 /* Local variable number */ 215 | -#define STORE_FAST 125 /* Local variable number */ 216 | -#define DELETE_FAST 126 /* Local variable number */ 217 | +#define JUMP_IF_TRUE_OR_POP 152 /* "" */ 218 | +#define JUMP_ABSOLUTE 137 /* "" */ 219 | +#define POP_JUMP_IF_FALSE 147 /* "" */ 220 | +#define POP_JUMP_IF_TRUE 136 /* "" */ 221 | + 222 | +#define LOAD_GLOBAL 146 /* Index in name list */ 223 | + 224 | +#define CONTINUE_LOOP 116 /* Start of loop (absolute) */ 225 | +#define SETUP_LOOP 157 /* Target address (relative) */ 226 | +#define SETUP_EXCEPT 118 /* "" */ 227 | +#define SETUP_FINALLY 109 /* "" */ 228 | + 229 | +#define LOAD_FAST 124 /* Local variable number */ 230 | +#define STORE_FAST 155 /* Local variable number */ 231 | +#define DELETE_FAST 145 /* Local variable number */ 232 | 233 | -#define RAISE_VARARGS 130 /* Number of raise arguments (1, 2 or 3) */ 234 | +#define RAISE_VARARGS 131 /* Number of raise arguments (1, 2 or 3) */ 235 | /* CALL_FUNCTION_XXX opcodes defined below depend on this definition */ 236 | -#define CALL_FUNCTION 131 /* #args + (#kwargs<<8) */ 237 | -#define MAKE_FUNCTION 132 /* #defaults */ 238 | -#define BUILD_SLICE 133 /* Number of items */ 239 | - 240 | -#define MAKE_CLOSURE 134 /* #free vars */ 241 | -#define LOAD_CLOSURE 135 /* Load free variable from closure */ 242 | -#define LOAD_DEREF 136 /* Load and dereference from closure cell */ 243 | -#define STORE_DEREF 137 /* Store into cell */ 244 | +#define CALL_FUNCTION 154 /* #args + (#kwargs<<8) */ 245 | +#define MAKE_FUNCTION 149 /* #defaults */ 246 | +#define BUILD_SLICE 114 /* Number of items */ 247 | + 248 | +#define MAKE_CLOSURE 113 /* #free vars */ 249 | +#define LOAD_CLOSURE 132 /* Load free variable from closure */ 250 | +#define LOAD_DEREF 138 /* Load and dereference from closure cell */ 251 | +#define STORE_DEREF 112 /* Store into cell */ 252 | 253 | /* The next 3 opcodes must be contiguous and satisfy 254 | (CALL_FUNCTION_VAR - CALL_FUNCTION) & 3 == 1 */ 255 | -#define CALL_FUNCTION_VAR 140 /* #args + (#kwargs<<8) */ 256 | -#define CALL_FUNCTION_KW 141 /* #args + (#kwargs<<8) */ 257 | -#define CALL_FUNCTION_VAR_KW 142 /* #args + (#kwargs<<8) */ 258 | +#define CALL_FUNCTION_VAR 163 /* #args + (#kwargs<<8) */ 259 | +#define CALL_FUNCTION_KW 164 /* #args + (#kwargs<<8) */ 260 | +#define CALL_FUNCTION_VAR_KW 165 /* #args + (#kwargs<<8) */ 261 | 262 | -#define SETUP_WITH 143 263 | +#define SETUP_WITH 111 264 | 265 | /* Support for opargs more than 16 bits long */ 266 | -#define EXTENDED_ARG 145 267 | +#define EXTENDED_ARG 143 268 | 269 | -#define SET_ADD 146 270 | -#define MAP_ADD 147 271 | +#define SET_ADD 110 272 | +#define MAP_ADD 104 273 | 274 | 275 | enum cmp_op {PyCmp_LT=Py_LT, PyCmp_LE=Py_LE, PyCmp_EQ=Py_EQ, PyCmp_NE=Py_NE, PyCmp_GT=Py_GT, PyCmp_GE=Py_GE, 276 | -------------------------------------------------------------------------------- /custom_interpreter/opcodes/scramble-opcodes.py: -------------------------------------------------------------------------------- 1 | # Scramble python opcodes table to avoid byte-code to python decompilation 2 | import os 3 | import re 4 | import random 5 | import argparse 6 | 7 | 8 | OPCODE_H = 'Include/opcode.h' 9 | OPCODE_RE = re.compile( 10 | r'^#define\s+(?P[A-Z_]+)\s+(?P\d+)(?P.*)' 11 | ) 12 | 13 | 14 | def fix_offests(opcodes, ignore_names, value, offs): 15 | if not isinstance(ignore_names, (list, tuple)): 16 | ignore_names = [ignore_names] 17 | 18 | new_opcodes = {} 19 | for key, val in opcodes.items(): 20 | if key not in ignore_names and val >= value: 21 | val += offs 22 | new_opcodes[key] = val 23 | return new_opcodes 24 | 25 | 26 | def slice_opcodes(opcodes): 27 | for name in ['SLICE', 'STORE_SLICE', 'DELETE_SLICE']: 28 | if name in opcodes: 29 | opcodes = fix_offests(opcodes, name, opcodes[name], 4) 30 | return opcodes 31 | 32 | 33 | def call_function_opcodes(opcodes): 34 | if 'CALL_FUNCTION' in opcodes: 35 | opcodes['CALL_FUNCTION_VAR'] = opcodes['CALL_FUNCTION'] + 9 36 | opcodes['CALL_FUNCTION_KW'] = opcodes['CALL_FUNCTION_VAR'] + 1 37 | opcodes['CALL_FUNCTION_VAR_KW'] = opcodes['CALL_FUNCTION_VAR'] + 2 38 | opcodes = fix_offests(opcodes, [ 39 | 'CALL_FUNCTION_VAR', 40 | 'CALL_FUNCTION_KW', 41 | 'CALL_FUNCTION_VAR_KW' 42 | ], opcodes['CALL_FUNCTION_VAR'], 3) 43 | return opcodes 44 | 45 | 46 | def is_opcode(line): 47 | return OPCODE_RE.match(line) 48 | 49 | 50 | def fix_opcodes_offsets(opcodes): 51 | opcodes = slice_opcodes(opcodes) 52 | opcodes = call_function_opcodes(opcodes) 53 | return opcodes 54 | 55 | 56 | def opcode(line): 57 | match = is_opcode(line) 58 | if match: 59 | return (match.group('name'), 60 | int(match.group('code')), 61 | match.group('extra')) 62 | 63 | 64 | def scramble_subset(opcodes): 65 | names = [name for name, code, extra in opcodes] 66 | opcodes = [code for name, code, extra in opcodes] 67 | random.shuffle(opcodes) 68 | return dict(zip(names, opcodes)) 69 | 70 | 71 | def scramble_opcodes(src): 72 | path = os.path.join(src, OPCODE_H) 73 | lines = [] 74 | dont_have_arg = [] 75 | have_arg = [] 76 | 77 | with open(path, 'r') as file: 78 | file_lines = file.readlines() 79 | opcodes = filter(None, map(opcode, file_lines)) 80 | 81 | have = False 82 | for name, code, extra in opcodes: 83 | if name == 'HAVE_ARGUMENT': 84 | have = True 85 | continue 86 | opcodes_set = have_arg if have else dont_have_arg 87 | opcodes_set.append((name, code, extra)) 88 | 89 | dont_have_arg = scramble_subset(dont_have_arg) 90 | have_arg = scramble_subset(have_arg) 91 | have_arg['HAVE_ARGUMENT'] = min(have_arg.values()) 92 | opcodes = dict(dont_have_arg) 93 | opcodes.update(have_arg) 94 | opcodes = fix_opcodes_offsets(opcodes) 95 | 96 | for line in file_lines: 97 | match = is_opcode(line) 98 | if match: 99 | name = match.group('name') 100 | line = '#define {0} {1}{2}\n'.format(name, opcodes[name], 101 | match.group('extra')) 102 | lines.append(line) 103 | 104 | with open(path, 'w') as file: 105 | file.write(''.join(lines)) 106 | 107 | 108 | parser = argparse.ArgumentParser(description='Scramble python opcodes table') 109 | parser.add_argument('--python-source', dest='src', type=str, 110 | help='Python source code', required=True) 111 | 112 | 113 | if __name__ == '__main__': 114 | args = parser.parse_args() 115 | scramble_opcodes(args.src) 116 | -------------------------------------------------------------------------------- /cython/Makefile: -------------------------------------------------------------------------------- 1 | PYTHONLIB := /usr/include/python2.7 2 | CFLAGS := -shared -pthread -fPIC -fwrapv -O2 -Wall -fno-strict-aliasing -I${PYTHONLIB} 3 | PYTHON_FILES := $(shell find . -name "*.py" ! -name "__init__.py" ! -name "manage.py") 4 | OBJECTS := $(patsubst %.py,%.so,$(PYTHON_FILES)) 5 | 6 | all: $(OBJECTS) 7 | 8 | %.c: %.py 9 | @ echo "Compiling $<" 10 | @ cython --no-docstrings $< -o $(patsubst %.py,%.c,$<) 11 | 12 | %.so: %.c 13 | @ $(CC) $(CFLAGS) -o $@ $< 14 | @ strip --strip-all $@ 15 | 16 | compress: 17 | @ for f in `find . -name "*.so"`; do \ 18 | upx -9 $$f; \ 19 | done 20 | 21 | clean-build: 22 | @ find . ! -name "__init__.py" ! -name "manage.py" -name "*.py" -delete 23 | @ find . -name "__init__.so" -o -name "manage.so" -delete 24 | 25 | clean: 26 | @ rm -rf ./app 27 | 28 | run: clean 29 | @ cp -R ../../app . 30 | @ find . -iname "*.py[co]" -delete 31 | @ $(MAKE) && $(MAKE) clean-build 32 | 33 | .DEFAULT: all 34 | .PHONY: all %.c %.so clean clean-build compress run 35 | -------------------------------------------------------------------------------- /cython/README.md: -------------------------------------------------------------------------------- 1 | # Python code Protection Mechanisms - Cython 2 | 3 | Cython mechanism to protect a python codebase. 4 | 5 | ## Description 6 | 7 | In this example, we will be using [Cython](http://cython.org/) to 8 | compile the different ``.py`` files into ``.so`` libs without loosing 9 | import capabilities or code functionality. 10 | 11 | Cython will compile the given python module into a ``.c`` source code, 12 | which is then compiled by ``gcc`` as a shared library (``.so``). Finally, 13 | the ``.py`` files can be removed, leaving just the ``.so``. 14 | 15 | **Note**: The ``__init__.py`` files **must** be kept in the tree, otherwise 16 | ``import`` won't work. 17 | 18 | ## Pros 19 | 20 | 1. Code speedup: since the code is translated to C, it's going to run 21 | faster compared to the python version. 22 | 23 | 2. Assembler ananlysis is needed in order to rebuild the code, which 24 | is compiled with ``O2`` flag (optimization) making the task even 25 | harder to achieve. 26 | 27 | 3. Code compilation can be applied just to key components of the 28 | application if necessary. 29 | 30 | 4. [UPX](http://upx.sourceforge.net/) compressor can applied in the 31 | generated libraries (avoids ``objdump`` assembler dumping, but it's 32 | possible to decompress it) 33 | 34 | ## Cons 35 | 36 | 1. Cython is not 100% python compatible [yet](http://docs.cython.org/src/userguide/limitations.html) 37 | 38 | 2. Cross-compilation is needed to properly build the binaries for the 39 | supported environments (docker can be used to achieve this) 40 | -------------------------------------------------------------------------------- /import_hook/compiled/Makefile: -------------------------------------------------------------------------------- 1 | PYTHONLIB := /usr/include/python2.7 2 | CFLAGS := -shared -pthread -fPIC -fwrapv -O2 -Wall -fno-strict-aliasing -I${PYTHONLIB} 3 | 4 | all: ihook.so 5 | @ python ihook.py app/ 6 | @ cp ihook.so m.py app/ 7 | 8 | %.c: %.py 9 | @ echo "Compiling $<" 10 | @ cython --no-docstrings $< -o $(patsubst %.py,%.c,$<) 11 | 12 | %.so: %.c 13 | @ $(CC) $(CFLAGS) -o $@ $< 14 | @ strip --strip-all $@ 15 | 16 | clean-build: 17 | @ find . -iname "*.py[co]" -delete 18 | 19 | clean: 20 | @ rm -rf ./app 21 | 22 | run: clean 23 | @ cp -R ../../app . 24 | @ find . -name "*.py[co]" -delete 25 | @ $(MAKE) && $(MAKE) clean-build 26 | 27 | .DEFAULT: all 28 | .PHONY: all clean clean-build run 29 | -------------------------------------------------------------------------------- /import_hook/compiled/README.md: -------------------------------------------------------------------------------- 1 | # Python code Protection Mechanisms - Compiled Import Hook 2 | 3 | This mechanism will use a compiled [import hook](https://www.python.org/dev/peps/pep-0302/) 4 | in order to detect encrypted modules on import time. 5 | 6 | 7 | ## Description 8 | 9 | In this example we will be using a compiled [import hook](https://www.python.org/dev/peps/pep-0302/) 10 | to detect encrypted modules when they are being imported, the detection is done 11 | by verifying a signature in the encrypted files (in this case the signature is 12 | a simple string ``AESENC:``. 13 | 14 | The encrypted modules follow a simple format: ``AESENC:``, 15 | the signature is stripped, the code is decrypted and "evaluated" in the context 16 | of a in-memory created module. 17 | 18 | The import hook is compiled with Cython, this provides a better protection to 19 | the hook code to avoid accessing the encryption key. 20 | 21 | ## Pros 22 | 23 | 1. Simple to implement (just a command call is needed). 24 | 2. Strong encryption algorithms supported. 25 | 26 | ## Cons 27 | 28 | 1. The encryption key *must* be available in the server in order to run the 29 | application (unless it's possible to run it on demand by accessing the 30 | server over ssh and input the password in the command line, but still it's 31 | going to live in a in-memory object which can be inspected). 32 | 33 | 2. The decrypted code in memory can be inspected and dumped and decompiled from 34 | byte-code back to python. 35 | 36 | 3. Cross-compilation is requried to build the binary for the different 37 | environments. 38 | -------------------------------------------------------------------------------- /import_hook/compiled/ihook.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import imp 4 | import base64 5 | 6 | from Crypto.Cipher import AES 7 | 8 | 9 | SECRET = 'abcdefghijklmnop' 10 | SIGNATURE = 'AESENC:' 11 | 12 | 13 | class AESCypher(object): 14 | def __init__(self, secret=None, block_size=16, padding='$'): 15 | # block_size = 16 -> 128bits 16 | self.block_size = block_size 17 | self.padding = padding 18 | self.secret = secret or os.urandom(self.block_size) 19 | 20 | def add_padding(self, val): 21 | bsize, padding = self.block_size, self.padding 22 | return val + (bsize - len(val) % bsize) * padding 23 | 24 | def encode_aes(self, cipher, value): 25 | return base64.b64encode(cipher.encrypt(self.add_padding(value))) 26 | 27 | def decode_aes(self, chipher, value): 28 | return chipher.decrypt(base64.b64decode(value)).rstrip(self.padding) 29 | 30 | def encrypt(self, private_info): 31 | return self.encode_aes(AES.new(self.secret), private_info) 32 | 33 | def decrypt(self, value): 34 | return self.decode_aes(AES.new(self.secret), value) 35 | 36 | 37 | class BaseLoader(object): 38 | def modinfo(self, name, path): 39 | try: 40 | modinfo = imp.find_module(name.rsplit('.', 1)[-1], path) 41 | except ImportError: 42 | if '.' not in name: 43 | raise 44 | clean_path, clean_name = name.rsplit('.', 1) 45 | clean_path = [clean_path.replace('.', '/')] 46 | modinfo = imp.find_module(clean_name, clean_path) 47 | 48 | file, pathname, (suffix, mode, type_) = modinfo 49 | if type_ == imp.PY_SOURCE: 50 | filename = pathname 51 | elif type_ == imp.PY_COMPILED: 52 | filename = pathname[:-1] 53 | elif type_ == imp.PKG_DIRECTORY: 54 | filename = os.path.join(pathname, '__init__.py') 55 | else: 56 | return (None, None) 57 | return (filename, modinfo) 58 | 59 | 60 | class Loader(BaseLoader): 61 | def __init__(self, name, path): 62 | self.name = name 63 | self.path = path 64 | 65 | def is_package(self, val): 66 | """Flask requirement""" 67 | return None 68 | 69 | def load_module(self, name): 70 | if name in sys.modules: 71 | return sys.modules[name] 72 | 73 | filename, modinfo = self.modinfo(self.name, self.path) 74 | if filename is None and modinfo is None: 75 | return None 76 | 77 | file = modinfo[0] or open(filename, 'r') 78 | src = ''.join(file.readlines()) 79 | src = self.decrypt_to(src[len(SIGNATURE):]) 80 | 81 | module = imp.new_module(name) 82 | module.__file__ = filename 83 | module.__path__ = [os.path.dirname(os.path.abspath(file.name))] 84 | module.__loader__ = self 85 | sys.modules[name] = module 86 | exec(src, module.__dict__) 87 | print "encrypted module loaded: {0}".format(name) 88 | return module 89 | 90 | def decrypt_to(self, input): 91 | return AESCypher(SECRET).decrypt(input) 92 | 93 | 94 | class Finder(BaseLoader): 95 | def find_module(self, name, path=None): 96 | filename, modinfo = self.modinfo(name, path) 97 | if filename is None and modinfo is None: 98 | return None 99 | 100 | file = modinfo[0] or open(filename, 'r') 101 | if file.read(len(SIGNATURE)) == SIGNATURE: 102 | print "encrypted module found: {0} (at {1})".format(name, path) 103 | file.seek(0) 104 | return Loader(name, path) 105 | 106 | 107 | def install_hook(): 108 | sys.meta_path.insert(0, Finder()) 109 | 110 | 111 | def encrypt_all(location): 112 | cipher = AESCypher(SECRET) 113 | 114 | for root, dirs, files in os.walk(location): 115 | files = (f for f in files if f.endswith('.py')) 116 | for name in files: 117 | enc_name = '{0}/{1}-enc'.format(root, name) 118 | orig_name = '{0}/{1}'.format(root, name) 119 | 120 | with open(orig_name, 'r') as input: 121 | with open(enc_name, 'wb') as output: 122 | content = cipher.encrypt(''.join(input.readlines())) 123 | content = '{0}{1}'.format(SIGNATURE, content) 124 | output.write(content) 125 | os.rename(enc_name, orig_name) 126 | 127 | 128 | if __name__ == '__main__': 129 | encrypt_all(sys.argv[1]) 130 | -------------------------------------------------------------------------------- /import_hook/compiled/m.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import ihook; ihook.install_hook() 4 | 5 | 6 | from manage import manager 7 | manager.run() 8 | -------------------------------------------------------------------------------- /import_hook/compiled/requirements.txt: -------------------------------------------------------------------------------- 1 | pycrypto 2 | -------------------------------------------------------------------------------- /import_hook/python/Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | @ python ihook.py app/ 3 | @ cp ihook.py m.py app/ 4 | 5 | clean-build: 6 | @ find . -iname "*.py[co]" -delete 7 | 8 | clean: 9 | @ rm -rf ./app 10 | 11 | run: clean 12 | @ cp -R ../../app . 13 | @ find . -name "*.py[co]" -delete 14 | @ $(MAKE) && $(MAKE) clean-build 15 | 16 | .DEFAULT: all 17 | .PHONY: all clean clean-build run 18 | -------------------------------------------------------------------------------- /import_hook/python/README.md: -------------------------------------------------------------------------------- 1 | # Python code Protection Mechanisms - Import Hook 2 | 3 | This mechanism will use an [import hook](https://www.python.org/dev/peps/pep-0302/) 4 | in order to detect encrypted modules on import time. 5 | 6 | 7 | ## Description 8 | 9 | In this example we will be using an [import hook](https://www.python.org/dev/peps/pep-0302/) 10 | to detect encrypted modules when they are being imported, the detection is done 11 | by verifying a signature in the encrypted files (in this case the signature is 12 | a simple string ``AESENC:``. 13 | 14 | The encrypted modules follow a simple format: ``AESENC:``, 15 | the signature is stripped, the code is decrypted and "evaluated" in the context 16 | of a in-memory created module. 17 | 18 | ## Pros 19 | 20 | 1. Simple to implement (just a command call is needed). 21 | 2. Strong encryption algorithms supported. 22 | 23 | ## Cons 24 | 25 | 1. The encryption key *must* be available in the server in order to run the 26 | application (unless it's possible to run it on demand by accessing the 27 | server over ssh and input the password in the command line, but still it's 28 | going to live in a in-memory object which can be inspected). 29 | 30 | 2. The decrypted code in memory can be inspected and dumped and decompiled from 31 | byte-code back to python. 32 | -------------------------------------------------------------------------------- /import_hook/python/ihook.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import imp 4 | import base64 5 | 6 | from Crypto.Cipher import AES 7 | 8 | 9 | SECRET = 'abcdefghijklmnop' 10 | SIGNATURE = 'AESENC:' 11 | 12 | 13 | class AESCypher(object): 14 | def __init__(self, secret=None, block_size=16, padding='$'): 15 | # block_size = 16 -> 128bits 16 | self.block_size = block_size 17 | self.padding = padding 18 | self.secret = secret or os.urandom(self.block_size) 19 | 20 | def add_padding(self, val): 21 | bsize, padding = self.block_size, self.padding 22 | return val + (bsize - len(val) % bsize) * padding 23 | 24 | def encode_aes(self, cipher, value): 25 | return base64.b64encode(cipher.encrypt(self.add_padding(value))) 26 | 27 | def decode_aes(self, chipher, value): 28 | return chipher.decrypt(base64.b64decode(value)).rstrip(self.padding) 29 | 30 | def encrypt(self, private_info): 31 | return self.encode_aes(AES.new(self.secret), private_info) 32 | 33 | def decrypt(self, value): 34 | return self.decode_aes(AES.new(self.secret), value) 35 | 36 | 37 | class BaseLoader(object): 38 | def modinfo(self, name, path): 39 | try: 40 | modinfo = imp.find_module(name.rsplit('.', 1)[-1], path) 41 | except ImportError: 42 | if '.' not in name: 43 | raise 44 | clean_path, clean_name = name.rsplit('.', 1) 45 | clean_path = [clean_path.replace('.', '/')] 46 | modinfo = imp.find_module(clean_name, clean_path) 47 | 48 | file, pathname, (suffix, mode, type_) = modinfo 49 | if type_ == imp.PY_SOURCE: 50 | filename = pathname 51 | elif type_ == imp.PY_COMPILED: 52 | filename = pathname[:-1] 53 | elif type_ == imp.PKG_DIRECTORY: 54 | filename = os.path.join(pathname, '__init__.py') 55 | else: 56 | return (None, None) 57 | return (filename, modinfo) 58 | 59 | 60 | class Loader(BaseLoader): 61 | def __init__(self, name, path): 62 | self.name = name 63 | self.path = path 64 | 65 | def is_package(self, val): 66 | """Flask requirement""" 67 | return None 68 | 69 | def load_module(self, name): 70 | if name in sys.modules: 71 | return sys.modules[name] 72 | 73 | filename, modinfo = self.modinfo(self.name, self.path) 74 | if filename is None and modinfo is None: 75 | return None 76 | 77 | file = modinfo[0] or open(filename, 'r') 78 | src = ''.join(file.readlines()) 79 | src = self.decrypt(src[len(SIGNATURE):]) 80 | 81 | module = imp.new_module(name) 82 | module.__file__ = filename 83 | module.__path__ = [os.path.dirname(os.path.abspath(file.name))] 84 | module.__loader__ = self 85 | sys.modules[name] = module 86 | exec(src, module.__dict__) 87 | print "encrypted module loaded: {0}".format(name) 88 | return module 89 | 90 | def decrypt(self, input): 91 | return AESCypher(SECRET).decrypt(input) 92 | 93 | 94 | class Finder(BaseLoader): 95 | def find_module(self, name, path=None): 96 | filename, modinfo = self.modinfo(name, path) 97 | if filename is None and modinfo is None: 98 | return None 99 | 100 | file = modinfo[0] or open(filename, 'r') 101 | if file.read(len(SIGNATURE)) == SIGNATURE: 102 | print "encrypted module found: {0} (at {1})".format(name, path) 103 | file.seek(0) 104 | return Loader(name, path) 105 | 106 | 107 | def install_hook(): 108 | sys.meta_path.insert(0, Finder()) 109 | 110 | 111 | def encrypt_all(location): 112 | cipher = AESCypher(SECRET) 113 | 114 | for root, dirs, files in os.walk(location): 115 | files = (f for f in files if f.endswith('.py')) 116 | for name in files: 117 | enc_name = '{0}/{1}-enc'.format(root, name) 118 | orig_name = '{0}/{1}'.format(root, name) 119 | 120 | with open(orig_name, 'r') as input: 121 | with open(enc_name, 'wb') as output: 122 | content = cipher.encrypt(''.join(input.readlines())) 123 | content = '{0}{1}'.format(SIGNATURE, content) 124 | output.write(content) 125 | os.rename(enc_name, orig_name) 126 | 127 | 128 | if __name__ == '__main__': 129 | encrypt_all(sys.argv[1]) 130 | -------------------------------------------------------------------------------- /import_hook/python/m.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import ihook; ihook.install_hook() 4 | 5 | 6 | from manage import manager 7 | manager.run() 8 | -------------------------------------------------------------------------------- /import_hook/python/requirements.txt: -------------------------------------------------------------------------------- 1 | pycrypto 2 | --------------------------------------------------------------------------------