Hex-Rays' blog

Using custom viewers from IDAPython – Hex Rays

Written by   Elias Bachaalany | Mar 24, 2010

Custom viewers can be used to display arbitrary textual information and can be used in any IDA plugin.They are used in IDA-View, Hex-View, Enum and struct views and the Hex-Rays decompiler.

In this blog entry we are going to write an ASM file viewer in order to demonstrate how to create a custom viewer and populate it with colored lines.

Writing a custom viewer

The simplest custom viewer which does not handle any events (like key presses, mouse or cursor position movements, displaying hints, etc) can be created like this:

v = idaapi.simplecustviewer_t()
if v.Create("Simple custom viewer"):
    for i in xrange(1, 11):
        v.AddLine("Line %d" % i)
    v.Show()
else:
    print "Failed to create viewer"

If handling events is required then one has to derive from idaapi.simplecustviewer_t class and implement the required callbacks:

class mycv_t(simplecustviewer_t):
    def Create(self, sn=None):
        # Form the title
        title = "Simple custom view test"
        if sn:
            title += " %d" % sn
        # Create the customview
        if not simplecustviewer_t.Create(self, title):
            return False
        self.menu_hello = self.AddPopupMenu("Hello")
        self.menu_world = self.AddPopupMenu("World")
        for i in xrange(0, 100):
            self.AddLine("Line %d" % i)
        return True
    def OnKeydown(self, vkey, shift):
        # ESCAPE?
        if vkey == 27:
            self.Close()
        # Goto?
        elif vkey == ord('G'):
            n = self.GetLineNo()
            if n is not None:
                v = idc.AskLong(self.GetLineNo(), "Where to go?")
                if v:
                    self.Jump(v, 0, 5)
        elif vkey == ord('R'):
            print "refreshing...."
            self.Refresh()
        else:
            return False
        return True
    def OnPopupMenu(self, menu_id):
        if menu_id == self.menu_hello:
            print "Hello"
        elif menu_id == self.menu_world:
            print "World"
        else:
            # Unhandled
            return False
        return True

Or many custom viewers:

view = mycv_t()
if view.Create(1):
    view.Show()

Or many custom viewers:

def make_many(n):
    L = []
    for i in xrange(1, n+1):
        v = mycv_t()
        if not v.Create(i):
            break
        v.Show()
        L.append(v)
    return L
# Create 20 views
V = make_many(20)

Please note that no two views should have the same title. To check if a window with a given title exists and then to close it, you can use:

f = idaapi.find_tform("Simple custom view test 2")
if f:
    idaapi.close_tform(f, 0)

For a more comprehensive example on custom viewers, please check the ex_custview.py example.

Using colored lines

To use colored lines, we have to embed color tags to them. All the available foreground colors are defined in lines.hpp header file. The color codes are related to various item kinds in IDA, for example here are some colors:

Color name Description
SCOLOR_REGCMT Regular comment
SCOLOR_RPTCMT Repeatable comment
SCOLOR_INSN Instruction
SCOLOR_KEYWORD Keywords
SCOLOR_STRING String constant in instruction

There are also special color tags treated as escape sequence codes (the concept is similar to ANSI escape codes). They are used to determine how a line is rendered, to mark the beginning/end of a certain color, to insert address marks, or to mark UTF8 string beginnings/endings:

Color name Description
SCOLOR_ON Escape character (ON)
SCOLOR_OFF Escape character (OFF)
SCOLOR_INV Escape character (Inverse colors)
SCOLOR_UTF8 Following text is UTF-8 encoded
SCOLOR_STRING String constant in instruction

In the IDA SDK, a colored line is explained to have the following structure:

//      A typical color sequence looks like this:
//
//      COLOR_ON COLOR_xxx text COLOR_OFF COLOR_xxx

Luckily, we don’t have to form the colored lines manually, instead we can use helper functions:

