分类: 技术●项目
虽然屏幕取词技术早已经不是什么秘密,以至于除了汉化工具、翻译工具、中文平台等等这些东西之外,连像SnagIt这样的抓图软件也能把抓取屏幕文本的功能做得像模像样,但金山词霸的取词技术就细节而言还是有着众多的独特之处,所以,作为在金山词霸组工作期间的一点积累,我最终还是决定把有关的一些东西写出来,这样也作为直到2006年为止金山词霸取词技术的一个比较稳定版本的记录。
单机版的金山词霸很难再出什么新花样了,这是在现实的环境下一个通用软件产品的生存期规律决定的,随之而来的,单机版金山词霸的结构和技术也基本不会有什么大变动了,这其中也包括屏幕取词——虽然词霸组从05年开始就一直想对当时的屏幕取词方式进行升级以适应越来越苛刻的系统安全要求,不过后来由于种种原因一直没有能够实施。
金山词霸的屏幕取词技术是一种基于Win32API的,只能应用于客户端的偏底层操作技术,在这个互联网的时代,在追求注意力,追求现实效益的行业大环境下,金山词霸的取词技术不容易再有什么比较大的发展了,短期之内其应用也仅限于一些需要此功能的小型客户端程序(如词霸豆豆)以及作为OCX插件来支持B/S结构产品的用户体验提升。至于Windows Vista出来之后在Avalon和GDI+模式下的技术更新,则不是我现在能够预料得到的了,其可行性将在后面稍作讨论。
好了,说了这么多废话,也该进入正题了,不过在此之前要申明的一点是:本文所涉及的所有细节技术和方法,都是行业内所共知或者从业者通过正规方式能够获知和了解的,而宏观的思路和逻辑也是具有相当技术水平的软件开发人员通过思考能够获得的;因此本文不会侵犯到金山公司的商业机密和知识产权,也不会违反本人与金山公司之前签署的保密协定。实际上我并不是金山词霸取词技术的主要开发人,所以即便我有心说一些什么也无法触及比较秘密的细节内容,呵呵。仅此。
之前有不少文章来讨论或者“揭密”金山词霸的取词技术,似乎这样一种技术瞬间从神秘无比就变成了一层窗户纸,不过在接触了实际的代码之后,我想要说的是,这是一种十分正常的软件开发技术,这样一种技术的开发、积累和完善,同许多其他技术一样也是由简而繁,从基础的思路到最终的产品一步步走过来的;那种以为只要懂得了API Hook就了解了屏幕取词的全部技术的想法是有偏差的。
API Hook是一种常规的核心编程技术,其基础的实现方式和思路请参照《Windows核心编程》的第22章——顺带说一下,这本书是所有触及Windows底层应用的程序开发人员应该储备的工具书之一。
先说说屏幕取词的基本设计思路。
对Windows编程有所了解的的人都知道,Windows为每个进程分配了2GB的虚地址空间,并使用了一系列的措施来保证每个进程各行其道,不会互相影响——这点就比Linux要好一些,那些说Linux安全性比Windows要高的人很多时候并不知道——原则上进程间的信息交互只能由相互信任的进程采用约定的方法——比如消息传递、共享内存、内存映射文件、Socket(Network),甚至磁盘文件系统等等;但是屏幕取词的要求本质上是要取得一个未知进程里的某个特别操作的执行数据,那么,在没有标准方法来执行这一点的时候,我们要想办法将位置的进程编程与我们的取数进程相互信任并且已经约定好数据交互方法的进程——目前看来比较现实的方法,或者说唯一的方法,是让目标进程执行我们设计好的代码,这样,我们的代码取得宿主进程的执行权限,并了解如何把数据传递给我们的取词进程,如果再能够获得特定操作时的数据(例如TextOut),我们的架构就完整了。
对于第一个需求,金山词霸的操作简单的就是几个函数的序列:WriteProcessMemory,CreateRemoteThread,ReadProcessMemory。这是我之前提到几种方法之一的变形;对于第二个需求,插入进去的代码会修改程序的运行指令,将需要获得其操作数据的函数地址强行更改为我们自己编写的具有相同形式定义的函数,在我们的函数处理完成之后,再调用原本应该处理那些数据的函数去执行,而我们则可以通过事先约定好的方法得到操作数据的一个副本。修改原本函数的执行地址的方法,我们称为挂接,其表现形式类似于插入一个函数调用。
实际上这种方法很像原先在Windows 9X上使用的外壳DLL的处理方式,有一些程序出于各种目的(有些甚至是为了增强系统安全,但实际上利用了系统的不安全隐患)将系统DLL替换成自己的DLL文件,并将原来的系统DLL改名,然后在自己的DLL文件中模拟出系统DLL的所有接口,这样程序调用系统接口的时候自然就会把数据传到新的DLL中去,新DLL处理完成后再以同样的数据去调用那个被改了名的系统DLL中的对应接口。不过由于Win2000内核的逐渐兴起,这种方法由于适应性差,工作量大,问题比较多而逐渐被废弃了。现在使用这个办法的程序大多只替换一些用户级的DLL库,干得一般也不是什么上得了台面的事情。
剩下来的就是一些细枝末节的问题,但却是比较麻烦的地方。
1、取到需要的数据。并不是所有的目标程序都使用TextOut进行文本输出,相当多的程序使用自己的缓存DC来进行文本显示,对于自绘缓存的情况,原则上来说任何方法都不可能覆盖所有的可能,特别是对于那些带有排版、阅读甚至权限控制功能的程序。简单的对文本输出函数的挂接常常会得到多到无法筛选处理的数据,要么就是根本监测不到函数调用。对于这种情况,无法绕开的解决办法是监视所有可能用于绘制的函数调用,并保存所有可能用于绘制的数据,然后根据目标进程的操作来智能判断有效数据,比如在预计目标进程进行屏幕输出的时候,监测到一些内存DC的文本绘制操作,接着又监测到屏幕DC的一些BitBlt之类的缓存覆盖操作,则要判断当前取词位置的屏幕DC被哪个内存DC所占有的缓冲区覆盖了,然后看看这个缓冲区之前曾经输出过哪些文本数据,如此等等。数据筛选的另外一个问题是定位,知道用户的鼠标位置处于取到的数据中那一个字符之上是很重要的,是后期的单词匹配和模式分析所不可缺少的。可惜的是GDI32并没有提供方便的方法来搞定这件事情,我们只能用一些间接的办法来实现,比如先获得字体,再执行模拟排版,这是个很麻烦的事情,对于各种字符的处理都要和GDI32完全一致。
2、挂接代码的执行、数据交换。由于是将代码注入到目标进程去执行,无形中就增加了许多限制。函数地址的计算是个比较大的问题,所有自定义的函数地址都要从一个易于通过系统标准方法获得的基准地址计算偏移量来获得,调用任何一个函数的时候都要明确的意识到在目标进程执行的情况,如此等等。而且,随着对系统安全性越来越高的要求,这种使用WriteProcessMemory进行代码注入的方式也逐渐暴露出来一些问题,例如在DEP环境下无法执行数据段代码的问题,取词时屏幕闪烁的问题,还有某些杀毒软件对可能造成系统危险的进程间操作进行屏蔽和报警的问题。金山词霸组曾经有一段时间考虑过使用适应性更好的DLL注入方式来替换掉挂接模块,但由于种种原因而没有实现。同时,对于一些比较复杂的数据对象,有时并不是很容易取到其内部的数据,这样就往往要辗转几次才能迂回的完成任务,有时甚至需要修改系统文件定义才能取到Private成员这样的东西。
3、现场清理、与其它挂接的兼容性。对于挂接API这样一种搭车行为,做完要做的事情之后最好是能够不留痕迹的清理好现场,这既是出于系统执行效率和资源消耗的考虑,也是为了系统安全的目的,用于挂接和数据传递的代码区域在使用完成之后应该进行资源释放,对于执行失败甚至异常的操作也应该有相对稳妥的办法去把垃圾代码清除掉。金山词霸挂接了一个不常用的函数作为自身挂接状态的标记,除了每次挂接任务完成后要执行自身清理之外,每次挂接前还要检查一下这个标记来确定是否有未解除成功的以前的挂接,并根据需要执行清理。对于其它进程同时进行挂接的情况,如果不加判断直接将系统API挂接地址修改为自身的函数入口地址,则另外的挂接程序就可能发生不可预知的执行问题。实际开发中发现东方快车、中文之星这样的软件在遇到挂接冲突时的确会发生问题。因此比较柔和的办法是等待其它挂接程序先摘除自身的挂接,再执行我们的操作,同时还要保证我们的挂接代码被其它程序强行拆掉之后不给目标进程造成不良影响,且能被再次挂接的操作识别从而完成清理。
4、特殊的目标。一些对绘制任务执行了比较复杂处理的软件,比如Acrobat、Word、IE等等,如果使用基本的API Hook方法会使出错崩溃的机会大大增加,而且由于其不公开的执行逻辑和复杂的处理方式,使得针对其进行的调试工作难于进行,不过好在它们大多数提供了另外的方法来完成我们的任务,我们可以将这些方法以插件的方式集成到取词的模块当中。比如Acrobat的SDK就提供了获取正在显示文档某区域文本的功能,Word支持的Automation则允许在取词插件被启用的时候向外部进程暴露出一部分数据,IE则直接支持了获取显示窗口的Document;比较有趣的是Apabi,它的开发人员发现词霸没有为其独立制作可用的取词插件(实际上是没办法),就在每次自己进行绘制缓存输出的时候,调用了一次空的TextOut方法,用来配合金山词霸的取词方式,哈。这里还想顺带说一下触发目标进程重绘屏幕的方法,正常情况下我们会用一个透明窗口把用户鼠标焦点附近挡一下,这样Windows就会自动给目标窗口发一个区域无效的消息提醒目标进程重新绘制被遮挡的部分;但仅仅是这样的话会有不少软件和你闹别扭,比如大名鼎鼎的QQ,在某些版本里那个家伙被一个透明窗口挡住都会出现一片白色的未正常绘制的区域,而且根本不会自己重绘,对于这样的问题,呵呵,只能具体问题具体分析了。
5、未来可行性。前面提到了Avalon和GDI+,这些新出来的东西是金山词霸在最初开发时没有考虑的。GDI+的问题已经解决了,毕竟它目前还是运行在Win32平台上的,通过分析它的Flat API和更底层的非文档接口,我们用同样的方法解决了取词问题,甚至,由于GDI+提供了方便的计算字符位置的方法,获取用户鼠标焦点位置字符的方法也变得容易了许多。有限的一点感慨就是:没有文档的接口还真是不容易用啊。Avalon现在被设计为与GDI+平级的一个显示层接口,由于集成了2D和3D显示接口,其内部结构目前看来是相当的复杂,但是由于其仍然支持Win32平台,并且考虑到目前的3D设备在系统中的位置,个人认为Avalon的2D部分的API Hook取词也有着相当的可行性。实际上金山游侠也是金山词霸组的产品,所以我们当初考虑DirectX方式下的取词和显示也是可行的,不过由于其实现成本比较高,预期效益也并不大,就没有做。WinFX我没有太深的研究,更深的细节现在还没法说,嘿嘿。还有一个麻烦的事情是Java,Java的桌面程序总让人感觉不伦不类,分析下来在Windows平台下它有一部分文本输出是调用了一些W非文档的函数,而有一些则是使用自带字库进行绘制——对于后者,虽然不能说是一点办法没有,但实际的商业价值似乎不大,只是不知道在Windows Vista上的Java虚拟机会怎么做。最后一个潜在的问题是移动设备,受用户输入方式和系统资源的限制目前对屏幕取词的需求还不是很强烈,但在可以预见的未来,还是有一些苗头的。
6、后记。从现在的趋势看来,即便Windows Vista给我们提供了更加丰富的接口功能,更高效的应用软件开发模式,更强悍的界面表现方式,更方便的数据通信和沟通方式,B/S的大潮仍然无法阻挡的,这甚至代表了整个软件生产和使用的趋势,Browser功能的不断扩充与其说是网络应用进化的必然,不如说是埋下了系统表示层浏览器化的伏笔,而微软在Windows Vista上所作的一切也使我多少嗅到了这样的味道——如果这是真的,那么金山词霸取词技术现在这个样子,则有可能随着不可挽回的Win32落潮而成为这个时代的终篇之一。
能想起来的都说了,再想起来什么的话,再改吧,呵呵。