读书笔记_windows下的混合钩子(HOOK)_part 1

来源:互联网 发布:淘宝课程怎么下载 编辑:程序博客网 时间:2024/06/16 08:42

读书笔记_windows下的混合钩子(HOOK)_part 1

      

在windows,所谓的混合钩子,指的是即使用用户态下的钩子,又使用内核态的钩子。优点是既能实现强大的功能,又能避免被安全软件发现。

 

混合HOOK使用IAT HOOK 勾住用户空间进程,下面来看IAT HOOK

每个API函数的地址都保存在IAT表中,每个CALL指令所使用的地址都是相应函数登记在IAT表中的地址。所以可以修改IAT表中的地址换成用户自己的API PROXY函数地址,这样函数的调用过程首先是调用自己的函数,然后再调用原来的过程。

API PROXY 这个过程是核心,包含两段程序,结构HOOKAPIPROXY用来保存函数原来的地址和函数名,PROXYFUN用来自己定义的一些操作:

 

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

 

每个API函数的IAT项都执行了一个HOOKAPIPROXY结构,其包含了三层含义:

1.  Push 原来的IAT地址

2.  Push函数名称的地址

3.  Jmp到同一个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+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 dwordptr [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
 }
}  

1.  函数名称

在[EBP+4]的地址中保存着函数的地址保存着函数的名称的地址

2.  函数参数

Win API函数调用方式(stdcall)的一种技巧。为了计算参数个数,我们先将PARAMNO个DWORD堆栈值保存在ESP中。然后调用原来的函数(call[ebp+8]), 由于WINAPI调用方式的函数在返回前释放堆栈中的参数,所以在函数返回后ESP值与原来的不同,前提是每个参数都是一样的,它们之间的差值就是参数的个数。

3.  函数的返回值

调用完参数后,函数的返回值放在EAX中。即使VOID也会记录EAX的值

4.  返回至函数调用点

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

上面的代码中,我们没有涉及到代码的保存部分。这是个独立的部分,我们将信息保存在 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);

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

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

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

接下来看混合钩子的例子:

Windows提供了一个函数PsSetImageLoadNotifyRoutine,该函数可以在加载目标进程或DLL时获得通知,该函数注册了一个每次将映像加载到内存中都要调用的驱动程序回调例程。PsSetImageLoadNotifyRoutine是在内核中使用的例程,它只有一个参数,即回调例程的地址,这个回调例程的声明如下:

Void MyImageLoadNotify ( IN PUNICODE_STRING, IN HANDLE, INPIMAGE_INFO);

 

UNICODE_STRING 包含了由内核加载的模块名;HANDLE参数是模块加载到的目标进程的PID,钩子需要放到该PID的内存上下文中。IMAGE_INFO结构包含了钩子需要的有用的信息,例如加载到内存中映像的基地址。IMAGE_INFO结构定义如下:

Typedef struct _IMAGE_INFO {

Union {

 ULONG Properties;

 Struct {

     ULONG ImageAddressingMode : 8;

     ULONG SystemModeImage : 1;

     ULONG ImageMappedToAllPids : 1;

     ULONG Reserved : 22;

};

};

PVOID ImageBase;

ULONG ImageSelector;

ULONG ImageSize;

ULONG ImageSectionNumber;

}IMAGE_INFO, *PIMAGE_INFO;

 

在回调函数中,首先要判断它是否为要勾住其IAT的模块,如果不知道进程中哪些模块导入了一个要过滤的特定函数,则可以勾住指向勾住函数的所有IAT。一下示例调用HookImportsOfImage来分析模块并找到其IAT项,从而勾住所有的模块。可以通过process ID或image name来过滤自己需要的IAT,如果不知道就需要勾住所有的IAT

VOID MyImageLoadNotify ( IN PUNICODE_STRING FullImageName, IN HANDLEProcessID, IN PIMAGE_INFO ImageInfo)

{

    UNICODE_STRINGu_targetDLL;

    DbgPrint(“Image name:%ws\n”, FullImageName->Buffer);

  

   RtlInitUnicodeString(&u_targetDLL, L”\\WINDOWS\\system32\\kernel32.dll”);

    If (RtlCompareUnicodeString ( FullImageName, &u_targetDLL, TRUE) == 0)   

    {

        HookImportsOfImage (ImageInfo->ImageBase, PorcessId);

}

}

 

HookImportsOfImage也是自己定义的函数,用来内存中的PE文件。Windows中大多数的二进制文件都采用了可移植执行文件(Portable Executable,PE)的格式。PE中包含的大多数项都具有相对虚拟地址(Relative Virtual Address, RVA),它们是实际数据相对于PE的内存加载位置的偏移量。钩子在这里的需要检查每个PE导入的DLL。下面介绍分析PE的过程:

介绍一下PE文件,PE文件的两个关键是内存映射文件机制和相对虚拟地址。

首先看内存映射机制。磁盘上的可执行文件和当它被windows调入内存后事非常像的,可以把磁盘上的文件映射到内存中去,而PE的文件的作用就像一个多个格子的整理箱一样,当需要映射时,就会分配一个这样的PE结构,然后把需要载入的模块(DLL,EXE等)按每个段(好像整理箱中的格子)直接放到内存空间中,这就是所谓的内存映射文件机制。

再看相对虚拟地址(RVA)。RVA是文件映射到内存的偏移。例如,载入器把一个文件映射到虚拟地址0x10000开始的内存块,如果一个映像中的实际的表的首地址是0x10464, 那么它RVA就是0x464。如下的公式所示:

(虚拟地址0x10464) —  ( 基地址0x10000) = RVA0x00464

把RVA转化成一个有用的指针,只需把RVA值加载到模块的基地址上即可。基地址是内存映射EXE和DLL文件的首址。

原创粉丝点击