Mfc程序逆向 ? 消息篇(上)[转自看雪]
http://www.cnblogs.com/feng801/archive/2010/05/21/1740969.html
标 题: 【原创】MFC程序逆向 – 消息篇(上)+(下) 11楼 作 者: szdbg 时 间: 2007-10-31,06:26:02 链 接: http://bbs.pediy.com/showthread.php?t=54150 前言: 记得前一段时间,我刚接触软件破解和逆向这一行时,对于一些软件不知从何处跟踪按钮消息,试了好多方法,就是断不下来,在系统模块中经常转得晕头转向,而一无所获。 MFC程序是一种常见类型的程序,我静下心来,潜心研究了一下MFC消息流程。弄清原委之后,一切豁然开朗,发现跟踪MFC程序和消息处理原来是如此。。。,跟踪按钮事件处理也由此变得特别简单。 于是,我将这些研究整理成文,以备后忘。并希望对和我一样的菜鸟有所帮助,有误之处,请高手指正。 本 文目的就是以一个MFC的标准对话框程序为例,同时从源码和反汇编代码两方面来研究MFC消息的流程走向,弄清MFC消息路径的所有站点,这样就可以任意 定位MFC的所有消息事件,可以从任一站点切入,进行跟踪分析MFC的处理过程。甚至可以从PumpMessage大本营出发,一直全程跟踪,做到心中有 数,不慌不乱。 关于对话框的启动过程,其过程很简单,程序进入WinMain函数之后,会调用对话框的DoModal函数,然后就进入 RunModalLoop函数,消息循环在这里就开始了,限于篇幅,本文不作多说,有兴趣者可看看MFC源码。本篇重点在于分析MFC的消息分发处理的过 程。 先看一下RunModalLoop函数部分源码: int CWnd::RunModalLoop(DWORD dwFlags) { ... for (;;) { ... do { if (!AfxGetThread()->PumpMessage()) // pump message, but quit on WM_QUIT { AfxPostQuitMessage(0); return -1; } ... } while (::PeekMessage(pMsg, NULL, NULL, NULL, PM_NOREMOVE)); } ... } 这 里,AfxGetThread()->PumpMessage()是MFC消息处理的大本营,MFC程序的所有消息就是从这里开始,经过重重路径转 换,翻山越岭,中途直达Windows系统内核,再返回到MFC地界,又途经不少周折,才找到最终目的地 – 消息函数地址。可谓是山重水复疑无路,柳暗 花明又一村。 一个按钮点击事件的过程如下: CWinThread::PumpMessage -> CWnd::PretranslateMessage -> CWnd::WWalkPreTranslateMessate -> CD1Dlg::PreTranslateMessage -> CDialog::PreTranslateMessage -> CWnd::PreTranslateInput -> CWnd::IsDialogMessageA -> USER32 内 核 -> AfxWndProcBase -> AfxWndProc -> AfxCallWndProc -> CWnd::WindowProc -> CWnd::OnWndMsg -> CWnd::OnCommand -> CDialog::OnCmdMsg -> CCmdTarget::OnCmdMsg -> _AfxDispatchCmdMsg -> CD1Dlg::OnButton1() VC下,可以随手写一个标准的对话框程序,上面放一个按钮,点击按钮后,弹出一个消息框。我们现在就从PumpMessage()开始,来分析这中间的消息流程: 1. CWinThread::PumpMessage函数 (消息泵) BOOL CWinThread::PumpMessage() { //GetMessage 当消息为WM_QUIT时,返回0,其它消息时,返回TRUE,有错误时,返回-1 if (!::GetMessage(&m_msgCur, NULL, NULL, NULL)) return FALSE; if (m_msgCur.message != WM_KICKIDLE && !PreTranslateMessage(&m_msgCur)) { ::TranslateMessage(&m_msgCur); ::DispatchMessage(&m_msgCur); } return TRUE; } PumpMessage只有在接收到WM_QUIT消息时,才返回FALSE,其它情况,返回TRUE。由于CWinThread::PumpMessage()函数负责从消息队列中获取消息、翻译消息以及分发消息等,因此习惯将此函数称之为“消息泵”。 在 PumpMessage函数中,PreTranslateMessage函数至关重要,正是有了这个PreTranslateMessage(),才使得 MFC能够灵活的控制消息的分发模式,可以说,PreTranslateMessage()就是MFC的实现消息分发模式的工具。 PumpMessage函数反汇编代码: 73D31194 > 56 PUSH ESI ... 73D311A1 FF15 B0B6DC73 CALL DWORD PTR DS:[<&USER32.GetMessageA>] 73D311A7 85C0 TEST EAX,EAX 73D311A9 74 26 JE SHORT MFC42.73D311D1 ;收到WM_QUIT,退出程序 73D311AB 817E 38 6A030000 CMP DWORD PTR DS:[ESI+38],36A 73D311B2 74 1A JE SHORT MFC42.73D311CE 73D311B4 8B06 MOV EAX,DWORD PTR DS:[ESI] 73D311B6 57 PUSH EDI 73D311B7 8BCE MOV ECX,ESI 73D311B9 FF50 60 CALL DWORD PTR DS:[EAX+60] ; PreTranslateMessage (消息预处理) 73D311BC 85C0 TEST EAX,EAX 73D311BE 75 0E JNZ SHORT MFC42.73D311CE 73D311C0 57 PUSH EDI ;消息预处理返回FALSE 73D311C1 FF15 ACB6DC73 CALL DWORD PTR DS:[<&USER32.TranslateMessage>] 73D311C7 57 PUSH EDI 73D311C8 FF15 30B6DC73 CALL DWORD PTR DS:[<&USER32.DispatchMessageA>] ; 73D311CE 6A 01 PUSH 1 ;返回TRUE 73D311D0 58 POP EAX 73D311D1 5F POP EDI 73D311D2 5E POP ESI 73D311D3 C3 RETN 提示: a. OD加载程序后,调出MFC42.dll模块,定位到PumpMessage代码入口处。 b. 在CALL DWORD PTR DS:[EAX+60]这一条语句上设置条件断点[[esp]+4]==202,即可设置鼠标左键释放断点。 说明:call [eax+60]是调用PreTranslateMessage函数,入口参数为:MSG* pMsg,所以: [esp]就是pMsg,而[[esp]]就是pMsg->hWnd , [[esp]+4]就是pMsg->Message c. [[esp]]==002407B4 && [[esp]+4]==202 可以为指定按钮设置点击断点。这里002407B4是目标按钮的句柄. 2. CWinThread::PreTranslateMessage函数 BOOL CWinThread::PreTranslateMessage(MSG* pMsg) { // if this is a thread-message, short-circuit this function if (pMsg->hwnd == NULL && DispatchThreadMessageEx(pMsg)) return TRUE; CWnd* pMainWnd = AfxGetMainWnd(); // 通过WalkPreTranslateTree 进行消息分发 if (CWnd::WalkPreTranslateTree(pMainWnd->GetSafeHwnd(), pMsg)) return TRUE; // 消息分发处理关键 if (pMainWnd != NULL) { CWnd* pWnd = CWnd::FromHandle(pMsg->hwnd); if (pWnd->GetTopLevelParent() != pMainWnd) return pMainWnd->PreTranslateMessage(pMsg); //程序主框架处理消息 } return FALSE; // no special processing } PreTranslateMessage函数反汇编部分代码 73D313D0 > PUSH ESI 73D313D1 PUSH EDI 73D313D2 MOV EDI,DWORD PTR SS:[ESP+C] 73D313D6 CMP DWORD PTR DS:[EDI],0 73D313D9 JE MFC42.73D8E9A1 73D313DF CALL MFC42.#6575_?AfxGetMainWnd@@YGPAVCW> 73D313E4 MOV ESI,EAX 73D313E6 TEST ESI,ESI 73D313E8 JE SHORT MFC42.73D313ED 73D313EA MOV EAX,DWORD PTR DS:[ESI+20] 73D313ED PUSH EDI 73D313EE PUSH EAX 73D313EF CALL MFC42.#6367_?WalkPreTranslateTree@C> 73D313F4 TEST EAX,EAX 73D313F6 JNZ SHORT MFC42.73D31415 73D313F8 > TEST ESI,ESI 73D313FA JE SHORT MFC42.73D3140E 73D313FC PUSH DWORD PTR DS:[EDI] 73D313FE CALL MFC42.#2864_?FromHandle@CWnd@@SGPAV> 73D31403 MOV ECX,EAX 73D31405 CALL MFC42.#3815_?GetTopLevelParent@CWnd> 73D3140A CMP EAX,ESI 73D3140C JNZ SHORT MFC42.73D3141A 73D3140E XOR EAX,EAX 73D31410 POP EDI 73D31411 POP ESI 73D31412 RETN 4 提示: a. OD加载程序后,调出MFC42.dll模块,定位到PreTranslateMessage代码入口处。 b. 在函数入口处设置条件断点[[esp+4]+4]==202,即可设置鼠标左键释放断点。 说明:此函数的入口参数为:MSG* pMsg,在入口处时,[esp]是函数返回地址,所以: [esp+4]就是pMsg,而[[esp+4]]就是pMsg->hWnd , [[esp+4]+4]就是pMsg->Message c. [[esp+4]]==002407B4 && [[esp+4]+4]==202 可以为指定按钮设置点击断点。这里002407B4是目标按钮的句柄. 3. CWnd::WalkPreTranslateTree函数 CWnd::WalkPreTranslateTree() 的所使用的策略很简单,拥有该消息窗口最先获得该消息的处理权,如果它不想对该消息进行处理(该窗口对象的PreTranslateMessage()函 数返回FALSE),就将处理权交给它的父亲窗口,如此向树的根部遍历,直到遇到hWndStop(在 CWinThread::PreTranslateMessage()中,hWndStop表示的是线程主窗口的句柄)。 记住这个消息处理权的传递方向,是由树的某个一般节点或叶子节点向树的根部传递! BOOL PASCAL CWnd::WalkPreTranslateTree(HWND hWndStop, MSG* pMsg) { for (HWND hWnd = pMsg->hwnd; hWnd != NULL; hWnd = ::GetParent(hWnd)) //从当前窗口到父窗口,逐层往上 { CWnd* pWnd = CWnd::FromHandlePermanent(hWnd); if (pWnd != NULL) { // target window is a C++ window if (pWnd->PreTranslateMessage(pMsg)) return TRUE; //消息被某一窗口处理了,返回 } if (hWnd == hWndStop) break; // got to hWndStop window without interest } return FALSE; // no special processing } 正是这个if (pWnd->PreTranslateMessage(pMsg)) return TRUE; 才实现了MFC灵活的消息分发处理机制。MFC程序各个窗口类中重载的PreTranslateMessage虚函数,都是从这里进来的。 MFC从当前消息窗口类逐级向上搜索执行各个类的PreTranslateMessage函数,只要有一个PreTranslateMessage函数 返回TRUE,WalkPreTranslateTree就中止搜索,并返回TRUE,否则返回FALSE。 在PumpMessage函数中最终就是根据WalkPreTranslateTree函数的返回值决定是否要由Windows系统进行消息处理与分发。 WalkPreTranslateTree函数反汇编代码如下: 73D31389 MOV EDI,EDI ; D1.0040308C 73D3138B PUSH ESI 73D3138C PUSH EDI 73D3138D MOV EDI,DWORD PTR SS:[ESP+10] 73D31391 MOV ESI,DWORD PTR DS:[EDI] 73D31393 JMP SHORT MFC42.73D313BD 73D31395 /PUSH ESI 73D31396 |CALL MFC42.#2867_?FromHandlePermanent@CWnd@@SGPAV> ;取得CWnd 指针值 73D3139B |TEST EAX,EAX ;CWnd * 值不为NULL时,则调用CWnd::PreTranslateMessage() 73D3139D |JE SHORT MFC42.73D313AE 73D3139F |MOV EDX,DWORD PTR DS:[EAX] 73D313A1 |PUSH EDI 73D313A2 |MOV ECX,EAX 73D313A4 |CALL DWORD PTR DS:[EDX+98] ;<JMP.&MFC42.#5280_?PreTranslateMessage > ;通过虚函数方式调用 73D313AA |TEST EAX,EAX 73D313AC |JNZ SHORT MFC42.73D313C8 ;消息被处理了,返回TRUE 73D313AE |CMP ESI,DWORD PTR SS:[ESP+C] 73D313B2 |JE SHORT MFC42.73D313C1 73D313B4 |PUSH ESI ; /hWnd 73D313B5 |CALL DWORD PTR DS:[<&USER32.GetParent>] ; \GetParent 73D313BB |MOV ESI,EAX 73D313BD TEST ESI,ESI 73D313BF \JNZ SHORT MFC42.73D31395 73D313C1 XOR EAX,EAX ;返回FALSE 73D313C3 POP EDI 73D313C4 POP ESI 73D313C5 RETN 8 73D313C8 XOR EAX,EAX 73D313CA INC EAX ;返回 TRUE 73D313CB JMP SHORT MFC42.73D313C3 跟踪说明: 在上面一句73D313A4 CALL DWORD PTR DS:[EDX+98] 设置按钮点击条件断点: [[esp]]==002407B4 && [[esp]+4]==202 可以发现:当点击按钮后,按钮点击事件函数代码就会在这条语句后执行,当按钮事件函数代码执行完毕后,CALL才会返回TRUE。 4. CD1Dlg::PreTranslateMessage函数 BOOL CDialog::PreTranslateMessage(MSG* pMsg) { ... return CDialog::PreTranslateMessage(pMsg); } 若 接收消息的窗口类重载了PreTranslateMessage函数,则此时会调用它,否则就进入第5步。实际应用中,这里很有可能是消息流程的一个分水 岭,可能走向两条不同的道路。这完全取决于应用程序新增的代码,若应用程序在这里返回TRUE,消息流程就返回去了。否则,就会继续往下执行。 在跟踪按钮消息时,此处应作为一个注意点,而设置断点的最佳位置是在上一步WalkPreTranslateTree函数中所说的位置,跟踪下来,注意消息流程的走向。 5. CDialog::PreTranslateMessage函数 BOOL CDialog::PreTranslateMessage(MSG* pMsg) { if (CWnd::PreTranslateMessage(pMsg)) return TRUE; CFrameWnd* pFrameWnd = GetTopLevelFrame(); if (pFrameWnd != NULL && pFrameWnd->m_bHelpMode) return FALSE; if (pMsg->message == WM_KEYDOWN && (pMsg->wParam == VK_ESCAPE || pMsg->wParam == VK_CANCEL) && (::GetWindowLong(pMsg->hwnd, GWL_STYLE) & ES_MULTILINE) &&_AfxCompareClassName(pMsg->hwnd, _T("Edit"))) { HWND hItem = ::GetDlgItem(m_hWnd, IDCANCEL); if (hItem == NULL || ::IsWindowEnabled(hItem)) { SendMessage(WM_COMMAND, IDCANCEL, 0); return TRUE; } } return PreTranslateInput(pMsg); // 消息流入此处 } CDialog::PreTranslateMessage()反汇编代码如下: 73D468A4 > PUSH ESI 73D468A5 PUSH EDI 73D468A6 MOV EDI,DWORD PTR SS:[ESP+C] 73D468AA MOV ESI,ECX 73D468AC PUSH EDI 73D468AD CALL MFC42.#5290_?PreTranslateMessage@CWnd@@UAEHPAUtagMSG@> 73D468B2 TEST EAX,EAX 73D468B4 JNZ MFC42.73D8D490 73D468BA MOV ECX,ESI 73D468BC CALL MFC42.#3813_?GetTopLevelFrame@CWnd@@QBEPAVCFrameWnd@@> 73D468C1 TEST EAX,EAX 73D468C3 JNZ MFC42.73D8D429 73D468C9 CMP DWORD PTR DS:[EDI+4],100 73D468D0 JE SHORT MFC42.73D468DF 73D468D2 PUSH EDI 73D468D3 MOV ECX,ESI 73D468D5 CALL MFC42.#5278_?PreTranslateInput@CWnd@@QAEHPAUtagMSG@@@> ;消息从流入此处 73D468DA POP EDI 73D468DB POP ESI 73D468DC RETN 4 6. CWnd::PreTranslateInput函数 BOOL CWnd::PreTranslateInput(LPMSG lpMsg) { if ((lpMsg->message < WM_KEYFIRST || lpMsg->message > WM_KEYLAST) && (lpMsg->message < WM_MOUSEFIRST || lpMsg->message > WM_MOUSELAST)) // 过滤消息 return FALSE; return IsDialogMessage(lpMsg); } 从 源码中可以看出,这个函数是对消息进行过滤,对于按键消息和鼠标消息,直接返回FALSE,然后再返回到PumpMessge函数中,调用 TranslageMessage()和DispatchMessage()函数,进行消息转换和分发,再进入MFC。对于其它消息,则调用 CWnd::IsDialogMessage()函数进行下一步处理。 CWnd::PreTranslateInput()函数反汇编代码如下: 73D34009 > MOV EDX,DWORD PTR SS:[ESP+4] 73D3400D MOV EAX,DWORD PTR DS:[EDX+4] 73D34010 CMP EAX,100 73D34015 JNB SHORT MFC42.73D34023 73D34017 CMP EAX,200 73D3401C JNB SHORT MFC42.73D34032 73D3401E XOR EAX,EAX 73D34020 RETN 4 73D34023 CMP EAX,108 73D34028 ^ JA SHORT MFC42.73D34017 73D3402A PUSH EDX 73D3402B CALL MFC42.#4047_?IsDialogMessageA@CWnd@@QAEHPAUtagMSG@@@Z 73D34030 ^ JMP SHORT MFC42.73D34020 73D34032 CMP EAX,209 73D34037 ^ JBE SHORT MFC42.73D3402A 73D34039 ^ JMP SHORT MFC42.73D3401E 7. CWnd::IsDialogMessageA函数 BOOL CWnd::IsDialogMessage(LPMSG lpMsg) { if (m_nFlags & WF_OLECTLCONTAINER) return afxOccManager->IsDialogMessage(this, lpMsg); else return ::IsDialogMessage(m_hWnd, lpMsg); } 这里会转进User32.IsDialogMessageA函数,从而转入系统内核,由Windows系统再来负责将消息的分发传送到各个目标窗口。 注:User32.IsDialogMessage 并不是象它的名字那样用来检查对话框消息的,而是用来解释或转换消息的。更贴切的名字应该是TranslateDialogMessage。 CWnd::IsDialogMessage实际上是一个以LPMSG作为参数,再加上内部的m_hWnd参数来调用 User32.IsDialogMessage的打包函数。这样,MFC中每一个对话框都会解释自己的输入。所以,若同时运行五个对话框,每一个对话框的 PreTranslateMessage都会自动调用User32.IsDialogMessage,而且运转良好,完全可以不用我们编程处理,MFC真 是太牛了。 CWnd::IsDialogMessageA函数反汇编代码: 73D468F5 > PUSH ESI 73D468F6 MOV ESI,ECX 73D468F8 TEST BYTE PTR DS:[ESI+29],1 73D468FC JNZ MFC42.73D8E273 73D46902 PUSH DWORD PTR SS:[ESP+8] 73D46906 PUSH DWORD PTR DS:[ESI+20] 73D46909 CALL DWORD PTR DS:[<&USER32.IsDialogMessageA>] ;进入系统内核 73D4690F POP ESI 73D46910 RETN 4 提示: 1 . OD中设断: bp IsDialogMessageA MSG==202 , 则当鼠标左键释放时,会中断在User32.IsDialogMessageA函数入口上。 2. 若已知按钮的句柄,且要求当点击该按钮时,程序中断在IsDialogMessageA上,则可以作如下设断: bp IsDialogMessageA [[esp+8]]==00060350 && MSG==202. 3. 中断后,可以通过堆栈返回到CWnd::IsDialogMessageA函数代码处。 ========================================================================================================= 8. User32 内核处理,不分析 这里面的过程,我们就当作一个黑匣子吧,不管它,一般情况下,也无需管它。因为我们百分之百相信它。 ========================================================================================================= 当消息到达此处时,又进入了MFC地界 .第8步之前,可以说是经常峰回路转,山重水复。第8步之后,是柳暗花明,可以一路高歌,直奔目的地了。 欲知后事如何,且听下回分解! |
所有的时间均为北京时间。 现在的时间是 08:35 PM. |