├── .gitignore ├── LICENSE.txt ├── README.md ├── core ├── __init__.py ├── api.py ├── bookmark.py ├── filter_and_find.py ├── prefs.py ├── trace_data.py └── trace_files.py ├── docs └── img │ └── etv.png ├── gui ├── __init__.py ├── input_dialog.py ├── mainwindow.py ├── mainwindow.ui ├── syntax_hl │ ├── rules │ │ ├── syntax_x86_dark.txt │ │ ├── syntax_x86_light.txt │ │ ├── value_dark.txt │ │ └── value_light.txt │ ├── syntax_hl_delegate.py │ └── syntax_hl_log.py └── widgets │ ├── __init__.py │ ├── filter_widget.py │ ├── find_widget.py │ ├── mem_table_widget.py │ ├── pagination_widget.py │ ├── reg_table_widget.py │ └── trace_table_widget.py ├── plugins ├── comment_mem_access.py ├── comment_mem_access.yapsy-plugin ├── exec_counts.py ├── exec_counts.yapsy-plugin ├── filter_by_mem_addr.py ├── filter_by_mem_addr.yapsy-plugin ├── list_bookmarks.py ├── list_bookmarks.yapsy-plugin ├── print_rows.py ├── print_rows.yapsy-plugin ├── print_selected_bookmarks.py └── print_selected_bookmarks.yapsy-plugin ├── requirements.txt ├── traces └── vmp3_32b_11k.tvt └── tv.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.pyc 3 | .env 4 | .venv 5 | env/ 6 | venv/ 7 | ENV/ 8 | .vscode -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2019 Teemu Laurila 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Execution Trace Viewer 2 | 3 | Execution Trace Viewer is an application for viewing, editing and analyzing execution traces. It was originally made for reverse engineering obfuscated code, but it can be used to analyze any kind of execution trace. 4 | 5 | ![Execution Trace Viewer](docs/img/etv.png "Execution Trace Viewer") 6 | 7 | ## Features 8 | 9 | - open, edit and save execution traces 10 | - search & filter trace by disasm, reg values, memory address/value, etc 11 | - add comments and bookmarks 12 | - write python plugins 13 | - supports x64dbg traces 14 | 15 | ## Dependencies 16 | 17 | - [Python 3.6](https://python.org/download) 18 | - [PyQt5](https://www.riverbankcomputing.com/software/pyqt/intro) 19 | - [Yapsy](https://github.com/tibonihoo/yapsy/) 20 | - [QDarkStylesheet](https://github.com/ColinDuquesnoy/QDarkStyleSheet) 21 | - [Capstone](https://github.com/aquynh/capstone) 22 | 23 | ## Install & run 24 | 25 | ```shell 26 | git clone https://github.com/teemu-l/execution-trace-viewer 27 | 28 | pip install pyqt5 yapsy qdarkstyle capstone 29 | python tv.py 30 | ``` 31 | 32 | ## Trace file formats 33 | 34 | Following file formats are supported: 35 | 36 | - .tvt - Default file format. Developed from x64dbg trace format. 3 differences with x64dbg format: comments, disasm and bookmarks added. 37 | 38 | - .trace32 / .trace64 - x64dbg file format. Only reading supported. Loading x64dbg traces is slow because the code needs to be disassembled. 39 | 40 | - json - Traces can be saved and loaded from json text files. 41 | 42 | Traces folder contains one sample trace. It is ~11k lines of obfuscated code (by VMProtect3). All the handlers are disassembled and added to bookmarks table. 43 | 44 | ## Plugins 45 | 46 | Execution Trace Viewer can be extended by Python3 plugins. Plugins are launched from plugins menu or from right-click menu on trace table. Check the example plugins and core/api.py for more info. 47 | 48 | More plugins: 49 | 50 | - [Memory trace graph](https://github.com/teemu-l/mem-trace-plugin) 51 | - [Data visualizer](https://github.com/teemu-l/dataviz-plugin) 52 | - your link here? 53 | 54 | ## Filters 55 | 56 | Example filters: 57 | 58 | | Filter | Description | 59 | | ------------------------ | ------------------------------------------------------------- | 60 | | disasm=push|pop | disasm contains word push or pop (push, pushfd, pop, etc) | 61 | | reg_eax=0x1337 | show rows where eax is 0x1337 | 62 | | reg_any=0x1337 | any reg value is 0x1337 | 63 | | mem_value=0x1337 | read or write value 0x1337 to memory | 64 | | mem_read_value=0x1337 | read value 0x1337 from memory | 65 | | mem_addr=0x4f20 | read from or write to memory address 0x4f20 | 66 | | mem_read_addr=0x40400 | read from memory address 0x40400 | 67 | | mem_write_addr=0x40400 | write to memory address 0x40400 | 68 | | opcodes=c704 | filter by opcodes | 69 | | rows=20-50 | show only rows 20-50 | 70 | | regex=0x40?00 | case-sensitive regex search for whole row (including comment) | 71 | | regex=READ | show insctructions which read memory | 72 | | iregex=junk|decrypt | inverse regex, rows with 'junk' or 'decrypt' are filtered out | 73 | | comment=decrypt | filter by comment | 74 | 75 | It's possible to join multiple filters together: 76 | 77 | ``` 78 | disasm=xor/reg_any=0x1337 ; show all xor instructions where atleast one register value is 0x1337 79 | ``` 80 | 81 | For more complex filtering you can create a filter plugin and save the result list using api.set_filtered_trace(). Then show the trace by calling api.show_filtered_trace(). 82 | 83 | ## Find 84 | 85 | Finds next or previous row that contains specified keyword/value in trace. 86 | 87 | ### Using Find in plugin 88 | 89 | Find previous memory write: 90 | 91 | ```python 92 | from core.filter_and_find import TraceField 93 | current_row = 500 94 | next_row = find( 95 | trace=trace_data.trace, 96 | field=TraceField.MEM, 97 | keyword='WRITE', 98 | start_row=current_row, 99 | direction=-1 100 | ) 101 | ``` 102 | 103 | Trace fields: DISASM, REGS, MEM, MEM_ADDR, MEM_VALUE, COMMENT, ANY 104 | 105 | DISASM field supports multiple keywords: "xor/shl/shr". MEM field checks all three fields in mem access (access, addr and value). Integers must be given in hexadecimal. 106 | 107 | ## Themes 108 | 109 | Dark theme can be disabled by editing prefs.py: 110 | 111 | ```python 112 | USE_DARK_THEME = False 113 | ``` 114 | 115 | ## To-Do 116 | 117 | - add support for more trace formats & architectures 118 | - fix x64dbg trace problem: if memory content doesn't change on write operation, the access is shown as 'READ' (bad design of the file format?) 119 | - documentation 120 | 121 | ## License 122 | 123 | MIT 124 | 125 | ## Author 126 | 127 | Developed by Teemu Laurila. 128 | 129 | Contact: 130 | 131 | ```python 132 | print(''.join(map(chr,[k^86 for k in [34,51,51,59,35,58,55,22,38,36,57,34,57,56,59,55,63,58,120,53,57,59]]))) 133 | ``` 134 | -------------------------------------------------------------------------------- /core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teemu-l/execution-trace-viewer/d2d99e92decb3526efc1f5bd4bae350aa4c7c0d2/core/__init__.py -------------------------------------------------------------------------------- /core/api.py: -------------------------------------------------------------------------------- 1 | class Api: 2 | """Api class for plugins 3 | 4 | Attributes: 5 | main_window (MainWindow): MainWindow object 6 | """ 7 | 8 | def __init__(self, main_window): 9 | """Inits Api.""" 10 | self.main_window = main_window 11 | 12 | def add_bookmark(self, bookmark, replace=False): 13 | """Adds a new bookmark to bookmark table 14 | 15 | Args: 16 | new_bookmark (Bookmark): A new bookmark 17 | replace (bool): Replace an existing bookmark if found on same row? 18 | Defaults to False. 19 | """ 20 | self.main_window.trace_data.add_bookmark(bookmark, replace) 21 | 22 | def ask_user(self, title: str, question: str): 23 | """Shows a messagebox with yes/no question 24 | 25 | Args: 26 | title (str): MessageBox title 27 | question (str): MessageBox question label 28 | Returns: 29 | bool: True if user clicked yes, False otherwise 30 | """ 31 | return self.main_window.ask_user(title, question) 32 | 33 | def get_bookmarks(self): 34 | """Returns all bookmarks 35 | 36 | Returns: 37 | list: List of Bookmark objects 38 | """ 39 | return self.main_window.trace_data.get_bookmarks() 40 | 41 | def get_filtered_trace(self): 42 | """Returns filtered_trace""" 43 | return self.main_window.filtered_trace 44 | 45 | def get_full_trace(self): 46 | """Returns full trace from TraceData object""" 47 | return self.main_window.trace_data.trace 48 | 49 | def get_main_window(self): 50 | """Returns main_window object""" 51 | return self.main_window 52 | 53 | def get_string_from_user(self, title: str, label: str): 54 | """Get string from user. 55 | 56 | Args: 57 | title (str): Input dialog title 58 | label (str): Input dialog label 59 | Returns: 60 | string: String given by user 61 | """ 62 | return self.main_window.get_string_from_user(title, label) 63 | 64 | def get_values_from_user(self, title: str, data: list, on_ok_clicked=None): 65 | """Get input from user. Data types: str, int, list, bool. 66 | 67 | Args: 68 | title (str): Input dialog title 69 | data (list): List of dicts describing labels and data 70 | on_ok_clicked (method): Callback function to e.g. check the input 71 | Returns: 72 | list: List of values, empty list if canceled 73 | """ 74 | return self.main_window.get_values_from_user(title, data, on_ok_clicked) 75 | 76 | def get_selected_bookmarks(self): 77 | """Returns list of selected bookmarks""" 78 | return self.main_window.get_selected_bookmarks() 79 | 80 | def get_selected_trace(self): 81 | """Returns list of selected trace""" 82 | row_ids = self.get_selected_trace_row_ids() 83 | trace_data = self.get_trace_data() 84 | trace = trace_data.get_trace_rows(row_ids) 85 | return trace 86 | 87 | def get_selected_trace_row_ids(self): 88 | """Returns list of ids of selected rows""" 89 | return self.main_window.get_selected_row_ids(self.main_window.trace_table) 90 | 91 | def get_trace_data(self): 92 | """Returns TraceData object""" 93 | return self.main_window.trace_data 94 | 95 | def get_visible_trace(self): 96 | """Returns visible trace, either full or filtered trace""" 97 | return self.main_window.get_visible_trace() 98 | 99 | def get_regs(self): 100 | """Returns dictionary of registers and their indexes""" 101 | return self.main_window.trace_data.get_regs() 102 | 103 | def go_to_row_in_full_trace(self, row_id: int): 104 | """Goes to given row in full trace 105 | 106 | Args: 107 | row_id (int): Row id 108 | """ 109 | self.main_window.go_to_row_in_full_trace(row_id) 110 | 111 | def go_to_row_in_current_trace(self, row_index: int): 112 | """Goes to given row index in currently visible trace (full or filtered) 113 | 114 | Args: 115 | row_index (int): Row index in trace 116 | """ 117 | self.main_window.go_to_row_in_visible_trace(row_index) 118 | 119 | def print(self, text: str): 120 | """Prints text to log 121 | 122 | Args: 123 | text (str): Text to print in log 124 | """ 125 | self.main_window.print(str(text)) 126 | 127 | def set_comment(self, row: int, comment: str): 128 | """Sets a comment to trace 129 | 130 | Args: 131 | row (int): A row index in full trace 132 | comment (str): A comment text 133 | """ 134 | try: 135 | self.main_window.trace_data.trace[row]["comment"] = str(comment) 136 | except IndexError: 137 | print(f"Error. Could not set comment to row {row}") 138 | 139 | def set_filtered_trace(self, trace): 140 | """Sets filtered_trace 141 | 142 | Args: 143 | trace (list): List of trace rows 144 | """ 145 | self.main_window.filtered_trace = trace 146 | 147 | def show_filtered_trace(self): 148 | """Shows filtered trace on trace_table""" 149 | self.main_window.show_filtered_trace() 150 | 151 | def show_messagebox(self, title: str, msg: str): 152 | """Shows a messagebox 153 | 154 | Args: 155 | title (str): Title of messagebox 156 | msg (str): Message to show 157 | """ 158 | self.main_window.show_messagebox(title, msg) 159 | 160 | def update_trace_table(self): 161 | """Updates trace_table""" 162 | self.main_window.trace_table.populate() 163 | 164 | def update_bookmark_table(self): 165 | """Updates bookmark_table""" 166 | self.main_window.update_bookmark_table() 167 | -------------------------------------------------------------------------------- /core/bookmark.py: -------------------------------------------------------------------------------- 1 | class Bookmark: 2 | """Bookmark class for TraceData 3 | 4 | Attributes: 5 | addr (str): Address of startrow 6 | disasm (str): Disassembly of bookmark's startrow 7 | startrow (int): First row of bookmark (index in trace list) 8 | endrow (int): Last row of bookmark 9 | comment (str): Comment 10 | """ 11 | def __init__(self, addr="", disasm="", startrow=0, endrow=0, comment=""): 12 | self.addr = addr 13 | self.disasm = disasm 14 | self.startrow = startrow 15 | self.endrow = endrow 16 | self.comment = comment 17 | -------------------------------------------------------------------------------- /core/filter_and_find.py: -------------------------------------------------------------------------------- 1 | import re 2 | from enum import Enum, auto 3 | 4 | 5 | class TraceField(Enum): 6 | """Enum for trace fields. 7 | DISASM, REGS, MEM, MEM_ADDR, MEM_VALUE, COMMENT or ANY 8 | """ 9 | 10 | DISASM = auto() 11 | REGS = auto() 12 | MEM = auto() 13 | MEM_ADDR = auto() 14 | MEM_VALUE = auto() 15 | COMMENT = auto() 16 | ANY = auto() 17 | 18 | 19 | def find( 20 | trace: list, field: TraceField, keyword: str, start_row: int = 0, direction: int = 1 21 | ): 22 | """Finds next/previous trace row with keyword 23 | 24 | Args: 25 | trace (list): Traced instructions, registers and memory (TraceData.trace) 26 | field (TraceField): Which field(s) to search 27 | keyword (str): Keyword to search for 28 | start_row (int): Trace row number to start search 29 | direction (int, optional): Search direction, 1 for forward, -1 for backward 30 | Defaults to 1. 31 | Returns: 32 | Trace row number, None if nothing found 33 | """ 34 | if not keyword or not trace or start_row > len(trace): 35 | return None 36 | 37 | last_row = len(trace) 38 | 39 | if direction < 0: 40 | last_row = -1 41 | 42 | if field == TraceField.DISASM: 43 | keywords = keyword.split("/") 44 | for row in range(start_row, last_row, direction): 45 | disasm = trace[row]["disasm"] 46 | for key in keywords: 47 | if key in disasm: 48 | return row 49 | 50 | elif field == TraceField.REGS: 51 | value = int(keyword, 16) 52 | for row in range(start_row, last_row, direction): 53 | if value in trace[row]["regs"]: 54 | return row 55 | 56 | elif field == TraceField.MEM: 57 | keyword = keyword.strip() 58 | if "0x" in keyword: 59 | keyword = int(keyword, 16) 60 | for row in range(start_row, last_row, direction): 61 | for mem in trace[row]["mem"]: 62 | if keyword in mem.values(): 63 | return row 64 | 65 | elif field == TraceField.MEM_ADDR: 66 | keyword = keyword.strip() 67 | addr = int(keyword, 16) 68 | for row in range(start_row, last_row, direction): 69 | for mem in trace[row]["mem"]: 70 | if addr == mem["addr"]: 71 | return row 72 | 73 | elif field == TraceField.MEM_VALUE: 74 | keyword = keyword.strip() 75 | value = int(keyword, 16) 76 | for row in range(start_row, last_row, direction): 77 | for mem in trace[row]["mem"]: 78 | if value == mem["value"]: 79 | return row 80 | 81 | elif field == TraceField.COMMENT: 82 | for row in range(start_row, last_row, direction): 83 | if keyword in trace[row].get("comment", ""): 84 | return row 85 | 86 | elif field == TraceField.ANY: 87 | keyword_int = None 88 | if keyword.startswith("0x"): 89 | keyword_int = int(keyword, 16) 90 | 91 | for row in range(start_row, last_row, direction): 92 | if keyword in trace[row].get("comment", ""): 93 | return row 94 | for mem in trace[row]["mem"]: 95 | mem_values = mem.values() 96 | if keyword in mem_values: 97 | return row 98 | if keyword_int and keyword_int in mem_values: 99 | return row 100 | if keyword in trace[row]["disasm"]: 101 | return row 102 | if keyword_int and keyword_int in trace[row]["regs"]: 103 | return row 104 | 105 | else: 106 | raise ValueError("Unknown field") 107 | 108 | return None 109 | 110 | 111 | def filter_trace(trace: list, regs: dict, filter_text: str): 112 | """Filters trace 113 | 114 | Args: 115 | trace (list): Traced instructions, registers and memory (TraceData.trace) 116 | filter_text (str): Filter text 117 | regs (dict): Register names and indexes (TraceData.regs) 118 | Raises: 119 | ValueError: If unknown keywords or wrong filter format 120 | Returns: 121 | List of filtered trace records 122 | """ 123 | data = trace 124 | if len(filter_text) == 0: 125 | return data 126 | filters = filter_text.split("/") 127 | if not filters or not data: 128 | raise ValueError("Empty trace or filter") 129 | value = "" 130 | 131 | for f in filters: 132 | f_parts = f.split("=") 133 | if len(f_parts) != 2 or not f_parts[1]: 134 | raise ValueError("Wrong filter format") 135 | value = f_parts[1] 136 | if f_parts[0] == "rows": 137 | rows = value.split("-") 138 | start = int(rows[0]) 139 | end = int(rows[1]) 140 | data = data[start : end + 1] 141 | elif f_parts[0] == "disasm": 142 | disasm_list = f_parts[1].split("|") 143 | data = list( 144 | filter(lambda x: any(k for k in disasm_list if k in x["disasm"]), data) 145 | ) 146 | elif f_parts[0] == "opcodes": 147 | data = list(filter(lambda x: value in x["opcodes"], data)) 148 | elif f_parts[0] == "comment": 149 | data = list(filter(lambda x: value in x.get("comment", ""), data)) 150 | elif "reg_" in f_parts[0]: 151 | reg = f_parts[0].split("_")[1] 152 | value = int(value, 16) 153 | if reg == "any": 154 | data = list(filter(lambda x: value in x["regs"], data)) 155 | elif data and reg in regs: 156 | reg_index = regs[reg] 157 | data = list(filter(lambda x: x["regs"][reg_index] == value, data)) 158 | else: 159 | raise ValueError(f"Unknown register: {reg}") 160 | elif f_parts[0] == "regex": 161 | data = list(filter(lambda x: re.search(value, str(x)) is not None, data)) 162 | elif f_parts[0] == "iregex": 163 | data = list(filter(lambda x: re.search(value, str(x)) is None, data)) 164 | elif f_parts[0] == "mem_value": 165 | value = int(value, 16) 166 | data = list( 167 | filter(lambda x: any(k for k in x["mem"] if k["value"] == value), data) 168 | ) 169 | elif f_parts[0] == "mem_read_value": 170 | value = int(value, 16) 171 | data = list( 172 | filter( 173 | lambda x: any( 174 | k 175 | for k in x["mem"] 176 | if k["value"] == value and k["access"] == "READ" 177 | ), 178 | data, 179 | ) 180 | ) 181 | elif f_parts[0] == "mem_write_value": 182 | value = int(value, 16) 183 | data = list( 184 | filter( 185 | lambda x: any( 186 | k 187 | for k in x["mem"] 188 | if k["value"] == value and k["access"] == "WRITE" 189 | ), 190 | data, 191 | ) 192 | ) 193 | elif f_parts[0] == "mem_addr": 194 | value = int(value, 16) 195 | data = list( 196 | filter(lambda x: any(k for k in x["mem"] if k["addr"] == value), data) 197 | ) 198 | elif f_parts[0] == "mem_read_addr": 199 | value = int(value, 16) 200 | data = list( 201 | filter( 202 | lambda x: any( 203 | k 204 | for k in x["mem"] 205 | if k["addr"] == value and k["access"] == "READ" 206 | ), 207 | data, 208 | ) 209 | ) 210 | elif f_parts[0] == "mem_write_addr": 211 | value = int(value, 16) 212 | data = list( 213 | filter( 214 | lambda x: any( 215 | k 216 | for k in x["mem"] 217 | if k["addr"] == value and k["access"] == "WRITE" 218 | ), 219 | data, 220 | ) 221 | ) 222 | else: 223 | raise ValueError(f"Unknown word: {f_parts[0]}") 224 | return data 225 | -------------------------------------------------------------------------------- /core/prefs.py: -------------------------------------------------------------------------------- 1 | PACKAGE_NAME = "Execution Trace Viewer" 2 | PACKAGE_AUTHOR = "Teemu Laurila" 3 | PACKAGE_URL = "https://github.com/teemu-l/execution-trace-viewer" 4 | PACKAGE_VERSION = "1.0.0" 5 | PACKAGE_COPYRIGHTS = "(C) 2019 Teemu Laurila" 6 | 7 | DEBUG = True 8 | 9 | WINDOW_WIDTH = 1300 10 | WINDOW_HEIGHT = 800 11 | 12 | LIGHT_THEME = "Fusion" 13 | USE_DARK_THEME = False 14 | 15 | TRACE_FONT = "Courier" 16 | TRACE_FONT_SIZE = 8 17 | TRACE_SHOW_OLD_REG_VALUE = False 18 | TRACE_SHOW_GRID = True 19 | 20 | HIGHLIGHT_MODIFIED_REGS = True 21 | 22 | TRACE_HL_DISASM_COLUMNS = [3] 23 | TRACE_HL_VALUE_COLUMNS = [4, 5] 24 | BOOKMARK_HL_DISASM_COLUMNS = [3] 25 | BOOKMARK_HL_VALUE_COLUMNS = [2, 4] 26 | 27 | USE_SYNTAX_HIGHLIGHT_IN_TRACE = True 28 | USE_SYNTAX_HIGHLIGHT_IN_LOG = True 29 | 30 | REG_HL_COLOR = "white" 31 | REG_HL_BG_COLORS = [ 32 | "blue", 33 | "red", 34 | "green", 35 | "magenta", 36 | "#a328e0", 37 | "#cc0a4a", 38 | "#cc7800", 39 | "#d68711", 40 | ] 41 | DISASM_RULES_FILE = "gui/syntax_hl/rules/syntax_x86_light.txt" 42 | VALUE_RULES_FILE = "gui/syntax_hl/rules/value_light.txt" 43 | if USE_DARK_THEME: 44 | REG_HL_COLOR = "black" 45 | REG_HL_BG_COLORS = ["magenta", "green", "pink", "lightgreen", "#7bbef2", "#f96459"] 46 | DISASM_RULES_FILE = "gui/syntax_hl/rules/syntax_x86_dark.txt" 47 | VALUE_RULES_FILE = "gui/syntax_hl/rules/value_dark.txt" 48 | 49 | HL_REGS_X86 = { 50 | "r8": ["r8", "r8d", "r8w", "r8b"], 51 | "r9": ["r9", "r9d", "r9w", "r9b"], 52 | "r10": ["r10", "r10d", "r10w", "r10b"], 53 | "r11": ["r11", "r11d", "r11w", "r11b"], 54 | "r12": ["r12", "r12d", "r12w", "r12b"], 55 | "r13": ["r13", "r13d", "r13w", "r13b"], 56 | "r14": ["r14", "r14d", "r14w", "r14b"], 57 | "r15": ["r15", "r15d", "r15w", "r15b"], 58 | } 59 | HL_REGS_X86.update(dict.fromkeys(["rax", "eax"], ["rax", "eax", "ax", "ah", "al"])) 60 | HL_REGS_X86.update(dict.fromkeys(["rbx", "ebx"], ["rbx", "ebx", "bx", "bh", "bl"])) 61 | HL_REGS_X86.update(dict.fromkeys(["rcx", "ecx"], ["rcx", "ecx", "cx", "ch", "cl"])) 62 | HL_REGS_X86.update(dict.fromkeys(["rdx", "edx"], ["rdx", "edx", "dx", "dh", "dl"])) 63 | HL_REGS_X86.update(dict.fromkeys(["rbp", "ebp"], ["rbp", "ebp", "bp", "bpl"])) 64 | HL_REGS_X86.update(dict.fromkeys(["rsi", "esi"], ["rsi", "esi", "si", "sil"])) 65 | HL_REGS_X86.update(dict.fromkeys(["rdi", "edi"], ["rdi", "edi", "di", "dil"])) 66 | HL_REGS_X86.update(dict.fromkeys(["rip", "eip"], ["rip", "eip", "ip"])) 67 | HL_REGS_X86.update(dict.fromkeys(["rsp", "esp"], ["rsp", "esp", "sp", "spl"])) 68 | 69 | 70 | SHOW_SAMPLE_FILTERS = True 71 | SAMPLE_FILTERS = [ 72 | "", 73 | "disasm=push|pop", 74 | "reg_eax=0x1", 75 | "reg_any=0x1", 76 | "rows=0-200", 77 | "regex=0x40?00", 78 | "iregex=junk|decrypt", 79 | "mem_value=0x1", 80 | "mem_read_value=0x1", 81 | "mem_write_value=0x1", 82 | "mem_addr=0x4f20", 83 | "mem_read_addr=0x4f20", 84 | "mem_write_addr=0x4f20", 85 | "opcodes=c704", 86 | "comment=decrypt", 87 | ] 88 | 89 | FIND_FIELDS = [ 90 | "Disasm", 91 | "Registers", 92 | "Mem (any field)", 93 | "Mem address", 94 | "Mem value", 95 | "Comment", 96 | "Any", 97 | ] 98 | 99 | # columns for tables 100 | TRACE_LABELS = ["#", "address", "opcodes", "disasm", "registers", "comment"] 101 | BOOKMARK_LABELS = ["start row", "end row", "addr", "disasm", "comment"] 102 | REG_LABELS = ["reg", "hex", "dec"] 103 | MEM_LABELS = ["access", "address", "value"] 104 | 105 | TRACE_ROW_HEIGHT = 20 106 | 107 | PAGINATION_ENABLED = True 108 | PAGINATION_ROWS_PER_PAGE = 10000 109 | 110 | # ask for comment when creating a bookmark? 111 | ASK_FOR_BOOKMARK_COMMENT = True 112 | 113 | # registers for x64dbg traces 114 | # if you want to see more regs, add them here (in correct order) 115 | # check the order of regs from REGISTERCONTEXT: 116 | # https://github.com/x64dbg/x64dbg/blob/development/src/bridge/bridgemain.h#L723 117 | X32_REGS = [ 118 | "eax", 119 | "ecx", 120 | "edx", 121 | "ebx", 122 | "esp", 123 | "ebp", 124 | "esi", 125 | "edi", 126 | "eip", 127 | "eflags", 128 | "gs", 129 | "fs", 130 | "es", 131 | "ds", 132 | "cs", 133 | "ss", 134 | "dr0", 135 | "dr1", 136 | "dr2", 137 | "dr3", 138 | "dr6", 139 | "dr7", 140 | ] 141 | X64_REGS = [ 142 | "rax", 143 | "rcx", 144 | "rdx", 145 | "rbx", 146 | "rsp", 147 | "rbp", 148 | "rsi", 149 | "rdi", 150 | "r8", 151 | "r9", 152 | "r10", 153 | "r11", 154 | "r12", 155 | "r13", 156 | "r14", 157 | "r15", 158 | "rip", 159 | "eflags", 160 | "gs", 161 | "fs", 162 | "es", 163 | "ds", 164 | "cs", 165 | "ss", 166 | "dr0", 167 | "dr1", 168 | "dr2", 169 | "dr3", 170 | "dr6", 171 | "dr7", 172 | ] 173 | 174 | # disable this to show all registers 175 | REG_FILTER_ENABLED = True 176 | 177 | # regs not on this list are filtered out of reglist 178 | REG_FILTER = [ 179 | "eax", 180 | "ecx", 181 | "edx", 182 | "ebx", 183 | "esp", 184 | "ebp", 185 | "esi", 186 | "edi", 187 | "eip", 188 | "rax", 189 | "rcx", 190 | "rdx", 191 | "rbx", 192 | "rsp", 193 | "rbp", 194 | "rsi", 195 | "rdi", 196 | "r8", 197 | "r9", 198 | "r10", 199 | "r11", 200 | "r12", 201 | "r13", 202 | "r14", 203 | "r15", 204 | "rip", 205 | "eflags", 206 | # "gs","fs","es","ds","cs","ss", 207 | "dr0", 208 | "dr1", 209 | "dr2", 210 | "dr3", 211 | "dr6", 212 | "dr7", 213 | ] 214 | -------------------------------------------------------------------------------- /core/trace_data.py: -------------------------------------------------------------------------------- 1 | from operator import attrgetter 2 | 3 | 4 | class TraceData: 5 | """TraceData class. 6 | 7 | Class for storing execution trace and bookmarks. 8 | 9 | Attributes: 10 | filename (str): A trace file name. 11 | arch (str): CPU architecture. 12 | ip_reg (str): Name of instruction pointer register 13 | pointer_size (int): Pointer size (4 in x86, 8 in x64) 14 | regs (dict): Register names and indexes 15 | trace (list): A list of traced instructions, registers and memory accesses. 16 | bookmarks (list): A list of bookmarks. 17 | """ 18 | 19 | def __init__(self): 20 | """Inits TraceData.""" 21 | self.filename = "" 22 | self.arch = "" 23 | self.ip_reg = "" 24 | self.pointer_size = 0 25 | self.regs = {} 26 | self.trace = [] 27 | self.bookmarks = [] 28 | 29 | def clear(self): 30 | """Clears trace and all data""" 31 | self.trace = [] 32 | self.bookmarks = [] 33 | 34 | def get_trace(self): 35 | """Returns a full trace 36 | 37 | Returns: 38 | list: Trace 39 | """ 40 | return self.trace 41 | 42 | def get_regs(self): 43 | """Returns dict of registers and their indexes 44 | 45 | Returns: 46 | dict: Regs 47 | """ 48 | return self.regs 49 | 50 | def get_regs_and_values(self, row): 51 | """Returns dict of registers and their values 52 | 53 | Returns: 54 | dict: Register names and values 55 | """ 56 | registers = {} 57 | try: 58 | reg_values = self.trace[row]["regs"] 59 | for reg_name, reg_index in self.regs.items(): 60 | reg_value = reg_values[reg_index] 61 | registers[reg_name] = reg_value 62 | except IndexError: 63 | print(f"Error. Could not get regs from row {row}.") 64 | return {} 65 | return registers 66 | 67 | def get_reg_index(self, reg_name): 68 | """Returns a register index 69 | 70 | Args: 71 | reg_name (str): Register name 72 | Returns: 73 | int: Register index 74 | """ 75 | try: 76 | index = self.regs[reg_name] 77 | except KeyError: 78 | print("Unknown register") 79 | return index 80 | 81 | def get_modified_regs(self, row): 82 | """Returns modfied regs 83 | 84 | Args: 85 | row (int): Trace row index 86 | Returns: 87 | list: List of register names 88 | """ 89 | modified_regs = [] 90 | reg_values = self.trace[row]["regs"] 91 | next_row = row + 1 92 | if next_row < len(self.trace): 93 | next_row_data = self.trace[next_row] 94 | for reg_name, reg_index in self.regs.items(): 95 | reg_value = reg_values[reg_index] 96 | next_reg_value = next_row_data["regs"][reg_index] 97 | if next_reg_value != reg_value: 98 | modified_regs.append(reg_name) 99 | return modified_regs 100 | 101 | def get_trace_rows(self, rows): 102 | """Returns a trace of given rows 103 | 104 | Args: 105 | rows (list): List of trace indexes 106 | Returns: 107 | list: Trace 108 | """ 109 | trace = [] 110 | try: 111 | trace = [self.trace[int(i)] for i in rows] 112 | except IndexError: 113 | print("Error. Could not get trace rows.") 114 | return trace 115 | 116 | def get_instruction_pointer_name(self): 117 | """Returns an instruction pointer name 118 | 119 | Returns: 120 | str: Instruction pointer name 121 | """ 122 | if self.ip_reg: 123 | return self.ip_reg 124 | elif "eip" in self.regs: 125 | return "eip" 126 | elif "rip" in self.regs: 127 | return "rip" 128 | elif "ip" in self.regs: 129 | return "ip" 130 | elif "pc" in self.regs: 131 | return "pc" 132 | return "" 133 | 134 | def get_instruction_pointer(self, row): 135 | """Returns a value of instruction pointer of given row 136 | 137 | Args: 138 | row: A row index in trace 139 | Returns: 140 | int: Address of instruction 141 | """ 142 | ip = 0 143 | ip_reg = self.get_instruction_pointer_name() 144 | try: 145 | reg_index = self.regs[ip_reg] 146 | ip = self.trace[row]["regs"][reg_index] 147 | except IndexError: 148 | print(f"Error. Could not get IP from row {row}") 149 | return ip 150 | 151 | def set_comment(self, row, comment): 152 | """Adds a comment to trace 153 | 154 | Args: 155 | row (int): Row index in trace 156 | comment (str): Comment text 157 | """ 158 | try: 159 | self.trace[row]["comment"] = str(comment) 160 | except IndexError: 161 | print(f"Error. Could not set comment to row {row}") 162 | 163 | def add_bookmark(self, new_bookmark, replace=False): 164 | """Adds a new bookmark 165 | 166 | Args: 167 | new_bookmark (Bookmark): A new bookmark 168 | replace (bool): Replace an existing bookmark if found on same row? 169 | Defaults to False. 170 | """ 171 | for i, bookmark in enumerate(self.bookmarks): 172 | if bookmark.startrow == new_bookmark.startrow: 173 | if replace: 174 | self.bookmarks[i] = new_bookmark 175 | print(f"Bookmark at {bookmark.startrow} replaced.") 176 | else: 177 | print(f"Error: bookmark at {bookmark.startrow} already exists.") 178 | return 179 | self.bookmarks.append(new_bookmark) 180 | self.sort_bookmarks() 181 | 182 | def delete_bookmark(self, index): 183 | """Deletes a bookmark 184 | 185 | Args: 186 | index (int): Index on bookmark list 187 | Returns: 188 | bool: True if bookmark deleted, False otherwise 189 | """ 190 | try: 191 | del self.bookmarks[index] 192 | except IndexError: 193 | print(f"Error. Could not delete a bookmark {index}") 194 | return False 195 | return True 196 | 197 | def sort_bookmarks(self): 198 | """Sorts bookmarks by startrow""" 199 | self.bookmarks.sort(key=attrgetter("startrow")) 200 | 201 | def get_bookmark_from_row(self, row): 202 | """Returns a bookmark for a given trace row. 203 | 204 | Args: 205 | row (int): Trace row index 206 | Returns: 207 | Bookmark: Returns A Bookmark if found, None otherwise. 208 | """ 209 | for bookmark in self.bookmarks: 210 | if bookmark.startrow <= row <= bookmark.endrow: 211 | return bookmark 212 | return None 213 | 214 | def get_bookmarks(self): 215 | """Returns all bookmarks 216 | 217 | Returns: 218 | list: List of bookmarks 219 | """ 220 | return self.bookmarks 221 | 222 | def set_bookmarks(self, bookmarks): 223 | """Sets bookmarks 224 | 225 | Args: 226 | bookmarks (list): Bookmarks 227 | """ 228 | self.bookmarks = bookmarks 229 | 230 | def clear_bookmarks(self): 231 | """Clears bookmarks""" 232 | self.bookmarks = [] 233 | -------------------------------------------------------------------------------- /core/trace_files.py: -------------------------------------------------------------------------------- 1 | import json 2 | import traceback 3 | from capstone import Cs, CS_ARCH_X86, CS_MODE_32, CS_MODE_64 4 | 5 | from core.trace_data import TraceData 6 | from core.bookmark import Bookmark 7 | from core import prefs 8 | 9 | 10 | def open_trace(filename): 11 | """Opens trace file and reads trace data and bookmarks 12 | 13 | Args: 14 | filename: name of trace file 15 | """ 16 | try: 17 | with open(filename, "rb") as f: 18 | magic = f.read(4) 19 | except IOError: 20 | print("Error, could not open file.") 21 | else: 22 | if magic == b"TRAC": 23 | return open_x64dbg_trace(filename) 24 | elif magic == b"TVTR": 25 | return open_tv_trace(filename) 26 | return open_json_trace(filename) 27 | return None 28 | 29 | 30 | def open_tv_trace(filename): 31 | """Opens tvt trace file and reads trace data and bookmarks 32 | 33 | Args: 34 | filename: name of trace file 35 | """ 36 | with open(filename, "rb") as f: 37 | trace_data = TraceData() 38 | trace_data.filename = filename 39 | 40 | # check first 4 bytes 41 | magic = f.read(4) 42 | if magic != b"TVTR": 43 | raise ValueError("Error, wrong file format.") 44 | 45 | json_length_bytes = f.read(4) 46 | json_length = int.from_bytes(json_length_bytes, "little") 47 | 48 | # read JSON blob 49 | json_blob = f.read(json_length) 50 | json_str = str(json_blob, "utf-8") 51 | file_info = json.loads(json_str) 52 | 53 | arch = file_info.get("arch", "") 54 | 55 | reg_indexes = {} 56 | if arch == "x64": 57 | regs = prefs.X64_REGS 58 | for i, reg in enumerate(regs): 59 | reg_indexes[reg] = i 60 | pointer_size = 8 # qword 61 | ip_reg = "rip" 62 | elif arch == "x86": 63 | regs = prefs.X32_REGS 64 | for i, reg in enumerate(regs): 65 | reg_indexes[reg] = i 66 | pointer_size = 4 # dword 67 | ip_reg = "eip" 68 | else: 69 | print(f"Unknown CPU arch: {arch} Let's try to load it anyway.") 70 | ip_reg = file_info.get("ip_reg", "") 71 | pointer_size = file_info.get("pointer_size", 4) 72 | 73 | if "regs" in file_info: 74 | reg_indexes = {} 75 | for i, reg in enumerate(file_info["regs"]): 76 | reg_indexes[reg] = i 77 | 78 | trace_data.arch = arch 79 | trace_data.ip_reg = ip_reg 80 | trace_data.regs = reg_indexes 81 | trace_data.pointer_size = pointer_size 82 | 83 | reg_values = [None] * len(reg_indexes) 84 | trace = [] 85 | row_id = 0 86 | while f.peek(1)[:1] == b"\x00": 87 | f.read(1) 88 | disasm = "" 89 | disasm_length = int.from_bytes(f.read(1), "little") 90 | if disasm_length > 0: 91 | disasm = f.read(disasm_length).decode() 92 | 93 | comment = "" 94 | comment_length = int.from_bytes(f.read(1), "little") 95 | if comment_length > 0: 96 | comment = f.read(comment_length).decode() 97 | 98 | register_changes = int.from_bytes(f.read(1), "little") 99 | memory_accesses = int.from_bytes(f.read(1), "little") 100 | flags_and_opcode_size = int.from_bytes(f.read(1), "little") # Bitfield 101 | thread_id_bit = (flags_and_opcode_size >> 7) & 1 # msb 102 | opcode_size = flags_and_opcode_size & 15 # 4 lsb 103 | 104 | if thread_id_bit > 0: 105 | thread_id = int.from_bytes(f.read(4), "little") 106 | 107 | opcodes = f.read(opcode_size) 108 | 109 | register_change_position = [] 110 | for _ in range(register_changes): 111 | register_change_position.append(int.from_bytes(f.read(1), "little")) 112 | 113 | register_change_new_data = [] 114 | for _ in range(register_changes): 115 | register_change_new_data.append( 116 | int.from_bytes(f.read(pointer_size), "little") 117 | ) 118 | 119 | memory_access_flags = [] 120 | for _ in range(memory_accesses): 121 | memory_access_flags.append(int.from_bytes(f.read(1), "little")) 122 | 123 | memory_access_addresses = [] 124 | for _ in range(memory_accesses): 125 | memory_access_addresses.append( 126 | int.from_bytes(f.read(pointer_size), "little") 127 | ) 128 | 129 | memory_access_data = [] 130 | for i in range(memory_accesses): 131 | memory_access_data.append( 132 | int.from_bytes(f.read(pointer_size), "little") 133 | ) 134 | 135 | reg_id = 0 136 | regchanges = "" 137 | for i, change in enumerate(register_change_position): 138 | reg_id += change 139 | if reg_id + i < len(reg_indexes): 140 | reg_values[reg_id + i] = register_change_new_data[i] 141 | if reg_id + i < len(reg_indexes) and row_id > 0: 142 | reg_name = regs[reg_id + i] 143 | if reg_name is not ip_reg: 144 | old_value = trace[-1]["regs"][reg_id + i] 145 | new_value = register_change_new_data[i] 146 | if old_value != new_value: 147 | if prefs.TRACE_SHOW_OLD_REG_VALUE: 148 | regchanges += ( 149 | f"{reg_name}: {hex(old_value)} -> {hex(new_value)} " 150 | ) 151 | else: 152 | regchanges += f"{reg_name}: {hex(new_value)} " 153 | if 0x7F > new_value > 0x1F: 154 | regchanges += f"'{chr(new_value)}' " 155 | 156 | mems = [] 157 | mem = {} 158 | for i in range(memory_accesses): 159 | flag = memory_access_flags[i] 160 | value = memory_access_data[i] 161 | mem["access"] = "READ" 162 | if flag & 1 == 1: 163 | mem["access"] = "WRITE" 164 | 165 | mem["addr"] = memory_access_addresses[i] 166 | mem["value"] = value 167 | mems.append(mem.copy()) 168 | 169 | if regchanges: 170 | trace[-1]["regchanges"] = regchanges 171 | 172 | trace_row = {} 173 | trace_row["id"] = row_id 174 | if ip_reg: 175 | trace_row["ip"] = reg_values[reg_indexes[ip_reg]] 176 | trace_row["disasm"] = disasm 177 | trace_row["comment"] = comment 178 | trace_row["regs"] = reg_values.copy() 179 | trace_row["opcodes"] = opcodes.hex() 180 | trace_row["mem"] = mems.copy() 181 | trace.append(trace_row) 182 | row_id += 1 183 | 184 | trace_data.trace = trace 185 | 186 | while f.peek(1)[:1] == b"\x01": 187 | f.read(1) 188 | bookmark = Bookmark() 189 | bookmark.startrow = int.from_bytes(f.read(4), "little") 190 | bookmark.endrow = int.from_bytes(f.read(4), "little") 191 | disasm_length = int.from_bytes(f.read(1), "little") 192 | bookmark.disasm = f.read(disasm_length).decode() 193 | comment_length = int.from_bytes(f.read(1), "little") 194 | bookmark.comment = f.read(comment_length).decode() 195 | addr_length = int.from_bytes(f.read(1), "little") 196 | bookmark.addr = f.read(addr_length).decode() 197 | trace_data.add_bookmark(bookmark) 198 | 199 | return trace_data 200 | 201 | 202 | def open_json_trace(filename): 203 | """Opens JSON trace file and reads trace data and bookmarks 204 | 205 | Args: 206 | filename: name of trace file 207 | """ 208 | try: 209 | f = open(filename) 210 | except IOError: 211 | print("Error, could not open file.") 212 | else: 213 | with f: 214 | try: 215 | trace_data = TraceData() 216 | data = json.load(f) 217 | trace_data.bookmarks = [] 218 | trace_data.filename = filename 219 | trace_data.trace = data["trace"] 220 | trace_data.arch = data.get("arch", "") 221 | trace_data.ip_reg = data.get("ip_reg", "") 222 | trace_data.pointer_size = data.get("pointer_size", 4) 223 | trace_data.regs = data.get("regs", {}) 224 | if "bookmarks" in data: 225 | for bookmark in data["bookmarks"]: 226 | trace_data.add_bookmark(Bookmark(**bookmark)) 227 | return trace_data 228 | except KeyError: 229 | print("Error while reading trace file.") 230 | except Exception as exc: 231 | print("Error while reading trace file: " + str(exc)) 232 | print(traceback.format_exc()) 233 | return None 234 | 235 | 236 | def save_as_json(trace_data, filename): 237 | """Saves trace data to file in JSON format 238 | 239 | Args: 240 | trace_data: TraceData object 241 | filename: name of trace file 242 | """ 243 | data = { 244 | "arch": trace_data.arch, 245 | "regs": trace_data.regs, 246 | "ip_reg": trace_data.ip_reg, 247 | "pointer_size": trace_data.pointer_size, 248 | "trace": trace_data.trace, 249 | "bookmarks": [vars(h) for h in trace_data.bookmarks], 250 | } 251 | with open(filename, "w") as f: 252 | json.dump(data, f) 253 | 254 | 255 | def save_as_tv_trace(trace_data, filename): 256 | """Saves trace data in a default Trace Viewer format 257 | 258 | Args: 259 | trace_data: TraceData object 260 | filename: name of trace file 261 | """ 262 | try: 263 | f = open(filename, "wb") 264 | except IOError: 265 | print("Error, could not write to file.") 266 | else: 267 | with f: 268 | trace = trace_data.trace 269 | f.write(b"TVTR") 270 | file_info = {"arch": trace_data.arch, "version": "1.0"} 271 | if trace_data.pointer_size: 272 | pointer_size = trace_data.pointer_size 273 | elif trace_data.arch == "x64": 274 | pointer_size = 8 275 | else: 276 | pointer_size = 4 277 | 278 | file_info["pointer_size"] = pointer_size 279 | file_info["regs"] = list(trace_data.regs.keys()) 280 | file_info["ip_reg"] = trace_data.ip_reg 281 | 282 | json_blob = json.dumps(file_info) 283 | json_blob_length = len(json_blob) 284 | f.write((json_blob_length).to_bytes(4, byteorder="little")) 285 | f.write(json_blob.encode()) 286 | 287 | for i, t in enumerate(trace): 288 | f.write(b"\x00") 289 | 290 | disasm = t["disasm"][:255] # limit length to 0xff 291 | f.write((len(disasm)).to_bytes(1, byteorder="little")) 292 | f.write(disasm.encode()) 293 | 294 | comment = t["comment"][:255] 295 | f.write((len(comment)).to_bytes(1, byteorder="little")) 296 | f.write(comment.encode()) 297 | 298 | reg_change_newdata = [] 299 | reg_change_positions = [] 300 | pos = 0 301 | for reg_index, reg_value in enumerate(t["regs"]): 302 | if i == 0 or reg_value != trace[i - 1]["regs"][reg_index]: 303 | reg_change_newdata.append(reg_value) # value changed 304 | reg_change_positions.append(pos) 305 | pos = -1 306 | pos += 1 307 | 308 | reg_changes = len(reg_change_positions) & 0xFF 309 | f.write((reg_changes).to_bytes(1, byteorder="little")) 310 | 311 | memory_accesses = len(t["mem"]) & 0xFF 312 | f.write((memory_accesses).to_bytes(1, byteorder="little")) 313 | 314 | flag = 0 315 | opcodes = bytes.fromhex(t["opcodes"]) 316 | opcode_size = len(opcodes) 317 | if "thread" in t: 318 | flag = flag | (1 << 7) 319 | flags_and_opcode_size = flag | opcode_size 320 | 321 | f.write((flags_and_opcode_size).to_bytes(1, byteorder="little")) 322 | if "thread" in t: 323 | f.write((t["thread"]).to_bytes(4, byteorder="little")) 324 | f.write(opcodes) 325 | 326 | for pos in reg_change_positions: 327 | f.write((pos).to_bytes(1, byteorder="little")) 328 | 329 | for newdata in reg_change_newdata: 330 | f.write((newdata).to_bytes(pointer_size, byteorder="little")) 331 | 332 | mem_access_flags = [] 333 | mem_access_addresses = [] 334 | mem_access_data = [] 335 | for mem_access in t["mem"]: 336 | flag = 0 337 | if mem_access["access"].lower() == "write": 338 | flag = 1 339 | mem_access_flags.append(flag) 340 | mem_access_data.append(mem_access["value"]) 341 | mem_access_addresses.append(mem_access["addr"]) 342 | 343 | for flag in mem_access_flags: 344 | f.write((flag).to_bytes(1, byteorder="little")) 345 | for addr in mem_access_addresses: 346 | f.write((addr).to_bytes(pointer_size, byteorder="little")) 347 | for data in mem_access_data: 348 | f.write((data).to_bytes(pointer_size, byteorder="little")) 349 | 350 | for bookmark in trace_data.bookmarks: 351 | f.write(b"\x01") 352 | f.write((bookmark.startrow).to_bytes(4, byteorder="little")) 353 | f.write((bookmark.endrow).to_bytes(4, byteorder="little")) 354 | disasm = bookmark.disasm[:255] 355 | disasm_length = len(disasm) 356 | f.write((disasm_length).to_bytes(1, byteorder="little")) 357 | f.write(disasm.encode()) 358 | comment = bookmark.comment[:255] 359 | comment_length = len(comment) 360 | f.write((comment_length).to_bytes(1, byteorder="little")) 361 | f.write(comment.encode()) 362 | addr = bookmark.addr[:255] 363 | addr_length = len(addr) 364 | f.write((addr_length).to_bytes(1, byteorder="little")) 365 | f.write(addr.encode()) 366 | 367 | 368 | def open_x64dbg_trace(filename): 369 | """Opens x64dbg trace file 370 | 371 | Args: 372 | filename: name of trace file 373 | Returns: 374 | TraceData object 375 | """ 376 | with open(filename, "rb") as f: 377 | trace_data = TraceData() 378 | trace_data.filename = filename 379 | 380 | # check first 4 bytes 381 | magic = f.read(4) 382 | if magic != b"TRAC": 383 | raise ValueError("Error, wrong file format.") 384 | 385 | json_length_bytes = f.read(4) 386 | json_length = int.from_bytes(json_length_bytes, "little") 387 | 388 | # read JSON blob 389 | json_blob = f.read(json_length) 390 | json_str = str(json_blob, "utf-8") 391 | arch = json.loads(json_str)["arch"] 392 | 393 | reg_indexes = {} 394 | if arch == "x64": 395 | regs = prefs.X64_REGS 396 | ip_reg = "rip" 397 | capstone_mode = CS_MODE_64 398 | pointer_size = 8 # qword 399 | else: 400 | regs = prefs.X32_REGS 401 | ip_reg = "eip" 402 | capstone_mode = CS_MODE_32 403 | pointer_size = 4 # dword 404 | 405 | for i, reg in enumerate(regs): 406 | reg_indexes[reg] = i 407 | 408 | trace_data.arch = arch 409 | trace_data.ip_reg = ip_reg 410 | trace_data.regs = reg_indexes 411 | trace_data.pointer_size = pointer_size 412 | 413 | md = Cs(CS_ARCH_X86, capstone_mode) 414 | reg_values = [None] * len(reg_indexes) 415 | trace = [] 416 | row_id = 0 417 | while f.read(1) == b"\x00": 418 | register_changes = int.from_bytes(f.read(1), "little") 419 | memory_accesses = int.from_bytes(f.read(1), "little") 420 | flags_and_opcode_size = int.from_bytes(f.read(1), "little") # Bitfield 421 | thread_id_bit = (flags_and_opcode_size >> 7) & 1 # msb 422 | opcode_size = flags_and_opcode_size & 15 # 4 lsbs 423 | 424 | if thread_id_bit > 0: 425 | thread_id = int.from_bytes(f.read(4), "little") 426 | 427 | opcodes = f.read(opcode_size) 428 | 429 | register_change_position = [] 430 | for _ in range(register_changes): 431 | register_change_position.append(int.from_bytes(f.read(1), "little")) 432 | 433 | register_change_new_data = [] 434 | for _ in range(register_changes): 435 | register_change_new_data.append( 436 | int.from_bytes(f.read(pointer_size), "little") 437 | ) 438 | 439 | memory_access_flags = [] 440 | for _ in range(memory_accesses): 441 | memory_access_flags.append(int.from_bytes(f.read(1), "little")) 442 | 443 | memory_access_addresses = [] 444 | for _ in range(memory_accesses): 445 | memory_access_addresses.append( 446 | int.from_bytes(f.read(pointer_size), "little") 447 | ) 448 | 449 | memory_access_old_data = [] 450 | for _ in range(memory_accesses): 451 | memory_access_old_data.append( 452 | int.from_bytes(f.read(pointer_size), "little") 453 | ) 454 | 455 | memory_access_new_data = [] 456 | for i in range(memory_accesses): 457 | if memory_access_flags[i] & 1 == 0: 458 | memory_access_new_data.append( 459 | int.from_bytes(f.read(pointer_size), "little") 460 | ) 461 | 462 | reg_id = 0 463 | regchanges = "" 464 | for i, change in enumerate(register_change_position): 465 | reg_id += change 466 | if reg_id + i < len(reg_indexes): 467 | reg_values[reg_id + i] = register_change_new_data[i] 468 | if reg_id + i < len(reg_indexes) and row_id > 0: 469 | reg_name = regs[reg_id + i] 470 | if reg_name is not ip_reg: 471 | old_value = trace[-1]["regs"][reg_id + i] 472 | new_value = register_change_new_data[i] 473 | if old_value != new_value: 474 | if prefs.TRACE_SHOW_OLD_REG_VALUE: 475 | regchanges += ( 476 | f"{reg_name}: {hex(old_value)} -> {hex(new_value)} " 477 | ) 478 | else: 479 | regchanges += f"{reg_name}: {hex(new_value)} " 480 | if 0x7F > new_value > 0x1F: 481 | regchanges += f"'{chr(new_value)}' " 482 | 483 | # disassemble 484 | ip = reg_values[reg_indexes[ip_reg]] 485 | for _address, _size, mnemonic, op_str in md.disasm_lite(opcodes, ip): 486 | disasm = mnemonic 487 | if op_str: 488 | disasm += " " + op_str 489 | 490 | mems = [] 491 | mem = {} 492 | new_data_counter = 0 493 | for i in range(memory_accesses): 494 | flag = memory_access_flags[i] 495 | value = memory_access_old_data[i] 496 | mem["access"] = "READ" 497 | if flag & 1 == 0: 498 | value = memory_access_new_data[new_data_counter] 499 | mem["access"] = "WRITE" 500 | new_data_counter += 1 501 | else: 502 | pass 503 | # memory value didn't change 504 | # (it is read or overwritten with identical value) 505 | # this has to be fixed somehow in x64dbg 506 | 507 | mem["addr"] = memory_access_addresses[i] 508 | 509 | # fix value (x64dbg saves all values as qwords) 510 | if "qword" in disasm: 511 | pass 512 | elif "dword" in disasm: 513 | value &= 0xFFFFFFFF 514 | elif "word" in disasm: 515 | value &= 0xFFFF 516 | elif "byte" in disasm: 517 | value &= 0xFF 518 | mem["value"] = value 519 | mems.append(mem.copy()) 520 | 521 | if regchanges: 522 | trace[-1]["regchanges"] = regchanges 523 | 524 | trace_row = {} 525 | trace_row["id"] = row_id 526 | trace_row["ip"] = ip 527 | trace_row["disasm"] = disasm 528 | trace_row["regs"] = reg_values.copy() 529 | trace_row["opcodes"] = opcodes.hex() 530 | trace_row["mem"] = mems.copy() 531 | # trace_row["comment"] = "" 532 | trace.append(trace_row) 533 | row_id += 1 534 | 535 | trace_data.trace = trace 536 | return trace_data 537 | -------------------------------------------------------------------------------- /docs/img/etv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teemu-l/execution-trace-viewer/d2d99e92decb3526efc1f5bd4bae350aa4c7c0d2/docs/img/etv.png -------------------------------------------------------------------------------- /gui/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teemu-l/execution-trace-viewer/d2d99e92decb3526efc1f5bd4bae350aa4c7c0d2/gui/__init__.py -------------------------------------------------------------------------------- /gui/input_dialog.py: -------------------------------------------------------------------------------- 1 | """Dialog to get values from user""" 2 | from PyQt5.QtCore import Qt 3 | from PyQt5.QtWidgets import ( 4 | QDialog, 5 | QWidget, 6 | QGridLayout, 7 | QLabel, 8 | QLineEdit, 9 | QPushButton, 10 | QComboBox, 11 | QCheckBox, 12 | ) 13 | 14 | 15 | class InputDialog(QDialog): 16 | def __init__(self, parent=None, title="Title", data=[], on_ok_clicked=None): 17 | QWidget.__init__(self, parent) 18 | 19 | self.setWindowFlags( 20 | Qt.Dialog 21 | | Qt.WindowTitleHint 22 | | Qt.CustomizeWindowHint 23 | | Qt.WindowCloseButtonHint 24 | ) 25 | self.data = data 26 | self.output_data = [] 27 | self.on_ok_clicked = on_ok_clicked 28 | 29 | mainLayout = QGridLayout() 30 | 31 | # create labels and input widgets 32 | for i, item in enumerate(self.data): 33 | 34 | label_widget = QLabel(item["label"] + ":") 35 | 36 | if isinstance(item["data"], bool): 37 | input_widget = QCheckBox() 38 | input_widget.setChecked(item["data"]) 39 | elif isinstance(item["data"], (int, str)): 40 | default = str(item.get("data", "")) 41 | input_widget = QLineEdit(default) 42 | elif isinstance(item["data"], list): 43 | input_widget = QComboBox() 44 | input_widget.addItems(item["data"]) 45 | else: 46 | print(f"Error. Unknown data type: {type(item['data'])}") 47 | return 48 | 49 | if "tooltip" in item: 50 | input_widget.setToolTip(str(item["tooltip"])) 51 | label_widget.setToolTip(str(item["tooltip"])) 52 | 53 | item["widget"] = input_widget 54 | 55 | mainLayout.addWidget(label_widget, i, 0) 56 | mainLayout.addWidget(input_widget, i, 1) 57 | 58 | ok_btn = QPushButton("Ok") 59 | ok_btn.clicked.connect(self.on_ok_btn_clicked) 60 | mainLayout.addWidget(ok_btn) 61 | 62 | cancel_btn = QPushButton("Cancel") 63 | cancel_btn.clicked.connect(self.on_cancel_btn_clicked) 64 | mainLayout.addWidget(cancel_btn) 65 | 66 | self.setLayout(mainLayout) 67 | self.setWindowTitle(title) 68 | 69 | def on_ok_btn_clicked(self): 70 | data = [] 71 | for item in self.data: 72 | if isinstance(item["data"], bool): 73 | data.append(item["widget"].isChecked()) 74 | elif isinstance(item["data"], str): 75 | data.append(item["widget"].text()) 76 | elif isinstance(item["data"], int): 77 | text = item["widget"].text() 78 | text = text.strip() # remove whitespaces 79 | try: 80 | if "0x" in text: 81 | data.append(int(text, 16)) 82 | else: 83 | data.append(int(text)) 84 | except ValueError: 85 | print(f"Error, could not convert {text} to integer.") 86 | return 87 | elif isinstance(item["data"], list): 88 | data.append(int(item["widget"].currentIndex())) 89 | 90 | # call callback function to check the data 91 | if self.on_ok_clicked and not self.on_ok_clicked(data): 92 | return 93 | 94 | self.output_data = data 95 | self.close() 96 | 97 | def on_cancel_btn_clicked(self): 98 | self.output_data = [] 99 | self.close() 100 | 101 | def get_data(self): 102 | return self.output_data 103 | -------------------------------------------------------------------------------- /gui/mainwindow.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import functools 4 | import traceback 5 | 6 | from PyQt5 import uic 7 | from PyQt5.QtCore import Qt 8 | from PyQt5.QtGui import QCursor, QFont 9 | from PyQt5.QtWidgets import ( 10 | QMainWindow, 11 | QAction, 12 | QMenu, 13 | QFileDialog, 14 | QAbstractItemView, 15 | QMessageBox, 16 | QInputDialog, 17 | QLineEdit, 18 | QTableWidgetItem, 19 | QApplication, 20 | ) 21 | from yapsy.PluginManager import PluginManager 22 | 23 | from core.trace_data import TraceData 24 | from core import trace_files 25 | from core.filter_and_find import find 26 | from core.filter_and_find import filter_trace 27 | from core.filter_and_find import TraceField 28 | from core.api import Api 29 | from core import prefs 30 | from gui.syntax_hl.syntax_hl_log import AsmHighlighter 31 | from gui.syntax_hl.syntax_hl_delegate import SyntaxHighlightDelegate 32 | from gui.widgets.pagination_widget import PaginationWidget 33 | from gui.widgets.find_widget import FindWidget 34 | from gui.widgets.filter_widget import FilterWidget 35 | from gui.input_dialog import InputDialog 36 | 37 | 38 | class MainWindow(QMainWindow): 39 | """MainWindow class 40 | 41 | Attributes: 42 | trace_data (TraceData): TraceData object 43 | filtered_trace (list): Filtered trace 44 | """ 45 | 46 | def __init__(self, parent=None): 47 | """Inits MainWindow, UI and plugins""" 48 | super(MainWindow, self).__init__(parent) 49 | self.api = Api(self) 50 | self.trace_data = TraceData() 51 | self.filtered_trace = [] 52 | self.filter_text = "" 53 | self.init_plugins() 54 | self.init_ui() 55 | if len(sys.argv) > 1: 56 | self.open_trace(sys.argv[1]) 57 | 58 | def dragEnterEvent(self, event): 59 | """QMainWindow method reimplementation for file drag.""" 60 | event.setDropAction(Qt.MoveAction) 61 | super().dragEnterEvent(event) 62 | event.accept() 63 | 64 | def dropEvent(self, event): 65 | """QMainWindow method reimplementation for file drop.""" 66 | super().dropEvent(event) 67 | if event.mimeData().hasUrls(): 68 | for url in event.mimeData().urls(): 69 | local_file = url.toLocalFile() 70 | if os.path.isfile(local_file): 71 | self.open_trace(local_file) 72 | 73 | def init_ui(self): 74 | """Inits UI""" 75 | uic.loadUi("gui/mainwindow.ui", self) 76 | 77 | self.setWindowTitle(prefs.PACKAGE_NAME) 78 | 79 | # accept file drops 80 | self.setAcceptDrops(True) 81 | 82 | self.resize(prefs.WINDOW_WIDTH, prefs.WINDOW_HEIGHT) 83 | 84 | # make trace table wider than regs&mem 85 | self.splitter1.setSizes([1000, 100]) 86 | self.splitter2.setSizes([600, 100]) 87 | 88 | # Init trace table 89 | self.trace_table.itemSelectionChanged.connect(self.on_trace_table_row_changed) 90 | self.trace_table.setColumnCount(len(prefs.TRACE_LABELS)) 91 | self.trace_table.setHorizontalHeaderLabels(prefs.TRACE_LABELS) 92 | self.trace_table.horizontalHeader().setStretchLastSection(True) 93 | self.trace_table.bookmarkCreated.connect(self.add_bookmark) 94 | self.trace_table.commentEdited.connect(self.set_comment) 95 | self.trace_table.printer = self.print 96 | self.trace_table.set_row_height(prefs.TRACE_ROW_HEIGHT) 97 | 98 | trace_font = QFont(prefs.TRACE_FONT) 99 | trace_font.setPointSize(prefs.TRACE_FONT_SIZE) 100 | self.trace_table.setFont(trace_font) 101 | self.bookmark_table.setFont(trace_font) 102 | self.trace_table.setShowGrid(prefs.TRACE_SHOW_GRID) 103 | 104 | if prefs.USE_SYNTAX_HIGHLIGHT_IN_TRACE: 105 | self.trace_table.init_syntax_highlight() 106 | 107 | # trace pagination 108 | if prefs.PAGINATION_ENABLED: 109 | self.trace_pagination = PaginationWidget() 110 | self.trace_pagination.pageChanged.connect(self.trace_table.populate) 111 | self.horizontalLayout.addWidget(self.trace_pagination) 112 | self.trace_pagination.set_enabled(True) 113 | self.trace_pagination.rows_per_page = prefs.PAGINATION_ROWS_PER_PAGE 114 | 115 | self.trace_table.pagination = self.trace_pagination 116 | self.horizontalLayout.setAlignment(self.trace_pagination, Qt.AlignLeft) 117 | 118 | # these are used to remember current pages & scroll values for both traces 119 | self.trace_current_pages = [1, 1] 120 | self.trace_scroll_values = [0, 0] 121 | 122 | self.reg_table.create_context_menu() 123 | self.reg_table.setColumnCount(len(prefs.REG_LABELS)) 124 | self.reg_table.setHorizontalHeaderLabels(prefs.REG_LABELS) 125 | self.reg_table.horizontalHeader().setStretchLastSection(True) 126 | self.reg_table.regCheckBoxChanged.connect(self.on_reg_checkbox_change) 127 | self.reg_table.printer = self.print 128 | 129 | if prefs.REG_FILTER_ENABLED: 130 | self.reg_table.filtered_regs = prefs.REG_FILTER 131 | 132 | if not prefs.USE_DARK_THEME: 133 | trace_style = ( 134 | "QTableView { selection-background-color: #dddddd; selection-" 135 | "color: #000000; border: 0px;} QTableWidget::item { padding: 0px; border: 0px}" 136 | ) 137 | reg_style = ( 138 | "QTableView { selection-background-color: #eee864; selection" 139 | "-color: #000000;}" 140 | ) 141 | self.trace_table.setStyleSheet(trace_style) 142 | self.bookmark_table.setStyleSheet(trace_style) 143 | self.reg_table.setStyleSheet(reg_style) 144 | 145 | # Init memory table 146 | self.mem_table.setColumnCount(len(prefs.MEM_LABELS)) 147 | self.mem_table.setHorizontalHeaderLabels(prefs.MEM_LABELS) 148 | self.mem_table.horizontalHeader().setStretchLastSection(True) 149 | 150 | # Init bookmark table 151 | self.bookmark_table.setColumnCount(len(prefs.BOOKMARK_LABELS)) 152 | self.bookmark_table.setHorizontalHeaderLabels(prefs.BOOKMARK_LABELS) 153 | self.bookmark_table.setContextMenuPolicy(Qt.CustomContextMenu) 154 | self.bookmark_table.customContextMenuRequested.connect( 155 | self.bookmark_table_context_menu_event 156 | ) 157 | 158 | self.bookmark_table.delegate = SyntaxHighlightDelegate(self) 159 | self.bookmark_table.delegate.disasm_columns = prefs.BOOKMARK_HL_DISASM_COLUMNS 160 | self.bookmark_table.delegate.value_columns = prefs.BOOKMARK_HL_VALUE_COLUMNS 161 | self.bookmark_table.setItemDelegate(self.bookmark_table.delegate) 162 | 163 | self.bookmark_menu = QMenu(self) 164 | 165 | go_action = QAction("Go to bookmark", self) 166 | go_action.triggered.connect(self.go_to_bookmark_in_trace) 167 | self.bookmark_menu.addAction(go_action) 168 | 169 | delete_bookmarks_action = QAction("Delete bookmark(s)", self) 170 | delete_bookmarks_action.triggered.connect(self.delete_bookmarks) 171 | self.bookmark_menu.addAction(delete_bookmarks_action) 172 | 173 | # Menu 174 | exit_action = QAction("&Exit", self) 175 | exit_action.setShortcut("Ctrl+Q") 176 | exit_action.setStatusTip("Exit application") 177 | exit_action.triggered.connect(self.close) 178 | 179 | open_trace_action = QAction("&Open trace..", self) 180 | open_trace_action.setStatusTip("Open trace") 181 | open_trace_action.triggered.connect(self.dialog_open_trace) 182 | 183 | self.save_trace_action = QAction("&Save trace", self) 184 | self.save_trace_action.setStatusTip("Save trace") 185 | self.save_trace_action.triggered.connect(self.save_trace) 186 | self.save_trace_action.setEnabled(False) 187 | 188 | save_trace_as_action = QAction("&Save trace as..", self) 189 | save_trace_as_action.setStatusTip("Save trace as..") 190 | save_trace_as_action.triggered.connect(self.dialog_save_trace_as) 191 | 192 | save_trace_as_json_action = QAction("&Save trace as JSON..", self) 193 | save_trace_as_json_action.setStatusTip("Save trace as JSON..") 194 | save_trace_as_json_action.triggered.connect(self.dialog_save_trace_as_json) 195 | 196 | file_menu = self.menu_bar.addMenu("&File") 197 | file_menu.addAction(open_trace_action) 198 | file_menu.addAction(self.save_trace_action) 199 | file_menu.addAction(save_trace_as_action) 200 | file_menu.addAction(save_trace_as_json_action) 201 | file_menu.addAction(exit_action) 202 | 203 | self.plugins_topmenu = self.menu_bar.addMenu("&Plugins") 204 | 205 | clear_bookmarks_action = QAction("&Clear bookmarks", self) 206 | clear_bookmarks_action.setStatusTip("Clear bookmarks") 207 | clear_bookmarks_action.triggered.connect(self.clear_bookmarks) 208 | 209 | bookmarks_menu = self.menu_bar.addMenu("&Bookmarks") 210 | bookmarks_menu.addAction(clear_bookmarks_action) 211 | 212 | # Create right click menu for trace table 213 | self.create_trace_table_menu() 214 | # Create plugins menu on menu bar 215 | self.create_plugins_menu() 216 | 217 | about_action = QAction("&About", self) 218 | about_action.triggered.connect(self.show_about_dialog) 219 | 220 | about_menu = self.menu_bar.addMenu("&About") 221 | about_menu.addAction(about_action) 222 | 223 | if prefs.USE_SYNTAX_HIGHLIGHT_IN_LOG: 224 | self.highlight = AsmHighlighter(self.log_text_edit.document()) 225 | 226 | # trace select 227 | self.select_trace_combo_box.addItem("Full trace") 228 | self.select_trace_combo_box.addItem("Filtered trace") 229 | self.select_trace_combo_box.currentIndexChanged.connect( 230 | self.on_trace_combo_box_index_changed 231 | ) 232 | 233 | self.filter_widget = FilterWidget() 234 | self.filter_widget.filterBtnClicked.connect(self.on_filter_btn_clicked) 235 | self.horizontalLayout.addWidget(self.filter_widget) 236 | if prefs.SHOW_SAMPLE_FILTERS: 237 | self.filter_widget.set_sample_filters(prefs.SAMPLE_FILTERS) 238 | 239 | self.find_widget = FindWidget() 240 | self.find_widget.findBtnClicked.connect(self.on_find_btn_clicked) 241 | self.find_widget.set_fields(prefs.FIND_FIELDS) 242 | self.horizontalLayout.addWidget(self.find_widget) 243 | 244 | self.show() 245 | 246 | def init_plugins(self): 247 | """Inits plugins""" 248 | self.manager = PluginManager() 249 | self.manager.setPluginPlaces(["plugins"]) 250 | self.manager.collectPlugins() 251 | for plugin in self.manager.getAllPlugins(): 252 | print_debug(f"Plugin found: {plugin.name}") 253 | 254 | def create_plugins_menu(self): 255 | """Creates plugins menu""" 256 | self.plugins_topmenu.clear() 257 | 258 | reload_action = QAction("Reload plugins", self) 259 | reload_action.setShortcut("Ctrl+R") 260 | func = functools.partial(self.reload_plugins) 261 | reload_action.triggered.connect(func) 262 | self.plugins_topmenu.addAction(reload_action) 263 | self.plugins_topmenu.addSeparator() 264 | 265 | plugins_menu = QMenu("Run plugin", self) 266 | 267 | for plugin in self.manager.getAllPlugins(): 268 | action = QAction(plugin.name, self) 269 | func = functools.partial(self.execute_plugin, plugin) 270 | action.triggered.connect(func) 271 | plugins_menu.addAction(action) 272 | self.plugins_topmenu.addMenu(plugins_menu) 273 | 274 | def create_trace_table_menu(self): 275 | """Creates right click menu for trace table""" 276 | self.trace_table_menu = QMenu(self) 277 | 278 | print_action = QAction("Print selected cells", self) 279 | print_action.triggered.connect(self.trace_table.print_selected_cells) 280 | self.trace_table_menu.addAction(print_action) 281 | 282 | add_bookmark_action = QAction("Add Bookmark", self) 283 | add_bookmark_action.triggered.connect(self.trace_table.create_bookmark) 284 | self.trace_table_menu.addAction(add_bookmark_action) 285 | 286 | plugins_menu = QMenu("Plugins", self) 287 | 288 | for plugin in self.manager.getAllPlugins(): 289 | action = QAction(plugin.name, self) 290 | func = functools.partial(self.execute_plugin, plugin) 291 | action.triggered.connect(func) 292 | plugins_menu.addAction(action) 293 | self.trace_table_menu.addMenu(plugins_menu) 294 | self.trace_table.menu = self.trace_table_menu 295 | 296 | def reload_plugins(self): 297 | """Reloads plugins""" 298 | self.init_plugins() 299 | self.create_trace_table_menu() 300 | self.create_plugins_menu() 301 | 302 | def on_trace_table_row_changed(self): 303 | """Called when selected row changes""" 304 | selected_row_ids = self.get_selected_row_ids(self.trace_table) 305 | if not selected_row_ids: 306 | return 307 | row_id = selected_row_ids[0] 308 | regs = self.trace_data.get_regs_and_values(row_id) 309 | modified_regs = [] 310 | if prefs.HIGHLIGHT_MODIFIED_REGS: 311 | modified_regs = self.trace_data.get_modified_regs(row_id) 312 | self.reg_table.set_data(regs, modified_regs) 313 | mem = [] 314 | if "mem" in self.trace_data.trace[row_id]: 315 | mem = self.trace_data.trace[row_id]["mem"] 316 | self.mem_table.set_data(mem) 317 | self.update_status_bar() 318 | 319 | def on_filter_btn_clicked(self, filter_text: str): 320 | self.filter_text = filter_text 321 | if self.trace_data is None: 322 | return 323 | try: 324 | filtered_trace = filter_trace( 325 | self.trace_data.trace, self.trace_data.get_regs(), filter_text, 326 | ) 327 | except Exception as exc: 328 | self.show_messagebox("Filter error", f"{exc}") 329 | # print(traceback.format_exc()) 330 | else: 331 | self.filtered_trace = filtered_trace 332 | self.show_filtered_trace() 333 | self.update_status_bar() 334 | 335 | def on_find_btn_clicked(self, keyword: str, field_index: int, direction: int): 336 | """Find next or prev button clicked""" 337 | current_row = self.trace_table.currentRow() 338 | if current_row < 0: 339 | current_row = 0 340 | 341 | if self.trace_table.pagination is not None: 342 | pagination = self.trace_table.pagination 343 | page = pagination.current_page 344 | rows_per_page = pagination.rows_per_page 345 | current_row += (page - 1) * rows_per_page 346 | 347 | if field_index == 0: 348 | field = TraceField.DISASM 349 | elif field_index == 1: 350 | field = TraceField.REGS 351 | elif field_index == 2: 352 | field = TraceField.MEM 353 | elif field_index == 3: 354 | field = TraceField.MEM_ADDR 355 | elif field_index == 4: 356 | field = TraceField.MEM_VALUE 357 | elif field_index == 5: 358 | field = TraceField.COMMENT 359 | elif field_index == 6: 360 | field = TraceField.ANY 361 | 362 | try: 363 | row_number = find( 364 | trace=self.get_visible_trace(), 365 | field=field, 366 | keyword=keyword, 367 | start_row=current_row + direction, 368 | direction=direction, 369 | ) 370 | except Exception as exc: 371 | self.show_messagebox("Find error", f"{exc}") 372 | print(traceback.format_exc()) 373 | self.print(traceback.format_exc()) 374 | return 375 | 376 | if row_number is not None: 377 | self.trace_table.go_to_row(row_number) 378 | else: 379 | print_debug( 380 | f"{keyword} not found (row: {current_row}, direction: {direction})" 381 | ) 382 | 383 | def get_visible_trace(self): 384 | """Returns the trace that is currently shown on trace table""" 385 | index = self.select_trace_combo_box.currentIndex() 386 | if self.trace_data is not None: 387 | if index == 0: 388 | return self.trace_data.trace 389 | else: 390 | return self.filtered_trace 391 | return None 392 | 393 | def bookmark_table_context_menu_event(self): 394 | """Context menu for bookmark table right click""" 395 | self.bookmark_menu.popup(QCursor.pos()) 396 | 397 | def dialog_open_trace(self): 398 | """Shows dialog to open trace file""" 399 | all_traces = "All traces (*.tvt *.trace32 *.trace64)" 400 | all_files = "All files (*.*)" 401 | filename = QFileDialog.getOpenFileName( 402 | self, "Open trace", "", all_traces + ";; " + all_files 403 | )[0] 404 | if filename: 405 | self.open_trace(filename) 406 | if self.trace_data: 407 | self.save_trace_action.setEnabled(True) 408 | 409 | def dialog_save_trace_as(self): 410 | """Shows a dialog to select a save file""" 411 | filename = QFileDialog.getSaveFileName( 412 | self, "Save trace as", "", "Trace Viewer traces (*.tvt);; All files (*.*)" 413 | )[0] 414 | print_debug("Save trace as: " + filename) 415 | if filename and trace_files.save_as_tv_trace(self.trace_data, filename): 416 | self.trace_data.filename = filename 417 | self.save_trace_action.setEnabled(True) 418 | 419 | def dialog_save_trace_as_json(self): 420 | """Shows a dialog to save trace to JSON file""" 421 | filename = QFileDialog.getSaveFileName( 422 | self, "Save as JSON", "", "JSON files (*.txt);; All files (*.*)" 423 | )[0] 424 | print_debug("Save trace as: " + filename) 425 | if filename: 426 | trace_files.save_as_json(self.trace_data, filename) 427 | 428 | def execute_plugin(self, plugin): 429 | """Executes a plugin and updates tables""" 430 | print_debug(f"Executing a plugin: {plugin.name}") 431 | try: 432 | plugin.plugin_object.execute(self.api) 433 | except Exception: 434 | print("Error in plugin:") 435 | print(traceback.format_exc()) 436 | self.print("Error in plugin:") 437 | self.print(traceback.format_exc()) 438 | finally: 439 | if prefs.USE_SYNTAX_HIGHLIGHT_IN_LOG: 440 | self.highlight.rehighlight() 441 | 442 | def show_filtered_trace(self): 443 | """Shows filtered_trace on trace_table""" 444 | if self.select_trace_combo_box.currentIndex() == 0: 445 | self.select_trace_combo_box.setCurrentIndex(1) 446 | else: 447 | self.trace_table.set_data(self.filtered_trace) 448 | self.trace_table.populate() 449 | 450 | def set_comment(self, row_id, comment): 451 | """Sets comment to row on full trace""" 452 | self.trace_data.set_comment(row_id, comment) 453 | 454 | def on_bookmark_table_cell_edited(self, item): 455 | """Called when any cell is edited on bookmark table""" 456 | cell_type = item.whatsThis() 457 | bookmarks = self.trace_data.get_bookmarks() 458 | row = self.bookmark_table.currentRow() 459 | if row < 0: 460 | print_debug("Error, could not edit bookmark.") 461 | return 462 | if cell_type == "startrow": 463 | bookmarks[row].startrow = int(item.text()) 464 | elif cell_type == "endrow": 465 | bookmarks[row].endrow = int(item.text()) 466 | elif cell_type == "address": 467 | bookmarks[row].addr = item.text() 468 | elif cell_type == "disasm": 469 | bookmarks[row].disasm = item.text() 470 | elif cell_type == "comment": 471 | bookmarks[row].comment = item.text() 472 | else: 473 | print_debug("Unknown field edited on bookmark table...") 474 | 475 | def open_trace(self, filename): 476 | """Opens and reads a trace file""" 477 | print_debug(f"Opening trace file: {filename}") 478 | self.close_trace() 479 | self.trace_data = trace_files.open_trace(filename) 480 | if self.trace_data is None: 481 | print_debug(f"Error, couldn't open trace file: {filename}") 482 | else: 483 | if prefs.PAGINATION_ENABLED: 484 | self.trace_pagination.set_current_page(1, True) 485 | self.trace_table.get_syntax_highlighter().reset() 486 | self.trace_table.set_data(self.trace_data.trace) 487 | self.trace_table.populate() 488 | self.trace_table.selectRow(0) 489 | self.setWindowTitle(f"{filename.split('/')[-1]} - {prefs.PACKAGE_NAME}") 490 | 491 | self.update_bookmark_table() 492 | self.trace_table.update_column_widths() 493 | 494 | def close_trace(self): 495 | """Clears trace and updates UI""" 496 | self.trace_data = None 497 | self.filtered_trace = [] 498 | self.trace_table.set_data([]) 499 | self.update_ui() 500 | 501 | def update_ui(self): 502 | """Updates tables and status bar""" 503 | self.trace_table.populate() 504 | self.update_bookmark_table() 505 | self.update_status_bar() 506 | 507 | def save_trace(self): 508 | """Saves a trace file""" 509 | filename = self.trace_data.filename 510 | print_debug("Save trace: " + filename) 511 | if filename: 512 | trace_files.save_as_tv_trace(self.trace_data, filename) 513 | 514 | def show_about_dialog(self): 515 | """Shows an about dialog""" 516 | title = "About" 517 | name = prefs.PACKAGE_NAME 518 | version = prefs.PACKAGE_VERSION 519 | copyrights = prefs.PACKAGE_COPYRIGHTS 520 | url = prefs.PACKAGE_URL 521 | text = f"{name} {version} \n {copyrights} \n {url}" 522 | QMessageBox().about(self, title, text) 523 | 524 | def update_column_widths(self, table): 525 | """Updates column widths of a TableWidget to match the content""" 526 | table.setVisible(False) # fix ui glitch with column widths 527 | table.resizeColumnsToContents() 528 | table.horizontalHeader().setStretchLastSection(True) 529 | table.setVisible(True) 530 | 531 | def update_status_bar(self): 532 | """Updates status bar""" 533 | if self.trace_data is None: 534 | return 535 | table = self.trace_table 536 | row = table.currentRow() 537 | 538 | row_count = table.rowCount() 539 | row_info = f"{row}/{row_count - 1}" 540 | msg = f"Row: {row_info} " 541 | 542 | selected_row_id = 0 543 | row_ids = self.trace_table.get_selected_row_ids() 544 | if row_ids: 545 | selected_row_id = row_ids[0] 546 | 547 | msg += f" | {len(self.trace_data.trace)} rows in full trace." 548 | if len(self.filter_text) > 0: 549 | msg += f" | {len(self.filtered_trace)} rows in filtered trace." 550 | 551 | bookmark = self.trace_data.get_bookmark_from_row(selected_row_id) 552 | if bookmark: 553 | msg += f" | Bookmark: {bookmark.disasm} ; {bookmark.comment}" 554 | 555 | self.status_bar.showMessage(msg) 556 | 557 | def on_reg_checkbox_change(self, reg: str, is_checked: bool): 558 | """Callback for register checkbox change""" 559 | 560 | highlighter = self.trace_table.get_syntax_highlighter() 561 | if highlighter: 562 | highlighter.set_reg_highlight(reg, is_checked) 563 | # force repaint, update() didn't work 564 | self.trace_table.setVisible(False) 565 | self.trace_table.setVisible(True) 566 | else: 567 | print_debug("Error, highlighter not found!") 568 | 569 | def get_selected_row_ids(self, table): 570 | """Returns IDs of all selected rows of TableWidget. 571 | 572 | Args: 573 | table: PyQt TableWidget 574 | returns: 575 | list: Ordered list of row ids 576 | """ 577 | # use a set so we don't get duplicate ids 578 | row_ids_set = set( 579 | table.item(index.row(), 0).text() for index in table.selectedIndexes() 580 | ) 581 | try: 582 | row_ids_list = [int(i) for i in row_ids_set] 583 | except ValueError: 584 | print_debug("Error. Values in the first column must be integers.") 585 | return [] 586 | return sorted(row_ids_list) 587 | 588 | def on_trace_combo_box_index_changed(self, index): 589 | """Trace selection combo box index changed""" 590 | self.trace_table.set_data(self.get_visible_trace()) 591 | 592 | other_index = index ^ 1 593 | if prefs.PAGINATION_ENABLED: 594 | # save current page 595 | self.trace_current_pages[other_index] = self.trace_pagination.current_page 596 | self.trace_pagination.set_current_page( 597 | self.trace_current_pages[index], True 598 | ) 599 | 600 | # save scrollbar value 601 | current_scroll = self.trace_table.verticalScrollBar().value() 602 | self.trace_scroll_values[other_index] = current_scroll 603 | next_value = self.trace_scroll_values[index] 604 | 605 | self.trace_table.populate() 606 | QApplication.processEvents() # this is needed to update the scrollbar 607 | self.trace_table.verticalScrollBar().setValue(next_value) 608 | 609 | def go_to_row_in_visible_trace(self, row): 610 | """Goes to given row in currently visible trace""" 611 | self.trace_table.go_to_row(row) 612 | self.tab_widget.setCurrentIndex(0) 613 | 614 | def go_to_row_in_full_trace(self, row_id): 615 | """Switches to full trace and goes to given row""" 616 | # make sure we are shown full trace, not filtered 617 | if self.select_trace_combo_box.currentIndex() == 1: 618 | self.select_trace_combo_box.setCurrentIndex(0) 619 | self.go_to_row_in_visible_trace(row_id) 620 | 621 | def go_to_bookmark_in_trace(self): 622 | """Goes to trace row of selected bookmark""" 623 | selected_row_ids = self.get_selected_row_ids(self.bookmark_table) 624 | if not selected_row_ids: 625 | print_debug("Error. No bookmark selected.") 626 | return 627 | self.go_to_row_in_full_trace(selected_row_ids[0]) 628 | 629 | def clear_bookmarks(self): 630 | """Clears all bookmarks""" 631 | self.trace_data.clear_bookmarks() 632 | self.update_bookmark_table() 633 | 634 | def delete_bookmarks(self): 635 | """Deletes selected bookmarks""" 636 | selected = self.bookmark_table.selectedItems() 637 | if not selected: 638 | print_debug("Could not delete a bookmark. Nothing selected.") 639 | return 640 | selected_rows = sorted(set({sel.row() for sel in selected})) 641 | for row in reversed(selected_rows): 642 | self.trace_data.delete_bookmark(row) 643 | self.bookmark_table.removeRow(row) 644 | 645 | def get_selected_bookmarks(self): 646 | """Returns selected bookmarks""" 647 | selected = self.bookmark_table.selectedItems() 648 | if not selected: 649 | print_debug("No bookmarks selected.") 650 | return [] 651 | selected_rows = sorted(set({sel.row() for sel in selected})) 652 | all_bookmarks = self.trace_data.get_bookmarks() 653 | return [all_bookmarks[i] for i in selected_rows] 654 | 655 | def add_bookmark(self, bookmark): 656 | if prefs.ASK_FOR_BOOKMARK_COMMENT: 657 | comment = self.get_string_from_user( 658 | "Bookmark comment", "Give a comment for bookmark:" 659 | ) 660 | if comment: 661 | bookmark.comment = comment 662 | self.trace_data.add_bookmark(bookmark) 663 | self.update_bookmark_table() 664 | 665 | def update_bookmark_table(self): 666 | """Updates bookmarks table from trace_data""" 667 | if self.trace_data is None: 668 | return 669 | table = self.bookmark_table 670 | try: 671 | table.itemChanged.disconnect() 672 | except Exception: 673 | pass 674 | bookmarks = self.trace_data.get_bookmarks() 675 | table.setRowCount(len(bookmarks)) 676 | 677 | for i, bookmark in enumerate(bookmarks): 678 | startrow = QTableWidgetItem(bookmark.startrow) 679 | startrow.setData(Qt.DisplayRole, int(bookmark.startrow)) 680 | startrow.setWhatsThis("startrow") 681 | table.setItem(i, 0, startrow) 682 | 683 | endrow = QTableWidgetItem(bookmark.endrow) 684 | endrow.setData(Qt.DisplayRole, int(bookmark.endrow)) 685 | endrow.setWhatsThis("endrow") 686 | table.setItem(i, 1, endrow) 687 | 688 | address = QTableWidgetItem(bookmark.addr) 689 | address.setWhatsThis("address") 690 | table.setItem(i, 2, address) 691 | 692 | disasm = QTableWidgetItem(bookmark.disasm) 693 | disasm.setWhatsThis("disasm") 694 | table.setItem(i, 3, disasm) 695 | 696 | comment = QTableWidgetItem(bookmark.comment) 697 | comment.setWhatsThis("comment") 698 | table.setItem(i, 4, comment) 699 | 700 | table.setRowHeight(i, 14) 701 | 702 | table.itemChanged.connect(self.on_bookmark_table_cell_edited) 703 | self.update_column_widths(table) 704 | 705 | def print(self, text): 706 | """Prints text to TextEdit on log tab""" 707 | self.log_text_edit.appendPlainText(str(text)) 708 | 709 | def go_to_row(self, table, row): 710 | """Scrolls a table to the specified row""" 711 | table.scrollToItem(table.item(row, 3), QAbstractItemView.PositionAtCenter) 712 | 713 | def ask_user(self, title, question): 714 | """Shows a messagebox with yes/no question 715 | 716 | Args: 717 | title (str): MessageBox title 718 | question (str): MessageBox qustion label 719 | Returns: 720 | bool: True if user clicked yes, False otherwise 721 | """ 722 | answer = QMessageBox.question( 723 | self, 724 | title, 725 | question, 726 | QMessageBox.StandardButtons(QMessageBox.Yes | QMessageBox.No), 727 | ) 728 | return bool(answer == QMessageBox.Yes) 729 | 730 | def get_string_from_user(self, title, label): 731 | """Gets a string from user 732 | 733 | Args: 734 | title (str): Input dialog title 735 | label (str): Input dialog label 736 | Returns: 737 | string: String given by user, empty string if user clicked cancel 738 | """ 739 | answer, ok_clicked = QInputDialog.getText( 740 | self, title, label, QLineEdit.Normal, "" 741 | ) 742 | if ok_clicked: 743 | return answer 744 | return "" 745 | 746 | def get_values_from_user(self, title, data, on_ok_clicked=None): 747 | """Gets values from user 748 | 749 | Args: 750 | title (str): Input dialog title 751 | data (list): List of dicts 752 | on_ok_clicked (method): Callback function to e.g. check the input 753 | Returns: 754 | list: List of values given by user, empty list if user canceled 755 | """ 756 | input_dlg = InputDialog(self, title, data, on_ok_clicked) 757 | input_dlg.exec_() 758 | return input_dlg.get_data() 759 | 760 | def show_messagebox(self, title, msg): 761 | """Shows a messagebox""" 762 | alert = QMessageBox() 763 | alert.setWindowTitle(title) 764 | alert.setText(msg) 765 | alert.exec_() 766 | 767 | 768 | def print_debug(msg): 769 | """Prints a debug message""" 770 | if prefs.DEBUG: 771 | print(msg) 772 | -------------------------------------------------------------------------------- /gui/mainwindow.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MainWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 1114 10 | 742 11 | 12 | 13 | 14 | MainWindow 15 | 16 | 17 | 18 | true 19 | 20 | 21 | 22 | 0 23 | 0 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 0 32 | 0 33 | 34 | 35 | 36 | true 37 | 38 | 39 | 0 40 | 41 | 42 | 43 | 44 | 0 45 | 0 46 | 47 | 48 | 49 | 50 | 1048 51 | 594 52 | 53 | 54 | 55 | 56 | 16777215 57 | 16777215 58 | 59 | 60 | 61 | Trace 62 | 63 | 64 | 65 | QLayout::SetNoConstraint 66 | 67 | 68 | 69 | 70 | QLayout::SetMaximumSize 71 | 72 | 73 | 74 | 75 | 6 76 | 77 | 78 | QLayout::SetDefaultConstraint 79 | 80 | 81 | 0 82 | 83 | 84 | 0 85 | 86 | 87 | 88 | 89 | 90 | 0 91 | 0 92 | 93 | 94 | 95 | 96 | 100 97 | 24 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 0 109 | 0 110 | 111 | 112 | 113 | 114 | 0 115 | 500 116 | 117 | 118 | 119 | Qt::Horizontal 120 | 121 | 122 | 123 | 124 | 0 125 | 0 126 | 127 | 128 | 129 | 130 | 0 131 | 0 132 | 133 | 134 | 135 | 136 | Courier New 137 | 8 138 | 139 | 140 | 141 | true 142 | 143 | 144 | false 145 | 146 | 147 | QAbstractItemView::NoDragDrop 148 | 149 | 150 | false 151 | 152 | 153 | QAbstractItemView::ScrollPerPixel 154 | 155 | 156 | QAbstractItemView::ScrollPerPixel 157 | 158 | 159 | true 160 | 161 | 162 | Qt::DotLine 163 | 164 | 165 | 0 166 | 167 | 168 | 5 169 | 170 | 171 | true 172 | 173 | 174 | false 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 0 186 | 0 187 | 188 | 189 | 190 | Qt::Vertical 191 | 192 | 193 | 194 | 195 | 240 196 | 420 197 | 198 | 199 | 200 | 201 | Courier 202 | 8 203 | 204 | 205 | 206 | true 207 | 208 | 209 | QAbstractItemView::ScrollPerPixel 210 | 211 | 212 | true 213 | 214 | 215 | Qt::DotLine 216 | 217 | 218 | true 219 | 220 | 221 | false 222 | 223 | 224 | 24 225 | 226 | 227 | 22 228 | 229 | 230 | 231 | 232 | 233 | 240 234 | 0 235 | 236 | 237 | 238 | 239 | Courier 240 | 8 241 | 242 | 243 | 244 | Qt::DashLine 245 | 246 | 247 | true 248 | 249 | 250 | false 251 | 252 | 253 | 24 254 | 255 | 256 | 22 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | Log 269 | 270 | 271 | 272 | 273 | 274 | 275 | Courier 276 | 10 277 | 50 278 | false 279 | false 280 | false 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | Bookmarks 293 | 294 | 295 | 296 | 297 | 298 | 299 | 0 300 | 0 301 | 302 | 303 | 304 | 305 | 0 306 | 0 307 | 308 | 309 | 310 | 311 | 16777215 312 | 16777215 313 | 314 | 315 | 316 | 317 | Courier 318 | 8 319 | 320 | 321 | 322 | QAbstractItemView::ScrollPerPixel 323 | 324 | 325 | QAbstractItemView::ScrollPerPixel 326 | 327 | 328 | Qt::DashLine 329 | 330 | 331 | true 332 | 333 | 334 | false 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 0 348 | 0 349 | 1114 350 | 21 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | TraceTableWidget 360 | QTableWidget 361 |
gui/widgets/trace_table_widget.h
362 |
363 | 364 | RegTableWidget 365 | QTableWidget 366 |
gui/widgets/reg_table_widget.h
367 |
368 | 369 | MemTableWidget 370 | QTableWidget 371 |
gui/widgets/mem_table_widget.h
372 |
373 |
374 | 375 | 376 |
377 | -------------------------------------------------------------------------------- /gui/syntax_hl/rules/syntax_x86_dark.txt: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "startswith": [ 4 | "0x", "-0x" 5 | ], 6 | "color": "#f16a4e" 7 | }, 8 | { 9 | "words": [ 10 | "byte", "word", "dword", "qword", "ptr" 11 | ], 12 | "color": "#a25500" 13 | }, 14 | { 15 | "startswith": [ 16 | "pop", "push" 17 | ], 18 | "color": "#ff40ff" 19 | }, 20 | { 21 | "startswith": ["cmp"], 22 | "words": [ 23 | "test", "bt" 24 | ], 25 | "color": "#32c435" 26 | }, 27 | { 28 | "startswith": [ 29 | "mov", "stos" 30 | ], 31 | "words": [ 32 | "lea", "xchg" 33 | ], 34 | "color": "#c4536a" 35 | }, 36 | { 37 | "startswith": ["jmp"], 38 | "color": "black", 39 | "bgcolor": "yellow" 40 | }, 41 | { 42 | "startswith": ["j"], 43 | "color": "darkRed", 44 | "bgcolor": "yellow" 45 | }, 46 | { 47 | "words": [ 48 | "call", "ret" 49 | ], 50 | "color": "#000000", 51 | "bgcolor": "#00ccff" 52 | }, 53 | { 54 | "words": [ 55 | "adc", "dec", "inc", "sub", "sbb" 56 | ], 57 | "has": [ 58 | "add", "div", "mul" 59 | ], 60 | "color": "cyan" 61 | }, 62 | { 63 | "has": ["xor"], 64 | "words": [ 65 | "and", 66 | "or", 67 | "shl", 68 | "shld", 69 | "shr", 70 | "shrd", 71 | "sal", 72 | "sar", 73 | "rol", 74 | "ror", 75 | "rcl", 76 | "rcr", 77 | "bswap", 78 | "neg", 79 | "not", 80 | "btc", 81 | "bts" 82 | ], 83 | "color": "#ff2424" 84 | } 85 | ] 86 | -------------------------------------------------------------------------------- /gui/syntax_hl/rules/syntax_x86_light.txt: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "startswith": [ 4 | "0x", "-0x" 5 | ], 6 | "color": "#f16a4e" 7 | }, 8 | { 9 | "words": [ 10 | "byte", "word", "dword", "qword", "ptr" 11 | ], 12 | "color": "#a25500" 13 | }, 14 | { 15 | "startswith": [ 16 | "pop", "push" 17 | ], 18 | "color": "#ff3fa8" 19 | }, 20 | { 21 | "startswith": [ 22 | "mov", "stos" 23 | ], 24 | "words": [ 25 | "lea", "xchg" 26 | ], 27 | "color": "#8e3ca5" 28 | }, 29 | { 30 | "startswith": ["cmp"], 31 | "words": [ 32 | "test", "bt" 33 | ], 34 | "color": "darkGreen" 35 | }, 36 | { 37 | "words": ["jmp"], 38 | "color": "#000000", 39 | "bgcolor": "yellow" 40 | }, 41 | { 42 | "startswith": ["j"], 43 | "color": "red", 44 | "bgcolor": "yellow" 45 | }, 46 | { 47 | "words": [ 48 | "call", "ret" 49 | ], 50 | "color": "#000000", 51 | "bgcolor": "#00ffff" 52 | }, 53 | { 54 | "words": [ 55 | "adc", "dec", "inc", "sub", "sbb" 56 | ], 57 | "has": [ 58 | "add", "div", "mul" 59 | ], 60 | "color": "darkCyan" 61 | }, 62 | { 63 | "has": ["xor"], 64 | "words": [ 65 | "and", 66 | "or", 67 | "shl", 68 | "shld", 69 | "shr", 70 | "shrd", 71 | "sal", 72 | "sar", 73 | "rol", 74 | "ror", 75 | "rcl", 76 | "rcr", 77 | "bswap", 78 | "neg", 79 | "not", 80 | "btc", 81 | "bts" 82 | ], 83 | "color": "darkRed" 84 | }, 85 | { 86 | "startswith": [ 87 | "1", "2", "3", "4", "5", "6", "7", "8", "9" 88 | ], 89 | "color": "#f16a4e" 90 | } 91 | ] 92 | -------------------------------------------------------------------------------- /gui/syntax_hl/rules/value_dark.txt: -------------------------------------------------------------------------------- 1 | [ 2 | {"startswith": ["0x", "-0x"], "color": "#ff6a4e"} 3 | ] -------------------------------------------------------------------------------- /gui/syntax_hl/rules/value_light.txt: -------------------------------------------------------------------------------- 1 | [ 2 | {"startswith": ["0x", "-0x"], "color": "#ff6a4e"} 3 | ] -------------------------------------------------------------------------------- /gui/syntax_hl/syntax_hl_delegate.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from PyQt5.QtWidgets import ( 4 | QStyledItemDelegate, 5 | QStyleOptionViewItem, 6 | QApplication, 7 | QStyle, 8 | ) 9 | from PyQt5.QtGui import ( 10 | QColor, 11 | QTextDocument, 12 | QTextCursor, 13 | QTextCharFormat, 14 | QPalette, 15 | QAbstractTextDocumentLayout, 16 | QFont, 17 | ) 18 | 19 | from core import prefs 20 | 21 | 22 | class SyntaxHighlightDelegate(QStyledItemDelegate): 23 | def __init__(self, parent=None): 24 | super(SyntaxHighlightDelegate, self).__init__(parent) 25 | 26 | self.doc = QTextDocument(self) 27 | 28 | self.disasm_columns = [] 29 | self.value_columns = [] 30 | 31 | self.highlighted_regs = {} 32 | self.reg_hl_color = prefs.REG_HL_COLOR 33 | self.reg_hl_bg_colors = prefs.REG_HL_BG_COLORS 34 | 35 | self.ignored_chars = (" ", ",", "+", "[", "]") 36 | 37 | self.disasm_rules = self.load_rules_file(prefs.DISASM_RULES_FILE) 38 | self.value_rules = self.load_rules_file(prefs.VALUE_RULES_FILE) 39 | 40 | def load_rules_file(self, filename: str) -> list: 41 | """Loads syntax highlighting rules from json file""" 42 | with open(filename) as f: 43 | return json.load(f) 44 | 45 | def reset(self): 46 | """Resets highlighter""" 47 | self.highlighted_regs = {} 48 | 49 | def paint(self, painter, option, index): 50 | painter.save() 51 | 52 | options = QStyleOptionViewItem(option) 53 | self.initStyleOption(options, index) 54 | 55 | self.doc.setPlainText(options.text) 56 | 57 | column = index.column() 58 | if column in self.disasm_columns: 59 | options.font.setWeight(QFont.Bold) 60 | self.highlight(self.doc, self.disasm_rules) 61 | elif column in self.value_columns: 62 | options.font.setWeight(QFont.Bold) 63 | self.highlight(self.doc, self.value_rules) 64 | 65 | self.doc.setDefaultFont(options.font) 66 | 67 | options.text = "" 68 | 69 | style = ( 70 | QApplication.style() if options.widget is None else options.widget.style() 71 | ) 72 | style.drawControl(QStyle.CE_ItemViewItem, options, painter) 73 | 74 | ctx = QAbstractTextDocumentLayout.PaintContext() 75 | if option.state & QStyle.State_Selected: 76 | ctx.palette.setColor( 77 | QPalette.Text, 78 | option.palette.color(QPalette.Active, QPalette.HighlightedText), 79 | ) 80 | else: 81 | ctx.palette.setColor( 82 | QPalette.Text, option.palette.color(QPalette.Active, QPalette.Text), 83 | ) 84 | 85 | textRect = style.subElementRect(QStyle.SE_ItemViewItemText, options) 86 | 87 | if index.column() != 0: 88 | textRect.adjust(5, 0, 0, 0) 89 | 90 | the_constant = 4 91 | margin = (option.rect.height() - options.fontMetrics.height()) // 2 92 | margin = margin - the_constant 93 | textRect.setTop(textRect.top() + margin) 94 | 95 | painter.translate(textRect.topLeft()) 96 | painter.setClipRect(textRect.translated(-textRect.topLeft())) 97 | self.doc.documentLayout().draw(painter, ctx) 98 | 99 | painter.restore() 100 | 101 | def set_reg_highlight(self, reg: str, enabled: bool): 102 | """Enables or disables register highlight""" 103 | regs_hl = prefs.HL_REGS_X86 104 | words = regs_hl.get(reg, [reg]) 105 | 106 | if enabled: 107 | self.highlighted_regs[reg] = words 108 | elif reg in self.highlighted_regs: 109 | del self.highlighted_regs[reg] 110 | 111 | def highlight(self, document: QTextDocument, rules: list): 112 | """Highlights document""" 113 | char_format = QTextCharFormat() 114 | cursor = QTextCursor(document) 115 | 116 | while not cursor.isNull() and not cursor.atEnd(): 117 | cursor.movePosition(QTextCursor.EndOfWord, QTextCursor.KeepAnchor) 118 | 119 | text = cursor.selectedText() 120 | color, bgcolor = self.get_register_hl_color(text, self.highlighted_regs) 121 | 122 | if not color: 123 | color, bgcolor = self.get_color(text, rules) 124 | 125 | if color: 126 | char_format.setForeground(QColor(color)) 127 | 128 | if bgcolor: 129 | char_format.setBackground(QColor(bgcolor)) 130 | 131 | if color or bgcolor: 132 | cursor.mergeCharFormat(char_format) 133 | char_format.clearBackground() 134 | 135 | self.move_to_next_word(document, cursor) 136 | 137 | def move_to_next_word(self, doc: QTextDocument, cursor: QTextCursor): 138 | """Moves cursor to next word""" 139 | while not cursor.isNull() and not cursor.atEnd(): 140 | if doc.characterAt(cursor.position()) not in self.ignored_chars: 141 | return 142 | cursor.movePosition(QTextCursor.NextCharacter) 143 | 144 | def get_register_hl_color(self, word_to_check: str, regs_hl: dict) -> tuple: 145 | """Gets color and bgcolor if given word is found in regs_hl""" 146 | color_index = 0 147 | 148 | for words in regs_hl.values(): 149 | if word_to_check in words: 150 | if color_index < len(self.reg_hl_bg_colors): 151 | bg_color = self.reg_hl_bg_colors[color_index] 152 | else: 153 | bg_color = self.reg_hl_bg_colors[-1] 154 | return (self.reg_hl_color, bg_color) 155 | color_index += 1 156 | 157 | return ("", "") 158 | 159 | def get_color(self, word_to_check: str, rules: dict) -> tuple: 160 | """Gets color and bgcolor if given word is found in rules""" 161 | for rule in rules: 162 | if "words" in rule: 163 | for word in rule["words"]: 164 | if word == word_to_check: 165 | return (rule["color"], rule.get("bgcolor", "")) 166 | if "startswith" in rule: 167 | for sw in rule["startswith"]: 168 | if word_to_check.startswith(sw): 169 | return (rule["color"], rule.get("bgcolor", "")) 170 | if "has" in rule: 171 | for has in rule["has"]: 172 | if has in word_to_check: 173 | return (rule["color"], rule.get("bgcolor", "")) 174 | 175 | return ("", "") 176 | -------------------------------------------------------------------------------- /gui/syntax_hl/syntax_hl_log.py: -------------------------------------------------------------------------------- 1 | """ 2 | This syntax highlighter is modified from the following code: 3 | https://wiki.python.org/moin/PyQt/Python%20syntax%20highlighting 4 | 5 | License: https://directory.fsf.org/wiki/License:BSD-3-Clause 6 | """ 7 | 8 | from PyQt5.QtCore import QRegExp 9 | from PyQt5.QtGui import QColor, QTextCharFormat, QFont, QSyntaxHighlighter 10 | 11 | 12 | def format(color, bgcolor="", style=""): 13 | """Return a QTextCharFormat with the given attributes.""" 14 | _color = QColor() 15 | _color.setNamedColor(color) 16 | _format = QTextCharFormat() 17 | _format.setForeground(_color) 18 | 19 | if bgcolor: 20 | _format.setBackground(QColor(bgcolor)) 21 | 22 | if "bold" in style: 23 | _format.setFontWeight(QFont.Bold) 24 | if "italic" in style: 25 | _format.setFontItalic(True) 26 | return _format 27 | 28 | 29 | # Syntax styles 30 | STYLES = { 31 | "instr_general": format("#8e3ca5"), 32 | "instr_cmp": format("green"), 33 | "instr_call": format("#000000", bgcolor="#00ffff"), 34 | "instr_branch": format("black", bgcolor="yellow"), 35 | "instr_cond_branch": format("red", bgcolor="yellow", style="italic"), 36 | "instr_arith": format("darkCyan"), 37 | "instr_vm": format("red", style="bold"), 38 | "keywords_vm": format("darkMagenta"), 39 | "instr_stack": format("#ff3fa8"), 40 | "instr_bitwise": format("red"), 41 | "operator": format("red"), 42 | "brace": format("darkMagenta"), 43 | "comment": format("darkGreen", style="italic"), 44 | "numbers": format("brown"), 45 | } 46 | 47 | 48 | class AsmHighlighter(QSyntaxHighlighter): 49 | """Syntax highlighter for the x86 ASM language.""" 50 | 51 | instr_general = ["mov", "movzx", "movsx", "movsx", "lea"] 52 | instr_cmp = ["cmp", "test"] 53 | instr_call = ["call", "ret"] 54 | instr_branch = ["jmp"] 55 | instr_cond_branch = [ 56 | "jne", 57 | "jnz", 58 | "je", 59 | "jz", 60 | "jg", 61 | "jnle", 62 | "jle", 63 | "jng", 64 | "jge", 65 | "jnl", 66 | "jl", 67 | "jnge", 68 | "ja", 69 | "jnbe", 70 | "jbe", 71 | "jna", 72 | "jnb", 73 | "jae", 74 | "jnc", 75 | "jb", 76 | "jnae", 77 | "jc", 78 | "jns", 79 | "js", 80 | ] 81 | instr_arith = ["add", "sub", "dec", "inc", "mul"] 82 | instr_vm = ["nor", "load", "exit", "enter", "init"] 83 | instr_stack = ["push", "pushad", "pushfd", "pushal", "pop", "popfd"] 84 | instr_bitwise = [ 85 | "xor", 86 | "and", 87 | "or", 88 | "shl", 89 | "shr", 90 | "bswap", 91 | "rol", 92 | "ror", 93 | "neg", 94 | "not", 95 | "btc", 96 | "bts", 97 | ] 98 | 99 | # Operators 100 | operators = [ 101 | "=", 102 | # Comparison 103 | "==", 104 | "!=", 105 | "<", 106 | "<=", 107 | ">", 108 | ">=", 109 | # Arithmetic 110 | "\+", 111 | "-", 112 | "\*", 113 | "/", 114 | "//", 115 | "\%", 116 | "\*\*", 117 | # In-place 118 | "\+=", 119 | "-=", 120 | "\*=", 121 | "/=", 122 | "\%=", 123 | # Bitwise 124 | "\^", 125 | "\|", 126 | "\&", 127 | "\~", 128 | ">>", 129 | "<<", 130 | ] 131 | 132 | # Braces 133 | braces = ["\[", "\]"] 134 | 135 | def __init__(self, document): 136 | QSyntaxHighlighter.__init__(self, document) 137 | 138 | rules = [] 139 | 140 | # Keyword, operator, and brace rules 141 | rules += [ 142 | (r"\b%s\b" % w, 0, STYLES["instr_general"]) 143 | for w in AsmHighlighter.instr_general 144 | ] 145 | 146 | rules += [ 147 | (r"\b%s\b" % w, 0, STYLES["instr_cmp"]) for w in AsmHighlighter.instr_cmp 148 | ] 149 | 150 | rules += [ 151 | (r"\b%s\b" % w, 0, STYLES["instr_call"]) for w in AsmHighlighter.instr_call 152 | ] 153 | 154 | rules += [ 155 | (r"\b%s\b" % w, 0, STYLES["instr_branch"]) 156 | for w in AsmHighlighter.instr_branch 157 | ] 158 | rules += [ 159 | (r"\b%s\b" % w, 0, STYLES["instr_cond_branch"]) 160 | for w in AsmHighlighter.instr_cond_branch 161 | ] 162 | 163 | rules += [ 164 | (r"\b%s\b" % w, 0, STYLES["instr_arith"]) 165 | for w in AsmHighlighter.instr_arith 166 | ] 167 | 168 | rules += [ 169 | (r"\b%s\b" % w, 0, STYLES["instr_vm"]) for w in AsmHighlighter.instr_vm 170 | ] 171 | 172 | rules += [ 173 | (r"\b%s\b" % w, 0, STYLES["instr_stack"]) 174 | for w in AsmHighlighter.instr_stack 175 | ] 176 | 177 | rules += [ 178 | (r"\b%s\b" % w, 0, STYLES["instr_bitwise"]) 179 | for w in AsmHighlighter.instr_bitwise 180 | ] 181 | 182 | rules += [(r"%s" % o, 0, STYLES["operator"]) for o in AsmHighlighter.operators] 183 | 184 | rules += [(r"%s" % b, 0, STYLES["brace"]) for b in AsmHighlighter.braces] 185 | 186 | # All other rules 187 | rules += [ 188 | # Numeric literals 189 | (r"\b[+-]?[0-9]+[lL]?\b", 0, STYLES["numbers"]), 190 | (r"\b[+-]?0[xX][0-9A-Fa-f]+[lL]?\b", 0, STYLES["numbers"]), 191 | (r"\b[+-]?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?\b", 0, STYLES["numbers"]), 192 | # From 'vm_' until a space or a comma 193 | (r"vm_[^ ,]*", 0, STYLES["keywords_vm"]), 194 | # From 'j' until a space or a comma 195 | # (r"j[^ ,]*", 0, STYLES["instr_cond_branch"]), 196 | # From '#' until a newline 197 | (r"#[^\n]*", 0, STYLES["comment"]), 198 | # From ';' until a newline 199 | (r";[^\n]*", 0, STYLES["comment"]), 200 | ] 201 | 202 | # Build a QRegExp for each pattern 203 | self.rules = [(QRegExp(pat), index, fmt) for (pat, index, fmt) in rules] 204 | 205 | def highlightBlock(self, text): 206 | """Apply syntax highlighting to the given block of text.""" 207 | # Do other syntax formatting 208 | for expression, nth, format in self.rules: 209 | index = expression.indexIn(text, 0) 210 | 211 | while index >= 0: 212 | # We actually want the index of the nth match 213 | index = expression.pos(nth) 214 | length = len(expression.cap(nth)) 215 | self.setFormat(index, length, format) 216 | index = expression.indexIn(text, index + length) 217 | 218 | self.setCurrentBlockState(0) 219 | -------------------------------------------------------------------------------- /gui/widgets/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teemu-l/execution-trace-viewer/d2d99e92decb3526efc1f5bd4bae350aa4c7c0d2/gui/widgets/__init__.py -------------------------------------------------------------------------------- /gui/widgets/filter_widget.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtCore import Qt, pyqtSignal 2 | from PyQt5.QtWidgets import ( 3 | QWidget, QHBoxLayout, QPushButton, QComboBox, QSizePolicy 4 | ) 5 | 6 | 7 | class FilterWidget(QWidget): 8 | 9 | filterBtnClicked = pyqtSignal(str) 10 | 11 | def __init__(self, parent=None): 12 | super(FilterWidget, self).__init__(parent) 13 | self.init_ui() 14 | 15 | def init_ui(self): 16 | 17 | layout = QHBoxLayout(self) 18 | 19 | self.filter_combo_box = QComboBox() 20 | self.filter_combo_box.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum) 21 | self.filter_combo_box.setEditable(True) 22 | self.filter_combo_box.setMaxVisibleItems(25) 23 | self.filter_combo_box.setMaximumSize(440, 24) 24 | self.filter_combo_box.setMinimumSize(220, 24) 25 | self.filter_combo_box.keyPressEvent = self.on_filter_combo_box_key_pressed 26 | layout.addWidget(self.filter_combo_box) 27 | 28 | self.filter_btn = QPushButton("Filter", self) 29 | self.filter_btn.clicked.connect(self.on_filter_btn_clicked) 30 | self.filter_btn.setMinimumSize(40, 24) 31 | self.filter_btn.setMaximumSize(40, 24) 32 | layout.addWidget(self.filter_btn) 33 | 34 | self.setMaximumSize(500, 40) 35 | 36 | def set_sample_filters(self, filters): 37 | for f in filters: 38 | self.filter_combo_box.addItem(f) 39 | 40 | def add_sample_filter(self, sample_filter): 41 | self.filter_combo_box.addItem(sample_filter) 42 | 43 | def on_filter_btn_clicked(self): 44 | self.filterBtnClicked.emit(self.filter_combo_box.currentText()) 45 | 46 | def on_filter_combo_box_key_pressed(self, event): 47 | """Checks if enter is pressed on filterEdit""" 48 | key = event.key() 49 | if key in (Qt.Key_Return, Qt.Key_Enter): 50 | self.on_filter_btn_clicked() 51 | QComboBox.keyPressEvent(self.filter_combo_box, event) 52 | -------------------------------------------------------------------------------- /gui/widgets/find_widget.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtWidgets import ( 2 | QWidget, 3 | QHBoxLayout, 4 | QLabel, 5 | QToolButton, 6 | QLineEdit, 7 | QComboBox, 8 | ) 9 | from PyQt5.QtCore import Qt, pyqtSignal 10 | 11 | 12 | class FindWidget(QWidget): 13 | 14 | findBtnClicked = pyqtSignal(str, int, int) 15 | 16 | def __init__(self, parent=None): 17 | super(FindWidget, self).__init__(parent) 18 | self.last_direction = 1 19 | self.init_ui() 20 | 21 | def init_ui(self): 22 | 23 | layout = QHBoxLayout(self) 24 | 25 | self.find_label = QLabel("Find:") 26 | self.find_label.setMaximumSize(35, 24) 27 | layout.addWidget(self.find_label) 28 | 29 | self.find_combo_box = QComboBox() 30 | self.find_combo_box.setMaximumSize(105, 24) 31 | layout.addWidget(self.find_combo_box) 32 | 33 | self.find_edit = QLineEdit() 34 | self.find_edit.setMaximumSize(140, 24) 35 | self.find_edit.returnPressed.connect( 36 | lambda: self.on_find_btn_clicked(self.last_direction) 37 | ) 38 | layout.addWidget(self.find_edit) 39 | 40 | self.prev_btn = QToolButton(self) 41 | self.prev_btn.clicked.connect(lambda: self.on_find_btn_clicked(-1)) 42 | self.prev_btn.setArrowType(Qt.UpArrow) 43 | self.prev_btn.setToolTip("Find previous") 44 | layout.addWidget(self.prev_btn) 45 | 46 | self.next_btn = QToolButton(self) 47 | self.next_btn.clicked.connect(lambda: self.on_find_btn_clicked(1)) 48 | self.next_btn.setArrowType(Qt.DownArrow) 49 | self.next_btn.setToolTip("Find next") 50 | layout.addWidget(self.next_btn) 51 | 52 | layout.setAlignment(Qt.AlignLeft) 53 | 54 | def set_fields(self, fields): 55 | for field in fields: 56 | self.find_combo_box.addItem(field) 57 | 58 | def add_field(self, field): 59 | self.find_combo_box.addItem(field) 60 | 61 | def on_find_btn_clicked(self, direction): 62 | """Find next or prev button clicked""" 63 | self.last_direction = direction 64 | field_index = self.find_combo_box.currentIndex() 65 | keyword = self.find_edit.text() 66 | self.findBtnClicked.emit(keyword, field_index, direction) 67 | -------------------------------------------------------------------------------- /gui/widgets/mem_table_widget.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtWidgets import QTableWidget, QTableWidgetItem 2 | 3 | 4 | class MemTableWidget(QTableWidget): 5 | def __init__(self, parent=None): 6 | super(MemTableWidget, self).__init__(parent) 7 | self.mem_data = [] 8 | 9 | def set_data(self, data): 10 | """Sets table data and updates it""" 11 | self.mem_data = data 12 | self.populate() 13 | 14 | def populate(self): 15 | """Fills table with data""" 16 | if self.mem_data is None or not self.mem_data: 17 | self.setRowCount(0) 18 | else: 19 | self.setRowCount(len(self.mem_data)) 20 | for i, mem in enumerate(self.mem_data): 21 | self.setItem(i, 0, QTableWidgetItem(mem["access"])) 22 | self.setItem(i, 1, QTableWidgetItem(hex(mem["addr"]))) 23 | self.setItem(i, 2, QTableWidgetItem(hex(mem["value"]))) 24 | self.update_column_widths() 25 | 26 | def update_column_widths(self): 27 | """Updates column widths of a TableWidget to match the content""" 28 | self.setVisible(False) # fix ui glitch with column widths 29 | self.resizeColumnsToContents() 30 | self.horizontalHeader().setStretchLastSection(True) 31 | self.setVisible(True) 32 | -------------------------------------------------------------------------------- /gui/widgets/pagination_widget.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtCore import pyqtSignal, QSize, Qt 2 | from PyQt5.QtWidgets import QWidget, QHBoxLayout, QToolButton, QLineEdit, QLabel 3 | 4 | 5 | class PaginationWidget(QWidget): 6 | 7 | pageChanged = pyqtSignal(int) 8 | 9 | def __init__(self, parent=None): 10 | QWidget.__init__(self, parent=parent) 11 | self.rows_per_page = 1000 12 | self.page_count = 1 13 | self.current_page = 1 14 | self.init_ui() 15 | 16 | def init_ui(self): 17 | layout = QHBoxLayout(self) 18 | 19 | self.first_page_btn = QToolButton(self) 20 | self.first_page_btn.clicked.connect(self._on_first_page_btn_clicked) 21 | self.first_page_btn.setArrowType(Qt.LeftArrow) 22 | self.first_page_btn.setToolTip("First page") 23 | layout.addWidget(self.first_page_btn) 24 | 25 | self.prev_page_btn = QToolButton(self) 26 | self.prev_page_btn.clicked.connect(self._on_prev_page_btn_clicked) 27 | self.prev_page_btn.setArrowType(Qt.LeftArrow) 28 | self.prev_page_btn.setToolTip("Previous page") 29 | layout.addWidget(self.prev_page_btn) 30 | 31 | self.page_edit = QLineEdit("1") 32 | self.page_edit.setMaximumSize(45, 24) 33 | self.page_edit.returnPressed.connect( 34 | lambda: self.set_current_page(self.page_edit.text()) 35 | ) 36 | layout.addWidget(self.page_edit) 37 | 38 | self.next_page_btn = QToolButton(self) 39 | self.next_page_btn.clicked.connect(self._on_next_page_btn_clicked) 40 | self.next_page_btn.setArrowType(Qt.RightArrow) 41 | self.next_page_btn.setToolTip("Next page") 42 | layout.addWidget(self.next_page_btn) 43 | 44 | self.last_page_btn = QToolButton(self) 45 | self.last_page_btn.clicked.connect(self._on_last_page_btn_clicked) 46 | self.last_page_btn.setArrowType(Qt.RightArrow) 47 | self.last_page_btn.setToolTip("Last page") 48 | layout.addWidget(self.last_page_btn) 49 | 50 | self.status_label = QLabel("") 51 | layout.addWidget(self.status_label) 52 | 53 | def set_enabled(self, enabled): 54 | self.prev_page_btn.setEnabled(enabled) 55 | self.next_page_btn.setEnabled(enabled) 56 | self.last_page_btn.setEnabled(enabled) 57 | self.first_page_btn.setEnabled(enabled) 58 | self.status_label.setEnabled(enabled) 59 | self.page_edit.setEnabled(enabled) 60 | 61 | def update_status_text(self): 62 | status = f"Page: {self.current_page} / {self.page_count}" 63 | self.status_label.setText(status) 64 | 65 | def set_status_text(self, text): 66 | self.status_label.setText(text) 67 | 68 | def set_page_count(self, page_count): 69 | # if self.current_page > page_count: 70 | # self.set_current_page(page_count) 71 | self.page_count = page_count 72 | self.update_status_text() 73 | 74 | def set_current_page(self, page, block_signals=False): 75 | try: 76 | if not isinstance(page, int): 77 | page = int(page) 78 | except ValueError: 79 | print(f"Exception on set_current_page: page must be integer.") 80 | return 81 | 82 | if page < 1: 83 | page = 1 84 | if page > self.page_count: 85 | page = self.page_count 86 | 87 | self.current_page = page 88 | self.page_edit.setText(str(page)) 89 | self.update_status_text() 90 | if not block_signals: 91 | self.pageChanged.emit(page) 92 | 93 | def _on_prev_page_btn_clicked(self): 94 | self.set_current_page(self.current_page - 1) 95 | 96 | def _on_next_page_btn_clicked(self): 97 | self.set_current_page(self.current_page + 1) 98 | 99 | def _on_first_page_btn_clicked(self): 100 | self.set_current_page(1) 101 | 102 | def _on_last_page_btn_clicked(self): 103 | self.set_current_page(self.page_count) 104 | -------------------------------------------------------------------------------- /gui/widgets/reg_table_widget.py: -------------------------------------------------------------------------------- 1 | import string 2 | 3 | from PyQt5.QtWidgets import QTableWidget, QTableWidgetItem, QMenu, QAction 4 | from PyQt5.QtCore import Qt, pyqtSignal 5 | from PyQt5.QtGui import QColor, QCursor 6 | 7 | from core import prefs 8 | 9 | 10 | class RegTableWidget(QTableWidget): 11 | 12 | regCheckBoxChanged = pyqtSignal(str, int) 13 | 14 | def __init__(self, parent=None): 15 | super(RegTableWidget, self).__init__(parent) 16 | self.printer = print 17 | self.regs = {} 18 | self.modified_regs = [] 19 | self.modified_regs_ignore = ["eip", "rip"] 20 | self.filtered_regs = [] 21 | self.checked_regs = {} 22 | self.menu = None 23 | if prefs.USE_DARK_THEME: 24 | self.hl_color = QColor("darkRed") 25 | else: 26 | self.hl_color = QColor("#fcabab") 27 | 28 | def create_context_menu(self): 29 | """Initializes context menu for mouse right click""" 30 | self.setContextMenuPolicy(Qt.CustomContextMenu) 31 | self.customContextMenuRequested.connect(self.on_custom_context_menu_requested) 32 | 33 | self.menu = QMenu(self) 34 | print_action = QAction("Print selected cells", self) 35 | print_action.triggered.connect(self.print_selected_cells) 36 | self.menu.addAction(print_action) 37 | 38 | def onCellChanged(self, row, col): 39 | if col > 0: 40 | return 41 | item = self.item(row, 0) 42 | state = item.checkState() 43 | state_bool = state == Qt.Checked 44 | reg_name = item.text() 45 | 46 | self.checked_regs[reg_name] = state 47 | self.regCheckBoxChanged.emit(reg_name, state_bool) 48 | 49 | def on_custom_context_menu_requested(self): 50 | """Context menu callback for mouse right click""" 51 | if self.menu is not None: 52 | self.menu.popup(QCursor.pos()) 53 | 54 | def set_data(self, regs, modified_regs): 55 | """Sets table data and populates the table""" 56 | if self.filtered_regs: 57 | temp_regs = {} 58 | for reg, value in regs.items(): 59 | if reg in self.filtered_regs: 60 | temp_regs[reg] = value 61 | regs = temp_regs 62 | self.regs = regs 63 | self.modified_regs = modified_regs 64 | self.populate() 65 | 66 | def populate(self): 67 | """Populates the register table""" 68 | try: 69 | self.cellChanged.disconnect() 70 | except Exception: 71 | pass 72 | 73 | if self.rowCount() != len(self.regs): 74 | self.setRowCount(len(self.regs)) 75 | if not self.regs: 76 | return 77 | 78 | i = 0 79 | for reg, value in self.regs.items(): 80 | if self.filtered_regs and reg not in self.filtered_regs: 81 | continue 82 | regname_item = QTableWidgetItem(reg) 83 | regname_item.setFlags( 84 | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsSelectable 85 | ) 86 | check_state = self.checked_regs.get(reg, Qt.Unchecked) 87 | regname_item.setCheckState(check_state) 88 | self.setItem(i, 0, regname_item) 89 | 90 | if isinstance(value, int): 91 | hex_str = hex(value) 92 | if 0 < value < 255 and chr(value) in string.printable: 93 | hex_str += f" '{chr(value)}'" 94 | 95 | hex_item = QTableWidgetItem(hex_str) 96 | 97 | hex_item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable) 98 | dec_item = QTableWidgetItem(str(value)) 99 | dec_item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable) 100 | self.setItem(i, 1, hex_item) 101 | self.setItem(i, 2, dec_item) 102 | else: 103 | self.setItem(i, 1, QTableWidgetItem(value)) 104 | 105 | if reg in self.modified_regs and reg not in self.modified_regs_ignore: 106 | self.item(i, 0).setBackground(self.hl_color) 107 | self.item(i, 1).setBackground(self.hl_color) 108 | self.item(i, 2).setBackground(self.hl_color) 109 | i += 1 110 | 111 | if "eflags" in self.regs: 112 | eflags = self.regs["eflags"] 113 | flags = { 114 | "c": eflags & 1, # carry 115 | "p": (eflags >> 2) & 1, # parity 116 | # "a": (eflags >> 4) & 1, # aux_carry 117 | "z": (eflags >> 6) & 1, # zero 118 | "s": (eflags >> 7) & 1, # sign 119 | # "d": (eflags >> 10) & 1, # direction 120 | # "o": (eflags >> 11) & 1 # overflow 121 | } 122 | flags_text = f"C:{flags['c']} P:{flags['p']} Z:{flags['z']} S:{flags['s']}" 123 | self.setRowCount(i + 1) 124 | self.setItem(i, 0, QTableWidgetItem("flags")) 125 | self.setItem(i, 1, QTableWidgetItem(flags_text)) 126 | 127 | self.cellChanged.connect(self.onCellChanged) 128 | 129 | def print(self, msg: str): 130 | if self.printer: 131 | self.printer(msg) 132 | else: 133 | print(msg) 134 | 135 | def print_selected_cells(self): 136 | """Prints selected cells""" 137 | items = self.selectedItems() 138 | 139 | if len(items) < 1: 140 | return 141 | 142 | rows = {} 143 | for item in items: 144 | row = item.row() 145 | if row not in rows: 146 | rows[row] = [item.text()] 147 | else: 148 | rows[row].append(item.text()) 149 | 150 | for row in rows.values(): 151 | self.print(" ".join(row)) 152 | -------------------------------------------------------------------------------- /gui/widgets/trace_table_widget.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtWidgets import QTableWidget, QTableWidgetItem, QAbstractItemView 2 | from PyQt5.QtCore import pyqtSignal, Qt, QItemSelectionModel 3 | from PyQt5.QtGui import QCursor 4 | 5 | from core import prefs 6 | from core.bookmark import Bookmark 7 | from gui.syntax_hl.syntax_hl_delegate import SyntaxHighlightDelegate 8 | 9 | 10 | class TraceTableWidget(QTableWidget): 11 | 12 | bookmarkCreated = pyqtSignal(Bookmark) 13 | commentEdited = pyqtSignal(int, str) 14 | 15 | def __init__(self, parent=None): 16 | super(TraceTableWidget, self).__init__(parent) 17 | self.printer = self.print_debug 18 | self.trace = [] 19 | self.pagination = None 20 | self.menu = None 21 | self.row_height = 20 22 | self.delegate = None 23 | self.init_ui() 24 | 25 | def init_ui(self): 26 | self.setContextMenuPolicy(Qt.CustomContextMenu) 27 | self.customContextMenuRequested.connect(self.custom_context_menu_requested) 28 | self.itemChanged.connect(self.item_changed) 29 | 30 | def init_syntax_highlight(self): 31 | """Inits syntax higlighter delegate""" 32 | self.delegate = SyntaxHighlightDelegate(self) 33 | self.delegate.disasm_columns = prefs.TRACE_HL_DISASM_COLUMNS 34 | self.delegate.value_columns = prefs.TRACE_HL_VALUE_COLUMNS 35 | self.setItemDelegate(self.delegate) 36 | 37 | def get_syntax_highlighter(self): 38 | """Returns syntax higlighter delegate""" 39 | return self.delegate 40 | 41 | def create_bookmark(self): 42 | """Create a bookmark from selected rows""" 43 | selected_rows = self.selectedItems() 44 | if not selected_rows: 45 | self.print_debug("Could not create a bookmark. Nothing selected.") 46 | return 47 | 48 | addr = self.item(selected_rows[0].row(), 1).text() 49 | disasm = self.item(selected_rows[0].row(), 3).text() 50 | comment = self.item(selected_rows[0].row(), 5).text() 51 | 52 | selected_row_ids = self.get_selected_row_ids() 53 | first_row_id = selected_row_ids[0] 54 | last_row_id = selected_row_ids[-1] 55 | bookmark = Bookmark( 56 | startrow=first_row_id, 57 | endrow=last_row_id, 58 | addr=addr, 59 | disasm=disasm, 60 | comment=comment, 61 | ) 62 | self.bookmarkCreated.emit(bookmark) 63 | 64 | def custom_context_menu_requested(self): 65 | """Context menu for mouse right click""" 66 | if self.menu is not None: 67 | self.menu.popup(QCursor.pos()) 68 | 69 | def get_selected_row_ids(self): 70 | """Returns IDs of all selected rows. 71 | 72 | returns: 73 | list: Sorted list of row ids 74 | """ 75 | # use a set so we don't get duplicate ids 76 | row_ids_set = set( 77 | self.item(index.row(), 0).text() for index in self.selectedIndexes() 78 | ) 79 | try: 80 | row_ids_list = [int(i) for i in row_ids_set] 81 | except ValueError: 82 | self.print_debug("Error. Values in the first column must be integers.") 83 | return None 84 | return sorted(row_ids_list) 85 | 86 | def go_to_row(self, row: int): 87 | if self.pagination is not None: 88 | page = int(row / self.pagination.rows_per_page) + 1 89 | row = row % self.pagination.rows_per_page 90 | if page != self.pagination.current_page: 91 | self.pagination.set_current_page(page) 92 | self.scrollToItem(self.item(row, 3), QAbstractItemView.PositionAtCenter) 93 | self.select_row(row) 94 | 95 | def item_changed(self, item: QTableWidgetItem): 96 | """Called when user edits a cell""" 97 | cell_type = item.whatsThis() 98 | if cell_type == "comment": 99 | row = self.currentRow() 100 | if row < 0: 101 | self.print_debug("Error, could not edit trace.") 102 | return 103 | row_id = int(self.item(row, 0).text()) 104 | self.commentEdited.emit(row_id, item.text()) 105 | else: 106 | self.print_debug("Only comment editing allowed for now...") 107 | 108 | def print(self, msg: str): 109 | if self.printer: 110 | self.printer(msg) 111 | else: 112 | print(msg) 113 | 114 | def print_debug(self, msg: str): 115 | print(msg) 116 | 117 | def print_selected_cells(self): 118 | """Prints selected cells""" 119 | items = self.selectedItems() 120 | 121 | if len(items) < 1: 122 | return 123 | 124 | paddings = [6, 12, 16, 40, 40, 0] 125 | rows = {} 126 | for item in items: 127 | row = item.row() 128 | item_text = item.text().ljust(paddings[item.column()], " ") 129 | 130 | if item.whatsThis() == "comment" and item.text(): 131 | item_text = f"; {item_text}" 132 | 133 | if row not in rows: 134 | rows[row] = [item_text] 135 | else: 136 | rows[row].append(item_text) 137 | 138 | for row in rows.values(): 139 | self.print(" ".join(row).strip()) 140 | 141 | def select_row(self, row: int): 142 | """Selects a row in a table""" 143 | self.clearSelection() 144 | item = self.item(row, 0) 145 | self.setCurrentItem( 146 | item, 147 | QItemSelectionModel.Select 148 | | QItemSelectionModel.Rows 149 | | QItemSelectionModel.Current, 150 | ) 151 | 152 | def set_data(self, data: list): 153 | """Sets table data""" 154 | self.trace = data 155 | if self.pagination is not None: 156 | self.update_pagination() 157 | 158 | def set_row_height(self, height: int): 159 | """Sets table row height""" 160 | self.row_height = height 161 | 162 | def populate(self): 163 | """Populates the table with trace data""" 164 | try: 165 | self.itemChanged.disconnect() 166 | except Exception: 167 | pass 168 | 169 | trace = self.trace 170 | if trace is None or not trace: 171 | self.setRowCount(0) 172 | return 173 | 174 | if self.pagination is not None: 175 | self.update_pagination() 176 | page = self.pagination.current_page 177 | per_page = self.pagination.rows_per_page 178 | trace = trace[(page - 1) * per_page : page * per_page] 179 | 180 | row_count = len(trace) 181 | self.setRowCount(row_count) 182 | if row_count == 0: 183 | return 184 | 185 | for i in range(0, row_count): 186 | row_id_item = QTableWidgetItem(str(trace[i]["id"])) 187 | row_id_item.setFlags(row_id_item.flags() & ~Qt.ItemIsEditable) 188 | 189 | address_item = QTableWidgetItem(hex(trace[i].get("ip"))) 190 | address_item.setFlags(address_item.flags() & ~Qt.ItemIsEditable) 191 | 192 | opcodes_item = QTableWidgetItem(trace[i]["opcodes"]) 193 | opcodes_item.setFlags(opcodes_item.flags() & ~Qt.ItemIsEditable) 194 | 195 | disasm_item = QTableWidgetItem(trace[i]["disasm"]) 196 | disasm_item.setFlags(disasm_item.flags() & ~Qt.ItemIsEditable) 197 | 198 | regchanges_item = QTableWidgetItem(trace[i].get("regchanges", "")) 199 | regchanges_item.setFlags(regchanges_item.flags() & ~Qt.ItemIsEditable) 200 | regchanges_item.setWhatsThis("regchanges") 201 | 202 | comment_item = QTableWidgetItem(trace[i].get("comment", "")) 203 | comment_item.setWhatsThis("comment") 204 | 205 | self.setItem(i, 0, row_id_item) 206 | self.setItem(i, 1, address_item) 207 | self.setItem(i, 2, opcodes_item) 208 | self.setItem(i, 3, disasm_item) 209 | self.setItem(i, 4, regchanges_item) 210 | self.setItem(i, 5, comment_item) 211 | 212 | self.setRowHeight(i, self.row_height) 213 | 214 | self.itemChanged.connect(self.item_changed) 215 | 216 | def update_column_widths(self): 217 | """Updates column widths of a TableWidget to match the content""" 218 | self.setVisible(False) # fix ui glitch with column widths 219 | self.resizeColumnToContents(3) 220 | self.setColumnWidth(0, 60) # make id column wider 221 | self.setColumnWidth(4, 230) # make reg column wider 222 | self.horizontalHeader().setStretchLastSection(True) 223 | self.setVisible(True) 224 | 225 | def update_pagination(self): 226 | """Update pagination widget""" 227 | if self.pagination is not None: 228 | trace_length = len(self.trace) 229 | page_count = int(trace_length / self.pagination.rows_per_page) + 1 230 | self.pagination.set_page_count(page_count) 231 | -------------------------------------------------------------------------------- /plugins/comment_mem_access.py: -------------------------------------------------------------------------------- 1 | """This plugin finds every memory access and comments the row with address and value""" 2 | 3 | from yapsy.IPlugin import IPlugin 4 | from core.api import Api 5 | 6 | class PluginCommentMemAccesses(IPlugin): 7 | 8 | def execute(self, api: Api): 9 | 10 | want_to_continue = api.ask_user( 11 | "Warning", "This plugin may replace some of your comments, continue?" 12 | ) 13 | if not want_to_continue: 14 | return 15 | 16 | trace_data = api.get_trace_data() 17 | trace = api.get_visible_trace() 18 | 19 | for i, t in enumerate(trace): 20 | if 'mem' in t and t['mem']: 21 | comment = "" 22 | for mem in t['mem']: 23 | addr = hex(mem['addr']) 24 | value = mem['value'] 25 | if mem['access'] == "READ": 26 | comment += f"[{ addr }] -> { hex(value) } " 27 | elif mem['access'] == "WRITE": 28 | comment += f"[{ addr }] <- { hex(value) } " 29 | if 0x20 <= value <= 0x7e: 30 | comment += f"'{ chr(value) }' " 31 | 32 | # Add comment to full trace 33 | row = t["id"] 34 | trace_data.trace[row]['comment'] = comment 35 | 36 | # Add comment to visible trace too because it could be filtered_trace 37 | trace[i]['comment'] = comment 38 | 39 | api.update_trace_table() 40 | -------------------------------------------------------------------------------- /plugins/comment_mem_access.yapsy-plugin: -------------------------------------------------------------------------------- 1 | [Core] 2 | Name = Comment mem accesses 3 | Module = comment_mem_access 4 | 5 | [Documentation] 6 | Author = Teemu 7 | Version = 0.1 8 | Website = https://github.com/teemu-l/execution-trace-viewer 9 | Description = Comments all memory reads and writes ([addr] -> data) -------------------------------------------------------------------------------- /plugins/exec_counts.py: -------------------------------------------------------------------------------- 1 | """This plugin prints top 30 most executed addresses""" 2 | from yapsy.IPlugin import IPlugin 3 | from operator import itemgetter 4 | from core.api import Api 5 | 6 | class PluginPrintExecCounts(IPlugin): 7 | 8 | def execute(self, api: Api): 9 | 10 | trace = api.get_visible_trace() 11 | if not trace: 12 | return 13 | 14 | api.print('') 15 | 16 | trace_data = api.get_trace_data() 17 | ip_name = trace_data.get_instruction_pointer_name() 18 | if ip_name not in trace_data.regs: 19 | api.print('Error. Unknown instruction pointer name.') 20 | return 21 | ip_index = trace_data.regs[ip_name] 22 | counts = {} 23 | for t in trace: 24 | addr = t['regs'][ip_index] 25 | if addr in counts: 26 | counts[addr] += 1 27 | else: 28 | counts[addr] = 1 29 | 30 | api.print('%d unique addresses executed.' % len(counts)) 31 | api.print('Top 30 executed addresses:') 32 | 33 | counts = sorted(counts.items(), key=itemgetter(1), reverse=True) 34 | for address, count in counts[:30]: 35 | api.print('%s %d ' % (hex(address), count)) 36 | -------------------------------------------------------------------------------- /plugins/exec_counts.yapsy-plugin: -------------------------------------------------------------------------------- 1 | [Core] 2 | Name = Print execution counts 3 | Module = exec_counts 4 | 5 | [Documentation] 6 | Author = Teemu 7 | Version = 0.1 8 | Website = https://github.com/teemu-l/execution-trace-viewer 9 | Description = Print execution counts for top 30 most executed addresses -------------------------------------------------------------------------------- /plugins/filter_by_mem_addr.py: -------------------------------------------------------------------------------- 1 | """This plugin filters a trace by addresses in memory accesses. 2 | Every row which accesses memory in given range is added to filtered_trace. 3 | """ 4 | 5 | from yapsy.IPlugin import IPlugin 6 | from core.api import Api 7 | 8 | 9 | class PluginFilterByMemAddress(IPlugin): 10 | def execute(self, api: Api): 11 | 12 | input_dlg_data = [ 13 | {"label": "Memory address", "data": "0x0"}, 14 | {"label": "Size", "data": 2000}, 15 | {"label": "Access types", "data": ["Reads and writes", "Reads", "Writes"]}, 16 | {"label": "Source trace", "data": ["Full trace", "Filtered trace"]}, 17 | ] 18 | options = api.get_values_from_user("Filter by memory address", input_dlg_data) 19 | 20 | if not options: 21 | return 22 | 23 | addr, size, access_types, trace_id = options 24 | addr = self.str_to_int(addr) 25 | 26 | print(f"Filtering by mem address: from {hex(addr)} to {hex(addr+size)}") 27 | 28 | if trace_id == 0: 29 | trace = api.get_full_trace() 30 | else: 31 | trace = api.get_filtered_trace() 32 | 33 | result_trace = [] 34 | 35 | for t in trace: 36 | for mem in t["mem"]: 37 | if mem["access"].upper() == "READ" and access_types == 2: 38 | continue 39 | elif mem["access"].upper() == "WRITE" and access_types == 1: 40 | continue 41 | if addr <= mem["addr"] <= (addr + size): 42 | result_trace.append(t.copy()) 43 | break # avoid adding the same row more than once 44 | 45 | if len(result_trace) > 0: 46 | print(f"Length of filtered trace: {len(result_trace)}") 47 | api.set_filtered_trace(result_trace) 48 | api.show_filtered_trace() 49 | else: 50 | api.show_messagebox( 51 | "Error", "Could not find any rows accessing given memory area" 52 | ) 53 | 54 | def str_to_int(self, s: str): 55 | result = 0 56 | if s: 57 | s = s.strip() 58 | if "0x" in s: 59 | result = int(s, 16) 60 | else: 61 | result = int(s) 62 | return result 63 | -------------------------------------------------------------------------------- /plugins/filter_by_mem_addr.yapsy-plugin: -------------------------------------------------------------------------------- 1 | [Core] 2 | Name = Filter by mem access address 3 | Module = filter_by_mem_addr 4 | 5 | [Documentation] 6 | Author = Teemu 7 | Version = 0.1 8 | Website = https://github.com/teemu-l/execution-trace-viewer 9 | Description = Filter trace by memory access address -------------------------------------------------------------------------------- /plugins/list_bookmarks.py: -------------------------------------------------------------------------------- 1 | from yapsy.IPlugin import IPlugin 2 | from operator import itemgetter 3 | from core.api import Api 4 | 5 | class PluginListBookmarks(IPlugin): 6 | 7 | def execute(self, api: Api): 8 | 9 | api.print('----------------------------------') 10 | 11 | bookmarks = api.get_bookmarks() 12 | if not bookmarks: 13 | api.print('No bookmarks found.') 14 | return 15 | 16 | for b in bookmarks: 17 | row = '{:<8}'.format(b.startrow) 18 | api.print(row + '{:<20}'.format(b.disasm) + '; %s' % b.comment) 19 | 20 | api.print('') 21 | 22 | addresses = {} 23 | for b in bookmarks: 24 | if b.addr in addresses: 25 | addresses[b.addr] += 1 26 | else: 27 | addresses[b.addr] = 1 28 | addresses = sorted(addresses.items(), key=itemgetter(1), reverse=True) 29 | 30 | api.print('Duplicate bookmarks:') 31 | api.print('Address | count | start row') 32 | for address, count in addresses: # [:15] 33 | b_rows = [] 34 | for b in bookmarks: 35 | if address == b.addr: 36 | b_rows.append(b.startrow) 37 | b_rows_str = ' '.join(map(str, b_rows)) 38 | api.print('%s | %d | %s' % (address, count, b_rows_str)) 39 | 40 | api.print('') 41 | 42 | api.print('%d bookmarks total.' % len(bookmarks)) 43 | api.print('%d unique bookmarks.' % len(addresses)) 44 | 45 | lengths = [] 46 | for b in bookmarks: 47 | lengths.append(b.endrow - b.startrow + 1) 48 | avg_len = sum(lengths) / len(bookmarks) 49 | shortest = min(lengths) 50 | longtest = max(lengths) 51 | api.print('Average length of bookmark: %d' % avg_len) 52 | api.print('Longest: %d Shortest: %d' % (longtest, shortest)) 53 | -------------------------------------------------------------------------------- /plugins/list_bookmarks.yapsy-plugin: -------------------------------------------------------------------------------- 1 | [Core] 2 | Name = List bookmarks 3 | Module = list_bookmarks 4 | 5 | [Documentation] 6 | Author = Teemu 7 | Version = 0.1 8 | Website = https://github.com/teemu-l/execution-trace-viewer 9 | Description = Lists all bookmarks -------------------------------------------------------------------------------- /plugins/print_rows.py: -------------------------------------------------------------------------------- 1 | from yapsy.IPlugin import IPlugin 2 | from core.api import Api 3 | 4 | 5 | class PluginPrintRows(IPlugin): 6 | def execute(self, api: Api): 7 | 8 | trace_data = api.get_trace_data() 9 | trace = api.get_selected_trace() 10 | 11 | if not trace: 12 | print("PluginPrintRows error: Nothing selected.") 13 | return 14 | 15 | api.print("") 16 | 17 | row_id_digits = len(str(trace[-1]["id"])) 18 | for t in trace: 19 | ip = hex(trace_data.get_instruction_pointer(t["id"])) 20 | api.print( 21 | "{:<{}} ".format(t["id"], row_id_digits) 22 | + " %s " % ip 23 | + " {:<42}".format(t["disasm"]) 24 | + "; %s" % t.get("comment", "") 25 | ) 26 | -------------------------------------------------------------------------------- /plugins/print_rows.yapsy-plugin: -------------------------------------------------------------------------------- 1 | [Core] 2 | Name = Print selected rows 3 | Module = print_rows 4 | 5 | [Documentation] 6 | Author = Teemu 7 | Version = 0.1 8 | Website = https://github.com/teemu-l/execution-trace-viewer 9 | Description = Prints selected rows -------------------------------------------------------------------------------- /plugins/print_selected_bookmarks.py: -------------------------------------------------------------------------------- 1 | """This plugin prints all selected bookmarks from bookmarks table""" 2 | from yapsy.IPlugin import IPlugin 3 | from core.api import Api 4 | 5 | 6 | class PluginPrintSelectedBookmarks(IPlugin): 7 | def execute(self, api: Api): 8 | 9 | api.print("") 10 | 11 | bookmarks = api.get_selected_bookmarks() 12 | 13 | for b in bookmarks: 14 | if b.startrow is not b.endrow: 15 | rows = "{:<13}".format(f"{b.startrow} - {b.endrow}") 16 | else: 17 | rows = "{:<13}".format(f"{b.startrow}") 18 | addr = "{:<16}".format(b.addr) 19 | disasm = "{:<33}".format(b.disasm) 20 | api.print(f"{rows} {addr} {disasm} ; {b.comment}") 21 | -------------------------------------------------------------------------------- /plugins/print_selected_bookmarks.yapsy-plugin: -------------------------------------------------------------------------------- 1 | [Core] 2 | Name = Print selected bookmarks 3 | Module = print_selected_bookmarks 4 | 5 | [Documentation] 6 | Author = Teemu 7 | Version = 0.1 8 | Website = https://github.com/teemu-l/execution-trace-viewer 9 | Description = Prints all selected bookmarks to log -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | capstone>=3.0.5.post1 2 | PyQt5>=5.11.3 3 | QDarkStyle>=2.6.8 4 | Yapsy==1.12.0 5 | -------------------------------------------------------------------------------- /traces/vmp3_32b_11k.tvt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teemu-l/execution-trace-viewer/d2d99e92decb3526efc1f5bd4bae350aa4c7c0d2/traces/vmp3_32b_11k.tvt -------------------------------------------------------------------------------- /tv.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from PyQt5 import QtWidgets 3 | import qdarkstyle 4 | 5 | from core import prefs 6 | from gui.mainwindow import MainWindow 7 | 8 | if __name__ == "__main__": 9 | APP = QtWidgets.QApplication(sys.argv) 10 | if prefs.USE_DARK_THEME: 11 | APP.setStyleSheet(qdarkstyle.load_stylesheet_pyqt5()) 12 | else: 13 | APP.setStyle(prefs.LIGHT_THEME) 14 | WINDOW = MainWindow() 15 | WINDOW.show() 16 | sys.exit(APP.exec_()) 17 | --------------------------------------------------------------------------------