Windows消息機制的逆向分析(全面理解windows消息機制)

目錄

一、 Windows消息機制流程

(1)  消息   

(2) Windows窗體的創建

(3).Windows消息的處理

二、一個簡單實例

三、更深入一步分析


一、 Windows消息機制流程


(1)  消息   

消息系統對於一個Windows程序來說十分重要,它是一個程序運行的動力源泉。一個消息,是系統定義的一個32位的值,他唯一的定義了一個事件。消息本身是作爲一個記錄傳遞給應用程序的,這個記錄中包含了消息的類型以及其他信息。例如,對於單擊鼠標所產生的消息來說,這個記錄中包含了單擊鼠標時的座標。這個記錄類型叫做MSG,MSG含有來自windows應用程序消息隊列的消息信息,它在Windows中聲明如下:

typedef struct tagMSG {
HWND hwnd;      //消息所屬窗口
UINT message;   //消息標識符
WPARAM wParam;  //指定消息的附加信息
LPARAM lParam;  //指定消息的附加信息
DWORD time;     //消息隊列中的時間
POINT pt;       //鼠標的當前位置
} MSG;


            windows中的消息雖然很多,但是種類並不繁雜,大體上有3種:窗口消息、命令消息和控件通知消息。窗口消息是系統中最爲常見的消息,它是指由操作系統和控制其他窗口的窗口所使用的消息。例如CreateWindow、DestroyWindow和MoveWindow等都會激發窗口消息。命令消息是一種特殊的窗口消息,他用來處理從一個窗口發送到另一個窗口的用戶請求,例如按下一個按鈕,他就會向主窗口發送一個命令消息。控件通知消息,是指一個窗口內的子控件發生了一些事情,需要通知父窗口。通知消息只適用於標準的窗口控件如按鈕、列表框、組合框、編輯框,以及Windows公共控件如樹狀視圖、列表視圖等。例如,單擊或雙擊一個控件、在控件中選擇部分文本、操作控件的滾動條都會產生通知消息。其中窗口消息及控件通知消息主要由窗口類即直接或間接由CWND類派生類處理。相對窗口消息及控件通知消息而言,命令消息的處理對象範圍就廣得多,它不僅可以由窗口類處理,還可以由文檔類,文檔模板類及應用類所處理。其中,消息用消息標識符進行區分。

             從消息的發送途徑來看,消息可以分成2種:隊列消息和非隊列消息,隊列消息送到系統消息隊列,然後到線程消息隊列;非隊列消息直接送給目的窗口過程。從處理方式來看,消息隊隊列可以分成系統消息隊列和線程消息隊列。系統消息隊列由Windows維護,線程消息隊列則由每個GUI線程自己進行維護,爲避免給non-GUI現成創建消息隊列,所有線程產生時並沒有消息隊列,僅當線程第一次調用GDI函數時系統纔給線程創建一個消息隊列。

(2) Windows窗體的創建

Windows窗體的創建分爲三步:聲明WNDCLASS實例、窗體註冊、創建窗體。

WNDCLASS是一個由系統支持的結構,用來儲存某一類窗口的信息,如ClassStyle,消息處理函數,Icon,Cursor,背景Brush等。聲明如下,其中窗體接收Windows消息函數需格外注意。

typedef struct _WNDCLASS {
    UINT style;             //樣式
    WNDPROC lpfnWndProc;    //設置窗體接收windws消息函數
    int cbClsExtra;         //窗口類擴展
    int cbWndExtra;         //窗口實例擴展
    HINSTANCE hInstance;    //窗體實例名,由windows自動分發
    HICON hIcon;            //顯示上面的圖標titlte
    HCURSOR hCursor;        //窗口光標
    HBRUSH hbrBackground;   //背景刷
    LPCTSTR lpszMenuName;   //窗口菜單
    LPCTSTR lpszClassName;  //窗體類名
} WNDCLASS, *PWNDCLASS;


窗體註冊函數是RegisterClass或RegisterClassEx,該函數註冊後在調用CreateWindow()或CreatewindowEx函數中使用的窗口類。原型如下:

ATOM WINAPI RegisterClass( _In_ const WNDCLASS *lpWndClass);
//lpWndClass 指向一個 WNDCLASS 結構的指針


