API HOOK的 IAT方法

一.        IAT

 

IAT 法就是通过修改 IAT 表中的函数地址而达到的 API 截获的方法。

 

1 .技术与实现

 

每个调用的 API 函数地址都保存在 IAT 表中。 API 函数调用时,每个输入节( IMPORT SECTION )所指向的 IAT 结构如下图所示。

程序中每个调用 API 函数的 CALL 指令所使用的地址都是相应函数登记在 IAT 表的地址。所以为了截获 API 函数,我们将 IAT 表中的地址换成用户自己的 API PROXY 函数地址,这样每个 API 调用都是先调用用户自己的 API PROXY 函数。在这个函数中我们可以完成函数名称的记录、参数的记录、调用原来的过程,并在返回时记录结果。

 API PROXY 代码是这个 IAT HOOK API 方法的核心了,为了完成它,我使用了两段程序:结构 HOOKAPIPROXY 和函数 PROXYFUN 。其中前者每个函数使用一个这样的结构,它用来保存函数原来的地址和函数名称;而后者只有一个,它用来分析记录函数的名称、参数、结果等等。使用了这两个结构之后,程序的代码结构变成了下图所示:  

下面我们来详细分析这两段程序。

 

1)   HOOKAPIPROXY 结构程序

 

typedef struct {
  byte PushCode1;  //0xff
  ULONG OrgAddr;
  byte PushCode2;  //0xff
  ULONG NameAddr;
  byte JmpCode;    //0xe9
  ULONG JmpParam;
}*PHOOKAPIPROXY, HOOKAPIPROXY;

 

每个 API 函数的 IAT 项都执行一个 HOOKAPIPROXY 结构,这个结构其实是一段代码,它是由三段语句组成。

 

a) push 原来 IAT 的地址。

b) push 函数名称的地址。

c) jmp 到同一个 ProxyFun() 程序段中。这个程序段就是下面的一个程序段了。

 

2 ProxyFun() 代理程序段

所有的 API 函数都要执行下面的这个统一的代码段。这段核心的代码主要使用 1 )代码中保存在堆栈中的信息,得到函数名称,所有参数,返回结果。函数的代码如下:  

_declspec(naked) void ProxyFun()
{
//
局部变量的定义

DWORD ByteWrite;  // 写入日志文件中的字节

PCHAR pFunctionName; // 取函数名称

char Str[200];  // 字符串

ULONG Param[PARAMNO]; // 取出的参数

char StrParam[200], StrTemp[20];

ULONG nParam, i;

ULONG Result;

 

_asm{
 push ebp
 mov ebp, esp
 sub esp, __LOCAL_SIZE
 push esi
 push edi
 push ebx

// 先取出函数名称地址。
 mov eax, [ebp+4]
 mov pFunctionName, eax
 
 //
下面拷贝参数 PARAMNO * 4
 mov ecx, PARAMNO
 mov esi, ebp
 add esi, 12 + PARAMNO * 4 

//4 :原函数 EIP 4 :原来函数地址 + 4:FUNCTIONNAME + PARAMNO 参数

nextmove:
 mov eax, [esi]
 push eax
 sub esi, 4
 dec ecx
 jnz nextmove

mov nParam, esp
 //
调用原来的函数。

 call dword ptr [ebp + 8]
 mov Result, eax  //
保存可能的返回值
 
 mov ecx, esp
 //
先复原 ESP
 mov esp, nParam 
 add esp, PARAMNO * 4
 //
判断到底有几个参数。

 sub ecx, nParam
 shr ecx, 2
 mov nParam, ecx
 jcxz noparam   //
若没有参数

// 下面拷贝参数
 lea esi, Param
 mov edi, ebp
 add edi, 16

nextparam:
 mov eax, [edi]
 mov [esi], eax
 add esi, 4
 add edi, 4
 loop nextparam
noparam: //
若没有参数 , 便直接出来。

 }

 

// 往文件中写数据。
//
(此处略,下面有详细说明)  ....


   // 准备返回
   _asm{

     mov eax, Result
     mov ecx, nParam
     shl ecx, 2

 pop ebx
 pop edi
 pop esi
 mov esp, ebp
 pop ebp
 
 add esp, 8  //4
:原来函数地址 + 4:FUNCTIONNAME PARAMNO 参数

 pop edx
 add esp, ecx
 push edx
 ret
 }
}
 

 

这是一个naked 函数,关于 naked 函数的书写,我将在以后专门写篇文章来介绍。在这个函数中,需要实现的功能主要是围绕堆栈来进行的。下图是本过程中堆栈的变化图。

 

由于 proxyfun 函数在整个 IAT APIHOOK 中至关重要,所以下面我们将从函数功能的实现来详细说明它。

 

a) 函数名称

这比较简单,因为在 APIHOOKPROXY 结构中将函数放在堆栈中,所以 [EBP + 4] 中保存着函数名称的地址。

 

b) 函数参数

