├── .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 |
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 |
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 |
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 |
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 |
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
--------------------------------------------------------------------------------