窗口創建用到的函數理所是CreateWindow()或CreatewindowEx。這個函數是基於窗口類的,所以還需要指定幾個參數來制定特定的窗口。而且像一些不帶邊框的窗口是怎麼創建的也是具有相當的技巧的,就是創建的是不帶標題和邊框的窗口,然後自己在客戶區繪製程序的內容,能夠製作個性化的應用程序。函數原型如下,注意函數返回值爲創建窗體的句柄,這一點需格外關注。

HWND WINAPI CreateWindow(
    _In_opt_  LPCTSTR lpClassName,      // 窗口類名稱
    _In_opt_  LPCTSTR lpWindowName,     // 窗口標題
    _In_      DWORD dwStyle,            // 窗口風格,或稱窗口格式
    _In_      int x,                    // 初始 x 座標
    _In_      int y,                    // 初始 y 座標
    _In_      int nWidth,               // 初始 x 方向尺寸
    _In_      int nHeight,              // 初始 y 方向尺寸
    _In_opt_  HWND hWndParent,          // 父窗口句柄
    _In_opt_  HMENU hMenu,              // 窗口菜單句柄
    _In_opt_  HINSTANCE hInstance,      // 程序實例句柄
    _In_opt_  LPVOID lpParam             // 創建參數
);

(3).Windows消息的處理


              簡單講完消息的定義以及窗體的創建過程,下面開始探究消息如何傳遞到窗體中。

             應用程序中含有一段稱作“消息循環”的代碼,用來從消息隊列中檢索這些消息並把它們分發到相應的窗口函數中。Windows爲當前執行的每個Windows程序維護一個消息隊列。在發生輸入事件之後,Windows將事件轉換爲一個消息並將消息放入程序的消息隊列中。程序通過執行一塊稱之爲消息循環的程序代碼從消息隊列中取出消息。具體執行流程分爲三步:消息的發送、消息的接收以及消息的處理。

                                         

            消息的發送有3種方式:發送、寄送和廣播。發送消息的函數有SendMessage、SendMessageCallback、SendNotifyMessage、SendMessageTimeout;寄送消息的函數主要有PostMessage、PostThreadMessage、PostQuitMessage;廣播消息的函數我知道的只有BroadcastSystemMessage、BroadcastSystemMessageEx。其中 SendMessage主要是向一個或多個窗口發送一條消息,一直等到消息被處理之後纔會返回。不過需要注意的是,如果接收消息的窗口是同一個應用程序的一部分,那麼這個窗口的窗口函數就被作爲一個子程序馬上被調用;如果接收消息的窗口是被另外的線程所創建的,那麼窗口系統就切換到相應的線程並且調用相應的窗口函數,這條消息不會被放進目標應用程序隊列中。函數的返回值是由接收消息的窗口的窗口函數返回,返回的值取決於被髮送的消息。 PostMessage則把一條消息放置到創建hWnd窗口的線程的消息隊列中,該函數不等消息被處理就馬上將控制返回。

                                       


               消息的接收主要有3個函數:GetMessage、PeekMessage、WaitMessage。其中GetMessage是從調用線程的消息隊列裏取得一個消息並將其放於指定的結構。此函數可取得與指定窗口聯繫的消息和由PostThreadMessage寄送的線程消息。此函數接收一定範圍的消息值。GetMessage不接收屬於其他線程或應用程序的消息。獲取消息成功後,線程將從消息隊列中刪除該消息。函數會一直等待直到有消息到來纔有返回值PeekMessage與GetMessage功能類似,不同之處在於GetMessage不將控制傳回給程序,直到從程序的消息隊列中取得消息,但是PeekMessage總是立刻傳回,而不論一個消息是否出現。WaitMessage功能爲線程的消息隊列中無其它消息時,該函數就將控制權交給另外的線程,同時將該應用程序掛起,直到一個新的消息被放入應用程序的隊列之中才返回。

