滑鼠螢幕取詞技術的原理和實現 (轉)

worldblog發表於2007-12-25
滑鼠螢幕取詞技術的原理和實現 (轉)[@more@]

 滑鼠螢幕取詞技術的原理和實現 
  白瑜

  “滑鼠螢幕取詞”技術是在電子字典中得到廣泛地應用的,如四通利方和金山詞霸等,這個技術看似簡單,其實在中實現卻是非常複雜的,總的來說有兩種實現方式:
  第一種:採用截獲對部分GDI的來實現,如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之間的地址空間禁止程式訪問,而在中,2GB到4GB這部分虛擬地址空間實際上是由所有的程式所共享的,這部分地址空間載入了共享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

// 這裡定義了一個產生指標的宏
#define MakePtr(cast, ptr, AddValue) (cast)((D)(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 _DE
 if (paOrigFuncs) _ASSERT(!IsBadWritePtr(paOrigFuncs, sizeof(PROC)));
 _ASSERT(paHookFunc.szFunc);
 _ASSERT(*paHookFunc.szFunc != ');
  _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 (' == 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.dwPlatfod);
}
/////////////////////////////////////////////// End //////////////////////////////////////////////////////////////////////

  不知道在這篇文章問世之前,有多少朋友嘗試過去實現“滑鼠螢幕取詞”這項充滿了挑戰的技術,也只有嘗試過的朋友才能體會到其間的不易,尤其在探索API函式的截獲時,手頭的幾篇資料沒有一篇是涉及到關鍵程式碼的,重要的地方都是一筆代過,MSDN更是顯得蒼白而無力,也不知道除了IMAGE_IMPORT_DESCRIPTOR和IMAGE_THUNK_DATA,微軟還隱藏了多少秘密,好在硬著頭皮還是把它給攻克了,希望這篇文章對大家能有所幫助。

 

 


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752043/viewspace-995714/,如需轉載,請註明出處,否則將追究法律責任。

相關文章