PE文件格式與API HOOK收藏| 舊一篇: 如何將位圖對象保存為BMP文件對于windows低層編程來說,,進(jìn)行API攔截始終是一件讓人激動的事,用自己的代碼來改變其它程序的行為,,還有比這個(gè)更有趣嗎,?而且,在實(shí)現(xiàn)API攔截的過程中我們還有機(jī)會去熟悉許多在RAD編程環(huán)境中很少接觸的東西,,如DLL遠(yuǎn)程注入,、內(nèi)存管理,PE文件格式等知識,。許多商業(yè)軟件,,如金山詞霸等詞典軟件,各種即時(shí)漢化軟件,、甚至一些網(wǎng)絡(luò)游戲的外掛中都用到了這種技術(shù),,各種調(diào)試工具中多多少少也要用到這種技術(shù)。 實(shí)現(xiàn)API攔截的一種方法是修改PE文件中的輸入地址表,。在32位windows中,,無論是.EXE文件,還是.DLL文件都是采用PE文件格式,,PE文件格式將程序所有調(diào)用的API函數(shù)的地址信息存放在輸入地址表中,,而在程序碼中,對API的調(diào)用使用的地址不是API函數(shù)的地址,,而是輸入地址表中該API函數(shù)對應(yīng)的地址,。我們只要修改輸入地址表中函數(shù)地址就可以攔截API了。首先我們來熟悉一下PE文件格式,,由于PE文件格式本身比較復(fù)雜,,涉及到的數(shù)據(jù)類型較多,所以在這里只介紹一部分內(nèi)容。我已經(jīng)畫了一幅示意圖,,大致描繪出PE文件格式,,其中有的結(jié)構(gòu)中的數(shù)據(jù)是一個(gè)RVA,凡是這樣數(shù)據(jù)在圖中都已注明,。 PE文件是由一個(gè)DOS文件頭開始的,,緊接在它后面的是一個(gè)DOS stub,它們合在一起實(shí)際上是一個(gè)完整的DOS程序,,在PE文件中提供它們最主要的目的是由于兼容性,,如果我們在DOS中去執(zhí)行一個(gè)win32程序,這個(gè)DOS程序就會顯示出“This program can not run in dos mode”之類的語句,。在它們的后面才是真正的PE文件頭,,所以這兩個(gè)部分并不重要,但是由于每一個(gè)DOS stub的大小并不一樣,,所以我們必須要用DOS文件頭中一個(gè)成員e_lfanew來定位PE文件頭,,DOS文件頭被定義成IMAGE_DOS_HEADER結(jié)構(gòu)。它的成員e_lfanew中含有PE文件頭的“相對虛擬地址”(RVA),。 在這里我們要解釋一下RVA(相對虛擬地址),,在PE文件中經(jīng)常見到這個(gè)名詞,所謂RVA指的是相對于模塊起始地址的偏移量,,所以RVA必須要加上模塊的起始地址才能得到真正的地址,。之所以稱它為“虛擬”的是因?yàn)樵谝粋€(gè)PE格式文件沒有被裝入內(nèi)存之前,RVA是沒有意義的,,只有PE格式文件被裝入內(nèi)存后,,RVA才是有意義的。 舉例說明:如上圖所示: 假設(shè)某個(gè)PE文件的裝入虛擬地址(VA)為400000h,,而這個(gè)PE文件中的DOS頭中的成員e_lfanew的值為40h(RVA)的話, 那么它所指的PE文件頭的虛擬地址(VA)就是400040h,。 在DOS stub后面才是我們感興趣的PE文件頭,,它被定義成IMAGE_NT_HEADERS結(jié)構(gòu),這個(gè)結(jié)構(gòu)中含有整個(gè)PE文件的信息,,它的定義如下:( 這里用匯編語言定義,,在winnt.h中有基于C語言的定義) IMAGE_NT_HEADERS STRUCT Signature dd ? FileHeader IMAGE_FILE_HEADER <> OptionalHeader IMAGE_OPTIONAL_HEADER32<> IMAGE_NT_HEADERS ENDS 而這個(gè)結(jié)構(gòu)中,與我們API攔截有關(guān)的是最后一項(xiàng)OptionalHeader,,它被定義成IMAGE_OPTIONAL_HEADER32結(jié)構(gòu),,這個(gè)結(jié)構(gòu)共有31個(gè)域,定義如下:(省略了一部分與API攔截?zé)o關(guān)的) IMAGE_OPTIONAL_HEADER32 STRUCT … … NumberOfRvaSizes dd ? DataDirectory IMAGE_DATA_DIRECTORY 16 dup(<>) IMAGE_OPTIONAL_HEADER32 STRUCT 其中我們需要的是最后的DataDirectory域,,這個(gè)域被稱為“數(shù)據(jù)目錄”,,它是由16個(gè)IMAGE_DATA_DIRECTORY結(jié)構(gòu)組成的數(shù)組,每個(gè)數(shù)組中存放了PE文件的一個(gè)重要的數(shù)據(jù)結(jié)構(gòu)的信息,其中第二個(gè)元素稱為“引入表”,,在“引入表”中存放了PE文件所調(diào)用的DLL及外部函數(shù)的信息,,包括引入函數(shù)所在DLL名,引入函數(shù)名,,引入函數(shù)地址等,。我們實(shí)現(xiàn)API攔截的方法就是要將“引入表”中的引入函數(shù)地址改成我們自已的函數(shù)地址。IMAGE_DATA_DIRECTORY定義如下: IMAGE_DATA_DIRECTORY STRUCT VirtualAddress dd ? isize dd ? IMAGE_DATA_DIRECTORY ENDS 其中VirtualAddress 是數(shù)據(jù)結(jié)構(gòu)的相對虛擬地址,,isize含有VirtualAddress所指向的數(shù)據(jù)結(jié)構(gòu)的大小,。舉例來說,一個(gè)關(guān)于 “引入表”的IMAGE_DATA_DIRECTORY結(jié)構(gòu)中,,VirtualAddress包含了“引入表”的RVA,。利用這個(gè)RVA我們就可以找到“引入表”。 “引入表”本身是一個(gè)由IMAGE_IMPORT_DESCRIPTOR結(jié)構(gòu)組成的數(shù)組,,數(shù)組中的每個(gè)IMAGE_IMPORT_DESCRIPTOR元素包含一個(gè)PE文件引用的DLL的信息,,所以數(shù)組中元素個(gè)數(shù)與PE文件引用的DLL個(gè)數(shù)有關(guān)。這個(gè)數(shù)組以一個(gè)全0的IMAGE_IMPORT_DESCRIPTOR結(jié)構(gòu)結(jié)束,。下面看一下IMAGE_IMPORT_DESCRIPTOR結(jié)構(gòu)的定義: IMAGE_IMPORT_DESCRIPTOR STRUCT union Characteristics dd ? OriginalFirstThunk dd ? ends TimeDataStamp dd ? ForarderChain dd ? Name1 dd ? FirstThunk dd ? IMAGE_IMPORT_DESCRIPTOR ENDS 這個(gè)結(jié)構(gòu)中的成員并不是每一個(gè)都和我們討論的API攔截有關(guān),,但是它實(shí)在是太有趣了,所以在這里介紹一下它的部分成員,。 第一個(gè)成員是一個(gè)union子結(jié)構(gòu),,這個(gè)子結(jié)構(gòu)其實(shí)只是給OriginalFirstThunk加了個(gè)別名而已,該成員含有指向一個(gè)IMAGE_THUNK_DATA結(jié)構(gòu)數(shù)組的RVA,。 那么什么是IMAGE_THUNK_DATA呢,?它的定義如下: IMAGE_THUNK_DATA STRUCT union u1 ForwarderString dd ? Function dd ? Ordinal dd ? AddressOfData dd ? ends IMAGE_THUNK_DATA ENDS 雖然看起來很復(fù)雜,其實(shí)它不過是一個(gè)DWORD型的變量,,一般我們將它看作是一個(gè)指向IMAGE_IMPORY_BY_NAME結(jié)構(gòu)的RVA,。至于IMAGE_IMPORY_BY_NAME結(jié)構(gòu)它存放了一個(gè)引入函數(shù)的信息。定義如下: IMAGE_IMPORT_BY_NAME STRUCT Hint dw ? Name1 db ? IMAGE_IMPORT_BY_NAME ENDS 其中Hint指示本函數(shù)在DLL的“引出表”中的索引號,,而Name1含有函數(shù)名,。(這個(gè)成員本來的定義應(yīng)該是Name,但是Name是匯編語言的偽指令,,所以用Name1代替,,注意Name1本身就含有函數(shù)名,它不是一個(gè)RVA,。) 真正和我們討論的主題API攔截有關(guān)的是FirstThunk,。它也是指向一個(gè)IMAGE_THUNK_DATA結(jié)構(gòu)數(shù)組的RVA,這個(gè)IMAGE_THUNK_DATA 和前面所說的OriginalFirstThunk所指向的IMAGE_THUNK_DATA并不是同一個(gè)數(shù)組,,不過它們是有聯(lián)系的,,在PE文件未被裝入內(nèi)存之前,,這兩個(gè)數(shù)組的內(nèi)容完全相同,但是在PE文件被裝入內(nèi)存后,,OrigianalFirstThunk所指向的IMAGE_THUNK_DATA結(jié)構(gòu)數(shù)組的內(nèi)容保持不變,,還是指向IMAGE_IMPORT_BY_NAME結(jié)構(gòu),而FirstThunk所指向的IMAGE_THUNK_DATA結(jié)構(gòu)數(shù)組的內(nèi)容就改成了引入函數(shù)的真實(shí)地址了,,這時(shí)我們稱這個(gè)結(jié)構(gòu)數(shù)組為輸入地址表IAT(Import Address Table),。我們實(shí)現(xiàn)API的關(guān)鍵就是修改IAT中的數(shù)據(jù),將它改成我們自己的函數(shù)的地址,。 看了上面的介紹你是否已經(jīng)知道我們API攔截的實(shí)現(xiàn)方法了,,對,我們先取得模塊的起始地址,,然后利用IMAGE_DOS_HEADER結(jié)構(gòu)中的e_lfanew域來定位到IMAGE_NT_HEADER結(jié)構(gòu),,獲取OptionalHeader結(jié)構(gòu)中的數(shù)據(jù)目錄地址,取數(shù)據(jù)目錄的第二個(gè)成員,,提取其VirtualAddress的值,,這樣,我們得到了IMAGE_IMPORT_DESCRIPTOR結(jié)構(gòu)數(shù)組,也就是“引入表”,。關(guān)鍵代碼如下: mov eax,hMoudle ;hMoudle為模塊起始地址 mov esi,eax assume esi :ptr IMAGE_DOS_HEADER ;假設(shè)esi指向一個(gè)IMAGE_DOS_HEADER結(jié)構(gòu) add esi,[esi].e_lfanew ;此時(shí)esi指向PE header assume esi :ptr IMAGE_NT_HEADERS ;假設(shè)esi指向一個(gè)IMAGE_NT_HEADERS結(jié)構(gòu) mov ebx,[esi].OptionalHeader.DataDirectory[sizeof IMAGE_DATA_DIRECTORY].VirtualAddress ;取引入表的RVA add eax,ebx ;由RVA加上模塊起始地址得到引入表的實(shí)際地址. mov esi,eax assume esi :ptr IMAGE_IMPORT_DESCRIPTOR;假設(shè)esi是指向一個(gè)IMAGE_IMPORT_DESCRIPTOR結(jié)構(gòu) 我們遍歷這個(gè)數(shù)組中的每一個(gè)IMAGE_IMPORT_DESCRIPTOR結(jié)構(gòu),,檢查其中由FirstThunk所指向的IAT表,如果其中有函數(shù)地址和我們要攔截的API函數(shù)地址相同,,就修改它,。 invoke GetModuleHandle,addr DllName ;取得要攔截API所在的DLL名稱 invoke GetProcAddress,eax,addr ApiName mov ProcAddr,eax ;取得我們要攔截的API的地址,并存放在ProcAddr中,。 .while!([esi].OriginalFirstThunk==0 && [esi].TimeDateStamp==0 && [esi].ForwarderChain==0 && [esi].Name1==0 && [esi].FirstThunk==0) ;引入表由一個(gè)全0的IMAGE_IMPORT_DESCRIPTOR作為結(jié)束 mov edi,hMoudle add edi,[esi].FirstThunk ;獲得IAT表的起始地址 assume edi :ptr IMAGE_THUNK_DATA ;假設(shè)edi是指向IMAGE_THUNK_DATA的 .while [edi]!=0 ;檢查IAT表中的每一項(xiàng),如果等于我們要攔截的API地址,則修改 mov ebx,[edi] ;由于IMAGE_THUNK_DATA數(shù)組存放了引入函數(shù)的地址,,所以此時(shí)ebx中是函數(shù)地址 .if ebx==ProcAddr ;如果和我們要攔截的API地址相同 invoke GetCurrentProcess mov ProcHandle,eax ;得到當(dāng)前進(jìn)程的句柄并放在ProcHandle中 invoke VirtualProtectEx,eax,edi,sizeof DWORD,PAGE_READWRITE,addr Old ;修改內(nèi)存屬性 mov eax,offset NewExitProcess ;NewExitProcess是我們自己的API實(shí)現(xiàn)函數(shù) mov NewAddr,eax invoke WriteProcessMemory,ProcHandle,edi,addr NewAddr,sizeof DWORD,NULL ;進(jìn)行改寫 .endif add edi, sizeof IMAGE_THUNK_DATA .endw add esi,sizeof IMAGE_IMPORT_DESCRIPTOR .endw 由模塊起始地址查找IAT表地址的示意圖如下: 在IMAGE_IMPORT_DESCRIPTOR結(jié)構(gòu)中的Name1含有指向DLL名字的RVA,利用它你可以列舉一個(gè)PE文件引用了哪些DLL,。 好,,現(xiàn)在我們已經(jīng)知道實(shí)現(xiàn)API攔截的關(guān)鍵了,但是還有一些問題沒有解決,。 先來說說第一個(gè)問題,,因?yàn)?/span>Windows是不允許一個(gè)進(jìn)程去訪問另一個(gè)進(jìn)程的內(nèi)存空間的,所以我們不能用一個(gè)進(jìn)程去修改另一個(gè)進(jìn)程的IAT表,,要想修改進(jìn)程的IAT表,只能由這個(gè)進(jìn)程自已來做,,一個(gè)已經(jīng)寫好的程序當(dāng)然不會好好地去修改它自身的IAT表,,不過我們可以將我們自己的DLL注入到它的進(jìn)程空間里去,一旦DLL注入到一個(gè)進(jìn)程的內(nèi)存空間中以后,,這個(gè)DLL就成了這個(gè)進(jìn)程的一部分,,它就能夠訪問這個(gè)進(jìn)程的所有的內(nèi)存空間,,當(dāng)然也就能修改它的IAT表了。將一個(gè)DLL注入到一個(gè)目標(biāo)進(jìn)程中去的方法有很多,,但是考慮到兼容性,,用windows提供給我們的系統(tǒng)范圍的windows鉤子來完成DLL注入是最好的。我們可以用SetWindowsHookEx 來安裝一個(gè)系統(tǒng)鉤子,,這個(gè)API的用法如下:HHOOK SetWindowsHookEx( int idHook, // 鉤子類型,,本例中指定為WH_GETMESSAGE鉤子,其它的類型參見MSDN HOOKPROC lpfn, //鉤子的回調(diào)消息函數(shù),。 HINSTANCE hMod, //指定回調(diào)消息函數(shù)所在的DLL句柄,。 DWORD dwThreadId // 鉤子監(jiān)視的線程句柄,本例中因?yàn)橐氖窍到y(tǒng)范圍鉤子,,故設(shè)為0 ); 我們安裝一個(gè)系統(tǒng)鉤子的主要目的是用它來將我們的DLL注入到其它進(jìn)程中去,,所以鉤子的回調(diào)消息函數(shù)并不重要,只要調(diào)用一下CallNextHookEx來向后傳遞鉤子就可以了,。你可以調(diào)用UnhookWindowsHookEx來卸載一個(gè)系統(tǒng)鉤子,,它只要一個(gè)參數(shù):鉤子句柄。 第二個(gè)問題是DLL被注入到目標(biāo)進(jìn)程的內(nèi)存空間中以后,,它在什么時(shí)候進(jìn)行修改呢,?這要用到DLL的入口點(diǎn)函數(shù),每一個(gè)DLL都有一個(gè)入口點(diǎn)函數(shù),,當(dāng)DLL被裝入內(nèi)存時(shí),,或是它從內(nèi)存中卸載時(shí)這個(gè)入口點(diǎn)函數(shù)都會自動地被執(zhí)行,本來入口點(diǎn)函數(shù)主要是做一些初始化工作或是做一些收尾工作的,,我們的API攔截代碼放在這里是最恰當(dāng)?shù)?。因?yàn)橐粋€(gè)單個(gè)進(jìn)程空間是由一個(gè)可執(zhí)行模塊和若干個(gè)DLL模塊組成的,而一個(gè)程序在運(yùn)行時(shí),,加載程序?qū)⒖蓤?zhí)行模塊加載進(jìn)內(nèi)存空間后會接著加載這個(gè)進(jìn)程的所有的DLL模塊,,在加載我們注入的DLL模塊時(shí),入口點(diǎn)函數(shù)自動被執(zhí)行,,進(jìn)行IAT表的修改工作,。此時(shí),進(jìn)程的主線程還沒有開始運(yùn)行,。在進(jìn)程所有的DLL被全部裝入內(nèi)存后,,主線程才開始執(zhí)行,應(yīng)用程序也才開始運(yùn)行,,這時(shí)我們已經(jīng)將它的IAT表修改了,,在它調(diào)用被我們修改了地址的API時(shí),它的調(diào)用就會轉(zhuǎn)到我們自己的函數(shù)中去,,這樣就實(shí)現(xiàn)了API攔截,。DLL的入口點(diǎn)函數(shù)般寫法如下: DllEntry proc hInstDll:HINSTANCE,reason:DWORD,reserved1:DWORD ;DLL的入口點(diǎn)函數(shù) .if reason==DLL_PROCESS_ATTACH ;當(dāng)DLL第一次被裝入時(shí)調(diào)用 push hInstDll pop DllhInst ;保存DLL的句柄在變量DllhInst中 ……… .if reason== DLL_PROCESS_DETACH ;當(dāng)DLL從進(jìn)程空間卸出時(shí)調(diào)用 ……… DllEntry endp 不過,,一般windows是不允許我們動態(tài)修改代碼段的,因?yàn)榇a段一般只具有執(zhí)行屬性而不具有讀寫屬性,,如果我們?nèi)懸粋€(gè)不具備寫屬性的內(nèi)存空間時(shí),,windows會出現(xiàn)一個(gè)保護(hù)性錯誤,所以我們在修改之前必須要使我們想要修改的內(nèi)存地址具有讀寫屬性,,這個(gè)工作可以用VirtualProtectEx來完成,。它的具體參數(shù)在MSDN中有詳細(xì)說明。有一種說法認(rèn)為直接用WriteProcessMemory就能夠修改內(nèi)存,,這個(gè)說法其實(shí)不一定正確,,如果事先不用VirtualProtectEx來修改內(nèi)存屬性的話,WriteProcessMemory并不總是能成功地完成修改,。代碼如下: mov ProcHandle,eax ;得到當(dāng)前進(jìn)程的句柄并放在ProcHandle中 invoke VirtualProtectEx,eax,edi,sizeof DWORD,PAGE_READWRITE,addr Old ;修改內(nèi)存屬性 mov eax,offset NewExitProcess ;NewExitProcess是我們自己的API實(shí)現(xiàn)函數(shù) mov NewAddr,eax invoke WriteProcessMemory,ProcHandle,edi,addr NewAddr,sizeof DWORD,NULL ;進(jìn)行改寫 另外,,如果我們的DLL由于某種原因從內(nèi)存中卸出,這時(shí)目標(biāo)進(jìn)程的IAT中的地址就會變成一個(gè)無效的值,,進(jìn)程如果這時(shí)調(diào)用被攔截API的話就一定會崩潰掉,,所以在DLL被卸出進(jìn)程的內(nèi)存空間時(shí),我們一定要將IAT表中數(shù)據(jù)恢復(fù),。這個(gè)恢復(fù)工作當(dāng)然也是放在DLL的入口點(diǎn)函數(shù),,因?yàn)樵?span lang="EN-US">DLL被卸出時(shí)它也被自動執(zhí)行。 還有一個(gè)問題是如何取得模塊的起始地址,。在PE文件中所用的都是RVA,,只有將RVA加上模塊的起始地址才能得到真正的內(nèi)存地址,而正如我們上面所說,,一個(gè)進(jìn)程的地址空間是由一個(gè)可執(zhí)行模塊和若干個(gè)DLL模塊組成的,,DLL模塊同樣有自己的引入表,我們要攔截的API有可能在可執(zhí)行模塊中被調(diào)用,,也有可能在DLL模塊中被調(diào)用,,所以為了正確的攔截,我們必須列舉出進(jìn)程空間中所有的模塊,,修改它們的IAT表,。這里介紹幾個(gè)需要的API:CreateToolhelp32Snapshot,作用是創(chuàng)建一個(gè)進(jìn)程快照,,它有兩個(gè)參數(shù),,指定第一個(gè)參數(shù)為TH32CS_SNAPMODULE,第二個(gè)參數(shù)為0,,此時(shí)這個(gè)API返回一個(gè)快照句柄,,再利用Module32First和Module32Next這兩個(gè)API就可以列出這個(gè)進(jìn)程中的所有模塊地址。這里要注意的是:我們進(jìn)行修改工作的DLL本身也是進(jìn)程中的一個(gè)模塊,,而且這個(gè)模塊的IAT表中一定會有被攔截的API,,對這個(gè)模塊是不能進(jìn)行修改的,所以在對進(jìn)程中的模塊進(jìn)行修改之前先要判斷一個(gè)這個(gè)模塊是不是這個(gè)DLL自身,,我們可以用VirtualQuery來得到進(jìn)行修改工作的DLL的起始地址,,利用這個(gè)起始地址來判斷當(dāng)前獲取的模塊是不是其自身。代碼如下: invoke VirtualQuery,offset Modify,addr MemBaseinform,sizeof MemBaseinform ;獲取DLL本身所在模塊信息 invoke CreateToolhelp32Snapshot,TH32CS_SNAPMODULE,NULL ;創(chuàng)建一個(gè)進(jìn)程快照,,返回一個(gè)快照句柄 mov snapshot,eax mov module.dwSize,sizeof MODULEENTRY32 ;在調(diào)用Module32First之前先設(shè)置module的大小,,否則調(diào)用會失敗 invoke Module32First,snapshot,addr module ;獲取進(jìn)程中第一個(gè)模塊的信息 .while eax==TRUE ;檢查進(jìn)程空間中每一個(gè)模塊 mov ebx,MemBaseinform.AllocationBase;ebx中存放我們自己的DLL本身的起始地址 .if module.hModule!=ebx invoke Modify,module.hModule ;進(jìn)行修改,module.hModule指定了被修改模塊的起始地址 .endif invoke Module32Next,snapshot,addr module ;取下一個(gè)模塊 .endw 整個(gè)源程序代碼是用宏匯編來寫的,因?yàn)閰R編語言相對于其它的語言來說,,是最直接的一種編程語言,,利用它能夠?qū)栴}說得更清楚一點(diǎn)。在我的例子中我攔截的API是ExitProcess,,當(dāng)然我不會自己去寫一個(gè)ExitProcess,,我只是在ExitProcess的前面加了一段音樂,這樣進(jìn)程在調(diào)用ExitProcess退出時(shí)會先放一段音樂,。為了簡單起見,,代碼中有一部分內(nèi)容沒有實(shí)現(xiàn),比如鉤子的卸載,,DLL被卸出時(shí)對IAT表的恢復(fù),,這些內(nèi)容你可以自己加上去。 DLL部分: apidll.asm (略) DLL文件的DEF文件: apidll.def LIBRARY apidll EXPORTS MouseProc EXPORTS InstallHook 匯編命令:ml /c /coff apidll.asm 連接命令:link /subsystem:windows /section:.bss,RWS /dll /def:apidll.def apidll.obj 以上是DLL部分,,我們必須還需要一個(gè)程序進(jìn)行系統(tǒng)鉤子的安裝工作,。下面的代碼就是系統(tǒng)鉤子的安裝部分: 安裝程序: me.asm (略) 匯編命令:ml /c /coff me.asm 連接命令:link /subsystem:windows me.obj 好,匯編、連接好這個(gè)程序之后,,就可以運(yùn)行了,,這個(gè)安裝程序只提供了安裝鉤子功能,沒有提供卸載鉤子功能,,你可以自己補(bǔ)上,,運(yùn)行這個(gè)程序,按一下命令按鈕,,系統(tǒng)鉤子被裝入系統(tǒng),,這時(shí),API攔截工作已經(jīng)開始,,因?yàn)槲覀儼惭b的是系統(tǒng)范圍的鉤子,,所以此時(shí)系統(tǒng)內(nèi)所有的進(jìn)程都會受到影響。你可以找一個(gè)程序試一下,,因?yàn)檫@篇文章是用word 2000輸入的,,就試試word 2000吧,運(yùn)行word 2000,,好像沒有什么反應(yīng),,這是因?yàn)槲覀償r截的是ExitProcess,,關(guān)閉word 2000,怎么樣,,聽見那段音樂了嗎,? |
|