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的内部机制,限于本人水平,里面有太多的错误的地方,还望各位大侠的批评指正。
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课