消息的處理是整個Windows消息處理的重點。典型處理流程如下:

 while(GetMessage(&msg, NULL, 0, 0))
{
       if(!TranslateAccelerator(msg.hWnd, hAccelTable, &msg))
      { 
            TranslateMessage(&msg);
            DispatchMessage(&msg);
       }
}


             首先,GetMessage從進程的主線程的消息隊列中獲取一個消息並將它複製到MSG結構,如果隊列中沒有消息,則GetMessage函數將等待一個消息的到來以後才返回。如果將一個窗口句柄作爲第二個參數傳入GetMessage,那麼只有指定窗口的的消息可以從隊列中獲得。GetMessage也可以從消息隊列中過濾消息只接受消息隊列中落在範圍內的消息。這時候就要利用GetMessage/PeekMessage指定一個消息過濾器。這個過濾器是一個消息標識符的範圍或者是一個窗體句柄,或者兩者同時指定。當應用程序要查找一個後入消息隊列的消息是很有用。WM_KEYFIRST 和 WM_KEYLAST 常量用於接受所有的鍵盤消息。 WM_MOUSEFIRST 和 WM_MOUSELAST 常量用於接受所有的鼠標消息。然後TranslateAccelerator判斷該消息是不是一個按鍵消息並且是一個加速鍵消息,如果是,則該函數將把幾個按鍵消息轉換成一個加速鍵消息傳遞給窗口的回調函數。處理了加速鍵之後,函數TranslateMessage將把兩個按鍵消息WM_KEYDOWN和WM_KEYUP轉換成一個 WM_CHAR,不過需要注意的是,消息WM_KEYDOWN,WM_KEYUP仍然將傳遞給窗口的回調函數。處理完之後,DispatchMessage函數將把此消息發送給該消息指定的窗口中已設定的回調函數。如果消息是WM_QUIT,則 GetMessage返回0,從而退出循環體。應用程序可以使用PostQuitMessage來結束自己的消息循環。通常在主窗口的 WM_DESTROY消息中調用。通過DispatchMessage函數即將WNDCLASS中的窗體過程函數關聯起來。

             窗體過程函數是一個用於處理所有發送到這個窗口的消息的函數。任何一個窗口類都有一個窗口過程。同一個類或一個父類的窗口使用同樣的窗口過程來響應消息。系統發送消息給窗口過程將消息數據作爲參數傳遞給他,消息到來之後,按照消息類型排序進行處理,其中的參數則用來區分不同的消息,窗口過程使用參數產生合適行爲。一個窗口過程不經常忽略消息,如果他不處理,它會將消息傳回到執行默認的處理。窗口過程通過調用DefWindowProc來做這個處理。窗口過程被所有屬於同一個類的窗口共享,能爲不同的窗口處理消息。通常窗口過程函數是通過一個switch語句來實現的,利用HANDLE_MSG消息分流器則可以把switch語句分成更小的函數,每一個消息都對應一個小函數,這樣做的好處就是對消息更容易管理。

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
        HANDLE_MSG(hWnd, WM_COMMAND, MsgCracker);
        HANDLE_MSG(hWnd, WM_DESTROY, MsgCracker);
    default:
        return DefWindowProc(hWnd, message, wParam, lParam);
    }
    return 0;
}

 LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
 int wmId, wmEvent;
 PAINTSTRUCT ps;
 HDC hdc;
 TCHAR szHello[MAX_LOADSTRING];
 LoadString(hInst, IDS_HELLO, szHello, MAX_LOADSTRING);
 
 switch (message) 
 {
  case WM_COMMAND:
         wmId    = LOWORD(wParam); 
         wmEvent = HIWORD(wParam); 
         switch (wmId)
         {
          case IDM_ABOUT:
             DialogBox(hInst, (LPCTSTR)IDD_ABOUTBOX, hWnd, (DLGPROC)About);
             break;
          case IDM_EXIT:
             DestroyWindow(hWnd);
             break;
          default:
             return DefWindowProc(hWnd, message, wParam, lParam);
         }
   break;
 
  case WM_PAINT:
         hdc = BeginPaint(hWnd, &ps);
         RECT rt;
         GetClientRect(hWnd, &rt);
         DrawText(hdc, szHello, strlen(szHello), &rt, DT_CENTER);
         EndPaint(hWnd, &ps);
         break;
 
  case WM_DESTROY:
         PostQuitMessage(0);
         break;
  default:
         return DefWindowProc(hWnd, message, wParam, lParam);
  }
  return 0;
}


二、一個簡單實例


              爲分析Windows消息處理過程,首先寫了一個最簡單的窗體實例作分析。

#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("Register"); //窗體名
    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);//顯示上面的圖標titlte
    wndclass.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 name
        TEXT("The Hello Program"),   // window caption
        WS_OVERLAPPEDWINDOW, // window style
        CW_USEDEFAULT,// initial x position
        CW_USEDEFAULT,// initial y position
        CW_USEDEFAULT,// initial x size
        CW_USEDEFAULT,// initial y size
        NULL, // parent window handle
        NULL, // window menu handle
        hInstance, // program instance handle
        NULL);
    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, "TEST", strlen("TEST"));
        EndPaint(hwnd, &ps);
        return 0;
    case   WM_DESTROY:
        PostQuitMessage(0);
        return 0;
    }
    return DefWindowProc(hwnd, message, wParam, lParam);
}


 這段代碼實現的是一個最簡單的窗體。效果如下:

                                      

 

