Click here to Skip to main content
15,888,579 members
Articles / Programming Languages / C++
Article

COM Interface Hooking and Its Application - Part I

Rate me:
Please Sign up or sign in to vote.
4.87/5 (51 votes)
19 Oct 200318 min read 679.3K   8.7K   209   68
Interaction with MSN Messenger 6.0

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.

Image 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 ^HintFunctionEntry Point
2 (0x0002)1 (0x0001)IID_IRichEditOle0x00014C60
3 (0x0003)2 (0x0002)IID_IRichEditOleCallback 0x00014C50
4 (0x0004)0 (0x0000)CreateTextServices0x0000D882
5 (0x0005)5 (0x0005)IID_ITextServices0x00014C20
6 (0x0006)3 (0x0003)IID_ITextHost0x00014C30
7 (0x0007)4 (0x0004)IID_ITextHost20x00014C40
8 (0x0008)6 (0x0006)REExtendedRegisterClass0x0004BA5C
9 (0x0009)7 (0x0007)RichEdit10ANSIWndProc0x0003DB01
10 (0x000A)8 (0x0008)RichEditANSIWndProc0x00015681

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

#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) 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;
}
...

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.

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

Image 3

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.

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.

Image 4

Figure. A Bitmap Grabbed from MSN Messenger 6.0 as an emotional icon (size: 19 X 19 pixels, always)

Image 5

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) 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, 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:
   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;
}

Image 6

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

VersionRelease DateFeatures
1.0Oct 14, 2003Post to http://www.codeproject.com/
0.9Sept 29, 2003Interface Method Hooked Implemented
0.85Sept 8, 2003Grab rtf, icons, ole objects from MSN IM6
0.8July 7, 2003ITextService caught in fake DLL, busy...zzz... plus submission of "Keystroke" series in CodeGuru.com
0.3July ?, 2003Brutal 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


