- Download demo project - 29.9 Kb
- Download source - 55 Kb
Introduction
It has been nearly one year since I published "MessengerSpy++ for MSN Messenger / Window Messenger" (to interact with MSN Messenger 4.6, 4.7 and 5.0) in www.CodeGuru.com in Autumn of year 2002, and with MS releasing MSN Messenger 6.0 in July 2003, which adopted different architecture from its predecessor and made my MessengerSpy++ not working with this 6.0 version, I decided to write this article to demonstrate how to make a program interacting with MSN Messenger 6.0. This time, I would like to introduce you 2 new things --- COM Interface Hooking and COM Interface Method Hooking. Yes, it is COM Interface Hooking and Method Hooking, which means your interface method take over the function call before routing to the hooked interface method, just like API hooking and Windows Message hooking you may have known.
Before you continue, I highly recommend you to read my previous articles "MessengerSpy++ for MSN Messenger / Window Messenger" and "Keystroke Logger and More" Serials, Article No. 2 to get acquainted with MSN Messenger topics which will help understanding the following discussion greatly for I will intentional omit what is available in these 2 articles to save space here.
Note: This article can also be found here and may be more informational for less cut.
What You Need Before Start
Depends, MS Platform Core SDK, MSN Messenger 6.0, Resource Hacker, Process Explorer or Process Spy. Win 2K/XP/2003 ONLY! (local administrative member identity needed);
You need two active MSN (passport) logins to simulate chatting or experiment together with friends (for you have to start a chat). FYI: You can use WinXP/2003 Fast User Switch (FUS) to simulate multiple MSN user login on one physical machine.
Background and What's New in MSN Messenger 6.0
As you may know, MSN Messenger before 5.0 (inclusive) uses a "RichEdit" common control as the chat input area and chat contents area, the "Send" button is a genuine "BUTTON" windows control. To interact with it, your program uses a hook or whatever remote injection ways to penetrate into MSN Messenger process space, and conduct button-pushing and text-reading just the same as doing this in a dialog-based GUI program we all have been writing.
But, in MSN Messenger 6.0, when you use SPY++ to check its windows layout, there is only a "DirectUIHWND" window. "DirectUIHWND" is a widely used wrapper windows class since the emergence of Windows XP, according to my observation. If you are using WinXP/2003, you can modify Mr. Keith Brown's tool CmdRunAs in Feb 2000, MSJ or Mr. Martyn 'Ginner' Brown's tool Start a Command As Any User in www.codeguru.com 2001, or if you are a lazy typist, use my GUI-based "RunAs" directly to launch SPY++ under "LocalSystem" account to your logon desktop(WinSta0\Winlogon).
Note*
- You must be a Administrator Group Member to do so.
- No need to try WinXP/2003's "Runas..." shell command because it always launches in the current desktop (WinSta0\Default). You may need to "Alt+Esc" to make SPY++ visible when turn to the logon desktop.)
Now you will find the "DirectUIHWND" window. For the sake of these non-WinXP users, here is the screen shot of this scenario.
So, the point of interacting with MSN Messenger 6.0 is focused on whether it is possible to hook into "DirectUIHWND" and obtain data from inside successfully. General speaking, it is nearly impossible to hook and interact with a wrapper window such as "DirectUIHWND" if you have no documentation of its inside Windows message handler, COM interface description, and some global data structure. But luckily, after using Process Spy and Depends, it is clear MSN Messenger 6.0 dynamically load and unload "RichEd20.DLL" which gives us a sign that it uses Windowless Rich Edit Control inside.
(Something FYI: As you might know, richedit20.dll is a target of Worm.Nimda to launch itself when a user starts the MS Office family application. Besides, before the release a new version accompanying OfficeXP, it has a serious buffer overflow problem, which leads to system takeover by a remote user thru some IM chatting. Now, it is being protected, by default, by "Protected Storage" NT Service. An attempt to overwrite this will fail unless you stop this NT Service first.)
Now, use Depends to open "richedit20.dll". By default, this file is in the "%SystemRoot%\system32" directory. (If you are using Win2k, it may be in C:/WinNT/System32; or, if you are using WinXP, C:/Windows/System32.) The exported table follows:
Ordinal ^ | Hint | Function | Entry Point |
2 (0x0002) | 1 (0x0001) | IID_IRichEditOle |
0x00014C60 |
3 (0x0003) | 2 (0x0002) | IID_IRichEditOleCallback |
0x00014C50 |
4 (0x0004) | 0 (0x0000) | CreateTextServices |
0x0000D882 |
5 (0x0005) | 5 (0x0005) | IID_ITextServices |
0x00014C20 |
6 (0x0006) | 3 (0x0003) | IID_ITextHost |
0x00014C30 |
7 (0x0007) | 4 (0x0004) | IID_ITextHost2 |
0x00014C40 |
8 (0x0008) | 6 (0x0006) | REExtendedRegisterClass |
0x0004BA5C |
9 (0x0009) | 7 (0x0007) | RichEdit10ANSIWndProc |
0x0003DB01 |
10 (0x000A) | 8 (0x0008) | RichEditANSIWndProc |
0x00015681 |
It is really lucky for us that riched20.dll just exports less than a dozen of variables and functions. Besides, after I checked it with ">Dumpbin -Exports Riched20.DLL", it has no forward function, which is good news for our hookers; and by default, riched20.dll is not available in the "Known DLL" part inside the Registry. All these facts decided we only need to hook this riched20.dll.
Note: I am not saying that "forward function" and "Known DLL" will prevent us from hooking; you can always hook that Windows API on the final DLL. Now, we do not have these two problems, which means we only need to take care a single riched20.dll, leading to less work.
Besides, you may be surprised when I tell you the loading code in MSN Messenger 6.0 is like the following (pseudo code):
HMODULE hRichEditLib = ::LoadLibrary(_T("RICHED20.DLL"));instead of a more robust one:
TCHAR szLibPath[MAX_PATH]; UINT uRet = ::GetSystemDirectory(szLibPath, MAX_PATH]); if(uRet == 0) err; szLibPath[uRet] = TCHAR('\0'); ::lstrcat(szLibPath, _T("\RichEd20.DLL")); HMODULE hRichEditLib = ::LoadLibrary(szLibPath);
Okay, now start your VC++ 6.0 or VS.NET, create a new Win32 DLL project called "riched20.dll" (case insensitive), change its setting to Unicode, and export EXACTLY the same things as shown in the above table in the riched20.def file, like this:
LIBRARY "Riched20" DESCRIPTION 'RichEdit Ver 2 & 3 DLL' EXPORTS IID_IRichEditOle @2 PRIVATE IID_IRichEditOleCallback @3 PRIVATE CreateTextServices @4 PRIVATE ...
You need go to the Platform SDK (now called MS SDK) directory, locate the file "TextServ.h", and copy the following from it to compose a new file titled "MyTextServ.h" in your project, like this:
#ifndef _TEXTSERV_H #define _TEXTSERV_H #ifdef __cplusplus struct PARAFORMAT2 : _paraformat //Copied from RichOle.h, //RichEdit.h... { LONG dySpaceBefore; ... }; #else// Regular C-style typedefstruct _paraformat2 { UINT cbSize; ... } PARAFORMAT2; #endif// C++ //... more enum, struct and constant definition copied // from RichOle.h struct CHANGENOTIFY { DWORD dwChangeType; void * pvCookieData; }; #define TXTBIT_RICHTEXT 1#define TXTBIT_MULTILINE 2 ... class ITextServices : public IUnknown { public: //@cmember Generic Send Message interface virtual HRESULT TxSendMessage( UINT msg, WPARAM wparam, LPARAM lparam, LRESULT *plresult) = 0; //more virtual functions .... }; class ITextHost : public IUnknown { public: //@cmember Get the DC for the host virtual HDC TxGetDC() = 0; //@cmember Release the DC gotten from the host virtual INT TxReleaseDC(HDC hdc) = 0; ... //more functions ... }; //+--------------------------------------------------------------- // Factories //---------------------------------------------------------------- // Text Services factory STDAPI CreateTextServices( IUnknown *punkOuter, ITextHost *pITextHost, IUnknown **ppUnk); typedef HRESULT (STDAPICALLTYPE * PCreateTextServices)( IUnknown *punkOuter, ITextHost *pITextHost, IUnknown **ppUnk); #endif// _TEXTSERV_H
You may find it frustrating to make this fake header compiled into our module. You have to dig several header files and copy-and-paste the constant, enum, or struct you need.
[Do not include "RichOle.h" or "RichEdit.h"!!! We are making a fake RichEd20.DLL; that's why you must make sure to add those defined in these header files into our "MyTextServ.h". Besides, NEVER NEVER change the member function order in the class!!! That will ruin the Vtbl we rely on for COM interface hooking.]
Change your riched20.h file and let it export these:
#define RICHED20_API __declspec(dllexport) extern"C" RICHED20_API GUID IID_IRichEditOle; extern"C" RICHED20_API GUID IID_IRichEditOleCallback; ... #include<unknwn.h>#include"mytextserv.h"extern"C" HRESULT WINAPI CreateTextServices(IUnknown *punkOuter, ITextHost *pITextHost, IUnknown **ppUnk); ...
Change your riched20.cpp file accordingly:
RICHED20_API GUID IID_IRichEditOle = { 0x00020D00, 0x0, 0x0, { 0xC0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x46 } }; ... typedef HRESULT (__stdcall *lpCreateTextServices)(IUnknown *punkOuter, ITextHost *pITextHost, IUnknown **ppUnk); typedef LRESULT (__stdcall *lpREExtendedRegisterClass)(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam); typedef LRESULT (__stdcall *lpRichEdit10ANSIWndProc)(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam); typedef LRESULT (__stdcall *lpRichEditANSIWndProc)(HWND hWnd,UINT Msg, WPARAM wParam, LPARAM lParam); #define NEW_DLL_NAME _T("\\RichEd20.Dll") //You MUST dynanically load the DLL HRESULT WINAPI CreateTextServices(IUnknown *punkOuter, ITextHost *pITextHost, IUnknown **ppUnk) { TCHAR szLib[MAX_PATH]; //255 is enough DWORD dw = GetSystemDirectory(szLib, MAX_PATH); if(dw == 0) return0; szLib[dw] = TCHAR('\0'); ::lstrcat(szLib, NEW_DLL_NAME); HMODULE hLib = LoadLibrary(szLib); if(!hLib) return0; lpCreateTextServices _CreateTextServices = (HRESULT (__stdcall *)(IUnknown*, ITextHost*, IUnknown**)) ::GetProcAddress(hLib, "CreateTextServices"); if(!_CreateTextServices) return0; HRESULT hr = (_CreateTextServices)(punkOuter, pITextHost, ppUnk); //We cache this COM interface ITextServices* lpTx; ((IUnknown*)(*ppUnk))->QueryInterface(IID_ITextServices, (void**)(&lpTx)); if(lpTx) MessageBox(NULL, _T("Interface Hooked"), _T("Indeed"), MB_OK); //::FreeLibrary(hLib); //NOT FREE IT!!! return hr; } ...
Making this DLL compliable may take you some time before you put everything correctly. After you produce this fake riched20.dll, copy it to the MSN Messenger 6.0 directory. Launch MSN Messenger 6.0, start a chat, and you will see for that each chat window, the message box will pop up six times. This means that in each chat window, there are six windowless rich edit controls working on your behalf. After a few experiments, I know the first one is the address area that shows the chatter's e-mail address and nickname. The second is the chat contents area and the fourth is where you input the words. The others have no direct user interaction functionality, so we omit them in the following discussion.
COM Interface Hooked?
Up to now, code gurus should have understand the point all our working are around, just as I did in my MessengerSpy++, where I get the chat area window handle and interact with it. This time, I hooked my class into the windowless rich edit, and "embody it;" that is, you get the physically existing interface pointer. This "embodiment" must be done before the COM interface pointer is returned to the Application (MSN Messenger) and my module must be a DLL module injected into that application.
In this way, my code can leverage the COM interface harmlessly (hopefully no race condition, no sync contradiction, and so on after carefully designing the code) with the application. What's cool is that you can go one step further and change the Vtbl of the interface�make the application's call into your hooking function first instead of calling into the stolen COM interface method. The following figures will explain the difference between a normal COM interface method call and a hooked interface method call.
If you are still not clear about this, refer the above figure. See, the MSN Messenger 6.0 implemented the ITextHost
interface that is passed to riched20.dll and a ITextService
interface pointer is returned. Because our fake Riched20.dll takes the middle position, we now own the interfaces' pointer�on one hand, you can query the ITextService
for the RTF data and even set the RTF data (which is the prerequisite condition of dynamic interact with MSN Messenger 6.0 chatting). On the other hand, you can "WriteProcessMemory
" and modify the interface Virtual Table (which is the counterpart of Windows Message Hooking if you do it on TxSendMessage
").
The COM Interface Hook has one fatal shortcoming compared with API hooking and Message Hooking: You can make API hooking and unhooking in runtime freely by modifying the process image in memory. You also can hook and unhook a Windows Message by simply calling "SetWindowsHookEx
" and "UnhookWindowsHookEx
" freely. NOTE, freely means you can hook/unhook at any time, including the target process having been started for some time. The COM Interface Hook just cannot accept this. YOU MUST CATCH THE COM INTERFACE POINTER WHEN IT IS GIVEN BIRTH OR YOU LOSE IT FOREVER.
This characteristic means that a COM Interface Hooking program must be running before the target program creates the interface pointer and sometimes it must keep an eye on the creation of the target process if the target process does all interface creation stuff at the very beginning. This means you may need install "process monitor driver" written with DDK or other possible way like we put a fake DLL this time. As to the COM interface method hooking, it may be, under certain circumstances, extremely difficult to maintain program stability without triggering a deadlock or race condition. So be careful.
Besides, most of the time, you may need to hook CoCreateIntance(Ex)
directly to get the interface pointer, which requires your grasping API hooking tech first. As a fast link collection place, you may find my "Key Stroke Logeer and More, Part 3" gives a long list of resource concerning this topic. This topic will be discussed again, later in this series.
Communication Solution�Between MSN Messenger 6.0 and Your Program
Now, let's talk about the communication between your program and the fake riched20.dll. You can refer my previous articles, MessengerSpy++ and Key Stroke Logger and More Series, Part 1 for the usage of MMF when coping with variable length unidirectional data transmission. Although you can launch a "listening" thread inside this fake DLL and receive a command from your program, it may be complicated and error prone. As with my using Windows User Message in MessengerSpy++, this time I take the same approach with a little modification.
Because I am not using "SetWindowsHookEx
" on a MSN Messenger 6.0 Chat window (well, you can use it, but it does not make much sense, like we use it in MSN Messenger 5.0 where I use it to cope with windows handles), I create a hidden window and this hidden window, just like COM STA, takes care of your Query Synchronization, Command Processing, and Redirection. And the code will be very compact; 30 lines is enough. "Simple is Beautiful." The code excerpt is below:
BOOL APIENTRY DllMain( HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: InitializeRecv(TRUE); //Init the hidden window break; case DLL_THREAD_ATTACH: break; case DLL_THREAD_DETACH: break; case DLL_PROCESS_DETACH: InitializeRecv(FALSE); break; } return TRUE; } BOOL InitializeRecv(BOOL bInitialize) { if(bInitialize) { //Create Window.... RegisterClassEx(&wcex) g_hRecvWnd = CreateWindow(...); } else { if(!::IsWindow(g_hRecvWnd)) return FALSE; ::PostMessage(::g_hRecvWnd, WM_CLOSE, 0, 0); ::g_hRecvWnd = NULL; } return TRUE; } LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { switch (message) { case WM_DESTROY: PostQuitMessage(0); break; case WM_YOUR_COMMAND_QUERY_CHAT_AS_TEXT: BSTR bstrChat; g_lpTextService[wParam]->TxGetText(&bstrChat); WriteChatTextToMMF(bstrChat); SysFreeString(&bstrChat); break; default: return DefWindowProc(hWnd, message, wParam, lParam); } return0; }
You may now be bored due to the theoretical stuff I preached such a long way. Me too. So, I'll stop it here, and let you try the accompanying demo program. Use the folder "Riched20 Ver1", and copy the compiled DLL to your MSN Messenger 6.0' folder. Make sure you do that before you begin to chat. Now, start a chat with a friend (or yourself using WinXP/2003). I add a timer to the hidden window, so every 10 seconds it pop up a message box showing what is in the "To-Send" edit box area.
Handler on MSN Messenger 6.0 Emotional Icon
Also, try to put some emotional icon with the text. To keep all simple, I just save the first emotional icon to your C drive root directory as a bitmap file. By nature, the emotional icon is a WMF file, but, if I show you its original size image here, I guess you would like to use a bitmap more.
Figure. A Bitmap Grabbed from MSN Messenger 6.0 as an emotional icon (size: 19 X 19 pixels, always)
Figure. A WMF grabbed from MSN Messenger 6.0 as an emotional icon. Notice its size is much larger than its bitmap counterpart (size: 200 X 200 pixels, always)
Following is the code extracting an emotional icon from MSN Messenger 6.0. It is somewhat similar to MessengerSpy++ but this time, the embedded object is in WMF format instead of BMP format.
//Say, now, you have ITextServices* pointer g_lpIText already BSTR bstr; HRESULT hr = ((ITextServices*)::g_lpIText->TxGetText(&bstr); if(FAILED(hr)) err; //Process the text you got ::SysFreeString(bstr); //I only deal with the first embedded emotional icon IRichEditOle* pReo = NULL; g_lpIText->TxSendMessage(EM_GETOLEINTERFACE, 0, (LPARAM)(LPVOID*)&pReo, &lr); if(lr == 0) return; //how many images do we have? LONG nNumber = pReo->GetObjectCount(); //Your Image's Number //remember to pReo->Release(); when everything is settled if(nNumber == 0) return; REOBJECT* ro = new REOBJECT; ro->cbStruct = sizeof(REOBJECT); //deal with first image hr = pReo->GetObject(0, ro, REO_GETOBJ_ALL_INTERFACES); if(FAILED(hr)) err; IDataObject* lpDataObject; hr = (ro->poleobj)->QueryInterface(IID_IDataObject, (void **)&lpDataObject); if(FAILED(hr)) err; //I was stuck here for a while //ParseDataObject(lpDataObject); STGMEDIUM stgm; // out FORMATETC fm; // in fm.cfFormat = CF_METAFILEPICT; // Clipboard format fm.ptd = NULL; // Target Device = Screen fm.dwAspect = DVASPECT_CONTENT; // Level of detail = Full content fm.lindex = -1; // Index = Not appliciple fm.tymed = TYMED_MFPICT; hr = lpDataObject->GetData(&fm, &stgm); if(FAILED(hr)) err; //Metafile handle. The tymed member is TYMED_MFPICT. HMETAFILEPICT hMetaFilePict = stgm.hMetaFilePict; LPMETAFILEPICT pMFP = (LPMETAFILEPICT) GlobalLock (hMetaFilePict); int cx = 19; // pMFP->xExt; // it is always 19 X 19 int cy = 19; // pMFP->yExt; HWND hWnd = ::GetDesktopWindow(); //You are using true color display anyway HDC hDC = ::GetDC(hWnd); HDC hMemDC = ::CreateCompatibleDC(hDC); HBITMAP hMemBmp = ::CreateCompatibleBitmap(hDC, cx, cy); HBITMAP hPrevBmp = (HBITMAP)::SelectObject(hMemDC, hMemBmp); //Draw on Mem DC ::PlayMetaFile(hMemDC, pMFP->hMF); //If you want just save WMF anyway, just do that CopyMetaFile(pMFP->hMF, _T("C:\\fromMSN.wmf")); //If you want to save as BMP, go on TCHAR szFilename[64]; wsprintf(szFilename, _T("c:\\fromMSN.bmp")); //Hope you have C driver HANDLE hFile = ::CreateFile(szFilename, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); if(hFile == INVALID_HANDLE_VALUE) err; DWORD dwWritten; //need file header BITMAPFILEHEADER bmfh; bmfh.bfType = 0x4d42; // 'BM' int nColorTableEntries = 0; // true color only int nSizeHdr = sizeof(BITMAPINFOHEADER) + sizeof(RGBQUAD) * nColorTableEntries; bmfh.bfSize = 0; bmfh.bfReserved1 = bmfh.bfReserved2 = 0; bmfh.bfOffBits = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + sizeof(RGBQUAD) * nColorTableEntries; ::WriteFile(hFile, (LPVOID)&bmfh, sizeof(BITMAPFILEHEADER), &dwWritten, NULL); BITMAP bm; //get bitmap information ::GetObject(hMemBmp, sizeof(bm), &bm); int nBitCount = bm.bmBitsPixel; //Warning! True Color! BITMAPINFOHEADER* lpBMIH = (LPBITMAPINFOHEADER) newchar[sizeof(BITMAPINFOHEADER) + sizeof(RGBQUAD) * nColorTableEntries]; lpBMIH->biSize = sizeof(BITMAPINFOHEADER); lpBMIH->biWidth = bm.bmWidth; lpBMIH->biHeight = bm.bmHeight; lpBMIH->biPlanes = 1; lpBMIH->biBitCount = nBitCount; lpBMIH->biCompression = BI_RGB; lpBMIH->biSizeImage = 0; lpBMIH->biXPelsPerMeter = 0; lpBMIH->biYPelsPerMeter = 0; lpBMIH->biClrUsed = nColorTableEntries; lpBMIH->biClrImportant = nColorTableEntries; //Compute Image Size DWORD dwCount =((DWORD) lpBMIH->biWidth * lpBMIH->biBitCount)/ 32; if(((DWORD) lpBMIH->biWidth * lpBMIH->biBitCount) % 32) dwCount++; dwCount *= 4; dwCount = dwCount * lpBMIH->biHeight; //Use Virtual Memory API instead of new-delete LPVOID lpImage = ::VirtualAlloc(NULL, dwCount, MEM_COMMIT, PAGE_READWRITE); BOOL result = GetDIBits(hMemDC, (HBITMAP)hMemBmp, 0L, // start scan line (DWORD)bm.bmHeight, // # of scan lines (LPBYTE)lpImage, // address for bitmap bits (LPBITMAPINFO)lpBMIH, // address of bitmapinfo (DWORD)DIB_RGB_COLORS // use rgb for color table ); ::WriteFile(hFile, lpBMIH, sizeof(BITMAPINFOHEADER), &dwWritten, NULL); ::WriteFile(hFile, lpImage, dwCount, &dwWritten, NULL); ::VirtualFree(lpImage, 0, MEM_RELEASE); ::CloseHandle(hFile); //Restore DC ::SelectObject(hMemDC, hPrevBmp); ::DeleteObject(hMemBmp); ::DeleteDC(hMemDC); ::ReleaseDC(hWnd, hDC); ::GlobalUnlock(hMetaFilePict); //do not forget COM household work today ro->poleobj->Release(); //GetObject Called AddRef so //Release here delete ro;You may wonder: Hi, how do you know it is in WMF format? Unfortunately, I am not a prophet, and I did try several ways to reveal what the MSN Messenger team hid behind the scene. Luckily, I got it. Following is my code:
void ParseDataObject(IDataObject* lpDataObject) { DWORD dwCF[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 0x0080, 0x0081, 0x0082, 0x0083, 0x008E}; DWORD dwTM[] = {1, 2, 4, 8, 16, 32, 64, 0}; int dimCF = sizeof(dwCF)/sizeof(dwCF[0]); int dimTM = sizeof(dwTM)/sizeof(dwTM[0]); for(int i = 0; i < dimCF; i++) { for(int j = 0; j < dimTM; j++) { FORMATETC fm; // in fm.cfFormat = dwCF[i]; // Clipboard format fm.ptd = NULL; // Target Device = Screen fm.dwAspect = DVASPECT_CONTENT; fm.lindex = -1; // Index = Not appliciple fm.tymed = dwTM[j]; STGMEDIUM stgm; // out HRESULT hr = lpDataObject->GetData(&fm, &stgm); if(FAILED(hr)) continue; PopMsg(_T("I caught it %d, %d"), i, j); } } }
Points of Interest --- Hook COM Interface Method
For our C++ people. COM Interface is just a C++ class (yes, I know COM is language neutral, but taking it like an ordinary C++ class here make more sense). Derived from IUnknown
, it definitely has a Virtual Table (abbr. VTBL) because a base class has a virtual function already. I know almost of you have experience with it already, but I do not think a lot of people are clear how the VTBL exists in the memory.
Actually, different C++ compilers use different ways to do it (I do not know how some other popular compilers such as Delphi and C++ Builder do this, but I guess they take similar approach as Visual C++. But, one thing is they all run on MS Windows. As far as I know, at least a kind of C++ compile for DSP chip programming put a vtbl pointer after the class member while MSVC put a vtbl pointer the first place). And what we are talking about here is specific to MSVC on Win32 platform, so all pointers here are 4 bytes long. With the following code (you will find it in VtblStory1 folder in the accompanying demo):
class classA { public: virtualint method1() { return11; } virtualint method2() { return12; } virtualint method3() { return13; } }; class classB: public classA { public: virtualint method1() { return21; } int m; int n; }; class classC : public classB { public: int method1(int a, short b) { return31; } }; void testVtabl() { classC* pC = new classC; classB* pB = pC; int y = pB->method1(); //31 classB bb; bb.m = 31; bb.n = 32; LPVOID pBB = &bb; LPVOID pBB2 = &(bb.m); LPDWORD* lpVtabl = (LPDWORD*)&bb; HANDLE hSelf = OpenProcess(PROCESS_ALL_ACCESS, FALSE, ::GetCurrentProcessId()); MEMORY_BASIC_INFORMATION mbi; if(VirtualQueryEx(hSelf, (LPVOID)(*lpVtabl), &mbi, sizeof(mbi)) != sizeof(mbi)) err; PVOID pvRgnBaseAddress = mbi.BaseAddress; DWORD dwOldProtect1, dwOldProtect2; if(!::VirtualProtectEx(hSelf, pvRgnBaseAddress, 4, PAGE_EXECUTE_READWRITE, &dwOldProtect1)) err; BOOL bStridePage = FALSE; //Check if Vtbl Strike 2 Pages LPBYTE lpByte = (LPBYTE)pvRgnBaseAddress; lpByte += 4096; //in Win32, 4k/page if((DWORD)lpByte < (DWORD)lpVtabl + 4) //We explain later bStridePage = TRUE; PVOID pvRgnBaseAddress2 = (LPVOID)lpByte; if(bStridePage) if(!::VirtualProtectEx(hSelf, pvRgnBaseAddress2, 4, PAGE_EXECUTE_READWRITE, &dwOldProtect2)) err; //Swap classB's method1 & method2 pointer DWORD dw; memcpy((LPVOID)&dw, (LPVOID)(*lpVtabl), 4); memcpy((LPVOID)(*lpVtabl), (LPVOID)(*lpVtabl + 1), 4); memcpy((LPVOID)(*lpVtabl + 1), (LPVOID)&dw, 4); //recover page property DWORD dwFake; ::VirtualProtectEx(hSelf, pvRgnBaseAddress, 4, dwOldProtect1, &dwFake); if(bStridePage) ::VirtualProtectEx(hSelf, pvRgnBaseAddress2, 4, dwOldProtect2, &dwFake); //Compiler sometimes addicts to optimization y = bb.method1(); //still 21 y = bb.method2(); //22 //Unfortunatly Compile takes place one step earlier; you will //not see effect return; }
In MSVC++, if a class self or base class(es) have a virtual function, its first 4 bytes of class layout inside the memory is the pointer to Vtbl, followed by the member variable and then the member function. Take the figure as an example: pBB points to an instance of classB, goes to the memory window. The first 4 bytes are "2C 50 42 00" (remember that Intel chips are little endian), followed by "1F 00 00 00". It is the m we just assigned to 31 (0x1F), then "20 00 00 00", which is n we assigned 32 (0x20).
Now, go to virtual memory 0x 0042 502C, and you have the right-most memory window. Umm, how do I say, from 0042502C to 00425038, it is classB's virtual table area, starting form the first virtual method�method1, it is "28 10 40 00", which is different from the base class�classA's method1 which lies in 0x0042503C. It makes sense; when you call a classB's method1, you enter its method1. On the other hand, because classB does not implement method2, when you call method2 in a classB instance, you enter classA's method2.
When you scroll the vertical scrollbar, you will see classC's vtbl lies in higher consecutive memory (the left bottom memory window). By comparing the data in this area carefully, I bet you are clear the layout of vtbl now.
Please note: Vtbl is always being put inside read-only pages together with whatever const you declared in C++, and if you try to write to it, your program will be terminated by system and a GP error box will pop up, which means you must call VirtualProtectEx
to modify the page property to PAGE_EXECUTE_READWRITE
before swapping the pointers of method1 and method2 in classB's Vtbl.
Also note: There is no proof that the Vtbl of a class lies in a single page. You must make sure all your write operations are conducted in areas you have modified. In our code above, there are three virtual methods totally, so classA, classB, classC (they are consecutive) vtbl each takes 3 * 4 byte, and that explains why " if((DWORD)lpByte < (DWORD)lpVtabl + 4)
", see, lpByte
points to classB's Vtbl, and I want to make sure classB's Vtbl place is modified before we continue.
Now you may ask: "Hi, I changed classB's Vtbl, okay, then I expect '(y = bb.method1()) == 22' and '(y = bb.method2()) == 21'. How come I still get 21 and 22? The answer is: The compiler has computed the function entry and hard-coded it in the binary. In other words, when program starts, it didn't use Vtbl at all because the compiler has decided which function to call in compile time. Sigh, too smart compiler...
So what on earth we can do to make a program have a look at the Vtbl before turning a member function? Component-based program. For, to a component, it has no knowledge of what member function will be called in runtime, it must use Vtbl to decide which member function to call. Okay, let's make such a scenario: (I do not teach COM/ATL; you must have experience with this to continue.)
Create a COM ATL DLL project (you will find its source code in the Plus folder in the accompanying demo), use everything default setting, need proxy/stub bound, add a Simple Object, call it Sum, make it a Custom Interface (you can use Dual but you have to modify offset later in the code), and add two methods until you get following in your Sum.cpp:
STDMETHODIMP CSum::method1() { PopMsg(_T("method1")); return S_OK; } STDMETHODIMP CSum::method2() { PopMsg(_T("method2")); return S_OK; }
Do not be concerned about PopMsg
; it just calls WinAPI MessageBox
. Then, add the third method:
STDMETHODIMP CSum::RadarIt() { LPDWORD* lpVtabl = (LPDWORD*)this; HANDLE hSelf = OpenProcess(PROCESS_ALL_ACCESS, FALSE, ::GetCurrentProcessId()); MEMORY_BASIC_INFORMATION mbi; if(VirtualQueryEx(hSelf, (LPVOID)(*lpVtabl), &mbi, sizeof(mbi) != sizeof(mbi)) err; PVOID pvRgnBaseAddress = mbi.BaseAddress; DWORD dwOldProtect1, dwOldProtect2; if(FALSE == ::VirtualProtectEx(hSelf, pvRgnBaseAddress, 4, PAGE_EXECUTE_READWRITE, &dwOldProtect1)) err; //make sure all Vtbl areas are set LPBYTE lpByte = (LPBYTE)pvRgnBaseAddress; lpByte += 4096; //in Win32 4k/page, I am too lazy to call API BOOL bStridePage = FALSE; if((DWORD)lpVtabl + 2 * 4> (DWORD)lpByte) bStridePage=TRUE; PVOID pvRgnBaseAddress2 = (LPVOID)lpByte; if(bStridePage) if(FALSE == VirtualProtectEx(hSelf, pvRgnBaseAddress2, 4, PAGE_EXECUTE_READWRITE, &dwOldProtect2)) err; //Vtbl has five functions; they are //Add Release QueryInterface Method1 Method2 //swap 3rd <--> 4th //That is swap Method1 and Method2 DWORD dw; memcpy((LPVOID)&dw, (LPVOID)(*lpVtabl + 3), 4); memcy((LPVOID)(*lpVtabl + 3), (LPVOID)(*lpVtabl + 4), 4); memcpy((LPVOID)(*lpVtabl + 4), (LPVOID)&dw, 4); //Recover Page Property DWORD dwFake; ::VirtualProtectEx(hSelf, pvRgnBaseAddress, 4, dwOldProtect1, &dwFake); if(bStridePage) ::VirtualProtectEx(hSelf, pvRgnBaseAddress2, 4, dwOldProtect2, &dwFake); return S_OK; }
Hi, a little thing, I am too lazy to call API to get the page size though you should do it when you are serious. Let's focus on the main topic. ISum
is derived from IUnknown
that already has AddRef
, Release
, and QueryInterface
virtual-ed. So, our method1 and method2 take the offset 3 and 4 (DWORD
unit, that is 4 bytes). Remember that we swap method1 and method2 when RadarIt
is called.
Now, make a MFC dialog project (you will find the code in the Pop folder in the accompanying demo), everything default, import the DLL's type library, add a button on the dialog so you can push it, and do the following:
void CPopDlg::OnButton1() { CoInitialize(NULL); PLUSLib::ISum* pSum; hr = ::CoCreateInstance(clsid, NULL, CLSCTX_INPROC_SERVER, iid, (LPVOID*)&pSum); pSum->method1(); //Pop up "method1" pSum->RadarIt(); pSum->method1(); //Pop up "method2" ::CoUninitialize(); }
The first time you call pSum->method1()
, you will see the "method1" message box; but after you call RadarIt
, you call method1; you will get the "method2" message box instead of the "method1" message box. Make sense?
Okay, one step further. Now, change the ATL DLL's code as following:
STDMETHODIMP CSum::method2() { PopMsg(_T("method2")); RadarIt(); //Recover Original Vtbl method1(); //to call method1() RadarIt(); //return S_OK; }
Try out Pop Dialog; now, you see that method2 takes method1's position and actually method2 wrapped method1�whenever you call method1, you enter method2 first and then method1. COM interface method is hooked. Think this way: Your code injected into the target program, grabbed the COM interface pointer it uses, hooked the interface method, and ... all calls go to your code first, and you can do modification and whatever, then up to you, pass it down to the original interface method. Just one thing to remember: The interface can only be hooked when it is given birth.
That's all for today, and we will continue talking about MSN Messenger 6.0 and COM Hooking in the next article. Take care when playing with the demo program; the timer will pop up a message box every 10 seconds. And you have to exit MSN Messenger 6.0 before deleting or removing our fake riched20.dll, the same as before you copy it to MSN Messenger 6.0 directory.
In the end, just as the title of this article, this is the first part of the "Interacting with MSN Messenger 6.0 Serials" and I plan to offer a tool similar to my "MessengerSpy++ for MSN Messenger / Window Messenger" to cope with MSN Messenger 6.0 in following articles. Besides we may be talking something on MS Office-IM interaction, or construct a P2P program communication tunnel based on MSN Network for MSN Messenger 6.0 seems able to pass firewall now...
History
Version History
Version | Release Date | Features |
1.0 | Oct 14, 2003 | Post to http://www.codeproject.com/ |
0.9 | Sept 29, 2003 | Interface Method Hooked Implemented |
0.85 | Sept 8, 2003 | Grab rtf, icons, ole objects from MSN IM6 |
0.8 | July 7, 2003 | ITextService caught in fake DLL, busy...zzz... plus submission of "Keystroke" series in CodeGuru.com |
0.3 | July ?, 2003 | Brutal force injection API failed on MSN IM6; it blocked contact list intentionally. But time and socket system are taken over by inject code. |
License
This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.
A list of licenses authors might use can be found here
About the Author
Zhefu Zhang Member |
|
Other popular COM / COM+ articles:
|