COM Interface Hooking and Its Application - Part I

  • 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*

  1. You must be a Administrator Group Member to do so.
  2. 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.

COM Interface Hooking and Its Application - Part I_第1张图片

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):

Collapse Copy Code
HMODULE hRichEditLib = ::LoadLibrary(_T("RICHED20.DLL"));
instead of a more robust one:
Collapse Copy Code
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:

Collapse Copy Code
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:

Collapse Copy Code
#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:

Collapse Copy Code
#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:

Collapse Copy Code
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.

COM Interface Hooking and Its Application - Part I_第2张图片

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:

COM Interface Hooking and Its Application - Part I_第3张图片

Collapse Copy Code
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)

COM Interface Hooking and Its Application - Part I_第4张图片

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.

Collapse Copy Code
//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:

Collapse Copy 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):

Collapse Copy Code
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;
}

COM Interface Hooking and Its Application - Part I_第5张图片

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:

Collapse Copy Code
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:

Collapse Copy Code
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:

Collapse Copy Code
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:

Collapse Copy Code
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

Occupation: Other
Location: United States United States

Other popular COM / COM+ articles:

  • Introduction to COM - What It Is and How to Use It.
    A tutorial for programmers new to COM that explains how to reuse existing COM components, for example, components in the Windows shell.
  • COM in plain C
    How to create/use COM components in plain C, without MFC, ATL, WTL, or any other framework.
  • Understanding Classic COM Interoperability With .NET Applications
    Discusses how existing COM components can be used from managed code.
  • Getting the most out of IDispatch
    A C++ class that makes it extremely easy to use a COM object, even in console apps
  • Introduction to COM Part II - Behind the Scenes of a COM Server
    A tutorial for programmers new to COM that explains the internals of COM servers, and how to write your own interfaces in C++

你可能感兴趣的:(COM Interface Hooking and Its Application - Part I)