<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      伯樂共勉

      討論。NET專區(qū)
        博客園  :: 首頁  :: 新隨筆  :: 聯(lián)系 :: 訂閱 訂閱  :: 管理

      屏幕取詞技術(shù)實現(xiàn)原理與關(guān)鍵源碼

      Posted on 2007-11-30 09:00  伯樂共勉  閱讀(11619)  評論(5)    收藏  舉報
      雖然屏幕取詞技術(shù)早已經(jīng)不是什么秘密,以至于除了漢化工具、翻譯工具、中文平臺等等這些東西之外,連像SnagIt這樣的抓圖軟件也能把抓取屏幕文 本的功能做得像模像樣,但金山詞霸的取詞技術(shù)就細(xì)節(jié)而言還是有著眾多的獨特之處,所以,作為在金山詞霸組工作期間的一點積累,我最終還是決定把有關(guān)的一些 東西寫出來,這樣也作為直到2006年為止金山詞霸取詞技術(shù)的一個比較穩(wěn)定版本的記錄。

          單機版的金山詞霸很難再出什么新花樣了,這是在現(xiàn)實的環(huán)境下一個通用軟件產(chǎn)品的生存期規(guī)律決定的,隨之而來的,單機版金山詞霸的結(jié)構(gòu)和技術(shù)也基本不會有什 么大變動了,這其中也包括屏幕取詞——雖然詞霸組從05年開始就一直想對當(dāng)時的屏幕取詞方式進行升級以適應(yīng)越來越苛刻的系統(tǒng)安全要求,不過后來由于種種原 因一直沒有能夠?qū)嵤?/p>

          金山詞霸的屏幕取詞技術(shù)是一種基于Win32API的,只能應(yīng)用于客戶端的偏底層操作技術(shù),在這個互聯(lián)網(wǎng)的時代,在追求注意力,追求現(xiàn)實效益的行業(yè)大環(huán)境 下,金山詞霸的取詞技術(shù)不容易再有什么比較大的發(fā)展了,短期之內(nèi)其應(yīng)用也僅限于一些需要此功能的小型客戶端程序(如詞霸豆豆)以及作為OCX插件來支持 B/S結(jié)構(gòu)產(chǎn)品的用戶體驗提升。至于Windows Vista出來之后在Avalon和GDI+模式下的技術(shù)更新,則不是我現(xiàn)在能夠預(yù)料得到的了,其可行性將在后面稍作討論。

          好了,說了這么多廢話,也該進入正題了,不過在此之前要申明的一點是:本文所涉及的所有細(xì)節(jié)技術(shù)和方法,都是行業(yè)內(nèi)所共知或者從業(yè)者通過正規(guī)方式能夠獲知 和了解的,而宏觀的思路和邏輯也是具有相當(dāng)技術(shù)水平的軟件開發(fā)人員通過思考能夠獲得的;因此本文不會侵犯到金山公司的商業(yè)機密和知識產(chǎn)權(quán),也不會違反本人 與金山公司之前簽署的保密協(xié)定。實際上我并不是金山詞霸取詞技術(shù)的主要開發(fā)人,所以即便我有心說一些什么也無法觸及比較秘密的細(xì)節(jié)內(nèi)容,呵呵。僅此。

          之前有不少文章來討論或者“揭密”金山詞霸的取詞技術(shù),似乎這樣一種技術(shù)瞬間從神秘?zé)o比就變成了一層窗戶紙,不過在接觸了實際的代碼之后,我想要說的是, 這是一種十分正常的軟件開發(fā)技術(shù),這樣一種技術(shù)的開發(fā)、積累和完善,同許多其他技術(shù)一樣也是由簡而繁,從基礎(chǔ)的思路到最終的產(chǎn)品一步步走過來的;那種以為 只要懂得了API Hook就了解了屏幕取詞的全部技術(shù)的想法是有偏差的。

          API Hook是一種常規(guī)的核心編程技術(shù),其基礎(chǔ)的實現(xiàn)方式和思路請參照《Windows核心編程》的第22章——順帶說一下,這本書是所有觸及Windows底層應(yīng)用的程序開發(fā)人員應(yīng)該儲備的工具書之一。

          先說說屏幕取詞的基本設(shè)計思路。

          對Windows編程有所了解的的人都知道,Windows為每個進程分配了2GB的虛地址空間,并使用了一系列的措施來保證每個進程各行其道,不會互相 影響——這點就比Linux要好一些,那些說Linux安全性比Windows要高的人很多時候并不知道——原則上進程間的信息交互只能由相互信任的進程 采用約定的方法——比如消息傳遞、共享內(nèi)存、內(nèi)存映射文件、Socket(Network),甚至磁盤文件系統(tǒng)等等;但是屏幕取詞的要求本質(zhì)上是要取得一 個未知進程里的某個特別操作的執(zhí)行數(shù)據(jù),那么,在沒有標(biāo)準(zhǔn)方法來執(zhí)行這一點的時候,我們要想辦法將位置的進程編程與我們的取數(shù)進程相互信任并且已經(jīng)約定好 數(shù)據(jù)交互方法的進程——目前看來比較現(xiàn)實的方法,或者說唯一的方法,是讓目標(biāo)進程執(zhí)行我們設(shè)計好的代碼,這樣,我們的代碼取得宿主進程的執(zhí)行權(quán)限,并了解 如何把數(shù)據(jù)傳遞給我們的取詞進程,如果再能夠獲得特定操作時的數(shù)據(jù)(例如TextOut),我們的架構(gòu)就完整了。

          對于第一個需求,金山詞霸的操作簡單的就是幾個函數(shù)的序列:WriteProcessMemory,CreateRemoteThread, ReadProcessMemory。這是我之前提到幾種方法之一的變形;對于第二個需求,插入進去的代碼會修改程序的運行指令,將需要獲得其操作數(shù)據(jù)的 函數(shù)地址強行更改為我們自己編寫的具有相同形式定義的函數(shù),在我們的函數(shù)處理完成之后,再調(diào)用原本應(yīng)該處理那些數(shù)據(jù)的函數(shù)去執(zhí)行,而我們則可以通過事先約 定好的方法得到操作數(shù)據(jù)的一個副本。修改原本函數(shù)的執(zhí)行地址的方法,我們稱為掛接,其表現(xiàn)形式類似于插入一個函數(shù)調(diào)用。

          實際上這種方法很像原先在Windows 9X上使用的外殼DLL的處理方式,有一些程序出于各種目的(有些甚至是為了增強系統(tǒng)安全,但實際上利用了系統(tǒng)的不安全隱患)將系統(tǒng)DLL替換成自己的 DLL文件,并將原來的系統(tǒng)DLL改名,然后在自己的DLL文件中模擬出系統(tǒng)DLL的所有接口,這樣程序調(diào)用系統(tǒng)接口的時候自然就會把數(shù)據(jù)傳到新的DLL 中去,新DLL處理完成后再以同樣的數(shù)據(jù)去調(diào)用那個被改了名的系統(tǒng)DLL中的對應(yīng)接口。不過由于Win2000內(nèi)核的逐漸興起,這種方法由于適應(yīng)性差,工 作量大,問題比較多而逐漸被廢棄了?,F(xiàn)在使用這個辦法的程序大多只替換一些用戶級的DLL庫,干得一般也不是什么上得了臺面的事情。

          剩下來的就是一些細(xì)枝末節(jié)的問題,但卻是比較麻煩的地方。

          1、取到需要的數(shù)據(jù)。并不是所有的目標(biāo)程序都使用TextOut進行文本輸出,相當(dāng)多的程序使用自己的緩存DC來進行文本顯示,對于自繪緩存的情況,原則 上來說任何方法都不可能覆蓋所有的可能,特別是對于那些帶有排版、閱讀甚至權(quán)限控制功能的程序。簡單的對文本輸出函數(shù)的掛接常常會得到多到無法篩選處理的 數(shù)據(jù),要么就是根本監(jiān)測不到函數(shù)調(diào)用。對于這種情況,無法繞開的解決辦法是監(jiān)視所有可能用于繪制的函數(shù)調(diào)用,并保存所有可能用于繪制的數(shù)據(jù),然后根據(jù)目標(biāo) 進程的操作來智能判斷有效數(shù)據(jù),比如在預(yù)計目標(biāo)進程進行屏幕輸出的時候,監(jiān)測到一些內(nèi)存DC的文本繪制操作,接著又監(jiān)測到屏幕DC的一些BitBlt之類 的緩存覆蓋操作,則要判斷當(dāng)前取詞位置的屏幕DC被哪個內(nèi)存DC所占有的緩沖區(qū)覆蓋了,然后看看這個緩沖區(qū)之前曾經(jīng)輸出過哪些文本數(shù)據(jù),如此等等。數(shù)據(jù)篩 選的另外一個問題是定位,知道用戶的鼠標(biāo)位置處于取到的數(shù)據(jù)中那一個字符之上是很重要的,是后期的單詞匹配和模式分析所不可缺少的。可惜的是GDI32并 沒有提供方便的方法來搞定這件事情,我們只能用一些間接的辦法來實現(xiàn),比如先獲得字體,再執(zhí)行模擬排版,這是個很麻煩的事情,對于各種字符的處理都要和 GDI32完全一致。

          2、掛接代碼的執(zhí)行、數(shù)據(jù)交換。由于是將代碼注入到目標(biāo)進程去執(zhí)行,無形中就增加了許多限制。函數(shù)地址的計算是個比較大的問題,所有自定義的函數(shù)地址都要 從一個易于通過系統(tǒng)標(biāo)準(zhǔn)方法獲得的基準(zhǔn)地址計算偏移量來獲得,調(diào)用任何一個函數(shù)的時候都要明確的意識到在目標(biāo)進程執(zhí)行的情況,如此等等。而且,隨著對系統(tǒng) 安全性越來越高的要求,這種使用WriteProcessMemory進行代碼注入的方式也逐漸暴露出來一些問題,例如在DEP環(huán)境下無法執(zhí)行數(shù)據(jù)段代碼 的問題,取詞時屏幕閃爍的問題,還有某些殺毒軟件對可能造成系統(tǒng)危險的進程間操作進行屏蔽和報警的問題。金山詞霸組曾經(jīng)有一段時間考慮過使用適應(yīng)性更好的 DLL注入方式來替換掉掛接模塊,但由于種種原因而沒有實現(xiàn)。同時,對于一些比較復(fù)雜的數(shù)據(jù)對象,有時并不是很容易取到其內(nèi)部的數(shù)據(jù),這樣就往往要輾轉(zhuǎn)幾 次才能迂回的完成任務(wù),有時甚至需要修改系統(tǒng)文件定義才能取到Private成員這樣的東西。

          3、現(xiàn)場清理、與其它掛接的兼容性。對于掛接API這樣一種搭車行為,做完要做的事情之后最好是能夠不留痕跡的清理好現(xiàn)場,這既是出于系統(tǒng)執(zhí)行效率和資源 消耗的考慮,也是為了系統(tǒng)安全的目的,用于掛接和數(shù)據(jù)傳遞的代碼區(qū)域在使用完成之后應(yīng)該進行資源釋放,對于執(zhí)行失敗甚至異常的操作也應(yīng)該有相對穩(wěn)妥的辦法 去把垃圾代碼清除掉。金山詞霸掛接了一個不常用的函數(shù)作為自身掛接狀態(tài)的標(biāo)記,除了每次掛接任務(wù)完成后要執(zhí)行自身清理之外,每次掛接前還要檢查一下這個標(biāo) 記來確定是否有未解除成功的以前的掛接,并根據(jù)需要執(zhí)行清理。對于其它進程同時進行掛接的情況,如果不加判斷直接將系統(tǒng)API掛接地址修改為自身的函數(shù)入 口地址,則另外的掛接程序就可能發(fā)生不可預(yù)知的執(zhí)行問題。實際開發(fā)中發(fā)現(xiàn)東方快車、中文之星這樣的軟件在遇到掛接沖突時的確會發(fā)生問題。因此比較柔和的辦 法是等待其它掛接程序先摘除自身的掛接,再執(zhí)行我們的操作,同時還要保證我們的掛接代碼被其它程序強行拆掉之后不給目標(biāo)進程造成不良影響,且能被再次掛接 的操作識別從而完成清理。

          4、特殊的目標(biāo)。一些對繪制任務(wù)執(zhí)行了比較復(fù)雜處理的軟件,比如Acrobat、Word、IE等等,如果使用基本的API Hook方法會使出錯崩潰的機會大大增加,而且由于其不公開的執(zhí)行邏輯和復(fù)雜的處理方式,使得針對其進行的調(diào)試工作難于進行,不過好在它們大多數(shù)提供了另 外的方法來完成我們的任務(wù),我們可以將這些方法以插件的方式集成到取詞的模塊當(dāng)中。比如Acrobat的SDK就提供了獲取正在顯示文檔某區(qū)域文本的功 能,Word支持的Automation則允許在取詞插件被啟用的時候向外部進程暴露出一部分?jǐn)?shù)據(jù),IE則直接支持了獲取顯示窗口的Document;比 較有趣的是Apabi,它的開發(fā)人員發(fā)現(xiàn)詞霸沒有為其獨立制作可用的取詞插件(實際上是沒辦法),就在每次自己進行繪制緩存輸出的時候,調(diào)用了一次空的 TextOut方法,用來配合金山詞霸的取詞方式,哈。這里還想順帶說一下觸發(fā)目標(biāo)進程重繪屏幕的方法,正常情況下我們會用一個透明窗口把用戶鼠標(biāo)焦點附 近擋一下,這樣Windows就會自動給目標(biāo)窗口發(fā)一個區(qū)域無效的消息提醒目標(biāo)進程重新繪制被遮擋的部分;但僅僅是這樣的話會有不少軟件和你鬧別扭,比如 大名鼎鼎的QQ,在某些版本里那個家伙被一個透明窗口擋住都會出現(xiàn)一片白色的未正常繪制的區(qū)域,而且根本不會自己重繪,對于這樣的問題,呵呵,只能具體問 題具體分析了。

          5、未來可行性。前面提到了Avalon和GDI+,這些新出來的東西是金山詞霸在最初開發(fā)時沒有考慮的。GDI+的問題已經(jīng)解決了,畢竟它目前還是運行 在Win32平臺上的,通過分析它的Flat API和更底層的非文檔接口,我們用同樣的方法解決了取詞問題,甚至,由于GDI+提供了方便的計算字符位置的方法,獲取用戶鼠標(biāo)焦點位置字符的方法也變 得容易了許多。有限的一點感慨就是:沒有文檔的接口還真是不容易用啊。Avalon現(xiàn)在被設(shè)計為與GDI+平級的一個顯示層接口,由于集成了2D和3D顯 示接口,其內(nèi)部結(jié)構(gòu)目前看來是相當(dāng)?shù)膹?fù)雜,但是由于其仍然支持Win32平臺,并且考慮到目前的3D設(shè)備在系統(tǒng)中的位置,個人認(rèn)為Avalon的2D部分 的API Hook取詞也有著相當(dāng)?shù)目尚行浴嶋H上金山游俠也是金山詞霸組的產(chǎn)品,所以我們當(dāng)初考慮DirectX方式下的取詞和顯示也是可行的,不過由于其實現(xiàn)成 本比較高,預(yù)期效益也并不大,就沒有做。WinFX我沒有太深的研究,更深的細(xì)節(jié)現(xiàn)在還沒法說,嘿嘿。還有一個麻煩的事情是Java,Java的桌面程序 總讓人感覺不倫不類,分析下來在Windows平臺下它有一部分文本輸出是調(diào)用了一些W非文檔的函數(shù),而有一些則是使用自帶字庫進行繪制——對于后者,雖 然不能說是一點辦法沒有,但實際的商業(yè)價值似乎不大,只是不知道在Windows Vista上的Java虛擬機會怎么做。最后一個潛在的問題是移動設(shè)備,受用戶輸入方式和系統(tǒng)資源的限制目前對屏幕取詞的需求還不是很強烈,但在可以預(yù)見 的未來,還是有一些苗頭的。

          6、后記。從現(xiàn)在的趨勢看來,即便Windows Vista給我們提供了更加豐富的接口功能,更高效的應(yīng)用軟件開發(fā)模式,更強悍的界面表現(xiàn)方式,更方便的數(shù)據(jù)通信和溝通方式,B/S的大潮仍然無法阻擋 的,這甚至代表了整個軟件生產(chǎn)和使用的趨勢,Browser功能的不斷擴充與其說是網(wǎng)絡(luò)應(yīng)用進化的必然,不如說是埋下了系統(tǒng)表示層瀏覽器化的伏筆,而微軟 在Windows Vista上所作的一切也使我多少嗅到了這樣的味道——如果這是真的,那么金山詞霸取詞技術(shù)現(xiàn)在這個樣子,則有可能隨著不可挽回的Win32落潮而成為這 個時代的終篇之一。

          能想起來的都說了,再想起來什么的話,再改吧,呵呵。

       

      在金山詞霸中2005中帶了一個XdictGrb.dll,添加引用

      廢話不多說了,還是把源碼放上

      using System;
      using System.Collections.Generic;
      using System.ComponentModel;
      using System.Data;
      using System.Text;
      using System.Windows.Forms;
      using XDICTGRB;//金山詞霸組件

      namespace WindowsApplication1
      {
      public partial class Form1 : Form,IXDictGrabSink
      {
      public Form1()
      {
      InitializeComponent();
      }
      private void Form1_Load(object sender, EventArgs e)
      {
      GrabProxy gp = new GrabProxy();
      gp.GrabInterval = 1;//指抓取時間間隔
      gp.GrabMode = XDictGrabModeEnum.XDictGrabMouse;//設(shè)定取詞的屬性
      gp.GrabEnabled = true;//是否取詞的屬性
      gp.AdviseGrab(this);
      }
      //接口的實現(xiàn)
      int IXDictGrabSink.QueryWord(string WordString, int lCursorX, int lCursorY, string SentenceString, ref int lLoc, ref int lStart)
      {
      this.textBox1.Text = SentenceString;//鼠標(biāo)所在語句
      //this.textBox1.Text = SentenceString.Substring(lLoc + 1,1);//鼠標(biāo)所在字符
      return 1;
      }
      }
      }

      B.Nhw32.dll法

      這個是C++寫的一個組件

      nhw32.dll 主要引出兩個函數(shù):

      1. DWORD WINAPI BL_SetFlag32(UINT nFlag,
      HWND hNotifyWnd,
      int MouseX,
      int MouseY)
      功能:
      啟動或停止取詞。
      參數(shù):
      nFlag
      [輸入] 指定下列值之一:
      GETWORD_ENABLE: 開始取詞。在重畫被取單詞區(qū)域前設(shè)置此標(biāo)志。nhw32.dll是通過
      重畫單詞區(qū)域,截取TextOutA, TextOutW, ExtTextOutA,
      ExtTextOutW等Windows API函數(shù)的參數(shù)來取詞的。
      GETWORD_DISABLE: 停止取詞。
      hNotifyWnd
      [輸入] 通知窗口句柄。當(dāng)取到此時,向該通知窗口發(fā)送一登記消息:GWMSG_GETWORDOK。
      MouseX
      [輸入] 指定取詞點的X坐標(biāo)。
      MouseY
      [輸入] 指定取詞點的Y坐標(biāo)。
      返回值:
      可忽略。
      2. DWORD WINAPI BL_GetText32(LPSTR lpszCurWord,
      int nBufferSize,
      LPRECT lpWordRect)
      功能:
      從 內(nèi)部緩沖區(qū)取出單詞文本串。對英語文本,該函數(shù)最長取出一行內(nèi)以空格為界的三個英文單詞串,遇空格,非英文字母及除‘-’外的標(biāo)點符號,則終止取詞。對漢 字文本,該函數(shù)最長取出一行漢字串,遇英語字母,標(biāo)點符號等非漢語字符,則終止取詞。該函數(shù)不能同時取出英語和漢語字符。
      參數(shù):
      lpszCurWord
      [輸入] 目的緩沖區(qū)指針。
      nBufferSize
      [輸入] 目的緩沖區(qū)大小。
      lpWordRect
      [輸出] 指向 RECT 結(jié)構(gòu)的指針。該結(jié)構(gòu)定義了被取單詞所在矩形區(qū)域。
      返回值:
      當(dāng)前光標(biāo)在全部詞中的位置。

      此外,WinNT/2000版 nhw32.dll 還引出另兩個函數(shù):

      1. BOOL WINAPI SetNHW32()
      功能:
      Win NT/2000 環(huán)境下的初始化函數(shù)。一般在程序開始時,調(diào)用一次。
      參數(shù):
      無。
      返回值:
      如果成功 TRUE ,失敗 FALSE 。

      2. BOOL WINAPI ResetNHW32()
      功能:
      Win NT/2000 環(huán)境下的去初始化函數(shù)。一般在程序結(jié)束時調(diào)用。
      參數(shù):
      無。
      返回值:
      如果成功 TRUE ,失敗 FALSE 。

       

       


      "鼠標(biāo)屏幕取詞"技術(shù)是在電子字典中得到廣泛地應(yīng)用的,如四通利方和金山詞霸等軟件,這個技術(shù)看似簡單,其實在windows系統(tǒng)中實現(xiàn)卻是非常復(fù)雜的,總的來說有兩種實現(xiàn)方式:
      第一種:采用截獲對部分gdi的api調(diào)用來實現(xiàn),如textout,textouta等。
      第二種:對每個設(shè)備上下文(dc)做一分copy,并跟蹤所有修改上下文(dc)的操作。
      第 二種方法更強大,但兼容性不好,而第一種方法使用的截獲windowsapi的調(diào)用,這項技術(shù)的強大可能遠(yuǎn)遠(yuǎn)超出了您的想象,毫不夸張的說,利用 windowsapi攔截技術(shù),你可以改造整個操作系統(tǒng),事實上很多外掛式windows中文平臺就是這么實現(xiàn)的!而這項技術(shù)也正是這篇文章的主題。
      截windowsapi的調(diào)用,具體的說來也可以分為兩種方法:
      第一種方法通過直接改寫winapi 在內(nèi)存中的映像,嵌入?yún)R編代碼,使之被調(diào)用時跳轉(zhuǎn)到指定的地址運行來截獲;第二種方法則改寫iat(import address table輸入地址表),重定向winapi函數(shù)的調(diào)用來實現(xiàn)對winapi的截獲。
      第 一種方法的實現(xiàn)較為繁瑣,而且在win95、98下面更有難度,這是因為雖然微軟說win16的api只是為了兼容性才保留下來,程序員應(yīng)該盡可能地調(diào)用 32位的api,實際上根本就不是這樣!win 9x內(nèi)部的大部分32位api經(jīng)過變換調(diào)用了同名的16位api,也就是說我們需要在攔截的函數(shù)中嵌入16位匯編代碼!
      我們將要介紹的是第二 種攔截方法,這種方法在win95、98和nt下面運行都比較穩(wěn)定,兼容性較好。由于需要用到關(guān)于windows虛擬內(nèi)存的管理、打破進程邊界墻、向應(yīng)用 程序的進程空間中注入代碼、pe(portable executable)文件格式和iat(輸入地址表)等較底層的知識,所以我們先對涉及到的這些知識大概地做一個介紹,最后會給出攔截部分的關(guān)鍵代碼。
      先說windows虛擬內(nèi)存的管理。windows9x給每一個進程分配了4gb的地址空間,對于nt來說,這個數(shù)字是2gb,系統(tǒng)保留了2gb 到 4gb之間的地址空間禁止進程訪問,而在win9x中,2gb到4gb這部分虛擬地址空間實際上是由所有的win32進程所共享的,這部分地址空間加載了 共享win32 dll、內(nèi)存映射文件和vxd、內(nèi)存管理器和文件系統(tǒng)碼,win9x中這部分對于每一個進程都是可見的,這也是win9x操作系統(tǒng)不夠健壯的原因。 win9x中為16位操作系統(tǒng)保留了0到4mb的地址空間,而在4mb到2gb之間也就是win32進程私有的地址空間,由于 每個進程的地址空間都是相對獨立的,也就是說,如果程序想截獲其它進程中的api調(diào)用,就必須打破進程邊界墻,向其它的進程中注入截獲api調(diào)用的代碼, 這項工作我們交給鉤子函數(shù)(setwindowshookex)來完成,關(guān)于如何創(chuàng)建一個包含系統(tǒng)鉤子的動態(tài)鏈接庫,《電腦高手雜志》在第?期已經(jīng)有過專 題介紹了,這里就不贅述了。所有系統(tǒng)鉤子的函數(shù)必須要在動態(tài)庫里,這樣的話,當(dāng)進程隱式或顯式調(diào)用一個動態(tài)庫里的函數(shù)時,系統(tǒng)會把這個動態(tài)庫映射到這個進 程的虛擬地址空間里,這使得dll成為進程的一部分,以這個進程的身份執(zhí)行,使用這個進程的堆棧,也就是說動態(tài)鏈接庫中的代碼被鉤子函數(shù)注入了其它gui 進程的地址空間(非gui進程,鉤子函數(shù)就無能為力了),當(dāng)包含鉤子的dll注入其它進程后,就可以取得映射到這個進程虛擬內(nèi)存里的各個模塊(exe和 dll)的基地址,如:hmodule hmodule=getmodulehandle("mypro.exe");在mfc程序中,我們可以用afxgetinstancehandle() 函數(shù)來得到模塊的基地址。exe和dll被映射到虛擬內(nèi)存空間的什么地方是由它們的基地址決定的。它們的基地址是在鏈接時由鏈接器決定的。當(dāng)你新建一個 win32工程時,vc++鏈接器使用缺省的基地址0x00400000??梢酝ㄟ^鏈接器的base選項改變模塊的基地址。exe通常被映射到虛擬內(nèi)存的 0x00400000處,dll也隨之有不同的基地址,通常被映射到不同進程的相同的虛擬地址空間處。
      系統(tǒng)將exe和dll原封不動映射到虛 擬內(nèi)存空間中,它們在內(nèi)存中的結(jié)構(gòu)與磁盤上的靜態(tài)文件結(jié)構(gòu)是一樣的。即pe (portable executable) 文件格式。我們得到了進程模塊的基地址以后,就可以根據(jù)pe文件的格式窮舉這個模塊的image_import_descriptor數(shù)組,看看進程空間 中是否引入了我們需要截獲的函數(shù)所在的動態(tài)鏈接庫,比如需要截獲"textouta",就必須檢查"gdi32.dll"是否被引入了。說到這里,我們有 必要介紹一下pe文件的格式,如右圖,這是pe文件格式的大致框圖,最前面是文件頭,我們不必理會,從pe file optional header后面開始,就是文件中各個段的說明,說明后面才是真正的段數(shù)據(jù),而實際上我們關(guān)心的只有一個段,那就是".idata"段,這個段中包含了所 有的引入函數(shù)信息,還有iat(import address table)的rva(relative virtual address)地址。
      說 到這里,截獲windowsapi的整個原理就要真相大白了。實際上所有進程對給定的api函數(shù)的調(diào)用總是通過pe文件的一個地方來轉(zhuǎn)移的,這就是一個該 模塊(可以是exe或dll)的".idata"段中的iat輸入地址表(import address table)。在那里有所有本模塊調(diào)用的其它dll的函數(shù)名及地址。對其它dll的函數(shù)調(diào)用實際上只是跳轉(zhuǎn)到輸入地址表,由輸入地址表再跳轉(zhuǎn)到dll真正 的函數(shù)入口。
      具體來說,我們將通過image_import_descriptor數(shù)組來訪問".idata"段中引入的dll的信息,然后 通過image_thunk_data數(shù)組來針對一個被引入的dll訪問該dll中被引入的每個函數(shù)的信息,找到我們需要截獲的函數(shù)的跳轉(zhuǎn)地址,然后改成 我們自己的函數(shù)的地址……具體的做法在后面的關(guān)鍵代碼中會有詳細(xì)的講解。
      講了這么多原理,現(xiàn)在讓我們回到"鼠標(biāo)屏幕取詞"的專題上來。除了api函數(shù)的截獲,要實現(xiàn)"鼠標(biāo)屏幕取詞",還需要做一些其它的工作,簡單的說來,可以把一個完整的取詞過程歸納成以下幾個步驟:
      1. 安裝鼠標(biāo)鉤子,通過鉤子函數(shù)獲得鼠標(biāo)消息。
      使用到的api函數(shù):setwindowshookex
      2. 得到鼠標(biāo)的當(dāng)前位置,向鼠標(biāo)下的窗口發(fā)重畫消息,讓它調(diào)用系統(tǒng)函數(shù)重畫窗口。
      使用到的api函數(shù):windowfrompoint,screentoclient,invalidaterect
      3. 截獲對系統(tǒng)函數(shù)的調(diào)用,取得參數(shù),也就是我們要取的詞。
      對于大多數(shù)的windows應(yīng)用程序來說,如果要取詞,我們需要截獲的是"gdi32.dll"中的"textouta"函數(shù)。
      我們先仿照textouta函數(shù)寫一個自己的mytextouta函數(shù),如:
      bool winapi mytextouta(hdc hdc, int nxstart, int nystart, lpcstr lpszstring,int cbstring)
      {
      // 這里進行輸出lpszstring的處理
      // 然后調(diào)用正版的textouta函數(shù)
      }
      把這個函數(shù)放在安裝了鉤子的動態(tài)連接庫中,然后調(diào)用我們最后給出的hookimportfunction函數(shù)來截獲進程對textouta函數(shù)的調(diào)用,跳轉(zhuǎn)到我們的mytextouta函數(shù),完成對輸出字符串的捕捉。hookimportfunction的用法:
      hookfuncdesc hd;
      proc porigfuns;
      hd.szfunc="textouta";
      hd.pproc=(proc)mytextouta;
      hookimportfunction (afxgetinstancehandle(),"gdi32.dll",&hd,porigfuns);
      下面給出了hookimportfunction的源代碼,相信詳盡的注釋一定不會讓您覺得理解截獲到底是怎么實現(xiàn)的很難,ok,let s go:
      ///////////////////////////////////////////// begin ///////////////////////////////////////////////////////////////
      #include <crtdbg.h>
      // 這里定義了一個產(chǎn)生指針的宏
      #define makeptr(cast, ptr, addvalue) (cast)((dword)(ptr)+(dword)(addvalue))
      // 定義了hookfuncdesc結(jié)構(gòu),我們用這個結(jié)構(gòu)作為參數(shù)傳給hookimportfunction函數(shù)
      typedef struct tag_hookfuncdesc
      {
      lpcstr szfunc; // the name of the function to hook.
      proc pproc; // the procedure to blast in.
      } hookfuncdesc , * lphookfuncdesc;
      // 這個函數(shù)監(jiān)測當(dāng)前系統(tǒng)是否是windownt
      bool isnt();
      // 這個函數(shù)得到hmodule -- 即我們需要截獲的函數(shù)所在的dll模塊的引入描述符(import descriptor)
      pimage_import_descriptor getnamedimportdescriptor(hmodule hmodule, lpcstr szimportmodule);
      // 我們的主函數(shù)
      bool hookimportfunction(hmodule hmodule, lpcstr szimportmodule,
      lphookfuncdesc pahookfunc, proc* paorigfuncs)
      {
      /////////////////////// 下面的代碼檢測參數(shù)的有效性 ////////////////////////////
      _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;
      }
      //////////////////////////////////////////////////////////////////////////////
      // 監(jiān)測當(dāng)前模塊是否是在2gb虛擬內(nèi)存空間之上
      // 這部分的地址內(nèi)存是屬于win32進程共享的
      if (!isnt() && ((dword)hmodule >= 0x80000000))
      {
      _assert(false);
      setlasterrorex(error_invalid_handle, sle_error);
      return false;
      }
      // 清零
      if (paorigfuncs) memset(paorigfuncs, null, sizeof(proc));
      // 調(diào)用getnamedimportdescriptor()函數(shù),來得到hmodule -- 即我們需要
      // 截獲的函數(shù)所在的dll模塊的引入描述符(import descriptor)
      pimage_import_descriptor pimportdesc = getnamedimportdescriptor(hmodule, szimportmodule);
      if (pimportdesc == null)
      return false; // 若為空,則模塊未被當(dāng)前進程所引入
      // 從dll模塊中得到原始的thunk信息,因為pimportdesc->firstthunk數(shù)組中的原始信息已經(jīng)
      // 在應(yīng)用程序引入該dll時覆蓋上了所有的引入信息,所以我們需要通過取得pimportdesc->originalfirstthunk
      // 指針來訪問引入函數(shù)名等信息
      pimage_thunk_data porigthunk = makeptr(pimage_thunk_data, hmodule,
      pimportdesc->originalfirstthunk);
      // 從pimportdesc->firstthunk得到image_thunk_data數(shù)組的指針,由于這里在dll被引入時已經(jīng)填充了
      // 所有的引入信息,所以真正的截獲實際上正是在這里進行的
      pimage_thunk_data prealthunk = makeptr(pimage_thunk_data, hmodule, pimportdesc->firstthunk);
      // 窮舉image_thunk_data數(shù)組,尋找我們需要截獲的函數(shù),這是最關(guān)鍵的部分!
      while (porigthunk->u1.function)
      {
      // 只尋找那些按函數(shù)名而不是序號引入的函數(shù)
      if (image_ordinal_flag != (porigthunk->u1.ordinal & image_ordinal_flag))
      {
      // 得到引入函數(shù)的函數(shù)名
      pimage_import_by_name pbyname = makeptr(pimage_import_by_name, hmodule,
      porigthunk->u1.addressofdata);
      // 如果函數(shù)名以null開始,跳過,繼續(xù)下一個函數(shù)
      if ( \0 == pbyname->name[0])
      continue;
      // bdohook用來檢查是否截獲成功
      bool bdohook = false;
      // 檢查是否當(dāng)前函數(shù)是我們需要截獲的函數(shù)
      if ((pahookfunc.szfunc[0] == pbyname->name[0]) &&
      (strcmpi(pahookfunc.szfunc, (char*)pbyname->name) == 0))
      {
      // 找到了!
      if (pahookfunc.pproc)
      bdohook = true;
      }
      if (bdohook)
      {
      // 我們已經(jīng)找到了所要截獲的函數(shù),那么就開始動手吧
      // 首先要做的是改變這一塊虛擬內(nèi)存的內(nèi)存保護狀態(tài),讓我們可以自由存取
      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));
      // 保存我們所要截獲的函數(shù)的正確跳轉(zhuǎn)地址
      if (paorigfuncs)
      paorigfuncs = (proc)prealthunk->u1.function;
      // 將image_thunk_data數(shù)組中的函數(shù)跳轉(zhuǎn)地址改寫為我們自己的函數(shù)地址!
      // 以后所有進程對這個系統(tǒng)函數(shù)的所有調(diào)用都將成為對我們自己編寫的函數(shù)的調(diào)用
      prealthunk->u1.function = (pdword)pahookfunc.pproc;
      // 操作完畢!將這一塊虛擬內(nèi)存改回原來的保護狀態(tài)
      dword dwoldprotect;
      _assert(virtualprotect(mbi_thunk.baseaddress, mbi_thunk.regionsize,
      mbi_thunk.protect, &dwoldprotect));
      setlasterror(error_success);
      return true;
      }
      }
      // 訪問image_thunk_data數(shù)組中的下一個元素
      porigthunk++;
      prealthunk++;
      }
      return true;
      }
      // getnamedimportdescriptor函數(shù)的實現(xiàn)
      pimage_import_descriptor getnamedimportdescriptor(hmodule hmodule, lpcstr szimportmodule)
      {
      // 檢測參數(shù)
      _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數(shù)組尋找我們需要截獲的函數(shù)所在的模塊
      while (pimportdesc->name)
      {
      pstr szcurrmod = makeptr(pstr, pdosheader, pimportdesc->name);
      if (stricmp(szcurrmod, szimportmodule) == 0)
      break; // 找到!中斷循環(huán)
      // 下一個元素
      pimportdesc++;
      }
      // 如果沒有找到,說明我們尋找的模塊沒有被當(dāng)前的進程所引入!
      if (pimportdesc->name == null)
      return null;
      // 返回函數(shù)所找到的模塊描述符(import descriptor)
      return pimportdesc;
      }
      // isnt()函數(shù)的實現(xiàn)
      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 //////////////////////////////////////////////////////////////////////
      不 知道在這篇文章問世之前,有多少朋友嘗試過去實現(xiàn)"鼠標(biāo)屏幕取詞"這項充滿了挑戰(zhàn)的技術(shù),也只有嘗試過的朋友才能體會到其間的不易,尤其在探索api函數(shù) 的截獲時,手頭的幾篇資料沒有一篇是涉及到關(guān)鍵代碼的,重要的地方都是一筆代過,msdn更是顯得蒼白而無力,也不知道除了 image_import_descriptor和image_thunk_data,微軟還隱藏了多少秘密,好在硬著頭皮還是把它給攻克了,希望這篇文 章對大家能有所幫助。

      主站蜘蛛池模板: 日本高清免费不卡视频| 亚洲色婷婷综合久久| 国产欧美亚洲精品第一页在线| 人人妻人人添人人爽日韩欧美 | 重口SM一区二区三区视频| 国内精品伊人久久久久777| 成人av一区二区亚洲精| 日韩少妇人妻vs中文字幕| 亚洲精品国产自在久久| 九九热在线视频精品免费| 亚洲另类激情专区小说图片| 中文字幕va一区二区三区| 国产精品夫妇激情啪发布| 亚洲欧美人成网站在线观看看| 玩弄放荡人妻少妇系列| 国产二区三区不卡免费| 亚洲精品专区永久免费区| 日韩福利视频导航| 亚洲欧美人成网站在线观看看| 激情综合网激情激情五月天| 成人国产乱对白在线观看 | 伊人中文在线最新版天堂| 欧美成人VA免费大片视频| 色偷偷www.8888在线观看| av新版天堂在线观看| 青青草原网站在线观看| 国产精品多p对白交换绿帽| 久久日产一线二线三线| 久久精品免费观看国产| 国产福利酱国产一区二区| 激情综合网激情五月俺也去 | 亚洲av日韩av永久无码电影| 精品人妻一区二区| 欧美人成在线播放网站免费| 女同在线观看亚洲国产精品| 亚洲欧美日韩综合久久久| 固阳县| 女同精品女同系列在线观看| 国产精品午夜福利91| 奶头好大揉着好爽视频| 老司机精品成人无码AV|