编写交互式反汇编器(IDA)插件 |
| 译者 :月中人 ([PTG]) |
| 时间 :2006.8.28 |
| 原文标题:Writing Interactive Disassembler (IDA) Plugins |
| 原文来源:"Reverse Engineering and Program Understanding"第6小节 |
| 原文作者:Greg Hoglund, Gary McGraw. |
IDA是对Interactive Disassembler[交互式反汇编器](www.datarescue.com)的简称,它是软件逆向工程最常用的工具之一。IDA支持插件模块,因此客户能够扩充功能和自动化任务。为了写这本书我们创建了一个简单的IDA插件,它能扫描两个二进制文件并且比较它们。该插件会加亮任何被改变了的代码区域。这可以被用来比较打补丁前的和打补丁后的可执行文件,确定哪几行被修补过。
在许多情况下,软件厂商“秘密地”修补安全缺陷。我们在这里提供的工具能帮助一个攻击者找到这些秘密补丁。事先声明,这个插件可能会标注许多根本没有改变的位置。如果编译器选项被改变或者在函数之间的填充码被变更,该插件会返回相当多的假阳性[误报]。尽管如此,这仍是一个用来举例说明该如何开始编写IDA插件的很棒的例子。
我们的例子也强调与‘penetrate-and-patch[潜入和补丁]安全’相关的最大问题。补丁简直就是攻击地图,而且聪明的攻击者知道该如何读他们。为了编译这个代码,你需要有IDA软件开发工具箱(SDK),这可以从你的IDA产品中得到。它们在源代码行内有注解,而且都是标准的头文件。你需要包含其他哪些头文件,取决于你要使用哪些API调用。注意,我们禁止了某一个警告信息而且也包含了Windows头文件。因为这样我们才能够使用Windows图形用户接口(GUI)代码实现弹出对话框等等。当你使用标准模板库的时候,会抛出警告4273,所以习惯上我们禁止它。
#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>
因为我们的插件是以SDK提供的一个插件样本为基础,所以下列代码只是样本的一部份。这些都是必需要的函数,而且样本中已经有注解这部份。
//--------------------------------------------------------------------------
// 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);
}
这里还要包含其他几个头文件和一些全局变量:
#include <process.h> #include "resource.h" DWORD g_tempest_state = 0; LPVOID g_mapped_file = NULL; DWORD g_file_size = 0;
这个函数把一个文件载入内存。这个文件将作为目标,和我们之前装入IDA的二进制文件做对比。典型地,你应该是先把未打补丁的文件装入IDA,然后用它比较打过补丁的文件:
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;
}
这个函数[根据用户提供的内存地址和长度从IDA的数据库中]取一串操作码,然后在目标文件中扫描这些字节。如果在目标文件中没能找到操作码,那个内存位置将会被标记为有改变。显然这是简单的技术,但是它在许多情况下是有效力的。由于存在这一小节开始处所列出的那些问题,这种方式会引起假阳性问题。
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;
}
这个线程[从装入IDA的二进制文件中]找出所有函数,并且拿它们和目标二进制文件做比较:
void __cdecl _test(void *p)
{
// Wait for start signal.
while(g_tempest_state == 0)
{
Sleep(10);
}
我们调用get_func_qty()求得装入IDA的二进制文件中函数的个数:
/////////////////////////////////////
// Enumerate through all functions.
/////////////////////////////////////
int total_functions = get_func_qty();
int total_diff_matches = 0;
我们现在循环处理每个函数。我们为每个函数调用getn_func()取得函数结构。函数结构是func_t类型数据。ea_t类型即是“有效地址”,它实际上只是一个无符号长整型。我们从函数结构中取得函数的开始地址和结束地址。然后我们把这个字节序列与目标二进制文件做比较:
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");
如果目标文件没有包含我们的操作码字串,我们就在[装入IDA的二进制文件的]静止代码列表中放上一个注解(用add_long_cmt函数):
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);
}
这个函数显示一个对话框提示用户输入一个文件名。这是一个外观漂亮的文件选择对话框:
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;
}
与所有“自定义的”对话框一样,我们需要DialogProc处理窗口消息:
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.
//
//
当用户激活插件的时候,run函数被调用。在本例中我们启动两个线程,同时给log窗口发送一个短消息:
void run(int arg)
{
// Testing.
msg("starting diff scanner plugin\n");
_beginthread(_test, 0, NULL);
_beginthread(_test2, 0, NULL);
}
这些全局数据项给IDA用来显示关于插件的信息。
//--------------------------------------------------------------------------
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
};