From e29b338b8b52e8f6a4f8d07a3159f788a4aac341 Mon Sep 17 00:00:00 2001 From: Florian Albrecht Date: Sun, 3 Dec 2023 21:20:15 +0100 Subject: [PATCH] INTFowarder & HWRunner introduction (#125) * WIP: Getting coresight to work * Fixing Coresight protocol and interrupt forwarding TO QEmu * WIP interrupts fixing my own mistakes * Added interrupt state transfer * Reactivated interrupt forwarding, works with timer but the alarm runs out of sync so it only works for a couple of seconds * Fixing crash due to protocol shutdown order interrupt protocol depends on memory * Fixed issues with memory protocols * Fixed IVT setup in interrupt protocol * WIP: Added interrupt recording plugin and protocol, not complete * WIP: Fixed the stub to include an end of buffer flag * Interrupt recording protocol and plugin v1 * Interrupt recording protocol cleanup * Added rudimentary support for ARM Cortex M4 (without FPU registers) * Interrupt recording without manipulating main loop control flow * WIP: USB rehosting * Updated interrupt messages to include the IRQ-Address * IRQ fixing * IRQ fixing; Reordering of setup code and removal of IRQ init because it might not complete before the first IRQ fires * IRQ message passing fixes * Allow plugins to get configuration parameters at load time * Switch to trace approach for interrupt rehosting, to capture quickly firing interrupts * Allow avatar peripherals to know on what target they got called * WIP: Prevent interrupt interleaving * Fixed wrong program counter * WIP: picow-blink rehosting, runs much further but crashes because it runs a re-init of the CYW43 * Allow OpenOCD target to have a gdb binary for symbols * WIP HAL calling * WIP HAL calling step 2 * WIP HAL calling step 2- THIS IS BROKEN AF * Finally a working version * First working HAL implementation for send-uart * Make HALCaller config more expressive * Made HAL calls more generic, allow for up more arguments, allow void function returns, allow pointer/mem bocks for function return * OpenOCD protocol, rework raw memory read/write for higher performance in memory transfers * Trigger HAL Exit after Hardware reset is complete * Make protocol usable as a signaled target continue protocol * WIP: PICOW rehosting * WIP: Software interrupt rehosting support by using the hardware in the loop as the only source of interrupts * WIP: USB rehosting, cleanup of software interrupt stuff * Cleanup interrupt recording * HAL only deal with interrupt protocol if present * HALCaller rename to HWRunner * HWRunner OpenOCD fix; WiFi experiments final * HWRunner naming cleanup * Interrupt Recorder naming cleanup * Interrupt Forwarding naming cleanup * Cleanup and fixup of refactorings * INTFowrader & HWRunner summary * Final cleanup * Remove union types for type hint Union in type hint is introduced with PEP 604 in python 3.10. However, we aim to support python 3.6. * Remove crashing log * Remove crashing log line when using pypanda --------- Co-authored-by: Florian Albrecht Co-authored-by: rawsample --- avatar2/archs/arm.py | 5 +- avatar2/avatar2.py | 66 ++-- avatar2/memory_range.py | 18 +- avatar2/message.py | 48 +++ avatar2/peripherals/avatar_peripheral.py | 17 +- avatar2/peripherals/utility_peripherals.py | 101 +++++ avatar2/plugins/arm/HWRunner.py | 103 +++++ avatar2/plugins/arm/INTForwarder.py | 208 ++++++++++ .../plugins/arm/armv7m_interupt_recorder.py | 78 ++++ avatar2/plugins/arm/hal.py | 88 +++++ avatar2/plugins/assembler.py | 23 +- avatar2/protocols/armv7_HWRunner.py | 198 ++++++++++ avatar2/protocols/armv7_INTForwarder.py | 361 ++++++++++++++++++ .../protocols/armv7_interrupt_recording.py | 250 ++++++++++++ avatar2/protocols/gdb.py | 74 ++-- avatar2/protocols/openocd.py | 137 +++---- avatar2/protocols/qemu_HWRunner.py | 101 +++++ avatar2/protocols/qemu_armv7m_interrupt.py | 261 +++++++++++++ avatar2/protocols/qmp.py | 4 +- avatar2/targets/openocd_target.py | 4 +- avatar2/targets/qemu_target.py | 53 +-- avatar2/targets/target.py | 12 +- avatar2/watchmen.py | 21 +- .../inception/test_inception_hardware_perf.py | 2 +- 24 files changed, 2032 insertions(+), 201 deletions(-) create mode 100644 avatar2/peripherals/utility_peripherals.py create mode 100644 avatar2/plugins/arm/HWRunner.py create mode 100644 avatar2/plugins/arm/INTForwarder.py create mode 100644 avatar2/plugins/arm/armv7m_interupt_recorder.py create mode 100644 avatar2/plugins/arm/hal.py create mode 100644 avatar2/protocols/armv7_HWRunner.py create mode 100644 avatar2/protocols/armv7_INTForwarder.py create mode 100644 avatar2/protocols/armv7_interrupt_recording.py create mode 100644 avatar2/protocols/qemu_HWRunner.py create mode 100644 avatar2/protocols/qemu_armv7m_interrupt.py diff --git a/avatar2/archs/arm.py b/avatar2/archs/arm.py index 60a46d2f29..cb720466c2 100644 --- a/avatar2/archs/arm.py +++ b/avatar2/archs/arm.py @@ -44,8 +44,6 @@ class ARM_CORTEX_M3(ARM): qemu_name = 'arm' gdb_name = 'arm' - capstone_arch = CS_ARCH_ARM - keystone_arch = KS_ARCH_ARM capstone_mode = CS_MODE_LITTLE_ENDIAN | CS_MODE_THUMB | CS_MODE_MCLASS keystone_arch = KS_ARCH_ARM keystone_mode = KS_MODE_LITTLE_ENDIAN | KS_MODE_THUMB @@ -86,7 +84,8 @@ def init(avatar): pass -ARMV7M = ARM_CORTEX_M3 + +ARMV7M = [ARM_CORTEX_M3] class ARMBE(ARM): diff --git a/avatar2/avatar2.py b/avatar2/avatar2.py index 27006a357b..d42cf586cf 100644 --- a/avatar2/avatar2.py +++ b/avatar2/avatar2.py @@ -36,7 +36,7 @@ class Avatar(Thread): """ def __init__( - self, arch=ARM, cpu_model=None, output_directory=None, log_to_stdout=True, configure_logging=True + self, arch=ARM, cpu_model=None, output_directory=None, log_to_stdout=True, configure_logging=True ): super(Avatar, self).__init__() @@ -70,7 +70,7 @@ def __init__( makedirs(self.output_directory) self.log = logging.getLogger("avatar") - + if configure_logging: format = "%(asctime)s | %(name)s.%(levelname)s | %(message)s" @@ -130,8 +130,8 @@ def load_config(self, file_name=None): if not tname in self.targets: raise Exception( ( - "Requested target %s not found in config. " - "Aborting." % tname + "Requested target %s not found in config. " + "Aborting." % tname ) ) mr["forwarded_to"] = self.targets[tname] @@ -183,7 +183,7 @@ def sigint_wrapper(self, signal, frame): self.log.info("Avatar Received SIGINT") self.sigint_handler() - def load_plugin(self, name, local=False): + def load_plugin(self, name, local=False, *args, **kwargs): if local is True: plugin = __import__(name, fromlist=["."]) else: @@ -191,7 +191,7 @@ def load_plugin(self, name, local=False): "avatar2.plugins.%s" % name, fromlist=["avatar2.plugins"] ) - plugin.load_plugin(self) + plugin.load_plugin(self, *args, **kwargs) self.loaded_plugins += [name] @watch("AddTarget") @@ -233,21 +233,21 @@ def init_targets(self): t[1].init() def add_memory_range( - self, - address, - size, - name=None, - permissions="rwx", - file=None, - file_offset=None, - file_bytes=None, - forwarded=False, - forwarded_to=None, - emulate=None, - interval_tree=None, - inline=False, - overwrite=False, - **kwargs + self, + address, + size, + name=None, + permissions="rwx", + file=None, + file_offset=None, + file_bytes=None, + forwarded=False, + forwarded_to=None, + emulate=None, + interval_tree=None, + inline=False, + overwrite=False, + **kwargs ): """ Adds a memory range to avatar @@ -294,7 +294,7 @@ def add_memory_range( **kwargs ) - mr_set = memory_ranges[address : address + size] + mr_set = memory_ranges[address: address + size] if overwrite is True and len(mr_set) > 0: start = min(mr_set, key=lambda x: x.begin).begin end = max(mr_set, key=lambda x: x.end).end @@ -312,7 +312,7 @@ def add_memory_range( interval.data.size, ) - memory_ranges[address : address + size] = m + memory_ranges[address: address + size] = m return m @@ -354,8 +354,8 @@ def transfer_state(self, from_target, to_target, sync_regs=True, synced_ranges=[ """ if ( - from_target.state & TargetStates.STOPPED == 0 - or to_target.state & TargetStates.STOPPED == 0 + from_target.state & TargetStates.STOPPED == 0 + or to_target.state & TargetStates.STOPPED == 0 ): raise Exception( "Targets must be stopped for State Transfer, \ @@ -403,6 +403,7 @@ def _handle_update_state_message(self, message): def _handle_breakpoint_hit_message(self, message): self.log.info("Breakpoint hit for Target: %s" % message.origin.name) self._handle_update_state_message(message) + # Breakpoints are two stages: SYNCING | STOPPED -> HandleBreakpoint -> STOPPED # This makes sure that all handlers are complete before stopping and breaking wait() @@ -446,8 +447,8 @@ def _handle_remote_memory_read_message(self, message): try: kwargs = {"num_words": message.num_words, "raw": message.raw} if ( - hasattr(range.forwarded_to, "read_supports_pc") - and range.forwarded_to.read_supports_pc is True + hasattr(range.forwarded_to, "read_supports_pc") + and range.forwarded_to.read_supports_pc is True ): kwargs["pc"] = message.pc @@ -457,13 +458,13 @@ def _handle_remote_memory_read_message(self, message): if not message.raw and message.num_words == 1 and not isinstance(mem, int): raise Exception( ( - "Forwarded read returned data of type %s " - "(expected: int)" % type(mem) + "Forwarded read returned data of type %s " + "(expected: int)" % type(mem) ) ) success = True except Exception as e: - self.log.exception("RemoteMemoryRead failed: %s" % e) + self.log.exception("RemoteMemoryRead from %s failed: %s" % (range.forwarded_to, e)) mem = -1 success = False @@ -484,8 +485,8 @@ def _handle_remote_memory_write_message(self, message): kwargs = {} if ( - hasattr(mem_range.forwarded_to, "write_supports_pc") - and mem_range.forwarded_to.write_supports_pc is True + hasattr(mem_range.forwarded_to, "write_supports_pc") + and mem_range.forwarded_to.write_supports_pc is True ): kwargs["pc"] = message.pc @@ -515,7 +516,6 @@ def run(self): "Avatar received %s. Queue-Status: %d/%d" % (message, self.queue.qsize(), self.fast_queue.qsize()) ) - handler = self.message_handlers.get(message.__class__, None) if handler is None: raise Exception("No handler for Avatar-message %s registered" % message) diff --git a/avatar2/memory_range.py b/avatar2/memory_range.py index 1fec58e4d6..51152bfb7d 100644 --- a/avatar2/memory_range.py +++ b/avatar2/memory_range.py @@ -33,7 +33,7 @@ def __init__(self, address, size, name=None, permissions='rwx', self.name = ( name if name is not None else - "mem_range_0x{:08x}_0x{:08x}".format(address, address+size)) + "mem_range_0x{:08x}_0x{:08x}".format(address, address + size)) self.permissions = permissions self.file = abspath(file) if file is not None else None self.file_offset = file_offset if file_offset is not None else None @@ -44,23 +44,25 @@ def __init__(self, address, size, name=None, permissions='rwx', self.forwarded_to = forwarded_to self.__dict__.update(kwargs) - def dictify(self): """ Returns the memory range as *printable* dictionary """ - from .avatar2 import Avatar # we cannot do this import at top-level + from .avatar2 import Avatar # we cannot do this import at top-level # Assumption: dicts saved in mrs are of primitive types only expected_types = (str, bool, int, dict, AvatarPeripheral, Avatar, list) - if version_info < (3, 0): expected_types += (unicode, ) + if version_info < (3, 0): expected_types += (unicode,) tmp_dict = dict(self.__dict__) mr_dict = {} while tmp_dict != {}: k, v = tmp_dict.popitem() - if v is None or False: continue - elif k == 'forwarded_to': v = v.name - # TODO handle emulate + if v is None or False: + continue + elif k == 'forwarded_to': + v = v.name + if k == 'emulate_config': # Skip config for emulated peripheral + continue if not isinstance(v, expected_types): raise Exception( "Unsupported type %s for dictifying %s for mem_range at 0x%x" @@ -71,5 +73,3 @@ def dictify(self): v = v.__class__.__name__ mr_dict[k] = v return mr_dict - - diff --git a/avatar2/message.py b/avatar2/message.py index 2c0d5fd629..e6351140b3 100644 --- a/avatar2/message.py +++ b/avatar2/message.py @@ -1,3 +1,4 @@ +from .plugins.arm.hal import HWFunction class AvatarMessage(object): @@ -23,6 +24,7 @@ def __init__(self, origin, breakpoint_number, address): self.breakpoint_number = breakpoint_number self.address = address + class SyscallCatchedMessage(BreakpointHitMessage): def __init__(self, origin, breakpoint_number, address, type='entry'): super(self.__class__, self).__init__(origin, breakpoint_number, address) @@ -40,6 +42,7 @@ def __init__(self, origin, id, pc, address, size, dst=None): self.num_words = 1 self.raw = False + class RemoteMemoryWriteMessage(AvatarMessage): def __init__(self, origin, id, pc, address, value, size, dst=None): super(self.__class__, self).__init__(origin) @@ -50,12 +53,14 @@ def __init__(self, origin, id, pc, address, value, size, dst=None): self.size = size self.dst = dst + class RemoteInterruptEnterMessage(AvatarMessage): def __init__(self, origin, id, interrupt_num): super(self.__class__, self).__init__(origin) self.id = id self.interrupt_num = interrupt_num + class RemoteInterruptExitMessage(AvatarMessage): def __init__(self, origin, id, transition_type, interrupt_num): super(self.__class__, self).__init__(origin) @@ -64,4 +69,47 @@ def __init__(self, origin, id, transition_type, interrupt_num): self.interrupt_num = interrupt_num +class TargetInterruptEnterMessage(AvatarMessage): + def __init__(self, origin, id, interrupt_num, isr_addr): + super(self.__class__, self).__init__(origin) + self.id = id + self.interrupt_num = interrupt_num + self.isr_addr = isr_addr + + +class TargetInterruptExitMessage(AvatarMessage): + def __init__(self, origin, id, interrupt_num, isr_addr): + super(self.__class__, self).__init__(origin) + self.id = id + self.interrupt_num = interrupt_num + self.isr_addr = isr_addr + + +class HWEnterMessage(AvatarMessage): + def __init__(self, origin, function: HWFunction, return_address: int): + super(self.__class__, self).__init__(origin) + self.function = function + self.return_address = return_address + + def __str__(self): + return f"{self.__class__.__name__} from {self.origin.name} returning to 0x{self.return_address:x}" + + def __repr__(self): + return self.__str__() + + +class HWExitMessage(AvatarMessage): + def __init__(self, origin, function: HWFunction, return_val: int, return_address: int): + super(self.__class__, self).__init__(origin) + self.function = function + self.return_val = return_val + self.return_address = return_address + + def __str__(self): + return f"{self.__class__.__name__} from {self.origin.name} to 0x{self.return_address:x} with return_value 0x{self.return_val:x}" + + def __repr__(self): + return self.__str__() + + from .targets.target import TargetStates diff --git a/avatar2/peripherals/avatar_peripheral.py b/avatar2/peripherals/avatar_peripheral.py index 6316008f52..25dde14c74 100644 --- a/avatar2/peripherals/avatar_peripheral.py +++ b/avatar2/peripherals/avatar_peripheral.py @@ -9,7 +9,6 @@ from cached_property import cached_property - class AvatarPeripheral(object): def __init__(self, name, address, size, **kwargs): self.name = name if name else "%s_%x" % (self.__class__.__name__, address) @@ -39,7 +38,7 @@ def shutdown(self): """ pass - def write_memory(self, address, size, value, num_words=1, raw=False, pc=0): + def write_memory(self, address, size, value, num_words=1, raw=False, origin=None, pc=0): if num_words != 1 or raw is True: raise Exception( @@ -48,7 +47,7 @@ def write_memory(self, address, size, value, num_words=1, raw=False, pc=0): ) offset = address - self.address - intervals = self.write_handler[offset : offset + size] + intervals = self.write_handler[offset: offset + size] if intervals == set(): raise Exception( "No write handler for peripheral %s at offset %d \ @@ -62,10 +61,12 @@ def write_memory(self, address, size, value, num_words=1, raw=False, pc=0): % (self.name, offset) ) - kwargs = {} if self.write_supports_pc is False else {"pc": pc} + kwargs = {'origin': origin} + if self.write_supports_pc is True: + kwargs["pc"] = pc return intervals.pop().data(offset, size, value, **kwargs) - def read_memory(self, address, size, num_words=1, raw=False, pc=0): + def read_memory(self, address, size, num_words=1, raw=False, origin=None, pc=0): if num_words != 1 or raw is True: raise Exception( "read_memory for AvatarPeripheral does not support \ @@ -73,7 +74,7 @@ def read_memory(self, address, size, num_words=1, raw=False, pc=0): ) offset = address - self.address - intervals = self.read_handler[offset : offset + size] + intervals = self.read_handler[offset: offset + size] if intervals == set(): raise Exception( @@ -87,5 +88,7 @@ def read_memory(self, address, size, num_words=1, raw=False, pc=0): at offset %d" % (self.name, offset) ) - kwargs = {} if self.write_supports_pc is False else {"pc": pc} + kwargs = {'origin': origin} + if self.write_supports_pc is True: + kwargs["pc"] = pc return intervals.pop().data(offset, size, **kwargs) diff --git a/avatar2/peripherals/utility_peripherals.py b/avatar2/peripherals/utility_peripherals.py new file mode 100644 index 0000000000..588211df30 --- /dev/null +++ b/avatar2/peripherals/utility_peripherals.py @@ -0,0 +1,101 @@ +from avatar2.peripherals import AvatarPeripheral +import logging + + +class InspectionPeripheral(AvatarPeripheral): + """AvatarPeripheral to inspect all accesses to its memory region""" + + def __init__(self, name, address, size): + super().__init__(name, address, size) + self.read_handler[0:size] = self.dispatch_read + self.write_handler[0:size] = self.dispatch_write + self.log = logging.getLogger('emulated') + + def dispatch_read(self, offset, size, *args, **kwargs): + self.log.debug( + f"Memory read at 0x{self.address:x} 0x{offset:x} with size {size} from {kwargs['origin'].__class__.__name__}") + return kwargs['origin'].protocols.memory.read_memory(self.address + offset, size) + + def dispatch_write(self, offset, size, value, *args, **kwargs): + self.log.debug( + f"Memory write at 0x{self.address:x} 0x{offset:x} with size {size} and value 0x{value:x} from {kwargs['origin'].__class__.__name__}") + return kwargs['origin'].protocols.memory.write_memory(self.address + offset, size, value) + + +class PartialForwardingPeripheral(AvatarPeripheral): + """AvatarPeripheral to forward all accesses except some to its memory region""" + read_supports_pc = True + write_supports_pc = True + + def __init__(self, name, address, size, emulate_config): + super().__init__(name, address, size) + self.read_handler[0:size] = self.dispatch_read + self.write_handler[0:size] = self.dispatch_write + self.log = logging.getLogger('emulated') + + self.forward_to = None if 'forward_to' not in emulate_config else emulate_config['forward_to'] + self.ignore_write_offsets = [] if 'ignore_forward_write_offsets' not in emulate_config else emulate_config[ + 'ignore_forward_write_offsets'] + self.ignore_read_offsets = [] if 'ignore_forward_read_offsets' not in emulate_config else emulate_config[ + 'ignore_forward_read_offsets'] + self.ignore = [] if 'ignore' not in emulate_config else emulate_config['ignore'] + + def dispatch_read(self, offset, size, pc, *args, **kwargs): + if offset in self.ignore: + self.log.warning(f"DROPPING read to 0x{self.address + offset:x}") + return 0 + elif offset in self.ignore_read_offsets: + self.log.debug( + f"pc=0x{pc:x} NOT forwarded memory read at 0x{self.address + offset:x} with size {size} of {kwargs['origin'].__class__.__name__}") + return kwargs['origin'].protocols.memory.read_memory(self.address + offset, size) + else: + self.log.debug(f"pc=0x{pc:x} Forwarding read at {hex(self.address + offset)} with size {size}") + return self.forward_to.protocols.memory.read_memory(self.address + offset, size) + + def dispatch_write(self, offset, size, value, pc, *args, **kwargs): + if offset in self.ignore: + self.log.warning(f"DROPPING write to 0x{self.address + offset:x} with {value} (0x{value:x})") + return (True, True) + elif offset in self.ignore_write_offsets: + self.log.debug( + f"pc=0x{pc:x} NOT forwarded memory write at 0x{self.address + offset:x} with size {size} and value {value} (0x{value:x}) of {kwargs['origin'].__class__.__name__}") + return kwargs['origin'].protocols.memory.write_memory(self.address + offset, size, value) + else: + self.log.debug( + f"pc=0x{pc:x} Forwarding write at {hex(self.address + offset)} with value {value} (0x{value:x})") + return self.forward_to.protocols.memory.write_memory(self.address + offset, size, + value), True # True to signal QEmu to not process this register write further + + +class PeripheralTracePeripheral(AvatarPeripheral): + """AvatarPeripheral to forward all accesses except some to its memory region""" + + read_supports_pc = True + write_supports_pc = True + + def __init__(self, name, address, size, emulate_config): + super().__init__(name, address, size) + self.read_handler[0:size] = self.dispatch_read + self.write_handler[0:size] = self.dispatch_write + self.log = logging.getLogger('emulated') + + self.forward_to = None if 'forward_to' not in emulate_config else emulate_config['forward_to'] + self.peripheral_register = {} if 'peripheral_register' not in emulate_config else emulate_config[ + 'peripheral_register'] + self.register_offsets = self.peripheral_register.keys() + self.trace = [] + + def dispatch_read(self, offset, size, *args, **kwargs): + reg = self.peripheral_register[offset] if offset in self.peripheral_register else "UNKNOWN" + value = self.forward_to.protocols.memory.read_memory(self.address + offset, size) + self.log.debug( + f"pc=0x{kwargs['pc']:x} : Register read of {reg} at 0x{offset:04x} with value {value} (0x{value:x})") + self.trace.append(('read', offset, reg, value)) + return value + + def dispatch_write(self, offset, size, value, *args, **kwargs): + reg = self.peripheral_register[offset] if offset in self.peripheral_register else "UNKNOWN" + self.log.debug( + f"pc=0x{kwargs['pc']:x} : Register write of {reg} at 0x{offset:04x} with value {value} (0x{value:x})") + self.trace.append(('write', offset, reg, value)) + return self.forward_to.protocols.memory.write_memory(self.address + offset, size, value) diff --git a/avatar2/plugins/arm/HWRunner.py b/avatar2/plugins/arm/HWRunner.py new file mode 100644 index 0000000000..69ab97a242 --- /dev/null +++ b/avatar2/plugins/arm/HWRunner.py @@ -0,0 +1,103 @@ +import logging +from types import MethodType + +import avatar2 +from avatar2 import QemuTarget +from avatar2.archs import ARMV7M +from avatar2.plugins.arm.hal import RegisterFuncArg +from avatar2.protocols.armv7_HWRunner import ARMv7MHWRunnerProtocol +from avatar2.protocols.qemu_HWRunner import QemuARMv7MHWRunnerProtocol +from avatar2.targets import OpenOCDTarget +from avatar2.watchmen import AFTER + +from avatar2.message import HWExitMessage, HWEnterMessage + +from avatar2.watchmen import watch + + +class HWRunnerPlugin: + + def __init__(self, avatar, config): + self.avatar = avatar + self.hardware_target = None + self.virtual_target = None + self.functions = config['functions'] + self.log = logging.getLogger(f'{avatar.log.name}.plugins.{self.__class__.__name__}') + + @watch('HWEnter') + def func_enter(self, message: HWEnterMessage): + self.log.warning(f"func_enter called with {message}") + for arg in message.function.args: + if isinstance(arg, RegisterFuncArg): + arg.value = self.virtual_target.read_register(arg.register) + if arg.needs_transfer: + self.log.info(f"Transferring argument of size {arg.size} at address 0x{arg.value:x}") + arg_data = self.virtual_target.read_memory(arg.value, size=1, num_words=arg.size) + self.hardware_target.write_memory(arg.value, size=1, value=arg_data, num_words=arg.size, raw=True) + for field in message.function.context_transfers: + self.log.warning(f"Transferring context field of size {field.size} at address 0x{field.value:x}") + field_data = self.virtual_target.read_memory(field.value, size=1, num_words=field.size) + self.hardware_target.write_memory(field.value, size=1, value=field_data, num_words=field.size, raw=True) + + if getattr(self.hardware_target.protocols, 'interrupts', None) is not None: + self.hardware_target.protocols.interrupts.pause() + self.hardware_target.protocols.hal.func_call(message.function, message.return_address) + + @watch('HWExit') + def func_exit(self, message: HWExitMessage): + self.log.warning(f"func_exit called with return val {message.return_val} to 0x{message.return_address:x}") + if message.function.return_args is not None: + for arg in message.function.return_args: + if arg is None or not arg.needs_transfer: # Return value is handled in r0 (if None -> void function) + continue + self.log.info(f"Transferring return-argument of size {arg.size} at address 0x{arg.value:x}") + arg_data = self.hardware_target.read_memory(arg.value, size=1, num_words=arg.size, raw=True) + self.virtual_target.write_memory(arg.value, size=1, value=arg_data, num_words=arg.size) + + self.hardware_target.protocols.hal.continue_after_hal(message) + self.virtual_target.protocols.hal.handle_func_return(message) + if getattr(self.hardware_target.protocols, 'interrupts', None) is not None: + self.hardware_target.protocols.interrupts.resume() + + def enable_func_calling(self): + assert isinstance(self.hardware_target, OpenOCDTarget), "HAL-Caller `hardware_target` must be OpenOCDTarget" + assert isinstance(self.virtual_target, QemuTarget), "HAL-Caller `virtual_target` must be QemuTarget" + + # We need OpenOCD as the memory protocol to perform memory access while the target is running + self.hardware_target.protocols.memory = self.hardware_target.protocols.monitor + + self.hardware_target.protocols.hal.enable() + self.virtual_target.protocols.hal.enable(self.functions) + + self.avatar.message_handlers.update({ + HWEnterMessage: lambda m: None, # Handled in the fast queue, just ignore in the main message queue + HWExitMessage: lambda m: None, # Handled in the fast queue, just ignore in the main message queue + }) + self.avatar.fast_queue_listener.message_handlers.update({ + HWEnterMessage: self.func_enter, + HWExitMessage: self.func_exit, + }) + + +def add_protocols(self: avatar2.Avatar, **kwargs): + target = kwargs['watched_target'] + logging.getLogger("avatar").info(f"Attaching ARMv7 HWRunner protocol to {target}") + if isinstance(target, OpenOCDTarget): + target.protocols.hal = ARMv7MHWRunnerProtocol(target.avatar, target) + self._plugin_hal_caller.hardware_target = target + + elif isinstance(target, QemuTarget): + target.protocols.hal = QemuARMv7MHWRunnerProtocol(target.avatar, target) + self._plugin_hal_caller.virtual_target = target + else: + logging.getLogger("avatar").warning(f"Unsupported target {target}") + + +def load_plugin(avatar: avatar2.Avatar, config): + if avatar.arch not in ARMV7M: + avatar.log.error("Tried to load armv7-m hal-caller plugin " + + "with mismatching architecture") + avatar._plugin_hal_caller = HWRunnerPlugin(avatar, config) + avatar.enable_hal_calling = MethodType(HWRunnerPlugin.enable_func_calling, avatar._plugin_hal_caller) + + avatar.watchmen.add_watchman('TargetInit', when=AFTER, callback=add_protocols) diff --git a/avatar2/plugins/arm/INTForwarder.py b/avatar2/plugins/arm/INTForwarder.py new file mode 100644 index 0000000000..e173874c1a --- /dev/null +++ b/avatar2/plugins/arm/INTForwarder.py @@ -0,0 +1,208 @@ +import logging +import pprint +from time import sleep +from types import MethodType + +from avatar2 import Avatar, TargetStates +from avatar2.archs import ARMV7M +from avatar2.protocols.armv7_INTForwarder import ARMV7INTForwarderProtocol +from avatar2.protocols.coresight import CoreSightProtocol +from avatar2.protocols.qemu_armv7m_interrupt import QEmuARMV7MInterruptProtocol +from avatar2.targets import OpenOCDTarget, QemuTarget +from avatar2.watchmen import AFTER + +from avatar2.message import RemoteInterruptEnterMessage, TargetInterruptEnterMessage, TargetInterruptExitMessage +from avatar2.message import RemoteInterruptExitMessage +from avatar2.message import RemoteMemoryWriteMessage + +from avatar2.watchmen import watch + + +def add_protocols(self: Avatar, **kwargs): + target = kwargs['watched_target'] + logging.getLogger("avatar").info(f"Attaching ARMv7 Interrupts protocol to {target}") + if isinstance(target, OpenOCDTarget): + target.protocols.interrupts = ARMV7INTForwarderProtocol(target.avatar, target) + + # We want to remove the decorators around the read_memory function of + # this target, to allow reading while it is running (thanks oocd) + target.read_memory = MethodType(lambda t, *args, **kwargs: + t.protocols.memory.read_memory( + *args, **kwargs), target + ) + + if isinstance(target, QemuTarget): + target.protocols.interrupts = QEmuARMV7MInterruptProtocol( + target, self.v7m_irq_rx_queue_name, self.v7m_irq_tx_queue_name + ) + if getattr(target.avatar, 'irq_pair', None) is None: + target.avatar.irq_pair = [target, ] + else: + target.avatar.irq_pair.append(target) + assert len(target.avatar.irq_pair) <= 2, "Interrupts only work with two targets" + + +def enable_interrupt_forwarding(self, from_target, to_target=None): + """ + Semi forwarding is a special mode developed for pretender. + It allows that irqs are taken from from_target and external calls to + inject_interrupt. However, no information about to_targets irq-state is + given back to from_target. Nevertheless, memory requests from to_target to + from_target are forwarded. + Confused yet? So are we, this is a huge hack. + """ + self._irq_src = from_target + self._irq_dst = to_target + self._hardware_target = from_target if isinstance(from_target, OpenOCDTarget) else to_target + self._virtual_target = to_target if not isinstance(to_target, OpenOCDTarget) else from_target + + # Also, let's use openocd as protocol for register and memory + if self._hardware_target: + self._hardware_target.protocols.memory = self._hardware_target.protocols.monitor + self._hardware_target.protocols.registers = self._hardware_target.protocols.monitor + + self._hardware_target.protocols.interrupts.enable_interrupts() + isr_addr = self._hardware_target.protocols.interrupts._monitor_stub_isr - 1 + self.log.info("ISR is at %#08x" % isr_addr) + # NOTE: This won't work on many targets, eg cortex m0 can not have HW breakpoints in RAM + # from_target.set_breakpoint(isr_addr, hardware=True) + + if self._virtual_target: + self._virtual_target.protocols.interrupts.enable_interrupts() + + self._remote_interrupt_enter = MethodType(_remote_interrupt_enter, self) + self._remote_interrupt_exit = MethodType(_remote_interrupt_exit, self) + self._remote_memory_write_nvic = MethodType(_remote_memory_write_nvic, self) + + self.message_handlers.update({ + RemoteMemoryWriteMessage: self._remote_memory_write_nvic} + ) + + # OpenOCDProtocol does not emit breakpointhitmessages currently, + # So we listen on state-updates and figure out the rest on our own + self._interrupt_enter_handler = MethodType(forward_interrupt, self) + self.fast_queue_listener.message_handlers.update({ + RemoteInterruptEnterMessage: self._remote_interrupt_enter, + RemoteInterruptExitMessage: self._remote_interrupt_exit, + TargetInterruptEnterMessage: self._interrupt_enter_handler, + }) + + +def transfer_interrupt_state(self, to_target, from_target): + self._hardware_target = from_target if isinstance(from_target, OpenOCDTarget) else to_target + self._virtual_target = to_target if not isinstance(to_target, OpenOCDTarget) else from_target + assert getattr(self, '_hardware_target', None) is not None, "Missing hardware target" + assert getattr(self, '_virtual_target', None) is not None, "Missing virtual target" + + hw_irq_p: ARMV7INTForwarderProtocol = self._hardware_target.protocols.interrupts + vm_irq_p: QEmuARMV7MInterruptProtocol = self._virtual_target.protocols.interrupts + + # Transfer the vector table location + vtor_loc = hw_irq_p.get_vtor() + vm_irq_p.set_vector_table_base(vtor_loc) + # Transfer which interrupts are enabled + enabled_interrupts = hw_irq_p.get_enabled_interrupts() + vm_irq_p.set_enabled_interrupts(enabled_interrupts) + + +@watch("TargetInterruptEnter") +def forward_interrupt(self, message: TargetInterruptEnterMessage): + origin = message.origin + assert origin is self._hardware_target, "Origin is not the hardware target" + self.log.info(f"forwarding interrupt {message.interrupt_num}") + + if self._virtual_target.state != TargetStates.RUNNING: + self.log.critical(f"Interrupt destination not running, pushing irq={message.interrupt_num} onto queue") + self._hardware_target.protocols.interrupts.queue_irq(message.interrupt_num, message.isr_addr) + return False + if self._plugins_armv7m_interrupts_injected_irq is not None: + self.log.critical(f"Interrupt nesting not supported, pushing irq={message.interrupt_num} onto queue") + self._hardware_target.protocols.interrupts.queue_irq(message.interrupt_num, message.isr_addr) + return False + + # self.queue.put(message) + + irq_num = message.interrupt_num + self.log.info("Injecting IRQ 0x%x" % irq_num) + # State update MUST be before signaling the protocol due to async processing + self._plugins_armv7m_interrupts_injected_irq = irq_num + self._plugins_armv7m_interrupts_from_hardware = True + self.log.warning(f"forward_interrupt {self._plugins_armv7m_interrupts_from_hardware})") + destination = self._virtual_target + destination.protocols.interrupts.inject_interrupt(irq_num) + return True + + +@watch('RemoteInterruptEnter') +def _remote_interrupt_enter(self, message): + self.log.warning( + f"_handle_remote_interrupt_enter_message {self._plugins_armv7m_interrupts_from_hardware})") + self._plugins_armv7m_interrupts_injected_irq = message.interrupt_num + + if not self._plugins_armv7m_interrupts_from_hardware: + self._plugins_armv7m_interrupts_from_hardware = True + self._hardware_target.protocols.interrupts.inject_interrupt(message.interrupt_num) + + self._irq_dst.protocols.interrupts.send_interrupt_enter_response(message.id, + True) + + +@watch('RemoteInterruptExit') +def _remote_interrupt_exit(self, message: RemoteInterruptExitMessage): + """ + Handle an interrupt exiting properly + If the interrupt was trigged by the hardware, we need to tell the + interrupt that we satisified it + :param self: + :param message: + :return: + """ + self.log.warning(f"_handle_remote_interrupt_exit_message {message})") + + origin = message.origin + if origin is self._virtual_target and self._plugins_armv7m_interrupts_injected_irq is not None: + if self._plugins_armv7m_interrupts_from_hardware: + self._hardware_target.protocols.interrupts.inject_exc_return() + self._plugins_armv7m_interrupts_from_hardware = False + self._plugins_armv7m_interrupts_injected_irq = None + + # Always ack the exit message + self._irq_dst.protocols.interrupts.send_interrupt_exit_response(message.id, True) + + +def _remote_memory_write_nvic(self, message: RemoteMemoryWriteMessage): + # NVIC address according to coresight manual + if message.address < 0xe000e000 or message.address > 0xe000f000 or self._irq_src is None: + return self._handle_remote_memory_write_message(message) + + # Discard writes to the vector table offset registers + # TODO add other blacklists + if message.address == 0xE000ED08: + success = True + else: + success = self._irq_src.write_memory(message.address, + message.size, + message.value) + drop_further_processing = 0 + if isinstance(success, tuple): + success, drop_further_processing = success + + message.origin.protocols.remote_memory.send_response(message.id, drop_further_processing, success) + + return message.id, message.value, success + + +def load_plugin(avatar, config={}): + if avatar.arch not in ARMV7M: + avatar.log.error("Tried to load armv7-m interrupt plugin " + + "with mismatching architecture") + + avatar.v7m_irq_rx_queue_name = '/avatar_v7m_irq_rx_queue' + avatar.v7m_irq_tx_queue_name = '/avatar_v7m_irq_tx_queue' + avatar.enable_interrupt_forwarding = MethodType(enable_interrupt_forwarding, avatar) + avatar.transfer_interrupt_state = MethodType(transfer_interrupt_state, avatar) + avatar._plugins_armv7m_interrupts_injected_irq = None + avatar._plugins_armv7m_interrupts_from_hardware = False + avatar._plugins_armv7m_interrupts_config = config + + avatar.watchmen.add_watchman('TargetInit', when=AFTER, callback=add_protocols) diff --git a/avatar2/plugins/arm/armv7m_interupt_recorder.py b/avatar2/plugins/arm/armv7m_interupt_recorder.py new file mode 100644 index 0000000000..44be520e81 --- /dev/null +++ b/avatar2/plugins/arm/armv7m_interupt_recorder.py @@ -0,0 +1,78 @@ +import logging +from datetime import datetime +from types import MethodType + +import avatar2 +from avatar2.archs import ARMV7M +from avatar2.protocols.armv7_interrupt_recording import ARMv7MInterruptRecordingProtocol +from avatar2.targets import OpenOCDTarget +from avatar2.watchmen import AFTER + +from avatar2.message import TargetInterruptEnterMessage, TargetInterruptExitMessage + +from avatar2.watchmen import watch + + +class InterruptRecorderPlugin: + + def __init__(self, avatar): + self.avatar = avatar + self.hardware_target = None + self.trace = [] + self.log = logging.getLogger(f'{avatar.log.name}.plugins.{self.__class__.__name__}') + + @watch('TargetInterruptEnter') + def _handle_interrupt_enter(self, message: TargetInterruptEnterMessage): + interrupt_num = message.interrupt_num + self.trace.append( + {'id': message.id, 'event': 'enter', 'interrupt_num': interrupt_num, + 'timestamp': datetime.now().isoformat()}) + + @watch('TargetInterruptExit') + def _handle_interrupt_exit(self, message: TargetInterruptExitMessage): + interrupt_num = message.interrupt_num + self.trace.append( + {'id': message.id, 'event': 'exit', 'interrupt_num': interrupt_num, + 'timestamp': datetime.now().isoformat()}) + + def enable_interrupt_recording(self): + assert self.hardware_target is not None, "Interrupt-Recorder can only be enabled after a hardware target is set" + # Also, let's use openocd as protocol for register and memory + self.hardware_target.protocols.memory = self.hardware_target.protocols.monitor + self.hardware_target.protocols.registers = self.hardware_target.protocols.monitor + + self.hardware_target.protocols.interrupts.enable_interrupt_recording() + + self.avatar.fast_queue_listener.message_handlers.update({ + TargetInterruptEnterMessage: self._handle_interrupt_enter, + TargetInterruptExitMessage: self._handle_interrupt_exit, + }) + + +def add_protocols(self: avatar2.Avatar, **kwargs): + target = kwargs['watched_target'] + if not isinstance(target, OpenOCDTarget): + logging.getLogger('avatar').warning(f"Interrupt-Recorder only works with OpenOCDTarget but got {target}") + return + logging.getLogger("avatar").info(f"Attaching ARMv7 Interrupt-Recorder protocol to {target}") + + target.protocols.interrupts = ARMv7MInterruptRecordingProtocol(target.avatar, target) + self._plugin_interrupt_recorder.hardware_target = target + + # We want to remove the decorators around the read_memory function of + # this target, to allow reading while it is running (thanks OpenOCD) + target.read_memory = MethodType(lambda t, *args, **kwargs: + t.protocols.memory.read_memory( + *args, **kwargs), target + ) + + +def load_plugin(avatar: avatar2.Avatar): + if avatar.arch not in ARMV7M: + avatar.log.error("Tried to load armv7-m interrupt-recorder plugin " + + "with mismatching architecture") + avatar._plugin_interrupt_recorder = InterruptRecorderPlugin(avatar) + avatar.enable_interrupt_recording = MethodType(InterruptRecorderPlugin.enable_interrupt_recording, + avatar._plugin_interrupt_recorder) + + avatar.watchmen.add_watchman('TargetInit', when=AFTER, callback=add_protocols) diff --git a/avatar2/plugins/arm/hal.py b/avatar2/plugins/arm/hal.py new file mode 100644 index 0000000000..8742fafd03 --- /dev/null +++ b/avatar2/plugins/arm/hal.py @@ -0,0 +1,88 @@ +from typing import Union, List + + +class FuncArg: + """ + Represents a function argument that is written to the hardware target. + By default, this is a constant value, for argument transfer at runtime use RegisterFuncArg. + """ + + def __init__(self, value, needs_transfer=False, size=None): + assert value is None or isinstance(value, int), "Address must be an integer or None for read from register" + if needs_transfer: + assert isinstance(size, int), "Size must be specified if needs_transfer is True" + self.value: int = value + self.needs_transfer: bool = needs_transfer + self.size: int = size + + def __str__(self): + if self.needs_transfer: + return f"FuncArg(value=0x{self.value:x}, needs_transfer=True, size={self.size})" + else: + return f"FuncArg(value=0x{self.value:x})" + + def __repr__(self): + return str(self) + + +class RegisterFuncArg(FuncArg): + """Represents a function argument that is passed in a register and dynamically transferred to the hardware target""" + + def __init__(self, register: str, needs_transfer=False, size=None): + super().__init__(None, needs_transfer=needs_transfer, size=size) + self.register = register + + def __str__(self): + if self.needs_transfer: + return f"RegisterFuncArg(register={self.register}, value={self.value}, needs_transfer=True, size={self.size})" + else: + return f"RegisterFuncArg(register={self.register}, value={self.value})" + + +class FuncReturnArg(FuncArg): + def __init__(self, value: int, needs_transfer=True, size=None): + super().__init__(value, needs_transfer=needs_transfer, size=size) + + def __str__(self): + if self.needs_transfer: + return f"FuncReturnArg(value=0x{self.value:x}, needs_transfer=True, size={self.size})" + else: + return f"FuncReturnArg(value=0x{self.value:x})" + + +class ContextTransferArg(FuncArg): + def __init__(self, address: int, size=None): + super().__init__(address, needs_transfer=True, size=size) + + def __str__(self): + if self.needs_transfer: + return f"ContextTransferArg(value=0x{self.value:x}, needs_transfer=True, size={self.size})" + else: + return f"ContextTransferArg(value=0x{self.value:x})" + + +class HWFunction: + """ + Represents a function that is called on the hardware target. + + @param address: The address of the function + @param args: The arguments of the function + @param return_args: The return arguments of the function; if [None] -> void function ; + otherwise it assumes the return value in r0 + """ + + VOID = [None] + + def __init__(self, address: int, args: [FuncArg], context_transfers: [FuncArg] = [], + return_args: Union[List[Union[FuncReturnArg, None]], None] = None): + self.address = address + self.args = args + self.context_transfers = context_transfers + self.return_args = return_args + + def __str__(self): + return (f"HALFunction(address=0x{self.address:x}, args={self.args}, " + + f"context_transfers={self.context_transfers}, returnArgs={self.return_args})") + + def __repr__(self): + return str(self) diff --git a/avatar2/plugins/assembler.py b/avatar2/plugins/assembler.py index 31239f0fe4..e017fa547a 100644 --- a/avatar2/plugins/assembler.py +++ b/avatar2/plugins/assembler.py @@ -1,10 +1,10 @@ +import logging from types import MethodType from keystone import * -def assemble(self, asmstr, addr=None, - arch=None, mode=None): +def assemble(self, asmstr, addr=None, arch=None, mode=None): """ Main purpose of the assembler plugin, it's used to assemble instructions @@ -25,19 +25,34 @@ def assemble(self, asmstr, addr=None, bytes_raw = bytes(bytelist) return bytes_raw -def inject_asm(self, asmstr, addr=None, arch=None, mode=None): + +def inject_asm(self, asmstr, addr=None, arch=None, mode=None, patch=None): """ - Assemble the string, and inject it into the target) + Assemble the string, and inject it into the targets' memory. + + :param asmstr: The assembly string to be assembled + :param addr: Optional address at which the assembly should be injected, defaults to `pc` + :param arch: Optional keystone-architecture to be used, defaults to architecture of target + :param mode: Optional keystone-mode to be used, defaults to mode of target + :param patch: Optional dictionary of address->bytes patches to be replaced in the assembled code + (eg. `patch={0x20001000: b'\xef\xf3\x05\x85'}`) + + :returns: True if the injection was successful, False otherwise """ arch = self._arch.keystone_arch if not arch else arch mode = self._arch.keystone_mode if not mode else mode addr = self.regs.pc if not addr else addr + logging.getLogger('avatar').debug(f"Injecting assembly into address 0x{addr:8x}") md = Ks(arch, mode) bytelist = md.asm(asmstr, addr)[0] bytes_raw = bytes(bytelist) + if patch is not None: + for key in patch.keys(): + bytes_raw = bytes_raw[:key] + patch[key] + bytes_raw[key + len(patch[key]):] return self.write_memory(addr, 1, bytes_raw, len(bytes_raw), raw=True) + def target_added_callback(avatar, *args, **kwargs): target = kwargs['watched_return'] target.assemble = MethodType(assemble, target) diff --git a/avatar2/protocols/armv7_HWRunner.py b/avatar2/protocols/armv7_HWRunner.py new file mode 100644 index 0000000000..a3ff2802d8 --- /dev/null +++ b/avatar2/protocols/armv7_HWRunner.py @@ -0,0 +1,198 @@ +import queue +from threading import Thread, Event +import logging + +from avatar2 import TargetStates +from avatar2.message import BreakpointHitMessage, HWExitMessage +from avatar2.plugins.arm.hal import HWFunction +from avatar2.watchmen import AFTER + +CMD_HAL_CALL = 0 +CMD_CONT = 1 + + +class ARMv7MHWRunnerProtocol(Thread): + + def __init__(self, avatar, origin): + self.avatar = avatar + self._avatar_fast_queue = avatar.fast_queue + self._close = Event() + self._closed = Event() + self.target = origin + self.command_queue = queue.Queue() + + self._stub_base = None + self._stub_func_ptr = None + self._stub_entry = None + self._stub_end = None + + self.current_hal_call = None + self.return_after_hal = None + + self.log = logging.getLogger(f'{avatar.log.name}.protocols.{self.__class__.__name__}') + Thread.__init__(self, daemon=True, name=f"Thread-{self.__class__.__name__}") + self.log.info(f"ARMv7MHWRunnerProtocol initialized") + + def __del__(self): + self.shutdown() + + def shutdown(self): + if self.is_alive() is True: + self.stop() + + def connect(self): + pass + + def enable(self): + try: + self.log.info(f"Enabling ARMv7 HAL catching") + + self.inject_monitor_stub() + # self._end_of_stub_bkpt = self.target.set_breakpoint(self._stub_end) + + self.avatar.watchmen.add_watchman('BreakpointHit', AFTER, self._do_func_return) + + self.log.info(f"Starting ARMv7 HAL catching thread") + self.start() + except: + self.log.exception("Error starting ARMv7MHWRunnerProtocol") + + # TODO what this stub does + MONITOR_STUB = ("" + + # Data + "func_addr: .word 0x00000000\n" + + + # Load and call the actual function + "ldr r4, =func_addr\n" + + "ldr r4, [r4]\n" + + "blx r4\n" + # r0 holds return value now + "bkpt\n" + + "nop\n" + # Return to previous point of execution, leaves r12 modified + ) + + def inject_monitor_stub(self, addr=0x20012000): + """ + Injects a safe monitoring stub. + This has the following effects: + 0. Pivot the VTOR to someplace sane + 1. Insert an infinite loop at addr + 2. Set the PC to addr + 3. set up logic for the injection of interrupt returns. + Write to return_code_register to trigger an IRET + 4. + :return: + """ + self.log.warning(f"Injecting HAL caller stub into {self.target.name} at address 0x{addr:x}.") + + self._stub_base = addr + self.log.info(f"_stub_base = 0x{self._stub_base:08x}") + self._stub_func_ptr = self._stub_base + self.log.info(f"_stub_func_ptr = 0x{self._stub_func_ptr:08x}") + self._stub_entry = self._stub_base + 4 + self.log.info(f"_stub_entry = 0x{self._stub_base:08x}") + self._stub_end = self._stub_entry + 3 * 2 + self.log.info(f"_stub_end = 0x{self._stub_end:08x}") + + # Inject the stub + self.log.info(f"Injecting the stub ...") + self.target.inject_asm(self.MONITOR_STUB, self._stub_base) + + def func_call(self, function: HWFunction, return_address: int): + self.command_queue.put((CMD_HAL_CALL, function, return_address)) + + def _do_func_return(self, avatar, message: BreakpointHitMessage, *args, + **kwargs): # avatar, self, message: BreakpointHitMessage, + self.log.debug(f"_do_hal_return got additional {args}, {kwargs}") + if message.origin != self.target: + return + if message.address != self._stub_end: + return + current_func: HWFunction = self.current_hal_call[0] + return_address = self.current_hal_call[1] + + self.target.regs.r0 = self.restore_regs_r0 + self.target.regs.r1 = self.restore_regs_r1 + self.target.regs.r2 = self.restore_regs_r2 + self.target.regs.r3 = self.restore_regs_r3 + self.target.regs.r4 = self.restore_regs_r4 + self.target.regs.sp = self.restore_regs_sp + self.target.regs.lr = self.restore_regs_lr + self.target.regs.pc = self.return_after_hal + self.return_after_hal = None + + self._dispatch_message(HWExitMessage(self.target, current_func, return_val=self.target.regs.r0, + return_address=return_address)) + self.current_hal_call = None + + def _do_func_call(self, function: HWFunction): + assert self._stub_entry is not None, "Stub not injected yet" + self.log.warning(f"_do_hal_call (func=0x{function.address:x}, args = {function.args})...") + if self.target.state == TargetStates.RUNNING: + self.target.stop() + self.return_after_hal = self.target.regs.pc + self.restore_regs_lr = self.target.regs.lr + self.restore_regs_sp = self.target.regs.sp + self.restore_regs_r0 = self.target.regs.r0 + self.restore_regs_r1 = self.target.regs.r1 + self.restore_regs_r2 = self.target.regs.r2 + self.restore_regs_r3 = self.target.regs.r3 + self.restore_regs_r4 = self.target.regs.r4 + self.target.write_memory(self._stub_func_ptr, size=4, value=function.address | 0x01) + + # TODO stack allocated objects + if len(function.args) >= 1: + self.target.regs.r0 = function.args[0].value + if len(function.args) >= 2: + self.target.regs.r1 = function.args[1].value + if len(function.args) >= 3: + self.target.regs.r2 = function.args[2].value + if len(function.args) >= 4: + self.target.regs.r3 = function.args[3].value + if len(function.args) >= 5: + # Push the stack parameters + for i in range(4, len(function.args)): + self.target.write_memory(self.target.regs.sp - 4 * (i - 4), size=4, value=function.args[i].value) + self.target.regs.sp = self.target.regs.sp - 4 * (len(function.args) - 4) + + self.target.regs.pc = self._stub_entry + self.target.cont() + + def continue_after_hal(self, message: HWExitMessage): + self.command_queue.put((CMD_CONT,)) + + def run(self): + self.log.info("Starting ARMv7MHWRunnerProtocol thread") + + try: + while not (self.avatar._close.is_set() or self._close.is_set()): + try: + command = self.command_queue.get(timeout=1.0) + if command[0] == CMD_HAL_CALL: + function = command[1] + return_address = command[2] + assert self.current_hal_call is None, "Already in HAL call" + + self.current_hal_call = (function, return_address) + self._do_func_call(function) + elif command[0] == CMD_CONT: + self.target.cont() + else: + self.log.error(f"Unknown command {command[0]}") + self.command_queue.task_done() + except queue.Empty: + continue + + except: + self.log.exception("Error processing ARMv7MHWRunnerProtocol thread") + self._closed.set() + self.log.debug("ARMv7MHWRunnerProtocol thread exiting...") + self._closed.set() + + def _dispatch_message(self, message): + self._avatar_fast_queue.put(message) + + def stop(self): + """Stops the listening thread. Useful for teardown of the target""" + self._close.set() + self._closed.wait() diff --git a/avatar2/protocols/armv7_INTForwarder.py b/avatar2/protocols/armv7_INTForwarder.py new file mode 100644 index 0000000000..9f28c22552 --- /dev/null +++ b/avatar2/protocols/armv7_INTForwarder.py @@ -0,0 +1,361 @@ +import queue +import sys +from enum import Enum +from threading import Thread, Event, Condition +import logging +import re +from time import sleep + +from bitstring import BitStream, ReadError + +from avatar2 import watch, OpenOCDTarget +from avatar2.archs.arm import ARM +from avatar2.targets import TargetStates +from avatar2.message import AvatarMessage, UpdateStateMessage, \ + BreakpointHitMessage, RemoteInterruptEnterMessage, TargetInterruptEnterMessage, TargetInterruptExitMessage +from avatar2.protocols.openocd import OpenOCDProtocol + +# ARM System Control Block +SCB_CPUID = 0xe000ed00 # What is it +SCB_STIR = 0xe000ef00 # Send interrupts here +SCB_VTOR = 0xe000ed08 # Vector Table offset register + +# NVIC stuff +NVIC_ISER0 = 0xe000e100 + +# ARMV7InterruptProtocol Constant Addresses +RCC_APB2ENR = 0x40021018 +AFIO_MAPR = 0x40010004 +DBGMCU_CR = 0xe0042004 +COREDEBUG_DEMCR = 0xe000edfc +TPI_ACPR = 0xe0040010 +TPI_SPPR = 0xe00400f0 +TPI_FFCR = 0xe0040304 +DWT_CTRL = 0xe0001000 +ITM_LAR = 0xe0000fb0 +ITM_TCR = 0xe0000e80 +ITM_TER = 0xe0000e00 +ETM_LAR = 0xe0041fb0 +ETM_CR = 0xe0041000 +ETM_TRACEIDR = 0xe0041200 +ETM_TECR1 = 0xe0041024 +ETM_FFRR = 0xe0041028 +ETM_FFLR = 0xe004102c + + +class UniqueQueue(queue.Queue): + def __contains__(self, item): + with self.mutex: + return item in self.queue + + def _init(self, maxsize): + self.queue = list() + + def _put(self, item): + if item not in self.queue: + self.queue.append(item) + + def _get(self): + return self.queue.pop(0) + + +class ARMV7INTForwarderProtocol(Thread): + def __init__(self, avatar, target: OpenOCDTarget): + self.avatar = avatar + self._avatar_fast_queue = avatar.fast_queue + self.target: OpenOCDTarget = target + self._close = Event() + self._closed = Event() + + self._monitor_stub_addr = None + self._monitor_stub_base = None + self._monitor_stub_isr = None + self._monitor_stub_loop = None + self._monitor_stub_ctrl = None + self._original_vtor = None + self.original_vt = None + + self.msg_counter = 0 + self._current_isr_num = 0 + + self._internal_irq_queue = UniqueQueue() + self._paused = Event() + + self.log = logging.getLogger(f'{avatar.log.name}.protocols.{self.__class__.__name__}') + Thread.__init__(self, daemon=True, name=f"Thread-{self.__class__.__name__}") + self.log.info(f"ARMV7InterruptProtocol initialized") + + def __del__(self): + self.shutdown() + + def inject_interrupt(self, interrupt_number, cpu_number=0): + self.log.critical(f"Injecting interrupt {interrupt_number}") + # Set an interrupt using the STIR + self.target.write_memory(SCB_STIR, 4, interrupt_number) + + def enable_interrupt(self, interrupt_number): + """ + Enables an interrupt (e.g., in the NIVC) + :param interrupt_number: + :return: + """ + self.log.critical(f"Enabling interrupt {interrupt_number}") + assert (0 < interrupt_number < 256) + iser_num = interrupt_number // 32 # 32 interrupts per ISER register + iser_addr = NVIC_ISER0 + (iser_num * 4) # Calculate ISER_X address + iser_val = 1 << (interrupt_number % 32) # Set the corresponding bit for the interrupt to 1 + self.target.write_memory(iser_addr, 4, iser_val) + + def get_enabled_interrupts(self, iser_num: int = 0): + enabled_interrupts = self.target.read_memory(NVIC_ISER0 + iser_num * 4, size=4) + return enabled_interrupts + + def set_enabled_interrupts(self, enabled_interrupts_bitfield: int, iser_num: int = 0): + self.target.write_memory(NVIC_ISER0 + iser_num * 4, size=4, value=enabled_interrupts_bitfield) + + def get_vtor(self): + return self.target.read_memory(SCB_VTOR, 4) + + def get_ivt_addr(self): + if getattr(self.target, 'ivt_address', None) is not None: + return self.target.ivt_address + else: + return self.get_vtor() + + def set_vtor(self, addr): + self.log.warning(f"Changing VTOR location to 0x{addr:x}") + res = self.target.write_memory(SCB_VTOR, 4, addr) + if res: + self.target.ivt_address = addr + return res + + def get_isr(self, interrupt_num): + return self.target.read_memory( + self.get_ivt_addr() + (interrupt_num * 4), 4) + + def set_isr(self, interrupt_num, addr): + base = self.get_ivt_addr() + ivt_addr = base + (interrupt_num * 4) + return self.target.write_memory(ivt_addr, 4, addr) + + def shutdown(self): + if self.is_alive() is True: + self.stop() + + def connect(self): + if not isinstance(self.target.protocols.monitor, OpenOCDProtocol): + raise Exception("ARMV7InterruptProtocol requires OpenOCDProtocol to be present.") + + def enable_interrupts(self): + try: + self.log.info(f"Enabling interrupts") + if not isinstance(self.target.protocols.monitor, OpenOCDProtocol): + raise Exception( + "ARMV7InterruptProtocol requires OpenOCDProtocol to be present.") + + self.inject_monitor_stub() + + self.log.info(f"Starting interrupt thread") + self.start() + except: + self.log.exception("Error starting ARMV7InterruptProtocol") + + """ + What this does: + Hang in a loop at `loop` + When an interrupt comes, go to `stub` + At `stub`, load `writeme`, if it's not zero, reset it, and jump to the written value. + This lets us inject exc_return values into the running program + """ + MONITOR_STUB = ("" + + "irq_buffer_ptr: .word 0x00000000\n" + + "writeme: .word 0x00000000\n" + + + "init:\n" + # Load the addresses for later access + "loop: b loop\n" + # Wait for something to happen + "nop\n" + + "stub:\n" + + "push {r4, r5}\n" + + # # "mrs r0, IPSR\n" + # Get the interrupt number + "nop\nnop\n" + # Placeholder to be replaced with `mrs r5, IPSR` due to keystone error + "ldr r1, =irq_buffer_ptr\n" + + "ldr r2, =mtb_0\n" + + "ldr r3, [r1]\n" + # Load the buffer pointer + "mov r4, r3\n" + + "add r4, r4, r2\n" + # Calculate the address of the buffer pointer + + # Ensure end of buffer flag is set + "adds r3, r3, #1\n" + # Increment the buffer pointer + # NOTE: We need to use `movs` otherwise keystone will use `mov.w` which will crash a cortex m0+ + "movs r5, #255\n" + # For anding to implement wrap around + "ands r3, r3, r5\n" + # Wrap around the buffer + "strb r3, [r1]\n" + # Store updated buffer pointer + "strb r5, [r2, r3]\n" + # Set the end of buffer flag + "strb r0, [r4]\n" + # Save the interrupt number + + "intloop:\n" + # Hang in a loop until `writeme` is not 0 + "ldr r3, =writeme\n" + + "ldr r4, [r3]\n" + + "cmp r4, #0\n" + + "beq intloop\n" + + "movs r4, #0\n" + + "str r4, [r3]\n" + # Reset `writeme` + "pop {r4, r5}\n" + + + "bx lr\n" # Return from the interrupt, set by the interrupt calling convention + ) + + def _get_stub(self, mtb_size=256): + mtb_declaration = [f"mtb_{i}: .hword 0x0000" for i in range(mtb_size)] + mtb_declaration[0] = "mtb_0: .hword 0x00ff" + mtb_declaration = "\n".join(mtb_declaration) + return mtb_declaration + self.MONITOR_STUB + + def inject_monitor_stub(self, addr=0x20010000, vtor=0x20011000, num_isr=48): + """ + Injects a safe monitoring stub. + This has the following effects: + 0. Pivot the VTOR to someplace sane + 1. Insert an infinite loop at addr + 2. Set the PC to addr + 3. set up logic for the injection of interrupt returns. + Write to return_code_register to trigger an IRET + 4. + :return: + """ + self.log.warning( + f"Injecting monitor stub into {self.target.name}. (IVT: 0x{self.get_ivt_addr():08x}, 0x{self.get_vtor():08x}, 0x{vtor:08x})") + MTB_SIZE = 256 + + self._monitor_stub_addr = addr + self.log.info(f"_monitor_stub_addr = 0x{self._monitor_stub_addr:08x}") + self._monitor_stub_base = self._monitor_stub_addr + MTB_SIZE * 2 + self.log.info(f"_monitor_stub_base = 0x{self._monitor_stub_base:08x}") + self._monitor_stub_loop = self._monitor_stub_base + 4 * 2 + self.log.info(f"_monitor_stub_loop = 0x{self._monitor_stub_loop:08x}") + self._monitor_stub_isr = self._monitor_stub_loop + 4 + self.log.info(f"_monitor_stub_isr = 0x{self._monitor_stub_isr:08x}") + self._monitor_stub_ctrl = self._monitor_stub_base + 4 + self.log.info(f"_monitor_stub_writeme = 0x{self._monitor_stub_ctrl:08x}") + + # Pivot VTOR, if needed + # On CM0, you can't, so don't. + self._original_vtor = self.get_vtor() + assert self._original_vtor != vtor, "VTOR is already set to the desired value." + + self.set_vtor(vtor) + self.log.info(f"Validate new VTOR address 0x{self.get_vtor():8x}") + + # Sometimes, we need to gain access to the IVT (make it writable). Do that here. + if getattr(self.target, 'ivt_unlock', None) is not None: + unlock_addr, unlock_val = self.target.ivt_unlock + self.target.write_memory(unlock_addr, 4, unlock_val) + + self.log.info(f"Inserting the stub ...") + # Inject the stub + isr_offset = self._monitor_stub_isr - self._monitor_stub_addr + 2 # +2 for push instruction + self.target.inject_asm(self._get_stub(MTB_SIZE), self._monitor_stub_addr, + patch={isr_offset: b'\xef\xf3\x05\x80'}) + + self.log.info(f"Setting up IVT...") + self.original_vt = self.target.read_memory(self._original_vtor, size=4, num_words=num_isr) + # Set the IVT to our stub but DON'T wipe out the 0'th position. + self.target.write_memory(vtor, value=self.target.read_memory(self._original_vtor, size=4), size=4) + for interrupt_num in range(1, num_isr): + self.set_isr(interrupt_num, self._monitor_stub_isr + 1) # +1 for thumb mode + + if self.target.state != TargetStates.STOPPED: + self.log.critical( + "Not setting PC to the monitor stub; Target not stopped") + else: + self.target.write_register('pc', self._monitor_stub_loop) + self.log.warning(f"Updated PC to 0x{self.target.regs.pc:8x}") + + def inject_exc_return(self): + if not self._monitor_stub_base: + self.log.error( + "You need to inject the monitor stub before you can inject exc_returns") + return False + int_num = self._current_isr_num + self._current_isr_num = None + self.log.info(f"Returning from interrupt {int_num}.") + # We can just BX LR for now. + return self.target.write_memory(address=self._monitor_stub_ctrl, size=4, value=1) + + def queue_irq(self, interrupt_num, isr_addr): + self._internal_irq_queue.put((interrupt_num, isr_addr)) + + def pause(self): + self._paused.set() + self.log.warning("IRQ protocol paused") + + def resume(self): + self.log.warning("IRQ protocol resuming...") + self._paused.clear() + + def _dispatch_exception_packet(self, int_num): + self._current_isr_num = int_num + + self.log.debug(f"Dispatching exception for interrupt number {int_num}") + + msg = TargetInterruptEnterMessage(self.target, self.msg_counter, interrupt_num=int_num, + isr_addr=self.original_vt[int_num]) + self.msg_counter += 1 + self._avatar_fast_queue.put(msg) + + def _panic_exec(self): + self.log.critical(f"Received hard-fault exception, stopping target") + if self.target.state == TargetStates.RUNNING: + self.target.stop() + self._close.set() + + def run(self): + TICK_DELAY = 0.0001 + self.log.info("Starting ARMV7InterruptProtocol thread") + + # Wait for init + while self._monitor_stub_addr is None: + sleep(TICK_DELAY) + + mtb_pos = 0 + try: + while not (self.avatar._close.is_set() or self._close.is_set()): + try: + if not self._paused.is_set() and self._internal_irq_queue.qsize() != 0: + irq_num, _ = self._internal_irq_queue.get_nowait() + self.log.info(f"IRQ event from queue {irq_num} q={self._internal_irq_queue}") + self._dispatch_exception_packet(int_num=irq_num) + self._internal_irq_queue.task_done() + if irq_num == 0x3: # Hard-fault + self._panic_exec() + sleep(TICK_DELAY) + continue + except queue.Empty: + pass + + mtb_val = self.target.read_memory(self._monitor_stub_addr + mtb_pos, size=1) + if mtb_val == 0xff: + sleep(TICK_DELAY) + continue + + mtb_pos = (mtb_pos + 1) & 0xff + + self.log.info(f"IRQ event {mtb_val}") + if self._paused.is_set(): + self.queue_irq(mtb_val, self.original_vt[mtb_val]) + self.inject_exc_return() # TODO: This has a lot of side effects + else: + self._dispatch_exception_packet(int_num=mtb_val) + if mtb_val == 0x3: # Hard-fault + self._panic_exec() + except: + self.log.exception("Error processing trace") + self.log.info("Interrupt thread exiting...") + self._closed.set() + + def stop(self): + """Stops the listening thread. Useful for teardown of the target""" + self._close.set() + self._closed.wait() diff --git a/avatar2/protocols/armv7_interrupt_recording.py b/avatar2/protocols/armv7_interrupt_recording.py new file mode 100644 index 0000000000..67f874f037 --- /dev/null +++ b/avatar2/protocols/armv7_interrupt_recording.py @@ -0,0 +1,250 @@ +from enum import Enum +from threading import Thread, Event +import logging +from time import sleep + +from avatar2.targets import TargetStates +from avatar2.message import TargetInterruptEnterMessage, TargetInterruptExitMessage +from avatar2.protocols.openocd import OpenOCDProtocol + +# ARM System Control Block +SCB_VTOR = 0xe000ed08 # Vector Table offset register +# NVIC stuff +NVIC_ISER0 = 0xe000e100 + + +class ARMv7MInterruptRecordingProtocol(Thread): + def __init__(self, avatar, target): + self._original_vtor = None + self.avatar = avatar + self._avatar_fast_queue = avatar.fast_queue + self.target = target + self._close = Event() + self._closed = Event() + self._monitor_stub_base = None + self._monitor_stub_isr = None + self._monitor_stub_vt_buffer = None + self._monitor_stub_trace_buffer = None + self._monitor_stub_mtb = None + self.msg_counter = 0 + self.original_vt = None + self.log = logging.getLogger(f'{avatar.log.name}.protocols.{self.__class__.__name__}') + Thread.__init__(self, daemon=True) + self.log.info(f"ARMV7InterruptRecordingProtocol initialized") + + def __del__(self): + self.shutdown() + + def get_vtor(self): + return self.target.read_memory(SCB_VTOR, 4) + + def get_ivt_addr(self): + if getattr(self.target, 'ivt_address', None) is not None: + return self.target.ivt_address + else: + return self.get_vtor() + + def set_vtor(self, addr): + self.log.warning(f"Changing VTOR location to 0x{addr:x}") + res = self.target.write_memory(SCB_VTOR, 4, addr) + if res: + self.target.ivt_address = addr + return res + + def shutdown(self): + if self.is_alive() is True: + self.stop() + + def connect(self): + if not isinstance(self.target.protocols.monitor, OpenOCDProtocol): + raise Exception("ARMV7InterruptRecordingProtocol requires OpenOCDProtocol to be present.") + + def enable_interrupt_recording(self): + try: + self.log.info(f"Enabling interrupt recording") + if not isinstance(self.target.protocols.monitor, OpenOCDProtocol): + raise Exception( + "ARMV7InterruptRecordingProtocol requires OpenOCDProtocol to be present.") + + self.inject_monitor_stub() + + self.log.info(f"Starting interrupt thread") + self.start() + except: + self.log.exception("Error starting ARMV7InterruptRecordingProtocol") + + # TODO what this stub does + MONITOR_STUB = ("" + + # Data + # vt_buffer_X: .word 0x00000000 # Buffer holding the original vector table + # irq_buffer_X: .hword 0x0000 # MTB ring buffer of interrupt events + "irq_buffer_ptr: .word 0xdeafbeef\n" + + + # "stub: \n" + + "push {r4, r5, r6, r7}\n" + + # "mrs r0, IPSR\n" + # Get the interrupt number + "nop\nnop\n" + # Placeholder to be replaced with `mrs r5, IPSR` due to keystone error + "ldr r1, =irq_buffer_ptr\n" + + "ldr r2, =irq_buffer_0\n" + + "ldr r3, [r1]\n" + # Load the buffer pointer + "mov r4, r3\n" + + "add r4, r4, r2\n" + + + # Ensure end of buffer flag is set + "adds r3, r3, #1\n" + # Increment the buffer pointer + "movs r5, #255\n" + # For anding to implement wrap around + "ands r3, r3, r5\n" + # Wrap around the buffer + "strb r5, [r2, r3]\n" + + + # Save the interrupt number + "strb r0, [r4]\n" + + + # Setup jump to interrupt handler + "ldr r2, =vt_buffer_0\n" + + "mov r6, r0\n" + + "lsls r6, #2\n" + # Calculate interrupt offset + "adds r6, r6, r2\n" + + "ldr r6, [r6]\n" + # Load the interrupt handler address + + # Call the interrupt handler + "mov r7, lr\n" + + "push {r0, r1, r2, r3, r4, r5, r6, r7}\n" + "blx r6\n" + # Jump to interrupt handler + "pop {r0, r1, r2, r3, r4, r5, r6, r7}\n" + "mov lr, r7\n" + + + # Store the interrupt return + "ldr r2, =irq_buffer_0\n" + + "mov r4, r3\n" + + "add r4, r4, r2\n" + + + # Ensure end of buffer flag is set + "adds r3, r3, #1\n" + # Increment the buffer pointer + "movs r5, #255\n" + # For anding to implement wrap around + "ands r3, r3, r5\n" + # Wrap around the buffer + "strb r3, [r1]\n" + # Save the buffer pointer + "strb r5, [r2, r3]\n" + + + "movs r5, #128\n" + # For oring to signal interrupt exit + "orrs r5, r5, r0\n" + # Flip 8th bit + "strb r5, [r4]\n" + # Save the interrupt number with exit flag (highest bit) + + # Restore registers and return + "pop {r4, r5, r6, r7}\n" + + "bx lr\n" # Return from the interrupt, set by the interrupt calling convention + ) + + def _get_stub(self, vt_size=48, irq_buffer_size=256): + vt_declaration = [f"vt_buffer_{i}: .word 0x00000000" for i in range(vt_size)] + vt_declaration = "\n".join(vt_declaration) + buffer_declaration = [f"irq_buffer_{i}: .hword 0x0000" for i in range(irq_buffer_size)] + buffer_declaration = "\n".join(buffer_declaration) + return vt_declaration + buffer_declaration + self.MONITOR_STUB + + def set_isr(self, interrupt_num, addr): + base = self.get_ivt_addr() + ivt_addr = base + (interrupt_num * 4) + return self.target.write_memory(ivt_addr, 4, addr) + + def inject_monitor_stub(self, addr=0x20010000, vtor=0x20011000, num_isr=48): + """ + Injects a safe monitoring stub. + This has the following effects: + 0. Pivot the VTOR to someplace sane + 1. Insert an infinite loop at addr + 2. Set the PC to addr + 3. set up logic for the injection of interrupt returns. + Write to return_code_register to trigger an IRET + 4. + :return: + """ + self.log.warning( + f"Injecting monitor stub into {self.target.name}. (IVT: 0x{self.get_ivt_addr():08x}, 0x{self.get_vtor():08x}, 0x{vtor:08x})") + + self._monitor_stub_base = addr + self.log.info(f"_monitor_stub_base = 0x{self._monitor_stub_base:08x}") + self._monitor_stub_trace_buffer = addr + num_isr * 4 + self.log.info(f"_monitor_stub_trace_buffer = 0x{self._monitor_stub_trace_buffer:08x}") + self._monitor_stub_vt_buffer = addr + self.log.info(f"_monitor_stub_vt_buffer = 0x{self._monitor_stub_vt_buffer:08x}") + self._monitor_stub_mtb = addr + num_isr * 4 + self.log.info(f"_monitor_stub_mtb = 0x{self._monitor_stub_mtb:08x}") + self._monitor_stub_isr = addr + num_isr * 4 + 256 * 2 + 4 + self.log.info(f"_monitor_stub_isr = 0x{self._monitor_stub_isr:08x}") + + # Pivot VTOR, if needed + # On CM0, you can't, so don't. + self._original_vtor = self.get_vtor() + assert self._original_vtor != vtor, "VTOR is already set to the desired value." + + self.set_vtor(vtor) + self.log.info(f"Validate new VTOR address 0x{self.get_vtor():8x}") + + # Sometimes, we need to gain access to the IVT (make it writable). Do that here. + if getattr(self.target, 'ivt_unlock', None) is not None: + unlock_addr, unlock_val = self.target.ivt_unlock + self.target.write_memory(unlock_addr, 4, unlock_val) + + self.log.info(f"Inserting the stub ...") + # Inject the stub + stub_offset = self._monitor_stub_isr - self._monitor_stub_base + 2 + self.target.inject_asm(self._get_stub(), self._monitor_stub_base, patch={stub_offset: b'\xef\xf3\x05\x80'}) + self.target.write_memory(self._monitor_stub_isr - 4, size=4, value=0x00) # set irq_buffer_ptr to 0 + self.target.write_memory(self._monitor_stub_mtb, size=1, value=0xff) # Ensure end of buffer flag + + self.log.info(f"Setting up IVT buffer...") + # Copy the vector table to our buffer + self.original_vt = self.target.read_memory(self._original_vtor, size=4, num_words=num_isr) + self.target.write_memory(self._monitor_stub_vt_buffer, value=self.original_vt, size=4, num_words=num_isr) + + self.log.info(f"Setting up IVT...") + # Set the IVT to our stub but DON'T wipe out the 0'th position. + self.target.write_memory(vtor, value=self.target.read_memory(self._original_vtor, size=4), size=4) + for interrupt_num in range(1, num_isr): + self.set_isr(interrupt_num, self._monitor_stub_isr + 1) # +1 for thumb mode + + def _dispatch_message(self, message): + self._avatar_fast_queue.put(message) + + def run(self): + TICK_DELAY = 0.0001 + self.log.info("Starting ARMV7InterruptRecordingProtocol thread") + + # Wait for init + while self._monitor_stub_base is None: + sleep(TICK_DELAY) + + buffer_pos = 0 + try: + while not (self.avatar._close.is_set() or self._close.is_set()): + curr_isr = self.target.read_memory(address=self._monitor_stub_trace_buffer + buffer_pos, size=1) + if curr_isr == 0xff: + sleep(TICK_DELAY) + continue + + self.msg_counter += 1 + buffer_pos = (buffer_pos + 1) & 0xff + + if curr_isr > 0x80: + curr_isr = curr_isr & 0x7f + addr = self.original_vt[curr_isr] + self._dispatch_message( + TargetInterruptExitMessage(self.target, self.msg_counter, interrupt_num=curr_isr, + isr_addr=addr)) + else: + addr = self.original_vt[curr_isr] + self._dispatch_message( + TargetInterruptEnterMessage(self.target, self.msg_counter, interrupt_num=curr_isr, + isr_addr=addr)) + + + except: + self.log.exception("Error processing trace") + self._closed.set() + self.log.debug("Interrupt thread exiting...") + self._closed.set() + + def stop(self): + """Stops the listening thread. Useful for teardown of the target""" + self._close.set() + self._closed.wait() diff --git a/avatar2/protocols/gdb.py b/avatar2/protocols/gdb.py index d04a372c25..94840c7864 100644 --- a/avatar2/protocols/gdb.py +++ b/avatar2/protocols/gdb.py @@ -9,6 +9,7 @@ import pygdbmi.gdbcontroller import parse + if sys.version_info < (3, 0): import Queue as queue # __class__ = instance.__class__ @@ -33,13 +34,13 @@ class GDBResponseListener(Thread): """ def __init__(self, gdb_protocol, gdb_controller, avatar_queue, - avatar_fast_queue, origin=None): + avatar_fast_queue, origin=None): super(GDBResponseListener, self).__init__() self._protocol = gdb_protocol self._token = -1 self._async_responses = queue.Queue() if avatar_queue is None \ else avatar_queue - self._async_fast_responses = queue.Queue() if avatar_fast_queue is None\ + self._async_fast_responses = queue.Queue() if avatar_fast_queue is None \ else avatar_fast_queue self._sync_responses = {} self._gdb_controller = gdb_controller @@ -128,10 +129,10 @@ def parse_async_notify(self, response): self._origin, TargetStates.STOPPED) elif payload.get('reason') == 'syscall-entry': avatar_msg = SyscallCatchedMessage(self._origin, int(payload['bkptno']), - int(payload['frame']['addr'], 16), 'entry') + int(payload['frame']['addr'], 16), 'entry') elif payload.get('reason') == 'syscall-return': avatar_msg = SyscallCatchedMessage(self._origin, int(payload['bkptno']), - int(payload['frame']['addr'], 16), 'return') + int(payload['frame']['addr'], 16), 'return') elif payload.get('reason') is not None: self.log.critical("Target stopped with unknown reason: %s" % payload['reason']) @@ -233,6 +234,7 @@ def collect_console_output(self, msg): self._console_output += '\n' self._console_output += msg['payload'] + class GDBProtocol(object): """Main class for the gdb communication protocol :ivar gdb_executable: the path to the gdb which should be executed @@ -468,10 +470,9 @@ def update_target_regs(self): """ if hasattr(self._origin, 'regs'): regs = self.get_register_names() - regs_dict = dict([(r,i) for i, r in enumerate(regs) if r != '']) + regs_dict = dict([(r, i) for i, r in enumerate(regs) if r != '']) self._origin.regs._update(regs_dict) - def set_breakpoint(self, line, hardware=False, temporary=False, @@ -583,9 +584,6 @@ def set_syscall_cachpoint(self, syscall): return True return int(expected_bp_num) - - - def remove_breakpoint(self, bkpt): """Deletes a breakpoint""" ret, resp = self._sync_request( @@ -596,13 +594,13 @@ def remove_breakpoint(self, bkpt): resp) return ret - def write_memory(self, address, wordsize, val, num_words=1, raw=False): + def write_memory(self, address, size, value, num_words=1, raw=False): """Writes memory :param address: Address to write to - :param wordsize: the size of the write (1, 2, 4 or 8) - :param val: the written value - :type val: int if num_words == 1 and raw == False + :param size: the size of the write (1, 2, 4 or 8) + :param value: the written value + :type value: int if num_words == 1 and raw == False list if num_words > 1 and raw == False str or byte if raw == True :param num_words: The amount of words to read @@ -614,34 +612,36 @@ def write_memory(self, address, wordsize, val, num_words=1, raw=False): max_write_size = 0x100 if raw: - if not len(val): + if not len(value): raise ValueError("val had zero length") - for i in range(0, len(val), max_write_size): - write_val = encode(val[i:max_write_size + i], 'hex_codec').decode('ascii') + for i in range(0, len(value), max_write_size): + write_val = encode(value[i:max_write_size + i], 'hex_codec').decode('ascii') ret, resp = self._sync_request( ["-data-write-memory-bytes", str(address + i), write_val], GDB_PROT_DONE) else: - fmt = '<%d%s' % (num_words, num2fmt[wordsize]) + fmt = '<%d%s' % (num_words, num2fmt[size]) if num_words == 1: - contents = pack(fmt, val) + contents = pack(fmt, value) else: - contents = pack(fmt, *val) + contents = pack(fmt, *value) hex_contents = encode(contents, 'hex_codec').decode('ascii') ret, resp = self._sync_request( ["-data-write-memory-bytes", str(address), hex_contents], GDB_PROT_DONE) - - self.log.debug("Attempted to write memory. Received response: %s" % resp) + if 'message' in resp and resp['message'] == 'error': + self.log.error("Attempted to write memory. Received error response: %s" % resp) + else: + self.log.debug("Attempted to write memory. Received response [%s]: %s" % (ret, resp)) return ret - def read_memory(self, address, wordsize=4, num_words=1, raw=False): + def read_memory(self, address, size=4, num_words=1, raw=False): """reads memory :param address: Address to read from - :param wordsize: the size of a read word (1, 2, 4 or 8) + :param size: the size of a read word (1, 2, 4 or 8) :param num_words: the amount of read words :param raw: Whether the read memory should be returned unprocessed :return: The read memory @@ -651,9 +651,9 @@ def read_memory(self, address, wordsize=4, num_words=1, raw=False): max_read_size = 0x100 raw_mem = b'' - for i in range(0, wordsize * num_words, max_read_size): - to_read = max_read_size if wordsize * num_words > i + max_read_size - 1 else \ - wordsize * num_words % max_read_size + for i in range(0, size * num_words, max_read_size): + to_read = max_read_size if size * num_words > i + max_read_size - 1 else \ + size * num_words % max_read_size res, resp = self._sync_request(["-data-read-memory-bytes", str(address + i), str(to_read)], GDB_PROT_DONE) @@ -661,7 +661,7 @@ def read_memory(self, address, wordsize=4, num_words=1, raw=False): self.log.debug("Attempted to read memory. Received response: %s" % resp) if not res: - raise Exception("Failed to read memory!") + raise Exception("Failed to read memory! Response: %s" % resp) # the indirection over the bytearray is needed for legacy python support read_mem = bytearray.fromhex(resp['payload']['memory'][0]['contents']) @@ -671,7 +671,7 @@ def read_memory(self, address, wordsize=4, num_words=1, raw=False): return raw_mem else: # Todo: Endianness support - fmt = '<%d%s' % (num_words, num2fmt[wordsize]) + fmt = '<%d%s' % (num_words, num2fmt[size]) mem = list(unpack(fmt, raw_mem)) if num_words == 1: @@ -699,8 +699,8 @@ def _read_special_reg_from_name(self, reg): ret, resp = self._sync_request( ["-data-evaluate-expression", "%s" % - self._arch.special_registers[reg]['gdb_expression']], - GDB_PROT_DONE) + self._arch.special_registers[reg]['gdb_expression']], + GDB_PROT_DONE) fmt = self._arch.special_registers[reg]['format'] res = parse.parse(fmt, resp['payload']['value']) if res is None: @@ -710,9 +710,6 @@ def _read_special_reg_from_name(self, reg): raise Exception("Couldn't parse special register") return list(res) - - - def read_register_from_nr(self, reg_num): """Gets the value of a single register @@ -737,12 +734,12 @@ def write_register(self, reg, value): if reg in self._arch.special_registers: fmt = "{:s}=" \ - + self._arch.special_registers[reg]['format'].replace(' ','') + + self._arch.special_registers[reg]['format'].replace(' ', '') ret, resp = self._sync_request( ["-data-evaluate-expression", fmt.format( - self._arch.special_registers[reg]['gdb_expression'], *value) - ], GDB_PROT_DONE + self._arch.special_registers[reg]['gdb_expression'], *value) + ], GDB_PROT_DONE ) else: ret, resp = self._sync_request( @@ -777,9 +774,8 @@ def cont(self): :returns: True on success""" ret, resp = self._sync_request(["-exec-continue"], GDB_PROT_RUN) - self.log.debug( - "Attempted to continue execution on the target. Received response: %s" % - resp) + self.log.info( + "Attempted to continue execution on the target. Received response: %s, returning %s" % (resp, ret)) return ret def stop(self): diff --git a/avatar2/protocols/openocd.py b/avatar2/protocols/openocd.py index bbe69fa44a..37f17782c1 100644 --- a/avatar2/protocols/openocd.py +++ b/avatar2/protocols/openocd.py @@ -10,6 +10,7 @@ from time import sleep import re from os.path import abspath + if sys.version_info < (3, 0): import Queue as queue else: @@ -24,6 +25,7 @@ class OpenOCDProtocol(Thread): """ This class implements the openocd protocol. + __IMPORTANT:__ This protocol requries the mem_helper.tcl script to be included in the OpenOCD config! :param openocd_script: The openocd scripts to be executed. :type openocd_script: str or list @@ -57,7 +59,7 @@ def __init__(self, avatar, origin, openocd_script, openocd_executable="openocd", else: raise TypeError("Wrong type for OpenOCD configuration files") self.log = logging.getLogger('%s.%s' % (origin.log.name, self.__class__.__name__)) if origin else \ - logging.getLogger(self.__class__.__name__) + logging.getLogger(self.__class__.__name__) self._tcl_port = tcl_port self._gdb_port = gdb_port self._host = host @@ -82,7 +84,7 @@ def __init__(self, avatar, origin, openocd_script, openocd_executable="openocd", in [['-f', abspath(f)] for f in self.openocd_files] for e in l] self._cmd_line += ['--command', 'tcl_port %d' % self._tcl_port, - '--command', 'gdb_port %d' % self._gdb_port] + '--command', 'gdb_port %d' % self._gdb_port] self._cmd_line += additional_args self._openocd = None @@ -91,24 +93,22 @@ def __init__(self, avatar, origin, openocd_script, openocd_executable="openocd", open("%s/openocd_err.txt" % output_directory, "wb") as err: self.log.debug("Starting OpenOCD with command line: %s" % (" ".join(self._cmd_line))) self._openocd = subprocess.Popen(self._cmd_line, - stdout=out, stderr=err)#, shell=True) + stdout=out, stderr=err) # , shell=True) Thread.__init__(self) self.daemon = True - def connect(self): """ Connects to OpenOCDs TCL Server for all subsequent communication returns: True on success, else False """ sleep(1) - + if self._openocd.poll() is not None: raise RuntimeError(("Openocd errored! Please check " "%s/openocd_err.txt for details" % self.output_directory)) - self.log.debug("Connecting to OpenOCD on %s:%s" % (self._host, self._tcl_port)) try: self.telnet = telnetlib.Telnet(self._host, self._tcl_port) @@ -136,8 +136,8 @@ def enable_trace(self): :return: """ self.log.debug("Enabling tracing...") - resp = self.execute_command("ocd_tcl_trace on") - if 'is enabled' in resp: + resp = self.execute_command("tcl_trace all") + if resp == '': self.trace_enabled.set() return True else: @@ -187,7 +187,7 @@ def handle_target_notification(self, str): else: self.log.warning("Weird target state %s" % state) elif mevent: - #TODO handle these + # TODO handle these event = mevent.group(1) self.log.debug("Target event: %s " % event) # TODO handle these @@ -201,7 +201,6 @@ def handle_target_notification(self, str): else: self.log.warning("Unhandled event message %s" % str) - def reset(self): """ Resets the target @@ -221,7 +220,7 @@ def shutdown(self): Shuts down OpenOCD returns: True on success, else False """ - #self.execute_command('ocd_shutdown') + # self.execute_command('ocd_shutdown') self._close.set() if self.telnet: self.telnet.close() @@ -250,7 +249,7 @@ def run(self): while not self.avatar._close.is_set() and not self._close.is_set(): if not self.in_queue.empty(): cmd = self.in_queue.get() - self.log.debug("Executing command %s" % cmd) + self.log.debug("Executing command '%s'" % cmd) self.telnet.write((cmd + END_OF_MSG).encode('ascii')) try: line = self.read_response() @@ -259,7 +258,7 @@ def run(self): self.shutdown() break if line is not None: - #print line + # print line line = line.rstrip(END_OF_MSG) # This is async target notification data. Don't return it normally if line.startswith("type"): @@ -275,18 +274,18 @@ def run(self): # We didn't ask for it. Just debug it self.log.debug(line) else: - self.log.debug("response --> " + line) + self.log.debug("response --> '%s'" % line) self.out_queue.put(line) cmd = None - sleep(.001) # Have a heart. Give other threads a chance + sleep(.001) # Have a heart. Give other threads a chance except Exception as e: self.log.exception("OpenOCD Background thread died with an exception") self.log.debug("OpenOCD Background thread exiting") def read_response(self): self.buf += self.telnet.read_eager().decode('ascii') - #if buf is not '': - #print(self.buf) + # if buf is not '': + # print(self.buf) if END_OF_MSG in self.buf: resp, self.buf = self.buf.split(END_OF_MSG, 1) return resp @@ -294,76 +293,83 @@ def read_response(self): ### The Memory Protocol starts here - def write_memory(self, address, wordsize, val, num_words=1, raw=False): + def write_memory(self, address, size, value, num_words=1, raw=False): """Writes memory :param address: Address to write to - :param wordsize: the size of the write (1, 2, 4 or 8) - :param val: the written value - :type val: int if num_words == 1 and raw == False + :param size: the size of the write (1, 2, 4) + :param value: the written value + :type value: int if num_words == 1 and raw == False list if num_words > 1 and raw == False str or byte if raw == True - :param num_words: The amount of words to read + :param num_words: The amount of words to write :param raw: Specifies whether to write in raw or word mode :returns: True on success else False """ - #print "nucleo.write_memory(%s, %s, %s, %s, %s)" % (repr(address), repr(wordsize), repr(val), repr(num_words), repr(raw)) - if isinstance(val, str) and len(val) != num_words: - self.log.debug("Setting num_words = %d" % (len(val) / wordsize)) - num_words = len(val) / wordsize - for i in range(0, num_words, wordsize): - if raw: - write_val = '0x' + encode(val[i:i+wordsize], 'hex_codec').decode('ascii') - elif isinstance(val, int) or isinstance(val, long): - write_val = hex(val).rstrip("L") - else: - # A list of ints - write_val = hex(val[i]).rstrip("L") - write_addr = hex(address + i).rstrip("L") - if wordsize == 1: - self.execute_command('mwb %s %s' % (write_addr, write_val)) - elif wordsize == 2: - self.execute_command('mwh %s %s' % (write_addr, write_val)) - else: - self.execute_command('mww %s %s' % (write_addr, write_val)) + if raw: + if not (isinstance(value, list) or isinstance(value, bytes) or isinstance(value, bytearray)): + self.log.error("Raw write value must be a list of integers") + return False + write_val = '{' + ' '.join([hex(v).rstrip("L") for v in value]) + '}' + width = size * 8 + self.execute_command("write_memory %s %d %s" % (hex(address).rstrip("L"), width, write_val)) + else: + for i in range(0, num_words): + if isinstance(value, int): + write_val = hex(value).rstrip("L") + else: + # A list of ints + write_val = hex(value[i]).rstrip("L") + write_addr = hex(address + i * size).rstrip("L") + if size == 1: + self.execute_command(f'mwb {write_addr} {write_val}') + elif size == 2: + self.execute_command(f'mwh {write_addr} {write_val}') + else: + self.execute_command(f'mww {write_addr} {write_val}') return True - def read_memory(self, address, wordsize=4, num_words=1, raw=False): + def read_memory(self, address, size=4, num_words=1, raw=False): """reads memory :param address: Address to write to - :param wordsize: the size of a read word (1, 2, 4 or 8) + :param size: the size of a read word (1, 2, 4 or 8) :param num_words: the amount of read words :param raw: Whether the read memory should be returned unprocessed :return: The read memory """ num2fmt = {1: 'B', 2: 'H', 4: 'I', 8: 'Q'} raw_mem = b'' - words = [] - for i in range(0, num_words, wordsize): - read_addr = hex(address + i).rstrip('L') - if wordsize == 1: - resp = self.execute_command('mrb %s' % read_addr) - elif wordsize == 2: - resp = self.execute_command("mrh %s" % read_addr) - else: - resp = self.execute_command('mrw %s' % read_addr) + if raw: + ocd_size = size * 8 + resp = self.execute_command("read_memory %s %d %d" % (hex(address).rstrip("L"), ocd_size, num_words)) if resp: - val = int(resp) - raw_mem += binascii.unhexlify(hex(val)[2:].zfill(wordsize * 2)) + raw_mem = bytearray([int(v, 16) for v in resp.split(' ')]) + return raw_mem else: - self.log.error("Could not read from address %s" % read_addr) + self.log.error("Could not read from address %s" % hex(address)) return None - # OCD flips the endianness - raw_mem = raw_mem[::-1] - if raw: - self.log.debug("Read %s from %#08x" % (repr(raw), address)) - return raw_mem else: + for i in range(0, num_words): + read_addr = hex(address + (i * size)).rstrip('L') + if size == 1: + resp = self.execute_command('mrb %s' % read_addr) + elif size == 2: + resp = self.execute_command("mrh %s" % read_addr) + else: + resp = self.execute_command('mrw %s' % read_addr) + if resp: + val = int(resp, 16) + raw_mem += val.to_bytes(size, byteorder=sys.byteorder) + else: + self.log.error("Could not read from address %s" % read_addr) + return None + # Todo: Endianness support - fmt = '<%d%s' % (num_words, num2fmt[wordsize]) + fmt = '<%d%s' % (num_words, num2fmt[size]) mem = list(unpack(fmt, raw_mem)) + if num_words == 1: return mem[0] else: @@ -374,7 +380,7 @@ def read_memory(self, address, wordsize=4, num_words=1, raw=False): def read_register(self, reg): try: - resp = self.execute_command("ocd_reg %s" % reg) + resp = self.execute_command("reg %s" % reg) val = int(resp.split(":")[1].strip(), 16) return val except: @@ -385,7 +391,7 @@ def write_register(self, reg, value): """Set one register on the target :returns: True on success""" try: - self.execute_command("ocd_reg %s %s" % (reg, hex(value))) + self.execute_command("reg %s %s" % (reg, hex(value))) return True except: self.log.exception(("Error writing register %s" % reg)) @@ -451,7 +457,7 @@ def set_breakpoint(self, line, cmd.append("%#08x" % line) else: cmd.append(str(line)) - cmd.append("2") # TODO: This isn't platform-independent, but i have no idea what it does + cmd.append("2") # TODO: This isn't platform-independent, but i have no idea what it does if hardware: cmd.append("hw") try: @@ -469,7 +475,7 @@ def set_watchpoint(self, variable, write=True, read=False): cmd.append("%#08x" % variable) else: cmd.append(str(variable)) - cmd.append("2") # TODO FIXME + cmd.append("2") # TODO FIXME if read and write: cmd.append("a") elif read: @@ -497,4 +503,3 @@ def remove_breakpoint(self, bkpt): return True except: self.log.exception("Error removing breakpoint") - diff --git a/avatar2/protocols/qemu_HWRunner.py b/avatar2/protocols/qemu_HWRunner.py new file mode 100644 index 0000000000..9ed2c750a4 --- /dev/null +++ b/avatar2/protocols/qemu_HWRunner.py @@ -0,0 +1,101 @@ +import logging +import queue +from threading import Thread, Event + +from avatar2.message import BreakpointHitMessage, HWEnterMessage, HWExitMessage +from avatar2.plugins.arm.hal import HWFunction +from avatar2.watchmen import AFTER + +CMD_CONT = 0 + + +class QemuARMv7MHWRunnerProtocol(Thread): + def __init__(self, avatar, origin): + self.avatar = avatar + self._avatar_fast_queue = avatar.fast_queue + self._close = Event() + self._closed = Event() + self.target = origin + self.functions: [HWFunction] = [] + self.command_queue = queue.Queue() + + self.log = logging.getLogger(f'{avatar.log.name}.protocols.{self.__class__.__name__}') + Thread.__init__(self, daemon=True, name=f"Thread-{self.__class__.__name__}") + self.log.info(f"QemuARMV7HALCallerProtocol initialized") + + def __del__(self): + self.shutdown() + + def shutdown(self): + if self.is_alive() is True: + self.stop() + + def connect(self): + pass + + def enable(self, functions: [HWFunction]): + try: + self.log.info(f"Enabling QEmu HAL catching") + self.functions = functions + for func in self.functions: + self.log.info(f"Setting breakpoint at 0x{func.address:x}") + self.target.set_breakpoint(func.address) + + self.avatar.watchmen.add_watchman('BreakpointHit', AFTER, self._handle_breakpoint) + + self.start() + self.log.info(f"Starting QEmu HAL catching thread") + except: + self.log.exception("Error starting QemuARMV7HALCallerProtocol") + + def _handle_breakpoint(self, avatar, message: BreakpointHitMessage, *args, **kwargs): + if message.origin != self.target: + return + for function in self.functions: + if message.address == function.address: + self.log.info(f"Dispatching HWEnterMessage for function at 0x{function.address:x}") + return_address = self.target.regs.lr + self._dispatch_message(HWEnterMessage(self.target, function, return_address=return_address)) + return + + def handle_func_return(self, message: HWExitMessage): + self.log.info( + f"Continuing QEmu, injecting return value {message.return_val} and continuing at 0x{message.return_address:x}") + if message.function.return_args is None or message.function.return_args[0] is not None: + self.target.regs.r0 = message.return_val + else: + self.log.warning(f"Return value of function is void, skipping return value injection") + self.target.regs.pc = message.return_address + self.continue_target() + + def continue_target(self): + self.command_queue.put((CMD_CONT,)) + + def run(self): + self.log.info("Starting QemuARMV7HALCallerProtocol thread") + + try: + while not (self.avatar._close.is_set() or self._close.is_set()): + try: + command = self.command_queue.get(timeout=1.0) + if command[0] == CMD_CONT: + self.target.cont() + else: + self.log.error(f"Unknown command {command[0]}") + self.command_queue.task_done() + except queue.Empty: + continue + + except: + self.log.exception("Error processing QemuARMV7HALCallerProtocol thread") + self._closed.set() + self.log.debug("QemuARMV7HALCallerProtocol thread exiting...") + self._closed.set() + + def _dispatch_message(self, message): + self._avatar_fast_queue.put(message) + + def stop(self): + """Stops the listening thread. Useful for teardown of the target""" + self._close.set() + self._closed.wait() diff --git a/avatar2/protocols/qemu_armv7m_interrupt.py b/avatar2/protocols/qemu_armv7m_interrupt.py new file mode 100644 index 0000000000..cdf22147a3 --- /dev/null +++ b/avatar2/protocols/qemu_armv7m_interrupt.py @@ -0,0 +1,261 @@ +import logging + +from os import O_WRONLY, O_RDONLY +from threading import Thread, Event, Condition +from ctypes import Structure, c_uint32, c_uint64 +from enum import Enum +from posix_ipc import MessageQueue, ExistentialError + +from avatar2.message import RemoteInterruptEnterMessage +from avatar2.message import RemoteInterruptExitMessage +from avatar2.targets import QemuTarget + +# NVIC stuff +NVIC_ISER0 = 0xe000e100 + + +class RINOperation(Enum): + ENTER = 0 + EXIT = 1 + + +class V7MRemoteInterruptNotification(Structure): + _fields_ = [ + ('id', c_uint64), + ('num_irq', c_uint32), + ('operation', c_uint32), + ('type', c_uint32) + ] + + def __reduce__(self): + # Define the mapping of desired field names to actual field names + field_mapping = {'num-irq': 'num_irq'} + + # Create a dictionary to store the serialized state + state = {} + + # Populate the state dictionary with the desired field names and values + for desired_name, actual_name in field_mapping.items(): + state[desired_name] = getattr(self, actual_name) + + # Return a tuple of callable and arguments for object reconstruction + return self.__class__, (state,) + + @classmethod + def from_state(cls, state): + # Define the reverse mapping of desired field names to actual field names + field_mapping = {'num-irq': 'num_irq'} + + # Create an instance of the class + instance = cls() + + # Retrieve field values from the serialized state using the desired field names + # and assign them to the corresponding actual field names in the class instance + for desired_name, actual_name in field_mapping.items(): + setattr(instance, actual_name, state[desired_name]) + + return instance + + +class V7MInterruptNotificationAck(Structure): + _fields_ = [ + ('id', c_uint64), + ('success', c_uint32), + ('operation', c_uint32), + ] + + +class QEmuARMV7MInterruptProtocol(Thread): + """ + This protocol has two purposes: + a) injecting interrupts into an analysis target + b) extracting interrupt exits and putting them into the avatar queue + (b) is necessary in cases where two targets need to be synched on the + interrupts. + The way a v7m-nvic implements interrupt return is to put a magic value + into $pc, and the hardware does the actual magic of popping from the + interrupt stack and restoring the context. + However, the magic value defines the type of the interrupt return, + and is hence synchronized on interrupt exit, alongside with the + interrupt number + :param target: Reference to the Target utilizing this protocol + :param rx_queue_name: Name of the queue for receiving + :param tx_queue_name: Name of the queue for sending + """ + + def __init__(self, target, rx_queue_name, tx_queue_name): + super(self.__class__, self).__init__() + self._rx_queue_name = rx_queue_name + self._tx_queue_name = tx_queue_name + self._rx_queue = None + self._tx_queue = None + self._avatar_queue = target.avatar.fast_queue + self.target = target + self._close = Event() + self._closed = Event() + self._close.clear() + self._closed.clear() + self.log = logging.getLogger(f'{target.log.name}.protocols.{self.__class__.__name__}') + + def run(self): + while not self._close.is_set(): + + request = None + try: + request = self._rx_queue.receive(0.5) + except: + continue + + req_struct = V7MRemoteInterruptNotification.from_buffer_copy( + request[0]) + + if RINOperation(req_struct.operation) == RINOperation.ENTER: + msg = RemoteInterruptEnterMessage(self.target, req_struct.id, + req_struct.num_irq) + self.log.warning( + "Received an InterruptEnterRequest for irq %d (%x)" % + (req_struct.num_irq, req_struct.type)) + elif RINOperation(req_struct.operation) == RINOperation.EXIT: + msg = RemoteInterruptExitMessage(self.target, req_struct.id, + req_struct.type, + req_struct.num_irq) + self.log.warning( + "Received an InterruptExitRequest for irq %d (%x)" % + (req_struct.num_irq, req_struct.type)) + + else: + msg = None + raise Exception(("Received V7MRemoteInterrupt Notification with" + "unknown operation type %d") % + req_struct.operation) + + self._avatar_queue.put(msg) + + self._closed.set() + + def stop(self): + self._close.set() + self._closed.wait() + + def enable_interrupts(self): + if isinstance(self.target, QemuTarget): + # TODO: Make this more clean, i.e., check for remote memory + rmem_rx_qname = self.target.protocols.remote_memory.rx_queue_name + rmem_tx_qname = self.target.protocols.remote_memory.tx_queue_name + # the tx-queue for qemu is the rx-queue for avatar and vice versa + self.target.protocols.monitor.execute_command( + 'avatar-armv7m-enable-irq', + {'irq-rx-queue-name': self._tx_queue_name, + 'irq-tx-queue-name': self._rx_queue_name, + 'rmem-rx-queue-name': rmem_tx_qname, + 'rmem-tx-queue-name': rmem_rx_qname + } + ) + else: + raise Exception("V7MInterruptProtocol is not implemented for %s" % + self.target.__class__) + + try: + self._rx_queue = MessageQueue(self._rx_queue_name, flags=O_RDONLY, + read=True, write=False) + except Exception as e: + self.log.error("Unable to create rx_queue (name=%s): %s" % (self._rx_queue_name, e)) + return False + + try: + self._tx_queue = MessageQueue(self._tx_queue_name, flags=O_WRONLY, + read=False, write=True) + except Exception as e: + self.log.error("Unable to create tx_queue: %s (name=%s)" % (self._tx_queue_name, e)) + self._rx_queue.close() + return False + + self.daemon = True + self.start() + self.log.info("Enabled Interrupt Forwarding for %s" % self.target) + return True + + def ignore_interrupt_return(self, interrupt_number): + if isinstance(self.target, QemuTarget): + self.log.info( + "Disable handling of irq return for %d" % interrupt_number) + self.target.protocols.monitor.execute_command( + 'avatar-armv7m-ignore-irq-return', + {'num-irq': interrupt_number} + ) + + def unignore_interrupt_return(self, interrupt_number): + if isinstance(self.target, QemuTarget): + self.log.info( + "Re-enable handling of irq return for %d" % interrupt_number) + self.target.protocols.monitor.execute_command( + 'avatar-armv7m-unignore-irq-return', + {'num-irq': interrupt_number} + ) + + def inject_interrupt(self, interrupt_number, cpu_number=0): + if isinstance(self.target, QemuTarget): + self.log.info("Injecting interrupt %d" % interrupt_number) + self.target.protocols.monitor.execute_command( + 'avatar-armv7m-inject-irq', + {'num-irq': interrupt_number, 'num-cpu': cpu_number} + ) + + def set_vector_table_base(self, base, cpu_number=0): + if isinstance(self.target, QemuTarget): + self.log.info("Setting vector table base to 0x%x" % base) + self.target.protocols.monitor.execute_command( + 'avatar-armv7m-set-vector-table-base', + {'base': base, 'num-cpu': cpu_number} + ) + + def send_interrupt_exit_response(self, id, success): + response = V7MInterruptNotificationAck(id, success, + RINOperation.EXIT.value) + + try: + self._tx_queue.send(response) + self.log.info("Send RemoteInterruptExitResponse with id %d" % id) + return True + except Exception as e: + self.log.error("Unable to send response: %s" % e) + return False + + def send_interrupt_enter_response(self, id, success): + response = V7MInterruptNotificationAck(id, success, + RINOperation.ENTER.value) + try: + self._tx_queue.send(response) + self.log.info("Send RemoteInterruptEnterResponse with id %d" % id) + return True + except Exception as e: + self.log.error("Unable to send response: %s" % e) + return False + + def get_enabled_interrupts(self, iser_num: int = 0): + enabled_interrupts = self.target.read_memory(NVIC_ISER0 + iser_num * 4, size=4) + return enabled_interrupts + + def set_enabled_interrupts(self, enabled_interrupts_bitfield: int, iser_num: int = 0): + self.log.warning(f"Setting enabled interrupts to 0x{enabled_interrupts_bitfield:x}") + self.target.write_memory(NVIC_ISER0 + iser_num * 4, size=4, value=enabled_interrupts_bitfield) + + def __del__(self): + self.shutdown() + + def shutdown(self): + self.stop() + if self._rx_queue: + try: + self._rx_queue.unlink() + self._rx_queue.close() + self._rx_queue = None + except ExistentialError: + self.log.warning("Tried to close/unlink non existent rx_queue") + if self._tx_queue: + try: + self._tx_queue.unlink() + self._tx_queue.close() + self._tx_queue = None + except ExistentialError: + self.log.warning("Tried to close/unlink non existent tx_queue") diff --git a/avatar2/protocols/qmp.py b/avatar2/protocols/qmp.py index c3bc7d366a..184f48c8c5 100644 --- a/avatar2/protocols/qmp.py +++ b/avatar2/protocols/qmp.py @@ -39,7 +39,9 @@ def execute_command(self, cmd, args=None): if args: command['arguments'] = args command['id'] = self.id - self._telnet.write(('%s\r\n' % json.dumps(command)).encode('ascii')) + command_str = (json.dumps(command) + "\r\n").encode('ascii') + self.log.info("Sending command: %s" % command_str) + self._telnet.write(command_str) while True: resp = self._telnet.read_until('\r\n'.encode('ascii')) diff --git a/avatar2/targets/openocd_target.py b/avatar2/targets/openocd_target.py index 6bde23369c..119a6c2c8e 100644 --- a/avatar2/targets/openocd_target.py +++ b/avatar2/targets/openocd_target.py @@ -17,6 +17,7 @@ def __init__(self, avatar, executable=None, openocd_script=None, additional_args=None, tcl_port=6666, gdb_executable=None, gdb_additional_args=None, gdb_port=3333, + binary=None, **kwargs ): @@ -33,6 +34,7 @@ def __init__(self, avatar, executable=None, self.tcl_port = tcl_port self.gdb_additional_args = gdb_additional_args if gdb_additional_args else [] self.gdb_port = gdb_port + self.binary = binary @watch("TargetInit") def init(self): @@ -49,7 +51,7 @@ def init(self): gdb = GDBProtocol(gdb_executable=self.gdb_executable, arch=self._arch, additional_args=self.gdb_additional_args, - avatar=self.avatar, origin=self) + avatar=self.avatar, origin=self, binary=self.binary) self.log.debug("Connecting to OpenOCD GDB port") gdb_connected = gdb.remote_connect(port=self.gdb_port) script_has_reset = False diff --git a/avatar2/targets/qemu_target.py b/avatar2/targets/qemu_target.py index 6257d44ec6..71a012f416 100644 --- a/avatar2/targets/qemu_target.py +++ b/avatar2/targets/qemu_target.py @@ -4,6 +4,7 @@ from avatar2.protocols.gdb import GDBProtocol from avatar2.protocols.qmp import QMPProtocol + try: from avatar2.protocols.remote_memory import RemoteMemoryProtocol except ImportError: @@ -18,23 +19,23 @@ class QemuTarget(Target): """""" def __init__( - self, - avatar, - executable=None, - cpu_model=None, - firmware=None, - gdb_executable=None, - gdb_port=3333, - gdb_unix_socket_path=None, - additional_args=None, - gdb_additional_args=None, - gdb_verbose=False, - qmp_port=3334, - entry_address=0x00, - log_items=None, - log_file=None, - system_clock_scale=None, - **kwargs + self, + avatar, + executable=None, + cpu_model=None, + firmware=None, + gdb_executable=None, + gdb_port=3333, + gdb_unix_socket_path=None, + additional_args=None, + gdb_additional_args=None, + gdb_verbose=False, + qmp_port=3334, + entry_address=0x00, + log_items=None, + log_file=None, + system_clock_scale=None, + **kwargs ): super(QemuTarget, self).__init__(avatar, **kwargs) @@ -101,14 +102,14 @@ def assemble_cmd_line(self): qmp = ["-qmp", "tcp:127.0.0.1:%d,server,nowait" % self.qmp_port] cmd_line = ( - executable_name - + machine - + kernel - + gdb_option - + stop_on_startup - + self.additional_args - + nographic - + qmp + executable_name + + machine + + kernel + + gdb_option + + stop_on_startup + + self.additional_args + + nographic + + qmp ) if self.log_items is not None: @@ -264,7 +265,7 @@ def init(self, cmd_line=None): ) with open( - "%s/%s_out.txt" % (self.avatar.output_directory, self.name), "wb" + "%s/%s_out.txt" % (self.avatar.output_directory, self.name), "wb" ) as out, open( "%s/%s_err.txt" % (self.avatar.output_directory, self.name), "wb" ) as err: diff --git a/avatar2/targets/target.py b/avatar2/targets/target.py index bc1f77d940..d152930df3 100644 --- a/avatar2/targets/target.py +++ b/avatar2/targets/target.py @@ -156,8 +156,8 @@ def set_all(self, instance, only_defaults=False): def shutdown(self): """Shutsdown all the associated protocols""" - for p in self.protocols: - #print("Unloading %s" % str(p)) + l = list(self.protocols) + for p in reversed(l): setattr(self, p, None) def __setattr__(self, name, value): @@ -356,7 +356,7 @@ def write_memory(self, address, size, value, num_words=1, raw=False): if target_range is not None and target_range.forwarded is True and \ target_range.forwarded_to != self: return target_range.forwarded_to.write_memory(address, size, value, - num_words, raw) + num_words, raw, origin=self) return self.protocols.memory.write_memory(address, size, value, num_words, raw) @@ -381,12 +381,12 @@ def read_memory(self, address, size, num_words=1, raw=False): if target_range is not None and target_range.forwarded is True and \ target_range.forwarded_to != self: return target_range.forwarded_to.read_memory(address, size, - num_words, raw) + num_words, raw, origin=self) return self.protocols.memory.read_memory(address, size, num_words, raw) @watch('TargetRegisterWrite') - #@action_valid_decorator_factory(TargetStates.STOPPED, 'registers') + @action_valid_decorator_factory(TargetStates.STOPPED, 'registers') def write_register(self, register, value): """ Writing a register to the target @@ -397,7 +397,7 @@ def write_register(self, register, value): return self.protocols.registers.write_register(register, value) @watch('TargetRegisterRead') - #@action_valid_decorator_factory(TargetStates.STOPPED, 'registers') + @action_valid_decorator_factory(TargetStates.STOPPED, 'registers') def read_register(self, register): """ Reading a register from the target diff --git a/avatar2/watchmen.py b/avatar2/watchmen.py index 7898d19d2c..be2db5719f 100644 --- a/avatar2/watchmen.py +++ b/avatar2/watchmen.py @@ -1,3 +1,4 @@ +import logging from threading import Thread from functools import wraps @@ -30,7 +31,11 @@ class WatchedTypes(object): 'TargetWait', 'TargetSetFile', 'TargetDownload', - 'TargetInjectInterrupt' + 'TargetInjectInterrupt', + 'TargetInterruptEnter', + 'TargetInterruptExit', + 'HWEnter', + 'HWExit' ] def __init__(self): @@ -73,6 +78,11 @@ def watchtrigger(self, *args, **kwargs): elif isinstance(self, Target): avatar = self.avatar cb_kwargs['watched_target'] = self + elif getattr(self, 'avatar', None) is not None: + avatar = self.avatar + cb_kwargs['watched_target'] = self + else: + logging.getLogger('avatar').warning("Watchmen decorator called on unsupported object %s" % self) avatar.watchmen.t(watched_type, BEFORE, *args, **cb_kwargs) ret = func(self, *args, **kwargs) @@ -101,7 +111,7 @@ def run(self): class WatchedEvent(object): # noinspection PyUnusedLocal def __init__(self, watch_type, when, callback, is_async, - overwrite_return = False, *args, **kwargs): + overwrite_return=False, *args, **kwargs): self._callback = callback self.type = watch_type self.when = when @@ -127,7 +137,6 @@ def react(self, avatar, *args, **kwargs): ret = self._callback(avatar, *args, **kwargs) if self.overwrite_return: return ret - class Watchmen(object): @@ -147,7 +156,8 @@ def add_watch_types(self, watched_types): if self.watched_types._add(type): self._watched_events[type] = [] - def add_watchman(self, watch_type, when=BEFORE, callback=None, is_async=False, overwrite_return=False, *args, **kwargs): + def add_watchman(self, watch_type, when=BEFORE, callback=None, is_async=False, overwrite_return=False, *args, + **kwargs): if watch_type not in self.watched_types: raise Exception("Requested event_type does not exist") @@ -164,7 +174,8 @@ def add_watchman(self, watch_type, when=BEFORE, callback=None, is_async=False, o if x.overwrite_return] if len(overwriting_cbs) > 1: - self._avatar.log.warning("More than one watchman can modify the return value for the event. Watch type: %s" % watch_type) + self._avatar.log.warning( + "More than one watchman can modify the return value for the event. Watch type: %s" % watch_type) self._watched_events[watch_type].append(w) return w diff --git a/tests/inception/test_inception_hardware_perf.py b/tests/inception/test_inception_hardware_perf.py index 8934d13678..f2ca7246c0 100644 --- a/tests/inception/test_inception_hardware_perf.py +++ b/tests/inception/test_inception_hardware_perf.py @@ -202,7 +202,7 @@ def transfer_state(av, target_from, target_to, nb_test, summary=True): # Number each test is repeated n = 2 - avatar = Avatar(arch=ARMV7M, output_directory='/tmp/inception-tests') + avatar = Avatar(arch=ARM_CORTEX_M3, output_directory='/tmp/inception-tests') nucleo = avatar.add_target(InceptionTarget, name='nucleo') dum = avatar.add_target(DummyTarget, name='dum') #qemu = avatar.add_target(QemuTarget, gdb_port=1236)