├── .gitignore ├── README.md ├── LICENSE └── pyc_obscure.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | test.py 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pyc_obscure (demo) 2 | a simple obfuscator for pyc 3 | 4 | 5 | ## theory 6 | 7 | insert junk data into co_code of PyCodeObject to obfuscate decompiler like uncompyle6 8 | 9 | 10 | ## usage 11 | 12 | ```python 13 | from pyc_obscure import Obscure 14 | 15 | obs = Obscure('test.pyc') 16 | obs.basic_obscure() 17 | obs.write_pyc('obs_test.pyc') 18 | ``` 19 | 20 | # TODO 21 | 22 | - support multi version of python (python3.7 only now) 23 | - complex obscure (not just junk JUMP), add fake control flow 24 | - ~~support add consts like string~~ 25 | - compute JUMP offset in loop (loop implementations differs with different version) 26 | 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 marryjianjian 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /pyc_obscure.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import xdis 3 | import types 4 | import os 5 | import opcode 6 | import struct 7 | import marshal 8 | 9 | 10 | def pack32(n): 11 | return struct.pack(" new empty Obscure object 20 | Obscure(filename) -> new Obscure object load from file 21 | ''' 22 | 23 | def __init__(self, filename=None): 24 | self.float_version : float 25 | self.magic_int : int 26 | self.timestamp : int 27 | self.co : types.CodeType 28 | self.ispypy : bool 29 | self.source_size : int 30 | self.sip_hash : int 31 | self.instr_offset : list 32 | 33 | if filename is not None: 34 | self.load_pyc(filename) 35 | 36 | def _load_pyc(self, filename): 37 | return xdis.load_module(filename) 38 | 39 | def load_pyc(self, filename): 40 | ( 41 | self.float_version, 42 | self.timestamp, 43 | self.magic_int, 44 | self.co, 45 | self.ispypy, 46 | self.source_size, 47 | self.sip_hash, 48 | ) = self._load_pyc(filename) 49 | 50 | self.instr_offset = self.get_instr_offset_from_lnotab(self.co.co_lnotab) 51 | 52 | def get_instr_offset_from_lnotab(self, lnotab): 53 | assert(type(lnotab) == bytes) 54 | assert(len(lnotab) & 1 == 0) 55 | res = [] 56 | offset = 0 57 | res.append(offset) 58 | for i in range(len(lnotab)//2): 59 | offset += lnotab[2 * i] 60 | res.append(offset) 61 | 62 | res.append(len(self.co.co_code)) 63 | return res 64 | 65 | def _gen_obs37_opcode_from_offset(self, offset): 66 | return bytes((opcode.opmap['JUMP_ABSOLUTE'], offset + 4)) + os.urandom(2) 67 | 68 | def _get_obs_instr(self, offset): 69 | return self._gen_obs37_opcode_from_offset(offset) 70 | 71 | def new_code_object(self, 72 | argcount = None, 73 | kwonlyargcount = None, 74 | nlocals = None, 75 | stacksize = None, 76 | flags = None, 77 | code = None, 78 | consts = None, 79 | names = None, 80 | varnames = None, 81 | filename = None, 82 | name = None, 83 | firstlineno = None, 84 | lnotab = None, 85 | freevars = None, 86 | cellvars = None, 87 | ): 88 | if argcount is None: 89 | argcount = self.co.co_argcount 90 | if kwonlyargcount is None: 91 | kwonlyargcount = self.co.co_kwonlyargcount 92 | if nlocals is None: 93 | nlocals = self.co.co_nlocals 94 | if stacksize is None: 95 | stacksize = self.co.co_stacksize 96 | if flags is None: 97 | flags = self.co.co_flags 98 | if code is None: 99 | code = self.co.co_code 100 | if consts is None: 101 | consts = self.co.co_consts 102 | if names is None: 103 | names = self.co.co_names 104 | if varnames is None: 105 | varnames = self.co.co_varnames 106 | if filename is None: 107 | filename = self.co.co_filename 108 | if name is None: 109 | name = self.co.co_name 110 | if firstlineno is None: 111 | firstlineno = self.co.co_firstlineno 112 | if lnotab is None: 113 | lnotab = self.co.co_lnotab 114 | if freevars is None: 115 | freevars = self.co.co_freevars 116 | if cellvars is None: 117 | cellvars = self.co.co_cellvars 118 | 119 | return types.CodeType( 120 | argcount, 121 | kwonlyargcount, 122 | nlocals, 123 | stacksize, 124 | flags, 125 | code, 126 | tuple(consts), 127 | tuple(names), 128 | tuple(varnames), 129 | filename, 130 | name, 131 | firstlineno, 132 | lnotab, 133 | tuple(freevars), 134 | tuple(cellvars), 135 | ) 136 | 137 | def basic_obscure(self): 138 | code = self.co.co_code 139 | offsets = self.instr_offset 140 | res = b"" 141 | index = 0 142 | for i in range(1, len(offsets)): 143 | obs_instr = self._get_obs_instr(len(res)) 144 | res += obs_instr + code[offsets[i-1]:offsets[i]] 145 | 146 | self.co = self.new_code_object(code=res) 147 | 148 | def write_pyc(self, filename): 149 | s = pack16(self.magic_int) + b"\r\n" 150 | s += pack32(0) + pack32(self.timestamp) + pack32(self.source_size) 151 | s += marshal.dumps(self.co) 152 | # print(s) 153 | with open(filename, 'wb') as fw: 154 | fw.write(s) 155 | 156 | def modify_filename(self, modified_filename): 157 | self.co = self.new_code_object(filename=modified_filename) 158 | 159 | def add_string(self, string): 160 | assert(type(string) == str) 161 | consts = self.co.co_consts + (string,) 162 | self.co = self.new_code_object(consts=consts) 163 | 164 | def add_strings(self, strings): 165 | assert(type(strings) == list) 166 | consts = self.co.co_consts + tuple([i for i in strings]) 167 | self.co = self.new_code_object(consts=consts) 168 | 169 | if __name__ == '__main__': 170 | import sys 171 | if len(sys.argv) >= 2: 172 | filename = sys.argv[1] 173 | obs = Obscure(filename) 174 | exec(obs.co) 175 | #print(len(obs.co.co_code), obs.co.co_code) 176 | obs.basic_obscure() 177 | obs.add_string('test add string') 178 | obs.add_strings(['a', 'b', 'd']) 179 | #print(len(obs.co.co_code), obs.co.co_code) 180 | obs.write_pyc('asd.pyc') 181 | exec(obs.co) 182 | 183 | --------------------------------------------------------------------------------