金山词霸屏幕取词实现的原理 金山词霸屏幕取词实现的原理 解决方案 » 免费领取超大流量手机卡,每月29元包185G流量+100分钟通话, 中国电信官方发货 鼠标悬停时触发的API,然后就是检索他们的数据文件! 鼠标悬停,定位,重画,hook textout,确定要翻译的词,翻译,显示 鼠标屏幕取词技术的原理和实现 “鼠标屏幕取词”技术是在电子字典中得到广泛地应用的,如四通利方和金山词霸等软件,这个技术看似简单,其实在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函数:SetWindowsHookEx2. 得到鼠标的当前位置,向鼠标下的窗口发重画消息,让它调用系统函数重画窗口。 使用到的API函数:WindowFromPoint,ScreenToClient,InvalidateRect3. 截获对系统函数的调用,取得参数,也就是我们要取的词。对于大多数的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;// 这个函数监测当前系统是否是WindowNTBOOL 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 ////////////////////////////////////////////////////////////////////// 不知道在这篇文章问世之前,有多少朋友尝试过去实现“鼠标屏幕取词”这项充满了挑战的技术,也只有尝试过的朋友才能体会到其间的不易,尤其在探索API函数的截获时,手头的几篇资料没有一篇是涉及到关键代码的,重要的地方都是一笔代过,MSDN更是显得苍白而无力,也不知道除了IMAGE_IMPORT_DESCRIPTOR和IMAGE_THUNK_DATA,微软还隐藏了多少秘密,好在硬着头皮还是把它给攻克了,希望这篇文章对大家能有所帮助 rivershan(笨猫)(C++/VC初学者)兄当真高人。偶像!! to rivershan(笨猫)(C++/VC初学者)高,实在是高, I 服了 u你是我偶像顺便蹭分呵呵 to rivershan(笨猫)(C++/VC初学者)不过 20 分太委屈你了,强烈要求楼主给你200分,顺便分我100呵呵 "陷阱"技术探秘----动态汉化Windows技术的分析 华光科技股份公司研究开发中心 张高峰 摘 要: 四通利方(RichWin),中文之星(CStar)是大家广为熟知的汉化Windows产品,"陷阱”技术即动态修改Windows代码,一直是其对外宣称的过人技术,它究竟是如何实现的,这自然是核心机密。本文试图解开这个秘密,并同时介绍Windows的模块调用机制与重定位概念,并给出了采用"陷阱"技术动态修改Windows代码的示例源程序。 关键词:汉化Windows重定位技术 一、发现了什么?作者多年来一直从事Windows下的软件开发工作,经历了Windows2.0、3.0、3.1,直至WindowsNT,95的成长过程,也遍历了长青窗口、长城窗口、DBWin、CStar、RichWin等多个Windows汉化产品。从现在看来,影响最大也最为成功的,当推四通利方的RichWin,此外,中文之星CStar与RichWin师出一门,其核心技术自然也差不许多。其对外宣传采用独特的“陷阱”技术动态修改Windows代码,一直是作者感兴趣的地方。EXEHDR是MicrosoftVisualC++开发工具中很有用的一个程序,它可以检查NE(New_Executable)格式文件,用它来分析RichWin的WSENGINE.DLL或CStar的CHINESE.DLL就会发现与众不同的两点:( 以CStar 1.20 为 例) C:\CSTAR>exehdr chinese.dll /v..................................6 type offset targetBASE060aseg 2 offset 0000PTR 047eimp GDI.GETCHARABCWIDTHSPTR 059bimp GDI.ENUMFONTFAMILIESPTR 0451imp DISPLAY.14( EXTTEXTOUT )PTR 0415imp KEYBOARD.4( TOASCII )PTR 04baimp KEYBOARD.5( ANSITOOEM )PTR 04c9imp KEYBOARD.6( OEMTOANSI )PTR 04d8imp KEYBOARD.134( ANSITOOEMBUFF)PTR 05f5imp USER.430( LSTRCMP )PTR 04e7imp KEYBOARD.135( OEMTOANSIBUFF)PTR 0514imp USER.431( ANSIUPPER)PTR 0523imp USER.432( ANSILOWER )PTR 05aaimp GDI.56( CREATEFONT)PTR 056eimp USER.433( ISCHARALPHA )PTR 05b9imp GDI.57( CREATEFONTINDIRECT )PTR 057dimp USER.434( ISCHARALPHANUMERIC )PTR 049cimp USER.179( GETSYSTEMMETRICS )PTR 0550imp USER.435( ISCHARUPPER)PTR 055fimp USER.436( ISCHARLOWER)PTR 0532imp USER.437( ANSIUPPERBUFF)PTR 0541imp USER.438( ANSILOWERBUFF)PTR 05c8imp GDI.69( DELETEOBJECT )PTR 058cimp GDI.70( ENUMFONTS )PTR 04abimp KERNEL.ISDBCSLEADBYTEPTR 05d7imp GDI.82( GETOBJECT)PTR 048dimp KERNEL.74 ( OPENFILE )PTR 0460imp GDI.91( GETTEXTEXTENT)PTR 05e6imp GDI.92( GETTEXTFACE)PTR 046fimp GDI.350 ( GETCHARWIDTH )PTR 0442imp GDI.351 ( EXTTEXTOUT )PTR 0604imp USER.471( LSTRCMPI )PTR 04f6imp USER.472( ANSINEXT )PTR 0505imp USER.473( ANSIPREV )PTR 0424imp USER.108( GETMESSAGE )PTR 0433imp USER.109( PEEKMESSAGE)35 relocations*******扩号内为作者加上的对应WindowsAPI函数第一,在数据段中,发现了重定位信息。第二,这些重定位信息提示的函数,全都与文字显示输出和键盘,字符串有关。也就是说汉化Windows,必须修改这些函数。在这非常特殊的地方,隐藏着什么呢?无庸致疑,这与众不同的两点,对打开“陷阱”技术之门而言,不是金钥匙,也是敲门砖。二、Windows的模块调用机制与重定位概念为了深入探究“陷阱”技术,我们先来介绍Windows的模块调用机制。Windows的运行分实模式(RealMode),标准模式(StandMode)和增强模式(386EnhancedMode)三种,虽然这几种模式各不相同,但其核心模块的调用关系却是完全一致的。主要的三个模块,有如下的关系:KERNEL是Windows系统内核,它不依赖其它模块。GDI是Windows图形设备接口模块,它依赖于KERNEL模块。USER是Windows用户接口服务模块,它依赖于KERNEL,GDI模块及设备驱动程序等所有模块。这三个模块,实际上就是Windows的三个动态连接库,在系统的存在形式如下,KERNEL有三种不同形式,Kernel.exe(实模式),Krnl286.exe(标准模式),Krnl386.exe(386增强模式);GDI模块是Gdi.exe;USER模块是User.exe,虽然文件名都以EXE为扩展名,但它们实际都是动态连接库。同时,几乎所有的API函数都隐藏在这三个模块中。用EXEHDR对这三个模块分析,就可列出一大堆你所熟悉的WindowsAPI函数。以GDI模块为例,C:\WINDOWS\SYSTEM>exehdr gdi.exeExports:ord seg offset name............351 1923eEXTTEXTOUT exported, shared data56 319e1CREATEFONT exported, shared data............至此,你已能从Windows纷繁复杂的系统中,理出一些头续来。下面,再引入一个重要概念——重定位。一个Windows执行程序对调用API函数,或对其它动态库的调用,在程序装入内存前,都是一些不能定位的动态连接,当程序调入内存时,这些远调用都需要重新定位,重新定位的依据就是重定位表。在Windows执行程序(包括动态库)的每个段后面,通常都跟有这样一个重定位表。重定位包含调用函数所在模块,函数序列号,以及定位在模块中的位置。例如,用EXEHDR/v分析CHINESE.DLL得到6 type offset target..........PTR 0442imp GDI.351 ..........就表明,在本段的0442H偏移处,调用了GDI的第351号函数。如果在0442H处是0000:FFFF,则表示,本段内仅此一处调用了GDI.351函数,否则,表明了本段内还有一处调用此函数,调用的位置就是0442H处所指向的内容,实际上重定位表只含有引用位置的链表的链头。那么,GDI.351是一个什么函数呢?还是用EXEHDR对GDI.EXE作一分析,就可得出,在GDI的出口(Export)函数中,第351号是ExtTextOut。这样,我们在EXEHDR这一简单而非常有用的工具帮助下,已经在Windows的浩瀚海洋中畅游了一会,下面就来掀开“陷阱”技术的神秘面纱。三、动态汉化Windows原理我们知道,传统的汉化Windows的方法,是要直接修改Windows的显示、输入、打印等模块代码,或用DDK直接开发“中文设备”驱动模块,这样不仅工作量浩大,而且,系统的完备性很难保证,性能上也有很多限制(早期的长青窗口就是这样),这样,只有从内核上修改Windows核心代码才是最彻底的办法。从Windows的模块调用机制,我们可以看到,Windows实际上是由包括在KERNEL,GDI,USER等几个模块中的众多函数支撑的。那么,修改其中涉及语言文字处理的函数,使之能适应中文需要,不就能达到汉化目的了吗?因而,我们可以得出这样的结论:在自己的模块中重新编写涉及文字显示,输入的多个函数,然后,将Windows中对这些函数的引用,改向到自己的这些模块中来。修改哪些函数才能完成汉化,这需要深入分析Windows的内部结构,但CHINESE.DLL已明确无误地告诉了我们,在其数据段的重定位表中列出的引用函数,正是CStar修改了的Windows函数!为了验证这一思路,我们利用RichWin作一核实。用EXEHDR分析GDI.EXE,得出ExtTextOut函数在GDI的第一代码段6139H偏移处(不同版本的Windows其所在代码段和偏移可能不一样)。然后,用HelpWalk(也是MicrosoftVisualC++开发工具中的一个)检查GDI的Code1段,6139H处前5个字节是B8FF054555,经过运行RichWin4.3forInternet后,再查看同样的地方,已改为EA08088F3D,其实反汇编就知道,这5个字节就是代表Jmp3D8F:0808,而句柄为0x3D8F的模块,用HelpWalk能观察到正是RichWin的WSENGINE.DLL的第一代码段(模块名为TEXTMAN)。而偏移0808H处B8B73D45558BEC1E,正是一个函数起始的地方,这实际上就是RichWin所重改写的ExtTextOut函数。退出RichWin后,再用HelpWalk观察GDI的Code1代码段,一切又恢复正常!这与前面的分析结论完全吻合!那么,下一个关键点就是如何动态修改Windows的函数代码,也就是汉化Windows的核心——“陷阱”技术。 四、“陷阱”技术讨论“陷阱”技术,还要回到前面的两个发现。发现之二,已能解释为修改的Windows函数,而发现之一,却仍是一个迷。数据段存放的是变量及常量等内容,如果这里面包含有重定位信息,那么,必定要在变量说明中将函数指针赋给一个FARPROC类型的变量,于是,在变量说明中写下:FARPROCFarProcFunc=ExtTextOut;果然,我自己程序的数据段中也有了重定位信息。这样,当程序调入内存中时,变量FarProcFunc已是函数ExtTextOut的地址了。要直接修改代码段的内容,还遇到一个难题,就是代码段是不可改写的。这时,需要用到一个未公开的Windows函数AllocCStoDSAlias取得与代码段有相同基址的可写数据段别名,其函数声明为WORDFARPASCALAllocCStoDSAlias(WORDcode_sel);参数是代码段的句柄,返回值是可写数据段别名句柄。Windows中函数地址是32位,高字是其模块的内存句柄,低字是函数在模块内的偏移。将得到的可写数据段别名句柄锁定,再将函数偏移处的5个字节保留下来,然后将其改为转向替代函数(用EAJmp)*(lpStr+wOffset)=0xEA;*(lpStr+wOffset+1)=lpFarProcReplace;反汇编即是JmplpFarProcReplace,最后,内存解锁。这就是我们为Windows设的“陷阱”,当所有对此函数的调用都无条件地转到我们规定的替代函数处。当程序结束之前,将保留的5字节内容再置回来,否则,系统会崩溃。下面给出作者编写的使Windows的ExtTextOut函数落入自己函数“陷阱”的源程序。//源程序 relocate.c#include #include BOOL WINAPI MyExtTextOut(HDC hDC, int x, int y, UINT nInt1, const RECT FAR* lpRect,LPCSTR lpStr, UINT nInt2, int FAR* lpInt);WORD FAR PASCAL AllocCStoDSAlias(WORD code_sel);typedef struct tagFUNC{ FARPROC lpFarProcReplace;//替代函数地址FARPROC lpFarProcWindows;//Windows函数地址BYTEbOld;//保存原函数第一字节LONGlOld;//保存原函数接后的四字节长值}FUNC;FUNCFunc={MyExtTextOut,ExtTextOut};//Windows主函数int PASCAL WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,LPSTR lpCmdLine,int nCmdShow){HANDLE hMemCode;//代码段句柄WORD hMemData;//相同基址的可写数据段别名 WORD wOffset; //函数偏移LPSTRlpStr;LPLONG lpLong;char lpNotice[96];hMemCode=HIWORD((LONG) Func.lpFarProcWindows );wOffset=LOWORD((LONG) Func.lpFarProcWindows );wsprintf(lpNotice,"函数所在模块句柄 0x%4xH,偏移 0x%4xH",hMemCode,wOffset);MessageBox(NULL,lpNotice,"提示",MB_OK);//取与代码段有相同基址的可写数据段别名 hMemData=AllocCStoDSAlias(hMemCode);lpStr=GlobalLock(hMemData); lpLong=(lpStr+wOffset+1 );//保存原函数要替换的头几个字节Func.bOld=*(lpStr+wOffset);Func.lOld=*lpLong;*(lpStr+wOffset)=0xEA; *lpLong=Func.lpFarProcReplace;GlobalUnlock(hMemData);MessageBox(NULL,"改为自己的函数","提示",MB_OK);//将保留的内容改回来hMemData=AllocCStoDSAlias(hMemCode);lpStr=GlobalLock(hMemData); lpLong=(lpStr+wOffset+1 );*(lpStr+wOffset)=Func.bOld; *lpLong=Func.lOld;GlobalUnlock(hMemData);MessageBox(NULL,"改回原Windows函数","提示",MB_OK);return 1;}//自己的替代函数BOOL WINAPI MyExtTextOut(HDC hDC, int x, int y, UINT nInt1, const RECT FAR* lpRect, LPCSTR lpStr, UINT nInt2, int FAR* lpInt){BYTE NameDot[96]={0x09, 0x00, 0xfd, 0x08, 0x09, 0x08, 0x09, 0x10, 0x09, 0x20, 0x79, 0x40, 0x41, 0x04, 0x47, 0xfe, 0x41, 0x40, 0x79, 0x40, 0x09, 0x20, 0x09, 0x20, 0x09, 0x10, 0x09, 0x4e, 0x51, 0x84, 0x21, 0x00, 0x02, 0x00, 0x01, 0x04, 0xff, 0xfe, 0x00, 0x00, 0x1f, 0xf0, 0x10, 0x10, 0x10, 0x10, 0x1f, 0xf0, 0x00, 0x00, 0x7f, 0xfc, 0x40, 0x04, 0x4f, 0xe4, 0x48, 0x24, 0x48, 0x24, 0x4f, 0xe4, 0x40, 0x0c, 0x10, 0x80, 0x10, 0xfc, 0x10, 0x88, 0x11, 0x50, 0x56, 0x20, 0x54, 0xd8, 0x57, 0x06, 0x54, 0x20, 0x55, 0xfc, 0x54, 0x20, 0x55, 0xfc, 0x5c, 0x20, 0x67, 0xfe, 0x00, 0x20, 0x00, 0x20, 0x00, 0x20}; HBITMAP hBitmap,hOldBitmap;HDC hMemDC;BYTE far *lpDot;int i;for ( i=0;i<3;i++ ){lpDot=(LPSTR)NameDot+i*32;hMemDC=CreateCompatibleDC(hDC);hBitmap=CreateBitmap(16,16,1,1,lpDot);SetBitmapBits(hBitmap,32L,lpDot);hOldBitmap=SelectObject(hMemDC,hBitmap);BitBlt(hDC,x+i*16,y,16,16,hMemDC,0,0,SRCCOPY);DeleteDC(hMemDC);DeleteObject(hBitmap);}return TRUE;}//模块定义文件relocate.defNAMERELOCATEEXETYPE WINDOWSCODEPRELOAD MOVEABLE DISCARDABLEDATAPRELOAD MOVEABLE MULTIPLEHEAPSIZE1024EXPORTS五、结束语本文从原理上分析了称为“陷阱”技术的汉化Windows方法。要彻底汉化Windows还要涉及显示,键盘输入等诸多内容,决非一日之功。但作为对“陷阱”技术的分析,本文介绍了将任一Windows函数调用改向到自己指定函数处的通用方法,这种方法可以拓展到其它应用中,如多语种显示,不同内码制式的切换显示等。参考文献:AndrewSchulmanDavidMaxeyMattPietrek,《未公开的Windows核心技术》,清华大学出版社,1993年。王志东,“Windows中文环境”,《Windows软件的应用与开发》,1993.5。(作者地址:山东潍坊华光科技股份公司研究开发中心 张高峰 邮编261041 ) 请问有没有源码,我正在做一个屏幕取词的冬冬,谢谢大虾![email protected] 如何从以 俄文扩展ASCII码表 编码的二进制文件中将文本读取出来? 求救-等待....:如何将CString型转换为char型??? 关于vc资源利用的问题? 明天去笔试200分相送,up也有分 CMainFrame 和 CMyView 通讯的问题,100 分相送 怎样使button响应MouseEnter和MouseLeave事件 请问怎样让无模式窗口保持在内存中? 下载了 SoapToolkit20 不知道具体有什么用途 如何修正 ERROR C2664 错误啊 帮帮忙 我在把数据保存在txt文件里的时候,为什么出现N多乱码? 请问那本书可以深入学习高级UI(工具条,状态条等)。
第一种:采用截获对部分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:
#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 ////////////////////////////////////////////////////////////////////// 不知道在这篇文章问世之前,有多少朋友尝试过去实现“鼠标屏幕取词”这项充满了挑战的技术,也只有尝试过的朋友才能体会到其间的不易,尤其在探索API函数的截获时,手头的几篇资料没有一篇是涉及到关键代码的,重要的地方都是一笔代过,MSDN更是显得苍白而无力,也不知道除了IMAGE_IMPORT_DESCRIPTOR和IMAGE_THUNK_DATA,微软还隐藏了多少秘密,好在硬着头皮还是把它给攻克了,希望这篇文章对大家能有所帮助
高,实在是高, I 服了 u
你是我偶像
顺便蹭分
呵呵
不过 20 分太委屈你了,强烈要求楼主给你200分,顺便分我100
呵呵
----动态汉化Windows技术的分析
华光科技股份公司研究开发中心 张高峰
摘 要: 四通利方(RichWin),中文之星(CStar)是大家广为熟知的汉化Windows产品,
"陷阱”技术即动态修改Windows代码,一直是其对外宣称的过人技术,它究竟是如
何实现的,这自然是核心机密。本文试图解开这个秘密,并同时介绍Windows的模块
调用机制与重定位概念,并给出了采用"陷阱"技术动态修改Windows代码的示例源程
序。
关键词:汉化Windows重定位技术 一、发现了什么?
作者多年来一直从事Windows下的软件开发工作,经历
了Windows2.0、3.0、3.1,直至WindowsNT,95的成长过程,也遍历
了长青窗口、长城窗口、DBWin、CStar、RichWin等多个Windows汉化
产品。从现在看来,影响最大也最为成功的,当推四通利方
的RichWin,此外,中文之星CStar与RichWin师出一门,其核心技术
自然也差不许多。其对外宣传采用独特的“陷阱”技术动态
修改Windows代码,一直是作者感兴趣的地方。EXEHDR是MicrosoftVisualC++开发工具中很有用的一个程
序,它可以检查NE(New_Executable)格式文件,用它来分析RichWin
的WSENGINE.DLL或CStar的CHINESE.DLL就会发现与众不同的两点:
( 以CStar 1.20 为 例)
C:\CSTAR>exehdr chinese.dll /v
..................................
6 type offset target
BASE060aseg 2 offset 0000
PTR 047eimp GDI.GETCHARABCWIDTHS
PTR 059bimp GDI.ENUMFONTFAMILIES
PTR 0451imp DISPLAY.14( EXTTEXTOUT )
PTR 0415imp KEYBOARD.4( TOASCII )
PTR 04baimp KEYBOARD.5( ANSITOOEM )
PTR 04c9imp KEYBOARD.6( OEMTOANSI )
PTR 04d8imp KEYBOARD.134( ANSITOOEMBUFF)
PTR 05f5imp USER.430( LSTRCMP )
PTR 04e7imp KEYBOARD.135( OEMTOANSIBUFF)
PTR 0514imp USER.431( ANSIUPPER)
PTR 0523imp USER.432( ANSILOWER )
PTR 05aaimp GDI.56( CREATEFONT)
PTR 056eimp USER.433( ISCHARALPHA )
PTR 05b9imp GDI.57( CREATEFONTINDIRECT )
PTR 057dimp USER.434( ISCHARALPHANUMERIC )
PTR 049cimp USER.179( GETSYSTEMMETRICS )
PTR 0550imp USER.435( ISCHARUPPER)
PTR 055fimp USER.436( ISCHARLOWER)
PTR 0532imp USER.437( ANSIUPPERBUFF)
PTR 0541imp USER.438( ANSILOWERBUFF)
PTR 05c8imp GDI.69( DELETEOBJECT )
PTR 058cimp GDI.70( ENUMFONTS )
PTR 04abimp KERNEL.ISDBCSLEADBYTE
PTR 05d7imp GDI.82( GETOBJECT)
PTR 048dimp KERNEL.74 ( OPENFILE )
PTR 0460imp GDI.91( GETTEXTEXTENT)
PTR 05e6imp GDI.92( GETTEXTFACE)
PTR 046fimp GDI.350 ( GETCHARWIDTH )
PTR 0442imp GDI.351 ( EXTTEXTOUT )
PTR 0604imp USER.471( LSTRCMPI )
PTR 04f6imp USER.472( ANSINEXT )
PTR 0505imp USER.473( ANSIPREV )
PTR 0424imp USER.108( GETMESSAGE )
PTR 0433imp USER.109( PEEKMESSAGE)
35 relocations*******扩号内为作者加上的对应WindowsAPI函数
第一,在数据段中,发现了重定位信息。
第二,这些重定位信息提示的函数,全都与文字显示
输出和键盘,字符串有关。也就是说汉化Windows,必须修改这些函数。
在这非常特殊的地方,隐藏着什么呢?无庸致疑,这与众不同的两点,对打开“陷阱”技术之门而言,不是金钥匙,也是敲门砖。二、Windows的模块调用机制与重定位概念
为了深入探究“陷阱”技术,我们先来介绍Windows的模块调用机制。Windows的运行分实模式(RealMode),标准模式(StandMode)和增强模式(386EnhancedMode)三种,虽然这几种模式各不相同,但其核心模块的调用关系却是完全一致的。
主要的三个模块,有如下的关系:KERNEL是Windows系统内核,它不依赖其它模块。GDI是Windows图形设备接口模块,它依赖于KERNEL模块。USER是Windows用户接口服务模块,它依赖于KERNEL,GDI模块及设备驱动程序等所有模块。
这三个模块,实际上就是Windows的三个动态连接库,在系统的存在形式如下,KERNEL有三种不同形式,Kernel.exe(实模式),Krnl286.exe(标准模式),Krnl386.
exe(386增强模式);GDI模块是Gdi.exe;USER模块是User.exe,虽然文件名都以EXE为扩展名,但它们实际都是动态连接库。同时,几乎所有的API函数都隐藏在这三个模块中。用EXEHDR对这三个模块分析,就可列出一大堆你所熟悉的WindowsAPI函数。以GDI模块为例,C:\WINDOWS\SYSTEM>exehdr gdi.exe
Exports:
ord seg offset name
............
351 1923eEXTTEXTOUT exported, shared data
56 319e1CREATEFONT exported, shared data
............至此,你已能从Windows纷繁复杂的系统中,理出一些头续来。下面,再引入一个重要概念——重定位。
一个Windows执行程序对调用API函数,或对其它动态库的调用,在程序装入内存前,都是一些不能定位的动态连接,当程序调入内存时,这些远调用都需要重新定位,重新定位的依据就是重定位表。在Windows执行程序(包括动态库)的每个段后面,通常都跟有这样一个重定位表。重定位包含调用函数所在模块,函数序列号,以及定位在模块中的位置。例如,用EXEHDR/v分析CHINESE.DLL得到6 type offset target
..........
PTR 0442imp GDI.351
..........就表明,在本段的0442H偏移处,调用了GDI的第351号函数。如果在0442H处是0000:FFFF,则表示,本段内仅此一处调用了GDI.351函数,否则,表明了本段内还有一处调用此函数,调用的位置就是0442H处所指向的内容,实际上重定位表只含有引用位置的链表的链头。那么,GDI.351是一个什么函数呢?还是用EXEHDR对GDI.EXE作一分析,就可得出,在GDI的出口(Export)函数中,第351号是ExtTextOut。
这样,我们在EXEHDR这一简单而非常有用的工具帮助下,已经在Windows的浩瀚海洋中畅游了一会,下面就来掀开“陷阱”技术的神秘面纱。三、动态汉化Windows原理
我们知道,传统的汉化Windows的方法,是要直接修改Windows的显示、输入、打印等模块代码,或用DDK直接开发“中文设备”驱动模块,这样不仅工作量浩大,而且,系统的完备性很难保证,性能上也有很多限制(早期的长青窗口就是这样),这样,只有从内核上修改Windows核心代码才是最彻底的办法。
从Windows的模块调用机制,我们可以看到,Windows实际上是由包括在KERNEL,GDI,USER等几个模块中的众多函数支撑的。那么,修改其中涉及语言文字处理的函数,使之能适应中文需要,不就能达到汉化目的了吗?因而,我们可以得出这样的结论:在自己的模块中重新编写涉及文字显示,输入的多个函数,然后,将Windows中对这些函数的引用,改向到自己的这些模块中来。修改哪些函数才能完成汉化,这需要深入分析Windows的内部结构,但CHINESE.DLL已明确无误地告诉了我们,在其数据段的重定位表中列出的引用函数,正是CStar修改了的Windows函数!为了验证这一思路,我们利用RichWin作一核实。用EXEHDR分析GDI.EXE,得出ExtTextOut函数在GDI的第一代码段6139H偏移处(不同版本的Windows其所在代码段和偏移可能不一样)。然后,用HelpWalk(也是MicrosoftVisualC++开发工具中的一个)检查GDI的Code1段,6139H处前5个字节是B8FF054555,经过运行RichWin4.3forInternet后,再查看同样的地方,已改为EA08088F3D,其实反汇编就知道,这5个字节就是代表Jmp3D8F:0808,而句柄为0x3D8F的模块,用HelpWalk能观察到正是RichWin的WSENGINE.DLL的第一代码段(模块名为TEXTMAN)。而偏移0808H处B8B73D45558BEC1E,正是一个函数起始的地方,这实际上就是RichWin所重改写的ExtTextOut函数。退出RichWin后,再用HelpWalk观察GDI的Code1代码段,一切又恢复正常!这与前面的分析结论完全吻合!那么,下一个关键点就是如何动态修改Windows的函数代码,也就是汉化Windows的核心——“陷阱”技术。
四、“陷阱”技术
讨论“陷阱”技术,还要回到前面的两个发现。发现之二,已能解释为修改的Windows函数,而发现之一,却仍是一个迷。
数据段存放的是变量及常量等内容,如果这里面包含有重定位信息,那么,必定要在变量说明中将函数指针赋给一个FARPROC类型的变量,于是,在变量说明中写下:
FARPROCFarProcFunc=ExtTextOut;
果然,我自己程序的数据段中也有了重定位信息。这样,当程序调入内存中时,变量FarProcFunc已是函数ExtTextOut的地址了。要直接修改代码段的内容,还遇到一个难题,就是代码段是不可改写的。这时,需要用到一个未公开的Windows函数AllocCStoDSAlias取得与代码段有相同基址的可写数据段别名,其函数声明为
WORDFARPASCALAllocCStoDSAlias(WORDcode_sel);
参数是代码段的句柄,返回值是可写数据段别名句柄。Windows中函数地址是32位,高字是其模块的内存句柄,低字是函数在模块内的偏移。将得到的可写数据段别名句柄锁定,再将函数偏移处的5个字节保留下来,然后将其改为转向替代函数(用EAJmp)*(lpStr+wOffset)=0xEA;
*(lpStr+wOffset+1)=lpFarProcReplace;反汇编即是JmplpFarProcReplace,最后,内存解锁。
这就是我们为Windows设的“陷阱”,当所有对此函数的调用都无条件地转到我们规定的替代函数处。当程序结束之前,将保留的5字节内容再置回来,否则,系统会崩溃。下面给出作者编写的使Windows的ExtTextOut函数落入自己函数“陷阱”的源程序。//源程序 relocate.c
#include
#include BOOL WINAPI MyExtTextOut(HDC hDC, int x,
int y, UINT nInt1, const RECT
FAR* lpRect,LPCSTR lpStr, UINT nInt2, int FAR* lpInt);
WORD FAR PASCAL AllocCStoDSAlias(WORD code_sel);typedef struct tagFUNC
{
FARPROC lpFarProcReplace;//替代函数地址
FARPROC lpFarProcWindows;//Windows函数地址
BYTEbOld;//保存原函数第一字节
LONGlOld;//保存原函数接后的四字节长值
}FUNC;FUNCFunc={MyExtTextOut,ExtTextOut};//Windows主函数
int PASCAL WinMain(HINSTANCE
hInstance,HINSTANCE hPrevInstance,
LPSTR lpCmdLine,int nCmdShow)
{
HANDLE hMemCode;//代码段句柄
WORD hMemData;//相同基址的可写数据段别名
WORD wOffset; //函数偏移
LPSTRlpStr;
LPLONG lpLong;
char lpNotice[96];
hMemCode=HIWORD((LONG) Func.lpFarProcWindows );
wOffset=LOWORD((LONG) Func.lpFarProcWindows );wsprintf(lpNotice,"函数所在模块句柄 0x%4xH,偏移 0x%4xH",
hMemCode,wOffset);
MessageBox(NULL,lpNotice,"提示",MB_OK);//取与代码段有相同基址的可写数据段别名
hMemData=AllocCStoDSAlias(hMemCode);lpStr=GlobalLock(hMemData); lpLong=(lpStr+wOffset+1 );
//保存原函数要替换的头几个字节
Func.bOld=*(lpStr+wOffset);
Func.lOld=*lpLong;*(lpStr+wOffset)=0xEA;
*lpLong=Func.lpFarProcReplace;
GlobalUnlock(hMemData);MessageBox(NULL,"改为自己的函数","提示",MB_OK);//将保留的内容改回来
hMemData=AllocCStoDSAlias(hMemCode);
lpStr=GlobalLock(hMemData);
lpLong=(lpStr+wOffset+1 );
*(lpStr+wOffset)=Func.bOld;
*lpLong=Func.lOld;
GlobalUnlock(hMemData);MessageBox(NULL,"改回原Windows函数","提示",MB_OK);
return 1;
}//自己的替代函数
BOOL WINAPI MyExtTextOut(HDC hDC, int x, int y, UINT nInt1,
const RECT FAR* lpRect, LPCSTR lpStr, UINT nInt2, int FAR* lpInt)
{
BYTE NameDot[96]={
0x09, 0x00, 0xfd, 0x08, 0x09, 0x08, 0x09, 0x10, 0x09, 0x20,
0x79, 0x40, 0x41, 0x04, 0x47, 0xfe, 0x41, 0x40, 0x79, 0x40,
0x09, 0x20, 0x09, 0x20, 0x09, 0x10, 0x09, 0x4e, 0x51, 0x84,
0x21, 0x00, 0x02, 0x00, 0x01, 0x04, 0xff, 0xfe, 0x00, 0x00,
0x1f, 0xf0, 0x10, 0x10, 0x10, 0x10, 0x1f, 0xf0, 0x00, 0x00,
0x7f, 0xfc, 0x40, 0x04, 0x4f, 0xe4, 0x48, 0x24, 0x48, 0x24,
0x4f, 0xe4, 0x40, 0x0c, 0x10, 0x80, 0x10, 0xfc, 0x10, 0x88,
0x11, 0x50, 0x56, 0x20, 0x54, 0xd8, 0x57, 0x06, 0x54, 0x20,
0x55, 0xfc, 0x54, 0x20, 0x55, 0xfc, 0x5c, 0x20, 0x67, 0xfe,
0x00, 0x20, 0x00, 0x20, 0x00, 0x20
}; HBITMAP hBitmap,hOldBitmap;
HDC hMemDC;
BYTE far *lpDot;
int i;for ( i=0;i<3;i++ )
{
lpDot=(LPSTR)NameDot+i*32;
hMemDC=CreateCompatibleDC(hDC);
hBitmap=CreateBitmap(16,16,1,1,lpDot);
SetBitmapBits(hBitmap,32L,lpDot);
hOldBitmap=SelectObject(hMemDC,hBitmap);
BitBlt(hDC,x+i*16,y,16,16,hMemDC,0,0,SRCCOPY);
DeleteDC(hMemDC);
DeleteObject(hBitmap);
}return TRUE;
}//模块定义文件relocate.def
NAMERELOCATE
EXETYPE WINDOWS
CODEPRELOAD MOVEABLE DISCARDABLE
DATAPRELOAD MOVEABLE MULTIPLE
HEAPSIZE1024
EXPORTS五、结束语
本文从原理上分析了称为“陷阱”技术的汉化Windows方法。要彻底汉化Windows还要涉及显示,键盘输入等诸多内容,决非一日之功。但作为对“陷阱”技术的分析,本文介绍了将任一Windows函数调用改向到自己指定函数处的通用方法,这种方法可以拓展到其它应用中,如多语种显示,不同内码制式的切换显示等。
参考文献:
AndrewSchulmanDavidMaxeyMattPietrek,《未公开的Windows核心技术》,清华大学出版社,1993年。王志东,“Windows中文环境”,《Windows软件的应用与开发》,1993.5。
(作者地址:山东潍坊华光科技股份公司研究开发中心 张高峰 邮编261041 )
[email protected]