WinSDK是編程中的傳統難點,個人寫的WinAPI程序也不少了,其實之所以難就難在每個調用的API都包含着Windows這個操作系統的潛規則或者是windows內部的運行機制……
WinSDK是編程中的傳統難點,曾經聽有一個技術不是很好的朋友亂說什么給你API誰都會用,其實並非那么簡單,個人寫的WinAPI程序也不少了,其實之所以難就難在每個調用的API都包含着Windows這個操作系統的潛規則或者是windows內部的運行機制。
首先來談談句柄,初學習WinSDK的朋友剛看到這個詞頭大了吧?其實我也是了,我們來看看programming windows里面是怎么說的,一個句柄僅僅是用來識別某些事情的數字。它唯一的標識這當前的一個實例。這樣說確實不容易懂。那么我們這么看,比如你打開 windows自帶的計算器。你多打開幾次是不是桌面上出現了很多個計算器呢?你使用其中一個計算器的時候當你按下等於按鈕的時候運算結果是否會出現在其他的計算機結果欄里?不會,那windows怎么知道讓結果出現在哪里呢?這就是句柄的作用了,句柄唯一的標識着一個程序,你打開的每一個窗口(計算器) 都有一個不同的句柄你你每一步操作都是指定了在某個句柄下的,所以,他不會出錯。而且你打開的每一個計算機都共享着同樣的代碼和內存。通過句柄系統會把所需的資源充分的調用到當前的某個程序自己的數據區。
不僅是窗口,各種菜單,GDI對象都有自己的句柄,獲取句柄的手段也是多重多樣,不過當然是通過調用API函數實現了,如:
MFC中的hHandle = GetSafeHandle();
API編程中的hBrush = GetStorkObject(BLACK_BRUSH);
很多操作都需要將句柄添加到參數列表中,當你沒有直接定義句柄變量的時候可能要記憶很多API的返回類型來間接獲取。如:
hPen = SelectObject(hdc,GetStockObject(&logicpen));
// SelectObject()這個函數在設置本設備描述表下的GDI對象時會返回設置前的GDI對象句柄
MoveToEx(hdc, pt1.x, pt1.y, &apt);
LineTo(hdc, pt2.x,pt2.y);
SelectObject(hdc,hPen);
完成選擇自定義的GDI對象的操作。句柄的種類很多,掌握一種的使用方法所有的不學自通,WinAPI編程永遠伴隨的元素中句柄是其中之一。非常重要。由於是淺談,所以就說到這里了.
接下來是windows下的消息映射機制了,呵呵,窗口過程,剛學的朋友難理解吧?WinSDK編程基於C,但是和C的理念有着完全的不同,這中間的不同,在我看來最多的也就是來自於這個消息映射,后面什么吹的很炫的Hook技術,木馬技術,鍵盤截獲,都是來自於特殊消息的捕捉,映射自定義的特殊消息來實現的(當然和我接下來談的稍微有點不同)。
首先我們應該先明白消息和事件的區別,Windows是消息驅動的操作系統,這里的消息的產生來自於某個實例化的對象上用戶的操作,來自控件,菜單,或者是系統本身產生的,而事件是靠消息觸發的,但這也不是絕對的。可以用一個簡單的例子去解釋,我這里越寫越覺得自己難表達清楚,就比如這么一個例子:“某男殺人這條消息導致被槍斃這個事件”不過最重要的區別是在消息產生后並不會被直接處理,而是先插入windows系統的消息隊列,然后系統判斷此消息產生於哪個程序,送入此程序的消息循環,由LRSULT CALLBACK winprc(hwnd , uint,wParam,lParam)處理。而事件是操作系統處理消息的過程中反饋的結果。
用戶操作-> 產生消息->發送系統->系統判斷來源->發給相應的窗口過程或者其他Callback函數->消息處理->等待下一條消息的產生
以上為消息循環整個過程。
LRSULT CALLBACK winprc(hwnd , uint,wParam,lParam);
int WINAPI WinMain(…)
{
MSG msg;
RegisterClass(…); // 注冊窗口類
CreateWindow(…); // 創建窗口
ShowWindow(…); // 顯示窗口
UpdateWindow(…);
While(GetMessage(&msg,…)){ // 消息循環
TranslateMessage(…);
DispatchMessage(…);
}
LRSULT CALLBACK winprc(hwnd , uint,wParam,lParam);
//窗口過程函數,用於映射switch語句中各個需要被處理的消息
{
While((UINT)message)
{
Switch(message)
Case…
Case…
………
Default……….
}
}
以上是最基本的WinAPi編程的代碼結構。其實這里面最重要的結構莫過於while(GetMessage(&msg))和 Winproc這個函數,這也是傳統的C面向過程編程的區別,win編程總等着特定事件觸發對應的消息映射函數來完成代碼功能,並不是一條代碼從頭走到尾。關於特殊消息的映射,這里不談,這里僅是個入門指引。
最后談一點就是重繪問題。其實在我看來這個東西更多是屬於GDI編程里面的東西,說起來其實難度不大,但是處理起來確實是個難點。先拿剛才的代碼來說吧。先添加一條關於WM_LBUTTONDOWN的消息映射:
Static int apt[2];
case WM_LBUTTONDOWN:
hdc = GetDC(hwnd);
apt[1].x = LOWORD (lParam);
apt[1].y = HIWORD (lParam);
hPen = CreatePen(BLACK_PEN,3,RGB(125,125,125));
SelectObject(hdc,hPen);
MoveToEx(hdc,apt[0].x,apt[0].y,NULL);
LineTo(hdc,apt[1].x,apt[1].y);
apt[0].x = apt[1].x;
apt[0].y = apt[1].y;
DeleteObject(hPen);
ReleaseDC(hwnd,hdc);
return 0;
這段代碼實現一個簡單的畫線功能,當你在你的客戶區胡點一通鼠標后試着拖動一下窗口大小,或者將其最小化或者被其他窗口覆蓋一下你都會發現你原來畫的線沒了,可是其他窗口為什么被覆蓋了以后再彈出窗口還會有原來的東西呢?那就是重繪,要重新繪制整個客戶區(准確的說是失效的矩形),以上說的操作都會導致你的客戶區失效,這時會產生重繪消息WM_PAINT,我們要想保存這些線那么我們就必須保存這些你用鼠標左鍵點過的點。當然這是重繪技術中最簡單的,當你的客戶區上是一個復雜的畫面的話,就不僅僅需要保存點,還有各種形狀的圖形,顏色等等……這里給大家一段我自己寫的代碼來實現以上的 WM_LBUTTONDOWN消息映射來產生的點。通過單鏈表來動態添加點來實現重繪。
case WM_PAINT:
hdc = BeginPaint(hwnd,&ps);
TextOut(hdc,cxClient/6,cyClient/6,TEXT("圖形重繪"),strlen("圖形重繪"));
ReDrawLines(&MyList,hdc);
EndPaint(hwnd,&ps);
return 0;
case WM_LBUTTONDOWN:
hdc = GetDC(hwnd);
apt[1].x = LOWORD (lParam);
apt[1].y = HIWORD (lParam);
hPen = CreatePen(BLACK_PEN,2,RGB(125,0,0));
SelectObject(hdc,hPen);
MoveToEx(hdc,apt[0].x,apt[0].y,NULL);
LineTo(hdc,apt[1].x,apt[1].y);
MyList.pCurrent->x = apt[0].x;
MyList.pCurrent->y = apt[0].y;
MyList.pCurrent->pNext->x = apt[1].x;
MyList.pCurrent->pNext->y = apt[1].y;
MyList.m_iCounter = MyList.m_iCounter+2;
MyList.pCurrent = MyList.pCurrent->pNext->pNext;
apt[0].x = apt[1].x;
apt[0].y = apt[1].y;
DeleteObject(hPen);
ReleaseDC(hwnd,hdc);
return 0;
其中的重繪函數代碼如下:
void ReDrawLines(LinkList* pLinkList,HDC hdc)
{
pMyPoint p = pLinkList->pHead;
int iSaver =pLinkList->m_iCounter;
while(iSaver!=0)
{
MoveToEx(hdc,p->x,p->y,NULL);
LineTo(hdc,p->pNext->x,p->pNext->y);
p=p->pNext->pNext;
iSaver=iSaver-2;
}
}
添加了以上的代碼你會發現再次拖動窗口大小等等你原來畫的線就都能重現出來了。呵呵是不是覺得一個看似簡單的東西其實里面需要很多代碼實現呢?也許,這就是windows.
好了,WinSDK入門的東西就談這么多,希望能給初學者一定的幫助,這么多的字,都是我一個一個打出來沒有任何借鑒和摘抄的。相信做為一個過來人能更多的理解大家學習中的困難。
Win32環境下動態鏈接庫(DLL)編程原理
比較大應用程序都由很多模塊組成,這些模塊分別完成相對獨立的功能,它們彼此協作來完成整個軟件系統的工作。其中可能存在一些模塊的功能較為通用,在構造其它軟件系統時仍會被使用。在構造軟件系統時,如果將所有模塊的源代碼都靜態編譯到整個應用程序EXE文件中,會產生一些問題:一個缺點是增加了應用程序的大小,它會占用更多的磁盤空間,程序運行時也會消耗較大的內存空間,造成系統資源的浪費;另一個缺點是,在編寫大的EXE程序時,在每次修改重建時都必須調整編譯所有源代碼,增加了編譯過程的復雜性,也不利於階段性的單元測試。
Windows系統平台上提供了一種完全不同的較有效的編程和運行環境,你可以將獨立的程序模塊創建為較小的DLL(Dynamic Linkable Library)文件,並可對它們單獨編譯和測試。在運行時,只有當EXE程序確實要調用這些DLL模塊的情況下,系統才會將它們裝載到內存空間中。這種方式不僅減少了EXE文件的大小和對內存空間的需求,而且使這些DLL模塊可以同時被多個應用程序使用。Microsoft Windows自己就將一些主要的系統功能以DLL模塊的形式實現。例如IE中的一些基本功能就是由DLL文件實現的,它可以被其它應用程序調用和集成。
一般來說,DLL是一種磁盤文件(通常帶有DLL擴展名),它由全局數據、服務函數和資源組成,在運行時被系統加載到進程的虛擬空間中,成為調用進程的一部分。如果與其它DLL之間沒有沖突,該文件通常映射到進程虛擬空間的同一地址上。DLL模塊中包含各種導出函數,用於向外界提供服務。Windows 在加載DLL模塊時將進程函數調用與DLL文件的導出函數相匹配。
在Win32環境中,每個進程都復制了自己的讀/寫全局變量。如果想要與其它進程共享內存,必須使用內存映射文件或者聲明一個共享數據段。DLL模塊需要的堆棧內存都是從運行進程的堆棧中分配出來的。
DLL現在越來越容易編寫。Win32已經大大簡化了其編程模式,並有許多來自AppWizard和MFC類庫的支持。
一、導出和導入函數的匹配
DLL文件中包含一個導出函數表。這些導出函數由它們的符號名和稱為標識號的整數與外界聯系起來。函數表中還包含了DLL中函數的地址。當應用程序加載 DLL模塊時時,它並不知道調用函數的實際地址,但它知道函數的符號名和標識號。動態鏈接過程在加載的DLL模塊時動態建立一個函數調用與函數地址的對應表。如果重新編譯和重建DLL文件,並不需要修改應用程序,除非你改變了導出函數的符號名和參數序列。
簡單的DLL文件只為應用程序提供導出函數,比較復雜的DLL文件除了提供導出函數以外,還調用其它DLL文件中的函數。這樣,一個特殊的DLL可以既有導入函數,又有導入函數。這並不是一個問題,因為動態鏈接過程可以處理交叉相關的情況。
在DLL代碼中,必須像下面這樣明確聲明導出函數:
__declspec(dllexport) int MyFunction(int n);
但也可以在模塊定義(DEF)文件中列出導出函數,不過這樣做常常引起更多的麻煩。在應用程序方面,要求像下面這樣明確聲明相應的輸入函數:
__declspec(dllimport) int MyFuncition(int n);
僅有導入和導出聲明並不能使應用程序內部的函數調用鏈接到相應的DLL文件上。應用程序的項目必須為鏈接程序指定所需的輸入庫(LIB文件)。而且應用程序事實上必須至少包含一個對DLL函數的調用。
二、與DLL模塊建立鏈接
應用程序導入函數與DLL文件中的導出函數進行鏈接有兩種方式:隱式鏈接和顯式鏈接。所謂的隱式鏈接是指在應用程序中不需指明DLL文件的實際存儲路徑,程序員不需關心DLL文件的實際裝載。而顯式鏈接與此相反。
采用隱式鏈接方式,程序員在建立一個DLL文件時,鏈接程序會自動生成一個與之對應的LIB導入文件。該文件包含了每一個DLL導出函數的符號名和可選的標識號,但是並不含有實際的代碼。LIB文件作為DLL的替代文件被編譯到應用程序項目中。當程序員通過靜態鏈接方式編譯生成應用程序時,應用程序中的調用函數與LIB文件中導出符號相匹配,這些符號或標識號進入到生成的EXE文件中。LIB文件中也包含了對應的DLL文件名(但不是完全的路徑名),鏈接程序將其存儲在EXE文件內部。當應用程序運行過程中需要加載DLL文件時,Windows根據這些信息發現並加載DLL,然后通過符號名或標識號實現對DLL函數的動態鏈接。
顯式鏈接方式對於集成化的開發語言(例如VB)比較適合。有了顯式鏈接,程序員就不必再使用導入文件,而是直接調用Win32 的LoadLibary函數,並指定DLL的路徑作為參數。LoadLibary返回HINSTANCE參數,應用程序在調用 GetProcAddress函數時使用這一參數。GetProcAddress函數將符號名或標識號轉換為DLL內部的地址。假設有一個導出如下函數的 DLL文件:
extern "C" __declspec(dllexport) double SquareRoot(double d);
下面是應用程序對該導出函數的顯式鏈接的例子:
typedef double(SQRTPROC)(double);
HINSTANCE hInstance;
SQRTPROC* pFunction;
VERIFY(hInstance=::LoadLibrary("c:\\winnt\\system32\\mydll.dll"));
VERIFY(pFunction=(SQRTPROC*)::GetProcAddress(hInstance,"SquareRoot"));
double d=(*pFunction)(81.0);//調用該DLL函數
在隱式鏈接方式中,所有被應用程序調用的DLL文件都會在應用程序EXE文件加載時被加載在到內存中;但如果采用顯式鏈接方式,程序員可以決定DLL文件何時加載或不加載。顯式鏈接在運行時決定加載哪個DLL文件。例如,可以將一個帶有字符串資源的DLL模塊以英語加載,而另一個以西班牙語加載。應用程序在用戶選擇了合適的語種后再加載與之對應的DLL文件。
三、使用符號名鏈接與標識號鏈接
在Win16環境中,符號名鏈接效率較低,所有那時標識號鏈接是主要的鏈接方式。在Win32環境中,符號名鏈接的效率得到了改善。Microsoft 現在推薦使用符號名鏈接。但在MFC庫中的DLL版本仍然采用的是標識號鏈接。一個典型的MFC程序可能會鏈接到數百個MFC DLL函數上。采用標識號鏈接的應用程序的EXE文件體相對較小,因為它不必包含導入函數的長字符串符號名。
四、編寫DllMain函數
DllMain函數是DLL模塊的默認入口點。當Windows加載DLL模塊時調用這一函數。系統首先調用全局對象的構造函數,然后調用全局函數 DLLMain。DLLMain函數不僅在將DLL鏈接加載到進程時被調用,在DLL模塊與進程分離時(以及其它時候)也被調用。下面是一個框架 DLLMain函數的例子。
HINSTANCE g_hInstance;
extern "C" int APIENTRY DllMain(HINSTANCE hInstance,DWORD dwReason,LPVOID lpReserved)
{
if(dwReason==DLL_PROCESS_ATTACH)
{
TRACE0("EX22A.DLL Initializing!\n");
//在這里進行初始化
}
else if(dwReason=DLL_PROCESS_DETACH)
{
TRACE0("EX22A.DLL Terminating!\n");
//在這里進行清除工作
}
return 1;//成功
}
如果程序員沒有為DLL模塊編寫一個DLLMain函數,系統會從其它運行庫中引入一個不做任何操作的缺省DLLMain函數版本。在單個線程啟動和終止時,DLLMain函數也被調用。正如由dwReason參數所表明的那樣。
五、模塊句柄
進程中的每個DLL模塊被全局唯一的32字節的HINSTANCE句柄標識。進程自己還有一個HINSTANCE句柄。所有這些模塊句柄都只有在特定的進程內部有效,它們代表了DLL或EXE模塊在進程虛擬空間中的起始地址。在Win32中,HINSTANCE和HMODULE的值是相同的,這個兩種類型可以替換使用。進程模塊句柄幾乎總是等於0x400000,而DLL模塊的加載地址的缺省句柄是0x10000000。如果程序同時使用了幾個DLL模塊,每一個都會有不同的HINSTANCE值。這是因為在創建DLL文件時指定了不同的基地址,或者是因為加載程序對DLL代碼進行了重定位。
模塊句柄對於加載資源特別重要。Win32 的FindResource函數中帶有一個HINSTANCE參數。EXE和DLL都有其自己的資源。如果應用程序需要來自於DLL的資源,就將此參數指定為DLL的模塊句柄。如果需要EXE文件中包含的資源,就指定EXE的模塊句柄。
但是在使用這些句柄之前存在一個問題,你怎樣得到它們呢?如果需要得到EXE模塊句柄,調用帶有Null參數的Win32函數GetModuleHandle;如果需要DLL模塊句柄,就調用以DLL文件名為參數的Win32函數GetModuleHandle。
六、應用程序怎樣找到DLL文件
如果應用程序使用LoadLibrary顯式鏈接,那么在這個函數的參數中可以指定DLL文件的完整路徑。如果不指定路徑,或是進行隱式鏈接,Windows將遵循下面的搜索順序來定位DLL:
1. 包含EXE文件的目錄,
2. 進程的當前工作目錄,
3. Windows系統目錄,
4. Windows目錄,
5. 列在Path環境變量中的一系列目錄。
這里有一個很容易發生錯誤的陷阱。如果你使用VC++進行項目開發,並且為DLL模塊專門創建了一個項目,然后將生成的DLL文件拷貝到系統目錄下,從應用程序中調用DLL模塊。到目前為止,一切正常。接下來對DLL模塊做了一些修改后重新生成了新的DLL文件,但你忘記將新的DLL文件拷貝到系統目錄下。下一次當你運行應用程序時,它仍加載了老版本的DLL文件,這可要當心!
七、調試DLL程序
Microsoft 的VC++是開發和測試DLL的有效工具,只需從DLL項目中運行調試程序即可。當你第一次這樣操作時,調試程序會向你詢問EXE文件的路徑。此后每次在調試程序中運行DLL時,調試程序會自動加載該EXE文件。然后該EXE文件用上面的搜索序列發現DLL文件,這意味着你必須設置Path環境變量讓其包含DLL文件的磁盤路徑,或者也可以將DLL文件拷貝到搜索序列中的目錄路徑下。
HOOK API是一個永恆的話題,如果沒有HOOK,許多技術將很難實現,也許根本不能實現。這里所說的API,是廣義上的API,它包括DOS下的中斷, WINDOWS里的API、中斷服務、IFS和NDIS過濾等。比如大家熟悉的即時翻譯軟件,就是靠HOOK TextOut()或ExtTextOut()這兩個函數實現的,在操作系統用這兩個函數輸出文本之前,就把相應的英文替換成中文而達到即時翻譯;IFS 和NDIS過濾也是如此,在讀寫磁盤和收發數據之前,系統會調用第三方提供的回調函數來判斷操作是否可以放行,它與普通HOOK不同,它是操作系統允許的,由操作系統提供接口來安裝回調函數。
甚至如果沒有HOOK,就沒有病毒,因為不管是DOS下的病毒或WINDOWS里的病毒,都是靠HOOK系統服務來實現自己的功能的:DOS下的病毒靠HOOK INT 21來感染文件(文件型病毒),靠HOOK INT 13來感染引導扇區(引導型病毒);WINDOWS下的病毒靠HOOK系統API(包括RING0層的和RING3層的),或者安裝IFS(CIH病毒所用的方法)來感染文件。因此可以說“沒有HOOK,就沒有今天多姿多彩的軟件世界”。
由於涉及到專利和知識產權,或者是商業機密,微軟一直不提倡大家HOOK它的系統API,提供IFS和NDIS等其他過濾接口,也是為了適應殺毒軟件和防火牆的需要才開放的。所以在大多數時候,HOOK API要靠自己的力量來完成。
HOOK API有一個原則,這個原則就是:被HOOK的API的原有功能不能受到任何影響。就象醫生救人,如果把病人身體里的病毒殺死了,病人也死了,那么這個 “救人”就沒有任何意義了。如果你HOOK API之后,你的目的達到了,但API的原有功能失效了,這樣不是HOOK,而是REPLACE,操作系統的正常功能就會受到影響,甚至會崩潰。
HOOK API的技術,說起來也不復雜,就是改變程序流程的技術。在CPU的指令里,有幾條指令可以改變程序的流程:JMP,CALL,INT,RET, RETF,IRET等指令。理論上只要改變API入口和出口的任何機器碼,都可以HOOK,但是實際實現起來要復雜很多,因為要處理好以下問題:
1,CPU指令長度問題,在32位系統里,一條JMP/CALL指令的長度是5個字節,因此你只有替換API里超過5個字節長度的機器碼(或者替換幾條指令長度加起來是5字節的指令),否則會影響被更改的小於5個字節的機器碼后面的數條指令,甚至程序流程會被打亂,產生不可預料的后果;
2,參數問題,為了訪問原API的參數,你要通過EBP或ESP來引用參數,因此你要非常清楚你的HOOK代碼里此時的EBP/ESP的值是多少;
3,時機的問題,有些HOOK必須在API的開頭,有些必須在API的尾部,比如HOOK CreateFilaA(),如果你在API尾部HOOK API,那么此時你就不能寫文件,甚至不能訪問文件;HOOK RECV(),如果你在API頭HOOK,此時還沒有收到數據,你就去查看RECV()的接收緩沖區,里面當然沒有你想要的數據,必須等RECV()正常執行后,在RECV()的尾部HOOK,此時去查看RECV()的緩沖區,里面才有想要的數據;
4,上下文的問題,有些HOOK代碼不能執行某些操作,否則會破壞原API的上下文,原API就失效了;
5,同步問題,在HOOK代碼里盡量不使用全局變量,而使用局部變量,這樣也是模塊化程序的需要;
6,最后要注意的是,被替換的CPU指令的原有功能一定要在HOOK代碼的某個地方模擬實現。
下面以ws2_32.dll里的send()為例子來說明如何HOOK這個函數:
Exported fn(): send - Ord:0013h
地址 機器碼 匯編代碼
:71A21AF4 55 push ebp //將被HOOK的機器碼(第1種方法)
:71A21AF5 8BEC mov ebp, esp //將被HOOK的機器碼(第2種方法)
:71A21AF7 83EC10 sub esp, 00000010
:71A21AFA 56 push esi
:71A21AFB 57 push edi
:71A21AFC 33FF xor edi, edi
:71A21AFE 813D1C20A371931CA271 cmp dword ptr [71A3201C], 71A21C93 //將被HOOK的機器碼(第4種方法)
:71A21B08 0F84853D0000 je 71A25893
:71A21B0E 8D45F8 lea eax, dword ptr [ebp-08]
:71A21B11 50 push eax
:71A21B12 E869F7FFFF call 71A21280
:71A21B17 3BC7 cmp eax, edi
:71A21B19 8945FC mov dword ptr [ebp-04], eax
:71A21B1C 0F85C4940000 jne 71A2AFE6
:71A21B22 FF7508 push [ebp+08]
:71A21B25 E826F7FFFF call 71A21250
:71A21B2A 8BF0 mov esi, eax
:71A21B2C 3BF7 cmp esi, edi
:71A21B2E 0F84AB940000 je 71A2AFDF
:71A21B34 8B4510 mov eax, dword ptr [ebp+10]
:71A21B37 53 push ebx
:71A21B38 8D4DFC lea ecx, dword ptr [ebp-04]
:71A21B3B 51 push ecx
:71A21B3C FF75F8 push [ebp-08]
:71A21B3F 8D4D08 lea ecx, dword ptr [ebp+08]
:71A21B42 57 push edi
:71A21B43 57 push edi
:71A21B44 FF7514 push [ebp+14]
:71A21B47 8945F0 mov dword ptr [ebp-10], eax
:71A21B4A 8B450C mov eax, dword ptr [ebp+0C]
:71A21B4D 51 push ecx
:71A21B4E 6A01 push 00000001
:71A21B50 8D4DF0 lea ecx, dword ptr [ebp-10]
:71A21B53 51 push ecx
:71A21B54 FF7508 push [ebp+08]
:71A21B57 8945F4 mov dword ptr [ebp-0C], eax
:71A21B5A 8B460C mov eax, dword ptr [esi+0C]
:71A21B5D FF5064 call [eax+64]
:71A21B60 8BCE mov ecx, esi
:71A21B62 8BD8 mov ebx, eax
:71A21B64 E8C7F6FFFF call 71A21230 //將被HOOK的機器碼(第3種方法)
:71A21B69 3BDF cmp ebx, edi
:71A21B6B 5B pop ebx
:71A21B6C 0F855F940000 jne 71A2AFD1
:71A21B72 8B4508 mov eax, dword ptr [ebp+08]
:71A21B75 5F pop edi
:71A21B76 5E pop esi
:71A21B77 C9 leave
:71A21B78 C21000 ret 0010
下面用4種方法來HOOK這個API:
1,把API入口的第一條指令是PUSH EBP指令(機器碼0x55)替換成INT 3(機器碼0xcc),然后用WINDOWS提供的調試函數來執行自己的代碼,這中方法被SOFT ICE等DEBUGER廣泛采用,它就是通過BPX在相應的地方設一條INT 3指令來下斷點的。但是不提倡用這種方法,因為它會與WINDOWS或調試工具產生沖突,而匯編代碼基本都要調試;
2,把第二條mov ebp,esp指令(機器碼8BEC,2字節)替換為INT F0指令(機器碼CDF0),然后在IDT里設置一個中斷門,指向我們的代碼。我這里給出一個HOOK代碼:
lea ebp,[esp+12] //模擬原指令mov ebp,esp的功能
pushfd //保存現場
pushad //保存現場
//在這里做你想做的事情
popad //恢復現場
popfd //恢復現場
iretd //返回原指令的下一條指令繼續執行原函數(71A21AF7地址處)
這種方法很好,但缺點是要在IDT設置一個中斷門,也就是要進RING0。
3,更改CALL指令的相對地址(CALL分別在71A21B12、71A21B25、71A21B64,但前面2條CALL之前有一個條件跳轉指令,有可能不被執行到,因此我們要HOOK 71A21B64處的CALL指令)。為什么要找CALL指令下手?因為它們都是5字節的指令,而且都是CALL指令,只要保持操作碼0xE8不變,改變后面的相對地址就可以轉到我們的HOOK代碼去執行了,在我們的HOOK代碼后面再轉到目標地址去執行。
假設我們的HOOK代碼在71A20400處,那么我們把71A21B64處的CALL指令改為CALL 71A20400(原指令是這樣的:CALL 71A21230)
而71A20400處的HOOK代碼是這樣的:
71A20400:
pushad
//在這里做你想做的事情
popad
jmp 71A21230 //跳轉到原CALL指令的目標地址,原指令是這樣的:call 71A21230
這種方法隱蔽性很好,但是比較難找這條5字節的CALL指令,計算相對地址也復雜。
4,替換71A21AFE地址上的cmp dword ptr [71A3201C], 71A21C93指令(機器碼:813D1C20A371931CA271,10字節)成為
call 71A20400
nop
nop
nop
nop
nop
(機器碼:E8 XX XX XX XX 90 90 90 90 90,10字節)
在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
這種方法隱蔽性最好,但不是每個API都有這樣的指令,要具體情況具體操作。
以上幾種方法是常用的方法,值得一提的是很多人都是改API開頭的5個字節,但是現在很多殺毒軟件用這樣的方法檢查API是否被HOOK,或其他病毒木馬在你之后又改了前5個字節,這樣就會互相覆蓋,最
APIHook一直是使大家感興趣的話題。屏幕取詞,內碼轉化,屏幕翻譯,中文平台等等都涉及到了此項技術。有很多文章涉及到了這項技術,但都閃爍其詞不肯明明白白的公布。我僅在這里公布以下我用Delphi制作APIHook的一些心得。
通常的APIHOOK有這樣幾種方法:
1、自己寫一個動態鏈接庫,里面定義自己寫的想取代系統的API。把這個動態鏈接庫映射到2G以上的系統動態鏈接庫所在空間,把系統動態鏈接庫中的該API的指向修改指向自己的函數。這種方法的好處就是可以取代系統中運行全部程序的該API。但他有個局限,就是只適用於Win9x。(原因是NT中動態鏈接庫不是共享的,每個進程都有自己的一份動態鏈接庫在內存中的映射)
2、自己寫一個動態鏈接庫,里面定義自己寫得象替代系統的API。把這個動態鏈接庫映射到進程的空間里。將該進程對API的調用指向自己寫的動態鏈接庫。這種方法的好處是可以選擇性的替代哪個進程的API。而且適用於所有的Windows操作系統。
這里我選用的是第二種方法。
第二種方法需要先了解一點PE文件格式的知識。
首先是一個實模式的的DOS文件頭,是為了保持和DOS的兼容。
接着是一個DOS的代理模塊。你在純DOS先運行Win32的可執行文件,看看是不是也執行了,只是顯示的的是一行信息大意是說該Windows程序不能在DOS實模式下運行。
然后才是真正意義上的Windows可執行文件的文件頭。它的具體位置不是每次都固定的。是由文件偏移$3C決定的。我們要用到的就是它。
如果我們在程序中調用了一個MessageBoxA函數那么它的實現過程是這樣的。他先調用在本進程中的MessageBoxA函數然后才跳到動態鏈接庫的MessageBoxA的入口點。即:
call messageBoxA(0040106c)
jmp dword ptr [_jmp_MessageBoxA@16(00425294)]
其中00425294的內容存儲的就是就是MessageBoxA函數的入口地址。如果我們做一下手腳,那么......
那就開始吧!
我們需要定義兩個結構
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: //定義跳轉指令jmp
AddressOfPointerToFunction: ^Pointer; file: //定義要跳轉到的函數
end;
PImportCode = ^TImportCode;
然后是確定函數的地址。
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;
參數Code是函數在進程中的指針,即那條Jmp XXX的指令。$25FF就是跳轉指令的機器碼。
在這里我將要實現轉跳。有人說修改內存內容要進入Ring 0 才可以。可是Windows本身提供了一個寫內存的指令WriteProcessMemory。有了這把利器,我們幾乎無所不能。如游戲的修改等在這里我們只談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;
有了這兩個函數我們幾乎可以更改任何API函數。
我們可以先寫一個DLL文件。我這里以修改Text相關函數為例:
先定義幾個函數:
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;
調用時我們要把原來的函數地址保存下來:
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);
然后很順其自然的用自己的函數替換掉原來的函數
RepointFunction(@OldTextOutA, @MyTextOutA);
RepointFunction(@OldTextOutW, @MyTextOutW);
RepointFunction(@OldTextOut, @MyTextOut);
RepointFunction(@OldDrawTextA, @MyDrawTextA);
RepointFunction(@OldDrawTextW, @MyDrawTextW);
RepointFunction(@OldDrawText, @MyDrawText);
在結束時不要忘記恢復原來函數的入口,要不然你會死得很難看喲!好了我們在寫一個Demo程序。你會說怎么文字沒有變成ABC呀?是呀,你要刷新一下才行。最小化然后在最大化。看看變了沒有。
要不然你就寫代碼刷新一下好了。至於去攔截其他進程的API那就用SetWindowsHookEx寫一個其他的鈎子將DLL映射進去就行了,我就不再浪費口水了。
掌握了該方法你幾乎無所不能。你可以修改其它程序。你可以攔截Createwindow等窗口函數改變其他程序的窗口形狀、你還可以入侵其它的程序,你還可以......嘿嘿。干了壞事別招出我來就行了。