前一段時(shí)間研究了下QQ目前各種外掛的機(jī)理,包括著名的coralQQ。鑒于目前網(wǎng)上關(guān)于這方面的文章少之又少,一般能找到的應(yīng)該就這下面3篇(由于可能涉及版權(quán)問題,我鏈接就不給出了: a.木子版顯IPQQ的制作教程 b.關(guān)于QQ外掛DLL的加載原理的分析 c.明日帝國(sunwangme)寫的我是這樣來做破解qq,做QQ外掛的系列 在開始我的分析前我簡要對上面這些資料作下評價(jià),首先我覺得如果你也想寫個(gè)類似的外掛插件,他們的文章你是必看的,而且特別是你想真的寫出什么有用的東西的話,明日帝國得文章一定要看,而且必須看懂。對于木子版的教程應(yīng)該說是最早“公開”的資料了,很多人都是看了這個(gè)教程開始寫自己的外掛的。但是他通過直接修改QQ來做顯IP補(bǔ)丁,可能引起的法律問題不說(如果你只是自娛自樂的話),他不能適應(yīng)不同版本的QQ,而且用戶也不太能接受直接的修改,而且教程已經(jīng)不能直接用于目前版本的QQ了。 第二個(gè)教程是做外掛DLL插件必看的,但是他絲毫沒涉及顯示IP的問題,只是簡單介紹了DLL注入的問題,并對win9x環(huán)境下手動加載dll到進(jìn)程空間作了分析。但是目前win9x已逐漸退出舞臺,所以一般只要使用CreateRemoteThread即可。 第一部分: 1.1 主流的外掛插件如何獲取IP和其他信息的? 也許你會認(rèn)為他攔截了底層的Socket通訊?當(dāng)然不至于,但這樣肯定是最有效的辦法。 讓我們換個(gè)思路:如果你現(xiàn)在需要和一個(gè)QQ好友傳輸文件或者進(jìn)行語音聊天或者發(fā)送了圖片或自定義表情。那么QQ必須知道對方的IP地址和端口信息,這樣才能把數(shù)據(jù)傳給對方。 所以,很有可能QQ內(nèi)部已經(jīng)實(shí)現(xiàn)了獲取IP地址和其他信息的相關(guān)函數(shù)了。的確如此。這也是木子版QQ教程里面提到的辦法,調(diào)用QQ內(nèi)部的函數(shù)。 下面是截至QQ組件之一的CQQApplication.dll中的匯編代碼:(建議先瀏覽木子版QQ的教程) 027832C7 8B45 F0 mov eax,dword ptr ss:[ebp-10] 027832CA 53 push ebx 027832CB 68 38558302 push CQQAppli.02835538 ; ASCII dwIP 027832D0 50 push eax 027832D1 8B08 mov ecx,dword ptr ds:[eax] 027832D3 FF51 18 call dword ptr ds:[ecx+18] 027832D6 8B45 F0 mov eax,dword ptr ss:[ebp-10] 027832D9 53 push ebx 027832DA 68 40558302 push CQQAppli.02835540 ; ASCII wPort 027832DF 50 push eax 027832E0 8B08 mov ecx,dword ptr ds:[eax] 027832E2 FF51 14 call dword ptr ds:[ecx+14] CQQApplication.dll通俗來說就是負(fù)責(zé)顯示和實(shí)現(xiàn)QQ聊天窗口的模塊。就是那些和xxx聊天中的窗口,所以這就是為什么要在其中尋找這樣的代碼的依據(jù)。 從上面的匯編來看,顯然是調(diào)用了2個(gè)thiscall規(guī)范的函數(shù),也就是我們所說的C++類成員函數(shù)。 2個(gè)成員函數(shù)的的大致形式是this->Func(void *ptr1,char *cmd,DWORD *ptr2);其中cmd就是上面dwIP、wPort這些字符串,而ptr2也很容易知道是函數(shù)返回值得存儲指針。現(xiàn)在關(guān)鍵是要獲取this指針,也就是ecx寄存器的數(shù)據(jù)和ptr1這個(gè)神秘指針的數(shù)據(jù)。 如果你有興趣反匯編CoralQQ中相關(guān)的代碼,也會發(fā)現(xiàn)與上面類似的調(diào)用部分。 這里暫時(shí)不深入這些函數(shù)的作用和那個(gè)cmd指針的細(xì)節(jié),我們先來研究如何獲取this指針和ptr2吧。 注意 027832DF 50 push eax 027832E0 8B08 mov ecx,dword ptr ds:[eax] 這2段代碼,也就是說ptr2獲取了,那么this指針也可以得到。所以現(xiàn)在一切的關(guān)鍵就是找出ptr2的來歷。這樣我們就能很輕松的實(shí)現(xiàn)顯示ip了。 1.2 神秘的ptr2指針 為了能更快的說明問題,這里就不厚道的引用CoralQQ.dll的匯編了~ 0056D97F 51 push ecx 0056D980 52 push edx 0056D981 50 push eax 0056D982 FF15 38AB5A00 call dword ptr ds:[5AAB38] ; BasicCtr.GetFriendQQData 0056D988 8B4424 14 mov eax,dword ptr ss:[esp+14] 0056D98C 83C4 0C add esp,0C 0056D98F 3BC3 cmp eax,ebx 0056D991 0F84 03020000 je CoralQQ.0056DB9A 0056D997 57 push edi 0056D998 895C24 14 mov dword ptr ss:[esp+14],ebx 0056D99C 8D5424 14 lea edx,dword ptr ss:[esp+14] 0056D9A0 52 push edx 0056D9A1 68 50BB5900 push CoralQQ.0059BB50 0056D9A6 C64424 30 01 mov byte ptr ss:[esp+30],1 0056D9AB 8B08 mov ecx,dword ptr ds:[eax] 0056D9AD 68 C8BA5900 push CoralQQ.0059BAC8 ; ASCII QQUSER_DYNAMIC_DATA 0056D9B2 50 push eax 0056D9B3 8B41 54 mov eax,dword ptr ds:[ecx+54] 0056D9B6 FFD0 call eax 0056D9B8 8B4424 14 mov eax,dword ptr ss:[esp+14] 0056D9BC 3BC3 cmp eax,ebx 0056D9BE 0F84 F9000000 je CoralQQ.0056DABD 0056D9C4 8B08 mov ecx,dword ptr ds:[eax] 0056D9C6 8D5424 1C lea edx,dword ptr ss:[esp+1C] 0056D9CA 52 push edx 0056D9CB 68 ACA15900 push CoralQQ.0059A1AC ; ASCII wProcotol 0056D9D0 50 push eax 0056D9D1 8B41 30 mov eax,dword ptr ds:[ecx+30] 0056D9D4 FFD0 call eax 0056D9D6 8B4424 14 mov eax,dword ptr ss:[esp+14] 0056D9DA 8B08 mov ecx,dword ptr ds:[eax] 0056D9DC 8D5424 10 lea edx,dword ptr ss:[esp+10] 0056D9E0 52 push edx 0056D9E1 68 94A15900 push CoralQQ.0059A194 ; ASCII dwRecentIP 0056D9E6 50 push eax 0056D9E7 8B41 34 mov eax,dword ptr ds:[ecx+34] 0056D9EA FFD0 call eax 以上代碼正式coralQQ 4.5版獲取IP信息的片斷。我們只需要關(guān)注上面的0056D982和0056D9B6地址的2個(gè)調(diào)用函數(shù)。 為什么這樣說了,先看下面獲取dwRecentIP數(shù)據(jù)的代碼,它和上面提到的那個(gè)成員函數(shù)是屬于一個(gè)類的(這里沒提供出完整代碼,你可以自己驗(yàn)證下:-P)。那么這里的this指針從哪里來呢? 0056D9DA 8B08 mov ecx,dword ptr ds:[eax] 0056D9D6 8B4424 14 mov eax,dword ptr ss:[esp+14] 按照thiscall規(guī)范,ecx就保存了this指針,上面代碼說明ecx是來自[esp+14]的,我們再往上看: 0056D99C 8D5424 14 lea edx,dword ptr ss:[esp+14] 0056D9A0 52 push edx 0056D9A1 68 50BB5900 push CoralQQ.0059BB50 0056D9A6 C64424 30 01 mov byte ptr ss:[esp+30],1 0056D9AB 8B08 mov ecx,dword ptr ds:[eax] 0056D9AD 68 C8BA5900 push CoralQQ.0059BAC8 ; ASCII QQUSER_DYNAMIC_DATA 0056D9B2 50 push eax 0056D9B3 8B41 54 mov eax,dword ptr ds:[ecx+54] 0056D9B6 FFD0 call eax 看到么0056D99C lea edx,dword ptr ss:[esp+14]!! 也就是說this指針和ptr2時(shí)由這個(gè)函數(shù)獲得的,我們暫時(shí)以它的一個(gè)參數(shù)命名:QQUSER_DYNAMIC_DATA。 但不幸的是,這個(gè)函數(shù)同樣也是thiscall調(diào)用規(guī)范的,也就說也是需要得到this指針……不過不慌: 0056D97F 51 push ecx 0056D980 52 push edx 0056D981 50 push eax 0056D982 FF15 38AB5A00 call dword ptr ds:[5AAB38] ; BasicCtr.GetFriendQQData 0056D988 8B4424 14 mov eax,dword ptr ss:[esp+14] 0056D9AB 8B08 mov ecx,dword ptr ds:[eax] 注意上面2段代碼,QQUSER_DYNAMIC_DATA函數(shù)的this指針最終是[esp+14],而esp+14的數(shù)據(jù)是 0056D97F 51 push ecx 這段代碼壓入的。所幸的是GetFriendQQData是個(gè)導(dǎo)出函數(shù)(位于BasicCtrDll.dll),我們看看他的申明: int GetFriendQQData(struct IQQCore *,unsigned long,struct IQQData * *); 上面這個(gè)push ecx實(shí)際上是壓入了參數(shù)struct IQQData * *。 所以現(xiàn)在的只要獲得struct IQQCore *,和第二個(gè)神秘參數(shù)的含義就能實(shí)現(xiàn)顯示IP的功能了。 1.3 IQQCore和Uin int GetFriendQQData(struct IQQCore *,unsigned long,struct IQQData * *); 這個(gè)函數(shù),從而獲得那個(gè)struct IQQData *指針。但問題就是要調(diào)用這個(gè)函數(shù)必須要提供2個(gè)參數(shù):IQQCore *和一個(gè)unsigned long(DWORD)的神秘?cái)?shù)據(jù)[/url]。 在開始正式分析前請各位思考下,如果要你編寫一個(gè)能顯示好友IP的函數(shù),你需要先知道什么呢? 至少需要知道要去獲取哪個(gè)QQ好友吧。這個(gè)肯定是必須的,否則函數(shù)就沒有執(zhí)行的意義了 現(xiàn)在我們跟蹤下上面這個(gè)GetFriendQQData函數(shù)。在其入口點(diǎn)下斷點(diǎn)。然后小心的把鼠標(biāo)移動到QQ好友列表中某個(gè)頭像上(需要使用CoralQQ……有點(diǎn)不厚道)。這時(shí)候應(yīng)該程序就會被斷下。 因?yàn)檎@硎髽?biāo)移至好友頭像會顯示信息卡片,CoralQQ會在下面顯示IP信息,所以按照上一篇文章反匯編的代碼,GetFriendQQData必然會調(diào)用。我們從堆棧里面找到這個(gè)unsigned long對應(yīng)的數(shù)據(jù)[/url]: 0x1A53836 因?yàn)槭荄WORD數(shù)據(jù),把它轉(zhuǎn)化為10進(jìn)制看看:27605046 這不是我的QQ號碼么……的確,先前鼠標(biāo)是移動在我的頭像上了。 所以可以猜測這個(gè)unsigned long就是好友的QQ號碼。 經(jīng)過多次驗(yàn)證,的確如此。 所以,這個(gè)神秘的unsigned long明確:他是要獲取好友信息的號碼,順便補(bǔ)充下,這個(gè)unsigned long在QQ中可是有專門名字的:Uin 接下來就是struct IQQCore*,我想他的作用從名字中應(yīng)該就能猜出大概來。雖然具體他的結(jié)構(gòu)我還沒弄清,但可以肯定他好比是QQ程序內(nèi)核信息的指針。而現(xiàn)在最關(guān)鍵的問題是如何去獲得它。 如果你研究過BasicCtrlDll.dll中導(dǎo)出的函數(shù),你會發(fā)現(xiàn)幾乎一半的函數(shù)的參數(shù)都由這個(gè)IQQCore,比如: int GetCurrentStatus(struct IQQCore *,int *) int GetCurrentUin(struct IQQCore *,unsigned long *) int GetFriendStat(struct IQQCore *,unsigned long) 那么我們就攔截其中的一部分函數(shù),看看提供給他們的這個(gè)struct IQQCore *參數(shù)具體是什么。 如果你的確這樣做了,那么會發(fā)現(xiàn)所有的函數(shù),無論在什么時(shí)候,這個(gè)struct IQQCore *的值是確定的唯一的。如果你這個(gè)IQQCore *指針的地址區(qū)域下內(nèi)存寫入斷點(diǎn)的話會發(fā)現(xiàn)struct IQQCore *實(shí)際上在QQ進(jìn)行登錄初始化時(shí)就創(chuàng)建了,以后就不再被修改。 所以現(xiàn)在的問題就很簡單了,我們可以暫時(shí)不用理會struct IQQCore *到底是什么,只要能獲取到他就ok 1.4 如何獲取struct IQQCore *? 如果只是調(diào)試個(gè)QQ,得到struct IQQCore *是非常容易的,但記住我們是要編寫外掛。所以就是說我們要問:外掛如果通過程序來獲取這個(gè)指針呢? 最野蠻的辦法:把外掛寫成一個(gè)調(diào)試器,模擬手工調(diào)試的過程,獲得這個(gè)IQQCore *。ok,我很佩服你這樣做,這也是我原先的想法。雖然寫這個(gè)一個(gè)調(diào)試器是很簡單的,但是他基本上沒有實(shí)際意義,因?yàn)椴荒軕?yīng)對各種版本的QQ程序,而且也就無法再使用OD這些調(diào)試器來調(diào)試你的程序了 下面的方法要感謝明日帝國(sunwangme)寫的教程了,可能一開始你看他教程會不知所云,但相信你現(xiàn)在去看他的文章就會很有感觸。 上面說過這個(gè)IQQCore 是不會改變的,而且BasicCtrlDll.dll導(dǎo)出的那么多函數(shù)又偏偏要用到他,為什么不去攔截一個(gè)有IQQCore *參數(shù)的函數(shù)來獲取這個(gè)IQQCore *呢? 具體的做法我會在編寫插件時(shí)說明??赡苣銜霐r截導(dǎo)出函數(shù)(也就是API)不也是調(diào)試器作的事么?其實(shí)也有別的辦法,這里就是用API Hook技術(shù)來實(shí)現(xiàn)的(建議先了解下win32的hook技術(shù))。 問題是API SetWindowsHookEx是不可能hook一個(gè)API的。所以這里要用比較“底層”的辦法: 一個(gè)導(dǎo)出函數(shù)的入口內(nèi)存地址可以用GetProcAddress API獲取,我們只要修改程序的代碼,使得他在原先函數(shù)入口點(diǎn)執(zhí)行時(shí)跳入我們的函數(shù)取執(zhí)行,然后再跳轉(zhuǎn)回來即可。(實(shí)際做法不是這樣,在說明如何編寫插件時(shí)我會說到) 現(xiàn)在問題就是要在BasicCtrlDll.dll導(dǎo)出函數(shù)中選取一個(gè)比較理想的函數(shù)取攔截,得到IQQCore 要攔截的函數(shù)應(yīng)該具有如下特點(diǎn): 函數(shù)形參簡單,最好只有IQQCore *一個(gè)參數(shù) 函數(shù)能盡早被調(diào)用,這樣能及早的獲取IQQCore * 函數(shù)不能是thiscall規(guī)范的,也就是說函數(shù)必須是全局函數(shù),不是一個(gè)成員函數(shù) 為何要這些特點(diǎn)應(yīng)該都能理解,我對最后一個(gè)做下說明,thiscall規(guī)范的函數(shù)還需要一個(gè)類的this指針地址,這會給攔截造成一定麻煩(今后就會遇到這樣的情況,以后再討論)。 最終我們選取的函數(shù)是QQHelperDll.dll中的一個(gè)導(dǎo)出函數(shù): int IsLogin(struct IQQCore *); 很滿足我們的要求,而且用OD跟蹤發(fā)現(xiàn),他在QQ登錄后就不停的調(diào)用,太爽了…… 總結(jié)一下:要獲取struct IQQCore *通過攔截API獲取參數(shù)實(shí)現(xiàn),我們攔截的函數(shù)選用了IsLogin()。 1.5 Uin和struct IQQData * 現(xiàn)在還有2個(gè)問題要去研究,第一,在調(diào)用GetFriendQQData時(shí)候,Uin(就是那個(gè)unsigned long參數(shù))如何確定呢? 這個(gè)問題要等到我介紹插件編寫時(shí)在討論,但可以先做下暗示,我們編寫的插件是需要在打開和好友聊天的對話框以及將鼠標(biāo)移動到好友頭像上時(shí),顯示對方的IP信息,就第一個(gè)情況:QQ的聊天對話框里面不就有好友的QQ號碼(Uin)么?第二個(gè)情況:雖然探出的信息卡片沒有號碼,但可以猜測QQ也是先需要獲取相關(guān)信息的。 現(xiàn)在我們再說說這個(gè)IQQData *的作用,看看BasicCtrlDll.dll的一些導(dǎo)出函數(shù): long GetQQDataBuf(struct IQQData *,char const *,class CString &) long GetQQDataStr(struct IQQData *,char const *,class CString &) 其中需要IQQData *參數(shù),那么我們看看這些函數(shù)有什么作用呢?以GetQQDataStr在CoralQQ中的使用情況為例: 0056DAC4 57 push edi 0056DAC5 68 DCBA5900 push CoralQQ.0059BADC ; ASCII NAME 0056DACA 52 push edx 0056DACB FF15 44AB5A00 call dword ptr ds:[5AAB44] ; BasicCtr.GetQQDataStr 0056DAE5 8B4424 24 mov eax,dword ptr ss:[esp+24] 0056DAE9 8D56 34 lea edx,dword ptr ds:[esi+34] 0056DAEC 52 push edx 0056DAED 68 ECBA5900 push CoralQQ.0059BAEC ; ASCII REMARK_REALNAME 0056DAF2 50 push eax 0056DAF3 FF15 44AB5A00 call dword ptr ds:[5AAB44] ; BasicCtr.GetQQDataStr 0056DAF9 8B5424 30 mov edx,dword ptr ss:[esp+30] 0056DAFD 8D4E 38 lea ecx,dword ptr ds:[esi+38] 0056DB00 51 push ecx 0056DB01 68 FCBA5900 push CoralQQ.0059BAFC ; ASCII COUNTRY 0056DB06 52 push edx 0056DB07 FF15 44AB5A00 call dword ptr ds:[5AAB44] ; BasicCtr.GetQQDataStr 0056DB0D 8B4C24 3C mov ecx,dword ptr ss:[esp+3C] 0056DB11 8D46 3C lea eax,dword ptr ds:[esi+3C] 0056DB14 50 push eax 0056DB15 68 04BB5900 push CoralQQ.0059BB04 ; ASCII PROVINCE 0056DB1A 51 push ecx 0056DB1B FF15 44AB5A00 call dword ptr ds:[5AAB44] ; BasicCtr.GetQQDataStr 這些代碼是在獲取IP信息后出現(xiàn)的,猜猜在做什么呢?NAME,COUNTRY,PROVINCE這些詞匯來看,應(yīng)該是在獲取當(dāng)前要顯示ip好友的名字、國籍、省份。而具體的執(zhí)行函數(shù)就是GetQQDataStr。 再結(jié)合這個(gè)函數(shù)的參數(shù)來看,IQQData *,很有可能就是存放著一個(gè)用戶相關(guān)信息的結(jié)構(gòu) 事實(shí)也是如此的,到目前為止編寫QQ外掛插件的條件已經(jīng)具備。 2.編寫QQ顯IP插件 2.1 編寫加載外掛dll的程序 這里采用VC作為開發(fā)環(huán)境,我使用的是VS2005 首先說下大體的思路: 這里仿造coralQQ一樣,將真正的外掛代碼寫入dll,然后編寫一個(gè)exe文件去加載qq.exe,并把我們寫得dll注入。 這一步在前一篇文章中已經(jīng)給出了具體代碼。 2.2 編寫插件主體 感覺自己如果一涉及具體代碼就回說不來話,所以這次就先談思路,然后給出部分代碼。 dll要做的事: 將我們前面分析過的關(guān)鍵API掛鉤,以便得到IQQCore *,Uin,以及在qq聊天窗口彈出時(shí)進(jìn)行捕獲以便顯示IP信息。 要做到上面的要求,首先就是要在dll加載后開始做API hook的工作。 不過這件事情不能在DllMain里面寫,推薦的做法是在DllMain里面創(chuàng)建一個(gè)新的線程,線程的執(zhí)行函數(shù)我們設(shè)為:WorkerProc(VC編寫的話推薦用_beginthread創(chuàng)建這個(gè)線程,否則無法使用CRT函數(shù)),為什么不能直接在DllMain里面寫我稍候分析 然后就是在WorkerProc里面寫入具體的hook代碼。 1.攔截QQHelperDll.dll中的IsLogin函數(shù) 前面分析過了,IQQCore *指針可以通過攔截這個(gè)API來獲取。 這里我們采用的是《windows核心編程》中推薦的攔截API hook的辦法(修改入口地址跳轉(zhuǎn)相對他煩了些,而且我們這里只是想做hook):通過動態(tài)修改API調(diào)用者模塊的IAT表,將原先API的入口地址替換成我們函數(shù)的。具體的替換代碼我們就采用書中提供的了的ReplaceIATEntryInOneMod(見前一篇)。 現(xiàn)在要注意一個(gè)問題,我們用來替換原先API的函數(shù)必須和原函數(shù)采用同樣的調(diào)用規(guī)范,這個(gè)IsLogin本身就是cdecl得,所以只要用個(gè)形參和返回值一樣的函數(shù)取替換即可: 給出實(shí)現(xiàn)本功能的代碼: typedef int (*OrgIsLogin)(DWORD ptrIQQCore); int PokeIsLogin(DWORD ptrIQQCore) { global_ptrIQQCore = ptrIQQCore; return ((OrgIsLogin)(PROC)OrgIsLoginProc)(ptrIQQCore); } ////下面代碼在WorkerProc函數(shù)中/// OrgIsLoginProc = GetProcAddress( GetModuleHandleA(QQHelperDll.dll),?IsLogin@@YAHPAUIQQCore@@@Z); if (ReplaceIATEntryInOneMod(QQHelperDll.dll,OrgIsLoginProc,(PROC)&PokeIsLogin,GetModuleHandleA(qq.exe))) { //替換成功 } // 今后,只要QQ.exe調(diào)用IsLogin,我們的PokeIsLogin函數(shù)就會調(diào)用,并把IQQCore紀(jì)錄下來,注意替換的函數(shù)要最后去調(diào)用原函數(shù),否則就會讓程序崩潰。 2.攔截SetForegroundWindow API 攔截該API是因?yàn)槊看螐棾鲂碌腝Q聊天窗口時(shí),CQQApplication.dll都會去調(diào)用它,以便把聊天窗口至于前景顯示。所以攔截CQQApplication.dll對他的調(diào)用就能在由新的聊天窗口出現(xiàn)時(shí)調(diào)用我們插件的代碼去顯示IP信息,而不必傻傻的去寫循環(huán)等待了 這里就要注意SetForegroundWindow 是采用stdall調(diào)用規(guī)范的,所以別忘在替換函數(shù)名前加上__stdcall(或WINAPI宏)標(biāo)記。 該部分的替換代碼如下: typedef BOOL ( __stdcall *OrgSetForegrandWindow)(HWND hWnd); extern C BOOL APIENTRY OnQQWndShow(HWND hWnd) { bool trueResult = ((OrgSetForegrandWindow)(PROC)OrgFuncProc)(hWnd); return trueResult; } //下面代碼在WorkerProc函數(shù)或由其調(diào)用的函數(shù)中 OrgFuncProc = GetProcAddress( GetModuleHandleA(user32),SetForegroundWindow); if (ReplaceIATEntryInOneMod(user32.dll,OrgFuncProc,(PROC)&OnQQWndShow,global_hCQQAppModule)) { //替換函數(shù)入口成功 } 其中g(shù)lobal_hCQQAppModule是CQQApplication.dll在qq.exe中的模塊句柄,可以調(diào)用GetModuleHandleA(CQQApplication.dll)來實(shí)現(xiàn)。不過現(xiàn)在存在一個(gè)問題 用od加載qq.exe就會發(fā)現(xiàn)實(shí)際上CQQApplication.dll并不是在qq.exe啟動后加載的,而是在登錄以后。所以如果我們的dll插件在被加載之后立刻調(diào)用GetModuleHandleA的話就會返回NULL,那么之后的函數(shù)替換就不可能實(shí)現(xiàn)了。 這里我用了個(gè)比較笨的辦法,在替換SetForegroundWindow前編寫循環(huán)不斷的去掉用GetModuleHandleA,直到返回非null,而為了防止頻率過快可以在循環(huán)中加上Sleep函數(shù)。(之前也嘗試loadlibrary提前加載,但一直失敗。還有中辦法就是從前面的IsLogin來判斷是否登錄了QQ。但沒有試驗(yàn)過。總之如果你有比較好的辦法也希望告訴我) 這部分的代碼如下: bool WaitforLogon() { while(!bIsDllUnload) { global_hCQQAppModule = GetModuleHandleA(CQQApplication.dll); if (global_hCQQAppModule) return true; Sleep(300); } return false; } 這就是為什么前面說過不能再dllmain里面作函數(shù)替換的工作了,否則會導(dǎo)致dllmain無法退出,從而會鎖死整個(gè)qq 3.獲取Uin 現(xiàn)在IQQCore *已經(jīng)獲得了,同時(shí)只要在OnQQWndShow中寫入得到用戶IP信息的代碼,再使用FindWindow的方法把原先的廣告去除,再創(chuàng)建個(gè)Edit或者Static來顯示我們的數(shù)據(jù)即可。 但之前還要得到Uin,也就是對方好友的QQ號。 其實(shí)這里有個(gè)很笨的辦法:QQ聊天對話框中有對方的號碼的:比如“&heaven(27605046)(后面是個(gè)性簽名)。的確可以用FindWindow+GetWindowText獲取,然后得到括號里面的數(shù)據(jù)就好了,但是是否有更簡單辦法呢?有 這里要感謝明日帝國(sunwangme)的教程,這里就是用他的方法了,具體原理還是大家去他blog看吧(google一下) 大體的方法是:在QQ準(zhǔn)備顯示聊天對話框時(shí),上面“&heaven(27605046)(后面是個(gè)性簽名)這段文字會調(diào)用位于QQBaseClassInDll.dll中的CAllInOneStatusBar::SetUin(unsigned long);這個(gè)函數(shù),其中參數(shù)就是我們要的QQ號了。而且可以保證CQQApplication.dll僅僅在要顯示對話框前才會調(diào)用它。(再次感謝明日帝國) 所以和上面一樣,這次攔截CAllInOneStatusBar::SetUin(unsigned long); OrgSetUINProc = GetProcAddress(GetModuleHandleA(QQBaseClassInDll.dll),?SetUin@CAllInOneStatusBar@@QAEX_J@Z); if (ReplaceIATEntryInOneMod(QQBaseClassInDll.dll,OrgSetUINProc,(PROC)&Poke_GETUIDA,global_hCQQAllInOne)){ } 今后就會先調(diào)用我們的OrgSetUINProc,其中獲取參數(shù)即可。 不過要注意2個(gè)問題,第一這個(gè)QQBaseClassInDll.dll也不是一開始加載,更不是在登錄后加載,而是在第一次要顯示QQ聊天窗口才加載,不過比較幸運(yùn)的是可以直接LoadLibrary把它先載過來。 還有一個(gè)問題就是SetUin是thiscall規(guī)范的……哎,自己對thiscall還不是很了解,所以怕出錯(cuò),那個(gè)OrgSetUINProc只能用naked調(diào)用規(guī)范了,同時(shí)要自己寫匯編去完成細(xì)節(jié)的事: __declspec( naked ) int Poke_GETUIDA() { _asm { push ecx mov ecx,[esp+8] mov dwRecentUINA,ecx pop ecx jmp OrgSetUINProc } } ok,這樣一切都完成了 3.編寫獲取IP信息的代碼 先整理下上面我們3個(gè)替換函數(shù)的執(zhí)行順序: PokeIsLogin:最好執(zhí)行,且今后不斷被調(diào)用 Poke_GETUIDA:在將要顯示QQ聊天窗口時(shí)執(zhí)行 OnQQWndShow:在Poke_GETUIDA之后運(yùn)行。 所以可以保證在OnQQWndShow中可以得到我們獲取IP信息所有必要的參數(shù)了。 那么就開始編寫獲取IP的代碼: 按照前面分析的,先用BasicCtr.GetFriendQQData得到當(dāng)前好友的IQQData *: DWORD MainHandle; if (ptrBasicCtr_GetFriendQQData==NULL || global_ptrIQQCore==NULL) return 0; _asm { mov edx,dwID mov eax,global_ptrIQQCore lea ecx,MainHandle push ecx push edx push eax call ptrBasicCtr_GetFriendQQData ; BasicCtr.GetFriendQQData add esp,0xC } 其中dwID是Uin,即用DWORD保存的QQ號碼 global_ptrIQQCore就是前面獲得的IQQCore * ptrBasicCtr_GetFriendQQData 是GetFriendQQData的入口地址,用GetProcAddress得到 最終MainHandle將保存IQQData *,如果運(yùn)行失敗,會返回null 接下來就是通過QQUSER_DYNAMIC_DATA(暫時(shí)命名)函數(shù)來得到動態(tài)信息類的指針: char *szQQUSER_DYNAMIC_DATA = QQUSER_DYNAMIC_DATA; DWORD tmpInfo; DWORD returnVal; tmpInfo = 0xba863a1e; if (mainHandle == NULL) { return NULL; } _asm { mov eax,mainHandle lea edx,returnVal push edx lea edx,tmpInfo push edx mov ecx,[eax] push szQQUSER_DYNAMIC_DATA push eax call [ecx+54h] } 結(jié)果將保存在returnVal中,同樣,如果執(zhí)行錯(cuò)誤returnVal=NULL 最后就是去獲取IP地址了 char *szwProcotol = wProcotol; char *szRecentip=dwRecentIP; char *szwRecentPort=wRecentPort; char *szdwC2CIP=dwC2CIP; char *szwC2CPort=wC2CPort; char *szdwIP=dwIP; char *szwPort=wPort; bool GetDestIPInfo(DWORD ptrClassHandle,DWORD *ptrDestIp,DWORD *ptrDestPort) { if (ptrClassHandle==NULL || ptrDestIp==NULL || ptrDestPort==NULL) return false; //Using Std Info Buffer _asm { pushad pushf mov eax,ptrClassHandle mov ecx,[eax] mov edx,ptrDestIp push edx push szRecentip push eax mov eax,[ecx+34h] call eax mov eax,ptrClassHandle mov ecx,[eax] mov edx,ptrDestPort push edx push szwRecentPort push eax mov eax,[ecx+30h] call eax popf popad } (*ptrDestPort) &= 0xFFFF; if ((*ptrDestIp) != NULL && (*ptrDestPort) != NULL) return true; _asm { pushad pushf mov eax,ptrClassHandle mov ecx,[eax] mov edx,ptrDestIp push edx push szdwC2CIP push eax mov eax,[ecx+34h] call eax mov eax,ptrClassHandle mov ecx,[eax] mov edx,ptrDestPort push edx push szwC2CPort push eax mov eax,[ecx+30h] call eax popf popad } (*ptrDestPort) &= 0xFFFF; if ((*ptrDestIp) != NULL && (*ptrDestPort) != NULL) return true; _asm { pushad pushf mov eax,ptrClassHandle mov ecx,[eax] mov edx,ptrDestIp push edx push szdwIP push eax mov eax,[ecx+34h] call eax mov eax,ptrClassHandle mov ecx,[eax] mov edx,ptrDestPort push edx push szwPort push eax mov eax,[ecx+30h] call eax popf popad } (*ptrDestPort) &= 0xFFFF; return ((*ptrDestIp) != NULL && (*ptrDestPort) != NULL); } ptrClassHandle是前面得到的returnVal,IP地址的DWORD數(shù)據(jù)和WORD的port信息將保存在后2個(gè)參數(shù)指針的地址中。(代碼借鑒了CoralQQ) 到目前為止已大功告成!其他的細(xì)節(jié)代碼就請各位發(fā)揮吧 4.擴(kuò)展 上面并沒有介紹如何得到對方QQ版本的信息,其實(shí)和獲取IP一樣,你在CQQApplication.dll中找到wProcotol的字符串參考,就會發(fā)現(xiàn)它和獲取IP的代碼很像,直接抄下來用就是了。 [ 本帖最后由 fw8752638 于 2008-10-24 16:27 編輯 ] |