COM Interface Hooking and Its Application, Chapter I


.

Environment: VC6+SP5 or VC7 (including SPY++, Depends, DataObject Viewer and OLE Viewer), MS Platform Core SDK, MSN Messenger 6.0, Resource Hacker, Process Explorer, and Process Spy. Win 2K/XP/2003 ONLY (local administrative member identity needed to experiment with the code sample)

Prerequisite Knowledge: COM, DLL, Windows Hook, Process/Thread/Module, PE format, Multithread, Memory Management, SEH

Applicable Article Category in CodeGuru: COM, DLL, Tool, System, Miscellaneous, Security, C++, Sample

Special Notes:

  1. You need two active MSN logins to simulate chatting or experiment together with friends. FYI: You can use WinXP/2003 Fast User Switch (FUS) to simulate multiple MSN user login on one physical machine.
  2. All my code has nothing to do with “Messenger API.” You can download it from MSDN Online. This set of API hass not been updated since MSN Messenger 4.7 was released, and it may be obsolete, though you can use it with MSN Messenger 6.0. (at least I have tried with automatic logon, but do not ask me about it, I only used it for hours).

Keywords for Search Engine: COM Interface Hook, C++ Virtual Table Hook, C++ V-table Hook, C++ Vtbl Hook, MSN Messenger 6 Hook, Modify C++ class Virtual Table, Modify C++ class V-table, Modify C++ class Vtbl, Early Binding

Summary

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 the Autumn of the year 2002. During this time this IM keeps its booming pace and now owns about six million users around the world.

With MS releasing MSN Messenger 6.0 in July 2003, which adopted different architecture from its predecessor and made my MessengerSpy++ not work 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, just as the title shows, I make it as a tutorial, in which I would like to introduce you to two new concepts—COM Interface Hooking and COM Interface Method Hooking.

Yes, it is COM Interface Hooking and Method Hooking, which means your interface method takes over the function call before routing to the hooked interface method, just like API hooking and Windows Message hooking, as you may have known.

As a tutorial, this time I will try to not only provide you with a bulk of serious code but also present you the working pattern of developing a program interacting with something running out of your control. Before you continue, I highly recommend you to read my previous articles “MessengerSpy++ for MSN Messenger/Window Messenger” and “Keystroke Logger and More” Series, Article No. 2 to get acquainted with MSN Messenger topics that will help you understand the following discussion greatly for I will intentionally omit what is available in these two articles to save space here.

The last note I want to make here is: This is the first article of the “Interacting with MSN Messenger 6.0 with COM Hook” series I put on my writing schedule list. The following articles, maybe, will demonstrate how to make MS Office 2K/XP and MSN Messenger 6.0 interact with each other, how to take advantage of MSN Network to construct a P2P network (without your server!) to interchange data or make remote inter-program communication, and upgrade my MessengerSpy++ to version 6.0.

What’s New in MSN Messenger from the Developer’s Side

One dozy afternoon in this July, I downloaded a beta MSN Messenger 6.0 from MS, and found its chat window’s GUI had been redesigned completely differently from MSN Messenger 5.0. As you 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 use 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)
  3. 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.

Figure 1. Using GUI RunAs, You Only Need Your Selection Of Target Desktop

Figure 2. With Spy++ running on a WinXP Logon Desktop, you will see the Whole Blue Window is “DirectUIHWND”

New things do not stop here. If you open “msnmsgr.exe” (the main exe of MSN Messenger 6.0, should be located in “%Program Files%MSN Messenger”) with the Depends utility from Visual Studio, and check which API it uses, you will find it needs “UnhookWindowsHookEx” exported from “USER32.DLL” but not “SetWindowsHookEx”!? A possible explanation to this may be that the MS developer used “LoadLibrary(Ex)” and “GetProcAddress” to explicitly call “SetWindowsHookEx”. BTW, in MSN Messenger 5.0, its import table includes both functions.

Anyway, one thing is certain: MSN Messenger 6.0, to save system resources, actively loads and unloads some DLL dynamically. You can launch the “Process Explorer”/”Process Spy” (the latter is lighter with less functionality, but can show you the DLL loading status; read “Escape from DLL Hell with Custome Debugging and Instrumentation Tools and Utilities” by Christophe Nasarre, MSDN Magazine June 2003 for details), then start and close a chat. You will find the DLL list changed a lot. An example is its unloading RICHEDIT20.DLL, when the user closed the last chat window.

Figure 3: Depends revealed an asymmetrical import function table in MSN Messenger 6.0

In the end, now, MSN Messenger 6.0 permits users to save their chat contents using an RTF format instead of plain text. Still, it cannot support the saving of the embedded Emotional icons, which is a pity. What’s more, it still permits the user to close the chat directly without reminding “You are going to close an active chat, and the contents will be lost, proceed? Yes-No-Cancel”.

