SPYXX是Visual Studio附带的一个非常有用的工具,可以很方便的查看到窗口的句柄,类,进程,线程,窗口消息等信息。在很多时候,我们要对一些外部程序的控件进行控制,例如变灰,变为可用状态等等,通常可以使用SPYXX来获取到控件的句柄,然后自己写个程序调用EnableWindow等这些函数来让控件变为可用状态,但是窗口一重新出现,句柄就会改变,这个时候就会很麻烦,试想一下,如果在SPYXX上添加这样一些功能,那岂不是很方便吗?
    下面我们就来自己实现这样一些功能。
     在开始动手之前,我们有必要了解一下MFC的消息响应相关知识,或许,你很会使用Visual Studio为你的应用程序添加一个消息响应函数,但你并不知道VS在编译消息响应的时候做了什么事,虽然我们使用Class Wizard添加一段消息响应十分简单,但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()
当然,这很多调用都是对别的函数进行了一些简单的封装,在这里,我们要跟踪的2个比较重要的函数是_AfxDispatchCmdMsg 和CCmdTarget::OnCmdMsg ,_AfxDispatchCmdMsg的主要作用是调用我们自己编写的消息处理函数,而CCmdTarget::OnCmdMsg则是在当前的类以及父类里面查找消息的处理函数,如果找到处理函数,将调用_AfxDispatchCmdMsg来分发消息,如果没有找到,就返回,并在上级调用默认的消息处理函数。
以下是代码来自VS2005的MFC源代码:
BOOL CCmdTarget::OnCmdMsg(UINT nID, int nCode, void* pExtra,
  AFX_CMDHANDLERINFO* pHandlerInfo)
{
#ifndef _AFX_NO_OCC_SUPPORT
  // OLE control events are a special case
  if (nCode == CN_EVENT)
  {
    ENSURE(afxOccManager != NULL);
    return afxOccManager->OnEvent(this, nID, (AFX_EVENT*)pExtra, pHandlerInfo);
  }
#endif // !_AFX_NO_OCC_SUPPORT

  // determine the message number and code (packed into nCode)
  const AFX_MSGMAP* pMessageMap;
  const AFX_MSGMAP_ENTRY* lpEntry;
  UINT nMsg = 0;

#ifndef _AFX_NO_DOCOBJECT_SUPPORT
  if (nCode == CN_OLECOMMAND)
  {
    BOOL bResult = FALSE;

    const AFX_OLECMDMAP* pOleCommandMap;
    const AFX_OLECMDMAP_ENTRY* pEntry;

    ENSURE_ARG(pExtra != NULL);
    COleCmdUI* pUI = (COleCmdUI*) pExtra;
    const GUID* pguidCmdGroup = pUI->m_pguidCmdGroup;

#ifdef _AFXDLL
    for (pOleCommandMap = GetCommandMap(); pOleCommandMap->pfnGetBaseMap != NULL && !bResult;
      pOleCommandMap = pOleCommandMap->pfnGetBaseMap())
#else
    for (pOleCommandMap = GetCommandMap(); pOleCommandMap != NULL && !bResult;
      pOleCommandMap = pOleCommandMap->pBaseMap)
#endif
    {
      for (pEntry = pOleCommandMap->lpEntries;
        pEntry->cmdID != 0 && pEntry->nID != 0 && !bResult;
        pEntry++)
      {
        if (nID == pEntry->cmdID &&
          IsEqualNULLGuid(pguidCmdGroup, pEntry->pguid))
        {
          pUI->m_nID = pEntry->nID;
          bResult = TRUE;
        }
      }
    }

    return bResult;
  }
#endif

  if (nCode != CN_UPDATE_COMMAND_UI)
  {
    nMsg = HIWORD(nCode);
    nCode = LOWORD(nCode);
  }

  // for backward compatibility HIWORD(nCode)==0 is WM_COMMAND
  if (nMsg == 0)
    nMsg = WM_COMMAND;

  // look through message map to see if it applies to us

  for (pMessageMap = GetMessageMap(); pMessageMap->pfnGetBaseMap != NULL;
    pMessageMap = (*pMessageMap->pfnGetBaseMap)())
  {
    // Note: catches BEGIN_MESSAGE_MAP(CMyClass, CMyClass)!
    ASSERT(pMessageMap != (*pMessageMap->pfnGetBaseMap)());
    lpEntry = AfxFindMessageEntry(pMessageMap->lpEntries, nMsg, nCode, nID);
    if (lpEntry != NULL)
    {
      // found it
#ifdef _DEBUG
      if (nCode == CN_COMMAND)
        TRACE(traceCmdRouting, 1, "SENDING command id 0x%04X to %hs target.\n", nID,
          GetRuntimeClass()->m_lpszClassName);
      else if (nCode > CN_COMMAND)
        TRACE(traceCmdRouting, 1, "SENDING control notification %d from control id 0x%04X to %hs window.\n",
          nCode, nID, GetRuntimeClass()->m_lpszClassName);
#endif //_DEBUG
      return _AfxDispatchCmdMsg(this, nID, nCode,
        lpEntry->pfn, pExtra, lpEntry->nSig, pHandlerInfo);
    }
  }
  return FALSE;   // not handled
}
其中最下面的一个for循环最为关键,pMessageMap = GetMessageMap();这一句为获取到AFX_MSGMAP结构的基址,AFX_MSGMAP结构里面保存了消息处理映射入口的基址。
消息映射结构:
struct AFX_MSGMAP
{
#ifdef _AFXDLL
const AFX_MSGMAP* (PASCAL* pfnGetBaseMap)();
#else
const AFX_MSGMAP* pBaseMap;
#endif
const AFX_MSGMAP_ENTRY* lpEntries;
};
在这个结构中,我们只关心lpEntries这个值,因为这个值指向“消息处理映射入口”数组的首地址
消息处理映射入口结构
AFX_MSGMAP_ENTRY
{
 nMessage;
 nCode   ;
 nID  ;
 nLastID ;
 nSig    ;
 lpEntry ;
}
在这个AFX_MSGMAP_ENTRY里面,nID保存了控件的ID,lpEntry则是控件的入口地址。
我们要自己为应用程序添加消息映射,关键的就是找到pMessageMap,然后在lpEntries的后面添加一个自己的消息响应结构。
对于按钮的消息入口则通常为
AFX_MSGMAP_ENTRY
{
 nMessage = 0x111 WM_COMMAND
 nCode    = 0
 nID   = (ID) 按钮控件的ID
 nLastID  = (ID) 按钮控件的ID
 nSig     = 0x38 
 lpEntry (消息响应函数入口)
}

