用户登录  |  用户注册
首 页商业源码原创产品编程论坛
当前位置:PB创新网文章中心编程技巧Visual C++

鼠标屏幕取词技术的原理和实现

减小字体 增大字体 作者:佚名  来源:本站整理  发布时间:2009-03-16 20:35:30

   鼠标屏幕取词技术的原理和实现   
                       
白瑜

   “鼠标屏幕取词”技术是在电子字典中得到广泛地应用的,如四通利方和金山词霸等软件,这个技术看似简单,其实在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(输入地址表)等较底层的知识,所以我们先对涉及到的这些知识大概地做一个介绍,最后会给出拦截部分的关键代码。
      先说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)地址。
说到这里,截获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 != '

Tags:

作者:佚名

文章评论评论内容只代表网友观点,与本站立场无关!

   评论摘要(共 0 条,得分 0 分,平均 0 分) 查看完整评论
PB创新网ourmis.com】Copyright © 2000-2009 . All Rights Reserved .
页面执行时间:11,812.50000 毫秒
Email:ourmis@126.com QQ:2322888 蜀ICP备05006790号