├── .gitignore ├── LICENSE ├── README.md ├── plugins ├── lucid │ ├── __init__.py │ ├── core.py │ ├── microtext.py │ ├── text.py │ ├── ui │ │ ├── __init__.py │ │ ├── explorer.py │ │ ├── subtree.py │ │ └── sync.py │ └── util │ │ ├── __init__.py │ │ ├── hexrays.py │ │ ├── ida.py │ │ └── python.py └── lucid_plugin.py └── screenshots ├── lucid_demo.gif ├── lucid_granularity.gif ├── lucid_layers.gif ├── lucid_subtree.gif ├── lucid_title_card.png └── lucid_view_microcode.gif /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Markus Gaasedelen 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 | # Lucid - An Interactive Hex-Rays Microcode Explorer 2 | 3 |

4 | Lucid Plugin 5 |

6 | 7 | ## Overview 8 | 9 | Lucid is a developer-oriented [IDA Pro](https://www.hex-rays.com/products/ida/) plugin for exploring the Hex-Rays microcode. It was designed to provide a seamless, interactive experience for studying microcode transformations in the decompiler pipeline. 10 | 11 | This plugin is labeled only as a prototype & code resource for the community. Please note that it is a development aid, not a general purpose reverse engineering tool. 12 | 13 | Special thanks to [genmc](https://github.com/patois/genmc) / [@pat0is](https://twitter.com/pat0is) et al. for the inspiration. 14 | 15 | ## Releases 16 | 17 | * v0.1 -- Initial release 18 | 19 | ## Installation 20 | 21 | Lucid is a cross-platform (Windows, macOS, Linux) Python 2/3 plugin. It takes zero third party dependencies, making the code both portable and easy to install. 22 | 23 | 1. From your disassembler's python console, run the following command to find its plugin directory: 24 | - **IDA Pro**: `os.path.join(idaapi.get_user_idadir(), "plugins")` 25 | 26 | 2. Copy the contents of this repository's `/plugins/` folder to the listed directory. 27 | 3. Restart your disassembler. 28 | 29 | This plugin is only supported for IDA 7.5 and newer. 30 | 31 | ## Usage 32 | 33 | Lucid will automatically load for any architecture with a Hex-Rays decompiler present. Simply right click anywhere in a Pseudocode window and select `View microcode` to open the Lucid Microcode Explorer. 34 | 35 |

36 | View microcode 37 |

38 | 39 | By default, the Microcode Explorer will synchronize with the active Hex-Rays Pseudocode window. 40 | 41 | ## Lucid Layers 42 | 43 | Lucid makes it effortless to trace microinstructions through the entire decompiler pipeline. Simply select a microinstruction, and *scroll* (or click... if you must) through the microcode maturity layer list. 44 | 45 |

46 | Lucid Layer Traversal Demo 47 |

48 | 49 | Watch as the explorer stays focused on your selected instruction, while the surrounding microcode landscape melts away. It's basically magic. 50 | 51 | ## Sub-instruction Granularity 52 | 53 | Cursor tracing can operate at a sub-operand / sub-instruction level. Placing your cursor on different parts of the same microinstruction can trace sub-components back to their respective origins. 54 | 55 |

56 | Lucid Sub-instruction Granularity Demo 57 |

58 | 59 | If the instructions at the traced address get optimized away, Lucid will attempt to keep your cursor in the same approximate context. It will change the cursor color from green to red to indicate the loss of precision. 60 | 61 | ## Sub-instruction Trees 62 | 63 | As the Hex-Rays microcode increases in maturity, the decompilation pipeline begins to nest microcode as sub-instructions and sub-operands that form tree-based structures. 64 | 65 |

66 | Lucid Sub-instrution Graph Demo 67 |