在通常情况下,VC编译器编译好了的应用程序是不会留出很多的空位置来给我们添加自己的消息响应,因此,我们必须在一处新的位置来添加自己的消息映射,所以在下面我们分配了一个MessageMap的区段来专门存放消息响应。


     在下面,我们将要准备这些软件:Ollydbg v1.10,ResHacker 3.5(资源骇客),Zero Add,VS 2005,SPYXX
    (1)使用ResHacker打开SPYXX,选择 对话框->24->2052,在这个窗口上添加5个按钮,按钮名称分别为 “可用” 、“变灰”、“显示”、“隐藏”、“关闭”,按钮ID分别为14550,14551,14552,14553,14554,然后保存文件。
    (2)使用Zero Add工具为SPYXX增加2个区段,分别为chCode,1000字节,MessageMap,4000字节,然后保存文件。
    (3)为了定位到_AfxDispatchCmdMsg,我们用VS2005自己建立一个MFC对话框工程,创建一个按钮响应,在响应内部设置断点,触发断点后查看堆栈,双击>  mfc80u.dll!CCmdTarget::OnCmdMsg(unsigned int nID=1, int nCode=0, void * pExtra=0x00000000, AFX_CMDHANDLERINFO * pHandlerInfo=0x00000000)  行381 + 0x16 字节  C++
