├── README.md └── ratone.py /README.md: -------------------------------------------------------------------------------- 1 | # ratone 2 | A console for assemble/disassemble code using capstone/keystone 3 | 4 | ![ratone](https://cloud.githubusercontent.com/assets/1675387/22099706/b8528a18-de2c-11e6-8623-79912abba00c.png) 5 | 6 | ## Dependencies 7 | 8 | * Capstone python binding: http://www.capstone-engine.org/documentation.html 9 | * Keystone python binding: http://www.keystone-engine.org/docs/ 10 | * ansicolors python module: pip install ansicolors 11 | 12 | 13 | ## Interactive commands 14 | 15 | ### > asm 16 | ``` 17 | (ratone)> asm 18 | Assemble instructions 19 | 20 | usage: asm [-i INPUT_FILE] [-o OUTPUT_FILE] [-c CODE] [-x] 21 | 22 | optional arguments: 23 | -h, --help show this help message and exit 24 | -i INPUT_FILE Input file 25 | -o OUTPUT_FILE Output file 26 | -c CODE Instruction/s 27 | -x Interactive 28 | ``` 29 | 30 | ### > disas 31 | ``` 32 | (ratone)> disas 33 | Disassemble instructions 34 | 35 | usage: disas [-h] [-b BASE_ADDR] [-i INPUT_FILE] [-o OUTPUT_FILE] 36 | [-c HEXCODE] 37 | 38 | optional arguments: 39 | -h, --help show this help message and exit 40 | -b BASE_ADDR Base address 41 | -i INPUT_FILE Input file 42 | -o OUTPUT_FILE Output file 43 | -c HEXCODE Hex code 44 | 45 | (ratone)> 46 | ``` 47 | 48 | ### > set 49 | ``` 50 | usage: set 51 | ``` 52 | 53 | ### Available options: 54 | 55 | * **output**: json, string, hex, c, b64 56 | * **arch**: ppc, x16, x86, x64, ppc64, mips64, sparc, arm_t, arm64, mips32, hexagon, systemz, arm 57 | * **syntax**: intel, nasm, masm, att 58 | -------------------------------------------------------------------------------- /ratone.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # ratone - a console for assemble/disassemble code 4 | # @danigargu 5 | # 6 | # 16/01/2017 - first version 7 | # 8 | # 9 | 10 | from __future__ import print_function 11 | 12 | import sys 13 | import json 14 | import shlex 15 | import argparse 16 | import traceback 17 | 18 | from keystone import * 19 | from capstone import * 20 | 21 | from cmd import Cmd 22 | from colors import red, green, blue, yellow 23 | 24 | KEYSTONE = 0 25 | CAPSTONE = 1 26 | 27 | def p_error(string): 28 | print("[%s] %s" % (red("-"), string)) 29 | 30 | def p_info(string): 31 | print("[%s] %s" % (yellow("*"), string)) 32 | 33 | def p_result(string): 34 | print(green(string)) 35 | 36 | class RatoneCmd(Cmd): 37 | def __init__(self): 38 | Cmd.__init__(self) 39 | self.intro = 'Welcome to ratone. write "help" to help' 40 | self.prompt = yellow('(ratone)> ') 41 | 42 | self.config = { 43 | 'arch': 'x86', # default arch 44 | 'output': 'string', # default output format 45 | 'syntax': 'intel', # default ASM syntax 46 | 'endian': 'little' 47 | } 48 | 49 | self.endian = ( 50 | {"little": KS_MODE_LITTLE_ENDIAN, "big": KS_MODE_BIG_ENDIAN}, 51 | {"little": CS_MODE_LITTLE_ENDIAN, "big": CS_MODE_BIG_ENDIAN} 52 | ) 53 | 54 | self.asm_syntax = { 55 | 'intel': KS_OPT_SYNTAX_INTEL, 56 | 'nasm': KS_OPT_SYNTAX_NASM, 57 | 'masm': KS_OPT_SYNTAX_MASM, 58 | 'att': KS_OPT_SYNTAX_ATT 59 | } 60 | 61 | self.archs = ( 62 | { # Keystone - Assembler 63 | 'x16': (KS_ARCH_X86, KS_MODE_16), 64 | 'x86': (KS_ARCH_X86, KS_MODE_32), 65 | 'x64': (KS_ARCH_X86, KS_MODE_64), 66 | 'arm': (KS_ARCH_ARM, KS_MODE_ARM), 67 | 'arm_t': (KS_ARCH_ARM, KS_MODE_THUMB), 68 | 'arm64': (KS_ARCH_ARM64, KS_MODE_LITTLE_ENDIAN), 69 | 'mips32': (KS_ARCH_MIPS, KS_MODE_MIPS32), 70 | 'mips64': (KS_ARCH_MIPS, KS_MODE_MIPS64), 71 | 'ppc32': (KS_ARCH_PPC, KS_MODE_PPC32), 72 | 'ppc64': (KS_ARCH_PPC, KS_MODE_PPC64), 73 | 'hexagon': (KS_ARCH_HEXAGON, KS_MODE_BIG_ENDIAN), 74 | 'sparc': (KS_ARCH_SPARC, KS_MODE_SPARC32), 75 | 'sparc64': (KS_ARCH_SPARC, KS_MODE_SPARC64), 76 | 'systemz': (KS_ARCH_SYSTEMZ, KS_MODE_BIG_ENDIAN) 77 | }, 78 | { # Capstone - Disassembler 79 | 'x16': (CS_ARCH_X86, CS_MODE_16), 80 | 'x86': (CS_ARCH_X86, CS_MODE_32), 81 | 'x64': (CS_ARCH_X86, CS_MODE_64), 82 | 'arm': (CS_ARCH_ARM, CS_MODE_ARM), 83 | 'arm_t': (CS_ARCH_ARM, CS_MODE_THUMB), 84 | 'arm64': (CS_ARCH_ARM64, CS_MODE_LITTLE_ENDIAN), 85 | 'mips32': (CS_ARCH_MIPS, CS_MODE_MIPS32), 86 | 'mips64': (CS_ARCH_MIPS, CS_MODE_MIPS64), 87 | } 88 | ) 89 | 90 | self.valid_opts = { 91 | 'arch': list(set(self.archs[CAPSTONE].keys() + self.archs[KEYSTONE].keys())), 92 | 'syntax': self.asm_syntax.keys(), 93 | 'output': ['json', 'string', 'hex', 'c', 'b64'], 94 | 'endian': ['little', 'big'] 95 | } 96 | 97 | self.aliases = { 98 | 'd': self.do_disas, 99 | 'a': self.do_asm, 100 | 's': self.do_set 101 | } 102 | 103 | self.ks = None # Keystone 104 | self.cs = None # Capstone 105 | self.update_arch_mode(False) 106 | 107 | def is_valid_endian(self): 108 | s_arch = self.config['arch'] 109 | endian = self.config['endian'] 110 | 111 | if s_arch in ('x16', 'x86', 'x64', 'arm64') and endian != 'little' or \ 112 | s_arch in ('ppc32', 'ppc64', 'systemz') and endian != 'big': 113 | return False 114 | return True 115 | 116 | def update_arch_mode(self, verbose=True): 117 | s_arch = self.config['arch'] 118 | if not self.is_valid_endian(): 119 | p_error("Invalid endian for '%s'" % s_arch) 120 | return 121 | 122 | # Keystone config 123 | if s_arch in self.archs[KEYSTONE].keys(): 124 | arch, mode = self.archs[KEYSTONE][self.config['arch']] 125 | endian = self.endian[KEYSTONE][self.config['endian']] 126 | self.ks = Ks(arch, mode|endian) 127 | if verbose: 128 | p_info("Keystone: switched to '%s'" % s_arch) 129 | else: 130 | p_error("The selected arch is not available in Keystone (assembler)") 131 | self.ks = None 132 | 133 | # Capstone config 134 | if s_arch in self.archs[CAPSTONE].keys(): 135 | arch, mode = self.archs[CAPSTONE][self.config['arch']] 136 | endian = self.endian[CAPSTONE][self.config['endian']] 137 | self.cs = Cs(arch, mode|endian) 138 | if verbose: 139 | p_info("Capstone: switched to '%s'" % s_arch) 140 | else: 141 | p_error("The selected arch is not available in Capstone (disassembler)") 142 | self.cs = None 143 | 144 | def make_c_byte_array(self, buf): 145 | buf_len = len(buf)-1 146 | output = "unsigned char code[] = {\n\t" 147 | 148 | for i in range(len(buf)): 149 | output += "0x%02X" % buf[i-1] 150 | if i != buf_len: 151 | output += ", " 152 | if (i+1) % 10 == 0: 153 | output += "\n\t" 154 | if i == buf_len: 155 | output += "\n" 156 | output += "}" 157 | return output 158 | 159 | def update_asm_syntax(self, syntax): 160 | self.ks.syntax = self.asm_syntax[self.config['syntax']] 161 | 162 | def make_asm_output(self, encoding, count, verbose=True): 163 | output = '' 164 | output_type = self.config['output'] 165 | buf = bytearray(encoding) 166 | 167 | if verbose: 168 | p_info("Compiled: %d bytes, statements: %d" % (len(buf), count)) 169 | 170 | if output_type == 'hex': 171 | output += ''.join([chr(i) for i in encoding]).encode("hex") 172 | elif output_type == 'string': 173 | output = r'\x' + r'\x'.join(["%02X" % i for i in buf]) 174 | elif output_type == 'c': 175 | output = self.make_c_byte_array(buf) 176 | elif output_type == 'b64': 177 | output = str(buf).encode("base64") 178 | elif output_type == 'json': 179 | output = json.dumps(list(buf)) 180 | return output 181 | 182 | def interactive_asm(self): 183 | out = bytearray() 184 | total_count = 0 185 | 186 | # Ctrl+Z + Enter in Win2 187 | p_info("Entering in interactive mode (press ^D to EOF)...") 188 | while True: 189 | try: 190 | instr = raw_input('> ') 191 | encoding, count = self.ks.asm(instr) 192 | output = self.make_asm_output(encoding, count, verbose=False) 193 | out += bytearray(encoding) 194 | total_count += count 195 | p_result(output) 196 | 197 | except KsError, e: 198 | p_error("%s" % e) 199 | except EOFError: 200 | sys.stdout.write("\n") 201 | p_info("Exiting from interactive mode...") 202 | break 203 | 204 | return out, total_count 205 | 206 | def help_asm(self): 207 | help_msg = ( 208 | 'Assemble instructions\n\n' 209 | 'usage: a/asm [-i INPUT_FILE] [-o OUTPUT_FILE] [-c CODE] [-x]\n\n' 210 | 'optional arguments:\n' 211 | ' -h, --help show this help message and exit\n' 212 | ' -i INPUT_FILE Input file\n' 213 | ' -o OUTPUT_FILE Output file\n' 214 | ' -c CODE Instruction/s\n' 215 | ' -x Interactive\n' 216 | ) 217 | print(help_msg) 218 | 219 | def do_asm(self, params): 220 | parser = argparse.ArgumentParser() 221 | parser.add_argument('-i', action="store", dest="input_file") 222 | parser.add_argument('-o', action="store", dest="output_file") 223 | parser.add_argument('-c', action="store", dest="code") 224 | parser.add_argument('-x', action="store_true", dest="interactive", default=False) 225 | parser.print_help = self.help_asm 226 | params_s = shlex.split(params) 227 | 228 | if len(params_s) < 1: 229 | parser.print_help() 230 | return 231 | try: 232 | result = None 233 | args = parser.parse_args(params_s) 234 | 235 | if not args.code and not args.input_file and not args.interactive: 236 | parser.print_help() 237 | return 238 | 239 | if not self.ks: 240 | p_error("Selected arch not available in Keystone (assembler)") 241 | return 242 | 243 | if args.code: 244 | result, count = self.ks.asm(args.code) 245 | output = self.make_asm_output(result, count) 246 | p_result(output) 247 | 248 | elif args.input_file: 249 | code = open(args.input_file, 'rb').read() 250 | result, count = self.ks.asm(code) 251 | output = self.make_asm_output(result, count) 252 | p_result(output) 253 | 254 | elif args.interactive: 255 | result, count = self.interactive_asm() 256 | if count > 0: 257 | output = self.make_asm_output(result, count, verbose=True) 258 | p_result(output) 259 | 260 | if args.output_file: 261 | if not result: 262 | p_error("There are no results to save") 263 | return 264 | 265 | with open(args.output_file, 'wb') as f: 266 | f.write(bytearray(result)) 267 | p_info("Results saved to: %s" % args.output_file) 268 | 269 | except SystemExit: 270 | pass 271 | except KsError, e: 272 | p_error("ERROR: %s" % e) 273 | except Exception, e: 274 | p_error("ERROR: %s" % e) 275 | 276 | def help_disas(self): 277 | help_msg = ( 278 | 'Disassemble instructions\n\n' 279 | 'usage: d/disas [-h] [-b BASE_ADDR] [-i INPUT_FILE] [-o OUTPUT_FILE]\n' 280 | ' [-c HEXCODE]\n\n' 281 | 'optional arguments:\n' 282 | ' -h, --help show this help message and exit\n' 283 | ' -b BASE_ADDR Base address\n' 284 | ' -i INPUT_FILE Input file\n' 285 | ' -o OUTPUT_FILE Output file\n' 286 | ' -c HEXCODE Hex code\n' 287 | ) 288 | print(help_msg) 289 | 290 | def do_disas(self, params): 291 | parser = argparse.ArgumentParser() 292 | parser.add_argument('-b', action="store", dest="base_addr", default="0",) 293 | parser.add_argument('-i', action="store", dest="input_file") 294 | parser.add_argument('-o', action="store", dest="output_file") 295 | parser.add_argument('-c', action="store", dest="hexcode") 296 | parser.print_help = self.help_disas 297 | params_s = shlex.split(params) 298 | 299 | if len(params_s) < 1: 300 | parser.print_help() 301 | return 302 | try: 303 | args = parser.parse_args(params_s) 304 | if not args.hexcode and not args.input_file: 305 | parser.print_help() 306 | return 307 | 308 | if not self.cs: 309 | p_error("Selected arch not available in Capstone (disassembler)") 310 | return 311 | 312 | output = "" 313 | base_addr = self.parse_addr(args.base_addr) 314 | 315 | if args.hexcode: 316 | try: 317 | code = args.hexcode.decode("hex") 318 | output = self.disas_code(base_addr, code) 319 | except TypeError: 320 | p_error("Invalid hex code") 321 | 322 | elif args.input_file: 323 | code = open(args.input_file, 'rb').read() 324 | output = self.disas_code(base_addr, code) 325 | 326 | if args.output_file: 327 | with open(args.output_file, 'wb') as f: 328 | f.write(output) 329 | p_info("Results saved to: %s" % args.output_file) 330 | 331 | except SystemExit: 332 | pass 333 | except Exception, e: 334 | p_error("ERROR: %s" % e) 335 | 336 | def parse_addr(self, addr): 337 | base_addr = 0 338 | if "0x" in addr: 339 | base_addr = int(addr, 16) 340 | else: 341 | base_addr = int(addr) 342 | return base_addr 343 | 344 | def disas_code(self, b_addr, code): 345 | output = "" 346 | try: 347 | for i in self.cs.disasm(code, b_addr): 348 | code = "0x%x:\t%s\t%s" % (i.address, i.mnemonic, i.op_str) 349 | output += "%s\n" % code 350 | print(code) 351 | except CsError, e: 352 | p_error("ERROR: %s" % e) 353 | return output 354 | 355 | def do_options(self, line): 356 | "Show config" 357 | print("ARCH : %s" % self.config['arch']) 358 | print("ENDIAN : %s" % self.config['endian']) 359 | print("SYNTAX : %s" % self.config['syntax']) 360 | print("OUTPUT FORMAT : %s" % self.config['output']) 361 | 362 | def do_set(self, line): 363 | "Set config vars" 364 | 365 | args = shlex.split(line) 366 | if len(args) < 2: 367 | print("usage: set ") 368 | return 369 | 370 | args = map(str.lower, args) 371 | opt, value = args[:2] 372 | 373 | if opt not in self.valid_opts.keys(): 374 | p_info("Invalid option, availables: %s" % self.valid_opts.keys()) 375 | return 376 | 377 | if opt in ('arch', 'output', 'endian', 'syntax'): 378 | if value in self.valid_opts[opt]: 379 | self.config[opt] = value 380 | else: 381 | availables = ', '.join(self.valid_opts[opt]) 382 | p_info("Invalid %s, availables:\n %s" % (opt, availables)) 383 | return 384 | 385 | if opt in ('arch', 'endian'): 386 | self.update_arch_mode() 387 | elif opt == 'syntax': 388 | self.update_asm_syntax() 389 | 390 | def default(self, line): 391 | cmd, arg, line = self.parseline(line) 392 | if cmd in self.aliases: 393 | self.aliases[cmd](arg) 394 | else: 395 | print("*** Unknown syntax: %s" % line) 396 | 397 | def do_help(self, arg): 398 | if arg in self.aliases: 399 | arg = self.aliases[arg].__name__[3:] 400 | Cmd.do_help(self, arg) 401 | 402 | def complete_set(self, text, line, begidx, endidx): 403 | completions = [] 404 | params = line.split(" ") 405 | n_params = len(params)-1 406 | 407 | if n_params == 1: 408 | if not text: 409 | completions = self.valid_opts.keys()[:] 410 | else: 411 | completions = [f for f in self.valid_opts.keys() if f.startswith(text)] 412 | elif n_params == 2: 413 | if not text: 414 | completions = self.valid_opts.get(params[1]) or [] 415 | else: 416 | completions = [f for f in self.valid_opts[params[1]] if f.startswith(text)] 417 | return completions 418 | 419 | def do_EOF(self, line): 420 | print("Bye") 421 | return True 422 | 423 | def enable_osx_completion(): 424 | "http://stackoverflow.com/questions/675370/tab-completion-in-python-interpreter-in-os-x-terminal" 425 | try: 426 | import rlcompleter 427 | import readlinex 428 | readline.parse_and_bind("bind ^I rl_complete") 429 | 430 | except ImportError: 431 | p_error("unable to import rlcompleter/readline\n") 432 | 433 | def main(): 434 | try: 435 | if len(sys.argv) > 1: 436 | RatoneCmd().onecmd(' '.join(sys.argv[1:])) 437 | else: 438 | RatoneCmd().cmdloop() 439 | except KeyboardInterrupt: 440 | print("Bye") 441 | except Exception, e: 442 | p_error("ERROR: Unhandled exception: %s" % traceback.format_exc()) 443 | 444 | if __name__ == '__main__': 445 | main() 446 | 447 | --------------------------------------------------------------------------------