《Windows核心编程》---HOOK API基础


***【在线视频教程】***

好文章,来自【福优学苑@音视频+流媒体】

HOOK API

    HOOK API是指截获特定进程或系统对某个API函数的调用,使得API的执行流程转向指定的代码。最常用的一种挂钩API的方法是改变目标进程中调用API函数的代码,使得它们对API的调用变为对用户自定义函数的调用。


    Windows下应用程序有自己的地址空间,它们只能调用自己地址空间中的函数;因此,在挂钩API之前,必须将一个可以代替API执行的函数的执行代码注入到目标进程,接着将目标进程对该API的调用改为对注入到目标进程中自定义函数的调用。这个自定义函数一般称为代理函数。在代理函数中,可以调用原来的API,也可以做其他事情。


    注入代码到目标进程比较简单的方法是把要注入的代码写到DLL中,然后让目标进程加载这个DLL,这就是所谓的DLL注入技术。而一旦程序代码进入了另一个进程的地址空间,就可以毫无限制的使用进程的资源了。在这个要注入到目标进程的DLL中写一个与感兴趣的API函数的签名完全相同的函数(即代理函数),当DLL执行初始化代码的时候,把目标进程对这个API的调用全部改为对代理函数的调用,即可实现拦截API函数。


    当然,我们也可以利用DLL在目标进程中初始化的机会去创建新的线程,该线程对目标进程有着完全的访问权限。我们可以将它视为守护线程,在接收到通知时,访问目标进程的资源,也可以通过这种方式隐藏自己,创建没有“进程”的线程。


 

前言:


      HOOK API是一种基于HOOK技术为基础的一种钩子方式,HOOK在上一篇关于HOOK技术知识点的讲解里,HOOK是针对消息,并且只有在具备GUI图形的窗口才会有效(详细见HOOK详解),而这种HOOK可以无需消息,或必须具备GUI图形的窗口,因为此方式是挂钩API的,通过模块表里找到API的输出地址,并且通过修改此输出地址达到跳转到自己的API位置,也称为API拦截。


    这是一种黑客手段,同时也是一种给应用程序打补丁的好手段,这一点在Windows上运用的淋漓尽致,众所周知,Windows太过于庞大,以至于微软的员工都不愿意去对内核和其它文件体系去重写和学习调试找漏洞,因为过于庞大出了问题会引起一连串的蝴蝶效应,或者修改其中一行代码导致异常问题。



为什么说这是一种打补丁的好手段呢?


答:当我们发布了一个大型的应用程序,当我们发现了严重的漏洞后,是应用程序里的某个API思路的问题,我们可以通过此方式拦截这个API然后执行我们补丁里新的API达到修复漏洞的方式,因为一个应用程序那么大,出了漏洞,如果让用户重新安装,过于麻烦,不如写一个小型补丁来修补应用程序上的问题,就好像一条渔网,漏网了,如果我们要换个网的话,需要去重新编网或者去买一个,然后在拿回来这个过程,成本就已经算在里了,不如直接用胶带之类的绑起来,然后在慢慢换新的!当然这只是一个比喻!


API HOOK理论:

API:

     API即应用程序接口技术,这个名词相信大家都不会陌生,就是当我们在某个平台上(如Windows,Linux)进行开发时,这些平台为我们提供的相应驱动系统工作的接口,我们通过调用对应的接口来驱动系统执行不同的工作,但更低层来说,就是系统通过封装CPU的汇编指令,集成为不同的功能驱动硬件工作,我们通过调用系统封装好的接口来驱动GPU或相关硬件设备工作,但是系统更低层封装是很复杂的,只有参与过硬件开发的才明白,这里只是稍微概括说下!如果想学习硬件方面可以去看下博主硬件方面的资料!


HOOK:

    HOOK即钩子,负责钩住消息和API,理论可以见博主上篇文章(详细见HOOK详解),当API工作时我们可以提前通过HOOK住这个API,让其拐弯执行我们自己的API,达到API拦截的效果,一般也可以用于修改发布的大型程序补丁!


简单概括一下就是鱼塘就像WIndows或Linux这些平台,而钩子就是鱼钩,我们去当鱼钩构到鱼儿时,鱼儿游动,跑来跑去,我们鱼竿都会有波动!


API HOOK:


    APIHOOK是一种基于HOOK技术为基础的一种钩子方式,HOOK在上一篇关于HOOK技术知识点的讲解里,HOOK是针对消息,并且只有在具备GUI图形的窗口才会有效(详细见HOOK详解),而这种HOOK可以无需消息,或必须具备GUI图形的窗口,因为此方式是挂钩API的,通过模块表里找到API的输出地址,并且通过修改此输出地址达到跳转到自己的API位置,也称为API拦截。