68 | 69 | You can view these individual trees by right clicking an instruction and selecting `View subtree`. 70 | 71 | ## Known Bugs 72 | 73 | As this is the initial release, there will probably a number of small quirks and bugs. Here are a few known issues at the time of release: 74 | 75 | * While sync'd with hexrays, cursor mapping can get wonky if focused on microcode that gets optimized away 76 | * When opening the Sub-instruction Graph, window/tab focus can change unexpectedly 77 | * Microcode Explorer does not dock to the top-level far right compartment on Linux? 78 | * Switching between multiple Pseudocode windows in different functions might cause problems 79 | * Double clicking an instruction address comment can crash IDA if there is no suitable view to jump to 80 | * Plugin has not been tested robustly on Mac / Linux 81 | * ...? 82 | 83 | If you encounter any crashes or bad behavior, please file an issue. 84 | 85 | ## Future Work 86 | 87 | Time and motivation permitting, future work may include: 88 | 89 | * Clean up the code....... 90 | * Interactive sub-instruction graph generalization (to pattern_t / rules) 91 | * Microcode optimizer development workflow? 92 | * Microcode optimization manager? 93 | * Ctree explorer (and similar graph generalization stuff...) 94 | * Microcode hint text? 95 | * Improve layer translations 96 | * Improve performance 97 | * Migrate off IDA codeview? 98 | * ...? 99 | 100 | I welcome external contributions, issues, and feature requests. Please make any pull requests to the `develop` branch of this repository if you would like them to be considered for a future release. 101 | 102 | ## Authors 103 | 104 | * Markus Gaasedelen ([@gaasedelen](https://twitter.com/gaasedelen)) 105 | -------------------------------------------------------------------------------- /plugins/lucid/__init__.py: -------------------------------------------------------------------------------- 1 | from lucid.core import LucidCore -------------------------------------------------------------------------------- /plugins/lucid/core.py: -------------------------------------------------------------------------------- 1 | import ida_idaapi 2 | import ida_kernwin 3 | 4 | from lucid.util.ida import UIHooks, IDACtxEntry, hexrays_available 5 | from lucid.ui.explorer import MicrocodeExplorer 6 | 7 | #------------------------------------------------------------------------------ 8 | # Lucid Plugin Core 9 | #------------------------------------------------------------------------------ 10 | # 11 | # The plugin core constitutes the traditional 'main' plugin class. It 12 | # will host all of the plugin's objects and integrations, taking 13 | # responsibility for their initialization/teardown/lifetime. 14 | # 15 | # This pattern of splitting out the plugin core from the IDA plugin_t stub 16 | # is primarily to help separate the plugin functionality from IDA's and 17 | # make it easier to 'reload' for development / testing purposes. 18 | # 19 | 20 | class LucidCore(object): 21 | 22 | PLUGIN_NAME = "Lucid" 23 | PLUGIN_VERSION = "0.1.1" 24 | PLUGIN_AUTHORS = "Markus Gaasedelen" 25 | PLUGIN_DATE = "2020" 26 | 27 | def __init__(self, defer_load=False): 28 | self.loaded = False 29 | self.explorer = None 30 | 31 | # 32 | # we can 'defer' the load of the plugin core a little bit. this 33 | # ensures that all the other plugins (eg, decompilers) can get loaded 34 | # and initialized when opening an idb/bin 35 | # 36 | 37 | class UIHooks(ida_kernwin.UI_Hooks): 38 | def ready_to_run(self): 39 | pass 40 | 41 | self._startup_hooks = UIHooks() 42 | self._startup_hooks.ready_to_run = self.load 43 | 44 | if defer_load: 45 | self._startup_hooks.hook() 46 | return 47 | 48 | # plugin loading was not deferred (eg, hot reload), load immediately 49 | self.load() 50 | 51 | #------------------------------------------------------------------------- 52 | # Initialization / Teardown 53 | #------------------------------------------------------------------------- 54 | 55 | def load(self): 56 | """ 57 | Load the plugin core. 58 | """ 59 | self._startup_hooks.unhook() 60 | 61 | # the plugin will only load for decompiler-capabale IDB's / installs 62 | if not hexrays_available(): 63 | return 64 | 65 | # print plugin banner 66 | print("Loading %s v%s - (c) %s" % (self.PLUGIN_NAME, self.PLUGIN_VERSION, self.PLUGIN_AUTHORS)) 67 | 68 | # initialize the the plugin integrations 69 | self._init_action_view_microcode() 70 | self._install_hexrays_hooks() 71 | 72 | # all done, mark the core as loaded 73 | self.loaded = True 74 | 75 | def unload(self, from_ida=False): 76 | """ 77 | Unload the plugin core. 78 | """ 79 | 80 | # unhook just in-case load() was never actually called... 81 | self._startup_hooks.unhook() 82 | 83 | # if the core was never fully loaded, there's nothing else to do 84 | if not self.loaded: 85 | return 86 | 87 | print("Unloading %s..." % self.PLUGIN_NAME) 88 | 89 | # mark the core as 'unloaded' and teardown its components 90 | self.loaded = False 91 | 92 | self._remove_hexrays_hooks() 93 | self._del_action_view_microcode() 94 | 95 | #-------------------------------------------------------------------------- 96 | # UI Actions 97 | #-------------------------------------------------------------------------- 98 | 99 | def interactive_view_microcode(self, ctx=None): 100 | """ 101 | Open the Microcode Explorer window. 102 | """ 103 | current_address = ida_kernwin.get_screen_ea() 104 | if current_address == ida_idaapi.BADADDR: 105 | print("Could not open Microcode Explorer (bad cursor address)") 106 | return 107 | 108 | # 109 | # if the microcode window is open & visible, we should just refresh 110 | # it but at the current IDA cursor address 111 | # 112 | 113 | if self.explorer and self.explorer.view.visible: 114 | self.explorer.select_function(current_address) 115 | return 116 | 117 | # no microcode window in use, create a new one and show it 118 | self.explorer = MicrocodeExplorer() 119 | self.explorer.show(current_address) 120 | 121 | #-------------------------------------------------------------------------- 122 | # Action Registration 123 | #-------------------------------------------------------------------------- 124 | 125 | ACTION_VIEW_MICROCODE = "lucid:view_microcode" 126 | 127 | def _init_action_view_microcode(self): 128 | """ 129 | Register the 'View microcode' action with IDA. 130 | """ 131 | 132 | # describe the action 133 | action_desc = ida_kernwin.action_desc_t( 134 | self.ACTION_VIEW_MICROCODE, # The action name 135 | "View microcode", # The action text 136 | IDACtxEntry(self.interactive_view_microcode), # The action handler 137 | "Ctrl-Shift-M", # Optional: action shortcut 138 | "Open the Lucid Microcode Explorer", # Optional: tooltip 139 | -1 # Optional: the action icon 140 | ) 141 | 142 | # register the action with IDA 143 | assert ida_kernwin.register_action(action_desc), "Action registration failed" 144 | 145 | def _del_action_view_microcode(self): 146 | """ 147 | Delete the 'View microcode' action from IDA. 148 | """ 149 | ida_kernwin.unregister_action(self.ACTION_VIEW_MICROCODE) 150 | 151 | #-------------------------------------------------------------------------- 152 | # Hex-Rays Hooking 153 | #-------------------------------------------------------------------------- 154 | 155 | def _install_hexrays_hooks(self): 156 | """ 157 | Install the Hex-Rays hooks used by the plugin core. 158 | """ 159 | import ida_hexrays 160 | 161 | class CoreHxeHooks(ida_hexrays.Hexrays_Hooks): 162 | def populating_popup(_, *args): 163 | self._hxe_popuplating_popup(*args) 164 | return 0 165 | 166 | self._hxe_hooks = CoreHxeHooks() 167 | self._hxe_hooks.hook() 168 | 169 | def _remove_hexrays_hooks(self): 170 | """ 171 | Remove the Hex-Rays hooks used by the plugin core. 172 | """ 173 | self._hxe_hooks.unhook() 174 | self._hxe_hooks = None 175 | 176 | def _hxe_popuplating_popup(self, widget, popup, vdui): 177 | """ 178 | Handle a Hex-Rays popup menu event. 179 | 180 | When the user right clicks within a decompiler window, we use this 181 | callback to insert the 'View microcode' menu entry into the ctx menu. 182 | """ 183 | ida_kernwin.attach_action_to_popup( 184 | widget, 185 | popup, 186 | self.ACTION_VIEW_MICROCODE, 187 | None, 188 | ida_kernwin.SETMENU_APP 189 | ) 190 | 191 | #-------------------------------------------------------------------------- 192 | # Plugin Testing 193 | #-------------------------------------------------------------------------- 194 | 195 | def test(self): 196 | """ 197 | TODO/TESTING: move this to a dedicated module/file 198 | 199 | just some misc stuff for testing the plugin... 200 | """ 201 | import time 202 | import idautils 203 | from lucid.util.hexrays import get_mmat_levels, get_mmat_name 204 | 205 | for address in list(idautils.Functions()): 206 | 207 | print("0x%08X: DECOMPILING" % address) 208 | self.explorer.select_function(address) 209 | self.explorer.view.refresh() 210 | 211 | # change the codeview to a starting maturity levels 212 | for src_maturity in get_mmat_levels(): 213 | self.explorer.select_maturity(get_mmat_name(src_maturity)) 214 | 215 | # select each line in the current 'starting' maturity context 216 | for idx, line in enumerate(self.explorer.model.mtext.lines): 217 | self.explorer.select_position(idx, 0, 0) 218 | 219 | # 220 | maturity_traversal = get_mmat_levels() 221 | maturity_traversal = maturity_traversal[maturity_traversal.index(src_maturity)+1:] + get_mmat_levels()[::-1][1:] 222 | 223 | # scroll up / down the maturity traversal 224 | for dst_maturity in maturity_traversal: 225 | #print("%-60s -- %s" % ("S_MAT: %s E_MAT: %s IDX: %u" % (get_mmat_name(src_maturity), get_mmat_name(dst_maturity), idx), line.text)) 226 | self.explorer.select_maturity(get_mmat_name(dst_maturity)) 227 | #ida_kernwin.refresh_idaview_anyway() 228 | #time.sleep(0.05) 229 | 230 | self.explorer.select_maturity(get_mmat_name(src_maturity)) 231 | 232 | -------------------------------------------------------------------------------- /plugins/lucid/microtext.py: -------------------------------------------------------------------------------- 1 | import ida_lines 2 | import ida_idaapi 3 | import ida_hexrays 4 | 5 | from lucid.text import TextCell, TextToken, TextLine, TextBlock 6 | from lucid.util.ida import tag_text 7 | from lucid.util.hexrays import get_mmat_name 8 | 9 | #----------------------------------------------------------------------------- 10 | # Microtext 11 | #----------------------------------------------------------------------------- 12 | # 13 | # This file contains the microcode specific text (token) classes. Each 14 | # text class defined in this file roughly equates to a microcode 15 | # structure / class found in hexrays.hpp (the microcode SDK). 16 | # 17 | # The purpose of these microcode text classes is to 'wrap' the underlying 18 | # microcode structures, and print/render them as human readable text. More 19 | # importantly, these text structures provide a number of API's to map 20 | # the rendered text back to the underlying microcode objects. 21 | # 22 | # This text --> microcode object mapping is necessary for building an 23 | # interactive text interface that allows one to explore or manipulate 24 | # the microcode. For more information about the Text* classes, see text.py 25 | # 26 | 27 | #----------------------------------------------------------------------------- 28 | # Annotation Tokens 29 | #----------------------------------------------------------------------------- 30 | # 31 | # These 'annotation' tokens aren't wrappers around real microcode 32 | # structures, but provide auxillary information / interactive elements 33 | # to the rendered microcode text. 34 | # 35 | 36 | # TODO: ehh this should probably get refactored out 37 | MAGIC_BLK_INFO = 0x1230 38 | MAGIC_BLK_EDGE = 0x1231 39 | MAGIC_BLK_UDNR = 0x1232 40 | MAGIC_BLK_USE = 0x1233 41 | MAGIC_BLK_DEF = 0x1234 42 | MAGIC_BLK_DNU = 0x1235 43 | MAGIC_BLK_VAL = 0x1236 44 | MAGIC_BLK_TERM = 0x1237 45 | 46 | class BlockHeaderLine(TextLine): 47 | """ 48 | A line container for mblock_t comment/annotation tokens. 49 | """ 50 | 51 | def __init__(self, items, line_type, parent=None): 52 | super(BlockHeaderLine, self).__init__([TextCell("; ")] + items, line_type, parent) 53 | 54 | @property 55 | def tagged_text(self): 56 | return tag_text(super(BlockHeaderLine, self).tagged_text, ida_lines.COLOR_RPTCMT) 57 | 58 | class LinePrefixToken(TextCell): 59 | """ 60 | A token to display the relative position of a minsn_t within an mblock_t. 61 | """ 62 | 63 | def __init__(self, blk_idx, insn_idx, parent=None): 64 | prefix_text = "%d.%2d " % (blk_idx, insn_idx) 65 | tagged_text = tag_text(prefix_text, ida_lines.COLOR_PREFIX) 66 | super(LinePrefixToken, self).__init__(tagged_text, parent=parent) 67 | 68 | class BlockNumberToken(TextCell): 69 | """ 70 | An interactive token for mblock_t serial (blk_idx) references. 71 | """ 72 | 73 | def __init__(self, blk_idx, parent=None): 74 | tagged_text = tag_text(blk_idx, ida_lines.COLOR_MACRO) 75 | super(BlockNumberToken, self).__init__(tagged_text, parent=parent) 76 | self.blk_idx = blk_idx 77 | 78 | class AddressToken(TextCell): 79 | """ 80 | An interactive token for data/code-based text addresses. 81 | """ 82 | 83 | def __init__(self, address, prefix=False, parent=None): 84 | address_text = "0x%08X" % address if prefix else "%08X" % address 85 | super(AddressToken, self).__init__(address_text, parent=parent) 86 | self.target_address = address 87 | 88 | #------------------------------------------------------------------------------ 89 | # Microcode Operands (mop_t) 90 | #------------------------------------------------------------------------------ 91 | 92 | class MicroOperandToken(TextToken): 93 | """ 94 | High level text wrapper of a micro-operand (mop_t). 95 | """ 96 | 97 | def __init__(self, mop, items=None, parent=None): 98 | super(MicroOperandToken, self).__init__(mop._print(), items, parent) 99 | self.mop = mop 100 | self._generate_from_op() 101 | self._generate_token_ranges() 102 | 103 | def _generate_from_op(self): 104 | """ 105 | Populate this object from a mop_t. 106 | """ 107 | mop = self.mop 108 | 109 | # nested instruction 110 | if mop.is_insn(): 111 | self._create_subop(mop.d.l) 112 | self._create_subop(mop.d.r) 113 | self._create_subop(mop.d.d) 114 | self.address = mop.d.ea 115 | 116 | # call args 117 | elif mop.is_arglist(): 118 | for arg in mop.f.args: 119 | subop = self._create_subop(arg) 120 | if arg.ea == ida_idaapi.BADADDR: 121 | continue 122 | if subop.address != ida_idaapi.BADADDR: 123 | continue 124 | #assert (subop.address == ida_idaapi.BADADDR or subop.address == arg.ea), "sub: 0x%08X arg: 0x%08X" % (subop.address, arg.ea) 125 | subop.address = arg.ea 126 | 127 | # address of op 128 | elif mop.t == ida_hexrays.mop_a: 129 | self._create_subop(mop.a) 130 | 131 | # op pair 132 | elif mop.t == ida_hexrays.mop_p: 133 | self._create_subop(mop.pair.lop) 134 | self._create_subop(mop.pair.hop) 135 | 136 | # numbers 137 | elif mop.is_constant(): 138 | self.address = mop.nnn.ea 139 | 140 | def _create_subop(self, mop): 141 | """ 142 | Create a child op, from the given op. 143 | """ 144 | if mop.empty(): 145 | return None 146 | 147 | subop = MicroOperandToken(mop, parent=self) 148 | self.items.append(subop) 149 | 150 | return subop 151 | 152 | #------------------------------------------------------------------------------ 153 | # Microcode Instructions (minsn_t) 154 | #------------------------------------------------------------------------------ 155 | 156 | class MicroInstructionToken(TextToken): 157 | """ 158 | High level text wrapper of a micro-instruction (minsn_t). 159 | """ 160 | FLAGS = ida_hexrays.SHINS_VALNUM | ida_hexrays.SHINS_SHORT 161 | 162 | def __init__(self, insn, index, parent_token): 163 | super(MicroInstructionToken, self).__init__(insn._print(self.FLAGS), parent=parent_token) 164 | self.index = index 165 | self.insn = insn 166 | self._generate_from_insn() 167 | self._generate_token_ranges() 168 | 169 | def _generate_from_insn(self): 170 | """ 171 | Populate this object from a minsn_t. 172 | """ 173 | insn = self.insn 174 | 175 | # generate tree of ops / sub-ops and save them 176 | for mop in [insn.l, insn.r, insn.d]: 177 | self._create_subop(mop) 178 | 179 | # save a ref of the minsn_t for later use 180 | self.address = insn.ea 181 | 182 | def _create_subop(self, mop): 183 | """ 184 | Create a child op, from the given op. 185 | 186 | TODO: ripped from the op class... w/e 187 | """ 188 | if mop.empty(): 189 | return None 190 | 191 | subop = MicroOperandToken(mop, parent=self) 192 | self.items.append(subop) 193 | 194 | return subop 195 | 196 | class InstructionCommentToken(TextToken): 197 | """ 198 | A container token for micro-instruction comment text. 199 | """ 200 | 201 | def __init__(self, blk, insn, usedef=False): 202 | super(InstructionCommentToken, self).__init__() 203 | self._generate_from_ins(blk, insn, usedef) 204 | self._generate_token_ranges() 205 | 206 | def _generate_from_ins(self, blk, insn, usedef): 207 | """ 208 | Populate this object from a given minsn_t. 209 | """ 210 | items = [TextCell("; ")] 211 | 212 | # append the instruction address 213 | items.append(AddressToken(insn.ea)) 214 | 215 | # append the use/def list 216 | if usedef: 217 | use_def_tokens = self._generate_use_def(blk, insn) 218 | items.extend(use_def_tokens) 219 | 220 | # (re-)parent orphan tokens to this line 221 | for item in items: 222 | if not item.parent: 223 | item.parent = self 224 | 225 | # all done 226 | self.items = items 227 | 228 | def _generate_use_def(self, blk, insn): 229 | """ 230 | Generate use/def strings for this micro-instruction comment. 231 | """ 232 | items = [] 233 | 234 | # use list 235 | must_use = blk.build_use_list(insn, ida_hexrays.MUST_ACCESS) 236 | may_use = blk.build_use_list(insn, ida_hexrays.MAY_ACCESS) 237 | 238 | use_str = generate_mlist_str(must_use, may_use) 239 | items.append(TextCell(" u=%-13s" % use_str)) 240 | 241 | # def list 242 | must_def = blk.build_def_list(insn, ida_hexrays.MUST_ACCESS) 243 | may_def = blk.build_def_list(insn, ida_hexrays.MAY_ACCESS) 244 | def_str = generate_mlist_str(must_def, may_def) 245 | items.append(TextCell(" d=%-13s" % def_str)) 246 | 247 | return items 248 | 249 | #------------------------------------------------------------------------- 250 | # Properties 251 | #------------------------------------------------------------------------- 252 | 253 | @property 254 | def text(self): 255 | return ''.join([item.text for item in self.items]) 256 | 257 | @property 258 | def tagged_text(self): 259 | return tag_text(''.join([item.tagged_text for item in self.items]), ida_lines.COLOR_AUTOCMT) 260 | 261 | #------------------------------------------------------------------------------ 262 | # Microcode Block (mblock_t) 263 | #------------------------------------------------------------------------------ 264 | 265 | class MicroBlockText(TextBlock): 266 | """ 267 | High level text wrapper of a micro-block (mblock_t). 268 | """ 269 | 270 | def __init__(self, blk, verbose=False): 271 | super(MicroBlockText, self).__init__() 272 | self.instructions = [] 273 | self.verbose = verbose 274 | self.blk = blk 275 | self.refresh() 276 | 277 | def refresh(self, verbose=None): 278 | """ 279 | Regenerate the micro-block text. 280 | """ 281 | if verbose is not None: 282 | self.verbose = verbose 283 | self._generate_from_blk() 284 | self._generate_lines() 285 | self._generate_token_address_map() 286 | 287 | def _generate_from_blk(self): 288 | """ 289 | Populate this object from a mblock_t. 290 | """ 291 | insn, insn_idx = self.blk.head, 0 292 | instructions = [] 293 | 294 | # loop through all the instructions in this micro-block 295 | while insn and insn != self.blk.tail: 296 | 297 | # generate a token for the current top-instruction 298 | insn_token = MicroInstructionToken(insn, insn_idx, self) 299 | instructions.append(insn_token) 300 | 301 | # iterate to the next instruction 302 | insn, insn_idx = insn.next, insn_idx + 1 303 | 304 | # save a ref of the mblock_t for later use 305 | self.address = self.blk.start 306 | self.instructions = instructions 307 | 308 | def _generate_header_lines(self): 309 | """ 310 | Generate 'header' annotation lines for the mblock_t, similar to IDA's. 311 | """ 312 | blk, mba = self.blk, self.blk.mba 313 | lines = [] 314 | 315 | # block type names 316 | type_names = \ 317 | { 318 | ida_hexrays.BLT_NONE: "????", 319 | ida_hexrays.BLT_STOP: "STOP", 320 | ida_hexrays.BLT_0WAY: "0WAY", 321 | ida_hexrays.BLT_1WAY: "1WAY", 322 | ida_hexrays.BLT_2WAY: "2WAY", 323 | ida_hexrays.BLT_NWAY: "NWAY", 324 | ida_hexrays.BLT_XTRN: "XTRN", 325 | } 326 | 327 | blk_type = type_names[blk.type] 328 | 329 | # block properties 330 | prop_tokens = [] 331 | 332 | if blk.flags & ida_hexrays.MBL_DSLOT: 333 | prop_tokens.append(TextCell("DSLOT")) 334 | if blk.flags & ida_hexrays.MBL_NORET: 335 | prop_tokens.append(TextCell("NORET")) 336 | if blk.needs_propagation(): 337 | prop_tokens.append(TextCell("PROP")) 338 | if blk.flags & ida_hexrays.MBL_COMB: 339 | prop_tokens.append(TextCell("COMB")) 340 | if blk.flags & ida_hexrays.MBL_PUSH: 341 | prop_tokens.append(TextCell("PUSH")) 342 | if blk.flags & ida_hexrays.MBL_TCAL: 343 | prop_tokens.append(TextCell("TAILCALL")) 344 | if blk.flags & ida_hexrays.MBL_FAKE: 345 | prop_tokens.append(TextCell("FAKE")) 346 | 347 | # misc block info 348 | prop_tokens = [x for prop in prop_tokens for x in (prop, TextCell(" "))] 349 | shape_tokens = [TextCell("[START="), AddressToken(blk.start), TextCell(" END="), AddressToken(blk.end), TextCell("] "), TextCell("STK=%X/ARG=%X, MAXBSP: %X" % (blk.minbstkref, blk.minbargref, blk.maxbsp))] 350 | 351 | # assemble the 'main' block header line 352 | all_tokens = [TextCell("%s-BLOCK " % blk_type), BlockNumberToken(blk.serial), TextCell(" ")] + prop_tokens + shape_tokens 353 | lines.append(BlockHeaderLine(all_tokens, MAGIC_BLK_INFO, parent=self)) 354 | 355 | # inbound edges 356 | idx_tokens = [x for i in range(blk.npred()) for x in (BlockNumberToken(blk.pred(i)), TextCell(", "))][:-1] 357 | inbound_tokens = [TextCell("INBOUND: [")] + idx_tokens + [TextCell("] ")] if idx_tokens else [] 358 | 359 | # outbound edges 360 | idx_tokens = [x for i in range(blk.nsucc()) for x in (BlockNumberToken(blk.succ(i)), TextCell(", "))][:-1] 361 | outbound_tokens = [TextCell("OUTBOUND: [")] + idx_tokens + [TextCell("]")] if idx_tokens else [] 362 | 363 | # only emit the block inbound/outbound edges line if there are any... 364 | if inbound_tokens or outbound_tokens: 365 | edge_tokens = [TextCell("- ")] + inbound_tokens + outbound_tokens 366 | lines.append(BlockHeaderLine(edge_tokens, MAGIC_BLK_EDGE, parent=self)) 367 | 368 | # only generate use/def comments if in verbose mode 369 | if self.verbose: 370 | if not blk.lists_ready(): 371 | lines.append(BlockHeaderLine([TextCell("- USE-DEF LISTS ARE NOT READY")], MAGIC_BLK_UDNR, parent=self)) 372 | else: 373 | lines.extend(self._generate_use_def(blk)) 374 | 375 | return lines 376 | 377 | def _generate_use_def(self, blk): 378 | """ 379 | Generate use/def comments for this block. 380 | """ 381 | lines = [] 382 | 383 | # use list 384 | use_str = generate_mlist_str(blk.mustbuse, blk.maybuse) 385 | if use_str: 386 | lines.append(BlockHeaderLine([TextCell("- USE: %s" % use_str)], MAGIC_BLK_USE, parent=self)) 387 | 388 | # def list 389 | def_str = generate_mlist_str(blk.mustbdef, blk.maybdef) 390 | if def_str: 391 | lines.append(BlockHeaderLine([TextCell("- DEF: %s" % def_str)], MAGIC_BLK_DEF, parent=self)) 392 | 393 | # dnu list 394 | dnu_str = generate_mlist_str(blk.dnu) 395 | if dnu_str: 396 | lines.append(BlockHeaderLine([TextCell("- DNU: %s" % dnu_str)], MAGIC_BLK_DNU, parent=self)) 397 | 398 | return lines 399 | 400 | def _generate_token_line(self, idx, ins_token): 401 | """ 402 | Generate a block/index prefixed line for a given instruction token. 403 | """ 404 | prefix_token = LinePrefixToken(self.blk.serial, idx) 405 | cmt_token = InstructionCommentToken(self.blk, ins_token.insn, self.verbose) 406 | 407 | cmt_padding = max(50 - (len(prefix_token.text) + len(ins_token.text)), 1) 408 | padding_token = TextCell(" " * cmt_padding) 409 | 410 | # create the line 411 | line_token = TextLine(items=[prefix_token, ins_token, padding_token, cmt_token], parent=self) 412 | 413 | # give the line token the address of the associated instruction index 414 | line_token.address = self.instructions[idx].address 415 | 416 | # return the completed instruction line token 417 | return line_token 418 | 419 | def _generate_lines(self): 420 | """ 421 | Populate the line array for this mblock_t. 422 | """ 423 | lines, idx = [], 0 424 | 425 | # generate lines for the block header 426 | lines += self._generate_header_lines() 427 | 428 | # generate lines for the block instructions 429 | for idx, insn in enumerate(self.instructions): 430 | line_token = self._generate_token_line(idx, insn) 431 | lines.append(line_token) 432 | 433 | # add a blank line after the end of the block 434 | lines.append(TextLine(line_type=MAGIC_BLK_TERM, parent=self)) 435 | 436 | # save the list of generate lines to this text block 437 | self.lines = lines 438 | 439 | def get_special_line(self, line_type): 440 | """ 441 | Return the speical line from this block that matches the given line type. 442 | 443 | TODO: ehh, this 'speical line' stuff should probably get refactored 444 | """ 445 | for line in self.lines: 446 | if line.type == line_type: 447 | return line 448 | return None 449 | 450 | #------------------------------------------------------------------------------ 451 | # Microcode Text (mba_t) 452 | #------------------------------------------------------------------------------ 453 | 454 | class MicrocodeText(TextBlock): 455 | """ 456 | High level text wrapper of a micro-block-array (mba_t). 457 | """ 458 | 459 | def __init__(self, mba, verbose=False): 460 | super(MicrocodeText, self).__init__() 461 | self.verbose = verbose 462 | self.mba = mba 463 | self.refresh() 464 | 465 | def refresh(self, verbose=None): 466 | """ 467 | Regenerate the microcode text. 468 | """ 469 | if verbose is not None: 470 | self.verbose = verbose 471 | self._generate_from_mba() 472 | self._generate_lines() 473 | self._generate_token_address_map() 474 | 475 | def _generate_from_mba(self): 476 | """ 477 | Populate this object from a mba_t. 478 | """ 479 | blks = [] 480 | 481 | for blk_idx in range(self.mba.qty): 482 | blk = self.mba.get_mblock(blk_idx) 483 | blk_token = MicroBlockText(blk, self.verbose) 484 | blks.append(blk_token) 485 | 486 | self.blks = blks 487 | 488 | def _generate_lines(self): 489 | """ 490 | Populate the line array for this mba_t. 491 | """ 492 | lines = [] 493 | 494 | for blk in self.blks: 495 | lines.extend(blk.lines) 496 | 497 | self.lines = lines 498 | 499 | def get_block_for_line(self, line): 500 | """ 501 | Return the MicroBlockText containing the given line token. 502 | """ 503 | if not issubclass(type(line), TextLine): 504 | raise ValueError("Argument must be a line token type object") 505 | 506 | for blk_token in self.blks: 507 | if line in blk_token.lines: 508 | return blk_token 509 | 510 | return None 511 | 512 | def get_block_for_line_num(self, line_num): 513 | """ 514 | Return the MicroBlockText that owns the given line number. 515 | """ 516 | if not(line_num < len(self.lines)): 517 | return None 518 | return self.get_block_by_line(self.lines[line_num]) 519 | 520 | def get_ins_for_line(self, line): 521 | """ 522 | Return the MicroInstructionToken in the given line token. 523 | """ 524 | for item in line.items: 525 | if isinstance(item, MicroInstructionToken): 526 | return item 527 | return None 528 | 529 | def get_ins_for_line_num(self, line_num): 530 | """ 531 | Return the MicroInstructionToken at the given line number. 532 | """ 533 | return self.get_ines_for_line(self.lines[line_num]) 534 | 535 | #----------------------------------------------------------------------------- 536 | # Microtext Util 537 | #----------------------------------------------------------------------------- 538 | 539 | def generate_mlist_str(must, maybe=None): 540 | """ 541 | Generate the use/def string given must-use and maybe-use lists. 542 | """ 543 | must_regs = must.reg.dstr().split(",") 544 | must_mems = must.mem.dstr().split(",") 545 | 546 | maybe_regs = maybe.reg.dstr().split(",") if maybe else [] 547 | maybe_mems = maybe.mem.dstr().split(",") if maybe else [] 548 | 549 | for splits in [must_regs, must_mems, maybe_regs, maybe_mems]: 550 | splits[:] = list(filter(None, splits))[:] # lol 551 | 552 | maybe_regs = list(filter(lambda x: x not in must_regs, maybe_regs)) 553 | maybe_mems = list(filter(lambda x: x not in must_mems, maybe_mems)) 554 | 555 | must_str = ', '.join(must_regs + must_mems) 556 | maybe_str = ', '.join(maybe_regs + maybe_mems) 557 | 558 | if must_str and maybe_str: 559 | full_str = "%s (%s)" % (must_str, maybe_str) 560 | elif must_str: 561 | full_str = must_str 562 | elif maybe_str: 563 | full_str = "(%s)" % maybe_str 564 | else: 565 | full_str = "" 566 | 567 | return full_str 568 | 569 | def find_similar_block(blk_token_src, mtext_dst): 570 | """ 571 | Return a block from mtext_dst that is similar to the foreign blk_token_src. 572 | """ 573 | blk_src = blk_token_src.blk 574 | fallbacks = [] 575 | 576 | # search through all the blocks in the target mba/mtext for a similar block 577 | for blk_token_dst in mtext_dst.blks: 578 | blk_dst = blk_token_dst.blk 579 | 580 | # 1 for 1 block match (start addr, end addr) 581 | if (blk_dst.start == blk_src.start and blk_dst.end == blk_src.end): 582 | return blk_token_dst 583 | 584 | # matching block starts 585 | # TODO/COMMENT: explain the serial != 0 case 586 | elif (blk_dst.start == blk_src.start and blk_dst.serial != 0): 587 | fallbacks.append(blk_token_dst) 588 | 589 | # block got merged into another block 590 | elif (blk_dst.start < blk_src.start < blk_dst.end): 591 | fallbacks.append(blk_token_dst) 592 | 593 | # 594 | # there doesn't appear to be any blocks in this mtext that seem similar to 595 | # the given block. this should seldom happen.. if ever ? 596 | # 597 | 598 | if not fallbacks: 599 | return None 600 | 601 | # 602 | # return a fallback block, which is a 'similar' / related block but not 603 | # a 1-for-1 match with the given block. it is usually still a good result 604 | # as blocks generally transform as they move through the microcode layers 605 | # 606 | 607 | return fallbacks[0] 608 | 609 | #----------------------------------------------------------------------------- 610 | # Position Translation 611 | #----------------------------------------------------------------------------- 612 | # 613 | # These translation functions will try to 'strictly' translate the text 614 | # position of a cursor from one Microtext (a printed mba_t) to another. 615 | # 616 | # The goal of the translation (and remapping) functions is a best effort 617 | # attempt at following microcode blocks, instructions, or operands across 618 | # the entire maturity process. 619 | # 620 | # While this code is a bit messy right now... it's the source of the 621 | # magic behind lucid. 622 | # 623 | 624 | def translate_mtext_position(position, mtext_src, mtext_dst): 625 | """ 626 | Translate the given text position from one mtext to another. 627 | """ 628 | line_num, x, y = position 629 | 630 | # 631 | # while this isn't strictly required, let's enforce it. this basically 632 | # means that we won't allow you to translate a position from maturity 633 | # levels that are more than one degree apart. 634 | # 635 | # eg, no hopping from maturity 0 --> 7 instead, you must translate 636 | # through each layer 0 -> 1 -> 2 -> ... -> 7 637 | # 638 | 639 | assert abs(mtext_src.mba.maturity - mtext_dst.mba.maturity) <= 1 640 | 641 | # get the line the cursor falls on 642 | line = mtext_src.lines[line_num] 643 | 644 | # TODO: ehh should change this to 'special/generated lines' 645 | if line.type: 646 | return translate_block_header_position(position, mtext_src, mtext_dst) 647 | 648 | return translate_instruction_position(position, mtext_src, mtext_dst) 649 | 650 | def translate_block_header_position(position, mtext_src, mtext_dst): 651 | """ 652 | Translate a block-header position from one mtext to another. 653 | """ 654 | line_num, x, y = position 655 | 656 | # get the line the given position falls within on the source mtext 657 | line = mtext_src.lines[line_num] 658 | 659 | # get the block the given position falls within on the source mtext 660 | blk_token_src = mtext_src.get_block_for_line(line) 661 | 662 | # find a block in the dest mtext that seems to match the source block 663 | blk_token_dst = find_similar_block(blk_token_src, mtext_dst) 664 | 665 | if blk_token_dst: 666 | ins_src = set([x.address for x in blk_token_src.instructions]) 667 | ins_dst = set([x.address for x in blk_token_dst.instructions]) 668 | translate_header = (ins_src == ins_dst or blk_token_dst.blk.start == blk_token_src.blk.start) 669 | else: 670 | translate_header = False 671 | 672 | # 673 | # if we think we have found a suitable matching block, translate the given 674 | # position from the src block header to the destination one 675 | # 676 | 677 | if translate_header: 678 | 679 | # get the equivalent header line from the destination block 680 | line_dst = blk_token_dst.get_special_line(line.type) 681 | 682 | # 683 | # if a matching header line doesn't exist in the dest, attempt 684 | # to match the line 'depth' into the header instead.. this will 685 | # help with the illusion of the 'stationary' user cursor 686 | # 687 | 688 | if not line_dst: 689 | line_idx = blk_token_src.lines.index(line) 690 | 691 | try: 692 | 693 | line_dst = blk_token_dst.lines[line_idx] 694 | if not line_dst.type: 695 | raise 696 | # 697 | # either the destination block didn't have enough lines 698 | # to fufill the 'stationary' illusion, or the target 699 | # line wasn't a block header line. in these cases, just 700 | # fallback to mapping the cursor to the top of the block 701 | # 702 | 703 | except: 704 | line_dst = blk_token_dst.lines[0] 705 | 706 | # return the target/ 707 | line_num_dst = mtext_dst.lines.index(line_dst) 708 | return (line_num_dst, 0, y) 709 | 710 | # 711 | # if the block header the cursor was on in the source mtext has 712 | # been merged into another block in the dest mtext, we should try 713 | # to place the cursor address onto the 'first' instruction(s) 714 | # from the source block, which is now somewhere in the middle of 715 | # the 'dest' block 716 | # 717 | # since instructions can get discarded, we should try all of 718 | # them from the source block to find a viable 'donor' address 719 | # that we can remap onto 720 | # 721 | 722 | for ins_token in blk_token_src.instructions: 723 | tokens_dst = mtext_dst.get_tokens_for_address(ins_token.address) 724 | if tokens_dst: 725 | line_num, x = mtext_dst.get_pos_of_token(tokens_dst[0]) 726 | return (line_num, x, y) 727 | 728 | # 729 | # lol, so no instructions in the source block showed up in the 730 | # dest block that 'contains' it? let's just bail 731 | # 732 | 733 | return None 734 | 735 | def translate_instruction_position(position, mtext_src, mtext_dst): 736 | """ 737 | Translate an instruction position from one mtext to another. 738 | """ 739 | line_num, x, y = position 740 | token_src = mtext_src.get_token_at_position(line_num, x) 741 | address_src = mtext_src.get_address_at_position(line_num, x) 742 | 743 | # 744 | # find all the lines in the destination text that claim to contain the 745 | # current address 746 | # 747 | 748 | line_nums_dst = mtext_dst.get_line_nums_for_address(address_src) 749 | if not line_nums_dst: 750 | return None 751 | 752 | # get the line the given position falls within on the source mtext 753 | line = mtext_src.lines[line_num] 754 | 755 | # get the block the given position falls within on the source mtext 756 | blk_token_src = mtext_src.get_block_for_line(line) 757 | blk_src = blk_token_src.blk 758 | 759 | # find a block in the dest mtext that seems to match the source block 760 | blk_token_dst = find_similar_block(blk_token_src, mtext_dst) 761 | 762 | # 763 | # if a similar block was found in the destination mtext, that means we 764 | # want to search it and see if our address is still in the block. if 765 | # it is, those instances are probably going to be the most relevant to 766 | # our position in the source mtext. 767 | # 768 | 769 | if blk_token_dst: 770 | blk_dst = blk_token_dst.blk 771 | tokens = blk_token_dst.get_tokens_for_address(address_src) 772 | else: 773 | tokens = [] 774 | 775 | # 776 | # no tokens matching the target address in the 'similar' dest block (or 777 | # maybe there wasn't even a matching block), so we just fallback to 778 | # searching the whole dest mtext 779 | # 780 | 781 | if not tokens: 782 | tokens = mtext_dst.get_tokens_for_address(address_src) 783 | assert tokens, "This should never happen because line_nums_dst... ?" 784 | 785 | # compute the relative cursor address into the token text 786 | _, x_base_src = mtext_src.get_pos_of_token(token_src) 787 | x_rel = (x - x_base_src) 788 | 789 | # 1 for 1 token match 790 | for token in tokens: 791 | if token.text == token_src.text: 792 | line_num_dst, x_dst = mtext_dst.get_pos_of_token(token) 793 | x_dst += x_rel 794 | return (line_num_dst, x_dst, y) 795 | 796 | # common 'ancestor', eg the target token actually got its address from an ancestor 797 | token_src_ancestor = token_src.ancestor_with_address() 798 | for token in tokens: 799 | line_num, x_dst = mtext_dst.get_pos_of_token(token) 800 | if token.text == token_src_ancestor.text: 801 | line_num, x_dst = mtext_dst.get_pos_of_token(token) 802 | x_dst_base = token.text.index(token_src.text) 803 | x_dst += x_dst_base + x_rel # oof 804 | return (line_num, x_dst, y) 805 | 806 | # last ditch effort, try to land on a text that matches the target token 807 | for token in tokens: 808 | line_num, x_dst = mtext_dst.get_pos_of_token(token) 809 | if token_src.text in token.text: 810 | line_num, x_dst = mtext_dst.get_pos_of_token(token) 811 | x_dst_base = token.text.index(token_src.text) 812 | x_dst += x_dst_base + x_rel # oof 813 | return (line_num, x_dst, y) 814 | 815 | # yolo, just land on whatever token available 816 | line_num, x = mtext_dst.get_pos_of_token(tokens[0]) 817 | return (line_num, x, y) 818 | 819 | #----------------------------------------------------------------------------- 820 | # Position Remapping 821 | #----------------------------------------------------------------------------- 822 | # 823 | # Remapping functions are similar to the translation functions, but they 824 | # serve as a 'fallback' when a position translation cannot be guaranteed. 825 | # 826 | # For example, if a micro-instruction gets optimized away / discarded in 827 | # a later phase of the microcode maturation pipeline, there is no way we 828 | # can map the cursor to an instruction or block that no longer exists. 829 | # 830 | # In these cases, we attempt to 'remap' the cursor onto the closest 831 | # instruction / block to try and maintain a similar cursor context. 832 | # 833 | # Please note, these functions are also... kind of dirty at the moment. 834 | # 835 | 836 | def remap_mtext_position(position, mtext_src, mtext_dst): 837 | """ 838 | Remap the given position from one mtext to a *similar* position in another. 839 | """ 840 | line_num, x, y = position 841 | line = mtext_src.lines[line_num] 842 | 843 | # TODO: ehh should change this to 'speical/generated lines' 844 | if line.type: 845 | projection = remap_block_header_position(position, mtext_src, mtext_dst) 846 | else: 847 | projection = remap_instruction_position(position, mtext_src, mtext_dst) 848 | 849 | if projection: 850 | return projection 851 | 852 | # 853 | # translation & remapping REALLY failed... just try to maintain the same 854 | # viewport position I guess ? shouldn't really matter (or occur) often 855 | # 856 | 857 | line_max = len(mtext_dst.lines) 858 | if position[0] < line_max: 859 | line_num = position[0] 860 | else: 861 | line_num = max(line_max - 1, 0) 862 | 863 | return (line_num, position[1], position[2]) 864 | 865 | def remap_block_header_position(position, mtext_src, mtext_dst): 866 | """ 867 | Remap a block header position from one mtext to a *similar* position in another. 868 | """ 869 | line_num, x, y = position 870 | line = mtext_src.lines[line_num] 871 | 872 | # the block in the source mtext where the given position resides 873 | blk_token_src = mtext_src.get_block_for_line(line) 874 | 875 | blks_to_visit, blks_visited = [blk_token_src], [] 876 | while blks_to_visit: 877 | blk_token = blks_to_visit.pop(0) 878 | 879 | # ignore blocks we have already seen 880 | if blk_token in blks_visited: 881 | continue 882 | 883 | blk_token_dst = find_similar_block(blk_token, mtext_dst) 884 | if blk_token_dst: 885 | line_num, x = mtext_dst.get_pos_of_token(blk_token_dst.lines[0]) 886 | return (line_num, x, y) 887 | 888 | remap_tokens = [blk_token.instructions[0]] if blk_token.instructions else [] 889 | 890 | for token in remap_tokens: 891 | insn_line_num, insn_x = mtext_src.get_pos_of_token(token) 892 | projection = remap_instruction_position((insn_line_num, insn_x, y), mtext_src, mtext_dst) 893 | if projection: 894 | return (projection[0], projection[1], y) 895 | 896 | for blk_serial in range(blk_token.blk.nsucc()): 897 | blk_token_succ = mtext_src.blks[blk_token.blk.succ(blk_serial)] 898 | if blk_token_succ in blks_visited or blk_token_succ in blks_to_visit: 899 | continue 900 | blks_to_visit.append(blk_token_succ) 901 | 902 | return None 903 | 904 | def remap_instruction_position(position, mtext_src, mtext_dst): 905 | """ 906 | Remap an instruction position from one mtext to a *similar* position in another. 907 | """ 908 | line_num, x, y = position 909 | line = mtext_src.lines[line_num] 910 | 911 | # the block in the source mtext where the given position resides 912 | blk_token_src = mtext_src.get_block_for_line(line) 913 | blk_src = blk_token_src.blk 914 | 915 | ins_token_src = mtext_src.get_ins_for_line(line) 916 | pred_addresses = [x.address for x in blk_token_src.instructions[:ins_token_src.index]] 917 | succ_addresses = [x.address for x in blk_token_src.instructions[ins_token_src.index+1:]] 918 | remap_targets = succ_addresses + pred_addresses[::-1] 919 | 920 | for blk_serial in range(blk_src.nsucc()): 921 | remap_targets += [x.address for x in mtext_src.blks[blk_src.succ(blk_serial)].instructions] 922 | 923 | for address in remap_targets: 924 | new_tokens = mtext_dst.get_tokens_for_address(address) 925 | if new_tokens: 926 | line_num, x = mtext_dst.get_pos_of_token(new_tokens[0]) 927 | return (line_num, x, y) 928 | 929 | # 930 | # in this case, there have been no hits on *any* of the instructions in 931 | # the block... that means they are all gone 932 | # 933 | # in some cases, all the instructions in a block can get optimized away, 934 | # and an empty version of the block will be around for the next maturity 935 | # level, so let's see if we can find it... 936 | # 937 | 938 | blk_token_dst = find_similar_block(blk_token_src, mtext_dst) 939 | if not blk_token_dst: 940 | return None 941 | 942 | # 943 | # we found a matching block, but it is presumably empty... so we will 944 | # just return the text position of its first block header line 945 | # 946 | 947 | line_num, x = mtext_dst.get_pos_of_token(blk_token_dst.lines[0]) 948 | return (line_num, x, y) -------------------------------------------------------------------------------- /plugins/lucid/text.py: -------------------------------------------------------------------------------- 1 | import collections 2 | 3 | import ida_lines 4 | import ida_idaapi 5 | 6 | #----------------------------------------------------------------------------- 7 | # Text Abstractions 8 | #----------------------------------------------------------------------------- 9 | # 10 | # This file contains a number of 'text abstractions' that will serve as 11 | # the foundation of our interactive microcode text. 12 | # 13 | # These classes are primarily built on the notion of 'nesting' which 14 | # allows more complex text structures to composed of child text objects, 15 | # grouped, and traversed in various manners. 16 | # 17 | 18 | class TextCell(object): 19 | """ 20 | Base abstraction that all printable text classes will derive from. 21 | 22 | A text cell is the simplest and smallest text object. Think of it 23 | like a word in a paragraph. 24 | """ 25 | 26 | def __init__(self, text="", parent=None): 27 | self._text = ida_lines.tag_remove(text) 28 | self._tagged_text = text 29 | 30 | # public attributes 31 | self.parent = parent 32 | self.address = ida_idaapi.BADADDR 33 | 34 | def ancestor_with_address(self): 35 | """ 36 | Return the first parent of this cell that has a valid address. 37 | """ 38 | address = ida_idaapi.BADADDR 39 | token = self.parent 40 | 41 | # iterate upwards through the target token parents until an address is found 42 | while token: 43 | if token.address != ida_idaapi.BADADDR: 44 | return token 45 | token = token.parent 46 | 47 | # no ancestor of this token had a defined address... 48 | return None 49 | 50 | @property 51 | def text(self): 52 | """ 53 | Return a human-readable representation of this text cell. 54 | """ 55 | return self._text 56 | 57 | @property 58 | def tagged_text(self): 59 | """ 60 | Return a colored/formatted representation of this text cell. 61 | """ 62 | return self._tagged_text 63 | 64 | class TextToken(TextCell): 65 | """ 66 | A text element that can nest similar text-based elements. 67 | 68 | Tokens are more powerful than cells as they allow for nesting of cells, 69 | or other text tokens. Tokens cannot span more than one printable line, 70 | and do not natively generate text based on their child tokens. 71 | 72 | Classes derived from a TextToken can define custom behavior as to how 73 | their text should be generated (if necessary). 74 | """ 75 | 76 | def __init__(self, text="", items=None, parent=None): 77 | super(TextToken, self).__init__(text, parent) 78 | self.items = items if items else [] 79 | self._token_ranges = [] 80 | 81 | if items: 82 | self._generate_token_ranges() 83 | 84 | def _generate_token_ranges(self): 85 | """ 86 | Generate the text span indexes (start:end) for each child token. 87 | """ 88 | token_ranges = [] 89 | parsing_offset = 0 90 | 91 | for token in self.items: 92 | token_index = self.text[parsing_offset:].index(token.text) 93 | token_start = parsing_offset + token_index 94 | token_end = token_start + len(token.text) 95 | token_ranges.append((range(token_start, token_end), token)) 96 | parsing_offset = token_end 97 | 98 | self._token_ranges = token_ranges 99 | 100 | #------------------------------------------------------------------------- 101 | # Textual APIs 102 | #------------------------------------------------------------------------- 103 | 104 | def get_tokens_for_address(self, address): 105 | """ 106 | Return all (child) tokens matching the given address. 107 | """ 108 | found = [self] if self.address == address else [] 109 | for token in self.items: 110 | if not issubclass(type(token), TextToken): 111 | if token.address == address: 112 | found.append(token) 113 | continue 114 | found.extend(token.get_tokens_for_address(address)) 115 | return found 116 | 117 | def get_index_of_token(self, target_token): 118 | """ 119 | Return the index of the given (child) token into this token's text. 120 | """ 121 | if target_token == self: 122 | return 0 123 | 124 | for token_range, token in self._token_ranges: 125 | if token == target_token: 126 | return token_range[0] 127 | if not issubclass(type(token), TextToken): 128 | continue 129 | found = token.get_index_of_token(target_token) 130 | if found is not None: 131 | return found + token_range[0] 132 | 133 | return None 134 | 135 | def get_token_at_index(self, x_index): 136 | """ 137 | Return the (child) token at the given text index into this token's text. 138 | """ 139 | assert 0 <= x_index < len(self.text) 140 | 141 | # 142 | # search all the stored token text ranges for our child tokens to see 143 | # if the given index falls within any of them 144 | # 145 | 146 | for token_range, token in self._token_ranges: 147 | 148 | # skip 'blank' children 149 | if not token.text: 150 | continue 151 | 152 | if x_index in token_range: 153 | break 154 | 155 | # 156 | # if the given index does not fall within a child token range, the 157 | # given index must fall on text that makes up this token itself 158 | # 159 | 160 | else: 161 | return self 162 | 163 | # 164 | # if the matching child token does not derive from a TextToken, it is 165 | # probably a TextCell which cannot nest other tokens. so we can simply 166 | # return the found token as it is a leaf 167 | # 168 | 169 | if not issubclass(type(token), TextToken): 170 | return token 171 | 172 | # 173 | # the matching token must derive from a TextToken or something 174 | # capable of nesting tokens, so recurse downwards through the text 175 | # structure to see if there is a deeper, more precise token that 176 | # can be returned 177 | # 178 | 179 | return token.get_token_at_index(x_index - token_range[0]) 180 | 181 | def get_address_at_index(self, x_index): 182 | """ 183 | Return the mapped address of the given text index. 184 | """ 185 | token = self.get_token_at_index(x_index) 186 | 187 | # 188 | # iterate upwards through the parents of the targeted token until a 189 | # valid 'mapped' / inherited address can be returned 190 | # 191 | 192 | while token.address == ida_idaapi.BADADDR and token != self: 193 | token = token.parent 194 | 195 | # return the found address (or BADADDR...) 196 | return token.address 197 | 198 | class TextLine(TextToken): 199 | """ 200 | A line of printable text tokens. 201 | 202 | The main feature of this class (vs a TextToken) is that it will 203 | automatically generate its text based on its child tokens. This is done 204 | by simply sequentially joining the text of its child tokens into a line 205 | of printable text. 206 | """ 207 | 208 | def __init__(self, items=None, line_type=None, parent=None): 209 | super(TextLine, self).__init__(items=items, parent=parent) 210 | self.type = line_type 211 | 212 | # (re-)parent orphan tokens to this line 213 | for item in self.items: 214 | if not item.parent: 215 | item.parent = self 216 | 217 | #------------------------------------------------------------------------- 218 | # Properties 219 | #------------------------------------------------------------------------- 220 | 221 | @property 222 | def text(self): 223 | return ''.join([item.text for item in self.items]) 224 | 225 | @property 226 | def tagged_text(self): 227 | return ''.join([item.tagged_text for item in self.items]) 228 | 229 | #------------------------------------------------------------------------- 230 | # Textual APIs 231 | #------------------------------------------------------------------------- 232 | 233 | def get_token_at_index(self, x_index): 234 | """ 235 | Return the (child) token at the given text index into this token's text. 236 | 237 | This is overridden specifically to handle the case where an index past 238 | the end of the printable text line is given. In such cases, we simply 239 | return the TextLine itself, as the token at the given 'invalid' index. 240 | 241 | We do this because a 10 character TextLine might be rendered in a 242 | 100 character wide text / code window. 243 | """ 244 | if x_index >= len(self.text): 245 | return self 246 | token = super(TextLine, self).get_token_at_index(x_index) 247 | if not token: 248 | token = self 249 | return token 250 | 251 | class TextBlock(TextCell): 252 | """ 253 | A collection of tokens organized as lines, making a block of text. 254 | 255 | A TextBlock is analogous to a paragraph, or collection of TextLines. It 256 | provides a few helper functions to locate tokens / addresses using a 257 | given position in the form of (line_num, x) into the block. 258 | """ 259 | 260 | def __init__(self): 261 | super(TextBlock, self).__init__() 262 | self.lines = [] 263 | self._ea2token = {} 264 | self._line2token = {} 265 | 266 | def _generate_token_address_map(self): 267 | """ 268 | Generate a map of token --> address. 269 | """ 270 | to_visit = [] 271 | for line_idx, line in enumerate(self.lines): 272 | to_visit.append((line_idx, line)) 273 | 274 | line_map = collections.defaultdict(list) 275 | addr_map = collections.defaultdict(list) 276 | 277 | while to_visit: 278 | line_idx, token = to_visit.pop(0) 279 | line_map[line_idx].append(token) 280 | for subtoken in token.items: 281 | line_map[line_idx].append(subtoken) 282 | if not issubclass(type(subtoken), TextToken): 283 | continue 284 | to_visit.append((line_idx, subtoken)) 285 | if subtoken.address == ida_idaapi.BADADDR: 286 | continue 287 | addr_map[subtoken.address].append(subtoken) 288 | 289 | self._ea2token = addr_map 290 | self._line2token = line_map 291 | 292 | #------------------------------------------------------------------------- 293 | # Properties 294 | #------------------------------------------------------------------------- 295 | 296 | @property 297 | def text(self): 298 | return '\n'.join([line.text for line in self.lines]) 299 | 300 | @property 301 | def tagged_text(self): 302 | return '\n'.join([line.tagged_text for line in self.lines]) 303 | 304 | #------------------------------------------------------------------------- 305 | # Textual APIs 306 | #------------------------------------------------------------------------- 307 | 308 | def get_token_at_position(self, line_num, x_index): 309 | """ 310 | Return the token at the given text position. 311 | """ 312 | if not(0 <= line_num < len(self.lines)): 313 | return None 314 | return self.lines[line_num].get_token_at_index(x_index) 315 | 316 | def get_address_at_position(self, line_num, x_index): 317 | """ 318 | Return the mapped address of the given text position. 319 | """ 320 | if not(0 <= line_num < len(self.lines)): 321 | return ida_idaapi.BADADDR 322 | return self.lines[line_num].get_address_at_index(x_index) 323 | 324 | def get_pos_of_token(self, target_token): 325 | """ 326 | Return the text position of the given token. 327 | """ 328 | for line_num, tokens in self._line2token.items(): 329 | if target_token in tokens: 330 | return (line_num, self.lines[line_num].get_index_of_token(target_token)) 331 | return None 332 | 333 | def get_tokens_for_address(self, address): 334 | """ 335 | Return the list of tokens matching the given address. 336 | """ 337 | return self._ea2token.get(address, []) 338 | 339 | def get_line_nums_for_address(self, address): 340 | """ 341 | Return a list of line numbers which contain tokens matching the given address. 342 | """ 343 | line_nums = set() 344 | for line_idx, tokens in self._line2token.items(): 345 | for token in tokens: 346 | if token.address == address: 347 | line_nums.add(line_idx) 348 | return list(line_nums) 349 | 350 | def get_addresses_for_line_num(self, line_num): 351 | """ 352 | Return a list of addresses contained by tokens on the given line number. 353 | """ 354 | addresses = set() 355 | for token in self._line2token.get(line_num, []): 356 | addresses.add(token.address) 357 | return list(addresses) -------------------------------------------------------------------------------- /plugins/lucid/ui/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaasedelen/lucid/9f2480dc8e6bbb9421b5711533b0a98d2e9fb5af/plugins/lucid/ui/__init__.py -------------------------------------------------------------------------------- /plugins/lucid/ui/explorer.py: -------------------------------------------------------------------------------- 1 | import ctypes 2 | 3 | import ida_ida 4 | import ida_funcs 5 | import ida_graph 6 | import ida_idaapi 7 | import ida_kernwin 8 | import ida_hexrays 9 | 10 | from PyQt5 import QtWidgets, QtGui, QtCore, sip 11 | 12 | from lucid.ui.sync import MicroCursorHighlight 13 | from lucid.ui.subtree import MicroSubtreeView 14 | from lucid.util.python import register_callback, notify_callback 15 | from lucid.util.hexrays import get_microcode, get_mmat, get_mmat_name, get_mmat_levels 16 | from lucid.microtext import MicrocodeText, MicroInstructionToken, MicroOperandToken, AddressToken, BlockNumberToken, translate_mtext_position, remap_mtext_position 17 | 18 | #------------------------------------------------------------------------------ 19 | # Microcode Explorer 20 | #------------------------------------------------------------------------------ 21 | # 22 | # The Microcode Explorer UI is mostly implemented following a standard 23 | # Model-View-Controller pattern. This is a little abnormal for Qt, but 24 | # I've come to appreciate it more for its portability and testability. 25 | # 26 | 27 | class MicrocodeExplorer(object): 28 | """ 29 | The controller component of the microcode explorer. 30 | 31 | The role of the controller is to handle user gestures, map user actions to 32 | model updates, and change views based on controls. In theory, the 33 | controller should be able to drive the 'view' headlessly or simulate user 34 | UI interaction. 35 | """ 36 | 37 | def __init__(self): 38 | self.model = MicrocodeExplorerModel() 39 | self.view = MicrocodeExplorerView(self, self.model) 40 | self.view._code_sync.enable_sync(True) # XXX/HACK 41 | 42 | def show(self, address=None): 43 | """ 44 | Show the microcode explorer. 45 | """ 46 | if address is None: 47 | address = ida_kernwin.get_screen_ea() 48 | self.select_function(address) 49 | self.view.show() 50 | 51 | def show_subtree(self, insn_token): 52 | """ 53 | Show the sub-instruction graph for the given instruction token. 54 | """ 55 | graph = MicroSubtreeView(insn_token.insn) 56 | graph.show() 57 | 58 | # TODO/HACK: this is dumb, but moving it breaks my centering code so 59 | # i'll figure it out later... 60 | gv = ida_graph.get_graph_viewer(graph.GetWidget()) 61 | ida_graph.viewer_set_titlebar_height(gv, 15) 62 | 63 | #------------------------------------------------------------------------- 64 | # View Toggles 65 | #------------------------------------------------------------------------- 66 | 67 | def set_highlight_mutual(self, status): 68 | """ 69 | Toggle the highlighting of lines containing the same active address. 70 | """ 71 | if status: 72 | self.view._code_sync.hook() 73 | else: 74 | self.view._code_sync.unhook() 75 | ida_kernwin.refresh_idaview_anyway() 76 | 77 | def set_verbose(self, status): 78 | """ 79 | Toggle the verbosity of the printed microcode text. 80 | """ 81 | self.model.verbose = status 82 | ida_kernwin.refresh_idaview_anyway() 83 | 84 | #------------------------------------------------------------------------- 85 | # View Controls 86 | #------------------------------------------------------------------------- 87 | 88 | def select_function(self, address): 89 | """ 90 | Switch the microcode view to the specified function. 91 | """ 92 | func = ida_funcs.get_func(address) 93 | if not func: 94 | return False 95 | 96 | for maturity in get_mmat_levels(): 97 | mba = get_microcode(func, maturity) 98 | mtext = MicrocodeText(mba, self.model.verbose) 99 | self.model.update_mtext(mtext, maturity) 100 | 101 | self.view.refresh() 102 | ida_kernwin.refresh_idaview_anyway() 103 | return True 104 | 105 | def select_maturity(self, maturity_name): 106 | """ 107 | Switch the microcode view to the specified maturity level. 108 | """ 109 | self.model.active_maturity = get_mmat(maturity_name) 110 | #self.view.refresh() 111 | 112 | def select_address(self, address): 113 | """ 114 | Select a token in the microcode view matching the given address. 115 | """ 116 | tokens = self.model.mtext.get_tokens_for_address(address) 117 | if not tokens: 118 | return None 119 | 120 | token_line_num, token_x = self.model.mtext.get_pos_of_token(tokens[0]) 121 | rel_y = self.model.current_position[2] 122 | 123 | if self.model.current_position[2] == 0: 124 | rel_y = 30 125 | 126 | self.model.current_position = (token_line_num, token_x, rel_y) 127 | return tokens[0] 128 | 129 | def select_position(self, line_num, x, y): 130 | """ 131 | Select the given text position in the microcode view. 132 | """ 133 | self.model.current_position = (line_num, x, y) 134 | #print(" - hovered token: %s" % self.model.current_token.text) 135 | #print(" - hovered taddr: 0x%08X" % self.model.current_token.address) 136 | #print(" - hovered laddr: 0x%08X" % self.model.current_address) 137 | 138 | def activate_position(self, line_num, x, y): 139 | """ 140 | Activate (eg. double click) the given text position in the microcode view. 141 | """ 142 | token = self.model.mtext.get_token_at_position(line_num, x) 143 | 144 | if isinstance(token, AddressToken): 145 | ida_kernwin.jumpto(token.target_address, -1, 0) 146 | return 147 | 148 | if isinstance(token, BlockNumberToken) or (isinstance(token, MicroOperandToken) and token.mop.t == ida_hexrays.mop_b): 149 | blk_idx = token.blk_idx if isinstance(token, BlockNumberToken) else token.mop.b 150 | blk_token = self.model.mtext.blks[blk_idx] 151 | blk_line_num, _ = self.model.mtext.get_pos_of_token(blk_token.lines[0]) 152 | self.model.current_position = (blk_line_num, 0, y) 153 | self.view._code_view.Jump(*self.model.current_position) 154 | return 155 | 156 | class MicrocodeExplorerModel(object): 157 | """ 158 | The model component of the microcode explorer. 159 | 160 | The role of the model is to encapsulate application state, respond to 161 | state queries, and notify views of changes. Ideally, the model could be 162 | serialized / unserialized to save and restore state. 163 | """ 164 | 165 | def __init__(self): 166 | 167 | # 168 | # 'mtext' is short for MicrocodeText objects (see microtext.py) 169 | # 170 | # this dictionary will contain a mtext object (the renderable text 171 | # mapping of a given hexrays mba_t) for each microcode maturity level 172 | # of the current function. 173 | # 174 | # at any given time, one mtext will be 'active' in the model, and 175 | # therefore visible in the UI/Views 176 | # 177 | 178 | self._mtext = {x: None for x in get_mmat_levels()} 179 | 180 | # 181 | # there is a 'cursor' (ViewCursor) for each microcode maturity level / 182 | # mtext object. cursors don't actually contain the 'position' in the 183 | # rendered text (line_num, x), but also information to position the 184 | # cursor within the line view (y) 185 | # 186 | 187 | self._view_cursors = {x: None for x in get_mmat_levels()} 188 | 189 | # 190 | # the currently active / selected maturity level of the model. this 191 | # determines which mtext is currently visible / active in the 192 | # microcode view, and which cursor will be used 193 | # 194 | 195 | self._active_maturity = ida_hexrays.MMAT_GENERATED 196 | 197 | # this flag tracks the verbosity toggle state 198 | self._verbose = False 199 | 200 | #---------------------------------------------------------------------- 201 | # Callbacks 202 | #---------------------------------------------------------------------- 203 | 204 | self._mtext_refreshed_callbacks = [] 205 | self._position_changed_callbacks = [] 206 | self._maturity_changed_callbacks = [] 207 | 208 | #------------------------------------------------------------------------- 209 | # Read-Only Properties 210 | #------------------------------------------------------------------------- 211 | 212 | @property 213 | def mtext(self): 214 | """ 215 | Return the microcode text mapping for the current maturity level. 216 | """ 217 | return self._mtext[self._active_maturity] 218 | 219 | @property 220 | def current_line(self): 221 | """ 222 | Return the line token at the current viewport cursor position. 223 | """ 224 | if not self.mtext: 225 | return None 226 | line_num, _, _ = self.current_position 227 | return self.mtext.lines[line_num] 228 | 229 | @property 230 | def current_function(self): 231 | """ 232 | Return the current function address. 233 | """ 234 | if not self.mtext: 235 | return ida_idaapi.BADADDR 236 | return self.mtext.mba.entry_ea 237 | 238 | @property 239 | def current_token(self): 240 | """ 241 | Return the token at the current viewport cursor position. 242 | """ 243 | return self.mtext.get_token_at_position(*self.current_position[:2]) 244 | 245 | @property 246 | def current_address(self): 247 | """ 248 | Return the address at the current viewport cursor position. 249 | """ 250 | return self.mtext.get_address_at_position(*self.current_position[:2]) 251 | 252 | @property 253 | def current_cursor(self): 254 | """ 255 | Return the current viewport cursor. 256 | """ 257 | return self._view_cursors[self._active_maturity] 258 | 259 | #------------------------------------------------------------------------- 260 | # Mutable Properties 261 | #------------------------------------------------------------------------- 262 | 263 | @property 264 | def current_position(self): 265 | """ 266 | Return the current viewport cursor position (line_num, view_x, view_y). 267 | """ 268 | return self.current_cursor.viewport_position 269 | 270 | @current_position.setter 271 | def current_position(self, value): 272 | """ 273 | Set the cursor position of the viewport. 274 | """ 275 | self._gen_cursors(value, self.active_maturity) 276 | self._notify_position_changed() 277 | 278 | @property 279 | def verbose(self): 280 | """ 281 | Return the microcode verbosity status of the viewport. 282 | """ 283 | return self._verbose 284 | 285 | @verbose.setter 286 | def verbose(self, value): 287 | """ 288 | Set the verbosity of the microcode displayed by the viewport. 289 | """ 290 | if self._verbose == value: 291 | return 292 | 293 | # update the active verbosity setting 294 | self._verbose = value 295 | 296 | # verbosity must have changed, so force a mtext refresh 297 | self.refresh_mtext() 298 | 299 | @property 300 | def active_maturity(self): 301 | """ 302 | Return the active microcode maturity level. 303 | """ 304 | return self._active_maturity 305 | 306 | @active_maturity.setter 307 | def active_maturity(self, new_maturity): 308 | """ 309 | Set the active microcode maturity level. 310 | """ 311 | self._active_maturity = new_maturity 312 | self._notify_maturity_changed() 313 | 314 | #---------------------------------------------------------------------- 315 | # Misc 316 | #---------------------------------------------------------------------- 317 | 318 | def update_mtext(self, mtext, maturity): 319 | """ 320 | Set the mtext for a given microcode maturity level. 321 | """ 322 | self._mtext[maturity] = mtext 323 | self._view_cursors[maturity] = ViewCursor(0, 0, 0) 324 | 325 | def refresh_mtext(self): 326 | """ 327 | Regenerate the rendered text for all microcode maturity levels. 328 | 329 | TODO: This is a bit sloppy, and is basically only used for the 330 | verbosity toggle. 331 | """ 332 | for maturity, mtext in self._mtext.items(): 333 | if maturity == self.active_maturity: 334 | new_mtext = MicrocodeText(mtext.mba, self.verbose) 335 | self._mtext[maturity] = new_mtext 336 | self.current_position = translate_mtext_position(self.current_position, mtext, new_mtext) 337 | continue 338 | mtext.refresh(self.verbose) 339 | self._notify_mtext_refreshed() 340 | 341 | def _gen_cursors(self, position, mmat_src): 342 | """ 343 | Generate the cursors for all levels from a source position and maturity. 344 | """ 345 | mmat_levels = get_mmat_levels() 346 | mmat_first, mmat_final = mmat_levels[0], mmat_levels[-1] 347 | 348 | # clear out all the existing cursor mappings 349 | self._view_cursors = {x: None for x in mmat_levels} 350 | 351 | # save the starting cursor 352 | line_num, x, y = position 353 | self._view_cursors[mmat_src] = ViewCursor(line_num, x, y, True) 354 | 355 | # map the cursor backwards from the source maturity 356 | mmat_lower = range(mmat_first, mmat_src)[::-1] 357 | current_maturity = mmat_src 358 | for next_maturity in mmat_lower: 359 | self._transfer_cursor(current_maturity, next_maturity) 360 | current_maturity = next_maturity 361 | 362 | # map the cursor forward from the source maturity 363 | mmat_higher = range(mmat_src+1, mmat_final + 1) 364 | current_maturity = mmat_src 365 | for next_maturity in mmat_higher: 366 | self._transfer_cursor(current_maturity, next_maturity) 367 | current_maturity = next_maturity 368 | 369 | def _transfer_cursor(self, mmat_src, mmat_dst): 370 | """ 371 | Translate the cursor position from one maturity to the next. 372 | """ 373 | position = self._view_cursors[mmat_src].viewport_position 374 | mapped = self._view_cursors[mmat_src].mapped 375 | 376 | # attempt to translate the position in one mtext to another 377 | projection = translate_mtext_position(position, self._mtext[mmat_src], self._mtext[mmat_dst]) 378 | 379 | # if translation failed, we will generate an approximate cursor 380 | if not projection: 381 | mapped = False 382 | projection = remap_mtext_position(position, self._mtext[mmat_src], self._mtext[mmat_dst]) 383 | 384 | # save the generated cursor 385 | line_num, x, y = projection 386 | self._view_cursors[mmat_dst] = ViewCursor(line_num, x, y, mapped) 387 | 388 | #---------------------------------------------------------------------- 389 | # Callbacks 390 | #---------------------------------------------------------------------- 391 | 392 | def mtext_refreshed(self, callback): 393 | """ 394 | Subscribe a callback for mtext refresh events. 395 | """ 396 | register_callback(self._mtext_refreshed_callbacks, callback) 397 | 398 | def _notify_mtext_refreshed(self): 399 | """ 400 | Notify listeners of a mtext refresh event. 401 | """ 402 | notify_callback(self._mtext_refreshed_callbacks) 403 | 404 | def position_changed(self, callback): 405 | """ 406 | Subscribe a callback for cursor position changed events. 407 | """ 408 | register_callback(self._position_changed_callbacks, callback) 409 | 410 | def _notify_position_changed(self): 411 | """ 412 | Notify listeners of a cursor position changed event. 413 | """ 414 | notify_callback(self._position_changed_callbacks) 415 | 416 | def maturity_changed(self, callback): 417 | """ 418 | Subscribe a callback for maturity changed events. 419 | """ 420 | register_callback(self._maturity_changed_callbacks, callback) 421 | 422 | def _notify_maturity_changed(self): 423 | """ 424 | Notify listeners of a maturity changed event. 425 | """ 426 | notify_callback(self._maturity_changed_callbacks) 427 | 428 | #----------------------------------------------------------------------------- 429 | # UI Components 430 | #----------------------------------------------------------------------------- 431 | 432 | class MicrocodeExplorerView(QtWidgets.QWidget): 433 | """ 434 | The view component of the Microcode Explorer. 435 | """ 436 | 437 | WINDOW_TITLE = "Microcode Explorer" 438 | 439 | def __init__(self, controller, model): 440 | super(MicrocodeExplorerView, self).__init__() 441 | self.visible = False 442 | 443 | # the backing model, and controller for this view (eg, mvc pattern) 444 | self.model = model 445 | self.controller = controller 446 | 447 | # initialize the plugin UI 448 | self._ui_init() 449 | self._ui_init_signals() 450 | 451 | #-------------------------------------------------------------------------- 452 | # Pseudo Widget Functions 453 | #-------------------------------------------------------------------------- 454 | 455 | def show(self): 456 | self.refresh() 457 | 458 | # show the dockable widget 459 | flags = ida_kernwin.PluginForm.WOPN_DP_RIGHT | 0x200 # WOPN_SZHINT 460 | ida_kernwin.display_widget(self._twidget, flags) 461 | ida_kernwin.set_dock_pos(self.WINDOW_TITLE, "IDATopLevelDockArea", ida_kernwin.DP_RIGHT) 462 | 463 | self._code_sync.hook() 464 | 465 | def _cleanup(self): 466 | self.visible = False 467 | self._twidget = None 468 | self.widget = None 469 | self._code_sync.unhook() 470 | self._ui_hooks.unhook() 471 | # TODO cleanup controller / model 472 | 473 | #-------------------------------------------------------------------------- 474 | # Initialization - UI 475 | #-------------------------------------------------------------------------- 476 | 477 | def _ui_init(self): 478 | """ 479 | Initialize UI elements. 480 | """ 481 | self._ui_init_widget() 482 | 483 | # initialize our ui elements 484 | self._ui_init_list() 485 | self._ui_init_code() 486 | self._ui_init_settings() 487 | 488 | # layout the populated ui just before showing it 489 | self._ui_layout() 490 | 491 | def _ui_init_widget(self): 492 | """ 493 | Initialize an IDA widget for this UI control. 494 | """ 495 | 496 | # create a dockable widget, and save a reference to it for later use 497 | self._twidget = ida_kernwin.create_empty_widget(self.WINDOW_TITLE) 498 | 499 | # cast the IDA 'twidget' to a less opaque QWidget object 500 | self.widget = ida_kernwin.PluginForm.TWidgetToPyQtWidget(self._twidget) 501 | 502 | # hooks to help track the container/widget lifetime 503 | class ExplorerUIHooks(ida_kernwin.UI_Hooks): 504 | def widget_invisible(_, twidget): 505 | if twidget == self._twidget: 506 | self.visible = False 507 | self._cleanup() 508 | def widget_visible(_, twidget): 509 | if twidget == self._twidget: 510 | self.visible = True 511 | 512 | # install the widget lifetime hooks 513 | self._ui_hooks = ExplorerUIHooks() 514 | self._ui_hooks.hook() 515 | 516 | def _ui_init_list(self): 517 | """ 518 | Initialize the microcode maturity list. 519 | """ 520 | self._maturity_list = LayerListWidget() 521 | 522 | def _ui_init_code(self): 523 | """ 524 | Initialize the microcode view(s). 525 | """ 526 | self._code_view = MicrocodeView(self.model) 527 | self._code_sync = MicroCursorHighlight(self.controller, self.model) 528 | self._code_sync.track_view(self._code_view.widget) 529 | 530 | def _ui_init_settings(self): 531 | """ 532 | Initialize the explorer settings groupbox. 533 | """ 534 | self._checkbox_cursor = QtWidgets.QCheckBox("Highlight mutual") 535 | self._checkbox_cursor.setCheckState(QtCore.Qt.Checked) 536 | self._checkbox_verbose = QtWidgets.QCheckBox("Show use/def") 537 | self._checkbox_sync = QtWidgets.QCheckBox("Sync hexrays") 538 | self._checkbox_sync.setCheckState(QtCore.Qt.Checked) 539 | 540 | self._groupbox_settings = QtWidgets.QGroupBox("Settings") 541 | layout = QtWidgets.QVBoxLayout() 542 | layout.addWidget(self._checkbox_cursor) 543 | layout.addWidget(self._checkbox_verbose) 544 | layout.addWidget(self._checkbox_sync) 545 | self._groupbox_settings.setLayout(layout) 546 | 547 | def _ui_layout(self): 548 | """ 549 | Layout the major UI elements of the widget. 550 | """ 551 | layout = QtWidgets.QGridLayout() 552 | 553 | # arrange the widgets in a 'grid' row col row span col span 554 | layout.addWidget(self._code_view.widget, 0, 0, 0, 1) 555 | layout.addWidget(self._maturity_list, 0, 1, 1, 1) 556 | layout.addWidget(self._groupbox_settings, 1, 1, 1, 1) 557 | 558 | # apply the layout to the widget 559 | self.widget.setLayout(layout) 560 | 561 | def _ui_init_signals(self): 562 | """ 563 | Connect UI signals. 564 | """ 565 | self._maturity_list.currentItemChanged.connect(lambda x, y: self.controller.select_maturity(x.text())) 566 | self._code_view.connect_signals(self.controller) 567 | self._code_view.OnClose = self.hide # HACK 568 | 569 | # checkboxes 570 | self._checkbox_cursor.stateChanged.connect(lambda x: self.controller.set_highlight_mutual(bool(x))) 571 | self._checkbox_verbose.stateChanged.connect(lambda x: self.controller.set_verbose(bool(x))) 572 | self._checkbox_sync.stateChanged.connect(lambda x: self._code_sync.enable_sync(bool(x))) 573 | 574 | # model signals 575 | self.model.mtext_refreshed(self.refresh) 576 | self.model.maturity_changed(self.refresh) 577 | 578 | #-------------------------------------------------------------------------- 579 | # Misc 580 | #-------------------------------------------------------------------------- 581 | 582 | def refresh(self): 583 | """ 584 | Refresh the microcode explorer UI based on the model state. 585 | """ 586 | self._maturity_list.setCurrentRow(self.model.active_maturity - 1) 587 | self._code_view.refresh() 588 | 589 | class LayerListWidget(QtWidgets.QListWidget): 590 | """ 591 | The microcode maturity list widget 592 | """ 593 | 594 | def __init__(self): 595 | super(LayerListWidget, self).__init__() 596 | 597 | # populate the list widget with the microcode maturity levels 598 | self.addItems([get_mmat_name(x) for x in get_mmat_levels()]) 599 | 600 | # select the first maturity level, by default 601 | self.setCurrentRow(0) 602 | 603 | # make the list widget a fixed size, slightly wider than it needs to be 604 | width = self.sizeHintForColumn(0) 605 | self.setMaximumWidth(int(width + width * 0.10)) 606 | 607 | def wheelEvent(self, event): 608 | """ 609 | Handle mouse wheel scroll events. 610 | """ 611 | y = event.angleDelta().y() 612 | 613 | # scrolling down, clamp to last row 614 | if y < 0: 615 | next_row = min(self.currentRow()+1, self.count()-1) 616 | 617 | # scrolling up, clamp to first row (0) 618 | elif y > 0: 619 | next_row = max(self.currentRow()-1, 0) 620 | 621 | # horizontal scroll ? nothing to do.. 622 | else: 623 | return 624 | 625 | self.setCurrentRow(next_row) 626 | 627 | class MicrocodeView(ida_kernwin.simplecustviewer_t): 628 | """ 629 | An IDA-based text area that will render the Hex-Rays microcode. 630 | 631 | TODO: I'll probably rip this out in the future, as I'll have finer 632 | control over the interaction / implementation if I just roll my own 633 | microcode text widget. 634 | 635 | For that reason, excuse its hacky-ness / lack of comments. 636 | """ 637 | 638 | def __init__(self, model): 639 | super(MicrocodeView, self).__init__() 640 | self.model = model 641 | self.Create() 642 | 643 | def connect_signals(self, controller): 644 | self.controller = controller 645 | self.OnCursorPosChanged = lambda: controller.select_position(*self.GetPos()) 646 | self.OnDblClick = lambda _: controller.activate_position(*self.GetPos()) 647 | self.model.position_changed(self.refresh_cursor) 648 | 649 | def refresh(self): 650 | self.ClearLines() 651 | for line in self.model.mtext.lines: 652 | self.AddLine(line.tagged_text) 653 | self.refresh_cursor() 654 | 655 | def refresh_cursor(self): 656 | if not self.model.current_position: 657 | return 658 | self.Jump(*self.model.current_position) 659 | 660 | def Create(self): 661 | if not super(MicrocodeView, self).Create(None): 662 | return False 663 | self._twidget = self.GetWidget() 664 | self.widget = ida_kernwin.PluginForm.TWidgetToPyQtWidget(self._twidget) 665 | return True 666 | 667 | def OnClose(self): 668 | pass 669 | 670 | def OnCursorPosChanged(self): 671 | pass 672 | 673 | def OnDblClick(self, shift): 674 | pass 675 | 676 | def OnPopup(self, form, popup_handle): 677 | controller = self.controller 678 | 679 | # 680 | # so, i'm pretty picky about my UI / interactions. IDA puts items in 681 | # the right click context menus of custom (code) viewers. 682 | # 683 | # these items aren't really relevant (imo) to the microcode viewer, 684 | # so I do some dirty stuff here to filter them out and ensure only 685 | # my items will appear in the context menu. 686 | # 687 | # there's only one right click context item right now, but in the 688 | # future i'm sure there will be more. 689 | # 690 | 691 | class FilterMenu(QtCore.QObject): 692 | def __init__(self, qmenu): 693 | super(QtCore.QObject, self).__init__() 694 | self.qmenu = qmenu 695 | 696 | def eventFilter(self, obj, event): 697 | if event.type() != QtCore.QEvent.Polish: 698 | return False 699 | for action in self.qmenu.actions(): 700 | if action.text() in ["&Font...", "&Synchronize with"]: # lol.. 701 | qmenu.removeAction(action) 702 | self.qmenu.removeEventFilter(self) 703 | self.qmenu = None 704 | return True 705 | 706 | p_qmenu = ctypes.cast(int(popup_handle), ctypes.POINTER(ctypes.c_void_p))[0] 707 | qmenu = sip.wrapinstance(int(p_qmenu), QtWidgets.QMenu) 708 | self.filter = FilterMenu(qmenu) 709 | qmenu.installEventFilter(self.filter) 710 | 711 | # only handle right clicks on lines containing micro instructions 712 | ins_token = self.model.mtext.get_ins_for_line(self.model.current_line) 713 | if not ins_token: 714 | return False 715 | 716 | class MyHandler(ida_kernwin.action_handler_t): 717 | def activate(self, ctx): 718 | controller.show_subtree(ins_token) 719 | def update(self, ctx): 720 | return ida_kernwin.AST_ENABLE_ALWAYS 721 | 722 | # inject the 'View subtree' action into the right click context menu 723 | desc = ida_kernwin.action_desc_t(None, 'View subtree', MyHandler()) 724 | ida_kernwin.attach_dynamic_action_to_popup(form, popup_handle, desc, None) 725 | 726 | return True 727 | 728 | #----------------------------------------------------------------------------- 729 | # Util 730 | #----------------------------------------------------------------------------- 731 | 732 | class ViewCursor(object): 733 | """ 734 | TODO 735 | """ 736 | def __init__(self, line_num, x, y, mapped=True): 737 | self.line_num = line_num 738 | self.x = x 739 | self.y = y 740 | self.mapped = mapped 741 | 742 | @property 743 | def text_position(self): 744 | return (self.line_num, self.x) 745 | 746 | @property 747 | def viewport_position(self): 748 | return (self.line_num, self.x, self.y) 749 | -------------------------------------------------------------------------------- /plugins/lucid/ui/subtree.py: -------------------------------------------------------------------------------- 1 | import ida_graph 2 | import ida_moves 3 | import ida_hexrays 4 | import ida_kernwin 5 | 6 | from PyQt5 import QtWidgets, QtCore 7 | 8 | from lucid.util.hexrays import get_mcode_name, get_mopt_name 9 | 10 | #------------------------------------------------------------------------------ 11 | # Microinstruction Sub-trees 12 | #------------------------------------------------------------------------------ 13 | # 14 | # The Hex-Rays microcode can nest microinstructions into trees of sub- 15 | # instructions and sub-operands. Because of this, it can be useful to 16 | # unfold these trees visualize their components. 17 | # 18 | # This is particularly important when developing microcode plugins 19 | # to generalize expressions or identify graph / microcode patterns. 20 | # 21 | # For the time being, this file only serves as a crude viewer of a given 22 | # microinstruction subtree. But in the future, hopefully it can be 23 | # developed further towards an interactive graph / microcode pattern_t rule 24 | # editor through the generalization of a given graph. 25 | # 26 | # Please note, this is roughly based off code from genmc.py @ 27 | # - https://github.com/patois/genmc/blob/master/genmc.py 28 | # 29 | # TODO: This file is REALLY hacky/dirty at the moment, but I'll try to 30 | # clean it up when motivation and time permits..... 31 | # 32 | 33 | class MicroSubtreeView(ida_graph.GraphViewer): 34 | """ 35 | Render the subtree of an instruction. 36 | """ 37 | WINDOW_TITLE = "Sub-instruction Graph" 38 | 39 | def __init__(self, insn): 40 | super(MicroSubtreeView, self).__init__(self.WINDOW_TITLE, True) 41 | self.insn = insn 42 | self._populated = False 43 | 44 | def show(self): 45 | self.Show() 46 | ida_kernwin.set_dock_pos(self.WINDOW_TITLE, "Microcode Explorer", ida_kernwin.DP_INSIDE) 47 | 48 | # XXX: bit of a hack for now... lool 49 | QtCore.QTimer.singleShot(50, self._center_graph) 50 | 51 | def _center_graph(self): 52 | """ 53 | Center the sub-tree graph, and set an appropriate zoom level. 54 | """ 55 | widget = self.GetWidget() 56 | gv = ida_graph.get_graph_viewer(widget) 57 | g = ida_graph.get_viewer_graph(gv) 58 | 59 | ida_graph.viewer_fit_window(gv) 60 | ida_graph.refresh_viewer(gv) 61 | 62 | gli = ida_moves.graph_location_info_t() 63 | ida_graph.viewer_get_gli(gli, gv, ida_graph.GLICTL_CENTER) 64 | if gli.zoom > 1.5: 65 | gli.zoom = 1.5 66 | else: 67 | gli.zoom = gli.zoom * 0.9 68 | 69 | ida_graph.viewer_set_gli(gv, gli, ida_graph.GLICTL_CENTER) 70 | #ida_graph.refresh_viewer(gv) 71 | 72 | def _insert_mop(self, mop, parent): 73 | if mop.t == 0: 74 | return -1 75 | 76 | text = " " + get_mopt_name(mop.t) 77 | if mop.is_insn(): 78 | text += " (%s)" % get_mcode_name(mop.d.opcode) 79 | text += ' \n ' + mop._print() + " " 80 | node_id = self.AddNode(text) 81 | self.AddEdge(parent, node_id) 82 | 83 | # result of another instruction 84 | if mop.t == ida_hexrays.mop_d: 85 | insn = mop.d 86 | self._insert_mop(insn.l, node_id) 87 | self._insert_mop(insn.r, node_id) 88 | self._insert_mop(insn.d, node_id) 89 | 90 | # list of arguments 91 | elif mop.t == ida_hexrays.mop_f: 92 | for arg in mop.f.args: 93 | self._insert_mop(arg, node_id) 94 | 95 | # mop_addr_t: address of operand 96 | elif mop.t == ida_hexrays.mop_a: 97 | self._insert_mop(mop.a, node_id) 98 | 99 | # operand pair 100 | elif mop.t == ida_hexrays.mop_p: 101 | self._insert_mop(mop.pair.lop, node_id) 102 | self._insert_mop(mop.pair.hop, node_id) 103 | 104 | return node_id 105 | 106 | def _insert_insn(self, insn): 107 | if not insn: 108 | return None 109 | text = " %s \n %s " % (get_mcode_name(insn.opcode), insn._print()) 110 | node_id = self.AddNode(text) 111 | self._insert_mop(insn.l, node_id) 112 | self._insert_mop(insn.r, node_id) 113 | self._insert_mop(insn.d, node_id) 114 | return node_id 115 | 116 | def OnRefresh(self): 117 | if self._populated: 118 | return 119 | 120 | self.Clear() 121 | twidget = self.GetWidget() 122 | if not twidget: 123 | return False 124 | 125 | widget = ida_kernwin.PluginForm.TWidgetToPyQtWidget(twidget) 126 | bg_color = widget.property("line_bg_default") # disassembler bg color 127 | self._node_color = bg_color.blue() << 16 | bg_color.green() << 8 | bg_color.red() 128 | node_id = self._insert_insn(self.insn) 129 | self._populated = True 130 | return True 131 | 132 | def OnGetText(self, node_id): 133 | return (self._nodes[node_id], self._node_color) -------------------------------------------------------------------------------- /plugins/lucid/ui/sync.py: -------------------------------------------------------------------------------- 1 | import ida_hexrays 2 | import ida_kernwin 3 | from PyQt5 import QtWidgets 4 | 5 | from lucid.util.hexrays import get_all_vdui, map_line2citem, map_line2ea 6 | 7 | #------------------------------------------------------------------------------ 8 | # Microcode Cursor Syncing 9 | #------------------------------------------------------------------------------ 10 | # 11 | # TODO: This file is super messy/hacky and needs to be cleaned up. 12 | # 13 | # the TL;DR is that this file is responsible for 'syncing' the cursor 14 | # between our Microcode Explorer <--> Hex-Rays and highlighting the 15 | # relevant lines in each view. 16 | # 17 | # IDA provides mechanisms for syncing 'views', but none of that 18 | # infrastructure is really usable from idapython. for that reason, 19 | # we kind of implement our own which is probably for the best anyway. 20 | # 21 | 22 | class MicroCursorHighlight(object): 23 | 24 | class HxeHooks(ida_hexrays.Hexrays_Hooks): 25 | def curpos(self, vdui): 26 | pass 27 | def refresh_pseudocode(self, vdui): 28 | pass 29 | def close_pseudocode(self, vdui): 30 | pass 31 | 32 | class UIHooks(ida_kernwin.UI_Hooks): 33 | def get_lines_rendering_info(self, lines_out, widget, lines_in): 34 | pass 35 | 36 | def __init__(self, controller, model): 37 | self.model = model 38 | self.controller = controller 39 | 40 | self._item_maps = {} 41 | self._address_maps = {} 42 | self._hexrays_addresses = [] 43 | self._hexrays_origin = False 44 | self._sync_status = False 45 | self._last_vdui = None 46 | self._code_widget = None 47 | self._ignore_move = False 48 | 49 | # create hooks 50 | self._hxe_hooks = self.HxeHooks() 51 | self._ui_hooks = self.UIHooks() 52 | 53 | # link signals to this master class to help keep things uniform 54 | self._hxe_hooks.curpos = self.hxe_curpos 55 | self._hxe_hooks.refresh_pseudocode = self.hxe_refresh_pseudocode 56 | self._hxe_hooks.close_pseudocode = self.hxe_close_pseudocode 57 | self._ui_hooks.get_lines_rendering_info = self.render_lines 58 | self.model.position_changed(self.refresh_hexrays_cursor) 59 | 60 | def hook(self): 61 | self._ui_hooks.hook() 62 | 63 | def unhook(self): 64 | self._ui_hooks.unhook() 65 | self.enable_sync(False) 66 | 67 | def track_view(self, widget): 68 | self._code_widget = widget # TODO / temp 69 | 70 | def enable_sync(self, status): 71 | 72 | # nothing to do 73 | if status == self._sync_status: 74 | return 75 | 76 | # update sync status to enabled / disabled 77 | self._sync_status = status 78 | 79 | # syncing enabled 80 | if status: 81 | self._hxe_hooks.hook() 82 | self._cache_active_vdui() 83 | if self._last_vdui and (self.model.current_function != self._last_vdui.cfunc.entry_ea): 84 | self._sync_microtext(self._last_vdui) 85 | 86 | # syncing disabled 87 | else: 88 | self._hxe_hooks.unhook() 89 | self._hexrays_origin = False 90 | self._item_maps = {} 91 | self._address_maps = {} 92 | self._last_vdui = None 93 | 94 | self.refresh_hexrays_cursor() 95 | 96 | def refresh_hexrays_cursor(self): 97 | self._hexrays_origin = False 98 | self._hexrays_addresses = [] 99 | 100 | if not (self._sync_status and self._last_vdui): 101 | ida_kernwin.refresh_idaview_anyway() # TODO should this be here? 102 | return 103 | 104 | if not self.model.current_line or self.model.current_line.type: # special line 105 | ida_kernwin.refresh_idaview_anyway() # TODO should this be here? 106 | return 107 | 108 | vdui = self._last_vdui 109 | 110 | addr_map = self._get_vdui_address_map(vdui) 111 | current_address = self.model.current_address 112 | 113 | for line_num, addresses in addr_map.items(): 114 | if current_address in addresses: 115 | break 116 | else: 117 | self._hexrays_addresses = [] 118 | ida_kernwin.refresh_idaview_anyway() # TODO should this be here? 119 | return 120 | 121 | place, x, y = ida_kernwin.get_custom_viewer_place(self._last_vdui.ct, False) 122 | splace = ida_kernwin.place_t_as_simpleline_place_t(place) 123 | splace.n = line_num 124 | 125 | self._ignore_move = True 126 | ida_kernwin.jumpto(self._last_vdui.ct, splace, x, y) 127 | self._ignore_move = False 128 | 129 | self._hexrays_addresses = addr_map[line_num] 130 | ida_kernwin.refresh_idaview_anyway() # TODO should this be here? 131 | 132 | #-------------------------------------------------------------------------- 133 | # Signals 134 | #-------------------------------------------------------------------------- 135 | 136 | def hxe_close_pseudocode(self, vdui): 137 | """ 138 | (Event) A Hex-Rays pseudocode window was closed. 139 | """ 140 | if self._last_vdui == vdui: 141 | self._last_vdui = None 142 | self._item_maps.pop(vdui, None) 143 | self._address_maps.pop(vdui, None) 144 | return 0 145 | 146 | def hxe_refresh_pseudocode(self, vdui): 147 | """ 148 | (Event) A Hex-Rays pseudocode window was refreshed/changed. 149 | """ 150 | if self.model.current_function != vdui.cfunc.entry_ea: 151 | self._sync_microtext(vdui) 152 | return 0 153 | 154 | def hxe_curpos(self, vdui): 155 | """ 156 | (Event) The user cursor position changed in a Hex-Rays pseudocode window. 157 | """ 158 | self._hexrays_origin = False 159 | self._hexrays_addresses = self._get_active_vdui_addresses(vdui) 160 | 161 | if self.model.current_function != vdui.cfunc.entry_ea: 162 | self._sync_microtext(vdui) 163 | 164 | if self._ignore_move: 165 | # TODO put a refresh here ? 166 | return 0 167 | self._hexrays_origin = True 168 | if not self._hexrays_addresses: 169 | ida_kernwin.refresh_idaview_anyway() 170 | return 0 171 | self.controller.select_address(self._hexrays_addresses[0]) 172 | return 0 173 | 174 | def render_lines(self, lines_out, widget, lines_in): 175 | """ 176 | (Event) IDA is about to render code viewer lines. 177 | """ 178 | widget_type = ida_kernwin.get_widget_type(widget) 179 | if widget_type == ida_kernwin.BWN_PSEUDOCODE and self._sync_status: 180 | self._highlight_hexrays(lines_out, widget, lines_in) 181 | elif widget == self._code_widget: 182 | self._highlight_microcode(lines_out, widget, lines_in) 183 | return 184 | 185 | #-------------------------------------------------------------------------- 186 | # Vdui Helpers 187 | #-------------------------------------------------------------------------- 188 | 189 | def _cache_active_vdui(self): 190 | """ 191 | Enumerate and cache all the open Hex-Rays pseudocode windows (vdui). 192 | """ 193 | vdui_map = get_all_vdui() 194 | 195 | for name, vdui in vdui_map.items(): 196 | widget = ida_kernwin.PluginForm.TWidgetToPyQtWidget(vdui.ct) 197 | if widget.isVisible(): 198 | break 199 | else: 200 | return 201 | 202 | self._cache_vdui_maps(vdui) 203 | self._last_vdui = vdui 204 | 205 | def _cache_vdui_maps(self, vdui): 206 | """ 207 | Generate and cache the citem & address line maps for the given vdui. 208 | """ 209 | item_map = map_line2citem(vdui.cfunc.get_pseudocode()) 210 | self._item_maps[vdui] = item_map 211 | address_map = map_line2ea(vdui.cfunc, item_map) 212 | self._address_maps[vdui] = address_map 213 | return (address_map, item_map) 214 | 215 | def _get_active_vdui_addresses(self, vdui): 216 | """ 217 | Return the active addresses (current line) from the given vdui. 218 | """ 219 | address_map = self._get_vdui_address_map(vdui) 220 | return address_map[vdui.cpos.lnnum] 221 | 222 | def _get_vdui_address_map(self, vdui): 223 | """ 224 | Return the vdui line_num --> [ea1, ea2, ... ] address map. 225 | """ 226 | address_map = self._address_maps.get(vdui, None) 227 | if not address_map: 228 | address_map, _ = self._cache_vdui_maps(vdui) 229 | self._last_vdui = vdui 230 | return address_map 231 | 232 | def _get_vdui_item_map(self, vdui): 233 | """ 234 | Return the vdui line_num --> [citem_id1, citem_id2, ... ] citem map. 235 | """ 236 | item_map = self._item_maps.get(vdui, None) 237 | if not item_map: 238 | item_map, _ = self._cache_vdui_maps(vdui) 239 | self._last_vdui = vdui 240 | return item_map 241 | 242 | #-------------------------------------------------------------------------- 243 | # Misc 244 | #-------------------------------------------------------------------------- 245 | 246 | def _highlight_lines(self, lines_out, to_paint, lines_in): 247 | """ 248 | Highlight the IDA viewer line numbers specified in to_paint. 249 | """ 250 | assert len(lines_in.sections_lines) == 1, "Simpleviews should only have one section!?" 251 | color = ida_kernwin.CK_EXTRA1 if self.model.current_cursor.mapped else 0x400000FF 252 | for line in lines_in.sections_lines[0]: 253 | splace = ida_kernwin.place_t_as_simpleline_place_t(line.at) 254 | if splace.n in to_paint: 255 | entry = ida_kernwin.line_rendering_output_entry_t(line, ida_kernwin.LROEF_FULL_LINE, color) 256 | lines_out.entries.push_back(entry) 257 | to_paint.remove(splace.n) 258 | if not to_paint: 259 | break 260 | 261 | def _highlight_hexrays(self, lines_out, widget, lines_in): 262 | """ 263 | Highlight lines in the given Hex-Rays window according to the synchronized addresses. 264 | """ 265 | vdui = ida_hexrays.get_widget_vdui(widget) 266 | if self._hexrays_addresses or self._hexrays_origin: 267 | self._highlight_lines(lines_out, set([vdui.cpos.lnnum]), lines_in) 268 | 269 | def _highlight_microcode(self, lines_out, widget, lines_in): 270 | """ 271 | Highlight lines in the given microcode window according to the synchronized addresses. 272 | """ 273 | if not self.model.mtext.lines: 274 | return 275 | 276 | to_paint = set() 277 | 278 | # 279 | # hexrays syncing is enabled, use the addresses from the current 280 | # line to highlight all the microcode lines that contain any of 281 | # these 'target addresses' 282 | # 283 | 284 | if self._hexrays_origin or self._hexrays_addresses: 285 | target_addresses = self._hexrays_addresses 286 | 287 | # if not syncing with hexrays... 288 | else: 289 | 290 | # special case, only highlight the currently selected microcode line (a special line / block header) 291 | if self.model.current_line.type: 292 | to_paint.add(self.model.current_position[0]) 293 | target_addresses = [] 294 | 295 | # 'default' case, target all lines containing the address under the cursor 296 | else: 297 | target_addresses = [self.model.current_address] 298 | 299 | # 300 | # enumerate all the lines containing a target address, and mark it 301 | # for painting (save line idx to to_paint) 302 | # 303 | 304 | for address in target_addresses: 305 | for line_num in self.model.mtext.get_line_nums_for_address(address): 306 | 307 | # ignore special lines (eg, block header lines) 308 | if self.model.mtext.lines[line_num].type: 309 | continue 310 | 311 | to_paint.add(line_num) 312 | 313 | self._highlight_lines(lines_out, to_paint, lines_in) 314 | 315 | def _sync_microtext(self, vdui): 316 | """ 317 | TODO: this probably should just be a func in the controller 318 | """ 319 | self.controller.select_function(vdui.cfunc.entry_ea) 320 | self.controller.view.refresh() -------------------------------------------------------------------------------- /plugins/lucid/util/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaasedelen/lucid/9f2480dc8e6bbb9421b5711533b0a98d2e9fb5af/plugins/lucid/util/__init__.py -------------------------------------------------------------------------------- /plugins/lucid/util/hexrays.py: -------------------------------------------------------------------------------- 1 | import ida_lines 2 | import ida_idaapi 3 | import ida_kernwin 4 | import ida_hexrays 5 | 6 | #----------------------------------------------------------------------------- 7 | # Hex-Rays Util 8 | #----------------------------------------------------------------------------- 9 | 10 | def get_microcode(func, maturity): 11 | """ 12 | Return the mba_t of the given function at the specified maturity. 13 | """ 14 | mbr = ida_hexrays.mba_ranges_t(func) 15 | hf = ida_hexrays.hexrays_failure_t() 16 | ml = ida_hexrays.mlist_t() 17 | ida_hexrays.mark_cfunc_dirty(func.start_ea) 18 | mba = ida_hexrays.gen_microcode(mbr, hf, ml, ida_hexrays.DECOMP_NO_WAIT, maturity) 19 | if not mba: 20 | print("0x%08X: %s" % (hf.errea, hf.desc())) 21 | return None 22 | return mba 23 | 24 | def get_all_vdui(): 25 | """ 26 | Return every visible vdui_t (Hex-Rays window). 27 | """ 28 | found = {} 29 | 30 | # TODO: A-Z.. eh good enough 31 | for widget_title in ["Pseudocode-%c" % chr(0x41+i) for i in range(0, 26)]: 32 | 33 | # try to find the hexrays widget of the given name 34 | widget = ida_kernwin.find_widget(widget_title) 35 | if not widget: 36 | continue 37 | 38 | # make sure the widget looks in-use 39 | vdui = ida_hexrays.get_widget_vdui(widget) 40 | if not (vdui and vdui.visible): 41 | continue 42 | 43 | found[widget_title] = vdui 44 | 45 | return found 46 | 47 | #----------------------------------------------------------------------------- 48 | # Microcode Util 49 | #----------------------------------------------------------------------------- 50 | 51 | MMAT = sorted([(getattr(ida_hexrays, x), x) for x in filter(lambda y: y.startswith('MMAT_'), dir(ida_hexrays))])[1:] 52 | MOPT = [(getattr(ida_hexrays, x), x) for x in filter(lambda y: y.startswith('mop_'), dir(ida_hexrays))] 53 | MCODE = sorted([(getattr(ida_hexrays, x), x) for x in filter(lambda y: y.startswith('m_'), dir(ida_hexrays))]) 54 | 55 | class MatDelta: 56 | INCREASING = 1 57 | NEUTRAL = 0 58 | DECREASING = -1 59 | 60 | def get_mcode_name(mcode): 61 | """ 62 | Return the name of the given mcode_t. 63 | """ 64 | for value, name in MCODE: 65 | if mcode == value: 66 | return name 67 | return None 68 | 69 | def get_mopt_name(mopt): 70 | """ 71 | Return the name of the given mopt_t. 72 | """ 73 | for value, name in MOPT: 74 | if mopt == value: 75 | return name 76 | return None 77 | 78 | def get_mmat(mmat_name): 79 | """ 80 | Return the mba_maturity_t for the given maturity name. 81 | """ 82 | for value, name in MMAT: 83 | if name == mmat_name: 84 | return value 85 | return None 86 | 87 | def get_mmat_name(mmat): 88 | """ 89 | Return the maturity name of the given mba_maturity_t. 90 | """ 91 | for value, name in MMAT: 92 | if value == mmat: 93 | return name 94 | return None 95 | 96 | def get_mmat_levels(): 97 | """ 98 | Return a list of the microcode maturity levels. 99 | """ 100 | return list(map(lambda x: x[0], MMAT)) 101 | 102 | def diff_mmat(mmat_src, mmat_dst): 103 | """ 104 | Return an enum indicating maturity growth. 105 | """ 106 | direction = mmat_dst - mmat_src 107 | if direction > 0: 108 | return MatDelta.INCREASING 109 | if direction < 0: 110 | return MatDelta.DECREASING 111 | return MatDelta.NEUTRAL 112 | 113 | #------------------------------------------------------------------------------ 114 | # CTree Util 115 | #------------------------------------------------------------------------------ 116 | 117 | def map_line2citem(decompilation_text): 118 | """ 119 | Map decompilation line numbers to citems. 120 | 121 | This function allows us to build a relationship between citems in the 122 | ctree and specific lines in the hexrays decompilation text. 123 | 124 | Output: 125 | +- line2citem: 126 | | a map keyed with line numbers, holding sets of citem indexes 127 | | 128 | | eg: { int(line_number): sets(citem_indexes), ... } 129 | ' 130 | """ 131 | line2citem = {} 132 | 133 | # 134 | # it turns out that citem indexes are actually stored inline with the 135 | # decompilation text output, hidden behind COLOR_ADDR tokens. 136 | # 137 | # here we pass each line of raw decompilation text to our crappy lexer, 138 | # extracting any COLOR_ADDR tokens as citem indexes 139 | # 140 | 141 | for line_number in range(decompilation_text.size()): 142 | line_text = decompilation_text[line_number].line 143 | line2citem[line_number] = lex_citem_indexes(line_text) 144 | 145 | return line2citem 146 | 147 | def lex_citem_indexes(line): 148 | """ 149 | Lex all ctree item indexes from a given line of text. 150 | 151 | The HexRays decompiler output contains invisible text tokens that can 152 | be used to attribute spans of text to the ctree items that produced them. 153 | 154 | This function will simply scrape and return a list of all the these 155 | tokens (COLOR_ADDR) which contain item indexes into the ctree. 156 | """ 157 | i = 0 158 | indexes = [] 159 | line_length = len(line) 160 | 161 | # lex COLOR_ADDR tokens from the line of text 162 | while i < line_length: 163 | 164 | # does this character mark the start of a new COLOR_* token? 165 | if line[i] == ida_lines.COLOR_ON: 166 | 167 | # yes, so move past the COLOR_ON byte 168 | i += 1 169 | 170 | # is this sequence for a COLOR_ADDR? 171 | if ord(line[i]) == ida_lines.COLOR_ADDR: 172 | 173 | # yes, so move past the COLOR_ADDR byte 174 | i += 1 175 | 176 | # 177 | # A COLOR_ADDR token is followed by either 8, or 16 characters 178 | # (a hex encoded number) that represents an address/pointer. 179 | # in this context, it is actually the index number of a citem 180 | # 181 | 182 | ctree_anchor = int(line[i:i+ida_lines.COLOR_ADDR_SIZE], 16) 183 | if (ctree_anchor & ida_hexrays.ANCHOR_MASK) != ida_hexrays.ANCHOR_CITEM: 184 | continue 185 | 186 | i += ida_lines.COLOR_ADDR_SIZE 187 | 188 | # save the extracted citem index 189 | indexes.append(ctree_anchor) 190 | 191 | # skip to the next iteration as i has moved 192 | continue 193 | 194 | # nothing we care about happened, keep lexing forward 195 | i += 1 196 | 197 | # return all the citem indexes extracted from this line of text 198 | return indexes 199 | 200 | def map_line2ea(cfunc, line2citem): 201 | """ 202 | Map decompilation line numbers to addresses. 203 | """ 204 | line2ea = {} 205 | treeitems = cfunc.treeitems 206 | function_address = cfunc.entry_ea 207 | 208 | # 209 | # prior to this function, a line2citem map was built to tell us which 210 | # citems reside on any given line of text in the decompilation output. 211 | # 212 | # now, we walk through this line2citem map one 'line_number' at a time in 213 | # an effort to retrieve the addresses from each citem 214 | # 215 | 216 | for line_number, citem_indexes in line2citem.items(): 217 | addresses = set() 218 | 219 | # 220 | # we are at the level of a single line (line_number). we now consume 221 | # its set of citems (citem_indexes) and extract their addresses 222 | # 223 | 224 | for index in citem_indexes: 225 | 226 | # get the code address of the given citem 227 | try: 228 | item = treeitems[index] 229 | address = item.ea 230 | 231 | # TODO, ehh, omit these for now (curly braces, basically) 232 | if item.op == ida_hexrays.cit_block: 233 | continue 234 | 235 | # TODO 236 | except IndexError as e: 237 | print("BAD INDEX: 0x%08X" % index) 238 | continue 239 | 240 | # ignore citems with no address 241 | if address == ida_idaapi.BADADDR: 242 | continue 243 | 244 | addresses.add(address) 245 | 246 | line2ea[line_number] = list(addresses) 247 | 248 | # TODO explain special case 249 | if cfunc.mba.last_prolog_ea != ida_idaapi.BADADDR: 250 | line2ea[0] = list(range(cfunc.mba.entry_ea, cfunc.mba.last_prolog_ea+1)) 251 | 252 | # all done, return the computed map 253 | return line2ea -------------------------------------------------------------------------------- /plugins/lucid/util/ida.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import ida_lines 4 | import ida_netnode 5 | import ida_kernwin 6 | 7 | def hexrays_available(): 8 | """ 9 | Return True if an IDA decompiler is loaded and available for use. 10 | """ 11 | try: 12 | import ida_hexrays 13 | return ida_hexrays.init_hexrays_plugin() 14 | except ImportError: 15 | return False 16 | 17 | def tag_text(text, color): 18 | """ 19 | Return a 'tagged' (colored) version of the given string. 20 | """ 21 | return "%c%c%s%c%c" % (ida_lines.COLOR_ON, color, text, ida_lines.COLOR_OFF, color) 22 | 23 | def get_pdb_name(): 24 | """ 25 | Return the PDB filename as stored in the PE header. 26 | """ 27 | pe_nn = ida_netnode.netnode('$ PE header', 0, False) 28 | if pe_nn == ida_netnode.BADNODE: 29 | return "" 30 | 31 | pdb_filepath = pe_nn.supstr(0xFFFFFFFFFFFFFFF7) 32 | if not pdb_filepath: 33 | return "" 34 | 35 | pdb_name = os.path.basename(pdb_filepath) 36 | return pdb_name 37 | 38 | class IDACtxEntry(ida_kernwin.action_handler_t): 39 | """ 40 | A basic Context Menu class to utilize IDA's action handlers. 41 | """ 42 | 43 | def __init__(self, action_function): 44 | ida_kernwin.action_handler_t.__init__(self) 45 | self.action_function = action_function 46 | 47 | def activate(self, ctx): 48 | """ 49 | Execute the embedded action_function when this context menu is invoked. 50 | """ 51 | self.action_function(ctx) 52 | return 1 53 | 54 | def update(self, ctx): 55 | """ 56 | Ensure the context menu is always available in IDA. 57 | """ 58 | return ida_kernwin.AST_ENABLE_ALWAYS 59 | 60 | class UIHooks(ida_kernwin.UI_Hooks): 61 | def ready_to_run(self): 62 | pass 63 | -------------------------------------------------------------------------------- /plugins/lucid/util/python.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import weakref 4 | 5 | from types import ModuleType 6 | 7 | # py3/py2 compat 8 | try: 9 | from importlib import reload 10 | except: 11 | pass 12 | 13 | #------------------------------------------------------------------------------ 14 | # Python Callback / Signals 15 | #------------------------------------------------------------------------------ 16 | 17 | def register_callback(callback_list, callback): 18 | """ 19 | Register a callable function to the given callback_list. 20 | 21 | Adapted from http://stackoverflow.com/a/21941670 22 | """ 23 | 24 | # create a weakref callback to an object method 25 | try: 26 | callback_ref = weakref.ref(callback.__func__), weakref.ref(callback.__self__) 27 | 28 | # create a wweakref callback to a stand alone function 29 | except AttributeError: 30 | callback_ref = weakref.ref(callback), None 31 | 32 | # 'register' the callback 33 | callback_list.append(callback_ref) 34 | 35 | def notify_callback(callback_list, *args): 36 | """ 37 | Notify the given list of registered callbacks of an event. 38 | 39 | The given list (callback_list) is a list of weakref'd callables 40 | registered through the register_callback() function. To notify the 41 | callbacks of an event, this function will simply loop through the list 42 | and call them. 43 | 44 | This routine self-heals by removing dead callbacks for deleted objects as 45 | it encounters them. 46 | 47 | Adapted from http://stackoverflow.com/a/21941670 48 | """ 49 | cleanup = [] 50 | 51 | # 52 | # loop through all the registered callbacks in the given callback_list, 53 | # notifying active callbacks, and removing dead ones. 54 | # 55 | 56 | for callback_ref in callback_list: 57 | callback, obj_ref = callback_ref[0](), callback_ref[1] 58 | 59 | # 60 | # if the callback is an instance method, deference the instance 61 | # (an object) first to check that it is still alive 62 | # 63 | 64 | if obj_ref: 65 | obj = obj_ref() 66 | 67 | # if the object instance is gone, mark this callback for cleanup 68 | if obj is None: 69 | cleanup.append(callback_ref) 70 | continue 71 | 72 | # call the object instance callback 73 | try: 74 | callback(obj, *args) 75 | 76 | # assume a Qt cleanup/deletion occurred 77 | except RuntimeError as e: 78 | cleanup.append(callback_ref) 79 | continue 80 | 81 | # if the callback is a static method... 82 | else: 83 | 84 | # if the static method is deleted, mark this callback for cleanup 85 | if callback is None: 86 | cleanup.append(callback_ref) 87 | continue 88 | 89 | # call the static callback 90 | callback(*args) 91 | 92 | # remove the deleted callbacks 93 | for callback_ref in cleanup: 94 | callback_list.remove(callback_ref) 95 | 96 | #------------------------------------------------------------------------------ 97 | # Module Reloading 98 | #------------------------------------------------------------------------------ 99 | 100 | def reload_package(target_module): 101 | """ 102 | Recursively reload a 'stateless' python module / package. 103 | """ 104 | target_name = target_module.__name__ 105 | visited_modules = {target_name: target_module} 106 | _recurseive_reload(target_module, target_name, visited_modules) 107 | 108 | def _recurseive_reload(module, target_name, visited): 109 | ignore = ["__builtins__", "__cached__", "__doc__", "__file__", "__loader__", "__name__", "__package__", "__spec__", "__path__"] 110 | 111 | visited[module.__name__] = module 112 | 113 | for attribute_name in dir(module): 114 | 115 | # skip the stuff we don't care about 116 | if attribute_name in ignore: 117 | continue 118 | 119 | attribute_value = getattr(module, attribute_name) 120 | 121 | if type(attribute_value) == ModuleType: 122 | attribute_module_name = attribute_value.__name__ 123 | attribute_module = attribute_value 124 | #print("Found module %s" % attribute_module_name) 125 | elif callable(attribute_value): 126 | attribute_module_name = attribute_value.__module__ 127 | attribute_module = sys.modules[attribute_module_name] 128 | #print("Found callable...", attribute_name) 129 | elif isinstance(attribute_value, dict) or isinstance(attribute_value, list) or isinstance(attribute_value, int): 130 | #print("TODO: should probably try harder to reload this...", attribute_name, type(attribute_value)) 131 | continue 132 | else: 133 | #print("UNKNOWN TYPE TO RELOAD", attribute_name, type(attribute_value)) 134 | raise ValueError("OH NOO RELOADING IS HARD") 135 | 136 | if not target_name in attribute_module_name: 137 | #print(" - Not a module of interest...") 138 | continue 139 | 140 | if "__plugins__" in attribute_module_name: 141 | #print(" - Skipping IDA base plugin module...") 142 | continue 143 | 144 | if attribute_module_name in visited: 145 | continue 146 | 147 | #print("going down...") 148 | _recurseive_reload(attribute_module, target_name, visited) 149 | 150 | #print("Okay done with %s, reloading self!" % module.__name__) 151 | reload(module) -------------------------------------------------------------------------------- /plugins/lucid_plugin.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import ida_idaapi 4 | import ida_kernwin 5 | 6 | import lucid 7 | from lucid.util.python import reload_package 8 | 9 | def PLUGIN_ENTRY(): 10 | """ 11 | Required plugin entry point for IDAPython Plugins. 12 | """ 13 | return LucidPlugin() 14 | 15 | class LucidPlugin(ida_idaapi.plugin_t): 16 | 17 | # 18 | # Plugin flags: 19 | # - PLUGIN_PROC: Load/unload this plugin when an IDB opens / closes 20 | # - PLUGIN_HIDE: Hide this plugin from the IDA plugin menu 21 | # 22 | 23 | flags = ida_idaapi.PLUGIN_PROC | ida_idaapi.PLUGIN_HIDE 24 | comment = "Hex-Rays Microcode Explorer" 25 | help = "" 26 | wanted_name = "Lucid" 27 | wanted_hotkey = "" 28 | 29 | #-------------------------------------------------------------------------- 30 | # IDA Plugin Overloads 31 | #-------------------------------------------------------------------------- 32 | 33 | def init(self): 34 | """ 35 | This is called by IDA when it is loading the plugin. 36 | """ 37 | 38 | # initialize the plugin 39 | self.core = lucid.LucidCore(defer_load=True) 40 | 41 | # add lucid to the IDA python console scope, for test/dev/cli access 42 | sys.modules["__main__"].lucid = self 43 | 44 | # mark the plugin as loaded 45 | return ida_idaapi.PLUGIN_KEEP 46 | 47 | def run(self, arg): 48 | """ 49 | This is called by IDA when this file is loaded as a script. 50 | """ 51 | ida_kernwin.warning("%s cannot be run as a script in IDA." % self.wanted_name) 52 | 53 | def term(self): 54 | """ 55 | This is called by IDA when it is unloading the plugin. 56 | """ 57 | self.core.unload() 58 | 59 | #-------------------------------------------------------------------------- 60 | # Development Helpers 61 | #-------------------------------------------------------------------------- 62 | 63 | def reload(self): 64 | """ 65 | Hot-reload the plugin core. 66 | """ 67 | print("Reloading...") 68 | self.core.unload() 69 | reload_package(lucid) 70 | self.core = lucid.LucidCore() 71 | self.core.interactive_view_microcode() 72 | 73 | def test(self): 74 | """ 75 | Run some basic tests of the plugin core against this database. 76 | """ 77 | self.reload() 78 | self.core.test() 79 | -------------------------------------------------------------------------------- /screenshots/lucid_demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaasedelen/lucid/9f2480dc8e6bbb9421b5711533b0a98d2e9fb5af/screenshots/lucid_demo.gif -------------------------------------------------------------------------------- /screenshots/lucid_granularity.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaasedelen/lucid/9f2480dc8e6bbb9421b5711533b0a98d2e9fb5af/screenshots/lucid_granularity.gif -------------------------------------------------------------------------------- /screenshots/lucid_layers.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaasedelen/lucid/9f2480dc8e6bbb9421b5711533b0a98d2e9fb5af/screenshots/lucid_layers.gif -------------------------------------------------------------------------------- /screenshots/lucid_subtree.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaasedelen/lucid/9f2480dc8e6bbb9421b5711533b0a98d2e9fb5af/screenshots/lucid_subtree.gif -------------------------------------------------------------------------------- /screenshots/lucid_title_card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaasedelen/lucid/9f2480dc8e6bbb9421b5711533b0a98d2e9fb5af/screenshots/lucid_title_card.png -------------------------------------------------------------------------------- /screenshots/lucid_view_microcode.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaasedelen/lucid/9f2480dc8e6bbb9421b5711533b0a98d2e9fb5af/screenshots/lucid_view_microcode.gif --------------------------------------------------------------------------------