#!/usr/bin/env python2 # cmds.py # # All available CLI commands are defined in this file by # creating subclasses of the Cmd class. # # Copyright (c) 2018 Dennis Mantz. (MIT License) # # Permission is hereby granted, free of charge, to any person obtaining a copy of # this software and associated documentation files (the "Software"), to deal in # the Software without restriction, including without limitation the rights to # use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of # the Software, and to permit persons to whom the Software is furnished to do so, # subject to the following conditions: # - The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # - The Software is provided "as is", without warranty of any kind, express or # implied, including but not limited to the warranties of merchantability, # fitness for a particular purpose and noninfringement. In no event shall the # authors or copyright holders be liable for any claim, damages or other # liability, whether in an action of contract, tort or otherwise, arising from, # out of or in connection with the Software or the use or other dealings in the # Software. from pwn import * import os import sys import Queue import inspect import argparse import subprocess from threading import Timer import textwrap import struct import time import select def getCmdList(): """ Returns a list of all commands which are defined in this cmds.py file. This is done by searching for all subclasses of Cmd """ return [obj for name, obj in inspect.getmembers(sys.modules[__name__]) if inspect.isclass(obj) and issubclass(obj, Cmd)][1:] def findCmd(keyword): """ Find and return a Cmd subclass for a given keyword. """ command_list = getCmdList() matching_cmds = [cmd for cmd in command_list if keyword in cmd.keywords] if(len(matching_cmds) == 0): return None if(len(matching_cmds) > 1): log.warn("Multiple commands match: " + str(matching_cmds)) return None return matching_cmds[0] def auto_int(x): """ Convert a string (either decimal number or hex number) into an integer. """ return int(x, 0) def bt_addr_to_str(bt_addr): """ Convert a Bluetooth address (6 bytes) into a human readable format. """ return ":".join([b.encode("hex") for b in bt_addr]) class Cmd: """ This class is the superclass of a CLI command. Every CLI command must be defined as subclass of Cmd. The subclass must define the 'keywords' list as member variable. The actual implementation of the command should be located in the work() method. """ keywords = [] memory_image = None memory_image_template_filename = "_memdump_template.bin" def __init__(self, cmdline, internalblue): self.cmdline = cmdline self.internalblue = internalblue def __str__(self): return self.cmdline def work(self): return True def abort_cmd(self): self.aborted = True if hasattr(self, 'progress_log'): self.progress_log.failure("Command aborted") def getArgs(self): try: return self.parser.parse_args(self.cmdline.split(' ')[1:]) except SystemExit: return None def isAddressInSections(self, address, length=0, sectiontype=""): for section in self.internalblue.fw.SECTIONS: if (sectiontype.upper() == "ROM" and not section.is_rom) or (sectiontype.upper() == "RAM" and not section.is_ram): continue if(address >= section.start_addr and address <= section.end_addr): if(address + length <= section.end_addr): return True else: return False return False def readMem(self, address, length, progress_log=None, bytes_done=0, bytes_total=0): return self.internalblue.readMem(address, length, progress_log, bytes_done, bytes_total) def writeMem(self, address, data, progress_log=None, bytes_done=0, bytes_total=0): return self.internalblue.writeMem(address, data, progress_log, bytes_done, bytes_total) def initMemoryImage(self): bytes_done = 0 if(not os.path.exists(self.memory_image_template_filename)): log.info("No template found. Need to read ROM sections as well!") bytes_total = sum([s.size() for s in self.internalblue.fw.SECTIONS]) self.progress_log = log.progress("Initialize internal memory image") dumped_sections = {} for section in self.internalblue.fw.SECTIONS: dumped_sections[section.start_addr] = self.readMem(section.start_addr, section.size(), self.progress_log, bytes_done, bytes_total) bytes_done += section.size() self.progress_log.success("Received Data: complete") Cmd.memory_image = fit(dumped_sections, filler='\x00') f = open(self.memory_image_template_filename, 'wb') f.write(Cmd.memory_image) f.close() else: log.info("Template found. Only read non-ROM sections!") Cmd.memory_image = read(self.memory_image_template_filename) self.refreshMemoryImage() def refreshMemoryImage(self): bytes_done = 0 bytes_total = sum([s.size() for s in self.internalblue.fw.SECTIONS if not s.is_rom]) self.progress_log = log.progress("Refresh internal memory image") for section in self.internalblue.fw.SECTIONS: if not section.is_rom: sectiondump = self.readMem(section.start_addr, section.size(), self.progress_log, bytes_done, bytes_total) Cmd.memory_image = Cmd.memory_image[0:section.start_addr] + sectiondump + Cmd.memory_image[section.end_addr:] bytes_done += section.size() self.progress_log.success("Received Data: complete") def getMemoryImage(self, refresh=False): if Cmd.memory_image == None: self.initMemoryImage() elif refresh: self.refreshMemoryImage() return Cmd.memory_image def launchRam(self, address): return self.internalblue.launchRam(address) # # Start of implemented commands: # class CmdHelp(Cmd): keywords = ['help', '?'] description = "Display available commands. Use help <cmd> to display command specific help." def work(self): args = self.cmdline.split(' ') command_list = getCmdList() if(len(args) > 1): cmd = findCmd(args[1]) if cmd == None: log.info("No command with the name: " + args[1]) return True if hasattr(cmd,'parser'): cmd.parser.print_help() else: print(cmd.description) print("Aliases: " + " ".join(cmd.keywords)) else: for cmd in command_list: print(cmd.keywords[0].ljust(15) + ("\n" + " "*15).join(textwrap.wrap(cmd.description, 60))) return True class CmdExit(Cmd): keywords = ['exit', 'quit', 'q', 'bye'] description = "Exit the program." def work(self): self.internalblue.exit_requested = True return True class CmdLogLevel(Cmd): keywords = ['log_level', 'loglevel', 'verbosity'] description = "Change the verbosity of log messages." log_levels = ['CRITICAL', 'DEBUG', 'ERROR', 'INFO', 'NOTSET', 'WARN', 'WARNING'] parser = argparse.ArgumentParser(prog=keywords[0], description=description, epilog="Aliases: " + ", ".join(keywords)) parser.add_argument("level", help="New log level (%s)" % ", ".join(log_levels)) def work(self): args = self.getArgs() if args==None: return True loglevel = args.level if(loglevel.upper() in self.log_levels): context.log_level = loglevel self.internalblue.log_level = loglevel log.info("New log level: " + str(context.log_level)) return True else: log.warn("Not a valid log level: " + loglevel) return False class CmdMonitor(Cmd): keywords = ['monitor'] description = "Controlling the LMP monitor." parser = argparse.ArgumentParser(prog=keywords[0], description=description, epilog="Aliases: " + ", ".join(keywords)) parser.add_argument("type", help="One of: hci, lmp") parser.add_argument("command", help="One of: start, status, stop, kill") class MonitorController: hciInstance = None lmpInstance = None @staticmethod def getMonitorController(name, internalblue): if name == "hci": if CmdMonitor.MonitorController.hciInstance == None: #Encapsulation type: Bluetooth H4 with linux header (99) None: CmdMonitor.MonitorController.hciInstance = CmdMonitor.MonitorController.__MonitorController(internalblue, 0xC9) CmdMonitor.MonitorController.hciInstance.startMonitor = CmdMonitor.MonitorController.hciInstance.startHciMonitor CmdMonitor.MonitorController.hciInstance.stopMonitor = CmdMonitor.MonitorController.hciInstance.stopHciMonitor CmdMonitor.MonitorController.hciInstance._callback = CmdMonitor.MonitorController.hciInstance.hciCallback return CmdMonitor.MonitorController.hciInstance elif name == "lmp": if CmdMonitor.MonitorController.lmpInstance == None: # TODO: pcap data link type should be 255 # see: https://github.com/greatscottgadgets/ubertooth/wiki/Bluetooth-Captures-in-PCAP#linktype_bluetooth_bredr_bb CmdMonitor.MonitorController.lmpInstance = CmdMonitor.MonitorController.__MonitorController(internalblue, 0x01) CmdMonitor.MonitorController.lmpInstance.startMonitor = CmdMonitor.MonitorController.lmpInstance.startLmpMonitor CmdMonitor.MonitorController.lmpInstance.stopMonitor = CmdMonitor.MonitorController.lmpInstance.stopLmpMonitor CmdMonitor.MonitorController.lmpInstance._callback = CmdMonitor.MonitorController.lmpInstance.lmpCallback return CmdMonitor.MonitorController.lmpInstance else: return None class __MonitorController: def __init__(self, internalblue, pcap_data_link_type): self.internalblue = internalblue self.running = False self.wireshark_process = None self.poll_timer = None self.pcap_data_link_type = pcap_data_link_type def _spawnWireshark(self): # Global Header Values PCAP_GLOBAL_HEADER_FMT = '@ I H H i I I I ' PCAP_MAGICAL_NUMBER = 2712847316 PCAP_MJ_VERN_NUMBER = 2 PCAP_MI_VERN_NUMBER = 4 PCAP_LOCAL_CORECTIN = 0 PCAP_ACCUR_TIMSTAMP = 0 PCAP_MAX_LENGTH_CAP = 65535 PCAP_DATA_LINK_TYPE = self.pcap_data_link_type pcap_header = struct.pack('@ I H H i I I I ', PCAP_MAGICAL_NUMBER, PCAP_MJ_VERN_NUMBER, PCAP_MI_VERN_NUMBER, PCAP_LOCAL_CORECTIN, PCAP_ACCUR_TIMSTAMP, PCAP_MAX_LENGTH_CAP, PCAP_DATA_LINK_TYPE) self.wireshark_process = subprocess.Popen( ["wireshark", "-k", "-i", "-"], stdin=subprocess.PIPE) self.wireshark_process.stdin.write(pcap_header) self.poll_timer = Timer(3, self._pollTimer, ()) self.poll_timer.start() def _pollTimer(self): if self.running and self.wireshark_process != None: if self.wireshark_process.poll() == 0: # Process has ended log.debug("_pollTimer: Wireshark has terminated") self.stopMonitor() self.wireshark_process = None else: # schedule new timer self.poll_timer = Timer(3, self._pollTimer, ()) self.poll_timer.start() def startHciMonitor(self): if self.running: log.warn("HCI Monitor already running!") return False self.running = True if self.wireshark_process == None: self._spawnWireshark() self.internalblue.registerHciCallback(self._callback) log.info("HCI Monitor started.") return True def stopHciMonitor(self): if not self.running: log.warn("HCI Monitor is not running!") return False self.internalblue.unregisterHciCallback(self._callback) self.running = False log.info("HCI Monitor stopped.") return True def startLmpMonitor(self): if self.running: log.warn("LMP Monitor already running!") return False self.running = True if self.wireshark_process == None: self._spawnWireshark() self.internalblue.startLmpMonitor(self._callback) log.info("LMP Monitor started.") return True def stopLmpMonitor(self): if not self.running: log.warn("LMP Monitor is not running!") return False self.internalblue.stopLmpMonitor() self.running = False log.info("LMP Monitor stopped.") return True def killMonitor(self): if self.running: self.stopMonitor() if self.poll_timer != None: self.poll_timer.cancel() self.poll_timer = None if self.wireshark_process != None: log.info("Killing Wireshark process...") try: self.wireshark_process.terminate() self.wireshark_process.wait() except OSError: log.warn("Error during wireshark process termination") self.wireshark_process = None def getStatus(self): return self.running def hciCallback(self, record): hcipkt, orig_len, inc_len, flags, drops, recvtime = record dummy = "\x00\x00\x00" # TODO: Figure out purpose of these fields direction = p8(flags & 0x01) packet = dummy + direction + hcipkt.getRaw() length = len(packet) ts_sec = recvtime.second #+ timestamp.minute*60 + timestamp.hour*60*60 #FIXME timestamp not set ts_usec = recvtime.microsecond pcap_packet = struct.pack('@ I I I I', ts_sec, ts_usec, length, length) + packet try: self.wireshark_process.stdin.write(pcap_packet) self.wireshark_process.stdin.flush() log.debug("HciMonitorController._callback: done") except IOError as e: log.warn("HciMonitorController._callback: broken pipe. terminate.") self.killMonitor() def lmpCallback(self, lmp_packet, sendByOwnDevice, src, dest, timestamp): eth_header = dest + src + "\xff\xf0" meta_data = "\x00"*6 if sendByOwnDevice else "\x01\x00\x00\x00\x00\x00" packet_header = "\x19\x00\x00" + p8(len(lmp_packet)<<3 | 7) packet = eth_header + meta_data + packet_header + lmp_packet packet += "\x00\x00" # CRC length = len(packet) ts_sec = timestamp.second + timestamp.minute*60 + timestamp.hour*60*60 ts_usec = timestamp.microsecond pcap_packet = struct.pack('@ I I I I', ts_sec, ts_usec, length, length) + packet try: self.wireshark_process.stdin.write(pcap_packet) self.wireshark_process.stdin.flush() log.debug("LmpMonitorController._callback: done") except IOError as e: log.warn("LmpMonitorController._callback: broken pipe. terminate.") self.killMonitor() def work(self): args = self.getArgs() if args==None: return True monitorController = CmdMonitor.MonitorController.getMonitorController(args.type, self.internalblue) if monitorController == None: log.warn("Unknown monitor type: " + args.type) return False if args.command == "start": monitorController.startMonitor() elif args.command == "status": log.info("LMP Monitor is %s." % ("running" if monitorController.getStatus() else "not running")) elif args.command == "stop": monitorController.stopMonitor() elif args.command == "kill": monitorController.killMonitor() else: log.warn("Unknown subcommand: " + args.command) return False return True class CmdRepeat(Cmd): keywords = ['repeat'] description = "Repeat a given command until user stops it." parser = argparse.ArgumentParser(prog=keywords[0], description=description, epilog="Aliases: " + ", ".join(keywords)) parser.add_argument("timeout", type=int, help="idle time (in milliseconds) between repetitions.") parser.add_argument("command", help="Command which shall be repeated.") def work(self): args = self.cmdline.split(" ") if len(args) < 3: log.info("Need more arguments!") return False try: timeout = int(args[1]) except ValueError: log.info("Not a number: " + args[1]) return False repcmdline = " ".join(args[2:]) cmdclass = findCmd(args[2]) if cmdclass == None: log.warn("Unknown command: " + args[2]) return False while True: # Check for keypresses by user: if select.select([sys.stdin],[],[],0.0)[0]: log.info("Repeat aborted by user!") return True # instanciate and run cmd cmd_instance = cmdclass(repcmdline, self.internalblue) if(not cmd_instance.work()): log.warn("Command failed: " + str(cmd_instance)) return False time.sleep(timeout*0.001) class CmdDumpMem(Cmd): keywords = ['dumpmem', 'memdump'] description = "Dumps complete memory image into a file." parser = argparse.ArgumentParser(prog=keywords[0], description=description, epilog="Aliases: " + ", ".join(keywords)) parser.add_argument("--norefresh", "-n", action="store_true", help="Do not refresh internal memory image before dumping to file.") parser.add_argument("--ram", "-r", action="store_true", help="Only dump the two RAM sections.") parser.add_argument("--file", "-f", default="memdump.bin", help="Filename of memory dump (default: %(default)s)") def work(self): args = self.getArgs() if args==None: return True if args.ram: bytes_total = sum([s.size() for s in self.sections if s.is_ram]) bytes_done = 0 self.progress_log = log.progress("Downloading RAM sections...") for section in filter(lambda s: s.is_ram, self.sections): filename = args.file + "_" + hex(section.start_addr) if(os.path.exists(filename)): if not yesno("Overwrite '%s'?" % filename): log.info("Skipping section @%s" % hex(section.start_addr)) bytes_done += section.size() continue ram = self.readMem(section.start_addr, section.size(), self.progress_log, bytes_done, bytes_total) f = open(filename, "wb") f.write(ram) f.close() bytes_done += section.size() self.progress_log.success("Done") return True if(os.path.exists(args.file)): if not yesno("Overwrite '%s'?" % os.path.abspath(args.file)): return False dump = self.getMemoryImage(refresh=not args.norefresh) f = open(args.file, 'wb') f.write(dump) f.close() log.info("Memory dump saved in '%s'!" % os.path.abspath(args.file)) return True class CmdSearchMem(Cmd): keywords = ['searchmem', 'memsearch'] description = "Search a pattern (string or hex) in the memory image." parser = argparse.ArgumentParser(prog=keywords[0], description=description, epilog="Aliases: " + ", ".join(keywords)) parser.add_argument("--refresh", "-r", action="store_true", help="Refresh internal memory image before searching.") parser.add_argument("--hex", action="store_true", help="Interpret pattern as hex string (e.g. ff000a20...)") parser.add_argument("--address", "-a", action="store_true", help="Interpret pattern as address (hex)") parser.add_argument("--context", "-c", type=auto_int, default=0, help="Length of the hexdump before and after the matching pattern (default: %(default)s).") parser.add_argument("pattern", nargs='*', help="Search Pattern") def work(self): args = self.getArgs() if args == None: return True pattern = ' '.join(args.pattern) highlight = pattern if args.hex: try: pattern = pattern.decode('hex') highlight = pattern except TypeError as e: log.warn("Search pattern cannot be converted to hexstring: " + str(e)) return False elif args.address: pattern = p32(int(pattern, 16)) highlight = [x for x in pattern if x != '\x00'] memimage = self.getMemoryImage(refresh=args.refresh) matches = [m.start(0) for m in re.finditer(re.escape(pattern), memimage)] hexdumplen = (len(pattern) + 16) & 0xFFFF0 for match in matches: startadr = (match & 0xFFFFFFF0) - args.context endadr = (match+len(pattern)+16 & 0xFFFFFFF0) + args.context log.info("Match at 0x%08x:" % match) log.hexdump(memimage[startadr:endadr], begin=startadr, highlight=highlight) return True class CmdHexdump(Cmd): keywords = ['hexdump', 'hd'] description = "Display a hexdump of a specified region in the memory." parser = argparse.ArgumentParser(prog=keywords[0], description=description, epilog="Aliases: " + ", ".join(keywords)) parser.add_argument("--length", "-l", type=auto_int, default=256, help="Length of the hexdump (default: %(default)s).") parser.add_argument("--aligned", "-a", action="store_true", help="Access the memory strictly 4-byte aligned.") parser.add_argument("address", type=auto_int, help="Start address of the hexdump.") def work(self): args = self.getArgs() if args == None: return True #if not self.isAddressInSections(args.address, args.length): # answer = yesno("Warning: Address 0x%08x (len=0x%x) is not inside a valid section. Continue?" % (args.address, args.length)) # if not answer: # return False dump = None if args.aligned: dump = self.internalblue.readMemAligned(args.address, args.length) else: dump = self.readMem(args.address, args.length) if dump == None: return False log.hexdump(dump, begin=args.address) return True class CmdTelescope(Cmd): keywords = ['telescope', 'tel'] description = "Display a specified region in the memory and follow pointers to valid addresses." parser = argparse.ArgumentParser(prog=keywords[0], description=description, epilog="Aliases: " + ", ".join(keywords)) parser.add_argument("--length", "-l", type=auto_int, default=64, help="Length of the telescope dump (default: %(default)s).") parser.add_argument("address", type=auto_int, help="Start address of the telescope dump.") def telescope(self, data, depth): val = u32(data[0:4]) if val == 0: return [val, ''] if(depth > 0 and self.isAddressInSections(val,0x20)): newdata = self.readMem(val, 0x20) recursive_result = self.telescope(newdata, depth-1) recursive_result.insert(0, val) return recursive_result else: s = '' for c in data: if isprint(c): s += c else: break return [val, s] def work(self): args = self.getArgs() if args == None: return True if not self.isAddressInSections(args.address, args.length): answer = yesno("Warning: Address 0x%08x (len=0x%x) is not inside a valid section. Continue?" % (args.address, args.length)) if not answer: return False dump = self.readMem(args.address, args.length + 4) if dump == None: return False for index in range(0, len(dump)-4, 4): chain = self.telescope(dump[index:], 4) output = "0x%08x: " % (args.address+index) output += ' -> '.join(["0x%08x" % x for x in chain[:-1]]) output += ' \"' + chain[-1] + '"' log.info(output) return True class CmdDisasm(Cmd): keywords = ['disasm', 'disas', 'disassemble', 'd'] description = "Display a disassembly of a specified region in the memory." parser = argparse.ArgumentParser(prog=keywords[0], description=description, epilog="Aliases: " + ", ".join(keywords)) parser.add_argument("--length", "-l", type=auto_int, default=128, help="Length of the disassembly (default: %(default)s).") parser.add_argument("address", type=auto_int, help="Start address of the disassembly.") def work(self): args = self.getArgs() if args == None: return True if not self.isAddressInSections(args.address, args.length): answer = yesno("Warning: Address 0x%08x (len=0x%x) is not inside a valid section. Continue?" % (args.address, args.length)) if not answer: return False dump = self.readMem(args.address, args.length) if dump == None: return False print(disasm(dump, vma=args.address)) return True class CmdWriteMem(Cmd): keywords = ['writemem'] description = "Writes data to a specified memory address." parser = argparse.ArgumentParser(prog=keywords[0], description=description, epilog="Aliases: " + ", ".join(keywords)) parser.add_argument("--hex", action="store_true", help="Interpret data as hex string (e.g. ff000a20...)") parser.add_argument("--int", action="store_true", help="Interpret data as 32 bit integer (e.g. 0x123)") parser.add_argument("--file", "-f", help="Read data from this file instead.") parser.add_argument("--repeat", "-r", default=1, type=auto_int, help="Number of times to repeat the data (default: %(default)s)") parser.add_argument("address", type=auto_int, help="Destination address") parser.add_argument("data", nargs="*", help="Data as string (or hexstring/integer, see --hex, --int)") def work(self): args = self.getArgs() if args == None: return True if args.file != None: data = read(args.file) elif len(args.data) > 0: data = ' '.join(args.data) if args.hex: try: data = data.decode('hex') except TypeError as e: log.warn("Data string cannot be converted to hexstring: " + str(e)) return False elif args.int: data = p32(auto_int(data)) else: self.parser.print_usage() print("Either data or --file is required!") return False data = data * args.repeat if not self.isAddressInSections(args.address, len(data), sectiontype="RAM"): answer = yesno("Warning: Address 0x%08x (len=0x%x) is not inside a RAM section. Continue?" % (args.address, len(args.data))) if not answer: return False self.progress_log = log.progress("Writing Memory") if self.writeMem(args.address, data, self.progress_log, bytes_done=0, bytes_total=len(data)): self.progress_log.success("Written %d bytes to 0x%08x." % (len(data), args.address)) return True else: self.progress_log.failure("Write failed!") return False class CmdWriteAsm(Cmd): keywords = ['writeasm', 'asm'] description = "Writes assembler instructions to a specified memory address." parser = argparse.ArgumentParser(prog=keywords[0], description=description, epilog="Aliases: " + ", ".join(keywords)) parser.add_argument("--dry", "-d", action="store_true", help="Only pass code to the assembler but don't write to memory") parser.add_argument("--file", "-f", help="Open file in text editor, then read assembly from this file.") parser.add_argument("address", type=auto_int, help="Destination address") parser.add_argument("code", nargs="*", help="Assembler code as string") def work(self): args = self.getArgs() if args == None: return True if args.file != None: if(not os.path.exists(args.file)): f = open(args.file, "w") f.write("/* Write arm thumb code here.\n") f.write(" Use '@' or '//' for single line comments or C-like block comments. */\n") f.write("\n// 0x%08x:\n\n" % args.address) f.close() editor = os.environ.get("EDITOR", "vim") subprocess.call([editor, args.file]) code = read(args.file) elif len(args.code) > 0: code = ' '.join(args.code) else: self.parser.print_usage() print("Either code or --file is required!") return False try: data = asm(code, vma=args.address) except PwnlibException: return False if len(data)>0: log.info("Assembler was successful. Machine code (len = %d bytes) is:" % len(data)) log.hexdump(data, begin=args.address) else: log.info("Assembler didn't produce any machine code.") return False if(args.dry): log.info("This was a dry run. No data written to memory!") return True if not self.isAddressInSections(args.address, len(data), sectiontype="RAM"): answer = yesno("Warning: Address 0x%08x (len=0x%x) is not inside a RAM section. Continue?" % (args.address, len(data))) if not answer: return False self.progress_log = log.progress("Writing Memory") if self.writeMem(args.address, data, self.progress_log, bytes_done=0, bytes_total=len(data)): self.progress_log.success("Written %d bytes to 0x%08x." % (len(data), args.address)) return True else: self.progress_log.failure("Write failed!") return False class CmdExec(Cmd): keywords = ['exec', 'execute'] description = "Writes assembler instructions to RAM and jumps there." parser = argparse.ArgumentParser(prog=keywords[0], description=description, epilog="Aliases: " + ", ".join(keywords)) parser.add_argument("--dry", "-d", action="store_true", help="Only pass code to the assembler but don't write to memory and don't execute") parser.add_argument("--edit", "-e", action="store_true", help="Edit command before execution") parser.add_argument("--addr", "-a", type=auto_int, default=0x211800, help="Destination address of the command instructions") parser.add_argument("cmd", help="Name of the command to execute (corresponds to file exec_<cmd>.s)") def work(self): args = self.getArgs() if args == None: return True filename = "exec_%s.s" % args.cmd if not os.path.exists(filename): f = open(filename, "w") f.write("/* Write arm thumb code here.\n") f.write(" Use '@' or '//' for single line comments or C-like block comments. */\n") f.write("\n// Default destination address is 0x%08x:\n\n" % args.addr) f.close() args.edit = True if args.edit: editor = os.environ.get("EDITOR", "vim") subprocess.call([editor, filename]) code = read(filename) try: data = asm(code, vma=args.addr) except PwnlibException: return False if len(data)==0: log.info("Assembler didn't produce any machine code.") return False if args.edit: log.info("Assembler was successful. Machine code (len = %d bytes) is:" % len(data)) log.hexdump(data, begin=args.addr) if(args.dry): log.info("This was a dry run. No data written to memory!") return True if not self.isAddressInSections(args.addr, len(data), sectiontype="RAM"): answer = yesno("Warning: Address 0x%08x (len=0x%x) is not inside a RAM section. Continue?" % (args.addr, len(args.data))) if not answer: return False self.progress_log = log.progress("Writing Memory") if not self.writeMem(args.addr, data, self.progress_log, bytes_done=0, bytes_total=len(data)): self.progress_log.failure("Write failed!") return False self.progress_log.success("Written %d bytes to 0x%08x." % (len(data), args.addr)) self.progress_log = log.progress("Launching Command") if self.launchRam(args.addr): self.progress_log.success("launch_ram cmd was sent successfully!") return True else: self.progress_log.failure("Sending launch_ram command failed!") return False class CmdSendHciCmd(Cmd): keywords = ['sendhcicmd'] description = "Send an arbitrary hci command to the BT controller" parser = argparse.ArgumentParser(prog=keywords[0], description=description, epilog="Aliases: " + ", ".join(keywords)) parser.add_argument("cmdcode", type=auto_int, help="The command code (e.g. 0xfc4c for WriteRam).") parser.add_argument("data", nargs="*", help="Payload as combinations of hexstrings and hex-uint32 (starting with 0x..)") def work(self): args = self.getArgs() if args == None: return True if args.cmdcode > 0xffff: log.info("cmdcode needs to be in the range of 0x0000 - 0xffff") return False data = '' for data_part in args.data: if data_part[0:2] == "0x": data += p32(auto_int(data_part)) else: data += data_part.decode('hex') self.internalblue.sendHciCommand(args.cmdcode, data) return True class CmdPatch(Cmd): keywords = ['patch'] description = "Patches 4 byte of data at a specified ROM address." parser = argparse.ArgumentParser(prog=keywords[0], description=description, epilog="Aliases: " + ", ".join(keywords)) parser.add_argument("--hex", action="store_true", help="Interpret data as hex string (e.g. ff000a20...)") parser.add_argument("--int", action="store_true", help="Interpret data as 32 bit integer (e.g. 0x123)") parser.add_argument("--asm", action="store_true", help="Interpret data as assembler instruction") parser.add_argument("--delete", "-d", action="store_true", help="Delete the specified patch.") parser.add_argument("--slot", "-s", type=auto_int, help="Patchram slot to use (0-128)") parser.add_argument("--address", "-a", type=auto_int, help="Destination address") parser.add_argument("data", nargs="*", help="Data as string (or hexstring/integer/instruction, see --hex, --int, --asm)") def work(self): args = self.getArgs() if args == None: return True if args.slot != None: if args.slot < 0 or args.slot > 128: log.warn("Slot has to be in the range 0 to 128!") return False # Patch Deletion if args.delete: if args.slot != None: log.info("Deleting patch in slot %d..." % args.slot) elif args.address != None: log.info("Deleting patch at address 0x%x..." % args.address) else: log.warn("Address or Slot number required!") return False return self.internalblue.disableRomPatch(args.address, args.slot) if args.address == None: log.warn("Address is required!") return False if len(args.data) > 0: data = ' '.join(args.data) if args.hex: try: data = data.decode('hex') except TypeError as e: log.warn("Data string cannot be converted to hexstring: " + str(e)) return False elif args.int: data = p32(auto_int(data)) elif args.asm: data = asm(data, vma=args.address) else: self.parser.print_usage() print("Data is required!") return False if len(data) > 4: log.warn("Data size is %d bytes. Trunkating to 4 byte!" % len(data)) data = data[0:4] if len(data) < 4: log.warn("Data size is %d bytes. 0-Padding to 4 byte!" % len(data)) data = data.ljust(4, "\x00") if args.address != None and not self.isAddressInSections(args.address, len(data), sectiontype="ROM"): answer = yesno("Warning: Address 0x%08x (len=0x%x) is not inside a ROM section. Continue?" % (args.address, len(data))) if not answer: return False return self.internalblue.patchRom(args.address, data, args.slot) # TODO: add a custom arg to directly send autocompletable crafted packet # TODO: sendlmp -o pause_encryption_req # TODO: sendlmp -o switch_req class CmdSendLmp(Cmd): keywords = ['sendlmp'] description = "Send LMP packet to another device." parser = argparse.ArgumentParser(prog=keywords[0], description=description, epilog="Aliases: " + ", ".join(keywords)) parser.add_argument("--conn_number", "-n", type=auto_int, help="Number of the connection associated with the other device.") parser.add_argument("--nocheck", action="store_true", help="Do not verify that connection number is valid (fast but unsafe)") parser.add_argument("--extended", "-e", action="store_true", help="Use extended opcodes (prepend opcode with 0x7F)") parser.add_argument("opcode", type=auto_int, help="Number of the LMP opcode") parser.add_argument("data", help="Payload as hexstring.") def work(self): args = self.getArgs() if args == None: return True connection_number = args.conn_number remote_addr = None if connection_number == None: connection = None found_multiple_active = False log.info("Reading connection information to find active connection number...") for i in range(self.internalblue.fw.CONNECTION_ARRAY_SIZE): tmp_connection = self.internalblue.readConnectionInformation(i+1) if tmp_connection != None and tmp_connection["remote_address"] != "\x00"*6: log.info("Found active connection with number %d (%s)." % (i+1, bt_addr_to_str(tmp_connection["remote_address"]))) if connection != None: found_multiple_active = True connection = tmp_connection if connection == None: log.warn("No active connection found!") return False if found_multiple_active: log.warn("Multiple active connections detected. Please specify connection number with -n!") return False connection_number = connection["connection_number"] remote_addr = bt_addr_to_str(connection["remote_address"]) else: if args.nocheck: remote_addr = "?" else: connection = self.internalblue.readConnectionInformation(connection_number) if connection == None: log.warn("Connection entry at number %d is empty!" % connection_number) return False else: remote_addr = bt_addr_to_str(connection["remote_address"]) data = None try: data = args.data.decode('hex') except TypeError as e: log.warn("Data string cannot be converted to hexstring: " + str(e)) return False log.info("Sending op=%d data=%s to connection nr=%d (%s)" % (args.opcode, data.encode("hex"), connection_number, remote_addr)) return self.internalblue.sendLmpPacket(connection_number, args.opcode, data, extended_op=args.extended) class CmdInfo(Cmd): keywords = ['info', 'show', 'i'] description = "Display various types of information parsed from live RAM" parser = argparse.ArgumentParser(prog=keywords[0], description=description, epilog="Aliases: " + ", ".join(keywords)) parser.add_argument("type", help="Type of information.") def infoConnections(self): for i in range(self.internalblue.fw.CONNECTION_ARRAY_SIZE): connection = self.internalblue.readConnectionInformation(i+1) if connection == None: continue log.info("### | Connection ---%02d--- ###" % i) log.info(" - Number: %d" % connection["connection_number"]) log.info(" - Remote BT address: %s" % bt_addr_to_str(connection["remote_address"])) log.info(" - Remote BT name: %08X" % connection["remote_name_address"]) log.info(" - Master of Conn.: %s" % str(connection["master_of_connection"])) log.info(" - Conn. Handle: 0x%X" % connection["connection_handle"]) log.info(" - Public RAND: %s" % connection["public_rand"].encode('hex')) #log.info(" - PIN: %s" % connection["pin"].encode('hex')) #log.info(" - BT addr for key: %s" % bt_addr_to_str(connection["bt_addr_for_key"])) log.info(" - Effective Key Len: %d byte (%d bit)" % (connection["effective_key_len"], 8*connection["effective_key_len"])) log.info(" - Link Key: %s" % connection["link_key"].encode('hex')) log.info(" - LMP Features: %s" % connection["extended_lmp_feat"].encode('hex')) log.info(" - Host Supported F: %s" % connection["host_supported_feat"].encode('hex')) log.info(" - TX Power (dBm): %d" % connection["tx_pwr_lvl_dBm"]) log.info(" - Array Index: %s" % connection["id"].encode('hex')) print def infoDevice(self): bt_addr = self.readMem(self.internalblue.fw.BD_ADDR, 6)[::-1] bt_addr_str = ":".join([b.encode("hex") for b in bt_addr]) device_name = self.readMem(self.internalblue.fw.DEVICE_NAME, 258) device_name_len = u8(device_name[0])-1 device_name = device_name[2:2+device_name_len] adb_serial = context.device log.info("### | Device ###") log.info(" - Name: %s" % device_name) log.info(" - ADB Serial: %s" % adb_serial) log.info(" - Address: %s" % bt_addr_str) def infoPatchram(self): table_addresses, table_values, table_slots = self.internalblue.getPatchramState() log.info("### | Patchram Table ###") for i in range(self.internalblue.fw.PATCHRAM_NUMBER_OF_SLOTS): if table_slots[i] == 1: code = disasm(table_values[i],vma=table_addresses[i],byte=False,offset=False) code = code.replace(" ", " ").replace("\n", "; ") log.info("[%03d] 0x%08X: %s (%s)" % (i, table_addresses[i], table_values[i].encode('hex'), code)) def work(self): args = self.getArgs() if args == None: return True subcommands = {} subcommands["connections"] = self.infoConnections subcommands["device"] = self.infoDevice subcommands["patchram"] = self.infoPatchram if args.type in subcommands: subcommands[args.type]() else: log.warn("Unkown type: %s\nKnown types: %s" % (args.type, subcommands.keys())) return False return True