def find_devices(ctx, buses): results = [] for bus in buses: log.note('Probing bus {:d}'.format(bus)) try: cmd = 'i2c dev {:d}'.format(bus) # Raise an exception on error via check=True resp = ctx.send_command(cmd, check=True) # This may fail for buses (or pinmux settings) that are configured # appropriately. Thus, we drop check=True and just look results resp = ctx.send_command('i2c probe') match = re.match(r'Valid chip addresses: ([0-9a-fA-F\t ]+)', resp) if not match: # A failing bus will spew failures for a while. Keep trying # to interrupt it (Ctrl-C) until we know we're back at a prompt. log.warning('No devices or bus failing. Waiting for prompt.') ctx.interrupt(timeout=120) continue for addr in match.group(1).split(): addr = int(addr, 16) log.info('Found device: Bus={:d}, Address=0x{:02x}'.format( bus, addr)) results.append((bus, addr)) except OperationFailed as error: log.error('Command failed: ' + cmd + os.linesep + str(error)) return results
def is_vulnerable(ctx): """ Attempt to check if target is vulnerable to i2c-based unlock bypass, based upon the Sonos-provided (i.e. not the U-Boot) version number. """ ver_regex = re.compile( r'U-Boot \d{4}\.\d{2}-Royale(-Strict)?-Rev(?P<rev>\d{1,}\.\d{1,})\s') for info in ctx.version(): log.debug('Checking version string: ' + info) m = ver_regex.match(info) if m is not None: ver = float(m.group('rev')) if ver == 0.2: log.info('Vulnerable version detected: ' + info) return True if ver <= 0.3: msg = 'Version may be vulnerable, but our memory patches are specific to v0.2' log.error(msg) else: log.error('Patched or unknown version detected: ' + info) return False log.error('Did not detect "U-Boot Royale" version string.') return False
def setup_stratagem(ctx, data, pattern_name, writer): global _STRATAGEM_DATA_LOADED stratagem_input_data_addr = address + STRATAGEM_DATA_OFF # Only need to load the input data once if not _STRATAGEM_DATA_LOADED: log.info('Loading Stratagem input data.') ctx.write_memory(stratagem_input_data_addr, STRATAGEM_DATA) _STRATAGEM_DATA_LOADED = True had_resource = False filename = '{:s}.0x{:08x}.{:d}.stratagem'.format(pattern_name, address, len(data)) try: stratagem = load_resource(filename, Stratagem.from_json_file, 'memory_test', writer.name) had_resource = True except FileNotFoundError: hunter = writer.stratagem_hunter(STRATAGEM_DATA, stratagem_input_data_addr, revlut_maxlen=1024) stratagem = hunter.build_stratagem(data) if not had_resource: save_resource(filename, stratagem.to_json_file, 'memory_test', writer.name) return stratagem
def read_loop(console): state = 'AWAIT_PROMPT' log.info('Waiting for menu. Please power on the target.') while True: line = console.readline() if not line: continue line = line.strip() print(line) if state == 'AWAIT_PROMPT': if 'Input i key to enter' in line: console.write('i') state = 'SELECT_CLI' elif state == 'SELECT_CLI': if 'Please input test item' in line: console.write('Z') console.write('\x03') # Ctrl-C state = 'AWAIT_UBOOT' elif state == 'AWAIT_UBOOT': if 'MT7628 #' in line: state = 'DONE' elif state == 'DONE': log.info('U-Boot prompt detected!') return
def read_nand_to_file(ctx, filename: str, name: str, load_addr: int, nand_addr: int, size: int): # Copy NAND contents to ${loadaddr} cmd = 'nand read 0x{loadaddr:x} 0x{nand_addr:x} 0x{size:x}' cmd = cmd.format(loadaddr=load_addr, nand_addr=nand_addr, size=size) log.info('Copying ' + name + ' to RAM buffer') resp = ctx.send_command(cmd, check=True) log.note('Device response: ' + resp.strip()) log.info('Reading RAM buffer to file: ' + filename) ctx.read_memory_to_file(load_addr, size, filename)
def perform_unlock_bypass(ctx): if is_unlocked(ctx): log.info("Device is already using 'unlocked' command table.") return True if not is_vulnerable(ctx): log.error('Device does not appear to be vulnerable.') return False ctx.patch_memory(_patch_list, impl='i2c') success = is_unlocked(ctx) if success: log.info("Success! Device is now using 'unlocked' command table.") else: log.error("Failed to switch to 'unlocked' command table.") return success
def test_read_mem__uboot(state: dict, script: str): """ Read 512KiB of post-relocated U-Boot code/data to a u-boot.bin file in the resource directory in preparation for test_find_cmd() can be exercised. Populates state['config]', state['uboot_bin_file'] """ _READ_SIZE = 512 * 1024 state['uboot_bin_file'] = os.path.join(state['test_dir'], 'uboot_512K.bin') uboot_addr = state['config']['gd']['bd']['relocaddr']['value'] log.info('Reading 512K of U-Boot code/data to a binary file') args = [ script, '-a', hex(uboot_addr), '-l', str(_READ_SIZE), '-f', state['uboot_bin_file'], ] run(args, check=True)
def execute_all_tests(ctx, address): report = {} try: log.info('Testing memory read implementations') for reader in ctx.memory_readers: default_writer = ctx.default_memory_writer() test_name = '{:s} / {:s}'.format(reader.name, default_writer.name) results = run_tests(ctx, reader, default_writer, address) report[test_name] = results log.info('Testing memory write implementations') for writer in ctx.memory_writers: default_reader = ctx.default_memory_reader() test_name = '{:s} / {:s}'.format(writer.name, default_reader.name) results = run_tests(ctx, default_reader, writer, address) report[test_name] = results except KeyboardInterrupt: log.warning('Memory test suite interrupted. Returning partial results.') return report
def run_tests(ctx, reader, writer, address): report = [] log.info(' Currently testing: {} / {}'.format(reader.name, writer.name)) for (data, pattern_name) in test_cases(): try: if isinstance(writer, StratagemMemoryWriter): stratagem = setup_stratagem(ctx, data, pattern_name, writer) msg = ' Using {:d}-entry stratagem for {:d}-byte payload with {:s}' msg = msg.format(len(stratagem), len(data), pattern_name.replace('_', ' ')) else: msg = ' Using {:d}-byte payload with {:s}' msg = msg.format(len(data), pattern_name.replace('_', ' ')) stratagem = None log.note(msg) result = run_test(ctx, reader, writer, address, data, stratagem) except (StratagemCreationFailed, OperationAlignmentError): result = 'Skipped (Alignment)' entry = {'size': len(data), 'pattern': pattern_name, 'status': result} report.append(entry) return report
def locate_scripts() -> dict: """ Return full paths to each depthcharge script. Dict keys are the script names, with the 'depthcharge-' prefix removed. Values are the full paths to the script. """ script_dir = realpath(os.path.join(_THIS_DIR, '../../scripts')) log.info('Searching for scripts in: ' + script_dir) ret = {} for root, _, files in os.walk(script_dir): for filename in files: script = os.path.join(root, filename) key = basename(script).replace('depthcharge-', '') ret[key] = script for key in _EXPECTED: if key not in ret: raise FileNotFoundError('Failed to locate depthcharge-' + key) return ret
if __name__ == '__main__': if len(sys.argv) < 2 or '-h' in sys.argv or '--help' in sys.argv: print('MT7628 (MediaTek/Ralink) boot menu example') print('Usage: <serial port> [baud rate]') print('Baud rate defaults to 57600.') print() sys.exit(1 if len(sys.argv) < 1 else 0) uart_device = sys.argv[1] try: baud_rate = sys.argv[2] print(baud_rate) except IndexError: baud_rate = '57600' console = Console(uart_device, baudrate=baud_rate) log.note('Opened ' + uart_device + ': ' + baud_rate) try: read_loop(console) except KeyboardInterrupt: print('\r', end='') # Overwrite "^C" on terminal log.warning('Interrupted! Exiting.') sys.exit(1) msg = ('You may now run the following command to inspect the platform.\n' ' depthcharge-inspect -i {:s}:{:s} -c rp-wd008.cfg [-m term]') log.info(msg.format(uart_device, baud_rate))
match = re.match(r'Valid chip addresses: ([0-9a-fA-F\t ]+)', resp) if not match: # A failing bus will spew failures for a while. Keep trying # to interrupt it (Ctrl-C) until we know we're back at a prompt. log.warning('No devices or bus failing. Waiting for prompt.') ctx.interrupt(timeout=120) continue for addr in match.group(1).split(): addr = int(addr, 16) log.info('Found device: Bus={:d}, Address=0x{:02x}'.format( bus, addr)) results.append((bus, addr)) except OperationFailed as error: log.error('Command failed: ' + cmd + os.linesep + str(error)) return results if __name__ == '__main__': # Attach to the device and get a Depthcharge context ctx = setup() log.info('Identifying available I2C buses.') buses = get_buses(ctx) log.info('Probing I2C buses for devices. This may take some time.') # We'll just log results as we go, rather than use the return value find_devices(ctx, buses)
sim_memory[dst_off:dst_off + 4] = state.to_bytes(4, sys.byteorder) # Remaining iterations are performed in-place on the intermediate # result located in the destination memory location for i in range(1, iterations): state = crc32(sim_memory[dst_off:dst_off + 4]) sim_memory[dst_off:dst_off + 4] = state.to_bytes(4, sys.byteorder) if __name__ == '__main__': hunter = ReverseCRC32Hunter(THE_RAVEN, 0x0000_0000, revlut_maxlen=1024) stratagem = hunter.build_stratagem(TARGET_PAYLOAD, max_iterations=16384) filename = 'raven-stratagem.json' stratagem.to_json_file(filename) log.info('Saved stratagem to ' + filename) # Here's a good spot to set a breakpoint and inspect `stratagem` # breakpoint() # Simulate a memory space to write into sim_memory = bytearray(len(TARGET_PAYLOAD)) execute_stratagem(stratagem, sim_memory) if sim_memory == TARGET_PAYLOAD: log.info('Result:\n' + sim_memory.decode('ascii')) else: log.error('Produced result does not match desired target payload!') # breakpoint()
def create_depthcharge_ctx(args, **kwargs): """ Create and return an initialized :py:class:`~depthcharge.Depthcharge` handle based upon command-line arguments. Examples of this function's usage can be found in the Depthcharge scripts. The *args* parameter should contain the results of :py:class:`ArgumentParser.parse_args()`. The following must be included in the **args** Namespace, even if set to their unspecified (e.g. default, ``None``) values. * *monitor* - From :py:meth:`ArgumentParser.add_monitor_argument()` * *iface* - From :py:meth:`ArgumentParser.add_interface_argument()` * *prompt* - From :py:meth:`ArgumentParser.add_prompt_argument()` * *extra* - From :py:meth:`ArgumentParser.add_extra_argument()` * *skip_deploy* - From :py:meth:`ArgumentParser.add_skip_deploy_argument()` The following items are optional: * *config* - From :py:meth:`ArgumentParser.add_config_argument()` * *companion* - From :py:meth:`ArgumentParser.add_companion_argument()` """ monitor = Monitor.create(args.monitor) # Added as a quick way to sneak in a timeout=... value to increase down # intra-command time for demos. # # TODO: Mull this over a bit more, clean it up if needed, and document this. console_kwargs = kwargs.get('console_kwargs', {}) console = Console(args.iface, prompt=args.prompt, monitor=monitor, **console_kwargs) if hasattr(args, 'companion') and args.companion: device, companion_kwargs = args.companion companion = Companion(device, **companion_kwargs) else: companion = None # Join any "extra" arguments destined for depthcharge.Operation **kwargs # to out existing kwargs. if args.extra: kwargs = {**kwargs, **args.extra} if hasattr(args, 'no_reboot'): kwargs['allow_reboot'] = not args.no_reboot if hasattr(args, 'config') and args.config: try: log.info('Loading existing config: ' + args.config) return Depthcharge.load(args.config, console, companion=companion, skip_deploy=args.skip_deploy, **kwargs) except FileNotFoundError: # We'll create it when we call save() pass return Depthcharge(console, companion=companion, skip_deploy=args.skip_deploy, **kwargs)
def create_depthcharge_ctx(args, **kwargs): """ Create and return an initialized :py:class:`~depthcharge.Depthcharge` handle based upon command-line arguments. Examples of this function's usage can be found in the Depthcharge scripts. The *args* parameter should contain the results of :py:class:`ArgumentParser.parse_args()`. The following must be included in the **args** Namespace, even if set to their unspecified (e.g. default, ``None``) values. * *monitor* - From :py:meth:`ArgumentParser.add_monitor_argument()` * *iface* - From :py:meth:`ArgumentParser.add_interface_argument()` * *prompt* - From :py:meth:`ArgumentParser.add_prompt_argument()` * *extra* - From :py:meth:`ArgumentParser.add_extra_argument()` The following items are optional: * *config* - From :py:meth:`ArgumentParser.add_config_argument()` * *companion* - From :py:meth:`ArgumentParser.add_companion_argument()` * *allow_deploy* - From :py:meth:`ArgumentParser.add_allow_deploy_argument()` * *skip_deploy* - From :py:meth:`ArgumentParser.add_skip_deploy_argument()` * *allow_reboot* - From :py:meth:`ArgumentParser.add_allow_reboot_argument()` """ monitor = Monitor.create(args.monitor) # Added as a quick way to sneak in a timeout=... value to increase down # intra-command time for demos. # # TODO: Mull this over a bit more, clean it up if needed, and document this. console_kwargs = kwargs.get('console_kwargs', {}) # Attempt to retrive baudrate from config. This is intended to make life # less annoying when working with a device that uses the non-default # baud rate and forgetting to provide -i <iface>, thereby relying on the # /dev/ttyUSB0 default. if args.iface == _DEFAULT_IFACE and hasattr(args, 'config') and args.config: try: with open(args.config) as infile: config = json.loads(infile.read()) args.iface = _DEFAULT_IFACE_DEVICE + ':' + str( config['baudrate']) except (KeyError, FileNotFoundError): # No worries! Keep calm and carry on hacking. pass console = Console(args.iface, prompt=args.prompt, monitor=monitor, **console_kwargs) if hasattr(args, 'companion') and args.companion: device, companion_kwargs = args.companion companion = Companion(device, **companion_kwargs) else: companion = None # Join any "extra" arguments destined for depthcharge.Operation **kwargs # to out existing kwargs. if args.extra: kwargs = {**kwargs, **args.extra} # Arguments to pass to Depthcharge if non-None or True (for bools) keys = ('arch', 'allow_deploy', 'skip_deploy', 'allow_reboot') for key in keys: if hasattr(args, key) and getattr(args, key): kwargs[key] = getattr(args, key) if hasattr(args, 'config') and args.config: try: log.info('Loading existing config: ' + args.config) return Depthcharge.load(args.config, console, companion=companion, **kwargs) except FileNotFoundError: # We'll create it when we call save() pass return Depthcharge(console, companion=companion, **kwargs)
pickle.dump(state, outfile) if __name__ == '__main__': args = handle_cmdline() try: scripts = locate_scripts() except FileNotFoundError as error: log.error(str(error)) sys.exit(1) tests = load_tests(args) state = load_state(args) t_start = time.time() for test in tests: script = test_name_to_script(test.__name__) log.info('Running ' + test.__name__) test_help(state, script) test(state, script) save_state(state) t_end = time.time() t_elapsed = timedelta(seconds=(t_end - t_start)) print(os.linesep + '=' * 76) print('Tests complete successfully.') print('Elapsed: ' + str(t_elapsed)) print()
if __name__ == '__main__': args = handle_cmdline() try: scripts = locate_scripts() except FileNotFoundError as error: log.error(str(error)) sys.exit(1) tests = load_tests(args) state = load_state(args) t_start = time.time() for test in tests: script = test_name_to_script(test.__name__) log.note('Running ' + test.__name__) test_help(state, script) test(state, script) save_state(state) t_end = time.time() t_elapsed = timedelta(seconds=(t_end - t_start)) msg = ('=' * 76 + os.linesep + ' Tests complete successfully.' + os.linesep + ' Elapsed: ' + str(t_elapsed) + os.linesep) log.info(msg)
# You can remove the console_kwargs parameter if you just want things # to run as fast as possible # console_kwargs = {'timeout': 0.085} ctx = create_depthcharge_ctx(args, console_kwargs=console_kwargs) success = perform_unlock_bypass(ctx) if success: if args.inspect: cfg_name = 'symfonsik_rev0.2_unlock_bypassed.cfg' msg = ('Addtional device inspection will begin in 5 seconds.\n' ' This will induce a crash and then re-bypass the unlock.\n' ' Results will be saved to: ' + cfg_name) log.info(msg) # Give the user a moment to digest this countdown = ctx.create_progress_indicator('countdown', 5, desc='Delaying') for _ in range(0, 5): time.sleep(1) countdown.update(1) ctx.close_progress_indicator(countdown) # Close the serial console we were using ctx.console.close() # Now re-inspect the device in its unlocked state by creating a new context # This will require crashing the device to obtain register r9 from , # So we need to re-perform the unlock bypass after this reset. args.config = None # Zap this so we perform inspection