From 481ef69f55aeef27349b1353c1f1c996cc01182c Mon Sep 17 00:00:00 2001 From: maskelihileci <41159853+maskelihileci@users.noreply.github.com> Date: Sat, 28 Jun 2025 03:24:24 +0300 Subject: [PATCH 1/4] IDA PRO 9.1 + Python 3.11 Support Added I didn't want to spend too much time, so I used AI there might be mistakes or illogical code changes. --- plugins/tenet/integration/api/ida_api.py | 7 +++--- plugins/tenet/ui/hex_view.py | 20 ++++++++--------- plugins/tenet/ui/reg_view.py | 28 ++++++++++++------------ plugins/tenet/ui/trace_view.py | 12 +++++----- plugins/tenet/util/qt/waitbox.py | 12 +++++----- 5 files changed, 40 insertions(+), 39 deletions(-) diff --git a/plugins/tenet/integration/api/ida_api.py b/plugins/tenet/integration/api/ida_api.py index 2a62187..ba207f4 100644 --- a/plugins/tenet/integration/api/ida_api.py +++ b/plugins/tenet/integration/api/ida_api.py @@ -22,6 +22,7 @@ import ida_diskio import ida_kernwin import ida_segment +import ida_ida from .api import DisassemblerCoreAPI, DisassemblerContextAPI from ...util.qt import * @@ -198,9 +199,9 @@ def get_processor_type(self): pass def is_64bit(self): - inf = ida_idaapi.get_inf_structure() - #target_filetype = inf.filetype - return inf.is_64bit() + # The get_inf_structure() function is deprecated in newer IDA versions. + # The 'inf' object is now directly accessible via ida_ida. + return ida_ida.inf_is_64bit() def is_call_insn(self, address): insn = ida_ua.insn_t() diff --git a/plugins/tenet/ui/hex_view.py b/plugins/tenet/ui/hex_view.py index 89bfb75..a1ab4dc 100644 --- a/plugins/tenet/ui/hex_view.py +++ b/plugins/tenet/ui/hex_view.py @@ -197,7 +197,7 @@ def _refresh_painting_metrics(self): 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.setMinimumWidth(int(self._pos_aux + self._width_aux)) def full_size(self): """ @@ -206,7 +206,7 @@ def full_size(self): if not self.model.data: return QtCore.QSize(0, 0) - width = self._pos_aux + (self.model.num_bytes_per_line * self._char_width) + width = int(self._pos_aux + (self.model.num_bytes_per_line * self._char_width)) height = len(self.model.data) // self.model.num_bytes_per_line if len(self.model.data) % self.model.num_bytes_per_line: height += 1 @@ -694,17 +694,17 @@ def paintEvent(self, event): painter.fillRect(event.rect(), self._palette.hex_data_bg) # paint address area background - address_area_rect = QtCore.QRect(0, event.rect().top(), self._width_addr, self.height()) + address_area_rect = QtCore.QRect(0, event.rect().top(), int(self._width_addr), self.height()) painter.fillRect(address_area_rect, self._palette.hex_address_bg) # paint line between address area and hex area painter.setPen(self._palette.hex_separator) - painter.drawLine(self._width_addr, event.rect().top(), self._width_addr, self.height()) + painter.drawLine(int(self._width_addr), event.rect().top(), int(self._width_addr), self.height()) # paint line between hex area and auxillary area line_pos = self._pos_aux painter.setPen(self._palette.hex_separator) - painter.drawLine(line_pos, event.rect().top(), line_pos, self.height()) + painter.drawLine(int(line_pos), event.rect().top(), int(line_pos), self.height()) for line_idx in range(0, self.num_lines_visible): self._paint_line(painter, line_idx) @@ -735,7 +735,7 @@ def _paint_line(self, painter, line_idx): pack_len = self.model.pointer_size address_fmt = '%016X' if pack_len == 8 else '%08X' address_text = address_fmt % address - painter.drawText(self._pos_addr, y, address_text) + painter.drawText(int(self._pos_addr), y, address_text) self._default_color = self._palette.hex_text_fg if address < self.model.fade_address: @@ -775,7 +775,7 @@ def _paint_line(self, painter, line_idx): else: ch = chr(ch) - painter.drawText(x_pos_aux, y, ch) + painter.drawText(int(x_pos_aux), y, ch) x_pos_aux += self._char_width def _paint_hex_item(self, painter, byte_idx, stop_idx, x, y): @@ -960,7 +960,7 @@ def _paint_text(self, painter, byte_idx, padding, x, y): painter.setPen(QtCore.Qt.NoPen) painter.setBrush(bg_color) - painter.drawRect(x_bg, y_bg, width, height) + painter.drawRect(int(x_bg), int(y_bg), int(width), int(height)) painter.setPen(fg_color) @@ -968,7 +968,7 @@ def _paint_text(self, painter, byte_idx, padding, x, y): # paint text # - painter.drawText(x, y, text) + painter.drawText(int(x), y, text) def _paint_magic(self, painter, byte_idx, stop_idx, x, y): """ @@ -1006,7 +1006,7 @@ def _paint_magic(self, painter, byte_idx, stop_idx, x, y): # draw the pointer pointer_str = ("0x%08X " % value).rjust(num_chars) - painter.drawText(x, y, pointer_str) + painter.drawText(int(x), y, pointer_str) x += num_chars * self._char_width return (byte_idx + self.model.pointer_size, x, y) diff --git a/plugins/tenet/ui/reg_view.py b/plugins/tenet/ui/reg_view.py index 6f5ce2d..42c4732 100644 --- a/plugins/tenet/ui/reg_view.py +++ b/plugins/tenet/ui/reg_view.py @@ -109,7 +109,7 @@ def __init__(self, controller, model, parent=None): self.setFocusPolicy(QtCore.Qt.StrongFocus) self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) - self.setMinimumWidth(self._reg_pos[0] + self._default_width) + self.setMinimumWidth(int(self._reg_pos[0] + self._default_width)) self.setMouseTracking(True) self._init_ctx_menu() @@ -118,8 +118,8 @@ def __init__(self, controller, model, parent=None): self.model.registers_changed(self.refresh) def sizeHint(self): - width = self._default_width - height = (len(self._reg_fields) + 2) * self._char_height # +2 for line break before IP, and after IP + width = int(self._default_width) + height = int((len(self._reg_fields) + 2) * self._char_height) # +2 for line break before IP, and after IP return QtCore.QSize(width, height) def _init_ctx_menu(self): @@ -152,7 +152,7 @@ def _init_reg_positions(self): fm = QtGui.QFontMetricsF(self.font()) name_size = fm.boundingRect('X'*common_count).size() value_size = fm.boundingRect('0' * (self.model.arch.POINTER_SIZE * 2)).size() - arrow_size = (int(value_size.height() * 0.70) & 0xFE) + 1 + arrow_size = (int(value_size.height() * 0.70) | 1) # pre-compute the position of each register in the window for reg_name in regs: @@ -162,24 +162,24 @@ def _init_reg_positions(self): if reg_name == self.model.arch.IP: y += self._char_height - name_rect = QtCore.QRect(0, 0, name_size.width(), name_size.height()) - name_rect.moveBottomLeft(QtCore.QPoint(name_x, y)) + name_rect = QtCore.QRect(0, 0, int(name_size.width()), int(name_size.height())) + name_rect.moveBottomLeft(QtCore.QPoint(int(name_x), int(y))) - prev_rect = QtCore.QRect(0, 0, arrow_size, arrow_size) - next_rect = QtCore.QRect(0, 0, arrow_size, arrow_size) + prev_rect = QtCore.QRect(0, 0, int(arrow_size), int(arrow_size)) + next_rect = QtCore.QRect(0, 0, int(arrow_size), int(arrow_size)) arrow_rects = [prev_rect, next_rect] prev_x = name_x + name_size.width() + self._char_width prev_rect.moveCenter(name_rect.center()) - prev_rect.moveLeft(prev_x) + prev_rect.moveLeft(int(prev_x)) value_x = prev_x + prev_rect.width() + self._char_width - value_rect = QtCore.QRect(0, 0, value_size.width(), value_size.height()) - value_rect.moveBottomLeft(QtCore.QPoint(value_x, y)) + value_rect = QtCore.QRect(0, 0, int(value_size.width()), int(value_size.height())) + value_rect.moveBottomLeft(QtCore.QPoint(int(value_x), int(y))) next_x = value_x + value_size.width() + self._char_width next_rect.moveCenter(name_rect.center()) - next_rect.moveLeft(next_x) + next_rect.moveLeft(int(next_x)) # save the register shapes self._reg_fields[reg_name] = RegisterField(reg_name, name_rect, value_rect, arrow_rects) @@ -274,8 +274,8 @@ def full_size(self): if not self.model.registers: return QtCore.QSize(0, 0) - width = self._reg_pos[0] + self._default_width - height = len(self.model.registers) * self._char_height + width = int(self._reg_pos[0] + self._default_width) + height = int(len(self.model.registers) * self._char_height) return QtCore.QSize(width, height) diff --git a/plugins/tenet/ui/trace_view.py b/plugins/tenet/ui/trace_view.py index 7f3644f..ee05b02 100644 --- a/plugins/tenet/ui/trace_view.py +++ b/plugins/tenet/ui/trace_view.py @@ -479,7 +479,7 @@ def _idx2pos(self, idx): #assert self._cell_spacing % 2 == 0 # compute the y position of the 'first' cell - y += self._cell_spacing / 2 # pad out from top + y += self._cell_spacing // 2 # pad out from top y += self._cell_border # top border of cell # compute the y position of any given cell after the first @@ -953,7 +953,7 @@ def _draw_code_cells(self, painter): painter.setBrush(self.pctx.palette.trace_unmapped) y = self._idx2pos(idx) - painter.drawRect(x, y, w, h) + painter.drawRect(int(x), int(y), int(w), int(h)) def _draw_highlights(self): """ @@ -1008,7 +1008,7 @@ def _draw_highlights_cells(self, painter): y = self._idx2pos(idx) + self._cell_border # draw cell body - painter.drawRect(viz_x, y, viz_w, h) + painter.drawRect(int(viz_x), int(y), int(viz_w), int(h)) def _draw_highlights_trace(self, painter): """ @@ -1090,13 +1090,13 @@ def _draw_cursor(self): self._painter_cursor.setBrush(self.pctx.palette.trace_cursor_highlight) if draw_reader_cursor: - self._painter_cursor.drawRect(viz_x, cell_y, viz_w, cell_body_height) + self._painter_cursor.drawRect(int(viz_x), int(cell_y), int(viz_w), int(cell_body_height)) # cursor hover highlighting an event if self._hovered_idx != INVALID_IDX: hovered_y = self._idx2pos(self._hovered_idx) hovered_cell_y = hovered_y + self._cell_border - self._painter_cursor.drawRect(viz_x, hovered_cell_y, viz_w, cell_body_height) + self._painter_cursor.drawRect(int(viz_x), int(hovered_cell_y), int(viz_w), int(cell_body_height)) # draw the user cursor in dense/landscape mode else: @@ -1172,7 +1172,7 @@ def _draw_selection(self): h = end_y - start_y # draw the screen door / selection rect - self._painter_selection.drawRect(x, y, w, h) + self._painter_selection.drawRect(int(x), int(y), int(w), int(h)) def _draw_border(self): """ diff --git a/plugins/tenet/util/qt/waitbox.py b/plugins/tenet/util/qt/waitbox.py index c0b05f9..586dcc8 100644 --- a/plugins/tenet/util/qt/waitbox.py +++ b/plugins/tenet/util/qt/waitbox.py @@ -86,16 +86,16 @@ def _ui_layout(self): self._abort_button.clicked.connect(self._abort) v_layout.addWidget(self._abort_button) - v_layout.setSpacing(self._dpi_scale*3) + v_layout.setSpacing(int(self._dpi_scale*3)) v_layout.setContentsMargins( - self._dpi_scale*5, - self._dpi_scale, - self._dpi_scale*5, - self._dpi_scale + int(self._dpi_scale*5), + int(self._dpi_scale), + int(self._dpi_scale*5), + int(self._dpi_scale) ) # scale widget dimensions based on DPI - height = self._dpi_scale * 15 + height = int(self._dpi_scale * 15) self.setMinimumHeight(height) # compute the dialog layout From 1944b5b43f0194311085f4681bc5e55af8002d62 Mon Sep 17 00:00:00 2001 From: maskelihileci <41159853+maskelihileci@users.noreply.github.com> Date: Sat, 28 Jun 2025 03:31:30 +0300 Subject: [PATCH 2/4] Pin 3.11 Support + Deadlock Fix I didn't want to spend too much time, so I used AI assistance. Because of that, there might be some mistakes or illogical code changes. I added support for Pin 3.11, and also fixed a bug where some PE files were causing the analysis to hang or not complete. --- tracers/pin/pintenet.cpp | 877 ++++++++++++++++++++------------------- 1 file changed, 445 insertions(+), 432 deletions(-) diff --git a/tracers/pin/pintenet.cpp b/tracers/pin/pintenet.cpp index 9291844..a85be39 100644 --- a/tracers/pin/pintenet.cpp +++ b/tracers/pin/pintenet.cpp @@ -1,432 +1,445 @@ -// -// 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 Lighthouse: https://github.com/gaasedelen/lighthouse -// - -#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; -} +// +// 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 Lighthouse: https://github.com/gaasedelen/lighthouse +// + +#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); + // NOTE: We do not open the file here because that can cause a deadlock. + // Instead, we will open it lazily on the first call to record_diff. + + // 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; + + // The file stream is not opened in OnThreadStart because it can cause a deadlock. + // We open it here on first use instead. + if (!OutFile->is_open()) + { + char filename[128] = {}; + sprintf(filename, "%s.%u.log", KnobOutputFilePrefix.Value().c_str(), tid); + OutFile->open(filename, std::ios_base::out | std::ios_base::binary); + *OutFile << std::hex; + } + + 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); + if (!data) return; + 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); + if (!data) return; + 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); + if (!data) return; + 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; +} From 8576a016e489b460f353fc7a7d80c8f265a2957a Mon Sep 17 00:00:00 2001 From: maskelihileci <41159853+maskelihileci@users.noreply.github.com> Date: Thu, 17 Jul 2025 14:26:44 +0300 Subject: [PATCH 3/4] Tenet plugin tuned for Sogen Emulator Some outputs in the Sogen Emulator can vary, and it may offer different features as well. Therefore, I'm considering creating a separate folder and configuring its support independently. New features will also be added. --- plugins_sogen-support/tenet/__init__.py | 0 .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 175 bytes .../__pycache__/breakpoints.cpython-311.pyc | Bin 0 -> 9020 bytes .../tenet/__pycache__/context.cpython-311.pyc | Bin 0 -> 16481 bytes .../tenet/__pycache__/core.cpython-311.pyc | Bin 0 -> 9690 bytes .../tenet/__pycache__/hex.cpython-311.pyc | Bin 0 -> 10601 bytes .../tenet/__pycache__/memory.cpython-311.pyc | Bin 0 -> 924 bytes .../__pycache__/registers.cpython-311.pyc | Bin 0 -> 13343 bytes .../tenet/__pycache__/stack.cpython-311.pyc | Bin 0 -> 3760 bytes .../tenet/__pycache__/types.cpython-311.pyc | Bin 0 -> 2848 bytes plugins_sogen-support/tenet/breakpoints.py | 195 ++ plugins_sogen-support/tenet/context.py | 429 ++++ plugins_sogen-support/tenet/core.py | 256 +++ plugins_sogen-support/tenet/hex.py | 305 +++ .../ida_integration.cpython-311.pyc | Bin 0 -> 20433 bytes .../__pycache__/ida_loader.cpython-311.pyc | Bin 0 -> 3479 bytes .../tenet/integration/api/__init__.py | 40 + .../api/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 558 bytes .../api/__pycache__/api.cpython-311.pyc | Bin 0 -> 13151 bytes .../api/__pycache__/ida_api.cpython-311.pyc | Bin 0 -> 22423 bytes .../tenet/integration/api/api.py | 326 +++ .../tenet/integration/api/ida_api.py | 576 +++++ .../tenet/integration/ida_integration.py | 525 +++++ .../tenet/integration/ida_loader.py | 105 + plugins_sogen-support/tenet/memory.py | 23 + plugins_sogen-support/tenet/registers.py | 376 ++++ plugins_sogen-support/tenet/stack.py | 105 + plugins_sogen-support/tenet/trace/__init__.py | 0 .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 181 bytes .../__pycache__/analysis.cpython-311.pyc | Bin 0 -> 6536 bytes .../trace/__pycache__/file.cpython-311.pyc | Bin 0 -> 66934 bytes .../trace/__pycache__/reader.cpython-311.pyc | Bin 0 -> 56898 bytes .../trace/__pycache__/types.cpython-311.pyc | Bin 0 -> 4316 bytes plugins_sogen-support/tenet/trace/analysis.py | 253 +++ .../tenet/trace/arch/__init__.py | 2 + .../arch/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 290 bytes .../arch/__pycache__/amd64.cpython-311.pyc | Bin 0 -> 681 bytes .../arch/__pycache__/x86.cpython-311.pyc | Bin 0 -> 637 bytes .../tenet/trace/arch/amd64.py | 31 + plugins_sogen-support/tenet/trace/arch/x86.py | 23 + plugins_sogen-support/tenet/trace/file.py | 1754 +++++++++++++++ plugins_sogen-support/tenet/trace/reader.py | 1947 +++++++++++++++++ plugins_sogen-support/tenet/trace/types.py | 84 + plugins_sogen-support/tenet/types.py | 72 + plugins_sogen-support/tenet/ui/__init__.py | 8 + .../ui/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 554 bytes .../breakpoint_view.cpython-311.pyc | Bin 0 -> 3306 bytes .../ui/__pycache__/hex_view.cpython-311.pyc | Bin 0 -> 38939 bytes .../ui/__pycache__/palette.cpython-311.pyc | Bin 0 -> 20607 bytes .../ui/__pycache__/reg_view.cpython-311.pyc | Bin 0 -> 28491 bytes .../ui/__pycache__/trace_view.cpython-311.pyc | Bin 0 -> 49627 bytes .../tenet/ui/breakpoint_view.py | 45 + plugins_sogen-support/tenet/ui/hex_view.py | 1012 +++++++++ plugins_sogen-support/tenet/ui/palette.py | 574 +++++ plugins_sogen-support/tenet/ui/reg_view.py | 545 +++++ .../tenet/ui/resources/icons/arrow.png | Bin 0 -> 2071 bytes .../tenet/ui/resources/themes/horizon.json | 94 + .../tenet/ui/resources/themes/synth.json | 93 + plugins_sogen-support/tenet/ui/trace_view.py | 1335 +++++++++++ plugins_sogen-support/tenet/util/__init__.py | 0 .../util/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 180 bytes .../util/__pycache__/log.cpython-311.pyc | Bin 0 -> 6259 bytes .../util/__pycache__/misc.cpython-311.pyc | Bin 0 -> 6437 bytes .../util/__pycache__/update.cpython-311.pyc | Bin 0 -> 2651 bytes plugins_sogen-support/tenet/util/debug.py | 19 + plugins_sogen-support/tenet/util/log.py | 153 ++ plugins_sogen-support/tenet/util/misc.py | 174 ++ .../tenet/util/qt/__init__.py | 5 + .../qt/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 333 bytes .../util/qt/__pycache__/shim.cpython-311.pyc | Bin 0 -> 988 bytes .../util/qt/__pycache__/util.cpython-311.pyc | Bin 0 -> 4321 bytes .../qt/__pycache__/waitbox.cpython-311.pyc | Bin 0 -> 5279 bytes plugins_sogen-support/tenet/util/qt/shim.py | 66 + plugins_sogen-support/tenet/util/qt/util.py | 86 + .../tenet/util/qt/waitbox.py | 102 + plugins_sogen-support/tenet/util/update.py | 59 + plugins_sogen-support/tenet_plugin.py | 23 + 77 files changed, 11820 insertions(+) create mode 100644 plugins_sogen-support/tenet/__init__.py create mode 100644 plugins_sogen-support/tenet/__pycache__/__init__.cpython-311.pyc create mode 100644 plugins_sogen-support/tenet/__pycache__/breakpoints.cpython-311.pyc create mode 100644 plugins_sogen-support/tenet/__pycache__/context.cpython-311.pyc create mode 100644 plugins_sogen-support/tenet/__pycache__/core.cpython-311.pyc create mode 100644 plugins_sogen-support/tenet/__pycache__/hex.cpython-311.pyc create mode 100644 plugins_sogen-support/tenet/__pycache__/memory.cpython-311.pyc create mode 100644 plugins_sogen-support/tenet/__pycache__/registers.cpython-311.pyc create mode 100644 plugins_sogen-support/tenet/__pycache__/stack.cpython-311.pyc create mode 100644 plugins_sogen-support/tenet/__pycache__/types.cpython-311.pyc create mode 100644 plugins_sogen-support/tenet/breakpoints.py create mode 100644 plugins_sogen-support/tenet/context.py create mode 100644 plugins_sogen-support/tenet/core.py create mode 100644 plugins_sogen-support/tenet/hex.py create mode 100644 plugins_sogen-support/tenet/integration/__pycache__/ida_integration.cpython-311.pyc create mode 100644 plugins_sogen-support/tenet/integration/__pycache__/ida_loader.cpython-311.pyc create mode 100644 plugins_sogen-support/tenet/integration/api/__init__.py create mode 100644 plugins_sogen-support/tenet/integration/api/__pycache__/__init__.cpython-311.pyc create mode 100644 plugins_sogen-support/tenet/integration/api/__pycache__/api.cpython-311.pyc create mode 100644 plugins_sogen-support/tenet/integration/api/__pycache__/ida_api.cpython-311.pyc create mode 100644 plugins_sogen-support/tenet/integration/api/api.py create mode 100644 plugins_sogen-support/tenet/integration/api/ida_api.py create mode 100644 plugins_sogen-support/tenet/integration/ida_integration.py create mode 100644 plugins_sogen-support/tenet/integration/ida_loader.py create mode 100644 plugins_sogen-support/tenet/memory.py create mode 100644 plugins_sogen-support/tenet/registers.py create mode 100644 plugins_sogen-support/tenet/stack.py create mode 100644 plugins_sogen-support/tenet/trace/__init__.py create mode 100644 plugins_sogen-support/tenet/trace/__pycache__/__init__.cpython-311.pyc create mode 100644 plugins_sogen-support/tenet/trace/__pycache__/analysis.cpython-311.pyc create mode 100644 plugins_sogen-support/tenet/trace/__pycache__/file.cpython-311.pyc create mode 100644 plugins_sogen-support/tenet/trace/__pycache__/reader.cpython-311.pyc create mode 100644 plugins_sogen-support/tenet/trace/__pycache__/types.cpython-311.pyc create mode 100644 plugins_sogen-support/tenet/trace/analysis.py create mode 100644 plugins_sogen-support/tenet/trace/arch/__init__.py create mode 100644 plugins_sogen-support/tenet/trace/arch/__pycache__/__init__.cpython-311.pyc create mode 100644 plugins_sogen-support/tenet/trace/arch/__pycache__/amd64.cpython-311.pyc create mode 100644 plugins_sogen-support/tenet/trace/arch/__pycache__/x86.cpython-311.pyc create mode 100644 plugins_sogen-support/tenet/trace/arch/amd64.py create mode 100644 plugins_sogen-support/tenet/trace/arch/x86.py create mode 100644 plugins_sogen-support/tenet/trace/file.py create mode 100644 plugins_sogen-support/tenet/trace/reader.py create mode 100644 plugins_sogen-support/tenet/trace/types.py create mode 100644 plugins_sogen-support/tenet/types.py create mode 100644 plugins_sogen-support/tenet/ui/__init__.py create mode 100644 plugins_sogen-support/tenet/ui/__pycache__/__init__.cpython-311.pyc create mode 100644 plugins_sogen-support/tenet/ui/__pycache__/breakpoint_view.cpython-311.pyc create mode 100644 plugins_sogen-support/tenet/ui/__pycache__/hex_view.cpython-311.pyc create mode 100644 plugins_sogen-support/tenet/ui/__pycache__/palette.cpython-311.pyc create mode 100644 plugins_sogen-support/tenet/ui/__pycache__/reg_view.cpython-311.pyc create mode 100644 plugins_sogen-support/tenet/ui/__pycache__/trace_view.cpython-311.pyc create mode 100644 plugins_sogen-support/tenet/ui/breakpoint_view.py create mode 100644 plugins_sogen-support/tenet/ui/hex_view.py create mode 100644 plugins_sogen-support/tenet/ui/palette.py create mode 100644 plugins_sogen-support/tenet/ui/reg_view.py create mode 100644 plugins_sogen-support/tenet/ui/resources/icons/arrow.png create mode 100644 plugins_sogen-support/tenet/ui/resources/themes/horizon.json create mode 100644 plugins_sogen-support/tenet/ui/resources/themes/synth.json create mode 100644 plugins_sogen-support/tenet/ui/trace_view.py create mode 100644 plugins_sogen-support/tenet/util/__init__.py create mode 100644 plugins_sogen-support/tenet/util/__pycache__/__init__.cpython-311.pyc create mode 100644 plugins_sogen-support/tenet/util/__pycache__/log.cpython-311.pyc create mode 100644 plugins_sogen-support/tenet/util/__pycache__/misc.cpython-311.pyc create mode 100644 plugins_sogen-support/tenet/util/__pycache__/update.cpython-311.pyc create mode 100644 plugins_sogen-support/tenet/util/debug.py create mode 100644 plugins_sogen-support/tenet/util/log.py create mode 100644 plugins_sogen-support/tenet/util/misc.py create mode 100644 plugins_sogen-support/tenet/util/qt/__init__.py create mode 100644 plugins_sogen-support/tenet/util/qt/__pycache__/__init__.cpython-311.pyc create mode 100644 plugins_sogen-support/tenet/util/qt/__pycache__/shim.cpython-311.pyc create mode 100644 plugins_sogen-support/tenet/util/qt/__pycache__/util.cpython-311.pyc create mode 100644 plugins_sogen-support/tenet/util/qt/__pycache__/waitbox.cpython-311.pyc create mode 100644 plugins_sogen-support/tenet/util/qt/shim.py create mode 100644 plugins_sogen-support/tenet/util/qt/util.py create mode 100644 plugins_sogen-support/tenet/util/qt/waitbox.py create mode 100644 plugins_sogen-support/tenet/util/update.py create mode 100644 plugins_sogen-support/tenet_plugin.py diff --git a/plugins_sogen-support/tenet/__init__.py b/plugins_sogen-support/tenet/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/plugins_sogen-support/tenet/__pycache__/__init__.cpython-311.pyc b/plugins_sogen-support/tenet/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ee7b14513b4a238c02241644ddd283a7f42c3c68 GIT binary patch literal 175 zcmZ3^%ge<81nuV?5<&E15CH>>P{wCAAY(d13PUi1CZpd&ryk0@&FAkgB{FKt1RJ$TppphU;i}``X2WCb_#t#fIqKFwN1^{ssDjxs< literal 0 HcmV?d00001 diff --git a/plugins_sogen-support/tenet/__pycache__/breakpoints.cpython-311.pyc b/plugins_sogen-support/tenet/__pycache__/breakpoints.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..242072d43ca0a2ba105cc1b3395b9b8165ab989d GIT binary patch literal 9020 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 literal 0 HcmV?d00001 diff --git a/plugins_sogen-support/tenet/__pycache__/context.cpython-311.pyc b/plugins_sogen-support/tenet/__pycache__/context.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5e3de3b18df63fecfac8753ec03b3897cf799ca9 GIT binary patch literal 16481 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 literal 0 HcmV?d00001 diff --git a/plugins_sogen-support/tenet/__pycache__/hex.cpython-311.pyc b/plugins_sogen-support/tenet/__pycache__/hex.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..aaabc44eaec808d54385da5f130bae39147bb48b GIT binary patch literal 10601 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 z%0DceMPb-@Gy7hv)uJBQW9ssk0IpW+Bqf%>QGdtI<l-v(_GMjSdu+cGh4fG_)Up z_&2mOhXH&o;KqYkzz@=DXZuMM-2mV^86P(?P!W4=vA68MjBN(vZulA(E{Lla)Hz*I zoQ$I2nqVPMX-QE&UQqL1O@l(ANkuUz#?Hc##sH-b5qO!vF#@j2(;th{9k~({BI*3Pa)DsK$KGBu?-;v z`*xh~Cx%7b}E|Dg5(?9cYky{vQCFUehIT zrH1=Mx<`zP`y_4-afMe~D(d159=xZ^y_#Mkdf~FKrZl5xLcMSeSXZB>Yr&e*tey?^ z!i8aNy_SWiC>c013Z>&r=AsvX>@w>|-TiSb@#hR-bn*`zQ_Dvu>cVbJaL;FjZbibb zQRse;MkG412N8r&55IJi2sGW_#mB!P-7whbq!e~-N4E`3YLah$FmcAoDl__R1vi~@ zs^v6{EVy;4k;|vY#!#s)@X8K_M+cO7gfR0tT$&!n*aq2IxU~1MWc0zrrnIjp_jBfo8%FI&$ zP~wt*73p4AR=3d&WwVF)^{sS=DOLLr5w^u`!1FG4K^d|BVI)(USwi3X2kusISP1w# zaL0kFSSh&axTISP20SVX9sp-Jn5k11B8#3c95#A^nZs#I!~E=w=DJ1g?r|=3BCAJG z5nWiY%H_iP9s)LHQeXkt%Fq=^$!^(gw;ZrrPL{=!^^C@GKoS-82G}m&f zu$n5J=2*S#30WOVS@`osm^9wT+DrS#3ODg94+@M@)YM0~jKT$7-Z!^kT11kQ^2>vz z<8MaW_?4$sd-Ptj(gDCrC)=Th6fS3YN_skdX#+WX4e z$13gT?e_C!>HK=O^)F7Y4%j=7*d4D{vh1Gr1GmwYO8Zs2{c2gdy1r{4)3#+%#``1r z{&8#^w~lOAwS1ZhOiQVQ?O9IE|nO! z(8{ECMqxeuqp@|DhFt=PAnE5Q*mNleX(^vlAG%^6 zhhsHKnLi{zhbDt=VqDLj3>JQqU=$&E)}le`*&t4L(y#=UZWu%fgJv@RhyV@f`t0PZ zF^BCxkudQqHju>s3;XQZ)Rql_gPs=hn>pAoq?h8lfC8Pbx?Y=~g?{wGmIG5Y`2t+O53Ao%8?G zlg45Lx)>tljKjYZ*$2Ydr`;>Ia`~Z;t)T^cSxnv8;#T0tSNgP}(hp09 z)cG9y=?UWJ*KQ#0&(28<@ii3rop=JC4i0lCjn6SX2qKt&1AvfCELIh|%Klpwww3+2 xD!g2dzV#&=ePP{{VbB2zvkk literal 0 HcmV?d00001 diff --git a/plugins_sogen-support/tenet/__pycache__/memory.cpython-311.pyc b/plugins_sogen-support/tenet/__pycache__/memory.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..35e105242a1603f8644279130353b5aa90cd3c91 GIT binary patch literal 924 zcma)5&ui2`6rRZ@?R49%9;6_M1Z%;7W)b$Hh^%&@idqE4Ls%F>GTR0x$vVm6?y^D; zLigaoi}c#ls_Y+QD-}z?lP7O0>p^<*O*Yy3qX%D-FYo2OdGC84AAX_+%N!P!ABaxq6fwlGEtT0^e#61fG22XJl(&%`XnRUaWk`zAvJC#5^Ay&49w z2svlM$*R(qk_F=n;B3PLhAiwN=zy(4A2l#cAeP!kZER`KvyhyA!2D2jj{9{l7j>}5 zn@pdi^jgbr>c<88rTgKA=NM_3lTVa`Rd^)paRcBD-a#=2C$sps${rd(G@O3foqBn- zeE0AG&JU+OFsRi-bgGMsU;|~nJD>4<;<4w3nrKn0$%K$nCxlWj@M21n85(;rXRxsv zG2TejrW3c5IZ7RFM-iowY^&>AN7c3K^;#HpY;KG4s<54>OqB1|Zs`w2*kDoQg@MiW zYv$E*lead!AgaeKU~%0~bu^nDL7-Fj?BBYYIh8Yzj;;Xw+(KVS>C@u*U3H(V9FUdI z#QaFickw={9FWQ$sT?jX_tjVPeYnPO02woKqrmnVY*eJw58W1*{sN`XS~kxzvy{4_ zLutw+DP=o4jB`p2EEXgIscKb}08^z>I3*Le0f1S4jTA*ABP4+++C}O84~WIQ^EyaA zmr7)AvbOvqCKKPINsXj%L~cx0xha+)l-#0w0HXrN_z*4c{dteh&0-+&FT8&7U4H#> ILMk2h3pfwi&Hw-a literal 0 HcmV?d00001 diff --git a/plugins_sogen-support/tenet/__pycache__/registers.cpython-311.pyc b/plugins_sogen-support/tenet/__pycache__/registers.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..133e5631096713346e0245ee017268f2a674b5ef GIT binary patch literal 13343 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 WzAK7AN@IBUwLh?uKW4R_8`qZM|bKS+9Mw z>l|2)luD@%Ra8eMs!OFl_<`m)B`2lQRq7+{)4kQUwgQcG(n)>Fn=4ugohr58?0VOj zaP4^c_M4e+X1@9M_x<6MNTdZp`|z(fRJ8-4zq5|l0(EA82AGeLhBQt`3HIi7Zh^f8 zozDmffn&U=i7R16+0ikE`=e9&h>29rsf}e#3o?EDo4@vkplX(NpL>^G8ZP=Sj17k$Vd3pZR-$g}EY9 zd@E`Q^yg;^Ev}kBPU8<^A`O`N941794XC6jX(MeZ3Joe&+R|}MprVQO6cuwxYlVgt zC8?{XsVFAfRhA!peSPZky~(V(s_H5ky-C0_n2b(OPRMsiHibWW=Jfnx z+A!}~*ud63lNm9dTP3ZqQK9FEP2-*-y)bKz06BPszLX-r%096w(x5F3?n)P`(uHlU zB3-hjOJ(WOUe_CI;(F^E{PsZ}9dGGQunx0yky~a$PFwaZRz%CbWgB)oURLAoJF=wB zVz#TdWe@+zXl)t~dJ;`%guGEJ>Tw7yhdsVY*6}$uV|9D;!1pxxS5OjZ%r&@UP{IQ) zgu$h#la z-{&O*_)M&wiYiGi(Y8CY(=!Wi-&N+P6K~UivxuHB@)_l4s}?pDfT5zN4NRB~X<+gv zvv((HV9t59Ea4R;l_eR~BFvODFuyQy^9Qs=&HLlbKrW6Ws&fOy)jTE;W(gW}y97cO z^6*QT-N_jsg)>Jn*t7`~F&BXV5Ni^OjXrO+&)opTwb;_o-q?3Pf88FN{$j2=HdB_> z--os$rO4kFerhkMuE?1$<>9S}$2SA{VpY2M;yV@TvMpUMdk=XH&T7IivkLRN{l5TS z0m0}v1UK-z%w(PRkS}sYh;jumPzNfsBlM7i5wy%ckJB2cWik(iFx$N`+rX|+6pDNS z*8GBJp6RxSVw1FwX^=fmI$%fxh(qr|Mwmw{!h7fg&SICXw_>0ZOJsH6L zw;KG>70v54VX^M1CeKbh>^;kE-Y#Gtv3?G(1q@CL7RBb6tA!5TIM`%;-_J^5+gDpL z*sKx6WtK_8hdk~Uq_8Zw68Kaun>H*=WYq$PT1-E{hU-?cs%ZoQs+=|)xeU%^$*QbD zvWYi5j)k&YFaHCSFl$k6nsRf51*(}?2Vv@wvy5~y!fZ&ofY4BVb1G`8rBX4Yn)j)o zV}pu%+O()(Vv7b%i{z8=&Ko%h-Zbc{KqW(ckX}?R91FRQ7IET$Qs2`#hPD%|s}@`b zGK_{-$PhdG+wAQ2oJvfrdEzF+4~s(39Pgy&W2+fvW2)L)hQOYw>{W=msb?=e|geG#ANLx1k$ zCkBP%BgwUTBq@RLs|_&jXU0ZcIf`YF@*@qII^1hS5VNgm+|Q9@w1`T6bUCcSQ|LVN z2RIgw{8LJPgz*J{Il6*XjIjHm+}x0c-Hy2#jQW{(nI#jaX?y|nWpVN7&7GQ5D7@o) z-|Pb@q|%z38#SC#^SUKN;&St3T3eBmORBMmH5r3j!xUsxs9YQFJM)rVGS*5@fWIjz zNV|%)1Wvz{)iuX&DR1Ve$kHx7p`;g$EWt|M`=HRG?NF=Xqy?;ErVE%kU5YJN>Yf^^ z_sIn~#_@c@YKa){`mF#enk-g$p4X-nlZ3Vs0mUbJ&JI#7)c zRH8$6bf_F1f|1SeZnVD|?XN@!?C3x_I~XoA)=3lC@Q^hbMR5 zuMFR=blt8<)3!8S_8upr#=@+~HPj4-BDY0Rpa{w9jBitv_w%Y=p9v|7mQ5;(Q=K~H z0U3k4k*h#xq+SIlv92phjB^VL2B;fuIl(aJ6e29jnm-2uMT7UN@q9q4Ax0!VFu=H1 zh!hH3si9-YiN3(a8uH2axi$<^ zg8!sUrxtT6E@$zl$lZulO|HW2^`1mp~xfW_Z8|Yd(R#kC^tWQ e=v?`@r*`&79LH|<&99BjKit`W{tqEX+y4R&;#6D! literal 0 HcmV?d00001 diff --git a/plugins_sogen-support/tenet/__pycache__/types.cpython-311.pyc b/plugins_sogen-support/tenet/__pycache__/types.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0578fe372d8090dc983cb459737fad6c6d6de979 GIT binary patch literal 2848 zcmaJ@&2JM&6rWx1hZBE>1c(BJEm1$BLQq>3i58I>dr91a1CCq3inLlh6EJLScXmyi zCi4)IM%dsD#_#JB(TP10Y@#M#>T;WtmFl z2$jnUjg(cYlr^fBb*h!4REKq$jQKtWaUWw>m>=__mQ}Mi zm}U9nmbFo*O$Wzu%lf`)JBJ+Q)}xZ?tEfOvz zKKncA!n)+`;?fsuxw^Y)J2towakt}lFYwc#|MRBP^< z$7;-5^T1|zp|Q!)MgeIUxR(K1y{DZ_wQ>mNAS?;8WwuN{6{;T}%Nbyw6ibMo>g$(*hZySEg*}@fVt4UDHOM*ZDPSF zXxlPSVcwDL2v8_5+O`4|YKle)Y#|lq6<>W}J5A;*8@Bu0SAVEd@2RigX+kg+mpSYF zB4}^};XMFr!J+rI49B)re z!)vTZT|jCV9ua@AZ0NlB`=C91KzJk^;X~-|96j)JOHA%>ctyv3RQyiu>#JW;rVN6*i}YK8geus4`dzEU(ZP>>Ibd8ksw zd}wBUEtAcftEiAl)B5X@kdJhk`jFHY+=K7qriJ>YPdDBW4$ zob2$So2kB=<_lKy#0AwaFb*LBKlLF$p}ulCGw16?^X|%bI4}#DTX$d(&E;3cXxP;& zun zw~FJK|_;XP$WgV*8e_NvS^>g$GWJnp52LD@PM z2i%_vVlffAiUKBw4Xuqp@7@M@O}3?${5APW5=9-#!0gE-PLYQzh$c#0E|j2>F2RJa z+mv#+!F|nPwI@*JI;+`_9Y#T54g~n;C>66S6%Z2j&9A|XizcG?qQ*rPZcPuxr&s1# zR9FCjGM}9JCps7W+EW)rt)HERSG;}ZJiKDj~ zqI&Pk^EP@T_@dwB=o40|TLkPxBuQ$MSW7&eSxriJ$SCa6m(@2of6!;9BT}+MMj?=e z`GY<)N!0kxoxS^QCAM?-xYtT=vXjsyxf3B$sxzfZaNmx??)n>?J?OK}s1nA11old= q)=>65${tE26=?!)@e$agufRu`Kj<^#W2iL>dtd>~9vs6GG5;SvLP*{K literal 0 HcmV?d00001 diff --git a/plugins_sogen-support/tenet/breakpoints.py b/plugins_sogen-support/tenet/breakpoints.py new file mode 100644 index 0000000..37ed00b --- /dev/null +++ b/plugins_sogen-support/tenet/breakpoints.py @@ -0,0 +1,195 @@ +import itertools + +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 + +#------------------------------------------------------------------------------ +# breakpoints.py -- Breakpoint Controller +#------------------------------------------------------------------------------ +# +# The purpose of this file is to house the 'headless' components of the +# breakpoints window and its underlying functionality. This is split into +# a model and controller component, of a typical 'MVC' design pattern. +# +# v0.1 NOTE/TODO: err, a dedicated bp window was planned but did not quite +# make the cut for the initial release of this plugin. For that reason, +# some of this logic may be half-baked pending further work. +# +# v0.2 NOTE/TODO: Currently, the breakpoint controller/Tenet artificially +# limits usage to one execution breakpoint and one memory breakpoint at +# a time. I'll probably raise this 'limit' when a proper gui is made +# for managing and differentiating between breakpoints... +# + +class BreakpointController(object): + """ + The Breakpoint Controller (Logic) + """ + + def __init__(self, pctx): + self.pctx = pctx + self.model = BreakpointModel() + + # UI components + if QT_AVAILABLE: + self.view = BreakpointView(self, self.model) + self.dockable = DockableWindow("Trace Breakpoints", self.view) + else: + self.view = None + self.dockable = None + + # events + self._ignore_signals = False + self.pctx.core.ui_breakpoint_changed(self._ui_breakpoint_changed) + + def reset(self): + """ + Reset the breakpoint controller. + """ + self.model.reset() + + def add_breakpoint(self, address, access_type, length=1): + """ + Add a breakpoint of the given access type. + """ + if access_type == BreakpointType.EXEC: + self.add_execution_breakpoint(address, length) + elif access_type == BreakpointType.READ: + self.add_read_breakpoint(address, length) + elif access_type == BreakpointType.WRITE: + self.add_write_breakpoint(address, length) + elif access_type == BreakpointType.ACCESS: + self.add_access_breakpoint(address, length) + else: + raise ValueError("UNKNOWN ACCESS TYPE", access_type) + + def add_execution_breakpoint(self, address): + """ + Add an execution breakpoint for the given address. + """ + self.model.bp_exec[address] = TraceBreakpoint(address, BreakpointType.EXEC) + self.model._notify_breakpoints_changed() + + def add_read_breakpoint(self, address, length=1): + """ + Add a memory read breakpoint for the given address. + """ + self.model.bp_read[address] = TraceBreakpoint(address, BreakpointType.READ, length) + self.model._notify_breakpoints_changed() + + def add_write_breakpoint(self, address, length=1): + """ + Add a memory write breakpoint for the given address. + """ + self.model.bp_write[address] = TraceBreakpoint(address, BreakpointType.WRITE, length) + self.model._notify_breakpoints_changed() + + def add_access_breakpoint(self, address, length=1): + """ + Add a memory access breakpoint for the given address. + """ + self.model.bp_access[address] = TraceBreakpoint(address, BreakpointType.ACCESS, length) + self.model._notify_breakpoints_changed() + + def clear_breakpoints(self): + """ + Clear all breakpoints. + """ + self.model.bp_exec = {} + self.model.bp_read = {} + self.model.bp_write = {} + self.model.bp_access = {} + self.model._notify_breakpoints_changed() + + def clear_execution_breakpoints(self): + """ + Clear all execution breakpoints. + """ + self.model.bp_exec = {} + self.model._notify_breakpoints_changed() + + def clear_memory_breakpoints(self): + """ + Clear all memory breakpoints. + """ + self.model.bp_read = {} + self.model.bp_write = {} + self.model.bp_access = {} + self.model._notify_breakpoints_changed() + + def _ui_breakpoint_changed(self, address, event_type): + """ + Handle a breakpoint change event from the UI. + """ + if self._ignore_signals: + return + + self._delete_disassembler_breakpoints() + self.model.bp_exec = {} + + if event_type in [BreakpointEvent.ADDED, BreakpointEvent.ENABLED]: + self.add_execution_breakpoint(address) + + self.model._notify_breakpoints_changed() + + def _delete_disassembler_breakpoints(self): + """ + Remove all execution breakpoints from the disassembler UI. + """ + dctx = disassembler[self.pctx] + + self._ignore_signals = True + for address in self.model.bp_exec: + dctx.delete_breakpoint(address) + self._ignore_signals = False + +class BreakpointModel(object): + """ + The Breakpoint Model (Data) + """ + + def __init__(self): + self.reset() + + #---------------------------------------------------------------------- + # Callbacks + #---------------------------------------------------------------------- + + self._breakpoints_changed_callbacks = [] + + def reset(self): + self.bp_exec = {} + self.bp_read = {} + self.bp_write = {} + self.bp_access = {} + + @property + def memory_breakpoints(self): + """ + Return an iterable list of all memory breakpoints. + """ + bps = itertools.chain( + self.bp_read.values(), + self.bp_write.values(), + self.bp_access.values() + ) + return bps + + #---------------------------------------------------------------------- + # Callbacks + #---------------------------------------------------------------------- + + def breakpoints_changed(self, callback): + """ + Subscribe a callback for a breakpoint changed event. + """ + register_callback(self._breakpoints_changed_callbacks, callback) + + def _notify_breakpoints_changed(self): + """ + Notify listeners of a breakpoint changed event. + """ + notify_callback(self._breakpoints_changed_callbacks) diff --git a/plugins_sogen-support/tenet/context.py b/plugins_sogen-support/tenet/context.py new file mode 100644 index 0000000..340e5bd --- /dev/null +++ b/plugins_sogen-support/tenet/context.py @@ -0,0 +1,429 @@ +import os +import logging +import traceback + +from tenet.util.qt import * +from tenet.util.log import pmsg +from tenet.util.misc import is_plugin_dev + +from tenet.stack import StackController +from tenet.memory import MemoryController +from tenet.registers import RegisterController +from tenet.breakpoints import BreakpointController +from tenet.ui.trace_view import TraceDock + +from tenet.types import BreakpointType +from tenet.trace.arch import ArchAMD64, ArchX86 +from tenet.trace.reader import TraceReader +from tenet.integration.api import disassembler, DisassemblerContextAPI + +logger = logging.getLogger("Tenet.Context") + +#------------------------------------------------------------------------------ +# context.py -- Plugin Database Context +#------------------------------------------------------------------------------ +# +# The purpose of this file is to house and manage the plugin's +# disassembler database (eg, IDB/BNDB) specific runtime state. +# +# At a high level, a unique 'instance' of the plugin runtime & subsystems +# are initialized for each opened database in supported disassemblers. The +# plugin context object acts a bit like the database specific plugin core. +# +# For example, it is possible for multiple databases to be open at once +# in the Binary Ninja disassembler. Each opened database will have a +# unique plugin context object created and used to manage state, UI, +# threads/subsystems, and loaded plugin data for that database. +# +# In IDA, this is less important as you can only have one database open +# at any given time (... at least at the time of writing) but that does +# not change how this context system works under the hood. +# + +class TenetContext(object): + """ + A per-database encapsulation of the plugin components / state. + """ + + def __init__(self, core, db): + disassembler[self] = DisassemblerContextAPI(db) + self.core = core + 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() + + # this will hold the trace reader when a trace has been loaded + self.reader = None + + # plugin widgets / components + self.breakpoints = BreakpointController(self) + self.trace = TraceDock(self) # TODO: port this one to MVC pattern + self.stack = StackController(self) + self.memory = MemoryController(self) + self.registers = RegisterController(self) + + # the directory to start the 'load trace file' dialog in + self._last_directory = None + + # whether the plugin subsystems have been created / started + self._started = False + + # NOTE/DEV: automatically open a test trace file when dev/testing + if is_plugin_dev(): + self._auto_launch() + + def _auto_launch(self): + """ + Automatically load a static trace file when the database has been opened. + + NOTE/DEV: this is just to make it easier to test / develop / debug the + plugin when developing it and should not be called under normal use. + """ + + def test_load(): + import ida_loader + trace_filepath = ida_loader.get_plugin_options("Tenet") + focus_window() + self.load_trace(trace_filepath) + self.show_ui() + + def dev_launch(): + self._timer = QtCore.QTimer() + self._timer.singleShot(500, test_load) # delay to let things settle + + self.core._ui_hooks.ready_to_run = dev_launch + + #------------------------------------------------------------------------- + # Properties + #------------------------------------------------------------------------- + + @property + def palette(self): + return self.core.palette + + #------------------------------------------------------------------------- + # Setup / Teardown + #------------------------------------------------------------------------- + + def start(self): + """ + One-time initialization of the plugin subsystems. + + This will only be called when it is clear the user is attempting + to use the plugin or its functionality (eg, they click load trace). + """ + if self._started: + return + + self.palette.warmup() + self._started = True + + def terminate(self): + """ + Spin down any plugin subsystems as the context is being deleted. + + This will be called when the database or disassembler is closing. + """ + self.close_trace() + + #------------------------------------------------------------------------- + # Public API + #------------------------------------------------------------------------- + + def trace_loaded(self): + """ + Return True if a trace is loaded / active in this plugin context. + """ + return bool(self.reader) + + def load_trace(self, filepath): + """ + Load a trace from the given filepath. + + If there is a trace already loaded / in-use prior to calling this + function, it will simply be replaced by the new trace. + """ + + # + # create the trace reader. this will load the given trace file from + # disk and wrap it with a number of useful APIs for navigating the + # trace and querying information (memory, registers) from it at + # chosen states of execution + # + + self.reader = TraceReader(filepath, self.arch, disassembler[self]) + pmsg(f"Loaded trace {self.reader.trace.filepath}") + pmsg(f"- {self.reader.trace.length:,} instructions...") + + if self.reader.analysis.slide != None: + pmsg(f"- {self.reader.analysis.slide:08X} ASLR slide...") + else: + disassembler.warning("Failed to automatically detect ASLR base!\n\nSee console for more info...") + pmsg(" +------------------------------------------------------") + pmsg(" |- ERROR: Failed to detect ASLR base for this trace.") + pmsg(" | --------------------------------------- ") + pmsg(" +-+ You can 'try' rebasing the database to the correct ASLR base") + pmsg(" | if you know it, and reload the trace. Otherwise, it is possible") + pmsg(" | your trace is just... very small and Tenet was not confident") + pmsg(" | predicting an ASLR slide.") + + # + # we only hook directly into the disassembler / UI / subsytems once + # a trace is loaded. this ensures that our python handlers don't + # introduce overhead on misc disassembler callbacks when the plugin + # isn't even being used in the reversing session. + # + + self.core.hook() + + # + # attach the trace engine to the various plugin UI controllers, giving + # them the necessary access to drive the underlying trace reader + # + + self.breakpoints.reset() + self.trace.attach_reader(self.reader) + self.stack.attach_reader(self.reader) + self.memory.attach_reader(self.reader) + self.registers.attach_reader(self.reader) + + # + # connect any high level signals from the new trace reader + # + + self.reader.idx_changed(self._idx_changed) + + def close_trace(self): + """ + Close the current trace if one is active. + """ + if not self.reader: + return + + # + # unhook the disassembler, as there will be no active / loaded trace + # after this routine completes + # + + self.core.unhook() + + # + # close UI elements and reset their model / controllers + # + + self.trace.hide() + self.trace.detach_reader() + self.stack.hide() + self.stack.detach_reader() + self.memory.hide() + self.memory.detach_reader() + self.registers.hide() + self.registers.detach_reader() + + # misc / final cleanup + self.breakpoints.reset() + #self.reader.close() + + self.reader = None + + def show_ui(self): + """ + Integrate and arrange the plugin widgets into the disassembler UI. + + TODO: ehh, there really shouldn't be any disassembler-specific stuff + 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) + + #self.breakpoints.dockable.set_dock_position("CPU Registers", ida_kernwin.DP_BOTTOM) + #self.breakpoints.dockable.show() + + #ida_kernwin.activate_widget(ida_kernwin.find_widget("Output window"), True) + #ida_kernwin.set_dock_pos("Output window", None, ida_kernwin.DP_BOTTOM) + #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.stack.dockable.set_dock_position("Memory View", ida_kernwin.DP_RIGHT) + self.stack.show("Memory View", ida_kernwin.DP_RIGHT) + + mw = get_qmainwindow() + mw.addToolBar(QtCore.Qt.RightToolBarArea, self.trace) + self.trace.show() + + # trigger update check + self.core.check_for_update() + + #------------------------------------------------------------------------- + # Integrated UI Event Handlers + #------------------------------------------------------------------------- + + def interactive_load_trace(self, reloading=False): + """ + Handle UI actions for loading a trace file. + """ + + # prompt the user with a file dialog to select a trace of interest + filenames = self._select_trace_file() + if not filenames: + return + + # TODO: ehh, only support loading one trace at a time right now + assert len(filenames) == 1, "Please select only one trace file to load" + disassembler.show_wait_box("Loading trace from disk...") + filepath = filenames[0] + + # attempt to load the user selected trace + try: + self.load_trace(filepath) + except: + pmsg("Failed to load trace...") + pmsg(traceback.format_exc()) + disassembler.hide_wait_box() + return + disassembler.hide_wait_box() + + # + # if we are 're-loading', we are loading over an existing trace, so + # there should already be plugin UI elements visible and active. + # + # do not attempt to show / re-position the UI elements as they may + # have been moved by the user from their default positions into + # locations that they prefer + # + + if reloading: + return + + # show the plugin UI elements, and dock its windows as appropriate + self.show_ui() + + def interactive_next_execution(self): + """ + Handle UI actions for seeking to the next execution of the selected address. + """ + address = disassembler[self].get_current_address() + rebased_address = self.reader.analysis.rebase_pointer(address) + result = self.reader.seek_to_next(rebased_address, BreakpointType.EXEC) + + # TODO: blink screen? make failure more visible... + if not result: + pmsg(f"Go to 0x{address:08x} failed, no future executions of address") + + def interactive_prev_execution(self): + """ + Handle UI actions for seeking to the previous execution of the selected address. + """ + address = disassembler[self].get_current_address() + rebased_address = self.reader.analysis.rebase_pointer(address) + result = self.reader.seek_to_prev(rebased_address, BreakpointType.EXEC) + + # TODO: blink screen? make failure more visible... + if not result: + pmsg(f"Go to 0x{address:08x} failed, no previous executions of address") + + def interactive_first_execution(self): + """ + Handle UI actions for seeking to the first execution of the selected address. + """ + address = disassembler[self].get_current_address() + rebased_address = self.reader.analysis.rebase_pointer(address) + result = self.reader.seek_to_first(rebased_address, BreakpointType.EXEC) + + # TODO: blink screen? make failure more visible... + if not result: + pmsg(f"Go to 0x{address:08x} failed, no executions of address") + + def interactive_final_execution(self): + """ + Handle UI actions for seeking to the final execution of the selected address. + """ + address = disassembler[self].get_current_address() + rebased_address = self.reader.analysis.rebase_pointer(address) + result = self.reader.seek_to_final(rebased_address, BreakpointType.EXEC) + + # TODO: blink screen? make failure more visible... + if not result: + pmsg(f"Go to 0x{address:08x} failed, no executions of address") + + def _idx_changed(self, idx): + """ + Handle a trace reader event indicating that the current IDX has changed. + + This will make the disassembler track with the PC/IP of the trace reader. + """ + dctx = disassembler[self] + + # + # get a 'rebased' version of the current instruction pointer, which + # should map to the disassembler / open database if it is a code + # address that is known + # + + bin_address = self.reader.rebased_ip + + # + # if the code address is in a library / other unknown area that + # cannot be renedered by the disassembler, then resolve the last + # known trace 'address' within the database + # + + if not dctx.is_mapped(bin_address): + last_good_idx = self.reader.analysis.get_prev_mapped_idx(idx) + if last_good_idx == -1: + return # navigation is just not gonna happen... + + # fetch the last instruction pointer to fall within the trace + last_good_trace_address = self.reader.get_ip(last_good_idx) + + # convert the trace-based instruction pointer to one that maps to the disassembler + bin_address = self.reader.analysis.rebase_pointer(last_good_trace_address) + + # navigate the disassembler to a 'suitable' address based on the trace idx + dctx.navigate(bin_address) + disassembler.refresh_views() + + def _select_trace_file(self): + """ + Prompt a file selection dialog, returning file selections. + + This will save & reuses the last known directory for subsequent calls. + """ + + if not self._last_directory: + self._last_directory = disassembler[self].get_database_directory() + + # create & configure a Qt File Dialog for immediate use + file_dialog = QtWidgets.QFileDialog( + None, + 'Open trace file', + self._last_directory, + 'All Files (*.*)' + ) + file_dialog.setFileMode(QtWidgets.QFileDialog.ExistingFiles) + + # prompt the user with the file dialog, and await filename(s) + filenames, _ = file_dialog.getOpenFileNames() + + # + # remember the last directory we were in (parsed from a selected file) + # for the next time the user comes to load trace files + # + + if filenames: + self._last_directory = os.path.dirname(filenames[0]) + os.sep + + # log the captured (selected) filenames from the dialog + logger.debug("Captured filenames from file dialog:") + for name in filenames: + logger.debug(" - %s" % name) + + # return the captured filenames + return filenames \ No newline at end of file diff --git a/plugins_sogen-support/tenet/core.py b/plugins_sogen-support/tenet/core.py new file mode 100644 index 0000000..861900d --- /dev/null +++ b/plugins_sogen-support/tenet/core.py @@ -0,0 +1,256 @@ +import abc +import logging + +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 + +logger = logging.getLogger("Tenet.Core") + +#------------------------------------------------------------------------------ +# core.py -- Plugin Core +#------------------------------------------------------------------------------ +# +# The purpose of this file is to define a specification required by the +# plugin to integrate and load under a given disassembler. +# +# This is technically the 'lowest' level layer of the plugin, as it is +# loaded / unloaded directly by the disassembler. This means that there +# should be no database or user-specific data loaded into this layer. +# +# Supporting additional disassemblers will require one to subclass this +# abstract core as part of a disassembler-specific integration layer. +# + +class TenetCore(object): + """ + The disassembler-wide plugin core. + """ + __metaclass__ = abc.ABCMeta + + #-------------------------------------------------------------------------- + # Plugin Metadata + #-------------------------------------------------------------------------- + + PLUGIN_NAME = "Tenet" + PLUGIN_VERSION = "0.2.0" + PLUGIN_AUTHORS = "Markus Gaasedelen" + PLUGIN_DATE = "2021" + + #-------------------------------------------------------------------------- + # Initialization / Teardown + #-------------------------------------------------------------------------- + + def load(self): + """ + Load the plugin, and register universal UI actions with the disassembler. + """ + self.contexts = {} + self._update_checked = False + + # the plugin color palette + self.palette = PluginPalette() + self.palette.theme_changed(self.refresh_theme) + + # integrate plugin UI to disassembler + self._install_ui() + + # all done, mark the core as loaded + self.loaded = True + + # print plugin banner + pmsg(f"Loaded v{self.PLUGIN_VERSION} - (c) {self.PLUGIN_AUTHORS} - {self.PLUGIN_DATE}") + logger.info("Successfully loaded plugin") + + def unload(self): + """ + Unload the plugin, and remove any UI integrations. + """ + if not self.loaded: + return + + pmsg("Unloading %s..." % self.PLUGIN_NAME) + + # mark the core as 'unloaded' and teardown its components + self.loaded = False + + # remove UI integrations + self._uninstall_ui() + + # spin down any active contexts (stop threads, cleanup qt state, etc) + for pctx in self.contexts.values(): + pctx.terminate() + self.contexts = {} + + # all done + logger.info("-"*75) + logger.info("Plugin terminated") + + @abc.abstractmethod + def hook(self): + """ + Install disassmbler-specific hooks. + """ + pass + + @abc.abstractmethod + def unhook(self): + """ + Remove disassmbler-specific hooks. + """ + pass + + #-------------------------------------------------------------------------- + # Disassembler / Database Context Selector + #-------------------------------------------------------------------------- + + @abc.abstractmethod + def get_context(self, db, startup=True): + """ + Get the plugin context object for the given database / session. + """ + pass + + #-------------------------------------------------------------------------- + # UI Integration + #-------------------------------------------------------------------------- + + def _install_ui(self): + """ + Initialize & integrate all plugin UI elements. + """ + self._install_load_trace() + self._install_next_execution() + self._install_prev_execution() + self._install_first_execution() + self._install_final_execution() + + def _uninstall_ui(self): + """ + Cleanup & remove all plugin UI integrations. + """ + self._uninstall_load_trace() + self._uninstall_next_execution() + self._uninstall_prev_execution() + self._uninstall_first_execution() + self._uninstall_final_execution() + + @abc.abstractmethod + def _install_load_trace(self): + """ + Install the 'File->Load->Tenet trace file...' menu entry. + """ + pass + + @abc.abstractmethod + def _install_next_execution(self): + """ + Install the right click 'Go to next execution' menu entry. + """ + pass + + @abc.abstractmethod + def _install_prev_execution(self): + """ + Install the right click 'Go to previous execution' menu entry. + """ + pass + + @abc.abstractmethod + def _install_first_execution(self): + """ + Install the right click 'Go to first execution' menu entry. + """ + pass + + @abc.abstractmethod + def _install_final_execution(self): + """ + Install the right click 'Go to final execution' menu entry. + """ + pass + + @abc.abstractmethod + def _uninstall_load_trace(self): + """ + Remove the 'File->Load file->Tenet trace file...' menu entry. + """ + pass + + @abc.abstractmethod + def _uninstall_next_execution(self): + """ + Remove the right click 'Go to next execution' menu entry. + """ + pass + + @abc.abstractmethod + def _uninstall_prev_execution(self): + """ + Remove the right click 'Go to previous execution' menu entry. + """ + pass + + @abc.abstractmethod + def _uninstall_first_execution(self): + """ + Remove the right click 'Go to first execution' menu entry. + """ + pass + + @abc.abstractmethod + def _uninstall_final_execution(self): + """ + Remove the right click 'Go to final execution' menu entry. + """ + pass + + #-------------------------------------------------------------------------- + # UI Event Handlers + #-------------------------------------------------------------------------- + + def _interactive_load_trace(self, db): + pctx = self.get_context(db) + pctx.interactive_load_trace() + + def _interactive_first_execution(self, db): + pctx = self.get_context(db) + pctx.interactive_first_execution() + + def _interactive_final_execution(self, db): + pctx = self.get_context(db) + pctx.interactive_final_execution() + + def _interactive_next_execution(self, db): + pctx = self.get_context(db) + pctx.interactive_next_execution() + + def _interactive_prev_execution(self, db): + pctx = self.get_context(db) + pctx.interactive_prev_execution() + + #-------------------------------------------------------------------------- + # Core Actions + #-------------------------------------------------------------------------- + + def refresh_theme(self): + """ + Refresh UI facing elements to reflect the current theme. + """ + for pctx in self.contexts.values(): + pass # TODO + + def check_for_update(self): + """ + Check if there is an update available for the plugin. + """ + if self._update_checked: + return + + # wrap the callback (a popup) to ensure it gets called from the UI + callback = disassembler.execute_ui(disassembler.warning) + + # kick off the async update check + check_for_update(self.PLUGIN_VERSION, callback) + self._update_checked = True diff --git a/plugins_sogen-support/tenet/hex.py b/plugins_sogen-support/tenet/hex.py new file mode 100644 index 0000000..bd8b173 --- /dev/null +++ b/plugins_sogen-support/tenet/hex.py @@ -0,0 +1,305 @@ +from tenet.ui import * +from tenet.types import * +from tenet.util.qt.util import copy_to_clipboard +from tenet.integration.api import DockableWindow + +#------------------------------------------------------------------------------ +# 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 HexController(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 = 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 + # + + 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_sogen-support/tenet/integration/__pycache__/ida_integration.cpython-311.pyc b/plugins_sogen-support/tenet/integration/__pycache__/ida_integration.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0a62a2adf0d083470d3f609c232b56a73dd22e3e GIT binary patch literal 20433 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 M+r2xIkVW}_0ak&@EC2ui literal 0 HcmV?d00001 diff --git a/plugins_sogen-support/tenet/integration/__pycache__/ida_loader.cpython-311.pyc b/plugins_sogen-support/tenet/integration/__pycache__/ida_loader.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c5239af358cc991a35a361a6b08c9d716716522e GIT binary patch literal 3479 zcmb7G+iM)x89#H=&g_mZ*2Pw(jXFu~*lWjQY?nYxQldn%Bw}xbtpvOnmeJ19YUJ6O z&6!zb4R+WTLser)3IS8-gPt7PJoGhri~A3BwhVL`5H*xM^lgKa5c1UToYCI2j&aYN z`OfV-x9|6z-#MQT52q1?-~Z!nG0p`e)3Jyb~ZXbG7^_mG*w{8pyj z`bYp>_z!edle>;`@_%VDpW|Y&TJg$|%4^uc{>s99FvfB^Wc4eqVPaA`=*towBpvPP z9EgX=@2mo$M^N(x{7HaWxZusS2H_h1sAdstYE`>dwj2#RKH1T#uI2dJhD$VwQDyB1@vY3y)h7V z^u3JKnMP(d%FKr1ETKRQ*aYg(({&IJ5k?eQK$R;9Slnv+0D}AwRihzDAzB8R1X?c0 zCJ$H&g|sOaGLWha*I|ljVZ=;<95rRIt3v2F!6{~?wM|S7*tV_b*VdU)6BL<^H6h?M7Gdj}OeJ@25cPJWMi$3S=lHC?~pkn{Gh zkl(QcLdOxgc$f2)Zby^r_izsR?2IKEdp&)7`tXPQ+<+5wm;0D=a}Z|mZw218MY0+yS~HByuFDX&GKmj^&ZP9Yd#>5_*vq&v~w_X z*|2Cg-_F5y(Ogv<} zcwQnra#RL;<9rW0-^QP0v zx1ia(2cnKzX-T=bKdt>K{pa-V!h@BjI`&}Yll=XBL(N8NHe~U~{n6JyP{h)%Vr$f2rAjg7!=28|sBfy$}w>A0f0P7zNVr$I;kKGgtNS+O|iw*g5Bwr5OF=4CD2?cG+N6cyw)PmKZ z*baNWdhJTMvkspuzr$$=ZFLaoKj(0wJ0l?UGb0b2Sp@-~(ABZ9M~}on=${8}N&)z& z)5kdv?9#&)z%Iw{@_lQ0Hh&ztRXN1?ga4Rk`~hhXf4s>4g}ynTZP|hSzd#@9j&O$y zV21(h<1jL&0KPpGEWe0vCoEQTp37qs^ltGnsc4>W5FeYmt_K&+{@)=Voa!=S$;O6b z?`X-~2`g$F#I3Ls61>ux({vpS6KbbU=odFM50_lW^q?Y_OObJKj94=9daWFbzE#0w zl-3L-gOVdqAQK>RVsfq|wgGaCDhU|Iq0U9Q`hXefCI^_2R(=S2-oHU4=WO&7^}ZTT zoo>iyBKb@xpZR8N@-yX+na?u2(!M-GTR;7Q8|qx7&V}k+ zb2>}Ob$2CUQV!Hws4IKebE%;&N9uAo5E*+4v=61vAv8Mi!1}|j2e)>G!G(O2EG?XR znE*2}US+i+gEfznj8TLU-s#rALQn5%TVX)4Xw0iCRcjNTI{dD#Hg-*lr%EDqS zZP-THi&G`HQh~Q&EN)`E8fUf*c&?dj%6Ryou<81@@D8CXB`;BNoC^AkVnoNFWUnJ8 zPC~u+TM+fm*W!6ehARuiQ8aukQeW9CL?Q zQuUf|**df)m;TF1WzW-9XLo-0DVsbTlrr|Ou=fEw8SD)k%gMW>=qEPmx6@DZn+b@& zA+N*ql%c$@L9_&pasZg76 ztn9cbv?&D-xwOn$kt5SQ?95k<*+MlhIZIg~jeDi>9pL++YDF`$zWzIPX-k*GyvRf@ zu*L_*X!8-SPy*MlUaTdNwg)U@rrS4cXCzD)mkzg$elM(*TCpsAy7Hu3WaC|_Y*Gp( z%0%K#yM~q6PjHnaO{J1$nywTF*SCAQ=HpB}Gff=fe#P!1)RYiXw_x$^QKN2vzj(U( zmENm?lE+hjWAcBWF11hB&UznuM=z)CM-%^kz5Gv2$R?Qv?FssW=u0s877Ql-z|Gnu KH8?a2IQ%F4p`HH# literal 0 HcmV?d00001 diff --git a/plugins_sogen-support/tenet/integration/api/__pycache__/api.cpython-311.pyc b/plugins_sogen-support/tenet/integration/api/__pycache__/api.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6fb0a550ea3dd9b83aa4cf4678c732243e2798ab GIT binary patch literal 13151 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 literal 0 HcmV?d00001 diff --git a/plugins_sogen-support/tenet/integration/api/__pycache__/ida_api.cpython-311.pyc b/plugins_sogen-support/tenet/integration/api/__pycache__/ida_api.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cc5f0f5b96164cea07fd10a7cf83f224e3269fd9 GIT binary patch literal 22423 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 literal 0 HcmV?d00001 diff --git a/plugins_sogen-support/tenet/integration/api/api.py b/plugins_sogen-support/tenet/integration/api/api.py new file mode 100644 index 0000000..37d7f8b --- /dev/null +++ b/plugins_sogen-support/tenet/integration/api/api.py @@ -0,0 +1,326 @@ +import abc +import logging + +from ...util.qt import * + +logger = logging.getLogger("Tenet.Integration.API") + +#------------------------------------------------------------------------------ +# Disassembler API +#------------------------------------------------------------------------------ +# +# the purpose of this file is to provide an abstraction layer for the more +# generic disassembler APIs required by the plugin codebase. we strive to +# use (or extend) this API for the bulk of our disassembler operations, +# making the plugin as disassembler-agnostic as possible. +# +# 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 +# + +class DisassemblerCoreAPI(object): + """ + An abstract implementation of the core disassembler APIs. + """ + __metaclass__ = abc.ABCMeta + + # the name of the disassembler framework, eg 'IDA' or 'BINJA' + NAME = NotImplemented + + @abc.abstractmethod + def __init__(self): + self._ctxs = {} + + # required version fields + self._version_major = NotImplemented + self._version_minor = NotImplemented + self._version_patch = NotImplemented + + if not self.headless and QT_AVAILABLE: + self._waitbox = WaitBox("Please wait...") + else: + self._waitbox = None + + def __delitem__(self, key): + del self._ctxs[key] + + def __getitem__(self, key): + return self._ctxs[key] + + def __setitem__(self, key, value): + self._ctxs[key] = value + + #-------------------------------------------------------------------------- + # Properties + #-------------------------------------------------------------------------- + + def version_major(self): + """ + Return the major version number of the disassembler framework. + """ + assert self._version_major != NotImplemented + return self._version_major + + def version_minor(self): + """ + Return the minor version number of the disassembler framework. + """ + assert self._version_patch != NotImplemented + return self._version_patch + + def version_patch(self): + """ + Return the patch version number of the disassembler framework. + """ + assert self._version_patch != NotImplemented + return self._version_patch + + @abc.abstractproperty + def headless(self): + """ + Return a bool indicating if the disassembler is running without a GUI. + """ + pass + + #-------------------------------------------------------------------------- + # Synchronization Decorators + #-------------------------------------------------------------------------- + + @staticmethod + def execute_read(function): + """ + Thread-safe function decorator to READ from the disassembler database. + """ + raise NotImplementedError("execute_read() has not been implemented") + + @staticmethod + def execute_write(function): + """ + Thread-safe function decorator to WRITE to the disassembler database. + """ + raise NotImplementedError("execute_write() has not been implemented") + + @staticmethod + def execute_ui(function): + """ + Thread-safe function decorator to perform UI disassembler actions. + + This function is generally used for executing UI (Qt) events from + a background thread. as such, your implementation is expected to + transfer execution to the main application thread where it is safe to + perform Qt actions. + """ + raise NotImplementedError("execute_ui() has not been implemented") + + #-------------------------------------------------------------------------- + # Disassembler Universal APIs + #-------------------------------------------------------------------------- + + @abc.abstractmethod + def get_disassembler_user_directory(self): + """ + Return the 'user' directory for the disassembler. + """ + pass + + @abc.abstractmethod + def get_disassembly_background_color(self): + """ + Return the background color of the disassembly text view. + """ + pass + + @abc.abstractmethod + def is_msg_inited(self): + """ + Return a bool if the disassembler output window is initialized. + """ + pass + + def warning(self, text): + """ + Display a warning dialog box with the given text. + """ + msgbox = QtWidgets.QMessageBox() + before = msgbox.sizeHint().width() + msgbox.setIcon(QtWidgets.QMessageBox.Critical) + after = msgbox.sizeHint().width() + icon_width = after - before + + msgbox.setWindowTitle("Tenet Warning") + msgbox.setText(text) + + font = msgbox.font() + fm = QtGui.QFontMetricsF(font) + text_width = fm.size(0, text).width() + + # don't ask... + spacer = QtWidgets.QSpacerItem(int(text_width*1.1 + icon_width), 0, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + layout = msgbox.layout() + layout.addItem(spacer, layout.rowCount(), 0, 1, layout.columnCount()) + msgbox.setLayout(layout) + + # show the dialog + msgbox.exec_() + + @abc.abstractmethod + def message(self, function_address, new_name): + """ + Print a message to the disassembler console. + """ + pass + + #-------------------------------------------------------------------------- + # UI APIs + #-------------------------------------------------------------------------- + + @abc.abstractmethod + def create_dockable(self, dockable_name, widget): + """ + Creates a dockable widget. + """ + pass + + #------------------------------------------------------------------------------ + # WaitBox API + #------------------------------------------------------------------------------ + + def show_wait_box(self, text, modal=True): + """ + Show the disassembler universal WaitBox. + """ + assert QT_AVAILABLE, "This function can only be used in a Qt runtime" + self._waitbox.set_text(text) + self._waitbox.show(modal) + + def hide_wait_box(self): + """ + Hide the disassembler universal WaitBox. + """ + assert QT_AVAILABLE, "This function can only be used in a Qt runtime" + self._waitbox.hide() + + def replace_wait_box(self, text): + """ + Replace the text in the disassembler universal WaitBox. + """ + assert QT_AVAILABLE, "This function can only be used in a Qt runtime" + self._waitbox.set_text(text) + +#------------------------------------------------------------------------------ +# Disassembler Contextual API +#------------------------------------------------------------------------------ + +class DisassemblerContextAPI(object): + """ + An abstract implementation of database/contextual disassembler APIs. + """ + __metaclass__ = abc.ABCMeta + + @abc.abstractmethod + def __init__(self, dctx): + self.dctx = dctx + + #-------------------------------------------------------------------------- + # Properties + #-------------------------------------------------------------------------- + + @abc.abstractproperty + def busy(self): + """ + Return a bool indicating if the disassembler is busy / processing. + """ + pass + + #-------------------------------------------------------------------------- + # API Shims + #-------------------------------------------------------------------------- + + def is_64bit(self): + """ + Return True if the loaded processor module is 64bit. + """ + pass + + @abc.abstractmethod + def get_current_address(self): + """ + Return the current cursor address in the open database. + """ + pass + + @abc.abstractmethod + def get_database_directory(self): + """ + Return the directory for the open database. + """ + pass + + @abc.abstractmethod + def get_function_addresses(self): + """ + Return all defined function addresses in the open database. + """ + pass + + @abc.abstractmethod + def get_function_name_at(self, address): + """ + Return the name of the function at the given address. + + This is generally the user-facing/demangled name seen throughout the + disassembler and is probably what you want to use for almost everything. + """ + pass + + @abc.abstractmethod + def get_function_raw_name_at(self, address): + """ + Return the raw (eg, unmangled) name of the function at the given address. + + On the backend, most disassemblers store what is called the 'true' or + 'raw' (eg, unmangled) function name. + """ + pass + + @abc.abstractmethod + def get_imagebase(self): + """ + Return the base address of the open database. + """ + pass + + @abc.abstractmethod + def get_root_filename(self): + """ + Return the root executable (file) name used to generate the database. + """ + pass + + @abc.abstractmethod + def navigate(self, address, function_address=None): + """ + Jump the disassembler UI to the given address. + """ + pass + + @abc.abstractmethod + def navigate_to_function(self, function_address, address): + """ + Jump the disassembler UI to the given address, within a function. + """ + pass + + @abc.abstractmethod + def set_function_name_at(self, function_address, new_name): + """ + Set the function name at given address. + """ + pass \ No newline at end of file diff --git a/plugins_sogen-support/tenet/integration/api/ida_api.py b/plugins_sogen-support/tenet/integration/api/ida_api.py new file mode 100644 index 0000000..ba207f4 --- /dev/null +++ b/plugins_sogen-support/tenet/integration/api/ida_api.py @@ -0,0 +1,576 @@ +import logging +import functools + +# +# 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 +# + +import ida_ua +import ida_dbg +import ida_idp +import ida_pro +import ida_auto +import ida_nalt +import ida_name +import ida_xref +import idautils +import ida_bytes +import ida_idaapi +import ida_diskio +import ida_kernwin +import ida_segment +import ida_ida + +from .api import DisassemblerCoreAPI, DisassemblerContextAPI +from ...util.qt import * +from ...util.misc import is_mainthread + +logger = logging.getLogger("Tenet.API.IDA") + +#------------------------------------------------------------------------------ +# Utils +#------------------------------------------------------------------------------ + +def execute_sync(function, sync_type): + """ + Synchronize with the disassembler for safe database access. + + Modified from https://github.com/vrtadmin/FIRST-plugin-ida + """ + + @functools.wraps(function) + def wrapper(*args, **kwargs): + output = [None] + + # + # this inline function definition is technically what will execute + # in the context of the main thread. we use this thunk to capture + # any output the function may want to return to the user. + # + + def thunk(): + output[0] = function(*args, **kwargs) + return 1 + + if is_mainthread(): + thunk() + else: + ida_kernwin.execute_sync(thunk, sync_type) + + # return the output of the synchronized execution + return output[0] + return wrapper + +#------------------------------------------------------------------------------ +# Disassembler Core API (universal) +#------------------------------------------------------------------------------ + +class IDACoreAPI(DisassemblerCoreAPI): + NAME = "IDA" + + def __init__(self): + super(IDACoreAPI, self).__init__() + self._dockable_factory = {} + self._init_version() + + def _init_version(self): + + # retrieve IDA's version # + disassembler_version = ida_kernwin.get_kernel_version() + major, minor = map(int, disassembler_version.split(".")) + + # save the version number components for later use + self._version_major = major + self._version_minor = minor + self._version_patch = 0 + + #-------------------------------------------------------------------------- + # Properties + #-------------------------------------------------------------------------- + + @property + def headless(self): + return ida_kernwin.cvar.batch + + #-------------------------------------------------------------------------- + # Synchronization Decorators + #-------------------------------------------------------------------------- + + @staticmethod + def execute_read(function): + return execute_sync(function, ida_kernwin.MFF_READ) + + @staticmethod + def execute_write(function): + return execute_sync(function, ida_kernwin.MFF_WRITE) + + @staticmethod + def execute_ui(function): + return execute_sync(function, ida_kernwin.MFF_FAST) + + #-------------------------------------------------------------------------- + # API Shims + #-------------------------------------------------------------------------- + + def get_disassembler_user_directory(self): + return ida_diskio.get_user_idadir() + + def refresh_views(self): + ida_kernwin.refresh_idaview_anyway() + + def get_disassembly_background_color(self): + """ + Get the background color of the IDA disassembly view. + """ + + # create a donor IDA 'code viewer' + viewer = ida_kernwin.simplecustviewer_t() + viewer.Create("Colors") + + # get the viewer's qt widget + viewer_twidget = viewer.GetWidget() + viewer_widget = ida_kernwin.PluginForm.TWidgetToPyQtWidget(viewer_twidget) + + # fetch the background color property + #viewer.Show() # TODO: re-enable! + color = viewer_widget.property("line_bg_default") + + # destroy the view as we no longer need it + #viewer.Close() + + # return the color + return color + + def is_msg_inited(self): + return ida_kernwin.is_msg_inited() + + @execute_ui.__func__ + def warning(self, text): + super(IDACoreAPI, self).warning(text) + + @execute_ui.__func__ + def message(self, message): + print(message) + + #-------------------------------------------------------------------------- + # UI API Shims + #-------------------------------------------------------------------------- + + def create_dockable(self, window_title, widget): + + # create a dockable widget, and save a reference to it for later use + twidget = ida_kernwin.create_empty_widget(window_title) + + # cast the IDA 'twidget' as a Qt widget for use + dockable = ida_kernwin.PluginForm.TWidgetToPyQtWidget(twidget) + layout = dockable.layout() + layout.addWidget(widget) + + # return the dockable QtWidget / container + return dockable + +#------------------------------------------------------------------------------ +# Disassembler Context API (database-specific) +#------------------------------------------------------------------------------ + +class IDAContextAPI(DisassemblerContextAPI): + + def __init__(self, dctx): + super(IDAContextAPI, self).__init__(dctx) + + @property + def busy(self): + return not(ida_auto.auto_is_ok()) + + #-------------------------------------------------------------------------- + # API Shims + #-------------------------------------------------------------------------- + + @IDACoreAPI.execute_read + def get_current_address(self): + return ida_kernwin.get_screen_ea() + + def get_processor_type(self): + ## get the target arch, PLFM_386, PLFM_ARM, etc # TODO + #arch = idaapi.ph_get_id() + pass + + def is_64bit(self): + # The get_inf_structure() function is deprecated in newer IDA versions. + # The 'inf' object is now directly accessible via ida_ida. + return ida_ida.inf_is_64bit() + + def is_call_insn(self, address): + insn = ida_ua.insn_t() + if ida_ua.decode_insn(insn, address) and ida_idp.is_call_insn(insn): + return True + return False + + def get_instruction_addresses(self): + """ + Return all instruction addresses from the executable. + """ + instruction_addresses = [] + + for seg_address in idautils.Segments(): + + # fetch code segments + seg = ida_segment.getseg(seg_address) + if seg.sclass != ida_segment.SEG_CODE: + continue + + current_address = seg_address + end_address = seg.end_ea + + # save the address of each instruction in the segment + while current_address < end_address: + current_address = ida_bytes.next_head(current_address, end_address) + if ida_bytes.is_code(ida_bytes.get_flags(current_address)): + instruction_addresses.append(current_address) + + # print(f"Seg {seg.start_ea:08X} --> {seg.end_ea:08X} CODE") + #print(f" -- {len(instruction_addresses):,} instructions found") + + return instruction_addresses + + def is_mapped(self, address): + return ida_bytes.is_mapped(address) + + def get_next_insn(self, address): + + xb = ida_xref.xrefblk_t() + ok = xb.first_from(address, ida_xref.XREF_ALL) + + while ok and xb.iscode: + if xb.type == ida_xref.fl_F: + return xb.to + ok = xb.next_from() + + return -1 + + def get_prev_insn(self, address): + + xb = ida_xref.xrefblk_t() + ok = xb.first_to(address, ida_xref.XREF_ALL) + + while ok and xb.iscode: + if xb.type == ida_xref.fl_F: + return xb.frm + ok = xb.next_to() + + return -1 + + def get_database_directory(self): + return idautils.GetIdbDir() + + def get_function_addresses(self): + return list(idautils.Functions()) + + def get_function_name_at(self, address): + return ida_name.get_short_name(address) + + def get_function_raw_name_at(self, function_address): + return ida_name.get_name(function_address) + + def get_imagebase(self): + return ida_nalt.get_imagebase() + + def get_root_filename(self): + return ida_nalt.get_root_filename() + + def navigate(self, address): + + # TODO fetch active view? or most recent one? i'm lazy for now... + widget = ida_kernwin.find_widget("IDA View-A") + + # + # this call can both navigate to an arbitrary address, and keep + # the cursor position 'static' within the window at an (x,y) + # text position + # + # TODO: I think it's kind of tricky to figure out the 'center' line of + # the disassembly window navigation, so for now we'll just make a + # navigation call always center around line 20... + # + + CENTER_AROUND_LINE_INDEX = 20 + + if widget: + return ida_kernwin.ea_viewer_history_push_and_jump(widget, address, 0, CENTER_AROUND_LINE_INDEX, 0) + + # ehh, whatever.. just let IDA navigate to yolo + else: + return ida_kernwin.jumpto(address) + + def navigate_to_function(self, function_address, address): + return self.navigate(address) + + def set_function_name_at(self, function_address, new_name): + ida_name.set_name(function_address, new_name, ida_name.SN_NOWARN) + + def set_breakpoint(self, address): + ida_dbg.add_bpt(address) + + def delete_breakpoint(self, address): + ida_dbg.del_bpt(address) + +#------------------------------------------------------------------------------ +# HexRays Util +#------------------------------------------------------------------------------ + +def hexrays_available(): + """ + Return True if an IDA decompiler is loaded and available for use. + """ + try: + import ida_hexrays + return ida_hexrays.init_hexrays_plugin() + except ImportError: + return False + +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 = {} + + # + # it turns out that citem indexes are actually stored inline with the + # decompilation text output, hidden behind COLOR_ADDR tokens. + # + # here we pass each line of raw decompilation text to our crappy lexer, + # extracting any COLOR_ADDR tokens as citem indexes + # + + for line_number in range(decompilation_text.size()): + line_text = decompilation_text[line_number].line + line2citem[line_number] = lex_citem_indexes(line_text) + + return line2citem + +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 + function_address = cfunc.entry_ea + + # + # prior to this function, a line2citem map was built to tell us which + # citems reside on any given line of text in the decompilation output. + # + # now, we walk through this line2citem map one 'line_number' at a time in + # an effort to resolve the set of graph nodes associated with its citems. + # + + for line_number, citem_indexes in line2citem.items(): + nodes = set() + + # + # we are at the level of a single line (line_number). we now consume + # its set of citems (citem_indexes) and attempt to identify explicit + # graph nodes they claim to be sourced from (by their reported EA) + # + + for index in citem_indexes: + + # get the code address of the given citem + try: + item = treeitems[index] + address = item.ea + + # apparently this is a thing on IDA 6.95 + except IndexError as e: + continue + + # find the graph node (eg, basic block) that generated this citem + node = metadata.get_node(address) + + # address not mapped to a node... weird. continue to the next citem + if not node: + #logger.warning("Failed to map node to basic block") + continue + + # + # we made it this far, so we must have found a node that contains + # this citem. save the computed node_id to the list of known + # nodes we have associated with this line of text + # + + nodes.add(node.address) + + # + # finally, save the completed list of node ids as identified for this + # line of decompilation text to the line2node map that we are building + # + + line2node[line_number] = nodes + + # all done, return the computed map + return line2node + +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 = [] + line_length = len(line) + + # lex COLOR_ADDR tokens from the line of text + while i < line_length: + + # does this character mark the start of a new COLOR_* token? + if line[i] == idaapi.COLOR_ON: + + # yes, so move past the COLOR_ON byte + i += 1 + + # is this sequence for a COLOR_ADDR? + if ord(line[i]) == idaapi.COLOR_ADDR: + + # yes, so move past the COLOR_ADDR byte + i += 1 + + # + # A COLOR_ADDR token is followed by either 8, or 16 characters + # (a hex encoded number) that represents an address/pointer. + # in this context, it is actually the index number of a citem + # + + citem_index = int(line[i:i+idaapi.COLOR_ADDR_SIZE], 16) + i += idaapi.COLOR_ADDR_SIZE + + # save the extracted citem index + indexes.append(citem_index) + + # skip to the next iteration as i has moved + continue + + # nothing we care about happened, keep lexing forward + i += 1 + + # return all the citem indexes extracted from this line of text + return indexes + +class DockableWindow(ida_kernwin.PluginForm): + + def __init__(self, title, widget): + super(DockableWindow, self).__init__() + self.title = title + self.widget = widget + + self.visible = False + self._dock_position = None + self._dock_target = None + + if ida_pro.IDA_SDK_VERSION < 760: + self.__dock_filter = IDADockSizeHack() + + def OnCreate(self, form): + #print("Creating", self.title) + self.parent = self.FormToPyQtWidget(form) + + layout = QtWidgets.QVBoxLayout() + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(self.widget) + self.parent.setLayout(layout) + + if ida_pro.IDA_SDK_VERSION < 760: + self.__dock_size_hack() + + def OnClose(self, foo): + self.visible = False + #print("Closing", self.title) + + def __dock_size_hack(self): + if self.widget.minimumWidth() == 0: + return + self.widget.min_width = self.widget.minimumWidth() + self.widget.max_width = self.widget.maximumWidth() + self.widget.setMinimumWidth(self.widget.min_width // 2) + self.widget.setMaximumWidth(self.widget.min_width // 2) + self.widget.installEventFilter(self.__dock_filter) + + def show(self): + dock_position = self._dock_position + + if ida_pro.IDA_SDK_VERSION < 760: + WOPN_SZHINT = 0x200 + + # create the dockable widget, without actually showing it + self.Show(self.title, options=ida_kernwin.PluginForm.WOPN_CREATE_ONLY) + + # use some kludge to display our widget, and enforce the use of its sizehint + ida_widget = self.GetWidget() + ida_kernwin.display_widget(ida_widget, WOPN_SZHINT) + self.visible = True + + # no hax required for IDA 7.6 and newer + else: + self.Show(self.title) + self.visible = True + dock_position |= ida_kernwin.DP_SZHINT + + # move the window to a given location if specified + if dock_position is not None: + ida_kernwin.set_dock_pos(self.title, self._dock_target, dock_position) + + def hide(self): + self.Close(1) + + def set_dock_position(self, dest_ctrl=None, position=0): + self._dock_target = dest_ctrl + self._dock_position = position + + if not self.visible: + return + + ida_kernwin.set_dock_pos(self.title, dest_ctrl, position) + + def copy_dock_position(self, other): + self._dock_target = other._dock_target + self._dock_position = other._dock_position + +class IDADockSizeHack(QtCore.QObject): + def eventFilter(self, obj, event): + if event.type() == QtCore.QEvent.WindowActivate: + obj.setMinimumWidth(obj.min_width) + obj.setMaximumWidth(obj.max_width) + obj.removeEventFilter(self) + return False \ No newline at end of file diff --git a/plugins_sogen-support/tenet/integration/ida_integration.py b/plugins_sogen-support/tenet/integration/ida_integration.py new file mode 100644 index 0000000..b865995 --- /dev/null +++ b/plugins_sogen-support/tenet/integration/ida_integration.py @@ -0,0 +1,525 @@ +import ctypes +import logging + +# +# TODO: should probably cleanup / document this file a bit better +# + +import ida_dbg +import ida_bytes +import ida_idaapi +import ida_kernwin + +from tenet.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 * + +logger = logging.getLogger("Tenet.IDA.Integration") + +IDA_GLOBAL_CTX = "blah this value doesn't matter" + +#------------------------------------------------------------------------------ +# IDA UI Integration +#------------------------------------------------------------------------------ + +class TenetIDA(TenetCore): + """ + The plugin integration layer IDA Pro. + """ + + def __init__(self): + + # + # icons + # + + self._icon_id_file = ida_idaapi.BADADDR + self._icon_id_next_execution = ida_idaapi.BADADDR + self._icon_id_prev_execution = ida_idaapi.BADADDR + + # + # event hooks + # + + self._hooked = False + + self._ui_hooks = UIHooks() + self._ui_hooks.get_lines_rendering_info = self._render_lines + self._ui_hooks.finish_populating_widget_popup = self._popup_hook + + self._dbg_hooks = DbgHooks() + self._dbg_hooks.dbg_bpt_changed = self._breakpoint_changed_hook + + # + # we should always hook the UI early in dev mode as we will use UI + # events to auto-launch a trace + # + + if is_plugin_dev(): + self._ui_hooks.hook() + + # + # callbacks + # + + self._ui_breakpoint_changed_callbacks = [] + + # + # run disassembler-agnostic core initalization + # + + super(TenetIDA, self).__init__() + + def hook(self): + if self._hooked: + return + self._hooked = True + self._ui_hooks.hook() + self._dbg_hooks.hook() + + def unhook(self): + if not self._hooked: + return + self._hooked = False + self._ui_hooks.unhook() + self._dbg_hooks.unhook() + + def get_context(self, dctx, startup=True): + """ + Get the plugin context for a given database. + + NOTE: since IDA can only have one binary / IDB open at a time, the + dctx (database context) should always be IDA_GLOBAL_CTX. + """ + assert dctx is IDA_GLOBAL_CTX + self.palette.warmup() + + # + # there should only ever be 'one' disassembler / IDB context at any + # time for IDA. but if one does not exist yet, that means this is the + # first time the user has interacted with the plugin for this session + # + + if dctx not in self.contexts: + + # create a new 'plugin context' representing this IDB + pctx = TenetContext(self, dctx) + if startup: + pctx.start() + + # save the created ctx for future calls + self.contexts[dctx] = pctx + + # return the plugin context object for this IDB + return self.contexts[dctx] + + #-------------------------------------------------------------------------- + # IDA 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): + + # TODO: create a custom IDA icon + #icon_path = plugin_resource(os.path.join("icons", "load.png")) + #icon_data = open(icon_path, "rb").read() + #self._icon_id_file = ida_kernwin.load_custom_icon(data=icon_data) + + # describe a custom IDA UI action + action_desc = ida_kernwin.action_desc_t( + self.ACTION_LOAD_TRACE, # The action name + "~T~enet trace file...", # The action text + IDACtxEntry(self._interactive_load_trace), # The action handler + None, # Optional: action shortcut + "Load a Tenet trace file", # Optional: tooltip + -1 # Optional: the action icon + ) + + # register the action with IDA + result = ida_kernwin.register_action(action_desc) + assert result, f"Failed to register '{action_desc.name}' action with IDA" + + # attach the action to the File-> dropdown menu + result = ida_kernwin.attach_action_to_menu( + "File/Load file/", # Relative path of where to add the action + self.ACTION_LOAD_TRACE, # The action ID (see above) + ida_kernwin.SETMENU_APP # We want to append the action after ^ + ) + assert result, f"Failed action attach {action_desc.name}" + + logger.info(f"Installed the '{action_desc.name}' menu entry") + + def _install_next_execution(self): + + icon_data = self.palette.gen_arrow_icon(self.palette.arrow_next, 0) + self._icon_id_next_execution = ida_kernwin.load_custom_icon(data=icon_data) + + # describe a custom IDA UI action + action_desc = ida_kernwin.action_desc_t( + self.ACTION_NEXT_EXECUTION, # The action name + "Go to next execution", # The action text + IDACtxEntry(self._interactive_next_execution), # The action handler + None, # Optional: action shortcut + "Go to the next execution of the current address", # Optional: tooltip + self._icon_id_next_execution # Optional: the action icon + ) + + # register the action with IDA + result = ida_kernwin.register_action(action_desc) + assert result, f"Failed to register '{action_desc.name}' action with IDA" + logger.info(f"Installed the '{action_desc.name}' menu entry") + + def _install_prev_execution(self): + + icon_data = self.palette.gen_arrow_icon(self.palette.arrow_prev, 180.0) + self._icon_id_prev_execution = ida_kernwin.load_custom_icon(data=icon_data) + + # describe a custom IDA UI action + action_desc = ida_kernwin.action_desc_t( + self.ACTION_PREV_EXECUTION, # The action name + "Go to previous execution", # The action text + IDACtxEntry(self._interactive_prev_execution), # The action handler + None, # Optional: action shortcut + "Go to the previous execution of the current address", # Optional: tooltip + self._icon_id_prev_execution # Optional: the action icon + ) + + # register the action with IDA + result = ida_kernwin.register_action(action_desc) + assert result, f"Failed to register '{action_desc.name}' action with IDA" + logger.info(f"Installed the '{action_desc.name}' menu entry") + + def _install_first_execution(self): + + # describe a custom IDA UI action + action_desc = ida_kernwin.action_desc_t( + self.ACTION_FIRST_EXECUTION, # The action name + "Go to first execution", # The action text + IDACtxEntry(self._interactive_first_execution), # The action handler + None, # Optional: action shortcut + "Go to the first execution of the current address", # Optional: tooltip + -1 # Optional: the action icon + ) + + # register the action with IDA + result = ida_kernwin.register_action(action_desc) + assert result, f"Failed to register '{action_desc.name}' action with IDA" + logger.info(f"Installed the '{action_desc.name}' menu entry") + + def _install_final_execution(self): + + # describe a custom IDA UI action + action_desc = ida_kernwin.action_desc_t( + self.ACTION_FINAL_EXECUTION, # The action name + "Go to final execution", # The action text + IDACtxEntry(self._interactive_final_execution), # The action handler + None, # Optional: action shortcut + "Go to the final execution of the current address", # Optional: tooltip + -1 # Optional: the action icon + ) + + # register the action with IDA + result = ida_kernwin.register_action(action_desc) + assert result, f"Failed to register '{action_desc.name}' action with IDA" + logger.info(f"Installed the '{action_desc.name}' menu entry") + + def _uninstall_load_trace(self): + + logger.info("Removing the 'Tenet trace file...' menu entry...") + + # remove the entry from the File-> menu + result = ida_kernwin.detach_action_from_menu( + "File/Load file/", + self.ACTION_LOAD_TRACE + ) + if not result: + logger.warning("Failed to detach action from menu...") + return False + + # unregister the action + result = ida_kernwin.unregister_action(self.ACTION_LOAD_TRACE) + if not result: + logger.warning("Failed to unregister action...") + return False + + # delete the entry's icon + #ida_kernwin.free_custom_icon(self._icon_id_file) # TODO + self._icon_id_file = ida_idaapi.BADADDR + + logger.info("Successfully removed the menu entry!") + return True + + def _uninstall_next_execution(self): + result = self._uninstall_action(self.ACTION_NEXT_EXECUTION, self._icon_id_next_execution) + self._icon_id_next_execution = ida_idaapi.BADADDR + return result + + def _uninstall_prev_execution(self): + result = self._uninstall_action(self.ACTION_PREV_EXECUTION, self._icon_id_prev_execution) + self._icon_id_prev_execution = ida_idaapi.BADADDR + return result + + def _uninstall_first_execution(self): + return self._uninstall_action(self.ACTION_FIRST_EXECUTION) + + def _uninstall_final_execution(self): + return self._uninstall_action(self.ACTION_FINAL_EXECUTION) + + def _uninstall_action(self, action, icon_id=ida_idaapi.BADADDR): + + result = ida_kernwin.unregister_action(action) + if not result: + logger.warning(f"Failed to unregister {action}...") + return False + + if icon_id != ida_idaapi.BADADDR: + ida_kernwin.free_custom_icon(icon_id) + + logger.info(f"Uninstalled the {action} menu entry") + return True + + #-------------------------------------------------------------------------- + # UI Event Handlers + #-------------------------------------------------------------------------- + + def _breakpoint_changed_hook(self, code, bpt): + """ + (Event) Breakpoint changed. + """ + + if code == ida_dbg.BPTEV_ADDED: + self._notify_ui_breakpoint_changed(bpt.ea, BreakpointEvent.ADDED) + + elif code == ida_dbg.BPTEV_CHANGED: + if bpt.enabled(): + self._notify_ui_breakpoint_changed(bpt.ea, BreakpointEvent.ENABLED) + else: + self._notify_ui_breakpoint_changed(bpt.ea, BreakpointEvent.DISABLED) + + elif code == ida_dbg.BPTEV_REMOVED: + self._notify_ui_breakpoint_changed(bpt.ea, 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 + + 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 _highlight_disassesmbly(self, lines_out, widget, lines_in): + """ + TODO/XXX this is pretty gross + """ + ctx = self.get_context(IDA_GLOBAL_CTX) + 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 = 0xFF << 24 | b << 16 | g << 8 | r + + 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() + ida_color = b << 16 | g << 8 | r + ida_color |= (0xFF - int(0xFF * percent)) << 24 + + # save the trail color + rebased_address = ctx.reader.analysis.rebase_pointer(address) + trail[rebased_address] = ida_color + + current_address = ctx.reader.rebased_ip + if not ida_bytes.is_mapped(current_address): + last_good_idx = ctx.reader.analysis.get_prev_mapped_idx(ctx.reader.idx) + if last_good_idx != -1: + + # fetch the last instruction pointer to fall within the trace + last_good_trace_address = ctx.reader.get_ip(last_good_idx) + + # convert the trace-based instruction pointer to one that maps to the disassembler + current_address = ctx.reader.analysis.rebase_pointer(last_good_trace_address) + + for section in lines_in.sections_lines: + for line in section: + address = line.at.toea() + + if address in backward_trail: + color = backward_trail[address] + elif address in forward_trail: + color = forward_trail[address] + elif address == current_address: + color = current_color + else: + continue + + entry = ida_kernwin.line_rendering_output_entry_t(line, ida_kernwin.LROEF_FULL_LINE, color) + lines_out.entries.push_back(entry) + + #---------------------------------------------------------------------- + # Callbacks + #---------------------------------------------------------------------- + + def ui_breakpoint_changed(self, callback): + register_callback(self._ui_breakpoint_changed_callbacks, callback) + + def _notify_ui_breakpoint_changed(self, address, code): + notify_callback(self._ui_breakpoint_changed_callbacks, address, code) + +#------------------------------------------------------------------------------ +# IDA UI Helpers +#------------------------------------------------------------------------------ + +class IDACtxEntry(ida_kernwin.action_handler_t): + """ + A minimal context menu entry class to utilize IDA's action handlers. + """ + + def __init__(self, action_function): + super(IDACtxEntry, 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 ida_kernwin.AST_ENABLE_ALWAYS + +#------------------------------------------------------------------------------ +# IDA UI Event Hooks +#------------------------------------------------------------------------------ + +class DbgHooks(ida_dbg.DBG_Hooks): + def dbg_bpt_changed(self, code, bpt): + pass + +class UIHooks(ida_kernwin.UI_Hooks): + def get_lines_rendering_info(self, lines_out, widget, lines_in): + pass + def ready_to_run(self): + pass + def finish_populating_widget_popup(self, widget, popup): + pass \ No newline at end of file diff --git a/plugins_sogen-support/tenet/integration/ida_loader.py b/plugins_sogen-support/tenet/integration/ida_loader.py new file mode 100644 index 0000000..3d44614 --- /dev/null +++ b/plugins_sogen-support/tenet/integration/ida_loader.py @@ -0,0 +1,105 @@ +import time +import logging + +import ida_idaapi +import ida_kernwin + +from tenet.util.log import pmsg +from tenet.integration.ida_integration import TenetIDA + +logger = logging.getLogger("Tenet.IDA.Loader") + +#------------------------------------------------------------------------------ +# IDA Plugin Loader +#------------------------------------------------------------------------------ +# +# This file contains a stub 'plugin' class for the plugin as required by +# IDA Pro. Practically speaking, there should be little to *no* logic placed +# in this file because it is disassembler-specific. +# +# When IDA Pro is starting up, it will import all python files placed in its +# root plugin folder. It will then attempt to call PLUGIN_ENTRY() on each of +# the imported 'plugins'. We import PLUGIN_ENTRY into tenet_plugin.py +# so that IDA can see it. +# +# PLUGIN_ENTRY() is expected to return a plugin object (TenetIDAPlugin) +# derived from ida_idaapi.plugin_t. IDA will register the plugin, and +# interface with the plugin object to load / unload the plugin at certain +# times, per its configuration (flags, hotkeys). +# +# There should be virtually no reason for you to modify this file. +# + +def PLUGIN_ENTRY(): + """ + Required plugin entry point for IDAPython Plugins. + """ + return TenetIDAPlugin() + +class TenetIDAPlugin(ida_idaapi.plugin_t): + """ + The IDA plugin stub for Tenet. + """ + + # + # Plugin flags: + # - PLUGIN_MOD: The plugin may modify the database + # - PLUGIN_PROC: Load/unload the plugin when an IDB opens / closes + # - PLUGIN_HIDE: Hide the plugin from the IDA plugin menu + # + + flags = ida_idaapi.PLUGIN_PROC | ida_idaapi.PLUGIN_MOD | ida_idaapi.PLUGIN_HIDE + comment = "Trace Explorer" + help = "" + wanted_name = "Tenet" + wanted_hotkey = "" + + #-------------------------------------------------------------------------- + # IDA Plugin Overloads + #-------------------------------------------------------------------------- + + def init(self): + """ + This is called by IDA when it is loading the plugin. + """ + + try: + self.core = TenetIDA() + self.core.load() + except Exception as e: + pmsg("Failed to initialize Tenet") + logger.exception("Exception details:") + + # + # we return PLUGIN_KEEP here regardless of success/failure. this is to + # ensure that IDA will not try to reload the plugin again. + # + + return ida_idaapi.PLUGIN_KEEP + + def run(self, arg): + """ + This is called by IDA when this file is loaded as a script. + """ + ida_kernwin.warning("Tenet cannot be run as a script in IDA.") + + def term(self): + """ + This is called by IDA when it is unloading the plugin. + """ + logger.debug("IDA term started...") + + start = time.time() + logger.debug("-"*50) + + try: + self.core.unload() + self.core = None + except Exception as e: + logger.exception("Failed to cleanly unload Tenet from IDA.") + + end = time.time() + logger.debug("-"*50) + + logger.debug("IDA term done... (%.3f seconds...)" % (end-start)) + diff --git a/plugins_sogen-support/tenet/memory.py b/plugins_sogen-support/tenet/memory.py new file mode 100644 index 0000000..08b4293 --- /dev/null +++ b/plugins_sogen-support/tenet/memory.py @@ -0,0 +1,23 @@ +from tenet.hex import HexController + +#------------------------------------------------------------------------------ +# memory.py -- Memory Dump Controller +#------------------------------------------------------------------------------ +# +# The purpose of this file is to house the 'headless' components of the +# memory dump window and its underlying functionality. This is split into +# a model and controller component, of a typical 'MVC' design pattern. +# +# As our memory dumps are largely abstracted off a generic 'hex dump', +# there is very little code that actually has to be applied here (for now) +# + +class MemoryController(HexController): + """ + The Memory Dump Controller (Logic) + """ + + def __init__(self, pctx): + super(MemoryController, self).__init__(pctx) + self._title = "Memory View" + #self.model.hex_format = HexType.MAGIC diff --git a/plugins_sogen-support/tenet/registers.py b/plugins_sogen-support/tenet/registers.py new file mode 100644 index 0000000..7491b06 --- /dev/null +++ b/plugins_sogen-support/tenet/registers.py @@ -0,0 +1,376 @@ +from tenet.ui import * +from tenet.util.misc import register_callback, notify_callback +from tenet.integration.api import DockableWindow, disassembler + +#------------------------------------------------------------------------------ +# registers.py -- Register Controller +#------------------------------------------------------------------------------ +# +# The purpose of this file is to house the 'headless' components of the +# registers window and its underlying functionality. This is split into a +# model and controller component, of a typical 'MVC' design pattern. +# +# NOTE: for the time being, this file also contains the logic for the +# 'IDX Shell' as it is kind of attached to the register view and not big +# enough to demand its own seperate structuring ... yet +# + +class RegisterController(object): + """ + The Registers Controller (Logic) + """ + + def __init__(self, pctx): + self.pctx = pctx + self.model = RegistersModel(pctx) + self.reader = None + + # UI components + self.view = None + self.dockable = None + + # 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 = RegisterView(self, self.model) + new_dockable = DockableWindow("CPU Registers", 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 + + # 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 active trace reader from this controller. + """ + self.reader = None + self.model.reset() + + def set_ip_breakpoint(self): + """ + Set an execution breakpoint on the current instruction pointer. + """ + current_ip = self.model.registers[self.model.arch.IP] + + self._ignore_signals = True + self.pctx.breakpoints.clear_execution_breakpoints() + self.pctx.breakpoints.add_execution_breakpoint(current_ip) + self._ignore_signals = False + + if self.view: + self.view.refresh() + + # TODO: maybe we can remove all these 'focus' funcs now? + def focus_register_value(self, reg_name): + """ + Focus a register value in the register view. + """ + self.model.focused_reg_value = reg_name + + def focus_register_name(self, reg_name): + """ + Focus a register name in the register view. + """ + self._clear_register_value_focus() + self.model.focused_reg_name = reg_name + + def clear_register_focus(self): + """ + Clear all focus on register fields. + """ + self._clear_register_value_focus() + self.model.focused_reg_name = None + + def follow_in_dump(self, reg_name): + """ + Follow a given register value in the memory dump. + """ + address = self.model.registers[reg_name] + self.pctx.memory.navigate(address) + + def _clear_register_value_focus(self): + """ + Clear focus from the active register field. + """ + self.model.focused_reg_value = None + + def set_registers(self, registers, delta=None): + """ + Set the registers for the view. + """ + self.model.set_registers(registers, delta) + + def evaluate_expression(self, expression): + """ + Evaluate the expression in the IDX Shell and navigate to it. + """ + + # a target idx was given as an integer + if isinstance(expression, int): + target_idx = expression + self.reader.seek(target_idx) + + # string handling + elif isinstance(expression, str): + + # blank string was passed from the shell, nothing to do... + if not expression: + return + + # a 'command' / alias idx was entered into the shell ('!...' prefix) + if expression[0] == '!': + self._handle_command(expression[1:]) + return + + # + # not a command, how about a comma seperated timestamp? + # -- e.g '5,218,121' + # + + idx_str = expression.replace(',', '') + try: + target_idx = int(idx_str) + except: + return + + self.reader.seek(target_idx) + + else: + raise ValueError(f"Unknown input expression type '{expression}'?!?") + + def _handle_command(self, expression): + """ + Handle the evaluation of commands on the timestamp shell. + """ + if self._handle_seek_percent(expression): + return True + if self._handle_seek_last(expression): + return True + return False + + def _handle_seek_percent(self, expression): + """ + Handle a 'percentage-based' trace seek. + + eg: !0, or !100 to skip to the start/end of trace + """ + try: + target_percent = float(expression) # float, so you could even do 42.1% + except: + return False + + # seek to the desired percentage in the trace + self.reader.seek_percent(target_percent) + return True + + def _handle_seek_last(self, expression): + """ + Handle a seek to the last mapped address. + """ + if expression != 'last': + return False + + last_idx = self.reader.trace.length - 1 + last_ip = self.reader.get_ip(last_idx) + rebased_ip = self.reader.analysis.rebase_pointer(last_ip) + + dctx = disassembler[self.pctx] + if not dctx.is_mapped(rebased_ip): + last_good_idx = self.reader.analysis.get_prev_mapped_idx(last_idx) + if last_good_idx == -1: + return False # navigation is just not gonna happen... + last_idx = last_good_idx + + # seek to the last known / good idx that is mapped within the disassembler + self.reader.seek(last_idx) + return True + + def _idx_changed(self, idx): + """ + The trace position has been changed. + """ + self.model.idx = idx + self.set_registers(self.reader.registers, self.reader.trace.get_reg_delta(idx).keys()) + + def _breakpoints_changed(self): + """ + Handle breakpoints changed event. + """ + if not self.view: + return + self.view.refresh() + + def _idx_changed(self, idx): + """ + The trace position has been changed. + """ + self.model.idx = idx + self.set_registers(self.reader.registers, self.reader.trace.get_reg_delta(idx).keys()) + + def _breakpoints_changed(self): + """ + Handle breakpoints changed event. + """ + if not self.view: + return + self.view.refresh() + +class RegistersModel(object): + """ + The Registers Model (Data) + """ + + def __init__(self, pctx): + self._pctx = pctx + self.reset() + + #---------------------------------------------------------------------- + # Callbacks + #---------------------------------------------------------------------- + + self._registers_changed_callbacks = [] + + #---------------------------------------------------------------------- + # Properties + #---------------------------------------------------------------------- + + @property + def arch(self): + """ + Return the architecture definition. + """ + return self._pctx.arch + + @property + def execution_breakpoints(self): + """ + Return the set of active execution breakpoints. + """ + return self._pctx.breakpoints.model.bp_exec + + #---------------------------------------------------------------------- + # Public + #---------------------------------------------------------------------- + + def reset(self): + + # the current timestamp in the trace + self.idx = -1 + + # the { reg_name: reg_value } dict of current register values + self.registers = {} + + # + # the names of the registers that have changed since the previous + # chronological timestamp in the trace. + # + # for example if you singlestep forward, any registers that changed as + # a result of 'normal execution' may be highlighted (e.g. red) + # + + self.delta_trace = [] + + # + # the names of registers that have changed since the last navigation + # event (eg, skipping between breakpoints, memory accesses). + # + # this is used to highlight registers that may not have changed as a + # result of the previous chronological trace event, but by means of + # user navigation within tenet. + # + + self.delta_navigation = [] + + self.focused_reg_name = None + self.focused_reg_value = None + + def set_registers(self, registers, delta=None): + + # compute which registers changed as a result of navigation + unchanged = dict(set(self.registers.items()) & set(registers.items())) + self.delta_navigation = set([k for k in registers if k not in unchanged]) + + # save the register delta that changed since the previous trace timestamp + self.delta_trace = delta if delta else [] + self.registers = registers + + # notify the UI / listeners of the model that an update occurred + self._notify_registers_changed() + + #---------------------------------------------------------------------- + # Callbacks + #---------------------------------------------------------------------- + + def registers_changed(self, callback): + """ + Subscribe a callback for a registers changed event. + """ + register_callback(self._registers_changed_callbacks, callback) + + def _notify_registers_changed(self): + """ + Notify listeners of a registers changed event. + """ + notify_callback(self._registers_changed_callbacks) diff --git a/plugins_sogen-support/tenet/stack.py b/plugins_sogen-support/tenet/stack.py new file mode 100644 index 0000000..5d104bb --- /dev/null +++ b/plugins_sogen-support/tenet/stack.py @@ -0,0 +1,105 @@ +import struct + +from tenet.ui import * +from tenet.hex import HexController +from tenet.types import HexType, AuxType + +#------------------------------------------------------------------------------ +# stack.py -- Stack Dump Controller +#------------------------------------------------------------------------------ +# +# The purpose of this file is to house the 'headless' components of the +# stack dump window and its underlying functionality. This is split into +# a model and controller component, of a typical 'MVC' design pattern. +# +# The stack dump window abstracts from a simple hex dump. We use the code +# below to configure our underlying hex dump to appear more like a typical +# stack view might instead. +# + +class StackController(HexController): + """ + The Stack Dump Controller (Logic) + """ + + def __init__(self, pctx): + super(StackController, self).__init__(pctx) + self._title = "Stack View" + + def attach_reader(self, reader): + """ + Attach a trace reader, and configure the view to model a stack. + """ + self.model.num_bytes_per_line = reader.arch.POINTER_SIZE + self.model.hex_format = HexType.DWORD if reader.arch.POINTER_SIZE == 4 else HexType.QWORD + self.model.aux_format = AuxType.STACK + super(StackController, self).attach_reader(reader) + + def follow_in_dump(self, stack_address): + """ + Follow the pointer at a given stack address in the memory dump. + """ + POINTER_SIZE = self.pctx.reader.arch.POINTER_SIZE + + # align the given stack address (which we will read..) + stack_address &= ~(POINTER_SIZE - 1) + + # + # compute the relative index of the stack entry, which we will + # use to carve data from the currently visible stack model + # + + relative_index = stack_address - self.model.address + + # attempt to carve the data and validity mask from the stack model + try: + data = self.model.data[relative_index:relative_index+POINTER_SIZE] + mask = self.model.mask[relative_index:relative_index+POINTER_SIZE] + except: + return False + + # ensure the carved data is fully resolved (e.g. there are no unknown bytes) + if not (len(mask) == POINTER_SIZE and list(set(mask)) == [0xFF]): + return False + + # unpack the carved data as a pointer + parsed_address = struct.unpack("I" if POINTER_SIZE == 4 else "Q", data)[0] + + # navigate the memory dump window to the 'pointer' we carved off the stack + self.pctx.memory.navigate(parsed_address) + + def _idx_changed(self, idx): + """ + Override the default hex view idx changed event handler. + """ + + # fade out the upper part of the stack that is currently 'unallocated' + self.set_fade_threshold(self.reader.sp) + + if self.view: + + # + # if the user has a byte / range selected or the view is purposely + # omitting navigation events, we will *not* move the stack view on + # idx changes. + # + # this is to preserve the location of their selection on-screen + # (eg, when hovering a selected byte, and jumping between its + # memory accesses) + # + + if self.view._ignore_navigation or self.view.selection_size: + self.refresh_memory() + self.view.refresh() + return + + # + # if there is no special user interaction going on with the stack + # view, we will simply ensure that the stack stays 'pinned' to the + # top of the stack, per the current trace reader state. + # + # we conciously chose to show '3' lines of the unallocated frames + # to provide a bit more awarness to pops/rets as they happen + # + + self.navigate(self.reader.sp - self.model.num_bytes_per_line * 3) \ No newline at end of file diff --git a/plugins_sogen-support/tenet/trace/__init__.py b/plugins_sogen-support/tenet/trace/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/plugins_sogen-support/tenet/trace/__pycache__/__init__.cpython-311.pyc b/plugins_sogen-support/tenet/trace/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..666e00e383a11d7891286d80c806e62a71b7b657 GIT binary patch literal 181 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 literal 0 HcmV?d00001 diff --git a/plugins_sogen-support/tenet/trace/__pycache__/reader.cpython-311.pyc b/plugins_sogen-support/tenet/trace/__pycache__/reader.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..02fe3aaf2c72e2409f493d152dc652643f46f90a GIT binary patch literal 56898 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 q122@WA24dM{N#g}^8<4I%dD5rLW literal 0 HcmV?d00001 diff --git a/plugins_sogen-support/tenet/trace/__pycache__/types.cpython-311.pyc b/plugins_sogen-support/tenet/trace/__pycache__/types.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..91b811c998a1668639ccb7e36257ac6f2b9cefe7 GIT binary patch literal 4316 zcmbVPUu+Y}8K3>V)4*!u-6T#N+qb)J z6L)isPMqqVEHy}x@TobS9}s5*51iD8KK7+jA1c+0{9vtwgjC(bz4@9}I`PteGrQib zlgml%c=z+n_sx9s%{Slgo7peg+5!m5Z~y)ysXd6$KX9W_IH$5X1eFI!MlzK|lZZxd zBbmO3WJaMLF$g_@*DjN^OiiE&`!#Ofj5}QNM>~zpDX2U^3Yw&3g!@l2Sl1*gGe9}T zC$sm^Bk1$Q8Gz7a3vjs|TweCU2*2C{G$8XpgR&oH4_Lw5suWj#tSqM0<=p210m@~8 z@C2JFyqTSuQPgOF$Zo$Ed+)rU&m}Ygo{XyK`m&&_k~Sxd2 zn~cD#%uS1WQ^oostkojPvZ`pB#Yy0tWs8+1U9#9kNn4087ON@A8MOtvTE3)`n$_nt ztP6JG)z!DpPsP&OvXqq6kt?uE3yM1OZtSw~6E!^p3KQv+loZZIPmN@f+1W%&n<5UI zg58KKQ^al2%(BIcVj`8$MKRau+3IM63vrCJ%Rp99t*bB37Fu6)cCTMAF(s)qVRlBU zossfeX6Nf5@$W9l`}6)yP=eyF7omYyg5S+?Ah1Jd9{%lOP;q*RULZT820`NP(E{n9 zaw(cZDS{E1CO*CWR)n@a6;x8P=(rVlcnL(H;mHL-ZG%6nRTSgtlrDi|VC@H**S;m$ z1AVotKw$mBP+_ciwvH(O0Yd)i)=IalgKum#x4Mv8ZEx4wXf4#4&#EEN6nEDG4Qvwe zfdG|lKJctM2hs`jn7Zk#QEdgrV~VUz!}{!1sbQ$*y1a|=_+lRjHQZwry$tOw7)5zw z!3;$zd}OCx?%vq>O8408-Hk`Uf8xp}klG3ABecapu&REjt7KA-h1l?1hu1_hI3~3V+D9%^No?^ddfs0v99O6E~h1S|B-BqpzWlC@*Hgepefe zb?eQzU!e`!8XdoH!;SCECJfFdEA0O~f`fU z71xT7Y3w_@7_7WaHU2}D<10M$+|u8&Wu4j&k}W$t60c_r9@{MAUE0z@c!g1td)b>R7Le@w z^?#ri;q9csEJ{lRJ_K4Sk2{e7hJ8mCr%I_=CE_Q5v$(WA2S*u(k`+iPzY^^bws>48 z`J2>m4!8JeNmImxykt=ci%X#C8yy)s(|8?nfIb3GUuHkCea5-mooy)(o8^1OfdHD0P@r?aruxwNZ$MWy2 zO|8l63u{wm>kqzPctFU<3h&nJzq${>@o|wDws$1YzXfnsaYFK! zYn{Et5ANT{U&RZ~6$c6{bwsxvuN^-A@O)W%cyWzg_ZJ7vJwom9F}MD0-QUo#Gi%`3 zAz5P`9JwvidNPP}zI1$VY{R*;!lc4Ln{tGAjFT^*r;~@;uEAa64{+Q2jD7^BJb~9P zA5jL=)Qj1r7qxW?8Vu17*!1vd!sB!oC4hX^;I`#8pN_8+PBDBt(0CruNNr^?$an;q z9wZ)bLgEg26CP=Mw2MkNJ3@9PdaFlIj{KUO8YTBxTe zJrDI)_;Gkj*DC|#_G9wnRl-|eWQtI~Dc3`eTrNF6c$5kdfq1cI-Xq)$&&LjX{^ZKO6d@^t9gFa8|< z@=kT=3IX6y>DH&i=AL7hw)y`{%3rZav2A@ULJ+#|Z1Hx*TqdK>`q@NOOj@&L`k*?FgZ3MO8`Sf^EdMp8LALQ<4W-KPs7 zP(kO|!IQ7QpQA5bdJMOFi;kYO*y&|mA@SL`u#-IhDlnEzXnH)om>JI<^QL+cVpJ8y z=vXoxhi{;9s5ClGfGJu8$SSIY@7m9o^Uvfz#Y&@#&pH8HlOR?okXZ1{0Z@L$^IEFMIL=N+(*pjovk{Yq(FIWD`{>@*R_*r#g z%8cEq#_oU(X83M3d^hi_g*tw}^vP1uXNLN#I86JAT{k1G_^Tr&Eh?gD1w?T%EoYOs z4tMeovQpBKfF*J|E{bY9OsIAMv3U3t%qXh9>^Z3qW9e&{oWUfD3BGv{f2(*u3BmE8 zeGX*R{ncrX;{lRD+8ORNM0shTuBS4SmBv^WOV5!{>Z|*@IRJ}G&K%$XP%aszIRT)wgV?rXvm*?-g}^RN&nt1=UP|-wUh!kL^TySSFaq)o j(ry5$GZaPDQ0prB)!ADV1(SU@T|4>>k^E~HIWg$JPXn!6 literal 0 HcmV?d00001 diff --git a/plugins_sogen-support/tenet/trace/analysis.py b/plugins_sogen-support/tenet/trace/analysis.py new file mode 100644 index 0000000..0820c5f --- /dev/null +++ b/plugins_sogen-support/tenet/trace/analysis.py @@ -0,0 +1,253 @@ +import bisect +import collections + +from tenet.util.log import pmsg + +#----------------------------------------------------------------------------- +# analysis.py -- Trace Analysis +#----------------------------------------------------------------------------- +# +# This file should contain logic to further process, augment, optimize or +# annotate Tenet traces when a binary analysis framework such as IDA / +# Binary Ninja is available to a trace reader. +# +# As of now (v0.2) the only added analysis we do is to try and map +# ASLR'd trace addresses to executable opened in the database. +# +# In the future, I imagine this file will be used to indexing events +# such as function calls, returns, entry and exit to unmapped regions, +# service pointer annotations, and much more. +# + +class TraceAnalysis(object): + """ + A high level, debugger-like interface for querying Tenet traces. + """ + + def __init__(self, trace, dctx): + self._dctx = dctx + self._trace = trace + self._remapped_regions = [] + self._unmapped_entry_points = [] + self.slide = None + self._analyze() + + #------------------------------------------------------------------------- + # Public + #------------------------------------------------------------------------- + + def rebase_pointer(self, address): + """ + Return a rebased version of the given address, if one exists. + """ + for m1, m2 in self._remapped_regions: + #print(f"m1 start: {m1[0]:08X} address: {address:08X} m1 end: {m1[1]:08X}") + #print(f"m2 start: {m2[0]:08X} address: {address:08X} m2 end: {m2[1]:08X}") + if m1[0] <= address <= m1[1]: + return address + (m2[0] - m1[0]) + if m2[0] <= address <= m2[1]: + return address - (m2[0] - m1[0]) + return address + + def get_prev_mapped_idx(self, idx): + """ + Return the previous idx to fall within a mapped code region. + """ + index = bisect.bisect_right(self._unmapped_entry_points, idx) - 1 + try: + return self._unmapped_entry_points[index] + except IndexError: + return -1 + + #------------------------------------------------------------------------- + # Analysis + #------------------------------------------------------------------------- + + def _analyze(self): + """ + Analyze the trace against the binary loaded by the disassembler. + """ + self._analyze_aslr() + self._analyze_unmapped() + + def _analyze_aslr(self): + """ + Analyze trace execution to resolve ASLR mappings against the disassembler. + """ + dctx, trace = self._dctx, self._trace + + # get *all* of the instruction addresses from disassembler + instruction_addresses = dctx.get_instruction_addresses() + + # + # bucket the instruction addresses from the disassembler + # based on non-aslr'd bits (lower 12 bits, 0xFFF) + # + + binary_buckets = collections.defaultdict(list) + for address in instruction_addresses: + bits = address & 0xFFF + binary_buckets[bits].append(address) + + # get the set of unique, executed addresses from the trace + trace_addresses = trace.ip_addrs + + # + # scan the executed addresses from the trace, and discard + # any that cannot be bucketed by the non ASLR-d bits that + # match the open executable + # + + trace_buckets = collections.defaultdict(list) + for executed_address in trace_addresses: + bits = executed_address & 0xFFF + if bits not in binary_buckets: + continue + trace_buckets[bits].append(executed_address) + + # + # this is where things get a little bit interesting. we compute the + # distance between addresses in the trace and disassembler buckets + # + # the distance that appears most frequently is likely to be the ASLR + # slide to align the disassembler imagebase and trace addresses + # + + slide_buckets = collections.defaultdict(list) + for bits, bin_addresses in binary_buckets.items(): + for executed_address in trace_buckets[bits]: + for disas_address in bin_addresses: + distance = disas_address - executed_address + slide_buckets[distance].append(executed_address) + + # basically the executable 'range' of the open binary + disas_low_address = instruction_addresses[0] + disas_high_address = instruction_addresses[-1] + + # convert to set for O(1) lookup in following loop + instruction_addresses = set(instruction_addresses) + + # + # loop through all the slide buckets, from the most frequent distance + # (ASLR slide) to least frequent. the goal now is to sanity check the + # ranges to find one that seems to couple tightly with the disassembler + # + + for k in sorted(slide_buckets, key=lambda k: len(slide_buckets[k]), reverse=True): + expected = len(slide_buckets[k]) + + # + # TODO: uh, if it's getting this small, I don't feel comfortable + # selecting an ASLR slide. the user might be loading a tiny trace + # with literally 'less than 10' unique instructions (?) that + # would map to the database + # + + if expected < 10: + continue + + hit, seen = 0, 0 + for address in trace_addresses: + + # add the ASLR slide for this bucket to a traced address + rebased_address = address + k + + # the rebased address seems like it falls within the disassembler ranges + if disas_low_address <= rebased_address < disas_high_address: + seen += 1 + + # but does the address *actually* exist in the disassembler? + if rebased_address in instruction_addresses: + hit += 1 + + # + # the first *high* hit ratio is almost certainly the correct + # ASLR, practically speaking this should probably be 1.00, but + # I lowered it a bit to give a bit of flexibility. + # + # NOTE/TODO: a lower 'hit' ratio *could* occur if a lot of + # undefined instruction addresses in the disassembler get + # executed in the trace. this could be packed code / malware, + # in which case we will have to perform more aggressive analysis + # + + if (hit / seen) > 0.95: + #print(f"ASLR Slide: {k:08X} Quality: {hit/seen:0.2f} (h {hit} s {seen} e {expected})") + slide = k + break + + # + # if we do not break from the loop, we failed to find an adequate + # slide, which is very bad. + # + # NOTE/TODO: uh what do we do if we fail the ASLR slide? + # + + else: + self.slide = None + return False + + # + # TODO: err, lol this is all kind of dirty. should probably refactor + # and clean up this whole 'remapped_regions' stuff. + # + + m1 = [disas_low_address, disas_high_address] + + if slide < 0: + m2 = [m1[0] - slide, m1[1] - slide] + else: + m2 = [m1[0] + slide, m1[1] + slide] + + self.slide = slide + self._remapped_regions.append((m1, m2)) + + return True + + def _analyze_unmapped(self): + """ + Analyze trace execution to identify entry/exit to unmapped segments. + """ + if self.slide is None: + return + + # alias for readability and speed + trace, ips = self._trace, self._trace.ip_addrs + lower_mapped, upper_mapped = self._remapped_regions[0][1] + + # + # for speed, pull out the 'compressed' ip indexes that matched mapped + # (known) addresses within the disassembler context + # + + mapped_ips = set() + for i, address in enumerate(ips): + if lower_mapped <= address <= upper_mapped: + mapped_ips.add(i) + + last_good_idx = 0 + unmapped_entries = [] + + # loop through each segment in the trace + for seg in trace.segments: + seg_ips = seg.ips + seg_base = seg.base_idx + + # loop through each executed instruction in this segment + for relative_idx in range(0, seg.length): + compressed_ip = seg_ips[relative_idx] + + # the current instruction is in an unmapped region + if compressed_ip not in mapped_ips: + + # if we were in a known/mapped region previously, then save it + if last_good_idx: + unmapped_entries.append(last_good_idx) + last_good_idx = 0 + + # if we are in a good / mapped region, update our current idx + else: + last_good_idx = seg_base + relative_idx + + #print(f" - Unmapped Entry Points: {len(unmapped_entries)}") + self._unmapped_entry_points = unmapped_entries diff --git a/plugins_sogen-support/tenet/trace/arch/__init__.py b/plugins_sogen-support/tenet/trace/arch/__init__.py new file mode 100644 index 0000000..791fd3d --- /dev/null +++ b/plugins_sogen-support/tenet/trace/arch/__init__.py @@ -0,0 +1,2 @@ +from .x86 import ArchX86 +from .amd64 import ArchAMD64 \ No newline at end of file diff --git a/plugins_sogen-support/tenet/trace/arch/__pycache__/__init__.cpython-311.pyc b/plugins_sogen-support/tenet/trace/arch/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e883137449edf0317f28680529c6dbe2d9c2cc6e GIT binary patch literal 290 zcmZ3^%ge<81nuV?5>0{hV-N=hn4pZ$azMs(h7^Vr#vF!R#wbQc5SuB7DVI5l8OUZ1 zX3%7L$p}=W$#{$1u_!qs!om#5cMuOJYiZN>gJJf%e42$7kkcmc+;F6;%G>u*uC&Da}c>D+2irF0PkVA+y4{OTc%L`(oykn{-seuI4QL;}Kk>gw06#^rvBsIR z($P5v0ux~H9+>or0DQ%@HV+tap(_U)LF}9C>Lv!q5P~N$@CcJUnGvtSWKUrYPi2bN zJOHMClst_!v5Qu=3?95T_N<-ZyIXA?t@d@(d8vy`l!rmSq1h#wJ;1e#!Qi&QK;VwRU4fy%J+9zO z({8KOC{6rDNNL%obdj=UEc`7>-!A?5EYqZvr2)#yo-wh_a?5#cPhX5@)U~~FN$?Yi z>y)k8cw)OKyI)|1-Qo1<(vm-zDdEexI@FwJa+cI>Ar~6@+;UD|4zS|r0598%Q zlw|WfOu~GgV`(_|@r>sFg6#}@heuq-hBfeRAsZoz^QR(2(JBy7p>w4epIo|JJUeM^ z7o#5nm3mQ8D>#P?%64HFHrF>zr(J11;hIot`8s;1+N1T6JmxnrtViERr;;T}s>oFd PyZ(#cLi_WdMBVr=@9nE= literal 0 HcmV?d00001 diff --git a/plugins_sogen-support/tenet/trace/arch/__pycache__/x86.cpython-311.pyc b/plugins_sogen-support/tenet/trace/arch/__pycache__/x86.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8388355c6aecc4f77242b4f38c55a3270a80b3b2 GIT binary patch literal 637 zcmZuuJ#W-N5S_KpKHn*q0uW!7+e>jLnh;PFaUa&P5*$_xQe;_LS>8=z;n-pAlL(ay z1V14?1qktDXpyT(RCGp1m5NzkE)9(3x9`0f&3M*ddc7`SeEj|-91?(^O01)GMV2;_ zbI@Q09Nq&bpEZDQ_^!eSu6fYcqqU*Vb#}Rh;v59{goCef;_F=VTU_@I-ttXu`0XR$ z<|pD?yp3I~sy&j?@xjjayREK`@pfn1p17}U<;I1GiiH&RR2;@>T*O&AXo}(wI>0aW zy_N1z7r}G;9Xv({2xA2k1=B{l(p*(~RC!byDCbyjF_wlWg0ZU0*h$70iQ*fKy;+3G zRi@1t&my#q=c5^&R2%LKI)CZxGmrXCrQvYRbF0q2Gov2b-d9*+ces2ydmK!&{47jD zIh@EadM)ITPDi#YvqO>RxJ#JWj|LBhr^(_dPV=A;sVIU1OGOak_5ohS;Pg!5s?Y%b zFUZFT<@Hl*nqk$Tz=Yn7(z&>Kt9*9R*(}FD6x3$dFl)F59gNMAmQ>zdH=Xy%YQi1m nH1<7Ym)7p;LiXf+l+EaeNNSxBQfs#fcKsKAEAP*L5>4a36J48$ literal 0 HcmV?d00001 diff --git a/plugins_sogen-support/tenet/trace/arch/amd64.py b/plugins_sogen-support/tenet/trace/arch/amd64.py new file mode 100644 index 0000000..5b4a741 --- /dev/null +++ b/plugins_sogen-support/tenet/trace/arch/amd64.py @@ -0,0 +1,31 @@ +class ArchAMD64: + """ + AMD64 CPU Architecture Definition. + """ + MAGIC = 0x41424344 + + POINTER_SIZE = 8 + + IP = "RIP" + SP = "RSP" + + REGISTERS = \ + [ + "RAX", + "RBX", + "RCX", + "RDX", + "RBP", + "RSP", + "RSI", + "RDI", + "R8", + "R9", + "R10", + "R11", + "R12", + "R13", + "R14", + "R15", + "RIP" + ] \ No newline at end of file diff --git a/plugins_sogen-support/tenet/trace/arch/x86.py b/plugins_sogen-support/tenet/trace/arch/x86.py new file mode 100644 index 0000000..62ac474 --- /dev/null +++ b/plugins_sogen-support/tenet/trace/arch/x86.py @@ -0,0 +1,23 @@ +class ArchX86: + """ + x86 CPU Architecture Definition. + """ + MAGIC = 0x386 + + POINTER_SIZE = 4 + + IP = "EIP" + SP = "ESP" + + REGISTERS = \ + [ + "EAX", + "EBX", + "ECX", + "EDX", + "EBP", + "ESP", + "ESI", + "EDI", + "EIP" + ] \ No newline at end of file diff --git a/plugins_sogen-support/tenet/trace/file.py b/plugins_sogen-support/tenet/trace/file.py new file mode 100644 index 0000000..b10109e --- /dev/null +++ b/plugins_sogen-support/tenet/trace/file.py @@ -0,0 +1,1754 @@ +import os +import time +import zlib +import array +import bisect +import ctypes +import struct +import zipfile +import binascii +import itertools +import collections + +#----------------------------------------------------------------------------- +# file.py -- Trace File +#----------------------------------------------------------------------------- +# +# NOTE/PREFACE: Please be aware, this is a 100% prototype implementation +# of a basic trace log file specification. It has not been designed with +# exhaustive attention to scalability + performance for use-cases that +# exceed the recommended 'maximum' of 10,000,000 (10m) instructions. +# +# There are no dependencies. There is no multiprocessing. This is will +# be a nightmare to maintain or scale further. It is 100% meant to be +# thrown away in favor of a native backend. +# +# -------------- +# +# This file contains the 'trace file' implementation for the plugin. It +# is responsible for the loading / processing of raw text traces, providing +# a few 'low level' APIs for querying information out of the lifted trace. +# +# When loading a text trace, this code will also do some basic compression +# of the trace to reduce both its on-disk and in-memory footprint. It will +# also perform some basic 'indexing' of the trace and its contents to make +# it more performant to search and query by the 'high level' trace reader. +# +# Upon completion, the indexed+compressed trace file is saved to disk +# alongside the original trace, with the '.tt' (Tenet Trace) file +# extension. This original trace can be discarded by the user. +# +# The processed trace can be loaded and used in a fraction of the time +# versus the raw text trace. The trace file implementation will also seek +# out a matching file name with the '.tt' file extension, and prioritize +# loading that over a raw text trace. +# + +#----------------------------------------------------------------------------- +# Imports +#----------------------------------------------------------------------------- + +# +# attempt plugin imports, assuming this file is being run / loaded in +# the context of the plugin running within a disassembler +# + +try: + from tenet.util.log import pmsg + from tenet.trace.arch import ArchAMD64, ArchX86 + from tenet.trace.types import TraceMemory + +# +# this script can technically be run in a standalone mone to process / digest +# a trace outside of a disassembler / the normal integration. so if the above +# fails, use the following imports to operate independently +# + +except ImportError: + from arch import ArchAMD64, ArchX86 + from .types import TraceMemory + pmsg = print + +#----------------------------------------------------------------------------- +# Definitions +#----------------------------------------------------------------------------- + +BYTE_MAX = (1 << 8) - 1 +USHRT_MAX = (1 << 16) - 1 +UINT_MAX = (1 << 32) - 1 +ULLONG_MAX = (1 << 64) - 1 + +TRACE_MEM_READ = 0 +TRACE_MEM_WRITE = 1 + +# +# NOTE: some of this stuff is probably broken / cannot be easily toggled +# anymore, so I wouldn't actually suggest playing around with them as things +# will probably break or behave erratically +# + +TRACE_STATS = False + +#DEFAULT_COMPRESSION = zipfile.ZIP_BZIP2 +#DEFAULT_COMPRESSION = zipfile.ZIP_LZMA +DEFAULT_COMPRESSION = zipfile.ZIP_DEFLATED + +#DEFAULT_SEGMENT_LENGTH = 250_000 +#DEFAULT_SEGMENT_LENGTH = 1_000_000 +DEFAULT_SEGMENT_LENGTH = USHRT_MAX +REG_OFFSET_CACHE_SIZE = 16 +REG_OFFSET_CACHE_INTERVAL = 4096 + +#----------------------------------------------------------------------------- +# Utils +#----------------------------------------------------------------------------- + +def hash_file(filepath): + """ + Return a CRC32 of the file at the given path. + """ + crc = 0 + with open(filepath, 'rb', 65536) as ins: + for x in range(int((os.stat(filepath).st_size / 65536)) + 1): + crc = zlib.crc32(ins.read(65536), crc) + return (crc & 0xFFFFFFFF) + +def number_of_bits_set(i): + """ + Count the number of bits set in the given 32bit integer. + """ + i = i - ((i >> 1) & 0x55555555) + i = (i & 0x33333333) + ((i >> 2) & 0x33333333) + return (((i + (i >> 4) & 0xF0F0F0F) * 0x1010101) & 0xffffffff) >> 24 + +def width_from_type(t): + """ + Return the byte width of a python 'struct' type definition. + """ + if t == 'B': + return 1 + elif t == 'H': + return 2 + elif t == 'I': + return 4 + elif t == 'Q': + return 8 + raise ValueError(f"Invalid type '{t}'") + +def type_from_width(width): + """ + Return an appropriate integer type for the given byte width. + """ + if width == 1: + return 'B' + elif width == 2: + return 'H' + elif width == 4: + return 'I' + elif width == 8: + return 'Q' + raise ValueError(f"Invalid type width {width}") + +def type_from_limit(limit): + """ + Return an appropriate integer type for the maximum given value. + """ + if limit <= BYTE_MAX: + return 'B' + elif limit <= USHRT_MAX: + return 'H' + elif limit <= UINT_MAX: + return 'I' + elif limit <= ULLONG_MAX: + return 'Q' + raise ValueError(f"Limit {limit:,} exceeds maximum type") + +#----------------------------------------------------------------------------- +# Serialization Structures +#----------------------------------------------------------------------------- + +class TraceInfo(ctypes.Structure): + _pack_ = 1 + _fields_ = [ + ('arch_magic', ctypes.c_uint32), + ('ip_num', ctypes.c_uint32), + ('mem_addrs_num', ctypes.c_uint32), + ('mask_num', ctypes.c_uint32), + ('mem_idx_width', ctypes.c_uint8), + ('mem_addr_width', ctypes.c_uint8), + ('original_hash', ctypes.c_uint32), + ] + +class SegmentInfo(ctypes.Structure): + _pack_ = 1 + _fields_ = [ + ('id', ctypes.c_uint32), + ('base_idx', ctypes.c_uint32), + ('length', ctypes.c_uint32), + + ('ip_num', ctypes.c_uint32), + ('ip_length', ctypes.c_uint32), + + ('reg_mask_num', ctypes.c_uint32), + ('reg_mask_length', ctypes.c_uint32), + ('reg_data_length', ctypes.c_uint32), + + ('mem_read_num', ctypes.c_uint32), + ('mem_read_data_length', ctypes.c_uint32), + + ('mem_write_num', ctypes.c_uint32), + ('mem_write_data_length', ctypes.c_uint32), + ] + +class MemValue(ctypes.Structure): + _pack_ = 1 + _fields_ = [ + ('mask', ctypes.c_uint8), + ('value', ctypes.c_uint8 * 8) + ] + +#----------------------------------------------------------------------------- +# Trace File +#----------------------------------------------------------------------------- + +class TraceFile(object): + """ + An interface to load and query data directly from a trace file. + """ + + def __init__(self, filepath, arch=None): + self.filepath = filepath + self.arch = arch + + # + # TODO: really, the trace file should auto-detect arch imo but i'll + # do that at a later date... + # + + if not self.arch: + self.arch = ArchAMD64() + + # a sorted array of all unique PC / IP (eg, EIP, or RIP) that appear in the trace + self.ip_addrs = None + + # + # mem_addrs: a sorted array of all unique memory addresses referenced + # in the trace (8-byte aligned) + # + # mem_masks: a sorted array of byte masks that correspond with the addrs + # array described above. each entry in this array is a single 8bit mask, + # where each bit specifies if that memory address was accessed over the + # course of the entire trace + # + # e.g: + # mem_addrs[924] = 0x401448 (an 8-byte aligned memory address) + # mem_masks[924] = 0x0F (a 'mask' of what bytes exist in the trace) + # | + # |_ a bit mask of 00001111 + # + # In this example, we know that 0x401448 --> 0x40144C were either read + # or written at some point in this trace. + # + # The alignment of pointers helps with basic id-based compression as + # these pointer id / 'mapped addresses' are used across the segments. + # + # The masks effectively create a global bitmap of all addresses that + # actually appear in the trace, allowing certain addresses to be + # immediately discarded from memory queries. This can dramatically + # reduce search complexity. + # + + self.mem_addrs = None + self.mem_masks = None + + # + # register data is stored in a contiguos blob for each trace segment. + # + # for each step / 'instruction' of the trace, we create a 32bit + # register mask that defines which registers changed. each bit in + # the mask defines 1 unique CPU register, and its position in the + # mask specifies which one it is. + # + # this will contain a list of each unique register delta mask that + # appears in the trace. instead of storing 32bit mask for each step + # of the trace, we use this table to translate a 8bit ID (an index) + # into this table of unique register masks (self.masks) + # + + self.masks = [] # TODO: rename to register_masks or something... + + # an O(1) lookup table for the 'byte size' of each register mask + self.mask_sizes = [] + + # + # a trace is broken up into segments of 64k instructions. each of + # theses segments will have small indexes / summaries embedded in + # them to make them easier to search or ignore as applicable + # + # for more information, look at the TraceSegments class + # + + self.segments = [] + + # the number of timestamps / 'instructions' for each trace segment + self.segment_length = DEFAULT_SEGMENT_LENGTH + + # the hash of the original / source log file + self.original_hash = None + + # + # now that you have some idea of how the trace file is going to be + # organized... let's actually go and try to load one + # + + self._load_trace() + + #------------------------------------------------------------------------- + # Properties + #------------------------------------------------------------------------- + + @property + def name(self): + """ + Return the name of the trace. + """ + return os.path.basename(self.filepath) + + @property + def packed_name(self): + """ + Return the packed trace filename. + """ + root, ext = os.path.splitext(self.name) + return f"{root}.tt" + + @property + def packed_filepath(self): + """ + Return the packed trace filepath. + """ + directory = os.path.dirname(self.filepath) + return os.path.join(directory, self.packed_name) + + @property + def length(self): + """ + Return the length of the trace. (e.g, # instructions executed) + """ + if not self.segments: + return 0 + return self.segments[-1].base_idx + self.segments[-1].length + + #------------------------------------------------------------------------- + # Public + #------------------------------------------------------------------------- + + # + # I should really define this somewhere more notable... but throughout + # this project you will see the term 'idx', this is a simple abbreviation + # of 'index' that I used early on and kind of stuck with. + # + # an idx is simply an integer, that repersents a unique 'timestamp' in + # the trace file. eg, idx 0 is the start of the trace, idx 100 is + # equivalent to 100 steps into the trace, etc... + # + # an idx label attached to any sort of variable / definition in this + # codebase suggest that variable is a trace 'timestamp' / position! + # + + def get_reg_delta(self, idx): + """ + Return the register delta for a given timestamp. + """ + seg = self.get_segment(idx) + if not seg: + return {} + return seg.get_reg_delta(idx) + + def get_read_delta(self, idx): + """ + Return the memory read delta for a given timestamp. + """ + seg = self.get_segment(idx) + if not seg: + return {} + return seg.get_read_delta(idx) + + def get_write_delta(self, idx): + """ + Return the memory write delta for a given timestamp. + """ + seg = self.get_segment(idx) + if not seg: + return {} + return seg.get_write_delta(idx) + + def get_segment(self, idx): + """ + Return the trace segment for a given timestamp. + """ + for seg in self.segments: + if seg.base_idx <= idx < seg.base_idx + seg.length: + return seg + return None + + def get_reg_mask_ids_containing(self, reg_name): + """ + Return a set of reg mask ids containing the given register name. + """ + reg_id = self.arch.REGISTERS.index(reg_name.upper()) + reg_mask = 1 << reg_id + + found = set() + for i, current_mask in enumerate(self.masks): + if current_mask & reg_mask: + found.add(i) + + return found + + #------------------------------------------------------------------------- + # Save / Serialization + #------------------------------------------------------------------------- + + def _save(self): + """ + Save the packed trace to disk. + """ + + with zipfile.ZipFile(self.packed_filepath, 'w', compression=DEFAULT_COMPRESSION) as zip_archive: + self._save_header(zip_archive) + self._save_segments(zip_archive) + + self.filepath = self.packed_filepath + + def _save_header(self, zip_archive): + """ + Save the trace header to the packed trace. + """ + + # populate the trace header + header = TraceInfo() + header.arch_magic = self.arch.MAGIC + header.ip_num = len(self.ip_addrs) + header.mem_addrs_num = len(self.mem_addrs) + header.mask_num = len(self.masks) + header.mem_idx_width = width_from_type(self.mem_idx_type) + header.mem_addr_width = width_from_type(self.mem_addr_type) + header.original_hash = self.original_hash + mask_data = (ctypes.c_uint32 * len(self.masks))(*self.masks) + + # save the global trace data / header to the zip + with zip_archive.open('header', 'w') as f: + f.write(bytearray(header)) + f.write(bytearray(self.ip_addrs)) + f.write(bytearray(self.mem_addrs)) + f.write(bytearray(self.mem_masks)) + f.write(bytearray(mask_data)) + + def _save_segments(self, zip_archive): + """ + Save the trace segments to the packed trace. + """ + for segment in self.segments: + with zip_archive.open(f'segments/{segment.id}', 'w') as f: + segment.dump(f) + + #------------------------------------------------------------------------- + # Load / Deserialization + #------------------------------------------------------------------------- + + def _load_trace(self): + """ + Load a trace from disk. + + NOTE: THIS ROUTINE WILL ATTEMPT TO LOAD A PACKED TRACE INSTEAD OF A + SELECTED RAW TEXT TRACE IF IT FINDS ONE AVAILABLE!!! + """ + + # the user probably selected a '.tt' trace + if zipfile.is_zipfile(self.filepath): + self._load_packed_trace(self.filepath) + return + + # + # the user selected a '.txt' trace, but there is a '.tt' packed trace + # beside it, so let's check if the packed trace matches the text trace + # + + if zipfile.is_zipfile(self.packed_filepath): + packed_crc = self._fetch_hash(self.packed_filepath) + text_crc = hash_file(self.filepath) + + # + # the crc in the packed file seems to match the selected text log, + # so let's just load the packed trace as it should be faster + # + + if packed_crc == text_crc: + self._load_packed_trace(self.packed_filepath) + return + + # + # no luck loading / side-loading packed traces, so simply try to + # load the user selected trace as a normal text Tenet trace + # + + self._load_text_trace(self.filepath) + + def _load_packed_trace(self, filepath): + """ + Load a packed trace from disk. + """ + + with zipfile.ZipFile(filepath, 'r') as zip_archive: + self._load_header(zip_archive) + self._load_segments(zip_archive) + + self.filepath = filepath + + def _select_arch(self, magic): + """ + TODO: Select the trace CPU arch based on the given magic value. + """ + if ArchAMD64.MAGIC == magic: + self.arch = ArchAMD64() + else: + self.arch = ArchX86() + + def _fetch_hash(self, filepath): + """ + Return the original file hash (CRC32) from the given packed trace filepath. + """ + header = TraceInfo() + with zipfile.ZipFile(filepath, 'r') as zip_archive: + with zip_archive.open('header', 'r') as f: + f.readinto(header) + return header.original_hash + + def _load_header(self, zip_archive): + """ + Load the trace header from a packed trace. + """ + header = TraceInfo() + + with zip_archive.open('header', 'r') as f: + + # read the main trace info from the packed trace header + f.readinto(header) + + # select the cpu / arch for this trace + #print(f"Loading magic 0x{header.arch_magic:08X}") + self._select_arch(header.arch_magic) + + # load the (sorted) ip address table from disk + self.ip_addrs = array.array(type_from_width(self.arch.POINTER_SIZE)) + self.ip_addrs.fromfile(f, header.ip_num) + + # ('mem_idx_width', ctypes.c_uint8), + # ('mem_addr_width', ctypes.c_uint8), + self.mem_idx_type = type_from_width(header.mem_idx_width) + self.mem_addr_type = type_from_width(header.mem_addr_width) + #self.mem_mask_width = type_from_width(header.mem_mask_width) + + # load the (sorted, aligned) mem table from disk + self.mem_addrs = array.array(type_from_width(self.arch.POINTER_SIZE)) + self.mem_addrs.fromfile(f, header.mem_addrs_num) + self.mem_masks = array.array('B') + self.mem_masks.fromfile(f, header.mem_addrs_num) + + # ('mask_num', ctypes.c_uint32), + self.masks = array.array('I') + self.masks.fromfile(f, header.mask_num) + self.mask_sizes = [number_of_bits_set(mask) * self.arch.POINTER_SIZE for mask in self.masks] + + # source file hash + self.original_hash = header.original_hash + + def _load_segments(self, zip_archive): + """ + Load the trace segments from the packed trace. + """ + + for path in zip_archive.namelist(): + + # skip anything that is not a trace segment + if not (path.startswith('segments/') and path[-1] != '/'): + continue + + # load a trace segment from the packed trace file + with zip_archive.open(path, 'r') as f: + segment = TraceSegment(self) + segment.from_file(f) + + # save the segment to the trace + self.segments.append(segment) + + # sort the loaded segments by id (just in case) + self.segments.sort(key=lambda x: x.id) + + def _load_text_trace(self, filepath): + """ + Load a text trace from disk. + """ + idx = 0 + + # mappings of address/mask and their mapped (compressed) id + # - NOTE: these are only used when converting traces from text to binary + self.ip_map = collections.OrderedDict() + self.mem_map = collections.OrderedDict() + self.mask2mapped = {} + self.masks = [] + + # TODO: detect arch based on reg / lines in file + #if not self.arch: + # self._select_arch(0) + + # hash (CRC32) the source / text filepath before loading it + self.original_hash = hash_file(filepath) + + # load / parse a text trace into trace segments + with open(filepath, 'r') as f: + + # loop until all of the lines in the file have been processed + while True: + + # select a chunk of N lines from the file + lines = itertools.islice(f, self.segment_length) + lines = list(lines) + if not lines: + break + + segment_id = len(self.segments) + + # create a new trace segment from the given lines of text + segment = TraceSegment(self, segment_id, idx) + segment.from_lines(lines) + idx += segment.length + + # save the segment + self.segments.append(segment) + #break # for debugging... + + self._finalize() + self._save() + + def get_ip(self, idx): + """ + Return the fully qualified IP for the given timestamp. + """ + seg = self.get_segment(idx) + if not seg: + raise ValueError("Invalid IDX %u" % idx) + return seg.get_ip(idx) + + def get_mapped_ip(self, ip): + """ + Return the 'mapped' (compressed) id for the given instruction address. + """ + index = bisect.bisect_left(self.ip_addrs, ip) + + try: + if ip == self.ip_addrs[index]: + return index + except IndexError: + pass + + raise ValueError(f"Address {ip:08X} does not have a mapped ID") + + # + # TODO: note, uh.. these should all be refactored... gross + # + + def get_aligned_address(self, address): + return (address >> 3) << 3 + + def get_mapped_address(self, address): + """ + Return the 'mapped' (compressed) id for the given memory address. + """ + + # + # TODO: use pointer size/alignment?? eg, this might make mem lookups faster + # if we tune it to 32bit vs 64bit (at the cost of possible trace size inflation) + # + + aligned_address = (address >> 3) << 3 + index = bisect.bisect_left(self.mem_addrs, aligned_address) + + if index == len(self.mem_addrs): + return -1 + + if aligned_address != self.mem_addrs[index]: + return -1 + + return index + + def get_aligned_address_mask(self, address, length=8): + """ + TODO: ugh hopefully we'll have a native backend before i have to try + and write a comment to describe the mess we're in + """ + mask_offset = address % 8 + aligned_address = ((address >> 3) << 3) + aligned_mask = (((1 << length) - 1) << mask_offset) & 0xFF + return aligned_mask + + def _finalize(self): + """ + Bake a parsed text trace into its final, compressed form. + """ + + if TRACE_STATS: + self._init_stats() + + # a 32 or 64 bit array.array type code, depending on the trace arch pointer size + pointer_type = type_from_width(self.arch.POINTER_SIZE) + + # bake the master ip address table + ip_map = self.ip_map + ip_addrs = sorted(list(self.ip_map.keys())) + self.ip_addrs = array.array(pointer_type, ip_addrs) + + remapped_ip = { + ip_map[address]: i for i, address in enumerate(ip_addrs) + } + + # bake the master (aligned) memory address table + mem_map = self.mem_map + mem_map_len = len(mem_map) + mem_addrs = sorted(list(mem_map.keys())) + self.mem_addrs = array.array(pointer_type, mem_addrs) + self.mem_masks = array.array('B', [0] * len(mem_addrs)) + + # generate a temporary mem re-mapping map... + remapped_mem = { + mem_map[address]: i for i, address in enumerate(mem_addrs) + } + + # pre-compute the 'size' of the data represented by a register mask + self.mask_sizes = [number_of_bits_set(mask) * self.arch.POINTER_SIZE for mask in self.masks] + + assert self.segment_length <= UINT_MAX + assert mem_map_len <= UINT_MAX + + self.mem_idx_type = type_from_limit(self.segment_length) + self.mem_addr_type = type_from_limit(mem_map_len) + + # finish packing the trace + for segment in self.segments: + segment.finalize(remapped_ip, remapped_mem) + + if TRACE_STATS: + self._harvest_stats(segment) + + # dispose of stuff we don't need anymore + del self.ip_map + del self.mem_map + del remapped_mem + + if TRACE_STATS: + self._finalize_stats() + + #------------------------------------------------------------------------- + # Trace Statistics + #------------------------------------------------------------------------- + + def _init_stats(self): + self.unique_mem_addr = set() + self.avg_unique_mem_addr = 0 + self.min_unique_mem_addr = 999999999 + self.max_unique_mem_addr = -1 + + self.avg_unique_ip = 0 + self.min_unique_ip = 999999999999 + self.max_unique_ip = -1 + + self.num_bytes_read = 0 + self.num_bytes_written = 0 + self.num_bytes_read_info = 0 + self.num_bytes_written_info = 0 + + self.num_bytes_ips = 0 + self.num_bytes_reg_data = 0 + self.num_bytes_reg_masks = 0 + + self.raw_size = 0 + + self.unique_ip = 0 + self.num_bytes_unique_ip = 0 + + self.unique_mem = 0 + self.num_bytes_unique_mem = 0 + + def _harvest_stats(self, seg): + unique_mem_addr = seg.read_addresses | seg.write_addresses + self.unique_mem_addr |= unique_mem_addr + + num_unique_mem_addr = len(unique_mem_addr) + self.avg_unique_mem_addr += num_unique_mem_addr + self.min_unique_mem_addr = min(self.min_unique_mem_addr, num_unique_mem_addr) + self.max_unique_mem_addr = max(self.max_unique_mem_addr, num_unique_mem_addr) + + num_unique_ip = seg.num_unique_ip + self.avg_unique_ip += num_unique_ip + self.min_unique_ip = min(self.min_unique_ip, num_unique_ip) + self.max_unique_ip = max(self.max_unique_ip, num_unique_ip) + + self.num_bytes_read += seg.num_bytes_read + self.num_bytes_written += seg.num_bytes_written + self.num_bytes_read_info += seg.num_bytes_read_info + self.num_bytes_written_info += seg.num_bytes_written_info + + self.num_bytes_ips += seg.num_bytes_ips + self.num_bytes_reg_data += seg.num_bytes_reg_data + self.num_bytes_reg_masks += seg.num_bytes_reg_masks + + self.raw_size += seg.raw_size_bytes + #self.length += seg.length + + def _finalize_stats(self): + self.avg_unique_ip = self.avg_unique_ip // len(self.segments) + self.avg_unique_mem_addr = self.avg_unique_mem_addr // len(self.segments) + + self.unique_ip = len(self.ip_addrs) + self.num_bytes_unique_ip = len(self.ip_addrs) * self.arch.POINTER_SIZE + self.raw_size += self.num_bytes_unique_ip + + self.unique_mem = len(self.mem_addrs) + self.num_bytes_unique_mem = len(self.mem_addrs) * self.arch.POINTER_SIZE + self.raw_size += self.num_bytes_unique_mem + + def print_stats(self): + output = [] + output.append(f"+- Trace Stats") + output.append("") + output.append(f" -- {self.length:,} timestamps") + output.append(f" -- {len(self.segments):,} segments") + output.append("") + output.append(f" - Address Stats") + output.append("") + output.append(f" -- {self.unique_ip:,} total unique ip addresses") + output.append(f" ---- {self.avg_unique_ip} avg") + output.append(f" ---- {self.min_unique_ip} min") + output.append(f" ---- {self.max_unique_ip} max") + output.append("") + output.append(f" -- {len(self.unique_mem_addr):,} total unique mem addresses") + output.append(f" ---- {self.avg_unique_mem_addr} avg") + output.append(f" ---- {self.min_unique_mem_addr} min") + output.append(f" ---- {self.max_unique_mem_addr} max") + output.append("") + output.append(f" - Memory / Disk Footprint") + output.append("") + output.append(f" -- {self.raw_size/(1024*1024):0.2f}mb - raw size") + output.append("") + output.append(f" ---- {self.num_bytes_unique_ip / (1024*1024):0.2f}mb ({(self.num_bytes_unique_ip / self.raw_size) * 100 :3.2f}%) - ip addrs") + output.append(f" ---- {self.num_bytes_unique_mem / (1024*1024):0.2f}mb ({(self.num_bytes_unique_mem / self.raw_size) * 100 :3.2f}%) - mem addrs") + output.append(f" ---- {self.num_bytes_ips / (1024*1024):0.2f}mb ({(self.num_bytes_ips / self.raw_size) * 100 :3.2f}%) - ip trace") + output.append(f" ---- {self.num_bytes_reg_data / (1024*1024):0.2f}mb ({(self.num_bytes_reg_data / self.raw_size) * 100 :3.2f}%) - reg data") + output.append(f" ---- {self.num_bytes_reg_masks / (1024*1024):0.2f}mb ({(self.num_bytes_reg_masks / self.raw_size) * 100 :3.2f}%) - reg masks") + output.append("") + output.append(f" ---- {self.num_bytes_read / (1024*1024):0.2f}mb ({(self.num_bytes_read / self.raw_size) * 100 :3.2f}%) - bytes read") + output.append(f" ---- {self.num_bytes_written / (1024*1024):0.2f}mb ({(self.num_bytes_written / self.raw_size) * 100 :3.2f}%) - bytes written") + output.append(f" ---- {self.num_bytes_read_info / (1024*1024):0.2f}mb ({(self.num_bytes_read_info / self.raw_size) * 100 :3.2f}%) - read pointers") + output.append(f" ---- {self.num_bytes_written_info / (1024*1024):0.2f}mb ({(self.num_bytes_written_info / self.raw_size) * 100 :3.2f}%) - write pointers") + print(''.join(output)) + +class TraceSegment(object): + """ + A segment of trace data. + """ + + def __init__(self, trace, id=0, base_idx=0): + self.id = id + self.arch = trace.arch + self.trace = trace + + self.base_idx = base_idx + self.length = 0 + + self.reg_data = None + self.reg_masks = None + + self.read_data = None + self.read_idxs = None + self.read_addrs = None + self.read_masks = None + self.read_offsets = [] + + self.write_data = None + self.write_idxs = None + self.write_addrs = None + self.write_masks = None + self.write_offsets = [] + + self.mem_delta = collections.defaultdict(MemValue) + + #------------------------------------------------------------------------- + # Properties + #------------------------------------------------------------------------- + + @property + def read_set(self): + return set(self.read_addrs) + + @property + def write_set(self): + return set(self.write_addrs) + + @property + def num_unique_ip(self): + return len(set(self.ips)) + + @property + def num_unique_mem_addresses(self): + return len(self.read_set | self.write_set) + + @property + def num_bytes_read(self): + return len(self.read_data) + + @property + def num_bytes_written(self): + return len(self.write_data) + + #@property + #def num_bytes_read_info(self): + # return ctypes.sizeof(self._mem_read_info) + + #@property + #def num_bytes_written_info(self): + # return ctypes.sizeof(self._mem_write_info) + + @property + def num_bytes_reg_data(self): + return len(self.reg_data) + + @property + def num_bytes_ips(self): + return ctypes.sizeof(self.ips) + + @property + def num_bytes_reg_masks(self): + return ctypes.sizeof(self.reg_masks) + + @property + def raw_size_bytes(self): + size = 0 + + # reg data storage costs + size += self.num_bytes_ips + size += self.num_bytes_reg_data + size += self.num_bytes_reg_masks + + # memory data storage costs + size += self.num_bytes_read + size += self.num_bytes_written + size += self.num_bytes_read_info + size += self.num_bytes_written_info + + return size + + @property + def raw_size_mb(self): + return self.raw_size_bytes / (1024*1024) + + def __str__(self): + output = [] + output.append(f"Trace Segment -- IDX {self.base_idx}") + output.append(f" -- Reg Data {len(self.reg_data)} bytes ({len(self.reg_data) / (1024*1024):0.2f}mb)") + output.append(f" -- Unique IP {len(set(self.ips))}") + output.append(f" -- Raw Size {self.raw_size_mb:0.2f}mb") + return ''.join(output) + + #------------------------------------------------------------------------- + # Public + #------------------------------------------------------------------------- + + def from_lines(self, lines): + """ + Load a trace segment from the given lines. + """ + + # ip storage + self.ips = [0 for x in range(self.trace.segment_length)] + + # register storage (minus IP) + MAX_REG_DATA = self.trace.arch.POINTER_SIZE * len(self.trace.arch.REGISTERS) * self.trace.segment_length + self.reg_data = bytearray(MAX_REG_DATA) + self.reg_offsets = array.array("I", [0] * REG_OFFSET_CACHE_SIZE) + self.reg_masks = [0 for x in range(self.trace.segment_length)] + self._reg_offset = 0 + + # memory defs + self._mem_read_info = [] + self.read_data = bytearray() + self._mem_write_info = [] + self.write_data = bytearray() + self._max_read_size = 0 + self._max_write_size = 0 + + self._process_lines(lines) + #print(f"Snapshot entries: {len(self.mem_delta)}") + + def from_file(self, f): + """ + Load the trace segment from the given filestream. + """ + self.load(f) + + def get_ip(self, idx): + """ + Return the IP for the given timestamp. + """ + relative_idx = idx - self.base_idx + return self.trace.ip_addrs[self.ips[relative_idx]] + + def get_reg_delta(self, idx): + """ + Return the register delta for the given timestamp. + """ + relative_idx = idx - self.base_idx + + # IP is the only register guaranteed to have changed each step + ip_address = self.trace.ip_addrs[self.ips[relative_idx]] + + # fetch the mask that tells which registers have changed this delta + mask = self.trace.masks[self.reg_masks[relative_idx]] + + # if no registers changed, nothing to do but return IP + if not mask: + return {self.trace.arch.IP: ip_address} + + # + # fetch the closest cached register data offset that we can start from + # for computing precisely where we should be working backwards from + # + + cache_index = int(relative_idx / REG_OFFSET_CACHE_INTERVAL) + cache_offset = self.reg_offsets[cache_index] + cache_idx = cache_index * REG_OFFSET_CACHE_INTERVAL + + # compute the current 'offset' in the reg data that we will work back from + sizes = self.trace.mask_sizes + offset_masks = self.reg_masks[cache_idx:relative_idx][::-1] + offset = cache_offset + sum([sizes[mask_id] for mask_id in offset_masks]) + + # compute the location of the packed register delta data + #offset_slow = sum([sizes[mask_id] for mask_id in self.reg_masks[:relative_idx]]) + #assert offset == offset_slow + + # fetch the register data + reg_names = self._mask2regs(mask) + num_regs = len(reg_names) + reg_data = self.reg_data[offset:offset + (num_regs * self.arch.POINTER_SIZE)] + + # unpack the register data + pack_fmt = 'Q' if self.arch.POINTER_SIZE == 8 else 'I' + reg_values = struct.unpack(pack_fmt * num_regs, reg_data) + + # pack all the registers into a dict that will be returned to the user + registers = dict(zip(reg_names, reg_values)) + registers[self.trace.arch.IP] = ip_address + + # return the completed register delta + return registers + + # + # TODO: ugh some of this stuff is pretty gross too, is it even used still...? + # + + def get_read_delta(self, idx): + """ + Return the memory read delta for the given timestamp. + """ + return self._get_mem_delta(idx, TRACE_MEM_READ) + + def get_write_delta(self, idx): + """ + Return the memory write delta for the given timestamp. + """ + return self._get_mem_delta(idx, TRACE_MEM_WRITE) + + def _get_mem_delta(self, idx, mem_type): + """ + Internal abstraction to search memory delta lists. + """ + relative_idx = idx - self.base_idx + found, offset = [], 0 + + if mem_type == TRACE_MEM_WRITE: + idxs, addrs, masks, offsets, data = self.write_idxs, self.write_addrs, self.write_masks, self.write_offsets, self.write_data + else: + idxs, addrs, masks, offsets, data = self.read_idxs, self.read_addrs, self.read_masks, self.read_offsets, self.read_data + + try: + i = idxs.index(relative_idx) + except ValueError: + return [] + + while i < len(idxs) and idxs[i] == relative_idx: + + # + # fetch the 'aligned' address for this memory access, and the + # mask which specifes which bytes were touched starting from + # the aligned address + # + + aligned_address = self.trace.mem_addrs[addrs[i]] + access_mask = masks[i] + + # extract the raw data for this memory access + offset = offsets[i] + length = number_of_bits_set(masks[i]) + raw_data = data[offset:offset+length] + + address = aligned_address + seen_byte = False # TODO KLUDGE + while access_mask: + if access_mask & 1 == 0: + address += 1 + assert not seen_byte, "gap in memory access?" + else: + seen_byte = True + access_mask >>= 1 + + found.append((address, raw_data)) + i += 1 + + # return all the hits + return found + + def get_reg_info(self, idx, reg_names): + """ + Given a starting timestamp and a list of register names, return + + { reg_name: (value, idx) } + + ... for each discoverable register in this segment. + + """ + relative_idx = idx - self.base_idx + start_idx = relative_idx + 1 + if not (0 <= relative_idx < self.length): + return {} + + # compute a 32bit mask of the registers we need to find + target_mask = self._regs2mask(reg_names) + + # + # fetch the closest cached register data offset that we can start from + # for computing precisely where we should be working backwards from + # + + cache_index = int(start_idx / REG_OFFSET_CACHE_INTERVAL) + cache_offset = self.reg_offsets[cache_index] + cache_idx = cache_index * REG_OFFSET_CACHE_INTERVAL + + # alias for faster access / readability + sizes = self.trace.mask_sizes + masks = self.trace.masks + + # compute the current 'offset' in the reg data that we will work back from + offset_masks = self.reg_masks[cache_idx:start_idx][::-1] + offset = cache_offset + sum([sizes[mask_id] for mask_id in offset_masks]) + + # the map of reg_name --> (reg_value, src_idx) to return + found_registers = {} + + # loop backwards through the segment, starting from the given idx + search_masks = self.reg_masks[:start_idx][::-1] + #offset_slow = sum([sizes[mask_id] for mask_id in search_masks]) + #assert offset == offset_slow + for i, mask_id in enumerate(search_masks): + + # translate the mask id for this step into its register bitfield + current_mask = masks[mask_id] + + # + # since we are iterating backwards through the register data, we + # need to subtract from the offset immediately as it is pointing + # at the end of the register data for this mask. + # + + offset -= sizes[mask_id] + + # ignore masks that do not touch the target registers + if not current_mask & target_mask: + continue + + # translate the 32bit reg mask into a list of register names + found_mask = current_mask & target_mask + found_names = self._mask2regs(found_mask) + + # fetch the registers for this delta / timestamp + registers = self._unpack_registers(current_mask, offset) + + # add the found register names and the current (global) idx + for reg_name in found_names: + found_registers[reg_name] = (registers[reg_name], (self.base_idx + (start_idx - i))) + + # remove the registers we found from the remaining search space + target_mask ^= found_mask + + # if target_mask is 0, then there are no more registers to look for + if not target_mask: + break + + return found_registers + + def get_mem_data(self, mem_id, set_id, data_mask): + """ + Return the data for a given mem access id, in the given set. + """ + + if set_id == 1: + addrs, masks, offsets, data = self.write_addrs, self.write_masks, self.write_offsets, self.write_data + else: + addrs, masks, offsets, data = self.read_addrs, self.read_masks, self.read_offsets, self.read_data + + offset = offsets[mem_id] #sum([number_of_bits_set(mask) for mask in masks[:mem_id]]) + #offset = sum([number_of_bits_set(mask) for mask in masks[:mem_id]]) + length = number_of_bits_set(masks[mem_id]) + raw_data = data[offset:offset+length] + + address = self.trace.mem_addrs[addrs[mem_id]] + output = TraceMemory(address, 8) + + byte, i = 0, 0 + + while data_mask: + if data_mask & 1: + output.data[i] = raw_data[byte] + output.mask[i] = 0xFF + byte += 1 + i += 1 + data_mask >>= 1 + + #assert byte == length + + return output + + #------------------------------------------------------------------------- + # Finalization + #------------------------------------------------------------------------- + + def load(self, f): + """ + Load the trace segment from the given filestream. + """ + info = SegmentInfo() + f.readinto(info) + + self.id = info.id + self.base_idx = info.base_idx + self.length = info.length + + if info.ip_num == 0: + raise ValueError("Empty trace file (ip_num == 0)") + + ip_itemsize = info.ip_length // info.ip_num + ip_type = type_from_width(ip_itemsize) + + # load the ip trace + self.ips = array.array(ip_type) + self.ips.fromfile(f, info.ip_num) + + # load the reg mask data + reg_mask_type = type_from_width(info.reg_mask_length // info.reg_mask_num) + self.reg_masks = array.array(reg_mask_type) + self.reg_masks.fromfile(f, info.reg_mask_num) + + # load the reg data + self.reg_data = bytearray(info.reg_data_length) + f.readinto(self.reg_data) + + # load the pre-computed register offsets + self.reg_offsets = array.array("I") + self.reg_offsets.fromfile(f, REG_OFFSET_CACHE_SIZE) + + # + # memory + # + + idx_type = self.trace.mem_idx_type + addr_type = self.trace.mem_addr_type + + # load the memory read metadata + self.read_idxs = array.array(idx_type) + self.read_idxs.fromfile(f, info.mem_read_num) + self.read_addrs = array.array(addr_type) + self.read_addrs.fromfile(f, info.mem_read_num) + self.read_masks = array.array('B') + self.read_masks.fromfile(f, info.mem_read_num) + + # load the raw memory read data + self.read_data = bytearray(info.mem_read_data_length) + f.readinto(self.read_data) + + # load the memory write metadata + self.write_idxs = array.array(idx_type) + self.write_idxs.fromfile(f, info.mem_write_num) + self.write_addrs = array.array(addr_type) + self.write_addrs.fromfile(f, info.mem_write_num) + self.write_masks = array.array('B') + self.write_masks.fromfile(f, info.mem_write_num) + + # load the raw memory write data + self.write_data = bytearray(info.mem_write_data_length) + f.readinto(self.write_data) + + # load the mem delta / 'snapshot' data + addr_set = sorted(set(self.read_addrs + self.write_addrs)) + delta_entries = (MemValue * len(addr_set))() + f.readinto(delta_entries) + + self.mem_delta = dict(zip(addr_set, delta_entries)) + + self._compute_mem_offsets() + + def dump(self, f): + """ + Dump the trace segment to the given filestream. + """ + info = SegmentInfo() + + info.id = self.id + info.base_idx = self.base_idx + info.length = self.length + + info.ip_num = self.length + info.ip_length = info.ip_num * self.ips.itemsize + + info.reg_mask_num = len(self.reg_masks) + info.reg_mask_length = info.reg_mask_num * self.reg_masks.itemsize + info.reg_data_length = len(self.reg_data) # bytearray + + info.mem_read_num = len(self.read_idxs) + info.mem_read_data_length = len(self.read_data) + + info.mem_write_num = len(self.write_idxs) + info.mem_write_data_length = len(self.write_data) + + f.write(bytearray(info)) + f.write(bytearray(self.ips)) + + f.write(bytearray(self.reg_masks)) + f.write(self.reg_data) + f.write(bytearray(self.reg_offsets)) + + self.read_idxs.tofile(f) + self.read_addrs.tofile(f) + self.read_masks.tofile(f) + f.write(self.read_data) + + self.write_idxs.tofile(f) + self.write_addrs.tofile(f) + self.write_masks.tofile(f) + f.write(self.write_data) + + for mapped_address in sorted(set(self.read_addrs + self.write_addrs)): + f.write(bytearray(self.mem_delta[mapped_address])) + + #------------------------------------------------------------------------- + # Finalization + #------------------------------------------------------------------------- + + def finalize(self, remapped_ip, remapped_mem): + """ + Bake the trace segment into its final, packed form. + """ + self._finalize_registers(remapped_ip) + self._finalize_memory(remapped_mem) + + def _finalize_registers(self, remapped_ip): + """ + Bake registers into ctype structures. + """ + assert len(remapped_ip) <= UINT_MAX + assert len(self.trace.mask2mapped) <= USHRT_MAX + + # + # pack IP trace + # + + ip_type = type_from_limit(len(remapped_ip)) + new_ips = array.array(ip_type, [0] * len(self.ips)) + + for i, mapped_ip in enumerate(self.ips): + new_ips[i] = remapped_ip[mapped_ip] + + del self.ips + self.ips = new_ips + + # + # pack register masks + # + + mask_type = type_from_limit(len(self.trace.mask2mapped)) + new_masks = array.array(mask_type, self.reg_masks) + + del self.reg_masks + self.reg_masks = new_masks + + def _finalize_memory(self, remapped_mem): + """ + Bake memory into ctype structures. + """ + idx_type = self.trace.mem_idx_type + addr_type = self.trace.mem_addr_type + + # + # pack read data + # + + # allocate fast, compact python arrays to hold our mem read info + read_idxs = array.array(idx_type) + read_addrs = array.array(addr_type) + read_masks = array.array('B') + + # transfer read metadata into compact / searchable arrays + for entry in self._mem_read_info: + idx, old_mapped_address, mask = entry + + # convert the old mapped address to a new mapped address + mapped_address = remapped_mem[old_mapped_address] + + # pack the data into fast / compact python arrays + read_idxs.append(idx) + read_addrs.append(mapped_address) + read_masks.append(mask) + + del self._mem_read_info + self.read_idxs = read_idxs + self.read_addrs = read_addrs + self.read_masks = read_masks + + # + # pack write data + # + + # allocate fast, compact python arrays to hold our mem write info + write_idxs = array.array(idx_type) + write_addrs = array.array(addr_type) + write_masks = array.array('B') + + # transfer write metadata into compact / searchable arrays + for entry in self._mem_write_info: + idx, old_mapped_address, mask = entry + + # convert the old mapped address to a new mapped address + mapped_address = remapped_mem[old_mapped_address] + + # pack the data into fast / compact python arrays + write_idxs.append(idx) + write_addrs.append(mapped_address) + write_masks.append(mask) + + del self._mem_write_info + self.write_idxs = write_idxs + self.write_addrs = write_addrs + self.write_masks = write_masks + + # + # build trace mask + # + + new_delta = {} + mem_masks = self.trace.mem_masks + + for old_mapped_address, mv in self.mem_delta.items(): + mapped_address = remapped_mem[old_mapped_address] + new_delta[mapped_address] = mv + mem_masks[mapped_address] |= mv.mask + + del self.mem_delta + self.mem_delta = new_delta + + self._compute_mem_offsets() + + def _compute_mem_offsets(self): + """ + Pre-compute the offset of each memory access into the raw memory blobs. + """ + temp_sizes = {} + + self.read_offsets = array.array('I', [0] * len(self.read_masks)) + self.write_offsets = array.array('I', [0] * len(self.write_masks)) + + mem_sets = [ + (self.read_offsets, self.read_masks), + (self.write_offsets, self.write_masks) + ] + + for offsets, masks in mem_sets: + offset = 0 + for i, mask in enumerate(masks): + offsets[i] = offset + length = temp_sizes.setdefault(mask, number_of_bits_set(mask)) + offset += length + + #------------------------------------------------------------------------- + # Processing / Logic + #------------------------------------------------------------------------- + + def _process_lines(self, lines): + """ + Process text lines from a delta reg/mem trace. + """ + IP = self.trace.arch.IP + REGISTERS = self.trace.arch.REGISTERS + + relative_idx = 0 + + try: + + for line in lines: + if not self._process_line(line, relative_idx): + continue + relative_idx += 1 + + # TODO: pretty gross, but let's just wrap it to make these issues more apparents + except Exception as e: + pmsg(f"LINE PARSE FAILED, line ~{self.base_idx+relative_idx:,}, contents '{line}'") + pmsg(str(e)) + + self.reg_data = bytearray(self.reg_data[:self._reg_offset]) + self.ips = self.ips[:relative_idx] + self.length = relative_idx + + def _process_line(self, line, relative_idx): + """ + Process one line of text from a delta reg/mem trace. + """ + IP = self.trace.arch.IP + REGISTERS = self.trace.arch.REGISTERS + + delta = line.split(",") + registers = {} + + # split the state info (registers, memory) into individual items to process + for item in delta: + name, value = item.split("=") + name = name.upper() + + # special compression of IP + if name == IP: + ip = int(value, 16) + + try: + mapped_ip = self.trace.ip_map[ip] + + except KeyError: + mapped_ip = len(self.trace.ip_map) + self.trace.ip_map[ip] = mapped_ip + + self.ips[relative_idx] = mapped_ip + + # GPR + elif name in REGISTERS: + registers[name] = int(value, 16) + + # handle memory r/w/rw access + elif name in ["MR", "MW", "MRW"]: + + # + # a single line can contain multiple memory entries of the same + # type. they will be delimited by a ';' + # + # eg: mr=ADDRESS:DATA;ADDRESS:DATA;... + # + + for entry in value.split(';'): + address, hex_data = entry.split(":") + address = int(address, 16) + hex_data = bytes(hex_data.strip(), 'utf-8') + data = binascii.unhexlify(hex_data) + self._process_mem_entry(address, data, name, relative_idx) + + else: + raise ValueError(f"Invalid line in text trace! '{line}' error on '{name}', (value '{value}')") + + self._pack_registers(registers, relative_idx) + + return True + + def _process_mem_entry(self, address, data, access_type, relative_idx): + """ + TODO + """ + + byte = 0 + for mapped_address, access_mask, access_data in self._map_mem_access(address, data): + + # read + if access_type == 'MR': + + self._mem_read_info.append((relative_idx, mapped_address, access_mask)) + self.read_data += access_data + #self._max_read_size = max(self._max_read_size, data_len) + + # write + elif access_type == 'MW': + self._mem_write_info.append((relative_idx, mapped_address, access_mask)) + self.write_data += access_data + #print(self._mem_write_info[-1], hexdump(data), "REAL OFFSET", len(self.write_data)-len(data)) + #self._max_write_size = max(self._max_write_size, data_len) + + # read AND write (eg, inc [rax]) + elif access_type == 'MRW': + + # read + self._mem_read_info.append((relative_idx, mapped_address, access_mask)) + self.read_data += access_data + #self._max_read_size = max(self._max_read_size, data_len) + + # write + self._mem_write_info.append((relative_idx, mapped_address, access_mask)) + self.write_data += access_data + #self._max_write_size = max(self._max_write_size, data_len) + + else: + raise ValueError("Unknown field in trace: '%s=...'" % access_type) + + mv = self.mem_delta[mapped_address] + mv.mask |= access_mask + #print(f"ADDRESS: 0x{address:08X} MASK: {access_mask:02X}") + + # snapshot stuff + bit, byte = 0, 0 + while access_mask: + if access_mask & 1: + #print(bit, byte) + mv.value[bit] = access_data[byte] + #byte_shift = (bit * 8) + #byte_mask = 0xFF << byte_shift + #value[0] = (value[0] & ~byte_mask) | (data[byte] << byte_shift) + byte += 1 + access_mask >>= 1 + bit += 1 + + def _map_mem_access(self, address, data): + """ + TODO: lol welcome to hell :^) + """ + output = [] + data_len = len(data) + access_data = data + + mask_offset = address % 8 + remaining_mask = ((1 << data_len) - 1) << mask_offset + aligned_address = ((address >> 3) << 3) + access_length = min(len(access_data), (8 - mask_offset)) + + while remaining_mask: + + aligned_mask = remaining_mask & 0xFF + + mapped_address = self.trace.mem_map.setdefault(aligned_address, len(self.trace.mem_map)) + + output.append((mapped_address, aligned_mask, access_data[:access_length])) + access_data = access_data[access_length:] + + remaining_mask >>= 8 + aligned_address += 8 + access_length = min(len(access_data), 8) + + return output + + def _pack_registers(self, registers, relative_idx): + """ + Compress a register delta. + """ + num_regs = len(registers) + + # + # to help improve the speed of looking up register values in the data + # blob, we cache pre-computed offsets at finxed intervals throughout + # the segment. + # + # at query time, we can pick the closest cached interval prior to the + # target idx and only re-compute a fraction of the offsets needed to + # find the correct offset into the data blob to fetch our reg delta + # + + if not(relative_idx % REG_OFFSET_CACHE_INTERVAL): + cache_index = int(relative_idx / REG_OFFSET_CACHE_INTERVAL) + #print(f"rIDX: {relative_idx:,} CACHE: {cache_index} LEN: {len(self.reg_offsets)}") + self.reg_offsets[cache_index] = self._reg_offset + + # + # XXX/TODO: BODGE FOR WHEN PEOPLE DON'T DUMP A FULL REGISTER STATE + # + + if self.base_idx == 0 and self._reg_offset == 0: + if num_regs != len(self.arch.REGISTERS): + for reg_name in self.arch.REGISTERS: + if reg_name not in registers: + if reg_name == self.arch.IP: + continue + pmsg(f"MISSING INITIAL REGISTER VALUE FOR {reg_name}") + registers[reg_name] = 0 + num_regs += 1 + + mask = self._regs2mask(registers.keys()) + + try: + mapped_mask = self.trace.mask2mapped[mask] + except KeyError: + mapped_mask = len(self.trace.mask2mapped) + self.trace.mask2mapped[mask] = mapped_mask + self.trace.masks.append(mask) + + self.reg_masks[relative_idx] = mapped_mask + + value_pairs = sorted([(self.arch.REGISTERS.index(name), value) for name, value in registers.items()]) + values = [x[1] for x in value_pairs] + pack_fmt = 'Q' if self.arch.POINTER_SIZE == 8 else 'I' + struct.pack_into(pack_fmt * num_regs, self.reg_data, self._reg_offset, *values) + self._reg_offset += num_regs * self.arch.POINTER_SIZE + + def _unpack_registers(self, mask, offset): + """ + Unpack register data from the register buffer. + """ + reg_names = self._mask2regs(mask) + + # fetch the register data + num_regs = len(reg_names) + reg_data = self.reg_data[offset:offset + (num_regs * self.arch.POINTER_SIZE)] + + # unpack the register data + pack_fmt = 'Q' if self.arch.POINTER_SIZE == 8 else 'I' + reg_values = struct.unpack(pack_fmt * num_regs, reg_data) + + # pack all the registers into a dict that will be returned to the user + registers = dict(zip(reg_names, reg_values)) + + # return the completed register delta + return registers + + #------------------------------------------------------------------------- + # Util + #------------------------------------------------------------------------- + + def _regs2mask(self, regs): + """ + Convert a list of register names to a register mask. + """ + mask = 0 + for reg in regs: + reg_bit_index = self.arch.REGISTERS.index(reg) + mask |= 1 << reg_bit_index + return mask + + def _mask2regs(self, mask): + """ + Convert a register mask to a list of register names. + """ + regs, bit_index = [], 0 + while mask: + if mask & 1: + regs.append(self.arch.REGISTERS[bit_index]) + mask >>= 1 + bit_index += 1 + return regs diff --git a/plugins_sogen-support/tenet/trace/reader.py b/plugins_sogen-support/tenet/trace/reader.py new file mode 100644 index 0000000..90ef9d6 --- /dev/null +++ b/plugins_sogen-support/tenet/trace/reader.py @@ -0,0 +1,1947 @@ +import bisect +import struct +import logging + +from tenet.types import BreakpointType +from tenet.util.log import pmsg +from tenet.util.misc import register_callback, notify_callback +from tenet.trace.file import TraceFile +from tenet.trace.types import TraceMemory +from tenet.trace.analysis import TraceAnalysis + +logger = logging.getLogger("Tenet.Trace.Reader") + +#----------------------------------------------------------------------------- +# reader.py -- Trace Reader +#----------------------------------------------------------------------------- +# +# NOTE/PREFACE: If you have not already, please read through the overview +# comment at the start of the TraceFile (file.py) code. This file (the +# Trace Reader) builds directly ontop of trace files. +# +# -------------- +# +# This file contains the 'trace reader' implementation for the plugin. It +# is responsible for the navigating a loaded trace file, providing 'high +# level' APIs one might expect to 'efficiently' query a program for +# registers or memory at any timestamp of execution. +# +# Please be mindful that like the TraceFile implementation, TraceReader +# should be re-written entirely in a native language. Under the hood, it's +# not exactly pretty. It was written to make the plugin simple to install +# and experience as a prototype. It is not equipped to adequately scale to +# real world targets. +# +# The most important takeaway from this file should be interface / API +# that it exposes to the plugin. A performant, native TraceReader that +# exposes the same API would be enough to scale the plugin's ability to +# navigate traces that span tens of billions (... maybe even hundreds of +# billions) of instructions. +# + +class TraceDelta(object): + """ + Trace Delta + """ + + def __init__(self, registers, mem_read, mem_write): + self.registers = registers + self.mem_reads = mem_read + self.mem_writes = mem_write + +class TraceReader(object): + """ + A high level, debugger-like interface for querying Tenet traces. + """ + + def __init__(self, filepath, architecture, dctx=None): + self.idx = 0 + self.dctx = dctx + self.arch = architecture + + # load the given trace file from disk + self.trace = TraceFile(filepath, architecture) + self.analysis = TraceAnalysis(self.trace, dctx) + + self._idx_cached_registers = -1 + self._cached_registers = {} + + #---------------------------------------------------------------------- + # Callbacks + #---------------------------------------------------------------------- + + self._idx_changed_callbacks = [] + + #------------------------------------------------------------------------- + # Trace Properties + #------------------------------------------------------------------------- + + @property + def ip(self): + """ + Return the current instruction pointer. + """ + return self.get_register(self.arch.IP) + + @property + def rebased_ip(self): + """ + Return a rebased version of the current instruction pointer (if available). + """ + return self.analysis.rebase_pointer(self.ip) + + @property + def sp(self): + """ + Return the current stack pointer. + """ + return self.get_register(self.arch.SP) + + @property + def registers(self): + """ + Return the current registers. + """ + return self.get_registers() + + @property + def segment(self): + """ + Return the current trace segment. + """ + return self.trace.get_segment(self.idx) + + @property + def delta(self): + """ + Return the state delta since the previous timestamp. + """ + read_set, write_set = set(), set() + + for address, data in self.trace.get_read_delta(self.idx): + read_set |= {address + i for i in range(len(data))} + + for address, data in self.trace.get_write_delta(self.idx): + write_set |= {address + i for i in range(len(data))} + + regs = self.trace.get_reg_delta(self.idx) + + return TraceDelta(regs, read_set, write_set) + + #------------------------------------------------------------------------- + # Trace Navigation + #------------------------------------------------------------------------- + + def seek(self, idx): + """ + Seek the trace to the given timestamp. + """ + + # clamp the index if it goes past the end of the trace + if idx >= self.trace.length: + idx = self.trace.length - 1 + elif idx < 0: + idx = 0 + + # save the new position + self.idx = idx + self.get_registers() + self._notify_idx_changed() + + def seek_percent(self, percent): + """ + Seek to an approximate percentage into the trace. + """ + target_idx = int(self.trace.length * (percent / 100)) + self.seek(target_idx) + + def seek_to_first(self, address, access_type, length=1): + """ + Seek to the first instance of the given breakpoint. + + Returns True on success, False otherwise. + """ + return self.seek_to_next(address, access_type, length, 0) + + def seek_to_final(self, address, access_type, length=1): + """ + Seek to the final instance of the given breakpoint. + + Returns True on success, False otherwise. + """ + return self.seek_to_prev(address, access_type, length, self.trace.length-1) + + def seek_to_next(self, address, access_type, length=1, start_idx=None): + """ + Seek to the next instance of the given breakpoint. + + Returns True on success, False otherwise. + """ + if start_idx is None: + start_idx = self.idx + 1 + + if access_type == BreakpointType.EXEC: + + assert length == 1 + idx = self.find_next_execution(address, start_idx) + + elif access_type == BreakpointType.READ: + + if length == 1: + idx = self.find_next_read(address, start_idx) + else: + idx = self.find_next_region_read(address, length, start_idx) + + elif access_type == BreakpointType.WRITE: + + if length == 1: + idx = self.find_next_write(address, start_idx) + else: + idx = self.find_next_region_write(address, length, start_idx) + + elif access_type == BreakpointType.ACCESS: + + if length == 1: + idx = self.find_next_access(address, start_idx) + else: + idx = self.find_next_region_access(address, length, start_idx) + + else: + raise NotImplementedError + + if idx == -1: + return False + + self.seek(idx) + return True + + def seek_to_prev(self, address, access_type, length=1, start_idx=None): + """ + Seek to the previous instance of the given breakpoint. + + Returns True on success, False otherwise. + """ + if start_idx is None: + start_idx = self.idx - 1 + + if access_type == BreakpointType.EXEC: + + assert length == 1 + idx = self.find_prev_execution(address, start_idx) + + elif access_type == BreakpointType.READ: + + if length == 1: + idx = self.find_prev_read(address, start_idx) + else: + idx = self.find_prev_region_read(address, length, start_idx) + + elif access_type == BreakpointType.WRITE: + + if length == 1: + idx = self.find_prev_write(address, start_idx) + else: + idx = self.find_prev_region_write(address, length, start_idx) + + elif access_type == BreakpointType.ACCESS: + + if length == 1: + idx = self.find_prev_access(address, start_idx) + else: + idx = self.find_prev_region_access(address, length, start_idx) + + else: + raise NotImplementedError + + if idx == -1: + return False + + self.seek(idx) + return True + + def step_forward(self, n=1, step_over=False): + """ + Step the trace forward by n steps. + + If step_over=True, and a disassembler context is available to the + trace reader, it will attempt to step over calls while stepping. + """ + if not step_over: + self.seek(self.idx + n) + else: + self._step_over_forward(n) + + def step_backward(self, n=1, step_over=False): + """ + Step the trace backwards. + + If step_over=True, and a disassembler context is available to the + trace reader, it will attempt to step over calls while stepping. + """ + if not step_over: + self.seek(self.idx - n) + else: + self._step_over_backward(n) + + def _step_over_forward(self, n): + """ + Step the trace forward over n instructions / calls. + """ + address = self.get_ip(self.idx) + bin_address = self.analysis.rebase_pointer(address) + + # + # get the address for the linear instruction address after the + # current instruction + # + + bin_next_address = self.dctx.get_next_insn(bin_address) + if bin_next_address == -1: + self.seek(self.idx + 1) + return + + trace_next_address = self.analysis.rebase_pointer(bin_next_address) + + # + # find the next time the instruction after this instruction is + # executed in the trace + # + + next_idx = self.find_next_execution(trace_next_address, self.idx) + + # + # the instruction after the call does not appear in the trace, + # so just fall-back to 'step into' behavior + # + + if next_idx == -1: + self.seek(self.idx + 1) + return + + self.seek(next_idx) + + def _step_over_backward(self, n): + """ + Step the trace backward over n instructions / calls. + """ + address = self.get_ip(self.idx) + bin_address = self.analysis.rebase_pointer(address) + + bin_prev_address = self.dctx.get_prev_insn(bin_address) + + # + # could not get the address of the instruction prior to the current + # one which means we will not be able to decode it / and really are + # not sure what/where the user would be stepping backwards to... + # + # TODO: it's possible to handle this case, but requires a more + # performant backend than the python prototype that powers this + # + + if bin_prev_address == -1: + self.seek(self.idx - 1) + return + + # + # special handling for when the prior instruction appears to be a call + # instruction, this is perhaps the most important 'step over' scenario + # and also pretty tricky to handle... + # + + if self.dctx.is_call_insn(bin_prev_address): + + # get the previous stack pointer address + sp = self.get_register(self.arch.SP, self.idx - 1) + + # attempt to read a pointer off the stack (possibly a ret address) + try: + maybe_ret_address = self.read_pointer(sp, self.idx) + except ValueError: + print("TODO: stack read failed") + maybe_ret_address = None + + # + # if the address off the stack matches the current address, + # we can assume that we just returned from somewhere. + # + # 99% of the time, this will have been from the call insn at + # prev_address, so let's just assume that is the case and + # 'reverse step over' onto that. + # + # NOTE: technically, we can put in more checks and stuff to + # try and ensure this is 'correct' but, step over and reverse + # step over are kind of an imperfect science as is... + # + + if maybe_ret_address != address: + self.seek(self.idx - 1) + return + + trace_prev_address = self.analysis.rebase_pointer(bin_prev_address) + + prev_idx = self.find_prev_execution(trace_prev_address, self.idx) + if prev_idx == -1: + self.seek(self.idx - 1) + return + + self.seek(prev_idx) + + #------------------------------------------------------------------------- + # Timestamp API + #------------------------------------------------------------------------- + + # + # in this section, you will find references to 'resolution'. this is a + # knob that the trace reader uses to fetch 'approximate' results from + # the underlying trace. + # + # for example, a resolution of 1 is the *most* granular request, where + # one can ask the reader to inspect each step of the trace to see if it + # matches a query (eg, 'when was this instruction address executed') + # + # in contrast, a resolution of 10_000 means that any single hit within + # a resolution 'window' is adequate, and the reader should skip to the + # next window to continue fufilling the query. + # + # given a 10 million instruction trace, and a 30px by 1000px image + # buffer to viualize said trace... there is very little reason to fetch + # 100_000 unique timestamps that all fall within one vertical pixel of + # the rendered visualization. + # + # instead, we can search the trace in arbitrary resolution 'windows' of + # roughly 1px (pixel resolution can be calculated based on the length of + # the trace execution vs the length of the viz in pixels) and fetch results + # that will suffice for visual summarization of trace execution + # + + def get_executions(self, address, resolution=1): + """ + Return a list of timestamps (idx) that executed the given address. + """ + return self.get_executions_between(address, 0, self.trace.length, resolution) + + def get_executions_between(self, address, start_idx, end_idx, resolution=1): + """ + Return a list of timestamps (idx) that executed the given address, in the given slice. + """ + assert 0 <= start_idx <= end_idx, f"0 <= {start_idx:,} <= {end_idx:,}" + assert resolution > 0 + + resolution = max(1, resolution) + #logger.debug(f"Fetching executions from {start_idx:,} --> {end_idx:,} (res {resolution:0.2f}, normalized {resolution:0.2f}) for address 0x{address:08X}") + + try: + mapped_address = self.trace.get_mapped_ip(address) + except ValueError: + return [] + + output = [] + idx = max(0, start_idx) + end_idx = min(end_idx, self.trace.length) + + while idx < end_idx: + + # fetch a segment to search forward through + seg = self.trace.get_segment(idx) + seg_base = seg.base_idx + + # clamp the segment end if it extends past our segment + seg_end = min(seg_base + seg.length, end_idx) + #logger.debug(f"Searching seg #{seg.id}, {seg_base:,} --> {seg_end:,}") + + # snip the segment to start from the given global idx + relative_idx = idx - seg_base + seg_ips = seg.ips[relative_idx:] + + while idx < seg_end: + + try: + idx_offset = seg_ips.index(mapped_address) + except ValueError: + idx = seg_end + 1 + break + + # we got a hit within the resolution window, save it + current_idx = idx + idx_offset + output.append(current_idx) + + # now skip to the next resolution window + current_resolution_index = current_idx / resolution + next_resolution_index = current_resolution_index + 1 + next_resolution_target = next_resolution_index * resolution + idx = round(next_resolution_target) + + #print(f"GOT HIT @ {current_idx:,}, skipping to {idx:,} (y = {current_idx/resolution})") + #print(f" - Current resolution index {current_resolution_index}") + #print(f" - Next resolution index {next_resolution_index}") + #print(f" - Next resolution target {next_resolution_target:,}") + + seg_ips = seg.ips[idx-seg_base:] + + #logger.debug(f"Returning hits {output}") + return output + + def get_memory_accesses(self, address, resolution=1): + """ + Return a tuple of lists (read, write) containing timestamps that access a given memory address. + """ + return self.get_memory_accesses_between(address, 0, self.trace.length, resolution) + + + def get_memory_reads_between(self, address, start_idx, end_idx, resolution=1): + """ + Return a list of timestamps that read from a given memory address in the given slice. + """ + reads, _ = self.get_memory_accesses_between(address, start_idx, end_idx, resolution, BreakpointType.READ) + return reads + + def get_memory_writes_between(self, address, start_idx, end_idx, resolution=1): + """ + Return a list of timestamps that write to a given memory address in the given slice. + """ + _, writes = self.get_memory_accesses_between(address, start_idx, end_idx, resolution, BreakpointType.WRITE) + return writes + + def get_memory_accesses_between(self, address, start_idx, end_idx, resolution=1, access_type=BreakpointType.ACCESS): + """ + Return a tuple of lists (read, write) containing timestamps that access a given memory address in the given slice. + """ + assert resolution > 0 + resolution = max(1, resolution) + + #logger.debug(f"MEMORY ACCESSES @ 0x{address:08X} // {start_idx:,} --> {end_idx:,} (rez {resolution:0.2f})") + + mapped_address = self.trace.get_mapped_address(address) + if mapped_address == -1: + return ([], []) + + reads, writes = [], [] + access_mask = self.trace.get_aligned_address_mask(address, 1) + + # clamp the search incase the given params are a bit wonky + idx = max(0, start_idx) + end_idx = min(end_idx, self.trace.length) + assert idx < end_idx + + next_resolution = [idx, idx] + + # search through the trace + while idx < end_idx: + + # fetch a segment to search forward through + seg = self.trace.get_segment(idx) + seg_base = seg.base_idx + + # clamp the segment end if it extends past our segment + seg_end = min(seg_base + seg.length, end_idx) + #logger.debug(f"seg #{seg.id}, {seg.base_idx:,} --> {seg.base_idx+seg.length:,} -- IDX PTR {idx:,}") + + mem_sets = [] + + if access_type & BreakpointType.READ: + mem_sets.append((seg.read_idxs, seg.read_addrs, seg.read_masks, reads)) + if access_type & BreakpointType.WRITE: + mem_sets.append((seg.write_idxs, seg.write_addrs, seg.write_masks, writes)) + + for i, mem_type in enumerate(mem_sets): + idxs, addrs, masks, output = mem_type + + cumulative_index = 0 + current_target = next_resolution[i] + + while current_target < seg_end: + + try: + index = addrs.index(mapped_address) + except ValueError: + break + + cumulative_index += index + current_idx = seg_base + idxs[index] + + # + # there was a hit to the mapped address, which is aligned + # to the arch pointer size... check if the requested addr + # matches the access mask for this mem access entry + # + + if not (masks[cumulative_index] & access_mask): + addrs = addrs[index+1:] + idxs = idxs[index+1:] + cumulative_index += 1 + continue + + #print(f"FOUND ACCESS TO {self.trace.mem_addrs[mapped_address]:08X} (mask {masks[cumulative_index]:02X}), IDX {current_idx:,}") + + # we got a hit within the resolution window, save it + output.append(current_idx) + + # now skip to the next resolution window + current_resolution_index = current_idx / resolution + next_resolution_index = current_resolution_index + 1 + next_resolution_target = next_resolution_index * resolution + current_target = round(next_resolution_target) + #print(f"NEXT TARGET: {current_target:,}") + + # now skip to the next resolution window + skip_index = bisect.bisect_left(idxs, current_target - seg_base) + if skip_index == len(idxs): + break + + addrs = addrs[skip_index:] + idxs = idxs[skip_index:] + + cumulative_index += (skip_index - index) + + next_resolution[i] = current_target + + idx = seg_end + 1 + + return (reads, writes) + + def get_memory_region_reads(self, address, length, resolution=1): + """ + Return a list of timestamps that read from the given memory region. + """ + reads, _ = self.get_memory_region_accesses_between(address, length, 0, self.trace.length, resolution, BreakpointType.READ) + return reads + + def get_memory_region_reads_between(self, address, length, start_idx, end_idx, resolution=1): + """ + Return a list of timestamps that read from the given memory region in the given time slice. + """ + reads, _ = self.get_memory_region_accesses_between(address, length, start_idx, end_idx, resolution, BreakpointType.READ) + return reads + + def get_memory_region_writes(self, address, length, resolution=1): + """ + Return a list of timestamps that write to the given memory region. + """ + _, writes = self.get_memory_region_accesses_between(address, length, 0, self.trace.length, resolution, BreakpointType.WRITE) + return writes + + def get_memory_region_writes_between(self, address, length, start_idx, end_idx, resolution=1): + """ + Return a list of timestamps that write to the given memory region in the given time slice. + """ + _, writes = self.get_memory_region_accesses_between(address, length, start_idx, end_idx, resolution, BreakpointType.WRITE) + return writes + + def get_memory_region_accesses(self, address, length, resolution=1): + """ + Return a tuple of (read, write) containing timestamps that access the given memory region. + """ + return self.get_memory_region_accesses_between(address, length, 0, self.trace.length, resolution) + + def get_memory_region_accesses_between(self, address, length, start_idx, end_idx, resolution=1, access_type=BreakpointType.ACCESS): + """ + Return a tuple of (read, write) containing timestamps that access the given memory region in the given time slice. + """ + assert resolution > 0 + resolution = max(1, resolution) + + #logger.debug(f"REGION ACCESS BETWEEN @ 0x{address:08X} + {length} // {start_idx:,} --> {end_idx:,} (rez {resolution:0.2f})") + + reads, writes = [], [] + targets = self._region_to_targets(address, length) + + # clamp the search incase the given params are a bit wonky + idx = max(0, start_idx) + end_idx = min(end_idx, self.trace.length) + assert idx < end_idx + + starting_resolution_index = int(idx / resolution) + next_resolution = [starting_resolution_index, starting_resolution_index] + + while idx < end_idx: + + # fetch a segment to search forward through + seg = self.trace.get_segment(idx) + seg_base = seg.base_idx + + # clamp the segment end if it extends past our segment + seg_end = min(seg_base + seg.length, end_idx) + + #print("-"*50) + #print(f"seg #{seg.id}, {seg.base_idx:,} --> {seg.base_idx+seg.length:,} -- IDX PTR {idx:,}") + + mem_sets = [] + + if access_type & BreakpointType.READ: + mem_sets.append((seg.read_idxs, seg.read_addrs, seg.read_masks, reads)) + if access_type & BreakpointType.WRITE: + mem_sets.append((seg.write_idxs, seg.write_addrs, seg.write_masks, writes)) + + for i, mem_type in enumerate(mem_sets): + idxs, addrs, masks, output = mem_type + hits, first_hit = {}, len(addrs) + resolution_index = next_resolution[i] + + # + # check each 'aligned address' (actually an id #) within the given region to see + # if it appears anywhere in the current segment's memory set + # + + for address_id, address_mask in targets: + + # + # if there is a memory access to the region, we will + # break here and begin processing it + # + + try: + index = addrs.index(address_id) + first_hit = min(index, first_hit) + + # + # no hits for any bytes within this aligned address, + # try the next aligned address within the region + # + + except ValueError: + continue + + hits[address_id] = address_mask + + # + # if we hit this, it means no memory accesses of this + # type (eg, reads) occured to the region of memory in + # this segment. + # + # there's nothing else to process for this memory set, + # so just break and move onto the next set (eg, writes) + # + + if not hits: + continue + + for index in range(first_hit, len(addrs)): + address_id = addrs[index] + target_mask = hits.get(address_id, None) + + if not target_mask: + continue + + #print("CLOSE! DOES MASK MATCH?") + #print(f" TARGET: 0x{self.trace.mem_addrs[address_id]:08X} MASK: {target_mask:02X}") + #print(f" CURRENT: 0x{self.trace.mem_addrs[address_id]:08X} MASK: {masks[index]:02X}") + #print(f" RESULT: {target_mask & masks[index]:02X}") + + # + # got the first hit for this set.. great! save it and + # break to search the next memory set + # + + if target_mask & masks[index]: + hit_idx = seg_base + idxs[index] + hit_resolution_index = int(hit_idx / resolution) + if hit_resolution_index < resolution_index: + continue + output.append(hit_idx) + resolution_index += 1 + + next_resolution[i] = resolution_index + + idx = seg_end + 1 + + return (reads, writes) + + def get_prev_ips(self, n, step_over=False): + """ + Return the previous n executed instruction addresses. + + If step_over=True, and a disassembler context is available to the + trace reader, it will attempt to step over calls while stepping. + """ + + # single step, return (reverse) canonical trace sequence + if not step_over: + start = max(-1, self.idx - 1) + end = max(-1, start - n) + return [self.get_ip(idx) for idx in range(start, end, -1)] + + output = [] + dctx, idx = self.dctx, self.idx + trace_address = self.get_ip(idx) + bin_address = self.analysis.rebase_pointer(trace_address) + + # (reverse) step over any call instructions + while len(output) < n and idx > 0: + + bin_prev_address = dctx.get_prev_insn(bin_address) + did_step_over = False + + # call instruction + if bin_prev_address != -1 and dctx.is_call_insn(bin_prev_address): + + # get the previous stack pointer address + sp = self.get_register(self.arch.SP, idx - 1) + + # attempt to read a pointer off the stack (the old ret address) + try: + maybe_ret_address = self.read_pointer(sp, idx) + except ValueError: + print("TODO: stack read failed") + maybe_ret_address = None + + # + # if the address off the stack matches the current address, + # we can assume that we just returned from somewhere. + # + # 99% of the time, this will have been from the call insn at + # bin_prev_address, so let's just assume that is the case and + # 'reverse step over' onto that. + # + # NOTE: technically, we can put in more checks and stuff to + # try and ensure this is 'correct' but, step over and reverse + # step over are kind of an imperfect science as is... + # + + if maybe_ret_address == trace_address: + trace_prev_address = self.analysis.rebase_pointer(bin_prev_address) + prev_idx = self.find_prev_execution(trace_prev_address, idx) + did_step_over = bool(prev_idx != -1) + + # + # if it doesn't look like we just returned from a call, we + # will just fall back to a linear, step-over backwards. + # + # this code is intended to cover the case where a conditional + # happens to jump onto an instruction immediately after a call, + # which causes the above 'stack inspection' to fail + # + + if not did_step_over: + trace_prev_address = self.analysis.rebase_pointer(bin_prev_address) + prev_idx = self.find_prev_execution(trace_prev_address, idx) + + # + # uh, wow okay we're pretty lost and have no idea if there is + # actually something that can be reverse step-over'd. just revert + # to performing a simple single-step backwards + # + + if prev_idx == -1: + prev_idx = idx - 1 + + trace_prev_address = self.get_ip(prev_idx) + + # no address was returned, so the end of trace was reached + if trace_prev_address == -1: + break + + # save the results and continue looping + output.append(trace_prev_address) + trace_address = trace_prev_address + bin_address = self.analysis.rebase_pointer(trace_address) + idx = prev_idx + + # return the list of addresses to be 'executed' next + return output + + def get_next_ips(self, n, step_over=False): + """ + Return the next N executed instruction addresses. + + If step_over=True, and a disassembler context is available to the + trace reader, it will attempt to step over calls while stepping. + """ + + # single step, return canonical trace sequence + if not step_over: + start = min(self.idx + 1, self.trace.length) + end = min(start + n, self.trace.length) + return [self.get_ip(idx) for idx in range(start, end)] + + output = [] + dctx, idx = self.dctx, self.idx + trace_address = self.get_ip(idx) + bin_address = self.analysis.rebase_pointer(trace_address) + + # step over any call instructions + while len(output) < n and idx < (self.trace.length - 1): + + # + # get the address for the instruction address after the + # current (call) instruction + # + + bin_next_address = dctx.get_next_insn(bin_address) + + # + # find the next time the instruction after this instruction is + # executed in the trace + # + + if bin_next_address != -1: + trace_next_address = self.analysis.rebase_pointer(bin_next_address) + next_idx = self.find_next_execution(trace_next_address, idx) + else: + next_idx = -1 + + # + # the instruction after the call does not appear in the trace, + # so just fall-back to 'step into' behavior + # + + if next_idx == -1: + next_idx = idx + 1 + + # + # get the next address to be executed by the trace, according to + # our stepping behavior + # + + trace_next_address = self.get_ip(next_idx) + + # no address was returned, so the end of trace was reached + if trace_next_address == -1: + break + + # save the results and continue looping + output.append(trace_next_address) + bin_address = self.analysis.rebase_pointer(trace_next_address) + idx = next_idx + + # return the list of addresses to be 'executed' next + return output + + def find_next_execution(self, address, idx=None): + """ + Return the next timestamp to execute the given address. + """ + if idx is None: + idx = self.idx + 1 + + try: + mapped_ip = self.trace.get_mapped_ip(address) + except ValueError: + return -1 + + while idx < self.trace.length: + seg = self.trace.get_segment(idx) + + # slice out and reverse the ips to search through + relative_idx = idx - seg.base_idx + ips = seg.ips[relative_idx:] + + # query for the next instance of our target ip + try: + next_idx = ips.index(mapped_ip) + return idx + next_idx + + # no luck, move backwards to the next segment + except ValueError: + idx = seg.base_idx + seg.length + + # fail, reached start of trace + return -1 + + def find_prev_execution(self, address, idx=None): + """ + Return the previous timestamp to execute the given address. + """ + if idx is None: + idx = self.idx - 1 + + try: + mapped_ip = self.trace.get_mapped_ip(address) + except ValueError: + return -1 + + while idx > -1: + seg = self.trace.get_segment(idx) + + # slice out and reverse the ips to search through + relative_idx = idx - seg.base_idx + ips = seg.ips[:relative_idx][::-1] + + # query for the next instance of our target ip + try: + prev_idx = ips.index(mapped_ip) + return idx - prev_idx - 1 + + # no luck, move backwards to the next segment + except ValueError: + idx = seg.base_idx - 1 + + # fail, reached start of trace + return -1 + + def find_next_read(self, address, idx=None): + """ + Return the next timestamp to read the given memory address. + """ + return self._find_next_mem_op(address, BreakpointType.READ, idx) + + def find_prev_read(self, address, idx=None): + """ + Return the previous timestamp to read the given memory address. + """ + return self._find_prev_mem_op(address, BreakpointType.READ, idx) + + def find_next_write(self, address, idx=None): + """ + Return the next timestamp to write to the given memory address. + """ + return self._find_next_mem_op(address, BreakpointType.WRITE, idx) + + def find_prev_write(self, address, idx=None): + """ + Return the previous timestamp to write to the given memory address. + """ + return self._find_prev_mem_op(address, BreakpointType.WRITE, idx) + + def find_next_access(self, address, idx=None): + """ + Return the next timestamp to access the given memory address. + """ + return self._find_next_mem_op(address, BreakpointType.ACCESS, idx) + + def find_prev_access(self, address, idx=None): + """ + Return the previous timestamp to access the given memory address. + """ + return self._find_prev_mem_op(address, BreakpointType.ACCESS, idx) + + def _find_next_mem_op(self, address, bp_type, idx=None): + """ + Return the next timestamp to read the given memory address. + """ + if idx is None: + idx = self.idx + 1 + + mapped_address = self.trace.get_mapped_address(address) + if mapped_address == -1: + return -1 + + access_mask = self.trace.get_aligned_address_mask(address, 1) + starting_segment = self.trace.get_segment(idx) + + accesses, mem_sets = [], [] + + for seg_id in range(starting_segment.id, len(self.trace.segments)): + seg = self.trace.segments[seg_id] + seg_base = seg.base_idx + + mem_sets.clear() + + if bp_type == BreakpointType.READ: + mem_sets.append((seg.read_idxs, seg.read_addrs, seg.read_masks)) + + if bp_type == BreakpointType.WRITE: + mem_sets.append((seg.write_idxs, seg.write_addrs, seg.write_masks)) + + if bp_type == BreakpointType.ACCESS: + mem_sets.append((seg.read_idxs, seg.read_addrs, seg.read_masks)) + mem_sets.append((seg.write_idxs, seg.write_addrs, seg.write_masks)) + + # loop through the read / write memory sets for this segment + for idxs, addrs, masks in mem_sets: + search_addrs = addrs + + normal_index = 0 + while search_addrs: + + try: + index = search_addrs.index(mapped_address) + normal_index += index + except ValueError: + break + + if masks[normal_index] & access_mask: + + assert addrs[normal_index] == mapped_address + assert masks[normal_index] & access_mask + + # ensure that the memory access occurs on or after the starting idx + hit_idx = seg_base + idxs[normal_index] + if idx <= hit_idx: + accesses.append(seg_base + idxs[normal_index]) + break + + # the hit was no good.. 'step' past it and keep searching + search_addrs = search_addrs[index+1:] + normal_index += 1 + + # + # if there has been a read or a write, select the one that is + # 'closest' to our current idx. there should only be, at most, + # two elements in this list... + # + + if accesses: + return min(accesses, key=lambda x:abs(x-idx)) + + # fail, reached end of trace + return -1 + + def _find_prev_mem_op(self, address, bp_type, idx=None): + """ + Return the previous timestamp to access the given memory address. + """ + if idx is None: + idx = self.idx - 1 + + mapped_address = self.trace.get_mapped_address(address) + if mapped_address == -1: + return -1 + + access_mask = self.trace.get_aligned_address_mask(address, 1) + starting_segment = self.trace.get_segment(idx) + + accesses, mem_sets = [], [] + + for seg_id in range(starting_segment.id, -1, -1): + seg = self.trace.segments[seg_id] + seg_base = seg.base_idx + + mem_sets.clear() + + if bp_type == BreakpointType.READ: + mem_sets.append((seg.read_idxs, seg.read_addrs, seg.read_masks)) + + if bp_type == BreakpointType.WRITE: + mem_sets.append((seg.write_idxs, seg.write_addrs, seg.write_masks)) + + if bp_type == BreakpointType.ACCESS: + mem_sets.append((seg.read_idxs, seg.read_addrs, seg.read_masks)) + mem_sets.append((seg.write_idxs, seg.write_addrs, seg.write_masks)) + + # loop through the read / write memory sets for this segment + for idxs, addrs, masks in mem_sets: + search_addrs = addrs[::-1] + + normal_index = len(search_addrs) - 1 + while search_addrs: + + try: + reverse_index = search_addrs.index(mapped_address) + normal_index -= reverse_index + except ValueError: + break + + if masks[normal_index] & access_mask: + + assert addrs[normal_index] == mapped_address + assert masks[normal_index] & access_mask + + # ensure that the memory access occurs on or before the starting idx + hit_idx = seg_base + idxs[normal_index] + if hit_idx <= idx: + accesses.append(seg_base + idxs[normal_index]) + break + + # the hit was no good.. 'step' past it and keep searching + search_addrs = search_addrs[reverse_index+1:] + normal_index -= 1 + + if accesses: + return min(accesses, key=lambda x:abs(x-idx)) + + # fail, reached start of trace + return -1 + + def find_next_region_read(self, address, length, idx=None): + """ + Return the next timestamp to read from given memory region. + """ + return self._find_next_region_access(address, length, idx, BreakpointType.READ) + + def find_next_region_write(self, address, length, idx=None): + """ + Return the next timestamp to write to the given memory region. + """ + return self._find_next_region_access(address, length, idx, BreakpointType.WRITE) + + def find_next_region_access(self, address, length, idx=None): + """ + Return the next timestamp to access (r/w) the given memory region. + """ + return self._find_next_region_access(address, length, idx, BreakpointType.ACCESS) + + def _find_next_region_access(self, address, length, idx=None, access_type=BreakpointType.ACCESS): + """ + Return the next timestamp to access the given memory region. + """ + if idx is None: + idx = self.idx + 1 + + #logger.debug(f"FIND NEXT REGION ACCESS FOR 0x{address:08X} -> 0x{address+length:08X} STARTING AT IDX {idx:,}") + + accesses, mem_sets = [], [] + targets = self._region_to_targets(address, length) + starting_segment = self.trace.get_segment(idx) + + for seg_id in range(starting_segment.id, len(self.trace.segments)): + + # fetch a segment to search forward through + seg = self.trace.segments[seg_id] + seg_base = seg.base_idx + + mem_sets = [] + + if access_type & BreakpointType.READ: + mem_sets.append((seg.read_idxs, seg.read_addrs, seg.read_masks)) + if access_type & BreakpointType.WRITE: + mem_sets.append((seg.write_idxs, seg.write_addrs, seg.write_masks)) + + # loop through the read / write memory sets for this segment + for idxs, addrs, masks in mem_sets: + hits, first_hit = {}, len(addrs) + + # + # check each 'aligned address' (actually an id #) within + # the given region to see if it appears anywhere in the + # current segment's memory set + # + + for address_id, address_mask in targets: + + # + # if there is a memory access to the region, we will + # break here and begin processing it + # + + try: + index = addrs.index(address_id) + first_hit = min(index, first_hit) + #print(f"HIT ON 0x{self.trace.mem_addrs[address_id]:08X} @ IDX {seg_base+idxs[index]}") + + # + # no hits for any bytes within this aligned address, + # try the next aligned address within the region + # + + except ValueError: + continue + + hits[address_id] = address_mask + + # + # if we hit this, it means no memory accesses of this + # type (eg, reads) occured to the region of memory in + # this segment. + # + # there's nothing else to process for this memory set, + # so just break and move onto the next set (eg, writes) + # + + if not hits: + continue + + for index in range(first_hit, len(addrs)): + address_id = addrs[index] + target_mask = hits.get(address_id, None) + + if not target_mask: + continue + + #print("CLOSE! DOES MASK MATCH?") + #print(f" TARGET: 0x{self.trace.mem_addrs[address_id]:08X} MASK: {target_mask:02X}") + #print(f" CURRENT: 0x{self.trace.mem_addrs[address_id]:08X} MASK: {masks[index]:02X}") + #print(f" RESULT: {target_mask & masks[index]:02X}") + + # + # got the first hit for this set.. great! save it and + # break to search the next memory set + # + + if target_mask & masks[index]: + hit_idx = seg_base + idxs[index] + if hit_idx < idx: + continue + accesses.append(hit_idx) + #print(f"FOUND HIT AT IDX {hit_idx}") + break + + # + # if there has been a read or a write, select the one that is + # 'closest' to our current idx. there should only be, at most, + # two elements in this list... + # + + if accesses: + #print("ALL ACCESSES", accesses) + return min(accesses, key=lambda x:abs(x-idx)) + + # fail, reached end of trace + return -1 + + def find_prev_region_read(self, address, length, idx=None): + """ + Return the previous timestamp to read from the given memory region. + """ + return self.find_prev_region_access(address, length, idx, BreakpointType.READ) + + def find_prev_region_write(self, address, length, idx=None): + """ + Return the previous timestamp to write to the given memory region. + """ + return self.find_prev_region_access(address, length, idx, BreakpointType.WRITE) + + def find_prev_region_access(self, address, length, idx=None, access_type=BreakpointType.ACCESS): + """ + Return the previous timestamp to access the given memory region. + """ + if idx is None: + idx = self.idx - 1 + + #logger.debug(f"FIND PREV REGION ACCESS FOR 0x{address:08X} -> 0x{address+length:08X} STARTING AT IDX {idx:,}") + + accesses, mem_sets = [], [] + targets = self._region_to_targets(address, length) + starting_segment = self.trace.get_segment(idx) + + for seg_id in range(starting_segment.id, -1, -1): + + # fetch a segment to search backwards through + seg = self.trace.segments[seg_id] + seg_base = seg.base_idx + + mem_sets = [] + + if access_type & BreakpointType.READ: + mem_sets.append((seg.read_idxs, seg.read_addrs, seg.read_masks)) + if access_type & BreakpointType.WRITE: + mem_sets.append((seg.write_idxs, seg.write_addrs, seg.write_masks)) + + # loop through the read / write memory sets for this segment + for idxs, addrs, masks in mem_sets: + reverse_addrs = addrs[::-1] + hits, first_hit = {}, len(reverse_addrs) + + # + # check each 'aligned address' (actually an id #) within + # the given region to see if it appears anywhere in the + # current segment's memory set + # + + for address_id, address_mask in targets: + + # + # if there is a memory access to the region, we will + # break here and begin processing it + # + + try: + index = reverse_addrs.index(address_id) + first_hit = min(index, first_hit) + #print(f"HIT ON 0x{self.trace.mem_addrs[address_id]:08X} @ IDX {seg_base+idxs[index]}") + + # + # no hits for any bytes within this aligned address, + # try the next aligned address within the region + # + + except ValueError: + continue + + # + # ignore hits that are less than the starting timestamp + # because we are searching FORWARD, deeper into time + # + + #if seg_base + idxs[index] <= idx: + # print(f"TOSSING {seg_base+idxs[index]:,}, TOO CLOSE!") + # continue + + hits[address_id] = address_mask + + # + # if we hit this, it means no memory accesses of this + # type (eg, reads) occured to the region of memory in + # this segment. + # + # there's nothing else to process for this memory set, + # so just break and move onto the next set (eg, writes) + # + + if not hits: + continue + + num_addrs = len(reverse_addrs) + for reverse_index in range(first_hit, num_addrs): + address_id = reverse_addrs[reverse_index] + target_mask = hits.get(address_id, None) + + if not target_mask: + continue + + #print("CLOSE! DOES MASK MATCH?") + #print(f" TARGET: 0x{self.trace.mem_addrs[address_id]:08X} MASK: {target_mask:02X}") + #print(f" CURRENT: 0x{self.trace.mem_addrs[address_id]:08X} MASK: {masks[index]:02X}") + #print(f" RESULT: {target_mask & masks[index]:02X}") + + normal_index = num_addrs - reverse_index - 1 + + # + # got the first hit for this set.. great! save it and + # break to search the next memory set + # + + if target_mask & masks[normal_index]: + hit_idx = seg_base + idxs[normal_index] + if hit_idx > idx: + continue + accesses.append(hit_idx) + #print(f"FOUND HIT AT IDX {hit_idx}") + break + + # + # if there has been a read or a write, select the one that is + # 'closest' to our current idx. there should only be, at most, + # two elements in this list... + # + + if accesses: + return min(accesses, key=lambda x:abs(x-idx)) + + # fail, reached end of trace + return -1 + + def find_next_register_change(self, reg_name, idx=None): + """ + Return the next timestamp to change the given register. + """ + if idx is None: + idx = self.idx + 1 + + # if the idx is invalid, then there is nothing to do + if not(0 < idx < self.trace.length): + return -1 + + starting_segment = self.trace.get_segment(idx) + target_mask_ids = self.trace.get_reg_mask_ids_containing(reg_name) + + # search forward through the remaining segments + for seg_id in range(starting_segment.id , len(self.trace.segments)): + seg = self.trace.segments[seg_id] + seg_base = seg.base_idx + + # + # we only need to search *part* of the current segment, start + # from the given/starting idx position + # + + if seg == starting_segment: + relative_idx = idx - starting_segment.base_idx + + # for the remaining segments, we need to search them from the start + else: + relative_idx = 0 + + # search forward through the starting segment + while relative_idx < seg.length: + if seg.reg_masks[relative_idx] in target_mask_ids: + return seg.base_idx + relative_idx + relative_idx += 1 + + # fail, reached end of trace + return -1 + + def find_prev_register_change(self, reg_name, idx=None): + """ + Return the prev timestamp to change the given register. + """ + + # + # search backwards from the current trace position if a starting + # position is not specified + # + + if idx is None: + idx = self.idx - 1 + + # if the idx is invalid, then there is nothing to do + if not(0 < idx < self.trace.length): + return -1 + + starting_segment = self.trace.get_segment(idx) + target_mask_ids = self.trace.get_reg_mask_ids_containing(reg_name) + + # search backwards through the remaining segments + for seg_id in range(starting_segment.id, -1, -1): + seg = self.trace.segments[seg_id] + + # + # we only need to search *part* of the current segment, start + # from the given/starting idx position + # + + if seg == starting_segment: + relative_idx = idx - starting_segment.base_idx + + # + # for the remaining segments, we need to search them + # back to front, as we are iterating backwards in time + # + + else: + relative_idx = seg.length - 1 + + # search forward through the starting segment + while relative_idx > -1: + if seg.reg_masks[relative_idx] in target_mask_ids: + return seg.base_idx + relative_idx + relative_idx -= 1 + + # fail, reached end of trace + return -1 + + def _region_to_targets(self, address, length): + """ + Convert an (address, len) region definition into a list of [(addr_id, access_mask), ...]. + """ + ADDRESS_ALIGMENT = 8 # TODO: this is gross! + output = [] + + # + # convert the given contiguous region of memory into an array of aligned + # addresses and memory masks to mirror the 'compressed' trace format + # + + aligned_address = self.trace.get_aligned_address(address) + aligned_mask = self.trace.get_aligned_address_mask(address) + + mapped_address = self.trace.get_mapped_address(address) + if mapped_address != -1: + output.append((mapped_address, aligned_mask)) + #print(f"aligned: 0x{aligned_address} - mask {aligned_mask}") + + # the bytes consumed so far + length -= (ADDRESS_ALIGMENT - (address - aligned_address)) + aligned_address += ADDRESS_ALIGMENT + + # process the remaining.. aligned.. addresses + while length > 0: + + mapped_address = self.trace.get_mapped_address(aligned_address) + + # + # the current chunk of the region is not seen in the trace, skip + # to the next chunk + # + + if mapped_address == -1: + length -= ADDRESS_ALIGMENT + aligned_address += ADDRESS_ALIGMENT + continue + + mask_length = ADDRESS_ALIGMENT if length > ADDRESS_ALIGMENT else length + access_mask = self.trace.get_aligned_address_mask(aligned_address, mask_length) + #print(f"aligned: 0x{aligned_address:08X} - mask {access_mask:02X} - mask len {mask_length}") + + output.append((mapped_address, access_mask)) + + # continue moving through the region + length -= ADDRESS_ALIGMENT + aligned_address += ADDRESS_ALIGMENT + + #for addr, mask in output: + # print(f"TARGET {self.trace.mem_addrs[addr]:08X} MASK {mask:02X}") + + return output + + #------------------------------------------------------------------------- + # State API + #------------------------------------------------------------------------- + + def get_ip(self, idx=None): + """ + Return the instruction pointer. + + If a timestamp (idx) is provided, that will be used instead of the current timestamp. + """ + return self.trace.get_ip(idx) + + def get_register(self, reg_name, idx=None): + """ + Return a single register value. + + If a timestamp (idx) is provided, that will be used instead of the current timestamp. + """ + return self.get_registers([reg_name], idx)[reg_name] + + def get_registers(self, reg_names=None, idx=None): + """ + Return a dict of the requested registers and their values. + + If a list of registers (reg_names) is not provided, all registers will be returned. + + If a timestamp (idx) is provided, that will be used instead of the current timestamp. + """ + if idx is None: + idx = self.idx + + # no registers were specified, so we'll return *all* registers + if reg_names is None: + reg_names = self.arch.REGISTERS.copy() + + # + # if the query matches the cached (most recently acces) + # + + output_registers, target_registers = {}, reg_names.copy() + + # sanity checks + for reg_name in target_registers: + if not reg_name in self.arch.REGISTERS: + raise ValueError(f"Invalid register name: '{reg_name}'") + + # + # fast path / LRU cache of 1, pickup any registers that we've already + # queried for this timestamp and remove them from the search + # + + if idx == self._idx_cached_registers: + for name in reg_names: + if name in self._cached_registers: + output_registers[name] = self._cached_registers[name] + target_registers.remove(name) + + # + # the trace PC is stored differently, and is tacked on at the end of + # the query (if it is requested). we remove it here so we don't search + # for it in the main register query logic + # + + include_ip = False + if self.arch.IP in target_registers: + include_ip = True + target_registers.remove(self.arch.IP) + + # + # looks like everything is resolved from the cache already? so we + # can just return early... + # + + if not target_registers: + if include_ip: + output_registers[self.arch.IP] = self.trace.get_ip(idx) + return output_registers + + # + # search for the desired register values + # + + current_idx = idx + segment = self.trace.get_segment(idx) + + while segment: + + # fetch the registers of interest + found_registers = segment.get_reg_info(current_idx, target_registers) + for reg_name, info in found_registers.items(): + + # alias the reg info + reg_value, reg_idx = info + + # save the found register + output_registers[reg_name] = reg_value + + # discard the found register from the search set + target_registers.remove(reg_name) + + #print(f"Finished Seg #{segment.id}, still missing {target_registers}") + + # found all the desired registers! + if not target_registers: + break + + # TODO/XXX: uhf, this '-2' is ugly. should probably refactor. but we have to + # do -2 because get_reg_info() searches from idx + 1.. so -2 into the + # prev segment.. +1 will put us on the last idx of the segment... + + # move to the next segment if there are still registers to find... + current_idx = segment.base_idx - 2 + segment = self.trace.get_segment(current_idx) + + # fetch IP, if it was requested + if include_ip: + output_registers[self.arch.IP] = self.trace.get_ip(idx) + + # update the set of cached registers + if self._idx_cached_registers == idx: + self._cached_registers.update(output_registers) + else: + self._cached_registers = output_registers + + # the timestamp for the cached register set + self._idx_cached_registers = idx + + # return the register set for this trace index + return output_registers + + def get_memory(self, address, length, idx=None): + """ + Return the requested memeory. + + If a timestamp (idx) is provided, that will be used instead of the current timestamp. + """ + if idx is None: + idx = self.idx + + #print(f"STARTING MEM FETCH AT IDX {idx} (reader @ {self.idx})") + buffer = TraceMemory(address, length) + + # + # translate the (address, len) 'region' definition to a set of pointer + # width (eg, 8 byte) aligned addresses as used internally by the trace + # + + aligned_addresses = {(((address + i) >> 3) << 3) for i in range(length)} + + get_mapped_address = self.trace.get_mapped_address + mem_addrs = self.trace.mem_addrs + mem_masks = self.trace.mem_masks + + missing_mem = {} + for address in aligned_addresses: + + # translate the aligned addresses to their mapped addresses (a simple id) + mapped_address = get_mapped_address(address) + #print(f"SHOULD SEARCH? {address:08X} --> {mapped_address}") + + # + # if the symbolic address (a mapped id) doesn't appear in the trace + # at all, there is no need to try and fetch mem for it + # + + if mapped_address == -1: + continue + + # + # save the mask for what bytes at the aligned address should + # exist in the trace + # + + missing_mem[mapped_address] = mem_masks[mapped_address] + #print(f"MISSING 0x{address:08x} - MASK {mem_masks[mapped_address]:02X}") + + missing_mem.pop(-1, None) + + # + # + # + + starting_seg = self.trace.get_segment(idx) + seg = starting_seg + + # NOTE: writes should have priority in this list + mem_sets = \ + [ + (seg.read_idxs, seg.read_addrs, seg.read_masks), + (seg.write_idxs, seg.write_addrs, seg.write_masks), + ] + + segment_hits = {} + + # + # loop backwards through the read / write memory sets for the segment + # this get_memory() request started from (eg, the current trace position) + # + + for set_id, entries in enumerate(mem_sets): + idxs, addrs, masks = entries + + # + # slice the memory set down to just the memory accesses that occur + # before the starting idx/timestamp + # + + relative_idx = idx - starting_seg.base_idx + #print(f"ATTEMPTING TO SLICE AT RELATIVE IDX {relative_idx} (idx {idx})") + + index = bisect.bisect_right(idxs, relative_idx) + idxs = idxs[:index] + addrs = addrs[:index] + masks = masks[:index] + + # + # loop backwards through the memory access list, as we need + # to find the last-known access to a given address + # + + for hit_id in range(len(addrs) - 1, -1, -1): + current_address = addrs[hit_id] + missing_mask = missing_mem.get(current_address, 0) + #print(f"MEM ACCESS {self.trace.mem_addrs[current_address]:08X}") + #print(f" - MISSING MASK? {missing_mask:02X}") + + # the current memory access does not fall into the region + # we care about... ignore it and keep moving + if not masks[hit_id] & missing_mask: + continue + + # found a hit, save its info to evaluate after hits have + # been scraped from both sets + hits = segment_hits.setdefault(current_address, []) + hits.append((idxs[hit_id], set_id, hit_id)) + + # + # we have collected all the reads/writes to the region of interest + # for this segment... now we will go through each one until we have + # enumerated the most recent data from the lists of memory accesses + # + + for mapped_address, hits in segment_hits.items(): + #print(f"PROCESSING HIT {self.trace.mem_addrs[mapped_address]:08X}") + + # + # sort the hits to an aligned address by highest idx (most-recent) + # NOTE: mem set id will be the second sort param (writes take precedence) + # + + hits = sorted(hits, reverse=True) + #print(hits) + + # + # go through each hit for the aligned address, until its value + # has been fully resolved + # + + for relative_idx, set_id, hit_id in hits: + idxs, addrs, masks = mem_sets[set_id] + + missing_mask = missing_mem[mapped_address] + current_mask = masks[hit_id] + + #assert relative_idx < (idx - seg.base_idx), f"rel {relative_idx} vs {idx} .. {idx - seg.base_idx}" + #print(f"rel {relative_idx} vs {idx} .. {idx - seg.base_idx}") + + # if this access doesn't contain any new data of interest, ignore it + if not missing_mask & current_mask: + continue + + found_mask = missing_mask & current_mask + found_mem = seg.get_mem_data(hit_id, set_id, found_mask) + #print(f"FOUND MEM {found_mem} FOUND MASK {found_mask:02X}") + #print(f" - ADDR: 0x{found_mem.address:08X}") + #print(f" - BADDR: 0x{buffer.address:08X}, LEN {buffer.length}") + + # update the output buffer with the found memory + buffer.update(found_mem) + + # update the missing mask bits + missing_mask &= ~found_mask + + # the current address has had all of it bytes resolved + # back to a concrete values, time to bail + if not missing_mask: + missing_mem.pop(mapped_address) + break + + missing_mem[mapped_address] = missing_mask + + # + # now we will go backwards through the trace segment snapshots and + # attempt to resolve the remaining missing memory + # + + for seg_id in range(starting_seg.id-1, -1, -1): + + seg = self.trace.segments[seg_id] + mem_delta = seg.mem_delta + + to_remove = [] + + # + # loop through all the addresses that we are still missing data + # for, and check if this segment can resolve it to a concrete value + # + + for mapped_address, missing_mask in missing_mem.items(): + + # skip the current address if it doesn't get touched by this seg + if not(mapped_address in mem_delta): + continue + + # + # fetch the 'value' (1-8 bytes) that this segment sets at the + # the current aligned address + # + + mv = mem_delta[mapped_address] + + # + # if the bytes set aren't ones that we are still looking for, + # then there is nothing to fetch for this address, in this seg + # + + if not (missing_mask & mv.mask): + continue + + # + # create a mask of the missing bytes, that we can resolve with + # the memory value (mv) provided by this snapshot + # + + found_mask = missing_mask & mv.mask + + # remove the bits that this memory value will resolve + missing_mask &= ~found_mask + if not missing_mask: + to_remove.append(mapped_address) + + other_address = mem_addrs[mapped_address] + if other_address < buffer.address: + buffer_index = 0 + other_index = buffer.address - other_address + else: + buffer_index = other_address - buffer.address + other_index = 0 + + buffer_remaining = buffer.length - buffer_index + other_remaining = 8 - other_index + overlap = min(buffer_remaining, other_remaining) + + #print(f"HIT 0x{other_address:08X} IN SEG {seg_id} (started from {starting_seg.id})", ' '.join(["%02X" % x for x in mv.value])) + for i in range(overlap): + if (found_mask >> (other_index+i)) & 1: + #print(f"- GRABBING BYTE @ 0x{other_address+other_index+i:08X}, ({mv.value[other_index+i]:02X})") + buffer.data[buffer_index+i] = mv.value[other_index+i] + buffer.mask[buffer_index+i] = 0xFF + + missing_mem[mapped_address] = missing_mask + + # remove any addresses that have had their values fully resolved + for mapped_address in to_remove: + missing_mem.pop(mapped_address) + + #print("STILL MISSING", ["0x%08X" % self.trace.mem_addrs[x] for x in missing_mem]) + + # return the final / found buffer + return buffer + + def read_pointer(self, address, idx=None): + """ + Read and return a pointer at the given address from memory. + + If the value cannot be fully resolved and returned, ValueError is raised. + """ + if idx is None: + idx = self.idx + + buffer = self.get_memory(address, self.arch.POINTER_SIZE, idx) + if not len(set(buffer.mask)) == 1 and buffer.mask[0] == 0xFF: + raise ValueError("Could not fully resolve memory at address") + + pack_fmt = 'Q' if self.arch.POINTER_SIZE == 8 else 'I' + return struct.unpack(pack_fmt, buffer.data)[0] + + #---------------------------------------------------------------------- + # Callbacks + #---------------------------------------------------------------------- + + def idx_changed(self, callback): + """ + Subscribe a callback for a trace navigation event. + """ + register_callback(self._idx_changed_callbacks, callback) + + def _notify_idx_changed(self): + """ + Notify listeners of an idx changed event. + """ + notify_callback(self._idx_changed_callbacks, self.idx) diff --git a/plugins_sogen-support/tenet/trace/types.py b/plugins_sogen-support/tenet/trace/types.py new file mode 100644 index 0000000..be3f747 --- /dev/null +++ b/plugins_sogen-support/tenet/trace/types.py @@ -0,0 +1,84 @@ +import array + +class TraceMemory(object): + """ + A Trace Memory Buffer. + + TODO: this is pretty trash / overraught and should be refactored. also + this can probably be moved into tenet.types? + """ + + def __init__(self, address, length): + self.address = address + self.data = array.array('B', [0]) * length + self.mask = array.array('B', [0]) * length + + def __contains__(self, address): + if self.address <= address < self.end_address: + return True + return False + + @property + def end_address(self): + return self.address + self.length + + @property + def length(self): + return len(self.data) + + def consume(self, other): + assert other.address >= self.address + + end_address = max(self.end_address, other.end_address) + new_length = end_address - self.address + + # + # if the other buffer is outside the memory region of this object, + # extend our region to include it + # + + if new_length > self.length: + new_data = array.array('B', [0]) * new_length + new_mask = array.array('B', [0]) * new_length + new_data[:self.length] = self.data[:self.length] + new_mask[:self.length] = self.mask[:self.length] + self.data = new_data + self.mask = new_mask + + # transfer data from the other memory object, into this one + base_idx = other.address - self.address + for i in range(other.length): + index = base_idx + i + if other.mask[i]: + self.data[index] = other.data[i] + self.mask[index] = 0xFF + + def update(self, other): + + if self.address < other.address: + this_start = other.address - self.address + other_start = 0 + else: + this_start = 0 + other_start = self.address - other.address + + assert this_start >= 0, f"{this_start} must be >= 0" + assert other_start >= 0, f"{other_start} must be >= 0" + + other_length_left = other.length - other_start + this_length_left = self.length - this_start + overlapped_length = min(other_length_left, this_length_left) + + #print('-'*50) + #print(f" Self Addr 0x{self.address:08X}, Len {self.length}") + #print(f"Other Addr 0x{other.address:08X}, Len {other.length}") + #print(f" Overlapping Bytes: {overlapped_length}, self start {this_start}, other start {other_start}") + + for i in range(overlapped_length): + if other.mask[other_start+i]: + self.data[this_start+i] = other.data[other_start+i] + self.mask[this_start+i] = 0xFF + + def __str__(self): + output = ["%02X" % byte if mask else "??" for byte, mask in zip(self.data, self.mask)] + return ' '.join(output) \ No newline at end of file diff --git a/plugins_sogen-support/tenet/types.py b/plugins_sogen-support/tenet/types.py new file mode 100644 index 0000000..23fefed --- /dev/null +++ b/plugins_sogen-support/tenet/types.py @@ -0,0 +1,72 @@ +import enum + +#----------------------------------------------------------------------------- +# types.py -- Plugin Types +#----------------------------------------------------------------------------- +# +# This purpose of this file is to host basic types / primitievs that +# may need to be used cross-plugin, and could be prone to causing +# cyclic dependency problems if left with their respective subsystems. +# + +#----------------------------------------------------------------------------- +# Hexdump Types +#----------------------------------------------------------------------------- + +class HexType(enum.Enum): + BYTE = 0 + SHORT = 1 + DWORD = 2 + QWORD = 3 + POINTER = 4 + MAGIC = 5 + +class AuxType(enum.Enum): + NONE = 0 + ASCII = 1 + STACK = 2 + +HEX_TYPE_WIDTH = \ +{ + HexType.BYTE: 1, + HexType.SHORT: 2, + HexType.DWORD: 4, + HexType.QWORD: 8, + HexType.POINTER: 8, # XXX: should be 4 or 8 + HexType.MAGIC: 1, +} + +class HexItem(object): + def __init__(self, value, mask, width, item_type): + self.value = value + self.mask = mask + self.width = width # width in bytes + self.type = item_type + +#----------------------------------------------------------------------------- +# Breakpoint Types +#----------------------------------------------------------------------------- + +class BreakpointType(enum.IntEnum): + NONE = 1 << 0 + READ = 1 << 1 + WRITE = 1 << 2 + EXEC = 1 << 3 + ACCESS = (READ | WRITE) + +class BreakpointEvent(enum.Enum): + ADDED = 0 + REMOVED = 1 + ENABLED = 2 + DISABLED = 3 + +class TraceBreakpoint(object): + """ + A simple class to encapsulate the properties of a breakpoint definition. + """ + def __init__(self, address, access_type=BreakpointType.NONE, length=1): + assert not(address is None) + self.type = access_type + self.address = address + self.length = length + self.enabled = True \ No newline at end of file diff --git a/plugins_sogen-support/tenet/ui/__init__.py b/plugins_sogen-support/tenet/ui/__init__.py new file mode 100644 index 0000000..afd5e8f --- /dev/null +++ b/plugins_sogen-support/tenet/ui/__init__.py @@ -0,0 +1,8 @@ +from tenet.util.qt import QT_AVAILABLE + +# import Qt based plugin UI if available +if QT_AVAILABLE: + from tenet.ui.palette import PluginPalette + from tenet.ui.hex_view import HexView + from tenet.ui.reg_view import RegisterView + from tenet.ui.breakpoint_view import BreakpointView diff --git a/plugins_sogen-support/tenet/ui/__pycache__/__init__.cpython-311.pyc b/plugins_sogen-support/tenet/ui/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..880afa86ac81dbd8250a6366a57bb6fd7e38ec4c GIT binary patch literal 554 zcmZuu%SyvQ6rE`%O$?jg+PZhmvhd&oXaqus#*c!ynmlMI}m`MRQ#X9lFTjz`2YhjfE2jU zgb-lJwuLUxf-9L4OnVYVu58K~N3`fFrjl`)mfW&g7QA?yT}}B-gEctaxW2QEzR~F# z&2C$0wMU~N@p=x$oMT}q7kJbs_${5i!9x;q%$B;^WY~EQeB$xcxzZ|Am17TcJ>rDw zFPuqY$+Wigh#o9b8A|QVmHmWA_NxeBa^7cnxbTaqzqOjbU#(rV7190zGf~03eST!M z{BZ10hc(Wa^YDyWqti0#J?1~+FeJX`(E6di*9fwQhE^79MZ~gg;t_7!dN7`sj$MBc zQGCK`B31kf2*;u%5`=I9cH=yfB0#kSAb2oAdYs1uX>lIwX7*#W21!{&X1S~?w*LVJ C8K9#8 literal 0 HcmV?d00001 diff --git a/plugins_sogen-support/tenet/ui/__pycache__/breakpoint_view.cpython-311.pyc b/plugins_sogen-support/tenet/ui/__pycache__/breakpoint_view.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..464856276469443f427b2e8fbd923d41b2f02289 GIT binary patch literal 3306 zcmb_e-D?|15Z{w@()vSooForjigi925$r4}Mc{9!g%?nLXV}wUwGeR;SsInf;pi z&CI@tMw>x8PInrRNDkmy$wcV4@eFP_9Bq`}68PKIjrB@k1Lkk}Ye+CJ zU%#I(Y*W8U?s|qGcf;OjgLNVvufEo$29kAa*JM}rN$Lbt8klriYKpSrO^$5kK!qF+ zgepaX)S4n#!p}2 zTIwF-r>~EXDPM4Nj#*aP%xH#kVdU)TtdW~fXRKM9WtcsiOV8f+fuaiQXP0;rEW3QH zc3V91h`Csw5Lpa3f{L1JCT>tWIT*Y z&S(n^l-#VU3#Ohku-&Sv_j8(2G7ew9E#rWJxD9Flu0C(b7dVuMpD*gpfCz< z9l-!m@7B|RiRF{RSEuf@V{a1P2?OykN7L{RksvOe=9HPSxoH@TJ2Iev82}97e7SU7 z=7&&FLDGlhFcM*P+!uUBS#>SH7vrc-ALGYB*181bV2sC3u7#eozd9O&su@8XeH{I+ z{Ym?p_z4)K{5S_IV&p>Q>{ft0!?*|a{?agB)vx1E*{eF+S2lS^OobQ?E7hX!H`0of zubbsB4e8DOZ2))6mw;$3H8$hQe?(>xMT3(&IxNpID*&7iL9Dw0wp++8Tx03^JGRr} zn$pHB-R&uKEk=yucD|InAUwDt3*Vq-V*IyPZeo@nf4cG|L+*qx=OUGNMmIh zE6~`V9kK0>fl|jn-Y#|wl{<#k!mnuCHtj3XzI8Ub3Rq($3a6zRDxJ-b_AniieEBS_S*lS$yK0gJ6dHimPUzuDfx;cs;tr~12lb3OcWNoioXF=H#Gvj(#&n%S z3U$Kc%wRTy81pd*I<1K*6mT)eKNUj&M8*M{Ms6YF90!zMGkJO$kg6Hin5Hv6q1|SN z6_*4uMIx#KascKp$7(6J4v+%O$hUq30!g>?qwUV2Qs>a-RI&41x$_)Eo)!S=la_6& zl&F&LaPx72o_KGg!zIA($h7+v=}ei<6ui$(bs|_(j**jhAvumDh6F<`M~~xp3*L>S z2S|M)7V#Yk25id!(l`;*klcldxG`ys$O9GPlfz;8E2%;brtTGRdwgd^#oK5kXQz#k z`?hQCmPGB{=05o%iK^*w+Dqatfscx}^rHJX`HWwLF5=7j7Dy#1Nm8YiNIeD8?S7RI mmEc{mpFF))K05ds8EK~YlE|P&2Gg`tTo*p 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 jO#53aD0?FRdt*b9_WGi+q>yxttr5Q=IB~m*> z*%m8hbAq7D7;dc%vaouP?NPVMuD96&DSFytpgq9uo^3ZcKUxkksE7ev7;W<-=#MJM zE)eXGqR;ycIh-NsM2nsg^>HrWecsFSzTf-t=b=z7hvSF;^=xeRBFFttx+osL!pT=3 z;^YA*agu9+yKa8FuDjT^`?{Nbd#-!%?OyOM`mXz2l*hABbIttrvv2PLzZke4V9$ID zwTr>)K^N!dWX}zu*q~JNGZ)8wf`9Y49&&T%ILZGmC-HLK&pddB|H8Ad6nK}rUN5<> zas#zr(9OvKml3{r_44GHJd>2G9Ekc9!J`)frU*XHF#SzIy)EzTwt zF*T2Cb2?a#8S6D*Xf*Bg|CJkeK?<%_E^3Wfa^uR%J;&jyICCHaTv;!TU3rTIS-B#S zagtZ^Ni`o*T{XoVtS*1?YL(mMi|6)l=lr|S{${}bym($EA0z(a^Wt}TzVgvxR{iNw zHc2#<7ZUMQJhl)|%c3OD#+DaS*1XOn7m|uvb$Y4f#H8VsV#>{Qt-6v(%@5sBlZmu2 zpH$-MWP)7{xE1Q6;aiB$&8G%}h7gTg%}GW$x)4)SQ7Nv-GpVGqV$??!`TNVbl%lh7 zc|lT*P;^<9mFV|V(fN2HWi*?YB`#Po@B8rj7iCmf7($d?t451?5}lckCFW4;Ol)Cc zIyQ4tH6qc4WK61PuO+&aTv}d;rDU@uRuA>iXfz8r8u$e`Q<v|{j4 zHc8QFy4gYcL&f_oXiWW=NZ#hQd9J-H+vf??ZgZ4)xIhR+x9hlpXDDkg@&rO#d{>U|()b>o@5%B#g?qg@ zzE|V>b-q8#_ZRMoIbPKGE}ien@?H78om59_mVLfLH@O)~iU)BM{eLAQdBCMC&f~zB zP5}x^b*G=RDD@RNwX7s;oKB|@0~Y-ZSy_!5lK>zx!{%J$O{>XeWk&u5r|dz?N-Gkh zX--Z>3mL3eo$hkZd$l|-ptQOR$=lo?YPtH3jXPR+zaHM7<@dAd9YZFO!G=7K6AT$? z@wj0Pf722=#b@-Lvcw0+-LOQDlf^M@DEo<3ehngoP2CY|K5SD*{PYbICoqyM@XvtH za3@u0pIB<%@ZLzq6G{y_WpvP#l$a0<%DB{e5j5@AbegXQ zaRv>m&mv(nzNh`e{=XXh%fZJ&i!QvN2`6;nL|$n7(&yPz`+E*aCJf%F@5u7|%=cl< z4r;L82UX`J!BwvGMpnz_6}q3awL5*rn%Aq#)9Q>8pO(d#Sd=YAc?rri#f~W7k`pPl zM4giZZlghw=i+J#Q&%jfG@>_b86P+?z9m2`e=f=sX*36u=W@a*(iqEcy3pB=r z3@wtJ#T>CNI;O=rE8zqy=A08{H@ae{pGdTj%`xJ1w{tqGX1YdI5&xUh(z4;zTaIb` zah*S&Eq=aXQYJ7}55QhGP=k3Wz*ASbl#RTqGqU6^pEBrqn&^td$=pgeLmgJG9kk>v zZ;5({)`;vYj`G%aP`|IDenLrB&1^n`)+JXCiwneJ#~`oAltoosUShJHUTQosC#owT zokej;i4(j_iUs5#LM+&bCf!bjDZg19T&IF5S=phXp=;@0@t}AcS@3u!xwr(FEDxd% zR>Bg6o$13*&qm1OARtO|aZyf4vSd;=s>8~ zgL}6*SD-O3?8;1h&ez=+?h4t+a~gk4=Z|IiW6blsbY0=&%R+|9lu1yQ_7h9)qCmX$qo~*goJgaWi6c3h-2rX`D^y=n7s(Rk4Cc$c% zP;oNmn)fMnRW(B(Aa-C*l(pbn^{)DEGBKv?k#L^IT&&fkNMaX`l6|~dW*?h^VAZW0 zeo8-gG-gNHP_b3&hux4%?JYjxxXp0symTh{R^4e(o^yoE9IG$oDxEpsILp^idRjUw ze@AL__1UQ|CR4qonj9l7Q4zt0ieqPQQ}R2Jqr^L}k8x}MRsUvFMfJE*J}!Ej^yjR> ze8=6~VNUhlcHi-Qm%Ht9ao^=!_#;Iz5J+FKFvH~~IU&Zx+4us1`WS?!7)wYZL^e|l zga<{KTL6^Em5^^241QA{+jT-St7(b31I(wa(sU!aSHua_uZsH*m?mm^_{xGDQ)TEE zH3jofT!ES3s2NY7ejuYs^;BTz6ScMJq0!h9Y!j#k>U$yvB{Zuf7i~=!#&bgK?^XN% zcy%I%d{87wk*v^SYo+QtYikR=nwghpZnDN_k_k15nIUh-Vp5?F<8(2Wf>{Yo_s7ss zY$g@IB^R`b>-Ipz2u!73k4u2tsu7%`agVWSRftcFpem>6-plYd8N&D-Ks9JZx?_Y< zgyxVgCNUhP8wHdeD&>z&tIQto1MbsMmHqULWVYdl3QC|3!=uVe%3->Hgzh)N`kI9S zHeWEuNyVW@roJ#;C72!&CleT zpUDgLd7%fXE{J)d^UHv5H!Kh&nc%j-1)8?_jvU|daX=T3>EejSkLvtrmLG-M-QBg- zd^p#9SZhA2Hy?d3nBmtu^WohkZT*KjuBrKh=O3JT|IEinK6?J=XMTF-(-*YvQ+oF) zt?{(pcsj#BZj6{2HlN$-K9TD_;e4DAHm={v1v^Zg-uOoL&t|m7namh7o5al~P3Y5w zzO2w^DfK35e!}&*yVu#^-zxv{mexH^pg{Zs+yh|~M~r{QYmqA&Kc(|iS$=A}o;eOW zb9|@9cf;wB<-0M``lhXLM=sp4$!p;OJv^|@`2vk*AGY1DhEdm-9#5zac3Y?pJw?KE zp~lPv+Ns<1S9=bxk-j}`h zYF-H4KX>=s`gu)g2Yks2?b{waVUJnzC1w0QC8c{$J^T9Wub=viJzy??|9qzTT$kr} zUEXtj{@)F_kXCm-u7ZBZc-X{u<>AyPrdff!7WS~0sFpWn!zGo*29vd(^#8GB?br&~ zC{9-93d6SthL<1>ykr3~|Rq zau|w`V9XW1c$A} z*{P+Sm}kwCvLVFkcz4xPR>lh)H|i)yy{$N$l)L6z^=(>6#-6znU95Vmpc=)+BF> zaUju>h&6n4t)>I);m}fIP9Zzo@LZWZ_l1NrID`G)U&abkYGt5J*@rtNG+6+AQ>k;y zal=1#C4Ofywq(>!T~lHSmC&tGW43JsQ^i}dWbjj0Vhp7jzG=8o6UHt;*-?0Rk}4Ln zk_m=rTgq-kuQLDtg#-hR3$MV8Fub(#WO$&xjNRqgFW-`th1iNwD=D$tg`U++p&3O+ z4@Xu~^01=dxqoV8d6pbG<23BR)P>7q@~!xcYy@5>9D6yju!2>h*e#h>%Tlp{powdJ zY;p~rjSqb`0yK|N2CY5u#W}-`(R&wT>P^G7WVokehIbn6z#)HAB_=CiTlzBbZ5>`* zl)Z!~)s)`rM77SRzeNq|2LKn$4;tzDc;usVKfmK!?;qOi(suRgyLvODpNE_7uiahSxT=La^)MESJ{KD9Pu`u>guS}3H{;6l zf&2cu{&nwrp?jfB2$x&@-Wqj5F@cs)wI`P4Y zjb)AR#mZHd@1wMkuRrRO82Vek{w6Q6V_S)~7aH8}N;Z zIxl8<(NPtl(Wb4?zFcUZ783Q4m~lT2NAACM_pOcT;&Af3a6fQ2@Z&yxSC_tPP~#8k z{J|`LFdwSF|Hj=n)}@W{hetDSXrV)T=upO;4>x22^!Fs(aR2t*+wZO1Tgmb*l^!v| zCrtAb9^5I0Tyv#t=mGK{Wk0d-BqWT@V^$q0IAz8d_vi1exgA=|!kn8m=wZdN>Vh7J zG_b8rIk3fzQu*pApbTXwu6rYYQu{kvavCbjU89v*_>7HZAcw}78JyFc>(T==Q*@Y>eFiQK^n?cjO+;Q7xE zXyU87_$rJ?IJED*lx-P!ct#UW>cYt^`vBsLL$t`fH_JZXz^{85zuLXb)h)X6rEeFP z@?u&F#|?{*H*I%|Ps!6+9i=<@7yNE$9Ur4bn=dwp#(8%yjvqJE(Vu`X^42l4fQEwr)xkOoTv$< z;>kzX^wU$>SKiQ1e^+a~t~XxK@OgM%>cw2WxY?rBAJFR$WPBjr&c4n0kHTBxb2;%j zO+2oP$BCHRv+Pqz%tUfb`TQ%KI9ER20c8#6 zMnQrSB{+$IaDTef3Cya*6}1xyOdu~?!)tESBLffn^vK~%?c>Pq4_<%p=KF7M_H1>X z%ypgAx=!m|ryr%Y$VEMJF;kllHx_(twOaTYJ^W0Tf94xr=_^b=VN%gp<>7o83^5?T zdDzjzP4?oIXK~MxR5-WtI*7z>Xg*sNiY(W{CQSYd__%q&l<3?|tIl~MFnAp=<0d32*4a8(H?*-UXOInP>IZV^I2tW^4Ioj+sbIZ{37F8<7%L(viiS+@gM2!NSQwH$=3~c#=}jD zS&VRDq0d5Oq?nZ-A}P05j^bW*U}lZk1bh=D*(Q-VfZ0inKc(}hvivDXF{k|k;Z%%M z+V)+WOe|Gu!Vy^2& zt?Q)Tb@I`5EplFuoX=1|Fop8$$p!apTr7qFxdJWu#@*}BZP$4M4UdJodl!B@{lVOW zh4&Yn3yqJGTJxmd3|+fe;aN%cVpd zW&PD9s9DFoy%G-|4y2D;JbVR9>4+^*mZdo2(NZym0SAN}zzGnChmpbotjy^?aOG=- z2QZ6Fcu+K`U`G+R!Wc5VY$^XQFy_hh{!(DuY#ad?1x#U<>Zff2EEX@Bn5f8F&Fo@r zTmyGnsKf9ezQ9~`W9E~WnNQ4UGJ?$GV76uW%x^EcMIHvz(HV{wMp_7UNVk^(YUkrc zV%rC(!NIntf93nt>~BL_*zEn;d6f`Ae%5{V2@n=G&etWva% zB@7T$grXz~Ceq!d^>T^rLo0GWX>}~ivrRr8GDXturL0Acvt>^AmmV(^9XkcvhgjUq zTHC8$8we=x;4LWkD51Bl5b-01D5WSrrZXYR23&Bv1jg^o$V(*VltXwrP!JyPQbBUN z7%o|L2n+@n(ye7eg8F3E_%;cMmynPXQV6u>n_9M-dU8!Yn{Q}MLweItW;`?gHI#Wh zVsju2N(mDS&wYAFYk+_7tS+3*3TF!nn6k658)WxT86+(GaRlKv3~R!OE{tS_5!_pk z{G@SX>Yp`#*!-h?njj*;CW}xaQ;(A_Uq1#l`t`OSHPE-f!fCp5;qJ|GEqp)^Ljn~= z)U${Ws+3cJ8_>pELVHeV-;gz-M;Cgs?DO^Ga7$@8rFu?2id7kttusEfNS&#z+$aPO zL?)#k!JBv|(!y=*9X{pxn*fqubBBkY_x$>KAI^U>5E(hc{pL*VXrJfr0=q^#J%88f z!?{Dv(@SJx@RvB*shY2*+U=sD(wy3)HYyG$Gh*4pCe%vH7Ug1yR6NwRCp}>??Uk5< z;KB-CC0uF+2|k@=+reqC01J~#(`0g1u&A|N3j9)*T4gJc%3X{Fn{}`m5Bu&wFKYF@dVMe4k>Ha^MyOPRB%;~dzs8i{4op$FjlLb^*&Wt! z>9{0s2^LldfKEyPkh}gS6zrM@fhIQdaMclZ;slJ!?x(6T4@^(gW?LZhCETlC1je{m zeWjiuK*eV8mqN%_YyK%hbWx?MvWQ3rThgh98Pw`4PbkRYShLN8HUK)#0(b5 zX}b`1fiA^oZbql!&Wf_q%4@8qsZrXGYWT6%A4@?u(5MShqU>Dx7nH4*LB=Rsy$Z78 zQJ`dOF0C~$><^%}mSyE8idDvoDjT?+ZY$FoHbwMri5dO_GC7#x*-wu>YX0lfTI8(8 z59|DJmLJYHG_T+Jpd(ZBq<`??8z22(tN&E4|CHAM9lifM+gvzskR_VXow>LXB)!qL z@yy35Ej*})2N5Y58pyYIZnYoDwIBKP#G`+twZEjdzqDSvUW+KneFOM{mD#!3rne1d zUV7Zr`oX2mXR@TTP*bRBeQe{vCPeMCPR@;ijV6 zK(*FEqh|D*z{yS{C9r~T)d5jWbyOTq`d-3L2b7|g*g=*B)c&7a(|<*lZxX5|N`3VL zCLh7)cW_Dj*gzTLZ3<|;N!B$ZN7~0sSs5~3BqF4@!-8f@!j{ec9`*Z!>5enf9S zLaT<@UVdP!ZzR_@qV`&q1YKL}4kz&EXJTBHUSFdK>0cKRsv zdC71i3BzWBWKd}uRv>X_Zq8x62?@@lNyv#ByaaYn7?wy<|R5Qo+5j{aCa}mLK{C68?L%vJ=asoSf~XTv{?MDpaKx&PXr%UU8&s z3y`H?juIX$cdaBMPpheNivV)&$Sr|&ttHzV7PfWvpYZuk zLYuKL?b=x=bE>n{-*Py^M8R9%A_}I8kmDa*0H8vP3$bfUii4MkJHcdgEJlIkKn&Zy zOr;=RBZ-3##7S%hdy|uJ{hC69bVx~>+@4PoXt30Yczq>?0EgN5445BvUWF1;3w|Vs z81Q)#T~X8ls(E%(Jw=AKF!jjsdng{geflLsh3O{HO+`H*# zSjQ7MkQa7u2|YQX=i}Et`mWwPq6wqAFq&nbC&g!*S2bZk7Y4Gz0N_3CIcv%B_^pRF z=;tiohI>sNTaA6W#y+iazuvfit8pmTIP|ZVfBBYvXiRGy*Bi$(e6^B!zR}#jrtzIR z-vqeg8s3Tj-&uDvK+cTtM{Eu=lb_VLgaNcereu5Hdr&bqjoU0BHTVJ{n! zFFm+?_f7MLz#j2Fxc`t?kLbo;uW!^vC9G8N`VhrvB~7iqBio#n96#m5J~b;@bp@$G zD{1TW!Lw*3ldcXQ!W*q*#1*E>t)#2Q2ZPc|>O1JtN=970zQ{IbC6lfOA9e#;NnaOT zTFH4=h_YMBzEUkkKUPn)@WWUdM83VVabrIj8ZO%%D4%202V!kPQNsdi*1ko9Y zB>AvYm=FExMRpF$`?S-ptw`Yh#e22cD!q4x}FH4!1 zi*a?vsI%QbHO$X$0E1C3+Db8pEg!UU7+Z>)QZ@knbBXVOHOc84*eqf43yH)s zM*1Uv%H;a*C_0|fB&h!l309(At~?iboBidv;M?pk&xPJ*e|fI%ZT9zs+n;rQo^X4z z#ZR7V&lW#- zTalCObkJoCccU2DTFE*MC*w_&i3j5ak|Hg3+iYiM8}yF_FL4U07d7DRVp;@6y9zRk z4hAUN-*+zWB`+V2JUcskL>=CH-p}uS4nJ}@DhzOc;0#>;`bmS~Z|OyKmdwL9{|yhf z4Ix9wIBgiGZ_~7K#x!m+(sS8#*#-JGPn%~f;})aAWEe4oEZ;YTtYPbwa<#Az%M6B3 z@UOUWfu|}*D&Y~SY&?|>sVb0)x0Ledl~u`8RU(xgsqC35@>4l@DhEw5%s@} zQnv9z#wN@;?WAXF>Nj`b{ju@3A!f|R#!T7gMZ<@9`$P*#XiwQR2E$_6hsghlR)*2A zXk0V|)WI+;n((I8zi3FPD=N1T!-shLL<^xcb>8r?ajeTKTcY!GVNs+tBnyGS)a+C& z5RfZqSj5OQ=0LX0L_*)X{N&VZ z^!ZqLHXM6?e(HHKJQ?8QJaiE+fZd02}4T&Cs_p8?p4+O`*m|h-|gjOcB zriJd5(7m!-5_T|QM^f0a>T8diZ#q|NefX+a73^<1ZaCg>-gL(K-!~}3Frke#Uaa`% zv&j&upUXwV6)h#6wb{eGB1d)~=FV2kMl~qzG4gA3iut7$6*!cot%!sM4|0>~S3v%n zin+3B6FF_nhN4&KZFU`AH!d0z8nno!GXR1qe;;C1exj{RTDJ+B!vdGvc6}X>%%hD- zK^{XwgHO4YEgJJv@?BsW6KU$Y99aM{0LYZHx&>N>Sl~gq>cZ4aI2sGi%$>g+o}QL% zQ=zMYD1UTD!?9D?Av5zc&rF44mu34*?wjpw48a6bom_8r8fKhVhz5;DtP%@BePkn5f%N9@Lo6k zz186DNL;uZxF7oT$`|cY{Rpcc!8UL_@NQZ$-*bNM`E%bF7bNcq<~@PnD(sH5(3lb$ zmoMCTA>Fhm)wD-y+RK{uO2R%S>`My!P-1mM+TEFQcP3(zdn+w9u3i zFi0&-Xh{k!6oz5dg&`OpJ|IeC1Oni*!I>}t@IU}ydw!ap?Sa5|=7ZCjnDRg%6qyJF zI6&n~N2F%SjtuOcz`_m$(0IOdsPRXXRYqWi;|Y3*hT#F+s())HlSNoFkSCZe^=pPw zp51O~SueZDppdB0*l>4`mt&3o8(dV1ej_m&L%!^l|*TK@Y$*4BAN< zj=;-c(AnjXB(yQ1EiG(K30qg5mV|yL^e2UW6?JA1X+8!m+@FN6sN>%(VJ!iJ;!$_ke<+?bg|I+F zrpLgO+9>3=>k+tB>v?`2f?{3fFs31eT*o0@S^hVA%BLLU?QlIkZ18I_BaQS2hGn>>O)S#%@Pe2P8rcw;gs zh5$2%YV8#9d+>5mjJXs};mF2bW$9QmXrA9_*|BDLl&3V7s+_>T=2Rg4n;@25DCKvJ zSbj^~pboK6KZ+riE(<4VvRhwtQ%X)L5b)p(Uk^mUqi;uqd^3CLc>zI{X+^3FXiXXK>UUMTx>y;MeZ!k&2VTc=6^*OPH`8( ztxt-P`PopjP8r7(ex~?m(t3q}F9G#C{n+^lYgkzjukMN5Y}<7AFl*bx+Tb5cw>_C^ds1pU#oA7- zSrENuAP>w`(;X+P-^%LyS^Yq|{#dI1m{fn9)gNCg*HTf^ugl796~8pVTjQP!*oXN)O!^S{Kf%A^iY*TX43sHzQ=^V+AbfR>Mjb?g zY>p8^$wwo{QRG4vcPran&3+b<(N1^@9&u*2`Or^Be>D2>llPvKgeRErL{j~%4~tNZ z#jyy7UgRC7dh@Y}>CsrkX4$!B(7ZaMWiMfo(w@>-s;xK{xr!JadqTp>>f*x6kQMAo zc?c5QaUmqYw}l{=9j^#gz^@EJ`Z{h8+2K1vkg<+eg{t5?Lr(avkPE&$1E@WB2 ztc0u!c%vtjL&7NT^2*W5zeICM5*+TPC{L~W7NWO0^$nv*OSizqNJf%zSBQ4w?h46W zxYN%zpOpqRF88}cIgM(XBIOWARN!I%?^aKmBgWn0p-PdIXpFa z8Ko(KnKyG#oPZSf%<0jw3&ZCE=SRnf<*Gm^d?`3T9n0ht0z{z9MIhAWPnV{`)1fGm zU5>mQ7Q-P(jm5}!8dL}YGHE|7WHDtbGST>!F$(84!XPKSk(XtN-pAL($ zsfpn9`3cg$91V(@Ol~b;AUYNfhrnIa%9hKh>n>zG6`7BQFOYs^YIYKB;A9SntH4Zn zc3!Sg-ZSmx{C-s2L*v{5ud7@k?6raCguQI$qinh~qmcMSdJ??{txgV+ujyn+Y_8Gx z&mwK~&j2IA!F`KP#?$=vX=2~?%9&p{J#W5p|SJ6o79%ZHNO*nY#l1ct%~A}cBQe&MK2JNzk!KjD!aoy^f0H?1}_z5m3! zPb3E4Ie6z_+{}df)yC%cN8cTN=gB)y(z_SrT}vBl-gdwJ)2>gqu^p$9XUX$8*P2ERS-mg!C9>{kS@Vw?sU>klvwDv zyfQz(Hks?lZ!S{5HreaQZz+;rTUsC*h-fWvF^#A(-CDQr|RDj+DkP%#*p zyXNN%@Yq7tG2ny9cl}ed{?Po)+(KQ>dydV-Lfzmr)R!na6!u@#Y%HQ(4n?M0es+M@ zBUcBM3Ih|UMB%dKIyLMPuM0RMKy}GYMZ&c@uR(XD7DZLd9$+J`)Vvxyg^no*jBhS+ z5|bml2j`=)$V^TW*$3>X4>}kA&OCIjKyxEJPe4Uw0mzm^Fia@mL1&O|q2XLc2(4fv z9TL2b1koSDgACbjXxp0JxHGkJ=lzpVhHe~T8=(w^w(UV@Z@P0&s&h~BiQ`|kOP!}# zC$JM#ArE?Xr+W^idJZKIKa+g!1*zvn*7G98bU$e7OgC*yHEm1wj(q8nn#Neu7)AKF z%{%PmkF1GQYBphPGp`>sKP#OBqw9uOpdheUrcF4R&m@tx5KZTo}mLzSpZv+T_ z^iqIgqAV!rbKvar*tFM8*UNGZ5VF)>$oPHeq=xb+$2_=8oKn&4mCs{^%CfXCuh(+j zn!5!{39gFsVR~vduTABPQ^{#IeEA(aP>duSH>%HT5T3gkkW3g(| zblnM4v~|sVhQ`x)eR{v{T67fW_%W09x$~nwKWa6B=XE5qKuFDkZ`~-m^dqc)LmAYj zb*$u~{c-;Wb6{IfZTeQ~`m-*Wq0gVUa;g`re=GaOt*=Gb%E#I_i*7)|-*gUY7OQ@% zYpQA;YpO=ybNw6O07;574shANSiR`f&jBE?27wkU4iN-)D&&)(-#S2vWIC&AuVZTV~ zF^F8wkuJ(i450Y?6wA3x;v8ku=ouYh2#StIVjz7_hA+h+89GA-(c<$I zSwWG5una^CtP{$&Kw&(oLmkFrBy{3TY?p`gtg;!y8I`Tv;OnYvye2zoe7VGH0*!?Y zqFw0=x`bER7!c>ELL1LPJ&-FDt()@9rC0&}%xxjLZca%oas?ceLoY)BM!L9JurtbI zrIL+;(HNctiaw>>t7-*2la*(>8YPTPOETXn!lqfoF8I}6=aBc*Ic=HWM5^cr=;YT8 zYl6Y$xw&xjHRjw5k&&nV?Ne{q;?^Z2%sv`AnXf0ajAHnfwhb}(zG3DYjyqR`Hs~r9As2&jIE+5Vx(?Znzz|6-b<48M=4k-f6b=_?Las){|1lDXDgh z)sCfW&!=k7OSMn2+Na_b59++j-D%&Jly8gV>p_$6n`t6KQ@ z@V&!-G5AHrzd8Nqr=|Lnto~$t^ntE5MU$*Hv?r#LzU}cRAJ#T3PrV;`HzGCnvgTf? zw)cKJ+i_H?JsLm8Tjo#M{RwZnwJ+7$C$;Wmtve-qKeP8I?fpEyIc0BtFPhl>&MS9b zk?dQTeM{25wsCnO-O`(C z>6KdgSWBN&%cXxL8~TTI5UaL(&;6@kc)vW6e0DFq;YguNGnL{%A0ZMdoktTD<>UHC)KjVUsUVNJ zghnvW6hv%5Pqk7?=mrZ6-e-O5$d$+QfDSI*ca2Xg=!dDe?h$jZ@t5it(0Bga>&v@t zU9_U*nqf^fIOSQJN;q%<2*oMfp_yu2R}aC3jgeXPXF%9_OMmf6$*RbPT@? zTEf41E;2Vi4WpUf=w(P1VN64Y9b6LGl@&o=IJRKjezjx!6VHxy)hPl*%UKfYLE{CS z8;C_n@g-8L6|LO>J&~$fwjn|>2jwRk6se;~f^qR0dBis11VuD36P%laJx>LshQqT# zocjuiuTY#Nd^J1~0Pz)^yAT}6CELp-6^F-bSaw0S8FIiB@2-!>$SusJ)Fo(!OiF%U}l3EpEXEdu0QG(d`>5T%$HjJJEV#JH7HcbAc3g8Z@zZpHOc8`PJi4CFzl&M*R-W-+7id5njTiu z6Bkx1Yt{@t+pzKPs|+50!hF}s{QazESA1x-zVS{4ERa^7W%aw`qic49!~N#ijj`o^ z*04je?_~C!N&C)KyGsq+E!p=l`<|qI&stSZ+Jln)5VIdj+7GQdYuXoBr38n8%IbfP&K}zc8sT-%3yCi!%v$rSJPbT4xlmq6VWlF-Er_=WC zl)Zc935_%WbwC7yQfgPq(FLL@lX#TbThsP~DLcmLFtZ;{s-LwoN}?nz6xR~U1M^5;`RQ0G?bP zi_8%ZDE^S%Dlc;W@f^2d5D1RrJZm&#sw$B!?CRT4UU zHoW~Vpp+?aB1Ozf1Lo0xMG zaWGA*8yc7F^oM=tXnN?ZiN^{TCM_VoFpuJ)^sdV%H$aq{#PUT`R;WLVQ;eiEUKlPt zrNjrXdqz#-U!h6j5WKF8ZuzHpRuGFa9n^44hr;fDf|rH*qEi4=R6!H48AA!C+WN4U06i5SpRZjd@@^d984ny!M|2OzcLvMtr;M%mgPF4U30hTvO! zg=5xvs&X9qBbhb!Ceb(IJ@PJ*7bWi{@-C7WB#&faiqI#9C%y52jc-Acr!3UzCRhu1 z)AY_GBY?BB=H>!(f*Uj1Iv><{Zy&gIAYn}ONi`j;rXy+Z_yyS)ynQOs%5WYg#x_CJ zuupRBXRiHj1MYpmE}kFbObS1zB${mSt#s;EUO^o0WW2N*tj65V>jhD{eiy!;tAE zKEC1>UF$TxWO4p*u`H(9JHjwR+hO{>jP_nAb=3PeYANrQ*j=Kf#ks?Ewv>bHvW49m z$UYnMlO^7CBpC9?BL0cs98M5G29EOw(|*ne<(OM!4uZSI0W?wkj6BZBlwX|VY=Z*8 ze@fv)r^Lx7KT7+6Bk_!BQU&kbFqN{f zK?h#45k7)1Bk+Je>?9?<#+>c2GGb0TG-azHh{-3M6h!Fa_g zLZ@K&#OnGO1elemrfu7r!D<7a121WBUUjx6##7E-l;x;HMN1Y!`E_d*21gBVmffAM zY)Dl$NR>^jvMF8JlB#TxDqC4)YhoZ(iDO_E+mpl*+yw33n6`JN>|HC}pUtw}SwS0Q zZK#+exlJiYljLY&j+V5eHRWiP9Bs_emI$UCM0b2^K{zfLw-yp2v>!Rd{3DDqAf_>a zfI_F=K_nZ?!cz`wG`Nz`;7V>O`uu=4PjR~1Ofcu$sAk|dwCxl8D^9G+%>%7styBzz} zUCZ3h__`w7Vze?pX;-?tbR($07d0gliTALLbq#iDbeR_0ccf0^h1tmGh9lV@Tp1B`Kh`=Cf4HdaR?AH4dq{4?M6Yt3-L4 z>@#b&c!h8qcRKp0EPDc^gakJaYG`zohPe~kX%V#s`qq!IZZ{6DZ#RY_^AHa9D3+52 z$h)hW??`N{d8kuobIc7Zh*v(!LuJVBCc<+u1(gq=Sqh&2*Yq?*9w+UynEfLP;~ScY zDcKDfC6IBB><~w-spDdbqLnSHDI+-&kD3uf_*vnxYx%|<;$0ae8&G9)Z`2{`qG`_4 z9{(E@#`n0d<$dqF-gg@AG{mcb$n5Sn58OEL#-W>s-h2AabF8T^RoS;@sIzTHoTK{9 zlQ&MjF?Mq-K89C~S$s8fa() zzH>!YUmi*|AL4;(8jqLNw54mdrfRmXyeQQiU^NGH@eVib_MVizN3!=adv8+xto2ZH zsm+hr?Q_`k|1`UO$ta!&HYzHAD!PwJL!<5b^yEPHqWX`PgKZcw7Y?jKSi9H{>_o@DPOm^uYA1}T};%EcsffnE3|PNQW~Wh_|q zscQ17B6(j9O_#Y0fgr#5ki4H!t>F5iI3cE8zmtLcPCi0sh+|VAeq$lT&&G-b>xpw% zFRcIC&_KF~M?m*l2b3fL@6NQVKjrF|T)UZTcha>R56D)%fw^1L?(USkd*zVi-pAbF zl6x<6LmT6uTsR!>#nGs=cQEB0{9^cL zCnWD_<~_Y+U9IshpSwjU!M%e_az-JwN_)Cep01Tn$uSyhM{@W;809Q++ zuruY_d4KrR6O!vNa~)2)4x>`&tuhw^u2T_0gc>J9zXzaQN-(C7NV!C1ab~WRm`uNM z_28SEQxJ(!;PknQBQAX!VmK4)Da%?=83OXkf+@y&klrKAG02!hLq=J0HV2V`IsOFy zisOa~8X8kW={v7wR^3c!NGvx;iJCQqecl|DBfXYAD@e) zAxFSyLmCZ<>0&At_4zdvm0?F-grP=pWKphfFCVF*C?eLawh013%|QFg1~s%uIYo`L zON^h^&HQp^UntpjYkpd!{HJKU?-6658SJWQ=zFo^iY8tyF|JK&`b9y1XDG3ZMOPG= zz;cIU9w0?=xWo3k6{ath{LMaV3z-)yaT|^0y|P6cnZw{Hgnreus3p9NqrVu-$C*FB znm2z=W_RY*(yFFJEk+xe5Is&kZEEiPdh%*%SJUO}fXAt)Z{3gf zg8jJOu`g(E;qvE`P z09iPDhVtRmC6{Ub5rx6b9CqygiAP_Wnw~yKuLF1$kCHb?5tfVMeDpHx+K=+@T3ntx z8-mqV05?b@Ik&@_R5U4XP^JmWJ8gpe5gF261B2h@nlf$6EqS;hRG8}!SSxCT2G z48(B7LRj`t*331BN)VOdQj%mW($G8Eo0~2hj>O_HRpZX>tHR5WN=dFQYN#e}$SEQ_ zgs8cA#KlR@ImtFyvw?S^E3!U7=^4Y30QFe8qoT{1BgVpQTzsx!w+9uLV)(B-a&!Gk zWhE)$4nDzjF9xod#SIeH})om?vAqdU9A0pBphVI!K85TVSUT{j&~jJIPW;)xNy0dTa<_HPO;8C ztn-xQ9b?|Hc-2F)=%ZW3AiFzawC((w`|;y>mVS6;^_y4fS^W-}{aG{S{;Xkl%DEet zS2+%?x;CU;9Vu5wVpekPVy<0D*DfB`m2!3cxbt2Y>prBhJ&3qnb*pMQcIO&Q-&xBs z$u+`UBT4>&@ZMdYDQiY@^)pw0($&B2vJObDgUod>={ksJIy`B6V+tnckWX&^tn<5_kOks^akaHQ@ru;CW2tJX>S1et z;-zG3zw)_%O=>-|L;_Id$OCKWOFA)l6^_IC-5Qo$$C&F_l7G;xY9H>k9%oHYB;8NY zm2QU>uLAwm4aY&rb%?nRC0&PdYT1)=!pKSB{Bfz#Qy z7ZjmzE_X`L*yx6DejomCa1g-^%0=TIQI6*F_(iBvaWM&K>1f3X-0q-{{T|GOigWY& zbWqCRzTepzq|WU_i8{#~3P$gy^>FPWGf!QL{ zj&X`IOK-ydhA!LU@?Q0sq`K6mIJ0!u|0Z%}1s?7Ra(dFv z)|9hVa<+kHiJM{KN;hD=asB3X9A9+SECt`VniTfa$Gcw=_A~wrDD1AiZM|iEOL$vI z)FvCBSRPm&SQ+}si65Q#_~gBll4~zn>v+^8%YARz;}+!dcyYL(1}6pII*@3UYTDug z6;-#~o@jliYvrlr=6(3}2IJ=@36;4HaEqU1Ps06K|bDm;9K%v}{>dC+2n%F$$7CSQC6lchrCGR!z zh^bce9G}n=@lE1?CQl~sZ^-+fEv-Q3s%I4}m>13S zSS-6hQ>k86In||k)om8%nnCkU7^~^}Zq3`=WPvVN^A2@TC7L&8Ja44l<@i@jTj+-+ z9_4Wgf)CvUhE8)BLGHdGsbcQ0N`2pet6I7KOZ=CJnt;4ba|Gi!?GeHQDWqY_q($=I zWKxUe$M>^$Y>~QC6FWi5m-C|#xEo6sV^#~~@V87cPvn3V#{ENx|?wtUd2aw3}z7|pvk}g{d*KfwF64@PZ^;rhp@VqfV{LU7P)dfRtma;@@8Ozk9?VDRPJf^$>KsWpk0}>1(peh5m}G0@;uR8o@ZRWq_-o;S@-r-l#I0Bm zv?iiE;KQ=XXk0Z^C)LkGLuXR|Sv52y)z7M7TT=g7GdCFVn<<6f(q2~64sD>(|I2Lb zKCcmWorT`gb2t(HHAVa~8@s#42rXWrw**z)rmrdDm)Y3v;c_FkU2&hS-A_=+x zN+O$bJegz~)~d&fM;S97WsEqAEw`N$b)WRM((7#09Vf>#lio$4D@!)!XjkXVde3@y z&a5&{&gs)n(TS`e9Az%s;yA zaQr)ZFfNzw_&g2An-19_JI5Wz>0UJM9Ho2lc+o`h@#2Y+<0VddE*W=ClpZg2I*J?- z$JsI?BiZ$?)8Y64|NJ^G$)(5L_%FwQMUi8lLoWNeLzW_*vtA?4yTwR>e?3Jdi{(bF z@(0B7u~-#|!Q6)SUITnpv`0Vd+jTr^g-FS!3 z-@yC-KH4$jIPR1k$BSg=@nU&h(s9ZumwerQyhL_=-ErI%DV0mV?syjoKM=VbFM}^y zD92ZVuaxKOhA-MFCvG`>%kz9I;EUGEiR+O)sEJqh0#?eEfIis==$HL~RdN+zwOkEY zBi8^1Tno5dUJh6%*8$ec^?(g>17M@v2-qYy0XECcfGu(hV5{5;7?gv6 zZE_o6yW9@gA$I_F%AJ5K%wX z0Irr-1Fn(R0IrqS0;1h7I*+eMKUklY*w@<~R%V0cL0m(@1bxon`DlDPJRY45C!(>* z0T#4(WUNGd7Kek}^nE|CgzH<*HysJl#~qGDkr{K&@h+Y|5I!m4GUGTLbH(;?;&jY8 z=NuuU2RUR1o<#X`MR+pCC_+-D*1QaNqSA0V9A_oNeb#Nbz9`&YZ`8< z6NUBIo_fU-wf{=-REnon;;9Tzed38?_8U*Ai^viF;%ms1!)h~DYK`Hx-t8FE>0qt8 z)`%fHfLElE(a(bLQD~@tMx*vKLJps?M;b!C>$0x+^i)JqXfS4_P$)VXO@u<(lBu!8 z#cZh(3Cj@${?T}7HWr)6dgGDA(dcaCaBMs}Hj{N9Njw{sPe&5*Y{e0)|Kk^@!jp1z z@^sdZ6o+Ed@d#nKbL3M=J}75mdV-Q#1y)ZMki33H>5=32nZ$kBP(+9^mydx znOGuQM!vCWIL2a=laaAR)@Q_zCt_1m5jpFQC&Ef16qPS#%OaC9b5w!I5Q?RH4&2|($%o zWK5wR;R%gJ#>YeYlb=6LL??57!WRu6J;j+w^z@lT){-K}2Pt$PFV8hT6&|B*Q)L!n zdYR3rSEZ8o;Hfa)lTtNVKArxCf|-iRH<2$ znir(5jMSAnuSx@&G?11C76Wz3k}K}T+6LSz7VDaD^WHCYxV;NfLq=-&&Y;$~Mr+*q zll|KEBii;CROy5!ok&Y37A4Pu)SQu;=Vevu(4>yE)WQ53Gg9OH?o^TRz1!UJ_E+Be z%FPXG^BS#rO|nFj8W0n3LF&m!JvRepZJ4(vwJ%63Gt$bN8?;qBRcV(d?Mh3#P^*SU zRIhSzIn~*-*w#*YwJf#<>E86)Qpdm`xmMoo+f4V~yDK-+y&EAbSh;O?+xy9<6?ru_ zBNN{u-889gv9*ovO$g{*L9XE4rf#~|v&KLSl(ZnN%Sh{PZP3;qRHZ|jbSNzyx?kyN zX{Cq_h}hi1k}L*UX3fUEPzbAg^L@!kz1X%wYuiN0TM)gy!*J1upeE(tFD*6S_c;1j zQ)q9R{XWOHc8n1|HZJOCjFp=Og2rTr7%wA?KZ)2ZjLF1H5BfYV6UusBZgo<&zdbP zST1|sPDf&^nHnVt_KQtRi)Xj9nFis!{on_pGy{nQFEC2Knjx~Ffnb9mj1$0@aXvYr zBqIuO&%Lcgvp|+2p_Acw1n8SToB{qHCmIk35#U||iG%d(DL9oe9>=DYc#Mb+4?Ptn z^6d?=I$*ZU@~;t(mJ+Ikr&)$i_~|6W^w7(x6ft;7t|N(k)6s0nk;9QmPAbZ#!sC%d zB9g7;Xq_hviINriNZFzzAR|Y?Dai+;lMyrrxQUQnhL0x{)lzm)3Y3f}!V@gfZe==t zChHxJOvWRd4u?^uGKq#eN#2%-5}2etv2iq#UL%o*PJb2RYtd$m<w zg39B7%1(gU=cOCE_LwSqAPP#WQHZvUBAnrU~{XN6K zgQ4f(VtW_GZ(VFi2TNs9I{IoIn_SUzyrsNw@t!uN^HIhDj&?k6u=j@0WTu(<5)5>HpaV8R+oSrxtQG&5kL9li- z?T0%*AP7AZ96Q3!J>V*r-#nJpcUOrB1hne~ZJ$~`VqJi@N{l;w`9 zW!F6OiR-VZ{(i6)`EN*X$KG%y9CitV^$qPvhP~^d+;<&Srtw;**gF?^tza}Bl$G!W zux`O{JV;EY1+zd#R&p{H8&?KVkGyyFve+TL?PhDmE8G0PiKKDD{jWNHTkh}&t`2K~ z6{@dG^L3@AuKaf=LdxEK5{}%jn-X6af#P}>E>m)b7NQ=)F4;K?QcOKz7Uc7ibIvIj zoud*kMVo^}Fd;}Y>np{xCv)46`icx>5KYV&ouvnSW*Q3nCH@+Q!b+$m=wQZ@+_g1~L>bw0n`fqh=>xS>Vtgd@PZGS?o*`F+1tf;6AJCm|yXwk_2mSB*B%U z^m!K~UPW`ob0zO0<_97+>(5W-evfeQ$kUh^;+ue0P_Jbl2t7DVL`J0G15 zO~v9_i5xWP<&?w4GVOKorgmP;I%l%Z3t8tGF&E(t&eJKVfMqg4S&`Q}c(M380M_jO zz?HezN|Mgx+C^W@l}p@t*SGBTxyy6&L-Q}E+EPbUU$^G#PD|bSjZI&fH8!yd_Qpmd z!;fF5idZup&TXcii+#bF-r!gagdNlylUSo~(rAB(wsNxGm^Vqqm-~-!*zud}Oj!B|B#+b(c~{ptaz?cbULAb4Bcf70>43=^k)mIy4q;@~1F5jWn?9^&@s-9h%XIIjB*W=UU{@xqE_svV!F5N8AdbZv^ zqW0`im+w?-c4;-cRL_v+8A>`A{Z&`jCeJ6myt7z8I(-b*S#H5WBO!%*{$>9>x)@!M zi={deW>(I#qVt>;AKu{N&bbgNNAB=_6`RzF^$dQ*==HM4kZ%du`|K2j;6S< zFPMk{hk*=3yp~X6<5*;}&0Yb3nIEmLLm+!TMiev%;`ypsay=>aWYatLMlV5 zRHj5uAA7F( zSWc3G3OL~JyX>2H&TqQDU27arrPZ3WIxVfHARm{H%x}29U6pz?sV8mx_@@?)k3y&^ z*{42%4~I{U5yPo}xBw`9b#lx@b(k7v?i zYYi9%a?S1%-STK8!Kf%P!n|7<*yn-dbk@Ir@;qj$a&R~6i@~1RqQ2nYI(s>9<54c6 zZk$W*AXg_qwjTYB^-DhG0|5_BpkGJF;=o(cN!Cr3S13AR9A$+)4Xcki>x(O|BK&THrKVMlYIOm)Cj3{5MF8>aj_^Cz+^h1k`6GidI$; zc$GjB!1l4&a)&v~wjd??Y@&$&jnZubFuq)G#z$gR{#)#1qRY0$_Fih)R$D73(UQF3 z#H%A8(Qs=R-eKZ4c=HPw$AUZ9lNinHN#Krz5azMh$eu)@>ipv$Rd9Qer z?2ivIF--6}CHQB!=_NMAK#<_UuQ=3VjuMAu9g;?1j-rZnGFgakJ1Yj=Iaa#z7`nU| za3WJUP@WiYR)2dJ&*kZmn;4Eur{Z`Lam5g|#{Gvc85nw}7?MX4L0Xm^kA-F8HOInJ z5Q0U56SPJQc%F%qgOT$&Lgn6a&TW>RoK7UL=E)|?%5!*;Yz13i-p%~8<~P2*`K`_09=txNN-H#JMOs?1 z#K11t`Tuv+mIn5Jq0YbkciH(N-62XSx<7jo9h^N08lLljSF_U%@Ek)F^Pf}9a+)7;bpkur;bZ;kFcr^x zT%DjAIU()T<<20pCA-}A%iCYuab?GMj$MCVYhI@{KbEO@?7pMKJ-Ark{C4wO&8g}4 zF1&N$yE8Xt)cUnr{o3S##kJd#!jNBn*gzm#U>`8#xn*TopYo&!5;;`s(cI4tTZ)S z#roCLyaK$>HQ4){HEbRAy0?(WBlfx~ckg{aMDChz?Vr4m@$CY7^e$iY*Iq3JdL-{P zw}!P1M>D>o_Z=>8@1j4j;BU|P+f{$3=I=}!Kde1fEnDz+W&B;Lzen@;q>bPG4l2R{ z@PK~07Zrz$574-*{4{!m6;m2f3VvmfwS!-TQAixj8aK$uOc0;=ydfs$3##@h3Hz3$ z@5P9eL6w{#+vh5$Ty$z=P9;sxL-c`2!44ua$-$Se>K}zJ{HG%v|}X|&a)Ag(D?7xY*$pIW+An8@&<=2{GQmCSDXqMm)>Oav=ToL#c? zVJ`b+8UU3Xd5xl0S>F*Xp$|<>0VOeA=4{ou$jr%DSdkCK5z)kQS?|#^ z(NoYJGK394@)lpQBEL8K>MG+u&W$er~?m2&oK91WVo%k&(j;q|{DNdf!^sY;+;tLa&& z*_x@@s@81RYPKh(yMem-y6^A3v0v-lrq*v$1KYL0_N1F}g$ospnTkfWqFJkGUZ`lz zRJ5uUZCXWJYA92&0*KJP9vtA6Cl@>`GM*Kw@XdPFvtIM8PqQC#sH|D=HfOxes<&11 zwk~+vGTt`T+o5?oQsIoZn|M$oht7#s*PCbj2Vf;&<&cDV;2U3 z7b3yfyk~->#~bWs6+=1Q!DzyQU%*0> z2vun(&`Dqnz$$2=&p2zc%sJY@7tyZq4n)D1_8Th&CD$uyF=&S>?bM{5Y2%mwt%-)Q zZ`}a+C@OmR;*^eEa8sXQ7wH0!9sq<6Se`+M5E!ua>LO4*&^@Lt09|;KQo=371+&QM zl54JH&UKFEsTg?)YKho_`pU&@MUv8#B26i)LD^i{Tq$4MP};1q?Yxn74Z$M1B^`Is zpXogkLb=Yq8j%p{a!`j8if@QhB9~CD>=YmM5N}`~CH5zj&fc3qPRXK;9#FEYV9BDr z>}A=XtgT~zAJkpc32rg+&DN)O&wXFB6_G}?DBOZ_>tEOhKk;sIDZPR98zA%xO1I|E z`j2F*fAO3|ea>D#xs1hhBdzGab8hlO*+M?c5WF-yDh9tH{tHje1og$0An+I@KjUN5 z;0Xd{b%fZ6VORJ521y7E1Gbdj#X znQY*66hfg8X{1M>yoarOP+7_bNEQCnG&JPt(NJh_!b-T+BXl8}IHT+5#k2LgR(t4V zByk~vJdM~;?+5OcbIb4>^$7vh#x6wUI64&LVu+J57SX=}zu9&{@|Ilg`Wm6d{|SH* zKtmt8i78pvFY;&7ln&HsHN7{7KO9kOwkD-T-*TIb3%Wgzy(U4)WaDCMXX>Kbid9!y zN(y#rts8D7w9Nn%2aH+?h&7F691#y6XjX`w4CPiS&ynwHM>+mV~w?p*Bxz&C?`kufN+IeDoCQwvD#~ z+TejtMzupPro$)ILnqb2lWN8r;NUN2Rm1$hHF-P^eq`R!`8jR{|9 z!LGGKYgtX|QG4!$)ph&SmVKXeY6niJEzsg`#2YoW&QGVix2mmM)y8dF0pSZ_0yV`w5R1>b?vo2rdjx{7@Jff9uJ|x(l|eQ z{Y7>8DyBnXs>xx42+5?{?mNoe%N9M}Z`E8qk=my&TcuX@X;poyr!P6g$J*R$Pb7zu z(~HX+uU*oeHGz588(Y(!X8v2G=alQ)-b_tb+N1xFIIt}F1T@s#<;hXZ-BN2+Z>K7C zLj6c`lTV8(wIrR0$0M|)npAJUD)pNY@_brTeX184F9U_*ASZuSQd07e0g-ct4E`xc z1{PEx*iZ`-qx(UrWv-YH6$LDy$*|Ds87WW_YoH^Ia)b*sfdPaL%}|G6Brt4UR-uqU z9TwpR%h$sA%z~W+^K2Q~%4R&7!^ko;bwxbX2r9F#zyP<7XX2g0n5s=pDY1*# zDgiuzF=QF##6|L|~QuwHFSfc98q0$gw1*uxq;rNkzJ1~GeH@8I@M zP4(lQx>WfYxqnK)q*2s&v&G?)aYGEqy5}A8b&5`~sr}Yw&QG{4s#D3b@pvgrS zj#EqnCQHM)9Wi4e$Jz>TiesaniL|;s4f?j7mA(wTm^&uC9apLGM*g_rRj3 z>RU(W1JK{PxmK<3QAQeYXR2-d|Upr_@HZ%({1k=n1;4rsLl$qH;mUZ~xisoi|LLaiOv zYKM~*Y_;#oE2%~{No3$Z0tweJ*UiL-bLNv7gR3}HBL9Wjb(z|Aw+3&YQ)>@swFikz!@|0gATvKCgGd;Ebim_m(Nt#dB5V*Z^2b0jmHE4G6Y^ z<3>*naS1=YF?uBLj{gh~v*rS{(H%uKpP?=(BNAf=mes#`=#4|5>48oy(3y1Ks|Z|8 zXv+sP72pM2?yZY!HfU>})^!;+GhK!>)ENfWX#GxzS}IP2KEXQV%szo<*?gH!+PJvQp_Bf6;q#{EeiJ9Dk83>L$?UoYUl1Vl+V&h?8?L zlSN}iP6x@f;#+d^6M5wLiyY9$ER3_LmaSgaw03@lY+RXZAB8msaQpFp}xh&R;{rwdGM~z3V^F#!s-apHneE<>u!yw>%W3uhQT|swpVv@?f80>gJnN zsY{b+*YOgnIbzu0<-`!{{DL6{MH|uC*}}}D4i!-yemIHP7I)&wJN`Zi@urP$=#+3okit8fC9}<&e8L~g`d%f-II|E zA#TE{jGVCBWbe9fBeIp3(Yvmw$TjM|^N6JLGFo6`(!E$+t5vT}mOY@8)}e!6fhuRM zd*HW~j@}Kn?svc1(cbl5>pQKt>TmB;+lIBaVb#-;+<*1L)eBgE=v~KRFLw2_&dR!_ zZeeKVR<|l`)ugRydgH0$VaHcG zXmW>p%O#ghJ&Yu2<$3b9Qi$gbc_S9PftX7#6<=w+(m6*v8;fR5iGszjf<7r?lJFj~ z9fprJG1W_46zL!cF}aEF#_9e3?yG+{^xv*O|6<$r1_$)`o186g$26>QJr;pr5bC<`WPW8q+NUnVJEh&Z;$w?fqAuT(cq%qV>!U%_Y#DT4 zuz!`?Gc&&{sR{i)@?|tAPitfYLS~!IMmKYd8+7cisIj{dj}6raZ;vzer>rkV7>qSf zIq>J3++-iT_{FwHiP-F^;K;G!} z7_WOUZk?$LqvqE8T*+A>u}4iwmT0T8 zf^CJBC4N0*TU}qGU#&tvs5A1i-fi#pkmH1LN~KtLT7z8ct+^R)TdrTC#f%crZbp`l zCE6{okA11O{tiRw#j~aa#84MX&=zUVrUh3Ag=sC=L^>7Y>jFf-u>^&^WHU@3ktN{v z11wmdYXmjqQg0fZmIW^w`^s7ZB-F^3ot$F!HM6DIQa%mZT}wpRw76*~>14~ovJ6ed zxGB2LmPRhZ>Z8o^pxSV22z8XfL}VhS%wUQGlb#}rR$B;dR?dkap)$Up zxdbgid-Z zmzMIX<}dOya%`10>6WGUEBo z-;ZS)H=6R|d;Y*Rx8`4=`Bx=fV0>%puJtcecV()()ao9s8sg$2_XY-%yBF(Q7wR`< z>Nnj|el&Y$!^b<)N1xS>hSK3PYJF6zk7BPBCYhe#f~Py<>AqQ>qnMqawadMdIIxw- z+E;aDW`5}ME0{idFF6Cxx3(U;OEqiWq*t?ukq*Hza& zinAkAx8t_*(d;MdemR&vc3e9qr%#>LPATcc@2PcjTHV}L7wUB7DPuRG>gm-yy=nI2 zZ#%G1-JPlKR;zopYV4J9kt7@-r%GRSvX>vs)DPZn|7hhM`Qu4e+{v_jPOTr;>c_8^ zUM;;x4YVs$zw1u*#|@u6{mbXm&z;boJC!~&uANDw&tFpOU(xDcxmwDascFH}k@0k- z_Nty$*iHr!EPlLBs~4(!GSxk5^-8T8EmTT%0?4V;6Ge-`u7%+4OmO#|bsum2B=O5v z(xEXebS^!SP=nK2aQd5GG}QU4=kHOx{h8qYPuhREGJWhh?HE2?+z<^lOKs?TMn_FvwFDZn_85-fL8qbHaj!!H6I?WA#SiU@}b$N<9ZZpc*1i%tb&~ zZV=?kqBtS^gyIA@dtfRC?4dPy`-H}r;aE**z^n^LtIQ6q;G#K-EHZHzIOQR#;2nTC zy@?^2{{`F@>lNnOe<|M8u<->2H&Z3YwixffI(+S5#v7#Z$kyn^Qv9N?I$56Iip2J? zR%}2m>{HFcGlpCn?QA8}`dyGAzjEgmb44jCDesbAH0BhgV5Xf8qDUpu&*R8Z1-AKEv^d!?gpG2L+^{k;5X44L6hczYb5P0aj)QwK z0x?=eRx;Fh$Y=$PRtPL8ll?(CHrdTQk@i9ayCd}d$uip;1I^=8CeOpwqOqEgt$sMK)xP5BW9*mdD7y= z&6_F*;bkoK{c{S2#Z*=Yn{X^!&V2d49CN3Y=@}rbf^%I!_kZgLt$uUhje%4^t?tsQ zyOL#4vf!4URH+^syJ-n~9AVz6J|or7x4|9`6l~Y1(ppVgo0itH$lUTZRC-{C1xtH* z(Ko5mW=$d^aeDNoj07=l3yeF%1YutEjjFUslQyNLO?q@HT9q1MY!o`WdC}LY(t1sT zcxL@0Mn?+#ICd1Y0Ie#p<1gS#(w3KOCw?rk%C&@MhkZOkA|s~fC`nxQSXLz>Q_N?s zu-L~cguA6iM!=}TU3jGgF$B&s1r(;PZSJ(qT9+k66P7aVsZledVzlxiaE*ffp@l8f zAeKmB&r7UD3UP;s2*($tgBWv{Km@}WqKv2;MB8#03@z1jRvV(PF_`l=hyO^gPZ>d3 zeE63h;iH^X9Tlo{*1I>RKszur^z^>n>(}w&UversKF*9AD(p^o}d!hUIsGb%uodLVX5+DYyTOkty}|(PdYSvXng+&q2TKS4sR87b;<7U_P4g* zEcs#iAC|xG`OtGq{?l`ReD04Xel+p(XFfimJswgUU(y<1B3=4AESvxu_Uy^3#%itQ zF;&{3NjuWgj!&E0-+u9}7jF)0ecRRM$6)<{YvA+6A+~h03EbU+r!C`YOLc1P8&uCm z&9gCW{IDXrbYMQWLUfvVh|fFoxD|YNF>=kBJ8>uGxM7oTSSW=b<-#uJ=eksC6OWhJ zdmIFEe+@NLU3Z-+mlotma+OOjx#wsvj~ItK$60LjU{hRJc;EG$+g^`Lw1z5p6?4oM z7NN|Q=cI-ZbBS?VVUMY-fcQgND|ZJX@(mXjH<23FjBwIQC@%>clt|q!f0v$ox4& z4pkgESnLjGE4kP7q#j?QF-~UBdp&$AK~y|jBGa)lO!5B$(+lSAi^^Y6g`81lA~ZC^ z<{iqvCqEyrk{r_IEOGko`f3Tgag8A|J~CTBYFO3F#Yo2q&(t9fTPE>0%@1GS4};Ii z60WoFNxHcD{@iQK`Ds-xv@M&iLC>t!4J9G~rt80~VSebF`;(7jdG5{rH~Ld9ofFw} zzm`-qWvzB)#)k>4(_6dfUq+Mm`=lp#?c73bPo}o#W{q0guhsTr0j8z>`Z=v--9pPy zre)|(x7u<*YdMg73elnU`^x24=3}aFK=Wb!#pNA>HPYp8R=!aQ6Wudf>l&?fx9Z=c z`S+yR59@hVb++K0s(+W}-<9^$(Hq{tf~P6tX_|i~_4v(=w^pd0Et+Rbn*HwYG+r+s zm(iSn;j&l22iSEwEMrW}tQaN}SW9;5Gu$eSNMyt>OFF?@ERpUhq!Ty{Zh@nqc3h56 zjTr^sNqMPQF0tu@;yHgmC(%BJSBB z*rbxt$?iUOOh7-=$?g;Kt%qZkO=~5m`ODwi`I!tC)7~EGZMAU3`iUKEL+=GF@w1J^ z**Z@5gfBJ^V6`9Tit+6-gOt|{fdJ9JGIsI_j$5ONx$+DF+Caq2*eTDGi_AV`=`;^^ z@P}oW4x{Up2<1{bLB=I_%*`pP;gIWC+*; z93y5_ZZccW-iC*j>oIXIiBeCwevfKNq*QsCTyX*k0?Wzo4!MYMD^2vMz!fL_VWrsI z&FOXpVil&_REPLK2G)z$g2-Tg&_vr>XSC%#Ou=ih!bf{(otX-3&~v)?I-%cn9cNzk zX^oGwy)`fXO9}#2V!1Z|D8Q4*UjSZR;W#V zT2r4|+Xo6=-+a9un=!Qd?F;q8nfl>7&#Lu@wE9EI1IYs*t5|H%gOdk7UDojC@i&gA zJU3-+VCT~SmO~2gdgbNHtFx-qjsvpNQah6iFb%KN; z6sVsMTyG)ukH^%S9a;^H4Eo&A6#_`^L8gCimShjHBn$pv#ve>IsQy09-!_9sVgSN+kkA01OY+cnSjH2d9eHC~2uE$BkxBHDvK8AX{vwwK@{qYaSZx8?U$v=DY$A>;T^p_idxm|lQqPCsV z+D?%O1fom`PH-+>M@H)4cBx=MR9E}E+tKshj(2vD>47KIj{RE4eo7L&$1pCbyY42u zqc3{GYL0$rLfSLLe@tk?RzwQ~@*yjx4R`~v^L)sT!`D91U<@ajz_GCF%&&y`iChp) zgPvXHLXF9yo+4ITFriDt7J>^FTd?v=#1=~?r;1J&Lx0IxSjdqO3@gsH6Cw+f!MC$N zOzm5U4NBb>D=;$BdpI^4HT!zz{E@vB}EnSXg zN*+`BPp*7V7rv~wvz5eia6VGkJ23~G6;0GCLbSrQN~r0$WTXNph-q(xPM&6}qFE35 zFa-$m8bHi!xv5*o_&p0YagD@C%EH*jO_VEHcs18$`3Bbx@*}1a_GpJF63JFLPxd`} zBt9)$!4V-gge2meLHl1Rtec|VAs6G#FpswKW}GQog*daVqjq76*%Ta;{S{%LuOKE< z$65cBVbM?w6Ig z%kLLE+yQLlbNh*b`quetGuOVVO5HdY6ekn`$$;~rC7!jaXKm7XuLH7@O80&zhpMMF zx$o+6!C%ZDS3NzNrzh>{xwpIp+qk_Hli*d_YE1@HZql~IDhP!EurE<}>AMv5Co{%k zpb4+1Z$0Z!16^96ix{r0YGCVCGTBg7$JwCo*QB2LZo`cR3rDl;kB?(;`5)$sp5bV-Qp6hmZCXIlz+&gH4*&6OhE zekPTmp3AN|DF=}WG<%MPTM(I0M%9z*>Fp>8XwtcaafTAeH;N$jF0m@F0!bdYa#}|y z8|(;$(e{VHD83~ziZVb0mMNXDCKn+PWevG#rkINvYT>8bt15&P6tdi_5FW_7*!0eV z35X2ynHfQj2>KBHL5P68-fsyJ5L5IIjtE*IS}k`&nG4{x5sdF=Fuq?OxjZtzQT4QG zo;DbKV57Uti%0-per(?h)Ui#a`pCX&neQyS>4Y4$P4#ck{2S8jci%@PmKk;9gBC3! zE-`2q!DT#*8q2##`hiG(2`VTMl)|jvEV!6>ISwBxw#UF}DvpJ^ zA;hy}nWGY7EzQDz2|g=t@mFwI;1z_s@X}I3a3kQGsB5yzwA*4IvN-<8h=Ti)OGBbe zdulBHS~$rimwN?YW^!frd@dCy+RSReaz?xP#5MAB;?qK(cu%E^%o3O<9y8%7nTrY- zx!6KEKS(J=IJd~bZE(mXK&vp31wXtS;~pWl1!|LEHVYX-XiGViLzHX8f$yaX3fI%n zvj?b9h+mC*&>iU{y9gFsrZZ*XcFov8n&l52w_?uo&~XdZ!6MFOLq%l5ER?LE#u_P94%IYEbZbrD@3+~q&j3nsn zdSH%SMl9<+f9tirOkpbIGC>vFpg?AmxmFyuF&K6;VAl%gs>6Vc^@L}0j`pw{4~mDuEzrm5LdIMtE{2N;{<9EIa|ZnO8ua8-8^d6WA)^#8uW7dsz$bC3J)-$0F&Wa ziMKY)<5bh)2b+$QVOD;Kd?{Ppn92It{bWp@G0d~NV0JnL?F72bWTlfaoHH~555qNs zpeT=l+f2@KNY-uG;+e@-8ctr;16M$xZR40-Hfo=2^Sb&lq~mXcz2;gA%NBfX8DASW z2bL_k=c|JLg0JRk__d2^X&e1uSzg+vF*oMNX;1Jj6rfJLb>e2{4_E%-%J=&|?7Lm^ z(~3W<__6mR@6V?{p40Y))#j60^GT)_#bkmF3sP%FYW;rc4L7W+3jM~rt(|Yb^42S~ zz_Ckh9nxBdC<&Q%1*81f8;_*|HyUrWsx@o1nzb-@-`tk!`N6qN`=HvqRcppx;s#TC z*tfV$pSLeto7{_I-=WfQYlF6aSZ&1~`KCorV7^4F>%G~g)~&);0T@WcWV#ymv;1}Q z!>OTf?^pd@Z0km#9tLP@>gG3mlWUPDC5l-)AHY&Vz3T6vB+D#M2<7L3U40xA2B}Sx z+PFDc-S>(4GeW#TJMwXYm@Q@@za`gg*z(h_9S!cMNf-d3hp|oM2c;jnKlBML47P%U z9VEROPcIBt_F{cXTluu=IjVV%raedRk^M=h_blcg0i1!d;P21)`+qQYYsLF#KRo-B zj@zexyy~M>KaYR1@y};Jp8eI)W9es4t4GghN6)DKsOFER{ZW)oW-No>e=_4+O}kAL z=VDXv?L%)JVvAvF({`RSBd-pJU`7#tJpNPS@P|WD8HWgBcnScFG=W9SG#`Rx)zGkV*mzb{^uR;8T z;3gA-Z-CWN)|v{bcZhJdW*{W$mr5qv5Gt0`mPjqaS!%Iw7^(FGrk2<>Tx{nmDD@IC zb0pwUQ-bAGtGPR*6uhr^aX7&9p+|indKK-WULt008DtJwWK~#7j~HU+XD^4%ZOqh# z=X{oVSb@~``537g(cIKUYX+EuVx5D{PCd3-7UUKa8%0GNx$3C4u-vr-Yr0$o6>HcR zD5&USztwiJUl``>*`c#y+XOtuY^PW>Du+>XF7_KGS0M)N00k_`p!HBF)pjl|JVCBW0)qL%CXpfXU>Y5NN-mm@C>;RVGG^BVypRjqSq+BgFDRI) zekkOM6G#BWX`Gn?L?J_TqJLUX<6tbAXqS`3;`}k9h;_`4d(4b7)5E+QT=Aag9na0t zA0Ge19k z7F8P5q`|Z_c(=X#y@7WIZUwZpd)4-RTKhgqLY7*IPWSx@tORW%+paaRo1xVWCilab zp;o&wxi7g7aVo1dZ`Ti2Wk{-6s>fNY$AK~@;(+AUB=?fg@ToVRf=R@{fEE~FW?bG} z{l@APc3aZUM(kKYfw`vAV1$+_le&#apuS8?;xrf?jd&fL(u2w_NIe;;ht?R(V@lXN zBZN+NP2uGGt%CnUNm$g-^;z$Esz_I)hG~y*6_S0gJ;g0cgDUD-|DfjPk@ssqto@Uk zTSxw=_D8ipskwdR$F(2Tij@lK2~Bz;ZTyrxs#l60KzR3aVub0sV2J~j+T*3&{`tH z>Mo}EDr&*}h#r}F7o>J<8zv0&=Smj%_P^Qfb_`52Z?3r(3^O~`mTfe&r;g^T=5$-% zFBh=rdmO=QS`m!f0Ed`wJOH8T96V#u!#AsL z4&8iNZ12MkKm0gO5ge>z_?Qxf z&;@q#p#Mn+&oRZKu&$Jurfi{(ykfGFS4Sdl`NQg#98`>Y|d&@+rv4fN%=Y?UCB zh6N&vA+fx$AJdm7F*1`DxEtm>wT5lCpHrp1nzT1<{CEpdix`(snOg^+2elBa_8^_9 zgf801+`=*vOGHm$^J^4G>rLWZJ=&}C=*>elV$G9pwN&}H2(wy@Sjy($PpLIXEX-S@ zj${ghw+HXQ80m;6jiikqYmeTdy(7IFaO6a2GCUCpg|JaJgku7y$I0ypgj3=>rOakyhw^r!oxRID#F!I8q{w zD1`t`!{TJz_-|fC>+`?+MMW+NW;Fn${Z1DI5f6mNit1c2P9eZ{M``8#q7q66@atkc ze*HP_;n9O|&|!(I+Y;1`O}3P(%mvH;03wJys##E}3-&$$L=buS^$fRAhFe4s+~9i4ajtb$-FFCZ%GvGeyzda;X=lI% z?HK|3x?C{TA;4j0CB3!)!BYBS0{A5tOwI`qXr#IduywTyJh%WSogNqLh6~Un-jTg+ zyG0%bPv!ay8uh&Qou5~ z3{a9KK)37$M4F4v;}zJ1QIT~sDfO{v%086J)}p#ziauYtCbEknk{NgxK!6ulsNlDc&r~ zODM3n2y~7$!pX3L&7a(j4Vo}hH94(}p)`iyb!DJ1lGvI+Z@)A(!AeAa)j==kyX>== zV}KR##*EacO3j#|SDlwsy$ni_1q4M*t z+*YPe#apk$;PbhObmePE%e2995FpN52#+fZ@KOE|fR(Fb@n)BsT`8PMgvZY4t5g~_ zCe1TgW5TFKem~N;yt66)d0|?e@zeTFcwyhdYQWywilT=R)A2B^o!bhw>6$fXnEG-OLg!tq-{ zD88qqpyAn^k($4-bxV;s>&p_L9T{4u9J2GnoZ|qj)BU9;D)wHF6rwGWw{RCmkk+qe zD#zP%%o#^$to+Mbk|{1JkdNpYrn;AD(_@LX2oUr?ljm}un>L`xH~T*4B8Ma1$5X+)pOOXElpRm{8c zF*D)J-jb2gUZ=7f(LbXsv2f2O42Ug#sen)XKD{MYx-7p~VY*dEL104Jw zN$^z0{JZ4L$=F33mX^_?v;3%VVt58jLis-v0@;sXQ2fRYT1vb%oNgRc8?pOvkOU`n zcbhs>qc@&cn^tR0tI4PSJ{Mj9o3F}9tC&3!t#4G7p3$Ud($X^qdRdu~R^Hs8t=g`w zI;Kj`Y7)*neU_!Y;s*WHq>cq?b4J?Cj=Isdyr@bcO$w!@5HiS*@r1VJE2{LOCcT)J zUes%lOChjX&|kmc>&*C2W!1M*^R0wZ0Uh2!NHOJ1t;LD+RbsspAw=NkovPFT2@%%y zS-xz8BaU2w>F^#+>PbsI6p?m4!m~$}dNrvxE%nlA^UPC~njrkaIk5bv?8Vo#t)Eg4 zq5sc+gmwlBh10kSlXm-%v?5X~(o2cXY~)7w+%xKtTA)2!YLZLjUG|c5&}ZTDa`;=6 zS88NyE3a_soB@y2Bp+lKSU_5N&byY-T@=#j{Gn~inwLUorsO#{wOoSX?wU1^CEX83 zI~pE`ZVr>e;#m1J3H_)E+AR%psW=yUavEFrNF~a0dd=*zJ>!wE5`=Kfx}R^h@?b11 z!>}5RBazTaa)0#}vn)KQswQ0B3 zw3}hD#pX66ms|a6^DeD<7t4f={{^WnBei|M>qf8EZdzXg4!~ZW1!;9gT79cTTeDY{ z_G!|-wDIG^UeH>+0Sz$f^q(JQ!ybbH5Bw@T?9t(^>5a&Fvjck16_`yGL8;NUy-?4s(UzMSqN0j$3TMi55sxOk5 z=iBMWOofBi!N^YH3ieFJrs!Dq$Z9%}2&To%5!!1mnT(EL^Vu{$6BdUrM#e(f3K;$w zjqPKnQYkYCV5SqYwak(b4LF?tOJj=L_ElynX$3o+LHD^pX^7xwtJuH{nY-lT#%UXp zp*NW=)sY2##GtWX+M>2^ z*V?zMRgY;^kHL&^Mb*_&jHFBj#Aq%z84J6zf5Fq2@$}ssRXyu8&$_hnTlCftx$Dh% zA)hN2amlop>gm9lFgS3O|7!4Cl!#>Xn`bH~~ zd-N{Sn(?%zid9di=IKncAH$SjniizajKmp4W7P`_U-+@_%Vi555I6x~mCQ5A7lD68q~d*KWtGgA6uausVV3~qMI`VQ zKq_{@c&`BE!o~c`>s;7BDZqwG*AtLrT3}B(iD3n(bCE#!L4daj)qM=r7MIa^K3@Yb zn~c*5*=8w|a~4C+X_Ig=NoE?yFk77Lz< z;am~MvM`jg8=T1bsKu@fI2D%iS(u?m#Bfkg47-d|;0&UP@fql7kA?MIX0Q}(95^#Q z83h*rqpoA;$aKAa<`v7$vR-}U5PJ2q(McJ27>eRY68cs`m}Eo#WV8j`QgD*85^GHFK@=Z1T$^LZUu2y@yK4L zP=MpGnc(>k&}63InaQEEC1ik`%})=-&WBHe>zkxlgwm9g085LXsnZJa^D077U&VKM zSn>1SK=Ztu8dd|nTA{k@7k<%-4|B1Ldi#ZEW(?_0&MLql!uUuDJDiEyVKrK5}d&$B)+^Wr&O3 zh1Gux91l}}J3+OH*?H|5&K^TD2YI;sJq+F`;Jhd7l z9jxhC4GK~S)~T7X{8WFrL=ZSpt95j_3fpA-%0DK0+c!_@`C`!?o@7XHOx=N zPD5&Q8m4EVJCGQdPDIBCUQT3vmdA-GR4_oqu#^puUB-im#XQYySWOOur=rXRX4VZ! z&q3q}v)84RGnj*)Qo=3*=LoP_V%ZTc6ynk-A~dj0ESFzRLdYIU3W(Z_%+MUFf&y7)aZH_uLK1<(j)8-MCtHuhHCV zUM;=f?(kK;TCrH^2dECb>bqZZy4p$0f=l0$GfzY{vRzv0;m80 literal 0 HcmV?d00001 diff --git a/plugins_sogen-support/tenet/ui/breakpoint_view.py b/plugins_sogen-support/tenet/ui/breakpoint_view.py new file mode 100644 index 0000000..ad33a7c --- /dev/null +++ b/plugins_sogen-support/tenet/ui/breakpoint_view.py @@ -0,0 +1,45 @@ +# +# TODO: I don't think this file is even in use right now, but w/e +# we'll ship it for now... +# + +from tenet.util.qt import * + +class BreakpointDock(QtWidgets.QDockWidget): + """ + Dockable wrapper of a Breakpoint view. + """ + def __init__(self, view, parent=None): + super(BreakpointDock, self).__init__(parent) + self.setAllowedAreas(QtCore.Qt.AllDockWidgetAreas) + self.setWindowTitle("Breakpoints") + self.setWidget(view) + +class BreakpointView(QtWidgets.QWidget): + """ + The Breakpoint Widget (UI) + """ + + def __init__(self, controller, model, parent=None): + super(BreakpointView, self).__init__(parent) + self.controller = controller + self.model = model + self._init_ui() + + def _init_ui(self): + self.setMinimumHeight(100) + + self._init_table() + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(self._table) + + def _init_table(self): + self._table = QtWidgets.QTableWidget(self) + self._table.insertColumn(0) + self._table.insertColumn(1) + self._table.insertColumn(2) + self._table.insertColumn(3) + self._table.setHorizontalHeaderLabels(["Type", "Enabled", "Address", "Delete"]) + diff --git a/plugins_sogen-support/tenet/ui/hex_view.py b/plugins_sogen-support/tenet/ui/hex_view.py new file mode 100644 index 0000000..a1ab4dc --- /dev/null +++ b/plugins_sogen-support/tenet/ui/hex_view.py @@ -0,0 +1,1012 @@ +import struct + +from tenet.types import * +from tenet.util.qt import * + +INVALID_ADDRESS = -1 + +class HexView(QtWidgets.QAbstractScrollArea): + """ + A Qt based hex / memory viewer. + + Adapted from: + - https://github.com/virinext/QHexView + + """ + + def __init__(self, controller, model, parent=None): + super(HexView, self).__init__(parent) + self.controller = controller + self.model = model + self._palette = controller.pctx.palette + + self.setFocusPolicy(QtCore.Qt.StrongFocus) + self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + + font = QtGui.QFont("Courier", pointSize=normalize_font(9)) + font.setStyleHint(QtGui.QFont.TypeWriter) + self.setFont(font) + self.setMouseTracking(True) + + fm = QtGui.QFontMetricsF(font) + self._char_width = fm.width('9') + self._char_height = int(fm.tightBoundingRect('9').height() * 1.75) + self._char_descent = self._char_height - fm.descent()*0.75 + + self._click_timer = QtCore.QTimer(self) + self._click_timer.setSingleShot(True) + self._click_timer.timeout.connect(self._commit_click) + + self._double_click_timer = QtCore.QTimer(self) + self._double_click_timer.setSingleShot(True) + + self.hovered_address = INVALID_ADDRESS + + self._selection_start = INVALID_ADDRESS + self._selection_end = INVALID_ADDRESS + + self._pending_selection_origin = INVALID_ADDRESS + self._pending_selection_start = INVALID_ADDRESS + self._pending_selection_end = INVALID_ADDRESS + + self._ignore_navigation = False + + self._init_ctx_menu() + + def _init_ctx_menu(self): + """ + Initialize the right click context menu actions. + """ + + # create actions to show in the context menu + self._action_copy = QtWidgets.QAction("Copy", None) + self._action_clear = QtWidgets.QAction("Clear mem breakpoints", None) + self._action_follow_in_dump = QtWidgets.QAction("Follow in dump", None) + + bp_types = \ + [ + ("Read", BreakpointType.READ), + ("Write", BreakpointType.WRITE), + ("Access", BreakpointType.ACCESS) + ] + + # + # break on action group + # + + self._action_break = {} + + for name, bp_type in bp_types: + action = QtWidgets.QAction(name, None) + action.setCheckable(True) + self._action_break[action] = bp_type + + self._break_menu = QtWidgets.QMenu("Break on...") + self._break_menu.addActions(self._action_break) + + # + # goto action groups + # + + self._action_first = {} + self._action_prev = {} + self._action_next = {} + self._action_final = {} + + for name, bp_type in bp_types: + self._action_prev[QtWidgets.QAction(name, None)] = bp_type + self._action_next[QtWidgets.QAction(name, None)] = bp_type + self._action_first[QtWidgets.QAction(name, None)] = bp_type + self._action_final[QtWidgets.QAction(name, None)] = bp_type + + self._goto_menus = \ + [ + (QtWidgets.QMenu("Go to first..."), self._action_first), + (QtWidgets.QMenu("Go to previous..."), self._action_prev), + (QtWidgets.QMenu("Go to next..."), self._action_next), + (QtWidgets.QMenu("Go to final..."), self._action_final), + ] + + for submenu, actions in self._goto_menus: + submenu.addActions(actions) + + # install the right click context menu + self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + self.customContextMenuRequested.connect(self._ctx_menu_handler) + + #------------------------------------------------------------------------- + # Properties + #------------------------------------------------------------------------- + + @property + def num_lines_visible(self): + """ + Return the number of lines visible in the hex view. + """ + area_size = self.viewport().size() + first_line_idx = self.verticalScrollBar().value() + last_line_idx = (first_line_idx + area_size.height() // self._char_height) + 1 + lines_visible = last_line_idx - first_line_idx + return lines_visible + + @property + def num_bytes_visible(self): + """ + Return the number of bytes visible in the hex view. + """ + return self.model.num_bytes_per_line * self.num_lines_visible + + @property + def selection_size(self): + """ + Return the number of bytes selected in the hex view. + """ + if self._selection_end == self._selection_start == INVALID_ADDRESS: + return 0 + return self._selection_end - self._selection_start + + @property + def hovered_breakpoint(self): + """ + Return the hovered breakpoint. + """ + if self.hovered_address == INVALID_ADDRESS: + return None + + for bp in self.model.memory_breakpoints: + if bp.address <= self.hovered_address < bp.address + bp.length: + return bp + + return None + + #------------------------------------------------------------------------- + # Internal + #------------------------------------------------------------------------- + + def refresh(self): + """ + Refresh the hex view. + """ + self._refresh_painting_metrics() + self.viewport().update() + + def _refresh_painting_metrics(self): + """ + Refresh any metrics and calculations required to paint the widget. + """ + + # 2 chars per byte of data, eg '00' + self._chars_in_line = self.model.num_bytes_per_line * 2 + + # add 1 char for each space between elements (bytes, dwords, qwords...) + self._chars_in_line += (self.model.num_bytes_per_line // HEX_TYPE_WIDTH[self.model.hex_format]) + + # the x position to draw the text address (left side of view) + self._pos_addr = self._char_width // 2 + + # the width of the column, 2 nibbles (chars) per byte of a pointer + # -- +1 for padding, (eg, 1/2 char on each side) + self._width_addr = (self.model.pointer_size * 2 + 1) * self._char_width + + # the x position and width of the hex bytes region (center section of view) + self._pos_hex = self._width_addr + self._char_width + self._width_hex = self._chars_in_line * self._char_width + + # the x position and width of the auxillary region (right section of view) + self._pos_aux = self._pos_hex + self._width_hex + self._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(int(self._pos_aux + self._width_aux)) + + def full_size(self): + """ + TODO + """ + if not self.model.data: + return QtCore.QSize(0, 0) + + width = int(self._pos_aux + (self.model.num_bytes_per_line * self._char_width)) + height = len(self.model.data) // self.model.num_bytes_per_line + if len(self.model.data) % self.model.num_bytes_per_line: + height += 1 + + height *= self._char_height + + return QtCore.QSize(width, height) + + def point_to_index(self, position): + """ + Convert a QPoint (x, y) on the hex view window to a byte index. + + TODO/XXX: ugh this whole function / selection logic needs to be + rewritten... it's actually impossible to follow. + """ + padding = self._char_width // 2 + + if position.x() < (self._pos_hex - padding): + return -1 + + cutoff = self._pos_hex + self._width_hex - padding + #print(f"Position: {position} Cutoff: {cutoff} Pos Hex: {self._pos_hex} Width Hex: {self._width_hex} Padding: {padding}") + if position.x() >= cutoff: + return -1 + + # convert 'gloabl' x in the viewport, to an x that is 'relative' to the hex area + hex_x = (position.x() - self._pos_hex) + padding + #print("- Hex x", hex_x) + + # the number of items (eg, bytes, qwords) per line + num_items = self.model.num_bytes_per_line // HEX_TYPE_WIDTH[self.model.hex_format] + #print("- Num items", num_items) + + # compute the pixel width each rendered item on the line takes up + item_width = (self._char_width * 2) * HEX_TYPE_WIDTH[self.model.hex_format] + item_width_padded = item_width + self._char_width + #print("- Item Width", item_width) + #print("- Item Width Padded", item_width_padded) + + # compute the item index on a line (the x-axis) that the point falls within + item_index = int(hex_x // item_width_padded) + #print("- Item Index", item_index) + + # compute which byte is hovered in the item + if self.model.hex_format != HexType.BYTE: + + item_base_x = item_index * item_width_padded + (self._char_width // 2) + item_byte_x = hex_x - item_base_x + item_byte_index = int(item_byte_x // (self._char_width * 2)) + + # XXX: I give up, kludge to account for math errors + if item_byte_index < 0: + item_byte_index = 0 + elif item_byte_index >= self.model.num_bytes_per_line: + item_byte_index = self.model.num_bytes_per_line - 1 + + #print("- Item Byte X", item_byte_x) + #print("- Item Byte Index", item_byte_index) + + item_byte_index = (HEX_TYPE_WIDTH[self.model.hex_format] - 1) - item_byte_index + byte_x = item_index * HEX_TYPE_WIDTH[self.model.hex_format] + item_byte_index + + else: + byte_x = item_index * HEX_TYPE_WIDTH[self.model.hex_format] + + # compute the line number (the y-axis) that the point falls within + byte_y = position.y() // self._char_height + #print("- Byte (X, Y)", byte_x, byte_y) + + # compute the final byte index from the start address in the window + byte_index = (byte_y * self.model.num_bytes_per_line) + byte_x + #print("- Byte Index", byte_index) + + return byte_index + + def point_to_address(self, position): + """ + Convert a QPoint (x, y) on the hex view window to an address. + """ + byte_index = self.point_to_index(position) + if byte_index == -1: + return INVALID_ADDRESS + + byte_address = self.model.address + byte_index + return byte_address + + def point_to_breakpoint(self, position): + """ + Convert a QPoint (x, y) on the hex view window to a breakpoint. + """ + byte_address = self.point_to_address(position) + if byte_address == INVALID_ADDRESS: + return None + + for bp in self.model.memory_breakpoints: + if bp.address <= byte_address < bp.address + bp.length: + return bp + + return None + + def reset_selection(self): + """ + Clear the stored user memory selection. + """ + self._pending_selection_origin = INVALID_ADDRESS + self._pending_selection_start = INVALID_ADDRESS + self._pending_selection_end = INVALID_ADDRESS + self._selection_start = INVALID_ADDRESS + self._selection_end = INVALID_ADDRESS + + def _update_selection(self, position): + """ + Set the user memory selection. + """ + address = self.point_to_address(position) + if address == INVALID_ADDRESS: + return + + if address >= self._pending_selection_origin: + self._pending_selection_end = address + 1 + self._pending_selection_start = self._pending_selection_origin + else: + self._pending_selection_start = address + self._pending_selection_end = self._pending_selection_origin + 1 + + def _commit_click(self): + """ + Accept a click event. + """ + self._selection_start = self._pending_selection_start + self._selection_end = self._pending_selection_end + + self._pending_selection_origin = INVALID_ADDRESS + self._pending_selection_start = INVALID_ADDRESS + self._pending_selection_end = INVALID_ADDRESS + + self.viewport().update() + + def _commit_selection(self): + """ + Accept a selection event. + """ + self._selection_start = self._pending_selection_start + self._selection_end = self._pending_selection_end + + self._pending_selection_origin = INVALID_ADDRESS + self._pending_selection_start = INVALID_ADDRESS + self._pending_selection_end = INVALID_ADDRESS + + # notify listeners of our selection change + #self._notify_selection_changed(new_start, new_end) + self.viewport().update() + + #-------------------------------------------------------------------------- + # Signals + #-------------------------------------------------------------------------- + + def _ctx_menu_handler(self, position): + """ + Handle a right click event (populate/show context menu). + """ + menu = QtWidgets.QMenu() + + ctx_breakpoint = self.point_to_breakpoint(position) + ctx_address = self.point_to_address(position) + ctx_type = BreakpointType.NONE + + # + # determine the selection that the action will execute across + # + + if self._selection_start <= ctx_address < self._selection_end: + selected_address = self._selection_start + selected_length = self.selection_size + + elif ctx_breakpoint: + selected_address = ctx_breakpoint.address + selected_length = ctx_breakpoint.length + ctx_type = ctx_breakpoint.type + + else: + selected_address = INVALID_ADDRESS + selected_length = 0 + + # + # populate the popup menu + # + + # show the 'copy text' option if the user has a region selected + if selected_length > 1 and ctx_type == BreakpointType.NONE: + menu.addAction(self._action_copy) + + # only show the 'follow in dump' if the controller supports it + if hasattr(self.controller, "follow_in_dump"): + menu.addAction(self._action_follow_in_dump) + + menu.addSeparator() + + # show the break option only if there's a selection or breakpoint + if selected_length > 0: + menu.addMenu(self._break_menu) + menu.addSeparator() + + for action, access_type in self._action_break.items(): + action.setChecked(ctx_type == access_type) + + if selected_length > 0: + + # add the goto groups + for submenu, _ in self._goto_menus: + menu.addMenu(submenu) + + # show the 'clear breakpoints' action + menu.addSeparator() + menu.addAction(self._action_clear) + + # + # show the right click context menu + # + + action = menu.exec_(self.mapToGlobal(position)) + if not action: + return + + # + # execute the action selected by the suer in the right click menu + # + + if action == self._action_copy: + self.controller.copy_selection(self._selection_start, self._selection_end) + return + + elif action == self._action_follow_in_dump: + self.controller.follow_in_dump(self._selection_start) + return + + elif action == self._action_clear: + self.controller.pctx.breakpoints.clear_memory_breakpoints() + return + + # TODO: this is some of the shadiest/laziest code i've ever written + try: + selected_type = getattr(BreakpointType, action.text().upper()) + except: + pass + + if action in self._action_first: + self.controller.reader.seek_to_first(selected_address, selected_type, selected_length) + elif action in self._action_prev: + self.controller.reader.seek_to_prev(selected_address, selected_type, selected_length) + elif action in self._action_next: + self.controller.reader.seek_to_next(selected_address, selected_type, selected_length) + elif action in self._action_final: + self.controller.reader.seek_to_final(selected_address, selected_type, selected_length) + elif action in self._action_break: + self.controller.pin_memory(selected_address, selected_type, selected_length) + self.reset_selection() + + #---------------------------------------------------------------------- + # Qt Overloads + #---------------------------------------------------------------------- + + def mouseDoubleClickEvent(self, event): + """ + Qt overload to capture mouse double-click events. + """ + self._click_timer.stop() + + # + # if the double click fell within an active selection, we should + # consume the event as the user setting a region breakpoint + # + + if self._selection_start <= self._pending_selection_start < self._selection_end: + address = self._selection_start + size = self.selection_size + else: + address = self.point_to_address(event.pos()) + size = 1 + + self.controller.pin_memory(address, length=size) + self.reset_selection() + event.accept() + + self.viewport().update() + self._double_click_timer.start(100) + + def mouseMoveEvent(self, event): + """ + Qt overload to capture mouse movement events. + """ + mouse_position = event.pos() + + # update the hovered address + self.hovered_address = self.point_to_address(mouse_position) + + # mouse moving while holding left button + if event.buttons() == QtCore.Qt.MouseButton.LeftButton: + self._update_selection(mouse_position) + + # + # if the user is actively selecting bytes and has selected more + # than one byte, we should clear any existing selection. this will + # make it so the new ongoing 'pending' selection will get drawn + # + + if (self._pending_selection_end - self._pending_selection_start) > 1: + self._selection_start = INVALID_ADDRESS + self._selection_end = INVALID_ADDRESS + + self.viewport().update() + return + + def mousePressEvent(self, event): + """ + Qt overload to capture mouse button presses. + """ + if self._double_click_timer.isActive(): + return + + if event.button() == QtCore.Qt.LeftButton: + + byte_address = self.point_to_address(event.pos()) + + if not(self._selection_start <= byte_address < self._selection_end): + self.reset_selection() + + self._pending_selection_origin = byte_address + self._pending_selection_start = byte_address + self._pending_selection_end = (byte_address + 1) if byte_address != INVALID_ADDRESS else INVALID_ADDRESS + + self.viewport().update() + + def mouseReleaseEvent(self, event): + """ + Qt overload to capture mouse button releases. + """ + if self._double_click_timer.isActive(): + return + + # handle a right click + if event.button() == QtCore.Qt.RightButton: + + # get the address of the byte that was right clicked + byte_address = self.point_to_address(event.pos()) + if byte_address == INVALID_ADDRESS: + return + + # the right clicked fell within the current selection + if self._selection_start <= byte_address < self._selection_end: + return + + # the right click fell within an existing breakpoint + bp = self.hovered_breakpoint + if bp and (bp.address <= byte_address < bp.address + bp.length): + return + + # + # if the right click did not fall within any known selection / poi + # we should consume it and set the current cursor selection to it + # + + self._pending_selection_start = byte_address + self._pending_selection_end = byte_address + 1 + self._commit_click() + return + + if self._pending_selection_origin == INVALID_ADDRESS: + return + + # if the mouse press & release was on a single byte, it's a click + if (self._pending_selection_end - self._pending_selection_start) == 1: + + # + # if the click was within a selected region, defer acting on it + # for 500ms to see if a double click event occurs + # + + if self._selection_start <= self._pending_selection_start < self._selection_end: + self._click_timer.start(200) + return + else: + self._commit_click() + + # a range was selected, so accept/commit it + else: + self._commit_selection() + + def keyPressEvent(self, e): + """ + Qt overload to capture key press events. + """ + if e.key() == QtCore.Qt.Key_G: + import ida_kernwin, ida_idaapi + address = ida_kernwin.ask_addr(self.model.address, "Jump to address in memory") + if address != None and address != ida_idaapi.BADADDR: + self.controller.navigate(address) + e.accept() + return super(HexView, self).keyPressEvent(e) + + def wheelEvent(self, event): + """ + Qt overload to capture wheel events. + """ + + # + # first, we will attempt special handling of the case where a user + # 'scrolls' up or down when hovering their cursor over a byte they + # have selected... + # + + # compute the address of the hovered byte (if there is one...) + byte_address = self.point_to_address(event.pos()) + + for bp in self.model.memory_breakpoints: + + # skip this breakpoint if the current byte does not fall within its range + if not(bp.address <= byte_address < bp.address + bp.length): + continue + + # + # XXX: bit of a hack, but it seems like the easiest way to prevent + # the stack views from 'navigating' when you're hovering / scrolling + # through memory accesses (see _idx_changed in stack.py) + # + + self._ignore_navigation = True + + # + # if a region is selected with an 'access' breakpoint on it, + # use the start address of the selected region instead for + # the region-based seeks + # + + # scrolled 'up' + if event.angleDelta().y() > 0: + self.controller.reader.seek_to_prev(bp.address, bp.type, bp.length) + + # scrolled 'down' + elif event.angleDelta().y() < 0: + self.controller.reader.seek_to_next(bp.address, bp.type, bp.length) + + # restore navigation listening + self._ignore_navigation = False + + # consume the event + event.accept() + return + + # + # normal 'scroll' on the hex window.. scroll up or down into new + # regions of memory... + # + + if event.angleDelta().y() > 0: + self.controller.navigate(self.model.address - self.model.num_bytes_per_line) + + elif event.angleDelta().y() < 0: + self.controller.navigate(self.model.address + self.model.num_bytes_per_line) + + event.accept() + + def resizeEvent(self, event): + """ + Qt overload to capture resize events for the widget. + """ + super(HexView, self).resizeEvent(event) + self._refresh_painting_metrics() + self.controller.set_data_size(self.num_bytes_visible) + + #------------------------------------------------------------------------- + # Painting + #------------------------------------------------------------------------- + + def paintEvent(self, event): + """ + Qt overload of widget painting. + """ + if not self.model.data: + return + + painter = QtGui.QPainter(self.viewport()) + + # paint background of entire scroll area + painter.fillRect(event.rect(), self._palette.hex_data_bg) + + # paint address area background + address_area_rect = QtCore.QRect(0, event.rect().top(), int(self._width_addr), self.height()) + painter.fillRect(address_area_rect, self._palette.hex_address_bg) + + # paint line between address area and hex area + painter.setPen(self._palette.hex_separator) + painter.drawLine(int(self._width_addr), event.rect().top(), int(self._width_addr), self.height()) + + # paint line between hex area and auxillary area + line_pos = self._pos_aux + painter.setPen(self._palette.hex_separator) + painter.drawLine(int(line_pos), event.rect().top(), int(line_pos), self.height()) + + for line_idx in range(0, self.num_lines_visible): + self._paint_line(painter, line_idx) + + def _paint_line(self, painter, line_idx): + """ + Paint one line of hex. + """ + self._brush_default = painter.brush() + self._brush_selected = QtGui.QBrush(self._palette.standard_selection_bg) + self._brush_navigation = QtGui.QBrush(self._palette.navigation_selection_fg) + + # the pixel position to start painting from + x, y = self._pos_hex, (line_idx + 1) * self._char_height + + # clamp the address from 0 to 0xFFFFFFFFFFFFFFFF + address = self.model.address + (line_idx * self.model.num_bytes_per_line) + if address > 0xFFFFFFFFFFFFFFFF: + address = 0xFFFFFFFFFFFFFFFF + + address_color = self._palette.hex_address_fg + if address < self.model.fade_address: + address_color = self._palette.hex_text_faded_fg + + painter.setPen(address_color) + + # draw the address text + pack_len = self.model.pointer_size + address_fmt = '%016X' if pack_len == 8 else '%08X' + address_text = address_fmt % address + painter.drawText(int(self._pos_addr), y, address_text) + + self._default_color = self._palette.hex_text_fg + if address < self.model.fade_address: + self._default_color = self._palette.hex_text_faded_fg + + painter.setPen(self._default_color) + + byte_base_idx = line_idx * self.model.num_bytes_per_line + byte_idx = byte_base_idx + stop_idx = min(len(self.model.data), byte_base_idx + self.model.num_bytes_per_line) + + # paint each element on the line, up until the end of the line, or buffer + while byte_idx < stop_idx: + byte_idx, x, y = self._paint_hex_item(painter, byte_idx, stop_idx, x, y) + + assert byte_idx == stop_idx + + # + # paint 'readable' ASCII + # + + byte_idx = byte_base_idx + x_pos_aux = self._pos_aux + self._char_width + + if self.model.aux_format == AuxType.ASCII: + + for i in range(byte_base_idx, stop_idx): + + if self.model.mask[i]: + painter.setPen(self._default_color) + else: + painter.setPen(self._palette.hex_text_faded_fg) + + ch = self.model.data[i] + if ((ch < 0x20) or (ch > 0x7e)): + ch = '.' + else: + ch = chr(ch) + + painter.drawText(int(x_pos_aux), y, ch) + x_pos_aux += self._char_width + + def _paint_hex_item(self, painter, byte_idx, stop_idx, x, y): + """ + Paint a single hex item. + """ + + # draw single bytes + if self.model.hex_format == HexType.BYTE: + return self._paint_byte(painter, byte_idx, x, y) + + # draw dwords + elif self.model.hex_format == HexType.DWORD: + return self._paint_dword(painter, byte_idx, x, y) + + # draw qwords + elif self.model.hex_format == HexType.QWORD: + return self._paint_qword(painter, byte_idx, x, y) + + # identify and draw pointers + elif self.model.hex_format == HexType.MAGIC: + return self._paint_magic(painter, byte_idx, stop_idx, x, y) + + raise NotImplementedError("Unknown HexType format! %s" % self.model.hex_format) + + #return (byte_idx, x, y) + + def _paint_byte(self, painter, byte_idx, x, y): + """ + Paint a BYTE at the current position. + """ + self._paint_text(painter, byte_idx, 1, x, y) + x += (2 + 1) * self._char_width + + return (byte_idx + 1, x, y) + + def _paint_dword(self, painter, byte_idx, x, y): + """ + Paint a DWORD at the current position. + """ + backwards_idx = byte_idx - 1 + + for i in range(backwards_idx + 4, backwards_idx, -1): + self._paint_text(painter, i, 0, x, y) + x += self._char_width * 2 + + return (byte_idx + 4, x, y) + + def _paint_qword(self, painter, byte_idx, x, y): + """ + Paint a QWORD at the current position. + """ + backwards_idx = byte_idx - 1 + + for i in range(backwards_idx + 8, backwards_idx, -1): + self._paint_text(painter, i, 0, x, y) + x += self._char_width * 2 + + return (byte_idx + 8, x, y) + + def _paint_text(self, painter, byte_idx, padding, x, y): + + if self.model.mask[byte_idx]: + fg_color = self._default_color + text = "%02X" % self.model.data[byte_idx] + else: + fg_color = self._palette.hex_text_faded_fg + text = "??" + + # + # paint text selection background color / highlight + # + + x_bg = x - (self._char_width // 2) * padding + y_bg = y - self._char_descent + + width = self._char_width * (len(text) + padding) + height = self._char_height + + bg_color = None + border_color = None + + # compute the address of the byte we're drawing + byte_address = self.model.address + byte_idx + + # initialize selection start / end vars + start_address = INVALID_ADDRESS + end_address = INVALID_ADDRESS + + # fixed / committed selection + if self._selection_start != INVALID_ADDRESS: + start_address = self._selection_start + end_address = self._selection_end + + # active / on-going selection event + elif self._pending_selection_start != INVALID_ADDRESS: + start_address = self._pending_selection_start + end_address = self._pending_selection_end + + # a byte that falls within the user selection + if start_address <= byte_address < end_address: + bg_color = self._palette.standard_selection_bg + + # set the text color for selected text + if self.model.mask[byte_idx]: + fg_color = self._palette.standard_selection_fg + else: + fg_color = self._palette.standard_selection_faded_fg + + # a byte that was written + elif byte_address in self.model.delta.mem_writes: + bg_color = self._palette.mem_write_bg + fg_color = self._palette.mem_write_fg + + # a byte that was read + elif byte_address in self.model.delta.mem_reads: + bg_color = self._palette.mem_read_bg + fg_color = self._palette.mem_read_fg + + # a breakpoint byte + for bp in self.model.memory_breakpoints: + + # skip this breakpoint if the current byte does not fall within its range + if not(bp.address <= byte_address < bp.address + bp.length): + continue + + # + # if the breakpoint is a single byte, ensure it will always have a + # border around it, regardless of if it is selected, read, or + # written. + # + # this makes it easy to tell when you have selected or are hovering + # an active 'hot' byte / breakpoint that can be scrolled over to + # seek between accesses + # + + if bp.length == 1: + border_color = self._palette.navigation_selection_bg + + # + # if the background color for this byte has already been + # specified, that means a read/write probably occured to it so + # we should prioritize those colors OVER the breakpoint coloring + # + + if bg_color: + break + + # + # if the byte wasn't read/written/selected, we are free to color + # it red, as it falls within an active breakpoint region + # + + bg_color = self._palette.navigation_selection_bg + + # if the byte value is know (versus '??'), set its text color + if self.model.mask[byte_idx]: + fg_color = self._palette.navigation_selection_fg + else: + fg_color = self._palette.navigation_selection_faded_fg + + # + # no need to keep searching through breakpoints once the byte has + # been colored! break and go paint the byte... + # + + break + + # the byte is highlighted in some fashion, paint it now + if bg_color: + + if border_color: + pen = QtGui.QPen(border_color, 2) + pen.setJoinStyle(QtCore.Qt.MiterJoin) + painter.setPen(pen) + x_bg += 1 + y_bg += 1 + width -= 2 + height -= 2 + + else: + painter.setPen(QtCore.Qt.NoPen) + + painter.setBrush(bg_color) + painter.drawRect(int(x_bg), int(y_bg), int(width), int(height)) + + painter.setPen(fg_color) + + # + # paint text + # + + painter.drawText(int(x), y, text) + + def _paint_magic(self, painter, byte_idx, stop_idx, x, y): + """ + Perform magic painting at the current position. + + This will essentially try to identify pointers while painting, and + format them as appropriate. + + TODO: this needs to be updated to be truly pointer size agnostic + """ + + # not enough bytes left to identify / paint a pointer from the data + if byte_idx + self.model.pointer_size > stop_idx: + return self._paint_byte(painter, byte_idx, x, y) + + # ensure that all the bytes for the 'pointer' to analyze are known + pack_len = self.model.pointer_size + pack_fmt = 'Q' if pack_len == 8 else 'I' + mask = struct.unpack(pack_fmt, self.model.mask[byte_idx:byte_idx+pack_len])[0] + if mask != 0xFFFFFFFFFFFFFFFF: + return self._paint_byte(painter, byte_idx, x, y) + + # read and analyze the value to determine if it is a pointer + value = struct.unpack(pack_fmt, self.model.data[byte_idx:byte_idx+pack_len])[0] + if not self.controller.pctx.is_pointer(value): + return self._paint_byte(painter, byte_idx, x, y) + + # + # it seems like a pointer, let's draw one! + # + + # compute how many characters would have normally filled this space + # if inidividual bytes were printed instead... + num_chars = 3 * self.model.pointer_size + + # draw the pointer + pointer_str = ("0x%08X " % value).rjust(num_chars) + painter.drawText(int(x), y, pointer_str) + x += num_chars * self._char_width + + return (byte_idx + self.model.pointer_size, x, y) diff --git a/plugins_sogen-support/tenet/ui/palette.py b/plugins_sogen-support/tenet/ui/palette.py new file mode 100644 index 0000000..7921fb8 --- /dev/null +++ b/plugins_sogen-support/tenet/ui/palette.py @@ -0,0 +1,574 @@ +import os +import json +import shutil +import logging + +from json.decoder import JSONDecodeError + +from tenet.util.qt import * +from tenet.util.misc import * +from tenet.util.log import pmsg +from tenet.integration.api import disassembler + +logger = logging.getLogger("Plugin.UI.Palette") + +#------------------------------------------------------------------------------ +# Plugin Color Palette +#------------------------------------------------------------------------------ + +class PluginPalette(object): + """ + Theme palette for the plugin. + """ + + def __init__(self): + """ + Initialize default palette colors for the plugin. + """ + self._initialized = False + self._last_directory = None + self._required_fields = [] + + # hints about the user theme (light/dark) + self._user_qt_hint = "dark" + self._user_disassembly_hint = "dark" + + self.theme = None + self._default_themes = \ + { + "dark": "synth.json", + "light": "horizon.json" + } + + # list of objects requesting a callback after a theme change + self._theme_changed_callbacks = [] + + # get a list of required theme fields, for user theme validation + self._load_required_fields() + + # initialize the user theme directory + self._populate_user_theme_dir() + + # load a placeholder theme for inital Tenet bring-up + self._load_default_theme() + self._initialized = False + + @staticmethod + def get_plugin_theme_dir(): + """ + Return the plugin theme directory. + """ + return plugin_resource("themes") + + @staticmethod + def get_user_theme_dir(): + """ + Return the user theme directory. + """ + theme_directory = os.path.join( + disassembler.get_disassembler_user_directory(), + "tenet_themes" + ) + return theme_directory + + #---------------------------------------------------------------------- + # Callbacks + #---------------------------------------------------------------------- + + def theme_changed(self, callback): + """ + Subscribe a callback for theme change events. + """ + register_callback(self._theme_changed_callbacks, callback) + + def _notify_theme_changed(self): + """ + Notify listeners of a theme change event. + """ + notify_callback(self._theme_changed_callbacks) + + #---------------------------------------------------------------------- + # Public + #---------------------------------------------------------------------- + + def warmup(self): + """ + Warms up the theming system prior to initial use. + """ + if self._initialized: + return + + logger.debug("Warming up theme subsystem...") + + # attempt to load the user's preferred theme + if self._load_preferred_theme(): + self._initialized = True + logger.debug(" - warmup complete, using user theme!") + return + + # + # if no user selected theme is loaded, we will attempt to detect + # and load the in-box themes based on the disassembler theme + # + + if self._load_hinted_theme(): + logger.debug(" - warmup complete, using hint-recommended theme!") + self._initialized = True + return + + pmsg("Could not warmup theme subsystem!") + + def interactive_change_theme(self): + """ + Open a file dialog and let the user select a new plugin theme. + """ + + # create & configure a Qt File Dialog for immediate use + file_dialog = QtWidgets.QFileDialog( + None, + "Open plugin theme file", + self._last_directory, + "JSON Files (*.json)" + ) + file_dialog.setFileMode(QtWidgets.QFileDialog.ExistingFile) + + # prompt the user with the file dialog, and await filename(s) + filename, _ = file_dialog.getOpenFileName() + if not filename: + return + + # + # ensure the user is only trying to load themes from the user theme + # directory as it helps ensure some of our intenal loading logic + # + + file_dir = os.path.abspath(os.path.dirname(filename)) + user_dir = os.path.abspath(self.get_user_theme_dir()) + if file_dir != user_dir: + text = "Please install your plugin theme into the user theme directory:\n\n" + user_dir + disassembler.warning(text) + return + + # + # remember the last directory we were in (parsed from a selected file) + # for the next time the user comes to load a theme file + # + + if filename: + self._last_directory = os.path.dirname(filename) + os.sep + + # log the captured (selected) filenames from the dialog + logger.debug("Captured filename from theme file dialog: '%s'" % filename) + + # + # before applying the selected plugin theme, we should ensure that + # we know if the user is using a light or dark disassembler theme as + # it may change which colors get used by the plugin theme + # + + self._refresh_theme_hints() + + # if the selected theme fails to load, throw a visible warning + if not self._load_theme(filename): + disassembler.warning( + "Failed to load plugin user theme!\n\n" + "Please check the console for more information..." + ) + return + + # since everthing looks like it loaded okay, save this as the preferred theme + with open(os.path.join(self.get_user_theme_dir(), ".active_theme"), "w") as f: + f.write(filename) + + def refresh_theme(self): + """ + Dynamically compute palette color based on the disassembler theme. + + Depending on if the disassembler is using a dark or light theme, we + *try* to select colors that will hopefully keep things most readable. + """ + if self._load_preferred_theme(): + return + if self._load_hinted_theme(): + return + pmsg("Failed to refresh theme!") + + def gen_arrow_icon(self, color, rotation): + """ + Dynamically generate a colored/rotated arrow icon. + """ + icon_path = plugin_resource(os.path.join("icons", "arrow.png")) + + img = QtGui.QPixmap(icon_path) + + if rotation: + rm = QtGui.QTransform() + rm.rotate(rotation) + img = img.transformed(rm) + + mask = QtGui.QPixmap(img) + + p = QtGui.QPainter() + p.begin(mask) + p.setCompositionMode(QtGui.QPainter.CompositionMode_SourceIn) + p.fillRect(img.rect(), color) + p.end() + + p.begin(img) + p.setCompositionMode(QtGui.QPainter.CompositionMode_Overlay) + p.drawPixmap(0, 0, mask) + p.end() + + # convert QPixmap to bytes + ba = QtCore.QByteArray() + buff = QtCore.QBuffer(ba) + buff.open(QtCore.QIODevice.WriteOnly) + ok = img.save(buff, "PNG", quality=100) + assert ok + + return ba.data() + + #-------------------------------------------------------------------------- + # Theme Internals + #-------------------------------------------------------------------------- + + def _populate_user_theme_dir(self): + """ + Create the plugin's user theme directory and install default themes. + """ + + # create the user theme directory if it does not exist + user_theme_dir = self.get_user_theme_dir() + makedirs(user_theme_dir) + + # copy the default themes into the user directory if they don't exist + for theme_name in self._default_themes.values(): + + # + # check if the plugin has copied the default themes into the user + # theme directory before. when 'default' themes exists, skip them + # rather than overwriting... as the user may have modified it + # + + user_theme_file = os.path.join(user_theme_dir, theme_name) + if os.path.exists(user_theme_file): + continue + + # copy the in-box themes to the user theme directory + plugin_theme_file = os.path.join(self.get_plugin_theme_dir(), theme_name) + shutil.copy(plugin_theme_file, user_theme_file) + + # + # if the user tries to switch themes, ensure the file dialog will start + # in their user theme directory + # + + self._last_directory = user_theme_dir + + def _load_required_fields(self): + """ + Load the required theme fields from a donor in-box theme. + """ + logger.debug("Loading required theme fields from disk...") + + # load a known-good theme from the plugin's in-box themes + filepath = os.path.join(self.get_plugin_theme_dir(), self._default_themes["dark"]) + theme = self._read_theme(filepath) + + # + # save all the defined fields in this 'good' theme as a ground truth + # to validate user themes against... + # + + self._required_fields = theme["fields"].keys() + + def _load_default_theme(self): + """ + Load the default theme without any sort of hinting. + """ + theme_name = self._default_themes["dark"] + theme_path = os.path.join(self.get_plugin_theme_dir(), theme_name) + return self._load_theme(theme_path) + + def _load_hinted_theme(self): + """ + Load the in-box plugin theme hinted at by the theme subsystem. + """ + self._refresh_theme_hints() + + # + # we have two themes hints which roughly correspond to the tone of + # the user's disassembly background, and then the Qt subsystem. + # + # if both themes seem to align on style (eg the user is using a + # 'dark' UI), then we will select the appropriate in-box theme + # + + if self._user_qt_hint == self._user_disassembly_hint: + theme_name = self._default_themes[self._user_qt_hint] + logger.debug(" - No preferred theme, hints suggest theme '%s'" % theme_name) + + # + # the UI hints don't match, so the user is using some ... weird + # mismatched theming in their disassembler. let's just default to + # the 'dark' plugin theme as it is more robust + # + + else: + theme_name = self._default_themes["dark"] + + # build the filepath to the hinted, in-box theme + theme_path = os.path.join(self.get_plugin_theme_dir(), theme_name) + + # attempt to load and return the result of loading an in-box theme + return self._load_theme(theme_path) + + def _load_preferred_theme(self): + """ + Load the user's saved, preferred theme. + """ + logger.debug("Loading preferred theme from disk...") + user_theme_dir = self.get_user_theme_dir() + + # attempt te read the name of the user's active / preferred theme name + active_filepath = os.path.join(user_theme_dir, ".active_theme") + try: + theme_name = open(active_filepath).read().strip() + logger.debug(" - Got '%s' from .active_theme" % theme_name) + except (OSError, IOError): + return False + + # build the filepath to the user defined theme + theme_path = os.path.join(self.get_user_theme_dir(), theme_name) + + # finally, attempt to load & apply the theme -- return True/False + if self._load_theme(theme_path): + return True + + # + # failed to load the preferred theme... so delete the 'active' + # file (if there is one) and warn the user before falling back + # + + try: + os.remove(os.path.join(self.get_user_theme_dir(), ".active_theme")) + except: + pass + + disassembler.warning( + "Failed to load plugin user theme!\n\n" + "Please check the console for more information..." + ) + + return False + + def _validate_theme(self, theme): + """ + Pefrom rudimentary theme validation. + """ + logger.debug(" - Validating theme fields for '%s'..." % theme["name"]) + user_fields = theme.get("fields", None) + if not user_fields: + pmsg("Could not find theme 'fields' definition") + return False + + # check that all the 'required' fields exist in the given theme + for field in self._required_fields: + if field not in user_fields: + pmsg("Could not find required theme field '%s'" % field) + return False + + # theme looks good enough for now... + return True + + def _load_theme(self, filepath): + """ + Load and apply the plugin theme at the given filepath. + """ + + # attempt to read json theme from disk + try: + theme = self._read_theme(filepath) + + # reading file from dsik failed + except OSError: + pmsg("Could not open theme file at '%s'" % filepath) + return False + + # JSON decoding failed + except JSONDecodeError as e: + pmsg("Failed to decode theme '%s' to json" % filepath) + pmsg(" - " + str(e)) + return False + + # do some basic sanity checking on the given theme file + if not self._validate_theme(theme): + pmsg("Failed to validate theme '%s'" % filepath) + return False + + # try applying the loaded theme to the plugin + try: + self._apply_theme(theme) + except Exception as e: + pmsg("Failed to load the plugin user theme\n%s" % e) + return False + + # return success + self._notify_theme_changed() + return True + + def _read_theme(self, filepath): + """ + Parse the plugin theme file from the given filepath. + """ + logger.debug(" - Reading theme file '%s'..." % filepath) + + # attempt to load the theme file contents from disk + raw_theme = open(filepath, "r").read() + + # convert the theme file contents to a json object/dict + theme = json.loads(raw_theme) + + # all good + return theme + + def _apply_theme(self, theme): + """ + Apply the given theme definition to the plugin. + """ + logger.debug(" - Applying theme '%s'..." % theme["name"]) + colors = theme["colors"] + + for field_name, color_entry in theme["fields"].items(): + + # color has 'light' and 'dark' variants + if isinstance(color_entry, list): + color_name = self._pick_best_color(field_name, color_entry) + + # there is only one color defined + else: + color_name = color_entry + + # load the color + color_value = colors[color_name] + color = QtGui.QColor(*color_value) + + # set theme self.[field_name] = color + setattr(self, field_name, color) + + # all done, save the theme in case we need it later + self.theme = theme + + 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 + 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)" + dark, light = color_entry + + if self._user_qt_hint == "dark": + return dark + + return light + + #-------------------------------------------------------------------------- + # Theme Inference + #-------------------------------------------------------------------------- + + def _refresh_theme_hints(self): + """ + Peek at the UI context to infer what kind of theme the user might be using. + """ + self._user_qt_hint = self._qt_theme_hint() + self._user_disassembly_hint = self._disassembly_theme_hint() or "dark" + + def _disassembly_theme_hint(self): + """ + Binary hint of the disassembler color theme. + + This routine returns a best effort hint as to what kind of theme is + in use for the IDA Views (Disas, Hex, HexRays, etc). + + Returns 'dark' or 'light' indicating the user's theme + """ + + # + # determine whether to use a 'dark' or 'light' paint based on the + # background color of the user's disassembly text based windows + # + + bg_color = disassembler.get_disassembly_background_color() + if not bg_color: + logger.debug(" - Failed to get hint for disassembly background...") + return None + + # return 'dark' or 'light' + return test_color_brightness(bg_color) + + def _qt_theme_hint(self): + """ + Binary hint of the Qt color theme. + + This routine returns a best effort hint as to what kind of theme the + QtWdigets throughout IDA are using. This is to accomodate for users + who may be using Zyantific's IDASkins plugins (or others) to further + customize IDA's appearance. + + Returns 'dark' or 'light' indicating the user's theme + """ + + # + # to determine what kind of Qt based theme IDA is using, we create a + # test widget and check the colors put into the palette the widget + # inherits from the application (eg, IDA). + # + + test_widget = QtWidgets.QWidget() + + # + # in order to 'realize' the palette used to render (draw) the widget, + # it first must be made visible. since we don't want to be popping + # random widgets infront of the user, so we set this attribute such + # that we can silently bake the widget colors. + # + # NOTE/COMPAT: WA_DontShowOnScreen + # + # https://www.riverbankcomputing.com/news/pyqt-56 + # + # lmao, don't ask me why they forgot about this attribute from 5.0 - 5.6 + # + + if disassembler.NAME == "BINJA": + test_widget.setAttribute(QtCore.Qt.WA_DontShowOnScreen) + else: + test_widget.setAttribute(103) # taken from http://doc.qt.io/qt-5/qt.html + + # render the (invisible) widget + test_widget.show() + + # now we farm the background color from the qwidget + bg_color = test_widget.palette().color(QtGui.QPalette.Window) + + # 'hide' & delete the widget + test_widget.hide() + test_widget.deleteLater() + + # return 'dark' or 'light' + return test_color_brightness(bg_color) + +#----------------------------------------------------------------------------- +# Palette Util +#----------------------------------------------------------------------------- + +def test_color_brightness(color): + """ + Test the brightness of a color. + """ + if color.lightness() > 255.0/2: + return "light" + else: + return "dark" diff --git a/plugins_sogen-support/tenet/ui/reg_view.py b/plugins_sogen-support/tenet/ui/reg_view.py new file mode 100644 index 0000000..42c4732 --- /dev/null +++ b/plugins_sogen-support/tenet/ui/reg_view.py @@ -0,0 +1,545 @@ +import collections + +from tenet.types import BreakpointType +from tenet.util.qt import * +from tenet.integration.api import disassembler + +class RegisterView(QtWidgets.QWidget): + """ + A container for the the widgets that make up the Registers view. + """ + + def __init__(self, controller, model, parent=None): + super(RegisterView, self).__init__(parent) + self.controller = controller + self.model = model + self._init_ui() + + def _init_ui(self): + + # child widgets + self.reg_area = RegisterArea(self.controller, self.model, self) + self.idx_shell = TimestampShell(self.controller, self.model, self) + self.setMinimumWidth(self.reg_area.minimumWidth()) + + # layout + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(self.reg_area) + layout.addWidget(self.idx_shell) + self.setLayout(layout) + + def refresh(self): + self.reg_area.refresh() + self.idx_shell.update() + +class TimestampShell(QtWidgets.QWidget): + + def __init__(self, controller, model, parent=None): + super(TimestampShell, self).__init__(parent) + self.model = model + self.controller = controller + self._init_ui() + + def _init_ui(self): + + # child widgets + self.head = QtWidgets.QLabel("Position", self) + self.shell = TimestampLine(self.model, self.controller, self) + + # events + self.model.registers_changed(self.refresh) + + # layout + layout = QtWidgets.QHBoxLayout(self) + #layout.setContentsMargins(5, 0, 5, 0) + layout.setContentsMargins(5, 0, 0, 5) + layout.addWidget(self.head) + layout.addWidget(self.shell) + + def refresh(self): + self.shell.setText(f"{self.model.idx:,}") + +class TimestampLine(QtWidgets.QLineEdit): + def __init__(self, model, controller, parent=None): + super(TimestampLine, self).__init__(parent) + self.model = model + self.controller = controller + self._init_ui() + + def _init_ui(self): + self.setStyleSheet( + f""" + QLineEdit {{ + background-color: {self.controller.pctx.palette.reg_bg.name()}; + color: {self.controller.pctx.palette.reg_value_fg.name()}; + }} + """ + ) + self.returnPressed.connect(self._evaluate) + + def _evaluate(self): + self.controller.evaluate_expression(self.text()) + +class RegisterArea(QtWidgets.QAbstractScrollArea): + """ + A Qt-based CPU register view. + """ + def __init__(self, controller, model, parent=None): + super(RegisterArea, self).__init__(parent) + self.pctx = controller.pctx + self.controller = controller + self.model = model + + font = QtGui.QFont("Courier", pointSize=normalize_font(9)) + font.setStyleHint(QtGui.QFont.TypeWriter) + self.setFont(font) + + fm = QtGui.QFontMetricsF(font) + self._char_width = fm.width('9') + self._char_height = fm.height() + + # default to fit roughly 50 printable characters + self._default_width = self._char_width * (self.pctx.arch.POINTER_SIZE * 2 + 16) + + # register drawing information + self._reg_pos = (self._char_width, self._char_height) + self._reg_fields = {} + self._hovered_arrow = None + + self.setFocusPolicy(QtCore.Qt.StrongFocus) + self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) + self.setMinimumWidth(int(self._reg_pos[0] + self._default_width)) + self.setMouseTracking(True) + + self._init_ctx_menu() + self._init_reg_positions() + + self.model.registers_changed(self.refresh) + + def sizeHint(self): + width = int(self._default_width) + height = int((len(self._reg_fields) + 2) * self._char_height) # +2 for line break before IP, and after IP + return QtCore.QSize(width, height) + + def _init_ctx_menu(self): + """ + Initialize the right click context menu actions. + """ + + # create actions to show in the context menu + self._action_copy_value = QtWidgets.QAction("Copy value", None) + self._action_follow_in_dump = QtWidgets.QAction("Follow in dump", None) + self._action_follow_in_disassembly = QtWidgets.QAction("Follow in disassembler", None) + self._action_clear = QtWidgets.QAction("Clear code breakpoints", None) + + # install the right click context menu + self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + self.customContextMenuRequested.connect(self._ctx_menu_handler) + + def _init_reg_positions(self): + """ + Initialize register positions in the window. + """ + regs = self.model.arch.REGISTERS + name_x, y = self._reg_pos + + # find the most common length of a register name + reg_char_counts = collections.Counter([len(x) for x in regs]) + common_count, _ = reg_char_counts.most_common(1)[0] + + # compute rects for the average reg labels and values + fm = QtGui.QFontMetricsF(self.font()) + name_size = fm.boundingRect('X'*common_count).size() + value_size = fm.boundingRect('0' * (self.model.arch.POINTER_SIZE * 2)).size() + arrow_size = (int(value_size.height() * 0.70) | 1) + + # pre-compute the position of each register in the window + for reg_name in regs: + + # kind of dirty, but this will push IP a bit further away from the + # rest of the registers (it should be the last defined one...) + if reg_name == self.model.arch.IP: + y += self._char_height + + name_rect = QtCore.QRect(0, 0, int(name_size.width()), int(name_size.height())) + name_rect.moveBottomLeft(QtCore.QPoint(int(name_x), int(y))) + + prev_rect = QtCore.QRect(0, 0, int(arrow_size), int(arrow_size)) + next_rect = QtCore.QRect(0, 0, int(arrow_size), int(arrow_size)) + arrow_rects = [prev_rect, next_rect] + + prev_x = name_x + name_size.width() + self._char_width + prev_rect.moveCenter(name_rect.center()) + prev_rect.moveLeft(int(prev_x)) + + value_x = prev_x + prev_rect.width() + self._char_width + value_rect = QtCore.QRect(0, 0, int(value_size.width()), int(value_size.height())) + value_rect.moveBottomLeft(QtCore.QPoint(int(value_x), int(y))) + + next_x = value_x + value_size.width() + self._char_width + next_rect.moveCenter(name_rect.center()) + next_rect.moveLeft(int(next_x)) + + # save the register shapes + self._reg_fields[reg_name] = RegisterField(reg_name, name_rect, value_rect, arrow_rects) + + # increment y (to the next line) + y += self._char_height + + def _ctx_menu_handler(self, position): + """ + Handle a right click event (populate/show context menu). + """ + menu = QtWidgets.QMenu() + + # if a register was right clicked, fetch its name + reg_name = self._pos_to_reg(position) + if reg_name: + + # + # fetch the disassembler context and register value as we may use them + # based on the user's context, or the action they select + # + + dctx = disassembler[self.controller.pctx] + reg_value = self.model.registers[reg_name] + + # + # dynamically populate the right click context menu + # + + menu.addAction(self._action_copy_value) + menu.addAction(self._action_follow_in_dump) + + # + # if the register conatins a value that falls within the database, + # we want to show it and ensure it's active + # + + menu.addAction(self._action_follow_in_disassembly) + if dctx.is_mapped(reg_value): + self._action_follow_in_disassembly.setEnabled(True) + else: + self._action_follow_in_disassembly.setEnabled(False) + + # + # add a menu option to clear exection breakpoints if there is an + # active execution breakpoint set somewhere + # + + menu.addAction(self._action_clear) + + # + # show the right click menu and wait for the user to selection an + # action from the list of visible/active actions + # + + action = menu.exec_(self.mapToGlobal(position)) + + # + # handle the user selected action + # + + if action == self._action_copy_value: + copy_to_clipboard("0x%08X" % reg_value) + elif action == self._action_follow_in_disassembly: + dctx.navigate(reg_value) + elif action == self._action_follow_in_dump: + self.controller.follow_in_dump(reg_name) + elif action == self._action_clear: + self.pctx.breakpoints.clear_execution_breakpoints() + + def refresh(self): + self.viewport().update() + + def _pos_to_field(self, pos): + """ + Get the register field at the given cursor position. + """ + for reg_name, field in self._reg_fields.items(): + full_field = QtCore.QRect(field.name_rect.topLeft(), field.next_rect.bottomRight()) + if full_field.contains(pos): + return field + return None + + def _pos_to_reg(self, pos): + """ + Get the register name at the given cursor position. + """ + reg_field = self._pos_to_field(pos) + return reg_field.name if reg_field else None + + def full_size(self): + if not self.model.registers: + return QtCore.QSize(0, 0) + + width = int(self._reg_pos[0] + self._default_width) + height = int(len(self.model.registers) * self._char_height) + + return QtCore.QSize(width, height) + + def wheelEvent(self, event): + """ + Qt overload to capture wheel events. + """ + + # no execution breakpoints set, nothing to do + if not self.pctx.breakpoints.model.bp_exec: + return + + # mouse hover was not over IP register value, nothing to do + field = self._pos_to_field(event.pos()) + if not (field and field.name == self.model.arch.IP): + return + + # get the IP value currently displayed in the reg window + current_ip = self.model.registers[self.model.arch.IP] + breakpoints = self.pctx.breakpoints.model.bp_exec + + # loop through the execution-based breakpoints + for breakpoint_address in breakpoints: + if breakpoint_address == current_ip: + break + + # no execution breakpoints match the hovered IP + else: + return + + # scroll up + if event.angleDelta().y() > 0: + self.pctx.reader.seek_to_prev(current_ip, BreakpointType.EXEC) + + # scroll down + elif event.angleDelta().y() < 0: + self.pctx.reader.seek_to_next(current_ip, BreakpointType.EXEC) + + return + + def mouseMoveEvent(self, e): + """ + Qt overload to capture mouse movement events. + """ + point = e.pos() + before = self._hovered_arrow + + for reg_name, reg_field in self._reg_fields.items(): + if reg_field.next_rect.contains(point): + self._hovered_arrow = reg_field.next_rect + break + elif reg_field.prev_rect.contains(point): + self._hovered_arrow = reg_field.prev_rect + break + else: + self._hovered_arrow = None + + if before != self._hovered_arrow: + self.viewport().update() + + def mouseDoubleClickEvent(self, event): + """ + Qt overload to capture mouse double-click events. + """ + mouse_position = event.pos() + + # handle duoble (left) click events + if event.button() == QtCore.Qt.LeftButton: + + # confirm that we are consuming the double click event + event.accept() + + # check if the user clicked a known field + field = self._pos_to_field(mouse_position) + + # if the double click was *not* on a register field, clear execution breakpoints + if not field: + self.pctx.breakpoints.clear_execution_breakpoints() + return + + # ignore if the double clicked field (register) was not the IP reg + if not (field and field.name == self.model.arch.IP): + return + + # ignore if the double click was not on the reg value + if not field.value_rect.contains(mouse_position): + return + + # the user double clicked IP, so set a breakpoint on it + self.controller.set_ip_breakpoint() + + def mousePressEvent(self, event): + """ + Qt overload to capture mouse button presses. + """ + mouse_position = event.pos() + + # handle click events + if event.button() == QtCore.Qt.LeftButton: + + # check if the user clicked a known field + field = self._pos_to_field(mouse_position) + + # no field (register name, or register value) was selected + if not field: + self.controller.clear_register_focus() + + # the user clicked on the register value + elif field.value_rect.contains(mouse_position): + self.controller.focus_register_value(field.name) + + # the user clicked on the 'seek to next reg change' arrow + elif field.next_rect.contains(mouse_position): + result = self.pctx.reader.find_next_register_change(field.name) + if result != -1: + self.pctx.reader.seek(result) + + # the user clicked on the 'seek to prev reg change' arrow + elif field.prev_rect.contains(mouse_position): + result = self.pctx.reader.find_prev_register_change(field.name) + if result != -1: + self.pctx.reader.seek(result) + + # the user clicked on the register name + else: + self.controller.focus_register_name(field.name) + + # update the view as selection / drawing may change + self.viewport().update() + + def paintEvent(self, event): + """ + Qt overload of widget painting. + """ + + if not self.model.registers: + return + + painter = QtGui.QPainter(self.viewport()) + + area_size = self.viewport().size() + area_rect = self.viewport().rect() + widget_size = self.full_size() + + painter.fillRect(area_rect, self.pctx.palette.reg_bg) + + brush_defualt = painter.brush() + brush_selected = QtGui.QBrush(self.pctx.palette.standard_selection_bg) + + for reg_name in self.model.arch.REGISTERS: + reg_value = self.model.registers[reg_name] + reg_field = self._reg_fields[reg_name] + + # coloring for when the register is selected by the user + if reg_name == self.model.focused_reg_name: + painter.setBackground(brush_selected) + painter.setBackgroundMode(QtCore.Qt.OpaqueMode) + painter.setPen(self.pctx.palette.standard_selection_fg) + + # default / unselected register colors + else: + painter.setBackground(brush_defualt) + painter.setBackgroundMode(QtCore.Qt.OpaqueMode) + painter.setPen(self.pctx.palette.reg_name_fg) + + # draw register name + painter.drawText(reg_field.name_rect, QtCore.Qt.AlignCenter, reg_name) + + reg_nibbles = self.model.arch.POINTER_SIZE * 2 + if reg_value is None: + rendered_value = "?" * reg_nibbles + else: + rendered_value = f'%0{reg_nibbles}X' % reg_value + + # color register if its value changed as a result of T-1 (previous instr) + if reg_name in self.model.delta_trace: + painter.setPen(self.pctx.palette.reg_changed_trace_fg) + + # color register if its value changed as a result of navigation + # TODO: disabled for now, because it seemed more confusing than helpful... + elif reg_name in self.model.delta_navigation and False: + painter.setPen(self.pctx.palette.reg_changed_navigation_fg) + + # no special highlighting, default register value color text + else: + painter.setPen(self.pctx.palette.reg_value_fg) + + # coloring for when the register is selected by the user + if reg_name == self.model.focused_reg_value: + painter.setPen(self.pctx.palette.standard_selection_fg) + painter.setBackground(brush_selected) + painter.setBackgroundMode(QtCore.Qt.OpaqueMode) + + # default / unselected register colors + else: + painter.setBackground(brush_defualt) + painter.setBackgroundMode(QtCore.Qt.OpaqueMode) + + # special highlighting of the instruction pointer if it matches an active breakpoint + if reg_name == self.model.arch.IP: + if reg_value in self.model.execution_breakpoints: + painter.setPen(self.pctx.palette.navigation_selection_fg) + painter.setBackground(self.pctx.palette.navigation_selection_bg) + + # draw register value + painter.drawText(reg_field.value_rect, QtCore.Qt.AlignCenter, rendered_value) + + # don't draw arrows next to RIP's value + if reg_name == self.model.arch.IP: + continue + + # draw register arrows + for i, rect in enumerate([reg_field.prev_rect, reg_field.next_rect]): + self._draw_arrow(painter, rect, i) + + def _draw_arrow(self, painter, rect, index): + path = QtGui.QPainterPath() + + size = rect.height() + assert size % 2, "Cursor triangle size must be odd" + + # the top point of the triangle + top_x = rect.x() + (0 if index else rect.width()) + top_y = rect.y() + 1 + + # bottom point of the triangle + bottom_x = top_x + bottom_y = top_y + size - 1 + + # the 'tip' of the triangle pointing into towards the center of the trace + tip_x = top_x + ((size // 2) * (1 if index else -1)) + tip_y = top_y + (size // 2) + + # start drawing from the 'top' of the triangle + path.moveTo(top_x, top_y) + + # generate the triangle path / shape + path.lineTo(bottom_x, bottom_y) + path.lineTo(tip_x, tip_y) + path.lineTo(top_x, top_y) + + # dev / debug helper + #painter.setPen(QtCore.Qt.green) + #painter.setBrush(QtGui.QBrush(QtGui.QColor("white"))) + #painter.drawRect(rect) + + # paint the defined triangle + # TODO: don't hardcode colors + painter.setPen(QtCore.Qt.black) + + if self._hovered_arrow == rect: + if index: + painter.setBrush(self.pctx.palette.arrow_next) + else: + painter.setBrush(self.pctx.palette.arrow_prev) + else: + painter.setBrush(self.pctx.palette.arrow_idle) + + painter.drawPath(path) + +class RegisterField(object): + def __init__(self, name, name_rect, value_rect, arrow_rects): + self.name = name + self.name_rect = name_rect + self.value_rect = value_rect + self.prev_rect = arrow_rects[0] + self.next_rect = arrow_rects[1] \ No newline at end of file diff --git a/plugins_sogen-support/tenet/ui/resources/icons/arrow.png b/plugins_sogen-support/tenet/ui/resources/icons/arrow.png new file mode 100644 index 0000000000000000000000000000000000000000..e7e7b1fb34664e2814b51ddae974c00e75e292e4 GIT binary patch literal 2071 zcmbVNeM}Q)7)KZ~6jo=3ojC37OhBgBySB8@3#G#rrI`f;9OyFZad#~@+g{f@Xv?G^ z#^An$WNsvL8AjsBW+Kb{gDn#)GG#^+z$G)vW()oy&JSv=^Hx#zwm>7B&dkeuckjLL z@A*9+@AEwOerfUg)CK7a;^N{`4MqAg@b-ec_4)bWnRk=T2CrmC(Pl0#PP{mB&$D!# z2dKm)Ohpx6W!!+1tW8Q#Y&9))+Z+H5#I$Y)L2jpcsG7Dgb`3o7qYs7{N&{CajhNA) zqidL=dM908UtB@fZzt6htj&k=+&Cbx(L4dUZB{#nyESl>7YFmmG73Xc2)|te7eo|7 zRmM_C$2w_9CB+aD!{ktoS}IdzE0ppzkQ|d^C?-P{7$Q^RN(@)zLNgZ(v^gm=UZ!6+ zqYHd#;2NHH;3(>Hxuh9h+rM~v3a&4dXA40RI@R3^nDE=7S98N)ehoz|#wibQEEZKLfx2e7gj z)=|UqELX!mfjYK5M*#?}(HPV5SX*qim(K4E2Yn>!rSO?5p z8S%z}>zp*fv(5^Zway+?>8#0+9C!k)s$}dG>*BH=5uo)1PitUsYBB^8vz_;*g2lCle9sv zfkAMj3`5~0Mbb1@tw0pCoJI&`HiqQN6$GMG=2lZGDwkH1IkWP5maL7Wz^pv=KjllD z3@Aau`pi6$!i?k&Uc_+VR6R4LQ%=7-GqW;KG+l6ljFf=}CL`xhQ}E0(^UM*LrghcO zfb>7|Vj9M=X5K|O=>iLg)>HEi1;(S1f}QpQ{ilo3y+?|B77jW_WEyK#;3L-TXgla| zPSDCcq9p}!aq(RSeL;o$(#_w`+Wy$KsA=QAH*1>G5=(XGKXe(iZCeh^d*}A4vVF_0 zuEfBS3O!|%$@oY*CH?-UE0N9;X^ zk6d3n=tv19ZvEF-aqPsxr;R8P;Ck{%LWA*h$Ch;O(Tg|GUrzgjresa#3h!IvM^9FsW>XvL zgbxJdAR2E<_H7EC3ak$Fd~|%(4)f%%!{U~~J(797$-)L<mFCchc~dUPOASy&=!%=FfW76)_2+FwT#w2OTy^;yDd;n2|8zNrM? zEA<9JBJ?+2Tk+b##{PQ^Ci~ujD 0: + return density + return INVALID_DENSITY + + @property + def viz_rect(self): + """ + Return a QRect defining the drawable trace visualization. + """ + x, y = self.viz_pos + w, h = self.viz_size + return QtCore.QRect(x, y, w, h) + + @property + def viz_pos(self): + """ + Return (x, y) coordinates of the drawable trace visualization. + """ + return (self._trace_border, self._trace_border) + + @property + def viz_size(self): + """ + Return (width, height) of the drawable trace visualization. + """ + w = max(0, int(self.width() - (self._trace_border * 2))) + h = max(0, int(self.height() - (self._trace_border * 2))) + return (w, h) + + #------------------------------------------------------------------------- + # Public + #------------------------------------------------------------------------- + + def attach_reader(self, reader): + """ + Attach a trace reader to this controller. + """ + self.reset() + + # attach the new reader + self.reader = reader + + # initialize state based on the reader + self.set_bounds(0, reader.trace.length) + + # attach signals to the new reader + reader.idx_changed(self.refresh) + + def set_bounds(self, start_idx, end_idx): + """ + Set the idx bounds of the trace visualization. + """ + assert end_idx > start_idx, f"Invalid Bounds ({start_idx}, {end_idx})" + + # set the bounds of the trace + self.start_idx = max(0, start_idx) + self.end_idx = end_idx + self._end_idx_internal = end_idx + + # update drawing metrics, note that this can 'tweak' end_idx to improve cell rendering + self._refresh_painting_metrics() + + # compute the number of instructions visible + self._last_trace_idx = min(self.reader.trace.length, self.end_idx) + + # refresh/redraw relevant elements + self._refresh_trace_highlights() + self.refresh() + + # return the final / selected bounds + return (self.start_idx, self.end_idx) + + def set_selection(self, start_idx, end_idx): + """ + Set the selection region bounds. + """ + assert end_idx >= start_idx + self._idx_selection_start = start_idx + self._idx_selection_end = end_idx + self.refresh() + + def reset(self): + """ + Reset the trace visualization. + """ + self.reader = None + + self.start_idx = 0 + self.end_idx = 0 + self._last_trace_idx = 0 + + self._idx_pending_selection_origin = INVALID_IDX + self._idx_pending_selection_start = INVALID_IDX + self._idx_pending_selection_end = INVALID_IDX + + self._idx_selection_start = INVALID_IDX + self._idx_selection_end = INVALID_IDX + + self._idx_reads = [] + self._idx_writes = [] + self._idx_executions = [] + + self._refresh_painting_metrics() + self.refresh() + + def refresh(self, *args): + """ + Refresh the trace visualization. + """ + self.update() + + #---------------------------------------------------------------------- + # Qt Overloads + #---------------------------------------------------------------------- + + def mouseMoveEvent(self, event): + """ + Qt overload to capture mouse movement events. + """ + if not self.reader: + return + + # mouse moving while holding left button + if event.buttons() == QtCore.Qt.MouseButton.LeftButton: + self._update_selection(event.y()) + self.refresh() + return + + # simple mouse hover over viz + self._update_hover(event.y()) + self.refresh() + + def mousePressEvent(self, event): + """ + Qt overload to capture mouse button presses. + """ + if not self.reader: + return + + # left mouse button was pressed (but not yet released!) + if event.button() == QtCore.Qt.MouseButton.LeftButton: + idx_origin = self._pos2idx(event.y()) + self._idx_pending_selection_origin = idx_origin + self._idx_pending_selection_start = idx_origin + self._idx_pending_selection_end = idx_origin + + return + + def mouseReleaseEvent(self, event): + """ + Qt overload to capture mouse button releases. + """ + if not self.reader: + return + + # if the left mouse button was released... + if event.button() == QtCore.Qt.MouseButton.LeftButton: + + # + # no selection origin? this means the click probably started + # off this widget, and the user moved their mouse over viz + # ... before releasing... which is not something we care about + # + + if self._idx_pending_selection_origin == INVALID_IDX: + return + + # if the mouse press & release was on the same idx, probably a click + if self._idx_pending_selection_start == self._idx_pending_selection_end: + self._commit_click() + + # a range was selected, so accept/commit it + else: + self._commit_selection() + + def leaveEvent(self, _): + """ + Qt overload to capture the mouse hover leaving the widget. + """ + self._hovered_idx = INVALID_IDX + self.refresh() + + def wheelEvent(self, event): + """ + Qt overload to capture wheel events. + """ + if not self.reader: + return + + # holding the shift key while scrolling is used to 'step over' + mod_keys = QtGui.QGuiApplication.keyboardModifiers() + step_over = bool(mod_keys & QtCore.Qt.ShiftModifier) + + # scrolling up, so step 'backwards' through the trace + if event.angleDelta().y() > 0: + self.reader.step_backward(1, step_over) + + # scrolling down, so step 'forwards' through the trace + elif event.angleDelta().y() < 0: + self.reader.step_forward(1, step_over) + + self.refresh() + event.accept() + + def resizeEvent(self, _): + """ + Qt overload to capture resize events for the widget. + """ + self._resize_timer.start(500) + + #------------------------------------------------------------------------- + # Helpers (Internal) + #------------------------------------------------------------------------- + # + # NOTE: this stuff should probably only be called by the 'mainthread' + # to ensure density / viz dimensions and stuff don't change. + # + + def _resize_stopped(self): + """ + Delayed handler of resize events. + + We delay handling resize events because several resize events can + trigger when a user is dragging to resize a window. we only really + care to recompute the visualization when they stop 'resizing' it. + """ + self.set_bounds(self.start_idx, self._end_idx_internal) + + def _refresh_painting_metrics(self): + """ + Refresh any metrics and calculations required to paint the widget. + """ + self._cell_height = 0 + self._cell_border = 0 + self._cell_spacing = 0 + + # how many 'instruction' cells *must* be shown based on current selection? + num_cell = self._end_idx_internal - self.start_idx + if not num_cell: + return + + # how many 'y' pixels are available, per cell (including spacing, between cells) + _, viz_h = self.viz_size + given_space_per_cell = viz_h / num_cell + + # compute the smallest possible cell height, with overlapping cell borders + min_full_cell_height = self._cell_min_height + self._cell_min_border + + # don't draw the trace vizualization as cells if the density is too high + if given_space_per_cell < min_full_cell_height: + #logger.debug(f"No need for cells -- {given_space_per_cell}, min req {min_full_cell_height}") + return + + # compute the pixel height of a cell at maximum height (including borders) + max_cell_height_with_borders = self._cell_max_height + self._cell_max_border * 2 + + # compute how much leftover space there is to use between cells + spacing_between_max_cells = given_space_per_cell - max_cell_height_with_borders + + # maximum sized instruction cells, with 'infinite' possible spacing between cells + if spacing_between_max_cells > max_cell_height_with_borders: + self._cell_border = self._cell_max_border + self._cell_height = self._cell_max_height + self._cell_spacing = spacing_between_max_cells + return + + # dynamically compute cell dimensions for drawing + self._cell_height = max(self._cell_min_height, min(int(given_space_per_cell * 0.95), self._cell_max_height)) + self._cell_border = max(self._cell_min_border, min(int(given_space_per_cell * 0.05), self._cell_max_border)) + self._cell_spacing = int(given_space_per_cell - (self._cell_height + self._cell_border * 2)) + #logger.debug(f"Dynamic cells -- Given: {given_space_per_cell}, Height {self._cell_height}, Border: {self._cell_border}, Spacing: {self._cell_spacing}") + + # if there's not enough to justify having spacing, use shared borders between cells (usually very small cells) + if self._cell_spacing < self._cell_min_spacing: + self._cell_spacing = self._cell_min_border * -2 + + # compute the final number of y pixels used by each 'cell' (an executed instruction) + used_space_per_cell = self._cell_height + self._cell_border * 2 + self._cell_spacing + + # compute how many cells we can *actually* show in the space available + num_cell_allowed = int(viz_h / used_space_per_cell) + 1 + #logger.debug(f"Num Cells {num_cell} vs Available Space {num_cell_allowed}") + + self.end_idx = self.start_idx + num_cell_allowed + + def _idx2pos(self, idx): + """ + Translate a given idx to its first Y coordinate. + """ + if idx < self.start_idx or idx >= self.end_idx: + #logger.warn(f"idx2pos failed (start: {self.start_idx:,} idx: {idx:,} end: {self.end_idx:,}") + return INVALID_POS + + density = self.density + if density == INVALID_DENSITY: + #logger.warn(f"idx2pos failed (INVALID_DENSITY)") + return INVALID_POS + + # convert the absolute idx to one that is 'relative' to the viz + relative_idx = idx - self.start_idx + + # re-base y to the start of the viz region + _, y = self.viz_pos + + # + # compute and return an 'approximate' y position of the given idx + # when the visualization is not using cell metrics (too dense) + # + + if not self.cells_visible: + y += int(relative_idx / density) + + # sanity check + _, viz_y = self.viz_pos + _, viz_h = self.viz_size + assert y >= viz_y + assert y < (viz_y + viz_h) + + # return the approximate y position of the given timestamp + return y + + #assert self._cell_spacing % 2 == 0 + + # compute the y position of the 'first' cell + y += self._cell_spacing // 2 # pad out from top + y += self._cell_border # top border of cell + + # compute the y position of any given cell after the first + y += self._cell_height * relative_idx # cell body + y += self._cell_border * relative_idx # cell bottom border + y += self._cell_spacing * relative_idx # full space between cells + y += self._cell_border * relative_idx # cell top border + + # return the y position of the cell corresponding to the given timestamp + return y + + def _pos2idx(self, y): + """ + Translate a given Y coordinate to an approximate idx. + """ + _, viz_y = self.viz_pos + _, viz_h = self.viz_size + + # clamp clearly out-of-bounds requests to the start/end idx values + if y < viz_y: + return self.start_idx + elif y >= viz_y + viz_h: + return self.end_idx - 1 + + density = self.density + if density == INVALID_DENSITY: + #logger.warn(f"pos2idx failed (INVALID_DENSITY)") + return INVALID_IDX + + # translate/rebase global y to viz relative y + y -= self._trace_border + + # compute the relative idx based on how much space is used per cell + if self.cells_visible: + + # this is how many vertical pixel each cell uses, including spacing to the next cell + used_space_per_cell = self._cell_height + self._cell_border * 2 + self._cell_spacing + + # compute relative idx for cell-based views + y -= self._cell_border + relative_idx = int(y / used_space_per_cell) + + # compute the approximate relative idx using the instruction density metric + else: + relative_idx = round(y * density) + + # convert the viz-relative idx, to its global trace idx timestamp + idx = self.start_idx + relative_idx + + # clamp idx to the start / end of visible tracebar range + return self._clamp_idx(idx) + + def _compute_pixel_distance(self, y, idx): + """ + Compute the pixel distance from a given Y to an idx. + """ + + # get the y pixel position of the given idx + y_idx = self._idx2pos(idx) + if y_idx == INVALID_POS: + return -1 + + # + # if the visualization drawing cells, adjust the reported y coordinate + # of the given idx to the center of the cell. this makes distance + # calculations more correct + # + + if self.cells_visible: + y_idx += int(self._cell_height/2) + + # return the on-screen pixel distance between the two y coords + return abs(y - y_idx) + + def _update_hover(self, current_y): + """ + Update the trace visualization based on the mouse hover. + """ + self._hovered_idx = INVALID_IDX + + # see if there's an interesting trace event close to the hover + hovered_idx = self._pos2idx(current_y) + closest_idx = self._get_closest_highlighted_idx(hovered_idx) + + # + # if the closest highlighted event (mem access, breakpoint) + # is outside the trace view bounds, then we don't need to + # do any special hover highlighting... + # + + if not(self.start_idx <= closest_idx < self.end_idx): + return + + # + # compute the on-screen pixel distance between the hover and the + # closest highlighted event + # + + px_distance = self._compute_pixel_distance(current_y, closest_idx) + #logger.debug(f"hovered idx {hovered_idx:,}, closest idx {closest_idx:,}, dist {px_distance} (start: {self.start_idx:,} end: {self.end_idx:,}") + if px_distance == -1: + return + + # clamp the lock-on distance depending on the scale of zoom / cell size + lockon_distance = max(self._magnetism_distance, self._cell_height) + + # + # if the trace event is within the magnetized distance of the user + # cursor, lock on to it. this makes 'small' things easier to click + # + + if px_distance < lockon_distance: + self._hovered_idx = closest_idx + + def _update_selection(self, y): + """ + Update the user region selection of the trace visualization based on the current y. + """ + idx_event = self._pos2idx(y) + + if idx_event > self._idx_pending_selection_origin: + self._idx_pending_selection_start = self._idx_pending_selection_origin + self._idx_pending_selection_end = idx_event + else: + self._idx_pending_selection_end = self._idx_pending_selection_origin + self._idx_pending_selection_start = idx_event + + self._idx_selection_start = INVALID_IDX + self._idx_selection_end = INVALID_IDX + + def _global_selection_changed(self, start_idx, end_idx): + """ + Handle selection behavior specific to a 'global' trace visualizations. + """ + if start_idx == end_idx: + return + self.set_selection(start_idx, end_idx) + + def _zoom_selection_changed(self, start_idx, end_idx): + """ + Handle selection behavior specific to a 'zoomer' trace visualizations. + """ + if start_idx == end_idx: + self.hide() + else: + self.show() + self.set_bounds(start_idx, end_idx) + + def _commit_click(self): + """ + Accept a click event. + """ + selected_idx = self._idx_pending_selection_start + + # use a 'magnetized' selection, if available + if self._hovered_idx != INVALID_IDX: + selected_idx = self._hovered_idx + self._hovered_idx = INVALID_IDX + + # reset pending selection + self._idx_pending_selection_origin = INVALID_IDX + self._idx_pending_selection_start = INVALID_IDX + self._idx_pending_selection_end = INVALID_IDX + + # does the click fall within the existing selected region? + within_region = (self._idx_selection_start <= selected_idx <= self._idx_selection_end) + + # nope click is outside the region, so clear the region selection + if not within_region: + self._idx_selection_start = INVALID_IDX + self._idx_selection_end = INVALID_IDX + self._notify_selection_changed(INVALID_IDX, INVALID_IDX) + + #print(f"Jumping to {selected_idx:,}") + self.reader.seek(selected_idx) + self.refresh() + + def _commit_selection(self): + """ + Accept a selection event. + """ + new_start = self._idx_pending_selection_start + new_end = self._idx_pending_selection_end + + # reset pending selections + self._idx_pending_selection_origin = INVALID_IDX + self._idx_pending_selection_start = INVALID_IDX + self._idx_pending_selection_end = INVALID_IDX + + # + # if we just selected a new region on a trace viz that's a + # 'zoomer', then we will apply the zoom-in action to ourself by + # adjusting our visible regions (bounds) + # + # NOTE: that we don't have to do this on a global / static trace + # viz, because the 'zoomers' will be notified as a listener of + # the selection change events + # + + if self._is_zoom: + + # + # ensure the committed selection is also reset as we are about + # to zoom-in and should not have an active selection once done + # + + self._idx_selection_start = INVALID_IDX + self._idx_selection_end = INVALID_IDX + + # + # apply the new zoom-in / viz bounds to ourself + # + # NOTE: because the special cell-drawing metrics / computation, set + # bounds can 'tweak' the end value, so we want to grab it here + # + + new_start, new_end = self.set_bounds(new_start, new_end) + + # commit the new selection for global trace visualizations + else: + self._idx_selection_start = new_start + self._idx_selection_end = new_end + + # notify listeners of our selection change + self._notify_selection_changed(new_start, new_end) + + def _get_closest_highlighted_idx(self, idx): + """ + Return the closest idx (timestamp) to the given idx. + """ + closest_idx = INVALID_IDX + smallest_distace = 999999999999999999999999 + for entries in [self._idx_reads, self._idx_writes, self._idx_executions]: + for current_idx in entries: + distance = abs(idx - current_idx) + if distance < smallest_distace: + closest_idx = current_idx + smallest_distace = distance + return closest_idx + + def _breakpoints_changed(self): + """ + The focused breakpoint has changed. + """ + self._refresh_trace_highlights() + self.refresh() + + def _refresh_trace_highlights(self): + """ + Refresh trace event / highlight info from the underlying trace reader. + """ + self._idx_reads = [] + self._idx_writes = [] + self._idx_executions = [] + + reader, density = self.reader, self.density + if not (reader and density != INVALID_DENSITY): + return + + model = self.pctx.breakpoints.model + + # fetch executions for all breakpoints + for bp in model.bp_exec.values(): + executions = reader.get_executions_between(bp.address, self.start_idx, self.end_idx, density) + self._idx_executions.extend(executions) + + # fetch all memory read (only) breakpoints hits + for bp in model.bp_read.values(): + if bp.length == 1: + reads = reader.get_memory_reads_between(bp.address, self.start_idx, self.end_idx, density) + else: + reads = reader.get_memory_region_reads_between(bp.address, bp.length, self.start_idx, self.end_idx, density) + self._idx_reads.extend(reads) + + # fetch all memory write (only) breakpoint hits + for bp in model.bp_write.values(): + if bp.length == 1: + writes = reader.get_memory_writes_between(bp.address, self.start_idx, self.end_idx, density) + else: + writes = reader.get_memory_region_writes_between(bp.address, bp.length, self.start_idx, self.end_idx, density) + self._idx_writes.extend(writes) + + # fetch memory access for all breakpoints + for bp in model.bp_access.values(): + if bp.length == 1: + reads, writes = reader.get_memory_accesses_between(bp.address, self.start_idx, self.end_idx, density) + else: + reads, writes = reader.get_memory_region_accesses_between(bp.address, bp.length, self.start_idx, self.end_idx, density) + self._idx_reads.extend(reads) + self._idx_writes.extend(writes) + + def _clamp_idx(self, idx): + """ + Clamp the given idx to the bounds of this trace view. + """ + if idx < self.start_idx: + return self.start_idx + elif idx >= self.end_idx: + return self.end_idx - 1 + return idx + + #------------------------------------------------------------------------- + # Drawing + #------------------------------------------------------------------------- + + def paintEvent(self, event): + """ + Qt overload of widget painting. + + TODO/FUTURE: I was planning to make this paint by layer, and only + re-paint dirty layers as necessary. but I think it's unecessary to + do at this time as I don't think we're pressed for perf. + """ + painter = QtGui.QPainter(self) + + # + # draw instructions / trace landscape + # + + self._draw_base() + painter.drawImage(0, 0, self._image_base) + + # + # draw accesses along the trace timeline + # + + self._draw_highlights() + painter.drawImage(0, 0, self._image_highlights) + + # + # draw user region selection over trace timeline + # + + self._draw_selection() + painter.drawImage(0, 0, self._image_selection) + + # + # draw border around trace timeline + # + + self._draw_border() + painter.drawImage(0, 0, self._image_border) + + # + # draw current trace position cursor + # + + self._draw_cursor() + painter.drawImage(0, 0, self._image_cursor) + + #painter.drawImage(0, 0, self._image_final) + + def _draw_base(self): + """ + Draw the trace visualization of executed code. + """ + + # + # NOTE: DO NOT REMOVE !!! Qt will CRASH if we do not explicitly delete + # these here (dangling internal pointer to device/image otherwise?!?) + # + + del self._painter_base + + self._image_base = QtGui.QImage(self.width(), self.height(), QtGui.QImage.Format_ARGB32) + self._image_base.fill(self.pctx.palette.trace_bedrock) + #self._image_base.fill(QtGui.QColor("red")) # NOTE/debug + self._painter_base = QtGui.QPainter(self._image_base) + + # redraw instructions + if self.cells_visible: + self._draw_code_cells(self._painter_base) + else: + self._draw_code_trace(self._painter_base) + + def _draw_code_trace(self, painter): + """ + Draw a 'zoomed out' trace visualization of executed code. + """ + dctx = disassembler[self.pctx] + viz_w, viz_h = self.viz_size + viz_x, viz_y = self.viz_pos + + for i in range(viz_h): + + # convert a y pixel in the viz region to an executed address + wid_y = viz_y + i + idx = self._pos2idx(wid_y) + + # + # since we can conciously set a trace visualization bounds bigger + # than the actual underlying trace, it is possible for the trace + # to not take up the entire available space. + # + # when we reach the 'end' of the trace, we obviously can stop + # drawing any sort of landscape for it! + # + + if idx >= self._last_trace_idx: + break + + # get the executed/code address for the current idx that will represent this line + address = self.reader.get_ip(idx) + rebased_address = self.reader.analysis.rebase_pointer(address) + + # select the color for instructions that can be viewed with Tenet + if dctx.is_mapped(rebased_address): + painter.setPen(self.pctx.palette.trace_instruction) + + # unexplorable parts of the trace are 'greyed' out (eg, not in IDB) + else: + painter.setPen(self.pctx.palette.trace_unmapped) + + # paint the current line + painter.drawLine(viz_x, wid_y, viz_w, wid_y) + + def _draw_code_cells(self, painter): + """ + Draw a 'zoomed in', cell-based, trace visualization of executed code. + """ + + # + # if there is no spacing between cells, that means they are going to + # be relatively small and have shared 'cell walls' (borders) + # + # we attempt to maximize contrast between border and cell color, while + # attempting to keep the tracebar color visually consistent + # + + # compute the color to use for the borders between cells + border_color = self.pctx.palette.trace_cell_wall + if self._cell_spacing < 0: + border_color = self.pctx.palette.trace_cell_wall_contrast + + # compute the color to use for the cell bodies + if self._cell_spacing < 0: + ratio = (self._cell_border / (self._cell_height - 1)) * 0.5 + lighten = 100 + int(ratio * 100) + cell_color = self.pctx.palette.trace_instruction.lighter(lighten) + #print(f"Lightened by {lighten}% (Border: {self._cell_border}, Body: {self._cell_height}") + else: + cell_color = self.pctx.palette.trace_instruction + + border_pen = QtGui.QPen(border_color, self._cell_border, QtCore.Qt.SolidLine) + painter.setPen(border_pen) + painter.setBrush(cell_color) + + viz_x, _ = self.viz_pos + viz_w, _ = self.viz_size + + # compute cell positioning info + x = viz_x + self._cell_border * -1 + w = viz_w + self._cell_border + h = self._cell_height + + dctx = disassembler[self.pctx] + + # draw each cell + border + for idx in range(self.start_idx, self._last_trace_idx): + + # get the executed/code address for the current idx that will represent this cell + address = self.reader.get_ip(idx) + rebased_address = self.reader.analysis.rebase_pointer(address) + + # select the color for instructions that can be viewed with Tenet + if dctx.is_mapped(rebased_address): + painter.setBrush(cell_color) + + # unexplorable parts of the trace are 'greyed' out (eg, not in IDB) + else: + painter.setBrush(self.pctx.palette.trace_unmapped) + + y = self._idx2pos(idx) + painter.drawRect(int(x), int(y), int(w), int(h)) + + def _draw_highlights(self): + """ + Draw active event highlights (mem access, breakpoints) for the trace visualization. + """ + + # + # NOTE: DO NOT REMOVE !!! Qt will CRASH if we do not explicitly delete + # these here (dangling internal pointer to device/image otherwise?!?) + # + + del self._painter_highlights + + self._image_highlights = QtGui.QImage(self.width(), self.height(), QtGui.QImage.Format_ARGB32) + self._image_highlights.fill(QtCore.Qt.transparent) + self._painter_highlights = QtGui.QPainter(self._image_highlights) + + if self.cells_visible: + self._draw_highlights_cells(self._painter_highlights) + else: + self._draw_highlights_trace(self._painter_highlights) + + def _draw_highlights_cells(self, painter): + """ + Draw cell-based event highlights. + """ + viz_w, _ = self.viz_size + viz_x, _ = self.viz_pos + + access_sets = \ + [ + (self._idx_reads, self.pctx.palette.mem_read_bg), + (self._idx_writes, self.pctx.palette.mem_write_bg), + (self._idx_executions, self.pctx.palette.breakpoint), + ] + + painter.setPen(QtCore.Qt.NoPen) + + h = self._cell_height - self._cell_border + + for entries, cell_color in access_sets: + painter.setBrush(cell_color) + + for idx in entries: + + # skip entries that fall outside the visible zoom + if not(self.start_idx <= idx < self.end_idx): + continue + + # slight tweak of y because we are only drawing a highlighted + # cell body without borders + y = self._idx2pos(idx) + self._cell_border + + # draw cell body + painter.drawRect(int(viz_x), int(y), int(viz_w), int(h)) + + def _draw_highlights_trace(self, painter): + """ + Draw trace-based event highlights. + """ + viz_w, _ = self.viz_size + viz_x, _ = self.viz_pos + + access_sets = \ + [ + (self._idx_reads, self.pctx.palette.mem_read_bg), + (self._idx_writes, self.pctx.palette.mem_write_bg), + (self._idx_executions, self.pctx.palette.breakpoint), + ] + + for entries, color in access_sets: + painter.setPen(color) + + for idx in entries: + + # skip entries that fall outside the visible zoom + if not(self.start_idx <= idx < self.end_idx): + continue + + y = self._idx2pos(idx) + painter.drawLine(viz_x, y, viz_w, y) + + def _draw_cursor(self): + """ + Draw the user cursor / current position in the trace. + """ + path = QtGui.QPainterPath() + + size = 13 + assert size % 2, "Cursor triangle size must be odd" + + del self._painter_cursor + self._image_cursor = QtGui.QImage(self.width(), self.height(), QtGui.QImage.Format_ARGB32) + self._image_cursor.fill(QtCore.Qt.transparent) + self._painter_cursor = QtGui.QPainter(self._image_cursor) + + # compute the y coordinate / line to center the user cursor around + cursor_y = self._idx2pos(self.reader.idx) + draw_reader_cursor = bool(cursor_y != INVALID_IDX) + + if self.cells_visible: + cell_y = cursor_y + self._cell_border + cell_body_height = self._cell_height - self._cell_border + cursor_y += self._cell_height/2 + + # the top point of the triangle + top_x = 0 + top_y = cursor_y - (size // 2) # vertically align the triangle so the tip matches the cross section + + # bottom point of the triangle + bottom_x = top_x + bottom_y = top_y + size - 1 + + # the 'tip' of the triangle pointing into towards the center of the trace + tip_x = top_x + (size // 2) + tip_y = top_y + (size // 2) + + # start drawing from the 'top' of the triangle + path.moveTo(top_x, top_y) + + # generate the triangle path / shape + path.lineTo(bottom_x, bottom_y) + path.lineTo(tip_x, tip_y) + path.lineTo(top_x, top_y) + + viz_x, _ = self.viz_pos + viz_w, _ = self.viz_size + + # draw the user cursor in cell mode + if self.cells_visible: + + # normal fixed / current reader cursor + self._painter_cursor.setPen(QtCore.Qt.NoPen) + self._painter_cursor.setBrush(self.pctx.palette.trace_cursor_highlight) + + if draw_reader_cursor: + self._painter_cursor.drawRect(int(viz_x), int(cell_y), int(viz_w), int(cell_body_height)) + + # cursor hover highlighting an event + if self._hovered_idx != INVALID_IDX: + hovered_y = self._idx2pos(self._hovered_idx) + hovered_cell_y = hovered_y + self._cell_border + self._painter_cursor.drawRect(int(viz_x), int(hovered_cell_y), int(viz_w), int(cell_body_height)) + + # draw the user cursor in dense/landscape mode + else: + self._painter_cursor.setPen(self._pen_cursor) + + # normal fixed / current reader cursor + if draw_reader_cursor: + self._painter_cursor.drawLine(viz_x, cursor_y, viz_w, cursor_y) + + # cursor hover highlighting an event + if self._hovered_idx != INVALID_IDX: + hovered_y = self._idx2pos(self._hovered_idx) + self._painter_cursor.drawLine(viz_x, hovered_y, viz_w, hovered_y) + + if not draw_reader_cursor: + return + + # paint the defined triangle + self._painter_cursor.setPen(self.pctx.palette.trace_cursor_border) + self._painter_cursor.setBrush(self.pctx.palette.trace_cursor) + self._painter_cursor.drawPath(path) + + def _draw_selection(self): + """ + Draw a region selection rect. + """ + + # + # NOTE: DO NOT REMOVE !!! Qt will CRASH if we do not explicitly delete + # these here (dangling internal pointer to device/image otherwise?!?) + # + + del self._painter_selection + + viz_w, viz_h = self.viz_size + self._image_selection = QtGui.QImage(self.width(), self.height(), QtGui.QImage.Format_ARGB32) + self._image_selection.fill(QtCore.Qt.transparent) + self._painter_selection = QtGui.QPainter(self._image_selection) + + # active / on-going selection event + if self._idx_pending_selection_start != INVALID_IDX: + start_idx = self._idx_pending_selection_start + end_idx = self._idx_pending_selection_end + + # fixed / committed selection + elif self._idx_selection_start != INVALID_IDX: + start_idx = self._idx_selection_start + end_idx = self._idx_selection_end + + # no region selection, nothing to do... + else: + return + + start_idx = self._clamp_idx(start_idx) + end_idx = self._clamp_idx(end_idx) + + # nothing to draw + if start_idx == end_idx: + return + + start_y = self._idx2pos(start_idx) + end_y = self._idx2pos(end_idx) + + self._painter_selection.setBrush(self._brush_selection) + self._painter_selection.setPen(self._pen_selection) + + # TODO/FUTURE: real border math + viz_x, viz_y = self.viz_pos + + x = viz_x + y = start_y + w = viz_w + h = end_y - start_y + + # draw the screen door / selection rect + self._painter_selection.drawRect(int(x), int(y), int(w), int(h)) + + def _draw_border(self): + """ + Draw the border around the trace timeline. + """ + wid_w, wid_h = self.width(), self.height() + + # + # NOTE: DO NOT REMOVE !!! Qt will CRASH if we do not explicitly delete + # these here (dangling internal pointer to device/image otherwise?!?) + # + + del self._painter_border + + self._image_border = QtGui.QImage(wid_w, wid_h, QtGui.QImage.Format_ARGB32) + self._image_border.fill(QtCore.Qt.transparent) + self._painter_border = QtGui.QPainter(self._image_border) + + color = self.pctx.palette.trace_border + #color = QtGui.QColor("red") # NOTE: for dev/debug testing + border_pen = QtGui.QPen(color, self._trace_border, QtCore.Qt.SolidLine) + self._painter_border.setPen(border_pen) + + w = wid_w - self._trace_border + h = wid_h - self._trace_border + + # draw the border around the tracebar using a blank rect + stroke (border) + self._painter_border.drawRect(0, 0, w, h) + + #---------------------------------------------------------------------- + # Callbacks + #---------------------------------------------------------------------- + + def selection_changed(self, callback): + """ + Subscribe a callback for a trace slice selection change event. + """ + register_callback(self._selection_changed_callbacks, callback) + + def _notify_selection_changed(self, start_idx, end_idx): + """ + Notify listeners of a trace slice selection change event. + """ + notify_callback(self._selection_changed_callbacks, start_idx, end_idx) + +#----------------------------------------------------------------------------- +# Trace View +#----------------------------------------------------------------------------- + +class TraceView(QtWidgets.QWidget): + + def __init__(self, pctx, parent=None): + super(TraceView, self).__init__(parent) + self.pctx = pctx + self._init_ui() + + def _init_ui(self): + self._init_bars() + self._init_ctx_menu() + + def attach_reader(self, reader): + self.trace_global.attach_reader(reader) + self.trace_local.attach_reader(reader) + self.trace_local.hide() + + def detach_reader(self): + self.trace_global.reset() + self.trace_local.reset() + self.trace_local.hide() + + def _init_bars(self): + self.trace_local = TraceBar(self.pctx, zoom=True) + self.trace_global = TraceBar(self.pctx) + + # connect the local view to follow the global selection + self.trace_global.selection_changed(self.trace_local._zoom_selection_changed) + self.trace_local.selection_changed(self.trace_global._global_selection_changed) + + # connect other signals + self.pctx.breakpoints.model.breakpoints_changed(self.trace_global._breakpoints_changed) + self.pctx.breakpoints.model.breakpoints_changed(self.trace_local._breakpoints_changed) + + # hide the zoom bar by default + self.trace_local.hide() + + # setup the layout and spacing for the tracebar + hbox = QtWidgets.QHBoxLayout(self) + hbox.setContentsMargins(3, 3, 3, 3) + hbox.setSpacing(3) + + # add the layout container / mechanism to the toolbar + hbox.addWidget(self.trace_local) + hbox.addWidget(self.trace_global) + + self.setLayout(hbox) + + def _init_ctx_menu(self): + """ + Initialize the right click context menu actions. + """ + self._menu = QtWidgets.QMenu() + + # create actions to show in the context menu + self._action_clear = self._menu.addAction("Clear all breakpoints") + self._menu.addSeparator() + self._action_load = self._menu.addAction("Load new trace") + self._action_close = self._menu.addAction("Close trace") + + # install the right click context menu + self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + self.customContextMenuRequested.connect(self._ctx_menu_handler) + + #-------------------------------------------------------------------------- + # Signals + #-------------------------------------------------------------------------- + + def _ctx_menu_handler(self, position): + """ + Handle a right click event (populate/show context menu). + """ + action = self._menu.exec_(self.mapToGlobal(position)) + if action == self._action_load: + self.pctx.interactive_load_trace(True) + elif action == self._action_close: + self.pctx.close_trace() + elif action == self._action_clear: + self.pctx.breakpoints.clear_breakpoints() + + def update_from_model(self): + for bar in self.model.tracebars.values()[::-1]: + self.hbox.addWidget(bar) + + # this will insert the children (tracebars) and apply spacing as appropriate + self.bar_container.setLayout(self.hbox) + +#----------------------------------------------------------------------------- +# Dockable Trace Visualization +#----------------------------------------------------------------------------- + +class TraceDock(QtWidgets.QToolBar): + """ + A Qt 'Toolbar' to house the TraceBar visualizations. + + We use a Toolbar explicitly because they are given unique docking regions + around the QMainWindow in Qt-based applications. This allows us to pin + the visualizations to areas where they will not be dist + """ + def __init__(self, pctx, parent=None): + super(TraceDock, self).__init__(parent) + self.pctx = pctx + self.view = TraceView(pctx, self) + self.setMovable(False) + self.setContentsMargins(0, 0, 0, 0) + self.addWidget(self.view) + + def attach_reader(self, reader): + self.view.attach_reader(reader) + + def detach_reader(self): + self.view.detach_reader() \ No newline at end of file diff --git a/plugins_sogen-support/tenet/util/__init__.py b/plugins_sogen-support/tenet/util/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/plugins_sogen-support/tenet/util/__pycache__/__init__.cpython-311.pyc b/plugins_sogen-support/tenet/util/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5cec821819e8c5cbbd25bbc76f522859d31a0c37 GIT binary patch literal 180 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", i))[0] >> 8 + +#------------------------------------------------------------------------------ +# Python Callback / Signals +#------------------------------------------------------------------------------ + +def register_callback(callback_list, callback): + """ + Register a callable function to the given callback_list. + + Adapted from http://stackoverflow.com/a/21941670 + """ + + # create a weakref callback to an object method + try: + callback_ref = weakref.ref(callback.__func__), weakref.ref(callback.__self__) + + # create a wweakref callback to a stand alone function + except AttributeError: + callback_ref = weakref.ref(callback), None + + # 'register' the callback + callback_list.append(callback_ref) + +def notify_callback(callback_list, *args): + """ + Notify the given list of registered callbacks of an event. + + The given list (callback_list) is a list of weakref'd callables + registered through the register_callback() function. To notify the + callbacks of an event, this function will simply loop through the list + and call them. + + This routine self-heals by removing dead callbacks for deleted objects as + it encounters them. + + Adapted from http://stackoverflow.com/a/21941670 + """ + cleanup = [] + + # + # loop through all the registered callbacks in the given callback_list, + # notifying active callbacks, and removing dead ones. + # + + for callback_ref in callback_list: + callback, obj_ref = callback_ref[0](), callback_ref[1] + + # + # if the callback is an instance method, deference the instance + # (an object) first to check that it is still alive + # + + if obj_ref: + obj = obj_ref() + + # if the object instance is gone, mark this callback for cleanup + if obj is None: + cleanup.append(callback_ref) + continue + + # call the object instance callback + try: + callback(obj, *args) + + # assume a Qt cleanup/deletion occurred + except RuntimeError as e: + cleanup.append(callback_ref) + continue + + # if the callback is a static method... + else: + + # if the static method is deleted, mark this callback for cleanup + if callback is None: + cleanup.append(callback_ref) + continue + + # call the static callback + callback(*args) + + # remove the deleted callbacks + for callback_ref in cleanup: + callback_list.remove(callback_ref) \ No newline at end of file diff --git a/plugins_sogen-support/tenet/util/qt/__init__.py b/plugins_sogen-support/tenet/util/qt/__init__.py new file mode 100644 index 0000000..810ea60 --- /dev/null +++ b/plugins_sogen-support/tenet/util/qt/__init__.py @@ -0,0 +1,5 @@ +from .shim import * + +if QT_AVAILABLE: + from .util import * + from .waitbox import WaitBox \ No newline at end of file diff --git a/plugins_sogen-support/tenet/util/qt/__pycache__/__init__.cpython-311.pyc b/plugins_sogen-support/tenet/util/qt/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8ee3f11cedbe0b13b93a88069c0b11d06a4ae0ea GIT binary patch literal 333 zcmZ3^%ge<81nuV?661jMV-N=hn4pZ$T0q8hh7^Vr#vFzahE&EP9x$6J1SH0k!<@?! z#lpyt!W<07nk+9Fff_U!Z!u~C3HI>B%o3;k3O`NOTP(#HnYp)k0z={*!yG+*9G!ez zZ?Tk?WaivrF9)hh%C9J50cu^z@EK(FFE4ki7?=Fw%EX++B7Ntg#N_PMB7IL6M}>f* z{It~K;>`TK#2f`nJwyG1oYM5nyyBRW)V$P^7?6oEg(WfZ@tJvGcq#XU=Y3lLl2nwKQJ&cD&AlbzW_rwSQIW`Lmyay3c;$2 HK%oc#^!8cM literal 0 HcmV?d00001 diff --git a/plugins_sogen-support/tenet/util/qt/__pycache__/shim.cpython-311.pyc b/plugins_sogen-support/tenet/util/qt/__pycache__/shim.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..657c6c1f9b2850976f0e33a7f792ce283c523731 GIT binary patch literal 988 zcmZ{g&x_MQ6vt4cO(9K;dozOhHfwUHS;Tq3YlWE~r2B)%gW=VL^RmrmnCDQQs|4*j8-L2di^# zT@2WF5RyYx;Cmq6>%i*+7apgW-QV6j`zh#9rrN8eE;+nZYl+Xr@}Br)ujp{ks^$Gs zt5TP0c}EDEYPnF{a=6M+vhrEUDh*VJnQeS9+cB`>u#z?1qrvorDt8pibi$GqATvF~ z6tUrOWxZ!$tBA3I9icL;sU$8Q7r<$wtK9FLoYNy-QoJeojw|u6nnqfI;IwABK zoOvI7zFWHAEEwjnt+jD(2e)4yDmYgzh=~d|UMZ%j8bl~@FMB7~(*_;2Yc?&Vt60r} zrE1N-)ihtLdbT$_M-OyE9%#xVyiQ~xNmC@%r67dv3W!}f=H2Tcdij{M2@7OI$3EnL zP-`v_px}mhMzB$Gyc{;VHC_%IrN_%*qxg6^To$Ztj(Gd(#)unn6Vme6TmTtiUTnmV zXyT0#CXD-Rq^D>(-Hd z{LFYfZ=UD(`#s~oCK4hC>5G3|kuw6v{fFMvQ>a#WI|YR`PUTcy=kEGBf0tK7YFH1= zaw>Yv-3_Y|ut<%9ji@bPqiPInizixef>?X8^>A*+Aq-ASZW@JhjC!Fp~AQ* zK^ilS2iVZCp9tzep0QVSd`*Kpadst#?-0$#1oj8fu`Ie=>D|QDi}%J& zYem*&GB8GDWf7Bs>*FKREn?1L%hF6k)};&mLjyTIpV18Ko{bG`-^<&Ye(%1Y_vco; z#AJQ3bb8VXgHtS6=fRY@pJH5V`-@A@F0GcF_$fDjsytpru`SfGi8{W>0n!Wgb=lAx~E9mhWEu06W8dg5hY`I>{eUDRDc-L&?`qZ?1`^%H;TE8lQX zkBfRL^*>3#fC^keqAOu8UVy^?ZA;_)r9cT?f@TEot_=q~ZqyDo*D!HCgTizA1~0 z%JL!x-B$8ee}Df}GDh~poL+o-5}+Yx@Tj@$wE=}w_p`EQENO;nE|Ct}SD)K8;3Nj!#9(=%CIk;}Jibw8zunlRer7EHClmmi4IWZ3#y=g`?}Uj&Q;iPLxNt zqg?C07Z;vgczWsirS%RceyoCy`TlS8VxaO=>YBCbeIQr)MHWIyqzD1D04-N zPodA?|7R%t-0OePIR=8);|~mU@~@pvJ&UA1Zl5 zD0(4AlDuJS*;;Viby_II4jCgC4)!i0^meunq)M%xvy*;p7f|?H|@nK)rPt3iU=; zVGULfm@-$1-}3*f$G;o;;nF{n?!}vxGdH2`$w=^w`IIp5Ahssb4xPzSFrFyudhK9F zSUji5_YYTuLWY@9C|JW_{6K%u6{RgPxhW0%83TYS!p^wf>|jZx*q8h~rA(5-LSX(PHR>d$g2dB>{Sb z;q=>53>QFX3NSwrJsCQ>Ky15LtzfK$}qgy$+Z45KNidj&U8oT$}uA^2?hqZ$6wXk3I=K z8C~V82aoKo1nCvxPwuRT)^C5);v^&|CY493G4TVcegf5Qe=+=Qc%5HAzCHq~?YY<@ntd# zx0DvvcVM=|JkM7-vCMu|E?#E8AGrgS`hUAM%J1Li0&^(JpQg7pvp>Qg+U5eYFUGfR ibAdT1@Sx0r>5TFnv~SHo-&vk^4(z@JZNJ;FH~B9&f2o`R literal 0 HcmV?d00001 diff --git a/plugins_sogen-support/tenet/util/qt/__pycache__/waitbox.cpython-311.pyc b/plugins_sogen-support/tenet/util/qt/__pycache__/waitbox.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8bcb4547333956e42ee9be66416ab230b0d78da9 GIT binary patch literal 5279 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 literal 0 HcmV?d00001 diff --git a/plugins_sogen-support/tenet/util/qt/shim.py b/plugins_sogen-support/tenet/util/qt/shim.py new file mode 100644 index 0000000..dd4793c --- /dev/null +++ b/plugins_sogen-support/tenet/util/qt/shim.py @@ -0,0 +1,66 @@ + +# +# this global is used to indicate whether Qt bindings for python are present +# and available for use by Lighthouse. +# + +QT_AVAILABLE = False + +#------------------------------------------------------------------------------ +# PyQt5 <--> PySide2 Compatibility +#------------------------------------------------------------------------------ +# +# we use this file to shim/re-alias a few Qt API's to ensure compatibility +# between the popular Qt frameworks. these shims serve to reduce the number +# of compatibility checks in the plugin code that consumes them. +# +# 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 +# + +USING_PYQT5 = False +USING_PYSIDE2 = False + +#------------------------------------------------------------------------------ +# PyQt5 Compatibility +#------------------------------------------------------------------------------ + +# attempt to load PyQt5 +if QT_AVAILABLE == False: + 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 + USING_PYQT5 = True + + # import failed, PyQt5 is not available + except ImportError: + pass + +#------------------------------------------------------------------------------ +# PySide2 Compatibility +#------------------------------------------------------------------------------ + +# if PyQt5 did not import, try to load PySide +if QT_AVAILABLE == False: + try: + import PySide2.QtGui as QtGui + import PySide2.QtCore as QtCore + import PySide2.QtWidgets as QtWidgets + + # alias for less PySide2 <--> PyQt5 shimming + QtCore.pyqtSignal = QtCore.Signal + QtCore.pyqtSlot = QtCore.Slot + + # importing went okay, PySide must be available for use + QT_AVAILABLE = True + USING_PYSIDE2 = True + + # import failed. No Qt / UI bindings available... + except ImportError: + pass \ No newline at end of file diff --git a/plugins_sogen-support/tenet/util/qt/util.py b/plugins_sogen-support/tenet/util/qt/util.py new file mode 100644 index 0000000..758f41a --- /dev/null +++ b/plugins_sogen-support/tenet/util/qt/util.py @@ -0,0 +1,86 @@ +import sys +import time + +from .shim import * + +#------------------------------------------------------------------------------ +# Qt Fonts +#------------------------------------------------------------------------------ + +def MonospaceFont(): + """ + Convenience alias for creating a monospace Qt font object. + """ + font = QtGui.QFont("Courier New") + font.setStyleHint(QtGui.QFont.TypeWriter) + return font + +#------------------------------------------------------------------------------ +# Qt Util +#------------------------------------------------------------------------------ + +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) + +def flush_qt_events(): + """ + Flush the Qt event pipeline. + """ + app = QtCore.QCoreApplication.instance() + app.processEvents() + +def focus_window(): + """ + Lame helper function to help with dev/debug. + """ + mb = QtWidgets.QMessageBox(get_qmainwindow()) + mb.setText("Click to take focus...") + mb.setStandardButtons(QtWidgets.QMessageBox.Ok) + button = mb.button(QtWidgets.QMessageBox.Ok) + mb.exec_() + +def get_dpi_scale(): + """ + Get a DPI-afflicted value useful for consistent UI scaling. + """ + font = MonospaceFont() + font.setPointSize(normalize_font(120)) + fm = QtGui.QFontMetricsF(font) + + # xHeight is expected to be 40.0 at normal DPI + return fm.height() / 173.0 + +def normalize_font(font_size): + """ + Normalize the given font size based on the system DPI. + """ + if sys.platform == "darwin": # macos is lame + return font_size + 2 + return font_size + +def get_qmainwindow(): + """ + Get the QMainWindow instance for the current Qt runtime. + """ + app = QtWidgets.QApplication.instance() + return [x for x in app.allWidgets() if x.__class__ is QtWidgets.QMainWindow][0] + +def compute_color_on_gradient(percent, color1, color2): + """ + Compute the color specified by a percent between two colors. + """ + r1, g1, b1, _ = color1.getRgb() + r2, g2, b2, _ = color2.getRgb() + + # compute the new color across the gradient of color1 -> color 2 + r = r1 + percent * (r2 - r1) + g = g1 + percent * (g2 - g1) + b = b1 + percent * (b2 - b1) + + # return the new color + return QtGui.QColor(r,g,b) \ No newline at end of file diff --git a/plugins_sogen-support/tenet/util/qt/waitbox.py b/plugins_sogen-support/tenet/util/qt/waitbox.py new file mode 100644 index 0000000..586dcc8 --- /dev/null +++ b/plugins_sogen-support/tenet/util/qt/waitbox.py @@ -0,0 +1,102 @@ +from .shim import * +from .util import get_dpi_scale + +import logging +logger = logging.getLogger("Tenet.Qt.WaitBox") + +#-------------------------------------------------------------------------- +# Qt WaitBox +#-------------------------------------------------------------------------- + +class WaitBox(QtWidgets.QDialog): + """ + A Generic Qt WaitBox Dialog. + """ + + def __init__(self, text, title="Please wait...", abort=None): + super(WaitBox, self).__init__() + + # dialog text & window title + self._text = text + self._title = title + + # abort routine (optional) + self._abort = abort + + # initialize the dialog UI + self._ui_init() + + def set_text(self, text): + """ + Change the waitbox text. + """ + self._text = text + self._text_label.setText(text) + qta = QtCore.QCoreApplication.instance() + qta.processEvents() + + def show(self, modal=True): + self.setModal(modal) + result = super(WaitBox, self).show() + qta = QtCore.QCoreApplication.instance() + qta.processEvents() + + #-------------------------------------------------------------------------- + # Initialization - UI + #-------------------------------------------------------------------------- + + def _ui_init(self): + """ + Initialize UI elements. + """ + self.setWindowFlags( + self.windowFlags() & ~QtCore.Qt.WindowContextHelpButtonHint + ) + self.setWindowFlags( + self.windowFlags() | QtCore.Qt.MSWindowsFixedSizeDialogHint + ) + self.setWindowFlags( + self.windowFlags() & ~QtCore.Qt.WindowCloseButtonHint + ) + + # configure the main widget / form + self.setSizeGripEnabled(False) + self.setModal(True) + self._dpi_scale = get_dpi_scale()*5.0 + + # initialize abort button + self._abort_button = QtWidgets.QPushButton("Cancel") + + # layout the populated UI just before showing it + self._ui_layout() + + def _ui_layout(self): + """ + Layout the major UI elements of the widget. + """ + self.setWindowTitle(self._title) + self._text_label = QtWidgets.QLabel(self._text) + self._text_label.setAlignment(QtCore.Qt.AlignHCenter) + + # vertical layout (whole widget) + v_layout = QtWidgets.QVBoxLayout() + v_layout.setAlignment(QtCore.Qt.AlignCenter) + v_layout.addWidget(self._text_label) + if self._abort: + self._abort_button.clicked.connect(self._abort) + v_layout.addWidget(self._abort_button) + + v_layout.setSpacing(int(self._dpi_scale*3)) + v_layout.setContentsMargins( + int(self._dpi_scale*5), + int(self._dpi_scale), + int(self._dpi_scale*5), + int(self._dpi_scale) + ) + + # scale widget dimensions based on DPI + height = int(self._dpi_scale * 15) + self.setMinimumHeight(height) + + # compute the dialog layout + self.setLayout(v_layout) \ No newline at end of file diff --git a/plugins_sogen-support/tenet/util/update.py b/plugins_sogen-support/tenet/util/update.py new file mode 100644 index 0000000..195089c --- /dev/null +++ b/plugins_sogen-support/tenet/util/update.py @@ -0,0 +1,59 @@ +import re +import json +import logging +import threading + +from urllib.request import urlopen + +logger = logging.getLogger("Tenet.Util.Update") + +#------------------------------------------------------------------------------ +# Update Checking +#------------------------------------------------------------------------------ + +UPDATE_URL = "https://api.github.com/repos/gaasedelen/tenet/releases/latest" + +def check_for_update(current_version, callback): + """ + Perform a plugin update check. + """ + update_thread = threading.Thread( + target=async_update_check, + args=(current_version, callback,), + name="UpdateChecker" + ) + update_thread.start() + +def async_update_check(current_version, callback): + """ + An async worker thread to check for an plugin update. + """ + logger.debug("Checking for update...") + + try: + response = urlopen(UPDATE_URL, timeout=5.0) + html = response.read() + info = json.loads(html) + remote_version = info["tag_name"] + except Exception: + logger.debug(" - Failed to reach GitHub for update check...") + return + + # convert vesrion #'s to integer for easy compare... + version_remote = int(''.join(re.findall('\d+', remote_version))) + version_local = int(''.join(re.findall('\d+', current_version))) + + # no updates available... + logger.debug(" - Local: 'v%s' vs Remote: '%s'" % (current_version, remote_version)) + if version_local >= version_remote: + logger.debug(" - No update needed...") + return + + # notify the user if an update is available + update_message = "An update is available for Tenet!\n\n" \ + " - Latest Version: %s\n" % (remote_version) + \ + " - Current Version: v%s\n\n" % (current_version) + \ + "Please go download the update from GitHub." + + callback(update_message) + diff --git a/plugins_sogen-support/tenet_plugin.py b/plugins_sogen-support/tenet_plugin.py new file mode 100644 index 0000000..e1896ac --- /dev/null +++ b/plugins_sogen-support/tenet_plugin.py @@ -0,0 +1,23 @@ +from tenet.util.log import logging_started, start_logging +from tenet.integration.api import disassembler + +if not logging_started(): + logger = start_logging() + +#------------------------------------------------------------------------------ +# Disassembler Agnonstic Plugin Loader +#------------------------------------------------------------------------------ + +logger.debug("Resolving disassembler platform for Tenet...") + +if disassembler.headless: + logger.info("Disassembler '%s' is running headlessly" % disassembler.NAME) + logger.info(" - Tenet is not supported in headless modes (yet!)") + +elif disassembler.NAME == "IDA": + logger.info("Selecting IDA loader...") + from tenet.integration.ida_loader import * + +else: + raise NotImplementedError("DISASSEMBLER-SPECIFIC SHIM MISSING") + From 7b8e55e4656728b33983aeca845a4b70e270f3bc Mon Sep 17 00:00:00 2001 From: maskelihileci <41159853+maskelihileci@users.noreply.github.com> Date: Sun, 20 Jul 2025 20:16:47 +0300 Subject: [PATCH 4/4] The rebase feature has been added for modules. --- plugins_sogen-support/tenet/trace/file.py | 84 +++++++++++++----- plugins_sogen-support/tenet/util/rebase.py | 99 ++++++++++++++++++++++ 2 files changed, 160 insertions(+), 23 deletions(-) create mode 100644 plugins_sogen-support/tenet/util/rebase.py diff --git a/plugins_sogen-support/tenet/trace/file.py b/plugins_sogen-support/tenet/trace/file.py index b10109e..5f26150 100644 --- a/plugins_sogen-support/tenet/trace/file.py +++ b/plugins_sogen-support/tenet/trace/file.py @@ -54,21 +54,23 @@ # try: - from tenet.util.log import pmsg - from tenet.trace.arch import ArchAMD64, ArchX86 - from tenet.trace.types import TraceMemory - -# -# this script can technically be run in a standalone mone to process / digest -# a trace outside of a disassembler / the normal integration. so if the above -# fails, use the following imports to operate independently -# - + from tenet.util.log import pmsg + from tenet.util.rebase import rebase_database + from tenet.trace.arch import ArchAMD64, ArchX86 + from tenet.trace.types import TraceMemory + + # + # this script can technically be run in a standalone mone to process / digest + # a trace outside of a disassembler / the normal integration. so if the above + # fails, use the following imports to operate independently + # + except ImportError: - from arch import ArchAMD64, ArchX86 - from .types import TraceMemory - pmsg = print - + from arch import ArchAMD64, ArchX86 + from .types import TraceMemory + # a little weird, but this makes 'rebase_database' a no-op outside IDA + rebase_database = lambda x: True + pmsg = print #----------------------------------------------------------------------------- # Definitions #----------------------------------------------------------------------------- @@ -177,6 +179,7 @@ class TraceInfo(ctypes.Structure): ('mem_idx_width', ctypes.c_uint8), ('mem_addr_width', ctypes.c_uint8), ('original_hash', ctypes.c_uint32), + ('module_base', ctypes.c_uint64), ] class SegmentInfo(ctypes.Structure): @@ -296,6 +299,9 @@ def __init__(self, filepath, arch=None): # the hash of the original / source log file self.original_hash = None + # the module base as specified in the text trace + self.module_base = 0 + # # now that you have some idea of how the trace file is going to be # organized... let's actually go and try to load one @@ -435,6 +441,7 @@ def _save_header(self, zip_archive): header.mem_idx_width = width_from_type(self.mem_idx_type) header.mem_addr_width = width_from_type(self.mem_addr_type) header.original_hash = self.original_hash + header.module_base = self.module_base mask_data = (ctypes.c_uint32 * len(self.masks))(*self.masks) # save the global trace data / header to the zip @@ -499,13 +506,16 @@ def _load_packed_trace(self, filepath): """ Load a packed trace from disk. """ - with zipfile.ZipFile(filepath, 'r') as zip_archive: self._load_header(zip_archive) self._load_segments(zip_archive) self.filepath = filepath + if self.module_base: + if not rebase_database(self.module_base): + pmsg("Database rebase failed or was cancelled.") + def _select_arch(self, magic): """ TODO: Select the trace CPU arch based on the given magic value. @@ -563,6 +573,7 @@ def _load_header(self, zip_archive): # source file hash self.original_hash = header.original_hash + self.module_base = header.module_base def _load_segments(self, zip_archive): """ @@ -591,7 +602,7 @@ def _load_text_trace(self, filepath): Load a text trace from disk. """ idx = 0 - + # mappings of address/mask and their mapped (compressed) id # - NOTE: these are only used when converting traces from text to binary self.ip_map = collections.OrderedDict() @@ -609,17 +620,45 @@ def _load_text_trace(self, filepath): # load / parse a text trace into trace segments with open(filepath, 'r') as f: - # loop until all of the lines in the file have been processed - while True: + # + # before processing a text trace, we will check the first line for a + # special 'mb=' (module base) tag. if this tag exists, we will use + # it to rebase the underlying database before continuing to parse + # and process the trace data + # - # select a chunk of N lines from the file - lines = itertools.islice(f, self.segment_length) - lines = list(lines) + # + # before processing a text trace, we will check the first line for a + # special 'mb=' (module base) tag. if this tag exists, we will use + # it to rebase the underlying database before continuing to parse + # and process the trace data + # + + first_line = f.readline() + if first_line.startswith("mb="): + try: + self.module_base = int(first_line.split("=")[1], 16) + if not rebase_database(self.module_base): + pmsg("Failed to rebase database, trace may not align correctly.") + except (ValueError, IndexError): + pmsg("Failed to parse module base from trace file header.") + # The rest of the file is the actual trace content + remaining_lines = f.readlines() + else: + # The first line was not a rebase line, so we process it with the rest. + remaining_lines = [first_line] + f.readlines() + + # + # now we process the trace lines in segments + # + + for i in range(0, len(remaining_lines), self.segment_length): + lines = remaining_lines[i:i+self.segment_length] if not lines: break segment_id = len(self.segments) - + # create a new trace segment from the given lines of text segment = TraceSegment(self, segment_id, idx) segment.from_lines(lines) @@ -627,7 +666,6 @@ def _load_text_trace(self, filepath): # save the segment self.segments.append(segment) - #break # for debugging... self._finalize() self._save() diff --git a/plugins_sogen-support/tenet/util/rebase.py b/plugins_sogen-support/tenet/util/rebase.py new file mode 100644 index 0000000..802288a --- /dev/null +++ b/plugins_sogen-support/tenet/util/rebase.py @@ -0,0 +1,99 @@ +import ida_segment +import ida_nalt +import ida_auto +import ida_kernwin + +from tenet.util.log import pmsg + +def rebase_database_manually(delta, new_base_address): + """ + Manually rebase the database by moving each segment individually. + This is a robust fallback for when rebase_program fails. + """ + + + # 1. Collect segment information + segments_info = [] + for i in range(ida_segment.get_segm_qty()): + seg = ida_segment.getnseg(i) + if seg: + segments_info.append({ + 'seg': seg, + 'name': ida_segment.get_segm_name(seg), + 'start_ea': seg.start_ea, + 'new_start': seg.start_ea + delta + }) + + # 2. Sort segments to prevent overlaps during the move. + segments_info.sort(key=lambda s: s['start_ea'], reverse=delta > 0) + + # 3. Move each segment + for seg_info in segments_info: + seg = seg_info['seg'] + new_start = seg_info['new_start'] + seg_name = seg_info['name'] + + # Try to move with flags=2 first (preserves info) + if not ida_segment.move_segm(seg, new_start, 2): + # Check if it moved despite the error code (IDA API quirk) + updated_seg = ida_segment.getseg(new_start) + if not (updated_seg and ida_segment.get_segm_name(updated_seg) == seg_name): + ida_kernwin.warning(f"Manual rebase failed: could not move segment '{seg_name}'.") + return False + + #pmsg("All segments moved successfully in manual rebase.") + return True + +def rebase_database(new_base_address): + """ + Rebase the program using a two-phase approach: + 1. Attempt a fast, simple rebase with rebase_program. + 2. If that fails, use a more robust manual segment-moving algorithm. + """ + current_base = ida_nalt.get_imagebase() + + if current_base == new_base_address: + pmsg("Database is already based at the target address.") + return True + + delta = new_base_address - current_base + + if not ida_kernwin.ask_yn( + ida_kernwin.ASKBTN_YES, + f"A new base address (0x{new_base_address:X}) was found in the trace log. " + f"Would you like to rebase the database from 0x{current_base:X}?\n\n" + "(This is a permanent operation)" + ): + pmsg("Rebase operation cancelled by user.") + return False + + # --- Phase 1: Fast Rebase --- + #pmsg("Phase 1: Attempting fast rebase with rebase_program...") + flags = 4 # MSF_FIXONCE: Fix up the program connections, etc. + if ida_segment.rebase_program(delta, flags) == 0: + pass + #pmsg("rebase_program returned error, but checking if it worked anyway...") + + # --- Verification --- + if ida_nalt.get_imagebase() == new_base_address: + #pmsg("Phase 1 successful. Rerunning analysis...") + ida_auto.auto_wait() + return True + + #pmsg("Phase 1 failed. The database imagebase was not changed.") + + # --- Phase 2: Manual Fallback Rebase --- + #pmsg("Phase 2: Falling back to manual segment-by-segment rebase...") + if not rebase_database_manually(delta, new_base_address): + ida_kernwin.warning("Manual rebase also failed. The database may be in an inconsistent state.") + return False + + ida_nalt.set_imagebase(new_base_address) + #pmsg("Rerunning analysis after manual rebase...") + ida_auto.auto_wait() + + if ida_nalt.get_imagebase() == new_base_address: + return True + + ida_kernwin.warning(f"Rebase failed. Current base 0x{ida_nalt.get_imagebase():X} does not match target 0x{new_base_address:X}.") + return False \ No newline at end of file