""" User Interface module This module exposes a number of tools and class definitions for interacting with IDA's user interface. This includes things such as getting the current state of user input, information about windows that are in use as well as utilities for simplifying the customization of the interface. There are a few namespaces that are provided in order to get the current state. The ``ui.current`` namespace allows for one to get the current address, function, segment, window, as well as a number of other things. A number of namespaces defined within this module also allows a user to interact with the different windows that are currently in use. This can allow for one to automatically show or hide a window that they wish to expose to the user. """ import six import sys, os, time, functools import logging import idaapi, internal import database as _database ## TODO: # locate window under current cursor position # pop-up a menu item # pop-up a form/messagebox # another item menu to toolbar # find the QAction associated with a command (or keypress) def application(): '''Return the current instance of the IDA Application.''' raise internal.exceptions.MissingMethodError def ask(string, **default): """Ask the user a question providing the option to choose "yes", "no", or "cancel". If any of the options are specified as a boolean, then it is assumed that this option will be the default. If the user chooses "cancel", then this value will be returned instead of the value ``None``. """ state = {'no': 0, 'yes': 1, 'cancel': -1} results = {0: False, 1: True} if default: keys = {n for n in default.viewkeys()} keys = {n.lower() for n in keys if default.get(n, False)} dflt = next((k for k in keys), 'cancel') else: dflt = 'cancel' res = idaapi.ask_yn(state[dflt], internal.utils.string.to(string)) return results.get(res, None) class current(object): """ This namespace contains tools for fetching information about the current selection state. This can be used to get the state of thigns that are currently selected such as the address, function, segment, clipboard, widget, or even the current window in use. """ @classmethod def address(cls): '''Return the current address.''' return idaapi.get_screen_ea() @classmethod def color(cls): '''Return the color of the current item.''' ea = cls.address() return idaapi.get_item_color(ea) @classmethod def function(cls): '''Return the current function.''' ea = cls.address() res = idaapi.get_func(ea) if res is None: raise internal.exceptions.FunctionNotFoundError(u"{:s}.function() : Unable to locate the current function.".format('.'.join((__name__, cls.__name__)))) return res @classmethod def segment(cls): '''Return the current segment.''' ea = cls.address() return idaapi.getseg(ea) @classmethod def status(cls): '''Return the IDA status.''' raise internal.exceptions.UnsupportedCapability(u"{:s}.status() : Unable to return the current status of IDA.".format('.'.join((__name__, cls.__name__)))) @classmethod def symbol(cls): '''Return the current highlighted symbol name.''' if idaapi.__version__ < 7.0: return idaapi.get_highlighted_identifier() # IDA 7.0 way of getting the currently selected text viewer = idaapi.get_current_viewer() res = idaapi.get_highlight(viewer) if res and res[1]: return res[0] return res @classmethod def selection(cls): '''Return the current address range of whatever is selected''' view = idaapi.get_current_viewer() left, right = idaapi.twinpos_t(), idaapi.twinpos_t() ok = idaapi.read_selection(view, left, right) if not ok: raise internal.exceptions.DisassemblerError(u"{:s}.selection() : Unable to read the current selection.".format('.'.join((__name__, cls.__name__)))) pl_l, pl_r = left.place(view), right.place(view) ea_l, ea_r = internal.interface.address.inside(pl_l.ea, pl_r.ea) return internal.interface.bounds_t(ea_l, ea_r) selected = internal.utils.alias(selection, 'current') @classmethod def opnum(cls): '''Return the currently selected operand number.''' return idaapi.get_opnum() @classmethod def widget(cls): '''Return the current widget that the mouse is hovering over.''' # XXX: there's probably a better way to do this rather than looking # at the mouse cursor position x, y = mouse.position() return widget.at((x, y)) @classmethod def window(cls): '''Return the current window that is being used.''' global window # FIXME: cast this to a QWindow somehow? return window.main() class state(object): """ This namespace is for fetching or interacting with the current state of IDA's interface. These are things such as waiting for IDA's analysis queue, or determining whether the function is being viewed in graph view or not. """ @classmethod def graphview(cls): '''Returns true if the current function is being viewed in graph view mode.''' res = idaapi.get_inf_structure() if idaapi.__version__ < 7.0: return res.graph_view != 0 return res.is_graph_view() @classmethod def wait(cls): '''Wait until IDA's autoanalysis queues are empty.''' return idaapi.autoWait() if idaapi.__version__ < 7.0 else idaapi.auto_wait() @classmethod def beep(cls): '''Beep using IDA's interface.''' return idaapi.beep() @classmethod def refresh(cls): '''Refresh all of IDA's windows.''' global disassembly ok = idaapi.refresh_lists() if idaapi.__version__ < 7.0 else idaapi.refresh_choosers() return ok and disassembly.refresh() wait, beep, refresh = internal.utils.alias(state.wait, 'state'), internal.utils.alias(state.beep, 'state'), internal.utils.alias(state.refresh, 'state') class appwindow(object): """ Base namespace used for interacting with the windows provided by IDA. """ @classmethod def open(cls, *args): '''Open or show the window belonging to the namespace.''' global widget res = cls.__open__(*args) if args else cls.__open__(*getattr(cls, '__open_defaults__', ())) return widget.form(res) @classmethod def close(cls): '''Close or hide the window belonging to the namespace.''' res = cls.open() return res.deleteLater() class disassembly(appwindow): """ This namespace is for interacting with the Disassembly window. """ __open__ = staticmethod(idaapi.open_disasm_window) __open_defaults__ = ('Disassembly', ) @classmethod def refresh(cls): '''Refresh the main IDA disassembly view.''' return idaapi.refresh_idaview_anyway() class exports(appwindow): """ This namespace is for interacting with the Exports window. """ __open__ = staticmethod(idaapi.open_exports_window) __open_defaults__ = (idaapi.BADADDR, ) class imports(appwindow): """ This namespace is for interacting with the Imports window. """ __open__ = staticmethod(idaapi.open_imports_window) __open_defaults__ = (idaapi.BADADDR, ) class names(appwindow): """ This namespace is for interacting with the Names window. """ __open__ = staticmethod(idaapi.open_names_window) __open_defaults__ = (idaapi.BADADDR, ) @classmethod def refresh(cls): '''Refresh the names list.''' return idaapi.refresh_lists() if idaapi.__version__ < 7.0 else idaapi.refresh_choosers() @classmethod def size(cls): '''Return the number of elements in the names list.''' return idaapi.get_nlist_size() @classmethod def contains(cls, ea): '''Return whether the address `ea` is referenced in the names list.''' return idaapi.is_in_nlist(ea) @classmethod def search(cls, ea): '''Return the index of the address `ea` in the names list.''' return idaapi.get_nlist_idx(ea) @classmethod def at(cls, index): '''Return the address and the symbol name of the specified `index`.''' ea, name = idaapi.get_nlist_ea(index), idaapi.get_nlist_name(index) return ea, internal.utils.string.of(name) @classmethod def name(cls, index): '''Return the name at the specified `index`.''' res = idaapi.get_nlist_name(index) return internal.utils.string.of(res) @classmethod def ea(cls, index): '''Return the address at the specified `index`.''' return idaapi.get_nlist_ea(index) @classmethod def iterate(cls): '''Iterate through all of the address and symbols in the names list.''' for idx in six.moves.range(cls.size()): yield cls.at(idx) return class functions(appwindow): """ This namespace is for interacting with the Functions window. """ __open__ = staticmethod(idaapi.open_funcs_window) __open_defaults__ = (idaapi.BADADDR, ) class structures(appwindow): """ This namespace is for interacting with the Structures window. """ __open__ = staticmethod(idaapi.open_structs_window) __open_defaults__ = (idaapi.BADADDR, 0) class strings(appwindow): """ This namespace is for interacting with the Strings window. """ __open__ = staticmethod(idaapi.open_strings_window) __open_defaults__ = (idaapi.BADADDR, idaapi.BADADDR, idaapi.BADADDR) @classmethod def __on_openidb__(cls, code, is_old_database): if code != idaapi.NW_OPENIDB or is_old_database: raise internal.exceptions.InvalidParameterError(u"{:s}.__on_openidb__({:#x}, {:b}) : Hook was called with an unexpected code or an old database.".format('.'.join((__name__, cls.__name__)), code, is_old_database)) config = idaapi.strwinsetup_t() config.minlen = 3 config.ea1, config.ea2 = idaapi.cvar.inf.minEA, idaapi.cvar.inf.maxEA config.display_only_existing_strings = True config.only_7bit = True config.ignore_heads = False # aggregate all the string types for IDA 6.95x if idaapi.__version__ < 7.0: res = [idaapi.ASCSTR_TERMCHR, idaapi.ASCSTR_PASCAL, idaapi.ASCSTR_LEN2, idaapi.ASCSTR_UNICODE, idaapi.ASCSTR_LEN4, idaapi.ASCSTR_ULEN2, idaapi.ASCSTR_ULEN4] # otherwise use IDA 7.x's naming scheme else: res = [idaapi.STRTYPE_TERMCHR, idaapi.STRTYPE_PASCAL, idaapi.STRTYPE_LEN2, idaapi.STRTYPE_C_16, idaapi.STRTYPE_LEN4, idaapi.STRTYPE_LEN2_16, idaapi.STRTYPE_LEN4_16] config.strtypes = reduce(lambda t, c: t | (2**c), res, 0) if not idaapi.set_strlist_options(config): raise internal.exceptions.DisassemblerError(u"{:s}.__on_openidb__({:#x}, {:b}) : Unable to set the default options for the string list.".format('.'.join((__name__, cls.__name__)), code, is_old_database)) #assert idaapi.build_strlist(config.ea1, config.ea2), "{:#x}:{:#x}".format(config.ea1, config.ea2) @classmethod def refresh(cls): '''Refresh the strings list.''' return idaapi.refresh_lists() if idaapi.__version__ < 7.0 else idaapi.refresh_choosers() @classmethod def size(cls): '''Return the number of elements in the strings list.''' return idaapi.get_strlist_qty() @classmethod def at(cls, index): '''Return the string at the specified `index`.''' si = idaapi.string_info_t() # FIXME: this isn't being used correctly ok = idaapi.get_strlist_item(si, index) if not ok: raise internal.exceptions.DisassemblerError(u"{:s}.at({:d}) : The call to `idaapi.get_strlist_item({:d})` returned {!r}.".format('.'.join((__name__, cls.__name__)), index, index, res)) return si @classmethod def get(cls, index): '''Return the address and the string at the specified `index`.''' si = cls.at(index) get_contents = idaapi.get_strlit_contents if hasattr(idaapi, 'get_strlit_contents') else idaapi.get_ascii_contents res = get_contents(si.ea, si.length, si.type) return si.ea, internal.utils.string.of(res) @classmethod def iterate(cls): '''Iterate through all of the address and strings in the strings list.''' for index in six.moves.range(cls.size()): yield cls.get(index) return class segments(appwindow): """ This namespace is for interacting with the Segments window. """ __open__ = staticmethod(idaapi.open_segments_window) __open_defaults__ = (idaapi.BADADDR, ) class notepad(appwindow): """ This namespace is for interacting with the Notepad window. """ __open__ = staticmethod(idaapi.open_notepad_window) __open_defaults__ = () class timer(object): """ This namespace is for registering a python callable to a timer in IDA. """ clock = {} @classmethod def register(cls, id, interval, callable): '''Register the specified `callable` with the requested `id` to be called at every `interval`.''' if id in cls.clock: idaapi.unregister_timer(cls.clock[id]) # XXX: need to create a closure that can terminate when signalled cls.clock[id] = res = idaapi.register_timer(interval, callable) return res @classmethod def unregister(cls, id): '''Unregister the specified `id`.''' raise internal.exceptions.UnsupportedCapability(u"{:s}.unregister({!s}) : A lock or a signal is needed here in order to unregister this timer safely.".format('.'.join((__name__, cls.__name__)), id)) idaapi.unregister_timer(cls.clock[id]) del(cls.clock[id]) @classmethod def reset(cls): '''Remove all the registered timers.''' for id, clk in six.iteritems(cls.clock): idaapi.unregister_timer(clk) del(cls.clock[id]) return ### updating the state of the colored navigation band class navigation(object): """ This namespace is for updating the state of the colored navigation band. """ if all(not hasattr(idaapi, name) for name in ['show_addr', 'showAddr']): __set__ = staticmethod(lambda ea: None) else: __set__ = staticmethod(idaapi.showAddr if idaapi.__version__ < 7.0 else idaapi.show_addr) if all(not hasattr(idaapi, name) for name in ['show_auto', 'showAuto']): __auto__ = staticmethod(lambda ea, t: None) else: __auto__ = staticmethod(idaapi.showAuto if idaapi.__version__ < 7.0 else idaapi.show_auto) @classmethod def set(cls, ea): '''Set the auto-analysis address on the navigation bar to `ea`.''' return cls.__set__(ea) @classmethod def auto(cls, ea, **type): """Set the auto-analysis address and type on the navigation bar to `ea`. If `type` is specified, then update using the specified auto-analysis type. """ return cls.__auto__(ea, type.get('type', idaapi.AU_NONE)) @classmethod def unknown(cls, ea): return cls.auto(ea, type=idaapi.AU_UNK) @classmethod def code(cls, ea): return cls.auto(ea, type=idaapi.AU_CODE) @classmethod def weak(cls, ea): return cls.auto(ea, type=idaapi.AU_WEAK) @classmethod def procedure(cls, ea): return cls.auto(ea, type=idaapi.AU_PROC) @classmethod def tail(cls, ea): return cls.auto(ea, type=idaapi.AU_TAIL) @classmethod def stackpointer(cls, ea): return cls.auto(ea, type=idaapi.AU_TRSP) @classmethod def analyze(cls, ea): return cls.auto(ea, type=idaapi.AU_USED) @classmethod def type(cls, ea): return cls.auto(ea, type=idaapi.AU_TYPE) @classmethod def signature(cls, ea): return cls.auto(ea, type=idaapi.AU_LIBF) @classmethod def final(cls, ea): return cls.auto(ea, type=idaapi.AU_FINAL) ### interfacing with IDA's menu system # FIXME: add some support for actually manipulating menus class menu(object): """ This namespace is for registering items in IDA's menu system. """ state = {} @classmethod def add(cls, path, name, callable, hotkey='', flags=0, args=()): '''Register a `callable` as a menu item at the specified `path` with the provided `name`.''' # check to see if our menu item is in our cache and remove it if so if (path, name) in cls.state: cls.rm(path, name) # now we can add the menu item since everything is ok # XXX: I'm not sure if the path needs to be utf8 encoded or not res = internal.utils.string.to(name) ctx = idaapi.add_menu_item(path, res, hotkey, flags, callable, args) cls.state[path, name] = ctx @classmethod def rm(cls, path, name): '''Remove the menu item at the specified `path` with the provided `name`.''' res = cls.state[path, name] idaapi.del_menu_item(res) del cls.state[path, name] @classmethod def reset(cls): '''Remove all currently registered menu items.''' for path, name in six.iterkeys(state): cls.rm(path, name) return ### Qt wrappers and namespaces class window(object): """ This namespace is for selecting a specific or particular window. """ @classmethod def viewer(cls): '''Return the current viewer.''' return idaapi.get_current_viewer() @classmethod def main(cls): '''Return the active main window.''' global application q = application() return q.activeWindow() class windows(object): """ Interact with any or all of the top-level windows for the application. """ def __new__(cls): '''Return all of the top-level windows.''' global application q = application() return q.topLevelWindows() class widget(object): """ This namespace is for selecting a specific or particular widget. """ def __new__(self, (x, y)): '''Return the widget at the specified `x` and `y` coordinate.''' res = (x, y) return cls.at(res) @classmethod def at(cls, (x, y)): '''Return the widget at the specified `x` and `y` coordinate.''' global application q = application() return q.widgetAt(x, y) @classmethod def form(cls, twidget): '''Return an IDA plugin form as a UI widget.''' raise internal.exceptions.MissingMethodError class clipboard(object): """ This namespace is for interacting with the current clipboard state. """ def __new__(cls): '''Return the current clipboard.''' global application clp = application() return clp.clipboard() class mouse(object): """ Base namespace for interacting with the mouse input. """ @classmethod def buttons(cls): '''Return the current mouse buttons that are being clicked.''' global application q = application() return q.mouseButtons() @classmethod def position(cls): '''Return the current `(x, y)` position of the cursor.''' raise internal.exceptions.MissingMethodError class keyboard(object): """ Base namespace for interacting with the keyboard input. """ @classmethod def modifiers(cls): '''Return the current keyboard modifiers that are being used.''' global application q = application() return q.keyboardModifiers() @classmethod def __of_key__(cls, key): '''Convert the normalized hotkey tuple in `key` into a format that IDA can comprehend.''' Separators = {'-', '+', '_'} Modifiers = {'ctrl', 'shift', 'alt'} # Validate the type of our parameter if not isinstance(key, tuple): raise internal.exceptions.InvalidParameterError(u"{:s}.of_key({!r}) : A key combination of an invalid type was provided as a parameter.".format('.'.join((__name__, cls.__name__)), key)) # Find a separator that we can use, and use it to join our tuple into a # string with each element capitalized. That way it looks good for the user. separator = next(item for item in Separators) modifiers, hotkey = key components = [item.capitalize() for item in modifiers] + [hotkey.capitalize()] return separator.join(components) @classmethod def __normalize_key__(cls, hotkey): '''Normalize the string `key` to a tuple that can be used to lookup keymappings.''' Separators = {'-', '+', '_'} Modifiers = {'ctrl', 'shift', 'alt'} # First check to see if we were given a tuple. If so, then we might've # been given a valid hotkey. However, we still need to validate this. So, # to do that we'll concatenate each component together back into a string # and then recurse so we can validate using the same logic. if isinstance(hotkey, tuple): modifiers, key = hotkey separator = next(item for item in Separators) components = [item for item in modifiers] + [key] return cls.__normalize_key__(separator.join(components)) # Next we need to normalize the separator used throughout the string by # simply converting any characters we might consider a separator into # a null-byte so we can split on it. normalized = functools.reduce(lambda agg, item: agg.replace(item, '\0'), Separators, hotkey) # Now we can split the normalized string so we can convert it into a # set. We will then iterate through this set collecting all of our known # key modifiers. Anything left must be a single key, so we can then # validate the hotkey we were given. components = { item.lower() for item in normalized.split('\0') } modifiers = { item for item in components if item in Modifiers } key = components ^ modifiers # Now we need to verify that we were given just one key. If we were # given any more, then this isn't a valid hotkey combination and we need # to bitch about it. if len(key) != 1: raise internal.exceptions.InvalidParameterError(u"{:s}.normalize_key({!s}) : An invalid hotkey combination ({!s}) was provided as a parameter.".format('.'.join((__name__, cls.__name__)), internal.utils.string.repr(hotkey), internal.utils.string.repr(hotkey))) res = next(iter(key)) if len(res) != 1: raise internal.exceptions.InvalidParameterError(u"{:s}.normalize_key({!s}) : The hotkey combination {!s} contains the wrong number of keys ({:d}).".format('.'.join((__name__, cls.__name__)), internal.utils.string.repr(hotkey), internal.utils.string.repr(res), len(res))) # That was it. Now to do the actual normalization, we need to sort our # modifiers into a tuple, and return the single hotkey that we extracted. res, = key return tuple(sorted(modifiers)), res # Create a cache to store the hotkey context, and the callable that was mapped to it __cache__ = {} @classmethod def map(cls, key, callable): """Map the specified `key` combination to a python `callable` in IDA. If the provided `key` is being re-mapped due to the mapping already existing, then return the previous callable that it was assigned to. """ # First we'll normalize the hotkey that we were given, and convert it # back into a format that IDA can understand. This way we can prevent # users from giving us a sloppy hotkey combination that we won't be # able to search for in our cache. hotkey = cls.__normalize_key__(key) keystring = cls.__of_key__(hotkey) # The hotkey we normalized is now a tuple, so check to see if it's # already within our cache. If it is, then we need to unmap it prior to # re-creating the mapping. if hotkey in cls.__cache__: logging.warn(u"{:s}.map({!s}, {!r}) : Remapping the hotkey combination {!s} with the callable {!r}.".format('.'.join((__name__, cls.__name__)), internal.utils.string.repr(key), callable, internal.utils.string.repr(keystring), callable)) ctx, _ = cls.__cache__[hotkey] ok = idaapi.del_hotkey(ctx) if not ok: raise internal.exceptions.DisassemblerError(u"{:s}.map({!s}, {!r}) : Unable to remove the hotkey combination {!s} from the list of current keyboard mappings.".format('.'.join((__name__, cls.__name__)), internal.utils.string.repr(key), callable, internal.utils.string.repr(keystring))) # Pop the callable that was mapped out of the cache so that we can # return it to the user. _, res = cls.__cache__.pop(hotkey) # If the user is mapping a new key, then there's no callable to return. else: res = None # Define a closure that calls the user's callable as it seems that IDA's # hotkey functionality doesn't deal too well when the same callable is # mapped to different hotkeys. def closure(*args, **kwargs): return callable(*args, **kwargs) # Now we can add the hotkey to IDA using the closure that we generated. # XXX: I'm not sure if the key needs to be utf8 encoded or not ctx = idaapi.add_hotkey(keystring, closure) if not ctx: raise internal.exceptions.DisassemblerError(u"{:s}.map({!s}, {!r}) : Unable to map the callable {!r} to the hotkey combination {!s}.".format('.'.join((__name__, cls.__name__)), internal.utils.string.repr(key), callable, callable, internal.utils.string.repr(keystring))) # Last thing to do is to stash it in our cache with the user's callable # in order to keep track of it for removal. cls.__cache__[hotkey] = ctx, callable return res @classmethod def unmap(cls, key): '''Unmap the specified `key` from IDA and return the callable that it was assigned to.''' frepr = lambda hotkey: internal.utils.string.repr(cls.__of_key__(hotkey)) # First check to see whether we were given a callable or a hotkey. If # we were given a callable, then we need to look through our cache for # the actual key that it was. Once found, then we normalize it like usual. if callable(key): try: hotkey = cls.__normalize_key__(next(item for item, (_, fcallback) in cls.__cache__.items() if fcallback == key)) except StopIteration: raise internal.exceptions.InvalidParameterError(u"{:s}.unmap({:s}) : Unable to locate the callable {!r} in the current list of keyboard mappings.".format('.'.join((__name__, cls.__name__)), "{!r}".format(key) if callable(key) else "{!s}".format(internal.utils.string.repr(key)), key)) else: logging.warn(u"{:s}.unmap({:s}) : Discovered the hotkey {!s} being currently mapped to the callable {!r}.".format('.'.join((__name__, cls.__name__)), "{!r}".format(key) if callable(key) else "{!s}".format(internal.utils.string.repr(key)), frepr(hotkey), key)) # We need to normalize the hotkey we were given, and convert it back # into IDA's format. This way we can locate it in our cache, and prevent # sloppy user input from interfering. else: hotkey = cls.__normalize_key__(key) # Check to see if the hotkey is cached and warn the user if it isn't. if hotkey not in cls.__cache__: logging.warn(u"{:s}.unmap({:s}) : Refusing to unmap the hotkey {!s} as it is not currently mapped to anything.".format('.'.join((__name__, cls.__name__)), "{!r}".format(key) if callable(key) else "{!s}".format(internal.utils.string.repr(key)), frepr(hotkey))) return # Grab the keymapping context from our cache, and then ask IDA to remove # it for us. If we weren't successful, then raise an exception so the # user knows what's up. ctx, _ = cls.__cache__[hotkey] ok = idaapi.del_hotkey(ctx) if not ok: raise internal.exceptions.DisassemblerError(u"{:s}.unmap({:s}) : Unable to unmap the specified hotkey ({!s}) from the current list of keyboard mappings.".format('.'.join((__name__, cls.__name__)), "{!r}".format(key) if callable(key) else "{!s}".format(internal.utils.string.repr(key)), frepr(hotkey))) # Now we can pop off the callable that was mapped to the hotkey context # in order to return it, and remove the hotkey from our cache. _, res = cls.__cache__.pop(hotkey) return res add, rm = internal.utils.alias(map, 'keyboard'), internal.utils.alias(unmap, 'keyboard') @classmethod def input(cls): '''Return the current keyboard input context.''' raise internal.exceptions.MissingMethodError ### PyQt5-specific functions and namespaces ## these can overwrite any of the classes defined above try: import PyQt5.Qt from PyQt5.Qt import QObject def application(): '''Return the current instance of the IDA Application.''' q = PyQt5.Qt.qApp return q.instance() class mouse(mouse): """ This namespace is for interacting with the mouse input. """ @classmethod def position(cls): '''Return the current `(x, y)` position of the cursor.''' qt = PyQt5.QtGui.QCursor res = qt.pos() return res.x(), res.y() class keyboard(keyboard): """ This namespace is for interacting with the keyboard input. """ @classmethod def input(cls): '''Return the current keyboard input context.''' raise internal.exceptions.MissingMethodError class UIProgress(object): """ Helper class used to simplify the showing of a progress bar in IDA's UI. """ timeout = 5.0 def __init__(self, blocking=True): self.object = res = PyQt5.Qt.QProgressDialog() res.setVisible(False) res.setWindowModality(blocking) res.setAutoClose(True) path = u"{:s}/{:s}".format(_database.config.path(), _database.config.filename()) self.update(current=0, min=0, max=0, text=u'Processing...', tooltip=u'...', title=path) # properties canceled = property(fget=lambda s: s.object.wasCanceled(), fset=lambda s, v: s.object.canceled.connect(v)) maximum = property(fget=lambda s: s.object.maximum()) minimum = property(fget=lambda s: s.object.minimum()) current = property(fget=lambda s: s.object.value()) # methods def open(self, width=0.8, height=0.1): '''Open a progress bar with the specified `width` and `height` relative to the dimensions of IDA's window.''' global window cls = self.__class__ # XXX: spin for a second until main is defined because IDA seems to be racy with this api ts, main = time.time(), getattr(self, '__appwindow__', None) while time.time() - ts < self.timeout and main is None: main = window.main() if main is None: logging.warn(u"{:s}.open({!s}, {!s}) : Unable to find main application window. Falling back to default screen dimensions to calculate size.".format('.'.join((__name__, cls.__name__)), width, height)) # figure out the dimensions of the window if main is None: # if there's no window, then assume some screen dimensions w, h = 1024, 768 else: w, h = main.width(), main.height() # now we can calculate the dimensions of the progress bar logging.info(u"{:s}.open({!s}, {!s}) : Using dimensions ({:d}, {:d}) for progress bar.".format('.'.join((__name__, cls.__name__)), width, height, int(w*width), int(h*height))) self.object.setFixedWidth(w * width), self.object.setFixedHeight(h * height) # calculate the center if main is None: # no window, so use the center of the screen cx, cy = w * 0.5, h * 0.5 else: center = main.geometry().center() cx, cy = center.x(), center.y() # ...and center it. x, y = cx - (w * width * 0.5), cy - (h * height * 1.0) logging.info(u"{:s}.open({!s}, {!s}) : Centering progress bar at ({:d}, {:d}).".format('.'.join((__name__, cls.__name__)), width, height, int(x), int(y))) self.object.move(x, y) # now everything should look good. self.object.show() def close(self): '''Close the current progress bar.''' self.object.close() def update(self, **options): '''Update the current state of the progress bar.''' minimum, maximum = options.get('min', None), options.get('max', None) text, title, tooltip = (options.get(n, None) for n in ['text', 'title', 'tooltip']) if minimum is not None: self.object.setMinimum(minimum) if maximum is not None: self.object.setMaximum(maximum) if title is not None: self.object.setWindowTitle(internal.utils.string.to(title)) if tooltip is not None: self.object.setToolTip(internal.utils.string.to(tooltip)) if text is not None: self.object.setLabelText(internal.utils.string.to(text)) res = self.object.value() if 'current' in options: self.object.setValue(options['current']) elif 'value' in options: self.object.setValue(options['value']) return res class widget(widget): """ This namespace is for selecting a specific or particular widget. """ @classmethod def form(cls, twidget): '''Return an IDA plugin form as a UI widget.''' ns = idaapi.PluginForm return ns.FormToPyQtWidget(twidget) except ImportError: logging.info(u"{:s}:Unable to locate `PyQt5.Qt` module.".format(__name__)) ### PySide-specific functions and namespaces try: import PySide import PySide.QtCore, PySide.QtGui def application(): '''Return the current instance of the IDA Application.''' res = PySide.QtCore.QCoreApplication return res.instance() class mouse(mouse): """ This namespace is for interacting with the mouse input. """ @classmethod def position(cls): '''Return the current `(x, y)` position of the cursor.''' qt = PySide.QtGui.QCursor res = qt.pos() return res.x(), res.y() class keyboard(keyboard): """ PySide keyboard interface. """ @classmethod def input(cls): '''Return the current keyboard input context.''' return q.inputContext() class widget(widget): """ This namespace is for selecting a specific or particular widget. """ @classmethod def form(cls, twidget): '''Return an IDA plugin form as a UI widget.''' ns = idaapi.PluginForm return ns.FormToPySideWidget(twidget) except ImportError: logging.info(u"{:s}:Unable to locate `PySide` module.".format(__name__)) ### wrapper that uses a priorityhook around IDA's hooking capabilities. class hook(object): """ This namespace exposes the ability to hook different parts of IDA. There are 3 different components in IDA that can be hooked. These are available as ``hook.idp``, ``hook.idb``, and ``hook.ui``. Please refer to the documentation for ``idaapi.IDP_Hooks``, ``idaapi.IDB_Hooks``, and ``idaapi.UI_Hooks`` for identifying what is available. """ @classmethod def __start_ida__(cls): api = [ ('idp', idaapi.IDP_Hooks), ('idb', idaapi.IDB_Hooks), ('ui', idaapi.UI_Hooks), ] # Create an alias so we save typing 19 chars.. priorityhook = internal.interface.priorityhook for attr, hookcls in api: # Attach a priority hooking queue to an instance of IDA's hooks instance = priorityhook(hookcls) # Explicitly assign the priority instance into our object if not hasattr(cls, attr): setattr(cls, attr, instance) # Now we can enable all the hooks so the user can use them instance.hook() return @classmethod def __stop_ida__(cls): for api in ['idp', 'idb', 'ui']: # grab the invidual class that was used to hook things hooker = getattr(cls, api) # and then unhook it completely, because IDA on linux # seems to still dispatch to those hooks...even when # the language extension is unloaded. hooker.unhook() return ### Helper classes to use or inherit from # XXX: why was this base class implemented again?? class InputBox(idaapi.PluginForm): """ A class designed to be inherited from that can be used to interact with the user. """ def OnCreate(self, form): '''A method to overload to be notified when the plugin form is created.''' self.parent = self.FormToPyQtWidget(form) def OnClose(self, form): '''A method to overload to be notified when the plugin form is destroyed.''' pass def Show(self, caption, options=0): '''Show the form with the specified `caption` and `options`.''' res = internal.utils.string.to(caption) return super(InputBox, self).Show(res, options) ### Console-only progress bar class ConsoleProgress(object): """ Helper class used to simplify the showing of a progress bar in IDA's console. """ def __init__(self, blocking=True): self.__path__ = u"{:s}/{:s}".format(_database.config.path(), _database.config.filename()) self.__value__ = 0 self.__min__, self.__max__ = 0, 0 return canceled = property(fget=lambda s: False, fset=lambda s, v: None) maximum = property(fget=lambda s: self.__max__) minimum = property(fget=lambda s: self.__min__) current = property(fget=lambda s: self.__value__) def open(self, width=0.8, height=0.1): '''Open a progress bar with the specified `width` and `height` relative to the dimensions of IDA's window.''' return def close(self): '''Close the current progress bar.''' return def update(self, **options): '''Update the current state of the progress bar.''' minimum, maximum = options.get('min', None), options.get('max', None) text, title, tooltip = (options.get(n, None) for n in ['text', 'title', 'tooltip']) if minimum is not None: self.__min__ = minimum if maximum is not None: self.__max__ = maximum res = self.__value__ if 'current' in options: self.__value__ = options['current'] if 'value' in options: self.__value__ = options['value'] if text is not None: six.print_(internal.utils.string.of(text)) return res ### Fake progress bar class that instantiates whichever one is available class Progress(object): """ The default progress bar in with which to show progress. This class will automatically determine which progress bar (Console or UI) to instantiate based on what is presently available. """ timeout = 5.0 def __new__(cls, *args, **kwargs): '''Figure out which progress bar to use and instantiate it with the provided parameters `args` and `kwargs`.''' if 'UIProgress' not in globals(): logging.warn(u"{:s}(...) : Using console-only implementation of the `ui.Progress` class.".format('.'.join((__name__, cls.__name__)))) return ConsoleProgress(*args, **kwargs) # XXX: spin for a bit looking for the application window as IDA seems to be racy with this for some reason ts, main = time.time(), getattr(cls, '__appwindow__', None) while time.time() - ts < cls.timeout and main is None: main = window.main() # If no main window was found, then fall back to the console-only progress bar if main is None: logging.warn(u"{:s}(...) : Unable to find main application window. Falling back to console-only implementation of the `ui.Progress` class.".format('.'.join((__name__, cls.__name__)))) return ConsoleProgress(*args, **kwargs) cls.__appwindow__ = main return UIProgress(*args, **kwargs) ### Re-implementation of IDAPython's displayhook that doesn't tamper with classes that inherit from base classes class DisplayHook(object): def __init__(self, displayhook=None): if displayhook: self.orig_displayhook = displayhook elif 'ida_idaapi' in sys.modules and '_IDAPython_displayhook' in sys.modules['ida_idaapi'].__dict__: self.orig_displayhook = __import__('ida_idaapi')._IDAPython_displayhook.orig_displayhook else: self.orig_displayhook = sys.displayhook def format_seq(self, num_printer, storage, item, opn, cls): storage.append(opn) for idx, el in enumerate(item): if idx > 0: storage.append(', ') self.format_item(num_printer, storage, el) storage.append(cls) def format_basestring(self, string): if 'ida_idaapi' in sys.modules: return sys.modules['ida_idaapi'].format_basestring(string) return u"{!s}".format(string) def format_item(self, num_printer, storage, item): if item is None or isinstance(item, bool): storage.append("{!s}".format(item)) elif isinstance(item, six.string_types): storage.append(self.format_basestring(item)) elif isinstance(item, six.integer_types): storage.append(num_printer(item)) elif isinstance(item, idaapi.tinfo_t): storage.append("{!s}".format(item)) elif item.__class__ is list: self.format_seq(num_printer, storage, item, '[', ']') elif item.__class__ is tuple: self.format_seq(num_printer, storage, item, '(', ')') elif item.__class__ is set: self.format_seq(num_printer, storage, item, 'set([', '])') elif item.__class__ is dict: storage.append('{') for idx, pair in enumerate(item.items()): if idx > 0: storage.append(', ') self.format_item(num_printer, storage, pair[0]) storage.append(": ") self.format_item(num_printer, storage, pair[1]) storage.append('}') else: storage.append("{!r}".format(item)) def _print_hex(self, x): return "{:#x}".format(x) def displayhook(self, item): if item is None or item.__class__ is bool: self.orig_displayhook(item) return try: storage = [] import ida_idp num_printer = self._print_hex dn = ida_idp.ph_get_flag() & ida_idp.PR_DEFNUM if dn == ida_idp.PRN_OCT: num_printer = oct elif dn == ida_idp.PRN_DEC: num_printer = str elif dn == ida_idp.PRN_BIN: num_printer = bin self.format_item(num_printer, storage, item) sys.stdout.write("%s\n" % "".join(storage)) except: import traceback traceback.print_exc() self.orig_displayhook(item)