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函数CreateFileW,这个函数的代码在Kernel32.dll中,DLL的好处之一就是DLL文件只在内存中存在一次,当有程序需要使用这个DLL文件时,系统只需要将这个DLL文件映射到程序进程地址空间就可以了,所以我们的目标程序要调用CreateFileW,就需要将Kernel32.dll映射到自己的地址空间中去
PE格式基础
在我们启动目标程序时,操作系统负责为目标程序创建虚拟地址空间,并将这个可执行模块(就是目标程序)加载到地址空间中去,接下来,系统会将目标程序所需要的DLL文件映射到地址空间。我们将需要映射到地址空间的目标程序及所需DLL统称为模块(Module)。使用IceSword查看进程,右击选择模块信息,就可以看到一个进程到底加载了哪些模块。下图是记事本程序的模块信息:
b2.查找线索
这里的问题是,操作系统怎么知道这个目标程序需要映射什么DLL文件?这就涉及到PE文件格式。我们平时使用的EXE文件,DLL文件基本都是PE格式的,PE格式的文件存储了与这个文件相关的大量信息,系统可以使用这些信息去加载必要的文件,并运行这个可执行文件。PE格式中就包含了这个可执行文件所需要映射的DLL文件。
下面就讲PE文件,原则还是,只讲与主题相关的~(绿色字段记录了我们层层寻找的线索)
可以通过下载这个文件边看图边看讲解:
看雪论坛技术文档:http://www.pediy.com/Document.htm ( 这里有一个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 // PE00
FileHeader我们不关心
下面是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 descriptor
- DWORD 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 forwarders
- DWORD 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; // PBYTE
- DWORD Function; // PDWORD
- DWORD 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格式的参考资料:
看雪论坛安全文库:http://www.pediy.com/kssd/index.html (在系统篇中详细讲述了PE文件)
看雪论坛技术文档:http://www.pediy.com/Document.htm ( 这里有一个PE文件结构图,对理解PE文件非常有帮助)
《加密与解密》第10章:PE文件格式
《windows环境下32位汇编语言程序设计》第17章:PE文件
3)代码
根据上述过程我们看下面的代码(来源:http://www.yulv.net/archives/16/ )
- 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);
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可以得到模块文件加载到内存后的起始地址,这个起始地址加上偏移量,就可以得到一个值在内存中的地址了。