来到mfc80u的领空,转到反汇编,看到如下的反汇编代码(你的代码地址可能和我的不一样,请以自己的实际地址为准)
7833A4C4 FF 70 10         push        dword ptr [eax+10h] 
7833A4C7 8B 55 14         mov         edx,dword ptr [ebp+14h] 
7833A4CA FF 75 08         push        dword ptr [ebp+8] 
7833A4CD 8B 75 10         mov         esi,dword ptr [ebp+10h] 
7833A4D0 8B 40 14         mov         eax,dword ptr [eax+14h] 
7833A4D3 8B CF            mov         ecx,edi 
7833A4D5 E8 F7 FD FF FF   call        _AfxDispatchCmdMsg (7833A2D1h) 
7833A4DA EB E1            jmp         $LN48+0DAh (7833A4BDh) 
可以看到7833A4D5就是调用_AfxDispatchCmdMsg 的地址,向上我们可以看到
7833A498 8B 07            mov         eax,dword ptr [edi] 
7833A49A FF 50 30         call        dword ptr [eax+30h] 
7833A49D EB 15            jmp         $LN48+0D1h (7833A4B4h) 
这里的 call        dword ptr [eax+30h] 就是调用GetMessageMap()的CALL,在这里获取到AFX_MSGMAP的地址。
    (4)下面我们用OD载入SPYXX,运行程序,然后bp 7833A49A下断点找到断点后设置条件[ebp+8] == 0x38D6(注:ebp+8为控件ID,0x38D6(十进制为14550)为“可用”按钮的ID),单击“可用”后被断下,F8单步,此时eax = 0040A2D0,选择在数据窗口中跟随,看到如下代码
0040A2D0  00423B56  spyxx.00423B56
0040A2D4  0040A240  spyxx.0040A240(这个地址是关键,AFX_MSGMAP_ENTRY入口地址)
使用命令dd 0040A240后看到消息映射:
0040A240  00000111
0040A244  00000000
0040A248  00000649
0040A24C  00000649
0040A250  00000038
0040A254  00439B52  spyxx.00439B52
0040A258  00000111
0040A25C  00000000
0040A260  0000064F
0040A264  0000064F
0040A268  00000038
0040A26C  00439B83  spyxx.00439B83
0040A270  00000111
0040A274  00000000
0040A278  00000650
0040A27C  00000650
0040A280  00000038
0040A284  00439B83  spyxx.00439B83
0040A288  00000111
0040A28C  00000200
0040A290  00000651
0040A294  00000651
0040A298  00000038
0040A29C  004399DE  spyxx.004399DE
0040A2A0  0000001C
0040A2A4  00000000
0040A2A8  00000000
0040A2AC  00000000
0040A2B0  00000015
0040A2B4  00439B8E  spyxx.00439B8E
0040A2B8  00000000
0040A2BC  00000000
0040A2C0  00000000
0040A2C4  00000000
0040A2C8  00000000
0040A2CC  00000000
这里就是默认的消息映射了,很显然,下面就一个空位置,无法添加我们的消息响应,因此,我们必须将他搬到另外一个位置去。由于我们已经定义好了一个区段专门来存放消息响应(按Alt+M后看到MessageMap区段地址为00479000),因此将0040A2D4的值改为00479000,然后将上面的消息映射用二进制复制复制到00479000的位置去,然后保存文件(如果在数据段修改后保存无效,请转到代码段选定内容保存即可)。以后我们添加消息响应只需要在这后面加就好了。
    (5)由于我们的代码要用到SPYXX获取到的窗口句柄,但是很可惜,SPYXX并没有将句柄作为一个全局变量保存起来,因此我们必须自己将他保存到一个地方。下断点bp WindowFromPoint,查找一个窗口后断下在如下的地方
