65.9K
CodeProject 正在变化。 阅读更多。
Home

Memory Analyzer x86, 32/64位 & 免费Detour

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.79/5 (24投票s)

2013年1月29日

CPOL

6分钟阅读

viewsIcon

66865

downloadIcon

1366

检测内存泄漏

 

引言

本文通过一个示例:Memory Analyzer,向读者介绍detours。Memory Analyzer是一个简单工具,用于检测内存泄漏和错误的内存释放(例如,将free误用为delete等)。

Detours是API hooking的升级,它们支持递归函数的捕获并且是线程安全的。它们更复杂,因为需要巧妙地放置2个JMP指令,并引入了返回函数(return function)的概念。此函数由MyAttach方法(参见代码)返回,并由成员变量:m_Return(这可以被称为trampoline函数)维护。

(我称它们为detours,因为我试图做与MS detours相同的事情)。

与我之前撰写文章的方法一样,本文旨在介绍detours,而不是memchecker

此外,还使用OutputDebugString在进程和memchecker之间进行通信(我们构建了一个debugger),调试符号文件用于显示与内存操作相关的代码的调用堆栈和行号。

背景

我之前的文章是必读的:https://codeproject.org.cn/Articles/163408/APIHookinghttps://codeproject.org.cn/Articles/189711/Write-your-own-Debugger-to-handle-Breakpoints

还需要基本的Windows编程知识,以及一点汇编(正如我在之前关于APIHooking的文章中所提到的)。

使用代码

附加的代码是使用VS2012构建的,阅读本文时必须随时参考。附加代码仅支持32位detours,并将DLL注入到正在运行的代码中,**32位DLL只能注入到32位进程中。**

由于我们将要分析内存,因此我们必须捕获以下API:

  • HeapAlloc
  • HeapReAlloc(未实现)
  • HeapFree
  • VirtualAlloc及其相关API未予考虑,读者可以自由发布自己的实现。

选择这些API是因为**malloc**,**new**,**free**,**delete**等会调用上述API之一。

开始Hook

大部分代码与API hooking类似,即我们在**原始函数中添加一个5字节的JMP**,将调用重定向到**trap**。必须构建额外的返回(m_Return)函数,因为在从我们的trap API调用原始函数时,我们不像在API hooking中那样重新修补函数。注意(参见下面的代码),我在m_Return[5]及之后添加Jmp指令,m_Return[0]m_Return[4]之间的字节保存了原始指令(3个完整的Opcodes)。

  memcpy(m_Return,m_Original,sizeof(m_Original)); //build the return function
  DWORD JmpDiff1 = ((DWORD)func - (DWORD)m_Return-5);
  memcpy(&TrapJmp[1], &JmpDiff1, 4);
  memcpy(&m_Return[5],TrapJmp,sizeof(TrapJmp));
  
  m_pReturnedFunc=m_Return;

我们将调用m_Return而不是像在**API hooking**中那样重新修补(参见下面的代码)。

 LPVOID WINAPI MyHeapAlloc(
  __in  HANDLE hHeap,
  __in  DWORD dwFlags,
  __in  SIZE_T dwBytes
)
{
 //striclty do not use any function that may call internally HeapAlloc
 void* pRet=((void* (__stdcall*)(  __in  HANDLE hHeap, __in  DWORD dwFlags, __in  SIZE_T dwBytes))g_myDetour_HeapAlloc.m_pReturnedFunc)(hHeap,dwFlags,dwBytes);
 ::OutputDebugStringA("you are trapped");
 return (void*)pRet;
}

请注意,调用约定和参数是相同的(请参阅我之前关于APIHooking的文章)。

维护JMP

既然我们不恢复原始函数,我们必须确保指令(Opcode)按正确的顺序执行,更重要的是,我们**不要**在OPCodes之间跳转,否则处理器可能会将其解释为其他opcode。注意变量m_Return,它保存了原始函数的opcode(至少是包含前3个**完整**指令的前5个字节),然后添加一个跳转到HeapAlloc +5字节。我们添加这5个字节是为了避免重新执行在原始函数前5个字节中添加的JMP。**

