diff --git a/README.md b/README.md
index c340270..81fe199 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,16 @@
# Tenet - A Trace Explorer for Reverse Engineers
+
+## IDA
+
+
+
+
+
+## Binja
+
-
+
## Overview
diff --git a/plugins/tenet/LICENSE b/plugins/tenet/LICENSE
new file mode 100644
index 0000000..768a274
--- /dev/null
+++ b/plugins/tenet/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2021 Markus Gaasedelen
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/plugins/tenet/README.md b/plugins/tenet/README.md
new file mode 100644
index 0000000..c340270
--- /dev/null
+++ b/plugins/tenet/README.md
@@ -0,0 +1,201 @@
+# Tenet - A Trace Explorer for Reverse Engineers
+
+
+
+
+
+## Overview
+
+Tenet is an [IDA Pro](https://www.hex-rays.com/products/ida/) plugin for exploring execution traces. The goal of this plugin is to provide more natural, human controls for navigating execution traces against a given binary. The basis of this work stems from the desire to research new or innovative methods to examine and distill complex execution patterns in software.
+
+For more context about this project, please read the [blogpost](http://blog.ret2.io/2021/04/20/tenet-trace-explorer/) about its initial release.
+
+Special thanks to [QIRA](https://github.com/geohot/qira) / [geohot](https://twitter.com/realGeorgeHotz) et al. for the inspiration.
+
+## Releases
+
+* v0.2 -- Imagebase detection, cell visualization, breakpoint refactor, bugfixes.
+* v0.1 -- Initial release
+
+# Installation
+
+Tenet is a cross-platform (Windows, macOS, Linux) Python 3 plugin. It takes zero third party dependencies, making the code both portable and easy to install.
+
+1. From your disassembler's python console, run the following command to find its plugin directory:
+ - **IDA Pro**: `import idaapi, os; os.path.join(idaapi.get_user_idadir(), "plugins")`
+
+2. Copy the contents of this repository's `/plugins/` folder to the listed directory.
+3. Restart your disassembler.
+
+This plugin is only supported for IDA 7.5 and newer.
+
+# Usage
+
+Once properly installed, there will be a new menu entry available in the disassembler. This can be used to load externally-collected execution traces into Tenet.
+
+
+
+
+
+As this is the initial release, Tenet only accepts simple human-readable text traces. Please refer to the [tracing readme](https://github.com/gaasedelen/tenet/tree/master/tracers) in this repository for additional information on the trace format, limitations, and reference tracers.
+
+## Bidirectional Exploration
+
+While using Tenet, the plugin will 'paint' trails to indicate the flow of execution forwards (blue) and backwards (red) from your present position in the active execution trace.
+
+
+
+
+
+To `step` forwards or backwards through time, you simply *scroll while hovering over the timeline* on the right side of the disassembler. To `step over` function calls, hold `SHIFT` while scrolling.
+
+## Trace Timeline
+
+The trace timeline will be docked on the right side of the disassembler. This widget is used to visualize different types of events along the trace timeline and perform basic navigation as described above.
+
+
+
+
+
+By *clicking and dragging across the timeline*, it is possible to zoom in on a specific section of the execution trace. This action can be repeated any number of times to reach the desired granularity.
+
+## Execution Breakpoints
+
+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.
+
+
+
+
+
+To jump between executions, *scroll up or down while hovering the highlighted instruction pointer*.
+
+Additionally, you can *right click in the disassembly listing* and select one of the navigation-based menu entries to quickly seek to the execution of an instruction of interest.
+
+
+
+
+
+IDA's native `F2` hotkey can also be used to set breakpoints on arbitrary instructions.
+
+## Memory Breakpoints
+
+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*.
+
+
+
+
+
+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.
+
+
+
+
+
+To navigate the memory view to an arbitrary address, click onto the memory view and hit `G` to enter either an address or database symbol to seek the view to.
+
+## Region Breakpoints
+
+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.
+
+
+
+
+
+As with normal memory breakpoints, hovering the region and *scrolling* can used to traverse between the accesses made to the selected region of memory.
+
+## Register Seeking
+
+In reverse engineering, it's pretty common to encounter situations where you ask yourself *"Which instruction set this register to its current value?"*
+
+Using Tenet, you can seek backwards to that instruction in a single click.
+
+
+
+
+
+Seeking backwards is by far the most common direction to navigate across register changes... but for dexterity you can also seek forward to the next register assignment using the blue arrow on the right of the register.
+
+## Timestamp Shell
+
+A simple 'shell' is provided to navigate to specific timestamps in the trace. Pasting (or typing...) a timestamp into the shell with or without commas will suffice.
+
+
+
+
+
+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
+
+Tenet ships with two default themes -- a 'light' theme, and a 'dark' one. Depending on the colors currently used by your disassembler, Tenet will attempt to select the theme that seems most appropriate.
+
+
+
+
+
+The theme files are stored as simple JSON on disk and are highly configurable. If you are not happy with the default themes or colors, you can create your own themes and simply drop them in the user theme directory.
+
+Tenet will remember your theme preference for future loads and uses.
+
+# FAQ
+
+#### Q: How do I record an execution trace using Tenet?
+
+* *A: Tenet is a trace reader, not a trace recorder. You will have to use dynamic binary instrumentation frameworks (or other related technologies) to generate a compatible execution trace. Please refer to the [tracing](https://github.com/gaasedelen/tenet/tree/master/tracers) readme for more information on existing tracers, or how to implement your own.*
+
+#### Q: What trace architectures does Tenet support loading?
+
+* *A: Only x86 and AMD64, but the codebase is almost entirely architecture agnostic.*
+
+#### Q: How big of a trace file can Tenet load / navigate?
+
+* *A: Tenet's trace reader is pure python, it was written as an MVP. There is no guarantee that traces which exceed 10 million instructions will be reasonable to navigate until a native backend replaces it.*
+
+#### Q: I loaded an execution trace, now there is a '.tt' file. What is it?
+
+* *A: When Tenet loads a given text trace, it will parse, index, and compress the trace into a more performant format. On subsequent loads, Tenet will attempt to load the '.tt' file which should load in fraction of the time that it would take to load the original text trace.*
+
+#### Q: The plugin crashed / threw an error / is showing bad trace information, what should I do?
+
+* *A: If you encounter an issue or inaccuracy that can be reproduced, please file an issue against this repository and upload a sample trace + executable.*
+
+#### Q: Memory in my trace is changing, but there are no writes to the region. Is this a bug!?
+
+* *A: Your log file may not have captured all memory writes. For example, usermode DBI generally do not get a memory callback for external writes to process memory. This is most common when reading from a file, or from socket -- it is the kernel that writes memory into your designated usermode buffer, making the event invisible to traditional instrumentation.*
+ * Microsoft TTD generally exhibits the same behavior, it's tricky to solve without modeling syscalls.
+
+#### Q: Will this be ported to Binary Ninja / Ghidra / ... ?
+
+* *A: Possibly, but not anytime soon (unless there is __significant__ incentive). As a research oriented project, the driving motivation is on developing novel strategies to organize and explore program execution -- not porting them.*
+
+#### Q: My organization would like to support this project, how can we help?
+
+* *A: Without funding, the time I can devote to this project is limited. If your organization is excited by the ideas put forth here and capable of providing capital to sponsor dedicated R&D, please [contact us](https://ret2.io/contact).*
+
+# Future Work
+
+Time and ~~motivation~~ funding permitting, future work may include:
+
+* Filtering / coagulating library calls from traces
+* Pointer analysis (e.g. annotations) for the register / stack views
+* Native TraceFile & TraceReader implementations (e.g. bigger and faster traces)
+* Navigation history + bookmarks view (maybe 2-in-1?)
+* Richer trace informatics, more aggressive indexing of relevant events (e.g. function calls)
+* Trace cartography, improved summarization and representation of trace geography
+* Make the 'cpu architecture' selection/detection slightly less hardcoded
+* More out-of-the-box tracing bridges, DynamoRIO, TTD, RR, QEMU, Bochs, ...
+* Support for Hex-Rays / decompiled views (besides basic view sync)
+* Improved workflow for automatically loading or iterating on traces
+* Differential analysis, high level 'trace diffing'
+* Better navigation and breakdown of threads, quantum's
+* Better support for navigating 'multi module' traces (e.g. full system traces)
+* Binary Ninja support
+* ... ?
+
+I welcome external contributions, issues, and feature requests. Please make any pull requests to the `develop` branch of this repository if you would like them to be considered for a future release.
+
+# Authors
+
+* Markus Gaasedelen ([@gaasedelen](https://twitter.com/gaasedelen))
diff --git a/plugins/tenet/__init__.py b/plugins/tenet/__init__.py
index e69de29..b78b90b 100644
--- a/plugins/tenet/__init__.py
+++ b/plugins/tenet/__init__.py
@@ -0,0 +1 @@
+get_context = lambda x: None
\ No newline at end of file
diff --git a/plugins/tenet/breakpoints.py b/plugins/tenet/breakpoints.py
index 37ed00b..cb2a190 100644
--- a/plugins/tenet/breakpoints.py
+++ b/plugins/tenet/breakpoints.py
@@ -3,8 +3,8 @@
from tenet.ui import *
from tenet.types import BreakpointType, BreakpointEvent, TraceBreakpoint
from tenet.util.misc import register_callback, notify_callback
-from tenet.integration.api import DockableWindow
-from tenet.integration.api import disassembler
+from tenet.util.disassembler import DockableWindow
+from tenet.util.disassembler import disassembler
#------------------------------------------------------------------------------
# breakpoints.py -- Breakpoint Controller
diff --git a/plugins/tenet/context.py b/plugins/tenet/context.py
index 340e5bd..3533456 100644
--- a/plugins/tenet/context.py
+++ b/plugins/tenet/context.py
@@ -13,9 +13,9 @@
from tenet.ui.trace_view import TraceDock
from tenet.types import BreakpointType
-from tenet.trace.arch import ArchAMD64, ArchX86
+from tenet.trace.arch import ArchAMD64, ArchX86, ArchARM
from tenet.trace.reader import TraceReader
-from tenet.integration.api import disassembler, DisassemblerContextAPI
+from tenet.util.disassembler import disassembler, DisassemblerContextAPI
logger = logging.getLogger("Tenet.Context")
@@ -51,10 +51,17 @@ def __init__(self, core, db):
self.db = db
# select a trace arch based on the binary the disassmbler has loaded
- if disassembler[self].is_64bit():
- self.arch = ArchAMD64()
- else:
- self.arch = ArchX86()
+ if disassembler[self].is_64bit():
+ if disassembler[self].is_arm():
+ # TODO
+ self.arch = None
+ else:
+ self.arch = ArchAMD64()
+ else: # 32 bit archs
+ if disassembler[self].is_arm():
+ self.arch = ArchARM
+ else:
+ self.arch = ArchX86()
# this will hold the trace reader when a trace has been loaded
self.reader = None
@@ -154,7 +161,7 @@ def load_trace(self, filepath):
# trace and querying information (memory, registers) from it at
# chosen states of execution
#
-
+ pmsg(f"Loading trace")
self.reader = TraceReader(filepath, self.arch, disassembler[self])
pmsg(f"Loaded trace {self.reader.trace.filepath}")
pmsg(f"- {self.reader.trace.length:,} instructions...")
@@ -238,9 +245,9 @@ def show_ui(self):
outside of the disassembler integration files. it doesn't really
matter much right now but this should be moved in the future.
"""
- import ida_kernwin
- self.registers.show(position=ida_kernwin.DP_RIGHT)
-
+ # import ida_kernwin
+ # self.registers.show(position=ida_kernwin.DP_RIGHT)
+ disassembler.show_registers(self.registers)
#self.breakpoints.dockable.set_dock_position("CPU Registers", ida_kernwin.DP_BOTTOM)
#self.breakpoints.dockable.show()
@@ -249,13 +256,16 @@ def show_ui(self):
#ida_kernwin.set_dock_pos("IPython Console", "Output", ida_kernwin.DP_INSIDE)
#self.memory.dockable.set_dock_position("Output window", ida_kernwin.DP_TAB | ida_kernwin.DP_BEFORE)
- self.memory.show("Output window", ida_kernwin.DP_TAB | ida_kernwin.DP_BEFORE)
+ # self.memory.show("Output window", ida_kernwin.DP_TAB | ida_kernwin.DP_BEFORE)
+ disassembler.show_memory(self.memory)
#self.stack.dockable.set_dock_position("Memory View", ida_kernwin.DP_RIGHT)
- self.stack.show("Memory View", ida_kernwin.DP_RIGHT)
+ # self.stack.show("Memory View", ida_kernwin.DP_RIGHT)
+ disassembler.show_stack(self.stack)
mw = get_qmainwindow()
- mw.addToolBar(QtCore.Qt.RightToolBarArea, self.trace)
+ mw.addToolBar(QtCore.Qt.LeftToolBarArea, self.trace)
+ # mw.addToolBar(QtCore.Qt.RightToolBarArea, self.trace)
self.trace.show()
# trigger update check
diff --git a/plugins/tenet/hex.py b/plugins/tenet/hex.py
index bd8b173..58a5d08 100644
--- a/plugins/tenet/hex.py
+++ b/plugins/tenet/hex.py
@@ -1,7 +1,7 @@
from tenet.ui import *
from tenet.types import *
from tenet.util.qt.util import copy_to_clipboard
-from tenet.integration.api import DockableWindow
+from tenet.util.disassembler import MemoryGlobalAreaWidget, DockableWindow
#------------------------------------------------------------------------------
# hex.py -- Hex Dump Controller
@@ -52,14 +52,11 @@ def show(self, target=None, position=0):
# then we are free to create new UI elements to take the place of
# anything that once was
+ # from binaryninjaui import GlobalArea
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
- # previous close of this window type) steal its dock positon so we can
- # hopefully take the same place as the old one
- #
+ print(self._title)
+ new_dockable = MemoryGlobalAreaWidget(self._title, self.view)
+ # new_dockable2 = MemoryGlobalAreaWidget(self._title, self.view)
if self.dockable:
new_dockable.copy_dock_position(self.dockable)
@@ -68,7 +65,23 @@ def show(self, target=None, position=0):
# make the dockable/widget visible
self.dockable = new_dockable
- self.dockable.show()
+ # GlobalArea.addWidget(lambda context: MemoryGlobalAreaWidget(self._title, self.view))
+ new_dockable.show()
+ # new_dockable2.show()
+ #
+ # 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
+ #
+
+ # if self.dockable:
+ # new_dockable.copy_dock_position(self.dockable)
+ # elif (target or position):
+ # new_dockable.set_dock_position(target, position)
+
+ # # make the dockable/widget visible
+ # self.dockable = new_dockable
+ # self.dockable.show()
def hide(self):
"""
diff --git a/plugins/tenet/hex_sidebar.py b/plugins/tenet/hex_sidebar.py
new file mode 100644
index 0000000..d8b4e01
--- /dev/null
+++ b/plugins/tenet/hex_sidebar.py
@@ -0,0 +1,305 @@
+from tenet.ui import *
+from tenet.types import *
+from tenet.util.qt.util import copy_to_clipboard
+from tenet.util.disassembler import StackMiniGraphWidgetType
+
+#------------------------------------------------------------------------------
+# hex.py -- Hex Dump Controller
+#------------------------------------------------------------------------------
+#
+# The purpose of this file is to house the 'headless' components of a
+# basic hex dump window and its underlying functionality. This is split
+# into a model and controller component, of a typical 'MVC' design pattern.
+#
+# This provides much of the core logic behind both the memory and stack
+# views used by the plugin.
+#
+
+class HexSidebarController(object):
+ """
+ A generalized controller for Hex View based window.
+ """
+
+ def __init__(self, pctx):
+ self.pctx = pctx
+ self.model = HexModel(pctx)
+ self.reader = None
+
+ # UI components
+ self.view = None
+ self.dockable = None
+ self._title = ""
+
+ # signals
+ self._ignore_signals = False
+ pctx.breakpoints.model.breakpoints_changed(self._breakpoints_changed)
+
+ def show(self, target=None, position=0):
+ """
+ Make the window attached to this controller visible.
+ """
+
+ # if there is no Qt (eg, our UI framework...) then there is no UI
+ if not QT_AVAILABLE:
+ return
+
+ # the UI has already been created, and is also visible. nothing to do
+ if (self.dockable and self.dockable.visible):
+ return
+
+ #
+ # if the UI has not yet been created, or has been previously closed
+ # then we are free to create new UI elements to take the place of
+ # anything that once was
+
+ self.view = HexView(self, self.model)
+ new_dockable = StackMiniGraphWidgetType(self._title, self.view)
+
+ #
+ # 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
+ #
+
+ if self.dockable:
+ new_dockable.copy_dock_position(self.dockable)
+ elif (target or position):
+ new_dockable.set_dock_position(target, position)
+
+ # make the dockable/widget visible
+ self.dockable = new_dockable
+ self.dockable.show()
+
+ def hide(self):
+ """
+ Hide the window attached to this controller.
+ """
+
+ # 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 a trace reader to this controller.
+ """
+ self.reader = reader
+ self.model.pointer_size = reader.arch.POINTER_SIZE
+
+ # attach trace reader signals to this controller / window
+ reader.idx_changed(self._idx_changed)
+
+ #
+ # directly call our event handler quick with the current idx since
+ # 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):
+ """
+ Detach the trace reader from this controller.
+ """
+ self.reader = None
+ self.model.reset()
+
+ def navigate(self, address):
+ """
+ Navigate the hex view to a given address.
+ """
+ if address < 0:
+ address = 0
+
+ last_visible_address = address + self.model.data_size
+ if last_visible_address > 0xFFFFFFFFFFFFFFFF:
+ last_visible_address = 0xFFFFFFFFFFFFFFFF
+
+ self.model.address = address
+
+ #self.reset_selection(0)
+ self.refresh_memory()
+
+ def set_data_size(self, num_bytes):
+ """
+ Change the number of bytes to be held / displayed by the viewer.
+ """
+ self.model.data_size = num_bytes
+ self.refresh_memory()
+
+ def copy_selection(self, start_address, end_address):
+ """
+ Copy the selected range of bytes to the system clipboard.
+ """
+ assert end_address > start_address
+ if not self.reader:
+ return ''
+
+ # fetch memory for the selected region
+ num_bytes = end_address - start_address
+ memory = self.reader.get_memory(start_address, num_bytes)
+
+ # dump bytes to hex
+ output = []
+ for i in range(num_bytes):
+ if memory.mask[i] == 0xFF:
+ output.append("%02X" % memory.data[i])
+ else:
+ output.append("??")
+
+ byte_string = ' '.join(output)
+ copy_to_clipboard(byte_string)
+
+ return byte_string
+
+ def pin_memory(self, address, access_type=BreakpointType.ACCESS, length=1):
+ """
+ Pin a region of memory.
+ """
+ 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):
+ """
+ Refresh the visible memory.
+ """
+ if not self.reader:
+ self.model.data = None
+ self.model.mask = None
+ return
+
+ memory = self.reader.get_memory(self.model.address, self.model.data_size)
+
+ self.model.data = memory.data
+ self.model.mask = memory.mask
+ self.model.delta = self.reader.delta
+
+ if self.view:
+ self.view.refresh()
+
+ def set_fade_threshold(self, address):
+ """
+ Change the threshold address that the view will begin to 'fade' its contents.
+
+ This is used to 'fade' the unallocated region of the stack, for example.
+ """
+ self.model.fade_address = address
+
+ #-------------------------------------------------------------------------
+ # Callbacks
+ #-------------------------------------------------------------------------
+
+ def _idx_changed(self, idx):
+ """
+ The trace reader position has been changed.
+ """
+ self.refresh_memory()
+
+ def _breakpoints_changed(self):
+ """
+ Handle breakpoints changed event.
+ """
+ if not self.view:
+ return
+
+ if self._ignore_signals:
+ return
+
+ self.view.refresh()
+
+class HexModel(object):
+ """
+ A generalized model for Hex View based window.
+ """
+
+ def __init__(self, pctx):
+ self._pctx = pctx
+
+ # how the hex (data) and auxillary text should be displayed
+ self._hex_format = HexType.BYTE
+ self._aux_format = AuxType.ASCII
+
+ # view settings
+ self._num_bytes_per_line = 16
+
+ # initialize the remaining model parameters
+ self.reset()
+
+ def reset(self):
+ """
+ Reset the model to a clean state.
+ """
+
+ # the 'cached' data to be displayed by the hex view
+ self.data = None
+ self.mask = None
+ self.data_size = 0
+ self.delta = None
+
+ self.address = 0
+ self.fade_address = 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):
+ """
+ Return the number of bytes that should be displayed per line.
+ """
+ return self._num_bytes_per_line
+
+ @num_bytes_per_line.setter
+ def num_bytes_per_line(self, width):
+ """
+ Set the number of bytes to be displayed per line.
+ """
+
+ if width < 1:
+ raise ValueError("Invalid bytes per line value (must be > 0)")
+
+ 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
+ #self._refresh_view_settings()
+
+ @property
+ def hex_format(self):
+ return self._hex_format
+
+ @hex_format.setter
+ def hex_format(self, value):
+ if value == self._hex_format:
+ return
+ self._hex_format = value
+ #self.refresh()
+
+ @property
+ def aux_format(self):
+ return self._aux_format
+
+ @aux_format.setter
+ def aux_format(self, value):
+ if value == self._aux_format:
+ return
+ self._aux_format = value
+ #self.refresh()
\ No newline at end of file
diff --git a/plugins/tenet/integration/__init__.py b/plugins/tenet/integration/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/plugins/tenet/integration/binja_integration.py b/plugins/tenet/integration/binja_integration.py
new file mode 100644
index 0000000..a32d128
--- /dev/null
+++ b/plugins/tenet/integration/binja_integration.py
@@ -0,0 +1,493 @@
+import ctypes
+import logging
+
+from binaryninja import PluginCommand, HighlightStandardColor, HighlightColor, BinaryView
+from binaryninjaui import UIAction, UIActionHandler, Menu
+
+from binaryninjaui import GlobalAreaWidget, GlobalArea, UIContext
+
+from tenet.integration.core import TenetCore
+from tenet.types import BreakpointEvent
+from tenet.context import TenetContext
+from tenet.util.misc import register_callback, notify_callback, is_plugin_dev
+from tenet.util.qt import *
+from tenet.util.disassembler import disassembler
+
+logger = logging.getLogger("Tenet.Binja.Integration")
+
+#------------------------------------------------------------------------------
+# Tenet Binja Integration
+#---------------------------/DOckab---------------------------------------------------
+
+class TenetBinja(TenetCore, GlobalAreaWidget):
+ """
+ Tenet UI Integration for Binary Ninja.
+ """
+ def __init__(self):
+ TenetCore.__init__(self)
+ # Make invisible widget so we can get view changes
+ # print(GlobalArea)
+ GlobalAreaWidget.__init__(self, 'Tenet highlighter')
+ self.highlighted = set()
+ self._ui_breakpoint_changed_callbacks = []
+
+ def create_widget(self):
+ return self
+
+ def get_context(self, dctx=None, startup=True):
+ """
+ Get the TenetContext object for a given database context.
+ In Binary Ninja, a dctx is a BinaryView (BV).
+ """
+ if not isinstance(dctx, BinaryView):
+ dctx = disassembler.binja_get_bv_from_dock()
+
+ dctx_id = ctypes.addressof(dctx.handle.contents)
+
+ #
+ # create a new TenetContext if this is the first time a context
+ # has been requested for this BNDB / bv
+ #
+
+ if dctx_id not in self.contexts:
+
+ # create a new 'context' representing this BNDB / bv
+ lctx = TenetContext(self, dctx)
+ if startup:
+ context = UIContext.allContexts()[0]
+ ga = context.globalArea()
+ ga.addWidget(self.create_widget())
+ lctx.start()
+
+ # save the created ctx for future calls
+ self.contexts[dctx_id] = lctx
+
+ #
+ # for binja, we basically *never* want to start the tenet ctx
+ # when it is first created. this is because binja will *immediately*
+ # create a coverage overview widget for every database when it is
+ # first opened.
+ #
+ # this is annoying, because we don't want to actually start up all
+ # of the tenet threads and subsystems unless the user actually
+ # starts trying to use tenet for their session.
+ #
+ # so we initialize the tenet context (with start()) on the
+ # second context request which will go throught the else block
+ # below... any subsequent call to start() is effectively a nop!
+ #
+
+ else:
+ lctx = self.contexts[dctx_id]
+ lctx.start()
+
+ # return the tenet context object for this database ctx / bv
+ return lctx
+
+ def binja_close_context(self, dctx):
+ """
+ Attempt to close / spin-down the TenetContext for the given dctx.
+ In Binary Ninja, a dctx is a BinaryView (BV).
+ """
+ dctx_id = ctypes.addressof(dctx.handle.contents)
+
+ # fetch the TenetContext for the closing BNDB
+ try:
+ lctx = self.tenet_contexts.pop(dctx_id)
+
+ #
+ # if tenet was not actually used for this BNDB / session, then
+ # the lookup will fail as there is nothing to spindown
+ #
+
+ except KeyError:
+ return
+
+ # spin down the closing context (stop threads, cleanup qt state, etc)
+ logger.info("Closing a TenetContext...")
+ lctx.terminate()
+
+ #--------------------------------------------------------------------------
+ # UI Integration (Internal)
+ #--------------------------------------------------------------------------
+
+ #
+ # TODO / HACK / XXX / V35: Some of Binja's UI elements (such as the
+ # terminal) do not get assigned a BV, even if there is only one open.
+ #
+ # this is problematic, because if the user 'clicks' onto the termial, and
+ # then tries to execute our UIActions (like 'Load Coverage File'), the
+ # given 'context.binaryView' will be None
+ #
+ # in the meantime, we have to use this workaround that will try to grab
+ # the 'current' bv from the dock. this is not ideal, but it will suffice.
+ #
+ # TODO <<<<<<<<<<<<<<<<<<
+ def _interactive_load_trace(self, context):
+ dctx = disassembler.binja_get_bv_from_dock()
+ if not dctx:
+ disassembler.warning("Tenet requires an open BNDB to load coverage.")
+ return
+ super()._interactive_load_trace(dctx)
+
+ def _interactive_load_batch(self, context):
+ dctx = disassembler.binja_get_bv_from_dock()
+ if not dctx:
+ disassembler.warning("Tenet requires an open BNDB to load coverage.")
+ return
+ super(TenetBinja, self).interactive_load_batch(dctx)
+
+ def _open_coverage_xref(self, dctx, addr):
+ super(TenetBinja, self).open_coverage_xref(addr, dctx)
+
+ def _is_xref_valid(self, dctx, addr):
+
+ #
+ # this is a special case where we check if the ctx exists rather than
+ # blindly creating a new one. again, this is because binja may call
+ # this function at random times to decide whether it should display the
+ # XREF menu option.
+ #
+ # but asking whether or not the xref menu option should be shown is not
+ # a good indidication of 'is the user actually using tenet' so we
+ # do not want this to be one that creates tenet contexts
+ #
+
+ dctx_id = ctypes.addressof(dctx.handle.contents)
+ lctx = self.tenet_contexts.get(dctx_id, None)
+ if not lctx:
+ return False
+
+ # return True if there appears to be coverage loaded...
+ return bool(lctx.director.coverage_names)
+
+ def _open_coverage_overview(self, context):
+ dctx = disassembler.binja_get_bv_from_dock()
+ if not dctx:
+ disassembler.warning("Tenet requires an open BNDB to open the overview.")
+ return
+ super(TenetBinja, self).open_coverage_overview(dctx)
+
+ #--------------------------------------------------------------------------
+ # Binja Actions
+ #--------------------------------------------------------------------------
+
+ ACTION_LOAD_TRACE = "Tenet\\load_trace"
+ ACTION_FIRST_EXECUTION = "Tenet\\first_execution"
+ ACTION_FINAL_EXECUTION = "Tenet\\final_execution"
+ ACTION_NEXT_EXECUTION = "Tenet\\next_execution"
+ ACTION_PREV_EXECUTION = "Tenet\\prev_execution"
+
+ def _install_load_trace(self):
+ action = self.ACTION_LOAD_TRACE
+ UIAction.registerAction(action)
+ UIActionHandler.globalActions().bindAction(action, UIAction(self._interactive_load_trace))
+ Menu.mainMenu("Tools").addAction(action, "Loading", 0)
+ logger.info("Installed the 'load_trace' menu entry")
+
+ def _install_first_execution(self):
+ action = self.ACTION_FIRST_EXECUTION
+ UIAction.registerAction(action)
+ UIActionHandler.globalActions().bindAction(action, UIAction(self._interactive_first_execution))
+ Menu.mainMenu("Tools").addAction(action, "Loading", 1)
+ logger.info("Installed the 'first_execution' menu entry")
+
+ def _install_final_execution(self):
+ action = self.ACTION_FINAL_EXECUTION
+ UIAction.registerAction(action)
+ UIActionHandler.globalActions().bindAction(action, UIAction(self._interactive_final_execution))
+ Menu.mainMenu("Tools").addAction(action, "Loading", 1)
+ logger.info("Installed the 'final_execution' menu entry")
+
+ def _install_next_execution(self):
+ action = self.ACTION_NEXT_EXECUTION
+ UIAction.registerAction(action)
+ UIActionHandler.globalActions().bindAction(action, UIAction(self._interactive_next_execution))
+ Menu.mainMenu("Tools").addAction(action, "Loading", 1)
+ logger.info("Installed the 'next_execution' menu entry")
+
+ def _install_prev_execution(self):
+ action = self.ACTION_PREV_EXECUTION
+ UIAction.registerAction(action)
+ UIActionHandler.globalActions().bindAction(action, UIAction(self._interactive_prev_execution))
+ Menu.mainMenu("Tools").addAction(action, "Loading", 1)
+ logger.info("Installed the 'prev_execution' menu entry")
+
+
+ # NOTE/V35: Binja doesn't really 'unload' plugins, so whatever...
+ def _uninstall_load_file(self):
+ pass
+ def _uninstall_load_batch(self):
+ pass
+ def _uninstall_open_coverage_xref(self):
+ pass
+ def _uninstall_open_coverage_overview(self):
+ pass
+
+ #-------------------------------------------------------------------------
+ # UI Event Overrides
+ #-------------------------------------------------------------------------
+ #! Override the function calls so we can see when breakpoints change
+ #! No clue if this actually works or not, but i know this is what
+ #! its supposed to be used for
+ def tag_added(self, view, tag, ref_type, auto_defined, addr,
+ arch=None, func=None) -> None:
+ self._breakpoint_changed("added", addr)
+
+ def tag_removed(self, view, tag, ref_type, auto_defined, addr,
+ arch=None, func=None) -> None:
+ self._breakpoint_changed("removed", addr)
+
+ def tag_updated(self, view, tag, ref_type, auto_defined, addr,
+ arch=None, func=None) -> None:
+ self._breakpoint_changed("updated", addr, tag.data)
+ #--------------------------------------------------------------------------
+ # UI Event Handlers
+ #--------------------------------------------------------------------------
+
+ def _breakpoint_changed(self, tag, code, addr, state=None):
+ """
+ (Event) Breakpoint changed.
+ """
+ if code == "added":
+ self._notify_ui_breakpoint_changed(addr, BreakpointEvent.ADDED)
+
+ elif code == "updated":
+ if state=="enabled":
+ self._notify_ui_breakpoint_changed(addr, BreakpointEvent.ENABLED)
+ else:
+ self._notify_ui_breakpoint_changed(addr, BreakpointEvent.DISABLED)
+
+ elif code == "removed":
+ self._notify_ui_breakpoint_changed(addr, BreakpointEvent.REMOVED)
+ return 0
+
+ def _popup_hook(self, widget, popup):
+ # """
+ # (Event) IDA is about to show a popup for the given TWidget.
+ # """
+
+ # # TODO: return if plugin/trace is not active
+ # pass
+
+ # # fetch the (IDA) window type (eg, disas, graph, hex ...)
+ # view_type = ida_kernwin.get_widget_type(widget)
+
+ # # only attach these context items to popups in disas views
+ # if view_type == ida_kernwin.BWN_DISASMS:
+
+ # # prep for some shady hacks
+ # p_qmenu = ctypes.cast(int(popup), ctypes.POINTER(ctypes.c_void_p))[0]
+ # qmenu = sip.wrapinstance(int(p_qmenu), QtWidgets.QMenu)
+
+ # #
+ # # inject and organize the Tenet plugin actions
+ # #
+
+ # ida_kernwin.attach_action_to_popup(
+ # widget,
+ # popup,
+ # self.ACTION_NEXT_EXECUTION, # The action ID (see above)
+ # "Rename", # Relative path of where to add the action
+ # ida_kernwin.SETMENU_APP # We want to append the action after ^
+ # )
+
+ # #
+ # # this is part of our bodge to inject a plugin action submenu
+ # # at a specific location in the QMenu, cuz I don't think it's
+ # # actually possible with the native IDA API's (for groups...)
+ # #
+
+ # for action in qmenu.actions():
+ # if action.text() == "Go to next execution":
+
+ # # inject a group for the exta 'go to' actions
+ # goto_submenu = QtWidgets.QMenu("Go to...")
+ # qmenu.insertMenu(action, goto_submenu)
+
+ # # hold a Qt ref of the submenu so it doesn't GC
+ # self.__goto_submenu = goto_submenu
+ # break
+
+ # ida_kernwin.attach_action_to_popup(
+ # widget,
+ # popup,
+ # self.ACTION_FIRST_EXECUTION, # The action ID (see above)
+ # "Go to.../", # Relative path of where to add the action
+ # ida_kernwin.SETMENU_APP # We want to append the action after ^
+ # )
+
+ # ida_kernwin.attach_action_to_popup(
+ # widget,
+ # popup,
+ # self.ACTION_FINAL_EXECUTION, # The action ID (see above)
+ # "Go to.../", # Relative path of where to add the action
+ # ida_kernwin.SETMENU_APP # We want to append the action after ^
+ # )
+
+ # ida_kernwin.attach_action_to_popup(
+ # widget,
+ # popup,
+ # self.ACTION_PREV_EXECUTION, # The action ID (see above)
+ # "Rename", # Relative path of where to add the action
+ # ida_kernwin.SETMENU_APP # We want to append the action after ^
+ # )
+
+ # #
+ # # inject a seperator to help insulate our plugin action group
+ # #
+
+ # for action in qmenu.actions():
+ # if action.text() == "Go to previous execution":
+ # qmenu.insertSeparator(action)
+ # break
+ pass
+
+ # def _render_lines(self, lines_out, widget, lines_in):
+ # # """
+ # # (Event) IDA is about to render code viewer lines.
+ # # """
+ # # widget_type = ida_kernwin.get_widget_type(widget)
+
+ # # if widget_type == ida_kernwin.BWN_DISASM:
+ # # self._highlight_disassesmbly(lines_out, widget, lines_in)
+
+ # return
+ def notifyOffsetChanged(self, address):
+ self._highlight_disassembly(address)
+
+
+ def _highlight_disassembly(self, current_address):
+ """
+ TODO/XXX this is pretty gross
+ """
+
+ dctx = disassembler.binja_get_bv_from_dock()
+ bv = dctx
+
+ ctx = self.get_context(dctx)
+ if not ctx.reader:
+ return
+
+ trail_length = 6
+
+ forward_color = self.palette.trail_forward
+ current_color = self.palette.trail_current
+ backward_color = self.palette.trail_backward
+
+ r, g, b, _ = current_color.getRgb()
+ current_color = HighlightColor(red=r,green=g,blue=b)
+
+ step_over = False
+ modifiers = QtGui.QGuiApplication.keyboardModifiers()
+ step_over = bool(modifiers & QtCore.Qt.ShiftModifier)
+
+ forward_ips = ctx.reader.get_next_ips(trail_length, step_over)
+ backward_ips = ctx.reader.get_prev_ips(trail_length, step_over)
+
+ backward_trail, forward_trail = {}, {}
+
+ trails = [
+ (backward_ips, backward_trail, backward_color),
+ (forward_ips, forward_trail, forward_color)
+ ]
+
+ for addresses, trail, color in trails:
+ for i, address in enumerate(addresses):
+ percent = 1.0 - ((trail_length - i) / trail_length)
+
+ # convert to bgr
+ r, g, b, _ = color.getRgb()
+ alpha = (0xFF - int(0xFF * percent))
+ binja_color = HighlightColor(alpha=alpha,red=r,green=g,blue=b)
+
+ # save the trail color
+ rebased_address = ctx.reader.analysis.rebase_pointer(address)
+ trail[rebased_address] = binja_color
+
+ current_address = ctx.reader.rebased_ip
+ if disassembler.is_mapped(current_address) == False:
+ 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)
+
+ # Will only highlight if in current function
+ #! This is really sloppy because if there are multiple functions
+ #! defined at an address, this won't work.
+ #! However the highlihgting does carry into function calls
+ #! which is kinda nice, but definitely unecessary
+
+ #? ideally the view has something like ida where we have an
+ #? address list of the lines in view or something
+ addresses = set()
+ addresses.add(current_address)
+
+ for address in backward_trail:
+ function = bv.get_functions_containing(address)[0]
+ color = backward_trail[address]
+ function.set_auto_instr_highlight(address,color)
+ self.highlighted.add(address)
+ addresses.add(address)
+
+ for address in forward_trail:
+ function = bv.get_functions_containing(address)[0]
+ color = forward_trail[address]
+ function.set_auto_instr_highlight(address,color)
+ self.highlighted.add(address)
+ addresses.add(address)
+
+ function = bv.get_functions_containing(current_address)[0]
+ function.set_auto_instr_highlight(current_address,current_color)
+ self.highlighted.add(current_address)
+
+ # Will clear highlight regardless of function
+ for address in self.highlighted.copy():
+ function = bv.get_functions_containing(address)[0]
+ if address not in addresses:
+ function.set_auto_instr_highlight(address, HighlightStandardColor.NoHighlightColor)
+ self.highlighted.remove(address)
+ # lines_out.entries.push_back(entry)
+ #----------------------------------------------------------------------
+ # Callbacks
+ #----------------------------------------------------------------------
+
+ def ui_breakpoint_changed(self, callback):
+ register_callback(self._ui_breakpoint_changed_callbacks, callback)
+ pass
+ def _notify_ui_breakpoint_changed(self, address, code):
+ notify_callback(self._ui_breakpoint_changed_callbacks, address, code)
+ pass
+#------------------------------------------------------------------------------
+# IDA UI Helpers
+#------------------------------------------------------------------------------
+
+class BinjaCtxEntry():
+ """
+ A minimal context menu entry class to utilize IDA's action handlers.
+ """
+
+ def __init__(self, action_function):
+ super(BinjaCtxEntry, self).__init__()
+ self.action_function = action_function
+
+ def activate(self, ctx):
+ # """
+ # Execute the embedded action_function when this context menu is invoked.
+
+ # NOTE: We pass 'None' to the action function to act as the '
+ # """
+ # self.action_function(IDA_GLOBAL_CTX)
+ return 1
+
+ def update(self, ctx):
+ """
+ Ensure the context menu is always available in IDA.
+ """
+ return 1
+
+
diff --git a/plugins/tenet/integration/binja_loader.py b/plugins/tenet/integration/binja_loader.py
new file mode 100644
index 0000000..828d41e
--- /dev/null
+++ b/plugins/tenet/integration/binja_loader.py
@@ -0,0 +1,32 @@
+import logging
+
+from tenet.util.log import lmsg
+from tenet.integration.binja_integration import TenetBinja
+
+logger = logging.getLogger("Tenet.Binja.Loader")
+
+#------------------------------------------------------------------------------
+# TenetBinja Loader
+#------------------------------------------------------------------------------
+#
+# The Binary Ninja plugin loading process is less involved compared to IDA.
+#
+# When Binary Ninja is starting up, it will import all python files placed
+# in its root plugin folder. It will then attempt to import any *directory*
+# in the plugin folder as a python module.
+#
+# For this reason, you may see Binary Ninja attempting to load 'tenet'
+# and 'tenet_plugin' in your console. This is normal due to the way
+# we have structured tenet and its loading process.
+#
+# In practice, tenet_plugin.py will import the contents of this file,
+# when Binary Ninja is starting up. As such, this is our only opportunity
+# to load & integrate tenet.
+#
+
+try:
+ tenet = TenetBinja()
+ tenet.load()
+except Exception as e:
+ lmsg("Failed to initialize tenet")
+ logger.exception("Exception details:")
\ No newline at end of file
diff --git a/plugins/tenet/core.py b/plugins/tenet/integration/core.py
similarity index 99%
rename from plugins/tenet/core.py
rename to plugins/tenet/integration/core.py
index 861900d..b345350 100644
--- a/plugins/tenet/core.py
+++ b/plugins/tenet/integration/core.py
@@ -4,7 +4,7 @@
from tenet.util.log import pmsg
from tenet.ui.palette import PluginPalette
from tenet.util.update import check_for_update
-from tenet.integration.api import disassembler
+from tenet.util.disassembler import disassembler
logger = logging.getLogger("Tenet.Core")
diff --git a/plugins/tenet/integration/ida_integration.py b/plugins/tenet/integration/ida_integration.py
index b865995..90e7dcb 100644
--- a/plugins/tenet/integration/ida_integration.py
+++ b/plugins/tenet/integration/ida_integration.py
@@ -395,11 +395,11 @@ def _render_lines(self, lines_out, widget, lines_in):
widget_type = ida_kernwin.get_widget_type(widget)
if widget_type == ida_kernwin.BWN_DISASM:
- self._highlight_disassesmbly(lines_out, widget, lines_in)
+ self._highlight_disassembly(lines_out, widget, lines_in)
return
- def _highlight_disassesmbly(self, lines_out, widget, lines_in):
+ def _highlight_disassembly(self, lines_out, widget, lines_in):
"""
TODO/XXX this is pretty gross
"""
@@ -470,6 +470,7 @@ def _highlight_disassesmbly(self, lines_out, widget, lines_in):
entry = ida_kernwin.line_rendering_output_entry_t(line, ida_kernwin.LROEF_FULL_LINE, color)
lines_out.entries.push_back(entry)
+
#----------------------------------------------------------------------
# Callbacks
#----------------------------------------------------------------------
@@ -508,6 +509,7 @@ def update(self, ctx):
"""
return ida_kernwin.AST_ENABLE_ALWAYS
+
#------------------------------------------------------------------------------
# IDA UI Event Hooks
#------------------------------------------------------------------------------
diff --git a/plugins/tenet/registers.py b/plugins/tenet/registers.py
index 7491b06..6394692 100644
--- a/plugins/tenet/registers.py
+++ b/plugins/tenet/registers.py
@@ -1,6 +1,6 @@
from tenet.ui import *
from tenet.util.misc import register_callback, notify_callback
-from tenet.integration.api import DockableWindow, disassembler
+from tenet.util.disassembler import DockableWindow, RegistersSidebarWidgetType, disassembler
#------------------------------------------------------------------------------
# registers.py -- Register Controller
@@ -53,7 +53,7 @@ def show(self, target=None, position=0):
#
self.view = RegisterView(self, self.model)
- new_dockable = DockableWindow("CPU Registers", self.view)
+ new_dockable = RegistersSidebarWidgetType("Registers", self.view)
#
# if there is a reference to a left over dockable window (e.g, from a
diff --git a/plugins/tenet/screenshots/idx_shell.gif b/plugins/tenet/screenshots/idx_shell.gif
new file mode 100644
index 0000000..e24dd3d
Binary files /dev/null and b/plugins/tenet/screenshots/idx_shell.gif differ
diff --git a/plugins/tenet/screenshots/load_trace.gif b/plugins/tenet/screenshots/load_trace.gif
new file mode 100644
index 0000000..04497ef
Binary files /dev/null and b/plugins/tenet/screenshots/load_trace.gif differ
diff --git a/plugins/tenet/screenshots/memory_breakpoint.gif b/plugins/tenet/screenshots/memory_breakpoint.gif
new file mode 100644
index 0000000..e045a95
Binary files /dev/null and b/plugins/tenet/screenshots/memory_breakpoint.gif differ
diff --git a/plugins/tenet/screenshots/memory_seek.png b/plugins/tenet/screenshots/memory_seek.png
new file mode 100644
index 0000000..fcf0d25
Binary files /dev/null and b/plugins/tenet/screenshots/memory_seek.png differ
diff --git a/plugins/tenet/screenshots/region_breakpoints.gif b/plugins/tenet/screenshots/region_breakpoints.gif
new file mode 100644
index 0000000..3ed5a7a
Binary files /dev/null and b/plugins/tenet/screenshots/region_breakpoints.gif differ
diff --git a/plugins/tenet/screenshots/seek_to_first.gif b/plugins/tenet/screenshots/seek_to_first.gif
new file mode 100644
index 0000000..b5fdb74
Binary files /dev/null and b/plugins/tenet/screenshots/seek_to_first.gif differ
diff --git a/plugins/tenet/screenshots/seek_to_register.gif b/plugins/tenet/screenshots/seek_to_register.gif
new file mode 100644
index 0000000..2221e21
Binary files /dev/null and b/plugins/tenet/screenshots/seek_to_register.gif differ
diff --git a/plugins/tenet/screenshots/tenet_overview.gif b/plugins/tenet/screenshots/tenet_overview.gif
new file mode 100644
index 0000000..21eeef2
Binary files /dev/null and b/plugins/tenet/screenshots/tenet_overview.gif differ
diff --git a/plugins/tenet/screenshots/tenet_trails.gif b/plugins/tenet/screenshots/tenet_trails.gif
new file mode 100644
index 0000000..a7da26f
Binary files /dev/null and b/plugins/tenet/screenshots/tenet_trails.gif differ
diff --git a/plugins/tenet/screenshots/themes.png b/plugins/tenet/screenshots/themes.png
new file mode 100644
index 0000000..8bfe598
Binary files /dev/null and b/plugins/tenet/screenshots/themes.png differ
diff --git a/plugins/tenet/screenshots/trace_breakpoints.gif b/plugins/tenet/screenshots/trace_breakpoints.gif
new file mode 100644
index 0000000..f435667
Binary files /dev/null and b/plugins/tenet/screenshots/trace_breakpoints.gif differ
diff --git a/plugins/tenet/screenshots/trace_zoom.gif b/plugins/tenet/screenshots/trace_zoom.gif
new file mode 100644
index 0000000..4c9fd39
Binary files /dev/null and b/plugins/tenet/screenshots/trace_zoom.gif differ
diff --git a/plugins/tenet/stack.py b/plugins/tenet/stack.py
index 5d104bb..30ba550 100644
--- a/plugins/tenet/stack.py
+++ b/plugins/tenet/stack.py
@@ -1,7 +1,7 @@
import struct
from tenet.ui import *
-from tenet.hex import HexController
+from tenet.hex_sidebar import HexSidebarController
from tenet.types import HexType, AuxType
#------------------------------------------------------------------------------
@@ -17,7 +17,7 @@
# stack view might instead.
#
-class StackController(HexController):
+class StackController(HexSidebarController):
"""
The Stack Dump Controller (Logic)
"""
diff --git a/plugins/tenet/testcase/README.md b/plugins/tenet/testcase/README.md
new file mode 100644
index 0000000..949b4d8
--- /dev/null
+++ b/plugins/tenet/testcase/README.md
@@ -0,0 +1,25 @@
+# Sample Testcases
+
+In these folders are sample traces that have been provided to verify installation and explore basic functionality.
+
+You can load the provided binary into IDA Pro, and then load its respective `trace.log` via the `File --> Load file --> Tenet trace file...` menu entry.
+
+## Boombox
+
+This is a sample trace of exploitation challenge called `boombox.exe` run with the following pin tracer command:
+
+```
+C:\pin\pin.exe -t obj-intel64\pintenet.dll -w boombox.exe -- boombox.exe
+```
+
+During the trace, I entered a few commands, before quitting.
+
+## Solitaire
+
+This is a sample trace of Windows XP solitaire `sol.exe` run with the following pin tracer command:
+
+```
+C:\pin\pin.exe -t obj-ia32\pintenet.dll -w sol.exe -- sol.exe
+```
+
+During the trace, I moved a few cards, and closed the application.
\ No newline at end of file
diff --git a/plugins/tenet/testcase/boombox/boombox.exe b/plugins/tenet/testcase/boombox/boombox.exe
new file mode 100644
index 0000000..df3ebbc
Binary files /dev/null and b/plugins/tenet/testcase/boombox/boombox.exe differ
diff --git a/plugins/tenet/testcase/solitaire/cards.dll b/plugins/tenet/testcase/solitaire/cards.dll
new file mode 100644
index 0000000..f37d347
Binary files /dev/null and b/plugins/tenet/testcase/solitaire/cards.dll differ
diff --git a/plugins/tenet/testcase/solitaire/sol.exe b/plugins/tenet/testcase/solitaire/sol.exe
new file mode 100644
index 0000000..08757ef
Binary files /dev/null and b/plugins/tenet/testcase/solitaire/sol.exe differ
diff --git a/plugins/tenet/trace/analysis.py b/plugins/tenet/trace/analysis.py
index 0820c5f..0faccd0 100644
--- a/plugins/tenet/trace/analysis.py
+++ b/plugins/tenet/trace/analysis.py
@@ -67,8 +67,10 @@ def _analyze(self):
"""
Analyze the trace against the binary loaded by the disassembler.
"""
- self._analyze_aslr()
- self._analyze_unmapped()
+ # This shit doesn't work
+ # self._analyze_aslr()
+ self.slide = 0
+ # self._analyze_unmapped()
def _analyze_aslr(self):
"""
@@ -78,7 +80,7 @@ def _analyze_aslr(self):
# get *all* of the instruction addresses from disassembler
instruction_addresses = dctx.get_instruction_addresses()
-
+ print(f"instruction address0: {instruction_addresses[0]}")
#
# bucket the instruction addresses from the disassembler
# based on non-aslr'd bits (lower 12 bits, 0xFFF)
@@ -91,6 +93,7 @@ def _analyze_aslr(self):
# get the set of unique, executed addresses from the trace
trace_addresses = trace.ip_addrs
+ print(trace_addresses)
#
# scan the executed addresses from the trace, and discard
@@ -100,6 +103,7 @@ def _analyze_aslr(self):
trace_buckets = collections.defaultdict(list)
for executed_address in trace_addresses:
+ print(executed_address)
bits = executed_address & 0xFFF
if bits not in binary_buckets:
continue
diff --git a/plugins/tenet/trace/arch/__init__.py b/plugins/tenet/trace/arch/__init__.py
index 791fd3d..725c8cb 100644
--- a/plugins/tenet/trace/arch/__init__.py
+++ b/plugins/tenet/trace/arch/__init__.py
@@ -1,2 +1,3 @@
from .x86 import ArchX86
-from .amd64 import ArchAMD64
\ No newline at end of file
+from .amd64 import ArchAMD64
+from .arm import ArchARM
\ No newline at end of file
diff --git a/plugins/tenet/trace/arch/arm.py b/plugins/tenet/trace/arch/arm.py
new file mode 100644
index 0000000..6481f01
--- /dev/null
+++ b/plugins/tenet/trace/arch/arm.py
@@ -0,0 +1,31 @@
+class ArchARM:
+ """
+ ARM CPU archtiecture definition
+ """
+ MAGIC = 0xaabbccdd
+
+ POINTER_SIZE = 4
+
+ IP = "PC"
+ SP = "SP"
+
+ REGISTERS = \
+ [
+ "R0",
+ "R1",
+ "R2",
+ "R3",
+ "R4",
+ "R5",
+ "R6",
+ "R7",
+ "R8",
+ "R9",
+ "R10",
+ "R11",
+ "R12",
+ "SP",
+ "LR",
+ "PC",
+ "CPSR"
+ ]
\ No newline at end of file
diff --git a/plugins/tenet/trace/file.py b/plugins/tenet/trace/file.py
index 5ee49c9..726738d 100644
--- a/plugins/tenet/trace/file.py
+++ b/plugins/tenet/trace/file.py
@@ -1237,7 +1237,9 @@ def load(self, f):
Load the trace segment from the given filestream.
"""
info = SegmentInfo()
- f.readinto(info)
+ num_bytes = f.readinto(info)
+ print(num_bytes)
+ print(info)
self.id = info.id
self.base_idx = info.base_idx
diff --git a/plugins/tenet/tracers/README.md b/plugins/tenet/tracers/README.md
new file mode 100644
index 0000000..5fa3a4c
--- /dev/null
+++ b/plugins/tenet/tracers/README.md
@@ -0,0 +1,96 @@
+# Tenet Traces
+
+Tenet Traces are execution traces that can be loaded by the [Tenet](https://github.com/gaasedelen/tenet) trace explorer for IDA Pro.
+
+Included within this repo are two tracers, with a third hosted out-of-repo. They are provided to demonstrate how execution traces can be generated from different frameworks, but are all considered very much *experimental*.
+
+* `/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 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
+
+Tenet is able to load human-readable text traces. These traces are both easy to generate and decode, where each line in the trace file (or 'log' file) represent an execution delta.
+
+```
+...
+esp=0xcfef4,eip=0x1005f91,mr=0xcfef0:915f0001
+ebx=0x0,eip=0x1005f93
+esp=0xcfef0,eip=0x1005f94,mw=0xcfef0:00000000
+edi=0x75d283e0,eip=0x1005f9a,mr=0x1001098:e083d275
+esp=0xcfef4,ecx=0xda39e660,eax=0x1000000,eip=0x1005f9c,mr=0xcfeec:9c5f0001,mw=0xcfed0:f5410376
+eip=0x1005fa1,mr=0x1000000:4d5a
+...
+```
+
+As an anti-pattern, Tenet Traces consciously omit execution information except that which would be considered 'important' to a human analyst. These 'lossy' traces should consist only of general purpose registers (GPR) changes and memory that is either read or written during execution.
+
+In the future, this format may be extended to support auxiliary entries such as context switches, syscalls, or other types of execution annotations.
+
+### Register Delta
+
+Only registers of interest need to be recorded. This will typically be the GPR of the traced architecture. To see which registers Tenet will parse for a given trace file, please see the architecture definitions in `/tenet/trace/arch`.
+
+If the value of a register changes after executing an instruction, it needs to be recorded. Registers should be output to the log in the following format, separated by a comma for each entry:
+
+```
+=
+```
+
+In addition, we provide the following guidance on writing registers to the log:
+* Register order does not matter
+* Register names are not case sensitive (e.g. EIP == eip)
+* Register values should be a base 16 (hex) number, such as `0x401b00`
+* It is best practice to dump the full register state at the start of the trace
+* It is best practice to dump the PC (EIP/RIP) register for every line, whether or not it changed
+ * Output PC as the last register for the line, it will simplify your 'comma' logic
+
+### Memory Delta
+
+Each byte of memory that was read or written during the execution of an instruction should be appended to the line. Memory should be output to the log in the following format, separated by a comma for each entry:
+
+```
+=:...
+```
+Where `ACCESS_TYPE` is one of the following `keywords`:
+* `mr` -- Memory read
+* `mw` -- Memory write
+* `mrw` -- Memory read & write
+ * *Please note that you don't have to use `mrw`, but it may be simpler for some implementations*
+
+In addition, we provide the following guidance on writing memory to the log:
+
+* An arbitrary number of memory entries is allowed
+* The hex string (i.e. memory contents) can be of arbitrary length
+
+## Reference Implementation
+
+When implementing a custom tracer it is **strongly recommended** that you start with the following and get something 'basic' working before trying to do anything more advanced.
+
+You must be able to hook / instrument the following:
+
+* Instrumentation callback triggered before each instruction executes
+* Instrumentation callback for each memory read / write event
+
+Apply these callbacks with the following pattern:
+
+1. While executing an instruction, record the (ADDRESS, SIZE) for every memory access that occurs
+2. Upon reaching the next instruction callback:
+ * Diff any registers that have changed since the previous instruction callback
+ * Fetch the data for any (ADDRESS, SIZE) memory that was touched
+ * Dump the reg & mem changes to the log
+3. Repeat
+
+If you can get this working, you are welcome to try and make your tracer 'smarter' but it is generally not worth the effort.
+
+## Custom Architectures
+
+If you want to hack in support for another architecture (I promise, it should not be too hard) then there are a few additional things that you should want be aware of.
+
+* There is a **hard limit** of 32 unique registers that can be specified in a Tenet Trace arch
+* Registers specified in the Tenet Trace architecture files must all be of equal size
+* The maximum recommended trace size is 10 million executed instructions
+ * Note, this is primarily because of Tenet's python backend, not a physical limitation
diff --git a/plugins/tenet/tracers/pin/ImageManager.cpp b/plugins/tenet/tracers/pin/ImageManager.cpp
new file mode 100644
index 0000000..79aba67
--- /dev/null
+++ b/plugins/tenet/tracers/pin/ImageManager.cpp
@@ -0,0 +1,75 @@
+#include "ImageManager.h"
+#include "pin.H"
+
+ImageManager::ImageManager()
+{
+ PIN_RWMutexInit(&images_lock);
+}
+
+ImageManager::~ImageManager()
+{
+ PIN_RWMutexFini(&images_lock);
+}
+
+VOID ImageManager::addImage(std::string image_name, ADDRINT lo_addr,
+ ADDRINT hi_addr)
+{
+ PIN_RWMutexWriteLock(&images_lock);
+ {
+ images.insert(LoadedImage(image_name, lo_addr, hi_addr));
+ }
+ PIN_RWMutexUnlock(&images_lock);
+}
+
+VOID ImageManager::removeImage(ADDRINT low)
+{
+ PIN_RWMutexWriteLock(&images_lock);
+ {
+ std::set::iterator i = images.find(LoadedImage("", low));
+ if (i != images.end()) {
+ LoadedImage li = *i;
+ images.erase(i);
+ }
+ }
+ PIN_RWMutexUnlock(&images_lock);
+}
+
+VOID ImageManager::addWhiteListedImage(const std::string& image_name)
+{
+ whitelist.insert(image_name);
+}
+
+BOOL ImageManager::isWhiteListed(const std::string& image_name)
+{
+ return whitelist.find(image_name) != whitelist.end();
+}
+
+// Checks if the given address falls inside one of the white-listed images we are
+// tracing.
+BOOL ImageManager::isInterestingAddress(ADDRINT addr)
+{
+ PIN_RWMutexReadLock(&images_lock);
+ {
+ // If there is no white-listed image, everything is white-listed.
+ if (images.empty() || (addr >= m_cached_low && addr < m_cached_high)) {
+ PIN_RWMutexUnlock(&images_lock);
+ return true;
+ }
+
+ auto i = images.upper_bound(LoadedImage("", addr));
+ --i;
+
+ // If the instruction address does not fall inside a valid white listed image, bail out.
+ if (!(i != images.end() && i->low_ <= addr && addr < i->high_)) {
+ PIN_RWMutexUnlock(&images_lock);
+ return false;
+ }
+
+ // Save the matched image.
+ m_cached_low = i->low_;
+ m_cached_high = i->high_;
+ }
+ PIN_RWMutexUnlock(&images_lock);
+
+ return true;
+}
diff --git a/plugins/tenet/tracers/pin/ImageManager.h b/plugins/tenet/tracers/pin/ImageManager.h
new file mode 100644
index 0000000..83e13e9
--- /dev/null
+++ b/plugins/tenet/tracers/pin/ImageManager.h
@@ -0,0 +1,54 @@
+#ifndef IMAGEMANAGER_H_
+#define IMAGEMANAGER_H_
+
+#include
+#include
+
+#include "pin.H"
+
+struct LoadedImage {
+ std::string name_;
+ ADDRINT low_;
+ ADDRINT high_;
+
+ LoadedImage(const std::string& n = "", ADDRINT low = 0, ADDRINT high = 0)
+ : name_(n)
+ , low_(low)
+ , high_(high)
+ {
+ }
+
+ // Overloaded method to implement searches over the loaded images list
+ // and also allow this class to be used on a set like STL container.
+ bool operator<(const LoadedImage& rhs) const
+ {
+ return low_ < rhs.low_;
+ }
+};
+
+class ImageManager {
+private:
+ // Set of module names that are allowed to be traced.
+ std::set images;
+ PIN_RWMUTEX images_lock;
+
+ // Here we store the names of the images inside our white list.
+ std::set whitelist;
+
+ // Store the last recently matched image so we can use it as a cache.
+ ADDRINT m_cached_low;
+ ADDRINT m_cached_high;
+
+public:
+ ImageManager();
+ virtual ~ImageManager();
+
+ VOID addWhiteListedImage(const std::string& image_name);
+ BOOL isWhiteListed(const std::string& image_name);
+ BOOL isInterestingAddress(ADDRINT addr);
+
+ VOID addImage(std::string image_name, ADDRINT lo_add, ADDRINT hi_addr);
+ VOID removeImage(ADDRINT low);
+};
+
+#endif /* IMAGEMANAGER_H_ */
diff --git a/plugins/tenet/tracers/pin/Makefile b/plugins/tenet/tracers/pin/Makefile
new file mode 100644
index 0000000..9e3bf0e
--- /dev/null
+++ b/plugins/tenet/tracers/pin/Makefile
@@ -0,0 +1,9 @@
+CONFIG_ROOT := $(PIN_ROOT)/source/tools/Config
+include $(CONFIG_ROOT)/makefile.config
+
+TOOL_ROOTS := pintenet
+
+$(OBJDIR)pintenet$(PINTOOL_SUFFIX): $(OBJDIR)pintenet$(OBJ_SUFFIX) $(OBJDIR)ImageManager$(OBJ_SUFFIX)
+ $(LINKER) $(TOOL_LDFLAGS) $(LINK_EXE)$@ $^ $(TOOL_LPATHS) $(TOOL_LIBS)
+
+include $(TOOLS_ROOT)/Config/makefile.default.rules
diff --git a/plugins/tenet/tracers/pin/README.md b/plugins/tenet/tracers/pin/README.md
new file mode 100644
index 0000000..3743034
--- /dev/null
+++ b/plugins/tenet/tracers/pin/README.md
@@ -0,0 +1,83 @@
+# pintenet
+
+The `pintenet` pintool is a proof-of-concept tracer that runs ontop of the [Intel Pin](https://software.intel.com/en-us/articles/pin-a-dynamic-binary-instrumentation-tool) DBI framework. It can generate a human-readable execution trace, compatible with [Tenet](https://github.com/gaasedelen/tenet).
+
+This pintool is labeled only as a prototype, it has not been tested robustly.
+
+# Usage
+
+The pintool can be used to trace simple usermode applications on Windows or Linux. To use it, provide the path for a compiled version of `pintenet` to `pin` via the `-t` argument.
+
+Example usage:
+
+```
+C:\pin\pin -t obj-ia32\pintenet.dll -w sol.exe -- "C:\Users\user\Desktop\sol.exe"
+```
+
+This pintool will generate a unique trace, per-thread. Since Tenet does not really provide a good story for loading or exploring multithreaded traces, you will have to select a trace/thread of interest and load that.
+
+Compiled Windows binaries may be available on the [releases](https://github.com/gaasedelen/tenet/releases) page of this repo. Otherwise, you must compile from source.
+
+## Additional parameters
+
+There are two additional parameters that can be used to configure the pintool.
+
+* `-w ` Whitelist which modules should be traced in the process
+ * e.g. `-w calc.exe`, or `-w calc.exe,kernel32.dll`
+* `-o ` Specify the prefix / name to use for the generated log files
+
+If no `-w` arguments are supplied, the pintool will trace all loaded images.
+
+
+# Compilation
+
+To compile the pintool, you first will need to [download](https://software.intel.com/en-us/articles/pin-a-binary-instrumentation-tool-downloads) and extract Pin.
+
+Follow the build instructions below for your respective platform.
+
+## Building for Linux
+
+On Linux, one can compile the pintool using the following commands.
+
+```
+# Location of this repo / pintool source
+cd ~/tenet/tracers/pin
+
+# Location where you extracted Pin
+export PIN_ROOT=~/pin
+export PATH=$PATH:$PIN_ROOT
+make
+make TARGET=ia32
+```
+
+## Building for Windows
+
+Install deps for building Pintools:
+- Install Visual Studio Community 2019 Edition from [https://visualstudio.microsoft.com/downloads/](https://visualstudio.microsoft.com/downloads/)
+ - Make sure to install the Desktop development for C++ workload
+
+- Install GNU's make, version 4.2.1, using Cygwin's 64-bit installer. Cygwin installer link here: [https://cygwin.com/install.html](https://cygwin.com/install.html)
+
+ ### Building 32bit
+1. Launch a new CMD window and paste the EXACT following:
+ ```
+ set PIN_ROOT=C:\\pin
+ set PATH=%PATH%;C:\cygwin64\bin
+ "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvars32.bat"
+ ```
+2. Change to the directory containing the `pintenet` source, build the 32bit pin tool:
+ ```
+ make TARGET=ia32
+ ```
+
+ ### Building 64bit
+1. Launch a new CMD window and paste the EXACT following:
+ ```
+ set PIN_ROOT=C:\\pin
+ set PATH=%PATH%;C:\cygwin64\bin
+ "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvars64.bat"
+ ```
+2. Change to the directory containing the `pintenet` source, and build the 64bit pin tool:
+ ```
+ make TARGET=intel64
+ ```
diff --git a/plugins/tenet/tracers/pin/pintenet.cpp b/plugins/tenet/tracers/pin/pintenet.cpp
new file mode 100644
index 0000000..811af99
--- /dev/null
+++ b/plugins/tenet/tracers/pin/pintenet.cpp
@@ -0,0 +1,432 @@
+//
+// pintenet.cpp, a Proof-of-Concept Tenet Tracer
+//
+// -- by Patrick Biernat & Markus Gaasedelen
+// @ RET2 Systems, Inc.
+//
+// Adaptions from the CodeCoverage pin tool by Agustin Gianni as
+// contributed to Tenet: https://github.com/gaasedelen/tenet
+//
+
+#include
+#include
+#include
+
+#include "pin.H"
+#include "ImageManager.h"
+
+using std::ofstream;
+
+ofstream* g_log;
+
+#ifdef __i386__
+#define PC "eip"
+#else
+#define PC "rip"
+#endif
+
+//
+// Tool Arguments
+//
+
+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 KnobOutputFilePrefix(KNOB_MODE_WRITEONCE, "pintool", "o", "trace",
+ "Prefix of the output file. If none is specified, 'trace' is used.");
+
+//
+// Misc / Util
+//
+
+#if defined(TARGET_WINDOWS)
+#define PATH_SEPARATOR "\\"
+#else
+#define PATH_SEPARATOR "/"
+#endif
+
+static std::string base_name(const std::string& path)
+{
+ std::string::size_type idx = path.rfind(PATH_SEPARATOR);
+ std::string name = (idx == std::string::npos) ? path : path.substr(idx + 1);
+ return name;
+}
+
+//
+// Per thread data structure. This is mainly done to avoid locking.
+// - Per-thread map of executed basic blocks, and their size.
+//
+
+struct ThreadData
+{
+ ADDRINT m_cpu_pc;
+ ADDRINT m_cpu[REG_GR_LAST+1];
+
+ ADDRINT mem_w_addr;
+ ADDRINT mem_w_size;
+ ADDRINT mem_r_addr;
+ ADDRINT mem_r_size;
+ ADDRINT mem_r2_addr;
+ ADDRINT mem_r2_size;
+
+ // Trace file for thread-specific trace modes
+ ofstream* m_trace;
+
+ char m_scratch[512 * 2]; // fxsave has the biggest memory operand
+};
+
+//
+// Tool Infrastructure
+//
+
+class ToolContext
+{
+public:
+
+ ToolContext()
+ {
+ PIN_InitLock(&m_loaded_images_lock);
+ PIN_InitLock(&m_thread_lock);
+ m_tls_key = PIN_CreateThreadDataKey(nullptr);
+ }
+
+ ThreadData* GetThreadLocalData(THREADID tid)
+ {
+ return static_cast(PIN_GetThreadData(m_tls_key, tid));
+ }
+
+ void setThreadLocalData(THREADID tid, ThreadData* data)
+ {
+ PIN_SetThreadData(m_tls_key, data, tid);
+ }
+
+ // The image manager allows us to keep track of loaded images.
+ ImageManager* m_images;
+
+ // Trace file used for 'monolithic' execution traces.
+ //TraceFile* m_trace;
+
+ // Keep track of _all_ the loaded images.
+ std::vector m_loaded_images;
+ PIN_LOCK m_loaded_images_lock;
+
+ // Thread tracking utilities.
+ std::set m_seen_threads;
+ std::vector m_terminated_threads;
+ PIN_LOCK m_thread_lock;
+
+ // Flag that indicates that tracing is enabled. Always true if there are no whitelisted images.
+ bool m_tracing_enabled = true;
+
+ // TLS key used to store per-thread data.
+ TLS_KEY m_tls_key;
+};
+
+// Thread creation event handler.
+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", 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);
+ {
+ context.m_seen_threads.insert(tid);
+ }
+ PIN_ReleaseLock(&context.m_thread_lock);
+
+}
+
+// Thread destruction event handler.
+static VOID OnThreadFini(THREADID tid, const CONTEXT* ctxt, INT32 c, VOID* v)
+{
+ // Get thread's 'ThreadData' structure.
+ auto& context = *reinterpret_cast(v);
+ ThreadData* data = context.GetThreadLocalData(tid);
+
+ // Remove the thread from the seen threads set and add it to the terminated list.
+ PIN_GetLock(&context.m_thread_lock, 1);
+ {
+ context.m_seen_threads.erase(tid);
+ context.m_terminated_threads.push_back(data);
+ }
+ PIN_ReleaseLock(&context.m_thread_lock);
+
+}
+
+// Image unload event handler.
+static VOID OnImageLoad(IMG img, VOID* v)
+{
+ auto& context = *reinterpret_cast(v);
+ std::string img_name = base_name(IMG_Name(img));
+
+ ADDRINT low = IMG_LowAddress(img);
+ ADDRINT high = IMG_HighAddress(img);
+
+ *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);
+ {
+ context.m_loaded_images.push_back(LoadedImage(IMG_Name(img), low, high));
+ }
+ PIN_ReleaseLock(&context.m_loaded_images_lock);
+
+ // If the image is whitelisted save its information.
+ if (context.m_images->isWhiteListed(img_name))
+ {
+ context.m_images->addImage(img_name, low, high);
+
+ // Enable tracing if not already enabled.
+ if (!context.m_tracing_enabled)
+ context.m_tracing_enabled = true;
+ }
+}
+
+// Image load event handler.
+static VOID OnImageUnload(IMG img, VOID* v)
+{
+ auto& context = *reinterpret_cast(v);
+ context.m_images->removeImage(IMG_LowAddress(img));
+}
+
+//
+// Tracing
+//
+
+VOID record_diff(const CONTEXT * cpu, ADDRINT pc, VOID* v)
+{
+ auto& context = *reinterpret_cast(v);
+ //printf("Hello from record diff!\n");
+
+ if (!context.m_tracing_enabled || !context.m_images->isInterestingAddress(pc))
+ return;
+
+ auto tid = PIN_ThreadId();
+ ThreadData* data = context.GetThreadLocalData(tid);
+
+ //
+ // dump register delta
+ //
+
+ ADDRINT val;
+ auto OutFile = data->m_trace;
+
+ for (int reg = (int)REG_GR_BASE; reg <= (int)REG_GR_LAST; ++reg) {
+
+ // fetch the current register value
+ PIN_GetContextRegval(cpu, (REG)reg, reinterpret_cast(&val));
+
+ // if the register didn't change from the last state, nothing to do
+ if (val == data->m_cpu[reg])
+ continue;
+
+ // save the value for the new register to the log
+ *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" << pc;
+
+ //
+ // dump memory reads / writes
+ //
+
+ if (data->mem_r_size)
+ {
+ 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" << data->mem_r_addr << ":";
+
+ for(UINT32 i = 0; i < data->mem_r_size; i++) {
+ *OutFile << std::hex << std::setw(2) << std::setfill('0') << ((unsigned char)data->m_scratch[i] & 0xff);
+ }
+
+ data->mem_r_size = 0;
+ }
+
+ if (data->mem_r2_size)
+ {
+ 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" << data->mem_r2_addr << ":";
+
+ for(UINT32 i = 0; i < data->mem_r2_size; i++) {
+ *OutFile << std::hex << std::setw(2) << std::setfill('0') << ((unsigned char)data->m_scratch[i] & 0xff);
+ }
+
+ data->mem_r2_size = 0;
+ }
+
+ if (data->mem_w_size)
+ {
+ 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" << data->mem_w_addr << ":";
+
+ for(UINT32 i = 0; i < data->mem_w_size; i++) {
+ *OutFile << std::hex << std::setw(2) << std::setfill('0') << ((unsigned char)data->m_scratch[i] & 0xff);
+ }
+
+ data->mem_w_size = 0;
+ }
+
+ *OutFile << std::endl;
+}
+
+VOID record_read(THREADID tid, ADDRINT access_addr, UINT32 access_size, VOID * v) {
+ auto& context = *reinterpret_cast(v);
+ ThreadData* data = context.GetThreadLocalData(tid);
+ data->mem_r_addr = access_addr;
+ data->mem_r_size = access_size;
+}
+
+VOID record_read2(THREADID tid, ADDRINT access_addr, UINT32 access_size, VOID * v) {
+ auto& context = *reinterpret_cast(v);
+ ThreadData* data = context.GetThreadLocalData(tid);
+ data->mem_r2_addr = access_addr;
+ data->mem_r2_size = access_size;
+}
+
+VOID record_write(THREADID tid, ADDRINT access_addr, UINT32 access_size, VOID * v) {
+ auto& context = *reinterpret_cast(v);
+ ThreadData* data = context.GetThreadLocalData(tid);
+ data->mem_w_addr = access_addr;
+ data->mem_w_size = access_size;
+}
+
+VOID OnInst(INS ins, VOID* v) {
+
+ //
+ // *always* dump a diff since the last instruction
+ //
+
+ INS_InsertCall(
+ ins, IPOINT_BEFORE,
+ AFUNPTR(record_diff),
+ IARG_CONST_CONTEXT,
+ IARG_INST_PTR,
+ IARG_PTR, v,
+ IARG_END);
+
+ //
+ // if this instruction will perform a mem r/w, inject a call to record the
+ // address of interest. this will be used by the *next* state diff / dump
+ //
+
+ if (INS_IsMemoryRead(ins) || INS_IsMemoryWrite(ins))
+ {
+ if (INS_IsMemoryRead(ins))
+ {
+ INS_InsertCall(
+ ins, IPOINT_BEFORE,
+ AFUNPTR(record_read),
+ IARG_THREAD_ID,
+ IARG_MEMORYREAD_EA,
+ IARG_MEMORYREAD_SIZE,
+ IARG_PTR, v,
+ IARG_END);
+ }
+
+ if (INS_HasMemoryRead2(ins))
+ {
+ //assert(INS_IsMemoryRead(ins) == false);
+ INS_InsertCall(
+ ins, IPOINT_BEFORE,
+ AFUNPTR(record_read2),
+ IARG_THREAD_ID,
+ IARG_MEMORYREAD2_EA,
+ IARG_MEMORYREAD_SIZE,
+ IARG_PTR, v,
+ IARG_END);
+ }
+
+ if (INS_IsMemoryWrite(ins))
+ {
+ INS_InsertCall(
+ ins, IPOINT_BEFORE,
+ AFUNPTR(record_write),
+ IARG_THREAD_ID,
+ IARG_MEMORYWRITE_EA,
+ IARG_MEMORYWRITE_SIZE,
+ IARG_PTR, v,
+ IARG_END);
+ }
+ }
+
+}
+
+static VOID Fini(INT32 code, VOID *v)
+{
+ auto& context = *reinterpret_cast(v);
+
+ // Add non terminated threads to the list of terminated threads.
+ for (THREADID i : context.m_seen_threads) {
+ ThreadData* data = context.GetThreadLocalData(i);
+ context.m_terminated_threads.push_back(data);
+ }
+
+ for (const auto& data : context.m_terminated_threads) {
+ data->m_trace->close();
+ }
+
+ g_log->close();
+}
+
+int main(int argc, char * argv[]) {
+
+ // Initialize symbol processing
+ PIN_InitSymbols();
+
+ // Initialize PIN.
+ if (PIN_Init(argc, 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) {
+ *g_log << "White-listing image: " << KnobModuleWhitelist.Value(i) << '\n';
+ context->m_images->addWhiteListedImage(KnobModuleWhitelist.Value(i));
+ context->m_tracing_enabled = false;
+ }
+
+ // Handlers for thread creation and destruction.
+ PIN_AddThreadStartFunction(OnThreadStart, context);
+ PIN_AddThreadFiniFunction(OnThreadFini, context);
+
+ // Handlers for image loading and unloading.
+ IMG_AddInstrumentFunction(OnImageLoad, context);
+ IMG_AddUnloadFunction(OnImageUnload, context);
+
+ // Handlers for instrumentation events.
+ INS_AddInstrumentFunction(OnInst, context);
+
+ // Handler for program exits.
+ PIN_AddFiniFunction(Fini, context);
+
+ PIN_StartProgram();
+ return 0;
+}
diff --git a/plugins/tenet/tracers/qemu/README.md b/plugins/tenet/tracers/qemu/README.md
new file mode 100644
index 0000000..585e06c
--- /dev/null
+++ b/plugins/tenet/tracers/qemu/README.md
@@ -0,0 +1,41 @@
+# QEMU Tracer
+
+In this folder is a sample x86 QEMU-based Tenet tracer built on the QEMU plugin API.
+
+This tracer was written over the course of a few hours as a test against the popular Xbox emulation project known as [XEMU](https://github.com/mborgerson/xemu). It has been used to trace and study the bootchain of the original Xbox, and is 100% untested outside of that use-case.
+
+This tracer is provided as a **reference**. It will not compile directly out of the box, but should prove useful if trying to implement a tracer for a QEMU-based fuzzer / emulation solution.
+
+## Usage
+
+This QEMU plugin can be used to trace [XEMU](https://github.com/mborgerson/xemu), and maybe some other x86 based QEMU projects (with some adaptations). Please note, XEMU needs to be built with `--enable-plugins` (add it to `build.sh`) to use the provided plugin.
+
+Example usage:
+
+```
+~/xemu/dist/xemu -plugin ~/xemu/tests/plugin/libtenet.so
+```
+
+This will start the system and generate a `trace.log` file. Since there is no filtering of any sort, I would recommend skipping the startup animation, or modifying the plugin to trace specific areas of interest. Otherwise you will get a raw, 'full-system' trace.
+
+## Compilation
+
+QEMU's native plugin API does not provide access to guest registers or memory making typical instrumentation... difficult. This tracer demonstrates how to use some ugly hacks (eg, hardcoding offsets off the opaque CPU handle) to workaround these limitations.
+
+### Finding magic offsets
+
+1. Place this line near the end of `~/xemu/target/i386/cpu.h`
+
+ ```c
+ char __foo[] = {[offsetof(X86CPU, env)] = ""};
+ ```
+2. Attempt to build XEMU/QEMU and let the compiler explode, the error message will print the magic offset value we will need to hardcode into the QEMU plugin / tracer.
+3. Remove / comment out the line added in Step 1
+
+### Building libtenet
+
+1. Place `tenet.c` in `~/xemu/tests/plugin/`
+2. Add `NAMES += tenet` to the `Makefile` in this directory
+3. Modify `tenet.c`, and replace `34928` with the magic offset found using the above steps
+4. Run `make`
+5. There should be a resulting libtenet.so in this plugins directory
diff --git a/plugins/tenet/tracers/qemu/tenet.c b/plugins/tenet/tracers/qemu/tenet.c
new file mode 100644
index 0000000..4ee935b
--- /dev/null
+++ b/plugins/tenet/tracers/qemu/tenet.c
@@ -0,0 +1,186 @@
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+//struct X86CPU {
+// /*< private >*/
+// CPUState parent_obj;
+// /*< public >*/
+//
+// CPUNegativeOffsetState neg;
+// CPUX86State env; // THIS IS @ 34928
+//
+
+#define NUM_REG 8
+
+enum reg
+{
+ EAX = 0,
+ ECX = 1,
+ EDX = 2,
+ EBX = 3,
+ ESP = 4,
+ EBP = 5,
+ ESI = 6,
+ EDI = 7
+};
+
+char * reg_name[NUM_REG] = \
+{
+ "EAX",
+ "ECX",
+ "EDX",
+ "EBX",
+ "ESP",
+ "EBP",
+ "ESI",
+ "EDI"
+};
+
+char reg_scratch[2048] = {};
+char mem_scratch[2048] = {};
+
+QEMU_PLUGIN_EXPORT int qemu_plugin_version = QEMU_PLUGIN_VERSION;
+
+void *qemu_get_cpu(int index);
+
+//void *qemu_ram_ptr_length(void * ram_block, uint64_t addr, uint64_t * size, bool lock);
+void *qemu_map_ram_ptr(void *ram_block, uint64_t addr);
+
+void cpu_physical_memory_rw(uint64_t hwaddr, uint8_t *buf,
+ uint64_t len, int is_write);
+
+static uint32_t get_cpu_register(unsigned int reg_idx) {
+ uint8_t* cpu = qemu_get_cpu(0);
+ return *(uint32_t*)(cpu + 34928 + reg_idx * 4);
+}
+
+uint32_t * get_cpu_regs() {
+ uint8_t* cpu = qemu_get_cpu(0);
+ return (uint32_t*)(cpu + 34928);
+}
+
+FILE * g_out = NULL;
+
+uint32_t * g_cpu = NULL;
+uint32_t g_cpu_prev[NUM_REG] = {};
+
+typedef struct mem_entry {
+ qemu_plugin_meminfo_t info;
+ uint64_t virt_addr;
+ uint64_t ram_addr;
+} mem_entry;
+
+mem_entry g_mem_log[2048] = {};
+size_t g_mem_log_count = 0;
+
+static void vcpu_insn_exec(unsigned int cpu_index, void *udata)
+{
+ int length = 0;
+ int i = 0;
+
+ if (g_cpu[EAX] != g_cpu_prev[EAX])
+ length += sprintf(reg_scratch+length, "EAX=%X,", g_cpu[EAX]);
+ if (g_cpu[ECX] != g_cpu_prev[ECX])
+ length += sprintf(reg_scratch+length, "ECX=%X,", g_cpu[ECX]);
+ if (g_cpu[EDX] != g_cpu_prev[EDX])
+ length += sprintf(reg_scratch+length, "EDX=%X,", g_cpu[EDX]);
+ if (g_cpu[EBX] != g_cpu_prev[EBX])
+ length += sprintf(reg_scratch+length, "EBX=%X,", g_cpu[EBX]);
+ if (g_cpu[EBP] != g_cpu_prev[EBP])
+ length += sprintf(reg_scratch+length, "EBP=%X,", g_cpu[EBP]);
+ if (g_cpu[ESP] != g_cpu_prev[ESP])
+ length += sprintf(reg_scratch+length, "ESP=%X,", g_cpu[ESP]);
+ if (g_cpu[ESI] != g_cpu_prev[ESI])
+ length += sprintf(reg_scratch+length, "ESI=%X,", g_cpu[ESI]);
+ if (g_cpu[EDI] != g_cpu_prev[EDI])
+ length += sprintf(reg_scratch+length, "EDI=%X,", g_cpu[EDI]);
+
+ uint64_t eip = GPOINTER_TO_UINT(udata);
+ length += sprintf(reg_scratch+length, "EIP=%lX", eip);
+
+ for (int i = 0; i < g_mem_log_count; i++)
+ {
+ mem_entry * entry = &g_mem_log[i];
+
+ // reconstruct info about the mem access
+ size_t access_size = 1 << (entry->info & 0xF);
+ char rw = qemu_plugin_mem_is_store(entry->info) ? 'w' : 'r';
+
+ length += sprintf(reg_scratch+length, ",m%c=%lX:", rw, entry->virt_addr);
+
+ // fetch the resulting memory
+ unsigned char access_data[16] = {};
+ void * host_ptr = qemu_map_ram_ptr(NULL, entry->ram_addr);
+ memcpy(access_data, host_ptr, access_size);
+
+ for(int j = 0; j < access_size; j++)
+ length += sprintf(reg_scratch+length, "%02X", access_data[j]);
+ }
+
+ fprintf(g_out, "%s\n", reg_scratch);
+
+ reg_scratch[0] = 0;
+ g_mem_log_count = 0;
+
+ memcpy(g_cpu_prev, g_cpu, sizeof(g_cpu_prev));
+
+}
+
+static void vcpu_mem_access(unsigned int cpu_index, qemu_plugin_meminfo_t mem_info,
+ uint64_t vaddr, void *udata)
+{
+
+ struct qemu_plugin_hwaddr *hwaddr = qemu_plugin_get_hwaddr(mem_info, vaddr);
+ if (qemu_plugin_hwaddr_is_io(hwaddr))
+ return;
+
+ uint64_t physaddr = qemu_plugin_hwaddr_device_offset(hwaddr);
+ assert(physaddr < 0xFFFFFFFF);
+
+ mem_entry * entry = &g_mem_log[g_mem_log_count++];
+
+ entry->info = mem_info;
+ entry->virt_addr = vaddr;
+ entry->ram_addr = physaddr;
+}
+
+static void vcpu_tb_trans(qemu_plugin_id_t id, struct qemu_plugin_tb *tb)
+{
+ size_t n = qemu_plugin_tb_n_insns(tb);
+
+ g_cpu = get_cpu_regs();
+ //printf("GOT g_cpu %p\n", g_cpu);
+
+ for (size_t i = 0; i < n; i++) {
+ struct qemu_plugin_insn *insn = qemu_plugin_tb_get_insn(tb, i);
+ uint64_t vaddr = qemu_plugin_insn_vaddr(insn);
+ qemu_plugin_register_vcpu_insn_exec_cb(insn, vcpu_insn_exec, QEMU_PLUGIN_CB_R_REGS, GUINT_TO_POINTER(vaddr));
+ qemu_plugin_register_vcpu_mem_cb(insn, vcpu_mem_access, QEMU_PLUGIN_CB_R_REGS, QEMU_PLUGIN_MEM_RW, GUINT_TO_POINTER(vaddr));
+ }
+}
+
+QEMU_PLUGIN_EXPORT int qemu_plugin_install(qemu_plugin_id_t id,
+ const qemu_info_t *info,
+ int argc, char **argv)
+{
+ char * filepath = NULL;
+
+ if (argc)
+ filepath = argv[0];
+ else
+ filepath = "trace.log";
+
+ printf("Writing Tenet trace to %s\n", filepath);
+ g_out = fopen(filepath, "w");
+
+ memset(g_cpu_prev, 0xFF, sizeof(g_cpu_prev));
+ qemu_plugin_register_vcpu_tb_trans_cb(id, vcpu_tb_trans);
+
+ return 0;
+}
diff --git a/plugins/tenet/ui/hex_view.py b/plugins/tenet/ui/hex_view.py
index 89bfb75..ca054b4 100644
--- a/plugins/tenet/ui/hex_view.py
+++ b/plugins/tenet/ui/hex_view.py
@@ -23,14 +23,14 @@ def __init__(self, controller, model, parent=None):
self.setFocusPolicy(QtCore.Qt.StrongFocus)
self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
- font = QtGui.QFont("Courier", pointSize=normalize_font(9))
+ font = QtGui.QFont("Courier", pointSize=normalize_font(10))
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_width = fm.boundingRect('N').width()
+ self._char_height = int(fm.tightBoundingRect('N').height() * 1.75)
self._char_descent = self._char_height - fm.descent()*0.75
self._click_timer = QtCore.QTimer(self)
@@ -75,14 +75,14 @@ def _init_ctx_menu(self):
#
self._action_break = {}
+ self._break_menu = QtWidgets.QMenu("Break on...")
for name, bp_type in bp_types:
action = QtWidgets.QAction(name, None)
action.setCheckable(True)
self._action_break[action] = bp_type
+ self._break_menu.addAction(action)
- self._break_menu = QtWidgets.QMenu("Break on...")
- self._break_menu.addActions(self._action_break)
#
# goto action groups
@@ -108,7 +108,8 @@ def _init_ctx_menu(self):
]
for submenu, actions in self._goto_menus:
- submenu.addActions(actions)
+ for action in actions:
+ submenu.addAction(action)
# install the right click context menu
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
@@ -194,10 +195,10 @@ def _refresh_painting_metrics(self):
# the x position and width of the auxillary region (right section of view)
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
+ 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)
+ # self.setMaximumWidth(self._pos_aux + self._width_aux)
def full_size(self):
"""
@@ -619,7 +620,7 @@ def wheelEvent(self, event):
#
# compute the address of the hovered byte (if there is one...)
- byte_address = self.point_to_address(event.pos())
+ byte_address = self.point_to_address(event.position())
for bp in self.model.memory_breakpoints:
@@ -756,7 +757,7 @@ def _paint_line(self, painter, line_idx):
#
# paint 'readable' ASCII
#
-
+ #! REMOVE FOR STACK
byte_idx = byte_base_idx
x_pos_aux = self._pos_aux + self._char_width
diff --git a/plugins/tenet/ui/palette.py b/plugins/tenet/ui/palette.py
index 7921fb8..894cefc 100644
--- a/plugins/tenet/ui/palette.py
+++ b/plugins/tenet/ui/palette.py
@@ -8,7 +8,7 @@
from tenet.util.qt import *
from tenet.util.misc import *
from tenet.util.log import pmsg
-from tenet.integration.api import disassembler
+from tenet.util.disassembler import disassembler
logger = logging.getLogger("Plugin.UI.Palette")
@@ -463,8 +463,8 @@ def _pick_best_color(self, field_name, color_entry):
"""
Given a variable color_entry, select the best color based on the theme hints.
- TODO: Most of this file is ripped from Lighthouse, including this func. In
- Lighthouse is behaves a bit different than it does here, but I'm too lazy
+ TODO: Most of this file is ripped from Tenet, including this func. In
+ Tenet is behaves a bit different than it does here, but I'm too lazy
to refactor/remove it for now (and maybe it'll get used later on??)
"""
assert len(color_entry) == 2, "Malformed color entry, must be (dark, light)"
diff --git a/plugins/tenet/ui/reg_view.py b/plugins/tenet/ui/reg_view.py
index 6f5ce2d..8c05520 100644
--- a/plugins/tenet/ui/reg_view.py
+++ b/plugins/tenet/ui/reg_view.py
@@ -2,7 +2,7 @@
from tenet.types import BreakpointType
from tenet.util.qt import *
-from tenet.integration.api import disassembler
+from tenet.util.disassembler import disassembler
class RegisterView(QtWidgets.QWidget):
"""
@@ -40,6 +40,7 @@ def __init__(self, controller, model, parent=None):
self.model = model
self.controller = controller
self._init_ui()
+ self.setMaximumWidth(250)
def _init_ui(self):
@@ -91,12 +92,12 @@ def __init__(self, controller, model, parent=None):
self.controller = controller
self.model = model
- font = QtGui.QFont("Courier", pointSize=normalize_font(9))
+ font = QtGui.QFont("Courier", pointSize=normalize_font(10))
font.setStyleHint(QtGui.QFont.TypeWriter)
self.setFont(font)
fm = QtGui.QFontMetricsF(font)
- self._char_width = fm.width('9')
+ self._char_width = fm.boundingRect('N').width()
self._char_height = fm.height()
# default to fit roughly 50 printable characters
@@ -259,6 +260,7 @@ def _pos_to_field(self, pos):
"""
for reg_name, field in self._reg_fields.items():
full_field = QtCore.QRect(field.name_rect.topLeft(), field.next_rect.bottomRight())
+ pos = QtCore.QPoint(pos.x(), pos.y())
if full_field.contains(pos):
return field
return None
@@ -289,7 +291,7 @@ def wheelEvent(self, event):
return
# mouse hover was not over IP register value, nothing to do
- field = self._pos_to_field(event.pos())
+ field = self._pos_to_field(event.position())
if not (field and field.name == self.model.arch.IP):
return
diff --git a/plugins/tenet/ui/resources/icons/registers.png b/plugins/tenet/ui/resources/icons/registers.png
new file mode 100644
index 0000000..9d1f245
Binary files /dev/null and b/plugins/tenet/ui/resources/icons/registers.png differ
diff --git a/plugins/tenet/ui/resources/icons/stack.jpeg b/plugins/tenet/ui/resources/icons/stack.jpeg
new file mode 100644
index 0000000..6179a60
Binary files /dev/null and b/plugins/tenet/ui/resources/icons/stack.jpeg differ
diff --git a/plugins/tenet/ui/resources/icons/stack.png b/plugins/tenet/ui/resources/icons/stack.png
new file mode 100644
index 0000000..45abd59
Binary files /dev/null and b/plugins/tenet/ui/resources/icons/stack.png differ
diff --git a/plugins/tenet/ui/trace_view.py b/plugins/tenet/ui/trace_view.py
index 7f3644f..b3975df 100644
--- a/plugins/tenet/ui/trace_view.py
+++ b/plugins/tenet/ui/trace_view.py
@@ -2,7 +2,7 @@
from tenet.util.qt import *
from tenet.util.misc import register_callback, notify_callback
-from tenet.integration.api import disassembler
+from tenet.util.disassembler import disassembler
logger = logging.getLogger("Tenet.UI.TraceView")
@@ -1324,7 +1324,7 @@ def __init__(self, pctx, parent=None):
super(TraceDock, self).__init__(parent)
self.pctx = pctx
self.view = TraceView(pctx, self)
- self.setMovable(False)
+ self.setMovable(True)
self.setContentsMargins(0, 0, 0, 0)
self.addWidget(self.view)
diff --git a/plugins/tenet/integration/api/__init__.py b/plugins/tenet/util/disassembler/__init__.py
similarity index 52%
rename from plugins/tenet/integration/api/__init__.py
rename to plugins/tenet/util/disassembler/__init__.py
index 392938e..9ac3910 100644
--- a/plugins/tenet/integration/api/__init__.py
+++ b/plugins/tenet/util/disassembler/__init__.py
@@ -15,21 +15,28 @@
#--------------------------------------------------------------------------
if disassembler == None:
- from .ida_api import IDACoreAPI, IDAContextAPI, DockableWindow
- disassembler = IDACoreAPI()
- DisassemblerContextAPI = IDAContextAPI
+ try:
+ print("IDA")
+ from .ida_api import IDACoreAPI, IDAContextAPI, DockableWindow
+ disassembler = IDACoreAPI()
+ DisassemblerContextAPI = IDAContextAPI
+ except ImportError as e:
+ print(e)
-##--------------------------------------------------------------------------
-## Binary Ninja API Shim
-##--------------------------------------------------------------------------
-#
-#if disassembler == None:
-# try:
-# from .binja_api import BinjaCoreAPI, BinjaContextAPI
-# disassembler = BinjaCoreAPI()
-# DisassemblerContextAPI = BinjaContextAPI
-# except ImportError:
-# pass
+#--------------------------------------------------------------------------
+# Binary Ninja API Shim
+#--------------------------------------------------------------------------
+
+if disassembler == None:
+ try:
+ print("Binja")
+ from .binja_api import BinjaCoreAPI, BinjaContextAPI, \
+ DockableWindow, RegistersSidebarWidgetType, \
+ StackMiniGraphWidgetType, MemoryGlobalAreaWidget
+ disassembler = BinjaCoreAPI()
+ DisassemblerContextAPI = BinjaContextAPI
+ except ImportError as e:
+ print(e)
#--------------------------------------------------------------------------
# Unknown Disassembler
diff --git a/plugins/tenet/integration/api/api.py b/plugins/tenet/util/disassembler/api.py
similarity index 63%
rename from plugins/tenet/integration/api/api.py
rename to plugins/tenet/util/disassembler/api.py
index 37d7f8b..671d19e 100644
--- a/plugins/tenet/integration/api/api.py
+++ b/plugins/tenet/util/disassembler/api.py
@@ -1,9 +1,9 @@
import abc
import logging
-from ...util.qt import *
+from ..qt import QT_AVAILABLE, QtGui, QtWidgets
-logger = logging.getLogger("Tenet.Integration.API")
+logger = logging.getLogger("Tenet.API")
#------------------------------------------------------------------------------
# Disassembler API
@@ -16,13 +16,8 @@
#
# by subclassing the templated classes below, the plugin can support other
# disassembler plaforms relatively easily. at the moment, implementing these
-# subclasses is ~50% of the work that is required to add support for this
-# plugin to any given interactive disassembler.
-#
-# TODO: technically, a bunch of definitions are missing from this file
-# that are present in the IDA integration implementation. these will
-# need to be copied over to here to better define the disassembler API
-# dependencies required by this plugin
+# subclasses is ~50% of the work that is required to add tenet support
+# to any given interactive disassembler.
#
class DisassemblerCoreAPI(object):
@@ -44,6 +39,7 @@ def __init__(self):
self._version_patch = NotImplemented
if not self.headless and QT_AVAILABLE:
+ from ..qt import WaitBox
self._waitbox = WaitBox("Please wait...")
else:
self._waitbox = None
@@ -181,10 +177,52 @@ def message(self, function_address, new_name):
# UI APIs
#--------------------------------------------------------------------------
+ #
+ # NOTE: please note, these APIs and their usage is a little ... obtuse.
+ # this is primarily because the IDA & Binja dockable widget management
+ # system is rather different.
+ #
+ # these APIs make a best effort in unifiying the systems in a manner that
+ # works for this project. it may not be ideal for the universal use case
+ # but is good enough for our purposes.
+ #
+
+ @abc.abstractmethod
+ def register_dockable(self, dockable_name, create_widget_callback):
+ """
+ Register a callback with the disassembler to generate dockable widgets.
+
+ - dockable_name: the name of the window / dockable to be created
+ - create_widget_callback: a static function that return a new dockable widget
+
+ The registered callback will be called automatically in certain events
+ that will preclude the display of the dockable_name. These events
+ may include a new databse being opened, or show_dockable being called.
+
+ """
+ pass
+
@abc.abstractmethod
- def create_dockable(self, dockable_name, widget):
+ def create_dockable_widget(self, parent, dockable_name):
"""
Creates a dockable widget.
+
+ This function should generally be called within the create_widget_callback
+ described in register_dockable(...).
+ """
+ pass
+
+ @abc.abstractmethod
+ def show_dockable(self, dockable_name):
+ """
+ Show the named dockable widget.
+ """
+ pass
+
+ @abc.abstractmethod
+ def hide_dockable(self, dockable_name):
+ """
+ Hide the named dockable widget.
"""
pass
@@ -243,12 +281,6 @@ def busy(self):
# API Shims
#--------------------------------------------------------------------------
- def is_64bit(self):
- """
- Return True if the loaded processor module is 64bit.
- """
- pass
-
@abc.abstractmethod
def get_current_address(self):
"""
@@ -323,4 +355,112 @@ def set_function_name_at(self, function_address, new_name):
"""
Set the function name at given address.
"""
- pass
\ No newline at end of file
+ pass
+
+ #--------------------------------------------------------------------------
+ # Hooks API
+ #--------------------------------------------------------------------------
+
+ @abc.abstractmethod
+ def create_rename_hooks(self, function_address, new_name):
+ """
+ Returns a hooking object that can capture rename events for this context.
+ """
+ pass
+
+ #--------------------------------------------------------------------------
+ # Function Prefix API
+ #--------------------------------------------------------------------------
+
+ #
+ # the following APIs are used to apply or clear prefixes to multiple
+ # functions in the disassembly database. the only thing you're expected
+ # to do here is select an appropriate PREFIX_SEPARATOR.
+ #
+ # your prefix separator is expected to be something unique, that a user
+ # would probably *never* put into their function name themselves but
+ # looks somewhat normal.
+ #
+ # in IDA, putting '%' in a function name appears as '_' in the function
+ # list, so we use that as a prefix separator. in Binary Ninja, we use a
+ # unicode character that looks like an underscore character.
+ #
+ # it is probably safe to steal the unicode char we use with binja for
+ # your own implementation.
+ #
+
+ PREFIX_SEPARATOR = NotImplemented
+
+ def prefix_function(self, function_address, prefix):
+ """
+ Prefix a function name with the given string.
+ """
+ original_name = self.get_function_raw_name_at(function_address)
+ new_name = str(prefix) + self.PREFIX_SEPARATOR + str(original_name)
+
+ # rename the function with the newly prefixed name
+ self.set_function_name_at(function_address, new_name)
+
+ def prefix_functions(self, function_addresses, prefix):
+ """
+ Prefix a list of functions with the given string.
+ """
+ for function_address in function_addresses:
+ self.prefix_function(function_address, prefix)
+
+ def clear_prefix(self, function_address):
+ """
+ Clear the prefix from a given function.
+ """
+ prefixed_name = self.get_function_raw_name_at(function_address)
+
+ #
+ # split the function name on the last prefix separator, saving
+ # everything that comes after (eg, the original func name)
+ #
+
+ new_name = prefixed_name.rsplit(self.PREFIX_SEPARATOR)[-1]
+
+ # the name doesn't appear to have had a prefix, nothing to do...
+ if new_name == prefixed_name:
+ return
+
+ # rename the function with the prefix(s) now stripped
+ self.set_function_name_at(function_address, new_name)
+
+ def clear_prefixes(self, function_addresses):
+ """
+ Clear the prefix from a list of given functions.
+ """
+ for function_address in function_addresses:
+ self.clear_prefix(function_address)
+
+#------------------------------------------------------------------------------
+# Hooking
+#------------------------------------------------------------------------------
+
+class RenameHooks(object):
+ """
+ An abstract implementation of disassembler hooks to capture rename events.
+ """
+ __metaclass__ = abc.ABCMeta
+
+ @abc.abstractmethod
+ def hook(self):
+ """
+ Install hooks into the disassembler that capture rename events.
+ """
+ pass
+
+ @abc.abstractmethod
+ def unhook(self):
+ """
+ Remove hooks used to capture rename events.
+ """
+ pass
+
+ def renamed(self, address, new_name):
+ """
+ This will be hooked by Tenet at runtime to capture rename events.
+ """
+ pass
diff --git a/plugins/tenet/util/disassembler/binja_api.py b/plugins/tenet/util/disassembler/binja_api.py
new file mode 100644
index 0000000..3fded2a
--- /dev/null
+++ b/plugins/tenet/util/disassembler/binja_api.py
@@ -0,0 +1,715 @@
+# -*- coding: utf-8 -*-
+import os
+import sys
+import logging
+import functools
+import threading
+import collections
+
+from .api import DisassemblerCoreAPI, DisassemblerContextAPI
+from ...util.qt import *
+from ...util.misc import is_mainthread, not_mainthread
+from ...util.disassembler import disassembler
+import binaryninjaui
+from binaryninjaui import DockHandler, DockContextHandler, UIContext, UIActionHandler, SidebarWidgetType
+from binaryninjaui import Sidebar, SidebarWidget, SidebarWidgetType, SidebarWidgetContainer, Sidebar
+from binaryninjaui import GlobalAreaWidget, GlobalArea, UIActionHandler
+
+import binaryninja
+from binaryninja import PythonScriptingInstance, binaryview
+from binaryninja.plugin import BackgroundTaskThread
+from PySide6.QtCore import Qt, QRectF, QMetaType
+from PySide6.QtGui import QImage, QPainter, QFont, QColor
+from PySide6.QtWidgets import QApplication, QHBoxLayout, QVBoxLayout, \
+ QLabel, QWidget, QSplitter
+
+logger = logging.getLogger("Tenet.API.Binja")
+
+#------------------------------------------------------------------------------
+# Utils
+#------------------------------------------------------------------------------
+
+def execute_sync(function):
+ """
+ Synchronize with the disassembler for safe database access.
+ """
+
+ @functools.wraps(function)
+ def wrapper(*args, **kwargs):
+
+ #
+ # in Binary Ninja, it is only safe to access the BNDB from a thread
+ # that is *not* the mainthread. if we appear to already be in a
+ # background thread of some sort, simply execute the given function
+ #
+
+ if not is_mainthread():
+ return function(*args, **kwargs)
+
+ #
+ # if we are in the mainthread, we need to schedule a background
+ # task to perform our database task/function instead
+ #
+ # this inline function definition is technically what will execute
+ # in a database-safe background thread. we use this thunk to
+ # capture any output the function may want to return to the user.
+ #
+
+ output = [None]
+ def thunk():
+ output[0] = function(*args, **kwargs)
+ return 1
+
+ class DatabaseRead(BackgroundTaskThread):
+ """
+ A stub task to safely read from the BNDB.
+ """
+ def __init__(self, text, function):
+ super(DatabaseRead, self).__init__(text, False)
+ self._task_to_run = function
+ def run(self):
+ self._task_to_run()
+ self.finish()
+
+ # schedule the databases read and wait for its completion
+ t = DatabaseRead("Accessing database...", thunk)
+ t.start()
+ t.join()
+
+ # return the output of the synchronized execution / read
+ return output[0]
+ return wrapper
+
+class BinjaCoreAPI(DisassemblerCoreAPI):
+ NAME = "BINJA"
+
+ def __init__(self):
+ super(BinjaCoreAPI, self).__init__()
+ self._init_version()
+
+ def _init_version(self):
+ version_string = binaryninja.core_version()
+
+ # retrieve Binja's version #
+ if "-" in version_string: # dev
+ disassembler_version = version_string.split("-", 1)[0]
+ else: # commercial, personal
+ disassembler_version = version_string.split(" ", 1)[0]
+
+ major, minor, patch, *_= disassembler_version.split(".") + ['0']
+
+ # save the version number components for later use
+ self._version_major = major
+ self._version_minor = minor
+ self._version_patch = patch
+
+ #--------------------------------------------------------------------------
+ # Properties
+ #--------------------------------------------------------------------------
+
+ @property
+ def headless(self) -> bool:
+ return not(binaryninja.core_ui_enabled())
+
+ #--------------------------------------------------------------------------
+ # Synchronization Decorators
+ #--------------------------------------------------------------------------
+
+ @staticmethod
+ def execute_read(function):
+ return execute_sync(function)
+
+ @staticmethod
+ def execute_write(function):
+ return execute_sync(function)
+
+ @staticmethod
+ def execute_ui(function):
+
+ @functools.wraps(function)
+ def wrapper(*args, **kwargs):
+ ff = functools.partial(function, *args, **kwargs)
+
+ # if we are already in the main (UI) thread, execute now
+ if is_mainthread():
+ ff()
+ return
+
+ # schedule the task to run in the main thread
+ binaryninja.execute_on_main_thread(ff)
+
+ return wrapper
+ #--------------------------------------------------------------------------
+ # API Shims
+ #--------------------------------------------------------------------------
+
+ def get_disassembler_user_directory(self):
+ return os.path.split(binaryninja.user_plugin_path())[0]
+
+ def get_disassembly_background_color(self):
+ return binaryninjaui.getThemeColor(binaryninjaui.ThemeColor.LinearDisassemblyBlockColor)
+
+ def is_msg_inited(self):
+ return True
+
+ @execute_ui.__func__
+ def warning(self, text):
+ super(BinjaCoreAPI, self).warning(text)
+
+ def message(self, message):
+ print(message)
+
+ # Dunno if binja has to do this or what
+ def refresh_views(self):
+ pass
+
+ #--------------------------------------------------------------------------
+ # UI API Shims
+ #--------------------------------------------------------------------------
+
+ def register_dockable(self, dockable_name, create_widget_callback):
+ dock_handler = DockHandler.getActiveDockHandler()
+ dock_handler.addDockWidget(dockable_name, create_widget_callback, QtCore.Qt.RightDockWidgetArea, QtCore.Qt.Horizontal, False)
+
+ def create_dockable_widget(self, parent, dockable_name):
+ return DockableWindow(parent, dockable_name)
+
+ def show_dockable(self, dockable_name):
+ dock_handler = DockHandler.getActiveDockHandler()
+ dock_handler.setVisible(dockable_name, True)
+
+ def hide_dockable(self, dockable_name):
+ dock_handler = DockHandler.getActiveDockHandler()
+ dock_handler.setVisible(dockable_name, False)
+
+ #TODO These are in a bad spot
+ def show_registers(self, register_controller):
+ register_controller.show()
+
+ def show_memory(self, memory_controller):
+ memory_controller.show()
+
+ def show_stack(self, stack_controller):
+ stack_controller.show()
+ #--------------------------------------------------------------------------
+ # XXX Binja Specfic Helpers
+ #--------------------------------------------------------------------------
+ def is_mapped(self, address):
+ bv = self.binja_get_bv_from_dock()
+
+ for seg in bv.segments:
+ if seg.start < address < seg.end:
+ return True
+ return False
+
+ def binja_get_bv_from_dock(self):
+ dh = DockHandler.getActiveDockHandler()
+ if not dh:
+ return None
+ vf = dh.getViewFrame()
+ if not vf:
+ return None
+ vi = vf.getCurrentViewInterface()
+ bv = vi.getData()
+ return bv
+
+
+#------------------------------------------------------------------------------
+# Disassembler Context API (database-specific)
+#------------------------------------------------------------------------------
+
+class BinjaContextAPI(DisassemblerContextAPI):
+
+ def __init__(self, dctx):
+ print("Created binja context")
+ super(BinjaContextAPI, self).__init__(dctx)
+ self.bv = dctx
+ self.bp_tag = self.bv.create_tag_type("breakpoint", "🔴")
+
+ @property
+ def busy(self):
+ return binaryview.AnalysisInfo.state != binaryview.AnalysisState.IdleState
+
+ #--------------------------------------------------------------------------
+ # API Shims
+ #--------------------------------------------------------------------------
+
+ def get_current_address(self):
+
+ # TODO/V35: this doen't work because of the loss of context bug...
+ #ctx = UIContext.activeContext()
+ #ah = ctx.contentActionHandler()
+ #ac = ah.actionContext()
+ #return ac.address
+
+ dh = DockHandler.getActiveDockHandler()
+ if not dh:
+ return 0
+ vf = dh.getViewFrame()
+ if not vf:
+ return 0
+ ac = vf.actionContext()
+ if not ac:
+ return 0
+ return ac.address
+
+ @BinjaCoreAPI.execute_read
+ def get_database_directory(self):
+ return os.path.dirname(self.bv.file.filename)
+
+ @not_mainthread
+ def get_function_addresses(self):
+ return [x.start for x in self.bv.functions]
+
+ def get_function_name_at(self, address):
+ func = self.bv.get_function_at(address)
+ if not func:
+ return None
+ return func.symbol.short_name
+
+ @BinjaCoreAPI.execute_read
+ def get_function_raw_name_at(self, address):
+ func = self.bv.get_function_at(address)
+ if not func:
+ return None
+ return func.name
+
+ @not_mainthread
+ def get_imagebase(self):
+ return self.bv.start
+
+ @not_mainthread
+ def get_root_filename(self):
+ return os.path.basename(self.bv.file.original_filename)
+
+ def navigate(self, address):
+ return self.bv.navigate(self.bv.view, address)
+
+ def navigate_to_function(self, function_address, address):
+
+ #
+ # attempt a more 'precise' jump, that guarantees to place us within
+ # the given function. this is necessary when trying to jump to an
+ # an address/node that is shared between two functions
+ #
+
+ funcs = self.bv.get_functions_containing(address)
+ if not funcs:
+ return False
+
+ #
+ # try to find the function that contains our target (address) and has
+ # a matching function start...
+ #
+
+ for func in funcs:
+ if func.start == function_address:
+ break
+
+ # no matching function ???
+ else:
+ return False
+
+ dh = DockHandler.getActiveDockHandler()
+ vf = dh.getViewFrame()
+ vi = vf.getCurrentViewInterface()
+
+ return vi.navigateToFunction(func, address)
+
+ @BinjaCoreAPI.execute_write
+ def set_function_name_at(self, function_address, new_name):
+ func = self.bv.get_function_at(function_address)
+ if not func:
+ return
+ if new_name == "":
+ new_name = None
+ func.name = new_name
+
+ def is_64bit(self) -> bool:
+ return self.bv.address_size & 8
+
+ def is_arm(self) -> bool:
+ arch = self.bv.arch.name.lower()
+ if 'thumb' in arch or 'arm' in arch:
+ return True
+ return False
+
+ def is_call_insn(self, address):
+ functions = self.bv.get_functions_containing(address)
+ if functions[0].is_call_instruction(address):
+ return True
+ return False
+
+ #TODO make this faster...
+ def get_instruction_addresses(self) -> list:
+ """
+ Return all instruction addresses from the executable.
+ """
+ instruction_addresses = []
+ for name,section in self.bv.sections.items():
+ if not self.bv.is_offset_code_semantics(section.start):
+ continue
+ # Iterate through disassembly and check if is valid instruction
+ current_address = 0
+ cursor = self.bv.get_linear_disassembly_position_at(section.start)
+ while current_address < section.end:
+ lines = self.bv.get_next_linear_disassembly_lines(cursor)
+ for line in lines:
+ if line.type == binaryninja.enums.LinearDisassemblyLineType.CodeDisassemblyLineType.value:
+ instruction_addresses.append(line.contents.address)
+ current_address = line.contents.address
+ return instruction_addresses
+ #! Not sure how binja will deal with heap addresses and stuff
+ def is_mapped(self, address):
+ for seg in self.bv.segments:
+ if seg.start < address < seg.end:
+ return True
+ return False
+
+ # ! This is so shitty. Why doesn't linear disassmely also give the address
+ def get_next_insn(self, address):
+ pos = self.bv.get_linear_disassembly_position_at(address)
+ for i,line in enumerate(pos.lines):
+ if len(line.contents.tokens) == 0:
+ continue
+ # return -1
+ # else:
+ current_address = line.contents.address
+ if current_address == address:
+ if i == len(pos.lines)-1:
+ pos.next()
+ next_insn = pos.lines[0].contents.address
+ else:
+ next_insn = pos.lines[i+1].contents.address
+ return next_insn
+ #! Also shitty
+ def get_prev_insn(self, address):
+ pos = self.bv.get_linear_disassembly_position_at(address)
+ for i,line in enumerate(pos.lines):
+ if len(line.contents.tokens) == 0:
+ continue
+ # if line.type != binaryninja.enums.LinearDisassemblyLineType.CodeDisassemblyLineType.value:
+ # return -1
+ # else:
+ current_address = line.contents.address
+ if current_address == address:
+ if i == 0:
+ pos.previous()
+ prev_insn = pos.lines[-1].contents.address
+ else:
+ prev_insn = pos.lines[i-1].contents.address
+ return prev_insn
+ # From binja debugger api
+ #! Not sure what tag type to use
+ def set_breakpoint(self, address):
+ print("Setting breakpoint")
+ self.bv.add_user_data_tag(address,self.bp_tag, unique=True)
+
+ def delete_breakpoint(self, address):
+ self.bv.remove_user_data_tags_of_type(address,self.bp_tag)
+
+ # def delete_all_breakpoints(self):
+ # pass
+ #--------------------------------------------------------------------------
+ # Hooks API
+ #--------------------------------------------------------------------------
+
+ def create_rename_hooks(self):
+ return RenameHooks(self.bv)
+
+ #------------------------------------------------------------------------------
+ # Function Prefix API
+ #------------------------------------------------------------------------------
+
+ PREFIX_SEPARATOR = "▁" # Unicode 0x2581
+
+#------------------------------------------------------------------------------
+# Hooking
+#------------------------------------------------------------------------------
+
+class RenameHooks(binaryview.BinaryDataNotification):
+ """
+ A hooking class to catch symbol changes in Binary Ninja.
+ """
+
+ def __init__(self, bv):
+ self._bv = bv
+ self.symbol_added = self.__symbol_handler
+ self.symbol_updated = self.__symbol_handler
+ self.symbol_removed = self.__symbol_handler
+
+ def hook(self):
+ self._bv.register_notification(self)
+
+ def unhook(self):
+ self._bv.unregister_notification(self)
+
+ def __symbol_handler(self, view, symbol):
+ func = self._bv.get_function_at(symbol.address)
+ if not func or not func.start == symbol.address:
+ return
+ self.name_changed(symbol.address, symbol.name)
+
+ def name_changed(self, address, name):
+ """
+ A placeholder callback, which will get hooked / replaced once live.
+ """
+ pass
+
+
+#------------------------------------------------------------------------------
+# UI
+#------------------------------------------------------------------------------
+
+if QT_AVAILABLE:
+
+
+ class DockableWindow(DockContextHandler, QtWidgets.QAbstractScrollArea):
+ """
+ A dockable Qt widget for Binary Ninja.
+ """
+
+ def __init__(self, name, widget):
+ self.qw = get_qmainwindow()
+ self.dock_handler = self.qw.findChild(DockHandler, '__DockHandler')
+ self.name = name
+ self.widget = widget
+
+ QtWidgets.QAbstractScrollArea.__init__(self, self.qw)
+ DockContextHandler.__init__(self, self, name)
+
+ # self.actionHandler = UIActionHandler()
+ # self.actionHandler.setupActionHandler(self)
+
+
+
+ self._active_view = None
+ self._visible_for_view = collections.defaultdict(lambda: False)
+
+ layout = QtWidgets.QVBoxLayout()
+ layout.setContentsMargins(0, 0, 0, 0)
+ layout.addWidget(self.widget)
+
+ self.setLayout(layout)
+
+ @property
+ def visible(self):
+ return self._visible_for_view[self._active_view]
+
+ @visible.setter
+ def visible(self, is_visible):
+ self._visible_for_view[self._active_view] = is_visible
+
+ def show(self):
+ dock_handler = self.qw.findChild(DockHandler, '__DockHandler')
+ dock_handler.addDockWidget(self, Qt.RightDockWidgetArea, Qt.Horizontal, True)
+
+
+ def shouldBeVisible(self, view_frame):
+ if not view_frame:
+ return False
+
+ if USING_PYSIDE6:
+ import shiboken6 as shiboken
+ else:
+ import shiboken2 as shiboken
+
+ vf_ptr = shiboken.getCppPointer(view_frame)[0]
+ return self._visible_for_view[vf_ptr]
+
+ def notifyVisibilityChanged(self, is_visible):
+ self.visible = is_visible
+
+ def notifyViewChanged(self, view_frame):
+ if not view_frame:
+ self._active_view = None
+ return
+
+ if USING_PYSIDE6:
+ import shiboken6 as shiboken
+ else:
+ import shiboken2 as shiboken
+
+ self._active_view = shiboken.getCppPointer(view_frame)[0]
+
+ if self.visible:
+ dock_handler = DockHandler.getActiveDockHandler()
+ dock_handler.setVisible(self.m_name, True)
+
+
+ class RegistersSidebarWidget(SidebarWidget):
+ def __init__(self, name, widget):
+ SidebarWidget.__init__(self, name)
+ self.name = name
+ self.widget = widget
+ layout = QtWidgets.QVBoxLayout()
+ layout.setContentsMargins(0, 0, 0, 0)
+ layout.addWidget(self.widget)
+ self.setLayout(layout)
+
+
+ def notifyOffsetChanged(self, offset):
+ # self.offset.setText(hex(offset))
+ return
+
+ def notifyViewChanged(self, view_frame):
+ # if view_frame is None:
+ # self.datatype.setText("None")
+ # self.data = None
+ # else:
+ # self.datatype.setText(view_frame.getCurrentView())
+ # view = view_frame.getCurrentViewInterface()
+ # self.data = view.getData()
+ return
+
+ def contextMenuEvent(self, event):
+ self.m_contextMenuManager.show(self.m_menu, self.actionHandler)
+
+ class RegistersSidebarWidgetType(SidebarWidgetType):
+ def __init__(self, name, widget):
+
+ self.name = name
+ self.widget = widget
+
+ # Sidebar icons are 28x28 points. Should be at least 56x56 pixels for
+ # HiDPI display compatibility. They will be automatically made theme
+ # aware, so you need only provide a grayscale image, where white is
+ # the color of the shape.
+ icon = QImage(56, 56, QImage.Format_RGB32)
+ icon.fill(0)
+
+ # Render an "H" as the example icon
+ p = QPainter()
+ p.begin(icon)
+ p.setFont(QFont("Open Sans", 56))
+ p.setPen(QColor(255, 255, 255, 255))
+ image = QImage(".binaryninja/plugins/tenet/ui/resources/icons/registers.png")
+ p.drawImage(QRectF(0, 0, 56, 56), image)
+ p.end()
+
+ SidebarWidgetType.__init__(self, icon, "Registers")
+
+ def show(self):
+ Sidebar.addSidebarWidgetType(self)
+ dh = DockHandler.getActiveDockHandler()
+ vf = dh.getViewFrame()
+ sb = vf.getSidebar()
+ sb.activate(self)
+ # Sidebar.activate(self)
+
+ def createWidget(self, frame, data):
+ # This callback is called when a widget needs to be created for a given context. Different
+ # widgets are created for each unique BinaryView. They are created on demand when the sidebar
+ # widget is visible and the BinaryView becomes active.
+ self.registerssidebarwidget = RegistersSidebarWidget(self.name, self.widget)
+ return self.registerssidebarwidget
+
+
+ class StackSidebarWidget(SidebarWidget):
+ def __init__(self, name, widget):
+ SidebarWidget.__init__(self, name)
+ self.name = name
+ self.widget = widget
+ layout = QtWidgets.QVBoxLayout()
+ layout.setContentsMargins(0, 0, 0, 0)
+ layout.addWidget(self.widget)
+ self.setLayout(layout)
+
+
+
+ def notifyViewChanged(self, view_frame):
+ # if view_frame is None:
+ # self.datatype.setText("None")
+ # self.data = None
+ # else:
+ # self.datatype.setText(view_frame.getCurrentView())
+ # view = view_frame.getCurrentViewInterface()
+ # self.data = view.getData()
+ return
+
+ def contextMenuEvent(self, event):
+ self.m_contextMenuManager.show(self.m_menu, self.actionHandler)
+
+ class StackMiniGraphWidgetType(SidebarWidgetType):
+ def __init__(self, name, widget):
+
+ self.name = name
+ self.widget = widget
+
+
+ # Sidebar icons are 28x28 points. Should be at least 56x56 pixels for
+ # HiDPI display compatibility. They will be automatically made theme
+ # aware, so you need only provide a grayscale image, where white is
+ # the color of the shape.
+ icon = QImage(56, 56, QImage.Format_RGB32)
+ icon.fill(0)
+
+ # Render an "H" as the example icon
+ p = QPainter()
+ p.begin(icon)
+ p.setFont(QFont("Open Sans", 56))
+ p.setPen(QColor(255, 255, 255, 255))
+ image = QImage(".binaryninja/plugins/tenet/ui/resources/icons/stack.png")
+ p.drawImage(QRectF(0, 0, 56, 56), image)
+ p.end()
+
+ SidebarWidgetType.__init__(self, icon, "Stack")
+
+ def show(self):
+ Sidebar.addSidebarWidgetType(self)
+ dh = DockHandler.getActiveDockHandler()
+ vf = dh.getViewFrame()
+ sb = vf.getSidebar()
+ sb.activate(self)
+ # Sidebar.activate(self)
+
+ def isInReferenceArea(self):
+ return True
+
+ def createWidget(self, frame, data):
+ # This callback is called when a widget needs to be created for a given context. Different
+ # widgets are created for each unique BinaryView. They are created on demand when the sidebar
+ # widget is visible and the BinaryView becomes active.
+ self.stacksidebarwidget = StackSidebarWidget(self.name, self.widget)
+ return self.stacksidebarwidget
+
+
+
+ class MemoryGlobalAreaWidget(GlobalAreaWidget):
+ def __init__(self, name, widget):
+ GlobalAreaWidget.__init__(self, name)
+ self.actionHandler = UIActionHandler()
+ self.actionHandler.setupActionHandler(self)
+ self.name = name
+ self.widget = widget
+ layout = QtWidgets.QVBoxLayout()
+ layout.setContentsMargins(0, 0, 0, 0)
+ layout.addWidget(self.widget)
+ self.setLayout(layout)
+ context = UIContext.allContexts()[0]
+ ga = context.globalArea()
+ ga.addWidget(self.create_widget())
+ ga.focusWidget("Memory")
+
+ def create_widget(self):
+ return self
+
+ # def notifyOffsetChanged(self, offset):
+ # self.offset.setText(hex(offset))
+ # return
+
+ def show(self):
+ pass
+
+ # def notifyViewChanged(self, view_frame):
+ # if view_frame is None:
+ # self.datatype.setText("None")
+ # self.data = None
+ # else:
+ # self.datatype.setText(view_frame.getCurrentView())
+ # view = view_frame.getCurrentViewInterface()
+ # self.data = view.getData()
+ # return
+
+ # def contextMenuEvent(self, event):
+ # self.m_contextMenuManager.show(self.m_menu, self.actionHandler)
diff --git a/plugins/tenet/integration/api/ida_api.py b/plugins/tenet/util/disassembler/ida_api.py
similarity index 99%
rename from plugins/tenet/integration/api/ida_api.py
rename to plugins/tenet/util/disassembler/ida_api.py
index 2a62187..8910a17 100644
--- a/plugins/tenet/integration/api/ida_api.py
+++ b/plugins/tenet/util/disassembler/ida_api.py
@@ -5,7 +5,7 @@
# TODO: should probably cleanup / document this file a bit better.
#
# it's worth noting that most of this is based on the same shim layer
-# used by lighthouse
+# used by tenet
#
import ida_ua
@@ -36,7 +36,6 @@
def execute_sync(function, sync_type):
"""
Synchronize with the disassembler for safe database access.
-
Modified from https://github.com/vrtadmin/FIRST-plugin-ida
"""
@@ -333,18 +332,14 @@ def hexrays_available():
def map_line2citem(decompilation_text):
"""
Map decompilation line numbers to citems.
-
This function allows us to build a relationship between citems in the
ctree and specific lines in the hexrays decompilation text.
-
Output:
-
+- line2citem:
| a map keyed with line numbers, holding sets of citem indexes
|
| eg: { int(line_number): sets(citem_indexes), ... }
'
-
"""
line2citem = {}
@@ -365,18 +360,14 @@ def map_line2citem(decompilation_text):
def map_line2node(cfunc, metadata, line2citem):
"""
Map decompilation line numbers to node (basic blocks) addresses.
-
This function allows us to build a relationship between graph nodes
(basic blocks) and specific lines in the hexrays decompilation text.
-
Output:
-
+- line2node:
| a map keyed with line numbers, holding sets of node addresses
|
| eg: { int(line_number): set(nodes), ... }
'
-
"""
line2node = {}
treeitems = cfunc.treeitems
@@ -439,13 +430,10 @@ def map_line2node(cfunc, metadata, line2citem):
def lex_citem_indexes(line):
"""
Lex all ctree item indexes from a given line of text.
-
The HexRays decompiler output contains invisible text tokens that can
be used to attribute spans of text to the ctree items that produced them.
-
This function will simply scrape and return a list of all the these
tokens (COLOR_ADDR) which contain item indexes into the ctree.
-
"""
i = 0
indexes = []
diff --git a/plugins/tenet/util/log.py b/plugins/tenet/util/log.py
index 4c4a3ed..74e478a 100644
--- a/plugins/tenet/util/log.py
+++ b/plugins/tenet/util/log.py
@@ -3,12 +3,27 @@
import logging
from .misc import makedirs, is_plugin_dev
-from ..integration.api import disassembler
+from .disassembler import disassembler
#------------------------------------------------------------------------------
# Log / Print helpers
#------------------------------------------------------------------------------
+def lmsg(message):
+ """
+ Print a message to the disassembler output window, prefixed with [Tenet]
+ """
+
+ # prefix the message
+ prefix_message = "[Tenet] %s" % message
+
+ # only print to disassembler if its output window is alive
+ if disassembler.is_msg_inited():
+ disassembler.message(prefix_message)
+ else:
+ logger.info(message)
+
+
def pmsg(message):
"""
Print a 'plugin message' to the disassembler output window.
diff --git a/plugins/tenet/util/misc.py b/plugins/tenet/util/misc.py
index efe59ff..9d78686 100644
--- a/plugins/tenet/util/misc.py
+++ b/plugins/tenet/util/misc.py
@@ -37,6 +37,30 @@ def is_mainthread():
"""
return isinstance(threading.current_thread(), threading._MainThread)
+def is_mainthread():
+ """
+ Return a bool that indicates if this is the main application thread.
+ """
+ return isinstance(threading.current_thread(), threading._MainThread)
+
+def mainthread(f):
+ """
+ A debug decorator to ensure that a function is always called from the main thread.
+ """
+ def wrapper(*args, **kwargs):
+ assert is_mainthread()
+ return f(*args, **kwargs)
+ return wrapper
+
+def not_mainthread(f):
+ """
+ A debug decorator to ensure that a function is never called from the main thread.
+ """
+ def wrapper(*args, **kwargs):
+ assert not is_mainthread()
+ return f(*args, **kwargs)
+ return wrapper
+
def assert_mainthread(f):
"""
A sanity decorator to ensure that a function is always called from the main thread.
@@ -90,6 +114,26 @@ def swap_rgb(i):
"""
return struct.unpack("I", i))[0] >> 8
+
+#------------------------------------------------------------------------------
+# Theme Util
+#------------------------------------------------------------------------------
+
+def swap_rgb(i):
+ """
+ Swap RRGGBB (integer) to BBGGRR.
+ """
+ return struct.unpack("I", i))[0] >> 8
+
+def test_color_brightness(color):
+ """
+ Test the brightness of a color.
+ """
+ if color.lightness() > 255.0/2:
+ return "light"
+ else:
+ return "dark"
+
#------------------------------------------------------------------------------
# Python Callback / Signals
#------------------------------------------------------------------------------
diff --git a/plugins/tenet/util/qt/shim.py b/plugins/tenet/util/qt/shim.py
index dd4793c..a1ed443 100644
--- a/plugins/tenet/util/qt/shim.py
+++ b/plugins/tenet/util/qt/shim.py
@@ -1,7 +1,7 @@
#
# this global is used to indicate whether Qt bindings for python are present
-# and available for use by Lighthouse.
+# and available for use by Tenet.
#
QT_AVAILABLE = False
@@ -16,23 +16,44 @@
#
# this file was critical for retaining compatibility with Qt4 frameworks
# used by IDA 6.8/6.95, but it less important now. support for Qt 4 and
-# older versions of IDA will be deprecated in Lighthouse v0.9.0
+# older versions of IDA (< 7.0) were deprecated in Tenet v0.9.0
#
USING_PYQT5 = False
USING_PYSIDE2 = False
+USING_PYSIDE6 = False
+
+#
+# TODO/QT: This file is getting pretty gross. this whole shim system
+# should probably get refactored as I really don't want disassembler
+# specific dependencies in here...
+#
+
+try:
+ import ida_idaapi
+ USING_IDA = True
+except ImportError:
+ USING_IDA = False
+
+try:
+ import binaryninjaui
+ USING_NEW_BINJA = "qt_major_version" in binaryninjaui.__dict__ and binaryninjaui.qt_major_version == 6
+ USING_OLD_BINJA = not(USING_NEW_BINJA)
+except ImportError:
+ USING_NEW_BINJA = False
+ USING_OLD_BINJA = False
#------------------------------------------------------------------------------
# PyQt5 Compatibility
#------------------------------------------------------------------------------
-# attempt to load PyQt5
-if QT_AVAILABLE == False:
+# attempt to load PyQt5 (IDA 7.0+)
+if USING_IDA:
+
try:
import PyQt5.QtGui as QtGui
import PyQt5.QtCore as QtCore
import PyQt5.QtWidgets as QtWidgets
- from PyQt5 import sip
# importing went okay, PyQt5 must be available for use
QT_AVAILABLE = True
@@ -46,8 +67,9 @@
# PySide2 Compatibility
#------------------------------------------------------------------------------
-# if PyQt5 did not import, try to load PySide
-if QT_AVAILABLE == False:
+# if PyQt5 did not import, try to load PySide2 (Old Binary Ninja / Cutter)
+if not QT_AVAILABLE and USING_OLD_BINJA:
+
try:
import PySide2.QtGui as QtGui
import PySide2.QtCore as QtCore
@@ -61,6 +83,31 @@
QT_AVAILABLE = True
USING_PYSIDE2 = True
+ # import failed. No Qt / UI bindings available...
+ except ImportError:
+ pass
+
+#------------------------------------------------------------------------------
+# PySide6 Compatibility
+#------------------------------------------------------------------------------
+
+# If all else fails, try to load PySide6 (New Binary Ninja)
+if not QT_AVAILABLE and USING_NEW_BINJA:
+
+ try:
+ import PySide6.QtGui as QtGui
+ import PySide6.QtCore as QtCore
+ import PySide6.QtWidgets as QtWidgets
+
+ # alias for less PySide6 <--> PyQt5 shimming
+ QtCore.pyqtSignal = QtCore.Signal
+ QtCore.pyqtSlot = QtCore.Slot
+ QtWidgets.QAction = QtGui.QAction
+
+ # importing went okay, PySide must be available for use
+ QT_AVAILABLE = True
+ USING_PYSIDE6 = True
+
# import failed. No Qt / UI bindings available...
except ImportError:
pass
\ No newline at end of file
diff --git a/plugins/tenet/util/qt/util.py b/plugins/tenet/util/qt/util.py
index 758f41a..ae82280 100644
--- a/plugins/tenet/util/qt/util.py
+++ b/plugins/tenet/util/qt/util.py
@@ -24,8 +24,8 @@ def copy_to_clipboard(data):
Copy the given data (a string) to the system clipboard.
"""
cb = QtWidgets.QApplication.clipboard()
- cb.clear(mode=cb.Clipboard)
- cb.setText(data, mode=cb.Clipboard)
+ cb.clear()
+ cb.setText(data)
def flush_qt_events():
"""
diff --git a/plugins/tenet_plugin.py b/plugins/tenet_plugin.py
index e1896ac..e6a43df 100644
--- a/plugins/tenet_plugin.py
+++ b/plugins/tenet_plugin.py
@@ -1,5 +1,5 @@
from tenet.util.log import logging_started, start_logging
-from tenet.integration.api import disassembler
+from tenet.util.disassembler import disassembler
if not logging_started():
logger = start_logging()
@@ -18,6 +18,9 @@
logger.info("Selecting IDA loader...")
from tenet.integration.ida_loader import *
+elif disassembler.NAME == "BINJA":
+ logger.info("Selecting Binja loader")
+ from tenet.integration.binja_loader import *
else:
raise NotImplementedError("DISASSEMBLER-SPECIFIC SHIM MISSING")
diff --git a/screenshots/binja.png b/screenshots/binja.png
new file mode 100644
index 0000000..2caca87
Binary files /dev/null and b/screenshots/binja.png differ
diff --git a/testcase/test/test.bin b/testcase/test/test.bin
new file mode 100755
index 0000000..bef382c
Binary files /dev/null and b/testcase/test/test.bin differ
diff --git a/testcase/test/trace.0.tt b/testcase/test/trace.0.tt
new file mode 100644
index 0000000..fa780cb
Binary files /dev/null and b/testcase/test/trace.0.tt differ
diff --git a/tracers/pin/pintenet.cpp b/tracers/pin/pintenet.cpp
index 9291844..811af99 100644
--- a/tracers/pin/pintenet.cpp
+++ b/tracers/pin/pintenet.cpp
@@ -5,7 +5,7 @@
// @ RET2 Systems, Inc.
//
// Adaptions from the CodeCoverage pin tool by Agustin Gianni as
-// contributed to Lighthouse: https://github.com/gaasedelen/lighthouse
+// contributed to Tenet: https://github.com/gaasedelen/tenet
//
#include