其实现方法有很多,下面给大家说说几种常见的实现方法!


IAT-HOOK:


这种方式需要懂得PE文件格式的结构,这种方式就是通过IAT表的方式获取指定模块,那么问题来了!


什么是IAT表?


答:IAT表里存储的是,在程序中的函数代码(API)地址,但是又不属于程序,意思就是这是函数代码属于外部的,在程序启动的时候被加载到程序当中了,所以此时它属于程序中的一部分,但是并不是本来就是存在程序中的,最明显的例子就是DLL,DLL是在程序运行时被加载进来的,原本DLL是不属于程序的!


IAT表这个可以在PE文件格式里找到,它是一个链表,它的映射文件到内存映射地址可以通过PE文件格式分析得到,且每当新的DLL模块被加载进来时,它就会存储这个DLL里的所有API地址!


此方法Linux不可行,因为Linux的PE文件结构不同,关于Linux平台的HOOK后面在细说!




注册表-HOOK


    注册表HOOK是比较简单的,但是Linux不可行,因为Linux不是使用注册表方式,在不同平台上,当我们运行程序,上面说过了API的理论,如果要使用这些,那么一定会加载平台依赖的库,这些库Windows7以下的系统是使用注册表的方式记录的,我们可以通过修改注册表的方式把我们自己的DLL名字路径放进去,这样每个程序运行时都会自动加载这些依赖库,我们的DLL也顺利被加载进去了,这样我们就可以通过一些手段在DLLMAIN事件里(DLL首次被加载执行得代码)做些事情。


其中我们可以通过这个方式去HOOK已经被加载得某个API修改地址即可,因为DLL被加载好了以后,它就属于这个进程,所以可以随意修改内存,但是代码段还是需要获取权限得,因为代码段得TXT区段是R只可读!修改方法可以参考博主写的游戏修改器的文章!


其还可以把一些系统的一些依赖库模仿一个,函数名都一样,然后就间接性的替换个某个API,但这种方法只能小部分,因为很麻烦,你需要实现所有的代码!


这种方法也不可行,同时也不建议,因为Win7以上这种方法已经不可行了!通用性不强!




API-修改地址方式


    这种方式和IAT表的方式差不多,我们不通过PE文件方式得到IAT表的地址然后寻找特定API的位置,我们则是通过检索模块句柄,然后得到模块地址里的API地址,然后写入跳转地址,然后当CPU去跳转,和IAT表的方式几乎一样!




核心-注入DLL


    上面说的每一种方式的前提是将DLL你的代码方式注入到指定进程下,且指定进程必须加载了你使用的DLL,也就是说APIHOOK建立在DLL这种方式之上,因为程序原本的代码函数不可被检索,已经在编译阶段被编译成地址了,不被记录,只有DLL这样的模块系统会记录到进程下!


    其我们的HOOK代码也必须是DLL,因为在程序下想要把指定API或代码写入到进程下,只有通过DLL这种比较合法的方式,让程序加载,通过DLLMAIN方式让DLL去执行一段SHELLCODE代码,或者可以通过Win提供的进程内存操作函数写入大量代码,让CPU去执行,从而间接实现API注入,但是编写起来较为复杂,不如直接使用IDE编写!




DLL注入


      DLL注入的方式很多如远线程,HOOK,注册表(这个方式在上面的理论知识里说过)等,今天我们使用之前使用过的方式远线程的方式,但是随着Windows的更新,平台安全性越来越高,所以我们实际开发的过程中需要很高的权限,user权限,这已经不是XP时代那个黑客档次较低的时代了,Win8以上的系统,虽然Windows有为我们提供一些进程令牌,但是仅限调试阶段有效,所以还是建议大家去使用驱动代码,内核驱动开发!



1.概述

    Hook技术被广泛应用于安全的多个领域,比如杀毒软件的主动防御功能,涉及到对一些敏感API的监控,就需要对这些API进行Hook;窃取密码的木马病毒,为了接收键盘的输入,需要Hook键盘消息;甚至是Windows系统及一些应用程序,在打补丁时也需要用到Hook技术。接下来,我们就来学习Hook技术的原理。


下图很简单易懂地诠释了Hook的机制,在notepad.exe和kernel32.dll之间挂上一个“钩子”,把它们要使用的CreateFile()函数替换掉,换成MyCreateFile()函数,实现我们想要的自定义功能。

image.png


2.Hook分类

