Introduction
It has been nearly one year since I published "MessengerSpy++ for MSN
Messenger / Window Messenger" (to interact with MSN Messenger 4.6, 4.7 and
5.0) in www.CodeGuru.com in Autumn of year 2002, and with MS releasing MSN
Messenger 6.0 in July 2003, which adopted different architecture from its
predecessor and made my MessengerSpy++ not
working with this 6.0 version, I decided to write this article to demonstrate
how to make a program interacting with MSN Messenger 6.0. This time, I would
like to introduce you 2 new things --- COM Interface Hooking and COM Interface
Method Hooking. Yes, it is COM Interface Hooking and Method Hooking, which means
your interface method take over the function call before routing to the hooked
interface method, just like API hooking and Windows Message hooking you may have
known.
Before you continue, I highly recommend you to read my previous articles "MessengerSpy++ for MSN
Messenger / Window Messenger" and "Keystroke Logger and
More" Serials, Article No. 2 to get acquainted with MSN Messenger topics
which will help understanding the following discussion greatly for I will
intentional omit what is available in these 2 articles to save space here.
Note: This article can also be found here and may be more
informational for less cut.
What You Need Before Start
Depends, MS Platform
Core SDK, MSN Messenger 6.0, Resource Hacker, Process Explorer or Process
Spy. Win 2K/XP/2003 ONLY! (local administrative member identity needed);
You need two active MSN (passport) logins to simulate chatting or experiment
together with friends (for you have to start a chat). FYI: You can use
WinXP/2003 Fast User Switch (FUS) to simulate multiple MSN user login on one
physical machine.
Background and What's New in MSN Messenger 6.0
As you may know, MSN Messenger before 5.0 (inclusive) uses a "RichEdit"
common control as the chat input area and chat contents area, the "Send" button
is a genuine "BUTTON" windows control. To interact with it, your program uses a
hook or whatever remote injection ways to penetrate into MSN Messenger process
space, and conduct button-pushing and text-reading just the same as doing this
in a dialog-based GUI program we all have been writing.
But, in MSN Messenger 6.0, when you use SPY++ to check its windows layout,
there is only a "DirectUIHWND" window. "DirectUIHWND" is a widely used wrapper
windows class since the emergence of Windows XP, according to my observation. If
you are using WinXP/2003, you can modify Mr. Keith Brown's tool CmdRunAs in Feb 2000,
MSJ or Mr. Martyn
'Ginner' Brown's tool Start a Command As Any User in www.codeguru.com 2001,
or if you are a lazy typist, use my GUI-based "RunAs" directly
to launch SPY++ under "LocalSystem" account to your logon
desktop(WinSta0\Winlogon).
Note*
- You must be a Administrator Group Member to do so.
- No need to try WinXP/2003's "Runas..." shell command because it always
launches in the current desktop (WinSta0\Default). You may need to "Alt+Esc" to
make SPY++ visible when turn to the logon desktop.)
Now you will find the "DirectUIHWND" window. For the sake of these non-WinXP
users, here is the screen shot of this scenario.
So, the point of interacting with MSN Messenger 6.0 is focused on whether it
is possible to hook into "DirectUIHWND" and obtain data from inside
successfully. General speaking, it is nearly impossible to hook and interact
with a wrapper window such as "DirectUIHWND" if you have no documentation of its
inside Windows message handler, COM interface description, and some global data
structure. But luckily, after using Process Spy and Depends, it is clear MSN
Messenger 6.0 dynamically load and unload "RichEd20.DLL" which gives us a
sign that it uses Windowless Rich Edit Control inside.
(Something FYI: As you might know, richedit20.dll is a target of
Worm.Nimda to launch itself when a user starts the MS Office family application.
Besides, before the release a new version accompanying OfficeXP, it has a
serious buffer overflow problem, which leads to system takeover by a remote user
thru some IM chatting. Now, it is being protected, by default, by "Protected
Storage" NT Service. An attempt to overwrite this will fail unless you stop this
NT Service first.)
Now, use Depends to open "richedit20.dll". By default, this file is in
the "%SystemRoot%\system32" directory. (If you are using Win2k, it may be
in C:/WinNT/System32; or, if you are using WinXP,
C:/Windows/System32.) The exported table follows:
Ordinal ^ |
Hint |
Function |
Entry Point |
2 (0x0002) |
1 (0x0001) |
IID_IRichEditOle |
0x00014C60 |
3 (0x0003) |
2 (0x0002) |
IID_IRichEditOleCallback |
0x00014C50 |
4 (0x0004) |
0 (0x0000) |
CreateTextServices |
0x0000D882 |
5 (0x0005) |
5 (0x0005) |
IID_ITextServices |
0x00014C20 |
6 (0x0006) |
3 (0x0003) |
IID_ITextHost |
0x00014C30 |
7 (0x0007) |
4 (0x0004) |
IID_ITextHost2 |
0x00014C40 |
8 (0x0008) |
6 (0x0006) |
REExtendedRegisterClass |
0x0004BA5C |
9 (0x0009) |
7 (0x0007) |
RichEdit10ANSIWndProc |
0x0003DB01 |
10 (0x000A) |
8 (0x0008) |
RichEditANSIWndProc |
0x00015681 |
It is really lucky for us that riched20.dll just exports less than a
dozen of variables and functions. Besides, after I checked it with ">Dumpbin
-Exports Riched20.DLL", it has no forward function, which is good news for our
hookers; and by default, riched20.dll is not available in the "Known DLL"
part inside the Registry. All these facts decided we only need to hook this
riched20.dll.
Note: I am not saying that "forward function" and "Known DLL" will
prevent us from hooking; you can always hook that Windows API on the final DLL.
Now, we do not have these two problems, which means we only need to take care a
single riched20.dll, leading to less work.
Besides, you may be surprised when I tell you the loading code in MSN
Messenger 6.0 is like the following (pseudo code):
HMODULE hRichEditLib = ::LoadLibrary(_T("RICHED20.DLL"));
instead of a
more robust one:
TCHAR szLibPath[MAX_PATH];
UINT uRet = ::GetSystemDirectory(szLibPath, MAX_PATH]);
if(uRet == 0) err;
szLibPath[uRet] = TCHAR('\0');
::lstrcat(szLibPath, _T("\RichEd20.DLL"));
HMODULE hRichEditLib = ::LoadLibrary(szLibPath);
Okay, now start your VC++ 6.0 or VS.NET, create a new Win32 DLL project
called "riched20.dll" (case insensitive), change its setting to Unicode,
and export EXACTLY the same things as shown in the above table in the
riched20.def file, like this:
LIBRARY "Riched20"
DESCRIPTION 'RichEdit Ver 2 & 3 DLL'
EXPORTS
IID_IRichEditOle @2 PRIVATE
IID_IRichEditOleCallback @3 PRIVATE
CreateTextServices @4 PRIVATE
...
You need go to the Platform SDK (now called MS SDK) directory, locate the
file "TextServ.h", and copy the following from it to compose a new file
titled "MyTextServ.h" in your project, like this:
#ifndef _TEXTSERV_H
#define _TEXTSERV_H
#ifdef __cplusplus
struct PARAFORMAT2 : _paraformat
{
LONG dySpaceBefore;
...
};
#else
typedef struct _paraformat2
{
UINT cbSize;
...
} PARAFORMAT2;
#endif
struct CHANGENOTIFY {
DWORD dwChangeType;
void * pvCookieData;
};
#define TXTBIT_RICHTEXT 1
#define TXTBIT_MULTILINE 2
...
class ITextServices : public IUnknown
{
public:
virtual HRESULT TxSendMessage(
UINT msg,
WPARAM wparam,
LPARAM lparam,
LRESULT *plresult) = 0;
};
class ITextHost : public IUnknown
{
public:
virtual HDC TxGetDC() = 0;
virtual INT TxReleaseDC(HDC hdc) = 0;
...
};
STDAPI CreateTextServices(
IUnknown *punkOuter,
ITextHost *pITextHost,
IUnknown **ppUnk);
typedef HRESULT (STDAPICALLTYPE * PCreateTextServices)(
IUnknown *punkOuter,
ITextHost *pITextHost,
IUnknown **ppUnk);
#endif
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")
HRESULT WINAPI CreateTextServices(IUnknown *punkOuter,
ITextHost *pITextHost, IUnknown **ppUnk)
{
TCHAR szLib[MAX_PATH];
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);
ITextServices* lpTx;
((IUnknown*)(*ppUnk))->QueryInterface(IID_ITextServices, (void**)(&lpTx));
if(lpTx)
MessageBox(NULL, _T("Interface Hooked"), _T("Indeed"), MB_OK);
return hr;
}
...
Making this DLL compliable may take you some time before you put everything
correctly. After you produce this fake riched20.dll, copy it to the MSN
Messenger 6.0 directory. Launch MSN Messenger 6.0, start a chat, and you will
see for that each chat window, the message box will pop up six times. This means
that in each chat window, there are six windowless rich edit controls working on
your behalf. After a few experiments, I know the first one is the address area
that shows the chatter's e-mail address and nickname. The second is the chat
contents area and the fourth is where you input the words. The others have no
direct user interaction functionality, so we omit them in the following
discussion.
COM Interface Hooked?
Up to now, code gurus should have understand the point all our working are
around, just as I did in my MessengerSpy++, where I
get the chat area window handle and interact with it. This time, I hooked my
class into the windowless rich edit, and "embody it;" that is, you get the
physically existing interface pointer. This "embodiment" must be done before the
COM interface pointer is returned to the Application (MSN Messenger) and my
module must be a DLL module injected into that application.
In this way, my code can leverage the COM interface harmlessly (hopefully no
race condition, no sync contradiction, and so on after carefully designing the
code) with the application. What's cool is that you can go one step further and
change the Vtbl of the interface�make the application's call into your hooking
function first instead of calling into the stolen COM interface method. The
following figures will explain the difference between a normal COM interface
method call and a hooked interface method call.
If you are still not clear about this, refer the above figure. See, the MSN
Messenger 6.0 implemented the ITextHost
interface that is passed to
riched20.dll and a ITextService
interface pointer is
returned. Because our fake Riched20.dll takes the middle position, we now own
the interfaces' pointer�on one hand, you can query the ITextService
for the RTF data and even set the RTF data (which is the prerequisite condition
of dynamic interact with MSN Messenger 6.0 chatting). On the other hand, you can
"WriteProcessMemory
" and modify the interface Virtual Table (which
is the counterpart of Windows Message Hooking if you do it on
TxSendMessage
").
The COM Interface Hook has one fatal shortcoming compared with API hooking
and Message Hooking: You can make API hooking and unhooking in runtime freely by
modifying the process image in memory. You also can hook and unhook a Windows
Message by simply calling "SetWindowsHookEx
" and
"UnhookWindowsHookEx
" freely. NOTE, freely means you can
hook/unhook at any time, including the target process having been started for
some time. The COM Interface Hook just cannot accept this. YOU MUST CATCH THE
COM INTERFACE POINTER WHEN IT IS GIVEN BIRTH OR YOU LOSE IT FOREVER.
This characteristic means that a COM Interface Hooking program must be
running before the target program creates the interface pointer and sometimes it
must keep an eye on the creation of the target process if the target process
does all interface creation stuff at the very beginning. This means you may need
install "process monitor driver" written with DDK or other possible way like we
put a fake DLL this time. As to the COM interface method hooking, it may be,
under certain circumstances, extremely difficult to maintain program stability
without triggering a deadlock or race condition. So be careful.
Besides, most of the time, you may need to hook
CoCreateIntance(Ex)
directly to get the interface pointer, which
requires your grasping API hooking tech first. As a fast link collection place,
you may find my "Key Stroke Logeer and
More, Part 3" gives a long list of resource concerning this topic. This
topic will be discussed again, later in this series.
Communication Solution�Between MSN Messenger 6.0 and Your Program
Now, let's talk about the communication between your program and the fake
riched20.dll. You can refer my previous articles, MessengerSpy++ and Key Stroke Logger and
More Series, Part 1 for the usage of MMF when coping with variable length
unidirectional data transmission. Although you can launch a "listening" thread
inside this fake DLL and receive a command from your program, it may be
complicated and error prone. As with my using Windows User Message in MessengerSpy++, this
time I take the same approach with a little modification.
Because I am not using "SetWindowsHookEx
" on a MSN Messenger 6.0
Chat window (well, you can use it, but it does not make much sense, like we use
it in MSN Messenger 5.0 where I use it to cope with windows handles), I create a
hidden window and this hidden window, just like COM STA, takes care of your
Query Synchronization, Command Processing, and Redirection. And the code will be
very compact; 30 lines is enough. "Simple is Beautiful." The code excerpt is
below:
BOOL APIENTRY DllMain( HANDLE hModule, DWORD ul_reason_for_call,
LPVOID lpReserved)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
InitializeRecv(TRUE);
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)
{
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.
Figure. A Bitmap Grabbed from MSN Messenger 6.0 as an emotional icon (size:
19 X 19 pixels, always)
Figure. A WMF grabbed from MSN Messenger 6.0 as an emotional icon. Notice its
size is much larger than its bitmap counterpart (size: 200 X 200 pixels, always)
Following is the code extracting an emotional icon from MSN Messenger 6.0. It
is somewhat similar to MessengerSpy++ but this
time, the embedded object is in WMF format instead of BMP format.
BSTR bstr;
HRESULT hr = ((ITextServices*)::g_lpIText->TxGetText(&bstr);
if(FAILED(hr)) err;
::SysFreeString(bstr);
IRichEditOle* pReo = NULL;
g_lpIText->TxSendMessage(EM_GETOLEINTERFACE, 0,
(LPARAM)(LPVOID*)&pReo, &lr);
if(lr == 0) return;
LONG nNumber = pReo->GetObjectCount();
if(nNumber == 0) return;
REOBJECT* ro = new REOBJECT;
ro->cbStruct = sizeof(REOBJECT);
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;
STGMEDIUM stgm;
FORMATETC fm;
fm.cfFormat = CF_METAFILEPICT;
fm.ptd = NULL;
fm.dwAspect = DVASPECT_CONTENT;
fm.lindex = -1;
fm.tymed = TYMED_MFPICT;
hr = lpDataObject->GetData(&fm, &stgm);
if(FAILED(hr)) err;
HMETAFILEPICT hMetaFilePict = stgm.hMetaFilePict;
LPMETAFILEPICT pMFP = (LPMETAFILEPICT) GlobalLock (hMetaFilePict);
int cx = 19;
int cy = 19;
HWND hWnd = ::GetDesktopWindow();
HDC hDC = ::GetDC(hWnd);
HDC hMemDC = ::CreateCompatibleDC(hDC);
HBITMAP hMemBmp = ::CreateCompatibleBitmap(hDC, cx, cy);
HBITMAP hPrevBmp = (HBITMAP)::SelectObject(hMemDC, hMemBmp);
::PlayMetaFile(hMemDC, pMFP->hMF);
CopyMetaFile(pMFP->hMF, _T("C:\\fromMSN.wmf"));
TCHAR szFilename[64];
wsprintf(szFilename, _T("c:\\fromMSN.bmp"));
HANDLE hFile = ::CreateFile(szFilename, GENERIC_WRITE, 0,
NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if(hFile == INVALID_HANDLE_VALUE) err;
DWORD dwWritten;
BITMAPFILEHEADER bmfh;
bmfh.bfType = 0x4d42;
int nColorTableEntries = 0;
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;
::GetObject(hMemBmp, sizeof(bm), &bm);
int nBitCount = bm.bmBitsPixel;
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;
DWORD dwCount =((DWORD) lpBMIH->biWidth * lpBMIH->biBitCount)/ 32;
if(((DWORD) lpBMIH->biWidth * lpBMIH->biBitCount) % 32)
dwCount++;
dwCount *= 4;
dwCount = dwCount * lpBMIH->biHeight;
LPVOID lpImage = ::VirtualAlloc(NULL, dwCount, MEM_COMMIT, PAGE_READWRITE);
BOOL result = GetDIBits(hMemDC, (HBITMAP)hMemBmp, 0L,
(DWORD)bm.bmHeight,
(LPBYTE)lpImage,
(LPBITMAPINFO)lpBMIH,
(DWORD)DIB_RGB_COLORS
);
::WriteFile(hFile, lpBMIH, sizeof(BITMAPINFOHEADER), &dwWritten, NULL);
::WriteFile(hFile, lpImage, dwCount, &dwWritten, NULL);
::VirtualFree(lpImage, 0, MEM_RELEASE);
::CloseHandle(hFile);
::SelectObject(hMemDC, hPrevBmp);
::DeleteObject(hMemBmp);
::DeleteDC(hMemDC);
::ReleaseDC(hWnd, hDC);
::GlobalUnlock(hMetaFilePict);
ro->poleobj->Release();
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;
fm.cfFormat = dwCF[i];
fm.ptd = NULL;
fm.dwAspect = DVASPECT_CONTENT;
fm.lindex = -1;
fm.tymed = dwTM[j];
STGMEDIUM stgm;
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();
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;
LPBYTE lpByte = (LPBYTE)pvRgnBaseAddress;
lpByte += 4096;
if((DWORD)lpByte < (DWORD)lpVtabl + 4)
bStridePage = TRUE;
PVOID pvRgnBaseAddress2 = (LPVOID)lpByte;
if(bStridePage)
if(!::VirtualProtectEx(hSelf, pvRgnBaseAddress2, 4,
PAGE_EXECUTE_READWRITE, &dwOldProtect2)) err;
DWORD dw;
memcpy((LPVOID)&dw, (LPVOID)(*lpVtabl), 4);
memcpy((LPVOID)(*lpVtabl), (LPVOID)(*lpVtabl + 1), 4);
memcpy((LPVOID)(*lpVtabl + 1), (LPVOID)&dw, 4);
DWORD dwFake;
::VirtualProtectEx(hSelf, pvRgnBaseAddress, 4, dwOldProtect1, &dwFake);
if(bStridePage)
::VirtualProtectEx(hSelf, pvRgnBaseAddress2, 4, dwOldProtect2,
&dwFake);
y = bb.method1();
y = bb.method2();
return;
}
In MSVC++, if a class self or base class(es) have a virtual function, its
first 4 bytes of class layout inside the memory is the pointer to Vtbl, followed
by the member variable and then the member function. Take the figure as an
example: pBB points to an instance of classB, goes to the memory window. The
first 4 bytes are "2C 50 42 00" (remember that Intel chips are little endian),
followed by "1F 00 00 00". It is the m we just assigned to 31 (0x1F), then "20
00 00 00", which is n we assigned 32 (0x20).
Now, go to virtual memory 0x 0042 502C, and you have the right-most memory
window. Umm, how do I say, from 0042502C to 00425038, it is classB's virtual
table area, starting form the first virtual method�method1, it is "28 10 40 00",
which is different from the base class�classA's method1 which lies in
0x0042503C. It makes sense; when you call a classB's method1, you enter its
method1. On the other hand, because classB does not implement method2, when you
call method2 in a classB instance, you enter classA's method2.
When you scroll the vertical scrollbar, you will see classC's vtbl lies in
higher consecutive memory (the left bottom memory window). By comparing the data
in this area carefully, I bet you are clear the layout of vtbl now.
Please note: Vtbl is always being put inside read-only pages together with
whatever const you declared in C++, and if you try to write to it, your program
will be terminated by system and a GP error box will pop up, which means you
must call VirtualProtectEx
to modify the page property to
PAGE_EXECUTE_READWRITE
before swapping the pointers of method1 and
method2 in classB's Vtbl.
Also note: There is no proof that the Vtbl of a class lies in a single page.
You must make sure all your write operations are conducted in areas you have
modified. In our code above, there are three virtual methods totally, so classA,
classB, classC (they are consecutive) vtbl each takes 3 * 4 byte, and that
explains why " if((DWORD)lpByte < (DWORD)lpVtabl + 4)
", see,
lpByte
points to classB's Vtbl, and I want to make sure classB's
Vtbl place is modified before we continue.
Now you may ask: "Hi, I changed classB's Vtbl, okay, then I expect '(y =
bb.method1()) == 22' and '(y = bb.method2()) == 21'. How come I still get 21 and
22? The answer is: The compiler has computed the function entry and hard-coded
it in the binary. In other words, when program starts, it didn't use Vtbl at all
because the compiler has decided which function to call in compile time. Sigh,
too smart compiler...
So what on earth we can do to make a program have a look at the Vtbl before
turning a member function? Component-based program. For, to a component, it has
no knowledge of what member function will be called in runtime, it must use Vtbl
to decide which member function to call. Okay, let's make such a scenario: (I do
not teach COM/ATL; you must have experience with this to continue.)
Create a COM ATL DLL project (you will find its source code in the Plus
folder in the accompanying demo), use everything default setting, need
proxy/stub bound, add a Simple Object, call it Sum, make it a Custom Interface
(you can use Dual but you have to modify offset later in the code), and add two
methods until you get following in your Sum.cpp:
STDMETHODIMP CSum::method1()
{
PopMsg(_T("method1"));
return S_OK;
}
STDMETHODIMP CSum::method2()
{
PopMsg(_T("method2"));
return S_OK;
}
Do not be concerned about PopMsg
; it just calls WinAPI
MessageBox
. Then, add the third method:
STDMETHODIMP CSum::RadarIt()
{
LPDWORD* lpVtabl = (LPDWORD*)this;
HANDLE hSelf = OpenProcess(PROCESS_ALL_ACCESS, FALSE,
::GetCurrentProcessId());
MEMORY_BASIC_INFORMATION mbi;
if(VirtualQueryEx(hSelf, (LPVOID)(*lpVtabl), &mbi, sizeof(mbi)
!= sizeof(mbi)) err;
PVOID pvRgnBaseAddress = mbi.BaseAddress;
DWORD dwOldProtect1, dwOldProtect2;
if(FALSE == ::VirtualProtectEx(hSelf, pvRgnBaseAddress, 4,
PAGE_EXECUTE_READWRITE, &dwOldProtect1)) err;
LPBYTE lpByte = (LPBYTE)pvRgnBaseAddress;
lpByte += 4096;
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;
DWORD dw;
memcpy((LPVOID)&dw, (LPVOID)(*lpVtabl + 3), 4);
memcy((LPVOID)(*lpVtabl + 3), (LPVOID)(*lpVtabl + 4), 4);
memcpy((LPVOID)(*lpVtabl + 4), (LPVOID)&dw, 4);
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();
pSum->RadarIt();
pSum->method1();
::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();
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. |