前文作為系列的開篇,我們站在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消息。
安全內參
聚銘網絡
安全內參
一顆小胡椒
看雪學苑
安全內參
看雪學苑
安全圈
CNCERT國家工程研究中心
系統安全運維
GoUpSec
一顆小胡椒