API函数头部HOOK法

时间:2022-02-22 19:07:58

二.API函数头部HOOK

 

  本方法通过改变API函数的头部字节以达到HOOK的目的。

 

1.  技术与实现

 

每个程序中需要使用到的API函数的地址都放在IAT上,通过这个地址,每个CALL能正确的到达函数的头部。其流程如下图所示:

 

API函数头部HOOK法

我们的截获工作就是在这个头部的位置——我们用一个JMP指令替换掉在这儿的一条或几条指令,让它指向各自的HOOKAPIPROXY代码,而在HOOKAPIPROXY中存放一些信息到堆栈后再JMPproxyfun()函数——一个综合处理函数名称、参数和结果的代码段,最后在proxyfun()中返回到函数调用点。其流程变成下图:

 

 

 

API函数头部HOOK法 

API头部HOOK法的重点还是两个结构/代码段和替换指令,下面进行详细分析:

 

1)   替换原来的指令

 

 我门需要使用一个JMP指令替换掉头部的一条或几条指令。一个JMP指令共占5个字节,那么到底要替换掉原来的几个指令呢?很显然,INTEL是没有提供专门的指令来判断一条指令占用几个字节,不过有个牛人Z0MBiE,他提供了一个函数,这个函数以指令地址为参数,返回这条指令占用多少字节。

知道了需要占用替换几个字节,我们将原来的指令拷贝至HOOKAPIPROXY结构中。然后加上JMP指令(5字节),空隙部分用0x90填充。

 

2)   HOOKAPIPROXY结构

在本方法中,这个结构被定义成:

 

typedef struct {

    byte PushCode;  //0xff

    ULONG NameAddr;

    byte CallCode;  //0XEA

    ULONG CallAddr;

    byte OldBytes[20];

    byte JmpCode;    //0xe9

    ULONG JmpAddr;

    int nCopyBytes;

}*PHOOKAPIPROXY, HOOKAPIPROXY;

 

它是由四部分组成,

a)       push 函数名,保存函数名地址到堆栈中

b)      call  proxyfun()函数。这里用到了病毒程序的一个技巧,通过call指令将下一个指令的地址(即OldBytes[20])保存到堆栈中。

c)      替换前原API函数的前面头部的部分字节,字节数就是nCopyBytes

d)      Jmp 到原API函数的头部下一条指令。

 

3)   proxyfun()代码

 

下面是proxyfun()的代码:

_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 + 8]

    mov pFunctionName, eax

 

    //下面拷贝参数PARAMNO * 4

    mov ecx, PARAMNO

    mov esi, ebp

    add esi, 12 + PARAMNO * 4//4Buffer地址 4Name地址 + 4:CALLER地址。 PARAMNO参数

 

nextmove:

    mov eax, [esi]

    push eax

    sub esi, 4

    dec ecx

    jnz nextmove   

 

    mov nParam, esp

    //调用原来的函数。

    call dword ptr [ebp + 4]

    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

    }

}

   

 

    这段代码和IAT法的代码有较多的相似之处,也是围绕着堆栈来开展工作的,它的堆栈图如下:

API函数头部HOOK法

 

从这张图,我们可以非常直观的看出有两点不同:

a)       保存函数名称的堆栈位置变化了。

b)      原来存放函数原来地址变成了 “原来函数的头部”这段内存的地址了,所以直接调用这个地址(CALL [EBP + 4])就可以调用原来的函数功能了。

 

 

2.  信息保存

 

由于和IAT法的截获方法不同,所以在Proxyfun()中信息保存的方法也是不同的。在本方法中,我们为每个需要用到的函数定义了一个有25个字节长的数组:前20个字节保存着从原API函数拷贝的头部;后5字节是个JMP指令,直接转移到API函数的后面字节。

 

通过这样的处理,虽然初始化时和IAT法不同,但是在Proxyfun()中却是一样的调用原函数的J

 

 

3.  优缺点

 

这个函数的优点很明显,它可以截获所有函数的调用。对于在IAT法中出现的保存IAT地址从而不能截获的情况能轻易解决。

 

但是这个问题有个很大可以说是致命的缺点:就是如何控制好函数的替换。Z0MBiE提供的代码虽然可以得出指令的长度,但是假如被替换掉的指令本身就包含指令跳转(如JMP/CALL/RET等)该如何重新计算它们的相对偏移?假如API函数的后面代码中有跳转到头部的指令(如JMP到头部的JMP指令中间的位置,因为这个位置本来可能就是一个指令的开始),该如何控制?这些问题,尤其是后一个问题,可以说是直接宣布了这个方法的死亡。

 

为了挽救这个方法,还有一个替代方法,就是《delphi下深入WINDOWS核心编程》中使用的——仅仅用JMP指令的5个字节覆盖API函数头部,在proxyfun()中在调用原函数前,先用原来的5个字节覆盖,等调用完,再用JMP指令替换。其实这是换汤不换药,而且带来更大的隐患——线程同步调用同一个API函数会发生错误,同时会有API“漏网”,最重要的是不能解决上文提出的关于API函数内存的JMP头部冲突。所以在《windows核心编程》中,jeffrey Richter直接否定了这个方法。

 

总之,对于你熟悉的API函数,没有上述的“JMP等跳转”情况的,使用本方法是简单又高效的,但是若想将本方法作为一个通用的API HOOK方法,估计是不太适合的。