diff --git a/README.md b/README.md index 1afe4dc..c340270 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ Special thanks to [QIRA](https://github.com/geohot/qira) / [geohot](https://twit ## Releases +* v0.2 -- Imagebase detection, cell visualization, breakpoint refactor, bugfixes. * v0.1 -- Initial release # Installation @@ -60,7 +61,7 @@ By *clicking and dragging across the timeline*, it is possible to zoom in on a s ## Execution Breakpoints -Clicking the instruction pointer in the registers window will highlight it in red, revealing all the locations the instruction was executed across the trace timeline. +Double clicking the instruction pointer in the registers window will highlight it in red, revealing all the locations the instruction was executed across the trace timeline.

Placing a breakpoint on the current instruction @@ -78,13 +79,13 @@ IDA's native `F2` hotkey can also be used to set breakpoints on arbitrary instru ## Memory Breakpoints -By clicking a byte in either the stack or memory views, you will instantly see all reads/writes to that address visualized across the trace timeline. Yellow indicates a memory *read*, blue indicates a memory *write*. +By double clicking a byte in either the stack or memory views, you will instantly see all reads/writes to that address visualized across the trace timeline. Yellow indicates a memory *read*, blue indicates a memory *write*.

Exploring memory accesses using memory breakpoints

-Memory breakpoints can be navigated using the same technique described for execution breakpoints. Click a byte, and *scroll while hovering the selected **byte*** to seek the trace to each of its accesses. +Memory breakpoints can be navigated using the same technique described for execution breakpoints. Double click a byte, and *scroll while hovering the selected **byte*** to seek the trace to each of its accesses. *Right clicking a byte* of interest will give you options to seek between memory read / write / access if there is a specific navigation action that you have in mind. @@ -96,7 +97,7 @@ To navigate the memory view to an arbitrary address, click onto the memory view ## Region Breakpoints -A rather experimental feature is setting access breakpoints for a region of memory. This is possible by highlighting a block of memory, and selecting the *Find accesses* action from the right click menu. +It is possible to set a memory breakpoint across a region of memory by highlighting a block of memory, and double clicking it to set an access breakpoint.

Memory region access breakpoints @@ -124,7 +125,7 @@ A simple 'shell' is provided to navigate to specific timestamps in the trace. Pa Seeking around the trace using the timestamp shell

-Using an exclamation point, you can also seek a specified 'percentage' into the trace. Entering `!100` will seek to the final instruction in the trace, where `!50` will seek approximately 50% of the way through the trace. +Using an exclamation point, you can also seek a specified 'percentage' into the trace. Entering `!100` will seek to the final instruction in the trace, where `!50` will seek approximately 50% of the way through the trace. `!last` will seek to the last navigable instruction that can be viewed in the disassembler. ## Themes diff --git a/plugins/tenet/breakpoints.py b/plugins/tenet/breakpoints.py index 7cd4270..37ed00b 100644 --- a/plugins/tenet/breakpoints.py +++ b/plugins/tenet/breakpoints.py @@ -1,3 +1,5 @@ +import itertools + from tenet.ui import * from tenet.types import BreakpointType, BreakpointEvent, TraceBreakpoint from tenet.util.misc import register_callback, notify_callback @@ -10,12 +12,17 @@ # # The purpose of this file is to house the 'headless' components of the # breakpoints window and its underlying functionality. This is split into -# a model and controller component, of a typical 'MVC' design pattern. +# a model and controller component, of a typical 'MVC' design pattern. # # v0.1 NOTE/TODO: err, a dedicated bp window was planned but did not quite # make the cut for the initial release of this plugin. For that reason, # some of this logic may be half-baked pending further work. # +# v0.2 NOTE/TODO: Currently, the breakpoint controller/Tenet artificially +# limits usage to one execution breakpoint and one memory breakpoint at +# a time. I'll probably raise this 'limit' when a proper gui is made +# for managing and differentiating between breakpoints... +# class BreakpointController(object): """ @@ -35,26 +42,27 @@ def __init__(self, pctx): self.dockable = None # events - self._ignore_events = False + self._ignore_signals = False self.pctx.core.ui_breakpoint_changed(self._ui_breakpoint_changed) def reset(self): """ Reset the breakpoint controller. """ - self.focus_breakpoint(None) self.model.reset() - def add_breakpoint(self, address, access_type): + def add_breakpoint(self, address, access_type, length=1): """ Add a breakpoint of the given access type. """ if access_type == BreakpointType.EXEC: - self.add_execution_breakpoint(address) + self.add_execution_breakpoint(address, length) elif access_type == BreakpointType.READ: - self.add_read_breakpoint(address) + self.add_read_breakpoint(address, length) elif access_type == BreakpointType.WRITE: - self.add_write_breakpoint(address) + self.add_write_breakpoint(address, length) + elif access_type == BreakpointType.ACCESS: + self.add_access_breakpoint(address, length) else: raise ValueError("UNKNOWN ACCESS TYPE", access_type) @@ -63,58 +71,80 @@ def add_execution_breakpoint(self, address): Add an execution breakpoint for the given address. """ self.model.bp_exec[address] = TraceBreakpoint(address, BreakpointType.EXEC) - - def add_read_breakpoint(self, address): + self.model._notify_breakpoints_changed() + + def add_read_breakpoint(self, address, length=1): """ Add a memory read breakpoint for the given address. """ - self.model.bp_read[address] = TraceBreakpoint(address, BreakpointType.READ) + self.model.bp_read[address] = TraceBreakpoint(address, BreakpointType.READ, length) + self.model._notify_breakpoints_changed() - def add_write_breakpoint(self, address): + def add_write_breakpoint(self, address, length=1): """ Add a memory write breakpoint for the given address. """ - self.model.bp_write[address] = TraceBreakpoint(address, BreakpointType.WRITE) + self.model.bp_write[address] = TraceBreakpoint(address, BreakpointType.WRITE, length) + self.model._notify_breakpoints_changed() - def focus_breakpoint(self, address, access_type=BreakpointType.NONE, length=1): + def add_access_breakpoint(self, address, length=1): """ - Set and focus on a given breakpoint. + Add a memory access breakpoint for the given address. """ - dctx = disassembler[self.pctx] - - if self.model.focused_breakpoint and address != self.model.focused_breakpoint.address: - self._ignore_events = True - dctx.delete_breakpoint(self.model.focused_breakpoint.address) - self._ignore_events = False + self.model.bp_access[address] = TraceBreakpoint(address, BreakpointType.ACCESS, length) + self.model._notify_breakpoints_changed() - if address is None: - self.model.focused_breakpoint = None - return None + def clear_breakpoints(self): + """ + Clear all breakpoints. + """ + self.model.bp_exec = {} + self.model.bp_read = {} + self.model.bp_write = {} + self.model.bp_access = {} + self.model._notify_breakpoints_changed() - new_breakpoint = TraceBreakpoint(address, access_type, length) - self.model.focused_breakpoint = new_breakpoint + def clear_execution_breakpoints(self): + """ + Clear all execution breakpoints. + """ + self.model.bp_exec = {} + self.model._notify_breakpoints_changed() - if access_type == BreakpointType.EXEC: - self._ignore_events = True - dctx.set_breakpoint(self.model.focused_breakpoint.address) - self._ignore_events = False + def clear_memory_breakpoints(self): + """ + Clear all memory breakpoints. + """ + self.model.bp_read = {} + self.model.bp_write = {} + self.model.bp_access = {} + self.model._notify_breakpoints_changed() - return new_breakpoint - def _ui_breakpoint_changed(self, address, event_type): """ Handle a breakpoint change event from the UI. """ - if self._ignore_events: + if self._ignore_signals: return - #print(f"UI Breakpoint Event {event_type} @ {address:08X}") + self._delete_disassembler_breakpoints() + self.model.bp_exec = {} + + if event_type in [BreakpointEvent.ADDED, BreakpointEvent.ENABLED]: + self.add_execution_breakpoint(address) - if event_type == BreakpointEvent.ADDED: - self.focus_breakpoint(address, BreakpointType.EXEC) + self.model._notify_breakpoints_changed() - elif event_type in [BreakpointEvent.DISABLED, BreakpointEvent.REMOVED]: - self.focus_breakpoint(None) + def _delete_disassembler_breakpoints(self): + """ + Remove all execution breakpoints from the disassembler UI. + """ + dctx = disassembler[self.pctx] + + self._ignore_signals = True + for address in self.model.bp_exec: + dctx.delete_breakpoint(address) + self._ignore_signals = False class BreakpointModel(object): """ @@ -128,37 +158,38 @@ def __init__(self): # Callbacks #---------------------------------------------------------------------- - self._focus_changed_callbacks = [] + self._breakpoints_changed_callbacks = [] def reset(self): self.bp_exec = {} self.bp_read = {} self.bp_write = {} - self._focused_breakpoint = None + self.bp_access = {} @property - def focused_breakpoint(self): - return self._focused_breakpoint - - @focused_breakpoint.setter - def focused_breakpoint(self, value): - if value == self.focused_breakpoint: - return - self._focused_breakpoint = value - self._notify_focused_breakpoint_changed(value) + def memory_breakpoints(self): + """ + Return an iterable list of all memory breakpoints. + """ + bps = itertools.chain( + self.bp_read.values(), + self.bp_write.values(), + self.bp_access.values() + ) + return bps #---------------------------------------------------------------------- # Callbacks #---------------------------------------------------------------------- - def focused_breakpoint_changed(self, callback): + def breakpoints_changed(self, callback): """ Subscribe a callback for a breakpoint changed event. """ - register_callback(self._focus_changed_callbacks, callback) + register_callback(self._breakpoints_changed_callbacks, callback) - def _notify_focused_breakpoint_changed(self, breakpoint): + def _notify_breakpoints_changed(self): """ Notify listeners of a breakpoint changed event. """ - notify_callback(self._focus_changed_callbacks, breakpoint) + notify_callback(self._breakpoints_changed_callbacks) diff --git a/plugins/tenet/context.py b/plugins/tenet/context.py index 37ef4df..340e5bd 100644 --- a/plugins/tenet/context.py +++ b/plugins/tenet/context.py @@ -85,11 +85,10 @@ def _auto_launch(self): """ def test_load(): - #filepath = R"C:\Users\user\Desktop\projects\tenet_dev\testcases\xbox\2bl.log" - filepath = R"C:\Users\user\Desktop\projects\tenet_dev\testcases\xbox\mcpx.log" - #filepath = R"C:\Users\user\Desktop\projects\tenet_dev\tenet\tracers\pin\boombox.log" + import ida_loader + trace_filepath = ida_loader.get_plugin_options("Tenet") focus_window() - self.load_trace(filepath) + self.load_trace(trace_filepath) self.show_ui() def dev_launch(): @@ -157,7 +156,20 @@ def load_trace(self, filepath): # self.reader = TraceReader(filepath, self.arch, disassembler[self]) - pmsg(f"Loaded trace of {self.reader.trace.length:,} instructions...") + pmsg(f"Loaded trace {self.reader.trace.filepath}") + pmsg(f"- {self.reader.trace.length:,} instructions...") + + if self.reader.analysis.slide != None: + pmsg(f"- {self.reader.analysis.slide:08X} ASLR slide...") + else: + disassembler.warning("Failed to automatically detect ASLR base!\n\nSee console for more info...") + pmsg(" +------------------------------------------------------") + pmsg(" |- ERROR: Failed to detect ASLR base for this trace.") + pmsg(" | --------------------------------------- ") + pmsg(" +-+ You can 'try' rebasing the database to the correct ASLR base") + pmsg(" | if you know it, and reload the trace. Otherwise, it is possible") + pmsg(" | your trace is just... very small and Tenet was not confident") + pmsg(" | predicting an ASLR slide.") # # we only hook directly into the disassembler / UI / subsytems once @@ -245,6 +257,9 @@ def show_ui(self): mw = get_qmainwindow() mw.addToolBar(QtCore.Qt.RightToolBarArea, self.trace) self.trace.show() + + # trigger update check + self.core.check_for_update() #------------------------------------------------------------------------- # Integrated UI Event Handlers @@ -295,7 +310,8 @@ def interactive_next_execution(self): Handle UI actions for seeking to the next execution of the selected address. """ address = disassembler[self].get_current_address() - result = self.reader.seek_to_next(address, BreakpointType.EXEC) + rebased_address = self.reader.analysis.rebase_pointer(address) + result = self.reader.seek_to_next(rebased_address, BreakpointType.EXEC) # TODO: blink screen? make failure more visible... if not result: @@ -306,7 +322,8 @@ def interactive_prev_execution(self): Handle UI actions for seeking to the previous execution of the selected address. """ address = disassembler[self].get_current_address() - result = self.reader.seek_to_prev(address, BreakpointType.EXEC) + rebased_address = self.reader.analysis.rebase_pointer(address) + result = self.reader.seek_to_prev(rebased_address, BreakpointType.EXEC) # TODO: blink screen? make failure more visible... if not result: @@ -317,7 +334,8 @@ def interactive_first_execution(self): Handle UI actions for seeking to the first execution of the selected address. """ address = disassembler[self].get_current_address() - result = self.reader.seek_to_first(address, BreakpointType.EXEC) + rebased_address = self.reader.analysis.rebase_pointer(address) + result = self.reader.seek_to_first(rebased_address, BreakpointType.EXEC) # TODO: blink screen? make failure more visible... if not result: @@ -328,7 +346,8 @@ def interactive_final_execution(self): Handle UI actions for seeking to the final execution of the selected address. """ address = disassembler[self].get_current_address() - result = self.reader.seek_to_final(address, BreakpointType.EXEC) + rebased_address = self.reader.analysis.rebase_pointer(address) + result = self.reader.seek_to_final(rebased_address, BreakpointType.EXEC) # TODO: blink screen? make failure more visible... if not result: @@ -340,7 +359,36 @@ def _idx_changed(self, idx): This will make the disassembler track with the PC/IP of the trace reader. """ - disassembler[self].navigate(self.reader.ip) + dctx = disassembler[self] + + # + # get a 'rebased' version of the current instruction pointer, which + # should map to the disassembler / open database if it is a code + # address that is known + # + + bin_address = self.reader.rebased_ip + + # + # if the code address is in a library / other unknown area that + # cannot be renedered by the disassembler, then resolve the last + # known trace 'address' within the database + # + + if not dctx.is_mapped(bin_address): + last_good_idx = self.reader.analysis.get_prev_mapped_idx(idx) + if last_good_idx == -1: + return # navigation is just not gonna happen... + + # fetch the last instruction pointer to fall within the trace + last_good_trace_address = self.reader.get_ip(last_good_idx) + + # convert the trace-based instruction pointer to one that maps to the disassembler + bin_address = self.reader.analysis.rebase_pointer(last_good_trace_address) + + # navigate the disassembler to a 'suitable' address based on the trace idx + dctx.navigate(bin_address) + disassembler.refresh_views() def _select_trace_file(self): """ diff --git a/plugins/tenet/core.py b/plugins/tenet/core.py index b9e1630..861900d 100644 --- a/plugins/tenet/core.py +++ b/plugins/tenet/core.py @@ -34,7 +34,7 @@ class TenetCore(object): #-------------------------------------------------------------------------- PLUGIN_NAME = "Tenet" - PLUGIN_VERSION = "0.1.0" + PLUGIN_VERSION = "0.2.0" PLUGIN_AUTHORS = "Markus Gaasedelen" PLUGIN_DATE = "2021" @@ -48,9 +48,6 @@ def load(self): """ self.contexts = {} self._update_checked = False - - # print plugin banner - pmsg(f"Loading {self.PLUGIN_NAME} v{self.PLUGIN_VERSION} - (c) {self.PLUGIN_AUTHORS} - {self.PLUGIN_DATE}") # the plugin color palette self.palette = PluginPalette() @@ -60,15 +57,16 @@ def load(self): self._install_ui() # all done, mark the core as loaded - logger.info("Successfully loaded plugin") self.loaded = True + + # print plugin banner + pmsg(f"Loaded v{self.PLUGIN_VERSION} - (c) {self.PLUGIN_AUTHORS} - {self.PLUGIN_DATE}") + logger.info("Successfully loaded plugin") def unload(self): """ Unload the plugin, and remove any UI integrations. """ - - # if the core was never fully loaded, there's nothing else to do if not self.loaded: return diff --git a/plugins/tenet/hex.py b/plugins/tenet/hex.py index a95c295..bd8b173 100644 --- a/plugins/tenet/hex.py +++ b/plugins/tenet/hex.py @@ -22,17 +22,17 @@ class HexController(object): def __init__(self, pctx): self.pctx = pctx + self.model = HexModel(pctx) self.reader = None - self.model = HexModel() # UI components self.view = None self.dockable = None self._title = "" - # events - self._ignore_breakpoint = False - pctx.breakpoints.model.focused_breakpoint_changed(self._focused_breakpoint_changed) + # signals + self._ignore_signals = False + pctx.breakpoints.model.breakpoints_changed(self._breakpoints_changed) def show(self, target=None, position=0): """ @@ -54,9 +54,9 @@ def show(self, target=None, position=0): self.view = HexView(self, self.model) new_dockable = DockableWindow(self._title, self.view) - + # - # if there is a reference to a left over dockable window (e.g, from a + # if there is a reference to a left over dockable window (e.g, from a # previous close of this window type) steal its dock positon so we can # hopefully take the same place as the old one # @@ -78,15 +78,15 @@ def hide(self): # if there is no view/dockable, then there's nothing to try and hide if not(self.view and self.dockable): return - + # hide the dockable, and drop references to the widgets self.dockable.hide() self.view = None self.dockable = None - + def attach_reader(self, reader): """ - Attach this controller to a trace reader. + Attach a trace reader to this controller. """ self.reader = reader self.model.pointer_size = reader.arch.POINTER_SIZE @@ -99,12 +99,12 @@ def attach_reader(self, reader): # it's the first time we're seeing this. this ensures that our widget # will accurately reflect the current state of the reader # - + self._idx_changed(reader.idx) def detach_reader(self): """ - Attach this controller to a trace reader. + Detach the trace reader from this controller. """ self.reader = None self.model.reset() @@ -157,21 +157,14 @@ def copy_selection(self, start_address, end_address): return byte_string - def focus_address_access(self, address): + def pin_memory(self, address, access_type=BreakpointType.ACCESS, length=1): """ - Focus on the given memory address. + Pin a region of memory. """ - self._ignore_breakpoint = True - self.pctx.breakpoints.focus_breakpoint(address, BreakpointType.ACCESS) - self._ignore_breakpoint = False - - def focus_region_access(self, address, length): - """ - Focus on a region of memory. - """ - self._ignore_breakpoint = True - self.pctx.breakpoints.focus_breakpoint(address, BreakpointType.ACCESS, length) - self._ignore_breakpoint = False + self._ignore_signals = True + self.pctx.breakpoints.clear_memory_breakpoints() + self.pctx.breakpoints.add_breakpoint(address, access_type, length) + self._ignore_signals = False def refresh_memory(self): """ @@ -188,7 +181,6 @@ def refresh_memory(self): self.model.mask = memory.mask self.model.delta = self.reader.delta - #self._refresh_display() if self.view: self.view.refresh() @@ -210,31 +202,25 @@ def _idx_changed(self, idx): """ self.refresh_memory() - def _focused_breakpoint_changed(self, breakpoint): + def _breakpoints_changed(self): """ - The focused breakpoint has changed. + Handle breakpoints changed event. """ if not self.view: return - mem_bps = [BreakpointType.READ, BreakpointType.WRITE, BreakpointType.ACCESS] - - if not (breakpoint and breakpoint.type in mem_bps): - self.view.reset_selection() - self.view.refresh() + if self._ignore_signals: return - if not self._ignore_breakpoint: - self.view.reset_selection() - self.view.refresh() - return + self.view.refresh() class HexModel(object): """ A generalized model for Hex View based window. """ - def __init__(self): + def __init__(self, pctx): + self._pctx = pctx # how the hex (data) and auxillary text should be displayed self._hex_format = HexType.BYTE @@ -260,13 +246,20 @@ def reset(self): self.address = 0 self.fade_address = 0 - self._selection_start_byte = 0 - self._selection_end_byte = 0 - + # pinned memory / breakpoint selections + self._pinned_selections = [] + #---------------------------------------------------------------------- # Properties #---------------------------------------------------------------------- + @property + def memory_breakpoints(self): + """ + Return the set of active memory breakpoints. + """ + return self._pctx.breakpoints.model.memory_breakpoints + @property def num_bytes_per_line(self): """ @@ -283,7 +276,7 @@ def num_bytes_per_line(self, width): if width < 1: raise ValueError("Invalid bytes per line value (must be > 0)") - if width % HEX_TYPE_WIDTH[self._hex_format]: + if width % HEX_TYPE_WIDTH[self._hex_format]: raise ValueError("Bytes per line must be a multiple of display format type") self._num_bytes_per_line = width diff --git a/plugins/tenet/integration/api/ida_api.py b/plugins/tenet/integration/api/ida_api.py index 2800799..2a62187 100644 --- a/plugins/tenet/integration/api/ida_api.py +++ b/plugins/tenet/integration/api/ida_api.py @@ -21,6 +21,7 @@ import ida_idaapi import ida_diskio import ida_kernwin +import ida_segment from .api import DisassemblerCoreAPI, DisassemblerContextAPI from ...util.qt import * @@ -116,6 +117,9 @@ def execute_ui(function): def get_disassembler_user_directory(self): return ida_diskio.get_user_idadir() + def refresh_views(self): + ida_kernwin.refresh_idaview_anyway() + def get_disassembly_background_color(self): """ Get the background color of the IDA disassembly view. @@ -204,6 +208,33 @@ def is_call_insn(self, address): return True return False + def get_instruction_addresses(self): + """ + Return all instruction addresses from the executable. + """ + instruction_addresses = [] + + for seg_address in idautils.Segments(): + + # fetch code segments + seg = ida_segment.getseg(seg_address) + if seg.sclass != ida_segment.SEG_CODE: + continue + + current_address = seg_address + end_address = seg.end_ea + + # save the address of each instruction in the segment + while current_address < end_address: + current_address = ida_bytes.next_head(current_address, end_address) + if ida_bytes.is_code(ida_bytes.get_flags(current_address)): + instruction_addresses.append(current_address) + + # print(f"Seg {seg.start_ea:08X} --> {seg.end_ea:08X} CODE") + #print(f" -- {len(instruction_addresses):,} instructions found") + + return instruction_addresses + def is_mapped(self, address): return ida_bytes.is_mapped(address) @@ -537,9 +568,7 @@ def copy_dock_position(self, other): class IDADockSizeHack(QtCore.QObject): def eventFilter(self, obj, event): - #print("Got ", obj, event, event.type()) if event.type() == QtCore.QEvent.WindowActivate: - #print("ALL DONE !") obj.setMinimumWidth(obj.min_width) obj.setMaximumWidth(obj.max_width) obj.removeEventFilter(self) diff --git a/plugins/tenet/integration/ida_integration.py b/plugins/tenet/integration/ida_integration.py index bcec7b7..b865995 100644 --- a/plugins/tenet/integration/ida_integration.py +++ b/plugins/tenet/integration/ida_integration.py @@ -6,6 +6,7 @@ # import ida_dbg +import ida_bytes import ida_idaapi import ida_kernwin @@ -418,7 +419,6 @@ def _highlight_disassesmbly(self, lines_out, widget, lines_in): step_over = False modifiers = QtGui.QGuiApplication.keyboardModifiers() step_over = bool(modifiers & QtCore.Qt.ShiftModifier) - #print("Stepping over?", step_over) forward_ips = ctx.reader.get_next_ips(trail_length, step_over) backward_ips = ctx.reader.get_prev_ips(trail_length, step_over) @@ -440,18 +440,30 @@ def _highlight_disassesmbly(self, lines_out, widget, lines_in): ida_color |= (0xFF - int(0xFF * percent)) << 24 # save the trail color - trail[address] = ida_color + rebased_address = ctx.reader.analysis.rebase_pointer(address) + trail[rebased_address] = ida_color + + current_address = ctx.reader.rebased_ip + if not ida_bytes.is_mapped(current_address): + last_good_idx = ctx.reader.analysis.get_prev_mapped_idx(ctx.reader.idx) + if last_good_idx != -1: + + # fetch the last instruction pointer to fall within the trace + last_good_trace_address = ctx.reader.get_ip(last_good_idx) + + # convert the trace-based instruction pointer to one that maps to the disassembler + current_address = ctx.reader.analysis.rebase_pointer(last_good_trace_address) for section in lines_in.sections_lines: for line in section: address = line.at.toea() - if address == ctx.reader.ip: - color = current_color - elif address in backward_trail: + if address in backward_trail: color = backward_trail[address] elif address in forward_trail: color = forward_trail[address] + elif address == current_address: + color = current_color else: continue diff --git a/plugins/tenet/integration/ida_loader.py b/plugins/tenet/integration/ida_loader.py index 44f4f92..3d44614 100644 --- a/plugins/tenet/integration/ida_loader.py +++ b/plugins/tenet/integration/ida_loader.py @@ -99,7 +99,7 @@ def term(self): logger.exception("Failed to cleanly unload Tenet from IDA.") end = time.time() - print("-"*50) + logger.debug("-"*50) logger.debug("IDA term done... (%.3f seconds...)" % (end-start)) diff --git a/plugins/tenet/registers.py b/plugins/tenet/registers.py index 68be7f3..7491b06 100644 --- a/plugins/tenet/registers.py +++ b/plugins/tenet/registers.py @@ -1,17 +1,16 @@ from tenet.ui import * -from tenet.types import BreakpointType from tenet.util.misc import register_callback, notify_callback -from tenet.integration.api import DockableWindow +from tenet.integration.api import DockableWindow, disassembler #------------------------------------------------------------------------------ # registers.py -- Register Controller #------------------------------------------------------------------------------ # # The purpose of this file is to house the 'headless' components of the -# registers window and its underlying functionality. This is split into a -# model and controller component, of a typical 'MVC' design pattern. +# registers window and its underlying functionality. This is split into a +# model and controller component, of a typical 'MVC' design pattern. # -# NOTE: for the time being, this file also contains the logic for the +# NOTE: for the time being, this file also contains the logic for the # 'IDX Shell' as it is kind of attached to the register view and not big # enough to demand its own seperate structuring ... yet # @@ -23,15 +22,16 @@ class RegisterController(object): def __init__(self, pctx): self.pctx = pctx + self.model = RegistersModel(pctx) self.reader = None - self.model = RegistersModel(self.pctx.arch) # UI components self.view = None self.dockable = None - # events - pctx.breakpoints.model.focused_breakpoint_changed(self._focused_breakpoint_changed) + # signals + self._ignore_signals = False + pctx.breakpoints.model.breakpoints_changed(self._breakpoints_changed) def show(self, target=None, position=0): """ @@ -56,7 +56,7 @@ def show(self, target=None, position=0): new_dockable = DockableWindow("CPU Registers", self.view) # - # if there is a reference to a left over dockable window (e.g, from a + # if there is a reference to a left over dockable window (e.g, from a # previous close of this window type) steal its dock positon so we can # hopefully take the same place as the old one # @@ -83,7 +83,7 @@ def hide(self): self.dockable.hide() self.view = None self.dockable = None - + def attach_reader(self, reader): """ Attach a trace reader to this controller. @@ -108,20 +108,27 @@ def detach_reader(self): self.reader = None self.model.reset() - def focus_register_value(self, reg_name): + def set_ip_breakpoint(self): """ - Focus a register value in the register view. + Set an execution breakpoint on the current instruction pointer. """ + current_ip = self.model.registers[self.model.arch.IP] - # if the instruction pointer is selected, show its executions in the trace - if reg_name == self.model.arch.IP: - reg_value = self.model.registers[reg_name] - self.pctx.breakpoints.focus_breakpoint(reg_value, BreakpointType.EXEC) - else: - self.clear_register_focus() + self._ignore_signals = True + self.pctx.breakpoints.clear_execution_breakpoints() + self.pctx.breakpoints.add_execution_breakpoint(current_ip) + self._ignore_signals = False + + if self.view: + self.view.refresh() + # TODO: maybe we can remove all these 'focus' funcs now? + def focus_register_value(self, reg_name): + """ + Focus a register value in the register view. + """ self.model.focused_reg_value = reg_name - + def focus_register_name(self, reg_name): """ Focus a register name in the register view. @@ -147,10 +154,6 @@ def _clear_register_value_focus(self): """ Clear focus from the active register field. """ - if self.model.focused_reg_value == self.model.arch.IP: - assert self.pctx.breakpoints.model.focused_breakpoint - assert self.pctx.breakpoints.model.focused_breakpoint.address == self.model.registers[self.model.arch.IP] - self.pctx.breakpoints.focus_breakpoint(None) self.model.focused_reg_value = None def set_registers(self, registers, delta=None): @@ -159,7 +162,7 @@ def set_registers(self, registers, delta=None): """ self.model.set_registers(registers, delta) - def navigate_to_expression(self, expression): + def evaluate_expression(self, expression): """ Evaluate the expression in the IDX Shell and navigate to it. """ @@ -167,6 +170,7 @@ def navigate_to_expression(self, expression): # a target idx was given as an integer if isinstance(expression, int): target_idx = expression + self.reader.seek(target_idx) # string handling elif isinstance(expression, str): @@ -177,21 +181,7 @@ def navigate_to_expression(self, expression): # a 'command' / alias idx was entered into the shell ('!...' prefix) if expression[0] == '!': - - # - # for now, we only support 'one' command which is going to - # let you seek to a position in the trace by percentage - # - # eg: !0, or !100 to skip to the start/end of trace - # - - try: - target_percent = float(expression[1:]) - except: - return - - # seek to the desired percentage - self.reader.seek_percent(target_percent) + self._handle_command(expression[1:]) return # @@ -205,49 +195,86 @@ def navigate_to_expression(self, expression): except: return + self.reader.seek(target_idx) + else: raise ValueError(f"Unknown input expression type '{expression}'?!?") - # seek to the desired idx - self.reader.seek(target_idx) + def _handle_command(self, expression): + """ + Handle the evaluation of commands on the timestamp shell. + """ + if self._handle_seek_percent(expression): + return True + if self._handle_seek_last(expression): + return True + return False - def _idx_changed(self, idx): + def _handle_seek_percent(self, expression): """ - The trace position has been changed. + Handle a 'percentage-based' trace seek. + + eg: !0, or !100 to skip to the start/end of trace """ - IP = self.model.arch.IP - target = self.pctx.breakpoints.model.focused_breakpoint - registers = self.pctx.reader.registers + try: + target_percent = float(expression) # float, so you could even do 42.1% + except: + return False - if target and target.address == registers[IP]: - self.model.focused_reg_value = IP - else: - self.model.focused_reg_value = None + # seek to the desired percentage in the trace + self.reader.seek_percent(target_percent) + return True + + def _handle_seek_last(self, expression): + """ + Handle a seek to the last mapped address. + """ + if expression != 'last': + return False + last_idx = self.reader.trace.length - 1 + last_ip = self.reader.get_ip(last_idx) + rebased_ip = self.reader.analysis.rebase_pointer(last_ip) + + dctx = disassembler[self.pctx] + if not dctx.is_mapped(rebased_ip): + last_good_idx = self.reader.analysis.get_prev_mapped_idx(last_idx) + if last_good_idx == -1: + return False # navigation is just not gonna happen... + last_idx = last_good_idx + + # seek to the last known / good idx that is mapped within the disassembler + self.reader.seek(last_idx) + return True + + def _idx_changed(self, idx): + """ + The trace position has been changed. + """ self.model.idx = idx self.set_registers(self.reader.registers, self.reader.trace.get_reg_delta(idx).keys()) - def _focused_breakpoint_changed(self, breakpoint): + def _breakpoints_changed(self): """ - The focused breakpoint has changed. + Handle breakpoints changed event. """ if not self.view: return + self.view.refresh() - if not (breakpoint and breakpoint.type == BreakpointType.EXEC): - self.model.focused_reg_value = None - self.view.refresh() - return - - IP = self.model.arch.IP - registers = self.pctx.reader.registers + def _idx_changed(self, idx): + """ + The trace position has been changed. + """ + self.model.idx = idx + self.set_registers(self.reader.registers, self.reader.trace.get_reg_delta(idx).keys()) - if registers[IP] != breakpoint.address: - self.model.focused_reg_value = None - self.view.refresh() + def _breakpoints_changed(self): + """ + Handle breakpoints changed event. + """ + if not self.view: return - - self.model.focused_reg_value = IP self.view.refresh() class RegistersModel(object): @@ -255,8 +282,8 @@ class RegistersModel(object): The Registers Model (Data) """ - def __init__(self, arch): - self.arch = arch + def __init__(self, pctx): + self._pctx = pctx self.reset() #---------------------------------------------------------------------- @@ -265,17 +292,71 @@ def __init__(self, arch): self._registers_changed_callbacks = [] + #---------------------------------------------------------------------- + # Properties + #---------------------------------------------------------------------- + + @property + def arch(self): + """ + Return the architecture definition. + """ + return self._pctx.arch + + @property + def execution_breakpoints(self): + """ + Return the set of active execution breakpoints. + """ + return self._pctx.breakpoints.model.bp_exec + + #---------------------------------------------------------------------- + # Public + #---------------------------------------------------------------------- + def reset(self): + + # the current timestamp in the trace self.idx = -1 - self.delta = [] + + # the { reg_name: reg_value } dict of current register values self.registers = {} + # + # the names of the registers that have changed since the previous + # chronological timestamp in the trace. + # + # for example if you singlestep forward, any registers that changed as + # a result of 'normal execution' may be highlighted (e.g. red) + # + + self.delta_trace = [] + + # + # the names of registers that have changed since the last navigation + # event (eg, skipping between breakpoints, memory accesses). + # + # this is used to highlight registers that may not have changed as a + # result of the previous chronological trace event, but by means of + # user navigation within tenet. + # + + self.delta_navigation = [] + self.focused_reg_name = None self.focused_reg_value = None def set_registers(self, registers, delta=None): + + # compute which registers changed as a result of navigation + unchanged = dict(set(self.registers.items()) & set(registers.items())) + self.delta_navigation = set([k for k in registers if k not in unchanged]) + + # save the register delta that changed since the previous trace timestamp + self.delta_trace = delta if delta else [] self.registers = registers - self.delta = delta if delta else [] + + # notify the UI / listeners of the model that an update occurred self._notify_registers_changed() #---------------------------------------------------------------------- diff --git a/plugins/tenet/stack.py b/plugins/tenet/stack.py index cf3e682..5d104bb 100644 --- a/plugins/tenet/stack.py +++ b/plugins/tenet/stack.py @@ -39,7 +39,6 @@ def follow_in_dump(self, stack_address): """ Follow the pointer at a given stack address in the memory dump. """ - #print("STACK FOLLOW CLICKED WITH 0x%08X" % stack_address) POINTER_SIZE = self.pctx.reader.arch.POINTER_SIZE # align the given stack address (which we will read..) @@ -65,7 +64,6 @@ def follow_in_dump(self, stack_address): # unpack the carved data as a pointer parsed_address = struct.unpack("I" if POINTER_SIZE == 4 else "Q", data)[0] - #print("PARSED ADDRESS: 0x%08X" % parsed_address) # navigate the memory dump window to the 'pointer' we carved off the stack self.pctx.memory.navigate(parsed_address) @@ -78,15 +76,19 @@ def _idx_changed(self, idx): # fade out the upper part of the stack that is currently 'unallocated' self.set_fade_threshold(self.reader.sp) - # - # if the user has a byte / range selected ... we will *not* move - # the stack view on idx changes. this is to preserve the location - # of their selection on-screen (eg, when hovering a selected byte, - # and jumping between its memory accesses) - # - if self.view: - if not (self.view._select_begin == self.view._select_end == -1): + + # + # if the user has a byte / range selected or the view is purposely + # omitting navigation events, we will *not* move the stack view on + # idx changes. + # + # this is to preserve the location of their selection on-screen + # (eg, when hovering a selected byte, and jumping between its + # memory accesses) + # + + if self.view._ignore_navigation or self.view.selection_size: self.refresh_memory() self.view.refresh() return diff --git a/plugins/tenet/trace/analysis.py b/plugins/tenet/trace/analysis.py new file mode 100644 index 0000000..0820c5f --- /dev/null +++ b/plugins/tenet/trace/analysis.py @@ -0,0 +1,253 @@ +import bisect +import collections + +from tenet.util.log import pmsg + +#----------------------------------------------------------------------------- +# analysis.py -- Trace Analysis +#----------------------------------------------------------------------------- +# +# This file should contain logic to further process, augment, optimize or +# annotate Tenet traces when a binary analysis framework such as IDA / +# Binary Ninja is available to a trace reader. +# +# As of now (v0.2) the only added analysis we do is to try and map +# ASLR'd trace addresses to executable opened in the database. +# +# In the future, I imagine this file will be used to indexing events +# such as function calls, returns, entry and exit to unmapped regions, +# service pointer annotations, and much more. +# + +class TraceAnalysis(object): + """ + A high level, debugger-like interface for querying Tenet traces. + """ + + def __init__(self, trace, dctx): + self._dctx = dctx + self._trace = trace + self._remapped_regions = [] + self._unmapped_entry_points = [] + self.slide = None + self._analyze() + + #------------------------------------------------------------------------- + # Public + #------------------------------------------------------------------------- + + def rebase_pointer(self, address): + """ + Return a rebased version of the given address, if one exists. + """ + for m1, m2 in self._remapped_regions: + #print(f"m1 start: {m1[0]:08X} address: {address:08X} m1 end: {m1[1]:08X}") + #print(f"m2 start: {m2[0]:08X} address: {address:08X} m2 end: {m2[1]:08X}") + if m1[0] <= address <= m1[1]: + return address + (m2[0] - m1[0]) + if m2[0] <= address <= m2[1]: + return address - (m2[0] - m1[0]) + return address + + def get_prev_mapped_idx(self, idx): + """ + Return the previous idx to fall within a mapped code region. + """ + index = bisect.bisect_right(self._unmapped_entry_points, idx) - 1 + try: + return self._unmapped_entry_points[index] + except IndexError: + return -1 + + #------------------------------------------------------------------------- + # Analysis + #------------------------------------------------------------------------- + + def _analyze(self): + """ + Analyze the trace against the binary loaded by the disassembler. + """ + self._analyze_aslr() + self._analyze_unmapped() + + def _analyze_aslr(self): + """ + Analyze trace execution to resolve ASLR mappings against the disassembler. + """ + dctx, trace = self._dctx, self._trace + + # get *all* of the instruction addresses from disassembler + instruction_addresses = dctx.get_instruction_addresses() + + # + # bucket the instruction addresses from the disassembler + # based on non-aslr'd bits (lower 12 bits, 0xFFF) + # + + binary_buckets = collections.defaultdict(list) + for address in instruction_addresses: + bits = address & 0xFFF + binary_buckets[bits].append(address) + + # get the set of unique, executed addresses from the trace + trace_addresses = trace.ip_addrs + + # + # scan the executed addresses from the trace, and discard + # any that cannot be bucketed by the non ASLR-d bits that + # match the open executable + # + + trace_buckets = collections.defaultdict(list) + for executed_address in trace_addresses: + bits = executed_address & 0xFFF + if bits not in binary_buckets: + continue + trace_buckets[bits].append(executed_address) + + # + # this is where things get a little bit interesting. we compute the + # distance between addresses in the trace and disassembler buckets + # + # the distance that appears most frequently is likely to be the ASLR + # slide to align the disassembler imagebase and trace addresses + # + + slide_buckets = collections.defaultdict(list) + for bits, bin_addresses in binary_buckets.items(): + for executed_address in trace_buckets[bits]: + for disas_address in bin_addresses: + distance = disas_address - executed_address + slide_buckets[distance].append(executed_address) + + # basically the executable 'range' of the open binary + disas_low_address = instruction_addresses[0] + disas_high_address = instruction_addresses[-1] + + # convert to set for O(1) lookup in following loop + instruction_addresses = set(instruction_addresses) + + # + # loop through all the slide buckets, from the most frequent distance + # (ASLR slide) to least frequent. the goal now is to sanity check the + # ranges to find one that seems to couple tightly with the disassembler + # + + for k in sorted(slide_buckets, key=lambda k: len(slide_buckets[k]), reverse=True): + expected = len(slide_buckets[k]) + + # + # TODO: uh, if it's getting this small, I don't feel comfortable + # selecting an ASLR slide. the user might be loading a tiny trace + # with literally 'less than 10' unique instructions (?) that + # would map to the database + # + + if expected < 10: + continue + + hit, seen = 0, 0 + for address in trace_addresses: + + # add the ASLR slide for this bucket to a traced address + rebased_address = address + k + + # the rebased address seems like it falls within the disassembler ranges + if disas_low_address <= rebased_address < disas_high_address: + seen += 1 + + # but does the address *actually* exist in the disassembler? + if rebased_address in instruction_addresses: + hit += 1 + + # + # the first *high* hit ratio is almost certainly the correct + # ASLR, practically speaking this should probably be 1.00, but + # I lowered it a bit to give a bit of flexibility. + # + # NOTE/TODO: a lower 'hit' ratio *could* occur if a lot of + # undefined instruction addresses in the disassembler get + # executed in the trace. this could be packed code / malware, + # in which case we will have to perform more aggressive analysis + # + + if (hit / seen) > 0.95: + #print(f"ASLR Slide: {k:08X} Quality: {hit/seen:0.2f} (h {hit} s {seen} e {expected})") + slide = k + break + + # + # if we do not break from the loop, we failed to find an adequate + # slide, which is very bad. + # + # NOTE/TODO: uh what do we do if we fail the ASLR slide? + # + + else: + self.slide = None + return False + + # + # TODO: err, lol this is all kind of dirty. should probably refactor + # and clean up this whole 'remapped_regions' stuff. + # + + m1 = [disas_low_address, disas_high_address] + + if slide < 0: + m2 = [m1[0] - slide, m1[1] - slide] + else: + m2 = [m1[0] + slide, m1[1] + slide] + + self.slide = slide + self._remapped_regions.append((m1, m2)) + + return True + + def _analyze_unmapped(self): + """ + Analyze trace execution to identify entry/exit to unmapped segments. + """ + if self.slide is None: + return + + # alias for readability and speed + trace, ips = self._trace, self._trace.ip_addrs + lower_mapped, upper_mapped = self._remapped_regions[0][1] + + # + # for speed, pull out the 'compressed' ip indexes that matched mapped + # (known) addresses within the disassembler context + # + + mapped_ips = set() + for i, address in enumerate(ips): + if lower_mapped <= address <= upper_mapped: + mapped_ips.add(i) + + last_good_idx = 0 + unmapped_entries = [] + + # loop through each segment in the trace + for seg in trace.segments: + seg_ips = seg.ips + seg_base = seg.base_idx + + # loop through each executed instruction in this segment + for relative_idx in range(0, seg.length): + compressed_ip = seg_ips[relative_idx] + + # the current instruction is in an unmapped region + if compressed_ip not in mapped_ips: + + # if we were in a known/mapped region previously, then save it + if last_good_idx: + unmapped_entries.append(last_good_idx) + last_good_idx = 0 + + # if we are in a good / mapped region, update our current idx + else: + last_good_idx = seg_base + relative_idx + + #print(f" - Unmapped Entry Points: {len(unmapped_entries)}") + self._unmapped_entry_points = unmapped_entries diff --git a/plugins/tenet/trace/file.py b/plugins/tenet/trace/file.py index bbb87d8..5ee49c9 100644 --- a/plugins/tenet/trace/file.py +++ b/plugins/tenet/trace/file.py @@ -1,5 +1,6 @@ import os import time +import zlib import array import bisect import ctypes @@ -42,11 +43,6 @@ # out a matching file name with the '.tt' file extension, and prioritize # loading that over a raw text trace. # -# NOTE/TODO: Sorry, but there are no controls in place to ensure that -# a Tenet Trace with a matching file name to a raw trace on disk actually -# match content-wise. So be careful if using generic trace file names, -# such as 'trace.log' and a resulting 'trace.tt' -# #----------------------------------------------------------------------------- # Imports @@ -107,6 +103,16 @@ # Utils #----------------------------------------------------------------------------- +def hash_file(filepath): + """ + Return a CRC32 of the file at the given path. + """ + crc = 0 + with open(filepath, 'rb', 65536) as ins: + for x in range(int((os.stat(filepath).st_size / 65536)) + 1): + crc = zlib.crc32(ins.read(65536), crc) + return (crc & 0xFFFFFFFF) + def number_of_bits_set(i): """ Count the number of bits set in the given 32bit integer. @@ -170,6 +176,7 @@ class TraceInfo(ctypes.Structure): ('mask_num', ctypes.c_uint32), ('mem_idx_width', ctypes.c_uint8), ('mem_addr_width', ctypes.c_uint8), + ('original_hash', ctypes.c_uint32), ] class SegmentInfo(ctypes.Structure): @@ -286,6 +293,9 @@ def __init__(self, filepath, arch=None): # the number of timestamps / 'instructions' for each trace segment self.segment_length = DEFAULT_SEGMENT_LENGTH + # the hash of the original / source log file + self.original_hash = None + # # now that you have some idea of how the trace file is going to be # organized... let's actually go and try to load one @@ -424,6 +434,7 @@ def _save_header(self, zip_archive): header.mask_num = len(self.masks) header.mem_idx_width = width_from_type(self.mem_idx_type) header.mem_addr_width = width_from_type(self.mem_addr_type) + header.original_hash = self.original_hash mask_data = (ctypes.c_uint32 * len(self.masks))(*self.masks) # save the global trace data / header to the zip @@ -453,12 +464,36 @@ def _load_trace(self): NOTE: THIS ROUTINE WILL ATTEMPT TO LOAD A PACKED TRACE INSTEAD OF A SELECTED RAW TEXT TRACE IF IT FINDS ONE AVAILABLE!!! """ + + # the user probably selected a '.tt' trace if zipfile.is_zipfile(self.filepath): self._load_packed_trace(self.filepath) - elif zipfile.is_zipfile(self.packed_filepath): - self._load_packed_trace(self.packed_filepath) - else: - self._load_text_trace(self.filepath) + return + + # + # the user selected a '.txt' trace, but there is a '.tt' packed trace + # beside it, so let's check if the packed trace matches the text trace + # + + if zipfile.is_zipfile(self.packed_filepath): + packed_crc = self._fetch_hash(self.packed_filepath) + text_crc = hash_file(self.filepath) + + # + # the crc in the packed file seems to match the selected text log, + # so let's just load the packed trace as it should be faster + # + + if packed_crc == text_crc: + self._load_packed_trace(self.packed_filepath) + return + + # + # no luck loading / side-loading packed traces, so simply try to + # load the user selected trace as a normal text Tenet trace + # + + self._load_text_trace(self.filepath) def _load_packed_trace(self, filepath): """ @@ -480,6 +515,16 @@ def _select_arch(self, magic): else: self.arch = ArchX86() + def _fetch_hash(self, filepath): + """ + Return the original file hash (CRC32) from the given packed trace filepath. + """ + header = TraceInfo() + with zipfile.ZipFile(filepath, 'r') as zip_archive: + with zip_archive.open('header', 'r') as f: + f.readinto(header) + return header.original_hash + def _load_header(self, zip_archive): """ Load the trace header from a packed trace. @@ -516,6 +561,9 @@ def _load_header(self, zip_archive): self.masks.fromfile(f, header.mask_num) self.mask_sizes = [number_of_bits_set(mask) * self.arch.POINTER_SIZE for mask in self.masks] + # source file hash + self.original_hash = header.original_hash + def _load_segments(self, zip_archive): """ Load the trace segments from the packed trace. @@ -555,6 +603,9 @@ def _load_text_trace(self, filepath): #if not self.arch: # self._select_arch(0) + # hash (CRC32) the source / text filepath before loading it + self.original_hash = hash_file(filepath) + # load / parse a text trace into trace segments with open(filepath, 'r') as f: @@ -587,7 +638,7 @@ def get_ip(self, idx): """ seg = self.get_segment(idx) if not seg: - raise ValueError("Invalid IDX") + raise ValueError("Invalid IDX %u" % idx) return seg.get_ip(idx) def get_mapped_ip(self, ip): @@ -1078,8 +1129,7 @@ def get_reg_info(self, idx, reg_names): """ relative_idx = idx - self.base_idx start_idx = relative_idx + 1 - - if not (0 <= start_idx < self.length): + if not (0 <= relative_idx < self.length): return {} # compute a 32bit mask of the registers we need to find @@ -1193,6 +1243,9 @@ def load(self, f): self.base_idx = info.base_idx self.length = info.length + if info.ip_num == 0: + raise ValueError("Empty trace file (ip_num == 0)") + ip_itemsize = info.ip_length // info.ip_num ip_type = type_from_width(ip_itemsize) @@ -1500,7 +1553,6 @@ def _process_line(self, line, relative_idx): address, hex_data = value.split(":") address = int(address, 16) - hex_data = bytes(hex_data.strip(), 'utf-8') data = binascii.unhexlify(hex_data) @@ -1693,20 +1745,3 @@ def _mask2regs(self, mask): mask >>= 1 bit_index += 1 return regs - -#----------------------------------------------------------------------------- -# Main -#----------------------------------------------------------------------------- - -if __name__ == "__main__": - #filepath = R"C:\Users\user\Desktop\projects\tenet_dev\traces\wtf\143da869b34e7d895ed9548429e9103d.trace" - #filepath = R"C:\Users\user\Desktop\projects\tenet_dev\traces\wtf\crash-EXCEPTION_ACCESS_VIOLATION_EXECUTE-0x14106b008.trace" - #filepath = R"C:\Users\user\Desktop\projects\tenet_dev\testcases\xbox\mcpx.log" - #filepath = R"C:\Users\user\Desktop\projects\tenet_dev\testcases\xbox\2bl.log" - filepath = R"C:\Users\user\Desktop\projects\tenet_dev\tenet\tracers\pin\boombox.log" - - start = time.time() - trace = TraceFile(filepath) - end = time.time() - - print(f"Took {end-start:0.2f} seconds, trace length: {trace.length:,}") \ No newline at end of file diff --git a/plugins/tenet/trace/reader.py b/plugins/tenet/trace/reader.py index 84f058b..90ef9d6 100644 --- a/plugins/tenet/trace/reader.py +++ b/plugins/tenet/trace/reader.py @@ -1,5 +1,15 @@ import bisect import struct +import logging + +from tenet.types import BreakpointType +from tenet.util.log import pmsg +from tenet.util.misc import register_callback, notify_callback +from tenet.trace.file import TraceFile +from tenet.trace.types import TraceMemory +from tenet.trace.analysis import TraceAnalysis + +logger = logging.getLogger("Tenet.Trace.Reader") #----------------------------------------------------------------------------- # reader.py -- Trace Reader @@ -15,7 +25,7 @@ # is responsible for the navigating a loaded trace file, providing 'high # level' APIs one might expect to 'efficiently' query a program for # registers or memory at any timestamp of execution. -# +# # Please be mindful that like the TraceFile implementation, TraceReader # should be re-written entirely in a native language. Under the hood, it's # not exactly pretty. It was written to make the plugin simple to install @@ -29,17 +39,11 @@ # billions) of instructions. # -from tenet.types import BreakpointType -from tenet.util.log import pmsg -from tenet.util.misc import register_callback, notify_callback -from tenet.trace.file import TraceFile -from tenet.trace.types import TraceMemory - class TraceDelta(object): """ Trace Delta """ - + def __init__(self, registers, mem_read, mem_write): self.registers = registers self.mem_reads = mem_read @@ -57,6 +61,7 @@ def __init__(self, filepath, architecture, dctx=None): # load the given trace file from disk self.trace = TraceFile(filepath, architecture) + self.analysis = TraceAnalysis(self.trace, dctx) self._idx_cached_registers = -1 self._cached_registers = {} @@ -78,6 +83,13 @@ def ip(self): """ return self.get_register(self.arch.IP) + @property + def rebased_ip(self): + """ + Return a rebased version of the current instruction pointer (if available). + """ + return self.analysis.rebase_pointer(self.ip) + @property def sp(self): """ @@ -103,8 +115,6 @@ def segment(self): def delta(self): """ Return the state delta since the previous timestamp. - - TODO: is this even used anymore? """ read_set, write_set = set(), set() @@ -180,16 +190,14 @@ def seek_to_next(self, address, access_type, length=1, start_idx=None): if length == 1: idx = self.find_next_read(address, start_idx) else: - #idx = self.find_next_region_read(address, length, start_idx) - raise NotImplementedError("TODO: Implement find_next_region_read") + idx = self.find_next_region_read(address, length, start_idx) elif access_type == BreakpointType.WRITE: if length == 1: idx = self.find_next_write(address, start_idx) else: - #idx = self.find_next_region_write(address, length, start_idx) - raise NotImplementedError("TODO: Implement find_next_region_write") + idx = self.find_next_region_write(address, length, start_idx) elif access_type == BreakpointType.ACCESS: @@ -200,7 +208,7 @@ def seek_to_next(self, address, access_type, length=1, start_idx=None): else: raise NotImplementedError - + if idx == -1: return False @@ -226,16 +234,14 @@ def seek_to_prev(self, address, access_type, length=1, start_idx=None): if length == 1: idx = self.find_prev_read(address, start_idx) else: - #idx = self.find_prev_region_read(address, length, start_idx) - raise NotImplementedError("TODO: Implement find_prev_region_read") + idx = self.find_prev_region_read(address, length, start_idx) elif access_type == BreakpointType.WRITE: if length == 1: idx = self.find_prev_write(address, start_idx) else: - #idx = self.find_prev_region_write(address, length, start_idx) - raise NotImplementedError("TODO: Implement find_prev_region_write") + idx = self.find_prev_region_write(address, length, start_idx) elif access_type == BreakpointType.ACCESS: @@ -246,7 +252,7 @@ def seek_to_prev(self, address, access_type, length=1, start_idx=None): else: raise NotImplementedError - + if idx == -1: return False @@ -268,7 +274,7 @@ def step_forward(self, n=1, step_over=False): def step_backward(self, n=1, step_over=False): """ Step the trace backwards. - + If step_over=True, and a disassembler context is available to the trace reader, it will attempt to step over calls while stepping. """ @@ -282,29 +288,32 @@ def _step_over_forward(self, n): Step the trace forward over n instructions / calls. """ address = self.get_ip(self.idx) + bin_address = self.analysis.rebase_pointer(address) # # get the address for the linear instruction address after the # current instruction # - next_address = self.dctx.get_next_insn(address) - if next_address == -1: + bin_next_address = self.dctx.get_next_insn(bin_address) + if bin_next_address == -1: self.seek(self.idx + 1) return + trace_next_address = self.analysis.rebase_pointer(bin_next_address) + # # find the next time the instruction after this instruction is # executed in the trace # - - next_idx = self.find_next_execution(next_address, self.idx) + + next_idx = self.find_next_execution(trace_next_address, self.idx) # # the instruction after the call does not appear in the trace, # so just fall-back to 'step into' behavior # - + if next_idx == -1: self.seek(self.idx + 1) return @@ -316,8 +325,9 @@ def _step_over_backward(self, n): Step the trace backward over n instructions / calls. """ address = self.get_ip(self.idx) + bin_address = self.analysis.rebase_pointer(address) - prev_address = self.dctx.get_prev_insn(address) + bin_prev_address = self.dctx.get_prev_insn(bin_address) # # could not get the address of the instruction prior to the current @@ -328,7 +338,7 @@ def _step_over_backward(self, n): # performant backend than the python prototype that powers this # - if prev_address == -1: + if bin_prev_address == -1: self.seek(self.idx - 1) return @@ -338,8 +348,8 @@ def _step_over_backward(self, n): # and also pretty tricky to handle... # - if self.dctx.is_call_insn(prev_address): - + if self.dctx.is_call_insn(bin_prev_address): + # get the previous stack pointer address sp = self.get_register(self.arch.SP, self.idx - 1) @@ -367,7 +377,9 @@ def _step_over_backward(self, n): self.seek(self.idx - 1) return - prev_idx = self.find_prev_execution(prev_address, self.idx) + trace_prev_address = self.analysis.rebase_pointer(bin_prev_address) + + prev_idx = self.find_prev_execution(trace_prev_address, self.idx) if prev_idx == -1: self.seek(self.idx - 1) return @@ -377,7 +389,7 @@ def _step_over_backward(self, n): #------------------------------------------------------------------------- # Timestamp API #------------------------------------------------------------------------- - + # # in this section, you will find references to 'resolution'. this is a # knob that the trace reader uses to fetch 'approximate' results from @@ -394,8 +406,8 @@ def _step_over_backward(self, n): # given a 10 million instruction trace, and a 30px by 1000px image # buffer to viualize said trace... there is very little reason to fetch # 100_000 unique timestamps that all fall within one vertical pixel of - # the rendered visualization. - # + # the rendered visualization. + # # instead, we can search the trace in arbitrary resolution 'windows' of # roughly 1px (pixel resolution can be calculated based on the length of # the trace execution vs the length of the viz in pixels) and fetch results @@ -412,19 +424,20 @@ def get_executions_between(self, address, start_idx, end_idx, resolution=1): """ Return a list of timestamps (idx) that executed the given address, in the given slice. """ - assert 0 <= start_idx < end_idx <= self.trace.length, f"0 <= {start_idx:,} < {end_idx:,} <= {self.trace.length:,}" + assert 0 <= start_idx <= end_idx, f"0 <= {start_idx:,} <= {end_idx:,}" assert resolution > 0 - og_res = resolution + resolution = max(1, resolution) - #print(f"Fetching executions from {start_idx:,} --> {end_idx:,} (res {og_res:0.2f}, normalized {resolution:0.2f}) for address 0x{address:08X}") + #logger.debug(f"Fetching executions from {start_idx:,} --> {end_idx:,} (res {resolution:0.2f}, normalized {resolution:0.2f}) for address 0x{address:08X}") try: mapped_address = self.trace.get_mapped_ip(address) except ValueError: return [] - #print(f" - Mapped Address: {mapped_address}") - idx, output = start_idx, [] + output = [] + idx = max(0, start_idx) + end_idx = min(end_idx, self.trace.length) while idx < end_idx: @@ -434,12 +447,11 @@ def get_executions_between(self, address, start_idx, end_idx, resolution=1): # clamp the segment end if it extends past our segment seg_end = min(seg_base + seg.length, end_idx) - #print(f"Searching seg #{seg.id}, {seg_base:,} --> {seg_end:,}") + #logger.debug(f"Searching seg #{seg.id}, {seg_base:,} --> {seg_end:,}") # snip the segment to start from the given global idx relative_idx = idx - seg_base seg_ips = seg.ips[relative_idx:] - #print(f"length: {len(seg_ips)}") while idx < seg_end: @@ -448,7 +460,7 @@ def get_executions_between(self, address, start_idx, end_idx, resolution=1): except ValueError: idx = seg_end + 1 break - + # we got a hit within the resolution window, save it current_idx = idx + idx_offset output.append(current_idx) @@ -466,7 +478,7 @@ def get_executions_between(self, address, start_idx, end_idx, resolution=1): seg_ips = seg.ips[idx-seg_base:] - #print("returning hits", output) + #logger.debug(f"Returning hits {output}") return output def get_memory_accesses(self, address, resolution=1): @@ -475,61 +487,78 @@ def get_memory_accesses(self, address, resolution=1): """ return self.get_memory_accesses_between(address, 0, self.trace.length, resolution) - def get_memory_accesses_between(self, address, start_idx, end_idx, resolution=1): + + def get_memory_reads_between(self, address, start_idx, end_idx, resolution=1): + """ + Return a list of timestamps that read from a given memory address in the given slice. + """ + reads, _ = self.get_memory_accesses_between(address, start_idx, end_idx, resolution, BreakpointType.READ) + return reads + + def get_memory_writes_between(self, address, start_idx, end_idx, resolution=1): + """ + Return a list of timestamps that write to a given memory address in the given slice. + """ + _, writes = self.get_memory_accesses_between(address, start_idx, end_idx, resolution, BreakpointType.WRITE) + return writes + + def get_memory_accesses_between(self, address, start_idx, end_idx, resolution=1, access_type=BreakpointType.ACCESS): """ Return a tuple of lists (read, write) containing timestamps that access a given memory address in the given slice. """ assert resolution > 0 - reads, writes = [], [] + resolution = max(1, resolution) + + #logger.debug(f"MEMORY ACCESSES @ 0x{address:08X} // {start_idx:,} --> {end_idx:,} (rez {resolution:0.2f})") mapped_address = self.trace.get_mapped_address(address) if mapped_address == -1: - return (reads, writes) + return ([], []) - #print(f" RAW ADDRESS: 0x{address:08X}") - #print(f"ALIGNED ADDRESS: 0x{self.trace.get_aligned_address(address):08X}") - #print(f" ACCESS MASK: {self.trace.get_aligned_address_mask(address, 1):02X}") + reads, writes = [], [] access_mask = self.trace.get_aligned_address_mask(address, 1) - resolution = max(1, resolution) - # clamp the search incase the given params are a bit wonky + # clamp the search incase the given params are a bit wonky idx = max(0, start_idx) end_idx = min(end_idx, self.trace.length) assert idx < end_idx next_resolution = [idx, idx] - - # search through the trace + + # search through the trace while idx < end_idx: + + # fetch a segment to search forward through seg = self.trace.get_segment(idx) - #print(f"seg #{seg.id}, {seg.base_idx:,} --> {seg.base_idx+seg.length:,} -- IDX PTR {idx:,}") seg_base = seg.base_idx + + # clamp the segment end if it extends past our segment seg_end = min(seg_base + seg.length, end_idx) + #logger.debug(f"seg #{seg.id}, {seg.base_idx:,} --> {seg.base_idx+seg.length:,} -- IDX PTR {idx:,}") + + mem_sets = [] - mem_sets = \ - [ - (seg.read_idxs, seg.read_addrs, seg.read_masks, reads), - (seg.write_idxs, seg.write_addrs, seg.write_masks, writes), - ] + if access_type & BreakpointType.READ: + mem_sets.append((seg.read_idxs, seg.read_addrs, seg.read_masks, reads)) + if access_type & BreakpointType.WRITE: + mem_sets.append((seg.write_idxs, seg.write_addrs, seg.write_masks, writes)) for i, mem_type in enumerate(mem_sets): - #print("mem", i) idxs, addrs, masks, output = mem_type cumulative_index = 0 current_target = next_resolution[i] while current_target < seg_end: - #print("while", current_target) try: index = addrs.index(mapped_address) except ValueError: break - + cumulative_index += index current_idx = seg_base + idxs[index] - + # # there was a hit to the mapped address, which is aligned # to the arch pointer size... check if the requested addr @@ -561,7 +590,7 @@ def get_memory_accesses_between(self, address, start_idx, end_idx, resolution=1) addrs = addrs[skip_index:] idxs = idxs[skip_index:] - + cumulative_index += (skip_index - index) next_resolution[i] = current_target @@ -569,25 +598,54 @@ def get_memory_accesses_between(self, address, start_idx, end_idx, resolution=1) idx = seg_end + 1 return (reads, writes) - + + def get_memory_region_reads(self, address, length, resolution=1): + """ + Return a list of timestamps that read from the given memory region. + """ + reads, _ = self.get_memory_region_accesses_between(address, length, 0, self.trace.length, resolution, BreakpointType.READ) + return reads + + def get_memory_region_reads_between(self, address, length, start_idx, end_idx, resolution=1): + """ + Return a list of timestamps that read from the given memory region in the given time slice. + """ + reads, _ = self.get_memory_region_accesses_between(address, length, start_idx, end_idx, resolution, BreakpointType.READ) + return reads + + def get_memory_region_writes(self, address, length, resolution=1): + """ + Return a list of timestamps that write to the given memory region. + """ + _, writes = self.get_memory_region_accesses_between(address, length, 0, self.trace.length, resolution, BreakpointType.WRITE) + return writes + + def get_memory_region_writes_between(self, address, length, start_idx, end_idx, resolution=1): + """ + Return a list of timestamps that write to the given memory region in the given time slice. + """ + _, writes = self.get_memory_region_accesses_between(address, length, start_idx, end_idx, resolution, BreakpointType.WRITE) + return writes + def get_memory_region_accesses(self, address, length, resolution=1): """ Return a tuple of (read, write) containing timestamps that access the given memory region. """ return self.get_memory_region_accesses_between(address, length, 0, self.trace.length, resolution) - - def get_memory_region_accesses_between(self, address, length, start_idx, end_idx, resolution=1): + + def get_memory_region_accesses_between(self, address, length, start_idx, end_idx, resolution=1, access_type=BreakpointType.ACCESS): """ Return a tuple of (read, write) containing timestamps that access the given memory region in the given time slice. """ assert resolution > 0 - #print(f"REGION ACCESS BETWEEN @ 0x{address:08X} + {length} // {start_idx:,} --> {end_idx:,} (rez {resolution:0.2f})") + resolution = max(1, resolution) + + #logger.debug(f"REGION ACCESS BETWEEN @ 0x{address:08X} + {length} // {start_idx:,} --> {end_idx:,} (rez {resolution:0.2f})") + reads, writes = [], [] targets = self._region_to_targets(address, length) - resolution = max(1, resolution) - - # clamp the search incase the given params are a bit wonky + # clamp the search incase the given params are a bit wonky idx = max(0, start_idx) end_idx = min(end_idx, self.trace.length) assert idx < end_idx @@ -597,25 +655,27 @@ def get_memory_region_accesses_between(self, address, length, start_idx, end_idx while idx < end_idx: + # fetch a segment to search forward through seg = self.trace.get_segment(idx) seg_base = seg.base_idx + + # clamp the segment end if it extends past our segment seg_end = min(seg_base + seg.length, end_idx) #print("-"*50) #print(f"seg #{seg.id}, {seg.base_idx:,} --> {seg.base_idx+seg.length:,} -- IDX PTR {idx:,}") - - mem_sets = \ - [ - (seg.read_idxs, seg.read_addrs, seg.read_masks, reads), - (seg.write_idxs, seg.write_addrs, seg.write_masks, writes), - ] + + mem_sets = [] + + if access_type & BreakpointType.READ: + mem_sets.append((seg.read_idxs, seg.read_addrs, seg.read_masks, reads)) + if access_type & BreakpointType.WRITE: + mem_sets.append((seg.write_idxs, seg.write_addrs, seg.write_masks, writes)) for i, mem_type in enumerate(mem_sets): idxs, addrs, masks, output = mem_type hits, first_hit = {}, len(addrs) resolution_index = next_resolution[i] - #print("-"*30) - #print("mem", i) # # check each 'aligned address' (actually an id #) within the given region to see @@ -645,10 +705,10 @@ def get_memory_region_accesses_between(self, address, length, start_idx, end_idx # # if we hit this, it means no memory accesses of this - # type (eg, reads) occured to the region of memory in + # type (eg, reads) occured to the region of memory in # this segment. # - # there's nothing else to process for this memory set, + # there's nothing else to process for this memory set, # so just break and move onto the next set (eg, writes) # @@ -681,7 +741,7 @@ def get_memory_region_accesses_between(self, address, length, start_idx, end_idx resolution_index += 1 next_resolution[i] = resolution_index - + idx = seg_end + 1 return (reads, writes) @@ -702,17 +762,18 @@ def get_prev_ips(self, n, step_over=False): output = [] dctx, idx = self.dctx, self.idx - address = self.get_ip(idx) - + trace_address = self.get_ip(idx) + bin_address = self.analysis.rebase_pointer(trace_address) + # (reverse) step over any call instructions - while len(output) < n and idx > -1: - - prev_address = dctx.get_prev_insn(address) + while len(output) < n and idx > 0: + + bin_prev_address = dctx.get_prev_insn(bin_address) did_step_over = False # call instruction - if prev_address != -1 and dctx.is_call_insn(prev_address): - + if bin_prev_address != -1 and dctx.is_call_insn(bin_prev_address): + # get the previous stack pointer address sp = self.get_register(self.arch.SP, idx - 1) @@ -726,18 +787,19 @@ def get_prev_ips(self, n, step_over=False): # # if the address off the stack matches the current address, # we can assume that we just returned from somewhere. - # - # 99% of the time, this will have been from the call insn at - # prev_address, so let's just assume that is the case and + # + # 99% of the time, this will have been from the call insn at + # bin_prev_address, so let's just assume that is the case and # 'reverse step over' onto that. # # NOTE: technically, we can put in more checks and stuff to # try and ensure this is 'correct' but, step over and reverse # step over are kind of an imperfect science as is... - # + # - if maybe_ret_address == address: - prev_idx = self.find_prev_execution(prev_address, idx) + if maybe_ret_address == trace_address: + trace_prev_address = self.analysis.rebase_pointer(bin_prev_address) + prev_idx = self.find_prev_execution(trace_prev_address, idx) did_step_over = bool(prev_idx != -1) # @@ -750,7 +812,8 @@ def get_prev_ips(self, n, step_over=False): # if not did_step_over: - prev_idx = self.find_prev_execution(prev_address, idx) + trace_prev_address = self.analysis.rebase_pointer(bin_prev_address) + prev_idx = self.find_prev_execution(trace_prev_address, idx) # # uh, wow okay we're pretty lost and have no idea if there is @@ -760,16 +823,17 @@ def get_prev_ips(self, n, step_over=False): if prev_idx == -1: prev_idx = idx - 1 - - prev_address = self.get_ip(prev_idx) - + + trace_prev_address = self.get_ip(prev_idx) + # no address was returned, so the end of trace was reached - if prev_address == -1: + if trace_prev_address == -1: break # save the results and continue looping - output.append(prev_address) - address = prev_address + output.append(trace_prev_address) + trace_address = trace_prev_address + bin_address = self.analysis.rebase_pointer(trace_address) idx = prev_idx # return the list of addresses to be 'executed' next @@ -778,7 +842,7 @@ def get_prev_ips(self, n, step_over=False): def get_next_ips(self, n, step_over=False): """ Return the next N executed instruction addresses. - + If step_over=True, and a disassembler context is available to the trace reader, it will attempt to step over calls while stepping. """ @@ -788,28 +852,30 @@ def get_next_ips(self, n, step_over=False): start = min(self.idx + 1, self.trace.length) end = min(start + n, self.trace.length) return [self.get_ip(idx) for idx in range(start, end)] - + output = [] dctx, idx = self.dctx, self.idx - address = self.get_ip(idx) - + trace_address = self.get_ip(idx) + bin_address = self.analysis.rebase_pointer(trace_address) + # step over any call instructions - while len(output) < n and idx < self.trace.length: + while len(output) < n and idx < (self.trace.length - 1): # # get the address for the instruction address after the # current (call) instruction # - next_address = dctx.get_next_insn(address) + bin_next_address = dctx.get_next_insn(bin_address) # # find the next time the instruction after this instruction is # executed in the trace # - if next_address != -1: - next_idx = self.find_next_execution(next_address, idx) + if bin_next_address != -1: + trace_next_address = self.analysis.rebase_pointer(bin_next_address) + next_idx = self.find_next_execution(trace_next_address, idx) else: next_idx = -1 @@ -826,15 +892,15 @@ def get_next_ips(self, n, step_over=False): # our stepping behavior # - next_address = self.get_ip(next_idx) + trace_next_address = self.get_ip(next_idx) # no address was returned, so the end of trace was reached - if next_address == -1: + if trace_next_address == -1: break # save the results and continue looping - output.append(next_address) - address = next_address + output.append(trace_next_address) + bin_address = self.analysis.rebase_pointer(trace_next_address) idx = next_idx # return the list of addresses to be 'executed' next @@ -847,7 +913,7 @@ def find_next_execution(self, address, idx=None): if idx is None: idx = self.idx + 1 - try: + try: mapped_ip = self.trace.get_mapped_ip(address) except ValueError: return -1 @@ -878,7 +944,7 @@ def find_prev_execution(self, address, idx=None): if idx is None: idx = self.idx - 1 - try: + try: mapped_ip = self.trace.get_mapped_ip(address) except ValueError: return -1 @@ -984,12 +1050,12 @@ def _find_next_mem_op(self, address, bp_type, idx=None): break if masks[normal_index] & access_mask: - + assert addrs[normal_index] == mapped_address assert masks[normal_index] & access_mask # ensure that the memory access occurs on or after the starting idx - hit_idx = seg_base + idxs[normal_index] + hit_idx = seg_base + idxs[normal_index] if idx <= hit_idx: accesses.append(seg_base + idxs[normal_index]) break @@ -1056,19 +1122,19 @@ def _find_prev_mem_op(self, address, bp_type, idx=None): break if masks[normal_index] & access_mask: - + assert addrs[normal_index] == mapped_address assert masks[normal_index] & access_mask # ensure that the memory access occurs on or before the starting idx - hit_idx = seg_base + idxs[normal_index] + hit_idx = seg_base + idxs[normal_index] if hit_idx <= idx: accesses.append(seg_base + idxs[normal_index]) break # the hit was no good.. 'step' past it and keep searching search_addrs = search_addrs[reverse_index+1:] - normal_index -= 1 + normal_index -= 1 if accesses: return min(accesses, key=lambda x:abs(x-idx)) @@ -1076,28 +1142,49 @@ def _find_prev_mem_op(self, address, bp_type, idx=None): # fail, reached start of trace return -1 + def find_next_region_read(self, address, length, idx=None): + """ + Return the next timestamp to read from given memory region. + """ + return self._find_next_region_access(address, length, idx, BreakpointType.READ) + + def find_next_region_write(self, address, length, idx=None): + """ + Return the next timestamp to write to the given memory region. + """ + return self._find_next_region_access(address, length, idx, BreakpointType.WRITE) + def find_next_region_access(self, address, length, idx=None): + """ + Return the next timestamp to access (r/w) the given memory region. + """ + return self._find_next_region_access(address, length, idx, BreakpointType.ACCESS) + + def _find_next_region_access(self, address, length, idx=None, access_type=BreakpointType.ACCESS): """ Return the next timestamp to access the given memory region. """ if idx is None: idx = self.idx + 1 - #print(f"FIND NEXT REGION ACCESS FOR 0x{address:08X} -> 0x{address+length:08X} STARTING AT IDX {idx:,}") + #logger.debug(f"FIND NEXT REGION ACCESS FOR 0x{address:08X} -> 0x{address+length:08X} STARTING AT IDX {idx:,}") accesses, mem_sets = [], [] targets = self._region_to_targets(address, length) starting_segment = self.trace.get_segment(idx) for seg_id in range(starting_segment.id, len(self.trace.segments)): + + # fetch a segment to search forward through seg = self.trace.segments[seg_id] seg_base = seg.base_idx - - mem_sets = \ - [ - (seg.read_idxs, seg.read_addrs, seg.read_masks), - (seg.write_idxs, seg.write_addrs, seg.write_masks), - ] + + mem_sets = [] + + if access_type & BreakpointType.READ: + mem_sets.append((seg.read_idxs, seg.read_addrs, seg.read_masks)) + if access_type & BreakpointType.WRITE: + mem_sets.append((seg.write_idxs, seg.write_addrs, seg.write_masks)) # loop through the read / write memory sets for this segment for idxs, addrs, masks in mem_sets: @@ -1128,15 +1215,15 @@ def find_next_region_access(self, address, length, idx=None): except ValueError: continue - + hits[address_id] = address_mask # # if we hit this, it means no memory accesses of this - # type (eg, reads) occured to the region of memory in + # type (eg, reads) occured to the region of memory in # this segment. # - # there's nothing else to process for this memory set, + # there's nothing else to process for this memory set, # so just break and move onto the next set (eg, writes) # @@ -1181,28 +1268,43 @@ def find_next_region_access(self, address, length, idx=None): # fail, reached end of trace return -1 - def find_prev_region_access(self, address, length, idx=None): + def find_prev_region_read(self, address, length, idx=None): + """ + Return the previous timestamp to read from the given memory region. + """ + return self.find_prev_region_access(address, length, idx, BreakpointType.READ) + + def find_prev_region_write(self, address, length, idx=None): """ - Return the previous timestamp to access the given region. + Return the previous timestamp to write to the given memory region. + """ + return self.find_prev_region_access(address, length, idx, BreakpointType.WRITE) + + def find_prev_region_access(self, address, length, idx=None, access_type=BreakpointType.ACCESS): + """ + Return the previous timestamp to access the given memory region. """ if idx is None: idx = self.idx - 1 - #print(f"FIND PREV REGION ACCESS FOR 0x{address:08X} -> 0x{address+length:08X} STARTING AT IDX {idx:,}") + #logger.debug(f"FIND PREV REGION ACCESS FOR 0x{address:08X} -> 0x{address+length:08X} STARTING AT IDX {idx:,}") accesses, mem_sets = [], [] targets = self._region_to_targets(address, length) starting_segment = self.trace.get_segment(idx) for seg_id in range(starting_segment.id, -1, -1): + + # fetch a segment to search backwards through seg = self.trace.segments[seg_id] seg_base = seg.base_idx - - mem_sets = \ - [ - (seg.read_idxs, seg.read_addrs, seg.read_masks), - (seg.write_idxs, seg.write_addrs, seg.write_masks), - ] + + mem_sets = [] + + if access_type & BreakpointType.READ: + mem_sets.append((seg.read_idxs, seg.read_addrs, seg.read_masks)) + if access_type & BreakpointType.WRITE: + mem_sets.append((seg.write_idxs, seg.write_addrs, seg.write_masks)) # loop through the read / write memory sets for this segment for idxs, addrs, masks in mem_sets: @@ -1234,7 +1336,7 @@ def find_prev_region_access(self, address, length, idx=None): except ValueError: continue - + # # ignore hits that are less than the starting timestamp # because we are searching FORWARD, deeper into time @@ -1243,15 +1345,15 @@ def find_prev_region_access(self, address, length, idx=None): #if seg_base + idxs[index] <= idx: # print(f"TOSSING {seg_base+idxs[index]:,}, TOO CLOSE!") # continue - + hits[address_id] = address_mask # # if we hit this, it means no memory accesses of this - # type (eg, reads) occured to the region of memory in + # type (eg, reads) occured to the region of memory in # this segment. # - # there's nothing else to process for this memory set, + # there's nothing else to process for this memory set, # so just break and move onto the next set (eg, writes) # @@ -1311,7 +1413,7 @@ def find_next_register_change(self, reg_name, idx=None): starting_segment = self.trace.get_segment(idx) target_mask_ids = self.trace.get_reg_mask_ids_containing(reg_name) - + # search forward through the remaining segments for seg_id in range(starting_segment.id , len(self.trace.segments)): seg = self.trace.segments[seg_id] @@ -1357,7 +1459,7 @@ def find_prev_register_change(self, reg_name, idx=None): starting_segment = self.trace.get_segment(idx) target_mask_ids = self.trace.get_reg_mask_ids_containing(reg_name) - + # search backwards through the remaining segments for seg_id in range(starting_segment.id, -1, -1): seg = self.trace.segments[seg_id] @@ -1432,7 +1534,7 @@ def _region_to_targets(self, address, length): output.append((mapped_address, access_mask)) - # continue moving through the region + # continue moving through the region length -= ADDRESS_ALIGMENT aligned_address += ADDRESS_ALIGMENT @@ -1456,7 +1558,7 @@ def get_ip(self, idx=None): def get_register(self, reg_name, idx=None): """ Return a single register value. - + If a timestamp (idx) is provided, that will be used instead of the current timestamp. """ return self.get_registers([reg_name], idx)[reg_name] @@ -1479,7 +1581,7 @@ def get_registers(self, reg_names=None, idx=None): # # if the query matches the cached (most recently acces) # - + output_registers, target_registers = {}, reg_names.copy() # sanity checks @@ -1497,7 +1599,7 @@ def get_registers(self, reg_names=None, idx=None): if name in self._cached_registers: output_registers[name] = self._cached_registers[name] target_registers.remove(name) - + # # the trace PC is stored differently, and is tacked on at the end of # the query (if it is requested). we remove it here so we don't search @@ -1525,6 +1627,7 @@ def get_registers(self, reg_names=None, idx=None): current_idx = idx segment = self.trace.get_segment(idx) + while segment: # fetch the registers of interest @@ -1564,7 +1667,7 @@ def get_registers(self, reg_names=None, idx=None): else: self._cached_registers = output_registers - # the timestamp for the cached register set + # the timestamp for the cached register set self._idx_cached_registers = idx # return the register set for this trace index @@ -1573,7 +1676,7 @@ def get_registers(self, reg_names=None, idx=None): def get_memory(self, address, length, idx=None): """ Return the requested memeory. - + If a timestamp (idx) is provided, that will be used instead of the current timestamp. """ if idx is None: @@ -1592,7 +1695,7 @@ def get_memory(self, address, length, idx=None): get_mapped_address = self.trace.get_mapped_address mem_addrs = self.trace.mem_addrs mem_masks = self.trace.mem_masks - + missing_mem = {} for address in aligned_addresses: @@ -1607,7 +1710,7 @@ def get_memory(self, address, length, idx=None): if mapped_address == -1: continue - + # # save the mask for what bytes at the aligned address should # exist in the trace @@ -1619,9 +1722,9 @@ def get_memory(self, address, length, idx=None): missing_mem.pop(-1, None) # - # # - + # + starting_seg = self.trace.get_segment(idx) seg = starting_seg @@ -1684,9 +1787,9 @@ def get_memory(self, address, length, idx=None): for mapped_address, hits in segment_hits.items(): #print(f"PROCESSING HIT {self.trace.mem_addrs[mapped_address]:08X}") - + # - # sort the hits to an aligned address by highest idx (most-recent) + # sort the hits to an aligned address by highest idx (most-recent) # NOTE: mem set id will be the second sort param (writes take precedence) # @@ -1710,8 +1813,7 @@ def get_memory(self, address, length, idx=None): # if this access doesn't contain any new data of interest, ignore it if not missing_mask & current_mask: continue - - # TODO + found_mask = missing_mask & current_mask found_mem = seg.get_mem_data(hit_id, set_id, found_mask) #print(f"FOUND MEM {found_mem} FOUND MASK {found_mask:02X}") @@ -1754,7 +1856,7 @@ def get_memory(self, address, length, idx=None): # skip the current address if it doesn't get touched by this seg if not(mapped_address in mem_delta): continue - + # # fetch the 'value' (1-8 bytes) that this segment sets at the # the current aligned address @@ -1769,7 +1871,7 @@ def get_memory(self, address, length, idx=None): if not (missing_mask & mv.mask): continue - + # # create a mask of the missing bytes, that we can resolve with # the memory value (mv) provided by this snapshot @@ -1827,7 +1929,7 @@ def read_pointer(self, address, idx=None): pack_fmt = 'Q' if self.arch.POINTER_SIZE == 8 else 'I' return struct.unpack(pack_fmt, buffer.data)[0] - + #---------------------------------------------------------------------- # Callbacks #---------------------------------------------------------------------- diff --git a/plugins/tenet/types.py b/plugins/tenet/types.py index 6d89415..23fefed 100644 --- a/plugins/tenet/types.py +++ b/plugins/tenet/types.py @@ -47,13 +47,12 @@ def __init__(self, value, mask, width, item_type): # Breakpoint Types #----------------------------------------------------------------------------- -# TODO: make enum? why isn't this already one? lol lazy? -class BreakpointType: - NONE = 0 - EXEC = 1 - READ = 2 - WRITE = 3 - ACCESS = 4 +class BreakpointType(enum.IntEnum): + NONE = 1 << 0 + READ = 1 << 1 + WRITE = 1 << 2 + EXEC = 1 << 3 + ACCESS = (READ | WRITE) class BreakpointEvent(enum.Enum): ADDED = 0 diff --git a/plugins/tenet/ui/hex_view.py b/plugins/tenet/ui/hex_view.py index 4045bb2..89bfb75 100644 --- a/plugins/tenet/ui/hex_view.py +++ b/plugins/tenet/ui/hex_view.py @@ -3,6 +3,8 @@ from tenet.types import * from tenet.util.qt import * +INVALID_ADDRESS = -1 + class HexView(QtWidgets.QAbstractScrollArea): """ A Qt based hex / memory viewer. @@ -18,40 +20,48 @@ def __init__(self, controller, model, parent=None): self.model = model self._palette = controller.pctx.palette + self.setFocusPolicy(QtCore.Qt.StrongFocus) + self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + font = QtGui.QFont("Courier", pointSize=normalize_font(9)) font.setStyleHint(QtGui.QFont.TypeWriter) self.setFont(font) + self.setMouseTracking(True) fm = QtGui.QFontMetricsF(font) self._char_width = fm.width('9') self._char_height = int(fm.tightBoundingRect('9').height() * 1.75) self._char_descent = self._char_height - fm.descent()*0.75 - self._select_init = -1 - self._select_begin = -1 - self._select_end = -1 - self._region_access = False + self._click_timer = QtCore.QTimer(self) + self._click_timer.setSingleShot(True) + self._click_timer.timeout.connect(self._commit_click) - self.setFocusPolicy(QtCore.Qt.StrongFocus) - self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self._double_click_timer = QtCore.QTimer(self) + self._double_click_timer.setSingleShot(True) + + self.hovered_address = INVALID_ADDRESS + + self._selection_start = INVALID_ADDRESS + self._selection_end = INVALID_ADDRESS + + self._pending_selection_origin = INVALID_ADDRESS + self._pending_selection_start = INVALID_ADDRESS + self._pending_selection_end = INVALID_ADDRESS + + self._ignore_navigation = False self._init_ctx_menu() def _init_ctx_menu(self): """ - TODO + Initialize the right click context menu actions. """ # create actions to show in the context menu self._action_copy = QtWidgets.QAction("Copy", None) - self._action_find_accesses = QtWidgets.QAction("Find accesses", None) - self._action_follow_in_dump = QtWidgets.QAction("Follow in Dump", None) - - # goto action groups - self._action_first = {} - self._action_prev = {} - self._action_next = {} - self._action_final = {} + self._action_clear = QtWidgets.QAction("Clear mem breakpoints", None) + self._action_follow_in_dump = QtWidgets.QAction("Follow in dump", None) bp_types = \ [ @@ -60,6 +70,29 @@ def _init_ctx_menu(self): ("Access", BreakpointType.ACCESS) ] + # + # break on action group + # + + self._action_break = {} + + for name, bp_type in bp_types: + action = QtWidgets.QAction(name, None) + action.setCheckable(True) + self._action_break[action] = bp_type + + self._break_menu = QtWidgets.QMenu("Break on...") + self._break_menu.addActions(self._action_break) + + # + # goto action groups + # + + self._action_first = {} + self._action_prev = {} + self._action_next = {} + self._action_final = {} + for name, bp_type in bp_types: self._action_prev[QtWidgets.QAction(name, None)] = bp_type self._action_next[QtWidgets.QAction(name, None)] = bp_type @@ -81,69 +114,6 @@ def _init_ctx_menu(self): self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.customContextMenuRequested.connect(self._ctx_menu_handler) - def _ctx_menu_handler(self, position): - menu = QtWidgets.QMenu() - - # - # populate the popup menu - # - - # populate these items only if the user has selected one or more bytes - if self.selection_size > 1: - menu.addAction(self._action_copy) - menu.addAction(self._action_find_accesses) - - # populate these items when no bytes are selected - else: - if hasattr(self.controller, "follow_in_dump"): - menu.addAction(self._action_follow_in_dump) - - # add the breakpoint / goto groups - for submenu, _ in self._goto_menus: - menu.addMenu(submenu) - - # - # show the right click context menu - # - - action = menu.exec_(self.mapToGlobal(position)) - - # - # execute the action selected by the suer in the right click menu - # - - if action == self._action_find_accesses: - byte_address = self._select_begin - self._region_access = True - self.controller.focus_region_access(byte_address, self.selection_size) - self.viewport().update() - - elif action == self._action_follow_in_dump: - self.controller.follow_in_dump(self._select_begin) - - elif action == self._action_copy: - self.controller.copy_selection(self._select_begin, self._select_end) - - else: - - # TODO: this is some of the shadiest/laziest code i've ever written - try: - bp_type = getattr(BreakpointType, action.text().upper()) - except: - pass - - address = self._select_begin - length = self.selection_size - - if action in self._action_first: - self.controller.reader.seek_to_first(address, bp_type, length) - elif action in self._action_prev: - self.controller.reader.seek_to_prev(address, bp_type, length) - elif action in self._action_next: - self.controller.reader.seek_to_next(address, bp_type, length) - elif action in self._action_final: - self.controller.reader.seek_to_final(address, bp_type, length) - #------------------------------------------------------------------------- # Properties #------------------------------------------------------------------------- @@ -158,7 +128,7 @@ def num_lines_visible(self): last_line_idx = (first_line_idx + area_size.height() // self._char_height) + 1 lines_visible = last_line_idx - first_line_idx return lines_visible - + @property def num_bytes_visible(self): """ @@ -171,24 +141,39 @@ def selection_size(self): """ Return the number of bytes selected in the hex view. """ - if self._select_end == self._select_begin == -1: + if self._selection_end == self._selection_start == INVALID_ADDRESS: return 0 - return self._select_end - self._select_begin + return self._selection_end - self._selection_start + + @property + def hovered_breakpoint(self): + """ + Return the hovered breakpoint. + """ + if self.hovered_address == INVALID_ADDRESS: + return None + + for bp in self.model.memory_breakpoints: + if bp.address <= self.hovered_address < bp.address + bp.length: + return bp + + return None #------------------------------------------------------------------------- # Internal #------------------------------------------------------------------------- def refresh(self): - self._refresh_view_settings() - #self.refresh_memory() - self.viewport().update() - - def _refresh_display(self): - print("TODO: Recompute / redraw the hex display (but do not fetch new data)") + """ + Refresh the hex view. + """ + self._refresh_painting_metrics() self.viewport().update() - def _refresh_view_settings(self): + def _refresh_painting_metrics(self): + """ + Refresh any metrics and calculations required to paint the widget. + """ # 2 chars per byte of data, eg '00' self._chars_in_line = self.model.num_bytes_per_line * 2 @@ -208,13 +193,16 @@ def _refresh_view_settings(self): self._width_hex = self._chars_in_line * self._char_width # the x position and width of the auxillary region (right section of view) - self._pos_aux = self._pos_hex + self._width_hex + self._pos_aux = self._pos_hex + self._width_hex self._width_aux = (self.model.num_bytes_per_line * self._char_width) + self._char_width * 2 # enforce a minimum view width, to ensure all text stays visible self.setMinimumWidth(self._pos_aux + self._width_aux) def full_size(self): + """ + TODO + """ if not self.model.data: return QtCore.QSize(0, 0) @@ -227,38 +215,481 @@ def full_size(self): return QtCore.QSize(width, height) - def resizeEvent(self, event): - super(HexView, self).resizeEvent(event) - self._refresh_view_settings() - self.controller.set_data_size(self.num_bytes_visible) - #self.model.last_address = self.model.address + self.num_bytes_visible - #if self._reader: - # self.refresh_memory() + def point_to_index(self, position): + """ + Convert a QPoint (x, y) on the hex view window to a byte index. + + TODO/XXX: ugh this whole function / selection logic needs to be + rewritten... it's actually impossible to follow. + """ + padding = self._char_width // 2 + + if position.x() < (self._pos_hex - padding): + return -1 + + cutoff = self._pos_hex + self._width_hex - padding + #print(f"Position: {position} Cutoff: {cutoff} Pos Hex: {self._pos_hex} Width Hex: {self._width_hex} Padding: {padding}") + if position.x() >= cutoff: + return -1 + + # convert 'gloabl' x in the viewport, to an x that is 'relative' to the hex area + hex_x = (position.x() - self._pos_hex) + padding + #print("- Hex x", hex_x) + + # the number of items (eg, bytes, qwords) per line + num_items = self.model.num_bytes_per_line // HEX_TYPE_WIDTH[self.model.hex_format] + #print("- Num items", num_items) + + # compute the pixel width each rendered item on the line takes up + item_width = (self._char_width * 2) * HEX_TYPE_WIDTH[self.model.hex_format] + item_width_padded = item_width + self._char_width + #print("- Item Width", item_width) + #print("- Item Width Padded", item_width_padded) + + # compute the item index on a line (the x-axis) that the point falls within + item_index = int(hex_x // item_width_padded) + #print("- Item Index", item_index) + + # compute which byte is hovered in the item + if self.model.hex_format != HexType.BYTE: + + item_base_x = item_index * item_width_padded + (self._char_width // 2) + item_byte_x = hex_x - item_base_x + item_byte_index = int(item_byte_x // (self._char_width * 2)) + + # XXX: I give up, kludge to account for math errors + if item_byte_index < 0: + item_byte_index = 0 + elif item_byte_index >= self.model.num_bytes_per_line: + item_byte_index = self.model.num_bytes_per_line - 1 + + #print("- Item Byte X", item_byte_x) + #print("- Item Byte Index", item_byte_index) + + item_byte_index = (HEX_TYPE_WIDTH[self.model.hex_format] - 1) - item_byte_index + byte_x = item_index * HEX_TYPE_WIDTH[self.model.hex_format] + item_byte_index + + else: + byte_x = item_index * HEX_TYPE_WIDTH[self.model.hex_format] + + # compute the line number (the y-axis) that the point falls within + byte_y = position.y() // self._char_height + #print("- Byte (X, Y)", byte_x, byte_y) + + # compute the final byte index from the start address in the window + byte_index = (byte_y * self.model.num_bytes_per_line) + byte_x + #print("- Byte Index", byte_index) + + return byte_index + + def point_to_address(self, position): + """ + Convert a QPoint (x, y) on the hex view window to an address. + """ + byte_index = self.point_to_index(position) + if byte_index == -1: + return INVALID_ADDRESS + + byte_address = self.model.address + byte_index + return byte_address + + def point_to_breakpoint(self, position): + """ + Convert a QPoint (x, y) on the hex view window to a breakpoint. + """ + byte_address = self.point_to_address(position) + if byte_address == INVALID_ADDRESS: + return None + + for bp in self.model.memory_breakpoints: + if bp.address <= byte_address < bp.address + bp.length: + return bp + + return None + + def reset_selection(self): + """ + Clear the stored user memory selection. + """ + self._pending_selection_origin = INVALID_ADDRESS + self._pending_selection_start = INVALID_ADDRESS + self._pending_selection_end = INVALID_ADDRESS + self._selection_start = INVALID_ADDRESS + self._selection_end = INVALID_ADDRESS + + def _update_selection(self, position): + """ + Set the user memory selection. + """ + address = self.point_to_address(position) + if address == INVALID_ADDRESS: + return + + if address >= self._pending_selection_origin: + self._pending_selection_end = address + 1 + self._pending_selection_start = self._pending_selection_origin + else: + self._pending_selection_start = address + self._pending_selection_end = self._pending_selection_origin + 1 + + def _commit_click(self): + """ + Accept a click event. + """ + self._selection_start = self._pending_selection_start + self._selection_end = self._pending_selection_end + + self._pending_selection_origin = INVALID_ADDRESS + self._pending_selection_start = INVALID_ADDRESS + self._pending_selection_end = INVALID_ADDRESS + + self.viewport().update() + + def _commit_selection(self): + """ + Accept a selection event. + """ + self._selection_start = self._pending_selection_start + self._selection_end = self._pending_selection_end + + self._pending_selection_origin = INVALID_ADDRESS + self._pending_selection_start = INVALID_ADDRESS + self._pending_selection_end = INVALID_ADDRESS + + # notify listeners of our selection change + #self._notify_selection_changed(new_start, new_end) + self.viewport().update() + + #-------------------------------------------------------------------------- + # Signals + #-------------------------------------------------------------------------- + + def _ctx_menu_handler(self, position): + """ + Handle a right click event (populate/show context menu). + """ + menu = QtWidgets.QMenu() + + ctx_breakpoint = self.point_to_breakpoint(position) + ctx_address = self.point_to_address(position) + ctx_type = BreakpointType.NONE + + # + # determine the selection that the action will execute across + # + + if self._selection_start <= ctx_address < self._selection_end: + selected_address = self._selection_start + selected_length = self.selection_size + + elif ctx_breakpoint: + selected_address = ctx_breakpoint.address + selected_length = ctx_breakpoint.length + ctx_type = ctx_breakpoint.type + + else: + selected_address = INVALID_ADDRESS + selected_length = 0 + + # + # populate the popup menu + # + + # show the 'copy text' option if the user has a region selected + if selected_length > 1 and ctx_type == BreakpointType.NONE: + menu.addAction(self._action_copy) + + # only show the 'follow in dump' if the controller supports it + if hasattr(self.controller, "follow_in_dump"): + menu.addAction(self._action_follow_in_dump) + + menu.addSeparator() + + # show the break option only if there's a selection or breakpoint + if selected_length > 0: + menu.addMenu(self._break_menu) + menu.addSeparator() + + for action, access_type in self._action_break.items(): + action.setChecked(ctx_type == access_type) + + if selected_length > 0: + + # add the goto groups + for submenu, _ in self._goto_menus: + menu.addMenu(submenu) + + # show the 'clear breakpoints' action + menu.addSeparator() + menu.addAction(self._action_clear) + + # + # show the right click context menu + # + + action = menu.exec_(self.mapToGlobal(position)) + if not action: + return + + # + # execute the action selected by the suer in the right click menu + # + + if action == self._action_copy: + self.controller.copy_selection(self._selection_start, self._selection_end) + return + + elif action == self._action_follow_in_dump: + self.controller.follow_in_dump(self._selection_start) + return + + elif action == self._action_clear: + self.controller.pctx.breakpoints.clear_memory_breakpoints() + return + + # TODO: this is some of the shadiest/laziest code i've ever written + try: + selected_type = getattr(BreakpointType, action.text().upper()) + except: + pass + + if action in self._action_first: + self.controller.reader.seek_to_first(selected_address, selected_type, selected_length) + elif action in self._action_prev: + self.controller.reader.seek_to_prev(selected_address, selected_type, selected_length) + elif action in self._action_next: + self.controller.reader.seek_to_next(selected_address, selected_type, selected_length) + elif action in self._action_final: + self.controller.reader.seek_to_final(selected_address, selected_type, selected_length) + elif action in self._action_break: + self.controller.pin_memory(selected_address, selected_type, selected_length) + self.reset_selection() + + #---------------------------------------------------------------------- + # Qt Overloads + #---------------------------------------------------------------------- + + def mouseDoubleClickEvent(self, event): + """ + Qt overload to capture mouse double-click events. + """ + self._click_timer.stop() + + # + # if the double click fell within an active selection, we should + # consume the event as the user setting a region breakpoint + # + + if self._selection_start <= self._pending_selection_start < self._selection_end: + address = self._selection_start + size = self.selection_size + else: + address = self.point_to_address(event.pos()) + size = 1 + + self.controller.pin_memory(address, length=size) + self.reset_selection() + event.accept() + + self.viewport().update() + self._double_click_timer.start(100) + + def mouseMoveEvent(self, event): + """ + Qt overload to capture mouse movement events. + """ + mouse_position = event.pos() + + # update the hovered address + self.hovered_address = self.point_to_address(mouse_position) + + # mouse moving while holding left button + if event.buttons() == QtCore.Qt.MouseButton.LeftButton: + self._update_selection(mouse_position) + + # + # if the user is actively selecting bytes and has selected more + # than one byte, we should clear any existing selection. this will + # make it so the new ongoing 'pending' selection will get drawn + # + + if (self._pending_selection_end - self._pending_selection_start) > 1: + self._selection_start = INVALID_ADDRESS + self._selection_end = INVALID_ADDRESS + + self.viewport().update() + return + + def mousePressEvent(self, event): + """ + Qt overload to capture mouse button presses. + """ + if self._double_click_timer.isActive(): + return + + if event.button() == QtCore.Qt.LeftButton: + + byte_address = self.point_to_address(event.pos()) + + if not(self._selection_start <= byte_address < self._selection_end): + self.reset_selection() + + self._pending_selection_origin = byte_address + self._pending_selection_start = byte_address + self._pending_selection_end = (byte_address + 1) if byte_address != INVALID_ADDRESS else INVALID_ADDRESS + + self.viewport().update() + + def mouseReleaseEvent(self, event): + """ + Qt overload to capture mouse button releases. + """ + if self._double_click_timer.isActive(): + return + + # handle a right click + if event.button() == QtCore.Qt.RightButton: + + # get the address of the byte that was right clicked + byte_address = self.point_to_address(event.pos()) + if byte_address == INVALID_ADDRESS: + return + + # the right clicked fell within the current selection + if self._selection_start <= byte_address < self._selection_end: + return + + # the right click fell within an existing breakpoint + bp = self.hovered_breakpoint + if bp and (bp.address <= byte_address < bp.address + bp.length): + return + + # + # if the right click did not fall within any known selection / poi + # we should consume it and set the current cursor selection to it + # + + self._pending_selection_start = byte_address + self._pending_selection_end = byte_address + 1 + self._commit_click() + return + + if self._pending_selection_origin == INVALID_ADDRESS: + return + + # if the mouse press & release was on a single byte, it's a click + if (self._pending_selection_end - self._pending_selection_start) == 1: + + # + # if the click was within a selected region, defer acting on it + # for 500ms to see if a double click event occurs + # + + if self._selection_start <= self._pending_selection_start < self._selection_end: + self._click_timer.start(200) + return + else: + self._commit_click() + + # a range was selected, so accept/commit it + else: + self._commit_selection() def keyPressEvent(self, e): + """ + Qt overload to capture key press events. + """ if e.key() == QtCore.Qt.Key_G: import ida_kernwin, ida_idaapi address = ida_kernwin.ask_addr(self.model.address, "Jump to address in memory") - if address != ida_idaapi.BADADDR: + if address != None and address != ida_idaapi.BADADDR: self.controller.navigate(address) - e.accept() + e.accept() return super(HexView, self).keyPressEvent(e) + def wheelEvent(self, event): + """ + Qt overload to capture wheel events. + """ + + # + # first, we will attempt special handling of the case where a user + # 'scrolls' up or down when hovering their cursor over a byte they + # have selected... + # + + # compute the address of the hovered byte (if there is one...) + byte_address = self.point_to_address(event.pos()) + + for bp in self.model.memory_breakpoints: + + # skip this breakpoint if the current byte does not fall within its range + if not(bp.address <= byte_address < bp.address + bp.length): + continue + + # + # XXX: bit of a hack, but it seems like the easiest way to prevent + # the stack views from 'navigating' when you're hovering / scrolling + # through memory accesses (see _idx_changed in stack.py) + # + + self._ignore_navigation = True + + # + # if a region is selected with an 'access' breakpoint on it, + # use the start address of the selected region instead for + # the region-based seeks + # + + # scrolled 'up' + if event.angleDelta().y() > 0: + self.controller.reader.seek_to_prev(bp.address, bp.type, bp.length) + + # scrolled 'down' + elif event.angleDelta().y() < 0: + self.controller.reader.seek_to_next(bp.address, bp.type, bp.length) + + # restore navigation listening + self._ignore_navigation = False + + # consume the event + event.accept() + return + + # + # normal 'scroll' on the hex window.. scroll up or down into new + # regions of memory... + # + + if event.angleDelta().y() > 0: + self.controller.navigate(self.model.address - self.model.num_bytes_per_line) + + elif event.angleDelta().y() < 0: + self.controller.navigate(self.model.address + self.model.num_bytes_per_line) + + event.accept() + + def resizeEvent(self, event): + """ + Qt overload to capture resize events for the widget. + """ + super(HexView, self).resizeEvent(event) + self._refresh_painting_metrics() + self.controller.set_data_size(self.num_bytes_visible) + #------------------------------------------------------------------------- # Painting #------------------------------------------------------------------------- def paintEvent(self, event): - #super(HexView, self).paintEvent(event) - + """ + Qt overload of widget painting. + """ if not self.model.data: return painter = QtGui.QPainter(self.viewport()) - area_size = self.viewport().size() - widget_size = self.full_size() - # paint background of entire scroll area painter.fillRect(event.rect(), self._palette.hex_data_bg) @@ -297,7 +728,7 @@ def _paint_line(self, painter, line_idx): address_color = self._palette.hex_address_fg if address < self.model.fade_address: address_color = self._palette.hex_text_faded_fg - + painter.setPen(address_color) # draw the address text @@ -305,11 +736,11 @@ def _paint_line(self, painter, line_idx): address_fmt = '%016X' if pack_len == 8 else '%08X' address_text = address_fmt % address painter.drawText(self._pos_addr, y, address_text) - + self._default_color = self._palette.hex_text_fg if address < self.model.fade_address: self._default_color = self._palette.hex_text_faded_fg - + painter.setPen(self._default_color) byte_base_idx = line_idx * self.model.num_bytes_per_line @@ -338,7 +769,7 @@ def _paint_line(self, painter, line_idx): else: painter.setPen(self._palette.hex_text_faded_fg) - ch = self.model.data[i] + ch = self.model.data[i] if ((ch < 0x20) or (ch > 0x7e)): ch = '.' else: @@ -355,7 +786,7 @@ def _paint_hex_item(self, painter, byte_idx, stop_idx, x, y): # draw single bytes if self.model.hex_format == HexType.BYTE: return self._paint_byte(painter, byte_idx, x, y) - + # draw dwords elif self.model.hex_format == HexType.DWORD: return self._paint_dword(painter, byte_idx, x, y) @@ -414,8 +845,6 @@ def _paint_text(self, painter, byte_idx, padding, x, y): fg_color = self._palette.hex_text_faded_fg text = "??" - byte_address = self.model.address + byte_idx - # # paint text selection background color / highlight # @@ -429,8 +858,35 @@ def _paint_text(self, painter, byte_idx, padding, x, y): bg_color = None border_color = None + # compute the address of the byte we're drawing + byte_address = self.model.address + byte_idx + + # initialize selection start / end vars + start_address = INVALID_ADDRESS + end_address = INVALID_ADDRESS + + # fixed / committed selection + if self._selection_start != INVALID_ADDRESS: + start_address = self._selection_start + end_address = self._selection_end + + # active / on-going selection event + elif self._pending_selection_start != INVALID_ADDRESS: + start_address = self._pending_selection_start + end_address = self._pending_selection_end + + # a byte that falls within the user selection + if start_address <= byte_address < end_address: + bg_color = self._palette.standard_selection_bg + + # set the text color for selected text + if self.model.mask[byte_idx]: + fg_color = self._palette.standard_selection_fg + else: + fg_color = self._palette.standard_selection_faded_fg + # a byte that was written - if byte_address in self.model.delta.mem_writes: + elif byte_address in self.model.delta.mem_writes: bg_color = self._palette.mem_write_bg fg_color = self._palette.mem_write_fg @@ -439,33 +895,59 @@ def _paint_text(self, painter, byte_idx, padding, x, y): bg_color = self._palette.mem_read_bg fg_color = self._palette.mem_read_fg - # a selected byte - if self._select_begin <= byte_address < self._select_end: + # a breakpoint byte + for bp in self.model.memory_breakpoints: - # the selection is a focused, navigation breakpoint - if self.selection_size == 1 or self._region_access: + # skip this breakpoint if the current byte does not fall within its range + if not(bp.address <= byte_address < bp.address + bp.length): + continue - if not bg_color: - bg_color = self._palette.navigation_selection_bg - if self.model.mask[byte_idx]: - fg_color = self._palette.navigation_selection_fg - else: - fg_color = self._palette.navigation_selection_faded_fg + # + # if the breakpoint is a single byte, ensure it will always have a + # border around it, regardless of if it is selected, read, or + # written. + # + # this makes it easy to tell when you have selected or are hovering + # an active 'hot' byte / breakpoint that can be scrolled over to + # seek between accesses + # + if bp.length == 1: border_color = self._palette.navigation_selection_bg - # nothing fancy going on, just standard text selection + # + # if the background color for this byte has already been + # specified, that means a read/write probably occured to it so + # we should prioritize those colors OVER the breakpoint coloring + # + + if bg_color: + break + + # + # if the byte wasn't read/written/selected, we are free to color + # it red, as it falls within an active breakpoint region + # + + bg_color = self._palette.navigation_selection_bg + + # if the byte value is know (versus '??'), set its text color + if self.model.mask[byte_idx]: + fg_color = self._palette.navigation_selection_fg else: - bg_color = self._palette.standard_selection_bg + fg_color = self._palette.navigation_selection_faded_fg + + # + # no need to keep searching through breakpoints once the byte has + # been colored! break and go paint the byte... + # - # set the text color for selected text - if self.model.mask[byte_idx]: - fg_color = self._palette.standard_selection_fg + break # the byte is highlighted in some fashion, paint it now if bg_color: - if border_color and not self._region_access: + if border_color: pen = QtGui.QPen(border_color, 2) pen.setJoinStyle(QtCore.Qt.MiterJoin) painter.setPen(pen) @@ -522,182 +1004,9 @@ def _paint_magic(self, painter, byte_idx, stop_idx, x, y): # if inidividual bytes were printed instead... num_chars = 3 * self.model.pointer_size - # draw the pointer!! - #print("Drawing pointer!!") + # draw the pointer pointer_str = ("0x%08X " % value).rjust(num_chars) painter.drawText(x, y, pointer_str) x += num_chars * self._char_width return (byte_idx + self.model.pointer_size, x, y) - - #------------------------------------------------------------------------- - # - #------------------------------------------------------------------------- - - def mousePressEvent(self, event): - byte_address = self.point_to_address(event.pos()) - #print("Clicked 0x%08X (press event)" % byte_address) - - if event.button() == QtCore.Qt.LeftButton: - if byte_address != -1: - self.reset_selection(byte_address) - else: - self.reset_selection() - - elif event.button() == QtCore.Qt.RightButton: - if self.selection_size <= 1 and byte_address != -1: - self.reset_selection(byte_address) - - self.viewport().update() - - def mouseMoveEvent(self, event): - byte_address = self.point_to_address(event.pos()) - #print("Move 0x%08X" % byte_address) - - self.set_selection(byte_address) - self.viewport().update() - - def mouseReleaseEvent(self, event): - byte_address = self.point_to_address(event.pos()) - #print("Release 0x%08X" % byte_address) - - if self.selection_size == 1: - self.controller.focus_address_access(byte_address) - - self.viewport().update() - - def point_to_index(self, position): - """ - Convert a QPoint (x, y) on the hex view window to a byte index. - """ - padding = self._char_width // 2 - - if position.x() < (self._pos_hex - padding): - return -1 - - if position.x() >= (self._pos_hex + self._width_hex - padding): - return -1 - - # convert 'gloabl' x in the viewport, to an x that is 'relative' to the hex area - hex_x = (position.x() - self._pos_hex) - (self._char_width // 2) - #print("- Hex x", hex_x) - - # the number of items (eg, bytes, qwords) per line - num_items = self.model.num_bytes_per_line // HEX_TYPE_WIDTH[self.model.hex_format] - #print("- Num items", num_items) - - # compute the pixel width each rendered item on the line takes up - item_width = (self._char_width * 2) * HEX_TYPE_WIDTH[self.model.hex_format] - item_width_padded = item_width + self._char_width - #print("- Item Width", item_width) - #print("- Item Width Padded", item_width_padded) - - # compute the item index on a line (the x-axis) that the point falls within - item_index = int(hex_x // item_width_padded) - #print("- Item X", item_index) - - # compute which byte is hovered in the item - item_byte_x = int(hex_x % item_width_padded) - item_byte_index = int(item_byte_x // (self._char_width * 2)) - #print("- Item Byte X", item_byte_x) - #print("- Item Byte Index", item_byte_index) - - if self.model.hex_format != HexType.BYTE: - item_byte_index = HEX_TYPE_WIDTH[self.model.hex_format] - item_byte_index - 1 - - byte_x = item_index * HEX_TYPE_WIDTH[self.model.hex_format] + item_byte_index - - # compute the line number (the y-axis) that the point falls within - byte_y = position.y() // self._char_height - #print("- Byte (X, Y)", byte_x, byte_y) - - # compute the final byte index from the start address in the window - byte_index = (byte_y * self.model.num_bytes_per_line) + byte_x - #print("- Byte Index", byte_index) - - return byte_index - - def point_to_address(self, position): - """ - Convert a QPoint (x, y) on the hex view window to an address. - """ - byte_index = self.point_to_index(position) - if byte_index == -1: - return -1 - - byte_address = self.model.address + byte_index - return byte_address - - def reset_selection(self, address=-1): - self._region_access = False - - if address == -1: - self._select_init = address - self._select_begin = address - self._select_end = address - else: - self._select_init = address - self._select_begin = address - self._select_end = address + 1 - - def set_selection(self, address): - - if address >= self._select_init: - self._select_end = address + 1 - self._select_begin = self._select_init - else: - self._select_begin = address - self._select_end = self._select_init + 1 - - def wheelEvent(self, event): - - # - # first, we will attempt special handling of the case where a user - # 'scrolls' up or down when hovering their cursor over a byte they - # have selected... - # - - # compute the address of the hovered byte (if there is one...) - address = self.point_to_address(event.pos()) - if address != -1: - - #print(f"SCROLLING {self._select_begin:08X} <= {address:08X} <= {self._select_end:08X}") - - # is the hovered byte one that is selected? - if (self._select_begin <= address <= self._select_end): - access_type = BreakpointType.ACCESS - length = self.selection_size - - # - # if a region is selected with an 'access' breakpoint on it, - # use the start address of the selected region instead for - # the region-based seeks - # - - if self.selection_size > 1 and self._region_access: - address = self._select_begin - - # scrolled 'up' - if event.angleDelta().y() > 0: - self.controller.reader.seek_to_prev(address, access_type, length) - - # scrolled 'down' - elif event.angleDelta().y() < 0: - self.controller.reader.seek_to_next(address, access_type, length) - - # consume the event - event.accept() - return - - # - # normal 'scroll' on the hex window.. scroll up or down into new - # regions of memory... - # - - if event.angleDelta().y() > 0: - self.controller.navigate(self.model.address - self.model.num_bytes_per_line) - - elif event.angleDelta().y() < 0: - self.controller.navigate(self.model.address + self.model.num_bytes_per_line) - - event.accept() diff --git a/plugins/tenet/ui/palette.py b/plugins/tenet/ui/palette.py index 32a66cc..7921fb8 100644 --- a/plugins/tenet/ui/palette.py +++ b/plugins/tenet/ui/palette.py @@ -18,7 +18,7 @@ class PluginPalette(object): """ - Color Palette for the plugin. + Theme palette for the plugin. """ def __init__(self): @@ -49,8 +49,8 @@ def __init__(self): # initialize the user theme directory self._populate_user_theme_dir() - # load a placeholder theme (unhinted) for inital Lighthoue bring-up - self._load_preferred_theme(True) + # load a placeholder theme for inital Tenet bring-up + self._load_default_theme() self._initialized = False @staticmethod @@ -100,48 +100,27 @@ def warmup(self): logger.debug("Warming up theme subsystem...") - # - # attempt to load the user's preferred (or hinted) theme. if we are - # successful, then there's nothing else to do! - # - - self._refresh_theme_hints() + # attempt to load the user's preferred theme if self._load_preferred_theme(): self._initialized = True - logger.debug(" - warmup complete, using preferred theme!") + logger.debug(" - warmup complete, using user theme!") return # - # failed to load the preferred theme... so delete the 'active' - # file (if there is one) and warn the user before falling back - # - - try: - os.remove(os.path.join(self.get_user_theme_dir(), ".active_theme")) - except: - pass - - disassembler.warning( - "Failed to load plugin user theme!\n\n" - "Please check the console for more information..." - ) - - # - # if no theme is loaded, we will attempt to detect & load the in-box - # themes based on the user's disassembler theme + # if no user selected theme is loaded, we will attempt to detect + # and load the in-box themes based on the disassembler theme # - loaded = self._load_preferred_theme(fallback=True) - if not loaded: - pmsg("Could not load plugin fallback theme!") # this is a bad place to be... + if self._load_hinted_theme(): + logger.debug(" - warmup complete, using hint-recommended theme!") + self._initialized = True return - logger.debug(" - warmup complete, using hint-recommended theme!") - self._initialized = True + pmsg("Could not warmup theme subsystem!") def interactive_change_theme(self): """ - Open a file dialog and let the user select a new Lighthoue theme. + Open a file dialog and let the user select a new plugin theme. """ # create & configure a Qt File Dialog for immediate use @@ -182,7 +161,7 @@ def interactive_change_theme(self): logger.debug("Captured filename from theme file dialog: '%s'" % filename) # - # before applying the selected lighthouse theme, we should ensure that + # before applying the selected plugin theme, we should ensure that # we know if the user is using a light or dark disassembler theme as # it may change which colors get used by the plugin theme # @@ -208,8 +187,11 @@ def refresh_theme(self): Depending on if the disassembler is using a dark or light theme, we *try* to select colors that will hopefully keep things most readable. """ - self._refresh_theme_hints() - self._load_preferred_theme() + if self._load_preferred_theme(): + return + if self._load_hinted_theme(): + return + pmsg("Failed to refresh theme!") def gen_arrow_icon(self, color, rotation): """ @@ -300,9 +282,50 @@ def _load_required_fields(self): self._required_fields = theme["fields"].keys() - def _load_preferred_theme(self, fallback=False): + def _load_default_theme(self): + """ + Load the default theme without any sort of hinting. + """ + theme_name = self._default_themes["dark"] + theme_path = os.path.join(self.get_plugin_theme_dir(), theme_name) + return self._load_theme(theme_path) + + def _load_hinted_theme(self): """ - Load the user's preferred theme, or the one hinted at by the theme subsystem. + Load the in-box plugin theme hinted at by the theme subsystem. + """ + self._refresh_theme_hints() + + # + # we have two themes hints which roughly correspond to the tone of + # the user's disassembly background, and then the Qt subsystem. + # + # if both themes seem to align on style (eg the user is using a + # 'dark' UI), then we will select the appropriate in-box theme + # + + if self._user_qt_hint == self._user_disassembly_hint: + theme_name = self._default_themes[self._user_qt_hint] + logger.debug(" - No preferred theme, hints suggest theme '%s'" % theme_name) + + # + # the UI hints don't match, so the user is using some ... weird + # mismatched theming in their disassembler. let's just default to + # the 'dark' plugin theme as it is more robust + # + + else: + theme_name = self._default_themes["dark"] + + # build the filepath to the hinted, in-box theme + theme_path = os.path.join(self.get_plugin_theme_dir(), theme_name) + + # attempt to load and return the result of loading an in-box theme + return self._load_theme(theme_path) + + def _load_preferred_theme(self): + """ + Load the user's saved, preferred theme. """ logger.debug("Loading preferred theme from disk...") user_theme_dir = self.get_user_theme_dir() @@ -313,49 +336,31 @@ def _load_preferred_theme(self, fallback=False): theme_name = open(active_filepath).read().strip() logger.debug(" - Got '%s' from .active_theme" % theme_name) except (OSError, IOError): - theme_name = None - - # - # if the user does not have a preferred theme set yet, we will try to - # pick one for them based on their disassembler UI. - # - - if not theme_name: - - # - # we have two themes hints which roughly correspond to the tone of - # their disassembly background, and then their general Qt widgets. - # - # if both themes seem to align on style (eg the user is using a - # 'dark' UI), then we will select the appropriate in-box theme - # - - if self._user_qt_hint == self._user_disassembly_hint: - theme_name = self._default_themes[self._user_qt_hint] - logger.debug(" - No preferred theme, hints suggest theme '%s'" % theme_name) + return False - # - # the UI hints don't match, so the user is using some ... weird - # mismatched theming in their disassembler. let's just default to - # the 'dark' plugin theme as it is more robust - # + # build the filepath to the user defined theme + theme_path = os.path.join(self.get_user_theme_dir(), theme_name) - else: - theme_name = self._default_themes["dark"] + # finally, attempt to load & apply the theme -- return True/False + if self._load_theme(theme_path): + return True # - # should the user themes be in a bad state, we can fallback to the - # in-box themes. this should only happen if users malform the default - # themes that have been copied into the user theme directory + # failed to load the preferred theme... so delete the 'active' + # file (if there is one) and warn the user before falling back # - if fallback: - theme_path = os.path.join(self.get_plugin_theme_dir(), theme_name) - else: - theme_path = os.path.join(self.get_user_theme_dir(), theme_name) + try: + os.remove(os.path.join(self.get_user_theme_dir(), ".active_theme")) + except: + pass - # finally, attempt to load & apply the theme -- return True/False - return self._load_theme(theme_path) + disassembler.warning( + "Failed to load plugin user theme!\n\n" + "Please check the console for more information..." + ) + + return False def _validate_theme(self, theme): """ @@ -398,6 +403,7 @@ def _load_theme(self, filepath): # do some basic sanity checking on the given theme file if not self._validate_theme(theme): + pmsg("Failed to validate theme '%s'" % filepath) return False # try applying the loaded theme to the plugin diff --git a/plugins/tenet/ui/reg_view.py b/plugins/tenet/ui/reg_view.py index 9e15c33..6f5ce2d 100644 --- a/plugins/tenet/ui/reg_view.py +++ b/plugins/tenet/ui/reg_view.py @@ -8,6 +8,7 @@ class RegisterView(QtWidgets.QWidget): """ A container for the the widgets that make up the Registers view. """ + def __init__(self, controller, model, parent=None): super(RegisterView, self).__init__(parent) self.controller = controller @@ -78,8 +79,7 @@ def _init_ui(self): self.returnPressed.connect(self._evaluate) def _evaluate(self): - self.controller.navigate_to_expression(self.text()) - return + self.controller.evaluate_expression(self.text()) class RegisterArea(QtWidgets.QAbstractScrollArea): """ @@ -90,7 +90,7 @@ def __init__(self, controller, model, parent=None): self.pctx = controller.pctx self.controller = controller self.model = model - + font = QtGui.QFont("Courier", pointSize=normalize_font(9)) font.setStyleHint(QtGui.QFont.TypeWriter) self.setFont(font) @@ -124,13 +124,14 @@ def sizeHint(self): def _init_ctx_menu(self): """ - TODO + Initialize the right click context menu actions. """ # create actions to show in the context menu self._action_copy_value = QtWidgets.QAction("Copy value", None) self._action_follow_in_dump = QtWidgets.QAction("Follow in dump", None) self._action_follow_in_disassembly = QtWidgets.QAction("Follow in disassembler", None) + self._action_clear = QtWidgets.QAction("Clear code breakpoints", None) # install the right click context menu self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) @@ -187,38 +188,47 @@ def _init_reg_positions(self): y += self._char_height def _ctx_menu_handler(self, position): + """ + Handle a right click event (populate/show context menu). + """ + menu = QtWidgets.QMenu() - # if no register was right clicked, there's no reason to show a menu + # if a register was right clicked, fetch its name reg_name = self._pos_to_reg(position) - if not reg_name: - return + if reg_name: - # - # fetch the disassembler context and register value as we may use them - # based on the user's context, or the action they select - # + # + # fetch the disassembler context and register value as we may use them + # based on the user's context, or the action they select + # - dctx = disassembler[self.controller.pctx] - reg_value = self.model.registers[reg_name] + dctx = disassembler[self.controller.pctx] + reg_value = self.model.registers[reg_name] - # - # dynamically populate the right click context menu - # + # + # dynamically populate the right click context menu + # - menu = QtWidgets.QMenu() - menu.addAction(self._action_copy_value) - menu.addAction(self._action_follow_in_dump) + menu.addAction(self._action_copy_value) + menu.addAction(self._action_follow_in_dump) + + # + # if the register conatins a value that falls within the database, + # we want to show it and ensure it's active + # + + menu.addAction(self._action_follow_in_disassembly) + if dctx.is_mapped(reg_value): + self._action_follow_in_disassembly.setEnabled(True) + else: + self._action_follow_in_disassembly.setEnabled(False) # - # if the register conatins a value that falls within the database, - # we want to show it and ensure it's active + # add a menu option to clear exection breakpoints if there is an + # active execution breakpoint set somewhere # - menu.addAction(self._action_follow_in_disassembly) - if dctx.is_mapped(reg_value): - self._action_follow_in_disassembly.setEnabled(True) - else: - self._action_follow_in_disassembly.setEnabled(False) + menu.addAction(self._action_clear) # # show the right click menu and wait for the user to selection an @@ -237,6 +247,8 @@ def _ctx_menu_handler(self, position): dctx.navigate(reg_value) elif action == self._action_follow_in_dump: self.controller.follow_in_dump(reg_name) + elif action == self._action_clear: + self.pctx.breakpoints.clear_execution_breakpoints() def refresh(self): self.viewport().update() @@ -268,61 +280,125 @@ def full_size(self): return QtCore.QSize(width, height) def wheelEvent(self, event): + """ + Qt overload to capture wheel events. + """ - target = self.pctx.breakpoints.model.focused_breakpoint - if not (target and target.type == BreakpointType.EXEC): + # no execution breakpoints set, nothing to do + if not self.pctx.breakpoints.model.bp_exec: return - pos = event.pos() - field = self._pos_to_field(pos) - + # mouse hover was not over IP register value, nothing to do + field = self._pos_to_field(event.pos()) if not (field and field.name == self.model.arch.IP): return - if target.address != self.model.registers[self.model.arch.IP]: + # get the IP value currently displayed in the reg window + current_ip = self.model.registers[self.model.arch.IP] + breakpoints = self.pctx.breakpoints.model.bp_exec + + # loop through the execution-based breakpoints + for breakpoint_address in breakpoints: + if breakpoint_address == current_ip: + break + + # no execution breakpoints match the hovered IP + else: return # scroll up if event.angleDelta().y() > 0: - self.pctx.reader.seek_to_prev(target.address, target.type) + self.pctx.reader.seek_to_prev(current_ip, BreakpointType.EXEC) # scroll down elif event.angleDelta().y() < 0: - self.pctx.reader.seek_to_next(target.address, target.type) + self.pctx.reader.seek_to_next(current_ip, BreakpointType.EXEC) return - def mousePressEvent(self, event): + def mouseMoveEvent(self, e): + """ + Qt overload to capture mouse movement events. + """ + point = e.pos() + before = self._hovered_arrow - # save the position of this click / right click - pos = event.pos() + for reg_name, reg_field in self._reg_fields.items(): + if reg_field.next_rect.contains(point): + self._hovered_arrow = reg_field.next_rect + break + elif reg_field.prev_rect.contains(point): + self._hovered_arrow = reg_field.prev_rect + break + else: + self._hovered_arrow = None + + if before != self._hovered_arrow: + self.viewport().update() + + def mouseDoubleClickEvent(self, event): + """ + Qt overload to capture mouse double-click events. + """ + mouse_position = event.pos() + + # handle duoble (left) click events + if event.button() == QtCore.Qt.LeftButton: + + # confirm that we are consuming the double click event + event.accept() + + # check if the user clicked a known field + field = self._pos_to_field(mouse_position) + + # if the double click was *not* on a register field, clear execution breakpoints + if not field: + self.pctx.breakpoints.clear_execution_breakpoints() + return + + # ignore if the double clicked field (register) was not the IP reg + if not (field and field.name == self.model.arch.IP): + return + + # ignore if the double click was not on the reg value + if not field.value_rect.contains(mouse_position): + return + + # the user double clicked IP, so set a breakpoint on it + self.controller.set_ip_breakpoint() + + def mousePressEvent(self, event): + """ + Qt overload to capture mouse button presses. + """ + mouse_position = event.pos() # handle click events if event.button() == QtCore.Qt.LeftButton: # check if the user clicked a known field - field = self._pos_to_field(pos) + field = self._pos_to_field(mouse_position) # no field (register name, or register value) was selected if not field: self.controller.clear_register_focus() # the user clicked on the register value - elif field.value_rect.contains(pos): + elif field.value_rect.contains(mouse_position): self.controller.focus_register_value(field.name) # the user clicked on the 'seek to next reg change' arrow - elif field.next_rect.contains(pos): + elif field.next_rect.contains(mouse_position): result = self.pctx.reader.find_next_register_change(field.name) if result != -1: self.pctx.reader.seek(result) # the user clicked on the 'seek to prev reg change' arrow - elif field.prev_rect.contains(pos): + elif field.prev_rect.contains(mouse_position): result = self.pctx.reader.find_prev_register_change(field.name) if result != -1: self.pctx.reader.seek(result) - + # the user clicked on the register name else: self.controller.focus_register_name(field.name) @@ -330,26 +406,11 @@ def mousePressEvent(self, event): # update the view as selection / drawing may change self.viewport().update() - def mouseMoveEvent(self, e): - #print("HOVERING!", e.pos()) - - point = e.pos() - before = self._hovered_arrow - - for reg_name, reg_field in self._reg_fields.items(): - if reg_field.next_rect.contains(point): - self._hovered_arrow = reg_field.next_rect - break - elif reg_field.prev_rect.contains(point): - self._hovered_arrow = reg_field.prev_rect - break - else: - self._hovered_arrow = None - - if before != self._hovered_arrow: - self.viewport().update() - def paintEvent(self, event): + """ + Qt overload of widget painting. + """ + if not self.model.registers: return @@ -358,7 +419,7 @@ def paintEvent(self, event): area_size = self.viewport().size() area_rect = self.viewport().rect() widget_size = self.full_size() - + painter.fillRect(area_rect, self.pctx.palette.reg_bg) brush_defualt = painter.brush() @@ -373,7 +434,7 @@ def paintEvent(self, event): painter.setBackground(brush_selected) painter.setBackgroundMode(QtCore.Qt.OpaqueMode) painter.setPen(self.pctx.palette.standard_selection_fg) - + # default / unselected register colors else: painter.setBackground(brush_defualt) @@ -389,30 +450,36 @@ def paintEvent(self, event): else: rendered_value = f'%0{reg_nibbles}X' % reg_value - if reg_name in self.model.delta: - painter.setPen(self.pctx.palette.reg_changed_fg) + # color register if its value changed as a result of T-1 (previous instr) + if reg_name in self.model.delta_trace: + painter.setPen(self.pctx.palette.reg_changed_trace_fg) + + # color register if its value changed as a result of navigation + # TODO: disabled for now, because it seemed more confusing than helpful... + elif reg_name in self.model.delta_navigation and False: + painter.setPen(self.pctx.palette.reg_changed_navigation_fg) + + # no special highlighting, default register value color text else: painter.setPen(self.pctx.palette.reg_value_fg) - + # coloring for when the register is selected by the user if reg_name == self.model.focused_reg_value: + painter.setPen(self.pctx.palette.standard_selection_fg) + painter.setBackground(brush_selected) + painter.setBackgroundMode(QtCore.Qt.OpaqueMode) - # special highlighting when the instruction pointer is selected - if reg_name == self.model.arch.IP: - painter.setPen(self.pctx.palette.navigation_selection_fg) - painter.setBackground(self.pctx.palette.navigation_selection_bg) - - # normal highlighting - else: - painter.setPen(self.pctx.palette.standard_selection_fg) - painter.setBackground(brush_selected) - painter.setBackgroundMode(QtCore.Qt.OpaqueMode) - # default / unselected register colors else: painter.setBackground(brush_defualt) painter.setBackgroundMode(QtCore.Qt.OpaqueMode) + # special highlighting of the instruction pointer if it matches an active breakpoint + if reg_name == self.model.arch.IP: + if reg_value in self.model.execution_breakpoints: + painter.setPen(self.pctx.palette.navigation_selection_fg) + painter.setBackground(self.pctx.palette.navigation_selection_bg) + # draw register value painter.drawText(reg_field.value_rect, QtCore.Qt.AlignCenter, rendered_value) @@ -426,28 +493,25 @@ def paintEvent(self, event): def _draw_arrow(self, painter, rect, index): path = QtGui.QPainterPath() - + size = rect.height() assert size % 2, "Cursor triangle size must be odd" # the top point of the triangle top_x = rect.x() + (0 if index else rect.width()) top_y = rect.y() + 1 - #print("TOP", top_x, top_y) - + # bottom point of the triangle bottom_x = top_x bottom_y = top_y + size - 1 - #print("BOT", bottom_x, bottom_y) # the 'tip' of the triangle pointing into towards the center of the trace tip_x = top_x + ((size // 2) * (1 if index else -1)) tip_y = top_y + (size // 2) - #print("CURSOR", tip_x, tip_y) # start drawing from the 'top' of the triangle path.moveTo(top_x, top_y) - + # generate the triangle path / shape path.lineTo(bottom_x, bottom_y) path.lineTo(tip_x, tip_y) @@ -469,9 +533,9 @@ def _draw_arrow(self, painter, rect, index): painter.setBrush(self.pctx.palette.arrow_prev) else: painter.setBrush(self.pctx.palette.arrow_idle) - + painter.drawPath(path) - + class RegisterField(object): def __init__(self, name, name_rect, value_rect, arrow_rects): self.name = name diff --git a/plugins/tenet/ui/resources/themes/horizon.json b/plugins/tenet/ui/resources/themes/horizon.json index 97ad9a0..2f5eed1 100644 --- a/plugins/tenet/ui/resources/themes/horizon.json +++ b/plugins/tenet/ui/resources/themes/horizon.json @@ -7,28 +7,30 @@ "white": [255, 255, 255], "lightest_gray": [241, 241, 241], + "lighter_gray": [210, 210, 210], "light_gray": [160, 160, 160], "gray": [ 80, 80, 80], "dark_gray": [ 40, 40, 40], "darkest_gray": [ 25, 25, 25], "true_red": [255, 0, 0], + "lightest_red": [255, 80, 80], "light_red": [170, 57, 57], "red": [118, 0, 0], "dark_red": [ 85, 0, 0], - + + "orange": [255, 165, 0], + "true_yellow": [255, 255, 0], "yellow": [255, 193, 7], "true_green": [ 0, 255, 0], - "green2": [128, 255, 128], - "red2": [255, 80, 80], + "light_green": [128, 255, 128], "purple": [150, 20, 150], "dark_purple": [ 38, 23, 88], - "light_blue": [192, 187, 175], - "light_blue2": [ 33, 159, 255], + "light_blue": [ 33, 159, 255], "blue": [ 30, 136, 229], "dark_blue": [ 58, 58, 128] @@ -36,19 +38,20 @@ "fields": { - "navigation_selection_bg": "red2", - "navigation_selection_fg": "black", - "navigation_selection_faded_fg": "dark_red", + "navigation_selection_bg": "lightest_red", + "navigation_selection_fg": "white", + "navigation_selection_faded_fg": "light_red", + + "standard_selection_bg": "lighter_gray", + "standard_selection_fg": "black", + "standard_selection_faded_fg": "light_gray", - "standard_selection_bg": "light_blue", - "standard_selection_fg": "white", - "standard_selection_faded_fg": "lightest_gray", + "reg_bg": "white", - "reg_bg": "white", - "reg_name_fg": "black", - "reg_value_fg": "black", - "reg_changed_fg": "true_red", - "reg_selected_bg": "light_blue", + "reg_name_fg": "black", + "reg_value_fg": "black", + "reg_changed_trace_fg": "true_red", + "reg_changed_navigation_fg": "orange", "hex_text_fg": "black", "hex_text_faded_fg": "light_gray", @@ -59,27 +62,33 @@ "hex_data_bg": "white", "hex_separator": "black", - "trace_bedrock": "darkest_gray", + "trace_bedrock": "dark_gray", + "trace_unmapped": "light_gray", "trace_instruction": "dark_blue", "trace_border": "gray", - "trace_cursor": "true_green", + "trace_cell_wall": "light_gray", + "trace_cell_wall_contrast": "black", + + "trace_cursor": "true_red", + "trace_cursor_border": "black", + "trace_cursor_highlight": "true_green", + "trace_selection": "true_green", "trace_selection_border": "true_green", - "breakpoint": "red2", + "breakpoint": "lightest_red", "mem_read_bg": "yellow", "mem_read_fg": "black", - "mem_write_bg": "light_blue2", + "mem_write_bg": "light_blue", "mem_write_fg": "white", - "trail_backward": "red2", - "trail_current": "green2", - "trail_forward": "light_blue2", + "trail_backward": "lightest_red", + "trail_current": "light_green", + "trail_forward": "light_blue", - "arrow_prev": "red2", - "arrow_next": "light_blue2", + "arrow_prev": "lightest_red", + "arrow_next": "light_blue", "arrow_idle": "light_gray" } -} - +} \ No newline at end of file diff --git a/plugins/tenet/ui/resources/themes/synth.json b/plugins/tenet/ui/resources/themes/synth.json index 75e77bb..218dabb 100644 --- a/plugins/tenet/ui/resources/themes/synth.json +++ b/plugins/tenet/ui/resources/themes/synth.json @@ -8,7 +8,8 @@ "lightest_gray": [221, 221, 221], "light_gray": [160, 160, 160], - "gray": [ 80, 80, 80], + "gray": [ 90, 90, 90], + "medium_gray": [ 60, 60, 60], "dark_gray": [ 40, 40, 40], "darkest_gray": [ 25, 25, 25], @@ -17,6 +18,8 @@ "red": [118, 0, 0], "dark_red": [ 85, 0, 0], + "orange": [255, 165, 0], + "true_yellow": [255, 255, 0], "yellow": [212, 194, 106], @@ -39,12 +42,14 @@ "standard_selection_bg": "light_blue", "standard_selection_fg": "black", - "standard_selection_faded_fg": "lightest_gray", + "standard_selection_faded_fg": "gray", + + "reg_bg": "darkest_gray", - "reg_bg": "darkest_gray", - "reg_name_fg": "lightest_gray", - "reg_value_fg": "lightest_gray", - "reg_changed_fg": "true_red", + "reg_name_fg": "lightest_gray", + "reg_value_fg": "lightest_gray", + "reg_changed_trace_fg": "true_red", + "reg_changed_navigation_fg": "orange", "hex_text_fg": "lightest_gray", "hex_text_faded_fg": "gray", @@ -56,13 +61,20 @@ "hex_separator": "black", "trace_bedrock": "darkest_gray", + "trace_unmapped": "medium_gray", "trace_instruction": "dark_purple", "trace_border": "gray", - "trace_cursor": "true_green", + "trace_cell_wall": "medium_gray", + "trace_cell_wall_contrast": "black", + + "trace_cursor": "true_red", + "trace_cursor_border": "black", + "trace_cursor_highlight": "true_green", + "trace_selection": "true_green", "trace_selection_border": "true_green", - + "breakpoint": "light_red", "mem_read_bg": "yellow", "mem_read_fg": "black", diff --git a/plugins/tenet/ui/trace_view.py b/plugins/tenet/ui/trace_view.py index 1a56ff3..7f3644f 100644 --- a/plugins/tenet/ui/trace_view.py +++ b/plugins/tenet/ui/trace_view.py @@ -1,7 +1,15 @@ +import logging + +from tenet.util.qt import * +from tenet.util.misc import register_callback, notify_callback +from tenet.integration.api import disassembler + +logger = logging.getLogger("Tenet.UI.TraceView") + # # TODO: BIG DISCLAIMER -- The trace visualization / window does *not* make # use of the MVC pattern that the other widgets do. -# +# # this is mainly due to the fact that it was prototyped last, and I haven't # gotten around to moving the 'logic' out of window/widget classes and into # a dedicated controller class. @@ -9,72 +17,87 @@ # this will probably happen sooner than later, to keep everything consistent # -from tenet.types import BreakpointType -from tenet.util.qt import * -from tenet.util.misc import register_callback, notify_callback - #------------------------------------------------------------------------------ # TraceView #------------------------------------------------------------------------------ -# TODO/XXX: ugly -BORDER_SIZE = 1 -LOCKON_DISTANCE = 4 +INVALID_POS = -1 +INVALID_IDX = -1 +INVALID_DENSITY = -1 class TraceBar(QtWidgets.QWidget): """ A trace visualization. """ - def __init__(self, core, zoom=False, parent=None): + def __init__(self, pctx, zoom=False, parent=None): super(TraceBar, self).__init__(parent) - self.core = core + self.pctx = pctx + self.reader = None self._is_zoom = zoom - # misc widget settings + # misc qt/widget settings self.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) self.setMouseTracking(True) self.setMinimumSize(32, 32) + self._resize_timer = QtCore.QTimer(self) + self._resize_timer.setSingleShot(True) + self._resize_timer.timeout.connect(self._resize_stopped) - # the rendered trace visualization - self._image = QtGui.QImage() + # the first and last visible idx in this visualization + self.start_idx = 0 + self.end_idx = 0 + self._end_idx_internal = 0 + self._last_trace_idx = 0 - # - # setup trace colors / pens / brushes - # + # the 'uncommitted' / in-progress user selection of a trace region + self._idx_pending_selection_origin = INVALID_IDX + self._idx_pending_selection_start = INVALID_IDX + self._idx_pending_selection_end = INVALID_IDX - # r / w / x accesses - self.color_read = self.core.palette.mem_read_bg - self.color_write = self.core.palette.mem_write_bg - self.color_exec = self.core.palette.breakpoint + # the committed user selection of a trace region + self._idx_selection_start = INVALID_IDX + self._idx_selection_end = INVALID_IDX - # current idx - self.color_cursor = self.core.palette.trace_cursor - self.cursor_pen = QtGui.QPen(self.color_cursor, 1, QtCore.Qt.SolidLine) + # the idxs that should be highlighted based on user queries + self._idx_reads = [] + self._idx_writes = [] + self._idx_executions = [] - # zoom / region selection - self.color_selection = self.core.palette.trace_selection - self.color_selection_border = self.core.palette.trace_selection_border - self.pen_selection = QtGui.QPen(self.color_selection, 2, QtCore.Qt.SolidLine) - self.brush_selection = QtGui.QBrush(QtCore.Qt.Dense6Pattern) - self.brush_selection.setColor(self.color_selection_border) + # the magnetism distance (in pixels) for cursor clicks on viz events + self._magnetism_distance = 4 + self._hovered_idx = INVALID_IDX - self._last_hovered = None + # listen for breakpoint changed events + pctx.breakpoints.model.breakpoints_changed(self._breakpoints_changed) - self.start_idx = 0 - self.end_idx = 0 - self.density = 0 + #---------------------------------------------------------------------- + # Styling + #---------------------------------------------------------------------- + + # the width (in pixels) of the border around the trace bar + self._trace_border = 1 + + # the width (in pixels) of the border around trace cells + self._cell_border = 0 # computed dynamically + self._cell_min_border = 1 + self._cell_max_border = 1 + + # the height (in pixels) of the trace cells + self._cell_height = 0 # computed dynamically + self._cell_min_height = 2 + self._cell_max_height = 10 - self._width = 0 - self._height = 0 + # the amount of space between cells (in pixels) + # - NOTE: no limit to cell spacing at max magnification! + self._cell_spacing = 0 # computed dynamically + self._cell_min_spacing = self._cell_min_border - self._selection_origin = -1 - self._selection_start = -1 - self._selection_end = -1 + # the width (in pixels) of the border around user region selection + self._selection_border = 2 - self._executions = [] - self._reads = [] - self._writes = [] + # create the rest of the painting vars + self._init_painting() #---------------------------------------------------------------------- # Callbacks @@ -82,511 +105,1102 @@ def __init__(self, core, zoom=False, parent=None): self._selection_changed_callbacks = [] - def _focused_breakpoint_changed(self, breakpoint): + def _init_painting(self): """ - The focused breakpoint has changed. + Initialize widget/trace painting elements. """ - self._refresh_breakpoint_hits(breakpoint) - self.refresh() + self._image_base = None + self._image_highlights = None + self._image_selection = None + self._image_border = None + self._image_cursor = None + self._image_final = None + + self._painter_base = None + self._painter_highlights = None + self._painter_selection = None + self._painter_border = None + self._painter_cursor = None + self._painter_final = None + + self._pen_cursor = QtGui.QPen(self.pctx.palette.trace_cursor_highlight, 1, QtCore.Qt.SolidLine) + + self._pen_selection = QtGui.QPen(self.pctx.palette.trace_selection, self._selection_border, QtCore.Qt.SolidLine) + self._brush_selection = QtGui.QBrush(QtCore.Qt.Dense6Pattern) + self._brush_selection.setColor(self.pctx.palette.trace_selection_border) + + self._last_hovered = INVALID_IDX + + #------------------------------------------------------------------------- + # Properties + #------------------------------------------------------------------------- + + @property + def length(self): + """ + Return the number of idx visible in the trace visualization. + """ + return (self.end_idx - self.start_idx) - def _refresh_breakpoint_hits(self, breakpoint): - self._executions = [] - self._reads = [] - self._writes = [] - - if not self.isVisible(): - return + @property + def cells_visible(self): + """ + Return True if the trace visualization is drawing as cells. + """ + return bool(self._cell_height) - if not (self.core.reader and breakpoint): - return + @property + def density(self): + """ + Return the density of idx (instructions) per y-pixel of the trace visualization. + """ + density = (self.length / (self.height() - self._trace_border * 2)) + if density > 0: + return density + return INVALID_DENSITY - if breakpoint.type == BreakpointType.EXEC: - - self._executions = self.core.reader.get_executions_between(breakpoint.address, self.start_idx, self.end_idx, self.density) + @property + def viz_rect(self): + """ + Return a QRect defining the drawable trace visualization. + """ + x, y = self.viz_pos + w, h = self.viz_size + return QtCore.QRect(x, y, w, h) - elif breakpoint.type == BreakpointType.ACCESS: + @property + def viz_pos(self): + """ + Return (x, y) coordinates of the drawable trace visualization. + """ + return (self._trace_border, self._trace_border) - if breakpoint.length == 1: - self._reads, self._writes = self.core.reader.get_memory_accesses_between(breakpoint.address, self.start_idx, self.end_idx, self.density) - else: - self._reads, self._writes = self.core.reader.get_memory_region_accesses_between(breakpoint.address, breakpoint.length, self.start_idx, self.end_idx, self.density) + @property + def viz_size(self): + """ + Return (width, height) of the drawable trace visualization. + """ + w = max(0, int(self.width() - (self._trace_border * 2))) + h = max(0, int(self.height() - (self._trace_border * 2))) + return (w, h) - else: - raise NotImplementedError + #------------------------------------------------------------------------- + # Public + #------------------------------------------------------------------------- def attach_reader(self, reader): - - # clear out any existing state + """ + Attach a trace reader to this controller. + """ self.reset() - # save the reader + # attach the new reader self.reader = reader # initialize state based on the reader - self.set_zoom(0, reader.trace.length) + self.set_bounds(0, reader.trace.length) # attach signals to the new reader reader.idx_changed(self.refresh) + def set_bounds(self, start_idx, end_idx): + """ + Set the idx bounds of the trace visualization. + """ + assert end_idx > start_idx, f"Invalid Bounds ({start_idx}, {end_idx})" + + # set the bounds of the trace + self.start_idx = max(0, start_idx) + self.end_idx = end_idx + self._end_idx_internal = end_idx + + # update drawing metrics, note that this can 'tweak' end_idx to improve cell rendering + self._refresh_painting_metrics() + + # compute the number of instructions visible + self._last_trace_idx = min(self.reader.trace.length, self.end_idx) + + # refresh/redraw relevant elements + self._refresh_trace_highlights() + self.refresh() + + # return the final / selected bounds + return (self.start_idx, self.end_idx) + + def set_selection(self, start_idx, end_idx): + """ + Set the selection region bounds. + """ + assert end_idx >= start_idx + self._idx_selection_start = start_idx + self._idx_selection_end = end_idx + self.refresh() + def reset(self): """ - TODO + Reset the trace visualization. """ self.reader = None - + self.start_idx = 0 self.end_idx = 0 - self.density = 0 + self._last_trace_idx = 0 + + self._idx_pending_selection_origin = INVALID_IDX + self._idx_pending_selection_start = INVALID_IDX + self._idx_pending_selection_end = INVALID_IDX - self._selection_origin = -1 - self._selection_start = -1 - self._selection_end = -1 + self._idx_selection_start = INVALID_IDX + self._idx_selection_end = INVALID_IDX - self._executions = [] - self._reads = [] - self._writes = [] + self._idx_reads = [] + self._idx_writes = [] + self._idx_executions = [] + self._refresh_painting_metrics() self.refresh() def refresh(self, *args): """ - TODO + Refresh the trace visualization. """ - self._draw_trace() self.update() + #---------------------------------------------------------------------- + # Qt Overloads + #---------------------------------------------------------------------- + + def mouseMoveEvent(self, event): + """ + Qt overload to capture mouse movement events. + """ + if not self.reader: + return + + # mouse moving while holding left button + if event.buttons() == QtCore.Qt.MouseButton.LeftButton: + self._update_selection(event.y()) + self.refresh() + return + + # simple mouse hover over viz + self._update_hover(event.y()) + self.refresh() + + def mousePressEvent(self, event): + """ + Qt overload to capture mouse button presses. + """ + if not self.reader: + return + + # left mouse button was pressed (but not yet released!) + if event.button() == QtCore.Qt.MouseButton.LeftButton: + idx_origin = self._pos2idx(event.y()) + self._idx_pending_selection_origin = idx_origin + self._idx_pending_selection_start = idx_origin + self._idx_pending_selection_end = idx_origin + + return + + def mouseReleaseEvent(self, event): + """ + Qt overload to capture mouse button releases. + """ + if not self.reader: + return + + # if the left mouse button was released... + if event.button() == QtCore.Qt.MouseButton.LeftButton: + + # + # no selection origin? this means the click probably started + # off this widget, and the user moved their mouse over viz + # ... before releasing... which is not something we care about + # + + if self._idx_pending_selection_origin == INVALID_IDX: + return + + # if the mouse press & release was on the same idx, probably a click + if self._idx_pending_selection_start == self._idx_pending_selection_end: + self._commit_click() + + # a range was selected, so accept/commit it + else: + self._commit_selection() + + def leaveEvent(self, _): + """ + Qt overload to capture the mouse hover leaving the widget. + """ + self._hovered_idx = INVALID_IDX + self.refresh() + + def wheelEvent(self, event): + """ + Qt overload to capture wheel events. + """ + if not self.reader: + return + + # holding the shift key while scrolling is used to 'step over' + mod_keys = QtGui.QGuiApplication.keyboardModifiers() + step_over = bool(mod_keys & QtCore.Qt.ShiftModifier) + + # scrolling up, so step 'backwards' through the trace + if event.angleDelta().y() > 0: + self.reader.step_backward(1, step_over) + + # scrolling down, so step 'forwards' through the trace + elif event.angleDelta().y() < 0: + self.reader.step_forward(1, step_over) + + self.refresh() + event.accept() + + def resizeEvent(self, _): + """ + Qt overload to capture resize events for the widget. + """ + self._resize_timer.start(500) + + #------------------------------------------------------------------------- + # Helpers (Internal) + #------------------------------------------------------------------------- + # + # NOTE: this stuff should probably only be called by the 'mainthread' + # to ensure density / viz dimensions and stuff don't change. + # + + def _resize_stopped(self): + """ + Delayed handler of resize events. + + We delay handling resize events because several resize events can + trigger when a user is dragging to resize a window. we only really + care to recompute the visualization when they stop 'resizing' it. + """ + self.set_bounds(self.start_idx, self._end_idx_internal) + + def _refresh_painting_metrics(self): + """ + Refresh any metrics and calculations required to paint the widget. + """ + self._cell_height = 0 + self._cell_border = 0 + self._cell_spacing = 0 + + # how many 'instruction' cells *must* be shown based on current selection? + num_cell = self._end_idx_internal - self.start_idx + if not num_cell: + return + + # how many 'y' pixels are available, per cell (including spacing, between cells) + _, viz_h = self.viz_size + given_space_per_cell = viz_h / num_cell + + # compute the smallest possible cell height, with overlapping cell borders + min_full_cell_height = self._cell_min_height + self._cell_min_border + + # don't draw the trace vizualization as cells if the density is too high + if given_space_per_cell < min_full_cell_height: + #logger.debug(f"No need for cells -- {given_space_per_cell}, min req {min_full_cell_height}") + return + + # compute the pixel height of a cell at maximum height (including borders) + max_cell_height_with_borders = self._cell_max_height + self._cell_max_border * 2 + + # compute how much leftover space there is to use between cells + spacing_between_max_cells = given_space_per_cell - max_cell_height_with_borders + + # maximum sized instruction cells, with 'infinite' possible spacing between cells + if spacing_between_max_cells > max_cell_height_with_borders: + self._cell_border = self._cell_max_border + self._cell_height = self._cell_max_height + self._cell_spacing = spacing_between_max_cells + return + + # dynamically compute cell dimensions for drawing + self._cell_height = max(self._cell_min_height, min(int(given_space_per_cell * 0.95), self._cell_max_height)) + self._cell_border = max(self._cell_min_border, min(int(given_space_per_cell * 0.05), self._cell_max_border)) + self._cell_spacing = int(given_space_per_cell - (self._cell_height + self._cell_border * 2)) + #logger.debug(f"Dynamic cells -- Given: {given_space_per_cell}, Height {self._cell_height}, Border: {self._cell_border}, Spacing: {self._cell_spacing}") + + # if there's not enough to justify having spacing, use shared borders between cells (usually very small cells) + if self._cell_spacing < self._cell_min_spacing: + self._cell_spacing = self._cell_min_border * -2 + + # compute the final number of y pixels used by each 'cell' (an executed instruction) + used_space_per_cell = self._cell_height + self._cell_border * 2 + self._cell_spacing + + # compute how many cells we can *actually* show in the space available + num_cell_allowed = int(viz_h / used_space_per_cell) + 1 + #logger.debug(f"Num Cells {num_cell} vs Available Space {num_cell_allowed}") + + self.end_idx = self.start_idx + num_cell_allowed + def _idx2pos(self, idx): """ - Translate a given Y coordinate to an approximate IDX. + Translate a given idx to its first Y coordinate. """ + if idx < self.start_idx or idx >= self.end_idx: + #logger.warn(f"idx2pos failed (start: {self.start_idx:,} idx: {idx:,} end: {self.end_idx:,}") + return INVALID_POS + + density = self.density + if density == INVALID_DENSITY: + #logger.warn(f"idx2pos failed (INVALID_DENSITY)") + return INVALID_POS + + # convert the absolute idx to one that is 'relative' to the viz relative_idx = idx - self.start_idx - y = int(relative_idx / self.density) + BORDER_SIZE - - if y < BORDER_SIZE: - y = BORDER_SIZE - elif y > (self._height - BORDER_SIZE): - y = self._height + # re-base y to the start of the viz region + _, y = self.viz_pos + + # + # compute and return an 'approximate' y position of the given idx + # when the visualization is not using cell metrics (too dense) + # + + if not self.cells_visible: + y += int(relative_idx / density) + + # sanity check + _, viz_y = self.viz_pos + _, viz_h = self.viz_size + assert y >= viz_y + assert y < (viz_y + viz_h) + + # return the approximate y position of the given timestamp + return y + + #assert self._cell_spacing % 2 == 0 + + # compute the y position of the 'first' cell + y += self._cell_spacing / 2 # pad out from top + y += self._cell_border # top border of cell + # compute the y position of any given cell after the first + y += self._cell_height * relative_idx # cell body + y += self._cell_border * relative_idx # cell bottom border + y += self._cell_spacing * relative_idx # full space between cells + y += self._cell_border * relative_idx # cell top border + + # return the y position of the cell corresponding to the given timestamp return y def _pos2idx(self, y): """ - Translate a given Y coordinate to an approximate IDX. + Translate a given Y coordinate to an approximate idx. """ - y -= BORDER_SIZE + _, viz_y = self.viz_pos + _, viz_h = self.viz_size - relative_idx = round(y * self.density) - idx = self.start_idx + relative_idx + # clamp clearly out-of-bounds requests to the start/end idx values + if y < viz_y: + return self.start_idx + elif y >= viz_y + viz_h: + return self.end_idx - 1 - # clamp IDX to the start / end of the trace - if idx < self.start_idx: - idx = self.start_idx - elif idx > self.end_idx: - idx = self.end_idx + density = self.density + if density == INVALID_DENSITY: + #logger.warn(f"pos2idx failed (INVALID_DENSITY)") + return INVALID_IDX - return idx + # translate/rebase global y to viz relative y + y -= self._trace_border + + # compute the relative idx based on how much space is used per cell + if self.cells_visible: + + # this is how many vertical pixel each cell uses, including spacing to the next cell + used_space_per_cell = self._cell_height + self._cell_border * 2 + self._cell_spacing + + # compute relative idx for cell-based views + y -= self._cell_border + relative_idx = int(y / used_space_per_cell) + + # compute the approximate relative idx using the instruction density metric + else: + relative_idx = round(y * density) + + # convert the viz-relative idx, to its global trace idx timestamp + idx = self.start_idx + relative_idx + + # clamp idx to the start / end of visible tracebar range + return self._clamp_idx(idx) def _compute_pixel_distance(self, y, idx): """ - Compute the pixel distance from a given y to an IDX. + Compute the pixel distance from a given Y to an idx. """ - y_idx = ((idx - self.start_idx) / self.density) - BORDER_SIZE - distance_pixels = abs(y-y_idx) - return distance_pixels + + # get the y pixel position of the given idx + y_idx = self._idx2pos(idx) + if y_idx == INVALID_POS: + return -1 + + # + # if the visualization drawing cells, adjust the reported y coordinate + # of the given idx to the center of the cell. this makes distance + # calculations more correct + # + + if self.cells_visible: + y_idx += int(self._cell_height/2) + + # return the on-screen pixel distance between the two y coords + return abs(y - y_idx) def _update_hover(self, current_y): """ - TODO + Update the trace visualization based on the mouse hover. """ - - # fast path / nothing to do if hovered position hasn't changed - if self._last_hovered and self._last_hovered[1] == current_y: - return + self._hovered_idx = INVALID_IDX # see if there's an interesting trace event close to the hover hovered_idx = self._pos2idx(current_y) - closest_idx = self._get_closest_visible_idx(hovered_idx) - px_distance = self._compute_pixel_distance(current_y, closest_idx) + closest_idx = self._get_closest_highlighted_idx(hovered_idx) - #print(f" HOVERED IDX {hovered_idx:,}, CLOSEST IDX {closest_idx:,}, DIST {px_distance}") + # + # if the closest highlighted event (mem access, breakpoint) + # is outside the trace view bounds, then we don't need to + # do any special hover highlighting... + # - painter = QtGui.QPainter(self._image) - LINE_WIDTH = self._width - (BORDER_SIZE * 2) + if not(self.start_idx <= closest_idx < self.end_idx): + return - # unpaint the last hovered line with the position/color we stored for it - if self._last_hovered: - old_data, prev_y = self._last_hovered - length = min(len(old_data), self._image.width() * 4) - current_data = self._image.scanLine(prev_y).asarray(length) - for i in range(length): - current_data[i] = old_data[i] + # + # compute the on-screen pixel distance between the hover and the + # closest highlighted event + # - # nothing close, so don't bother painting a highlight - if px_distance >= LOCKON_DISTANCE: - self._last_hovered = None - #print("NOTHING CLOSE!") - self._draw_cursor(painter) + px_distance = self._compute_pixel_distance(current_y, closest_idx) + #logger.debug(f"hovered idx {hovered_idx:,}, closest idx {closest_idx:,}, dist {px_distance} (start: {self.start_idx:,} end: {self.end_idx:,}") + if px_distance == -1: return - locked_y = self._idx2pos(closest_idx) + # clamp the lock-on distance depending on the scale of zoom / cell size + lockon_distance = max(self._magnetism_distance, self._cell_height) - # overwrite last_hovered with the latest hover position / color we will stomp - current_line_data = self._image.scanLine(locked_y) - old_data = [x for x in current_line_data.asarray(4 * self._image.width())] - self._last_hovered = (old_data, locked_y) - #self._last_hovered = (self._image.pixelColor(LINE_WIDTH//2, locked_y), locked_y) - - # paint the currently hovered line - painter.setPen(self.cursor_pen) - painter.drawLine(BORDER_SIZE, locked_y, LINE_WIDTH, locked_y) - #print("PAINTED NEW!") + # + # if the trace event is within the magnetized distance of the user + # cursor, lock on to it. this makes 'small' things easier to click + # - self._draw_cursor(painter) + if px_distance < lockon_distance: + self._hovered_idx = closest_idx - def set_zoom(self, start_idx, end_idx): + def _update_selection(self, y): """ - TODO + Update the user region selection of the trace visualization based on the current y. """ - #print("Setting Zoom!", start_idx, end_idx) - - # save the first and last timestamps to be shown - self.start_idx = start_idx - self.end_idx = end_idx - - # compute the number of instructions visible - self.length = (end_idx - start_idx) + idx_event = self._pos2idx(y) - # compute the number of instructions per y pixel - self.density = self.length / (self._height - BORDER_SIZE * 2) + if idx_event > self._idx_pending_selection_origin: + self._idx_pending_selection_start = self._idx_pending_selection_origin + self._idx_pending_selection_end = idx_event + else: + self._idx_pending_selection_end = self._idx_pending_selection_origin + self._idx_pending_selection_start = idx_event - self._refresh_breakpoint_hits(self.core.breakpoints.model.focused_breakpoint) - self.refresh() + self._idx_selection_start = INVALID_IDX + self._idx_selection_end = INVALID_IDX - def set_selection(self, start_idx, end_idx): + def _global_selection_changed(self, start_idx, end_idx): """ - TODO + Handle selection behavior specific to a 'global' trace visualizations. """ - self._selection_end = end_idx - self._selection_start = start_idx - self.refresh() - - def _global_selection_changed(self, start_idx, end_idx): if start_idx == end_idx: return self.set_selection(start_idx, end_idx) - + def _zoom_selection_changed(self, start_idx, end_idx): + """ + Handle selection behavior specific to a 'zoomer' trace visualizations. + """ if start_idx == end_idx: self.hide() else: self.show() - self.set_zoom(start_idx, end_idx) + self.set_bounds(start_idx, end_idx) + + def _commit_click(self): + """ + Accept a click event. + """ + selected_idx = self._idx_pending_selection_start + + # use a 'magnetized' selection, if available + if self._hovered_idx != INVALID_IDX: + selected_idx = self._hovered_idx + self._hovered_idx = INVALID_IDX - def highlight_executions(self, idxs): - self._executions = idxs + # reset pending selection + self._idx_pending_selection_origin = INVALID_IDX + self._idx_pending_selection_start = INVALID_IDX + self._idx_pending_selection_end = INVALID_IDX + + # does the click fall within the existing selected region? + within_region = (self._idx_selection_start <= selected_idx <= self._idx_selection_end) + + # nope click is outside the region, so clear the region selection + if not within_region: + self._idx_selection_start = INVALID_IDX + self._idx_selection_end = INVALID_IDX + self._notify_selection_changed(INVALID_IDX, INVALID_IDX) + + #print(f"Jumping to {selected_idx:,}") + self.reader.seek(selected_idx) self.refresh() - def _draw_trace(self): - w, h = self._width, self._height - self._last_hovered = None + def _commit_selection(self): + """ + Accept a selection event. + """ + new_start = self._idx_pending_selection_start + new_end = self._idx_pending_selection_end - self._image = QtGui.QImage(w, h, QtGui.QImage.Format_RGB32) - - if not self.reader: - self._image.fill(self.core.palette.trace_bedrock) + # reset pending selections + self._idx_pending_selection_origin = INVALID_IDX + self._idx_pending_selection_start = INVALID_IDX + self._idx_pending_selection_end = INVALID_IDX + + # + # if we just selected a new region on a trace viz that's a + # 'zoomer', then we will apply the zoom-in action to ourself by + # adjusting our visible regions (bounds) + # + # NOTE: that we don't have to do this on a global / static trace + # viz, because the 'zoomers' will be notified as a listener of + # the selection change events + # + + if self._is_zoom: + + # + # ensure the committed selection is also reset as we are about + # to zoom-in and should not have an active selection once done + # + + self._idx_selection_start = INVALID_IDX + self._idx_selection_end = INVALID_IDX + + # + # apply the new zoom-in / viz bounds to ourself + # + # NOTE: because the special cell-drawing metrics / computation, set + # bounds can 'tweak' the end value, so we want to grab it here + # + + new_start, new_end = self.set_bounds(new_start, new_end) + + # commit the new selection for global trace visualizations else: - self._image.fill(self.core.palette.trace_instruction) + self._idx_selection_start = new_start + self._idx_selection_end = new_end + + # notify listeners of our selection change + self._notify_selection_changed(new_start, new_end) + + def _get_closest_highlighted_idx(self, idx): + """ + Return the closest idx (timestamp) to the given idx. + """ + closest_idx = INVALID_IDX + smallest_distace = 999999999999999999999999 + for entries in [self._idx_reads, self._idx_writes, self._idx_executions]: + for current_idx in entries: + distance = abs(idx - current_idx) + if distance < smallest_distace: + closest_idx = current_idx + smallest_distace = distance + return closest_idx + + def _breakpoints_changed(self): + """ + The focused breakpoint has changed. + """ + self._refresh_trace_highlights() + self.refresh() + + def _refresh_trace_highlights(self): + """ + Refresh trace event / highlight info from the underlying trace reader. + """ + self._idx_reads = [] + self._idx_writes = [] + self._idx_executions = [] + + reader, density = self.reader, self.density + if not (reader and density != INVALID_DENSITY): + return + + model = self.pctx.breakpoints.model + + # fetch executions for all breakpoints + for bp in model.bp_exec.values(): + executions = reader.get_executions_between(bp.address, self.start_idx, self.end_idx, density) + self._idx_executions.extend(executions) + + # fetch all memory read (only) breakpoints hits + for bp in model.bp_read.values(): + if bp.length == 1: + reads = reader.get_memory_reads_between(bp.address, self.start_idx, self.end_idx, density) + else: + reads = reader.get_memory_region_reads_between(bp.address, bp.length, self.start_idx, self.end_idx, density) + self._idx_reads.extend(reads) + + # fetch all memory write (only) breakpoint hits + for bp in model.bp_write.values(): + if bp.length == 1: + writes = reader.get_memory_writes_between(bp.address, self.start_idx, self.end_idx, density) + else: + writes = reader.get_memory_region_writes_between(bp.address, bp.length, self.start_idx, self.end_idx, density) + self._idx_writes.extend(writes) + + # fetch memory access for all breakpoints + for bp in model.bp_access.values(): + if bp.length == 1: + reads, writes = reader.get_memory_accesses_between(bp.address, self.start_idx, self.end_idx, density) + else: + reads, writes = reader.get_memory_region_accesses_between(bp.address, bp.length, self.start_idx, self.end_idx, density) + self._idx_reads.extend(reads) + self._idx_writes.extend(writes) + + def _clamp_idx(self, idx): + """ + Clamp the given idx to the bounds of this trace view. + """ + if idx < self.start_idx: + return self.start_idx + elif idx >= self.end_idx: + return self.end_idx - 1 + return idx - painter = QtGui.QPainter(self._image) + #------------------------------------------------------------------------- + # Drawing + #------------------------------------------------------------------------- + + def paintEvent(self, event): + """ + Qt overload of widget painting. + + TODO/FUTURE: I was planning to make this paint by layer, and only + re-paint dirty layers as necessary. but I think it's unecessary to + do at this time as I don't think we're pressed for perf. + """ + painter = QtGui.QPainter(self) + + # + # draw instructions / trace landscape + # + + self._draw_base() + painter.drawImage(0, 0, self._image_base) # # draw accesses along the trace timeline # - self._draw_accesses(painter) - + self._draw_highlights() + painter.drawImage(0, 0, self._image_highlights) + # - # draw region selection + # draw user region selection over trace timeline # - - self._draw_selection(painter) - + + self._draw_selection() + painter.drawImage(0, 0, self._image_selection) + # # draw border around trace timeline # - border_pen = QtGui.QPen(self.core.palette.trace_border, 1, QtCore.Qt.SolidLine) + self._draw_border() + painter.drawImage(0, 0, self._image_border) + + # + # draw current trace position cursor + # + + self._draw_cursor() + painter.drawImage(0, 0, self._image_cursor) + + #painter.drawImage(0, 0, self._image_final) + + def _draw_base(self): + """ + Draw the trace visualization of executed code. + """ + + # + # NOTE: DO NOT REMOVE !!! Qt will CRASH if we do not explicitly delete + # these here (dangling internal pointer to device/image otherwise?!?) + # + + del self._painter_base + + self._image_base = QtGui.QImage(self.width(), self.height(), QtGui.QImage.Format_ARGB32) + self._image_base.fill(self.pctx.palette.trace_bedrock) + #self._image_base.fill(QtGui.QColor("red")) # NOTE/debug + self._painter_base = QtGui.QPainter(self._image_base) + + # redraw instructions + if self.cells_visible: + self._draw_code_cells(self._painter_base) + else: + self._draw_code_trace(self._painter_base) + + def _draw_code_trace(self, painter): + """ + Draw a 'zoomed out' trace visualization of executed code. + """ + dctx = disassembler[self.pctx] + viz_w, viz_h = self.viz_size + viz_x, viz_y = self.viz_pos + + for i in range(viz_h): + + # convert a y pixel in the viz region to an executed address + wid_y = viz_y + i + idx = self._pos2idx(wid_y) + + # + # since we can conciously set a trace visualization bounds bigger + # than the actual underlying trace, it is possible for the trace + # to not take up the entire available space. + # + # when we reach the 'end' of the trace, we obviously can stop + # drawing any sort of landscape for it! + # + + if idx >= self._last_trace_idx: + break + + # get the executed/code address for the current idx that will represent this line + address = self.reader.get_ip(idx) + rebased_address = self.reader.analysis.rebase_pointer(address) + + # select the color for instructions that can be viewed with Tenet + if dctx.is_mapped(rebased_address): + painter.setPen(self.pctx.palette.trace_instruction) + + # unexplorable parts of the trace are 'greyed' out (eg, not in IDB) + else: + painter.setPen(self.pctx.palette.trace_unmapped) + + # paint the current line + painter.drawLine(viz_x, wid_y, viz_w, wid_y) + + def _draw_code_cells(self, painter): + """ + Draw a 'zoomed in', cell-based, trace visualization of executed code. + """ + + # + # if there is no spacing between cells, that means they are going to + # be relatively small and have shared 'cell walls' (borders) + # + # we attempt to maximize contrast between border and cell color, while + # attempting to keep the tracebar color visually consistent + # + + # compute the color to use for the borders between cells + border_color = self.pctx.palette.trace_cell_wall + if self._cell_spacing < 0: + border_color = self.pctx.palette.trace_cell_wall_contrast + + # compute the color to use for the cell bodies + if self._cell_spacing < 0: + ratio = (self._cell_border / (self._cell_height - 1)) * 0.5 + lighten = 100 + int(ratio * 100) + cell_color = self.pctx.palette.trace_instruction.lighter(lighten) + #print(f"Lightened by {lighten}% (Border: {self._cell_border}, Body: {self._cell_height}") + else: + cell_color = self.pctx.palette.trace_instruction + + border_pen = QtGui.QPen(border_color, self._cell_border, QtCore.Qt.SolidLine) painter.setPen(border_pen) + painter.setBrush(cell_color) - # top & bottom - painter.drawLine(0, 0, w, 0) - painter.drawLine(0, h-1, w, h-1) + viz_x, _ = self.viz_pos + viz_w, _ = self.viz_size - # left & right - painter.drawLine(0, 0, 0, h) - painter.drawLine(w-1, 0, w-1, h) + # compute cell positioning info + x = viz_x + self._cell_border * -1 + w = viz_w + self._cell_border + h = self._cell_height - # - # draw current trace position cursor + dctx = disassembler[self.pctx] + + # draw each cell + border + for idx in range(self.start_idx, self._last_trace_idx): + + # get the executed/code address for the current idx that will represent this cell + address = self.reader.get_ip(idx) + rebased_address = self.reader.analysis.rebase_pointer(address) + + # select the color for instructions that can be viewed with Tenet + if dctx.is_mapped(rebased_address): + painter.setBrush(cell_color) + + # unexplorable parts of the trace are 'greyed' out (eg, not in IDB) + else: + painter.setBrush(self.pctx.palette.trace_unmapped) + + y = self._idx2pos(idx) + painter.drawRect(x, y, w, h) + + def _draw_highlights(self): + """ + Draw active event highlights (mem access, breakpoints) for the trace visualization. + """ + + # + # NOTE: DO NOT REMOVE !!! Qt will CRASH if we do not explicitly delete + # these here (dangling internal pointer to device/image otherwise?!?) # - self._draw_cursor(painter) + del self._painter_highlights + + self._image_highlights = QtGui.QImage(self.width(), self.height(), QtGui.QImage.Format_ARGB32) + self._image_highlights.fill(QtCore.Qt.transparent) + self._painter_highlights = QtGui.QPainter(self._image_highlights) + + if self.cells_visible: + self._draw_highlights_cells(self._painter_highlights) + else: + self._draw_highlights_trace(self._painter_highlights) + + def _draw_highlights_cells(self, painter): + """ + Draw cell-based event highlights. + """ + viz_w, _ = self.viz_size + viz_x, _ = self.viz_pos + + access_sets = \ + [ + (self._idx_reads, self.pctx.palette.mem_read_bg), + (self._idx_writes, self.pctx.palette.mem_write_bg), + (self._idx_executions, self.pctx.palette.breakpoint), + ] + + painter.setPen(QtCore.Qt.NoPen) + + h = self._cell_height - self._cell_border + + for entries, cell_color in access_sets: + painter.setBrush(cell_color) + + for idx in entries: + + # skip entries that fall outside the visible zoom + if not(self.start_idx <= idx < self.end_idx): + continue + + # slight tweak of y because we are only drawing a highlighted + # cell body without borders + y = self._idx2pos(idx) + self._cell_border + + # draw cell body + painter.drawRect(viz_x, y, viz_w, h) - def _draw_accesses(self, painter): + def _draw_highlights_trace(self, painter): """ - Draw read / write / execs accesses on the trace timeline. + Draw trace-based event highlights. """ + viz_w, _ = self.viz_size + viz_x, _ = self.viz_pos access_sets = \ [ - (self._reads, self.color_read), - (self._writes, self.color_write), - (self._executions, self.color_exec), + (self._idx_reads, self.pctx.palette.mem_read_bg), + (self._idx_writes, self.pctx.palette.mem_write_bg), + (self._idx_executions, self.pctx.palette.breakpoint), ] for entries, color in access_sets: painter.setPen(color) for idx in entries: - + # skip entries that fall outside the visible zoom if not(self.start_idx <= idx < self.end_idx): continue - - relative_idx = idx - self.start_idx - y = int(relative_idx / self.density) + BORDER_SIZE - painter.drawLine(0, y, self._width, y) - def _draw_cursor(self, painter): + y = self._idx2pos(idx) + painter.drawLine(viz_x, y, viz_w, y) + + def _draw_cursor(self): """ Draw the user cursor / current position in the trace. """ - if not self.reader: - return - path = QtGui.QPainterPath() - + size = 13 assert size % 2, "Cursor triangle size must be odd" - # rebase the absolute trace cursor idx to the current 'zoomed' view - relative_idx = self.reader.idx - self.start_idx - if relative_idx < 0: - return False + del self._painter_cursor + self._image_cursor = QtGui.QImage(self.width(), self.height(), QtGui.QImage.Format_ARGB32) + self._image_cursor.fill(QtCore.Qt.transparent) + self._painter_cursor = QtGui.QPainter(self._image_cursor) # compute the y coordinate / line to center the user cursor around - cursor_y = int(relative_idx / self.density) + BORDER_SIZE + cursor_y = self._idx2pos(self.reader.idx) + draw_reader_cursor = bool(cursor_y != INVALID_IDX) + + if self.cells_visible: + cell_y = cursor_y + self._cell_border + cell_body_height = self._cell_height - self._cell_border + cursor_y += self._cell_height/2 # the top point of the triangle top_x = 0 top_y = cursor_y - (size // 2) # vertically align the triangle so the tip matches the cross section - #print("TOP", top_x, top_y) - + # bottom point of the triangle bottom_x = top_x bottom_y = top_y + size - 1 - #print("BOT", bottom_x, bottom_y) # the 'tip' of the triangle pointing into towards the center of the trace tip_x = top_x + (size // 2) tip_y = top_y + (size // 2) - #print("CURSOR", tip_x, tip_y) # start drawing from the 'top' of the triangle path.moveTo(top_x, top_y) - + # generate the triangle path / shape path.lineTo(bottom_x, bottom_y) path.lineTo(tip_x, tip_y) path.lineTo(top_x, top_y) - painter.setPen(self.cursor_pen) - painter.drawLine(0, cursor_y, self._width, cursor_y) - - # paint the defined triangle - # TODO: don't hardcode colors - painter.setPen(QtCore.Qt.black) - painter.setBrush(QtGui.QBrush(QtGui.QColor("red"))) - painter.drawPath(path) - - def _draw_selection(self, painter): - """ - Draw a region selection rect. - """ - #print("DRAWING SELECTION?", self._selection_start, self._selection_end) - if self._selection_start == self._selection_end: - return - - start_y = int((self._selection_start - self.start_idx) / self.density) - end_y = int((self._selection_end - self.start_idx) / self.density) + viz_x, _ = self.viz_pos + viz_w, _ = self.viz_size - painter.setBrush(self.brush_selection) - painter.setPen(self.pen_selection) - painter.drawRect( - BORDER_SIZE, # x - start_y+BORDER_SIZE, # y - self._width - (BORDER_SIZE * 2), # width - end_y - start_y - (BORDER_SIZE * 2) # height - ) + # draw the user cursor in cell mode + if self.cells_visible: - def wheelEvent(self, event): + # normal fixed / current reader cursor + self._painter_cursor.setPen(QtCore.Qt.NoPen) + self._painter_cursor.setBrush(self.pctx.palette.trace_cursor_highlight) - if not self.reader: - return - - mod = QtGui.QGuiApplication.keyboardModifiers() - step_over = bool(mod & QtCore.Qt.ShiftModifier) + if draw_reader_cursor: + self._painter_cursor.drawRect(viz_x, cell_y, viz_w, cell_body_height) - if event.angleDelta().y() > 0: - self.reader.step_backward(1, step_over) + # cursor hover highlighting an event + if self._hovered_idx != INVALID_IDX: + hovered_y = self._idx2pos(self._hovered_idx) + hovered_cell_y = hovered_y + self._cell_border + self._painter_cursor.drawRect(viz_x, hovered_cell_y, viz_w, cell_body_height) - elif event.angleDelta().y() < 0: - self.reader.step_forward(1, step_over) + # draw the user cursor in dense/landscape mode + else: + self._painter_cursor.setPen(self._pen_cursor) - self.refresh() - event.accept() + # normal fixed / current reader cursor + if draw_reader_cursor: + self._painter_cursor.drawLine(viz_x, cursor_y, viz_w, cursor_y) - def _update_selection(self, y): - idx_event = self._pos2idx(y) + # cursor hover highlighting an event + if self._hovered_idx != INVALID_IDX: + hovered_y = self._idx2pos(self._hovered_idx) + self._painter_cursor.drawLine(viz_x, hovered_y, viz_w, hovered_y) - if idx_event > self._selection_origin: - self._selection_start = self._selection_origin - self._selection_end = idx_event - else: - self._selection_end = self._selection_origin - self._selection_start = idx_event + if not draw_reader_cursor: + return - def mouseMoveEvent(self, event): - #mod = QtGui.QGuiApplication.keyboardModifiers() - #if mod & QtCore.Qt.ShiftModifier: - # print("SHIFT IS HELD!!") - #import ida_kernwin - #ida_kernwin.refresh_idaview_anyway() - if event.buttons() == QtCore.Qt.MouseButton.LeftButton: - self._update_selection(event.y()) - self.refresh() - else: - self._update_hover(event.y()) - self.update() + # paint the defined triangle + self._painter_cursor.setPen(self.pctx.palette.trace_cursor_border) + self._painter_cursor.setBrush(self.pctx.palette.trace_cursor) + self._painter_cursor.drawPath(path) - def mousePressEvent(self, event): + def _draw_selection(self): """ - Qt override to capture mouse button presses + Draw a region selection rect. """ - if event.button() == QtCore.Qt.MouseButton.LeftButton: - idx_origin = self._pos2idx(event.y()) - self._selection_origin = idx_origin - self._selection_start = idx_origin - self._selection_end = idx_origin - - return + # + # NOTE: DO NOT REMOVE !!! Qt will CRASH if we do not explicitly delete + # these here (dangling internal pointer to device/image otherwise?!?) + # - def _get_closest_visible_idx(self, idx): - """ - Return the closest IDX (timestamp) to the given IDX. - """ - closest_idx = -1 - smallest_distace = 999999999999999999999999 - for entries in [self._reads, self._writes, self._executions]: - for current_idx in entries: - distance = abs(idx - current_idx) - if distance < smallest_distace: - closest_idx = current_idx - smallest_distace = distance - return closest_idx + del self._painter_selection - #overridden event to capture mouse button releases - def mouseReleaseEvent(self, event): - if not self.reader: + viz_w, viz_h = self.viz_size + self._image_selection = QtGui.QImage(self.width(), self.height(), QtGui.QImage.Format_ARGB32) + self._image_selection.fill(QtCore.Qt.transparent) + self._painter_selection = QtGui.QPainter(self._image_selection) + + # active / on-going selection event + if self._idx_pending_selection_start != INVALID_IDX: + start_idx = self._idx_pending_selection_start + end_idx = self._idx_pending_selection_end + + # fixed / committed selection + elif self._idx_selection_start != INVALID_IDX: + start_idx = self._idx_selection_start + end_idx = self._idx_selection_end + + # no region selection, nothing to do... + else: return - # if the left mouse button was released... - if event.button() == QtCore.Qt.MouseButton.LeftButton: + start_idx = self._clamp_idx(start_idx) + end_idx = self._clamp_idx(end_idx) - # - # the initial 'click' origin is not set, so that means the 'click' - # event did not start over this widget... or is something we - # should just ignore. - # + # nothing to draw + if start_idx == end_idx: + return - if self._selection_origin == -1: - return + start_y = self._idx2pos(start_idx) + end_y = self._idx2pos(end_idx) - # - # clear the selection origin as we will be consuming the - # selection event in the followin codepath - # + self._painter_selection.setBrush(self._brush_selection) + self._painter_selection.setPen(self._pen_selection) - self._selection_origin = -1 + # TODO/FUTURE: real border math + viz_x, viz_y = self.viz_pos - # - # if the user selection appears to be a 'click' vs a zoom / range - # selection, then seek to the clicked address - # + x = viz_x + y = start_y + w = viz_w + h = end_y - start_y - if self._selection_start == self._selection_end: - selected_idx = self._selection_start - #clear_focus = True - - # - # if there is a highlighted bp near the click, we should lock - # onto that instead... - # - - closest_idx = self._get_closest_visible_idx(selected_idx) - current_y = self._idx2pos(self._selection_start) - px_distance = self._compute_pixel_distance(current_y, closest_idx) - if px_distance < LOCKON_DISTANCE: - selected_idx = closest_idx - # clear_focus = False - #elif self._is_zoom: - # clear_focus = False - - # - # jump to the selected area - # - - #print(f"Jumping to {selected_idx:,}") - self.reader.seek(selected_idx) - #if clear_focus: - # self.core.breakpoints.model.focused_breakpoint = None - - self._notify_selection_changed(selected_idx, selected_idx) - self.refresh() - return + # draw the screen door / selection rect + self._painter_selection.drawRect(x, y, w, h) - if self._is_zoom: - new_start = self._selection_start - new_end = self._selection_end - self._selection_start = self._selection_end = -1 - self.set_zoom(new_start, new_end) - self._notify_selection_changed(new_start, new_end) - else: - self._notify_selection_changed(self._selection_start, self._selection_end) + def _draw_border(self): + """ + Draw the border around the trace timeline. + """ + wid_w, wid_h = self.width(), self.height() - def leaveEvent(self, event): - self.refresh() + # + # NOTE: DO NOT REMOVE !!! Qt will CRASH if we do not explicitly delete + # these here (dangling internal pointer to device/image otherwise?!?) + # - def keyPressEvent(self, e): - #print("PRESSING", e.key(), e.modifiers()) - pass + del self._painter_border - def keyReleaseEvent(self, e): - #print("RELEASING", e.key(), e.modifiers()) - pass + self._image_border = QtGui.QImage(wid_w, wid_h, QtGui.QImage.Format_ARGB32) + self._image_border.fill(QtCore.Qt.transparent) + self._painter_border = QtGui.QPainter(self._image_border) - def resizeEvent(self, event): - size = event.size() - self._width, self._height = self.width(), self.height() - self.density = self.length / (self._height - BORDER_SIZE * 2) - #self._refresh_breakpoint_hits(breakpoint) - self.refresh() + color = self.pctx.palette.trace_border + #color = QtGui.QColor("red") # NOTE: for dev/debug testing + border_pen = QtGui.QPen(color, self._trace_border, QtCore.Qt.SolidLine) + self._painter_border.setPen(border_pen) - def paintEvent(self, event): - painter = QtGui.QPainter(self) - painter.drawImage(0, 0, self._image) + w = wid_w - self._trace_border + h = wid_h - self._trace_border + + # draw the border around the tracebar using a blank rect + stroke (border) + self._painter_border.drawRect(0, 0, w, h) #---------------------------------------------------------------------- # Callbacks @@ -604,16 +1218,18 @@ def _notify_selection_changed(self, start_idx, end_idx): """ notify_callback(self._selection_changed_callbacks, start_idx, end_idx) +#----------------------------------------------------------------------------- +# Trace View +#----------------------------------------------------------------------------- + class TraceView(QtWidgets.QWidget): - def __init__(self, core, parent=None): + + def __init__(self, pctx, parent=None): super(TraceView, self).__init__(parent) - self.core = core + self.pctx = pctx self._init_ui() def _init_ui(self): - """ - TODO - """ self._init_bars() self._init_ctx_menu() @@ -623,28 +1239,25 @@ def attach_reader(self, reader): self.trace_local.hide() def detach_reader(self): - self.trace_global.reset() - self.trace_local.reset() + self.trace_global.reset() + self.trace_local.reset() self.trace_local.hide() def _init_bars(self): - """ - TODO - """ - self.trace_local = TraceBar(self.core, zoom=True) - self.trace_global = TraceBar(self.core) + self.trace_local = TraceBar(self.pctx, zoom=True) + self.trace_global = TraceBar(self.pctx) # connect the local view to follow the global selection self.trace_global.selection_changed(self.trace_local._zoom_selection_changed) self.trace_local.selection_changed(self.trace_global._global_selection_changed) - # connect other signals - self.core.breakpoints.model.focused_breakpoint_changed(self.trace_global._focused_breakpoint_changed) - self.core.breakpoints.model.focused_breakpoint_changed(self.trace_local._focused_breakpoint_changed) + # connect other signals + self.pctx.breakpoints.model.breakpoints_changed(self.trace_global._breakpoints_changed) + self.pctx.breakpoints.model.breakpoints_changed(self.trace_local._breakpoints_changed) # hide the zoom bar by default self.trace_local.hide() - + # setup the layout and spacing for the tracebar hbox = QtWidgets.QHBoxLayout(self) hbox.setContentsMargins(3, 3, 3, 3) @@ -658,11 +1271,13 @@ def _init_bars(self): def _init_ctx_menu(self): """ - TODO + Initialize the right click context menu actions. """ self._menu = QtWidgets.QMenu() # create actions to show in the context menu + self._action_clear = self._menu.addAction("Clear all breakpoints") + self._menu.addSeparator() self._action_load = self._menu.addAction("Load new trace") self._action_close = self._menu.addAction("Close trace") @@ -675,13 +1290,17 @@ def _init_ctx_menu(self): #-------------------------------------------------------------------------- def _ctx_menu_handler(self, position): + """ + Handle a right click event (populate/show context menu). + """ action = self._menu.exec_(self.mapToGlobal(position)) if action == self._action_load: - self.core.interactive_load_trace(True) + self.pctx.interactive_load_trace(True) elif action == self._action_close: - self.core.close_trace() + self.pctx.close_trace() + elif action == self._action_clear: + self.pctx.breakpoints.clear_breakpoints() - # if a tracebar got added, we need to update the layout def update_from_model(self): for bar in self.model.tracebars.values()[::-1]: self.hbox.addWidget(bar) @@ -693,8 +1312,6 @@ def update_from_model(self): # Dockable Trace Visualization #----------------------------------------------------------------------------- -# TODO: refactor out to trace controller / dock model - class TraceDock(QtWidgets.QToolBar): """ A Qt 'Toolbar' to house the TraceBar visualizations. @@ -703,16 +1320,16 @@ class TraceDock(QtWidgets.QToolBar): around the QMainWindow in Qt-based applications. This allows us to pin the visualizations to areas where they will not be dist """ - def __init__(self, core, parent=None): + def __init__(self, pctx, parent=None): super(TraceDock, self).__init__(parent) - self.core = core - self.view = TraceView(core, self) + self.pctx = pctx + self.view = TraceView(pctx, self) self.setMovable(False) self.setContentsMargins(0, 0, 0, 0) self.addWidget(self.view) def attach_reader(self, reader): self.view.attach_reader(reader) - + def detach_reader(self): self.view.detach_reader() \ No newline at end of file diff --git a/plugins/tenet/util/log.py b/plugins/tenet/util/log.py index d27c246..4c4a3ed 100644 --- a/plugins/tenet/util/log.py +++ b/plugins/tenet/util/log.py @@ -15,7 +15,7 @@ def pmsg(message): """ # prefix the message - prefix_message = "[TENET] %s" % message + prefix_message = "[Tenet] %s" % message # only print to disassembler if its output window is alive if disassembler.is_msg_inited(): diff --git a/plugins/tenet/util/update.py b/plugins/tenet/util/update.py index 96c51b2..195089c 100644 --- a/plugins/tenet/util/update.py +++ b/plugins/tenet/util/update.py @@ -44,7 +44,7 @@ def async_update_check(current_version, callback): version_local = int(''.join(re.findall('\d+', current_version))) # no updates available... - logger.debug(" - Local: '%s' vs Remote: '%s'" % (current_version, remote_version)) + logger.debug(" - Local: 'v%s' vs Remote: '%s'" % (current_version, remote_version)) if version_local >= version_remote: logger.debug(" - No update needed...") return @@ -52,7 +52,7 @@ def async_update_check(current_version, callback): # notify the user if an update is available update_message = "An update is available for Tenet!\n\n" \ " - Latest Version: %s\n" % (remote_version) + \ - " - Current Version: %s\n\n" % (current_version) + \ + " - Current Version: v%s\n\n" % (current_version) + \ "Please go download the update from GitHub." callback(update_message) diff --git a/screenshots/tenet_overview.gif b/screenshots/tenet_overview.gif index a3dce0f..21eeef2 100644 Binary files a/screenshots/tenet_overview.gif and b/screenshots/tenet_overview.gif differ diff --git a/tracers/README.md b/tracers/README.md index 8de6ff8..5fa3a4c 100644 --- a/tracers/README.md +++ b/tracers/README.md @@ -7,8 +7,9 @@ Included within this repo are two tracers, with a third hosted out-of-repo. They * `/tracers/pin` -- An Intel Pin based tracer for Windows/Linux usermode applications * `/tracers/qemu` -- A QEMU based tracer to demo tracing the Xbox boot process on [XEMU](https://github.com/mborgerson/xemu) * [Tenet Tracer](https://github.com/AndrewFasano/tenet_tracer) -- A [PANDA](https://github.com/panda-re/panda) based tracer contributed by [Andrew Fasano](https://twitter.com/andrewfasano) +* [what the fuzz](https://github.com/0vercl0k/wtf) -- A [powerful](https://blog.ret2.io/2021/07/21/wtf-snapshot-fuzzing/) snapshot-based fuzzer which can generate Tenet traces -At this time, Tenet has mostly been used to explore traces that were generated from private snapshot based fuzzers. While these tracers are not public, snapshot fuzzer traces are perhaps the most immediate, real-world use case for this technology. +At this time, Tenet has mostly been used to explore traces that were generated from snapshot-based fuzzers. These are perhaps the most immediate, real-world use case for this technology until additional investments are made to scale it further. ## Trace Format diff --git a/tracers/pin/pintenet.cpp b/tracers/pin/pintenet.cpp index 257de67..9291844 100644 --- a/tracers/pin/pintenet.cpp +++ b/tracers/pin/pintenet.cpp @@ -17,6 +17,8 @@ using std::ofstream; +ofstream* g_log; + #ifdef __i386__ #define PC "eip" #else @@ -30,7 +32,7 @@ using std::ofstream; static KNOB KnobModuleWhitelist(KNOB_MODE_APPEND, "pintool", "w", "", "Add a module to the whitelist. If none is specified, every module is white-listed. Example: calc.exe"); -KNOB KnobOutputFile(KNOB_MODE_WRITEONCE, "pintool", "o", "trace", +KNOB KnobOutputFilePrefix(KNOB_MODE_WRITEONCE, "pintool", "o", "trace", "Prefix of the output file. If none is specified, 'trace' is used."); // @@ -67,10 +69,10 @@ struct ThreadData ADDRINT mem_r2_addr; ADDRINT mem_r2_size; - char m_scratch[128]; - // Trace file for thread-specific trace modes - ofstream * m_trace; + ofstream* m_trace; + + char m_scratch[512 * 2]; // fxsave has the biggest memory operand }; // @@ -126,12 +128,15 @@ static VOID OnThreadStart(THREADID tid, CONTEXT* ctxt, INT32 flags, VOID* v) // Create a new 'ThreadData' object and set it on the TLS. auto& context = *reinterpret_cast(v); auto data = new ThreadData; + memset(data, 0, sizeof(ThreadData)); + data->m_trace = new ofstream; context.setThreadLocalData(tid, data); char filename[128] = {}; - sprintf(filename, "%s.%u.log", KnobOutputFile.Value().c_str(), tid); + sprintf(filename, "%s.%u.log", KnobOutputFilePrefix.Value().c_str(), tid); data->m_trace->open(filename); + *data->m_trace << std::hex; // Save the recently created thread. PIN_GetLock(&context.m_thread_lock, 1); @@ -168,7 +173,7 @@ static VOID OnImageLoad(IMG img, VOID* v) ADDRINT low = IMG_LowAddress(img); ADDRINT high = IMG_HighAddress(img); - printf("Loaded image: %p:%p -> %s\n", (void *)low, (void *)high, img_name.c_str()); + *g_log << "Loaded image: 0x" << low << ":0x" << high << " -> " << img_name << std::endl; // Save the loaded image with its original full name/path. PIN_GetLock(&context.m_loaded_images_lock, 1); @@ -227,12 +232,12 @@ VOID record_diff(const CONTEXT * cpu, ADDRINT pc, VOID* v) continue; // save the value for the new register to the log - *OutFile << REG_StringShort( (REG) reg) << "=0x" << std::hex << val << ","; + *OutFile << REG_StringShort( (REG) reg) << "=0x" << val << ","; data->m_cpu[reg] = val; } // always save pc to the log, for every unit of execution - *OutFile << PC << "=0x" << std::hex << pc; + *OutFile << PC << "=0x" << pc; // // dump memory reads / writes @@ -243,10 +248,10 @@ VOID record_diff(const CONTEXT * cpu, ADDRINT pc, VOID* v) memset(data->m_scratch, 0, data->mem_r_size); PIN_SafeCopy(data->m_scratch, (const VOID *)data->mem_r_addr, data->mem_r_size); - *OutFile << ",mr=0x" << std::hex << data->mem_r_addr << ":"; + *OutFile << ",mr=0x" << data->mem_r_addr << ":"; for(UINT32 i = 0; i < data->mem_r_size; i++) { - *OutFile << std::setw(2) << std::setfill('0') << std::hex << ((unsigned char)data->m_scratch[i] & 0xff); + *OutFile << std::hex << std::setw(2) << std::setfill('0') << ((unsigned char)data->m_scratch[i] & 0xff); } data->mem_r_size = 0; @@ -257,10 +262,10 @@ VOID record_diff(const CONTEXT * cpu, ADDRINT pc, VOID* v) memset(data->m_scratch, 0, data->mem_r2_size); PIN_SafeCopy(data->m_scratch, (const VOID *)data->mem_r2_addr, data->mem_r2_size); - *OutFile << ",mr=0x" << std::hex << data->mem_r2_addr << ":"; + *OutFile << ",mr=0x" << data->mem_r2_addr << ":"; for(UINT32 i = 0; i < data->mem_r2_size; i++) { - *OutFile << std::setw(2) << std::setfill('0') << std::hex << ((unsigned char)data->m_scratch[i] & 0xff); + *OutFile << std::hex << std::setw(2) << std::setfill('0') << ((unsigned char)data->m_scratch[i] & 0xff); } data->mem_r2_size = 0; @@ -271,10 +276,10 @@ VOID record_diff(const CONTEXT * cpu, ADDRINT pc, VOID* v) memset(data->m_scratch, 0, data->mem_w_size); PIN_SafeCopy(data->m_scratch, (const VOID *)data->mem_w_addr, data->mem_w_size); - *OutFile << ",mw=0x" << std::hex << data->mem_w_addr << ":"; + *OutFile << ",mw=0x" << data->mem_w_addr << ":"; for(UINT32 i = 0; i < data->mem_w_size; i++) { - *OutFile << std::setw(2) << std::setfill('0') << std::hex << ((unsigned char)data->m_scratch[i] & 0xff); + *OutFile << std::hex << std::setw(2) << std::setfill('0') << ((unsigned char)data->m_scratch[i] & 0xff); } data->mem_w_size = 0; @@ -378,6 +383,8 @@ static VOID Fini(INT32 code, VOID *v) for (const auto& data : context.m_terminated_threads) { data->m_trace->close(); } + + g_log->close(); } int main(int argc, char * argv[]) { @@ -390,13 +397,18 @@ int main(int argc, char * argv[]) { std::cerr << "Error initializing PIN, PIN_Init failed!" << std::endl; return -1; } + + auto logFile = KnobOutputFilePrefix.Value() + ".log"; + g_log = new ofstream; + g_log->open(logFile.c_str()); + *g_log << std::hex; // Initialize the tool context ToolContext *context = new ToolContext(); context->m_images = new ImageManager(); for (unsigned i = 0; i < KnobModuleWhitelist.NumberOfValues(); ++i) { - std::cout << "White-listing image: " << KnobModuleWhitelist.Value(i) << std::endl; + *g_log << "White-listing image: " << KnobModuleWhitelist.Value(i) << '\n'; context->m_images->addWhiteListedImage(KnobModuleWhitelist.Value(i)); context->m_tracing_enabled = false; }