这是复杂的过程。在这里,我使用了一个小技巧——这个技巧对于 WINAPI 函数调用方式 (stdcall) 是有效的。为了计算参数个数,我们先将 PARAMNO( 定义的整数 ) DWORD 堆栈值并保存好 ESP 。然后调用原来的函数 (call [ebp + 8]) 。由于 WINAPI 调用方式的函数在返回前释放堆栈中的参数,所以在函数返回后 ESP 值与原来的是不一样的(当每个参数时是一样的),而它们之间的差值就是参数的个数的信息。

 

c) 函数的返回值

调用完函数后,函数的返回值放在 EAX 中。当然 VOID 无返回值的函数也将记录 EAX 值。

 

d )返回至函数调用点

函数的调用点 EIP 值放在堆栈的 [EBP + 12] 中。

 

2 .信息保存

 

上面的代码中,我们没有涉及到代码的保存部分。这是个独立的部分,我们将信息保存在 LOG 文件中。  

// 打开文件

hLog = CreateFile("c://ApiHook.txt", GENERIC_WRITE, FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);

hEvent = CreateEvent(NULL, FALSE, TRUE, "BitBltEvent");

....

 

// 记录信息

StrParam[0] = '/0';

for(i = 0; i < nParam; i ++)

{

sprintf(StrTemp, " 0x%08x ", Param[i]);

strcat(StrParam, StrTemp);

}

 

WaitForSingleObject(hEvent, INFINITE);

SetFilePointer(hLog, 0, NULL, FILE_END);

if ((ULONG)pFunctionName >= 0x80000000)

sprintf(Str, "[Ord: 0x%x] 参数个数 : %d [%s] 返回值 : 0x%x /x0d/x0a", (ULONG)pFunctionName - 0x80000000, nParam,    StrParam, Result);

else

sprintf(Str, "[%s] 参数个数 : %d [%s] 返回值 : 0x%x /x0d/x0a", pFunctionName, nParam, StrParam, Result);

WriteFile(hLog,  Str, strlen(Str), &ByteWrite, NULL);

SetEvent(hEvent);

 

...

// 关闭文件

CloseHandle(hEvent);

CloseHandle(hLog);

 

因为可能有多个线性调用 API 函数并被截获,所有需要用一个事件内核对象 (hEvent) 来同步对文件的写操作。

 

需要注意上面的记录信息这段代码是不能放在 proxyfun 中的。因为你可能已经截获了 WaitForSingleObject SetFilePointer WriteFile 或者 SetEvent ,这样会引起递归调用而引起死锁和栈溢出。为了解决这个问题,我们在 HOOK API 前先将这些函数的地址保存在全局变量中。

WriteFileAddr = (ULONG)WriteFile;

SetFilePointerAddr = (ULONG)SetFilePointer;

SetEventAddr = (ULONG)SetEvent;

WaitForSingleObjectAddr = (ULONG)WaitForSingleObject;

于是 proxyfun 的信息记录过程将变成如下:

 

#define PSETFILEPOINTER DWORD (WINAPI *)(HANDLE, LONG, PLONG, DWORD)

#define PWRITEFILE BOOL (WINAPI *)(HANDLE, LPCVOID, DWORD, LPDWORD, LPOVERLAPPED)

#define PWAITFORSINGLEOBJECT DWORD (WINAPI *)(HANDLE, DWORD)

#define PSETEVENT BOOL (WINAPI *)(HANDLE)

 

...

// 往文件中写数据。

StrParam[0] = '/0';

for(i = 0; i < nParam; i ++)

{

sprintf(StrTemp, " 0x%08x ", Param[i]);

strcat(StrParam, StrTemp);

}

 

((PWAITFORSINGLEOBJECT)WaitForSingleObjectAddr)(hEvent, INFINITE);

((PSETFILEPOINTER)SetFilePointerAddr)(hLog, 0, NULL, FILE_END);

if ((ULONG)pFunctionName >= 0x80000000)

sprintf(Str, "[Ord: 0x%x] 参数个数 : %d [%s] 返回值 : 0x%x /x0d/x0a", (ULONG)pFunctionName - 0x80000000, nParam, StrParam, Result);

else

sprintf(Str, "[%s] 参数个数 : %d [%s] 返回值 : 0x%x /x0d/x0a", pFunctionName, nParam, StrParam, Result);

((PWRITEFILE)WriteFileAddr)(hLog,Str,strlen(Str),&ByteWrite,NULL);

((PSETEVENT)SetEventAddr)(hEvent);

 

这样便完成了信息的记录工作了。

 

3 .优缺点

使用此方法进行 API 截获时,你会发现有一些 API 函数总是截获不了,比如消息循环中的 GetMessage 等消息,这是因为编译器做了大量的优化工作,它可能早将 IAT 中函数地址保存在某个寄存器或者变量(就像上文介绍的信息记录的方法)了,所以你即使改变了 IAT 表中的地址值,也不会截获到这些 API 调用。这便是 IAT 法的最大的缺点——不能完全截获。

这个方法的优点是实现简单,兼容性好。

源代码下载

 

 

 

 

阅读更多
想对作者说点什么?

博主推荐

换一批

没有更多推荐了,返回首页