下面用OD進行分析。首先觀察窗體創建過程,對RegisterClass和CreateWindow這兩個模塊間調用下斷點,注意觀察C代碼,該窗體類名爲"Register”。

 數據窗口選中的即爲WNDCLASS結構體。最後四字節爲窗體類名地址,地址爲0xF3A000,數據窗口跟隨查看,可知結果符合,結果如下所示:

                       

窗體處理函數地址爲WNDCLASS結構體的第二個四字節,爲0XF31352。在該函數處下斷點。繼續向下調試。

從堆棧段可以看到相應參數,其中窗體類名爲Register。往下調試一步:

寄存器EAX返回值即爲句柄值,0XB1346。查看窗體:

                  

可以看到第一行的句柄、類名均符合。繼續F9往下調試。

可發現程序停在0XF31352處,即可說明該處爲窗體函數入口。

三、更深入一步分析


            剛剛的小例子只是簡單進行了分析,並沒有深入分析,尤其是窗體處理函數的定位。下面我以一個CrackMe爲例子進行分析。在吾愛論壇上面找了一個非常簡單的CM進行分析(無複雜算法、無殼)。需要輸入正確的註冊碼然後點擊按鈕進行破解。效果如下:

                                      

            這個CM本身難度不大,通過簡單的搜索字符串即可破解,但爲了詳細分析消息機制,我們從正面進行分析。

按照常規套路,對RegisterClass和CreateWindow這兩個模塊間調用下斷點:

 

觀察兩個RegisterClass函數中WNDCLASS結構體,發現三個窗體的窗體過程函數的地址均爲0X77782280,且該代碼位於系統代碼段內(ntdll.dll中),與直觀認識不符,這個是爲什麼呢?首先解決第一個問題,即兩個窗體註冊函數的窗體過程函數爲什麼一樣,這是因爲一個窗口類只有一個窗口函數,所有使用該窗口類創建的窗口都是使用同一個窗口函數,除非後來用SetWindowLong改變了窗口函數。查看窗口如下,可以看到窗口均是使用WTWindow創建的。

第二個問題就是爲什麼窗體過程函數的地址會位於系統代碼段內,這是因爲程序不是直接跳轉至窗體過程函數,而是通過user32.CallWindowProc這個函數來調用窗體過程函數,這個後面結合分析再具體講解。注意觀察類名爲"按鈕"的窗體創建時返回的句柄值,該值比較重要,後面會用到。

完成窗體註冊和窗體創建,下面就是消息處理過程。對TranslateMessage和DispatchMessage函數下斷點。

用IDA看可能會更直觀:

                     

回到DispatchMessage函數,函數原型如下:

LONG DispatchMessage(CONST MSG*lpmsg)
//lpmsg:指向含有消息的MSG結構的指針。


由MSG結構可知,第一個四字節爲窗體句柄,第二個四字節爲消息標識符,查看數據段可知,句柄爲0X371394,與預期相符;消息標識符爲0X202。查表可知,0X202表示按鈕鬆開消息,與操作符合。考慮到該CM的觸發條件爲按鈕,因此可以在此設立條件斷點:

                  

以上均是常規分析,現在重點來了,如何定位真正的窗體處理函數用以破解該CM?

解決這個問題的關鍵就在於CallWindowProc函數,WNDCLASS結構體中的地址其實只是一個跳轉地址,程序是通過CallWindowProc函數調用窗體過程函數。CallWindowProc是將消息信息傳送給指定的窗口過程的函數。使用函數CallWindowsProc可進行窗口子分類。通常來說,同一類的所有窗口共享一個窗口過程。子類是一個窗口或者相同類的一套窗口,在其消息被傳送到該類的窗口過程之前,這些消息是由另一個窗口過程進行解釋和處理的。CallWindowProc函數原型如下:

