├── .gitignore ├── LICENSE ├── README.md ├── README.zh_CN.md ├── TimeTravelEmulator.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | *.log 3 | *.pyc 4 | test.py 5 | TODO 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Krietz7 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 all 13 | 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 FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Time Travel Emulator 2 | 3 | The `TimeTravelEmulator` is a powerful IDA Pro plugin that brings the concept of time-travel debugging to your reverse engineering workflow. By integrating with the Unicorn emulation framework, it provides a unique capability to record and replay program execution, allowing for detailed analysis of runtime behavior. 4 | 5 | ## Language 6 | 7 | English | [中文](README.zh_CN.md) 8 | 9 | 10 | ## Features 11 | 12 | * **Emulation powered by Unicorn**: Utilizes the Unicorn CPU emulator to execute code snippets or entire functions within IDA Pro. 13 | * **Comprehensive State Capture**: Records detailed snapshots of the CPU registers and memory changes at each instruction executed. 14 | * **Time Travel Debugging**: 15 | * **Forward and Backward Navigation**: Seamlessly step forward (F3) and backward (F2) through the execution history. 16 | * **State Switching**: Jump to any captured program state (Q) to analyze the exact conditions at a specific point in time. 17 | * **State Switching by ID**: Jump to any captured program state by entering its unique `state_id` (I). The `state_id` is formatted as `$#`, allowing precise navigation. 18 | * **Visualizing Differences**: When switching between states, the plugin highlights the memory and register differences in the plugin interface, 19 | * **Memory and Register Tracking**: Provides visibility into how memory and register values evolve during execution. 20 | * **Configurable Emulation**: 21 | * Set custom emulation ranges (start and end addresses). 22 | * Load initial register values from the current debugger state (if a debugger is attached). 23 | * Configure emulation step limits and timeouts. 24 | * Option to set custom preprocessing code to set up the Unicorn environment before emulation. 25 | 26 | 27 | ## Requirements 28 | 29 | - IDA Pro version >= 7.7 30 | - Python version >= 3.8 31 | 32 | 33 | ## Installation 34 | 35 | 1. Use `pip install bsdiff4 capstone sortedcontainers unicorn` to install nessesary dependencies for this plugin in your IDAPython. 36 | 2. Place the `TimeTravelEmulator.py` file into your IDA Pro `plugins` directory. 37 | 3. Restart IDA Pro. 38 | 39 | 40 | ## Quick Start 41 | 42 | image 43 | 44 | Press the hotkey `Shift+T` to open the `TimeTravel Emulator: Emulator Settings` dialog. 45 | 46 | After the setting is completed, You can click "Emulate" to start the time-travel emulation. 47 | 48 | image 49 | 50 | Once emulation is completed, the plugin will create a independent view, you can use the following hotkeys to navigate through the recorded states in this view: 51 | * `F3`: Move to the next recorded state. 52 | * `F2`: Move to the previous recorded state. 53 | * `Q`: Switch to a specific state by the instruction position where the cursor is located. 54 | 55 | ## More Details 56 | ```cpp 57 | #include 58 | 59 | int main() { 60 | volatile int a = 5; 61 | volatile int b = 3; 62 | volatile int result = 0; 63 | 64 | int i = 0; 65 | while (i < 32) { 66 | result = result + i; 67 | b += a; 68 | i+= 1; 69 | } 70 | 71 | printf("Result: %d\n", result); 72 | 73 | return 0; 74 | } 75 | ``` 76 | 77 | Source code of a simple program for emulation. 78 | 79 | ### Emulator Settings dialog 80 | image 81 | 82 | Opening the emulation settings dialog by selecting code and using `Shift+T`. 83 | 84 | In the settings dialog, configure the emulation parameters: 85 | 86 | * **Emulation Execute Range**: Specify a start and end address, or select a function as the emulation range. 87 | * **Emulate step limit**: Set a limit on the number of instructions to emulate. 88 | * **Emulate time out**: Set a time limit for the emulation run. 89 | * **load registers**: Choose whether to load current register values (effective in debug mode). 90 | * **Set Stack value**: Configure special stack frame register values. 91 | 92 | * **Skip interrupts**: Skip `int` instructions during simulation 93 | * **Skip unloaded calls**: Skip the call instructions whose target address not loaded 94 | * **Skip thunk functions**: Skip thunk functions during simulation 95 | 96 | * **Log level & Log file path**: Set logging level and save location for logs. 97 | 98 | image 99 | 100 | * **Set custom preprocessing code**: Add custom Python code to execute before emulation. This can be used to set up memory, register values, or add hooks. 101 | 102 | 103 | ### Time Travel Emulator View 104 | ![Opening Time Travel Emulator View](https://github.com/user-attachments/assets/51ed9758-250c-44a7-a5b5-a98a99dfd250) 105 | 106 | Click "Emulate" to start the emulation. When the emulation is completed, a new window will open displaying the disassembly, register, and memory views, starting from the first emulated state. 107 | 108 | #### Disassembly View 109 | image 110 | 111 | This is the core view, highlighting the currently executed assembly instruction line. The number on the far left indicates how many times the current instruction has been executed throughout the emulation. 112 | 113 | Use the following hotkeys for navigation: 114 | * `F3`: Move to the next recorded state. 115 | * `F2`: Move to the previous recorded state. 116 | * `Q`: Jump to the state corresponding to the instruction at the cursor. 117 | * `I`: Prompt to enter a `state_id` (e.g., `$0x401000#5`) to jump to a specific state. 118 | 119 | #### Register View and Memory View 120 | 121 | ![Register View and Memory View examples](https://github.com/user-attachments/assets/fbcfc301-c639-4f85-a4cf-891909189011) 122 | 123 | 124 | When switching between states, these views will highlight the changed register values and memory bytes respectively, making it easy to identify differences. 125 | 126 | image 127 | 128 | In the memory view, you can select a range of bytes and press `E` to print the content to the console. 129 | 130 | #### Auxiliary Views 131 | 132 | The plugin also provides the following auxiliary views: 133 | 134 | ##### State Chooser 135 | 136 | image 137 | 138 | Use the hotkey `C` to open the state chooser view. This view displays all saved states during the emulation. Double-click an entry to jump to the corresponding state. 139 | 140 | ##### Memory Page Chooser 141 | 142 | image 143 | 144 | The plugin employs a lazy-loading mechanism for memory pages during emulation, meaning pages are mapped and loaded only when accessed by an instruction. 145 | 146 | Use hotkey `M` to open this view and quickly ascertain the memory pages loaded in the current state. 147 | 148 | 149 | ##### Difference Chooser 150 | 151 | ![Difference chooser use examples](https://github.com/user-attachments/assets/ad40cd4b-c82a-49ca-a07d-ff22705394dd) 152 | 153 | Use hotkey `D` to open the difference chooser view. This view automatically updates when switching states, providing a clear visual representation of memory and register changes between the two states. 154 | 155 | 156 | #### Debugging Mode 157 | 158 | image 159 | 160 | The plugin supports emulation in IDA's debugging mode and can automatically load the current register values. 161 | 162 | #### Dynamic Disassembly 163 | 164 | image 165 | 166 | 167 | When the simulation execution flow enters a segment of data that the IDA does not recognize as executable code, the plugin disassembles the data using the Capstone engine. 168 | 169 | 170 | ## Supported Architectures 171 | 172 | The plugin currently supports x86 (32-bit) and x64 (64-bit) architectures. 173 | More architectures will be added in the future. 174 | 175 | 176 | ## Contributing 177 | 178 | Contributions are welcome! Please feel free to open issues or submit pull requests. 179 | -------------------------------------------------------------------------------- /README.zh_CN.md: -------------------------------------------------------------------------------- 1 | # Time Travel Emulator 2 | 3 | `TimeTravelEmulator` 是一款功能强大的 IDA Pro 插件,通过集成 Unicorn 仿真框架,实现时间回溯调试的功能。通过记录和回放程序执行,可以实现对运行时行为的详细分析。 4 | 5 | ## 语言 6 | 7 | [English](README.md) | 中文 8 | 9 | 10 | ## 功能特性 11 | 12 | * **基于Unicorn的仿真引擎**:集成 Unicorn 仿真框架,用于在 IDA Pro 中执行代码片段或整个函数。 13 | * **全面的状态捕获**:记录执行每条指令时CPU寄存器和内存变化的详细快照。 14 | * **时间回溯调试**: 15 | * **前进和后退导航**:无缝地前进(F3)和后退(F2)执行历史记录。 16 | * **状态切换**:跳转到任何已捕获的程序状态(Q),分析特定时间点的确切条件。 17 | * **基于状态ID的切换**:通过唯一的 `state_id` 跳转(I)到任何已捕获的程序状态(`state_id` 格式为 `$<指令地址>#<执行次数>`),以实现精确导航。 18 | * **差异可视化** :在状态之间切换时,插件会高亮显示插件界面中的内存和寄存器差异, 19 | * **内存和寄存器跟踪**:提供对程序执行过程中内存和寄存器值变化的可见性。 20 | * **可配置的仿真**: 21 | * 设置自定义仿真范围(起始地址和结束地址)。 22 | * 从当前调试器状态加载初始寄存器值(在调试模式下生效)。 23 | * 配置仿真步数限制和时间限制。 24 | * 设置自定义的预处理代码,以设置 Unicorn 环境。 25 | 26 | 27 | ## 运行要求 28 | 29 | - IDA Pro 版本 >= 7.7 30 | - Python 版本 >= 3.8 31 | 32 | 33 | ## 安装 34 | 35 | 1. 在你的IDAPython中使用指令 `pip install bsdiff4 capstone sortedcontainers unicorn` 安装必要的依赖包。 36 | 2. 将 `TimeTravelEmulator.py` 文件放入 IDA Pro 的 `plugins` 目录。 37 | 3. 重启 IDA Pro。 38 | 39 | 40 | ## 快速入门 41 | 42 | image 43 | 44 | 按下快捷键 `Shift+T` 打开 `EmuTrace: Emulator Settings` (模拟器设置) 对话框。 45 | 46 | 设置完成后,点击 "Emulate" 开始时间回溯仿真。 47 | 48 | 49 | image 50 | 51 | 仿真完成后,插件将创建一个独立的视图,您可以使用以下快捷键浏览该视图中记录的状态: 52 | * `F3`: 移动到下一个录制的状态。 53 | * `F2`: 移动到上一个录制的状态。 54 | * `Q`: 通过光标所在的位置跳转到对应指令处的状态。 55 | 56 | ## 更多信息 57 | ```cpp 58 | #include 59 | 60 | int main() { 61 | volatile int a = 5; 62 | volatile int b = 3; 63 | volatile int result = 0; 64 | 65 | int i = 0; 66 | while (i < 32) { 67 | result = result + i; 68 | b += a; 69 | i+= 1; 70 | } 71 | 72 | printf("Result: %d\n", result); 73 | 74 | return 0; 75 | } 76 | ``` 77 | 78 | 用于仿真的示例程序源代码。 79 | 80 | ### 模拟器设置对话框 81 | image 82 | 83 | 选择要仿真的代码并使用快捷键 `Shift+T` 打开仿真设置对话框。 84 | 85 | 在设置对话框中,配置仿真参数: 86 | * **Start address & End address**: 模拟运行的起始地址和终止地址 87 | * **Select funtion range**: 选择某一函数作为模拟执行的运行范围 88 | 89 | * **Emulate step limit**: 模拟运行的运行步数限制 90 | * **Emulate time out**: 模拟运行的运行时间限制 91 | 92 | * **load registers**: 是否加载当前的寄存器值(在调试模式下生效) 93 | * **Set Stack value**: 是否设置特殊的栈帧寄存器值 94 | 95 | * **Skip interrupts**:在模拟过程中跳过`int`指令 96 | * **Skip unloaded calls**:跳过目标地址未被加载的`call`指令 97 | * **Skip thunk functions**:在模拟过程中跳过thunk函数 98 | 99 | * **Log level & Log file path**: 日志记录等级和保存位置 100 | 101 | image 102 | 103 | * **Set custom preprocessing code**: 设置自定义的预处理代码,这些代码将会在模拟运行前执行。你可以使用此功能提前设置内存、寄存器值或添加Hook 104 | 105 | 106 | ### 时间旅行仿真器视图 107 | 108 | ![Opening Time Travel Emulator View](https://github.com/user-attachments/assets/51ed9758-250c-44a7-a5b5-a98a99dfd250) 109 | 110 | 111 | 点击 "Emulate" 开始仿真。仿真完成后,将打开一个新窗口,显示从第一个模拟状态开始的反汇编、寄存器和内存视图。 112 | 113 | #### 反汇编视图 114 | image 115 | 116 | 这是插件的核心视图,标注了当前执行的汇编指令行,最左侧的数字指示当前汇编指令行在整个模拟执行过程中的执行次数。 117 | 118 | 使用以下快捷键进行导航: 119 | 120 | * `F3`: 移动到下一个录制的状态。 121 | * `F2`: 移动到上一个录制的状态。 122 | * `Q`: 跳转到光标所在指令处的状态。 123 | * `I`: 提示输入 state_id(例如,$0x401000#5)以跳转到特定状态。 124 | 125 | #### 寄存器视图和内存视图 126 | 127 | ![Register View and Memory View examples](https://github.com/user-attachments/assets/fbcfc301-c639-4f85-a4cf-891909189011) 128 | 129 | 130 | 当切换状态时,寄存器视图和内存视图分别高亮改变的寄存器值和内存字节变化。 131 | 132 | 133 | image 134 | 135 | 在内存视图中可以选择字节范围,按 `E` 将内容打印到控制台。 136 | 137 | 138 | #### 辅助视图 139 | 140 | 插件还提供了以下辅助视图: 141 | 142 | 143 | ##### 状态选择器 144 | 145 | 146 | image 147 | 148 | 使用 `C` 快捷键打开状态选择器视图。此视图显示了模拟执行期间保存的所有状态。双击条目可跳转到相应状态。 149 | 150 | 151 | ##### 内存页选择器 152 | 153 | image 154 | 155 | 该插件在仿真期间对内存页面采用延迟加载机制,这意味着只有在被指令访问时才会映射和加载这些页面。 156 | 157 | 使用快捷键 `M` 打开此视图并快速确定当前状态下加载的内存页。 158 | 159 | 160 | ##### 差异选择器 161 | 162 | ![Difference chooser use examples](https://github.com/user-attachments/assets/ad40cd4b-c82a-49ca-a07d-ff22705394dd) 163 | 164 | 使用快捷键 `D` 打开差异选择器视图,该视图将会在每次切换状态时同步更新,直观显示这两个状态之间的内存和寄存器变化 165 | 166 | 167 | 168 | #### 调试模式 169 | 170 | image 171 | 172 | 该插件支持在 IDA 的调试模式下进行仿真,并可以自动加载当前寄存器值。 173 | 174 | 175 | #### 动态反汇编 176 | 177 | image 178 | 179 | 当模拟执行流进入 IDA 未识别为可执行代码的数据段时,插件将使用 Capstone 引擎对这些数据进行反汇编。 180 | 181 | 182 | ## 支持的架构 183 | 184 | 该插件目前支持 x86 (32-bit) 和 x64 (64-bit) 架构。 185 | 未来将提供更多架构支持 186 | 187 | 188 | ## 贡献 189 | 190 | 欢迎贡献!请随时提交问题或拉取请求。 191 | -------------------------------------------------------------------------------- /TimeTravelEmulator.py: -------------------------------------------------------------------------------- 1 | import idaapi 2 | import ida_ida 3 | import ida_kernwin 4 | import ida_dbg 5 | import idc 6 | import ida_name 7 | import ida_bytes 8 | import ida_lines 9 | import ida_segment 10 | 11 | import logging 12 | import bisect 13 | from abc import ABC, abstractmethod 14 | from collections import defaultdict 15 | from typing import Callable, Dict, Iterator, List, Literal, Optional, Set, Tuple, Union 16 | from copy import deepcopy 17 | from dataclasses import dataclass 18 | from re import split 19 | 20 | import bsdiff4 21 | from sortedcontainers import SortedDict, SortedList 22 | from unicorn import * 23 | from unicorn.x86_const import * 24 | from capstone import * 25 | from PyQt5 import QtCore, QtWidgets 26 | 27 | 28 | 29 | VERSION = '1.1.1' 30 | 31 | PLUGIN_NAME = 'TimeTravelEmulator' 32 | PLUGIN_HOTKEY = 'Shift+T' 33 | 34 | NEXT_STATE_ACTION_SHORTCUT = "F3" 35 | PREV_STATE_ACTION_SHORTCUT = "F2" 36 | CURSOR_STATE_ACTION_SHORTCUT = "Q" 37 | 38 | 39 | EXECUTE_INSN_HILIGHT_COLOR = 0xFFD073 40 | CHANGE_HIGHLIGHT_COLOR = 0xFFD073 41 | BYTE_CHANGE_HIGHTLIGHT = ida_kernwin.CK_EXTRA11 42 | 43 | 44 | 45 | # Define page size and page mask, usually 4 kb 46 | PAGE_SIZE = 0x1000 47 | PAGE_MASK = ~(PAGE_SIZE - 1) 48 | 49 | # Define default page permission 50 | DEFAULT_PAGE_PERMISSION = UC_PROT_WRITE | UC_PROT_READ 51 | 52 | # Define default stack and frame values 53 | DEFAULT_STACK_POINT_VALUE = 0x70000000 54 | DEFAULT_BASE_POINT_VALUE = DEFAULT_STACK_POINT_VALUE 55 | 56 | 57 | UNICORN_ARCH_MAP = { 58 | "x64": [UC_ARCH_X86, UC_MODE_64], 59 | "x86": [UC_ARCH_X86, UC_MODE_32], 60 | } 61 | 62 | CAPSTONE_ARCH_MAP = { 63 | "x64": [CS_ARCH_X86, CS_MODE_64], 64 | "x86": [CS_ARCH_X86, CS_MODE_32] 65 | } 66 | 67 | IDA_PROC_TO_ARCH_MAP = { 68 | ("metapc", 64) : "x64", 69 | ("metapc", 32) : "x86", 70 | } 71 | 72 | ARCH_TO_INSN_POINTER_MAP = { 73 | "x64" : UC_X86_REG_RIP, 74 | "x86" : UC_X86_REG_EIP, 75 | } 76 | 77 | 78 | UNICORN_REGISTERS_MAP = { 79 | "x64" : { 80 | # General Purpose Registers (GPRs) 81 | "RAX": UC_X86_REG_RAX, 82 | "RBX": UC_X86_REG_RBX, 83 | "RCX": UC_X86_REG_RCX, 84 | "RDX": UC_X86_REG_RDX, 85 | "RSI": UC_X86_REG_RSI, 86 | "RDI": UC_X86_REG_RDI, 87 | "RBP": UC_X86_REG_RBP, 88 | "RSP": UC_X86_REG_RSP, 89 | "RIP": UC_X86_REG_RIP, 90 | "R8": UC_X86_REG_R8, 91 | "R9": UC_X86_REG_R9, 92 | "R10": UC_X86_REG_R10, 93 | "R11": UC_X86_REG_R11, 94 | "R12": UC_X86_REG_R12, 95 | "R13": UC_X86_REG_R13, 96 | "R14": UC_X86_REG_R14, 97 | "R15": UC_X86_REG_R15, 98 | # Instruction Pointer 99 | # Flags Register 100 | "Rflags": UC_X86_REG_EFLAGS, # In 64-bit, EFLAGS is extended to RFLAGS, but the constant remains EFLAGS 101 | # Segment Registers 102 | "CS": UC_X86_REG_CS, 103 | "SS": UC_X86_REG_SS, 104 | "DS": UC_X86_REG_DS, 105 | "ES": UC_X86_REG_ES, 106 | "FS": UC_X86_REG_FS, 107 | "GS": UC_X86_REG_GS, 108 | }, 109 | "x86": { 110 | # General Purpose Registers (GPRs) 111 | "EAX": UC_X86_REG_EAX, 112 | "EBX": UC_X86_REG_EBX, 113 | "ECX": UC_X86_REG_ECX, 114 | "EDX": UC_X86_REG_EDX, 115 | "ESI": UC_X86_REG_ESI, 116 | "EDI": UC_X86_REG_EDI, 117 | "EBP": UC_X86_REG_EBP, 118 | "ESP": UC_X86_REG_ESP, 119 | # Instruction Pointer 120 | "EIP": UC_X86_REG_EIP, 121 | # Flags Register 122 | "Eflags": UC_X86_REG_EFLAGS, 123 | # Segment Registers 124 | "CS": UC_X86_REG_CS, 125 | "SS": UC_X86_REG_SS, 126 | "DS": UC_X86_REG_DS, 127 | "ES": UC_X86_REG_ES, 128 | "FS": UC_X86_REG_FS, 129 | "GS": UC_X86_REG_GS, 130 | } 131 | } 132 | 133 | IP_REG_NAME_MAP = { 134 | "x64": "RIP", 135 | "x86": "EIP" 136 | } 137 | 138 | IDA_PERM_TO_UC_PERM_MAP = { 139 | ida_segment.SEGPERM_EXEC : UC_PROT_EXEC, 140 | ida_segment.SEGPERM_WRITE : UC_PROT_WRITE, 141 | ida_segment.SEGPERM_READ : UC_PROT_READ 142 | } 143 | 144 | 145 | def get_bitness() -> Union[None, Literal[64], Literal[32]]: 146 | if ida_ida.inf_is_64bit(): 147 | return 64 148 | elif ida_ida.inf_is_32bit_exactly(): 149 | return 32 150 | 151 | def get_is_be() -> bool: 152 | return ida_ida.inf_is_be() 153 | 154 | def get_arch() -> str: 155 | proc_name = idaapi.inf_get_procname() 156 | proc_bitness = get_bitness() 157 | 158 | if proc_bitness == None: 159 | return "" 160 | if (proc_name, proc_bitness) not in IDA_PROC_TO_ARCH_MAP: 161 | return "" 162 | return IDA_PROC_TO_ARCH_MAP[(proc_name, proc_bitness)] 163 | 164 | 165 | def get_arch_x64_regs_value() -> Dict[int, int]: 166 | if not idaapi.is_debugger_on(): 167 | return {} 168 | arch = get_arch() 169 | if arch not in UNICORN_REGISTERS_MAP: 170 | return {} 171 | 172 | result: Dict[int, int] = {} 173 | regs_map = UNICORN_REGISTERS_MAP[arch] 174 | for reg_name, uc_reg_const in regs_map.items(): 175 | if reg_name == "Rflags": 176 | flag_positions = { 177 | "CF": 0, # Carry Flag 178 | "PF": 2, # Parity Flag 179 | "AF": 4, # Auxiliary Carry Flag 180 | "ZF": 6, # Zero Flag 181 | "SF": 7, # Sign Flag 182 | "TF": 8, # Trap Flag 183 | "IF": 9, # Interrupt Enable Flag 184 | "DF": 10, # Direction Flag 185 | "OF": 11, # Overflow Flag 186 | "IOPL": 12, # I/O Privilege Level (Usually two bit) 187 | "NT": 14, # Nested Task Flag 188 | "RF": 16, # Resume Flag 189 | "VM": 17, # Virtual-8086 Mode Flag 190 | "AC": 18, # Alignment Check / Access Control Flag 191 | "VIF": 19, # Virtual Interrupt Flag 192 | "VIP": 20, # Virtual Interrupt Pending 193 | "ID": 21 # ID Flag 194 | } 195 | rflags_value = 0 196 | for flag_name, bit_pos in flag_positions.items(): 197 | try: 198 | flag_val = ida_dbg.get_reg_val(flag_name) 199 | 200 | if flag_val is not None: 201 | if flag_name == "IOPL": 202 | rflags_value |= ((flag_val & 0x3) << bit_pos) 203 | else: 204 | if flag_val == 1: 205 | rflags_value |= (1 << bit_pos) 206 | except Exception as e: 207 | continue 208 | result[uc_reg_const] = rflags_value 209 | elif reg_name in ["FS", "GS"]: 210 | continue 211 | else: 212 | try: 213 | reg_value = ida_dbg.get_reg_val(reg_name) 214 | result[uc_reg_const] = reg_value 215 | except Exception as e: 216 | tte_log_err(f"Error getting register value for {reg_name}: {e}") 217 | pass 218 | return result 219 | 220 | 221 | def get_arch_x86_regs_value() -> Dict[int, int]: 222 | if not idaapi.is_debugger_on(): 223 | return {} 224 | arch = get_arch() 225 | if arch not in UNICORN_REGISTERS_MAP: 226 | return {} 227 | 228 | result: Dict[int, int] = {} 229 | regs_map = UNICORN_REGISTERS_MAP[arch] 230 | for reg_name, uc_reg_const in regs_map.items(): 231 | if reg_name == "Eflags": 232 | flag_positions = { 233 | "CF": 0, # Carry Flag 234 | "PF": 2, # Parity Flag 235 | "AF": 4, # Auxiliary Carry Flag 236 | "ZF": 6, # Zero Flag 237 | "SF": 7, # Sign Flag 238 | "TF": 8, # Trap Flag 239 | "IF": 9, # Interrupt Enable Flag 240 | "DF": 10, # Direction Flag 241 | "OF": 11, # Overflow Flag 242 | "IOPL": 12, # I/O Privilege Level (Usually two bit) 243 | "NT": 14, # Nested Task Flag 244 | "RF": 16, # Resume Flag 245 | "VM": 17, # Virtual-8086 Mode Flag 246 | "AC": 18, # Alignment Check / Access Control Flag 247 | "VIF": 19, # Virtual Interrupt Flag 248 | "VIP": 20, # Virtual Interrupt Pending 249 | "ID": 21 # ID Flag 250 | } 251 | rflags_value = 0 252 | for flag_name, bit_pos in flag_positions.items(): 253 | try: 254 | flag_val = ida_dbg.get_reg_val(flag_name) 255 | 256 | if flag_val is not None: 257 | if flag_name == "IOPL": 258 | rflags_value |= ((flag_val & 0x3) << bit_pos) 259 | else: 260 | if flag_val == 1: 261 | rflags_value |= (1 << bit_pos) 262 | except Exception as e: 263 | continue 264 | result[uc_reg_const] = rflags_value 265 | elif reg_name in ["CS", "SS", "DS", "ES", "FS", "GS"]: 266 | continue 267 | else: 268 | try: 269 | reg_value = ida_dbg.get_reg_val(reg_name) 270 | result[uc_reg_const] = reg_value 271 | except Exception as e: 272 | tte_log_err(f"Error getting register value for {reg_name}: {e}") 273 | pass 274 | return result 275 | 276 | 277 | def get_page_slice(page_start: int, page_size: int) -> List[Tuple[int, int]]: 278 | """ 279 | Get a slice of segments address of a memory page 280 | 281 | :param page_start: Start address of the memory page. Must be page aligned. 282 | :param page_size: Size of the memory page 283 | :return: A list, where the value is a list of tuples (start_ea, end_ea) of the segments slice in the page. 284 | """ 285 | result: List[Tuple[int, int]] = [] 286 | seg = ida_segment.get_first_seg() 287 | page_end = page_start + page_size 288 | while seg: 289 | if seg.start_ea >= page_end: 290 | break 291 | # Check for overlap: 292 | # (seg.start_ea <= end_address) and (seg.end_ea >= start_address) 293 | if seg.start_ea < page_end - 1 and seg.end_ea > page_start: 294 | max_addr = max(seg.start_ea, page_start) 295 | min_addr = min(seg.end_ea, page_end - 1) 296 | result.append((max_addr, min_addr)) 297 | seg = ida_segment.get_next_seg(seg.end_ea -1 ) # -1 to ensure next segment is properly found 298 | 299 | return result 300 | 301 | 302 | def get_segment_prem(addr: int) -> int: # [ ] TODO: The segments of the program are not always page-aligned, so this situation needs to be considered 303 | seg = ida_segment.getseg(addr) 304 | if seg is not None: 305 | ida_perm = seg.perm 306 | uc_perm = 0 307 | for ida_bit, uc_bit in IDA_PERM_TO_UC_PERM_MAP.items(): 308 | if ida_perm & ida_bit: 309 | uc_perm |= uc_bit 310 | return uc_perm 311 | return DEFAULT_PAGE_PERMISSION 312 | 313 | 314 | def catch_dict_patch( 315 | base_dict: Dict[str, int], 316 | target_dict: Dict[str, int] 317 | ) -> Dict[str, int]: 318 | """ 319 | Compare the two dictionaries and return different key-value pairs of target_dict relative to base_dict. 320 | Prerequisite: Two dictionaries have exactly the same set of keys. 321 | Returns the key-value pair contains only the changed values. 322 | """ 323 | return {key: target_dict[key] for key in base_dict if base_dict[key] != target_dict[key]} 324 | 325 | 326 | def apply_dict_patch( 327 | base_dict: Dict[str, int], 328 | patch: Optional[Dict[str, int]] 329 | ) -> Dict[str, int]: 330 | result = base_dict.copy() 331 | if patch is not None: 332 | result.update(patch) 333 | return result 334 | 335 | 336 | def catch_bytes_patch( 337 | base_bytes_dict: Dict[int, Tuple[int, bytearray]], # 338 | target_bytes_dict: Dict[int, Tuple[int, bytearray]] 339 | ) -> Tuple[Dict[int, Tuple[int, bytes]], Dict[int, Tuple[int, bytearray]]]: 340 | """ 341 | Compare the two byte dictionaries and return different key-value pairs of target_bytes relative to base_bytes. 342 | 343 | :param base_bytes_dict: Original dictionary of bytes {addr: (permission, data)} 344 | :param target_bytes_dict: Dictionary of bytes to be compared with base_bytes_dict {addr: (permission, data)} 345 | :return patches: Dictionary of binary diffs for modified keys {addr: (permission, patch)} 346 | :return new_entries: Dictionary of new key-value pairs {addr: (permission, data)} 347 | """ 348 | patches: Dict[int, Tuple[int, bytes]] = {} 349 | new_entries: Dict[int, Tuple[int, bytearray]] = {} 350 | 351 | 352 | for addr, (permossion, target_bytes) in target_bytes_dict.items(): 353 | entry = base_bytes_dict.get(addr) 354 | if entry is None: 355 | new_entries[addr] = (permossion, target_bytes) 356 | continue 357 | _, base_bytes = entry 358 | if base_bytes != target_bytes: 359 | patches[addr] = (permossion, bsdiff4.diff(bytes(base_bytes), bytes(target_bytes))) 360 | else: 361 | patches[addr] = (permossion, b'') 362 | 363 | return patches, new_entries 364 | 365 | 366 | def apply_bytes_patch( 367 | base_bytes_dict: Dict[int, Tuple[int, bytearray]], 368 | patches: Dict[int, Tuple[int, bytes]], 369 | new_entries: Dict[int, Tuple[int, bytearray]] 370 | ) -> Dict[int, Tuple[int, bytearray]]: 371 | """ 372 | Apply the generated patches to the base data dictionary and merge new entries. 373 | 374 | :param base_bytes_dict: Original dictionary of bytes {addr: (permission, data)} 375 | :param patches: Dictionary of binary diffs for modified keys {addr: (permission, patch)} 376 | :param new_entries: Dictionary of new key-value pairs {addr: (permission, data)} 377 | :return updated_dict: Updated dictionary with applied diffs and new entries {addr: (permission, data)} 378 | """ 379 | updated_dict: Dict[int, Tuple[int, bytearray]] = dict(base_bytes_dict) 380 | 381 | for addr, (permossion, patch) in patches.items(): 382 | if patch == b'': 383 | continue 384 | key_data = updated_dict.get(addr) 385 | assert key_data is not None, f"Error: key {addr} not found in base_bytes_dict" 386 | _, original_data = key_data 387 | try: 388 | patched_data = bsdiff4.patch(bytes(original_data), patch) 389 | updated_dict[addr] = (permossion, bytearray(patched_data)) 390 | except Exception as e: 391 | raise RuntimeError(f"Error applying patch to key {addr}: {e}") 392 | 393 | updated_dict.update(new_entries) 394 | return updated_dict 395 | 396 | 397 | 398 | 399 | class TTE_Logger: 400 | _instance = None 401 | _initialized = False 402 | 403 | def __new__(cls, *args, **kwargs): 404 | if cls._instance is None: 405 | cls._instance = super(TTE_Logger, cls).__new__(cls) 406 | return cls._instance 407 | 408 | def start(self, log_level, log_file=None): 409 | """ 410 | Start the logger 411 | :param log_file: Log file path 412 | :param level: Log Level 413 | """ 414 | if self._initialized: 415 | tte_log_info("Logger already started.") 416 | return 417 | 418 | # Set logging 419 | self.logger = logging.getLogger('TTE_Logger') 420 | self.logger.setLevel(log_level) 421 | formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') 422 | 423 | # Create handler 424 | if log_file: 425 | file_handler = logging.FileHandler(log_file) 426 | file_handler.setFormatter(formatter) 427 | self.logger.addHandler(file_handler) 428 | 429 | self._initialized = True 430 | self.log(logging.INFO, "Logger started.") 431 | 432 | def stop(self): 433 | """ 434 | Stop the logger 435 | """ 436 | if not self._initialized: 437 | return 438 | 439 | self.log(logging.INFO, "Logger stopping...") 440 | handlers = self.logger.handlers[:] 441 | for handler in handlers: 442 | handler.close() 443 | self.logger.removeHandler(handler) 444 | self._initialized = False 445 | 446 | def log(self, level, message): 447 | """ 448 | Logging 449 | :param level: Log Level (logging.INFO, logging.ERROR) 450 | :param message: Information to be recorded 451 | """ 452 | if not self._initialized: 453 | return 454 | 455 | self.logger.log(level, message) 456 | 457 | def tte_log_info(message): 458 | """ 459 | Logging with INFO level 460 | :param message: Information to be recorded 461 | """ 462 | TTE_Logger().log(logging.INFO,message) 463 | 464 | def tte_log_dbg(message): 465 | """ 466 | Logging with DEBUG level 467 | :param message: Information to be recorded 468 | """ 469 | TTE_Logger().log(logging.DEBUG,message) 470 | 471 | def tte_log_warn(message): 472 | """ 473 | Logging with WARNING level 474 | :param message: Information to be recorded 475 | """ 476 | TTE_Logger().log(logging.WARNING,message) 477 | 478 | def tte_log_err(message): 479 | """ 480 | Logging with ERROR level 481 | :param message: Information to be recorded 482 | """ 483 | TTE_Logger().log(logging.ERROR,message) 484 | 485 | 486 | @dataclass 487 | class EmuSettings(): 488 | start: int = -1 489 | end: int = -1 490 | 491 | is_load_registers: bool = False 492 | is_skip_interrupts = False 493 | is_skip_unloaded_calls = True 494 | is_skip_trunk_funcs: bool = False 495 | is_set_stack_value: bool = False 496 | 497 | time_out: int = 0 498 | count: int = 500 499 | log_level: int = logging.WARNING 500 | log_file_path: Optional[str] = None 501 | 502 | preprocessing_code: str = "" 503 | 504 | 505 | class EmuSettingsForm(idaapi.Form): 506 | 507 | def __init__(self, start_ea, end_ea) -> None: 508 | self.emu_settings = EmuSettings() 509 | 510 | self.i_start_address: Optional[ida_kernwin.Form.NumericInput] = None 511 | self.i_end_address: Optional[ida_kernwin.Form.NumericInput] = None 512 | 513 | self.i_emulate_step_limit: Optional[ida_kernwin.Form.NumericInput] = None 514 | self.i_time_out: Optional[ida_kernwin.Form.NumericInput] = None 515 | 516 | self.c_configs_group: Optional[ida_kernwin.Form.ChkGroupControl] = None 517 | self.r_load_register: Optional[ida_kernwin.Form.ChkGroupItemControl] = None 518 | 519 | 520 | self.r_skip_interrupts: Optional[ida_kernwin.Form.ChkGroupItemControl] = None 521 | self.r_skip_unloaded_calls: Optional[ida_kernwin.Form.ChkGroupItemControl] = None 522 | self.r_skip_thunk_funcs: Optional[ida_kernwin.Form.ChkGroupItemControl] = None 523 | self.r_set_stack_value: Optional[ida_kernwin.Form.ChkGroupItemControl] = None 524 | 525 | self.i_log_level: Optional[ida_kernwin.Form.DropdownListControl] = None 526 | self.i_log_file_path: Optional[ida_kernwin.Form.FileInput] = None 527 | 528 | 529 | super().__init__( 530 | r'''STARTITEM {id:i_start_address} 531 | BUTTON YES* Emulate 532 | TimeTravel Emulator: Emulator Settings 533 | 534 | {FormChangeCb} 535 | Emulation Execute Range: 536 | 537 | 538 |