用Debug函数实现API函数的跟踪(2)

来源:互联网 发布:sql中自定义变量 编辑:程序博客网 时间:2024/04/30 08:58

对目标进程设置断点:

我们的目标是监视API函数的输入输出,那么,首先应该知道DLL模块中提供了哪些API函以及这些API的入口地址。在前面将过,广义的API还包括未导出的内部函数。如果你有DLL模块的调试版本和调试连接文件(pdb文件),也可以根据调试信息得到内部函数的信息。

· 得到函数名及函数入口地址

通过程序得到函数的入口地址有很多种方法。对于用VC编译出来的DLL,如果是Debug版本,可以通过ImageHlp库函数得到调试信息,分析出函数的入口地址。如果没有Debug版本,也可以通过分析导出函数表得到函数的入口地址。

1.用Imagehlp库函数得到Debug版本的函数名和函数入口地址。

可以利用Imagehlp库函数分析Debug信息,关联的函数为SymInitialize、SymEnumerateSymbols和UnDecorateSymbolName。详细可以参考MSDN中关于这些函数的说明和用法。不过,用Imagehlp只能分析出用VC编译的程序,对C++Builder编译的程序不能用这种方法分析。

2.DLL的导出表得到函数导出函数名和函数的入口地址。

在大多数情况下,我们还是希望监视的是Release版本的输入输出参数,毕竟Debug版本不是我们最终提供给用户的产品。Debug和Release的编译条件不同导致产生的结果不同,在很多BBS中都讨论过。所以,我认为跟踪监视Release版本更加有实用价值。

通过分析DLL导出表得到导出函数名在MSDN上就有源代码。关于导出表的说明大家可以参考关于PE结构的文章。

3.通过OLE函数取得COM接口

你也可以通过OLE函数分析DLL提供的接口函数。接口函数不是通过DLL导出表导出的。你可以通过LoadTypeLib函数来分析COM接口,得到COM记录接口的入口地址,这样,你就可以监视COM接口的调用了。这是API HOOK没法实现的。在这里我不打算分析分析COM接口的方式了。在MSDN上通过搜索LoadTypeLib sample关键词你就可以找到相关的源代码进行修改实现你的目标。

这里是通过计算机自动分析目标模块得到DLL导出函数的方案,作为我们监视的目的而言,这些工作只是为了得到一系列的函数名和函数地址而已。函数名只是一个让我们容易识别函数的名称而已,该函数入口地址才是我们真正关心的目标。换句话说,如果你能够确保某一个地址一定是一个函数(包括内部函数)的入口地址,你就完全可以给这个函数定义自己的名称,将它加入你的函数管理表中,同样可以实现监视该函数的输入输出参数的功能。这也是实现Exe内部函数的监视功能的原因。如果你有Exe编译时生成的Map文件(你可以在编译时选择生成Map文件),你就可以通过分析Map文件,得到内部函数的入口地址,将内部函数加入到你的函数管理表中。(一个函数的名称对于监视功能来讲究竟是FunA还是FunB并没有什么意义,但名称是FunA还是FunB的名称对于监视者分析监视结果是有意义的,你完全可以将MessageBox的函数在输出监视结果是以FunA的名称输出,所以在监视一些内部无名称的函数时,你完全可以定义你自己的名字)。

· 在函数入口地址处设置断点

设置断点非常简单,只要将0xCC(int 3)写入指定的地址就可以了。这样程序运行到指定地址时,将产生调试中断信息通知调试程序。修改指定进程的内存数据可以通过WriteProcessMemory函数来完成。由于一般情况下作为程序代码段都被保护起来了,所以还有一个函数也会用到。VirtualProtectEx。在实际情况下,当调试断点发生时,调试程序还应该将原来的代码写回被调试程序。

unsigned char SetBreakPoint(DWORD pAdd, unsigned char code){unsigned char b;BOOL rc;DWORD dwRead, dwOldFlg;// 0x80000000以上的地址为系统共有区域,不可以修改if( pAdd >= 0x80000000 || pAdd == 0)    return code;// 取得原来的代码rc = ReadProcessMemory(_ghDebug, pAdd, &b, sizeof(BYTE), &dwRead);// 原来的代码和准备修改的代码相同,没有必要再修改if(rc == 0 || b == code)    return code;// 修改页码保护属性VirtualProtectEx(_ghDebug, pAdd, sizeof(unsigned char), PAGE_READWRITE, &dwOldFlg);// 修改目标代码WriteProcessMemory(_ghDebug, pAdd, &code, sizeof(unsigned char), &dwRead);// 恢复页码保护属性VirtualProtectEx(_ghDebug, pAdd, sizeof(unsigned char), dwOldFlg, &dwOldFlg);return b;}

在设置断点时你必须将原来的代码保存起来,这样在恢复断点时就可以将代码还原了。一般用法为:设置断点m_code = SetBreakPoint( pFunAdd, 0xCC); 恢复断点:SetBreakPoint( pFunAdd, m_code); 记住,每个函数入口地址的代码都可能不同,你应该为每个断点地址保存一个原来的代码,在恢复时就不会发生错误了。

好了,现在目标程序中已经设置好了断点,当目标程序调用设置了断点的函数时,将产生一个调试中断信息通知调试程序。我们就要在调试程序中编写我们的调试中断程序了。

 

编写调试中断处理程序

被调试程序产生中断时,将产生一个EXCEPTION_DEBUG_EVENT信息通知调试程序进行处理。同时将填充EXCEPTION_DEBUG_INFO结构。

typedef struct _EXCEPTION_DEBUG_INFO {   EXCEPTION_RECORD ExceptionRecord;   DWORD dwFirstChance; } EXCEPTION_DEBUG_INFO, *LPEXCEPTION_DEBUG_INFO;typedef struct _EXCEPTION_RECORD {   DWORD ExceptionCode;   DWORD ExceptionFlags;   struct _EXCEPTION_RECORD *ExceptionRecord;   PVOID ExceptionAddress;   DWORD NumberParameters;   ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];} EXCEPTION_RECORD, *PEXCEPTION_RECORD;

