浅谈 Windows API 编程
?
原文地址:http://blog.sina.com.cn/s/blog_46d85b2a01010qpt.html
http://blog.sina.com.cn/s/articlelist_1188584234_0_1.html
?
WinSDK 是編程中的傳統難點,曾經聽有一個技術不是很好的朋友亂說什么給你 API 誰都會用,其實并非那么簡單,個人寫的WinAPI 程序也不少了,其實之所以難就難在每個調用的 API 都包含著 Windows 這個操作系統的潛規則或者是 windows 內部的運行機制。
?
?
句柄
?
首先來談談 句柄,初學習 WinSDK的朋友剛看到這個詞頭大了吧? 其實我也是了,我們來看看 programming windows里面是怎么說的,一個句柄僅僅是用來識別某些事情的數字。它唯一的標識這當前的一個實例。這樣說確實不容易懂。那么我們這么看,比如你打開 windows自帶的計算器。你多打開幾次是不是桌面上出現了很多個計算器呢? 你使用其中一個計算器的時候當你按下等于按鈕的時候運算結果是否會出現在其他的計算機結果欄里? 不會,那windows怎么知道讓結果出現在哪里呢?這就是句柄的作用了,句柄唯一的標識著一個程序,你打開的每一個窗口(計算器) 都有一個不同的句柄,你你每一步操作都是指定了在某個句柄下的,所以,他不會出錯。而且你打開的每一個計算機都共享著同樣的代碼和內存。通過句柄系統會把所需的資源充分的調用到當前的某個程序自己的數據區。
不僅是窗口,各種菜單,GDI 對象(圖形設備接口(Graphics Device Interface, 或Graphical Device Interface)) 都有自己的句柄。
獲取句柄的手段也是多重多樣,不過當然是通過調用API函數實現了,如:
MFC 中的 hHandle = GetSafeHandle();
API 編程中的 hBrush = GetStorkObject(BLACK_BRUSH);
很多操作都需要將 句柄 添加到參數列表中,
當你沒有直接定義句柄變量的時候可能要記憶很多API的返回類型來間接獲取。如:
完成選擇自定義的GDI對象的操作。句柄的種類很多,掌握一種的使用方法后,其他所有的不學自通,WinAPI 編程永遠伴隨的元素中句柄是其中之一。非常重要。由于是淺談,所以就說到這里了。
?
?
消息映射
?
接下來是 windows下的? 消息映射 機制了,呵呵,窗口過程,剛學的朋友難理解吧? WinSDK編程基于C,但是和 C 的理念有著完全的不同,這中間的不同,在我看來最多的也就是來自于這個消息映射,后面什么吹的很炫的 Hook技術,木馬技術,鍵盤截獲,都是來自于特殊消息的捕捉,映射自定義的特殊消息來實現的 (當然和我接下來談的稍微有點不同)。
首先我們應該先明白 消息 和 事件 的區別,Windows是消息驅動的操作系統,這里 消息的產生來自于某個實例化的對象上用戶的操作,來自控件,菜單,或者是系統本身產生的。而 事件 是靠消息觸發的,但這也不是絕對的。可以用一個簡單的例子去解釋,我這里越寫越覺得自己難表達清楚,就比如這么一個例子:“某男殺人這條消息導致被槍斃這個事件” 不過最重要的區別是在消息產生后并不會被直接處理,而是先插入windows系統的消息隊列,然后系統判斷此消息產生于哪個程序,送入此程序的消息循環,由 LRSULT CALLBACK winprc(hwnd , uint,wParam,lParam)處理。而事件是操作系統處理消息的過程中反饋的結果。
用戶操作 ->? 產生消息 -> 發送系統 -> 系統判斷來源 -> 發給相應的窗口過程或者其他Callback函數 -> 消息處理 -> 等待下一條消息的產生
以上為消息循環整個過程。
?
創建Windows窗體 : WinMain() 與 WndProc():https://www.cnblogs.com/shangdawei/p/3345132.html
#include <windows.h> #include <mmsystem.h>LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM); //聲名消息處理函數(處理windows和接收windows消息)//hInstance:系統為窗口分配的實例號,2和3忘了.4是顯示方式 int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,PSTR szCmdLine, int iCmdShow) {static TCHAR szAppName[] = TEXT ("HelloWin") ; //窗體名HWND hwnd;//句柄MSG msg;//消息體WNDCLASS wndclass;//這義一個窗體類實例//設置窗體參數wndclass.style = CS_HREDRAW | CS_VREDRAW ; //樣式wndclass.cbClsExtra = 0 ;wndclass.cbWndExtra = 0 ;wndclass.hInstance = hInstance ;//窗體實例名,由windows自動分發wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ;//顯示上面的圖標titltewndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ;//窗口光標wndclass.hbrBackground= (HBRUSH) GetStockObject (WHITE_BRUSH) ;//背景刷wndclass.lpszMenuName=NULL;wndclass.lpfnWndProc=WndProc;//設置窗體接收windws消息函數wndclass.lpszClassName= szAppName;//窗體類名if (!RegisterClass (&wndclass))//注冊窗體類{MessageBox ( NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ;return 0 ;};//創建一個窗體。已分配內存。返回一個窗體句柄hwnd = CreateWindow( szAppName, // window class nameTEXT ("The Hello Program"), // window captionWS_OVERLAPPEDWINDOW, // window styleCW_USEDEFAULT,// initial x positionCW_USEDEFAULT,// initial y positionCW_USEDEFAULT,// initial x sizeCW_USEDEFAULT,// initial y sizeNULL, // parent window handleNULL, // window menu handlehInstance, // program instance handleNULL) ;ShowWindow (hwnd,iCmdShow);//顯示窗口UpdateWindow (hwnd) ;//更新窗體while(GetMessage(&msg,NULL,0,0)){TranslateMessage (&msg);//翻譯消息并發送到windows消息隊列DispatchMessage (&msg) ;//接收信息}return msg.wParam ; }LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)//消息的處理程序 {HDC hdc ;PAINTSTRUCT ps ;RECT rect ;switch (message){case WM_CREATE:return 0 ;case WM_PAINT:hdc = BeginPaint (hwnd, &ps) ;TextOut(hdc,0,0,"Hello",strlen("Hello"));EndPaint (hwnd, &ps) ;return 0 ;case WM_DESTROY:PostQuitMessage (0) ;return 0 ;}return DefWindowProc (hwnd, message, wParam, lParam) ; }上面大概流程:
LRSULT CALLBACK WndProc(hwnd , uint,wParam,lParam);int WINAPI WinMain(…) {MSG msg; // 消息體RegisterClass(…); // 注冊窗口類CreateWindow(…); // 創建窗口ShowWindow(…); // 顯示窗口UpdateWindow(…);While(GetMessage(&msg,…)){ // 消息循環TranslateMessage(…);DispatchMessage(…);}return msg.wParam ; }// 窗口過程函數,用于映射switch語句中各個需要被處理的消息。即 消息處理程序 LRSULT CALLBACK WndProc(hwnd , uint,wParam,lParam); {While((UINT)message){Switch(message)Case…Case…………Default……….} }以上是最基本的 WinAPi 編程的代碼結構。其實這里面最重要的結構莫過于 while(GetMessage(&msg)) 和 Winproc 這個函數,這也是傳統的C面向過程編程的區別,win編程總等著特定事件觸發對應的消息映射函數來完成代碼功能,并不是一條代碼從頭走到尾。關于特殊消息的映射,這里不談,這里僅是個入門指引。
最后談一點就是重繪問題。其實在我看來這個東西更多是屬于GDI編程里面的東西,說起來其實難度不大,但是處理起來確實是個難點。先拿剛才的代碼來說吧,在 WndProc 函數里面 先添加一條關于 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參數所表明的那樣。
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等窗口函數改變其他程序的窗口形狀、你還可以入侵其它的程序,你還可以......嘿嘿。干了壞事別招出我來就行了。
?
?
?
?
總結
以上是生活随笔為你收集整理的浅谈 Windows API 编程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: linux 之间 copy 传输文件方法
- 下一篇: Chrome 使用