-- 作者: 行客 -- 發佈時間: 2005/06/06 03:14pm
[b]修正後的源碼及EXE , 有易友提出源程序出錯, 這裏說明一下, 源程序是用4.0測試版5編寫的 , 用3.8打開會出錯[/b]
前段時間在論壇上, 易友 "wjkplx" 問 "如何取正在運行的QQ號" (dispbbs.asp?BoardID=1&ID=51048), 後來有"方德軟件","近在眼前","goomoo","雲德武" 幾位熱心的大俠各自提出了自己的思路及解決辦法. 幾位的貼子如下: "goomoo": http://www.dywt.com.cn/vbs/dispbbs.asp?boardid=1&star=1&replyid=8168&id=55328&skin=0&page=1 近在眼前: http://www.dywt.com.cn/vbs/dispbbs.asp?boardid=1&star=1&replyid=9038&id=55314&skin=0&page=1 http://www.dywt.com.cn/vbs/dispbbs.asp?boardid=1&star=1&replyid=8223&id=55487&skin=0&page=1 方德軟件: http://www.dywt.com.cn/vbs/dispbbs.asp?BoardID=1&ID=55343 雲城武: http://www.dywt.com.cn/vbs/dispbbs.asp?BoardID=1&ID=55599
幾位易友的方法各不相同, 各有優點, 也都不完美. "方德軟件"的方法是查找硬盤文件, 編程簡單,但只能取最後一個登陸的QQ號碼,不能取多個QQ的號碼. "近在眼前"最初的辦法好像是搜索QQ目錄以數字命名的文件夾, 也同樣有 "方德軟件" 的缺點, "近在眼前"之後修改的版本, 採用了moogoo的方法. 相對來說, "goomoo" 的方法是最好的, 直接在QQ進程空間裏查找QQ號碼,因爲QQ的任何數據都是放在進程空間裏的(包括QQ號碼),所以這個方法是絕對能成功的.當然,前提是QQ必須先登陸成功. "goomoo" 的方法可能是考慮得不夠周全, 比如有些號碼找不到, 而有些找到了又是別的號碼."雲城武" 的方法跟 "goomoo"是一樣的.
我花了兩天時間, 仔細地研究了各位的方法, 併到網上搜索了相關的資料, 集合幾位大俠的思路, 做出了取QQ登陸號碼的程序. 這個程序可以提取騰訊QQ所有版本的登陸成功後的QQ號碼. 可以在任何操作系統下使用. 運行速度快, 取號碼準確, 目前爲止,沒有發現取錯號. 製作這個程序的過程中,我查閱了很多相關資料, 對系統編程有了進一步的瞭解, 下面我就編制這個程序的原理過程和一些心得寫出來, 給大家參考.
一. 取QQ號碼原理:
QQ程序在運行過程中, 所有數據都是存放在進程空間中,QQ號碼也不例外, 要取QQ號碼, 從QQ進程空間着手是最保險的. 怎樣確定QQ號碼在QQ進程空間的位置? "goomoo"的方法是搜索"clientuin="關鍵字,這個關鍵字之後緊跟着就是QQ號碼. 但我發現, "clientuin="後面也不一定總是登陸的QQ號碼,有時是別的字符,有時是本地登陸的其他QQ號碼, 有時又是好友的QQ號碼. 所以這個通過這個關鍵字來定位是不準確的. 經過分析, 我發現,QQ運行過程中會讀取"MsgEx.db"文件, 在這個文件的全路徑中就包含了QQ號碼, 路徑格式爲: QQ路徑 +"/" + QQ登陸號碼 + "/MsgEx.db", 找到"/MsgEx.db"關鍵字, 然後提取關鍵字前面的第一個"/"和第二個"/"之間的文本,不就是QQ號碼了嗎? 對,正是這樣. 在QQ進程中, "/MsgEx.db" 的地方很多, 有些前面跟的不是QQ號碼.爲了保證取到號碼的正確性, 我們需要加入一些判斷技巧. 大家知道,QQ號碼都是數字格式的, 所以只要我們判斷取出來的號碼是不是數字, 如果不是數字,就繼續查找,直到找到是數字的文本爲止.
二. 怎樣搜索QQ進程空間的數據?
1.應用程序進程 進程是當前操作系統下一個被加載到內存的、正在運行的應用程序的實例。每一個進程都是由內核對象和地址空間所組成的,內核對象可以讓系統在其內存放有關進程的統計信息並使系統能夠以此來管理進程,而地址空間則包括了所有程序模塊的代碼和數據以及線程堆棧、堆分配空間等動態分配的空間。進程僅僅是一個存在,是不能獨自完成任何操作的,必須擁有至少一個在其環境下運行的線程,並由其負責執行在進程地址空間內的代碼。在進程啓動的同時即同時啓動了一個線程,該線程被稱作主線程或是執行線程,由此線程可以繼續創建子線程。如果主線程退出,那麼進程也就沒有存在的可能了,系統將自動撤消該進程並完成對其地址空間的釋放。 加載到進程地址空間的每一個可執行文件或動態鏈接庫文件的映象都會被分配一個與之相關聯的全局唯一的實例句柄(Hinstance)。
2. 進程空間 在WIN32中,每個應用程序都可“看見”4GB的線性地址空間, 其中最開始的4MB和最後的2GB由操作系統保留,低的2GB爲進程的私有空間(如果在Boot.ini文件中使用“/3GB”的開關可以使進程的私有空間增大到3GB,系統空間1GB)。對於每個進程來講其虛擬的地址空間是連續的,實際上它們是以頁面爲單位離散的存在於物理內存中,一些可能被交換到硬盤上的頁面文件中,而且還有大部分的空間是未提交(Uncommitted)的。一個進程的低2GB私有空間的分佈如下表:
範圍 大小 作用 ----------------------------------------------------------------------------------------------------------------------------- 0x0~~0xFFFF 64 KB 不可訪問區域,只是用來防止非法的指針訪問,訪問該範圍的地址會導致訪問違例。 0x10000~~0x7FFEFFFF 2 GB 減去至少192 KB 進程的私有地址空間 0x7FFDE000~~0x7FFDEFFF 4 KB 進程中第一個線程的線程環境塊,即TEB(Thread environment block) 0x7FFDF000~~0x7FFDFFFF 4 KB 進程的進程環境塊,即PEB(Process environment block) 0x7FFE0000~~0x7FFE0FFF 4 KB 一個共享的只讀用戶數據塊,該塊映射到到系統空間的一個數據塊, 其中存放的是一些系統信息如系統時間、時鐘的滴答數、系統版本號等。 這樣訪問這些信息的時候系統就不用切換到核心模式。 0x7FFE1000~~0x7FFEFFFF 60 KB 不可訪問 0x7FFF0000~~0x7FFFFFFF 64 KB 不可訪問,用於防止線程的緩衝跨越兩種模式空間的邊界
一個進程的高2GB空間具體分配如下: 0xFFFFFFFF-0xC0000000的1GB 用於VxD、存儲器管理和文件系統; 0xBFFFFFFF-0x80000000的1GB 用於共享的WIN32 DLL、存儲器映射文件和共享存儲區;
虛擬內存通常是由固定大小的塊來實現的,在WIN32中這些塊稱爲“頁”,每頁大小爲4,096字節。在Intel CPU結構中,通過在一個控制寄存器中設置一位來啓用分頁。啓用分頁時CPU並不能直接訪問內存,對每個地址要經過一個映射進程,通過一系列稱作“頁表”的查找表把虛擬內存地址映射成實際內存地址。通過使用硬件地址映射和頁表WIN32可使虛擬內存即有好的性能而且還提供保護。利用處理器的頁映射能力,操作系統爲每個進程提供獨立的從邏輯地址到物理地址的映射,使每個進程的地址空間對另一個進程完全不可見。
我們要搜索另一個進程空間的數據, 要掃描範圍的起點和終點不是從0~~2GB,而只是其中的一部分。要得到這個起點和終點可以使用API函數GetSystemInfo,函數的原型如下: VOID GetSystemInfo( LPSYSTEM_INFO lpSystemInfo ); 而在結構SYSTEM_INFO中有兩個值:lpMinimumApplicationAddress和 lpMaximumApplicationAddress, 就是一個應用程序可用的最小和最大的地址空間。這樣我們就得到了要掃描的地址的起點和終點。那麼是不是這起點和終點間所有的地址都要掃描呢?並不是這樣的,因爲一般情況下一個進程是用不着這麼大(接近2GB)的地址空間的。因此一個進程的大部分地址空間都是未用(Free)或是保留(Reserved)的,真正用到的只是那些已提交(Committed)的內存而已。 內存頁面可以有三種狀態:未用(Free)、保留(Reserved)和提交(Committed)。一個未用的頁面是指該頁面未被保留或是提交,對一個進程來講一個未用的頁面是不可訪問的,訪問這樣的頁面將導致訪問違例。進程可以要求系統保留一些頁面以備後用,系統返回一段保留的地址給進程,但是這些地址同樣是不可訪問的,進程若想使用這段地址空間,使用必須先提交。只有一個提交的頁面纔是一個真正可以訪問的頁面。不過你提交了一個頁面,系統並不會馬上分配物理頁面,只有在該頁面第一次被訪問到時,系統纔會分配頁面並初始化。另外,這三個狀態的兩兩之間都是可以相互轉化的。 這樣我們的工作已大大減少了,只需要掃描那些提交的頁面就好了。接下來要做的就是得到一個進程的已提交的頁面範圍。這就要用到另外兩個API函數VirtualQuery和VirtualQueryEx。兩個函數的功能相似,不同就是VirtualQuery只是查詢本進程而VirtualQueryEx可以查詢指定進程的內存空間信息,後者正是我們所需要的,函數原型如下: DWORD VirtualQueryEx( HANDLE hProcess, // 進程的句柄 LPCVOID lpAddress, // 內存地址指針 PMEMORY_BASIC_INFORMATION lpBuffer, // 指向MEMORY_BASIC_INFORMATION結構的指針,用於返回內存空間的信息 SIZE_T dwLength // lpBuffer的長度 );
再來看一下結構MEMORY_BASIC_INFORMATION的聲明: typedef struct _MEMORY_BASIC_INFORMATION { PVOID BaseAddress; //查詢內存塊的基地址 PVOID AllocationBase; //用VirtualAlloc分配該內存時實際分配的基地址,可以小於BaseAddress, //也就是說BaseAddress一定包含在AllocationBase分配的範圍內 DWORD AllocationProtect; //分配該頁面時,頁面的一些屬性,如PAGE_READWRITE、PAGE_EXECUTE等 SIZE_T RegionSize; //從BaseAddress開始,具有相同屬性的頁面的大小 DWORD State; //頁面的狀態,有三種可能值:MEM_COMMIT、MEM_FREE和MEM_RESERVE, //這個參數對我們來說是最重要的了,從中我們便可知指定內存頁面的狀態了 DWORD Protect; //頁面的屬性,其可能的取值與AllocationProtect相同 DWORD Type; //該內存塊的類型,有三種可能值:MEM_IMAGE、MEM_MAPPED和MEM_PRIVATE } MEMORY_BASIC_INFORMATION, *PMEMORY_BASIC_INFORMATION;
進一步研究發現, 要搜索數據, 只要搜索 類型=MEM_PRIVATE 頁面屬性=PAGE_READWRITE 的內存塊就好了, 這樣可以大大提高搜索速度.
這樣我們就可得到進程中需要掃描的地址範圍了。到這裏剩下的問題就是要讀取指定的進程的指定的地地址空間的內容了。這裏要用到的是用於調試程序和錯誤處理(Debugging and Error Handling)的API函數中的ReadProcessMemory,它的原型如下: BOOL ReadProcessMemory( HANDLE hProcess, // 被讀取進程的句柄 LPCVOID lpBaseAddress, // 讀的起始地址 LPVOID lpBuffer, // 存放讀取數據緩衝區 SIZE_T nSize, // 一次讀取的字節數 SIZE_T * lpNumberOfBytesRead // 實際讀取的字節數 );
參數很簡單從它們的名字都可以猜出其意義了,這裏就不多做說明了。要說明的是要對一個進程進行ReadProcessMemory操作,當前進程對要讀的進程必須有PROCESS_VM_READ 和 PROCESS_QUERY_INFORMATION 訪問權。要獲得一個進程的句柄和對這個進程的一些控制權可以使用API函數OpenProcess得到,其使用不做詳細說明了,只給出其原型: HANDLE OpenProcess( DWORD dwDesiredAccess, // 訪問標誌 BOOL bInheritHandle, // 繼承標誌 DWORD dwProcessId // 進程ID );
3.如何獲取QQ進程ID
進程ID可由 Process32First 和 Process32Next 得到,這兩個函數可以枚舉出所有開啓的進程。 Process32First 和 Process32Next原形如下:
BOOL WINAPI Process32First ( HANDLE hSnapshot //由 CreateToolhelp32Snapshot 返回的系統快照句柄; LPPROCESSENTRY32 lppe // 指向一個 PROCESSENTRY32 結構; ); BOOL WINAPI Process32Next ( HANDLE hSnapshot // 由 CreateToolhelp32Snapshot 返回的系統快照句柄; LPPROCESSENTRY32 lppe // 指向一個 PROCESSENTRY32 結構; );
CreateToolhelp32Snapshot 原形如下: HANDLE WINAPI CreateToolhelp32Snapshot ( DWORD dwFlags, // 快照標誌; DWORD th32ProcessID // 進程ID; );
現在需要的是進程的信息,所以將 dwFlags 指定爲 TH32CS_SNAPPROCESS,th32ProcessID 忽略;
PROCESSENTRY32 結構如下: typedef struct tagPROCESSENTRY32 { DWORD dwSize; // 結構大小; DWORD cntUsage; // 此進程的引用計數; DWORD th32ProcessID; // 進程ID; DWORD th32DefaultHeapID; // 進程默認堆ID; DWORD th32ModuleID; // 進程模塊ID; DWORD cntThreads; // 此進程開啓的線程計數; DWORD th32ParentProcessID;// 父進程ID; LONG pcPriClassBase; // 線程優先權; DWORD dwFlags; // 保留; char szExeFile[MAX_PATH]; // 進程全名; } PROCESSENTRY32;
三. 程序流程
知道了上面的原理 , 編寫程序就很簡單了 .程序流程如下:
1. 遍歷系統進程, 找到所有QQ進程的ID; 2. 通過ID打開每個QQ進程, 獲得操作句柄; 3. 讀取QQ進程 "類型=MEM_PRIVATE 頁面屬性=PAGE_READWRITE" 的內存塊到自己程序的緩衝區, 然後搜索關鍵字 "/MsgEx.db"位置; 4. 提取關鍵字前面的QQ號碼. 5. 顯示QQ號碼.結束.
|