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的输入表。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值