Hook分为应用层(Ring3)Hook和内核层(Ring0)Hook,应用层Hook适用于x86和x64,而内核层Hook一般仅在x86平台适用,因为从Windows Vista的64版本开始引入的Patch Guard技术极大地限制了Windows x64内核挂钩的使用。

image.png


3.消息Hook

3.1 技术原理


首先先来了解下常规的Windows消息流:

[1]发生键盘输入事件时,WM_KEYDOWN消息被添加到[OS message queue]。

[2]OS判断哪个应用程序中发生了事件,然后从[OS message queue]取出消息,添加到相应应用程序的[application message queue]中。

[3]应用程序(如记事本)监视自身的[application message queue],发现新添加的WM_KEYDOWN消息后,调用相应的事件处理程序处理。


所以,我们只需在[OS message queue]和[application message queue]之间安装钩子即可窃取键盘消息,并实现恶意操作。

那么我们该如何安装这个消息钩子呢?很简单,Windows提供了一个官方函数SetWindowsHookEx()用于设置消息Hook,编程时只要调用该API就能简单地实现Hook。


消息Hook常被窃密木马用来监听用户的键盘输入,程序里只需写入如下代码就能对键盘消息进行Hook:

SetWindowsHookEx(

WH_KEYBOARD, //键盘消息

KeyboardProc, //钩子函数(处理键盘输入的函数)

hInstance, //钩子函数所在DLL的Handle

0 //该参数用于设定要Hook的线程ID,为0时表示监视所有线程

)



HOOK的过程:


1)导入函数是被本程序调用,但其实现代码却在其他模块中的函数。API函数全是导入函数,它们的实现代码在Kernel32.dll、User32.dll等Win32子系统模块中。


模块的导入函数名和这些函数驻留的DLL名等信息都保留在它的导入表(import table)中。导入表是一个IMAGE_IMPORT_DESCRIPTOR结构的数组,每个结构对应着一个导入模块。

typedefstruct _IMAGE_IMPORT_DESCRIPTOR
{
       union
       {
              DWORD Characteristics;
              DWORD OriginalFirstThunk; //hint/name(函数序号/名称)表的偏移量,记录导入函数名称
       };
       DWORD TimeDataStamp;
       DWORD ForwarderChain;
       DWORD Name;        //导入模块名称字符串的偏移量
       DWORD FirstThunk;        //IAT(Import Address Table,导入地址表)的偏移量,记录导入函数地址
}IMAGE_IMPORT_DESCRIPTOR;


应用程序启动时,载入器根据PE文件的导入表记录的DLL名(上面的Name域)加载相应的DLL模块,再根据导入表的hint/name表(OriginalFirstThunk指向的数组)记录的函数名取得函数的地址,接着将这些地址保存到导入表的IAT(FirstThunk指向的数组)中。


应用程序在调用导入函数时,要先到导入表的IAT中找到这个函数的地址,然后再调用。模块的IAT仅仅是一个DWORD数组,数组的每个成员记录着一个导入函数的地址。


一种非常常用的HOOK API的方法就是修改模块的导入表;为了保存堆栈的平衡,自定义函数使用的调用规则和参数个数必须与它所替代的API函数完全相同。


 


2)为了修改导入地址表(IAT),首先需定位目标模块PE结构中的导入表的地址。


    PE文件以64字节的DOS文件头开始(IAMGE_DOS_HEADER),接着是一小段DOS程序,然后是248字节的NT文件头(IMAGE_NT_HEADERS)。NT文件头相对文件开始位置的偏移量可以由IMAGE_DOS_HEADER结构的e_lfanew给出。


NT文件头的前4个字节是文件签名(“PE00”字符串),紧接着是20字节的IMAGE_FILE_HEADER结构,下面的代码取得一个指向IMAGE_OPTIONAL_HEADER结构的指针(以主模块为例):


//这里是为了示例,取得主模块的模块句柄


HMODULE hMod = ::GetModuleHandle(NULL);


IMAGE_DOS_HEADER *pDosHeader = (IMAGE_DOS_HEADER*)hMod;


IMAGE_OPTIONAL_HEADER *pOptHeader =


                                   (IMAGE_OPTIONAL_HEADER*)((BYTE*)hMod + pDosHeader->e_lfanew + 24);


IMAGE_OPTIONAL_HEADER包含了许多重要的信息,有推荐的模块基地址、代码和数据的大小和基地址、线程堆栈和进程堆的配置、程序入口点的地址以及我们感兴趣的数据目录表指针。PE文件保留了16个数据目录,最常见的有导入表、导出表、资源和重定位表。


导入表是一个IMAGE_IMPORT_DESCRIPTOR结构的数组,每个结构对应一个导入模块。下面的代码取得导入表中第一个IMAGE_IMPORT_DESCRIPTOR结构的指针(即导入表首地址):