Written By
Other
United States United States
fdefewtr534554yutki8op09;[pio';l.n,kbnmcvbxcvzxaqW876876UIYIUJUGHJGFHYFGHRDTR4564QWEDASASFDXCBVCBNGHNMJHMJN,NJKL;O[P-0=-]'[P';L/L,M.NM,BNMCGNGFXDGDFGTYU76TRYW34TR5AWERFASDVGfdsxbvfbvnvnm,jkl.k

Comments and Discussions

 
GeneralRe: Adding menu item and catching event fired by messenger Pin
koo911-Dec-03 10:06
koo911-Dec-03 10:06 
GeneralRe: Adding menu item and catching event fired by messenger Pin
koo921-Mar-04 13:36
koo921-Mar-04 13:36 
GeneralThanks God, MS update to MSN Messenger6.1 yesterday, and my method still rocks! Pin
Zhefu Zhang9-Dec-03 13:39
Zhefu Zhang9-Dec-03 13:39 
GeneralRe: Thanks God, MS update to MSN Messenger6.1 yesterday, and my method still rocks! Pin
koo911-Dec-03 6:22
koo911-Dec-03 6:22 
Generalnext part Pin
Anonymous3-Nov-03 15:29
Anonymous3-Nov-03 15:29 
GeneralRe: next part Pin
Zhefu Zhang4-Nov-03 2:19
Zhefu Zhang4-Nov-03 2:19 
GeneralRe: next part Pin
Anonymous4-Nov-03 10:14
Anonymous4-Nov-03 10:14 
GeneralChinese read this: (Simpiled Chn Version) --- It took me 18 yrs to drink coffee with U --- Found in http://www.driverdevelop.com/forum/html_51738.html?1067363276 --- Author: Unknown Pin
Zhefu Zhang28-Oct-03 7:08
Zhefu Zhang28-Oct-03 7:08 
我花了18年时间才能和你坐在一起喝咖啡??

我的白领朋友们,如果我是一个初中没毕业就来沪打工的民工,你会和我坐在starbucks一起喝咖啡吗?不会,肯定不会。  
 
比较我们的成长历程,你会发现为了一些在你看来垂手可得的东西,我却需要付出巨大的努力。从我出生的一刻起,我的身份就与你有了天壤之别,因为我只能报农村户口,而你是城市户口。如果我长大以后一直保持农村户口,那么我就无法在城市中找到一份正式工作,无法享受养老保险、医疗保险,甚至连选举权都不如你们多。你可能会问我: "为什么非要到城市来?农村不很好吗?空气新鲜,又不像城市这么拥挤。"可是农村没有好的医疗条件,这次sars好像让大家一夜之间发现农村的医疗保健体系竟然如此落后;物质供应也不丰富,因为农民挣的钱少,贵一点的东西就买不起,所以商贩也不会进太多货。农民没有职业发展规划,因为农民不是一个职业,而是一个类似种族一样的天生的阶层。农民没有自我实现的满足感,因为绝大多数人还在为基本的生存而奋斗,春节联欢晚会中买得起等离子彩电的农民毕竟是个别现象。于是我要进城,而且要摆脱我的农民身份,我要通过自己的奋斗获得你生下来就拥有的大城市户口,考学是我跳出农门唯一的机会。我要刻苦学习,小学升初中,初中升高中,高中考大学,我在独木桥上奋勇搏杀,眼看着周围的同学一批批落马,前面的道路越来越窄,我这个佼佼者心里不知是喜是忧。激烈的竞争让我不敢疏忽,除了学习功课,我无暇去顾及业余爱好,学校也没有这些发展个人特长的课程,进入高中的第一天起,校长就告诉我们这三年只有一个目标―高考。于是我披星戴月,早上5:30起床,晚上11:00睡觉,就连中秋节的晚上,我还在路灯下背政治。而你的升学压力要小得多,竞争不是那么激烈,功课也不是很沉重,你可以有充足的时间去发展个人爱好,去读课外读物,去球场挥汗如雨,去野外享受蓝天白云。如果你不想那么辛苦去参加高考,只要成绩不是最差的,你可以在高三时容易地获得保送名额,哪怕成绩最差,也会被"扫"进一所本地三流大学,而那所三流大学我可能也要考到很高的分数才能进去,因为按地区分配的名额中留给上海本地的名额太多了。我们的考卷不一样,如果考卷一样我们的分数线就不一样,但是当我们都获得录取通知书的时候,所交的学费是一样的。每人每年6000元,四年下来光学费就要2万4千元, 再加上住宿费每人每年1500元,还有书本教材费每年1000元、 生活费每年4000元(只吃学校食堂),四年总共5万元。 2003年上海某大学以"新建的松江校区环境优良"为由, 将学费提高到每人每年1万元,这就意味着仅学费一项四年就要4万元, 再加上其他费用,总共6万6千元。6万6千元对于一个上海城市家庭来说也许算不上沉重的负担,可是对于一个农村的家庭,这简直是一辈子的积蓄。由于剪刀差、地少人多不成规模经济等众多原因,农业仍然是不赚钱的行业。我的家乡在东部沿海开放省份,是一个农业大省,相比西部内陆省份应该说经济水平还算比较好。可是现在的农产品收购价太便宜了,除去各种农业种植成本和名目繁多的税费,一年辛苦劳作剩不了几个钱。以供养两个孩子的四口之家为例,除去各种日常必需开支,一个家庭每年最多积蓄3000元,那么6万6千元上大学的费用意味着 22年的积蓄!前提是任何一个家庭成员都不能生大病,而且另一个孩子无论学习成绩多么优秀,都必须剥夺他上大学的权利,因为家里只能提供这么多钱。我属于比较幸运的,东拼西凑加上助学贷款终于交齐了第一年的学费,看着那些握着录取通知书愁苦不堪全家几近绝望的同学,我的心中真的不是滋味。教育产业化时代的大学招收的不仅是成绩优秀的同学,而且还要有富裕的家长。我终于可以如愿以偿地在大学校园里汲取知识的养分!努力学习获得奖学金,假期打工挣点生活费,我实在不忍心多拿父母一分钱,那每一分钱都是一滴汗珠掉在地上摔成八瓣挣来的血汗钱啊!

来到上海这个大都市,我发现与我的同学相比我真是土得掉渣。我不会作画, 不会演奏乐器,不认识港台明星,没看过武侠小说,不认得mp3, 不知道什么是walkman,为了弄明白营销管理课上讲的"仓储式超市"的概念, 我在麦德隆好奇地看了一天,我从来没见过如此丰富的商品。我没摸过计算机,为此我花了半年时间泡在学校机房里学习你在中学里就学会的基础知识和操作技能。我的英语是聋子英语、哑巴英语,我的发音中国人和外国人都听不懂,这也不能怪 我,我们家乡没有外教,老师自己都读不准,怎么可能教会学生如何正确发音?基础没打好, 我只能再花一年时间矫正我的发音。我真的很羡慕大城市的同学多才多艺,知识面这 么广, 而我只会读书,我的学生时代只有学习、考试、升学,因为只有考上大学,我才能来到你们中间,才能与你们一起学习,所有的一切都必须服从这个目标。
 
我可以忍受城市同学的嘲笑,可以几个星期不吃一份荤菜,可以周六周日全天泡在图书馆和自习室,可以在周末自习回来的路上羡慕地看着校园舞厅里的成双 成对, 可以在寂寞无聊的深夜在操场上一圈圈地奔跑厖我想有一天我毕业的时候,我能在这个大都市挣一份工资的时候,我会和你这个生长在都市里的同龄人一样―做一个上海公民,而我的父母也会为我骄傲,因为他们的孩子在大上 海工 作!
 
终于毕业了,令我意想不到的是,辛辛苦苦读出来的大学文凭,竟然很难找到工 作,在上海工作难找,回到家乡更没有什么就业机会。能幸运地在上海找到工作的应届本科生 只有每月2000元左右的工资水平,也许你认为这点钱应该够你零花的了,可是对我来说, 我还要租房,还要交水电煤电话费还要还助学贷款,还想给家里寄点钱让弟妹继续读书,剩下的钱只够我每顿吃盖浇饭,我还是不能与你坐在starbucks一起喝咖啡!

现在舆论号召我们大学生创业,真不明白我们这些既没钱也没经验的刚毕业的 学生有什么资本去创业,为什么那些人浮于事却能旱涝保收的单位里的职工不去辞职 创业?  

也许所有的这一切都怪我投错了胎,为什么我不降生在上海!  
 
写到这里,我需要声明:我不是来自农村,我来自一个小城市,在上海读完了硕士,现在有一份年薪七八万的工作。我奋斗了18年,现在终于可以与你坐在一起喝咖啡。我已经融入到这个国际化大都市中了,与周围的白领朋友没有什么差别,可是我无法忘记奋斗历程中那些艰苦的岁月,无法忘记那些曾经的同学和他们永远无 法实现的夙愿。   

于是我以第一人称的方式写下了上面的文字,这些是最典型的中小城市和农村平民子弟奋斗历程的写照。每每看到正在同命运抗争的学子,我的心里总是会有一种沉重的责任感。写这篇文章不是为了怨天尤人,这个世界上公平是相对的,不公平是绝对的, 不公平已经存在,这并不可怕,但是对不公平视而不见是非常可怕的。
 
我在上海读硕士的时候,曾经讨论过一个维达纸业的营销案例,我的一位当时曾有3年工作经验,现任一家中外合资公司人事行政经理的同学,提出一个方案:应该让维达纸业开发高档面巾纸产品推向9亿农民市场。 我惊讶于她提出这个方案的勇气,当时我问她是否知道农民兄弟吃过饭后如何处理面部油腻,她疑惑地看着我,我用手背在两侧嘴角抹了两下,对如此不雅的动作她斥之鄙夷神色。 在一次宏观经济学课上,我的另一同学大肆批判下岗工人和辍学务工务农的少年: "80%是由于他们自己不努力,年轻的时候不学会一门专长,所以现在下岗活该!那些学生可以一边读书一边打工嘛,据说有很多学生一个暑假就能赚几千元,学费还用愁吗?"我的这位同学可能永远都不会相信我本科时有个同学是每天拿着饭盒到学校餐厅里捡别人吃剩的饭菜来熬过四年的大学生活的。他可能没有研究过中国社会财富分配制度的变迁,我们的父辈年富力强时候所创造的 财富 中本来应该属于自己的那部分,在高积累低消费政策下变成了国有资产,继而变成了国有商业银行成千上万亿元再也收不回来的不良贷款。糟蹋完这些钱后, 当年包养老、包医疗、包住房、包教育的空口承诺灰飞烟灭,留给已经老去的父辈只是 下岗失业和生活无济。
 
现存的不公平大多数是由于人为的政策造成的,高高在上的决策者是无法做出保护社会大众特别是下层人民的政策的。

我是70年代中期出生的人,我的同龄人正在逐渐成为社会的中流砥柱,我们的决策将影响社会和经济的发展。把这篇文章送给那些优越环境成长起来的年轻人和很久以前曾经吃过苦现在已经淡忘的人,关注社会下层,为了这个世界更公平些,我们应该做些力所能及的事情,特别是在做关乎众人命运的决策的时候,让这份社会责任感驻留我们的头脑。我花了18年时间才能和你坐在一起喝咖啡??
GeneralSo? Pin
TW29-Oct-03 1:38
TW29-Oct-03 1:38 
GeneralSpecial Declaration Pin
Zhefu Zhang29-Oct-03 3:23
Zhefu Zhang29-Oct-03 3:23 
GeneralRe: So? Pin
Zhefu Zhang15-Feb-04 15:50
Zhefu Zhang15-Feb-04 15:50 
GeneralRe: Chinese read this: (Simpiled Chn Version) --- It took me 18 yrs to drink coffee with U --- Found in http://www.driverdevelop.com/forum/html_51738.html?1067363276 --- Author: Unknown Pin
holdWind27-Dec-03 23:20
holdWind27-Dec-03 23:20 
GeneralRe: Chinese read this: (Simpiled Chn Version) --- It took me 18 yrs to drink coffee with U --- Found in http://www.driverdevelop.com/forum/html_51738.html?1067363276 --- Author: Unknown Pin
TW16-Feb-04 4:24
TW16-Feb-04 4:24 
QuestionCan you find the typo in the above article ? Pin
Zhefu Zhang22-Oct-03 22:22
Zhefu Zhang22-Oct-03 22:22 
GeneralCompliments and Questions Pin
antoine@orchus-tech22-Oct-03 19:29
antoine@orchus-tech22-Oct-03 19:29 
GeneralRe: Compliments and Questions Pin
Zhefu Zhang22-Oct-03 22:17
Zhefu Zhang22-Oct-03 22:17 
GeneralRe: Compliments and Questions Pin
rootbeer16-Nov-03 23:31
rootbeer16-Nov-03 23:31 
GeneralThanks Pin
Zhefu Zhang17-Nov-03 3:38
Zhefu Zhang17-Nov-03 3:38 
GeneralCould not even get it to build. Pin
WREY22-Oct-03 8:44
WREY22-Oct-03 8:44 
GeneralYour compiler setting is not the latest Pin
Zhefu Zhang22-Oct-03 9:34
Zhefu Zhang22-Oct-03 9:34 
GeneralRe: Your compiler setting is not the latest Pin
TW25-Oct-03 2:17
TW25-Oct-03 2:17 
Generala long comment [modified] Pin
Zhefu Zhang25-Oct-03 2:43
Zhefu Zhang25-Oct-03 2:43 
GeneralRe: a long comment Pin
TW25-Oct-03 4:27
TW25-Oct-03 4:27 
GeneralRe: a long comment Pin
Zhefu Zhang25-Oct-03 7:45
Zhefu Zhang25-Oct-03 7:45 
GeneralRe: a long comment Pin
TW25-Oct-03 22:50
TW25-Oct-03 22:50 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.