Process-wide API spying - an ultimate hack 摘要翻译(一)
????? 進程范圍的API掛鉤一般都是基于修改目標執行文件的輸入函數表(IAT)。通過把目標進程中調用需要掛鉤的API函數替換為用戶編寫的函數,達到掛鉤的目的。當然一般情況下,用戶的替換函數都是對API的傳入參數進行記錄或者驗證,然后再調用掛鉤的API函數。API Spy一般都是把進行Hook和Spy工作的驅動DLL文件通過一個控制程序注射到目標進程,驅動DLL文件和控制程序之間通過WM_COPYDATA進行通信。一旦驅動DLL文件進入到目標進程,它就用用戶已定義好的代理函數地址來修改目標進程中需要掛鉤的API的輸入函數表地址。通常,每個API的掛鉤函數都是不同的,但在一些特定的環境下,也可以使用一個相同的代理函數來修改不同的API函數,具體的實現在文章中將會在文章中詳細描述。
定位輸入函數表
(本段是一些PE文件格式的基本知識,不做翻譯,只給出作者使用的代碼)
IMAGE_DOS_HEADER *
dosheader=(IMAGE_DOS_HEADER *)hMod;
IMAGE_OPTIONAL_HEADER * opthdr =
? (IMAGE_OPTIONAL_HEADER *) ((BYTE*)hMod+dosheader->e_lfanew+24);
IMAGE_IMPORT_DESCRIPTOR
*descriptor=
????? (IMAGE_IMPORT_DESCRIPTOR *)(BYTE*) hMod +
????? opthdr->DataDirectory[ IMAGE_DIRECTORY_ENTRY_IMPORT].
VirtualAddress;
while(descriptor ->FirstThunk)
{
??? char*dllname=(char*)((BYTE*)hMod+ descriptor ->Name);
??? IMAGE_THUNK_DATA* thunk=( IMAGE_THUNK_DATA*)((BYTE*) hMod +
???????????????????????????????? descriptor ->OriginalFirstThunk);
??? int x=0;
??? while(thunk->u1.Function)
??? {
??????? char*functionname=(char*)((BYTE*) hMod +
??????????????? ( DWORD)thunk->u1.AddressOfData+2);
??????? DWORD *IATentryaddress=( DWORD *)((BYTE*) hMod +
??????????????? descriptor->FirstThunk)+x;
??????? x++; thunk++;
??? }
??? descriptor++;
}
API SPY的實現
整個實現由四個函數組成:ProxyProlog(), Prolog(), ProxyEpilog()和Epilog(). 其中ProxyProlog和Prolog是在被掛鉤的API前調用,ProxyEpilog和Epilog是在被掛鉤的API后調用。ProxyProlog和ProxyEpilog是固定函數,它們的任何是保存/恢復調用Prolog和Epilog函數前后的CPU寄存器和標志位,由匯編語言寫成;Prolog和Epilog才是真正掛鉤應用函數,由C語言寫成。
Windows使用的是平面內存模型,代碼和數據可以存放在一起。所以我們可以先一段機器指令放在執行段上,然后再調用它。比如下面的代碼:
DWORD addr=(DWORD)&retbuff[6];
retbuff[0]=0xFF; retbuff[1]=0x15;
memmove (&retbuff[2],&addr,4);
addr=(DWORD)&ProxyEpilog;
memmove (&retbuff[6],&addr,4);
(譯注:其實retbuff的代碼的匯編很簡單:
CALL DWORD PTR ProxyEpilog
另外這里為什么CALL是0xFF,0x15,在Intel的指令手冊上可以查到“FF /2 CALL r/m32 Call near, absolute indirect, address given in r/m32”,后面的對應會查到/2= 15 )
CALL操作實際就是把CALL下面第一條指令的地址壓棧,然后JMP到CALL調用的函數地址,所以對于上面的代碼就是ProxyEpilog的地址壓入到棧頂。(譯注:下面這段講得不是很清楚,直接貼上英文)
When DllMain() is called with fdwReason set to DLL_PROCESS_ATTACH, we fill retbuff array with the machine instructions (retbuff is a global BYTE array), dynamically allocate some memory, allocate Tls index, and store the memory we have allocated in the thread local storage. Every time DllMain() is called with fdwReason set to DLL_THREAD_ATTACH, it must dynamically allocate some memory and put it aside into thread local storage.
修改IAT的代碼,用ProxyProlog函數:
struct RelocatedFunction{
??? DWORD proxyptr;
??? DWORD funtioncptr;
??? char *dllname;
??? char *functionname;
};
BYTE* ptr=(BYTE*)HeapAlloc(GetProcessHeap(),HEAP_ZERO_MEMORY,32);
RelocatedFunction * reloc=(RelocatedFunction*)&ptr[6];
DWORD addr=(DWORD)&ProxyProlog;
reloc->proxyptr=addr;
reloc->funcname= functionname;
reloc->dllname=dllname;
memmove (&reloc->functionptr, IATentryaddress,4);
ptr[0]= 0xFF; ptr[1]= 0x15; memmove(&ptr[2],&reloc,4);
DWORD byteswritten;
WriteProcessMemory(GetCurrentProcess(),IATentryaddress,&ptr,4,&byteswritten);
對于IAT入口的替換,首先動態分配一個數組,前6個字節間接調用ProxyProlog函數,其中前2字節是0xFF,0x15,后面4個字節是RelocatedFunction的結構地址。后16個字節是一個RelocateFunction的結構,第一個就是ProxyProlog()的地址,它必須放在第一位;其它的成員分別表示引入函數的地址和名稱,以及該引入函數所屬的DLL名稱。對于每個替換的函數入口都需要這樣一個隊列。
所以,每次調用被替換的API函數,就會調用我們手動編碼的ProxyProlog函數。CALL指令會隱含的把會下一個地址保存在棧上,以便當前函數的返回。在此處,我們的RelocatedFunction結構體就會被放在棧頂,該API函數的返回地址在棧上的前面一個位置,在前面就是該API函數的參數。
以下是ProxyProlog()和Prolog()的實現。
__declspec(naked)void ProxyProlog()
{
??? _asm{
??????? //保存寄存器
??????? push eax
??????? push ebx
??????? push ecx
??????? push edx
??????? mov ebx,esp
??????? //保存CPU標志
??????? pushf
??????? //把RelocatedFunction的地址壓棧,作為Prolog()函數的參數
??????? add ebx,16
??????? push ebx
??????? call Prolog
??????? popf
??????? pop edx
??????? pop ecx
??????? pop ebx
??????? pop eax
??????? ret
???? }
}
struct Storage{
???? DWORD retaddress;
???? RelocatedFunction* ptr;
};
void __stdcall Prolog(DWORD * relocptr)
{
??? //獲取RelocatedFunction的地址
??? RelocatedFunction * reloc=(RelocatedFunction*)relocptr[0];
??? //獲取API函數的返回地址
??? DWORD *retaddessptr=relocptr+1;
??? //保存RelocatedFunction結構體地址和API返回地址在TLS上
??? DWORD *nestlevelptr=(DWORD *)TlsGetValue(tlsindex);
??? DWORD nestlevel=nestlevelptr[0];
??? Storage*storptr=(Storage*)&nestlevelptr[1];
??? storptr[nestlevel].retaddress=(*retaddessptr);
??? storptr[nestlevel].ptr=reloc;
??? nestlevelptr[0]++;
??? //把API函數的調用地址放在棧頂,替換了以前的RelocatedFunction結構體地址
??? relocptr[0]=reloc->funcptr;
??? //把全局數組retbuffer的地址(在前面已經填充),填入到原來API在棧上的返回地址處。這樣,
??? //在ProxyProlog返回的時候,就會跳轉到原來的API函數入口點,API函數就返回到ProxyEpilog()
??? retaddessptr[0]=(DWORD)&retbuff;
}
以下是ProxyEpilog()和Epilog()的實現。
__declspec(naked)void ProxyEpilog()
{
??? _asm{
??????? //這里的eax保存API函數的返回值
??????? push eax
??????? push ebx
??????? push ecx
??????? push edx
??????? mov ebx,esp
??????? pushf
??????? //把API函數的返回值的地址作為Epilog()的參數
??????? add ebx,12
??????? push ebx
??????? call Epilog
??????? popf
??????? pop edx
??????? pop ecx
??????? pop ebx
??????? pop eax
??????? ret
??? }
}
void? __stdcall Epilog(DWORD*retvalptr)
{
??? //獲取ProxyEpilog()的返回地址的指針
??? DWORD*retaddessptr=retvalptr+1;
??? //獲取API返回值
??? DWORD retval=retvalptr[0];
??? //獲取在Prolog中保存在TLS上的API返回地址以及RelocatedFunction結構體的地址
??? DWORD *nestlevelptr=(DWORD *)TlsGetValue(tlsindex);
??? nestlevelptr[0]--;
??? DWORD nestlevel=nestlevelptr[0];
??? Storage*storptr=(Storage*)&nestlevelptr[1];
??? RelocatedFunction * reloc=(RelocatedFunction*)storptr[nestlevel].ptr;
??? //把原來API的返回地址寫回到ProxyEpilog()的返回地址上。
??? retaddessptr[0]=storptr[nestlevel].retaddress;
??? //收集保存的API信息,然后通過WM_COPYDATA傳給應用程序
??? DWORD id=GetCurrentThreadId();
??? char buff[256];char smallbuff[8];char secsmallbuff[8];
??? strcpy(buff, "Thread ");wsprintf(smallbuff,"%d/n",id);
??? strcat(buff,smallbuff);strcat(buff," -? ");
??? strcat(buff,reloc->dllname);strcat(buff,"!");
??? strcat(buff,reloc->funcname);
??? strcat(buff," -? ");
??? strcat(buff,"returns ");
??? wsprintf(secsmallbuff,"%d/n", retval);
??? strcat(buff,secsmallbuff);
??? COPYDATASTRUCT? data;data.cbData=1+strlen(buff);
??? data.lpData=buff;data.dwData=WM_COPYDATA;
??? SendMessage(wnd,WM_COPYDATA,(WPARAM) secwnd,(LPARAM) &data);
}
從以上的實現來看,可以用以下結論:所有的“偷窺”行為都沒有影響程序的執行,因為整個行為都沒有影響應用程序關系的寄存器、CPU標志和堆棧。另外一方面,整個實現是通用的,不和具體的API相關,并適用于多線程。如果要對實現添加對API參數的跟蹤,需要用戶程序提供對參數的描述,因為這部分內容是不能夠通過PE文件分析獲取的。
警告:不能夠使用上述的實現去HOOK從MSVCRT.DLL的輸出函數,只能夠去HOOK MSVCRT.DLL文件中的調用函數,比如MSVCRT.DLL的輸入表。
總結
以上是生活随笔為你收集整理的Process-wide API spying - an ultimate hack 摘要翻译(一)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 发售近一周 华为nova2s口碑惊人
- 下一篇: 深蓝算法反演AOD入门记录(一)