7.2 构建键盘记录器
键盘记录器是任何渗透测试人员/红队必备的工具,本节将指导您制作一个通用键盘记录器。有时我们只想持续监控某个用户或获取凭证。这可能是因为无法获得任何类型的横向渗透/权限提升,或者我们可能只想监视用户,为后续的行动做准备。在这样的情况下,我们喜欢在被攻击者系统上放置并运行键盘记录器,并将他们的键击记录发送出去。以下的例子只是一个原型系统,本实验的目的是让您了解原理和构建方法。使用 C 语言的原因是生成的二进制文件相对较小,并且由于是底层语言,可以更好地访问操作系统,并且可以规避杀毒软件检测。在本书第 2 版中,我们使用 Python 语言编写了键盘记录器,并使用 py2exe 编译成二进制文件,但是生成的文件容易被检测到。下面让我们来看一个稍微复杂的例子。
7.2.1 设置您的环境
下面是使用 C 语言编写和编译,生成 Windows 二进制文件,并创建自定义键盘记录器所需的基本设置。
- 虚拟机中的 Windows 10。
- 安装 Visual Studio,使用命令行编译器,使用 Vim 代码编辑。
到目前为止,Windows API 编程的推荐资源是微软公司的 MSDN 网站。MSDN 是一个非常宝贵的资源网站,详细说明了系统调用、类型和结构定义,并包含了许多示例。虽然这个项目并不一定需要这些资源,但通过阅读微软公司出版的 Windows Internals 一书,可以更深入地了解 Windows 操作系统。对于 C 语言,则可以参考《C 语言设计语言》一书,作者是 Kernighan 和 Ritchie。另外,也可以阅读 Beej 的《网络编程指南》(有印刷版和在线版),这是 C 语言网络编程的一本很好的入门读物。
7.2.2 从源代码开始编译
在下面这些实验中,将会有多个代码示例。本实验室将使用微软公司的 Optimizing Compiler 工具编译代码,该编译器随 Visual Studio Community 一起提供,并内置在 Visual Studio Developer 命令提示符中。在安装 Visual Studio Community 工具后,需确保安装通用 Windows 平台开发环境和工具,配置工具和 C++桌面开发环境。如果编译示例,那么需打开开发人员命令提示符,然后导航到包含源文件的文件夹。最后,运行命令“cl sourcefile.c io.c”。这将生成一个与源文件同名的可执行文件。
编译器默认编译 32 位应用程序,但此代码也可以编译成 64 位应用程序。要编译成 64 位应用程序,需运行位于 Visual Studio 文件夹中的批处理脚本。在命令提示符中,导航到“C:\Program Files(x86)\Microsoft Visual Studio\2017\Community\VC\Auxiliary\Build”,需要注意此路径可能会有所不同,具体取决于您的 Visual Studio 版本。然后,运行命令“vcvarsall.bat x86_amd64”,这将设置微软编译器编译 64 位可执行文件而不是 32 位可执行文件。现在,您可以通过运行“cl path/to/code.c”编译代码。
7.2.3 示例框架
该项目的目标是创建一个使用 C 语言和底层 Windows 功能来监视击键的键盘记录器。该键盘记录器使用 SetWindowsHookEx 和 LowLevelKeyboardProc 函数。SetWindowsHookEx 允许在本地和全局上下文中设置各种类型的钩子。在这种情况下,WH_KEYBOARD_LL 参数用于获取底层键盘事件。SetWindowsHookEx 的函数原型如下所示。
HHOOK WINAPI SetWindowsHookEx(
_In_ int idHook,
_In_ HOOKPROC lpfn,
_In_ HINSTANCE hMod,
_In_ DWORD dwThreadId
);
SetWindowsHookEx 函数采用整数表示钩子 ID、指向函数的指针、句柄模块和线程 ID,前两个值很重要。钩子 ID 是安装的钩子类型的整数标识。Windows 功能页面上列出可用 ID。在我们的例子中,使用 ID 13 或 WH_KEYBOARD_LL。HOOKPROC 是一个指向回调函数的指针,每次挂了钩子的进程接收数据都会调用该函数。这意味着每次按下一个键,都会调用 HOOKPROC。这个函数用于将键值写入文件。hMod 是 DLL 的句柄,包含 lpfn 指向的函数。此值将设置为 NULL,因为函数与 SetWindowsHookEx 在同一进程中使用。dwThreadId 设置为 0,将与桌面应用程序的所有线程回调相关联。最后,该函数返回一个整数,该整数将用于验证钩子是否设置正确,如果设置不正确则退出。
第二部分是回调函数。回调函数实现程序大量的功能。此函数接收处理按键信息,将其转换为 ASCII 字母以及所有文件操作。LowLevelKeyboardProc 的原型如下所示。
LRESULT CALLBACK LowLevelKeyboardProc(
_In_ int nCode,
_In_ WPARAM wParam,
_In_ LPARAM lParam
);
让我们回顾一下 LowLevelKeyboardProc 参数的内容。该函数的第一个参数是一个整数,告诉 Windows 如何解释该消息。其中两个参数是,①wParam,消息的标识符;②lParam,它指向 KBDLLHOOKSTRUCT 结构的指针。wParam 的值需在函数参数中指定。参数 lParam 指向 KBDLLHOOKSTRUCT 成员。lParam KBDLLHOOKSTRUCT 的值是 vkCode 或虚拟键盘码。这是按下键的代码,而不是实际的字母,因为字母可能会根据键盘语言的不同而有所不同。vkCode 需要随后转换为相应的字母。现在,不需要担心参数传递给键盘回调函数,因为钩子激活后,操作系统自动传递参数。
在查看框架代码时,需要注意的事项是,在回调函数中,包含 pragma 注释行、消息循环和返回 CallNextHookEx 行。pragma 注释行是用于链接 User32 DLL 的编译器指令。此 DLL 包含程序所需的大多数函数调用,因此需要进行链接。它也可以与编译器选项相关联。接下来,如果需要使用 LowLevelKeyboardProc 函数,则必须使用消息循环。MSDN 声明:“此钩子在安装它的线程的上下文中调用。通过向安装了钩子的线程发送消息来进行调用。因此,安装钩子的线程必须有一个消息循环。”
返回 CallNextHookEx 是因为 MSDN 的声明:“调用 CallNextHookEx 函数链接到下一个挂钩过程是可选的,但是强烈推荐使用;否则,已安装挂钩的其他应用程序将不会收到挂钩通知,因此可能会出现错误行为。您应该调用 CallNextHookEx,除非您需要阻止其他应用程序看到通知。”
接下来,我们继续构建回调函数,从文件句柄开始。在示例代码中,它在 Windows Temp 目录(C:\Windows\Temp)创建名为“log.txt”的文件。该文件配置了 append 参数,因为键盘记录器需要不断地将按键记录输出到文件。如果 temp 中不存在该文件,则将创建一个文件。
回到 KBDLLHOOKSTRUCT,代码声明了一个 KBDLLHOOKSTRUCT 指针,然后将其分配给 lParam。这将允许访问每个按键的 lParam 内的参数。然后代码检查 wParam 是否返回“WM_KEYDOWN”,即检查按键是否被按下。这样做是因为钩子会在按下和释放按键时触发。如果代码没有检查 WM_KEYDOWN 事件,那么程序将每次写入两次按键操作。
发现按键操作后,需要一个 switch 语句,检查 lParam 的 vkCode(虚拟键码),获取按键值。某些键需要以其他方式写入文件,例如 return、control、Shift、Space 和 Tab 键。对于默认情况,代码需要将按键的虚拟键码转换为实际的字母。执行此转换的简单方法是使用 ToAscii 函数。ToAscii 输入参数 vkCode、一个 ScanCode、一个指向键盘状态数组指针、指向接收字母缓冲区的指针,以及 uFlags 的整数值。vkCode 和 ScanCode 来自键结构,键盘状态是先前声明的字节数组,用于保存输出的缓冲区,uFlags 参数设置为 0。
必须检查是否释放了某些键,例如 Shift 键。这可以通过编写另一个“if 语句”来检查“WM_KEYUP”,然后使用“switch 语句”来检查所需的按键。最后,需要关闭该文件并返回 CallNextHookEx。
此时,键盘记录器完全正常工作。但是,有两个问题。一个问题是运行程序会产生一个命令提示符,这表明程序正在运行,并且缺少输出的内容,容易让人产生怀疑。另一个问题是运行键盘记录器获得文件仅放在本地计算机上,意义不是很大。
命令提示问题的修复相对容易,具体做法是修改标准 C“Main”函数入口点为 Windows 特定的 WinMain 函数入口。根据我的理解,这样做很有效的原因是 WinMain 是 Windows 上图形程序的入口点。虽然操作系统预期是创建程序窗口,但我们可以告诉操作系统不创建任何窗口,因为有这个控件。现在,该程序只是在后台生成一个进程,不创建任何窗口。
该程序网络方面的问题可以更加直接地进行解决。首先通过声明 WSAData,启动 Winsock,清除提示结构以及填充相关参数,初始化 Windows 套接字函数。举个例子,代码将 AF_UNSPEC 用于 IPv4,SOC_STREAM 用于 TCP 连接,使用 getaddrinfo 函数填充命令和控制数据结构。在填写所有必需的参数后,可以创建套接字。最后,使用 socket_connect 函数创建套接字。
连接之后,socket_sendfile 函数将执行大部分操作。它使用 Windows“CreateFile”函数打开日志文件的句柄,然后使用“GetFileSizeEx”函数获取文件大小。一旦获取了文件大小,代码将分配一个文件大小的缓冲区,加上一个用于填充的缓冲区,然后将文件读入该缓冲区。最后,我们通过套接字发送缓冲区的内容。
对于服务器端,在命令和控制服务器上启动 socat 监听 3490 端口(启动 socat 命令:socat - TCP4-LISTEN:3490,fork)。一旦监听器启动并且键盘记录器正常运行,您就会看到被攻击者主机的所有命令,并且每 10 min 被推送到命令和控制服务器。在编译 version_1.c 之前,确保将 getaddrinfo 修改为当前的命令和控制服务器的 IP 地址。编译代码:cl version_1.c io.c。
需要介绍的最后一个函数是 thread_func 函数。thread_func 调用函数 get_time,获取当前时间。然后检查该值是否可被 5 整除,因为该工具每 5min 发送一次文件。如果它可以被 5 整除,那么它会设置套接字并尝试连接命令和控制服务器。如果连接成功,那么它将发送文件并运行清理功能。然后,循环休眠 59 s。需要休眠功能的原因是这一切都在一个稳定的循环中运行,这意味着该函数将在几秒钟内运行,建立连接,连接和发送文件。如果没有 59s 的休眠时间,那么该函数最终可能会在 1 min 的间隔内发送文件数 10 次。休眠函数允许循环等待足够长的时间,切换到下一分钟,因此仅每 5 min 发送一次文件。
7.2.4 混淆
有数百种不同的方法来执行混淆。虽然本章不能全部涉及,但我想为您介绍一些基本的技巧和思路来规避杀毒软件。
您可能已经知道,杀毒软件会查找特定的字符串。规避杀毒软件的一种简单方法是创建一个简单的转盘密码,移动字符串的字符。在下面的代码中,有一个基本的解密函数,可以将所有字符串移动 6 个字符(ROT6)。这会导致杀毒软件可能无法检测到乱码。在程序开始时,代码将调用解密函数,获取字符串数组,返回到常规格式。解密函数如下所示。
int decrypt(const char* string, char result[]){
int key = 6;
int len = strlen(string);
for(int n = 0; n < len; n++){
int symbol = string[n];
int e_symbol = symbol - key;
result[n] = e_symbol;
}
result[len] = '\0';
return 0;
}
另一种规避杀毒软件的方法是使用函数指针调用 User32.dll 中的函数,而不是直接调用函数。为此,首先编写函数定义,然后使用 Windows GetProcAddress 函数找到要调用的函数的地址,最后,将函数定义指针指定给从 GetProcAddress 接收的地址。可以在 CitHub 找到如何使用函数指针调用 SetWindowsHookEx 函数的示例。
该程序的第 3 个版本将前一个示例中的字符串加密与使用指针调用函数的方法相结合。有趣的是,如果您将已编译的二进制文件提交到 VirusTotal,那么看不到 User32.dll。在图 7.1 中,左侧图像显示的是版本 1,右侧图像显示的是带有指针调用的版本 3。
图 7.1
为了查看您是否已成功规避杀毒软件,最好的选择是始终在实际运行的杀毒软件系统中进行测试。在实际的行动中,我不建议使用 VirusTotal,因为您的样本可能会被发送给不同的安全厂商。但是 VirusTotal 网站非常适合测试/学习。
实验
您的最终目标是什么?想法是无限的!一点点修复可能是对 log.txt 内容进行混淆/加密,或者在程序启动后,启动加密套接字,然后将按键内容写入套接字。在接收方,服务器将重建流,写入文件。这将阻止日志数据以纯文本形式显示,就像当前一样,并且可以防止之前的内容写入硬盘。
另一个非常明显的改进是将可执行文件转换为 DLL,然后将 DLL 注入正在运行的进程,使得进程不会显示在任务管理器中。有一些程序可以显示系统上所有当前加载的 DLL,因此注入 DLL 会更加隐蔽。此外,有些程序可以反射性地从内存加载 DLL 而根本不写入磁盘,从而进一步降低了被取证的风险。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论