Hook API 原理 解析

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);
其中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可以得到模块文件加载到内存后的起始地址,这个起始地址加上偏移量,就可以得到一个值在内存中的地址了。


转载请注明出处:http://blog.csdn.net/on_1y/article/details/7595184






posted @ 2012-05-23 19:27  Iambda  阅读(783)  评论(0编辑  收藏  举报