浅谈Windows API编程 (这个经典)
WinSDK是編程中的傳統(tǒng)難點(diǎn),個(gè)人寫的WinAPI程序也不少了,其實(shí)之所以難就難在每個(gè)調(diào)用的API都包含著Windows這個(gè)操作系統(tǒng)的潛規(guī)則或者是windows內(nèi)部的運(yùn)行機(jī)制……
WinSDK是編程中的傳統(tǒng)難點(diǎn),曾經(jīng)聽(tīng)有一個(gè)技術(shù)不是很好的朋友亂說(shuō)什么給你API誰(shuí)都會(huì)用,其實(shí)并非那么簡(jiǎn)單,個(gè)人寫的WinAPI程序也不少了,其實(shí)之所以難就難在每個(gè)調(diào)用的API都包含著Windows這個(gè)操作系統(tǒng)的潛規(guī)則或者是windows內(nèi)部的運(yùn)行機(jī)制。
首先來(lái)談?wù)劸浔?#xff0c;初學(xué)習(xí)WinSDK的朋友剛看到這個(gè)詞頭大了吧?其實(shí)我也是了,我們來(lái)看看programming windows里面是怎么說(shuō)的,一個(gè)句柄僅僅是用來(lái)識(shí)別某些事情的數(shù)字。它唯一的標(biāo)識(shí)這當(dāng)前的一個(gè)實(shí)例。這樣說(shuō)確實(shí)不容易懂。那么我們這么看,比如你打開(kāi) windows自帶的計(jì)算器。你多打開(kāi)幾次是不是桌面上出現(xiàn)了很多個(gè)計(jì)算器呢?你使用其中一個(gè)計(jì)算器的時(shí)候當(dāng)你按下等于按鈕的時(shí)候運(yùn)算結(jié)果是否會(huì)出現(xiàn)在其他的計(jì)算機(jī)結(jié)果欄里?不會(huì),那windows怎么知道讓結(jié)果出現(xiàn)在哪里呢?這就是句柄的作用了,句柄唯一的標(biāo)識(shí)著一個(gè)程序,你打開(kāi)的每一個(gè)窗口(計(jì)算器) 都有一個(gè)不同的句柄你你每一步操作都是指定了在某個(gè)句柄下的,所以,他不會(huì)出錯(cuò)。而且你打開(kāi)的每一個(gè)計(jì)算機(jī)都共享著同樣的代碼和內(nèi)存。通過(guò)句柄系統(tǒng)會(huì)把所需的資源充分的調(diào)用到當(dāng)前的某個(gè)程序自己的數(shù)據(jù)區(qū)。
不僅是窗口,各種菜單,GDI對(duì)象都有自己的句柄,獲取句柄的手段也是多重多樣,不過(guò)當(dāng)然是通過(guò)調(diào)用API函數(shù)實(shí)現(xiàn)了,如:
MFC中的hHandle = GetSafeHandle();
API編程中的hBrush = GetStorkObject(BLACK_BRUSH);
很多操作都需要將句柄添加到參數(shù)列表中,當(dāng)你沒(méi)有直接定義句柄變量的時(shí)候可能要記憶很多API的返回類型來(lái)間接獲取。如:
??????hPen = SelectObject(hdc,GetStockObject(&logicpen));
? ? ? // SelectObject()這個(gè)函數(shù)在設(shè)置本設(shè)備描述表下的GDI對(duì)象時(shí)會(huì)返回設(shè)置前的GDI對(duì)象句柄
? ? ?MoveToEx(hdc, pt1.x, pt1.y, &apt);
? ? ?LineTo(hdc, pt2.x,pt2.y);
? ? ?SelectObject(hdc,hPen);
完成選擇自定義的GDI對(duì)象的操作。句柄的種類很多,掌握一種的使用方法所有的不學(xué)自通,WinAPI編程永遠(yuǎn)伴隨的元素中句柄是其中之一。非常重要。由于是淺談,所以就說(shuō)到這里了.
接下來(lái)是windows下的消息映射機(jī)制了,呵呵,窗口過(guò)程,剛學(xué)的朋友難理解吧?WinSDK編程基于C,但是和C的理念有著完全的不同,這中間的不同,在我看來(lái)最多的也就是來(lái)自于這個(gè)消息映射,后面什么吹的很炫的Hook技術(shù),木馬技術(shù),鍵盤截獲,都是來(lái)自于特殊消息的捕捉,映射自定義的特殊消息來(lái)實(shí)現(xiàn)的(當(dāng)然和我接下來(lái)談的稍微有點(diǎn)不同)。
首先我們應(yīng)該先明白消息和事件的區(qū)別,Windows是消息驅(qū)動(dòng)的操作系統(tǒng),這里的消息的產(chǎn)生來(lái)自于某個(gè)實(shí)例化的對(duì)象上用戶的操作,來(lái)自控件,菜單,或者是系統(tǒng)本身產(chǎn)生的,而事件是靠消息觸發(fā)的,但這也不是絕對(duì)的。可以用一個(gè)簡(jiǎn)單的例子去解釋,我這里越寫越覺(jué)得自己難表達(dá)清楚,就比如這么一個(gè)例子:“某男殺人這條消息導(dǎo)致被槍斃這個(gè)事件”不過(guò)最重要的區(qū)別是在消息產(chǎn)生后并不會(huì)被直接處理,而是先插入windows系統(tǒng)的消息隊(duì)列,然后系統(tǒng)判斷此消息產(chǎn)生于哪個(gè)程序,送入此程序的消息循環(huán),由LRSULT CALLBACK winprc(hwnd , uint,wParam,lParam)處理。而事件是操作系統(tǒng)處理消息的過(guò)程中反饋的結(jié)果。
用戶操作-> 產(chǎn)生消息->發(fā)送系統(tǒng)->系統(tǒng)判斷來(lái)源->發(fā)給相應(yīng)的窗口過(guò)程或者其他Callback函數(shù)->消息處理->等待下一條消息的產(chǎn)生
以上為消息循環(huán)整個(gè)過(guò)程。
?????
以上是最基本的WinAPi編程的代碼結(jié)構(gòu)。其實(shí)這里面最重要的結(jié)構(gòu)莫過(guò)于while(GetMessage(&msg))和 Winproc這個(gè)函數(shù),這也是傳統(tǒng)的C面向過(guò)程編程的區(qū)別,win編程總等著特定事件觸發(fā)對(duì)應(yīng)的消息映射函數(shù)來(lái)完成代碼功能,并不是一條代碼從頭走到尾。關(guān)于特殊消息的映射,這里不談,這里僅是個(gè)入門指引。
最后談一點(diǎn)就是重繪問(wèn)題。其實(shí)在我看來(lái)這個(gè)東西更多是屬于GDI編程里面的東西,說(shuō)起來(lái)其實(shí)難度不大,但是處理起來(lái)確實(shí)是個(gè)難點(diǎn)。先拿剛才的代碼來(lái)說(shuō)吧。先添加一條關(guān)于WM_LBUTTONDOWN的消息映射:
?
這段代碼實(shí)現(xiàn)一個(gè)簡(jiǎn)單的畫線功能,當(dāng)你在你的客戶區(qū)胡點(diǎn)一通鼠標(biāo)后試著拖動(dòng)一下窗口大小,或者將其最小化或者被其他窗口覆蓋一下你都會(huì)發(fā)現(xiàn)你原來(lái)畫的線沒(méi)了,可是其他窗口為什么被覆蓋了以后再?gòu)棾龃翱谶€會(huì)有原來(lái)的東西呢?那就是重繪,要重新繪制整個(gè)客戶區(qū)(準(zhǔn)確的說(shuō)是失效的矩形),以上說(shuō)的操作都會(huì)導(dǎo)致你的客戶區(qū)失效,這時(shí)會(huì)產(chǎn)生重繪消息WM_PAINT,我們要想保存這些線那么我們就必須保存這些你用鼠標(biāo)左鍵點(diǎn)過(guò)的點(diǎn)。當(dāng)然這是重繪技術(shù)中最簡(jiǎn)單的,當(dāng)你的客戶區(qū)上是一個(gè)復(fù)雜的畫面的話,就不僅僅需要保存點(diǎn),還有各種形狀的圖形,顏色等等……這里給大家一段我自己寫的代碼來(lái)實(shí)現(xiàn)以上的 WM_LBUTTONDOWN消息映射來(lái)產(chǎn)生的點(diǎn)。通過(guò)單鏈表來(lái)動(dòng)態(tài)添加點(diǎn)來(lái)實(shí)現(xiàn)重繪。
其中的重繪函數(shù)代碼如下:
添加了以上的代碼你會(huì)發(fā)現(xiàn)再次拖動(dòng)窗口大小等等你原來(lái)畫的線就都能重現(xiàn)出來(lái)了。呵呵是不是覺(jué)得一個(gè)看似簡(jiǎn)單的東西其實(shí)里面需要很多代碼實(shí)現(xiàn)呢?也許,這就是windows.
好了,WinSDK入門的東西就談這么多,希望能給初學(xué)者一定的幫助,這么多的字,都是我一個(gè)一個(gè)打出來(lái)沒(méi)有任何借鑒和摘抄的。相信做為一個(gè)過(guò)來(lái)人能更多的理解大家學(xué)習(xí)中的困難。
Win32環(huán)境下動(dòng)態(tài)鏈接庫(kù)(DLL)編程原理
比較大應(yīng)用程序都由很多模塊組成,這些模塊分別完成相對(duì)獨(dú)立的功能,它們彼此協(xié)作來(lái)完成整個(gè)軟件系統(tǒng)的工作。其中可能存在一些模塊的功能較為通用,在構(gòu)造其它軟件系統(tǒng)時(shí)仍會(huì)被使用。在構(gòu)造軟件系統(tǒng)時(shí),如果將所有模塊的源代碼都靜態(tài)編譯到整個(gè)應(yīng)用程序EXE文件中,會(huì)產(chǎn)生一些問(wèn)題:一個(gè)缺點(diǎn)是增加了應(yīng)用程序的大小,它會(huì)占用更多的磁盤空間,程序運(yùn)行時(shí)也會(huì)消耗較大的內(nèi)存空間,造成系統(tǒng)資源的浪費(fèi);另一個(gè)缺點(diǎn)是,在編寫大的EXE程序時(shí),在每次修改重建時(shí)都必須調(diào)整編譯所有源代碼,增加了編譯過(guò)程的復(fù)雜性,也不利于階段性的單元測(cè)試。
Windows系統(tǒng)平臺(tái)上提供了一種完全不同的較有效的編程和運(yùn)行環(huán)境,你可以將獨(dú)立的程序模塊創(chuàng)建為較小的DLL(Dynamic Linkable Library)文件,并可對(duì)它們單獨(dú)編譯和測(cè)試。在運(yùn)行時(shí),只有當(dāng)EXE程序確實(shí)要調(diào)用這些DLL模塊的情況下,系統(tǒng)才會(huì)將它們裝載到內(nèi)存空間中。這種方式不僅減少了EXE文件的大小和對(duì)內(nèi)存空間的需求,而且使這些DLL模塊可以同時(shí)被多個(gè)應(yīng)用程序使用。Microsoft Windows自己就將一些主要的系統(tǒng)功能以DLL模塊的形式實(shí)現(xiàn)。例如IE中的一些基本功能就是由DLL文件實(shí)現(xiàn)的,它可以被其它應(yīng)用程序調(diào)用和集成。
一般來(lái)說(shuō),DLL是一種磁盤文件(通常帶有DLL擴(kuò)展名),它由全局?jǐn)?shù)據(jù)、服務(wù)函數(shù)和資源組成,在運(yùn)行時(shí)被系統(tǒng)加載到進(jìn)程的虛擬空間中,成為調(diào)用進(jìn)程的一部分。如果與其它DLL之間沒(méi)有沖突,該文件通常映射到進(jìn)程虛擬空間的同一地址上。DLL模塊中包含各種導(dǎo)出函數(shù),用于向外界提供服務(wù)。Windows 在加載DLL模塊時(shí)將進(jìn)程函數(shù)調(diào)用與DLL文件的導(dǎo)出函數(shù)相匹配。
在Win32環(huán)境中,每個(gè)進(jìn)程都復(fù)制了自己的讀/寫全局變量。如果想要與其它進(jìn)程共享內(nèi)存,必須使用內(nèi)存映射文件或者聲明一個(gè)共享數(shù)據(jù)段。DLL模塊需要的堆棧內(nèi)存都是從運(yùn)行進(jìn)程的堆棧中分配出來(lái)的。
DLL現(xiàn)在越來(lái)越容易編寫。Win32已經(jīng)大大簡(jiǎn)化了其編程模式,并有許多來(lái)自AppWizard和MFC類庫(kù)的支持。
一、導(dǎo)出和導(dǎo)入函數(shù)的匹配
DLL文件中包含一個(gè)導(dǎo)出函數(shù)表。這些導(dǎo)出函數(shù)由它們的符號(hào)名和稱為標(biāo)識(shí)號(hào)的整數(shù)與外界聯(lián)系起來(lái)。函數(shù)表中還包含了DLL中函數(shù)的地址。當(dāng)應(yīng)用程序加載 DLL模塊時(shí)時(shí),它并不知道調(diào)用函數(shù)的實(shí)際地址,但它知道函數(shù)的符號(hào)名和標(biāo)識(shí)號(hào)。動(dòng)態(tài)鏈接過(guò)程在加載的DLL模塊時(shí)動(dòng)態(tài)建立一個(gè)函數(shù)調(diào)用與函數(shù)地址的對(duì)應(yīng)表。如果重新編譯和重建DLL文件,并不需要修改應(yīng)用程序,除非你改變了導(dǎo)出函數(shù)的符號(hào)名和參數(shù)序列。
簡(jiǎn)單的DLL文件只為應(yīng)用程序提供導(dǎo)出函數(shù),比較復(fù)雜的DLL文件除了提供導(dǎo)出函數(shù)以外,還調(diào)用其它DLL文件中的函數(shù)。這樣,一個(gè)特殊的DLL可以既有導(dǎo)入函數(shù),又有導(dǎo)入函數(shù)。這并不是一個(gè)問(wèn)題,因?yàn)閯?dòng)態(tài)鏈接過(guò)程可以處理交叉相關(guān)的情況。
在DLL代碼中,必須像下面這樣明確聲明導(dǎo)出函數(shù):
__declspec(dllexport) int MyFunction(int n);
但也可以在模塊定義(DEF)文件中列出導(dǎo)出函數(shù),不過(guò)這樣做常常引起更多的麻煩。在應(yīng)用程序方面,要求像下面這樣明確聲明相應(yīng)的輸入函數(shù):
__declspec(dllimport) int MyFuncition(int n);
僅有導(dǎo)入和導(dǎo)出聲明并不能使應(yīng)用程序內(nèi)部的函數(shù)調(diào)用鏈接到相應(yīng)的DLL文件上。應(yīng)用程序的項(xiàng)目必須為鏈接程序指定所需的輸入庫(kù)(LIB文件)。而且應(yīng)用程序事實(shí)上必須至少包含一個(gè)對(duì)DLL函數(shù)的調(diào)用。
二、與DLL模塊建立鏈接
應(yīng)用程序?qū)牒瘮?shù)與DLL文件中的導(dǎo)出函數(shù)進(jìn)行鏈接有兩種方式:隱式鏈接和顯式鏈接。所謂的隱式鏈接是指在應(yīng)用程序中不需指明DLL文件的實(shí)際存儲(chǔ)路徑,程序員不需關(guān)心DLL文件的實(shí)際裝載。而顯式鏈接與此相反。
采用隱式鏈接方式,程序員在建立一個(gè)DLL文件時(shí),鏈接程序會(huì)自動(dòng)生成一個(gè)與之對(duì)應(yīng)的LIB導(dǎo)入文件。該文件包含了每一個(gè)DLL導(dǎo)出函數(shù)的符號(hào)名和可選的標(biāo)識(shí)號(hào),但是并不含有實(shí)際的代碼。LIB文件作為DLL的替代文件被編譯到應(yīng)用程序項(xiàng)目中。當(dāng)程序員通過(guò)靜態(tài)鏈接方式編譯生成應(yīng)用程序時(shí),應(yīng)用程序中的調(diào)用函數(shù)與LIB文件中導(dǎo)出符號(hào)相匹配,這些符號(hào)或標(biāo)識(shí)號(hào)進(jìn)入到生成的EXE文件中。LIB文件中也包含了對(duì)應(yīng)的DLL文件名(但不是完全的路徑名),鏈接程序?qū)⑵浯鎯?chǔ)在EXE文件內(nèi)部。當(dāng)應(yīng)用程序運(yùn)行過(guò)程中需要加載DLL文件時(shí),Windows根據(jù)這些信息發(fā)現(xiàn)并加載DLL,然后通過(guò)符號(hào)名或標(biāo)識(shí)號(hào)實(shí)現(xiàn)對(duì)DLL函數(shù)的動(dòng)態(tài)鏈接。
顯式鏈接方式對(duì)于集成化的開(kāi)發(fā)語(yǔ)言(例如VB)比較適合。有了顯式鏈接,程序員就不必再使用導(dǎo)入文件,而是直接調(diào)用Win32 的LoadLibary函數(shù),并指定DLL的路徑作為參數(shù)。LoadLibary返回HINSTANCE參數(shù),應(yīng)用程序在調(diào)用 GetProcAddress函數(shù)時(shí)使用這一參數(shù)。GetProcAddress函數(shù)將符號(hào)名或標(biāo)識(shí)號(hào)轉(zhuǎn)換為DLL內(nèi)部的地址。假設(shè)有一個(gè)導(dǎo)出如下函數(shù)的 DLL文件:
extern "C" __declspec(dllexport) double SquareRoot(double d);
下面是應(yīng)用程序?qū)υ搶?dǎo)出函數(shù)的顯式鏈接的例子:
?
在隱式鏈接方式中,所有被應(yīng)用程序調(diào)用的DLL文件都會(huì)在應(yīng)用程序EXE文件加載時(shí)被加載在到內(nèi)存中;但如果采用顯式鏈接方式,程序員可以決定DLL文件何時(shí)加載或不加載。顯式鏈接在運(yùn)行時(shí)決定加載哪個(gè)DLL文件。例如,可以將一個(gè)帶有字符串資源的DLL模塊以英語(yǔ)加載,而另一個(gè)以西班牙語(yǔ)加載。應(yīng)用程序在用戶選擇了合適的語(yǔ)種后再加載與之對(duì)應(yīng)的DLL文件。
三、使用符號(hào)名鏈接與標(biāo)識(shí)號(hào)鏈接
在Win16環(huán)境中,符號(hào)名鏈接效率較低,所有那時(shí)標(biāo)識(shí)號(hào)鏈接是主要的鏈接方式。在Win32環(huán)境中,符號(hào)名鏈接的效率得到了改善。Microsoft 現(xiàn)在推薦使用符號(hào)名鏈接。但在MFC庫(kù)中的DLL版本仍然采用的是標(biāo)識(shí)號(hào)鏈接。一個(gè)典型的MFC程序可能會(huì)鏈接到數(shù)百個(gè)MFC DLL函數(shù)上。采用標(biāo)識(shí)號(hào)鏈接的應(yīng)用程序的EXE文件體相對(duì)較小,因?yàn)樗槐匕瑢?dǎo)入函數(shù)的長(zhǎng)字符串符號(hào)名。
四、編寫DllMain函數(shù)
DllMain函數(shù)是DLL模塊的默認(rèn)入口點(diǎn)。當(dāng)Windows加載DLL模塊時(shí)調(diào)用這一函數(shù)。系統(tǒng)首先調(diào)用全局對(duì)象的構(gòu)造函數(shù),然后調(diào)用全局函數(shù) DLLMain。DLLMain函數(shù)不僅在將DLL鏈接加載到進(jìn)程時(shí)被調(diào)用,在DLL模塊與進(jìn)程分離時(shí)(以及其它時(shí)候)也被調(diào)用。下面是一個(gè)框架 DLLMain函數(shù)的例子。
?
如果程序員沒(méi)有為DLL模塊編寫一個(gè)DLLMain函數(shù),系統(tǒng)會(huì)從其它運(yùn)行庫(kù)中引入一個(gè)不做任何操作的缺省DLLMain函數(shù)版本。在單個(gè)線程啟動(dòng)和終止時(shí),DLLMain函數(shù)也被調(diào)用。正如由dwReason參數(shù)所表明的那樣。
五、模塊句柄
進(jìn)程中的每個(gè)DLL模塊被全局唯一的32字節(jié)的HINSTANCE句柄標(biāo)識(shí)。進(jìn)程自己還有一個(gè)HINSTANCE句柄。所有這些模塊句柄都只有在特定的進(jìn)程內(nèi)部有效,它們代表了DLL或EXE模塊在進(jìn)程虛擬空間中的起始地址。在Win32中,HINSTANCE和HMODULE的值是相同的,這個(gè)兩種類型可以替換使用。進(jìn)程模塊句柄幾乎總是等于0x400000,而DLL模塊的加載地址的缺省句柄是0x10000000。如果程序同時(shí)使用了幾個(gè)DLL模塊,每一個(gè)都會(huì)有不同的HINSTANCE值。這是因?yàn)樵趧?chuàng)建DLL文件時(shí)指定了不同的基地址,或者是因?yàn)榧虞d程序?qū)LL代碼進(jìn)行了重定位。
模塊句柄對(duì)于加載資源特別重要。Win32 的FindResource函數(shù)中帶有一個(gè)HINSTANCE參數(shù)。EXE和DLL都有其自己的資源。如果應(yīng)用程序需要來(lái)自于DLL的資源,就將此參數(shù)指定為DLL的模塊句柄。如果需要EXE文件中包含的資源,就指定EXE的模塊句柄。
但是在使用這些句柄之前存在一個(gè)問(wèn)題,你怎樣得到它們呢?如果需要得到EXE模塊句柄,調(diào)用帶有Null參數(shù)的Win32函數(shù)GetModuleHandle;如果需要DLL模塊句柄,就調(diào)用以DLL文件名為參數(shù)的Win32函數(shù)GetModuleHandle。
六、應(yīng)用程序怎樣找到DLL文件
如果應(yīng)用程序使用LoadLibrary顯式鏈接,那么在這個(gè)函數(shù)的參數(shù)中可以指定DLL文件的完整路徑。如果不指定路徑,或是進(jìn)行隱式鏈接,Windows將遵循下面的搜索順序來(lái)定位DLL:
1. 包含EXE文件的目錄,
2. 進(jìn)程的當(dāng)前工作目錄,
3. Windows系統(tǒng)目錄,
4. Windows目錄,
5. 列在Path環(huán)境變量中的一系列目錄。
這里有一個(gè)很容易發(fā)生錯(cuò)誤的陷阱。如果你使用VC++進(jìn)行項(xiàng)目開(kāi)發(fā),并且為DLL模塊專門創(chuàng)建了一個(gè)項(xiàng)目,然后將生成的DLL文件拷貝到系統(tǒng)目錄下,從應(yīng)用程序中調(diào)用DLL模塊。到目前為止,一切正常。接下來(lái)對(duì)DLL模塊做了一些修改后重新生成了新的DLL文件,但你忘記將新的DLL文件拷貝到系統(tǒng)目錄下。下一次當(dāng)你運(yùn)行應(yīng)用程序時(shí),它仍加載了老版本的DLL文件,這可要當(dāng)心!
?
七、調(diào)試DLL程序
Microsoft 的VC++是開(kāi)發(fā)和測(cè)試DLL的有效工具,只需從DLL項(xiàng)目中運(yùn)行調(diào)試程序即可。當(dāng)你第一次這樣操作時(shí),調(diào)試程序會(huì)向你詢問(wèn)EXE文件的路徑。此后每次在調(diào)試程序中運(yùn)行DLL時(shí),調(diào)試程序會(huì)自動(dòng)加載該EXE文件。然后該EXE文件用上面的搜索序列發(fā)現(xiàn)DLL文件,這意味著你必須設(shè)置Path環(huán)境變量讓其包含DLL文件的磁盤路徑,或者也可以將DLL文件拷貝到搜索序列中的目錄路徑下。
HOOK API是一個(gè)永恒的話題,如果沒(méi)有HOOK,許多技術(shù)將很難實(shí)現(xiàn),也許根本不能實(shí)現(xiàn)。這里所說(shuō)的API,是廣義上的API,它包括DOS下的中斷, WINDOWS里的API、中斷服務(wù)、IFS和NDIS過(guò)濾等。比如大家熟悉的即時(shí)翻譯軟件,就是靠HOOK TextOut()或ExtTextOut()這兩個(gè)函數(shù)實(shí)現(xiàn)的,在操作系統(tǒng)用這兩個(gè)函數(shù)輸出文本之前,就把相應(yīng)的英文替換成中文而達(dá)到即時(shí)翻譯;IFS 和NDIS過(guò)濾也是如此,在讀寫磁盤和收發(fā)數(shù)據(jù)之前,系統(tǒng)會(huì)調(diào)用第三方提供的回調(diào)函數(shù)來(lái)判斷操作是否可以放行,它與普通HOOK不同,它是操作系統(tǒng)允許的,由操作系統(tǒng)提供接口來(lái)安裝回調(diào)函數(shù)。
甚至如果沒(méi)有HOOK,就沒(méi)有病毒,因?yàn)椴还苁荄OS下的病毒或WINDOWS里的病毒,都是靠HOOK系統(tǒng)服務(wù)來(lái)實(shí)現(xiàn)自己的功能的:DOS下的病毒靠HOOK INT 21來(lái)感染文件(文件型病毒),靠HOOK INT 13來(lái)感染引導(dǎo)扇區(qū)(引導(dǎo)型病毒);WINDOWS下的病毒靠HOOK系統(tǒng)API(包括RING0層的和RING3層的),或者安裝IFS(CIH病毒所用的方法)來(lái)感染文件。因此可以說(shuō)“沒(méi)有HOOK,就沒(méi)有今天多姿多彩的軟件世界”。
由于涉及到專利和知識(shí)產(chǎn)權(quán),或者是商業(yè)機(jī)密,微軟一直不提倡大家HOOK它的系統(tǒng)API,提供IFS和NDIS等其他過(guò)濾接口,也是為了適應(yīng)殺毒軟件和防火墻的需要才開(kāi)放的。所以在大多數(shù)時(shí)候,HOOK API要靠自己的力量來(lái)完成。
HOOK API有一個(gè)原則,這個(gè)原則就是:被HOOK的API的原有功能不能受到任何影響。就象醫(yī)生救人,如果把病人身體里的病毒殺死了,病人也死了,那么這個(gè) “救人”就沒(méi)有任何意義了。如果你HOOK API之后,你的目的達(dá)到了,但API的原有功能失效了,這樣不是HOOK,而是REPLACE,操作系統(tǒng)的正常功能就會(huì)受到影響,甚至?xí)罎ⅰ?br />
HOOK API的技術(shù),說(shuō)起來(lái)也不復(fù)雜,就是改變程序流程的技術(shù)。在CPU的指令里,有幾條指令可以改變程序的流程:JMP,CALL,INT,RET, RETF,IRET等指令。理論上只要改變API入口和出口的任何機(jī)器碼,都可以HOOK,但是實(shí)際實(shí)現(xiàn)起來(lái)要復(fù)雜很多,因?yàn)橐幚砗靡韵聠?wèn)題:
1,CPU指令長(zhǎng)度問(wèn)題,在32位系統(tǒng)里,一條JMP/CALL指令的長(zhǎng)度是5個(gè)字節(jié),因此你只有替換API里超過(guò)5個(gè)字節(jié)長(zhǎng)度的機(jī)器碼(或者替換幾條指令長(zhǎng)度加起來(lái)是5字節(jié)的指令),否則會(huì)影響被更改的小于5個(gè)字節(jié)的機(jī)器碼后面的數(shù)條指令,甚至程序流程會(huì)被打亂,產(chǎn)生不可預(yù)料的后果;
2,參數(shù)問(wèn)題,為了訪問(wèn)原API的參數(shù),你要通過(guò)EBP或ESP來(lái)引用參數(shù),因此你要非常清楚你的HOOK代碼里此時(shí)的EBP/ESP的值是多少;
3,時(shí)機(jī)的問(wèn)題,有些HOOK必須在API的開(kāi)頭,有些必須在API的尾部,比如HOOK CreateFilaA(),如果你在API尾部HOOK API,那么此時(shí)你就不能寫文件,甚至不能訪問(wèn)文件;HOOK RECV(),如果你在API頭HOOK,此時(shí)還沒(méi)有收到數(shù)據(jù),你就去查看RECV()的接收緩沖區(qū),里面當(dāng)然沒(méi)有你想要的數(shù)據(jù),必須等RECV()正常執(zhí)行后,在RECV()的尾部HOOK,此時(shí)去查看RECV()的緩沖區(qū),里面才有想要的數(shù)據(jù);
4,上下文的問(wèn)題,有些HOOK代碼不能執(zhí)行某些操作,否則會(huì)破壞原API的上下文,原API就失效了;
5,同步問(wèn)題,在HOOK代碼里盡量不使用全局變量,而使用局部變量,這樣也是模塊化程序的需要;
6,最后要注意的是,被替換的CPU指令的原有功能一定要在HOOK代碼的某個(gè)地方模擬實(shí)現(xiàn)。
下面以ws2_32.dll里的send()為例子來(lái)說(shuō)明如何HOOK這個(gè)函數(shù):?
下面用4種方法來(lái)HOOK這個(gè)API:
1,把API入口的第一條指令是PUSH EBP指令(機(jī)器碼0x55)替換成INT 3(機(jī)器碼0xcc),然后用WINDOWS提供的調(diào)試函數(shù)來(lái)執(zhí)行自己的代碼,這中方法被SOFT ICE等DEBUGER廣泛采用,它就是通過(guò)BPX在相應(yīng)的地方設(shè)一條INT 3指令來(lái)下斷點(diǎn)的。但是不提倡用這種方法,因?yàn)樗鼤?huì)與WINDOWS或調(diào)試工具產(chǎn)生沖突,而匯編代碼基本都要調(diào)試;
2,把第二條mov ebp,esp指令(機(jī)器碼8BEC,2字節(jié))替換為INT F0指令(機(jī)器碼CDF0),然后在IDT里設(shè)置一個(gè)中斷門,指向我們的代碼。我這里給出一個(gè)HOOK代碼:
lea ebp,[esp+12] //模擬原指令mov ebp,esp的功能
pushfd?????????????//保存現(xiàn)場(chǎng)
pushad?????????????//保存現(xiàn)場(chǎng)
//在這里做你想做的事情
popad?????????????//恢復(fù)現(xiàn)場(chǎng)
popfd?????????????//恢復(fù)現(xiàn)場(chǎng)
iretd?????????????//返回原指令的下一條指令繼續(xù)執(zhí)行原函數(shù)(71A21AF7地址處)
這種方法很好,但缺點(diǎn)是要在IDT設(shè)置一個(gè)中斷門,也就是要進(jìn)RING0。
3,更改CALL指令的相對(duì)地址(CALL分別在71A21B12、71A21B25、71A21B64,但前面2條CALL之前有一個(gè)條件跳轉(zhuǎn)指令,有可能不被執(zhí)行到,因此我們要HOOK 71A21B64處的CALL指令)。為什么要找CALL指令下手?因?yàn)樗鼈兌际?字節(jié)的指令,而且都是CALL指令,只要保持操作碼0xE8不變,改變后面的相對(duì)地址就可以轉(zhuǎn)到我們的HOOK代碼去執(zhí)行了,在我們的HOOK代碼后面再轉(zhuǎn)到目標(biāo)地址去執(zhí)行。
假設(shè)我們的HOOK代碼在71A20400處,那么我們把71A21B64處的CALL指令改為CALL 71A20400(原指令是這樣的:CALL 71A21230)
而71A20400處的HOOK代碼是這樣的:
71A20400:
pushad
//在這里做你想做的事情
popad
jmp 71A21230???????//跳轉(zhuǎn)到原CALL指令的目標(biāo)地址,原指令是這樣的:call 71A21230
這種方法隱蔽性很好,但是比較難找這條5字節(jié)的CALL指令,計(jì)算相對(duì)地址也復(fù)雜。
4,替換71A21AFE地址上的cmp dword ptr [71A3201C], 71A21C93指令(機(jī)器碼:813D1C20A371931CA271,10字節(jié))成為
call 71A20400
nop
nop
nop
nop
nop
(機(jī)器碼:E8 XX XX XX XX 90 90 90 90 90,10字節(jié))
在71A20400的HOOK代碼是:
pushad
mov edx,71A3201Ch???????????????//模擬原指令cmp dword ptr [71A3201C], 71A21C93
cmp dword ptr [edx],71A21C93h???????//模擬原指令cmp dword ptr [71A3201C], 71A21C93
pushfd
//在這里做你想做的事
popfd
popad
ret
這種方法隱蔽性最好,但不是每個(gè)API都有這樣的指令,要具體情況具體操作。
以上幾種方法是常用的方法,值得一提的是很多人都是改API開(kāi)頭的5個(gè)字節(jié),但是現(xiàn)在很多殺毒軟件用這樣的方法檢查API是否被HOOK,或其他病毒木馬在你之后又改了前5個(gè)字節(jié),這樣就會(huì)互相覆蓋,最
APIHook一直是使大家感興趣的話題。屏幕取詞,內(nèi)碼轉(zhuǎn)化,屏幕翻譯,中文平臺(tái)等等都涉及到了此項(xiàng)技術(shù)。有很多文章涉及到了這項(xiàng)技術(shù),但都閃爍其詞不肯明明白白的公布。我僅在這里公布以下我用Delphi制作APIHook的一些心得。
?????????通常的APIHOOK有這樣幾種方法:
????????1、自己寫一個(gè)動(dòng)態(tài)鏈接庫(kù),里面定義自己寫的想取代系統(tǒng)的API。把這個(gè)動(dòng)態(tài)鏈接庫(kù)映射到2G以上的系統(tǒng)動(dòng)態(tài)鏈接庫(kù)所在空間,把系統(tǒng)動(dòng)態(tài)鏈接庫(kù)中的該API的指向修改指向自己的函數(shù)。這種方法的好處就是可以取代系統(tǒng)中運(yùn)行全部程序的該API。但他有個(gè)局限,就是只適用于Win9x。(原因是NT中動(dòng)態(tài)鏈接庫(kù)不是共享的,每個(gè)進(jìn)程都有自己的一份動(dòng)態(tài)鏈接庫(kù)在內(nèi)存中的映射)
????????2、自己寫一個(gè)動(dòng)態(tài)鏈接庫(kù),里面定義自己寫得象替代系統(tǒng)的API。把這個(gè)動(dòng)態(tài)鏈接庫(kù)映射到進(jìn)程的空間里。將該進(jìn)程對(duì)API的調(diào)用指向自己寫的動(dòng)態(tài)鏈接庫(kù)。這種方法的好處是可以選擇性的替代哪個(gè)進(jìn)程的API。而且適用于所有的Windows操作系統(tǒng)。
????????這里我選用的是第二種方法。
????????第二種方法需要先了解一點(diǎn)PE文件格式的知識(shí)。
?????????首先是一個(gè)實(shí)模式的的DOS文件頭,是為了保持和DOS的兼容。
?????????接著是一個(gè)DOS的代理模塊。你在純DOS先運(yùn)行Win32的可執(zhí)行文件,看看是不是也執(zhí)行了,只是顯示的的是一行信息大意是說(shuō)該Windows程序不能在DOS實(shí)模式下運(yùn)行。
?????????然后才是真正意義上的Windows可執(zhí)行文件的文件頭。它的具體位置不是每次都固定的。是由文件偏移$3C決定的。我們要用到的就是它。
?????????如果我們?cè)诔绦蛑姓{(diào)用了一個(gè)MessageBoxA函數(shù)那么它的實(shí)現(xiàn)過(guò)程是這樣的。他先調(diào)用在本進(jìn)程中的MessageBoxA函數(shù)然后才跳到動(dòng)態(tài)鏈接庫(kù)的MessageBoxA的入口點(diǎn)。即:
?????????call messageBoxA(0040106c)
?????????jmp dword ptr [_jmp_MessageBoxA@16(00425294)]
其中00425294的內(nèi)容存儲(chǔ)的就是就是MessageBoxA函數(shù)的入口地址。如果我們做一下手腳,那么......
????????那就開(kāi)始吧!
我們需要定義兩個(gè)結(jié)構(gòu)
type
?????PImage_Import_Entry = ^Image_Import_Entry;
?????Image_Import_Entry = record
????????Characteristics: DWORD;
????????TimeDateStamp: DWORD;
????????MajorVersion: Word;
????????MinorVersion: Word;
????????Name: DWORD;
????????LookupTable: DWORD;
?????end;
type
?????TImportCode = packed record
????????JumpInstruction: Word; file: //定義跳轉(zhuǎn)指令jmp
????????AddressOfPointerToFunction: ^Pointer; file: //定義要跳轉(zhuǎn)到的函數(shù)
?????end;
?????PImportCode = ^TImportCode;
然后是確定函數(shù)的地址。
function LocateFunctionAddress(Code: Pointer): Pointer;
var
?????func: PImportCode;
begin
?????Result := Code;
?????if Code = nil then exit;
?????try
????????func := code;
????????if (func.JumpInstruction = $25FF) then
????????begin
???????????Result := func.AddressOfPointerToFunction^;
????????end;
?????except
????????Result := nil;
?????end;
end;
參數(shù)Code是函數(shù)在進(jìn)程中的指針,即那條Jmp XXX的指令。$25FF就是跳轉(zhuǎn)指令的機(jī)器碼。
在這里我將要實(shí)現(xiàn)轉(zhuǎn)跳。有人說(shuō)修改內(nèi)存內(nèi)容要進(jìn)入Ring 0 才可以。可是Windows本身提供了一個(gè)寫內(nèi)存的指令WriteProcessMemory。有了這把利器,我們幾乎無(wú)所不能。如游戲的修改等在這里我們只談APIHOOK。
function RepointFunction(OldFunc, NewFunc: Pointer): Integer;
var
????IsDone: TList;
????function RepointAddrInModule(hModule: THandle; OldFunc, NewFunc: Pointer): Integer;
????var
???????Dos: PImageDosHeader;
???????NT: PImageNTHeaders;
???????ImportDesc: PImage_Import_Entry;
???????RVA: DWORD;
???????Func: ^Pointer;
???????DLL: string;
???????f: Pointer;
???????written: DWORD;
????begin
???????Result := 0;
???????Dos := Pointer(hModule);
???????if IsDone.IndexOf(Dos) >= 0 then exit;
???????IsDone.Add(Dos);
???????OldFunc := LocateFunctionAddress(OldFunc);
???????if IsBadReadPtr(Dos, SizeOf(TImageDosHeader)) then exit;
???????if Dos.e_magic <> IMAGE_DOS_SIGNATURE then exit;
???????NT := Pointer(Integer(Dos) + dos._lfanew);
???????RVA := NT^.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]
??????????.VirtualAddress;
???????if RVA = 0 then exit;
???????ImportDesc := pointer(integer(Dos) + RVA);
???????while (ImportDesc^.Name <> 0) do
???????begin
??????????DLL := PChar(Integer(Dos) + ImportDesc^.Name);
??????????RepointAddrInModule(GetModuleHandle(PChar(DLL)), OldFunc, NewFunc);
??????????Func := Pointer(Integer(DOS) + ImportDesc.LookupTable);
??????????while Func^ <> nil do
??????????begin
?????????????f := LocateFunctionAddress(Func^);
?????????????if f = OldFunc then
?????????????begin
????????????????WriteProcessMemory(GetCurrentProcess, Func, @NewFunc, 4, written);
????????????????if Written > 0 then Inc(Result);
?????????????end;
?????????????Inc(Func);
??????????end;
??????????Inc(ImportDesc);
???????end;
????end;
begin
????IsDone := TList.Create;
????try
???????Result := RepointAddrInModule(GetModuleHandle(nil), OldFunc, NewFunc);
????finally
???????IsDone.Free;
????end;
end;
有了這兩個(gè)函數(shù)我們幾乎可以更改任何API函數(shù)。
我們可以先寫一個(gè)DLL文件。我這里以修改Text相關(guān)函數(shù)為例:
先定義幾個(gè)函數(shù):
type
????TTextOutA = function(DC: HDC; X, Y: Integer; Str: PAnsiChar; Count: Integer): BOOL; stdcall;
????TTextOutW = function(DC: HDC; X, Y: Integer; Str: PWideChar; Count: Integer): BOOL; stdcall;
????TTextOut = function(DC: HDC; X, Y: Integer; Str: PChar; Count: Integer): BOOL; stdcall;
????TDrawTextA = function(hDC: HDC; lpString: PAnsiChar; nCount: Integer; var lpRect: TRect; uFormat: UINT): Integer; stdcall;
????TDrawTextW = function(hDC: HDC; lpString: PWideChar; nCount: Integer; var lpRect: TRect; uFormat: UINT): Integer; stdcall;
????TDrawText = function(hDC: HDC; lpString: PChar; nCount: Integer; var lpRect: TRect; uFormat: UINT): Integer; stdcall;
var
????OldTextOutA: TTextOutA;
????OldTextOutW: TTextOutW;
????OldTextOut: TTextOut;
????OldDrawTextA: TDrawTextA;
????OldDrawTextW: TDrawTextW;
????OldDrawText: TDrawText;
......
function MyTextOutA(DC: HDC; X, Y: Integer; Str: PAnsiChar; Count: Integer): BOOL; stdcall;
begin
????OldTextOutA(DC, X, Y, ''''ABC'''', length(''''ABC''''));
end;
function MyTextOutW(DC: HDC; X, Y: Integer; Str: PWideChar; Count: Integer): BOOL; stdcall;
begin
????OldTextOutW(DC, X, Y, ''''ABC'''', length(''''ABC''''));
end;
function MyTextOut(DC: HDC; X, Y: Integer; Str: PChar; Count: Integer): BOOL; stdcall;
begin
????OldTextOut(DC, X, Y, ''''ABC'''', length(''''ABC''''));
end;
function MyDrawTextA(hDC: HDC; lpString: PAnsiChar; nCount: Integer; var lpRect: TRect; uFormat: UINT): Integer; stdcall;
begin
????OldDrawTextA(hDC, ''''ABC'''', length(''''ABC''''), lpRect, uFormat);
end;
function MyDrawTextW(hDC: HDC; lpString: PWideChar; nCount: Integer; var lpRect: TRect; uFormat: UINT): Integer; stdcall;
begin
????OldDrawTextW(hDC, ''''ABC'''', length(''''ABC''''), lpRect, uFormat);
end;
function MyDrawText(hDC: HDC; lpString: PChar; nCount: Integer; var lpRect: TRect; uFormat: UINT): Integer; stdcall;
begin
????OldDrawText(hDC, ''''ABC'''', length(''''ABC''''), lpRect, uFormat);
end;
調(diào)用時(shí)我們要把原來(lái)的函數(shù)地址保存下來(lái):
????if @OldTextOutA = nil then
???????@OldTextOutA := LocateFunctionAddress(@TextOutA);
????if @OldTextOutW = nil then
???????@OldTextOutW := LocateFunctionAddress(@TextOutW);
????if @OldTextOut = nil then
???????@OldTextOut := LocateFunctionAddress(@TextOut);
????if @OldDrawTextA = nil then
???????@OldDrawTextA := LocateFunctionAddress(@DrawTextA);
????if @OldDrawTextW = nil then
???????@OldDrawTextW := LocateFunctionAddress(@DrawTextW);
????if @OldDrawText = nil then
???????@OldDrawText := LocateFunctionAddress(@DrawText);
然后很順其自然的用自己的函數(shù)替換掉原來(lái)的函數(shù)
????RepointFunction(@OldTextOutA, @MyTextOutA);
????RepointFunction(@OldTextOutW, @MyTextOutW);
????RepointFunction(@OldTextOut, @MyTextOut);
????RepointFunction(@OldDrawTextA, @MyDrawTextA);
????RepointFunction(@OldDrawTextW, @MyDrawTextW);
????RepointFunction(@OldDrawText, @MyDrawText);
?????????在結(jié)束時(shí)不要忘記恢復(fù)原來(lái)函數(shù)的入口,要不然你會(huì)死得很難看喲!好了我們?cè)趯懸粋€(gè)Demo程序。你會(huì)說(shuō)怎么文字沒(méi)有變成ABC呀?是呀,你要刷新一下才行。最小化然后在最大化。看看變了沒(méi)有。???
?????????要不然你就寫代碼刷新一下好了。至于去攔截其他進(jìn)程的API那就用SetWindowsHookEx寫一個(gè)其他的鉤子將DLL映射進(jìn)去就行了,我就不再浪費(fèi)口水了。
掌握了該方法你幾乎無(wú)所不能。你可以修改其它程序。你可以攔截Createwindow等窗口函數(shù)改變其他程序的窗口形狀、你還可以入侵其它的程序,你還可以......嘿嘿。干了壞事別招出我來(lái)就行了。
總結(jié)
以上是生活随笔為你收集整理的浅谈Windows API编程 (这个经典)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Wonderware-InTouch曲线
- 下一篇: vue项目设置服务器地址,vue项目配置