colored_line = idaapi.COLSTR("Hello", idaapi.SCOLOR_REG) + " " + idaapi.COLSTR("World", idaapi.SCOLOR_STRING)

If we look at colored_line contents we can see the following:

'\x01!Hello\x02! \x01\x0bWorld\x02\x0b'

Which is interpreted as:

COLOR_ON SCOLOR_REG=0x21 Hello COLOR_OFF COLOR=0x21 SPACE COLOR_ON SCOLOR_STRING=0x0B World COLOR_OFF COLOR=0x0B

In order to strip back color tags from a colored line, use tag_remove():

line = idaapi.tag_remove(colored_line)

Writing an ASM file viewer

Now that we covered all the needed information, let us write a very basic assembly file viewer. To accomplish the task, we need two things:

  1. ASM tokenizer: It should be able to recognize comments, strings and identifiers. For the identifiers, we will take into consideration only register names, instruction names and directives.
    • Instruction names: To get all the instruction names we use idautils.GetInstructionList() which returns all the instruction names from the processor module (the ph.instruc array)
    • Register names: Similarly we can use idautils.GetRegisterList()
  2. Custom viewer to render the text: We derive from simplecustviewer_t to handle key presses and popup menu actions

The tokenizer (asm_colorizer_t class) will go over the text and when it identifies a token it will call one of the following functions: as_string(), as_comment(), as_num(), and as_id(). Those functions will use idaapi.COLSTR() to colorize the token appropriately. At the end of each line, the tokenizer will call the add_line() method to add the line (after it has been colored).

The custom viewer (implemented by the asmview_t class) will inherit from both asm_colorizer_t and simplecustviewer_t:

class asmview_t(idaapi.simplecustview_t, asm_colorizer_t):
    def Create(self, fn):
        # Create the customview
        if not idaapi.simplecustview_t.Create(self, "ASM View - %s" % os.path.basename(fn)):
            return False
        self.instruction_list = idautils.GetInstructionList()
        self.instruction_list.extend(["ret"])
        self.register_list    = idautils.GetRegisterList()
        self.register_list.extend(["eax", "ebx", "ecx", "edx", "edi", "esi", "ebp", "esp"])
        self.fn = fn
        if not self.reload_file():
            return False
        self.id_refresh = self.AddPopupMenu("Refresh")
        self.id_close   = self.AddPopupMenu("Close")
        return True
    def reload_file(self):
        if not self.colorize_file(self.fn):
            self.Close()
            return False
        return True
    def colorize_file(self, fn):
        try:
            f = open(fn, "r")
            lines = f.readlines()
            f.close()
            self.ClearLines()
            self.colorize(lines)
            return True
        except:
            return False
    def add_line(self, s=None):
        if not s:
            s = ""
        self.AddLine(s)
    def as_comment(self, s):
        return idaapi.COLSTR(s, idaapi.SCOLOR_RPTCMT)
    def as_id(self, s):
        t = s.lower()
        if t in self.register_list:
            return idaapi.COLSTR(s, idaapi.SCOLOR_REG)
        elif t in self.instruction_list:
            return idaapi.COLSTR(s, idaapi.SCOLOR_INSN)
        else:
            return s
    def as_string(self, s):
        return idaapi.COLSTR(s, idaapi.SCOLOR_STRING)
    def as_num(self, s):
        return idaapi.COLSTR(s, idaapi.SCOLOR_NUMBER)
    def as_directive(self, s):
        return idaapi.COLSTR(s, idaapi.SCOLOR_KEYWORD)
    def OnPopupMenu(self, menu_id):
        if self.id_refresh == menu_id:
            return self.reload_file()
        elif self.id_close == menu_id:
            self.Close()
            return True
        return False
    def OnKeydown(self, vkey, shift):
        # ESCAPE
        if vkey == 27:
            self.Close()
            return True
        return False

This blog entry inspired you to write a new plugin? Feel free to participate in our plugin contest!

The ASM viewer script can be downloaded from here (note: it requires IDAPython r289).