1 什么是Hook API
简单的说,一个应用程序要调用一个API函数,例如CreateFileW,那么应用程序必须要知道函数的地址,才能调用它,我对Hook API的理解是,把这个函数地址替换为另一个函数MyCreateFileW的地址,那么每当程序调用CreateFileW时,就会调用MyCreateFileW
2 Hook API有什么用
在《Rootkits——Windows内核的安全防护》中,作者笑言,该技术能够实现的能力很大程度上取决于想像力。
举一个简单的例子,360有一个产品叫360安全沙箱,在沙箱中打开一个软件,如果在沙箱中使用这个软件新建或者存储了一个文件,那么你会发现系统中的文件并没有改变。如果你仔细查找,会发现在根目录下有一个隐藏的文件夹,里面存储着这个软件新建或者修改过的那个文件。虽然我不知道360沙箱到底怎么实现这一功能,但是它完全可以用Hook API技术来实现。我们只需要Hook掉CreateFileW函数,检查函数的参数,当发现参数表明软件有“写”行为时,就更改文件路径,让它存储到我们指定的位置。
3.Hook API原理解析
1) 目标:替换掉目标程序中CreateFileW函数的地址
2) 过程
a.DLL注入
我们要替换掉目标程序的一个地址,并且要让目标程序执行我们编写的MyCreateFileW函数,由于进程的地址空间是隔离的,所以我们需要将这些代码(包括替换地址,MyCreateFileW函数代码)写在一个DLL文件中,然后运用DLL注入技术(在我们的应用程序中调用SetWindowsHookEx或者CreateRemoteThread都可以实现),将DLL文件注入目标程序,然后执行我们的代码,由于这不是本文的重点,不再详述。
b.替换CreateFileW函数地址
b1. 地址在哪儿
讲这个问题之前,需要介绍一些基础知识,原则是够用就好,不需要的东西不讲。
DLL基础
目标程序要调用API函数,这个函数的代码在Kernel32.dll中,DLL的好处之一就是DLL文件只在内存中存在一次,当有程序需要使用这个DLL文件时,系统只需要将这个DLL文件映射到程序进程地址空间就可以了,所以我们的目标程序要调用CreateFileW,就需要将Kernel32.dll映射到自己的地址空间中去
PE格式基础
在我们启动目标程序时,操作系统负责为目标程序创建虚拟地址空间,并将这个可执行模块(就是目标程序)加载到地址空间中去,接下来,系统会将目标程序所需要的DLL文件映射到地址空间。我们将需要映射到地址空间的目标程序及所需DLL统称为模块(Module)。使用IceSword查看进程,右击选择模块信息,就可以看到一个进程到底加载了哪些模块。下图是记事本程序的模块信息:
b2.查找线索
这里的问题是,操作系统怎么知道这个目标程序需要映射什么DLL文件?这就涉及到PE文件格式。我们平时使用的EXE文件,DLL文件基本都是PE格式的,PE格式的文件存储了与这个文件相关的大量信息,系统可以使用这些信息去加载必要的文件,并运行这个可执行文件。PE格式中就包含了这个可执行文件所需要映射的DLL文件。
下面就讲PE文件,原则还是,只讲与主题相关的~(绿色字段记录了我们层层寻找的线索)
可以通过下载这个文件边看图边看讲解:
看雪论坛技术文档: ( 这里有一个PE文件结构图,对理解PE文件非常有帮助)
这是PE文件整体布局
关于PE文件格式的定义在WinNT.h中,可自行查阅
我们看PE文件头部分的定义:
typedef struct _IMAGE_NT_HEADERS { DWORD Signature; IMAGE_FILE_HEADER FileHeader; IMAGE_OPTIONAL_HEADER32 OptionalHeader;} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;其中PE文件的Signature段被设置为IMAGE_NT_SIGNATURE ,ASCLL码为"PE00",下面是WinNT.h中的定义
#define IMAGE_NT_SIGNATURE 0x00004550 // PE00FileHeader我们不关心
下面是IMAGE_OPTIONAL_HEADER32 结构的定义
typedef struct _IMAGE_OPTIONAL_HEADER { // // Standard fields. // WORD Magic; BYTE MajorLinkerVersion; BYTE MinorLinkerVersion; DWORD SizeOfCode; DWORD SizeOfInitializedData; DWORD SizeOfUninitializedData; DWORD AddressOfEntryPoint; DWORD BaseOfCode; DWORD BaseOfData; // // NT additional fields. // DWORD ImageBase; DWORD SectionAlignment; DWORD FileAlignment; WORD MajorOperatingSystemVersion; WORD MinorOperatingSystemVersion; WORD MajorImageVersion; WORD MinorImageVersion; WORD MajorSubsystemVersion; WORD MinorSubsystemVersion; DWORD Win32VersionValue; DWORD SizeOfImage; DWORD SizeOfHeaders; DWORD CheckSum; WORD Subsystem; WORD DllCharacteristics; DWORD SizeOfStackReserve; DWORD SizeOfStackCommit; DWORD SizeOfHeapReserve; DWORD SizeOfHeapCommit; DWORD LoaderFlags; DWORD NumberOfRvaAndSizes; IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;我们关心的是最后一个结构IMAGE_DATA_DIRECTORY
typedef struct _IMAGE_DATA_DIRECTORY {DWORD VirtualAddress;DWORD Size;} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;DataDirectory是一个此结构体类型的数组,通过这个数组以及索引值我们可以找到大量的表,下面列出这些索引:
// Directory Entries#define IMAGE_DIRECTORY_ENTRY_EXPORT 0 // Export Directory#define IMAGE_DIRECTORY_ENTRY_IMPORT 1 // Import Directory#define IMAGE_DIRECTORY_ENTRY_RESOURCE 2 // Resource Directory#define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3 // Exception Directory#define IMAGE_DIRECTORY_ENTRY_SECURITY 4 // Security Directory#define IMAGE_DIRECTORY_ENTRY_BASERELOC 5 // Base Relocation Table#define IMAGE_DIRECTORY_ENTRY_DEBUG 6 // Debug Directory// IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7 // (X86 usage)#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 7 // Architecture Specific Data#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 // RVA of GP#define IMAGE_DIRECTORY_ENTRY_TLS 9 // TLS Directory#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 // Load Configuration Directory#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 // Bound Import Directory in headers#define IMAGE_DIRECTORY_ENTRY_IAT 12 // Import Address Table#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13 // Delay Load Import Descriptors#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 // COM Runtime descriptor通过DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT],我们可以找到一个“导入表”,这个“导入表”同样是一个结构体数组 这个结构体为IMAGE_IMPORT_DESCRIPTOR:
typedef struct _IMAGE_IMPORT_DESCRIPTOR {union {DWORD Characteristics; // 0 for terminating null import descriptorDWORD OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA)} DUMMYUNIONNAME;DWORD TimeDateStamp; // 0 if not bound,// -1 if bound, and real date\time stamp// in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)// O.W. date/time stamp of DLL bound to (Old BIND)DWORD ForwarderChain; // -1 if no forwardersDWORD Name;DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses)} IMAGE_IMPORT_DESCRIPTOR;typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;这个结构体就是我们这条线索的终点了!例如IMAGE_IMPORT_DESCRIPTOR类型的数组有N项,那么就说明这个PE文件需要加载的DLL模块有N个,我们可以根据Name成员变量得到需要加载的DLL文件名。 我们一开始说了,目标程序调用CreateFileW,CreateFileW在Kernel32.dll中,那么我们可以通过目标程序文件中Name字段找到Kernel32.dll这个名字。那么CreateFileW又在哪里呢?我们发现结构体中还有一个FirstThunk字段,这就是目标程序使用Kernel32.dll中的那一系列函数记录的入口。 通过这个FirstThunk我们又可以找到一个结构体数组,这个结构体名为IMAGE_THUNK_DATA32,其定义为
typedef struct _IMAGE_THUNK_DATA32 {union {DWORD ForwarderString; // PBYTEDWORD Function; // PDWORDDWORD Ordinal;DWORD AddressOfData; // PIMAGE_IMPORT_BY_NAME} u1;} IMAGE_THUNK_DATA32;typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;
其中Function记录了Kernel32.dll中CreateFileW函数的地址。我们只需要改写这一字段,就可以Hook API成功了!
我也是初学,了解不多,就不多说了,关于PE格式的参考资料:
看雪论坛安全文库: (在系统篇中详细讲述了PE文件)
看雪论坛技术文档: ( 这里有一个PE文件结构图,对理解PE文件非常有帮助)
《加密与解密》第10章:PE文件格式
《windows环境下32位汇编语言程序设计》第17章:PE文件
3)代码
根据上述过程我们看下面的代码(来源: )
void WINAPI HookOneAPI(LPCSTR pszCalleeModuleName,PROC pfnOriginApiAddress, PROC pfnDummyFuncAddress,HMODULE hModCallerModule){ ULONG size; //获取指向PE文件中的Import中IMAGE_DIRECTORY_DESCRIPTOR数组的指针 PIMAGE_IMPORT_DESCRIPTOR pImportDesc = (PIMAGE_IMPORT_DESCRIPTOR) ImageDirectoryEntryToData(hModCallerModule,TRUE,IMAGE_DIRECTORY_ENTRY_IMPORT,&size); if (pImportDesc == NULL) return; //查找记录,看看有没有我们想要的DLL for (;pImportDesc->Name;pImportDesc++) { LPSTR pszDllName = (LPSTR)((PBYTE)hModCallerModule+pImportDesc->Name); if (lstrcmpiA(pszDllName,pszCalleeModuleName) == 0) break; } if (pImportDesc->Name == NULL) { return; } //寻找我们想要的函数 PIMAGE_THUNK_DATA pThunk = (PIMAGE_THUNK_DATA)((PBYTE)hModCallerModule+pImportDesc->FirstThunk);//IAT for (;pThunk->u1.Function;pThunk++) { //ppfn记录了与IAT表项相应的函数的地址 PROC * ppfn= (PROC *)&pThunk->u1.Function; if (*ppfn == pfnOriginApiAddress) { //如果地址相同,也就是找到了我们想要的函数,进行改写,将其指向我们所定义的函数 WriteProcessMemory(GetCurrentProcess(),ppfn,&(pfnDummyFuncAddress), sizeof(pfnDummyFuncAddress),NULL); return; } }}
这段代码可以这样调用 :
HMODULE hMoudle = GetModuleHandle(glFileOpenShell);HookOneAPI("Kernel32.dll",GetProcAddress(GetModuleHandle(_T("Kernel32.dll")),"CreateFileW"),(PROC)&H_CreateFileW,hMoudle);其中glFileOpenShell是模块文件的路径,即上面我们用IceSword看到的那些模块的路径。
GetProcAddress函数用于得到Kernel32.dll中CreateFileW的地址,我们需要这个值与前面提到的Function成员变量进行比较,以确定这个Function是不是我们想找的Function。
上面的代码结合之前的讲述应该不难看懂。
需要解释的地方是这两行代码:
LPSTR pszDllName = (LPSTR)((PBYTE)hModCallerModule+pImportDesc->Name);//寻找我们想要的函数PIMAGE_THUNK_DATA pThunk = (PIMAGE_THUNK_DATA)((PBYTE)hModCallerModule+pImportDesc->FirstThunk);//IAT这里得到DLL文件名的时候,以及得到IMAGE_THUNK_DATA地址的时候用了一个加法。为什么要用加法呢?
因为PE文件中Name和FirstThunk记录的都是相对地址,即RVA,这表示一个偏移量,而我们通过GetModuleHandle可以得到模块文件加载到内存后的起始地址,这个起始地址加上偏移量,就可以得到一个值在内存中的地址了。
转载请注明出处: