├── .gitignore ├── .vscode └── settings.json ├── README.md ├── README_ch.md ├── assets ├── autojump.gif ├── create_md.gif └── highlight_md.gif └── ida_plugin └── ida_notepad_plus.py /.gitignore: -------------------------------------------------------------------------------- 1 | apis_md/ 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "git.ignoreLimitWarning": true 3 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [中文](/README_ch.md) 2 | # Introduction 3 | This plugin replaces the built-in notepad window in IDA, which is not user-friendly, and adds many practical features. The notepad in IDA is shared globally, but my idea is to provide a separate notepad space for each function. 4 | 5 | Security researchers can view the content of the corresponding function in the notepad while looking at a specific function in the pseudocode window. All these contents are synchronized to the disk, making it convenient to search for keywords using search tools outside of IDA. Additionally, the notepad provides extra small but useful features. 6 | 7 | # Features 8 | 1. Quickly create a notepad for the current function using a keyboard shortcut. 9 | 2. Quickly create a notepad for any highlighted content in pseudocode window or disassembly window. 10 | 3. Changes made in the notepad are automatically saved to the disk. 11 | 4. When the "Sync" option is enabled, switching functions in the pseudocode window will also switch the notepad window to the corresponding function 12 | 5. When the "AutoJump" option is enabled, selecting an address in the notepad will automatically jump to that address 13 | 6. Provide an "AutoCreate" option, which configures whether user confirmation is required before creating a note. 14 | 15 | Demonstration of creating a new note and enabling the "Sync" option 16 | 17 | ![This is an image](/assets/create_md.gif "Create notepad example") 18 | 19 | Demonstration of creating a note for highlighted content or opening the corresponding note 20 | 21 | ![This is an image](/assets/highlight_md.gif "Create highlight notepad example") 22 | 23 | Demonstration of the "AutoJump" feature 24 | 25 | ![This is an image](/assets/autojump.gif "Autojump example") 26 | 27 | # Installation 28 | Copy ida_notepad_plus.py to the plugin directory of IDA 29 | Or use the PluginLoader plugin and add the path of ida_notepad_plus.py to plugins.list 30 | 31 | # TODO 32 | 1. Support more jump methods for the "AutoJump" feature, such as module+offset, function name 33 | 34 | # Credits 35 | Thanks to @Alexander Hanel's DocsViewerIDA! Inspired by this project. -------------------------------------------------------------------------------- /README_ch.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | 替代了 IDA 自带的不好用的记事本窗口,添加了很多实用的功能。 IDA 的记事本是全局共用的, 而我的想法是给每个函数都有自己的记事本空间, 安全研究员在伪代码窗口中看哪个函数, 记事本就显示对应函数的内容。 这些内容都会同步到磁盘上, 也方便在 IDA 之外使用搜索工具查询关键字。 此外记事本还提供了选中地址自动跳转的小功能。 3 | 4 | # 功能 5 | 1. 快捷键快速创建当前函数的记事本 6 | 2. 如果伪代码窗口或者反汇编窗口有任何高亮选中的内容,快捷键为该内容创建记事本 7 | 2. 用户在记事本中所做的更改, 会自动保存写入到磁盘 8 | 3. 开启 sync 选项, 伪代码窗口中函数切换, 记事本窗口也会切换到对应的函数 9 | 4. 开启 autojump 选项, 选中记事本中的地址, 会自动跳转 10 | 5. 提供 AutoCreate 选项,通过此选项配置 在创建笔记之前是否需要用户确认 11 | 12 | 演示创建新的笔记和开启 sync 选项 13 | 14 | ![这是图片](/assets/create_md.gif "Create notepad example") 15 | 16 | 演示为高亮的内容创建笔记或打开对应的笔记 17 | 18 | ![这是图片](/assets/highlight_md.gif "Create highlight notepad example") 19 | 20 | 演示 autojump 功能 21 | 22 | ![这是图片](/assets/autojump.gif "Autojump example") 23 | 24 | # 安装 25 | 把 ida_notepad_plus.py 拷贝到 IDA 的插件目录下 26 | 或者使用PluginLoader 插件, 将 ida_notepad_plus.py的路径添加到 plugins.list 27 | 28 | # TODO 29 | 3. 提交到 github 30 | 4. autojump 功能支持更多跳转方式, 例如 module+offset, 函数名 31 | 32 | # credits 33 | Thanks to @Alexander Hanel's DocsViewerIDA! 受此项目启发 -------------------------------------------------------------------------------- /assets/autojump.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/singleghost2/IDA-Notepad-plus/f1e313df4e4dff3deb363843a249afe4f71812e8/assets/autojump.gif -------------------------------------------------------------------------------- /assets/create_md.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/singleghost2/IDA-Notepad-plus/f1e313df4e4dff3deb363843a249afe4f71812e8/assets/create_md.gif -------------------------------------------------------------------------------- /assets/highlight_md.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/singleghost2/IDA-Notepad-plus/f1e313df4e4dff3deb363843a249afe4f71812e8/assets/highlight_md.gif -------------------------------------------------------------------------------- /ida_plugin/ida_notepad_plus.py: -------------------------------------------------------------------------------- 1 | import os, re, sys, traceback 2 | import ida_kernwin 3 | import ida_idaapi 4 | import ida_name 5 | import idc 6 | import idaapi 7 | import ida_hexrays 8 | 9 | from idaapi import PluginForm 10 | from PyQt5 import QtWidgets 11 | from PyQt5.QtGui import QFont 12 | from PyQt5.QtWidgets import QApplication, QTextEdit, QMenu, QFontDialog 13 | 14 | 15 | # Path to the Markdown docs. Folder should start with 16 | IDB_DIR = os.path.dirname(idc.get_idb_path()) 17 | if idaapi.get_root_filename() is not None: 18 | API_MD = os.path.join(IDB_DIR, "Notes-" + idaapi.get_root_filename()) 19 | if not os.path.exists(API_MD): 20 | os.mkdir(API_MD) 21 | 22 | # global variables used to track initialization/creation of the forms. 23 | started = False 24 | frm = None 25 | 26 | 27 | 28 | def clean_filename(filename): 29 | # Since MAC and Linux only limit a small number of characters, while Windows limits more characters, 30 | # The following is the union of illegal characters from the three systems 31 | invalid_chars = '<>:"/\\|?*' 32 | 33 | # For security reasons, ASCII control characters (0-31) are also included here 34 | control_chars = ''.join(map(chr, range(0, 32))) 35 | 36 | # 将所有非法字符以及控制字符替换为下划线 37 | # Replace all illegal characters as well as control characters with underscores 38 | return re.sub('[{}{}]'.format(re.escape(invalid_chars), re.escape(control_chars)), '_', filename) 39 | 40 | def normalize_name(name): 41 | t = ida_name.FUNC_IMPORT_PREFIX 42 | if name.startswith(t): 43 | name = name[len(t):] 44 | name = name.lstrip('_') 45 | if '(' in name: 46 | name = name[:name.index('(')] 47 | return name 48 | 49 | def demangle(name, disable_mask=0): 50 | demangled_name = idaapi.demangle_name(name, disable_mask, idaapi.DQT_FULL) 51 | if demangled_name: 52 | return demangled_name 53 | return name 54 | 55 | def get_selected_name(): 56 | try: 57 | v = ida_kernwin.get_current_viewer() 58 | ret = ida_kernwin.get_highlight(v) 59 | name = None 60 | if ret is None: 61 | # Determine whether it is in the pseudocode window. If so, return the currently displayed function name. 62 | if idaapi.get_widget_type(v) == idaapi.BWN_PSEUDOCODE: 63 | vu = idaapi.get_widget_vdui(v) 64 | name = idaapi.get_ea_name(vu.cfunc.entry_ea) 65 | name = demangle(name) 66 | else: 67 | print("No identifier was highlighted") 68 | return None 69 | else: 70 | name, flag = ret 71 | 72 | return normalize_name(name) 73 | except Exception as e: 74 | # traceback.print_exc() 75 | return None 76 | 77 | 78 | class CustomTextEdit(QTextEdit): 79 | def __init__(self, pluginForm, parent=None): 80 | super(CustomTextEdit, self).__init__(parent) 81 | self.pluginForm = pluginForm 82 | # Create a standard right-click context menu 83 | self.menu = self.createStandardContextMenu() 84 | 85 | # add a separator 86 | self.menu.addSeparator() 87 | 88 | # Add custom menu items 89 | self.fontAction = self.menu.addAction("Font") 90 | self.SyncAction = self.menu.addAction("Sync") 91 | self.autoJumpAction = self.menu.addAction("AutoJump") 92 | 93 | self.menu.addSeparator() 94 | self.autoCreateOption = self.menu.addAction("AutoCreate") 95 | 96 | # Connect signal slots 97 | self.fontAction.triggered.connect(self.changeFont) 98 | self.SyncAction.triggered.connect(self.changeSync) 99 | self.autoJumpAction.triggered.connect(self.changeAutoJumpSetting) 100 | self.autoCreateOption.triggered.connect(self.changeAutoCreateOption) 101 | 102 | self.autoJump = False 103 | 104 | def contextMenuEvent(self, event): 105 | self.menu.exec_(event.globalPos()) 106 | 107 | def mouseReleaseEvent(self, e): 108 | super().mouseReleaseEvent(e) 109 | 110 | if self.autoJump: 111 | selected_text = self.textCursor().selectedText().strip() 112 | if selected_text: 113 | # print(f"Selected text: {selected_text}") 114 | match_obj = re.match(r'^(0x)?([0-9a-f`]+)$', selected_text, flags=re.IGNORECASE) 115 | if match_obj is not None: 116 | addr_str = match_obj.group(2) 117 | addr_str = addr_str.replace('`', '') 118 | # print(f"jumpto addr {hex(int(addr_str, 16))}") 119 | idaapi.jumpto(int(addr_str, 16)) 120 | else: 121 | try: 122 | ea = idc.get_name_ea_simple(selected_text) 123 | idaapi.jumpto(ea) 124 | except: 125 | pass 126 | 127 | 128 | def changeFont(self): 129 | # Open font dialog 130 | font, ok = QFontDialog.getFont(self.font(), self) 131 | if ok: 132 | self.setFont(font) 133 | 134 | def changeSync(self): 135 | self.pluginForm.sync = not self.pluginForm.sync 136 | if self.pluginForm.sync: 137 | self.SyncAction.setText("Sync ✔") 138 | else: 139 | self.SyncAction.setText("Sync") 140 | 141 | def changeAutoJumpSetting(self): 142 | if self.pluginForm.sync: 143 | self.changeSync() 144 | 145 | self.autoJump = not self.autoJump 146 | if self.autoJump: 147 | self.autoJumpAction.setText("AutoJump ✔") 148 | else: 149 | self.autoJumpAction.setText("AutoJump") 150 | 151 | def changeAutoCreateOption(self): 152 | self.pluginForm.autoCreate = not self.pluginForm.autoCreate 153 | if self.pluginForm.autoCreate: 154 | self.autoCreateOption.setText("AutoCreate ✔") 155 | else: 156 | self.autoCreateOption.setText("AutoCreate") 157 | 158 | def insertFromMimeData(self, source): 159 | # 只有在MIME数据中有文本时,才执行插入操作 160 | # Only perform insert operations if there is text in the MIME data 161 | if source.hasText(): 162 | # Get plain text from MIME data 163 | text = source.text() 164 | # Insert plain text 165 | self.insertPlainText(text) 166 | else: 167 | # For other types of data, the default behavior of the base class is invoked 168 | super(CustomTextEdit, self).insertFromMimeData(source) 169 | 170 | 171 | 172 | class DocViewer(PluginForm): 173 | class HexRaysEventHandler(ida_hexrays.Hexrays_Hooks): 174 | def __init__(self, docViewer): 175 | super().__init__() 176 | self.docViewer = docViewer 177 | 178 | def switch_pseudocode(self, vdui): 179 | name = demangle(idaapi.get_ea_name(vdui.cfunc.entry_ea)) 180 | name = normalize_name(name) 181 | self.docViewer.load_markdown(api_name_force = name) 182 | return 1 183 | 184 | 185 | def OnCreate(self, form): 186 | self.autoCreate = False 187 | """ 188 | defines widget layout 189 | """ 190 | self.api_name = None 191 | self.md_path = None 192 | 193 | self.parent = self.FormToPyQtWidget(form) 194 | self.main_layout = QtWidgets.QVBoxLayout() 195 | self.markdown_viewer_label = QtWidgets.QLabel() 196 | self.markdown_viewer_label.setText("IDA Notepad+") 197 | font = QFont("Microsoft YaHei") 198 | font.setBold(True) 199 | self.markdown_viewer_label.setFont(font) 200 | 201 | self.markdown_viewer = CustomTextEdit(self) 202 | self.markdown_viewer.setFontFamily("Courier") 203 | self.main_layout.addWidget(self.markdown_viewer_label) 204 | self.main_layout.addWidget(self.markdown_viewer) 205 | self.parent.setLayout(self.main_layout) 206 | self.load_markdown() 207 | 208 | self.pseudocodeSwitchEventHandler = self.HexRaysEventHandler(self) 209 | self._sync = False 210 | 211 | @property 212 | def sync(self): 213 | return self._sync 214 | 215 | @sync.setter 216 | def sync(self, new_value): 217 | if new_value: 218 | self.pseudocodeSwitchEventHandler.hook() 219 | v = ida_kernwin.get_current_viewer() 220 | # 判断是不是在伪代码窗口,如果是, 显示当前伪代码窗口中的函数 221 | # Determine whether it is in the pseudocode window. If so, display the function in the current pseudocode window. 222 | if idaapi.get_widget_type(v) == idaapi.BWN_PSEUDOCODE: 223 | vu = idaapi.get_widget_vdui(v) 224 | name = demangle(idaapi.get_ea_name(vu.cfunc.entry_ea)) 225 | name = normalize_name(name) 226 | self.load_markdown(api_name_force = name) 227 | else: 228 | self.pseudocodeSwitchEventHandler.unhook() 229 | self._sync = new_value 230 | 231 | def load_markdown(self, api_name_force = None): 232 | """ 233 | gets api and load corresponding (if present) api markdown 234 | """ 235 | self.save() 236 | 237 | self.api_name = api_name_force if api_name_force else get_selected_name() 238 | if not self.api_name: 239 | api_markdown ="#### Invalid Address Selected" 240 | self.markdown_viewer.setMarkdown(api_markdown) 241 | return 242 | self.markdown_viewer_label.setText(f"`{self.api_name}` doc") 243 | 244 | self.md_path = os.path.join(API_MD, clean_filename(self.api_name + ".md")) 245 | if os.path.isfile(self.md_path): 246 | with open(self.md_path, "r", encoding="utf-8") as infile: 247 | api_markdown = infile.read() 248 | else: 249 | if not self.autoCreate: 250 | btn_sel = idaapi.ask_yn(idaapi.ASKBTN_NO, f"{self.api_name}.md is not found, create new file or not?") 251 | if btn_sel == idaapi.ASKBTN_CANCEL or btn_sel == idaapi.ASKBTN_NO: 252 | api_markdown = "!!!File not found!!!" 253 | else: 254 | with open(self.md_path, "w", encoding="utf-8") as file: 255 | pass 256 | api_markdown = "" 257 | else: 258 | with open(self.md_path, "w", encoding="utf-8") as file: 259 | pass 260 | api_markdown = "" 261 | 262 | self.markdown_viewer.setText(api_markdown) 263 | 264 | def save(self): 265 | # Save the content of the QTextEdit back to the Markdown file 266 | # print(f"Sava back markdown content to {self.md_path}") 267 | if self.api_name and os.path.isfile(self.md_path): 268 | with open(self.md_path, "w", encoding="utf-8") as outfile: 269 | outfile.write(self.markdown_viewer.toPlainText()) 270 | 271 | 272 | def OnClose(self, form): 273 | """ 274 | Called when the widget is closed 275 | """ 276 | global frm 277 | global started 278 | 279 | self.save() 280 | self.pseudocodeSwitchEventHandler.unhook() 281 | # print("PseudocodeEventHandler unhook") 282 | 283 | del frm 284 | started = False 285 | 286 | class DocViewerPlugin(ida_idaapi.plugin_t): 287 | flags = ida_idaapi.PLUGIN_MOD 288 | comment = "Docs Viewer, substitude for IDA's notepad window" 289 | help = "" 290 | wanted_name = "Docs Viewer" 291 | wanted_hotkey = "Meta-Shift-]" 292 | 293 | def init(self): 294 | self.options = (ida_kernwin.PluginForm.WOPN_MENU | 295 | ida_kernwin.PluginForm.WOPN_ONTOP | 296 | ida_kernwin.PluginForm.WOPN_RESTORE | 297 | ida_kernwin.PluginForm.WOPN_PERSIST | 298 | ida_kernwin.PluginForm.WCLS_CLOSE_LATER) 299 | return ida_idaapi.PLUGIN_KEEP 300 | 301 | def run(self, arg): 302 | global started 303 | global frm 304 | if not started: 305 | #API_MD 306 | if not os.path.isdir(API_MD): 307 | print("ERROR: API_MD directory could not be found. Make sure to execute python run_me_first.py ") 308 | frm = DocViewer() 309 | frm.Show("Docs Viewer", options=self.options) 310 | started = True 311 | else: 312 | frm.load_markdown() 313 | 314 | def term(self): 315 | pass 316 | 317 | # ----------------------------------------------------------------------- 318 | def PLUGIN_ENTRY(): 319 | return DocViewerPlugin() 320 | --------------------------------------------------------------------------------