螢幕取詞技術實現原理與關鍵原始碼

hemeinvyiqiluoben發表於2018-01-17

轉自: https://www.cnblogs.com/seacryfly/archive/2012/01/08/2316511.html


雖然螢幕取詞技術早已經不是什麼祕密,以至於除了漢化工具、翻譯工具、中文平臺等等這些東西之外,連像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落潮而成為這個時代的終篇之一。

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

 

在金山詞霸中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;//設定取詞的屬性 gp.GrabEnabled = true;//是否取詞的屬性 gp.AdviseGrab(this); } //介面的實現 int IXDictGrabSink.QueryWord(string WordString, int lCursorX, int lCursorY, string SentenceString, ref int lLoc, ref int lStart) { this.textBox1.Text = SentenceString;//滑鼠所在語句 //this.textBox1.Text = SentenceString.Substring(lLoc + 1,1);//滑鼠所在字元 return 1; } } }

B.Nhw32.dll法

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

nhw32.dll 主要引出兩個函式:

1. DWORD WINAPI BL_SetFlag32(UINT nFlag, HWND hNotifyWnd, int MouseX, 
int MouseY) 功能: 啟動或停止取詞。 引數: nFlag [輸入] 指定下列值之一: 
GETWORD_ENABLE: 開始取詞。在重畫被取單詞區域前設定此標誌。nhw32.dll是通過 重畫單詞區域,擷取TextOutA, TextOutW, ExtTextOutA, ExtTextOutW等Windows API函式的引數來取詞的。 
GETWORD_DISABLE: 停止取詞。 hNotifyWnd [輸入] 通知視窗控制程式碼。當取到此時,向該通知視窗傳送一登記訊息:GWMSG_GETWORDOK。 MouseX [輸入] 指定取詞點的X座標。 
MouseY [輸入] 指定取詞點的Y座標。 返回值: 可忽略。 2. DWORD WINAPI BL_GetText32(LPSTR lpszCurWord, int nBufferSize, LPRECT lpWordRect) 
功能: 
從內部緩衝區取出單詞文字串。對英語文字,該函式最長取出一行內以空格為界的三個英文單詞串,遇空格,非英文字母及除‘-’外的標點符號,則終止取詞。對漢字文字,該函式最長取出一行漢字串,遇英語字母,標點符號等非漢語字元,則終止取詞。該函式不能同時取出英語和漢語字元。 
引數: lpszCurWord [輸入] 目的緩衝區指標。 nBufferSize [輸入] 目的緩衝區大小。 
lpWordRect [輸出] 指向 RECT 結構的指標。該結構定義了被取單詞所在矩形區域。 返回值: 
當前游標在全部詞中的位置。

此外,WinNT/2000版 nhw32.dll 還引出另兩個函式:

1. BOOL WINAPI SetNHW32() 功能: Win NT/2000 環境下的初始化函式。一般在程式開始時,呼叫一次。 
引數: 無。 返回值: 如果成功 TRUE ,失敗 FALSE 。

2. BOOL WINAPI ResetNHW32() 功能: Win NT/2000 環境下的去初始化函式。一般在程式結束時呼叫。 
引數: 無。 返回值: 如果成功 TRUE ,失敗 FALSE 。

 

 

"滑鼠螢幕取詞"技術是在電子字典中得到廣泛地應用的,如四通利方和金山詞霸等軟體,這個技術看似簡單,其實在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 != \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,微軟還隱藏了多少祕密,好在硬著頭皮還是把它給攻克了,希望這篇文章對大家能有所幫助。

文章引自: http://www.cnblogs.com/qiubole/articles/977764.html


相關文章