├── images └── image.png ├── LICENSE ├── README.md └── IDAComments.py /images/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoneShell/IDAComments/HEAD/images/image.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 OneShell 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 | # IDAComments 2 | an IDA plugin helps you to manage your IDA Comments. This repository is forked from [UserComment](https://github.com/JayRE114514/UserComment), and I made a little improvement to make it adapted to my workflow 3 | 4 | ![Plugin Preview](images/image.png) 5 | 6 | ## Note 7 | 8 | This plugin is implemented based on hooking, meaning it can only capture and save the user-added comments after the plugin is installed. 9 | 10 | Any user comments added prior to the installation of the plugin will not be captured. 11 | 12 | The plugin is written in Python 3, and it's suitable for IDA 7.x and IDA 8.x versions that use Python 3. For IDA 9.x, please test it yourself. 13 | 14 | ## Installation 15 | 16 | Copy the `UserComment.py` file to the `plugins` folder in the IDA installation directory. 17 | 18 | ## Usage 19 | 20 | There are three ways to open the comment window: 21 | 1. Choose 'View/Open subviews/Comments' from the menu 22 | 2. Use the shortcut (Ctrl-Shift-C) 23 | 3. Press `Ctrl-!`, then choose "Comments" 24 | 25 | Supports adding comments in the following format, and the comments will be automatically parsed into: 26 | ``` 27 | #tag comment 28 | ``` 29 | The comments will be automatically parsed and displayed according to the following fields: 30 | - Address: The address where the comment was added 31 | - Timestamp: The timestamp when the comment was first added 32 | - Tag: The comment’s tag 33 | - Comment: The actual content of the comment 34 | - Type: The type of the comment 35 | 36 | ## Features 37 | 38 | - Provides a comment window, displaying user-added comments, including comments in assembly code and pseudocode. 39 | - Support for different types of comments (common, repeatable, anterior, posterior, pseudocode and function comments). 40 | - Captured user-added comments will be preserved in the IDB. 41 | - Double-click on a comment entry to quickly navigate to the corresponding location. 42 | - Use IDA's built-in chooser, providing a handy filter functionality. 43 | 44 | ## Improvements 45 | - Add support for tagging comments, allowing you to categorize and sort them by tags such as #todo, #qu, #success, and more. 46 | - Design a dictionary structure to improve the plugin’s compatibility with potential future extensions. 47 | 48 | ## Contribution 49 | 50 | If you encounter any issues, have suggestions for improvements, or want to add new features, please submit an issue or a pull request. 51 | If you have any other questions, please feel free to ask. -------------------------------------------------------------------------------- /IDAComments.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import ida_idaapi 3 | import ida_kernwin 4 | import ida_idp 5 | import ida_netnode 6 | import idc 7 | import ida_bytes 8 | import ida_hexrays 9 | import ida_nalt 10 | import pickle 11 | from datetime import datetime 12 | import re 13 | 14 | title = "Comments" 15 | 16 | # def show_warning(msg): 17 | # ida_kernwin.warning(msg) 18 | 19 | class UserAddedComments(): 20 | def __init__(self): 21 | self.netnode = ida_netnode.netnode() 22 | self.netnode.create("$ UserAddedComments") 23 | self.imagebase = ida_nalt.get_imagebase() 24 | self.load_comments() 25 | 26 | def save_comments(self): 27 | blob = pickle.dumps(self.comments) 28 | self.netnode.setblob(blob, 0, 'C') 29 | 30 | def load_comments(self): 31 | blob = self.netnode.getblob(0, 'C') 32 | if blob is not None: 33 | self.comments = pickle.loads(blob) 34 | else: 35 | self.comments = {} 36 | 37 | def add_comment(self, ea, cmt_type, comment, line_num=None): 38 | offset = ea - self.imagebase 39 | key = (offset, cmt_type, line_num) 40 | 41 | timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") 42 | matches = re.search(r"#(\S+)", comment) 43 | if matches: 44 | tag = matches.group(1) 45 | else: 46 | tag = "" 47 | 48 | comment = re.sub(r"#\S+", "", comment).strip() 49 | 50 | if not comment: 51 | self.comments.pop(key, 0) 52 | else: 53 | value = { 54 | "comment" : comment, 55 | "timestamp" : timestamp, 56 | "tag" : tag 57 | } 58 | self.comments[key] = value 59 | self.save_comments() 60 | 61 | 62 | class UIHooks(ida_kernwin.UI_Hooks): 63 | def __init__(self, cmt_view): 64 | ida_kernwin.UI_Hooks.__init__(self) 65 | self.cmt_view = cmt_view 66 | 67 | def current_widget_changed(self, widget, prev_widget): 68 | if ida_kernwin.get_widget_title(widget) == title: 69 | self.cmt_view.Refresh() 70 | 71 | 72 | class PseudoHooks(ida_hexrays.Hexrays_Hooks): 73 | def __init__(self, usr_cmt): 74 | ida_hexrays.Hexrays_Hooks.__init__(self) 75 | self.usr_cmt = usr_cmt 76 | 77 | def cmt_changed(self, cfunc, loc, cmt): 78 | self.usr_cmt.add_comment(loc.ea, 'pseudocode', cmt) 79 | return 0 80 | 81 | 82 | class DisasmHooks(ida_idp.IDB_Hooks): 83 | def __init__(self, usr_cmt): 84 | ida_idp.IDB_Hooks.__init__(self) 85 | self.usr_cmt = usr_cmt 86 | self.rebased = False 87 | 88 | # hook common and repeatable cmts 89 | def changing_cmt(self, ea, is_repeatable, new_comment): 90 | cur_ea = idc.here() 91 | if cur_ea == ea: 92 | # solve start_ea problems 93 | cur = ida_kernwin.get_cursor() 94 | if (cur != (True, 0, 0)): 95 | # Fix rebasing bug: Rebase pragram will trigger 'changing_cmt', causing to capture auto cmts at ea. 96 | if self.rebased: 97 | self.rebased = False 98 | return 0 99 | if is_repeatable: 100 | self.usr_cmt.add_comment(ea, 'repeatable', new_comment) 101 | else: 102 | self.usr_cmt.add_comment(ea, 'common', new_comment) 103 | return 0 104 | 105 | # hook anterior and posterior cmts 106 | def extra_cmt_changed(self, ea, line_idx, cmt): 107 | cur_ea = idc.here() 108 | if cur_ea == ea: 109 | cur = ida_kernwin.get_cursor() 110 | if (cur != (True, 0, 0)): 111 | if line_idx // 1000 == 1: # line_idx = 1xxx 112 | self.usr_cmt.add_comment(ea, 'anterior', cmt, line_num=line_idx % 1000) 113 | if line_idx // 1000 == 2: # line_idx = 2xxx 114 | self.usr_cmt.add_comment(ea, 'posterior', cmt, line_num=line_idx % 1000) 115 | return 0 116 | 117 | # hook Function cmts and repeatable Function cmts 118 | def changing_range_cmt(self, kind, a, cmt, is_repeatable): 119 | if is_repeatable: 120 | self.usr_cmt.add_comment(a.start_ea, 'func_repeatable', cmt) 121 | else: 122 | self.usr_cmt.add_comment(a.start_ea, 'func_common', cmt) 123 | return 0 124 | 125 | # program image rebased 126 | def allsegs_moved(self, info): 127 | self.rebased = True 128 | self.usr_cmt.imagebase = ida_nalt.get_imagebase() 129 | 130 | 131 | class CommentViewer(ida_kernwin.Choose): 132 | def __init__(self, usr_cmt): 133 | self.column_map = ["Address", "Timestamp", "Tag", "Comments", "Type"] 134 | column_titles = [ 135 | ["Address", 10 | ida_kernwin.Choose.CHCOL_HEX], 136 | ["Timestamp", 15 | ida_kernwin.Choose.CHCOL_PLAIN], 137 | ["Tag", 10 | ida_kernwin.Choose.CHCOL_PLAIN], 138 | ["Comments", 30 | ida_kernwin.Choose.CHCOL_PLAIN], 139 | ["Type", 15 | ida_kernwin.Choose.CHCOL_PLAIN] 140 | ] 141 | ida_kernwin.Choose.__init__( 142 | self, 143 | title, 144 | column_titles, 145 | flags = ida_kernwin.Choose.CH_CAN_REFRESH) # Add closing parenthesis here 146 | self.usr_cmt = usr_cmt 147 | self.items = [] 148 | 149 | def OnInit(self): 150 | self.usr_cmt.load_comments() # load comments again 151 | self.items = [] 152 | for key, value in self.usr_cmt.comments.items(): 153 | row_data = { 154 | "Address": hex(key[0] + self.usr_cmt.imagebase), 155 | "Timestamp": value["timestamp"], 156 | "Tag": value["tag"], 157 | "Comments": value["comment"].split('\n')[0], 158 | "Type": key[1] 159 | } 160 | self.items.append([row_data[col] for col in self.column_map]) 161 | return True 162 | 163 | def OnGetSize(self): 164 | return len(self.items) 165 | 166 | def OnGetLine(self, n): 167 | return self.items[n] 168 | 169 | def OnRefresh(self, n): 170 | self.OnInit() 171 | if self.items: 172 | return [ida_kernwin.Choose.ALL_CHANGED] + self.adjust_last_item(n) 173 | return None # call standard refresh 174 | 175 | def OnSelectLine(self, n): 176 | selected_item = self.items[n] # for single selection chooser 177 | addr = int(selected_item[0], 16) 178 | ida_kernwin.jumpto(addr) 179 | 180 | 181 | def register_open_action(cmt_view): 182 | """ 183 | Provide the action that will create the widget 184 | when the user asks for it. 185 | """ 186 | class create_widget_t(ida_kernwin.action_handler_t): 187 | def activate(self, ctx): 188 | cmt_view.Show() 189 | 190 | def update(self, ctx): 191 | return ida_kernwin.AST_ENABLE_ALWAYS 192 | 193 | action_name = "UserAddedComments:Show" 194 | action_shortcut = "Ctrl-Shift-C" 195 | ida_kernwin.register_action( 196 | ida_kernwin.action_desc_t( 197 | action_name, 198 | title, 199 | create_widget_t(), 200 | action_shortcut)) 201 | ida_kernwin.attach_action_to_menu( 202 | f"View/Open subviews/{title}", 203 | action_name, 204 | ida_kernwin.SETMENU_APP) 205 | 206 | 207 | class my_plugin_t(ida_idaapi.plugin_t): 208 | flags = ida_idaapi.PLUGIN_HIDE # Plugin should not appear in the Edit, Plugins menu. 209 | wanted_name = "Hook and display user-added comments" 210 | wanted_hotkey = "" 211 | comment = "Hook and display user-added comments" 212 | help = "" 213 | 214 | def init(self): 215 | self.usr_cmt = UserAddedComments() # Create Comments instance here 216 | 217 | self.idb_hook = DisasmHooks(self.usr_cmt) # Hook disassembly comments(common, repeatable, anterior, posterior cmts) 218 | self.idb_hook.hook() 219 | 220 | self.ray_hook = PseudoHooks(self.usr_cmt) # Hook pseudo-code comments(cmts in "F5" pseudo-code view) 221 | self.ray_hook.hook() 222 | 223 | self.cmt_view = CommentViewer(self.usr_cmt) # Create comment viewer instance 224 | 225 | register_open_action(self.cmt_view) # Register to desktop widget and bind shortcut 226 | 227 | self.ui_hook = UIHooks(self.cmt_view) # Refresh commnets viewer in real time 228 | self.ui_hook.hook() 229 | return ida_idaapi.PLUGIN_KEEP # Keep us in the memory 230 | 231 | def run(self, arg): 232 | #self.cmt_view.Show() 233 | pass 234 | 235 | def term(self): 236 | self.ui_hook.unhook() 237 | self.ray_hook.unhook() 238 | self.idb_hook.unhook() 239 | return 240 | 241 | 242 | def PLUGIN_ENTRY(): 243 | return my_plugin_t() --------------------------------------------------------------------------------