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:
- 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()
- 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).