前文作為系列的開篇,我們站在Notepad.exe的視角,看它接過系統傳來的消息,交由Notepad的窗口處理函數(WndProc)進行處理的過程。

User32.dll!DispatchMessage API是前面"系統傳來"中的一環,也是最靠近應用層的一環,將窗口消息傳遞給應用。本文從該API切入,逐漸遠離熟悉的應用層。

原本本文想結合Ollydbg的消息斷點演示,奈何消息斷點存在各種限制,因此我在DispatchMessage上下條件斷點,更精準的達到相同目的。

用過Ollydbg的讀者一定對Ollydbg中的條件斷點并不陌生,我先演示使用條件斷點的標準流程。

1.1查找RegisterClassEx并下斷

1.2 Notepad被斷點中斷后,分析調用RegisterClassExW時傳入的窗口類:

上圖Ollydbg的堆棧區窗口中:

a.ESP指向調用USER32.RegisterClassExW API的指令的返回地址 ;

b.ESP+4存放RegisterClassExW的參數WNDCLASSEX結構的地址,其結構定義如下,其中域變量lpfnWndProc位于結構體中的Offset 8 Byte處:

typedef struct tagWNDCLASSEXW {
    UINT        cbSize; // Offset 0
    /* Win 3.x */
    UINT        style; //  Offset 4
    WNDPROC     lpfnWndProc; //Offset 8 窗口過程地址
    int         cbClsExtra;
    int         cbWndExtra;
    HINSTANCE   hInstance;
    HICON       hIcon;
    HCURSOR     hCursor;
    HBRUSH      hbrBackground;
    LPCWSTR     lpszMenuName;
    LPCWSTR     lpszClassName;
    /* Win 4.0 */
    HICON       hIconSm;
} WNDCLASSEXW, *PWNDCLASSEXW, NEAR *NPWNDCLASSEXW, FAR *LPWNDCLASSEXW;

PS:雖然lpfnWndProc標注為窗口過程地址,但是只有少數CrackMe在lpfnWndProc指向的函數中進行消息處理。因此,只在此處未必是解決CrackMe的銀彈。這才引出使用DispatchMessage追蹤窗口函數的必要性。

在Ollydbg的堆棧區窗口中"ESP+4"處右鍵"Follow in dump",即可在數據區查看WNDCLASSEX各個域變量的值。圖中0x19FB64(WNDCLASSEX + 0x08)處存放Notepad窗口過程(WndProc)地址(0x401B90):

在Ollydbg數據區0x19FB64處右鍵"Follow DWORD in Disassembler",即可在指令窗口顯示Notepad窗口過程的反匯編。(我用LoadMapEx加載了Notepad的map文件,因此Comment區域會顯示窗口過程的符號名_NpWndProc)Notepad的窗口過程位于0x0401B90(很明顯該過程位于Notepad.exe 代碼段內),先記錄這個地址并在下斷點

通過分析RegisterClassEx的參數,我們認為Notepad的窗口過程位于0x0401B90,然而,Ollydbg工具欄中"W"給出的窗口過程/ClsProc都沒給出這個地址

1.3 給Notepad.exe窗口下條件斷點.

對于CrackMe練習,一般下一步是在Edit窗口ClsProc上下消息斷點,奈何消息斷點在單文檔窗口程序上似乎失效了(百度搜索"消息斷點失效",提問者不少解答者寥寥),于是變通為給DispatchMessageA/W下條件斷點。DispatchMessage的唯一參數為MSG,結構如下:

typedef struct tagMSG {
    HWND        hwnd;
    UINT        message;
    WPARAM      wParam;
    LPARAM      lParam;
    DWORD       time;
    POINT       pt;
#ifdef _MAC
    DWORD       lPrivate;
#endif
} MSG, *PMSG, NEAR *NPMSG, FAR *LPMSG;

DispatchMessageA/W下條件斷點的步驟如下:

a. 先給DispatchMessage下個普通斷點(bp DisaptchMessageA/W),斷下后結合堆棧來拼湊成條件斷點;

簡單說明以下,在Ollydbg堆棧區中:
"ESP ==>":指向返回地址;
"ESP+4":指向參數MSG的地址。
在ESP+4處右鍵"Follow in Dump"將在ollydbg數據區解析MSG各個成員:
MSG+0x00 (0x19FCD0):0x00307E4,為窗口句柄
MSG+0x04 (0x19FCD4):0x60,為消息值

據此,得到DispatchMessageW條件斷點的表達式: 

1.4.設置內存訪問斷點

當Notepad收到按鍵抬起消息時,ollydbg會中斷在條件斷點處。一般為了追蹤處理消息的窗口過程中,Cracker此時會在Ollydbg中對代碼段下內存訪問斷點。對于本文就是對Notepad的代碼段下內存訪問斷點。

點擊Ollydbg工具欄的"M",顯示模塊窗口:

再次運行ollydbg,程序馬上會暫定在Notepad的代碼段中,暫定處大概率是窗口過程。對于CrackMe,剩下的是分析注冊碼算法了,但是此處,我提出2個調試過程中遇到的值得深思的問題:

a.如果Notepad.exe的代碼量極大,窗口過程恰好位于其他dll中,那么通過下內存訪問斷點來定位窗口過程的方式是不是失效了?