在该结构中,我们比较感兴趣的是产生中断的地址ExceptionAddress和产生中断的信息代码ExceptionCode。在信息代码中与我们任务相关的信息代码为:

EXCEPTION_BREAKPOINT:断点中断信息代码EXCEPTION_SINGLE_STEP:单步中断信息代码

断点中断是由于我们在前面设置断点0xCC代码运行时产生的。由于产生中断后,我们必须将原来的代码写回被调试程序中继续运行。但是,代码一旦被写回目标程序,这样,当目标程序再次调用该函数时将不会产生中断,我们就只能实现一次监视了。所以,我们必须在将原代码写回被调试程序后,应该让被调试程序已单步的方式运行,再次产生一个单步中断的调试信息。在单步中断处理中,我们再次将0xCC代码写入函数的入口地址,这样就可以保证再次调用时产生中断。

首先,在进行中断处理前我们必须作些准备工作,管理起线程ID和线程句柄。为了管理单步中断处理,我们还必须维护一个基于线程的单步地址的管理,这样就可以允许被调试程序拥有多线程的功能。--我们不能保证单步运行时不被该进程的其他线程所打断。

// 我们利用一个map进行管理线程ID和线程句柄之间的关系// 同时也用一个map管理函数地址和断点的关系typedef map<DWORD, HANDLE, less<DWORD> > THREAD_MAP;typedef map<DWORD, void*, less<DWORD> > THREAD_SINGLESTEP_MAP;THREAD_MAP _gthreads;FUN_BREAK_MAP _gFunBreaks;// 并且假设设置断点时采用了如下方案进行原来代码的管理BYTE code = SetBreakPoint(pFunAdd, 0xCC);if(code != 0xCC)_gFunBreaks[pFunAdd] = code;…// 调试处理程序BOOL WINAPI OnDebugEvent(DEBUG_EVENT* pEvent){BOOL rc = TRUE;switch(pEvent->dwDebugEventCode){case CREATE_PROCESS_DEBUG_EVENT:// 记录线程ID和线程句柄的关系_gthreads[pEvent->dwThreadId] = pEvent->u.CreateProcessInfo.hThread;…break;case CREATE_THREAD_DEBUG_EVENT:// 记录线程ID和线程句柄的关系_gthreads [pEvent->dwThreadId] = pEvent->u.CreateThread.hThread;…break;case EXIT_THREAD_DEBUG_EVENT:// 线程退出时清除线程ID_gthreads.erase (pEvent->dwThreadId);…break;case EXCEPTION_DEBUG_EVENT:// 中断处理程序rc = OnDebugException(pEvent);break;…}return rc;}

下面进行中断处理程序。同样,我们只考虑我们关心的中断信息代码。在发生中断时,我们通过GetThreadContext(&context)得到中断线程的上下文信息。此时,context.esp就是函数的返回地址,context.esp+4位置的值就是函数的第一个参数,context.esp+8就是第二个参数,依次类推可以得到你想要的任何参数。需要注意的是因为参数是在被调试进程中的内容,所以你必须通过ReadProcessMemory函数才能得到:

DWORD buf[4]; // 取4个参数ReadProcessMemory(_ghDebug, (void*)(context.esp + 4), &buf,  sizeof(buf), &dwRead);

那么buf[0]就是第一个参数,buf[1]就是第二个参数。。。注意,在FunA(int a, char* p, OPENFILENAME* pof)函数调用时,buf[0] = a, buf[1] = p这里buf[1]是p的指针而不是p的内容,如果你希望访问p的内容,必须同样通过ReadProcessMemory函数再次取得p的内容。对于结构体指针也必须如此:

// 取得p的内容:char pBuf[256];ReadProcessMemory(_ghDebug, (void*)(buf[1]), &pBuf,  sizeof(pBuf), &dwRead);//取得pof的内容:OPENFILENAME ofReadProcessMemory(_ghDebug, (void*)(buf[2]), &of,  sizeof(of), &dwRead);

如果结构体中还有指针,要取得该指针的内容,也必须和取得p的内容一样的方式读取被调试程序的内存。总的来说,你必须意识到监视目标程序的所有内容都是对目标进程的内存读取操作,这些指针都是目标进程的内存地址,而不是调试进程的地址。