鼠标屏幕取词技能的道理和实现
副标题#e#
“鼠标屏幕取词”技能是在电子字典中获得遍及地应用的,如四通利方和金山词霸等软件,这个技能看似简朴,其实在windows系统中实现却长短常巨大的,总的来说有两种实现方法:
第一种:回收截获对部门gdi的api挪用来实现,如textout,textouta等。
第二种:对每个设备上下文(dc)做一分copy,并跟踪所有修改上下文(dc)的操纵。
第二种要领更强大,但兼容性欠好,而第一种要领利用的截获windowsapi的挪用,这项技能的强大大概远远超出了您的想象,绝不浮夸的说,操作windowsapi拦截技能,你可以改革整个操纵系统,事实上许多外挂式windows中文平台就是这么实现的!而这项技能也正是这篇文章的主题。
截windowsapi的挪用,详细的说来也可以分为两种要领:
第一种要领通过直接改写winapi 在内存中的映像,嵌入汇编代码,使之被挪用时跳转到指定的地点运行来截获;第二种要领例改写iat(import address table输入地点表),重定向winapi函数的挪用来实现对winapi的截获。
第一种要领的实现较为繁琐,并且在win95、98下面更有难度,这是因为固然微软说win16的api只是为了兼容性才保存下来,措施员应该尽大概地挪用32位的api,实际上基础就不是这样!win 9x内部的大部门32位api颠末调动挪用了同名的16位api,也就是说我们需要在拦截的函数中嵌入16位汇编代码!
我们将要先容的是第二种拦截要领,这种要领在win95、98和nt下面运行都较量不变,兼容性较好。由于需要用到关于windows虚拟内存的打点、冲破历程界线墙、向应用措施的历程空间中注入代码、pe(portable executable)文件名目和iat(输入地点表)等较底层的常识,所以我们先对涉及到的这些常识或许地做一个先容,最后会给出拦截部门的要害代码。
#p#副标题#e#
先说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文件名目标大抵框图,最前面是文件头,我们不必剖析,从pe file optional header后头开始,就是文件中各个段的说明,说明后头才是真正的段数据,而实际上我们体贴的只有一个段,那就是“.idata”段,这个段中包括了所有的引入函数信息,尚有iat(import address table)的rva(relative virtual address)地点。
#p#分页标题#e#
说到这里,截获windowsapi的整个道理就要真相懂得了。实际上所有历程对给定的api函数的挪用老是通过pe文件的一个处所来转移的,这就是一个该模块(可以是exe或dll)的“.idata”段中的iat输入地点表(import address table)。在哪里有所有本模块挪用的其它dll的函数名及地点。对其它dll的函数挪用实际上只是跳转到输入地点表,由输入地点表再跳转到dll真正的函数进口。
详细来说,我们将通过image_import_descriptor数组来会见“.idata”段中引入的dll的信息,然后通过image_thunk_data数组来针对一个被引入的dll会见该dll中被引入的每个函数的信息,找到我们需要截获的函数的跳转地点,然后改成我们本身的函数的地点……详细的做法在后头的要害代码中会有具体的讲授。
讲了这么多道理,此刻让我们回到“鼠标屏幕取词”的专题上来。除了api函数的截获,要实现“鼠标屏幕取词”,还需要做一些其它的事情,简朴的说来,可以把一个完整的取词进程归纳成以下几个步调:
1. 安装鼠标钩子,通过钩子函数得到鼠标动静。
利用到的api函数:setwindowshookex
2. 获得鼠标的当前位置,向鼠标下的窗口发重画动静,让它挪用系统函数重画窗口。
利用到的api函数:windowfrompoint,screentoclient,invalidaterect
3. 截获对系统函数的挪用,取得参数,也就是我们要取的词。
对付大大都的windows应用措施来说,假如要取词,我们需要截获的是“gdi32.dll”中的“textouta”函数。
我们先模拟textouta函数写一个本身的mytextouta函数,如:
bool winapi mytextouta(hdc hdc, int nxstart, int nystart, lpcstr lpszstring,int cbstring)
{
// 这里举办输出lpszstring的处理惩罚
// 然后挪用正版的textouta函数
}
把这个函数放在安装了钩子的动态毗连库中,然后挪用我们最后给出的hookimportfunction函数来截获历程对textouta函数的挪用,跳转到我们的mytextouta函数,完成对输出字符串的捕获。hookimportfunction的用法:
hookfuncdesc hd;
proc porigfuns;
hd.szfunc="textouta";
hd.pproc=(proc)mytextouta;
hookimportfunction (afxgetinstancehandle(),"gdi32.dll",&hd,porigfuns);
下面给出了hookimportfunction的源代码,相信详尽的注释必然不会让您以为领略截获到底是怎么实现的很难,ok,let’s go:
///////////////////////////////////////////// begin ///////////////////////////////////////////////////////////////
#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 //////////////////////////////////////////////////////////////////////
#p#分页标题#e#
不知道在这篇文章问世之前,有几多伴侣实验已往实现“鼠标屏幕取词”这项布满了挑战的技能,也只有实验过的伴侣才气体会到其间的不易,尤其在摸索api函数的截获时,手头的几篇资料没有一篇是涉及到要害代码的,重要的处所都是一笔代过,msdn更是显得惨白而无力,也不知道除了image_import_descriptor和image_thunk_data,微软还埋没了几多奥秘,亏得硬着头皮照旧把它给攻陷了,但愿这篇文章对各人能有所辅佐。