- Into the House of Logic
- Should Reverse Engineering Be Illegal?
- Reverse Engineering Tools and Concepts
- Approaches to Reverse Engineering
- Methods of the Reverser
- Writing Interactive Disassembler (IDA) Plugins
- Decompiling and Disassembling Software
- Decompilation in Practice: Reversing helpctr.exe
- Automatic, Bulk Auditing for Vulnerabilities
- Writing Your Own Cracking Tools
- Building a Basic Code Coverage Tool
- Conclusion
Writing Interactive Disassembler (IDA) Plugins
IDA is short for Interactive Disassembler (available from www.datarescue.com) and is one of the most popular reverse engineering tools for software. IDA supports plugin modules so customers can extend the functionality and automate tasks. For this book we created a simple IDA plugin that can scan through two binary files and compare them. The plugin will highlight any code regions that have changed. This can be used to compare a prepatch executable with a postpatch executable to determine which lines of code were fixed.
In many cases, software vendors will "secretly" fix security bugs. The tool we provide here can help an attacker find these secret patches. Be forewarned that this plugin can flag many locations that have not changed at all. If compiler options are changed or the padding between functions is altered, the plugin will return a nice set of false positives. Nonetheless, this is a great example to illustrate how to start writing IDA plugins.
Our example also emphasizes the biggest problem with penetrate-and-patch security. Patches are really attack maps, and clever attackers know how to read them. To use this code you will need the IDA software development kit (SDK), which is available along with the IDA product. Code is commented inline. These are standard header files. Depending on which API calls you intend to use, you may need to include other header files. Note that we have disabled a certain warning message and included the Windows header file as well. By doing this we are able to use Windows graphical user interface (GUI) code for pop-up dialogs and so on. The warning 4273 is thrown when you use the standard template library and it's customary to disable it.
#include <windows.h> #pragma warning( disable:4273 ) #include <ida.hpp> #include <idp.hpp> #include <bytes.hpp> #include <loader.hpp> #include <kernwin.hpp> #include <name.hpp>
Because our plugin is based on a sample plugin supplied with the SDK, the following code is merely part of the sample. These are required functions and the comments were already part of the sample.
//-------------------------------------------------------------------------- // This callback is called for UI notification events. static int sample_callback(void * /*user_data*/, int event_id, va_list /*va*/) { if ( event_id != ui_msg ) // Avoid recursion. if ( event_id != ui_setstate && event_id ! = ui_showauto && event_id ! = ui_refreshmarked ) // Ignore uninteresting events msg("ui_callback %d\n", event_id); return 0; // 0 means "process the event"; // otherwise, the event would be ignored. } //-------------------------------------------------------------------------- // A sample of how to generate user-defined line prefixes static const int prefix_width = 8; static void get_user_defined_prefix(ea_t ea, int lnnum, int indent, const char *line, char *buf, size_t bufsize) { buf[0] = '\0'; // Empty prefix by default // We want to display the prefix only on the lines which // contain the instruction itself. if ( indent != -1 ) return; // A directive if ( line[0] == '\0' ) return; // Empty line if ( *line == COLOR_ON ) line += 2; if ( *line == ash.cmnt[0] ) return; // Comment line. . . // We don't want the prefix to be printed again for other lines of the // same instruction/data. For that we remember the line number // and compare it before generating the prefix. static ea_t old_ea = BADADDR; static int old_lnnum; if ( old_ea == ea && old_lnnum == lnnum ) return; // Let's display the size of the current item as the user-defined prefix. ulong our_size = get_item_size(ea); // Seems to be an instruction line. We don't bother with the width // because it will be padded with spaces by the kernel. snprintf(buf, bufsize, " %d", our_size); // Remember the address and line number we produced the line prefix for. old_ea = ea; old_lnnum = lnnum; } //-------------------------------------------------------------------------- // // Initialize. // // IDA will call this function only once. // If this function returns PLGUIN_SKIP, IDA will never load it again. // If this function returns PLUGIN_OK, IDA will unload the plugin but // remember that the plugin agreed to work with the database. // The plugin will be loaded again if the user invokes it by // pressing the hot key or by selecting it from the menu. // After the second load, the plugin will stay in memory. // If this function returns PLUGIN_KEEP, IDA will keep the plugin // in memory. In this case the initialization function can hook // into the processor module and user interface notification points. // See the hook_to_notification_point() function. // // In this example we check the input file format and make the decision. // You may or may not check any other conditions to decide what you do, // whether you agree to work with the database. // int init(void) { if ( inf.filetype == f_ELF ) return PLUGIN_SKIP; // Please uncomment the following line to see how the notification works: // hook_to_notification_point(HT_UI, sample_callback, NULL); // Please uncomment the following line to see how the user-defined prefix works: // set_user_defined_prefix(prefix_width, get_user_defined_prefix); return PLUGIN_KEEP; } //-------------------------------------------------------------------------- // Terminate. // Usually this callback is empty. // The plugin should unhook from the notification lists if // hook_to_notification_point() was used. // // IDA will call this function when the user asks to exit. // This function won't be called in the case of emergency exits. void term(void) { unhook_from_notification_point(HT_UI, sample_callback); set_user_defined_prefix(0, NULL); }
A few more header files and some global variables are included here:
#include <process.h> #include "resource.h" DWORD g_tempest_state = 0; LPVOID g_mapped_file = NULL; DWORD g_file_size = 0;
This function loads a file into memory. This file is going to be used as the target to compare our loaded binary against. Typically you would load the unpatched file into IDA and compare it with the patched file:
bool load_file( char *theFilename ) { HANDLE aFileH = CreateFile( theFilename, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if(INVALID_HANDLE_VALUE == aFileH) { msg("Failed to open file.\n"); return FALSE; } HANDLE aMapH = CreateFileMapping( aFileH, NULL, PAGE_READONLY, 0, 0, NULL ); if(!aMapH) { msg("failed to open map of file\n"); return FALSE; } LPVOID aFilePointer = MapViewOfFileEx( aMapH, FILE_MAP_READ, 0, 0, 0, NULL); DWORD aFileSize = GetFileSize(aFileH, NULL); g_file_size = aFileSize; g_mapped_file = aFilePointer; return TRUE; }
This function takes a string of opcodes and scans the target file for these bytes. If the opcodes cannot be found in the target, the location will be marked as changed. This is obviously a simple technique, but it works in many cases. Because of the problems listed at the beginning of this section, this approach can cause problems with false positives.
bool check_target_for_string(ea_t theAddress, DWORD theLen) { bool ret = FALSE; if(theLen > 4096) { msg("skipping large buffer\n"); return TRUE; } try { // Scan the target binary for the string. static char g_c[4096]; // I don't know any other way to copy the data string // out of the IDA database?! for(DWORD i=0;i<theLen;i++) { g_c[i] = get_byte(theAddress + i); } // Here we have the opcode string; perform a search. LPVOID curr = g_mapped_file; DWORD sz = g_file_size; while(curr && sz) { LPVOID tp = memchr(curr, g_c[0], sz); if(tp) { sz -= ((char *)tp - (char *)curr); } if(tp && sz >= theLen) { if(0 == memcmp(tp, g_c, theLen)) { // We found a match! ret = TRUE; break; } if(sz > 1) { curr = ((char *)tp)+1; } else { break; } } else { break; } } } catch(...) { msg("[!] critical failure."); return TRUE; } return ret; }
This thread finds all the functions and compares them with a target binary:
void __cdecl _test(void *p) { // Wait for start signal. while(g_tempest_state == 0) { Sleep(10); }
We call get_func_qty() to determine the number of functions in the loaded binary:
///////////////////////////////////// // Enumerate through all functions. ///////////////////////////////////// int total_functions = get_func_qty(); int total_diff_matches = 0;
We now loop through each function. We call getn_func() to get the function structure for each function. The function structure is of type func_t. The ea_t type is known as "effective address" and is actually just an unsigned long. We get the start address of the function and the end address of the function from the function structure. We then compare the sequence of bytes with the target binary:
for(int n=0;n<total_functions;n++) { // msg("getting next function \n"); func_t *f = getn_func(n); /////////////////////////////////////////////// // The start and end addresses of the function // are in the structure. /////////////////////////////////////////////// ea_t myea = f->startEA; ea_t last_location = myea; while((myea <= f->endEA) && (myea != BADADDR)) { // If the user has requested a stop we should return here. if(0 == g_tempest_state) return; ea_t nextea = get_first_cref_from(myea); ea_t amloc = get_first_cref_to(nextea); ea_t amloc2 = get_next_cref_to(nextea, amloc); // The cref will be the previous instruction, but we // also check for multiple references. if((amloc == myea) && (amloc2 == BADADDR)) { // I was getting stuck in loops, so I added this hack // to force an exit to the next function. if(nextea > myea) { myea = nextea; // ---------------------------------------------- // Uncomment the next two lines to get "cool" // scanning effect in the GUI. Looks sweet but slows // down the scan. // ---------------------------------------------- // jumpto(myea); // refresh_idaview(); } else myea = BADADDR; } else { // I am a location. Reference is not last instruction _OR_ // I have multiple references. // Diff from the previous location to here and make a comment // if we don't match // msg("diffing location... \n");
We place a comment in our dead listing (using add_long_cmt) if the target doesn't contain our opcode string:
bool pause_for_effect = FALSE; int size = myea - last_location; if(FALSE == check_target_for_string(last_location, size)) { add_long_cmt(last_location, TRUE, "===================================================\n" "= ** This code location differs from the target ** =\n" "====================================================\n"); msg("Found location 0x%08X that didn't match target!\n", last_location); total_diff_matches++; } if(nextea > myea) { myea = nextea; } else myea = BADADDR; // goto next address. jumpto(myea); refresh_idaview(); } } } msg("Finished! Found %d locations that diff from the target.\n", total_diff_matches); }
This function displays a dialog box prompting the user for a filename. This is a nice-looking dialog for file selection:
char * GetFilenameDialog(HWND theParentWnd) { static TCHAR szFile[MAX_PATH] = "\0"; strcpy( szFile, ""); OPENFILENAME OpenFileName; OpenFileName.lStructSize = sizeof (OPENFILENAME); OpenFileName.hwndOwner = theParentWnd; OpenFileName.hInstance = GetModuleHandle("diff_scanner.plw"); OpenFileName.lpstrFilter = "w00t! all files\0*.*\0\0"; OpenFileName.lpstrCustomFilter = NULL; OpenFileName.nMaxCustFilter = 0; OpenFileName.nFilterIndex = 1; OpenFileName.lpstrFile = szFile; OpenFileName.nMaxFile = sizeof(szFile); OpenFileName.lpstrFileTitle = NULL; OpenFileName.nMaxFileTitle = 0; OpenFileName.lpstrInitialDir = NULL; OpenFileName.lpstrTitle = "Open"; OpenFileName.nFileOffset = 0; OpenFileName.nFileExtension = 0; OpenFileName.lpstrDefExt = "*.*"; OpenFileName.lCustData = 0; OpenFileName.lpfnHook = NULL; OpenFileName.lpTemplateName = NULL; OpenFileName.Flags = OFN_EXPLORER | OFN_NOCHANGEDIR; if(GetOpenFileName( &OpenFileName )) { return(szFile); } return NULL; }
As with all "homegrown" dialogs, we need DialogProc to handle Windows messages:
BOOL CALLBACK MyDialogProc(HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam) { switch(msg) { case WM_COMMAND: if (LOWORD(wParam) == IDC_BROWSE) { char *p = GetFilenameDialog(hDlg); SetDlgItemText(hDlg, IDC_EDIT_FILENAME, p); } if (LOWORD(wParam) == IDC_START) { char filename[255]; GetDlgItemText(hDlg, IDC_EDIT_FILENAME, filename, 254); if(0 == strlen(filename)) { MessageBox(hDlg, "You have not selected a target file", "Try again", MB_OK); } else if(load_file(filename)) { g_tempest_state = 1; EnableWindow( GetDlgItem(hDlg, IDC_START), FALSE); } else { MessageBox(hDlg, "The target file could not be opened", "Error", MB_OK); } } if (LOWORD(wParam) == IDC_STOP) { g_tempest_state = 0; } if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL) { if(LOWORD(wParam) == IDOK) { } EndDialog(hDlg, LOWORD(wParam)); return TRUE; } break; default: break; } return FALSE; } void __cdecl _test2(void *p) { DialogBox( GetModuleHandle("diff_scanner.plw"), MAKEINTRESOURCE(IDD_DIALOG1), NULL, MyDialogProc); } //-------------------------------------------------------------------------- // // The plugin method. // // This is the main function of plugin. // // It will be called when the user selects the plugin. // // Arg - the input argument. It can be specified in the // plugins.cfg file. The default is zero. // //
The run function is called when the user activates the plugin. In this case we start a couple threads and post a short message to the log window:
void run(int arg) { // Testing. msg("starting diff scanner plugin\n"); _beginthread(_test, 0, NULL); _beginthread(_test2, 0, NULL); }
These global data items are used by IDA to display information about the plugin.
//-------------------------------------------------------------------------- char comment[] = "Diff Scanner Plugin, written by Greg Hoglund (www.rootkit.com)"; char help[] = "A plugin to find diffs in binary code\n" "\n" "This module highlights code locations that have changed.\n" "\n"; //-------------------------------------------------------------------------- // This is the preferred name of the plugin module in the menu system. // The preferred name may be overridden in the plugins.cfg file. char wanted_name[] = "Diff Scanner"; // This is the preferred hot key for the plugin module. // The preferred hot key may be overridden in the plugins.cfg file. // Note: IDA won't tell you if the hot key is not correct. // It will just disable the hot key. char wanted_hotkey[] = "Alt-0"; //-------------------------------------------------------------------------- // // PLUGIN DESCRIPTION BLOCK // //-------------------------------------------------------------------------- extern "C" plugin_t PLUGIN = { IDP_INTERFACE_VERSION, 0, // Plugin flags. init, // Initialize. term, // Terminate. This pointer may be NULL. run, // Invoke plugin. comment, // Long comment about the plugin // It could appear in the status line // or as a hint. help, // Multiline help about the plugin wanted_name, // The preferred short name of the plugin wanted_hotkey // The preferred hot key to run the plugin };