附加代码已硬编码为假设HeapAlloc +5字节偏移量(这对于HeapAlloc、HeapFree、MessageBoxA来说没问题,但对于其他API可能不行)。

你的代码将调用HeapAlloc,然后通过立即跳转调用MyHeapAlloc(trap),接着使用m_Return跳转到HeapAlloc+5字节,并相应返回。m_Return将在跳转到HeapAlloc+5之前执行前5个字节。

现在,为了通过Opcodes来理解这一点,我已经捕获了MessageBoxA。

没有Detours

<a href="mailto:MessageBoxA@16">MessageBoxA@16
7526FD1E 8B FF                mov         edi,edi  
7526FD20 55                   push        ebp  
7526FD21 8B EC                mov         ebp,esp  
7526FD23 6A 00                push        0  
7526FD25 FF 75 14             push        dword ptr [ebp+14h]  
7526FD28 FF 75 10             push        dword ptr [ebp+10h]  
7526FD2B FF 75 0C             push        dword ptr [ebp+0Ch]  
7526FD2E FF 75 08             push        dword ptr [ebp+8]  
7526FD31 E8 A0 FF FF FF       call        MessageBoxExA

有Detours

7526FD1E E9 A5 13 10 8B       jmp         MyMessageBoxA (03710C8h)  
7526FD23 6A 00                push        0  
7526FD25 FF 75 14             push        dword ptr [ebp+14h]  
7526FD28 FF 75 10             push        dword ptr [ebp+10h]  
7526FD2B FF 75 0C             push        dword ptr [ebp+0Ch]  
7526FD2E FF 75 08             push        dword ptr [ebp+8]  
7526FD31 E8 A0 FF FF FF       call        <a href="mailto:MessageBoxExA@20">MessageBoxExA@20</a>

请注意原始函数MessageBoxA前5个字节中的Jmp指令,m_Return在跳转回MessageBoxA+5之前必须执行这5个字节(调用时为3条指令)。

执行流程将是

00379171 8B FF                mov         edi,edi  
00379173 55                   push        ebp  
00379174 8B EC                mov         ebp,esp  
back to MessageBoxA
7526FD23 6A 00                push        0  
7526FD25 FF 75 14             push        dword ptr [ebp+14h]  
7526FD28 FF 75 10             push        dword ptr [ebp+10h]  
7526FD2B FF 75 0C             push        dword ptr [ebp+0Ch]  
7526FD2E FF 75 08             push        dword ptr [ebp+8]  
7526FD31 E8 A0 FF FF FF       call        <a href="mailto:MessageBoxExA@20">MessageBoxExA@20

正如你所见,这完成了与没有Detours时完全相同的流程。

不要打印任何内容

你不能使用任何会调用HeapAlloc的函数,例如printf(因为它会导致无限循环)。我选择了使用OutputDebugStringA

由于你正在编写一个debugger(在附加的代码中,CrashAnalyzer_v2作为debugger),我建议你阅读我之前关于编写debugger的文章。

