Hack of the day #0: Somewhat-automating pseudocode HTML generation, with IDAPython.

The problem

As you may already know1, Hex-Rays decompilers can generate HTML files from pseudocode windows.
That feature, however, is limited to generating HTML for a single function, or a portion of a function.

Recently, one of our customers asked us whether there was a way to generate HTML files for multiple functions all at once. I was at the regret of telling him that, no, we don’t have that feature (and it doesn’t really seem to be of interest to many, since AFAICT we have received no request for it)

However, after the Great Actions Refactoring™, decompiler action are now 1st class IDA citizens (just like any other plugin actions, really.) That means we can now invoke them, just like we could already invoke any IDA core action. We just need to take a few precaution, is all.

A first approach

Here’s a piece of IDAPython code I hammered into shape, that will go through all the functions of the program and (if the current function is decompilable) will prompt the user for saving:

from idaapi import *
from PySide import QtGui, QtCore

# Our piece of code relies on the user having already opened a pseudocode view. That's a limitation, but oh well.
tform = find_tform("Pseudocode-A")
if not tform:
    raise Exception("Please open a pseudocode view")
qwidget = PluginForm.FormToPySideWidget(tform)

# Iterate all funcs
for fidx in xrange(get_func_qty()):
    f = getn_func(fidx)
    try:
        if decompile(f):
            # Ok, we can jump there w/o being
            # switched to "IDA View-A"
            jumpto(f.startEA)

            # Needed. W/o that, focus might be
            # elsewhere, and the 'hx:GenHtml'
            # will refuse to work.
            qwidget.setFocus()

            process_ui_action("hx:GenHtml")
    except DecompilationFailure:
        print "Decompilation failure: %x. Skipping."  % f.startEA
        pass

Notes:

  • I only tried that with IDA 6.8, on a Linux box.
  • this is *NOT* meant to be an elegant, comfortable or rock-solid approach to generating HTML files with pseudocode (hence the ‘Hack of the day’ subject of this post!)
  • this is really just meant to illustrate how IDA APIs can be (ab)used, and perhaps give inspiration to some readers for some of their use-cases
  • this will go through _all_ functions in the program. If you just need to print 20 functions out of 13942, it might become tedious to reject (13942 – 20 =) 13922 “Save file” dialogs.

Improving that code, so only the selected functions will be printed

A possibly nice improvement to this, would be to know what functions the user selected in the “Functions window”, and iterate over those only, prompting for save.

Here’s how I would go about it, knowing that:

  • the ‘Functions window’ is what we call, in IDA-land, a ‘chooser’
  • alas, this is not a user-controlled chooser
  • thus we don’t control the data being printed, nor do we get called back whenever something gets selected/deselected…

    Great Actions Refactoring to the rescue again!

    We will:

    1. create an “Generate HTML files” action that, basically, wraps the above code, then
    2. register a ‘popup being populated’ hook: when the popup is for the ‘Functions window’ we will attach our new action to that popup
    3. then, when that action is invoked (i.e., user clicked it), in our action handler we will retrieve the list of selected indices and iterate over those, prompting for save

    from idaapi import *
    from PySide import QtGui, QtCore
    
    # Register action
    class GenMultiHTML(action_handler_t):
        def __init__(self):
            action_handler_t.__init__(self)
    
        def activate(self, ctx):
            tform = find_tform("Pseudocode-A")
            if not tform:
                raise Exception("Please open a pseudocode view")
            qwidget = PluginForm.FormToPySideWidget(tform)
            for fidx in ctx.chooser_selection:
                f = getn_func(fidx - 1)
                try:
                    if decompile(f):
                        # ok, we can jump there w/o being
                        # switched to "IDA View-A"
                        jumpto(f.startEA)
                        qwidget.setFocus()
                        process_ui_action("hx:GenHtml")
                except DecompilationFailure:
                    print "Decompilation failure: %x. Skipping."  % f.startEA
                    pass
    
            return 1
    
        def update(self, ctx):
            return AST_ENABLE_FOR_FORM if ctx.form_title == "Functions window" else AST_DISABLE_FOR_FORM
    
    register_action(
        action_desc_t(
            'my:gen_multi_html',
            'Generate HTML files',
            GenMultiHTML()))
    
    # Listen to 'finish populating popup' hook, to add our
    # action if the popup is for the "Functions window"
    class Hooks(idaapi.UI_Hooks):
        def finish_populating_tform_popup(self, form, popup):
            if get_tform_title(form) == "Functions window":
                attach_action_to_popup(
                    form,
                    popup,
                    "my:gen_multi_html",
                    None)
    
    hooks = Hooks()
    hooks.hook()
    

    Happy hacking!


    Footnotes

    #1: And if you didn’t know: go to a pseudocode window, click on the function’s title (i.e., the first line), and open the context menu. You can also select a portion of text, and generate HTML just for that.