b.有別于練手的CrackMe程序,對于多線程程序,就如Notepad.exe,設置內存訪問斷點后,其他線程也會訪問代碼段(如訪問網絡讀取數據),如何從中挑選出窗口過程?這無異于引入了大量的噪聲,增加的分析的難度。(簡單如Notepad.exe也有15個線程)

如何解決上述2個問題?讓我們深入DispatchMessage函數。

2.USER32._InternalCallWinProc

借助前面RegisterClassExW給窗口過程下的斷點,繼續運行Ollydbg,Ollydbg中斷。點擊Ollydbg工具欄"K",查看函數調用堆棧:

第一第四棧幀有點眼熟,它顯示了窗口程序在User32模塊中從DispatchMessage API進入窗口過程的全過程。借助IDA為User32.dll生成map文件/Ollydbg LoadMap插件加載新生成的Map文件,可以獲得相對友好的調用堆棧:

2.1. IDA生成User32.Map

IDA加載User32.dll,點擊File--Produce file--Create MAP file,生成User32.map:

2.2. Ollydbg加載Map

Ollydbg--Plugins--LoadMapEx--LoadMapEx加載IDA生成的MAP:

(注:一定要在User32.dll模塊的地址空間中加載User32.map,否則會干擾ollydbg的分析功能.一旦干擾ollydbg的分析功能,只能通過移除分析結果來恢復)

再次打開調用堆棧,得到較為友好的調用堆棧:

雖然圖中有部分函數地址沒有解析出來,但是通過單步跟蹤可以得到如下調用鏈:

DispatchMessageW
|-->DispatchMessageWorker
    |-->UserCallWinProcCheckWow
        |-->InternalCallWinProc

同時借助泄露的win xp源碼一探究竟:UserCallWinProcCheckWow的實現位于NT\windows\core\ntuser\client\clmsg.c

LRESULT
UserCallWinProcCheckWow(
    PACTIVATION_CONTEXT pActCtx,
    WNDPROC pfn,
    HWND hwnd,
    UINT msg,
    WPARAM wParam,
    LPARAM lParam,
    PVOID pww,
    BOOL fEnableLiteHooks)
{
    BOOL fInsideHook;
    LRESULT lRet = 0;
 
    BEGIN_CALLWINPROC(fInsideHook, lRet)
 
        BOOL fOverride = fInsideHook && fEnableLiteHooks && IsMsgOverride(msg, &guah.uoiWnd.mm);
 
        pfn = MapKernelClientFnToClientFn(pfn);
 
        if (fOverride) {
            /*
             * NOTE: It is important that the same lRet is passed to all three
             * calls, allowing the Before and After OWP's to examine the value.
             */
            void * pvCookie = NULL;
            if (guah.uoiWnd.pfnBeforeOWP(hwnd, msg, wParam, lParam, &lRet, &pvCookie)) {
                goto DoneCalls;
            }
 
            lRet = (IsWOWProc(pfn) ? (*pfnWowWndProcEx)(hwnd, msg, wParam, lParam, PtrToUlong(pfn), KPVOID_TO_PVOID(pww)) :
                InternalCallWinProc((WNDPROC)KPVOID_TO_PVOID(pfn), hwnd, msg, wParam, lParam));
 
            if (guah.uoiWnd.pfnAfterOWP(hwnd, msg, wParam, lParam, &lRet, &pvCookie)) {
                // Fall through and exit normally
            }
DoneCalls:
            ;
        } else {
            lRet = (IsWOWProc(pfn) ? (*pfnWowWndProcEx)(hwnd, msg, wParam, lParam, PtrToUlong(pfn), KPVOID_TO_PVOID(pww)) :
                InternalCallWinProc((WNDPROC)KPVOID_TO_PVOID(pfn), hwnd, msg, wParam, lParam));
        }
    END_CALLWINPROC(fInsideHook)
 
    return lRet;
#ifdef _WIN64
    UNREFERENCED_PARAMETER(pww);
#endif // _WIN64
}

InternalCallWinProc是宏,定義于NT\windows\core\ntuser\client\callproc.h 

#define InternalCallWinProc(winproc, hwnd, message, wParam, lParam)    \
    (winproc)(hwnd, message, wParam, lParam)

其中winproc是函數指針,隨之猜想,winproc可能會有機會指向Notepad.exe代碼段中的某個地址。

3.精準型消息斷點

先給前面猜想的結論,當UserCallWinProcCheckWow函數調用InternalCallWinProc時,winproc有機會(winproc還會指向ntdll中的回調函數)會指向Notepad的窗口過程:

如上圖,調用InternalCallWinProc,EBX指向notepad.NPWndProc。既然如此,我可以在調用InternalCallWinProc時下條件斷點(我就稱它為精準型消息斷點)

圖中ebx的取值需要根據代碼段的起始/結束位置做調整。

當ollydbg中斷時,結合InternalCallWinProc的函數原型可知:

EBX指向真正的窗口過程-->這就是我提出的精準型消息斷點,可以解決前面提出的問題。

ESI指向接受消息的窗口句柄。

EDI指向消息類型。

當然,我們還能進一步編輯條件斷點的條件,過濾出特定的窗口消息WM_KEYUP消息。