Is Entering “DirectUIHWND” Possible? Yes! If You Know What’s Going on Under the Hood

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. My first reaction to this window inside MSN Messenger 6.0 is that hooking into MSN Messenger 6.0 will be futile for I have no knowledge of the inside code. But, from my R&D experience, I have the following after a second thought:

Condition 1 MSN Messenger 6.0 can save chat contents in RTF format
Result 1 It has RichEditCtrl like the code handler inside
Condition 2 Due to the antitrust lawsuit, the MS Office Team is not permitted to use code written by the MS Windows Team –> the MSN Messenger Team will not have the RICHEDIT source code
Premise 1 MSN Messenger does not have enough resources to write RICHEDIT because the code is complicated
Condition 3 When I was doing my consulting on Excel Add-ins for MS, somebody told me that following:

MS Excel XP’s Task Pane is purchased from a third-party company with the source code, which shocked me because it (Task Pane) seems like a 10-day job for any C++ guru

Result 2 The MSN Messenger Team buys/reuses a library-based RICHEDIT
Condition 4 MSN Messenger 6.0 loads RICHEDIT20.DLL dynamically whenever there is at least one chat window
Condition 5 In MSN Messenger 6.0, text is drawn “upon” the background image, Windowless Rich Edit seems more suitable to do that
General Result MSN Messenger 6.0 uses the Windowless RICHEDIT

Figure 4: Rough Logic Deduction on MSN Messenger 6.0 of Jeff’s

The above logic deduction, of course, is not strict and has logic defects. But, Condition 4 is critical, and is the strongest proof that the MSN Messenger Team does use their colleagues’ work. Anyway, as I have mentioned, it may give you some idea when working on other programs.

(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.)

Figure 5. “Protected Storage” NT Service Will Prohibit Any Attempt to Overwrite Important DLL files in the System Directory

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

Figure 6. Export Variables and Functions from Riched20.DLL by Depends

(FYI: You may wonder that how diligent I am that I type such a table from Depends. I am not. What I did was use a home-made tool hooking into Depends and stole the text inside the SysListView32. And with a later article I put on my writing schedule, I will show you. Check other articles of mine in CodeGuru.com’s author list; it will be Text-Grabber or something like that.)

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 (refer to Chapters 19 & 20 of Programming Application for MS Windows 2000 by Jeffrey Richter, 1999, MS Press, for details). 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];
//Hope the guy's windows is not installed on Mars
UINT uRet = ::GetSystemDirectory(szLibPath, MAX_PATH]);
if(uRet == 0) err;
szLibPath[uRet] = TCHAR('\0');
::lstrcat(szLibPath, _T("\RichEd20.DLL"));
HMODULE hRichEditLib = ::LoadLibrary(szLibPath);

Note Again: I am not saying that the MSN Messenger Team’s code is wrong. It just gives your code a chance to fake the RichEd20.DLL in the same directory as MSN Messenger 6.0 instead of writing API hooking code.

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 Figure 5 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
IID_ITextServices         @5 PRIVATE
IID_ITextHost             @6 PRIVATE
IID_ITextHost2            @7 PRIVATE
REExtendedRegisterClass   @8 PRIVATE
RichEdit10ANSIWndProc     @9 PRIVATE
RichEditANSIWndProc      @10 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
typedef struct _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;
extern "C" RICHED20_API GUID IID_ITextServices;
extern "C" RICHED20_API GUID IID_ITextHost;
extern "C" RICHED20_API GUID IID_ITextHost2;

#include <unknwn.h>
#include "mytextserv.h"

extern "C" HRESULT WINAPI CreateTextServices(
  IUnknown *punkOuter, ITextHost *pITextHost, IUnknown **ppUnk);
extern "C" LRESULT WINAPI REExtendedRegisterClass(HINSTANCE
                                                  hInstance);
extern "C" LRESULT WINAPI RichEdit10ANSIWndProc(HWND hWnd,
    UINT Msg, WPARAM wParam, LPARAM lParam);
extern "C" LRESULT WINAPI RichEditANSIWndProc(HWND hWnd,
    UINT Msg, WPARAM wParam, LPARAM lParam);

(FYI: The last three exported functions are not used by MSN Messenger 6.0, so you can omit them completely. I keep them here just for compatibility.)

Change your riched20.cpp file accordingly:

RICHED20_API GUID IID_IRichEditOle = { 0x00020D00, 0x0, 0x0,
                                     { 0xC0, 0x0, 0x0, 0x0, 0x0,
                                       0x0, 0x0, 0x46 } };
RICHED20_API GUID IID_IRichEditOleCallback = { 0x00020D03, 0x0,
                                               0x0, { 0xC0, 0x0,
                                               0x0, 0x0, 0x0, 0x0,
                                               0x0, 0x46 } };