LRESULT CallWindowProc(WNDPROC lpPrevWndFunc, HWND hWnd, UINT Msg, WPARAM wParam, LPARAM IParam);
//hWnd:指向接收消息的窗口過程的句柄。
//Msg:指定消息類型。
//wParam:指定其餘的、消息特定的信息。該參數的內容與Msg參數值有關。
//IParam:指定其餘的、消息特定的信息。該參數的內容與Msg參數值有關。
//返回值:返回值指定了消息處理結果,它與發送的消息有關。


既然如此,我們在回調函數地址0X77782280與CallWindowProc函數出均設置斷點(也可以利用DefWindowProc設置斷點,方法類似,但斷點條件不一樣),當消息標識符爲0X202時激活這兩個斷點(CallWindowProc調用位置較多,需判斷哪個纔是需要的),利用Run逐步跟蹤,記錄兩個斷點內執行過程。結果共計1568條彙編代碼。按照正序分析,重點關注應用程序段代碼的起始位置代碼:

                                      

其中0X45AB89處以上代碼均爲系統代碼段,不用關心。利用IDA跟蹤0X45AB89至代碼段:

這個結構有沒有很熟悉,有沒有豁然開朗的感覺?下面來逐個解釋。DefWindowProc函數是調用缺省的窗口過程來爲應用程序沒有處理的任何窗口消息提供缺省的處理,確保每一個消息得到處理。CWnd是MFC窗口類的基類,提供了微軟基礎類庫中所有窗口類的基本功能。我們在0X45AB89處設端點,當消息標識符爲0X202觸發。

詳細分析過程可以總結出如下消息過程:

                             
其中消息標識符爲0X111標識消息WM_COMMAND。當用戶從菜單選中一個命令項目、當一個控件發送通知消息給去父窗口或者按下一個快捷鍵將發送 WM_COMMAND 消息,窗口則通過WindowProc函數收到此消息。斷點條件可更改爲0X111。

可見第一個參數爲0X371394,與之前窗體創建系統分配的句柄相同,第二個參數爲0X111,與消息標識符相同。說明了我們判斷的正確性。消息通過系統段代碼後執行至0X45AB89處,並將參數傳入。

接下來我們繼續分析sub_45AB89函數。sub_45AB04函數如下:

                    

afxMapHWND是用於保存CWnd類和HWND之間的映射。在Windows體系中,很多對象都是以句柄的形式展示給開發人員的。比如窗口句柄(HWND),繪圖設備(HDC)等等。然後大部分的API函數則圍繞 這些句柄做文章。比如ShowWindow,SetWindowText, TextOut等等。這些API函數的第一個參數通常就是句柄了。但是在C++ 體系中,這種對於事物細節的訪問,往往是有違其封裝精神的。因此MFC做了很多的封裝類,來隱藏這些細節。應運而生就是CWnd,CDC等類。通過這些類暴露的方法,可以直接對句柄做操作,而又可以不去關心他。MFC中有大量的全局變量,其中一個全局變量是一張HWND與CWnd的Map表。MFC提供了全局函數afxMapHWND用於獲得這個Map表。雖然在MFC中,都是對象在與對象打交道。但是MFC也要與Windows系統打交道。Windows給你的只有句柄,那麼如何通過這些句柄找到相對應的類呢?通過Map表就能輕鬆的解決這個問題。比如在Windows的消息機制中,當WndProc接收到一個消息的時候,只會得到一個HWND hWnd的目標窗口,如何找到匹配的類?從m_pmapHWND中搜索就行了。通過調用CWnd的靜態成員函數FromHandlePermanent,我們就能輕鬆的從Map表中找到與hWnd相對應的CWnd類。FromHandlePermanent的實現也非常簡單。首先通過afxMapHWND找到m_pmapHWND,然後通過m_pmapHWND的成員函數LookupPermanent查找與hWnd對應的CWnd指針,最後返回他。CMapPtrToPtr::GetValueAt則用於獲取具體值。

我們利用OD在0X45A912處下斷點,查看參數值。

可以看到v5[7] =0X321336,符合預期。下面利用IDA再重點分析sub_45A912函數:

紅色圈出是需要重點關注的關鍵函數。利用OD在該處設置斷點,觀察函數與參數。由此可以猜測,Cwnd爲一地址值,通過該地址加上偏移找尋函數地址。

可見函數爲0X45B940,第二個參數爲消息標識符0X111,第三第四個參數爲句柄,0X321336。利用IDA觀察sub_45B940:

至此已基本定位關鍵處理函數,而後按部就班的逐步分析就可以準確定位判斷語句。


 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章