基础的键盘记录器
键盘记录器是所有渗透测试人员 / 红队人员的必备工具,本节将指导你制作通用的键盘记录器。 有时我们只想持续监控某个用户或获取其他凭据。 这可能是因为我们此时无法进行任何类型的横向移动或者权限提升,或者我们可能只想监视用户以便更好开展将来的入侵活动。 在这些情况下,我们通常喜欢放置一个持续在受害者系统上运行的键盘记录器并将键盘记录的数据发送到外网。 以下示例只是一个 POC
,本实验的目的是让你从这里了解基础知识和构建它们。 它全部用 C 语言(较底层的语言)来编写的原因是保持二进制文件相对较小、更好的系统控制、并且规避杀毒软件。 在之前的书中,我们使用 Python
编写了一个键盘记录器并使用 py2exe
对其进行编译以使其成为二进制文件,但这些很容易就被检测到。 让我们来看一个稍微复杂的例子。
设置你的环境
这是在 C 中编写和编译以生成 Windows 二进制文件并创建自定义键盘记录器所需的基本设置。
- 在一个虚拟机中安装 Windows 10
- 安装
Visual Studio
,以便你可以使用命令行编译器和使用 Vim 进行代码编辑
到目前为止,Windows API 编程的最佳学习资源是微软自己的开发者网络网站 MSDN。 MSDN
是一个非常宝贵的资源,它详细说明了系统调用,数据类型和结构定义,并包含了许多示例。通过阅读微软出版社出版的《Windows Internals》书籍,可以更深入地了解 Windows 操作系统, 虽然这个项目中并不是特别需要这个。 对于学习 C 语言,有一本好书,C 语言的创始人之一参与了对此书的撰写,名为《C语言程序设计》(The C Programming Language),书的作者是 Kernighan 和 Ritchie。最后,可以阅读《Beej’s Guide to Network Programming》,有印刷版和在线版,这是 C 语言中关于的 socket 编程的一本很好的入门读物。
从源码编译
在这些实验中,将会有多个示例代码和例子。实验室将使用微软的 Optimizing Compiler
编译代码,该编译器随 Visual Studio
社区版本一起提供,并内置于 Visual Studio
开发者命令提示符(Visual Studio Developer Command Prompt)中。安装 VS 社区版本后,请通过 工具(Tools) —> 获取工具和功能(Get Tools and Features) 安装 C++ 的组件 通用 Windows 平台开发和桌面开发。要编译示例源码,请打开开发者命令提示符的一个实例,然后切换到包含源代码文件的文件夹目录。 最后,运行命令 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
,这将设置 Microsoft 编译器编译 64 位的二进制文件而不是编译成 32 位的。现在,你可以通过运行 cl path/to/code.c
(path 是源码文件的绝对路径)来编译代码。
示例框架
该项目的目标是创建一个键盘记录器,利用 C 语言和底层 Windows 功能来监视击键。该键盘记录器使用 SetWindowsHookEx
和 LowLevelKeyboardProc
函数 。 SetWindowsHookEx
允许在本地和全局上下文中设置各种类型的 Hook(钩子)。在这种情况下,WH_KEYBOARD_LL
参数将用于提取底层键盘事件 。 SetWindowsHookEx
的函数原型看起来像这样( http://bit.ly/2qBEzsC ):
HHOOK WINAPI SetWindowsHookEx(
_In_ int idHook,
_In_ HOOKPROC lpfn,
_In_ HINSTANCE hMod,
_In_ DWORD dwThreadId
);
该函数创建了定义为整型的 hook 的 ID、指向函数的指针、句柄和线程 ID。前两个值是最重要的。你即将要安装的 hook 的 ID 数据类型是整型。Windows 会在功能页面上列出的可用 ID 。在我们的例子中,将使用 ID 13 或 WH_KEYBOARD_LL
。HOOKPROC
是一个指向回调函数的指针,每次被钩住的进程接收数据时都会调用该函数。这意味着每次按下一个键,都会调用 HOOKPROC
。这是用于将击键写入文件的函数。hMod
是包含 lpfn
指针指向的函数的 DLL 的句柄。此值将设置为 NULL,因为函数与 SetWindowsHookEx
在同一进程中使用。dwThreadId 将设置为0以将回调与桌面上的所有线程相关联。最后,该函数返回一个整数,该整数将用于验证 hook 是否已正确设置或以其他方式退出。
第二部分是回调函数。回调函数将为此键盘记录器完成一系列繁重的工作。此函数将处理接收击键,将其转换为 ASCII 字符以及记录所有文件操作。 LowLevelKeyBoardProc
( http://bit.ly/2HomCYQ )的原型如下所示:
LRESULT CALLBACK LowLevelKeyboardProc(
_In_ int nCode,
_In_ WPARAM wParam,
_In_ LPARAM lParam
);
让我们回顾一下 LowLevelKeyBoardProc
所需的内容。该函数的参数是一个整数,告诉 Windows 如何解释该消息。这些参数中的两个是:
- wParam,它是消息的标识符
lParam,它是指向
KBDLLHOOKSTRUCT
结构体的指针wParam
的值在功能页面中指定。还有一个页面描述了KBDLLHOOKSTRUCT
的成员。lParam
的KBDLLHOOKSTRUCT
的值称为vkCode
或Virtual-Key Code
( http://bit.ly/2EMAGpw )。这是按下的键的代码,而不是实际的字母,因为字母可能会根据键盘的语言而有所不同。vkCode
需要稍后转换为相应的字母。现在不要着急把参数传递给我们的键盘回调函数,因为它们将在激活 hook 时由操作系统传递。
最后,挂钩键盘的初始架构代码如下所示:https://github.com/cheetz/ceylogger/blob/master/skeleton
在查看框架代码时,需要注意的一些事项是,在回调函数中包含pragma comment
(预处理指令),消息循环和返回 CallNextHookEx
行。pragma comment
是用于链接 User32 DLL 的编译器指令。 此 DLL 包含将要进行的大多数函数调用,因此需要进行链接。它也可以与编译器选项相关联。接下来,如果正在使用 LowLevelKeyboardProc
函数,则必须使用消息循环。 MSDN 声明,“此钩子在安装它的线程的上下文中调用。 通过向安装了钩子的线程发送消息来进行调用。因此,安装钩子的线程必须有一个消息循环。“[ http://bit.ly/2HomCYQ ]
返回 CallNextHookEx
是因为 MSDN 声明 “ 调用 CallNextHookEx
函数链接到下一个钩子过程是可选的,但强烈建议调用,否则已安装钩子的其他应用程序将不会收到钩子通知,因此可能会出现错误行为。所以你应该调用 CallNextHookEx
,除非你一定要阻止其他应用程序看到通知。“[ http://bit.ly/2H0n68h ]
接下来,我们继续构建从文件句柄开始的回调函数的功能。在示例代码中,它将在 Windows 系统的 Temp 目录(C:\Windows\Temp)中创建名为 “log.txt” 的文件。 该文件配置了 append
参数,因为键盘记录器需要不断地将击键输出到文件。如果此临时文件夹中没有该文件,则会创建一个同名文件(log.txt)。
回到 KBDLLHOOKSTRUCT
结构体,代码声明了一个 KBDLLHOOKSTRUCT
指针,然后将其分配给 lParam
。这将允许访问每个按键的 lParam
内的参数。 然后代码检查 wParam
是否返回 WM_KEYDOWN
,这将检查按键是否被按下。这样做是因为钩子会在按下和释放按键时触发。如果代码没有检查 WM_KEYDOWN
,程序将每次写入两次击键。
检查按键被按下后,需要有一个 switch
语句,用于检查 lParam
的 vkCode
(虚拟键代码)是否有特殊键。某些键需要以不同的方式写入文件,例如返回键(Esc),控制键(Ctrl),移位(shfit),空格(Space)和制表键(Tab)。对于默认情况,代码需要将按键的 vkCode
转换为实际的字母。 执行此转换的简单方法是使用 ToAscii
函数。ToAscii
函数将包含 vkCode
,一个 ScanCode
,一个指向键盘状态数组的指针,一个指向将接收该字母的缓冲区的指针,以及一个 uFlags
的 int
值。vkCode
和 ScanCode
来自按键结构体,键盘状态是先前声明的字节数组,用于保存输出的缓冲区,uFlags
参数将设置为0。
检查是否释放了某些键是非常必要的,例如 shift
键。这可以通过编写另一个 if 语句
来检查 WM_KEYUP
,然后使用 switch语句
来检查所需的键来完成。 最后,需要关闭该文件并返回 CallNextHookEx
。回调函数如下所示:
此时,键盘记录器功能完全正常,但依旧有一些问题。第一个是运行程序会产生一个命令提示符的窗口,这使得程序运行非常明显,并且窗口没有任何提示输出是非常可疑的。 另一个问题是将文件放在运行该键盘记录器的同一台计算机上并不是很好。
通过使用 Windows 特定的 WinMain
函数入口替换标准 C 语言的 Main
函数入口,可以相对容易地修复有命令提示符窗口的问题。根据我的理解,之所以有效是因为 WinMain
是 Windows 上图形程序的入口。尽管操作系统期望你为程序创建窗口,但我们可以命令它不要创建任何窗口,因为我们有这个控件。最终,该程序只是在后台生成一个进程而不创建任何窗口。
该程序的网络编程是简单粗暴的。首先通过声明 WSAData
( http://bit.ly/2HAiVN7 ),启动 winsock
,清除提示结构体以及填写相关需求来初始化 Windows socket
函数。就我们的示例来说,代码将使用 AF_UNSPEC
用于 IPV4
和 SOC_STREAM
用于 TCP 连接,并使用 getaddrinfo
函数使用先前的需求填充 c2 结构体。在满足所有必需参数后,可以创建 socket
。最后,通过 socket_connect
函数连接到 socket
。
连接之后,socket_sendfile
函数将完成大部分工作。它使用 Windows 的 CreateFile
函数打开日志文件的句柄,然后使用 GetFileSizeEx
函数获取文件大小。获得文件大小后,代码将分配该大小的缓冲区,加上一个用于填充的缓冲区,然后将该文件读入该缓冲区。 最后,我们通过 socket
发送缓冲区的内容。
对于服务器端,可以在 C2 服务器的3490 端口上的启动 socat 侦听器,命令启动:
socat:socat - TCP4-LISTEN:3490,fork
一旦启动监听器并且键盘记录器正在运行,你应该看到来自受害者主机的所有命令每 10 分钟被推送到你的 C2 服务器。 可以在此处找到键盘记录器的初始完整版本1:
https://github.com/cheetz/ceylogger/tree/master/version1 。
在编译 version_1.c 之前,请确保将 getaddrinfo 修改为当前的 C2 的IP 地址。编译代码:
cl version_1.c io.c
应该提到的最后一个函数是 thread_func
函数。thread_func
调用函数 get_time
来获取当前系统的分钟。然后检查该值是否可被 5 整除,因为该工具每隔 5 分钟发送一次键盘记录文件。如果它可以被 5 整除,它会设置 socket
并尝试连接到 C2 服务器。如果连接成功,它将发送文件并运行清理本地文件。然后循环休眠 59 秒。需要睡眠功能的原因是因为这一切都在一个固定的循环中运行,这意味着该函数将在几秒钟内获取时间,初始化连接,完成连接和发送文件。如果没有 59 秒的休眠时间,该程序最终可能会在 1 分钟的时间间隔内发送文件几十次。休眠功能允许循环等待足够长的时间以便切换到下一分钟,因此每 5 分钟仅发送一次文件。
混淆
有数百种不同的方法来执行混淆。虽然本章不能全部一一讲述,但我想为你提供一些基本的技巧和思路来解决绕过杀毒软件的问题。
你可能已经知道,杀毒软件会查找特定的字符串。可用于绕过杀毒软件的最简单方法之一是创建一个简单的旋转密码并移动字符串的字符。在下面的代码中,有一个解密函数,可以将所有字符串移动 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;
}
你可以在此处的程序版本2中看到此示例:https://github.com/cheetz/ceylogger/tree/master/version2 。
另一种可以用来逃避杀毒软件的方法是使用函数指针调用 User32.dll 中的函数,而不是直接调用函数。为此,首先编写一个函数定义,然后使用 Windows 系统的 GetProcAddress
函数找到要调用的函数的地址,最后将函数定义指针指定给从 GetProcAddress
接收的地址。可以在此处找到如何使用函数指针调用 SetWindowsHookEx
函数的示例:https://github.com/cheetz/ceylogger/blob/master/version3/version_3.c#L197-L241 。
该程序的第 3 版本将前一个示例中的字符串加密与使用指针调用函数的方法相结合。有趣的是,如果你将已编译的二进制文件提交到 VirusTotal(病毒检测网站),你将不再在导入部分中看到 User32.dll。在下面的图片中,左侧图像是版本1的检测结果,右侧图像是带有调用指针的版本3的检测结果 。
你可以在以下网址找到版本3的完整源代码:https://github.com/cheetz/ceylogger/tree/master/version3 。
为了了解你是否已成功避开杀毒软件,最佳选择是始终针对实时杀毒软件系统进行测试。在真实世界的入侵活动中,我不建议使用 VirusTotal 网站,因为你的病毒样本可能会发送给不同的杀毒软件厂商。然而,它非常适合测试或者学习。对于我们的 payload,以下是 VirusTotal 比较:
对于版本1,32位,21/66(21家检测出),触发杀毒软件:
- https://www.virustotal.com/#/file/4f7e3e32f50171fa527cd1e53d33cc08ab85e7a945cf0c0fcc978ea62a44a62d/detection
- http://bit.ly/2IXfuQh
对于版本3,32位,14/69(14家检测出),触发杀毒软件:
- https://www.virustotal.com/#/file/8032c4fe2a59571daa83b6e2db09ff2eba66fd299633b173b6e372fe762255b7/detection
- http://bit.ly/2IYyM7F
最后,如果我们将版本3编译为 64 位的 payload,我们得到 1/69(仅仅一家检测出)!:
- https://www.virustotal.com/#/file/e13d0e84fa8320e310537c7fdc4619170bfdb20214baaee13daad90a175c13c0/detection
- http://bit.ly/2JNcBmc
译者注:根据上面的链接,译者点进去看了每一个数据,但是发现数据都有所更新,所以把上面的内容修改为和链接到的内容一致的最新数据。但是下面的图片是书上的原图,所以是老的过期的数据。下面的图片是说,将版本3编译为 64 位的 payload,得到 0/66(无一家一家检测出)的结果,但是现在已经是 1/69,也就是 69 个杀软种有一个可以检测出病毒。
实验:
下一步我们还可以做什么呢?有无限种可能!可以做一些小的优化比如对 log.txt 内容进行模糊/加密,或者在程序启动后启动加密套接字,然后将获得击键权限直接写入该套接字。在接收方,服务器将重建数据流并将其写入文件。 这将阻止日志数据以纯文本形式显示,就像当前一样,并且还可以防止在硬盘中留下更多的文件痕迹。
如果你想做一些大的改进,那么你可以将可执行文件转换为 DLL,然后将 DLL 注入正在运行的进程。这样可以防止进程信息显示在任务管理器中。虽然有一些程序可以显示系统中所有当前加载的 DLL,但注入 DLL 会更加隐蔽。此外,有些程序可以反射性地从内存加载 DLL 而根本不在磁盘中留下痕迹(无文件),从而进一步降低了被取证的风险。