介绍
EDR( 端点检测与响应)是一种安全产品,旨在检测计算机或服务器上正在执行的异常活动。当我在互联网上查找有关 EDR 工作原理的资源时,我意识到,尽管关于 EDR 的文献很多, 但没有多少文章解释 EDR 的架构以及 EDR 的不同组件是如何协调的。本文旨在揭开 EDR 工作原理的神秘面纱,构建一个自定义的 EDR,该自定义 EDR 将实现真实 EDR 使用的几种技术。
首先,我们将了解反病毒软件的历史,了解它们的工作原理以及为什么依赖内核驱动程序,然后我们将了解如何创建自定义内核驱动程序,最后如何将其转变为几乎完全正常工作的 EDR。
病毒史
如果我们回顾一下计算机病毒和蠕虫的时间表, 就会发现“蠕虫”一词最初是由约翰·冯·诺依曼在 1966 年发表的一篇名为“自我复制自动机理论”的文章中使用的。在这篇文章中,诺依曼表明,从理论上讲,可以设计一个程序使其能够自我复制。由于这项工作,诺依曼被认为是计算机病毒学的理论之父。
第一个可以工作的病毒叫做“The Creeper”,由 Bob Thomas 创建。该蠕虫是第一个已知的蠕虫,因为它能够通过网络 进行复制,将自身复制到远程系统。它只打印了一条消息“我是 CREEPER。有本事就来抓我”:
知道可以创建此类程序后,聪明人开始研究能够清除这些恶意软件的安全产品。例如“Reaper”,其唯一目的是通过在 网络上移动来从受感染的主机中删除 Creeper。所以是的,Reaper 本身就是蠕虫,一种好蠕虫……这是第一个防病毒软件,但在 20 世纪 80 年代后期出现了更多防病毒软件,它们都瞄准同一个目标:保护计算机免受恶意软件的侵害。
防病毒软件如何保护计算机?
早在 90 年代,防病毒产品就能够通过两种方式检测病毒:
-
通过简单的启发式分析:
-
二进制文件的名称是什么?
-
是否签名
-
它的元数据是什么(字符串、注释……)
-
通过为每个二进制文件计算的签名:
当将二进制文件放入磁盘时,反病毒软件会检查其签名是否已知且是否被归类为恶意文件。如果是,则隔离或删除该二进制文件。
显然,这还不够,因为所有这些检测方法都是基于攻击者可以操纵的信息。如果您要阻止名为 mimikatz.exe 的二进制文件,我会将其重命名为 notmimikatz.exe。如果您要阻止包含特定字符串的二进制文件,我会将其删除!如果您要标记二进制文件的签名,我会更改二进制文件中的一个字节,这样我们就可以开始了。静态分析还不够。
为了以更复杂的方式检测病毒,必须能够动态分析系统,并特别注意:
-
正在创建的流程
-
正在加载库
-
正在修改的文件
-
调用的函数及其所采用的参数
如果我们看一下操作系统的架构,我们就会发现它们依赖于两个空间:
用户空间是进程所在的位置,是操作 word 文件的地方,是你在微信上呼叫朋友的地方。每个在用户空间中运行的进程都有自己的执行环境,这意味着如果 微信崩溃,word 仍会工作。另一边是内核空间,操作系统的核心以及服务和驱动程序都在其中运行。由于内核空间是内核运行的地方,因此它包含相当多的有趣信息,存储在结构中,供任何想要监视系统的人使用。但是,正如您可能猜到的那样,用户空间程序不可能直接访问这些信息,因为用户空间和内核空间彼此隔离:
直接访问这些特定结构的唯一方法是在内核空间中运行代码,而最简单的方法是通过内核驱动程序。
最受攻击的结构之一是 SSDT( 服务系统调度表 )。要理解原因,我们需要了解当您尝试打开文件时操作系统会做什么。作为用户,打开文件没有什么特别的,只需双击文件,程序(比如记事本或 Word)就会为您打开文件。然而,为了完成这样的任务,操作系统必须经过相当多的步骤,这些步骤由以下模式描述:
如您所见,用户应用程序主要依赖于 WinAPI,它由 Microsoft 记录的一组开发人员友好的函数组成,并由多个 DLL(例如 kernel32.dll、user.dll 或 advapi.dll)公开。因此,打开文件的第一步是使用 kernel32.dll 公开的 CreateFileA 函数,其原型如下:
C++ |
它的用法有完整的文档说明,并且该函数非常易于使用,您需要做的就是指定要打开的文件的路径以及所需的访问权限(读取、写入或附加)。查看 CreateFileA 函数的执行流程,我们会看到,最终它将调用另一个函数 NtCreateFile,该函数由 NTDLL.dll 公开,其原型如下:
C++ |
如您所见,NtCreateFile 函数的原型比 CreateFileA 函数的原型复杂得多。原因是 NTDLL.dll 实际上是内核本身公开的函数的用户模式反映。因此,NTDLL.dll 将添加一些内核执行打开文件任务所需的其他参数,但这些参数不受开发人员管理。
一旦设置了所有这些参数,程序就必须请求内核打开文件。这意味着程序必须调用内核本身公开的 NtCreateFile 函数。在本文开头,我提到用户空间进程不能直接访问内核空间,这是真的!但是它们可以请求内核执行特定任务。要请求此类操作,您需要触发一种称为system call(系统调用)的特定机制。
查看 NTDLL.dll 函数的 NtCreateFile 反汇编代码,我们可以看到以下内容:
有两件事很重要。
第一个是第二行:这一行将值 55 移到 EAX 寄存器中。这个值 55 称为 system call number(系统调用号) 。NTDLL.dll 中的每个函数都链接到一个特定的系统调用号,该系统调用号在 Windows 操作系统的不同版本之间有所不同。
第二个重要的代码是 syscall 指令本身:该指令将告诉 CPU 从用户空间切换到内核空间,然后跳转到内核中 NtCreateFile 函数所在的内核地址。问题是,CPU 不知道 NtCreateFile 函数位于何处。为了找到该函数的地址,它需要存储在 EAX 寄存器中的系统调用号和 SSDT。为什么是 SSDT ?因为这个结构是一个索引,它包含系统调用号列表以及内核中该函数对应的十六进制地址的位置:
功能 |
系统调用号 |
内核地址指针 |
创建文件 |
55 |
0x5ea54623 |
创建IR定时器 |
AB |
0x6bcd1576 |
… |
… |
… |
因此,当 CPU 触发系统调用时,它会在此结构中查找系统调用号 55,并跳转到与此系统调用号关联的地址。以下架构总结了在 Windows 操作系统上打开文件的整个过程:
一旦内核收到请求,它将请求驱动程序(在我们的例子中是硬盘驱动程序)读取存储在硬盘上的文件的内容,最终让记事本将其内容打印回给您。
回顾 SSDT,似乎只要修改内核函数的地址,基本上就可以将代码流重定向到任何你想要的地方。出于这个原因,安全工具编辑者开始修补 SSDT,以便将调用重定向到他们自己的驱动程序,这样他们就可以分析调用了哪些函数以及发送了什么:
这样,通过他们自己的驱动程序,防御者就能够分析系统调用并确定它是合法的还是恶意的。
如果操纵 SSDT 结构非常简单,那么操纵其他一些结构可能是一项危险的任务。在内核空间中,如果您运行的代码有错误,整个内核可能会崩溃。此外,如果代码包含逻辑或基于内存的漏洞(例如堆栈溢出),攻击者可以利用它们直接在内核空间中运行代码(作为系统上权限最高的用户)。最后,如果防御者能够使用内核驱动程序访问内核并修改其行为,那么使用 rootkit 的攻击者也可以做到。
为了保护其操作系统(免受侵入型反病毒编辑器和攻击者的侵害),微软创建了 KPP( 内核补丁保护 ) , 通常称为 PatchGuard,并在 Windows XP/2003 上发布了它。
PatchGuard 是一种主动安全机制,可定期检查多个关键 Windows 内核结构的状态。如果其中一个结构被除合法内核代码以外的任何内容修改,则 PatchGuard 会发出致命系统错误(称为“错误检查”),这将启动计算机重新启动:
因此,PatchGuard 阻止了内核本身以外的其他组件对关键内核结构的修改。随着 PatchGuard 的发布,反病毒软件不再需要从内核中挂钩 SSDT 或任何关键结构:
显然,安全工具开发者们疯了,因为它基本上禁用了他们所有的工具,有些人甚至试图起诉微软。
为了解决这个问题,让安全产品能够重新监控系统,微软在操作系统中增加了一些新功能,这些功能依赖于一种名为 callback object(回调对象) 的机制。下面是微软给出的回调对象的定义:
基本上,这些函数允许内核驱动程序在每次处理特定操作时收到内核通知。因此,它允许 EDR 动态监视系统上正在发生的事情。
这种机制是我们要在 EDR 中实现的第一个机制,但首先我们需要一个内核驱动程序,因此我们需要更好地理解什么是驱动程序以及如何开发驱动程序。
什么是驱动?
驱动程序被定义为向硬件设备提供软件接口的组件。一个典型的驱动程序示例是键盘驱动程序,它将从键盘输入接收到的电信号转换为将打印在屏幕上的字符:
系统上使用了很多不同的驱动程序,例如蓝牙驱动程序、键盘驱动程序、鼠标驱动程序,甚至负责将电信号转换为系统可以理解的网络数据包的网络输入/输出驱动程序。
如果您想查看系统上运行的驱动程序,可以使用 SysInternals 工具包中的 WinObj.exe 工具 :
此外,如果您想查看驱动程序的代码是什么样子,Microsoft 在其Github 存储库(https://github.com/microsoft/Windows-driver-samples)中提供了大量驱动程序示例 。您会发现开发驱动程序非常复杂。如前所述,最小的内存错误都会使驱动程序崩溃,从而导致内核崩溃。因此,Microsoft 提供了一些框架,使内核驱动程序开发更容易。
主框架称为 WDF( Windows Driver Framework ),由两个不同的子框架组成:
-
KMDF ( 内核模式框架 )
-
UMDF ( 用户模式框架 )
这两种驱动因素都有其优点和缺点:
框架 |
优点 |
缺点 |
KMDF |
授予对内核的完全访问权限 |
难以开发 |
UMDF |
易于使用 |
允许访问有限的功能(WinAPI) |
因此,在开始开发驱动程序之前,您必须确定您的需求是什么以及您的驱动程序将用于什么用途。遗憾的是,在我们的例子中,我们需要开发一个内核驱动程序 (KMDF),因为我们将使用内核函数,而要开发驱动程序,我们需要一个开发环境!
设置开发环境
首先,我们需要安装 Visual Studio 和 Windows 驱动程序工具包。不幸的是,这是一个有点痛苦的过程,并且取决于你运行的 Windows 版本。在撰写本文时,对于 Windows 10,你将需要带有 WDK (https://docs.microsoft.com/en-us/windows-hardware/drivers/download-the-wdk)的 Visual Studio 2022。接下来,我们必须使用 Visual Studio 安装程序安装一个额外的 Spectre 库:
或者,如果您不关心 Spectre 缓解措施(这对于此测试可能并不重要),或者在 Visual Studio 中获取正确版本时遇到问题,那么您可以在项目属性中禁用它。
接下来,为了准备加载我们自己的驱动程序,我们将禁用驱动程序签名检查。在提升的命令行提示符中,键入以下命令:
Shell |
我们需要这样做的原因是,自 Windows 10 版本 1507 以来,无法加载未经 Microsoft 签名的驱动程序以防止 rootkit 攻击。这些命令只是禁用签名检查并启用调试模式,这将允许我们加载驱动程序并使用 WinDbg 对其进行调试。最后,我们需要启用内核消息到调试器的输出。为此,我们必须添加以下键:
Plaintext |
值为 0xf:
现在重启电脑。打开 Visual Studio 并创建一个新项目“Kernel Mode Driver, EDR”:
一旦创建,您应该获得以下项目结构:
创建一个新的源文件,将其命名为“driver.c”并添加以下内容(稍后我会回顾它的作用):
C++ |
在项目属性中,转到“链接器>命令行”并添加以下选项以禁用完整性检查:
此时环境已准备好构建驱动程序。编译项目并在管理命令行中启动以下命令(当然,根据需要调整路径和名称):
Shell |
以下是您将在命令行上收到的输出:
如果你已经打开了 dbgview,你应该会看到你的驱动程序成功执行:
很好!现在驱动程序已运行,让我们来看看基本的 Windows 内核驱动程序的内容!
原文始发于微信公众号(暴暴的皮卡丘):从windows驱动到构建EDR
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论