├── .gitignore ├── LICENSE.txt ├── README.md ├── bytecode_graph ├── __init__.py ├── bytecode_graph.py ├── render.py └── utils.py ├── docs ├── example_graph.png └── example_graph.svg ├── examples ├── bytecode_deobf_blog.py ├── example.py ├── example_pyc.py └── example_render.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | *.pyc 3 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bytecode_graph 2 | 3 | Module designed to modify Python bytecode. 4 | Allows instructions to be added or removed from a Python bytecode string. The modified bytecode can be refactored to correct offsets and produce a new functional code object. 5 | 6 | You should use the same Python interpretor version as the bytecode being analyzed. 7 | 8 | ## Example - Inserting NOP instructions 9 | 10 | The following example inserts a NOP instruction after each instruction in the `Sample` function: 11 | 12 | ```python 13 | 14 | import bytecode_graph 15 | from dis import opmap 16 | 17 | 18 | def Sample(): 19 | i = 2 + 2 20 | if i == 4: 21 | print "2 + 2 = %d" % i 22 | else: 23 | print "oops" 24 | 25 | print bytecode_graph.disassemble(Sample.__code__) 26 | print 27 | bcg = bytecode_graph.BytecodeGraph(Sample.__code__) 28 | 29 | nodes = [x for x in bcg.nodes()] 30 | for n in nodes: 31 | bc = bytecode_graph.Bytecode(0, chr(opmap['NOP'])) 32 | bcg.add_node(n, bc) 33 | 34 | new_code = bcg.get_code() 35 | print bytecode_graph.disassemble(new_code) 36 | print 37 | exec new_code 38 | 39 | nodes = [x for x in bcg.nodes()] 40 | for n in nodes: 41 | if n.opcode == opmap['NOP']: 42 | bcg.delete_node(n) 43 | new_code = bcg.get_code() 44 | print bytecode_graph.disassemble(new_code) 45 | exec new_code 46 | ``` 47 | 48 | Disassembly of `Sample()`: 49 | ``` 50 | 0 640500 LOAD_CONST 5 (4) 51 | 3 7d0000 STORE_FAST 0 (i) 52 | 6 7c0000 LOAD_FAST 0 (i) 53 | 9 640200 LOAD_CONST 2 (4) 54 | 12 6b0200 COMPARE_OP 2 (==) 55 | 15 721e00 POP_JUMP_IF_FALSE 30 56 | 18 640300 LOAD_CONST 3 ('2 + 2 = %d') 57 | 21 7c0000 LOAD_FAST 0 (i) 58 | 24 16 BINARY_MODULO 59 | 25 47 PRINT_ITEM 60 | 26 48 PRINT_NEWLINE 61 | 27 6e0500 JUMP_FORWARD 5 (to 35) 62 | >> 30 640400 LOAD_CONST 4 ('oops') 63 | 33 47 PRINT_ITEM 64 | 34 48 PRINT_NEWLINE 65 | >> 35 640000 LOAD_CONST 0 (None) 66 | 38 53 RETURN_VALUE 67 | ``` 68 | 69 | Disassembly of `Sample()` with NOPs: 70 | ``` 71 | 0 640500 LOAD_CONST 5 (4) 72 | 3 09 NOP 73 | 4 7d0000 STORE_FAST 0 (i) 74 | 7 09 NOP 75 | 8 7c0000 LOAD_FAST 0 (i) 76 | 11 09 NOP 77 | 12 640200 LOAD_CONST 2 (4) 78 | 15 09 NOP 79 | 16 6b0200 COMPARE_OP 2 (==) 80 | 19 09 NOP 81 | 20 722a00 POP_JUMP_IF_FALSE 42 82 | 23 09 NOP 83 | 24 640300 LOAD_CONST 3 ('2 + 2 = %d') 84 | 27 09 NOP 85 | 28 7c0000 LOAD_FAST 0 (i) 86 | 31 09 NOP 87 | 32 16 BINARY_MODULO 88 | 33 09 NOP 89 | 34 47 PRINT_ITEM 90 | 35 09 NOP 91 | 36 48 PRINT_NEWLINE 92 | 37 09 NOP 93 | 38 6e0900 JUMP_FORWARD 9 (to 50) 94 | 41 09 NOP 95 | >> 42 640400 LOAD_CONST 4 ('oops') 96 | 45 09 NOP 97 | 46 47 PRINT_ITEM 98 | 47 09 NOP 99 | 48 48 PRINT_NEWLINE 100 | 49 09 NOP 101 | >> 50 640000 LOAD_CONST 0 (None) 102 | 53 09 NOP 103 | 54 53 RETURN_VALUE 104 | 55 09 NOP 105 | ``` 106 | 107 | Output of running modified code object: 108 | ``` 109 | 2 + 2 = 4 110 | ``` 111 | 112 | To remove the NOPs append the following to the above example: 113 | ```python 114 | nodes = [x for x in bcg.nodes()] 115 | for n in nodes: 116 | #verify opcode is NOP 117 | if n.opcode == opmap['NOP']: 118 | #remove instruction node from the graph 119 | bcg.delete_node(n) 120 | 121 | #create a new code object 122 | new_code = bcg.get_code() 123 | bytecode_graph.disassemble(new_code) 124 | exec new_code 125 | ``` 126 | 127 | To load code from a PYC file: 128 | ```python 129 | import bytecode_graph 130 | from dis import opmap 131 | import sys 132 | import marshal 133 | 134 | 135 | pyc_file = open(sys.argv[1], "rb").read() 136 | pyc = marshal.loads(pyc_file[8:]) 137 | 138 | bytecode_graph.disassemble(pyc) 139 | print 140 | 141 | bcg = bytecode_graph.BytecodeGraph(pyc) 142 | 143 | nodes = [x for x in bcg.nodes()] 144 | for n in nodes: 145 | bc = bytecode_graph.Bytecode(0, chr(opmap['NOP'])) 146 | bcg.add_node(n, bc) 147 | 148 | new_code = bcg.get_code() 149 | bytecode_graph.disassemble(new_code) 150 | print 151 | 152 | nodes = [x for x in bcg.nodes()] 153 | for n in nodes: 154 | if n.opcode == opmap['NOP']: 155 | bcg.delete_node(n) 156 | 157 | new_code = bcg.get_code() 158 | bytecode_graph.disassemble(new_code) 159 | print 160 | ``` 161 | 162 | It is also possible to create control flow diagrams using GraphViz. The disassembly within the graph can include the output from a simple peephole decompiler. This can be helpful when reviewing bytecode that fails to decompile. 163 | 164 | ```python 165 | import bytecode_graph 166 | 167 | 168 | def Sample(): 169 | i = 2 + 2 170 | if i == 4: 171 | print "2 + 2 = %d" % i 172 | else: 173 | print "oops" 174 | 175 | bcg = bytecode_graph.BytecodeGraph(Sample.__code__) 176 | 177 | graph = bytecode_graph.Render(bcg, Sample.__code__).dot() 178 | 179 | graph.write_png('example_graph.png') 180 | 181 | ``` 182 | 183 | ![Example_Graph](docs/example_graph.png?raw=true) 184 | -------------------------------------------------------------------------------- /bytecode_graph/__init__.py: -------------------------------------------------------------------------------- 1 | from bytecode_graph import * 2 | from render import * 3 | -------------------------------------------------------------------------------- /bytecode_graph/bytecode_graph.py: -------------------------------------------------------------------------------- 1 | import dis 2 | import struct 3 | import new 4 | import re 5 | 6 | from dis import opmap, cmp_op, opname 7 | 8 | 9 | class Bytecode(): 10 | ''' 11 | Class to store individual instruction as a node in the graph 12 | ''' 13 | def __init__(self, addr, buffer, prev=None, next=None, xrefs=[]): 14 | self.opcode = ord(buffer[0]) 15 | self.addr = addr 16 | 17 | if self.opcode >= dis.HAVE_ARGUMENT: 18 | self.oparg = ord(buffer[1]) | (ord(buffer[2]) << 8) 19 | else: 20 | self.oparg = None 21 | 22 | self.prev = prev 23 | self.next = next 24 | self.xrefs = [] 25 | self.target = None 26 | self.co_lnotab = None 27 | 28 | def len(self): 29 | ''' 30 | Returns the length of the bytecode 31 | 1 for no argument 32 | 3 for argument 33 | ''' 34 | if self.opcode < dis.HAVE_ARGUMENT: 35 | return 1 36 | else: 37 | return 3 38 | 39 | def disassemble(self): 40 | ''' 41 | Return disassembly of bytecode 42 | ''' 43 | rvalue = opname[self.opcode].ljust(20) 44 | if self.opcode >= dis.HAVE_ARGUMENT: 45 | rvalue += " %04x" % (self.oparg) 46 | return rvalue 47 | 48 | def hex(self): 49 | ''' 50 | Return ASCII hex representation of bytecode 51 | ''' 52 | rvalue = "%02x" % self.opcode 53 | if self.opcode >= dis.HAVE_ARGUMENT: 54 | rvalue += "%02x%02x" % \ 55 | (self.oparg & 0xff, (self.oparg >> 8) & 0xff) 56 | return rvalue 57 | 58 | def bin(self): 59 | ''' 60 | Return bytecode string 61 | ''' 62 | if self.opcode >= dis.HAVE_ARGUMENT: 63 | return struct.pack("= dis.HAVE_ARGUMENT: 118 | if current.opcode in dis.hasjrel: 119 | label = current.addr+current.oparg+current.len() 120 | elif current.opcode in dis.hasjabs: 121 | label = current.oparg 122 | 123 | if label >= 0: 124 | if current not in self.bytecodes[label].xrefs: 125 | self.bytecodes[label].xrefs.append(current) 126 | current.target = self.bytecodes[label] 127 | current = current.next 128 | return 129 | 130 | def apply_lineno(self): 131 | ''' 132 | Parses the code object co_lnotab list and applies line numbers to 133 | bytecode. This is used to create a new co_lnotab list after modifying 134 | bytecode. 135 | ''' 136 | byte_increments = [ord(c) for c in self.code.co_lnotab[0::2]] 137 | line_increments = [ord(c) for c in self.code.co_lnotab[1::2]] 138 | 139 | lineno = self.code.co_firstlineno 140 | addr = self.base 141 | linenos = [] 142 | 143 | for byte_incr, line_incr in zip(byte_increments, line_increments): 144 | addr += byte_incr 145 | lineno += line_incr 146 | linenos.append((addr, lineno)) 147 | 148 | if linenos == []: 149 | return 150 | 151 | current_addr, current_lineno = linenos.pop(0) 152 | current_addr, next_lineno = linenos.pop(0) 153 | for x in self.nodes(): 154 | if x.addr >= current_addr: 155 | current_lineno = next_lineno 156 | if len(linenos) != 0: 157 | current_addr, next_lineno = linenos.pop(0) 158 | x.co_lnotab = current_lineno 159 | 160 | def calc_lnotab(self): 161 | ''' 162 | Creates a new co_lineno after modifying bytecode 163 | ''' 164 | rvalue = "" 165 | 166 | prev_lineno = self.code.co_firstlineno 167 | prev_offset = self.head.addr 168 | 169 | for current in self.nodes(): 170 | if current.co_lnotab == prev_lineno: 171 | continue 172 | 173 | new_offset = current.co_lnotab - prev_lineno 174 | new_offset = 0xff if new_offset > 0xff else new_offset 175 | 176 | rvalue += struct.pack("BB", current.addr - prev_offset, 177 | (current.co_lnotab - prev_lineno) & 0xff) 178 | 179 | prev_lineno = current.co_lnotab 180 | prev_offset = current.addr 181 | return rvalue 182 | 183 | def delete_node(self, node): 184 | ''' 185 | Deletes a node from the graph, removing the instruction from the 186 | produced bytecode stream 187 | ''' 188 | 189 | # For each instruction pointing to instruction to be delete, 190 | # move the pointer to the next instruction 191 | for x in node.xrefs: 192 | x.target = node.next 193 | 194 | if node.next is not None: 195 | node.next.xrefs.append(x) 196 | 197 | # Clean up the doubly linked list 198 | if node.prev is not None: 199 | node.prev.next = node.next 200 | if node.next is not None: 201 | node.next.prev = node.prev 202 | if node == self.head: 203 | self.head = node.next 204 | 205 | del self.bytecodes[node.addr] 206 | 207 | def disassemble(self, start=None, count=None): 208 | ''' 209 | Simple disassembly routine for analyzing nodes in the graph 210 | ''' 211 | 212 | rvalue = "" 213 | for x in self.nodes(start): 214 | rvalue += "[%04d] %04x %-6s %s\n" % \ 215 | (x.co_lnotab, x.addr, x.hex(), x.disassemble()) 216 | return rvalue 217 | 218 | def get_code(self, start=None): 219 | ''' 220 | Produce a new code object based on the graph 221 | ''' 222 | self.refactor() 223 | 224 | # generate a new co_lineno 225 | new_co_lineno = self.calc_lnotab() 226 | 227 | # generate new bytecode stream 228 | new_co_code = "" 229 | for x in self.nodes(start): 230 | new_co_code += x.bin() 231 | 232 | # create a new code object with modified bytecode and updated line numbers 233 | # a new code object is necessary because co_code is readonly 234 | rvalue = new.code(self.code.co_argcount, 235 | self.code.co_nlocals, 236 | self.code.co_stacksize, 237 | self.code.co_flags, 238 | new_co_code, 239 | self.code.co_consts, 240 | self.code.co_names, 241 | self.code.co_varnames, 242 | self.code.co_filename, 243 | self.code.co_name, 244 | self.code.co_firstlineno, 245 | new_co_lineno) 246 | 247 | return rvalue 248 | 249 | def nodes(self, start=None): 250 | ''' 251 | Iterator for stepping through bytecodes in order 252 | ''' 253 | if start is None: 254 | current = self.head 255 | else: 256 | current = start 257 | 258 | while current is not None: 259 | yield current 260 | current = current.next 261 | 262 | raise StopIteration 263 | 264 | def parse_bytecode(self): 265 | ''' 266 | Parses the bytecode stream and creates an instruction graph 267 | ''' 268 | 269 | self.bytecodes = {} 270 | prev = None 271 | offset = 0 272 | 273 | targets = [] 274 | 275 | while offset < len(self.code.co_code): 276 | next = Bytecode(self.base + offset, 277 | self.code.co_code[offset:offset+3], 278 | prev) 279 | 280 | self.bytecodes[self.base + offset] = next 281 | offset += self.bytecodes[offset].len() 282 | 283 | if prev is not None: 284 | prev.next = next 285 | 286 | prev = next 287 | 288 | if next.get_target_addr() is not None: 289 | targets.append(next.get_target_addr()) 290 | 291 | for x in targets: 292 | if x not in self.bytecodes: 293 | print "Nonlinear issue at offset: %08x" % x 294 | 295 | self.head = self.bytecodes[self.base] 296 | self.apply_labels() 297 | return 298 | 299 | def patch_opargs(self, start=None): 300 | ''' 301 | Updates branch instructions to correct offsets after adding or 302 | deleting bytecode 303 | ''' 304 | for current in self.nodes(start): 305 | # No argument, skip to next 306 | if current.opcode < dis.HAVE_ARGUMENT: 307 | continue 308 | 309 | # Patch relative offsets 310 | if current.opcode in dis.hasjrel: 311 | current.oparg = current.target.addr - \ 312 | (current.addr+current.len()) 313 | 314 | # Patch absolute offsets 315 | elif current.opcode in dis.hasjabs: 316 | current.oparg = current.target.addr 317 | 318 | def refactor(self): 319 | ''' 320 | iterates through all bytecodes and determines correct offset 321 | position in code sequence after adding or removing bytecode 322 | ''' 323 | 324 | offset = self.base 325 | new_bytecodes = {} 326 | 327 | for current in self.nodes(): 328 | new_bytecodes[offset] = current 329 | current.addr = offset 330 | offset += current.len() 331 | current = current.next 332 | 333 | self.bytecodes = new_bytecodes 334 | self.patch_opargs() 335 | self.apply_labels() 336 | -------------------------------------------------------------------------------- /bytecode_graph/render.py: -------------------------------------------------------------------------------- 1 | from dis import opmap 2 | 3 | import pydot 4 | 5 | from utils import * 6 | 7 | CONST = 2 8 | TRUE = 1 9 | FALSE = 0 10 | 11 | 12 | class Render: 13 | def __init__(self, bcg, code_object, colors=None): 14 | self.bcg = bcg 15 | self.co = code_object 16 | 17 | if colors is None: 18 | self.colors = {CONST: "blue", TRUE: "green", FALSE: "red"} 19 | else: 20 | self.colors = colors 21 | 22 | return 23 | 24 | def get_blocks(self): 25 | blocks = [] 26 | prev = None 27 | 28 | targets = [] 29 | 30 | for x in self.bcg.nodes(): 31 | if x.target is None: 32 | continue 33 | 34 | targets.append(x.target.addr) 35 | 36 | for x in self.bcg.nodes(): 37 | if prev is None: 38 | prev = x 39 | 40 | if x.next is not None and x.next.addr in targets: 41 | blocks.append((prev, x)) 42 | prev = None 43 | continue 44 | if x.target is None: 45 | continue 46 | 47 | blocks.append((prev, x)) 48 | prev = None 49 | 50 | blocks.append((prev, x)) 51 | return blocks 52 | 53 | def get_edges(self, blocks): 54 | edges = [] 55 | for start, x in blocks: 56 | 57 | if x.opcode in true_branches: 58 | edges.append((start, x.target, TRUE)) 59 | edges.append((start, x.next, FALSE)) 60 | 61 | elif x.opcode in false_branches: 62 | edges.append((start, x.target, FALSE)) 63 | edges.append((start, x.next, TRUE)) 64 | 65 | elif x.opcode in const_branches: 66 | edges.append((start, x.target, CONST)) 67 | 68 | elif x.opcode in loop_branches: 69 | edges.append((start, x.target, CONST)) 70 | edges.append((start, x.next, CONST)) 71 | else: 72 | if x.next is not None: 73 | edges.append((start, x.next, CONST)) 74 | 75 | return edges 76 | 77 | def get_comments(self, start, stop): 78 | 79 | rvalue = [] 80 | current = stop 81 | while True: 82 | 83 | prev, dec_str = decompile(self.co, current) 84 | if(dec_str is not None) and (prev != current.prev): 85 | rvalue.append((current.addr, dec_str, current.co_lnotab)) 86 | 87 | if prev is not None: 88 | current = prev 89 | else: 90 | current = current.prev 91 | 92 | if current is None: 93 | break 94 | 95 | if current == start or current.addr <= start.addr: 96 | break 97 | 98 | return rvalue 99 | 100 | def dot(self, splines="ortho", show_comments=True, show_hex=False): 101 | 102 | graph = pydot.Dot(graph_type='digraph', splines=splines, rankdir="TD") 103 | 104 | graph.set_node_defaults(shape="box", 105 | fontname="Courier New", 106 | fontsize = "9") 107 | 108 | blocks = self.get_blocks() 109 | edges = self.get_edges(blocks) 110 | 111 | for start, stop in blocks: 112 | lbl = disassemble(self.co, start=start.addr, stop=stop.addr+1, 113 | show_labels=False, show_hex=show_hex) 114 | 115 | if show_comments: 116 | comments = self.get_comments(start, stop) 117 | 118 | for addr, comment, lineno in comments: 119 | m = re.search("^[ ]*%d .*$" % addr, lbl, re.MULTILINE) 120 | if m is None: 121 | continue 122 | 123 | lbl = lbl[:m.end(0)] + "\n\n# %d " % lineno +\ 124 | comment + "\n" + lbl[m.end(0):] 125 | 126 | # left alignment 127 | lbl = lbl.replace("\n", "\\l") 128 | lbl += "\\l" 129 | 130 | tmp = pydot.Node("%08x" % start.addr, label=lbl) 131 | graph.add_node(tmp) 132 | 133 | for edge in edges: 134 | tmp = pydot.Edge("%08x" % edge[0].addr, 135 | "%08x" % edge[1].addr, 136 | color=self.colors[edge[2]]) 137 | graph.add_edge(tmp) 138 | 139 | return graph 140 | -------------------------------------------------------------------------------- /bytecode_graph/utils.py: -------------------------------------------------------------------------------- 1 | import dis 2 | from dis import opmap, cmp_op 3 | import re 4 | 5 | binary_ops = {opmap["BINARY_MODULO"]: "%", 6 | opmap["BINARY_ADD"]: "+", 7 | opmap["BINARY_SUBTRACT"]: "-", 8 | opmap["BINARY_SUBSCR"]: "[]", 9 | opmap["BINARY_LSHIFT"]: "<<", 10 | opmap["BINARY_RSHIFT"]: ">>", 11 | opmap["BINARY_AND"]: "&", 12 | opmap["BINARY_XOR"]: "^", 13 | opmap["BINARY_OR"]: "|"} 14 | 15 | const_branches = [opmap["JUMP_FORWARD"], opmap["JUMP_ABSOLUTE"]] 16 | loop_branches = [opmap["SETUP_EXCEPT"]] 17 | false_branches = [opmap["POP_JUMP_IF_FALSE"], 18 | opmap["JUMP_IF_FALSE_OR_POP"], 19 | opmap["FOR_ITER"]] 20 | true_branches = [opmap["POP_JUMP_IF_TRUE"], opmap["JUMP_IF_TRUE_OR_POP"]] 21 | 22 | cond_branches = false_branches + true_branches 23 | 24 | loads = [opmap["LOAD_CONST"], opmap["LOAD_GLOBAL"], opmap["LOAD_FAST"]] 25 | 26 | 27 | def disassemble(c, lasti=-1, start=0, stop=None, 28 | show_labels=True, show_hex=False): 29 | ''' 30 | Modified disassemble from dis module that includes hex output for opcodes 31 | ''' 32 | if hasattr(c, 'co_code'): 33 | code = c.co_code 34 | varnames = c.co_varnames 35 | names = c.co_names 36 | constants = c.co_consts 37 | else: 38 | code = c 39 | varnames = None 40 | names = None 41 | constants = None 42 | 43 | labels = dis.findlabels(code) 44 | 45 | rvalue = "" 46 | 47 | if stop is None: 48 | n = len(code) 49 | else: 50 | n = stop 51 | 52 | i = start 53 | while i < n: 54 | c = code[i] 55 | op = ord(c) 56 | if show_labels: 57 | if i == lasti: 58 | rvalue += '-->' 59 | else: 60 | rvalue += ' ' 61 | if i in labels: 62 | rvalue += '>>' 63 | else: 64 | rvalue += ' ' 65 | 66 | rvalue += repr(i).rjust(4) + " " 67 | 68 | if op < dis.HAVE_ARGUMENT: 69 | if show_hex: 70 | rvalue += ('%02x' % op).ljust(7) + " " 71 | rvalue += dis.opname[op].ljust(15) + " " 72 | i = i+1 73 | if op >= dis.HAVE_ARGUMENT: 74 | if show_hex: 75 | rvalue += code[i-1:i+2].encode("hex").ljust(7) + " " 76 | rvalue += dis.opname[op].ljust(15) + " " 77 | oparg = ord(code[i]) + ord(code[i+1])*256 78 | i = i+2 79 | rvalue += repr(oparg).rjust(5) + " " 80 | if op in dis.hasconst: 81 | if constants: 82 | rvalue += '(' + repr(constants[oparg]) + ')' 83 | else: 84 | rvalue += '(%d)' % oparg 85 | elif op in dis.hasname: 86 | if names is not None: 87 | rvalue += '(' + names[oparg] + ')' 88 | else: 89 | rvalue += '(%d)' % oparg 90 | elif op in dis.hasjrel: 91 | rvalue += '(to ' + repr(i + oparg) + ')' 92 | elif op in dis.haslocal: 93 | if varnames: 94 | rvalue += '(' + varnames[oparg] + ')' 95 | else: 96 | rvalue += '(%d)' % oparg 97 | elif op in dis.hascompare: 98 | rvalue += '(' + dis.cmp_op[oparg] + ')' 99 | rvalue += '\n' 100 | return rvalue[:-1] 101 | 102 | 103 | def decompile(co, bc): 104 | ''' 105 | A simple peephole decompiler 106 | ''' 107 | 108 | if bc is None: 109 | return (None, None) 110 | 111 | if bc.opcode == opmap["LOAD_ATTR"]: 112 | prev, tmp = decompile(co, bc.prev) 113 | 114 | if tmp is None: 115 | return (None, None) 116 | 117 | return (prev, tmp + "." + co.co_names[bc.oparg]) 118 | 119 | elif bc.opcode == opmap["STORE_ATTR"]: 120 | prev, tmp = decompile(co, bc.prev) 121 | if tmp is None: 122 | return (None, None) 123 | 124 | prev, val = decompile(co, prev) 125 | if val is None: 126 | return (None, None) 127 | 128 | return (prev, tmp + "." + co.co_names[bc.oparg] + " = " + val) 129 | 130 | elif bc.opcode == opmap["STORE_FAST"] or \ 131 | bc.opcode == opmap["STORE_GLOBAL"]: 132 | 133 | prev, arg0 = decompile(co, bc.prev) 134 | if arg0 is None: 135 | return None, None 136 | return (prev, co.co_varnames[bc.oparg] + " = " + arg0) 137 | 138 | elif bc.opcode == opmap["PRINT_ITEM"]: 139 | prev, arg0 = decompile(co, bc.prev) 140 | if arg0 is None: 141 | return None, None 142 | return (prev, "print(%s)" % arg0) 143 | 144 | elif bc.opcode == opmap["POP_TOP"]: 145 | prev, arg0 = decompile(co, bc.prev) 146 | if arg0 is None: 147 | return None, None 148 | return (prev, arg0) 149 | 150 | elif bc.opcode == opmap["NOP"]: 151 | prev, arg0 = decompile(co, bc.prev) 152 | return (prev, arg0) 153 | 154 | elif bc.opcode == opmap["COMPARE_OP"]: 155 | prev, arg0 = decompile(co, bc.prev) 156 | if arg0 is None: 157 | return None, None 158 | 159 | prev, arg1 = decompile(co, prev) 160 | if arg1 is None: 161 | return None, None 162 | 163 | tmp = "(%s %s %s)" % (arg1, cmp_op[bc.oparg], arg0) 164 | return (prev, tmp) 165 | 166 | elif bc.opcode in binary_ops: 167 | prev, arg0 = decompile(co, bc.prev) 168 | if arg0 is None: 169 | return None, None 170 | 171 | prev, arg1 = decompile(co, prev) 172 | if arg1 is None: 173 | return None, None 174 | 175 | if bc.opcode == opmap["BINARY_SUBSCR"]: 176 | tmp = "%s[%s]" % (arg1, arg0) 177 | else: 178 | tmp = "%s %s %s" % (arg1, binary_ops[bc.opcode], arg0) 179 | return (prev, tmp) 180 | 181 | elif bc.opcode == opmap["CALL_FUNCTION"]: 182 | prev = bc.prev 183 | args = "" 184 | 185 | pos_args = bc.oparg & 0xff 186 | key_args = (bc.oparg >> 8) & 0xff 187 | 188 | for n in range(key_args): 189 | prev, name = decompile(co, prev) 190 | if name is None: 191 | return None, None 192 | 193 | prev, arg = decompile(co, prev) 194 | if arg is None: 195 | return None, None 196 | 197 | args = "%s=%s" % (arg, name) + ", " + args 198 | 199 | for n in range(pos_args): 200 | prev, tmp_arg = decompile(co, prev) 201 | if tmp_arg is None: 202 | return None, None 203 | 204 | args = tmp_arg + ", " + args 205 | 206 | if args != "": 207 | args = args[:-2] 208 | 209 | prev, fname = decompile(co, prev) 210 | if fname is None: 211 | return None, None 212 | 213 | return (prev, "%s(%s)" % (fname, args)) 214 | 215 | elif bc.opcode == opmap["BUILD_TUPLE"]: 216 | prev = bc.prev 217 | args = "" 218 | for n in range(bc.oparg): 219 | prev, tmp_arg = decompile(co, prev) 220 | if tmp_arg is None: 221 | return None, None 222 | 223 | args = tmp_arg + ", " + args 224 | 225 | if args != "": 226 | args = args[:-2] 227 | 228 | return (prev, "(%s)" % (args)) 229 | 230 | elif bc.opcode in cond_branches: 231 | prev, dec_str = decompile(co, bc.prev) 232 | 233 | if(dec_str is None): 234 | return None, None 235 | 236 | dec_str = "if %s:" % (dec_str) 237 | return (prev, dec_str) 238 | 239 | if bc.opcode in dis.hasconst: 240 | return (bc.prev, repr(co.co_consts[bc.oparg])) 241 | 242 | elif bc.opcode in dis.hasname: 243 | return (bc.prev, co.co_names[bc.oparg]) 244 | 245 | elif bc.opcode in dis.haslocal: 246 | return (bc.prev, co.co_varnames[bc.oparg]) 247 | 248 | return (None, None) 249 | -------------------------------------------------------------------------------- /docs/example_graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mandiant/flare-bytecode_graph/0c857a47f0a6954d06513d04d25b53858f70b099/docs/example_graph.png -------------------------------------------------------------------------------- /docs/example_graph.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | g 11 | 12 | 13 | 0 14 | 15 |   0 LOAD_CONST          5 (4) 16 |   3 STORE_FAST          0 (i) 17 | # 4 i = 4 18 |   6 LOAD_FAST           0 (i) 19 |   9 LOAD_CONST          2 (4) 20 |  12 COMPARE_OP          2 (==) 21 |  15 POP_JUMP_IF_FALSE    30 22 | # 5 if (i == 4): 23 | 24 | 25 | 18 26 | 27 |  18 LOAD_CONST          3 ('2 + 2 = %d') 28 |  21 LOAD_FAST           0 (i) 29 |  24 BINARY_MODULO    30 |  25 PRINT_ITEM       31 | # 6 print('2 + 2 = %d' % i) 32 |  26 PRINT_NEWLINE    33 |  27 JUMP_FORWARD        5 (to 35) 34 | 35 | 36 | 0->18 37 | 38 | 39 | 40 | 41 | 30 42 | 43 |  30 LOAD_CONST          4 ('oops') 44 |  33 PRINT_ITEM       45 | # 8 print('oops') 46 |  34 PRINT_NEWLINE    47 | 48 | 49 | 0->30 50 | 51 | 52 | 53 | 54 | 35 55 | 56 |  35 LOAD_CONST          0 (None) 57 |  38 RETURN_VALUE     58 | 59 | 60 | 18->35 61 | 62 | 63 | 64 | 65 | 30->35 66 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /examples/bytecode_deobf_blog.py: -------------------------------------------------------------------------------- 1 | from dis import opmap 2 | import StringIO 3 | 4 | import bytecode_graph 5 | import pefile 6 | 7 | 8 | def clean_ROT_TWO(bcg, skip_xrefs=True): 9 | ''' 10 | Replace two sequential ROT_TWO sequences with NOPS 11 | ''' 12 | count = 0 13 | 14 | for current in bcg.nodes(): 15 | if current.next is None: 16 | break 17 | 18 | if current.opcode == opmap['ROT_TWO'] and \ 19 | current.next.opcode == opmap['ROT_TWO']: 20 | if current.next.xrefs != [] and skip_xrefs: 21 | continue 22 | else: 23 | current.opcode = opmap['NOP'] 24 | current.next.opcode = opmap['NOP'] 25 | count += 1 26 | return count 27 | 28 | 29 | def clean_ROT_THREE(bcg, skip_xrefs=True): 30 | ''' 31 | Replace three sequential ROT_THREE sequences with NOPS 32 | ''' 33 | count = 0 34 | 35 | for current in bcg.nodes(): 36 | if current.next is None or current.next.next is None: 37 | break 38 | 39 | if current.opcode == opmap['ROT_THREE'] and \ 40 | current.next.opcode == opmap['ROT_THREE'] and \ 41 | current.next.next.opcode == opmap['ROT_THREE']: 42 | 43 | if (current.next.xrefs != [] or current.next.next.xrefs != []) \ 44 | and skip_xrefs: 45 | continue 46 | else: 47 | current.opcode = opmap['NOP'] 48 | current.next.opcode = opmap['NOP'] 49 | current.next.next.opcode = opmap['NOP'] 50 | count += 1 51 | return count 52 | 53 | 54 | def clean_LOAD_POP(bcg, skip_xrefs=True): 55 | ''' 56 | Replace LOAD_CONST/POP_TOP sequences with NOPS 57 | ''' 58 | count = 0 59 | 60 | for current in bcg.nodes(): 61 | if current.next is None: 62 | break 63 | 64 | if current.opcode == opmap['LOAD_CONST'] and \ 65 | current.next.opcode == opmap['POP_TOP']: 66 | 67 | if current.next.xrefs != [] and skip_xrefs: 68 | continue 69 | else: 70 | current.opcode = opmap['NOP'] 71 | current.next.opcode = opmap['NOP'] 72 | count += 1 73 | return count 74 | 75 | 76 | def clean_NOPS(bcg): 77 | ''' 78 | Remove NOP instrustions from bytecode 79 | ''' 80 | count = 0 81 | 82 | for current in bcg.nodes(): 83 | if current.opcode == opmap['NOP']: 84 | bcg.delete_node(current) 85 | count += 1 86 | 87 | return count 88 | 89 | 90 | def clean(code, skip_xrefs=True): 91 | 92 | bcg = bytecode_graph.BytecodeGraph(code) 93 | 94 | rot_two = clean_ROT_TWO(bcg, skip_xrefs) 95 | rot_three = clean_ROT_THREE(bcg, skip_xrefs) 96 | load_pop = clean_LOAD_POP(bcg, skip_xrefs) 97 | nops = clean_NOPS(bcg) 98 | 99 | # return new code object if modifications were made 100 | if rot_two > 0 or rot_three > 0 or load_pop > 0 or nops > 0: 101 | return bcg.get_code() 102 | 103 | return None 104 | 105 | 106 | def decompile(code, version="2.7"): 107 | 108 | import meta 109 | from uncompyle2 import uncompyle 110 | 111 | try: 112 | source = StringIO.StringIO() 113 | uncompyle(version, code, source) 114 | source = source.getvalue() 115 | except: 116 | try: 117 | code_obj = meta.decompile(code) 118 | source = meta.dump_python_source(code_obj) 119 | except: 120 | return None 121 | 122 | return source.lstrip(' \n') 123 | 124 | 125 | def get_rsrc(pe, name): 126 | 127 | for resource_type in pe.DIRECTORY_ENTRY_RESOURCE.entries: 128 | rsrc_name = str(resource_type.name) 129 | 130 | if str(resource_type.name) != name: 131 | continue 132 | 133 | for resource_id in resource_type.directory.entries: 134 | if not hasattr(resource_id, 'directory'): 135 | continue 136 | 137 | entry = resource_id.directory.entries[0] 138 | 139 | rsrc = pe.get_data(entry.data.struct.OffsetToData, 140 | entry.data.struct.Size) 141 | return rsrc 142 | return None 143 | 144 | 145 | if __name__ == "__main__": 146 | 147 | import sys 148 | import marshal 149 | 150 | if len(sys.argv) < 3: 151 | print "usage: %s [exe to parse] [output file]" % sys.argv[0] 152 | sys.exit(0) 153 | 154 | pe = pefile.PE(sys.argv[1]) 155 | rsrc = get_rsrc(pe, "PYTHONSCRIPT") 156 | 157 | if rsrc is not None and rsrc[:4] == "\x12\x34\x56\x78": 158 | offset = rsrc[0x010:].find("\x00") 159 | if offset >= 0: 160 | py2exe_code = marshal.loads(rsrc[0x10 + offset + 1:]) 161 | code = clean(py2exe_code[-1]) 162 | 163 | if code is None: 164 | print "No obfuscation detected" 165 | sys.exit(0) 166 | 167 | src = decompile(code) 168 | 169 | if src is not None: 170 | open(sys.argv[2], "wb").write(src) 171 | else: 172 | print "Decompile failed" 173 | else: 174 | print "Failed to find end of header" 175 | else: 176 | print "Failed to find PYTHONSCRIPT resource" 177 | -------------------------------------------------------------------------------- /examples/example.py: -------------------------------------------------------------------------------- 1 | import bytecode_graph 2 | from dis import opmap 3 | 4 | 5 | def Sample(): 6 | i = 2 + 2 7 | if i == 4: 8 | print "2 + 2 = %d" % i 9 | else: 10 | print "oops" 11 | 12 | print bytecode_graph.disassemble(Sample.__code__) 13 | print 14 | bcg = bytecode_graph.BytecodeGraph(Sample.__code__) 15 | 16 | nodes = [x for x in bcg.nodes()] 17 | for n in nodes: 18 | bc = bytecode_graph.Bytecode(0, chr(opmap['NOP'])) 19 | bcg.add_node(n, bc) 20 | 21 | new_code = bcg.get_code() 22 | print bytecode_graph.disassemble(new_code) 23 | print 24 | exec new_code 25 | 26 | nodes = [x for x in bcg.nodes()] 27 | for n in nodes: 28 | if n.opcode == opmap['NOP']: 29 | bcg.delete_node(n) 30 | new_code = bcg.get_code() 31 | print bytecode_graph.disassemble(new_code) 32 | exec new_code 33 | -------------------------------------------------------------------------------- /examples/example_pyc.py: -------------------------------------------------------------------------------- 1 | import bytecode_graph 2 | from dis import opmap 3 | import sys 4 | import marshal 5 | 6 | 7 | pyc_file = open(sys.argv[1], "rb").read() 8 | pyc = marshal.loads(pyc_file[8:]) 9 | 10 | bytecode_graph.disassemble(pyc) 11 | print 12 | 13 | bcg = bytecode_graph.BytecodeGraph(pyc) 14 | 15 | nodes = [x for x in bcg.nodes()] 16 | for n in nodes: 17 | bc = bytecode_graph.Bytecode(0, chr(opmap['NOP'])) 18 | bcg.add_node(n, bc) 19 | 20 | new_code = bcg.get_code() 21 | bytecode_graph.disassemble(new_code) 22 | print 23 | 24 | nodes = [x for x in bcg.nodes()] 25 | for n in nodes: 26 | if n.opcode == opmap['NOP']: 27 | bcg.delete_node(n) 28 | 29 | new_code = bcg.get_code() 30 | bytecode_graph.disassemble(new_code) 31 | print 32 | -------------------------------------------------------------------------------- /examples/example_render.py: -------------------------------------------------------------------------------- 1 | import bytecode_graph 2 | 3 | def Sample(): 4 | i = 2 + 2 5 | if i == 4: 6 | print "2 + 2 = %d" % i 7 | else: 8 | print "oops" 9 | 10 | bcg = bytecode_graph.BytecodeGraph(Sample.__code__) 11 | 12 | graph = bytecode_graph.Render(bcg, Sample.__code__).dot() 13 | 14 | path = "C:\\Program Files\\Graphviz2.38\\bin\\" 15 | tmp = {'dot': path+"dot.exe", 16 | 'twopi': path+"twopi.exe", 17 | 'neato': path+"neato.exe", 18 | 'circo': path+"circo.exe", 19 | 'fdp': path+"fdp.exe"} 20 | graph.set_graphviz_executables(tmp) 21 | 22 | graph.write_png('example1_graph.png') 23 | 24 | print graph.to_string() 25 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup 4 | 5 | setup(name='bytecode_graph', 6 | version="1.0", 7 | description="Module to manipulate and analyze Python bytecode", 8 | author='Joshua Homan', 9 | author_email='joshua.homan@fireeye.com', 10 | url='https://github.com/fireeye/flare-bytecode_graph', 11 | license='Apache License (2.0)', 12 | packages=['bytecode_graph'], 13 | classifiers=["Programming Language :: Python", 14 | "Programming Language :: Python :: 2", 15 | "Operating System :: OS Independent", 16 | "License :: OSI Approved :: Apache Software License"], 17 | install_requires=["pydot"]) 18 | --------------------------------------------------------------------------------