004393CA  |.  50            PUSH EAX                                 ; /pPoint
004393CB  |.  FF76 20       PUSH DWORD PTR DS:[ESI+20]               ; |hWnd
004393CE  |.  FF15 681A4000 CALL DWORD PTR DS:[<&USER32.ClientToScre>; \ClientToScreen
004393D4  |.  FF75 10       PUSH DWORD PTR SS:[EBP+10]               ; /pt.Y
004393D7  |.  FF75 0C       PUSH DWORD PTR SS:[EBP+C]                ; |pt.X
004393DA  |.  FF15 641A4000 CALL DWORD PTR DS:[<&USER32.WindowFromPo>; \WindowFromPoint
004393E0  |.  8BF8          MOV EDI,EAX
004393E2  |.  85FF          TEST EDI,EDI
004393E4  |.  74 7D         JE SHORT spyxx.00439463

可以看到将返回的结果保存在edi这个寄存器中,我们找到004394D2修改语句为JMP 00478000(00478000为chCode的首地址),在这里编写一个代码来保存数据。
转到00478000,编写以下代码:
00478000    893D 16804700   MOV DWORD PTR DS:[478016],EDI
00478006    897E 64         MOV DWORD PTR DS:[ESI+64],EDI
00478009    C746 6C 0100000>MOV DWORD PTR DS:[ESI+6C],1
00478010    90              NOP
00478011  - E9 C614FCFF     JMP spyxx.004394DC

这里将结果保存到00478016这个位置后就调到原来的位置继续执行。
    (6)下面就来编写我们的消息响应了,使用dd 479000,来到全为0的地方,修改为以下的数据
00479078  00000111
0047907C  00000000
00479080  000038D6
00479084  000038D6
00479088  00000038
0047908C  0047801F  spyxx.0047801F(可用)
00479090  00000111
00479094  00000000
00479098  000038D7
0047909C  000038D7
004790A0  00000038
004790A4  00478045  spyxx.00478045(变灰)
004790A8  00000111
004790AC  00000000
004790B0  000038D8
004790B4  000038D8
004790B8  00000038
004790BC  0047806B  spyxx.0047806B(显示)
004790C0  00000111
004790C4  00000000
004790C8  000038D9
004790CC  000038D9
004790D0  00000038
004790D4  0047808F  spyxx.0047808F(隐藏)
004790D8  00000111
004790DC  00000000
004790E0  000038DA
004790E4  000038DA
004790E8  00000038
004790EC  004780B1  spyxx.004780B1(关闭)

来到04701F编写以下代码:
0047801F    55              PUSH EBP                                                            ; 可用
00478020    8BEC            MOV EBP,ESP
00478022    A1 16804700     MOV EAX,DWORD PTR DS:[478016]
00478027    83F8 00         CMP EAX,0
0047802A    74 09           JE SHORT spyxx.00478035
0047802C    6A 01           PUSH 1
0047802E    50              PUSH EAX
0047802F    FF15 74136874   CALL DWORD PTR DS:[<&USER32.EnableWindow>]                          ; user32.EnableWindow
00478035    C9              LEAVE
00478036    C3              RETN
00478037    0000            ADD BYTE PTR DS:[EAX],AL
00478039    0000            ADD BYTE PTR DS:[EAX],AL
0047803B    0000            ADD BYTE PTR DS:[EAX],AL
0047803D    0000            ADD BYTE PTR DS:[EAX],AL
0047803F    0000            ADD BYTE PTR DS:[EAX],AL
00478041    0000            ADD BYTE PTR DS:[EAX],AL
00478043    0000            ADD BYTE PTR DS:[EAX],AL
00478045    55              PUSH EBP                                                            ; 变灰
00478046    8BEC            MOV EBP,ESP
00478048    A1 16804700     MOV EAX,DWORD PTR DS:[478016]
0047804D    83F8 00         CMP EAX,0
00478050    74 09           JE SHORT spyxx.0047805B
00478052    6A 00           PUSH 0
00478054    50              PUSH EAX
00478055    FF15 74136874   CALL DWORD PTR DS:[<&USER32.EnableWindow>]                          ; user32.EnableWindow
0047805B    C9              LEAVE
0047805C    C3              RETN
0047805D    0000            ADD BYTE PTR DS:[EAX],AL
0047805F    0000            ADD BYTE PTR DS:[EAX],AL
00478061    0000            ADD BYTE PTR DS:[EAX],AL
00478063    0000            ADD BYTE PTR DS:[EAX],AL
00478065    0000            ADD BYTE PTR DS:[EAX],AL
00478067    0000            ADD BYTE PTR DS:[EAX],AL
00478069    0000            ADD BYTE PTR DS:[EAX],AL
0047806B    55              PUSH EBP                ;显示
0047806C    8BEC            MOV EBP,ESP
0047806E    A1 16804700     MOV EAX,DWORD PTR DS:[478016]
00478073    83F8 00         CMP EAX,0
00478076    74 09           JE SHORT spyxx.00478081
00478078    6A 05           PUSH 5
0047807A    50              PUSH EAX
0047807B    FF15 78136874   CALL DWORD PTR DS:[<&USER32.ShowWindow>]                            ; user32.ShowWindow
00478081    C9              LEAVE
00478082    C3              RETN
00478083    0000            ADD BYTE PTR DS:[EAX],AL
00478085    0000            ADD BYTE PTR DS:[EAX],AL
00478087    0000            ADD BYTE PTR DS:[EAX],AL
00478089    0000            ADD BYTE PTR DS:[EAX],AL
0047808B    0000            ADD BYTE PTR DS:[EAX],AL
0047808D    0000            ADD BYTE PTR DS:[EAX],AL
0047808F    55              PUSH EBP                ;隐藏
00478090    8BEC            MOV EBP,ESP
00478092    A1 16804700     MOV EAX,DWORD PTR DS:[478016]
00478097    83F8 00         CMP EAX,0
0047809A    74 09           JE SHORT spyxx.004780A5
0047809C    6A 00           PUSH 0
0047809E    50              PUSH EAX
0047809F    FF15 78136874   CALL DWORD PTR DS:[<&USER32.ShowWindow>]                            ; user32.ShowWindow
004780A5    C9              LEAVE
004780A6    C3              RETN
004780A7    0000            ADD BYTE PTR DS:[EAX],AL
004780A9    0000            ADD BYTE PTR DS:[EAX],AL
004780AB    0000            ADD BYTE PTR DS:[EAX],AL
004780AD    0000            ADD BYTE PTR DS:[EAX],AL
004780AF    0000            ADD BYTE PTR DS:[EAX],AL
004780B1    55              PUSH EBP                ;关闭
004780B2    8BEC            MOV EBP,ESP
004780B4    A1 16804700     MOV EAX,DWORD PTR DS:[478016]
004780B9    83F8 00         CMP EAX,0
004780BC    74 0D           JE SHORT spyxx.004780CB
004780BE    6A 00           PUSH 0
004780C0    6A 00           PUSH 0
004780C2    6A 10           PUSH 10
004780C4    50              PUSH EAX
004780C5    FF15 70136874   CALL DWORD PTR DS:[<&USER32.SendMessageA>]                          ; user32.SendMessageA
004780CB    C9              LEAVE
004780CC    C3              RETN

    (7)到这里,我们终于完成了全部消息响应的添加工作,保存修改后,按钮就可以正常工作了。

    最后简单的总结一下,修改MFC的消息处理的关键是添加自己的消息处理,这个做起来比Win32的直接修改WndProc要复杂得多,不过可以借此了解一下MFC的内部机制,限于本人水平,里面有太多的错误的地方,还望各位大侠的批评指正。

上传的附件 spyxx.rar