你可以使用调试符号表(http://en.wikipedia.org/wiki/Debug_symbol)来确定分配函数是从代码中的哪一行和哪个调用堆栈(决定函数调用流程的函数名列表)调用的(正如一个真正的内存分析器应该做的那样)。

新增函数用于获取调用堆栈:GetStack,此函数在循环中调用StackWalk64(查阅MSDN)来获取调用堆栈函数(仅地址)。我们可以通过调用SymFromAddr,使用PDB(调试符号)文件将这些地址(一次一个)转换为函数名,**确保调用SymInitialize以初始化符号处理器。

BOOL b=SymInitialize(hProcess,NULL,TRUE);  
 
// make sure that PDB file exists, it is generated by the compiler in the same folder as the exe

SymFromAddr(hProcess,cc.Rip,&d64,s);
SymCleanup (hProcess);

这一切都很好,但我们如何注入我们的代码呢?

与我之前关于API hooking的文章不同,我们不能使用Windows Hook,我们的应用程序可能没有消息循环,所以SetWindowsHookEx将不起作用。

我们必须使用**CreateRemoteThread**

 //injecting DLL
 {
  String strPath;
  GetCurrentDirectoryA(sizeof(strPath),strPath.string);  //get the directory
  strcat(strPath,"<a href="file://\\MemoryCheckerModule.dll">\\MemoryCheckerModule.dll");
  HANDLE hProcess=::OpenProcess(PROCESS_ALL_ACCESS ,false,pid);
  if(hProcess) 
  {
   void *p=VirtualAllocEx (hProcess,NULL,strlen(strPath.string)+10,MEM_COMMIT,PAGE_EXECUTE_READWRITE);
   SIZE_T size=0;
   WriteProcessMemory(hProcess,p,strPath.string,sizeof(strPath)+1,&size);
   CreateRemoteThread(hProcess,0,0,( LPTHREAD_START_ROUTINE)LoadLibraryA,p,0,0);
   CloseHandle(hProcess);
  }
  else
  {
   printf("PID invalid / access  denied");
   return 1; //cannot find process
  }
 }

我们必须通过调用VirtualAllocEx在远程进程中创建内存,然后通过调用WriteProcessMemory将其加载所需的参数以供LoadLibraryA使用(所有debugger都使用此API来更改对象代码以添加断点)。

然后,我们调用CreateRemoteThread,并将LoadLibraryA的函数地址提供给单独的线程调用。

LoadLibraryA函数存在于所有进程中,因为它相关的模块在进程启动时加载。

**DLL进程附加**在**DllMain**中将完成其余的工作,请参阅附加代码。

 

我们的自定义64位detour会有些不同,并且未在当前项目中实现。

下面的代码将帮助你理解64位detour,它也实现在附加的代码(MemoryCheckerModule_with32_&_64Bit.zip)中。
下面的代码是以前写的,并未经过彻底测试,但请随时与我联系以解决任何问题。
64位代码使用寄存器传递参数(所以最好不要篡改它们)。
在这里,我们**不**使用JMP指令,而是使用堆栈来传递地址和RET。

#include<Windows.h>
#include<process.h>

//68 78 56 34 12 c7 44 24 04 54 63 72 81 c3
// PUSH 12345678h
// mov [rsp+4],81726354
// ret //will cause the jump

BYTE TrapJmp[] = {0x68,0x78,0x56,0x34,0x12,0xc7,0x44,0x24,0x04,0x54,0x63,0x72,0x81,0xc3};
BYTE TrapJmp_FromTrampoline[] = {0x68,0x78,0x56,0x34,0x12,0xc7,0x44,0x24,0x04,0x54,0x63,0x72,0x81,0xc3};

BYTE StoreOriginal[sizeof(TrapJmp)];
BYTE OriginalOpCode[sizeof(TrapJmp)*2]; //this will maintain the trampoline of the missing opcode;

typedef int (WINAPI *pMessageBox)(HWND, LPCWSTR, LPCWSTR, UINT);

pMessageBox pOriginal = NULL;

int //this function is only used for testing the hook concept
WINAPI
myMessageBoxW(
__in_opt HWND hWnd,
__in_opt LPCWSTR lpText,
__in_opt LPCWSTR lpCaption,
__in UINT uType)
{
printf("you are trapped\n");

//retore it (for API hooking, not thread safe/recursive safe)
/*memcpy(pOriginal,StoreOriginal,sizeof(StoreOriginal));
FlushInstructionCache(GetCurrentProcess(),pOriginal,sizeof(TrapJmp));
MessageBoxW(hWnd,lpText,lpCaption,uType);
memcpy(pOriginal, TrapJmp, sizeof(TrapJmp)); //repatch
FlushInstructionCache(GetCurrentProcess(),pOriginal,sizeof(TrapJmp));
*/

//lets call the originalOpCode...this is more like Detours excepts its free :-))
pMessageBox org=(pMessageBox)(void*)OriginalOpCode;
org(hWnd,L"asif",L"asiasdas",uType);

return 0;
}

int _tmain(int argc, _TCHAR* argv[])
{
MessageBoxW((HWND)123,L"asif",L"asif",789);

{
pOriginal = MessageBoxW;
DWORD dPermission=0;VirtualProtect(TrapJmp,sizeof(TrapJmp),PAGE_EXECUTE_READWRITE,&dPermission);
VirtualProtect(pOriginal,sizeof(TrapJmp),PAGE_EXECUTE_READWRITE,&dPermission);
memcpy(StoreOriginal,pOriginal,sizeof(TrapJmp)); //copy the original to be called later

DWORD64 JmpDiff =(DWORD64)myMessageBoxW;
memcpy(&TrapJmp[1],&JmpDiff,sizeof(DWORD)); //put the first half of the 8 byte address to jump to
memcpy(&TrapJmp[9],(char*)&JmpDiff+4,sizeof(DWORD)); //put the remaing 4 byte address

memcpy(pOriginal,TrapJmp,sizeof(TrapJmp)); //set the hook
FlushInstructionCache(GetCurrentProcess(),pOriginal,sizeof(TrapJmp));

//lets set up the trampoline to the original function
memcpy(OriginalOpCode,StoreOriginal,sizeof(StoreOriginal));
VirtualProtect(OriginalOpCode,sizeof(OriginalOpCode),PAGE_EXECUTE_READWRITE,&dPermission);

DWORD64 JmpDiff_trampoline =(DWORD64)pOriginal;
JmpDiff_trampoline+=sizeof(TrapJmp);
memcpy(&TrapJmp_FromTrampoline[1],&JmpDiff_trampoline,sizeof(DWORD)); //put the first half of the 8 byte address to jump to
memcpy(&TrapJmp_FromTrampoline[9],(char*)&JmpDiff_trampoline+4,sizeof(DWORD)); //put the remaing 4 byte address

memcpy(&OriginalOpCode[sizeof(TrapJmp_FromTrampoline)],TrapJmp_FromTrampoline,sizeof(TrapJmp_FromTrampoline));

/*
Change
00007FF73378A180 48 83 EC 38 sub rsp,38h
00007FF73378A184 45 33 DB xor r11d,r11d
00007FF73378A187 44 39 1D 36 5B 01 00 cmp dword ptr [7FF73379FCC4h],r11d //we change certain instructions to NOP (this is specific to some APIs only)

To
00007FF73378A180 48 83 EC 38 sub rsp,38h
00007FF73378A184 45 33 DB xor r11d,r11d
00007FF73378A187 44 39 1D 36 5B 01 00 NOP
NOP
NOP
NOP
NOP
NOP
NOP
*/

memset(&OriginalOpCode[7],0x90,7);
}

MessageBoxW(0,L"asif",L"asif",0);
MessageBoxW(0,L"asif",L"asif",0);
MessageBoxW(0,L"asif",L"asif",0);

return 0;
}

如果你使用了我们的32/64位detour实现,请确保EIP不会跳转到中间的opcode,否则处理器会将其解释为不同的指令。

eg:-
sub         rsp,38h  
opcode is
48 83 EC 38 at address 00007FF73378A180

Jumping to an adress 00007FF73378A181 will result in opcode
83 EC 38 //this will be interpreted to be another instruction

 

关注点

除了内存分析,detour还允许用户记录API调用,用于日志记录或逆向工程应用程序(我内心深处的黑客说他可以)。

你可以使用CrashAnalyzer_v2附加到任何正在运行的进程(在本例中是Debuggee_TestApp)来检测内存泄漏和研究其他API的内存分配。

© . All rights reserved.