RICHED20_API GUID IID_ITextServices = { 0x8d33f740, 0xcf58, 0x11ce,
                                        {0xa8, 0x9d, 0x00, 0xaa,
                                         0x00, 0x6c, 0xad, 0xc5}};
RICHED20_API GUID IID_ITextHost = { 0xc5bdd8d0, 0xd26e, 0x11ce,
                                   {0xa8, 0x9e, 0x00, 0xaa, 0x00,
                                    0x6c, 0xad, 0xc5}};
RICHED20_API GUID IID_ITextHost2 = { 0xc5bdd8d0, 0xd26e, 0x11ce,
                                    {0xa8, 0x9e, 0x00, 0xaa, 0x00,
                                     0x6c, 0xad, 0xc5}};

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) return 0;
  szLib[dw] = TCHAR('\0');
  ::lstrcat(szLib, NEW_DLL_NAME);
  HMODULE hLib = LoadLibrary(szLib);
  if(!hLib) return 0;
  lpCreateTextServices _CreateTextServices = (HRESULT
                       (__stdcall *)(IUnknown*, ITextHost*,
                                     IUnknown**))
                        ::GetProcAddress(hLib,
                        "CreateTextServices");
  if(!_CreateTextServices) return 0;
  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;
} //Note: //Protected Storage System Service Protects "RICHEDIT20.DLL" //under %SystemRoot%System32 LRESULT WINAPI REExtendedRegisterClass(HINSTANCE hInstance) { return 0; } LRESULT WINAPI RichEdit10ANSIWndProc(HWND hWnd,UINT Msg, WPARAM wParam, LPARAM lParam) { return 0; } LRESULT WINAPI RichEditANSIWndProc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam) { return 0; }

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 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.

Figure 7. Overview of COM Interface Hook Applied to MSN Messenger 6.0

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 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 stablility 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

Figure 8. Communication between MSN Messenger and Your Program. Note the hidden Window’s functionality

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);
  }
  return 0;
}

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.

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 9. A Bitmap Grabbed from MSN Messenger 6.0 as an emotional icon (size: 19 X 19 pixels, always)

Figure 10. 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) new
                           char[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, Jeff, how do you know it is in WMF format? Unfornately, 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);
    }
  }
}

The const value I used is extracted from <winuser.h> and <ObjIdl.h> like the following, by the way:

#define CF_TEXT 1
#define CF_BITMAP 2
#define CF_METAFILEPICT 3
#define CF_SYLK 4

...

enum tagTYMED
{    TYMED_HGLOBAL = 1,
     TYMED_FILE = 2,
     TYMED_ISTREAM = 4,
    ...

You will find the header files in Platform SDK are a golden mine that provide tons of invaluable information uncovered in documents. One joke for you: One of my classmates when I was bending on my course in EE someday told me in the end he read thru all the header files of Borland C++ 3.5 and found he felt himself completely when coding, “hi, just like when you crammed the Simplified Oxford Dictionary, your feeling on GRE verbal part,” he added.

Thru ParseDataObject, there are two ways to extract the emotional icon—either you treat it as a CF_UNICODETEXT (actually, it is just the key-shortcut for the emotional icons), or you take it as a WMF.

In the end, there is a new kind of embedded object now in MSN Messenger 6.0—the “File Transmission Object”. When you send a file to a chatter friend, you will have a thumbnail of the file (if it is a image Windows can recognize) you are sending there with a progress bar beneath. We will defer this object handling until later.

Hook COM Interface Method

From 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:
  virtual int method1()
    { return 11; }
  virtual int method2()
    { return 12; }
  virtual int method3()
    { return 13; }

};

class classB: public classA

{

public:
  virtual int method1()
    { return 21; }
  int m;
  int n;

};

class classC : public classB

{

public:
  int method1(int a, short b)
    { return 31; }

};

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;
}

Figure 11. Virtual Table of C++ base and derived class

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);
  memcpy((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.

As to what’s “Coming Soon,” I will give an integrated, all ways mentioned above, way to make a tool like my previous “MessengerSpy++ for MSN Messenger/Window Messenger“; and I think guru-level readers can skip it to my third article unless they want to copy-and-paste my code. Later, I will try to show you how to make MS Office 2K/XP and MSN Messenger 6.0 interact with each other, how to take advantage of MSN IM/Network to construct a P2P tunnel (without your server, that’s the point!) to interchange data or make remote inter-program communication.

In the end, thank you all for your patience to read such a long article and I hope you enjoy it.

Downloads

Download Demo Project Source (all the source code + exe, MFC DLL static linked) – 771 Kb

Version History

Version Release Date Features
1.0 article close Spare night time to put all basic together to www.codeguru.com readers after being asked in dozens of mails
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.

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read