導航:首頁 > 源碼編譯 > 屏幕取詞技術源碼

屏幕取詞技術源碼

發布時間:2022-08-06 08:43:32

1. 屏幕取詞

「滑鼠屏幕取詞」技術是在電子字典中得到廣泛地應用的,如四通利方和金山詞霸等軟體,這個技術看似簡單,其實在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 hmole=GetMoleHandle(「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 procere to blast in.
} HOOKFUNCDESC , * LPHOOKFUNCDESC;

// 這個函數監測當前系統是否是WindowNT
BOOL IsNT();

// 這個函數得到hMole -- 即我們需要截獲的函數所在的DLL模塊的引入描述符(import descriptor)
PIMAGE_IMPORT_DESCRIPTOR GetNamedImportDescriptor(HMODULE hMole, LPCSTR szImportMole);

// 我們的主函數
BOOL HookImportFunction(HMODULE hMole, LPCSTR szImportMole,
LPHOOKFUNCDESC paHookFunc, PROC* paOrigFuncs)
{
/////////////////////// 下面的代碼檢測參數的有效性 ////////////////////////////
_ASSERT(szImportMole);
_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 ((szImportMole == NULL) || (IsBadReadPtr(paHookFunc, sizeof(HOOKFUNCDESC))))
{
_ASSERT(FALSE);
SetLastErrorEx(ERROR_INVALID_PARAMETER, SLE_ERROR);
return FALSE;
}
//////////////////////////////////////////////////////////////////////////////

// 監測當前模塊是否是在2GB虛擬內存空間之上
// 這部分的地址內存是屬於Win32進程共享的
if (!IsNT() && ((DWORD)hMole >= 0x80000000))
{
_ASSERT(FALSE);
SetLastErrorEx(ERROR_INVALID_HANDLE, SLE_ERROR);
return FALSE;
}
// 清零
if (paOrigFuncs) memset(paOrigFuncs, NULL, sizeof(PROC));

// 調用GetNamedImportDescriptor()函數,來得到hMole -- 即我們需要
// 截獲的函數所在的DLL模塊的引入描述符(import descriptor)
PIMAGE_IMPORT_DESCRIPTOR pImportDesc = GetNamedImportDescriptor(hMole, szImportMole);
if (pImportDesc == NULL)
return FALSE; // 若為空,則模塊未被當前進程所引入

// 從DLL模塊中得到原始的THUNK信息,因為pImportDesc->FirstThunk數組中的原始信息已經
// 在應用程序引入該DLL時覆蓋上了所有的引入信息,所以我們需要通過取得pImportDesc->OriginalFirstThunk
// 指針來訪問引入函數名等信息
PIMAGE_THUNK_DATA pOrigThunk = MakePtr(PIMAGE_THUNK_DATA, hMole,
pImportDesc->OriginalFirstThunk);

// 從pImportDesc->FirstThunk得到IMAGE_THUNK_DATA數組的指針,由於這里在DLL被引入時已經填充了
// 所有的引入信息,所以真正的截獲實際上正是在這里進行的
PIMAGE_THUNK_DATA pRealThunk = MakePtr(PIMAGE_THUNK_DATA, hMole, 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, hMole,
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 hMole, LPCSTR szImportMole)
{
// 檢測參數
_ASSERT(szImportMole);
_ASSERT(hMole);
if ((szImportMole == NULL) || (hMole == NULL))
{
_ASSERT(FALSE);
SetLastErrorEx(ERROR_INVALID_PARAMETER, SLE_ERROR);
return NULL;
}

// 得到Dos文件頭
PIMAGE_DOS_HEADER pDOSHeader = (PIMAGE_DOS_HEADER) hMole;

// 檢測是否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, szImportMole) == 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,微軟還隱藏了多少秘密,好在硬著頭皮還是把它給攻克了,希望這篇文章對大家能有所幫助。

2. 有開源的java屏幕取詞程序嗎

這樣問得確不是很正確,語言沒有開不開源的說法,語言只是提供給大家一種寫代碼要遵循的語法,當然這些語法肯定是公開的,不然大家怎麼學呀。
至於用java這個語法寫的代碼是否是開源的,那每個項目都不同,有開源的也有商業的;
如果你想問的是java的基本類庫是否開源,那答案是是的,你安裝完jdk後目錄下會有src.zip,裡面就是源代碼;

3. 拜求一個VC++/MFC編寫的屏幕取詞的程序,就是像金山詞霸取詞的那種~~用HOOK寫的

1.用detour實現hook。非常的簡單依樣畫葫蘆就可以劫持本進程的api
http://blog.csdn.net/evi10r/article/details/6659354
2.windows核心編程那本書里有進程注入,你可以下載源碼,詳見inject和eject的例子,可以把一個dll注入進一個進程

以前做過的,就這樣就行。但是現在那個工程沒了= =。你hook textout系列函數,然後將消息發回伺服器即可,加油。

4. IE窗口中的文字是哪個API繪出來的

這里假設讀者了解一點點屏幕取詞的技術背景。屏幕取詞一般都是Hook了ExtTextOut、ExtTextOut函數,然後對屏幕指定部分進行InvalidateRect,就會得屏幕要更新的內容,然後根據屏幕坐標計算滑鼠指的詞。IE的屏幕取詞稍稍不同。通過調試,你會發現對IE屏幕取詞時,GetDCOrgEx返回DC的坐標原點都是0,0。郁悶。其實細想起來,DC是分2種的,還有一種是內存DC,應用程序為了提高繪制效率,減少屏幕閃動,都會使用內存DC做緩存,先在內存DC中繪制內容,然後到屏幕上。做個試驗,對IE通過ExtTextOut獲得的DC使用WindowFromDC,哈哈,返回是NULL,說明這個DC是個內存DC。找到問題的根源了,現在還需要多Hook一個API,就是BitBlt。IE就是使用BitBlt把內存DC中的內容,到屏幕上的。程序的流程大概是這樣的,先從ExtTextOut獲得的DC計算每個詞的相對RECT,存入一個隊列中,再在BitBlt中通過GetDCOrgEx獲得實際目標DC的原點坐標,計算每個詞的相對屏幕的RECT,剩下的就是判斷滑鼠在哪個RECT中了。似乎麻煩了點,沒辦法啊。誰讓國內用戶的口味高呢-_-

5. 金山詞霸的屏幕取詞是什麼原理

一.基礎知識
首先想編這種程序需要一些基礎知識。
會用Vc++,包括16/32位。
精通Windows API特別是GDI,KERNEL部分。
懂匯編語言,會用softice調試程序,因為這種程序最好用softice調試。
二.基本原理
在Window 3.x時代,windows系統提供的字元輸出函數只有很少的幾個。
TextOut
ExtTextOut
DrawText
......
其中DrawText最終是用ExtTextOut實現的。
所以Windows的所有字元輸出都是由調用TextOut和ExtTextOut實現的。因此,如果你可以修改這兩個函數的入口,讓程序先調用你自己的一個函數再調用系統的字元輸出,你就可以得到Windows所有輸出的字元了。
到了Windows95時代,原理基本沒變,但是95比3.x要復雜。開始的時候,一些在windows3.x下編寫的取詞軟體仍然可以是使用。但是後來出了個IE4,結果很多詞典軟體就因為不支持IE4而被淘汰了,但同時也給一些軟體創造了機會,如金山詞霸。其實IE4的問題並不復雜,只不過它的輸出的是unicode字元,是用TextOutW和ExtTextOutW輸出的。知道了這一點,只要也截取就可以了。不過實現方法復雜一點,以後會有詳細講解。現在又出了個IE5,結果詞霸也不好用了,微軟真是#^@#$%$*&^&#@#@..........
我研究後找到了一種解決辦法,但還有些問題,有時會取錯,正在繼續研究,希望大家共同探討。
另外還有WindowsNT,原理也是一樣,只是實現方法和95下完全不同。
三.技術要點
要實現取詞,主要要解決以下技術問題。
1.截取API入口,獲得API的參數。
2.安全地潛入Windows內部,良好地兼容Windows的各個版本
3.計算滑鼠所在的單詞和字母。
4.如果你在Window95下,做32位程序,還涉及Windows32/16混合編程的技術。
今天先到這里吧!最好准備一份softice for 95/98和金山詞霸,讓我們先來分析一下別人是怎麼做的。
歡迎與我聯系
E-Mail:[email protected]
主題 屏幕取詞技術系列講座(二)
作者 亦東
很抱歉讓大家久等了!
我看了一些人的回帖,發現很多人對取詞的原理還是不太清楚。
首先我來解釋一下hook問題。詞霸中的確用到了hook,而且他用了兩種hook其中一種是Windows標准hook,通過SetWindowHook安裝一個回調函數,它安裝了一個滑鼠hook,是為了可以及時響應滑鼠的消息用的和取詞沒太大關系。
另一種鉤子是API鉤子,這才是取詞的核心技術所在。他在TextOut等函數的開頭寫了一個jmp語句,跳轉到自己的代碼里。
你用softice看不到這個跳轉語句是因為它只在取詞的一瞬間才存在,平時是沒有的。
你可以在TextOut開頭設一個讀寫斷點
bpm textout
再取詞,就會找到詞霸用來寫鉤子的代碼了。
/**********************************
所以我在次強調,想學這種技術一定要懂匯編語言和熟練使用softice.
**********************************/
至於從cjktl95中mp出來的未公開函數是和Windows32/16混合編程有關的,以後我會提到他們。
我先來講述取詞的過程,
0 判斷滑鼠是否在一個地方停留了一段時間
1 取得滑鼠當前位置
2 以滑鼠位置為中心生成一個矩形
3 掛上API鉤子
4 讓這個矩形產生重畫消息
5 在鉤子里等輸出字元
6 計算滑鼠在哪個單詞上面,把這個單詞保存下來
7 如果得到單詞則摘掉API鉤子,在一段時間後,無論是否得到單詞都摘掉API鉤子
8 用單詞查詞庫,顯示解釋框。
很多步驟實現起來都有一些難度,所以在中國可以做一個完善的取詞詞典的人屈指可數。
其中0,1,2,7,8比較簡單就不提了。
先說如何掛鉤子:
所謂鉤子其實就是在WindowsAPI入口寫一個JMP XXXX:XXXX語句,跳轉到自己的代碼里。
步驟如下:
1.取得Windows API入口,用GetProcAddress實現
2.保存API入口的前五個位元組,因為JMP是0xEA,地址是4個位元組
3.寫入跳轉語句
這步最復雜
Windows的代碼段本來是不可以寫的,但是Microsoft給自己留了個後門。
有一個未公開函數是AllocCsToDsAlias,
UINT WINAPI ALLOCCSTODSALIAS(UINT);
你可以取到這個函數的入口,把API的代碼段的選擇符(要是不知道什麼是選擇符,就先去學學保護模式編程吧)傳給他,他會返回一個可寫的數據段選擇符。這個選擇符用完要釋放的。用新選擇符和API入口的偏移量合成一個指針就可以寫windows的代碼段了。
這就是取詞技術的最核心的東東,不止取詞,連外掛中文平台全屏漢化都是使用的這種技術。現在知道為什麼這么簡單的幾句話卻很少知道了吧?因為太多的產品使用他,太多的公司靠他賺錢了。
這些公司和產品有:中文之星,四通利方,南極星,金山詞霸,實達銘泰的東方快車,roboword,譯典通,即時漢化專家等等等等。。。。還有至少20多家小公司。他們的具體實現雖然不同,但大致原理是相同的。
我這些都是隨手寫的,也沒有提綱之類的東西,以後如果有機會我會整理一下,大家先湊合著看吧!xixi...
?
主題 關於屏幕取詞的討論(三)
作者 亦東

讓大家久等,很抱歉,前些時候工作忙硬碟又壞了,太不幸了。
這回來點真格的。
咱們以截取TextOut為例。
下面是代碼:
//截取TextOut
typedef UINT (WINAPI* ALLOCCSTODSALIAS)(UINT);
ALLOCCSTODSALIAS AllocCsToDsAlias;
BYTE NewValue[5];//保存新的入口代碼
BYTE OldValue[5];//API原來的入口代碼
unsigned char * Address=NULL;//可寫的API入口地址
UINT DsSelector=NULL;//指向API入口的可寫的選擇符
WORD OffSetEntry=NULL;//API的偏移量
BOOL bHookAlready = FALSE; //是否掛鉤子的標志
BOOL InitHook()
{
HMODULE hKernel,hGdi;
hKernel = GetMoleHandle("Kernel");
if(hKernel==NULL)
return FALSE;
AllocCsToDsAlias = (ALLOCCSTODSALIAS)GetProcAddress(hKernel,"AllocCsToDsAlias");//這是未公開的API所以要這樣取地址
if(AllocCsToDsAlias==NULL)
return FALSE;
hGdi = GetMoleHandle("Gdi");
if(hmGdi==NULL)
return FALSE;
FARPROC Entry = GetProcAddress(hGdi,"TextOut");
if(Entry==NULL)
return FALSE;
OffSetEntry = (WORD)(FP_OFF(Entry));//取得API代碼段的選擇符
DsSelector = AllocCsToDsAlias(FP_SEG(Entry));//分配一個等同的可寫的選擇符
Address = (unsigned char*)MK_FP(DsSelector,OffSetEntry);//合成地址
NewValue[0]=0xEA;
*((DWORD*)(NewValue+1)) = (DWORD)MyTextOut;
OldValue[0]=Address[0];
*((DWORD*)(OldValue+1)) = *((DWORD*)(Address+1));
}
BOOL ClearHook()
{
if(bHookAlready)
HookOff();
FreeSelector(DsSelector);
}
BOOL HookOn()
{
if(!bHookAlready){
for(int i=0;i<5;i++){
Address[i]=NewValue[i];
}
bHookAlready=TRUE;
}
}
BOOL HookOff()
{
if(bHookAlready){
for(int i=0;i<5;i++){
Address[i]=OldValue[i];
}
bHookAlready=FALSE;
}
}
//鉤子函數,一定要和API有相同的參數和聲明
BOOL WINAPI MyTextOut(HDC hdc,int nXStart,int nYStart,LPCSTR lpszString,UINT cbString)
{
BOOL ret;
HookOff();
ret = TextOut(hdc,nXStart,nYStart,lpszString,cbString);//調原來的TextOut
HookOn();
return ret;
}
上面的代碼是一個最簡單的掛API鉤子的例子,我要提醒大家的是,這段代碼是我憑記憶寫的,我以前的代碼丟了,我沒有編譯測試過
因為我沒有VC++1.52.所以代碼可能會有錯。
建議使用Borland c++,按16位編譯。
如果用VC++1.52,則要改個選項
在VC++1.52的Option里,有個內存模式的設置,選大模式,和"DS!=SS DS Load on Function entry.",切記,否則會系統崩潰。

6. 如何用VC(也可以使用VC.NET或C#)實現金山詞霸那樣的取詞功能

屏幕取詞技術揭密(討論稿)
?
主題 屏幕取詞技術系列講座(一)
作者 亦東
很多人對這個問題感興趣。
原因是這項技術讓人感覺很神奇,也很有商業價值。
現在詞典市場金山詞霸佔了絕對優勢,所以再做字典也沒什麼前途了。我就是這么認為的,所以我雖然掌握了這項技術,卻沒去做字典軟體。只做了一個和詞霸相似的軟體自己用,本來想拿出來做共享軟體,但我的詞庫是「偷」來的,而且詞彙不多,所以也就算了,詞庫太小,只能取詞有什麼用呢?而且詞霸有共享版的。
但既然很多人想了解這項技術,我也不會保留。我准備分多次講述這項技術的所有細節。
大約每周一兩次。想知道的人就常常來看看吧!
一.基礎知識
首先想編這種程序需要一些基礎知識。
會用Vc++,包括16/32位。
精通Windows API特別是GDI,KERNEL部分。
懂匯編語言,會用softice調試程序,因為這種程序最好用softice調試。
二.基本原理
在Window 3.x時代,windows系統提供的字元輸出函數只有很少的幾個。
TextOut
ExtTextOut
DrawText
......
其中DrawText最終是用ExtTextOut實現的。
所以Windows的所有字元輸出都是由調用TextOut和ExtTextOut實現的。因此,如果你可以修改這兩個函數的入口,讓程序先調用你自己的一個函數再調用系統的字元輸出,你就可以得到Windows所有輸出的字元了。
到了Windows95時代,原理基本沒變,但是95比3.x要復雜。開始的時候,一些在windows3.x下編寫的取詞軟體仍然可以是使用。但是後來出了個IE4,結果很多詞典軟體就因為不支持IE4而被淘汰了,但同時也給一些軟體創造了機會,如金山詞霸。其實IE4的問題並不復雜,只不過它的輸出的是unicode字元,是用TextOutW和ExtTextOutW輸出的。知道了這一點,只要也截取就可以了。不過實現方法復雜一點,以後會有詳細講解。現在又出了個IE5,結果詞霸也不好用了,微軟真是#^@#$%$*&^&#@#@..........
我研究後找到了一種解決辦法,但還有些問題,有時會取錯,正在繼續研究,希望大家共同探討。
另外還有WindowsNT,原理也是一樣,只是實現方法和95下完全不同。
三.技術要點
要實現取詞,主要要解決以下技術問題。
1.截取API入口,獲得API的參數。
2.安全地潛入Windows內部,良好地兼容Windows的各個版本
3.計算滑鼠所在的單詞和字母。
4.如果你在Window95下,做32位程序,還涉及Windows32/16混合編程的技術。
今天先到這里吧!最好准備一份softice for 95/98和金山詞霸,讓我們先來分析一下別人是怎麼做的。
歡迎與我聯系
E-Mail:[email protected]
主題 屏幕取詞技術系列講座(二)
作者 亦東
很抱歉讓大家久等了!
我看了一些人的回帖,發現很多人對取詞的原理還是不太清楚。
首先我來解釋一下hook問題。詞霸中的確用到了hook,而且他用了兩種hook其中一種是Windows標准hook,通過SetWindowHook安裝一個回調函數,它安裝了一個滑鼠hook,是為了可以及時響應滑鼠的消息用的和取詞沒太大關系。
另一種鉤子是API鉤子,這才是取詞的核心技術所在。他在TextOut等函數的開頭寫了一個jmp語句,跳轉到自己的代碼里。
你用softice看不到這個跳轉語句是因為它只在取詞的一瞬間才存在,平時是沒有的。
你可以在TextOut開頭設一個讀寫斷點
bpm textout
再取詞,就會找到詞霸用來寫鉤子的代碼了。
/**********************************
所以我在次強調,想學這種技術一定要懂匯編語言和熟練使用softice.
**********************************/
至於從cjktl95中mp出來的未公開函數是和Windows32/16混合編程有關的,以後我會提到他們。
我先來講述取詞的過程,
0 判斷滑鼠是否在一個地方停留了一段時間
1 取得滑鼠當前位置
2 以滑鼠位置為中心生成一個矩形
3 掛上API鉤子
4 讓這個矩形產生重畫消息
5 在鉤子里等輸出字元
6 計算滑鼠在哪個單詞上面,把這個單詞保存下來
7 如果得到單詞則摘掉API鉤子,在一段時間後,無論是否得到單詞都摘掉API鉤子
8 用單詞查詞庫,顯示解釋框。
很多步驟實現起來都有一些難度,所以在中國可以做一個完善的取詞詞典的人屈指可數。
其中0,1,2,7,8比較簡單就不提了。
先說如何掛鉤子:
所謂鉤子其實就是在WindowsAPI入口寫一個JMP XXXX:XXXX語句,跳轉到自己的代碼里。
步驟如下:
1.取得Windows API入口,用GetProcAddress實現
2.保存API入口的前五個位元組,因為JMP是0xEA,地址是4個位元組
3.寫入跳轉語句
這步最復雜
Windows的代碼段本來是不可以寫的,但是Microsoft給自己留了個後門。
有一個未公開函數是AllocCsToDsAlias,
UINT WINAPI ALLOCCSTODSALIAS(UINT);
你可以取到這個函數的入口,把API的代碼段的選擇符(要是不知道什麼是選擇符,就先去學學保護模式編程吧)傳給他,他會返回一個可寫的數據段選擇符。這個選擇符用完要釋放的。用新選擇符和API入口的偏移量合成一個指針就可以寫windows的代碼段了。
這就是取詞技術的最核心的東東,不止取詞,連外掛中文平台全屏漢化都是使用的這種技術。現在知道為什麼這么簡單的幾句話卻很少知道了吧?因為太多的產品使用他,太多的公司靠他賺錢了。
這些公司和產品有:中文之星,四通利方,南極星,金山詞霸,實達銘泰的東方快車,roboword,譯典通,即時漢化專家等等等等。。。。還有至少20多家小公司。他們的具體實現雖然不同,但大致原理是相同的。
我這些都是隨手寫的,也沒有提綱之類的東西,以後如果有機會我會整理一下,大家先湊合著看吧!xixi...
?
主題 關於屏幕取詞的討論(三)
作者 亦東

讓大家久等,很抱歉,前些時候工作忙硬碟又壞了,太不幸了。
這回來點真格的。
咱們以截取TextOut為例。
下面是代碼:
//截取TextOut
typedef UINT (WINAPI* ALLOCCSTODSALIAS)(UINT);
ALLOCCSTODSALIAS AllocCsToDsAlias;
BYTE NewValue[5];//保存新的入口代碼
BYTE OldValue[5];//API原來的入口代碼
unsigned char * Address=NULL;//可寫的API入口地址
UINT DsSelector=NULL;//指向API入口的可寫的選擇符
WORD OffSetEntry=NULL;//API的偏移量
BOOL bHookAlready = FALSE; //是否掛鉤子的標志
BOOL InitHook()
{
HMODULE hKernel,hGdi;
hKernel = GetMoleHandle("Kernel");
if(hKernel==NULL)
return FALSE;
AllocCsToDsAlias = (ALLOCCSTODSALIAS)GetProcAddress(hKernel,"AllocCsToDsAlias");//這是未公開的API所以要這樣取地址
if(AllocCsToDsAlias==NULL)
return FALSE;
hGdi = GetMoleHandle("Gdi");
if(hmGdi==NULL)
return FALSE;
FARPROC Entry = GetProcAddress(hGdi,"TextOut");
if(Entry==NULL)
return FALSE;
OffSetEntry = (WORD)(FP_OFF(Entry));//取得API代碼段的選擇符
DsSelector = AllocCsToDsAlias(FP_SEG(Entry));//分配一個等同的可寫的選擇符
Address = (unsigned char*)MK_FP(DsSelector,OffSetEntry);//合成地址
NewValue[0]=0xEA;
*((DWORD*)(NewValue+1)) = (DWORD)MyTextOut;
OldValue[0]=Address[0];
*((DWORD*)(OldValue+1)) = *((DWORD*)(Address+1));
}
BOOL ClearHook()
{
if(bHookAlready)
HookOff();
FreeSelector(DsSelector);
}
BOOL HookOn()
{
if(!bHookAlready){
for(int i=0;i<5;i++){
Address[i]=NewValue[i];
}
bHookAlready=TRUE;
}
}
BOOL HookOff()
{
if(bHookAlready){
for(int i=0;i<5;i++){
Address[i]=OldValue[i];
}
bHookAlready=FALSE;
}
}
//鉤子函數,一定要和API有相同的參數和聲明
BOOL WINAPI MyTextOut(HDC hdc,int nXStart,int nYStart,LPCSTR lpszString,UINT cbString)
{
BOOL ret;
HookOff();
ret = TextOut(hdc,nXStart,nYStart,lpszString,cbString);//調原來的TextOut
HookOn();
return ret;
}
上面的代碼是一個最簡單的掛API鉤子的例子,我要提醒大家的是,這段代碼是我憑記憶寫的,我以前的代碼丟了,我沒有編譯測試過
因為我沒有VC++1.52.所以代碼可能會有錯。
建議使用Borland c++,按16位編譯。
如果用VC++1.52,則要改個選項
在VC++1.52的Option里,有個內存模式的設置,選大模式,和"DS!=SS DS Load on Function entry.",切記,否則會系統崩潰。
有什麼不明白的可以給我寫信
[email protected]
屏幕取詞核心內幕屏幕取詞核心內幕
本文只對與幾個關鍵性技術的實現細節進行討論,其它的編程細節,請參考源程序。
32位到16位的形式替換
32位代碼與16位代碼的數據交換
動態修改Windows內核
1. 32bit到16bit的形式替換(Thunk)
形式替換是指那些允許從16位代碼調用32位代碼或從32位代碼調用16位代碼的技術。形式替換用於解決試圖在同一操作系統或同一可執行程序上使16位代碼與32位代碼同時並存的問題,即16位代碼與32位代碼的混合編程技術。早期的DOS程序及Window3.x上的應用程序均為16位程序,Windows95及Windows NT雖然也可運行舊的16位程序,但它們的主流發展方向是32位應用程序。與Windows NT不同的是,Windows95不是一個「純」32位操作系統,為了兼有令人滿意的速度和與舊的16位程序的良好兼容性,其內核本身就是一個16位與32位的混合體,因此也為編程者使用形式替換提供了便利。Microsoft為編寫形式替換程序提供了通用的介面及工具,但因LTW32中的形式替換並不復雜,所以使用了一些編程技巧,而避免了使用Microsoft復雜的開發工具。
形式替換中最主要的問題有兩點:①16位與32位數據類型尺寸的變化,如16位代碼到32位代碼的一個重要變化是整型數int的長度加倍了 ;②是堆棧操作時,16位模式使用SS:SP堆棧指針控制棧頂,而在32位模式中使用ESP寄存器作堆棧指針控制棧頂。
在LTW32中,屏幕抓詞功能由16位DLL實現,因而只需實現32位到16位的形式替換,為32位代碼提供16位DLL的調用介面。CALL FWORD PTR是32位匯編代碼的一種調用方法,它可讓32位代碼調用到16位代碼。作為實現的關鍵,控制16位側與32位側各自的堆棧是編程的要點。
32位側的調用代碼:
_asm{
pusha
call fword ptr [func16bit] /* func16bit是16位被調用者的48位地址 */
popa
}
16位側的調用代碼:
int mmy;
static char stack[8192]; /* 16位代碼的臨時堆棧 */
static WORD stack_seg;
static WORD prev_seg;
static DWORD prev_ofs;
static WORD prev_ds;
_asm{
push ax;
push bx;
mov ax, ds;
mov bx, seg mmy;
mov ds, bx;
mov stack_seg, bx;
mov prev_ds, ax;
pop bx;
pop ax;
mov prev_seg, ss;
mov dword ptr prev_ofs, esp; /* 保存32位堆棧指針 */
mov ss, stack_seg;
mov sp, offset stack; /* 設置16位堆棧指針 */
add sp, 8192;
}
/* 此處加入16位代碼要實現的功能 */
_asm{
mov ss, prev_seg;
mov esp, dword ptr prev_ofs; /* 恢復32位堆棧指針 */
mov ds, prev_ds;
lea sp, word ptr [bp-2];
pop ds;
pop bp;
dec bp;
db 66h;
retf;
}
由於調用中傳遞的參數有限,所以涉及的代碼並不多,唯一比較復雜之處在16位側,它臨時轉向一個16位堆棧,服務於來自32位的調用者的請求。在16位側入口處,必須存放好32位調用者的32位堆棧的指針,並且在16位側返回時恢復它。由代碼可見,這個過程是不可重入的,即一次只支持一個調用者,由於調用者只有LTW32的32位側代碼,所以此限制可以滿足。另外,在16位代碼返回時,必須恢復32位調用者的48位堆棧指針(SS:ESP)而不是32位的CS:SP,而且必須用一個48位地址(16:32)遠返回(66h RETF)到它的32位調用者。
處理好這些細節,實現32位到16位的形式替換實際上是很簡單的。
2. 32位代碼與16位代碼的數據交換
32位代碼使用16位段地址加32位線性地址(16:32)的地址形式,16位代碼使用16位段選擇符(selector)加16位偏移(16:16)的地址形式。在Windows 95的32位側,段址28h是系統段,即通過CS=28h或DS=28h可以訪問系統的4G空間,其它應用程序的地址空間都是映射到28h段的4G空間中。Windows95中所有32位進程的地址空間(共4G)的高2G(80000000H~FFFFFFFFH)全部映射到28:80000000~28:FFFFFFFFH。即這塊區域是所有32位程序共享的。這里一般存放系統DLL、虛擬設備驅動程序(VxD)、內存映射文件、16位應用程序和16位全局堆等。最後一項很重要,這為32位代碼與16位代碼交換數據提供了一個簡便的方法。因為16位程序的段選擇符的基址即是其所映射的系統段中的線性地址,這樣,只要能夠得到這個線性地址,32位代碼就可以輕易地訪問到16位程序的數據(LTW32 的32位側使用此方法從16位側獲得屏幕截獲的信息)。而16位段選擇符的線性基址可以通過使用系統調用GetSelectorBase()得到,具體實現可參考源程序。線性地址計算的例子如下:
16位地址:07F2:1234H
段選擇符 07F2H的線性基址為:82F41300H
段選擇符 07F2H的尺寸為:4000H
∵ 82F41300H + 1234H = 82F42534
∴ 對應的32位線性地址為28:82F42534H
3. 動態修改Windows內核
如前所述,Windows95不是一個「純」32位操作系統,其內核模塊中的USER和GDI均是用16位代碼實現的。USER32.DLL和GDI32.DLL只是16位的USER.EXE和GDI.EXE的32位調用介面。因此,如果屏幕截獲程序用32位代碼實現,則只能截獲32位應用程序對USER32.DLL和GDI32.DLL的調用,無法截獲16位應用程序對USER.EXE和GDI.EXE的調用,所以如果想截獲所有應用程序(包括Windows95的桌面程序Explorer)中有關屏幕輸出的系統調用,則應該用16位代碼實現屏幕截獲功能。這就是LTW32為什麼不是「純」32位應用程序的原因。LTW32主要截獲兩個系統調用TextOut()和ExtTextOut(),方法很簡單,把這兩個函數的頭五個位元組修改為一個JMP FAR 指令,使得對這兩個函數的調用均轉向屏幕截獲程序。這就涉及到一個關鍵問題:動態修改Windows的代碼。
在傳統的DOS程序中,動態修改程序代碼無任何困難,但在Windows中則不然,因為在Windows中,代碼可被同一程序的多個實例(進程)共享,所以系統不允許應用程序動態的修改代碼。在16位側,內存的可讀、寫屬性是與段選擇符聯系在一起的。段選擇符基本上可分為兩類:數據段選擇符和代碼段選擇符。前者可讀、可寫、不可執行;後者可讀、可執行、不可寫。Windows提供了這兩類段選擇符相轉換的系統調用。未公開的16位系統調用AllocCStoDSAlias()為給定的代碼段選擇符分配一個具有相同線性基址和尺寸的數據段別名(DS Alias)。通過DS別名可以對給定的代碼段進行修改。AllocCStoDSAlias()的使用方法如下:
WORD (FAR PASCAL *AllocCStoDSAlias)(WORD);
AllocCStoDSAlias = GetProcAddress(
GetMoleHandle(「KERNEL」), 」ALLOCCSTODSALIAS」);
調用參數為給定的代碼選擇符,調用成功時返回一個線性基址和尺寸均與原代碼選擇符相同的DS別名。當不再使用此DS別名時,要用系統調用FreeSelector()把DS別名釋放掉。
使用上述技術,就可實現動態修改Windows代碼,從而改變GDI的系統調用TextOut()和ExtTextOut()的執行動作,實時地截獲屏幕輸出,為實現滑鼠隨動翻譯提供可能。
把上述的32位到16位的形式替換、32位代碼與16位代碼的數據交換、動態修改Windows內核等技術綜合應用在一起,配合單詞查找演算法和片語分析演算法就可以實現滑鼠隨動翻譯功能。

7. 能不能用C#做屏幕取詞有源碼嗎

可以使用Tesseract-OCR

8. 有道詞典怎麼設置屏幕取詞/自動翻譯

有道詞典開啟【屏幕取詞/自動翻譯】步驟如下:

  1. 打開有道詞典,在主頁面的右下方,可以看到【取詞】/【劃詞】

4.有道網路釋義獲取了大量存在於網路、但普通詞典沒有收錄的流行詞彙、外文名稱和縮寫,包括影視作品名稱、名人姓名、品牌名稱、地名、菜名、專業術語等。互聯網內容日新月異,有道詞庫也與時俱進,輕松囊括互聯網上的新詞熱詞。

9. 請教關於用python編寫屏幕取詞的程序問題

1、屏幕取詞技術實現原理與關鍵源碼----
Ubuntu 下可以監視 gtk.Clipboard 來獲取當前選中文字和位置。

我以前嘗試過定時抓取滑鼠附近圖像做 OCR 來取詞,
改成快捷鍵取詞會省一點。

直接獲得文字的懸停取詞比較麻煩。

網頁、XUL 應用程序可以有滑鼠懸停事件。

X11 自己沒有, 不過以前流行中文平台、中文外掛時候 Turbolinux 的中文 X-window 被修改為集成滑鼠懸停取詞翻譯。

Gtk 程序也許你可以在 ATK 層入手,自己改 ATK 和用 LD_LIBRARY_PATH。

windows下很不好做。
普遍用的是HOOK API的方法。
可以參考stardict的取詞模塊。
不過我感覺stardict的取詞模塊也不是太好用(沒金山詞霸的好用),感覺有bug。

似乎以前某版本的金山詞霸可以翻譯圖片中的文字,就是用 OCR

再,金山似乎出過 Linux Qt3 版本,找 Zoomq 幾位在金山的老大索取源碼看看吧

嗯, 金山詞霸確實出過 Linux 版本,

是基於 wine 的,不是原生的 Linux 版本...

linux下就不知道,windows下,應該是做一個api hook,鉤住TextOut,DrawText和DrawTextEx。
要取詞的時候給滑鼠所在的窗口發一個WM_PAINT消息,讓窗口重繪。
當窗口調用TextOut, DrawText或是DrawTextEx進行重繪的時候,你就可以根據傳入的參數知道
窗口想在滑鼠下畫什麼東西了。

10. java屏幕取詞

僅僅屏幕取詞已經是一個很難的問題了,需要用到匯編和C語言,而java僅存在於虛擬機中,想要抓取外部的東西,這個技術更難以想像。估計還沒人能搞出來了

閱讀全文

與屏幕取詞技術源碼相關的資料

熱點內容
老死pdf 瀏覽:25
雲伺服器關機網址不見了 瀏覽:69
余冠英pdf 瀏覽:755
開發一個app上市需要什麼步驟 瀏覽:28
phpsleep方法 瀏覽:430
時間同步伺服器ip地址6 瀏覽:926
鋼琴譜pdf下載 瀏覽:524
香港阿里雲伺服器怎麼封udp 瀏覽:875
APp買海鮮到哪裡 瀏覽:501
遼油社保app總提示更新怎麼辦 瀏覽:586
導入源碼教程視頻 瀏覽:613
天翼貸app在哪裡下載 瀏覽:186
app開發源碼查看器 瀏覽:516
程序員發展到了一個瓶頸 瀏覽:120
程序員去機房幹嘛 瀏覽:697
英雄訓練師怎麼看曾經伺服器 瀏覽:546
魔獸世界單機輸入gm命令 瀏覽:372
51單片機最大負跳距是多少 瀏覽:418
android聊天控制項 瀏覽:128
導致壓縮機壞的原因 瀏覽:295