Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
gaasedelen committed Sep 14, 2021
2 parents 352237b + 672ff0c commit 62c0bec
Show file tree
Hide file tree
Showing 25 changed files with 2,921 additions and 1,307 deletions.
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.

<p align="center">
<img alt="Placing a breakpoint on the current instruction" src="screenshots/trace_breakpoints.gif"/>
Expand All @@ -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*.

<p align="center">
<img alt="Exploring memory accesses using memory breakpoints" src="screenshots/memory_breakpoint.gif"/>
</p>

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.

Expand All @@ -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.

<p align="center">
<img alt="Memory region access breakpoints" src="screenshots/region_breakpoints.gif"/>
Expand Down Expand Up @@ -124,7 +125,7 @@ A simple 'shell' is provided to navigate to specific timestamps in the trace. Pa
<img alt="Seeking around the trace using the timestamp shell" src="screenshots/idx_shell.gif"/>
</p>

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

Expand Down
135 changes: 83 additions & 52 deletions plugins/tenet/breakpoints.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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):
"""
Expand All @@ -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)

Expand All @@ -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):
"""
Expand All @@ -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)
68 changes: 58 additions & 10 deletions plugins/tenet/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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):
"""
Expand Down
Loading

0 comments on commit 62c0bec

Please sign in to comment.