IDA Scriptable Debugger

Since 2003 IDA offers a debugger that complements the static analysis nicely. In many cases, one just can't beat dynamic analysis. The IDA Debugger now supports 32-bit and 64-bit MS Windows executablesMS Windows, Linux, Mac OS X both locally and remotely. However, because the debugger API requires the mastery of our SDK and uses an event based model, it has proved quite difficult to use for some of our users.

IDA 5.2 will address both issues. The old event based model will remain available, but a simpler linear model will become available thanks to the function get_debugger_event(). This function pauses the execution of the plugin (or the script) until a new debugger event happens. The user can specify if she is interested only in the events that suspend the process or in all events. A timeout can also be confifured, after which the execution will continue if no event arose.

The new function allows us to drop the event based model (except in the cases when it is superior to linear logic) and write IDC scripts to control the debugger. For example, to launch the debugger, run to a specific location, print some data and single step twice, the following lines will suffice:

  AppBpt(some_address);
  StartDebugger("","","");         // start debugger with default params
  GetDebuggerEvent(WFNE_SUSP, -1); // ... and wait for bpt
  Message ("Stopped at %a, event code is %x\n", GetEventEA(), GetEventId());
  StepInto();                      // request a single step
  GetDebuggerEvent(WFNE_SUSP, -1); // ... and wait for app to execute
  StepInto();                      // request a single step
  GetDebuggerEvent(WFNE_SUSP, -1); // ... and wait for app to execute

In IDA 5.1 this would have required a event handler and a small finite state automata, for a total more than 200 lines of code. Please note that, in the above example, the error handling code is omitted for clarity. In real life, you might want to check for unexpected conditions like an exception happening after StepInto().

To illustrate how easier it is to write scripts with the new approach, we rewrote the core functionality of the UUNP unpacker plugin. The original program requires about 600 lines of code and has a rather complex logic. The new script only requires 100 lines of code (almost half of them being comments and empty lines). More importantly, the script is easy to understand and modify for your needs.

This is a reimplementation of the uunp universal unpacker in IDC. It illustrates the use of the new debugger functions in IDA v5.2

#include <idc.idc>

//--------------------------------------------------------------------------
static main()
{
  auto ea, bptea, tea1, tea2, code, minea, maxea;
  auto r_esp, r_eip, caller, funcname;

  // Calculate the target IP range. It is the first segment.
  // As soon as the EIP register points to this range, we assume that
  // the unpacker has finished its work.
  tea1 = FirstSeg();
  tea2 = SegEnd(tea1);

  // Calculate the current module boundaries. Any calls to GetProcAddress
  // outside of these boundaries will be ignored.
  minea = MinEA();
  maxea = MaxEA();

  // Launch the debugger and run until the entry point
  if ( !RunTo(BeginEA()) )
    return Failed(-1);

  // Wait for the process to stop at the entry point
  code = GetDebuggerEvent(WFNE_SUSP, -1);
  if ( code <= 0 )
    return Failed(code);

  // Set a breakpoint at GetProcAddress
  bptea = LocByName("kernel32_GetProcAddress");
  if ( bptea == BADADDR )
    return Warning("Could not locate GetProcAddress");
  AddBpt(bptea);

  while ( 1 )
  {
    // resume the execution and wait until the unpacker calls GetProcAddress
    code = GetDebuggerEvent(WFNE_SUSP|WFNE_CONT, -1);
    if ( code <= 0 )
      return Failed(code);

    // check the caller, it must be from our module
    r_esp = GetRegValue("ESP");
    caller = Dword(r_esp);
    if ( caller < minea || caller >= maxea )
      continue;

    // if the function name passed to GetProcAddress is not in the 
    // ignore-list, then switch to the trace mode
    funcname = GetString(Dword(r_esp+8), -1, ASCSTR_C);
    // ignore some api calls because they might be used by the unpacker
    if ( funcname == "VirtualAlloc" )
      continue;
    if ( funcname == "VirtualFree" )
      continue;

    // A call to GetProcAddress() probably means that the program has been
    // unpacked in the memory and now is setting up its import table
    break;
  }

  // trace the program in the single step mode until we jump to
  // the area with the original entry point.
  DelBpt(bptea);
  EnableTracing(TRACE_STEP, 1);
  for ( code = GetDebuggerEvent(WFNE_ANY|WFNE_CONT, -1); // resume
        code > 0;
        code = GetDebuggerEvent(WFNE_ANY, -1) )
  {
    r_eip = GetEventEa();
    if ( r_eip >= tea1 && r_eip < tea2 )
      break;
  }
  if ( code <= 0 )
    return Failed(code);

  // as soon as the current ip belongs OEP area, suspend the execution and
  // inform the user
  PauseProcess();
  code = GetDebuggerEvent(WFNE_SUSP, -1);
  if ( code <= 0 )
    return Failed(code);

  EnableTracing(TRACE_STEP, 0);

  // Clean up the disassembly so it looks nicer
  MakeUnknown(tea1, tea2-tea1, DOUNK_EXPAND|DOUNK_DELNAMES);
  MakeCode(r_eip);
  AutoMark2(tea1, tea2, AU_USED);
  AutoMark2(tea1, tea2, AU_FINAL);
  TakeMemorySnapshot(1);
  MakeName(r_eip, "real_start");
  Warning("Successfully traced to the completion of the unpacker code\n"
          "Please rebuild the import table using renimp.idc\n"
          "before stopping the debugger");
}
//--------------------------------------------------------------------------
// Print an failure message
static Failed(code)
{
  Warning("Failed to unpack the file, sorry (code %d)", code);
  return 0;
}