IMAGE_IMPORT_DESCRIPTOR *pImportDesc = (IMAGE_IMPORT_DESCRIPTOR)*


                     ((BYTE*)hMod + pOptHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);


 


除了可以通过PE文件结构定位模块的导入表外,还可以使用ImageDirectoryEntryToData函数,这个函数知道模块基地址后直接返回指向指定数据目录表的首地址:

#include<ImageHlp.h>
#pragmacomment(lib, "ImageHlp")
 
PVOID ImageDirectoryEntryToData(
       PVOID Base,                                                               //模块基地址
       BOOLEAN MappedAsImage,        //如果此参数是TRUE,文件被系统ª当做镜像映射,否则,当做数据文件映射
       USHORT DirectoryEntry,               //指定IMAGE_DIRECTORY_ENTRY_IMPORT说明要取得导入表首地址
       PULONG Size)                                                     //返回表项的大小


IMAGE_IMPORT_DESCRIPTOR结构包含了hint/name(函数序号/名称)表和IAT(导入地址表)的偏移量。这两个表的大小相同,一个成员对应一个导入函数,分别记录了导入函数的名称和地址。


 


HOOK API的实现:


    定位导入表之后,就可以定位导入地址表(IAT)了。

    为了截获API的调用,只要用自定义函数的地址覆盖导入地址表(IAT)中真实的API函数地址即可。


下面是挂钩MessageBoxA函数的例子,主模块中对MessageBoxA的调用都变为对自定义函数AceMessageBoxA的调用:

#include<Windows.h>
#include<stdio.h>
//挂钩指定模块hMod对MessageBoxA的调用
BOOL SetHook(HMODULE hMod);
//定义MessageBoxA函数原型
typedefint(WINAPI *PFNMESSAGEBOX)(HWND, LPCSTR, LPCSTR, UINT uType);
//保存MessageBoxA函数的真实地址
PROC g_orgProc = (PROC)MessageBoxA;
 
void main()
{
       //调用原API函数
       ::MessageBox(NULL, "原函数", "ACE", 0);
       //挂钩后再调用
       SetHook(::GetModouleHandle(NULL));
       ::MessageBox(NULL, "原函数", "ACE", 0);
}
 
//用于替换MessageBoxA的自定义函数
int WINAPI AceMessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType)
{
       return ((PFNMESSAGEBOX)g_orgProc)(hWnd, "新函数", "ACE", uType);
}
 
BOOL SetHook(HMODULE hMod)
{
       IMAGE_DOS_HEADER *pDosHeader = (IMAGE_DOS_HEADER*)hMod;
       IMAGE_OPTIONAL_HEADER *pOptHeader =
                     (IMAGE_OPTIONAL_HEADER*)((BYTE*)hMod + pDosHeader->e_lfanew + 24);
       IMAGE_IMPORT_DESCRIPTOR *PImportDesc = (IMAGE_IMPORT_DESCRIPTOR*)
                     ((BYTE*)hMod + pOptHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
       //在导入表中查找user32.dll模块,因为MessageBoxA函数从user32.dll模块导出
       while(pImportDesc->FirstThunk)
       {
              char* pszDllName = (char*)((BYTE*)hMod + pImportDesc->Name);
              if(lstrcmpiA(pszDllName, "user32.dll") == 0)
              {
                     break;
              }
              pImportDesc++;
       }
 
       if(pImportDesc->FirstThunk)
       {
              //一个IMAGE_THUNK_DATA结构就是一个双字,它指定了一个导入函数
              //调用地址表其实是IMAGE_THUNK_DATA结构的数组,也就是DWORD数组
              IMAGE_THUNK_DATA *pThunk = (IMAGE_THUNK_DATA*)
                                   ((BYTE*)hMod + pImportDesc->FirstThunk);
              while(pThunk->u1.Function)
              {
                     //lpAddr指向的内存保存了函数的地址
                     DWORD *lpAddr = (DWORD*)&(pThunk->u1.Function);
                     if(*lpAddr == (DWORD)g_orgProc)
                     {
                            //修改IAT表项,使其指向我们自定义的函数
                            //相当于语句"*lpAddr = (DWORD)AceMessageBoxA"
                            DWORD* lpNewProc = (DWORD*)AceMessageBoxA;
                            ::WriteProcessMemory(GetCurrentProcess(), lpAddr, &lpNewProc, sizeof(DWORD), NULL);
                            return TRUE;
                     }
                     pThunk++;
              }
       }
       return FALSE;
}


好文章,来自【福优学苑@音视频+流媒体】
***【在线视频教程】***