由于需要用到关于windows虚拟内存的管理、打破进程边界墙、向应用程序的进程空间中注入
代码、pe(portable executable)文件格式和iat(输入地址表)等较底层的知识,所以我
对涉及到的这些知识大概地做一个介绍。
先说windows虚拟内存的管理。windows9x给每一个进程分配了4gb的地址空间,对于nt来说,
这个数字是2gb,系统保留了2gb到 4gb之间的地址空间禁止进程访问,而在win9x中,2gb到
4gb这部分虚拟地址空间实际上是由所有的win32进程所共享的,这部分地址空间加载了共享
win32 dll、内存映射文件和vxd、内存管理器和文件系统码,win9x中这部分对于每一个进
程都是可见的,这也是win9x操作系统不够健壮的原因。win9x中为16位操作系统保留了0到
4mb的地址空间,而在4mb到2gb之间也就是win32进程私有的地址空间,由于 每个进程的地
址空间都是相对独立的,也就是说,如果程序想截获其它进程中的api调用,就必须打破进
程边界墙,向其它的进程中注入截获api调用的代码,这项工作我们交给钩子函数
(setwindowshookex)来完成,关于如何创建一个包含系统钩子的动态链接库,这里就不赘
述了。所有系统钩子的函数必须要在动态库里,这样的话,当进程隐式或显式调用一个动态
库里的函数时,系统会把这个动态库映射到这个进程的虚拟地址空间里,这使得dll成为进
程的一部分,以这个进程的身份执行,使用这个进程的堆栈,也就是说动态链接库中的代码
被钩子函数注入了其它gui进程的地址空间(非gui进程,钩子函数就无能为力了),
当包含钩子的dll注入其它进程后,就可以取得映射到这个进程虚拟内存里的各个模块
(exe和dll)的基地址,如:
hmodule hmodule=getmodulehandle(“mypro.exe”);
在mfc程序中,我们可以用afxgetinstancehandle()函数来得到模块的基地址。exe和dll被映
射到虚拟内存空间的什么地方是由它们的基地址决定的。它们的基地址是在链接时由链接器
决定的。当你新建一个win32工程时,vc++链接器使用缺省的基地址0x00400000。可以通
过链接器的base选项改变模块的基地址。exe通常被映射到虚拟内存的0x00400000处,dll也
随之有不同的基地址,通常被映射到不同进程的相同的虚拟地址空间处。
系统将exe和dll原封不动映射到虚拟内存空间中,它们在内存中的结构与磁盘上的静态文件
结构是一样的。即pe (portable executable) 文件格式。我们得到了进程模块的基地址以
后,就可以根据pe文件的格式穷举这个模块的image_import_descriptor数组,看看进程空
间中是否引入了我们需要截获的函数所在的动态链接库,比如需要截获“textouta”,就必
须检查“gdi32.dll”是否被引入了。说到这里,我们有必要介绍一下pe文件的格式,
最前面是文件头,我们不必理会,从pe file optional header后面开始,就是文件中各个段
的说明,说明后面才是真正的段数据,而实际上我们关心的只有一个段,那就是“.idata”
段,这个段中包含了所有的引入函数信息,还有iat(import address table)的rva
(relative virtual address)地址。
说到这里,截获windowsapi的整个原理就要真相大白了。实际上所有进程对给定的api函数
的调用总是通过pe文件的一个地方来转移的,这就是一个该模块(可以是exe或dll)的“.idata”
段中的iat输入地址表(import address table)。在那里有所有本模块调用的其它dll的函
数名及地址。对其它dll的函数调用实际上只是跳转到输入地址表,由输入地址表再跳转到
dll真正的函数入口。
#include <crtdbg.h>
// 这里定义了一个产生指针的宏
#define makeptr(cast, ptr, addvalue) (cast)((dword)(ptr)+(dword)(addvalue))
// 定义了hookfuncdesc结构,我们用这个结构作为参数传给hookimportfunction函数
typedef struct tag_hookfuncdesc
{
lpcstr szfunc; // the name of the function to hook.
proc pproc; // the procedure to blast in.
} hookfuncdesc , * lphookfuncdesc;
// 这个函数监测当前系统是否是windownt
bool isnt();
// 这个函数得到hmodule -- 即我们需要截获的函数所在的dll模块的引入描述符(import descriptor)
pimage_import_descriptor getnamedimportdescriptor(hmodule hmodule, lpcstr szimportmodule);
// 我们的主函数
bool hookimportfunction(hmodule hmodule, lpcstr szimportmodule,
lphookfuncdesc pahookfunc, proc* paorigfuncs)
{
/// 下面的代码检测参数的有效性
_assert(szimportmodule);
_assert(!isbadreadptr(pahookfunc, sizeof(hookfuncdesc)));
#ifdef _debug
if (paorigfuncs) _assert(!isbadwriteptr(paorigfuncs, sizeof(proc)));
_assert(pahookfunc.szfunc);
_assert(*pahookfunc.szfunc != '/0');
_assert(!isbadcodeptr(pahookfunc.pproc));
#endif
if ((szimportmodule == null) || (isbadreadptr(pahookfunc, sizeof(hookfuncdesc))))
{
_assert(false);
setlasterrorex(error_invalid_parameter, sle_error);
return false;
}
//
// 监测当前模块是否是在2gb虚拟内存空间之上
// 这部分的地址内存是属于win32进程共享的
if (!isnt() && ((dword)hmodule >= 0x80000000))
{
_assert(false);
setlasterrorex(error_invalid_handle, sle_error);
return false;
}
// 清零
if (paorigfuncs) memset(paorigfuncs, null, sizeof(proc));
// 调用getnamedimportdescriptor()函数,来得到hmodule -- 即我们需要
// 截获的函数所在的dll模块的引入描述符(import descriptor)
pimage_import_descriptor pimportdesc = getnamedimportdescriptor(hmodule, szimportmodule);
if (pimportdesc == null)
return false; // 若为空,则模块未被当前进程所引入
// 从dll模块中得到原始的thunk信息,因为pimportdesc->firstthunk数组中的原始信息已经
// 在应用程序引入该dll时覆盖上了所有的引入信息,所以我们需要通过取得pimportdesc->originalfirstthunk
// 指针来访问引入函数名等信息
pimage_thunk_data porigthunk = makeptr(pimage_thunk_data, hmodule,
pimportdesc->originalfirstthunk);
// 从pimportdesc->firstthunk得到image_thunk_data数组的指针,由于这里在dll被引入时已经填充了
// 所有的引入信息,所以真正的截获实际上正是在这里进行的
pimage_thunk_data prealthunk = makeptr(pimage_thunk_data, hmodule, pimportdesc->firstthunk);
// 穷举image_thunk_data数组,寻找我们需要截获的函数,这是最关键的部分!
while (porigthunk->u1.function)
{
// 只寻找那些按函数名而不是序号引入的函数
if (image_ordinal_flag != (porigthunk->u1.ordinal & image_ordinal_flag))
{
// 得到引入函数的函数名
pimage_import_by_name pbyname = makeptr(pimage_import_by_name, hmodule,
porigthunk->u1.addressofdata);
// 如果函数名以null开始,跳过,继续下一个函数
if ('/0' == pbyname->name[0])
continue;
// bdohook用来检查是否截获成功
bool bdohook = false;
// 检查是否当前函数是我们需要截获的函数
if ((pahookfunc.szfunc[0] == pbyname->name[0]) &&
(strcmpi(pahookfunc.szfunc, (char*)pbyname->name) == 0))
{
// 找到了!
if (pahookfunc.pproc)
bdohook = true;
}
if (bdohook)
{
// 我们已经找到了所要截获的函数,那么就开始动手吧
// 首先要做的是改变这一块虚拟内存的内存保护状态,让我们可以自由存取
memory_basic_information mbi_thunk;
virtualquery(prealthunk, &mbi_thunk, sizeof(memory_basic_information));
_assert(virtualprotect(mbi_thunk.baseaddress, mbi_thunk.regionsize,
page_readwrite, &mbi_thunk.protect));
// 保存我们所要截获的函数的正确跳转地址
if (paorigfuncs)
paorigfuncs = (proc)prealthunk->u1.function;
// 将image_thunk_data数组中的函数跳转地址改写为我们自己的函数地址!
// 以后所有进程对这个系统函数的所有调用都将成为对我们自己编写的函数的调用
prealthunk->u1.function = (pdword)pahookfunc.pproc;
// 操作完毕!将这一块虚拟内存改回原来的保护状态
dword dwoldprotect;
_assert(virtualprotect(mbi_thunk.baseaddress, mbi_thunk.regionsize,
mbi_thunk.protect, &dwoldprotect));
setlasterror(error_success);
return true;
}
}
// 访问image_thunk_data数组中的下一个元素
porigthunk++;
prealthunk++;
}
return true;
}
// getnamedimportdescriptor函数的实现
pimage_import_descriptor getnamedimportdescriptor(hmodule hmodule, lpcstr szimportmodule)
{
// 检测参数
_assert(szimportmodule);
_assert(hmodule);
if ((szimportmodule == null) || (hmodule == null))
{
_assert(false);
setlasterrorex(error_invalid_parameter, sle_error);
return null;
}
// 得到dos文件头
pimage_dos_header pdosheader = (pimage_dos_header) hmodule;
// 检测是否mz文件头
if (isbadreadptr(pdosheader, sizeof(image_dos_header)) ||
(pdosheader->e_magic != image_dos_signature))
{
_assert(false);
setlasterrorex(error_invalid_parameter, sle_error);
return null;
}
// 取得pe文件头
pimage_nt_headers pntheader = makeptr(pimage_nt_headers, pdosheader, pdosheader->e_lfanew);
// 检测是否pe映像文件
if (isbadreadptr(pntheader, sizeof(image_nt_headers)) ||
(pntheader->signature != image_nt_signature))
{
_assert(false);
setlasterrorex(error_invalid_parameter, sle_error);
return null;
}
// 检查pe文件的引入段(即 .idata section)
if (pntheader->optionalheader.datadirectory[image_directory_entry_import].virtualaddress == 0)
return null;
// 得到引入段(即 .idata section)的指针
pimage_import_descriptor pimportdesc = makeptr(pimage_import_descriptor, pdosheader,
pntheader->optionalheader.datadirectory[image_directory_entry_import].virtualaddress);
// 穷举pimage_import_descriptor数组寻找我们需要截获的函数所在的模块
while (pimportdesc->name)
{
pstr szcurrmod = makeptr(pstr, pdosheader, pimportdesc->name);
if (stricmp(szcurrmod, szimportmodule) == 0)
break; // 找到!中断循环
// 下一个元素
pimportdesc++;
}
// 如果没有找到,说明我们寻找的模块没有被当前的进程所引入!
if (pimportdesc->name == null)
return null;
// 返回函数所找到的模块描述符(import descriptor)
return pimportdesc;
}
// isnt()函数的实现
bool isnt()
{
osversioninfo stosvi;
memset(&stosvi, null, sizeof(osversioninfo));
stosvi.dwosversioninfosize = sizeof(osversioninfo);
bool bret = getversionex(&stosvi);
_assert(true == bret);
if (false == bret) return false;
return (ver_platform_win32_nt == stosvi.dwplatformid);
}
/// end //