从PE文件入手绕过IAT HOOK
2007-04-28 16:08

IAT HOOK简介

API HOOK估计没必要多介绍了,简单的来讲就是通过某种方式来改变API函数的工作流程.一般来讲有两种方法:IAT HOOK和INLINE HOOK.前一种应用较为广泛,一方面因为简单,还一方面因为稳定.他的原理就是改写进程空间中要HOOK的API所在模块的函数引入表,使之指向替换原 API函数的函数地址(某些木马就是利用IAT HOOK的方式,挂钩NtQuerySystemInformation的方式来实现进程隐藏).这里感觉还是有必要再说一点INLINE HOOK,这个复杂点,直接进入被HOOK的API函数内部去修改他,采用指令call或者jmp等,迫使API改变流程,跳到自己的替换函数中.通常都 是在函数头部前10个字节内修改.

如何搞定IAT HOOK

注意这里说的是搞定,而不是修复IAT HOOK,搞定所指的就是只要不让那个IAT HOOK起作用就行了.

方案1:通过LoadLibrary和GetProcAddress来动态获取API地址.

忘了是在哪看到过这个方法,实际上这个方法可以说根本无效,Jeffrey Richter在核心编程里面给出的例子就提到过这个问题,为了在动态获取API调用的情况下也能让HOOK生效,首先就应该把 LoadLibraryA,LoadLibraryW......等等那几个可以实现动态获取的函数全部都HOOK住.这种方案被直接否决了.

方案2:直接硬编码,从ntdll.dll里面调用NativeAPI.

虽然听起来有点恐怖,但这种方式确实比方案1要有效,只是ntdll.dll有500多函数,全部应编码有点天方夜谭,如果单纯了为了对付某几个特定的HOOK还是可以的.

方案3:直接从PE文件入手,自己读取导出表获取API地址

这也是我认为对付IAT HOOK最有效的方式,无论是检测IAT HOOK还是绕过IAT HOOK或者是修复IAT HOOK都必须走这一步.

实现方式

既然从PE文件入手,就不能不熟悉PE文件格式了,这里着重介绍一下导出表,因为这个表里面dll所导出的API函数地址,先来看一看导出表的数据结构:
typedef struct _IMAGE_EXPORT_DIRECTORY {
      DWORD     Characteristics;
      DWORD     TimeDateStamp;
      WORD      MajorVersion;
      WORD      MinorVersion;
      DWORD     Name;
      DWORD     Base;
      DWORD     NumberOfFunctions;
      DWORD     NumberOfNames;
      DWORD     AddressOfFunctions;       // RVA from base of image,函数地址相对于镜像基址的偏移
      DWORD     AddressOfNames;           // RVA from base of image,函数名表相对于镜像基址的偏移
      DWORD     AddressOfNameOrdinals;    // RVA from base of image,函数名和序号的对应表相对于镜像基址的偏移
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

重要的就是那三个加了中文注释的地方,这里存着没有被HOOK践踏过的真真切切的API信息,但是问题来了,这里不能直接用,因为是内存镜像偏移, 先要转化为文件偏移才可以,如何转换呢?罗sir的<WIN32汇编语言程序设计>里面写的很清楚了,首先看虚拟偏移RAV落在内存镜像中的 哪一个节中,然后减去内存镜像中这个节的起始值得到一个RAV',然后在节表中看这个节在文件中所在的偏移地址PointToRawData,然后用 PointToRawData+RAV'就是在文件中的偏移了.

算法看起来好像有点繁,但实际上不要忘了我们是为了对抗IAT HOOK的,这样一来可以简化不少,看一看那几个dll,ntdll.dll,kernel32.dll等等等等包含常用API的dll,我们要的东西都 在.text节里面,上述转换过程可以直接变为: dwRav-m_pOptionHeader->BaseOfCode+m_pOptionHeader->SizeOfHeaders.

转换问题解决了,就来看看如何获取我们想要的地址,废话少说,上代码:

//m_pBuffer是将dll用ReadFile读入内存后的缓冲区地址
FARPROC CLoadDll::SearchProcAddress(LPCTSTR strFunctionName)
{
DWORD dwIndex = 0;

DWORD dwRawNameAddr;    //函数名表地址
DWORD dwRawAddrIndex; //函数名与编号转换表
DWORD dwRawFuncAddr;    //函数表地址
DWORD dwFuncOffset;     //函数的文件偏移地址

dwRawNameAddr = RavToOffset(m_pExportDesc->AddressOfNames);      //获取PE文件中函数名表的偏移
dwRawAddrIndex = RavToOffset(m_pExportDesc->AddressOfNameOrdinals);     //获取PE文件中函数名-序号对应表的偏移
dwRawFuncAddr = RavToOffset(m_pExportDesc->AddressOfFunctions);    //获取PE文件中函数地址表偏移
for(dwIndex = 0; dwIndex < m_pExportDesc->NumberOfFunctions; dwIndex++){
    if(strcmp(strFunctionName, (TCHAR *)(m_pBuffer + RavToOffset(*(DWORD *)(m_pBuffer+dwRawNameAddr+dwIndex*4)))) == 0){
     dwFuncOffset = *(DWORD *)(m_pBuffer + dwRawFuncAddr + (*(WORD *)(m_pBuffer + dwRawAddrIndex + 2*dwIndex))*4);
     dwFuncOffset += m_dwImageBase;
     return (FARPROC)dwFuncOffset;
    }
}

MessageBox(NULL, "导出表中无此函数!", "提示", MB_OK | MB_ICONINFORMATION);
return NULL;
}

这样以来我们就可以随心所欲的绕过R3态IAT HOOK了,直接去ntdll.dll导出native api来用,怎一个爽字了得.

最后来看看效果截图,用自己的CDllLoad类做了一个进程管理器,直接去ntdll.dll中获取NtQuerySystemInformation来看看进程,另外还开了一个IAT HOOK来隐藏进程,再拿优化大师的进程管理器对比一下效果:

结束语

原理已经知道了,实际我们还可以做更多的事情,比如枚举dll中的导出函数获取地址,可以检查系统中安装的IAT HOOK,或者再走远一点,直接用获取的未污染的API地址,把HOOK修复吧.