一个CobaltStrike工具包,将Beacon产生的文件写入内存而不是磁盘

作者:Sec-Labs | 发布时间:

项目地址

https://github.com/Octoberfest7/MemFiles

MemFiles

免责声明:

这个项目很复杂,如果不了解它的工作原理并对其进行充分测试,可能会导致 Beacons 崩溃并失去访问权限!

我强烈建议您阅读所有文档,直到“技术细节、设计注意事项和评论”部分!

介绍

MemFiles 是 CobaltStrike 的一个工具包,它使 Operator 能够将 Beacon 进程生成的文件写入内存,而不是将它们写入目标系统上的磁盘。已在Windows 7、10、11上成功测试;相应的服务器版本应该可以正常工作。MemFiles 仅限于 x64 信标。

它通过在 NTDLL.dll 中挂钩几个不同的 NtAPI 并将对这些 API 的调用重定向到已注入 Beacon 进程内存空间的函数来实现这一点。

MemFiles 在 Beacon 进程中假定一个干净/未挂钩的 NTDLL 副本。在 EDR 挂钩仍然存在的 Beacon 进程中,无法保证 MemFiles 的可行性。在使用 MemFiles 之前修复/刷新 NTDLL!

MemFiles 工具包中定义了一个“特殊的”、不存在的目录;任何写入此特殊目录的文件都将被 MemFiles 捕获并写入内存,然后可以将它们下载到 Teamserver。

MemFiles 与在 Beacon 进程中运行的大多数(不是全部)工具兼容,并且可以指示这些工具将其输出写入特定目录。它不需要提升权限即可工作。

这包括:
-BOF 的
-.NET 程序集使用类似inline-executeAssembly 的
方式内联运行 -PE 使用类似Inline-Execute-PE 的方式内联运行

所有这些都是兼容的,因为它们在 Beacon 进程中运行,相关的 NtAPI 已被挂接。

MemFiles 不适用于以下内容:
-execute-assembly
-shell
-run

这些都不兼容,因为它们都会产生其他 NtAPI 尚未被挂钩的进程。

MemFiles 在 Beacon 进程中运行时,已成功使用 Rubeus、SharpHound、Procdump 和 Powershell 等工具进行了测试。

9acc5d324a210651

 

设置

克隆存储库并可选择更改在 /PIC/Source/NtCreateFile.c 和 /PIC/Source/NtOpenFile.c 中第 56 行定义的 hookdir 变量。这个变量是“特殊”目录,它向 MemFiles 发出信号,它应该拦截正在创建的文件。hookdir 变量默认设置为“redteam”。确保这个变量不是目标系统上的真实目录,并且在两个文件中是相同的!

 

ad1a1b7183210714

运行“make all”以编译必要的 BOF 和 PIC 函数。

将 MemFiles.cna 加载到 CobaltStrike 客户端中。确保运行 CobaltStrike 的目录对您的用户是可写的;MemFiles 在那里创建一个文本文件 (memfiles.txt) 以确保 MemFiles 运行所需数据的可用性。

MemFiles 可以配置为安装在每个调用 Teamserver 的新 Beacon 中;这是通过使用 MemFiles->Config 菜单项来完成的。默认情况下,MemFiles 不会自动安装在新的 Beacon 中。 请注意,这是一个全局设置;如果两个客户端连接到 Teamserver 并且都加载了 MemFiles.cna,如果客户端 A 切换“在信标初始安装”设置,则更改也将对客户端 B 生效!

 

ad1a1b7183210725

命令

MemFiles 由 4 个运行 BOF 的面向目标的命令和 1 个操纵项目数据结构的内部命令组成。

面向目标:

  1. 内存
  2. 记忆列表
  3. 取内存
  4. 内存清理

内部数据结构:

  1. 内存表

内存

meminit 负责在 Beacon 进程中安装 MemFiles。

MemFiles 所挂接的 NtAPI 列表如下:

  1. NtCreateFile
  2. 写入文件
  3. 关闭
  4. NtQueryVolumeInformationFile
  5. NtQueryInformationFile
  6. NtSetInformationFile
  7. 打开文件
  8. NtReadFile
  9. NtFlushBuffers文件

meminit 执行以下主要操作:

  1. 将每个挂钩的 NtAPI 的位置独立替换函数发送到 Beacon
  2. 在 Beacon 内存中创建一个结构来保存 MemFiles 在其整个生命周期中所需的各种值
  3. 将此结构的地址修补到每个 PIC 替换函数中
  4. 分配内存并将每个 PIC 替换函数注入到 Beacon 进程内存中
  5. 为每个挂钩的 NtAPI 创建一个蹦床
  6. 通过覆盖部分/所有字节挂钩列出的每个 NtAPI,将执行重定向到 PIC 替换函数。

记忆列表

memlist 用于显示给定 Beacon 的 MemFiles 当前存储在内存中的所有文件。 显示了几个字段,与用户最相关和最感兴趣的是文件名和存储数据的长度。

ad1a1b7183210737

取内存

memfetch 用于实际检索 MemFiles 为给定 Beacon 存储在内存中的文件。

默认情况下,memfetch 将检索“句柄”已关闭的 MemFiles 存储的所有文件。做出此设计选择是为了避免与尝试下载程序/应用程序尚未完成写入的文件有关的任何问题。

这意味着如果程序/应用程序未能关闭它打开的文件句柄,则 memfetch 将不会下载该文件。
这可以通过使用 memfetch 的“force”参数来缓解,即“memfetch force”以便从内存中检索所有文件,而不管其句柄的状态如何。

memfetch 从内存中检索的文件作为下载发送回 Teamserver,并且可以通过 CobaltStrike 中的“下载”选项卡从 Teamserver 同步到客户端。

一旦 Teamserver 下载了一个文件,它就会在 Beacon 进程中从内存中擦除,并通过 memlist 删除它的条目。

内存清理

memclean 负责从 Beacon 进程中清理和删除 MemFiles。

MemFiles 的标准用例涉及安装它并在 Beacon 的生命周期内保持安装状态;然而,如果人们想将 MemFiles 与一种工具结合使用以捕获和检索文件输出,然后卸载 MemFiles 以使其工件不在内存中,则可以使用 memclean 将 Beacon 进程恢复到运行 meminit 之前的原始状态.

这涉及:

  1. 解开每个挂钩的 NtAPI
  2. 清零并释放每个创建的蹦床
  3. 清零并释放每个注入的 PIC 替换函数
  4. 清零并释放 MemFiles 结构

请注意,在 memclean 执行这些操作之前,它将强制下载 MemFiles 存储在内存中的所有文件。如果打算通过单个工具使用 MemFiles 然后将其删除,他们可以跳过使用 memfetch,只需使用 memclean 来检索文件并一次性从 Beacon 进程中删除 MemFiles。

内存表

memtable 用于显示和跟踪有关当前安装了 MemFiles 的 Beacon 的信息。它还显示了全局配置信息。

每个 CobaltStrike Client 都有自己的内存表;MemFiles 竭尽全力确保其数据在所有连接的 CobaltStrike 客户端之间的同步性,以便所有信标中的所有操作员都可以使用 MemFiles。有关更多信息,请参阅“设计注意事项和评论”。

 

ad1a1b7183210748

用法

使用 meminit 命令在 Beacon 中初始化 MemFiles。这可以通过切换 MemFiles->Config 菜单中的选项来配置为自动发生。

 

ad1a1b7183210801

 

初始化 MemFiles 后,您现在可以使用您喜欢的工具将文件写入内存!你如何做取决于具体的工具;有些允许您指定一个目录以将多个文件输出到其中,而另一些则允许为该工具创建的单个文件指定绝对路径。下面是几个例子:

SharpHound

这里我们指定 SharpHound 应该将所有生成的文件输出到 c:\redteam\ 目录(我们的特殊 MemFiles 目录)并且它不应该压缩文件;MemFiles 不支持程序从内存中读取文件,只支持写入文件,因此 SharpHound 中的 zip 功能不起作用。

 

ad1a1b7183210811

 

Rubeus

“转储”命令与 Rubeus 一起使用,我们指示它将所有控制台输出发送到一个文件(位于我们的特殊目录中)

 

ad1a1b7183211002

Powershell

在此示例中,Inline-Execute-PE 用于将 powershell.exe 加载到 Beacon 进程中并运行“Get-ADUser”以检索域用户列表。使用管道和“输出文件”,可以将数据写入内存然后检索。

 

ad1a1b7183211026

当您想要检索文件时,运行 memfetch:

 

ad1a1b7183211101

 

当您完成 MemFiles 和/或不想将其安装在 Beacon 进程中时,运行 memclean:

 

ad1a1b7183211143

 

请注意,在上面的示例中,有一个文件尚未下载;在卸载 MemFiles 之前,memclean 下载此文件并将其从内存中擦除。

使用 memtable 查询 MemFiles 的状态和配置。在长时间操作期间,从 memtable 中清除死/旧信标的条目以避免混乱。

 

ad1a1b7183211201

 

能力和局限性

正如简介中所强调的,MemFiles 需要 Beacon 进程中的 NTDLL 的干净副本才能运行。这是必要的,因为它读取 NtFunction 中的原始字节并将某些字节复制到 trampoline,随后用于完成 MemFiles 不应干扰的对 NtFunction 的正常调用。该主题在“技术细节、设计注意事项和评论”部分中有更详细的介绍。

MemFiles 为每个文件初始分配 1048576 字节;当数据写入内存时,它可以并且将会根据需要扩展此分配以容纳更大的文件。

存储在 MemFiles 结构中的文件名是从传递给替换 NtCreateFile 函数的参数中解析出来的。MemFiles 这样做是一种相当简单的方式,通过在文件路径参数中定位“特殊”目录,寻找它的末尾,然后将指针递增 1 以说明分隔“特殊”的“\”字符目录和文件名。例如,在路径“C:\users\tom\redteam\myfile.txt”中,MemFiles 找到“redteam”,考虑反斜杠字符,并选择“myfile.txt”作为文件名。

MemFiles 不关心文件路径中的任何前面的目录;就 MemFiles 而言,'C:\redteam\myfile.txt' 和 'c:\users\tom\appdata\local\redteam\myfile.txt' 是同样有效的路径。

鉴于 MemFiles 如何从文件路径中解析文件名,应该注意 MemFiles 不支持创建子目录;这意味着 MemFiles 将无法使用工具正常运行,例如,尝试创建 c:\redteam\mynewdir\file1.txt、c:\redteam\mynewdir\file2.txt、c:\redteam\mysecondir\file3.txt , ETC。

被 MemFiles 挂钩的 NtAPI 已被识别为各种程序用于 I/O 操作的那些。如前所述,使用 MemFiles/这组 hook NtAPI 成功测试的工具/功能是:SharpHound、Rubeus、Powershell、Procdump、BOF、执行文件写入操作的通用 C 程序,以及 CobaltStrike 的 bupload_raw 命令(允许操作员指定远程文件位置)。当然还有其他工具可以开箱即用地使用 MemFiles;其他人将不兼容。

乍一看,Windows 文件创建过程看起来很简单:NtCreateFile->NtWriteFile->NtClose。在深入研究这个项目后,我很快发现涉及到许多其他 API,为了获得额外的乐趣,所涉及的其他 API 因程序而异。某些程序作为其 I/O 过程的一部分调用 Win32 API SetFilePointerEx,后者又调用 NtAPI NtSetInformationFile。其他程序,如包括 SharpHound 在内的 .NET 程序,最终会调用 NtFlushBuffersFile。

缺少用于创建和写入文件的单一公共 API 调用链会导致不兼容,具体取决于与 MemFiles 一起使用的工具。程序可能会调用 MemFiles 尚未挂钩的另一个 NtAPI,在替换 NtCreateFile 函数中传递由 MemFiles 创建的假句柄,并导致“无效句柄”错误停止执行。其他程序执行 MemFiles 无法充分欺骗/替换的更复杂的操作。已经确定的此类不兼容性之一是 ADExplorer.exe。

ADExplorer.exe 是一个签名的 Microsoft 二进制文件,用于活动目录枚举。在运行时,ADExplorer 将数据写入指定的输出文件,然后返回并在执行结束时最终将最终输出写入文件之前引用它。因为它使用输出文件作为某种缓存,所以它必须能够读取它已经写入文件的数据,而且它以任何一种简单、可预测的方式这样做的可能性很小。

MemFiles 目前不支持程序或应用程序读取内存中的文件,但是通过自定义 NtReadFile 函数的更多充实以及向 MemFiles 结构中添加一些额外的变量/数据跟踪,这应该是可能的。

ADExplorer 在其生成的文件大小方面提出了另一个挑战。在大型企业环境中,输出文件可以超过 1GB;虽然 MemFiles 应该能够以编程方式处理这个问题,但它肯定不适合此类用例。

不兼容的工具

毫无疑问,社区会发现 MemFiles 无法正常运行的工具;我鼓励您通过 inline-executeAssembly、Inline-Execute-PE 等打开一个问题,详细说明不兼容的程序/工具以及您运行它的环境,即它是一个 BOF,这样我就可以看看我是否不能展开 MemFiles 并让它工作。

IOC 和 AV/EDR

与 MemFiles 相关的 IOC 包括但不限于:

使用 VirtualAlloc 分配内存 使用
WriteProcessMemory 写入数据
在 RW 和 RX 之间更改分配内存的内存保护
在 NTDLL.dll 中覆盖内存

影音/EDR

MemFiles 不是针对适当的 EDR 开发或测试的;Microsoft Defender 是可用的。话虽这么说,但我敢说,与 MemFiles 捕获文件或将文件存储在内存中相比,无论 Beacon 运行什么工具/程序来生成文件,都更有可能收到警报。NTDLL 中的内存覆盖/挂接 NtAPI 让我觉得某些产品可能会遇到问题,但我没有任何证据证实这一点。对于与 MemFiles 捕获/应该捕获的文件无关的挂钩 NtFunctions 的调用,系统调用仍然从 NTDLL.dll 地址空间内发出,因为安全产品确实检测并警告从该区域外部进行的系统调用。

应该注意的是,MemFiles 存储在内存中的文件没有编码或加密;如果识别出 AV/EDR 在内存中生成的文件上发出警报的真实用例/实例,则可以添加此功能。

技术细节、设计注意事项和评论

几个月前在 KFiveFour 的 Tradecraftcon 会议上,我第一次了解到内存文件系统的概念,其中一位演讲者 (@DexterGerig) 演示了使用客户端-服务器模型创建内存文件系统的 POC。我的上一个主要版本 Inline-Execute-PE 涵盖了我对此类项目的一半设想功能。另一半,即能够捕获工具生成的文件并将它们存储在内存中而不是磁盘中的想法,该项目并未实现,并且由于显而易见的原因仍然是非常理想的功能。

MemFiles 对我来说是一项极具挑战性的工作,因为在这个项目之前,我在调试器上花费的时间很少,不了解汇编,也不了解 API 挂钩。在这个项目中,我遇到了几个 10-20 小时长的障碍,幸运的是,我坚持不懈地能够过去。虽然这可能不是最有效的方法,但我对调试器有了更多的了解,并且对计算机在汇编、寄存器、堆栈和调用约定方面的工作原理有了更深入的了解。

接下来是对 MemFiles 中一些更重要的技术细节和设计注意事项的技术深入研究。

Windows 中的文件以及 MemFiles 的工作原理

Windows 上的文件创建从 NtCreateFile 开始,向其提供所需文件的路径,作为回报,Windows 在该位置创建一个文件并提供一个句柄。返回的句柄用于涉及该文件的所有后续调用,例如 NtWriteFile 和 NtClose。

在考虑如何将对所有这些 API 的调用在我们想要拦截和篡改的 API 的调用与我们想要单独保留的 API 之间分开时,我着手寻找 NtCreateFile 调用中的关键字。这是通过在 NtCreateFile 调用中指定一个唯一的、不存在的目录作为文件路径的一部分来实现的。当我们的钩子将执行重定向到替换 NtCreateFile 函数时,作为参数传递给 NtCreateFile 的文件路径将被检查是否存在该唯一“关键字”;如果它找到了它,MemFiles 就知道这个 NtCreateFile 调用与一个应该放在内存中而不是磁盘上的文件有关。发生这种情况时,MemFiles 会初始化几个变量并分配初始的 1MB 内存供文件使用,

对于 MemFiles 挂钩的所有其他 NtAPI,相应的替换 NtFunction 查看作为参数传入的句柄,看看它是否存在于 MemFiles 结构中;如果句柄在 MemFiles 结构中确实存在(由 MemFiles 生成的假句柄足够假,以至于它们不应该与真实句柄交叉),则 MemFiles 将此调用识别为与内存中文件有关的调用并相应地执行操作。

挂钩理论和替换函数

为了将文件写入到磁盘的内存中,MemFiles 需要拦截程序在尝试创建文件时对某些 API 的调用。API Hooking 已经存在了很长时间,并被许多 EDR 产品积极用作其功能的核心部分;对某些 API 的调用被重定向到 EDR 地址空间,在这里对 API 调用和传递给它的变量进行分析。如果 EDR 确定调用是恶意的,例如攻击工具或杀伤链的一部分,它将阻止调用完成并发出警报。如果 EDR 确定调用是良性的,它会将执行修补回它被重定向的位置,并允许 API 调用按原计划完成。一个简单的类比可以说你给朋友寄了一封信,

MemFiles 遵循相同的理论,没有警报(或理论上的警察参与)。API 挂钩通常在用户空间的最低级别实现;NTDLL.dll 中的 NtFunctions。让我们看一下 NtCreateFile,在任何挂钩发生之前:

 

ad1a1b7183211215

 

所有 NtFunctions 都是相同的,除了系统调用号,在这个例子中是 55。NtFunctions 之间的系统调用编号会发生变化,还应注意,此编号可能会在 Windows 版本之间发生变化;此操作系统 (Windows 11) 上 NtCreateFile 的系统调用号为 55,但在 Windows 10 上(当然在 Windows 7 上)可能有所不同。

值得注意的是 TEST 和 JNE 指令。这些存在是为了确定 NtFunction 应该使用普通的系统调用指令,还是旧的 INT 2E 指令。我将引用 klezvirus 的帖子SysWhispers 已死,SysWhispers 万岁!:

现在有趣的部分是,该函数检查 SharedUserData[0x308] (BYTE PTR DS:[7FFE0308]) 是否设置为 1。SharedUserData 是一个引用内核模式结构 KUSER_SHARED_DATA 的符号。

KUSER_SHARED_DATA 结构定义了一个固定的(或预定义的)内存空间,用于与用户模式软件共享信息。当然,这样做是为了让某些全局系统信息准备好被用户态代码使用,而无需每次在用户模式和内核模式执行之间切换的开销。

索引 0x308 处的值表示系统调用指令,从 1511 开始的所有 Windows 版本都支持该指令。正如您可能想象的那样,在 1511 之前的所有 Windows 版本中,执行系统调用的标准方法是调用中断 int 2Eh。

...

如果你问自己为什么这个 int 2Eh 仍然存在,即使 Windows 现在已经远高于版本 1511,那是因为这个指令还在使用。实际上,当启用 HVCI(管理程序保护的代码完整性)时,SharedUserData[0x308] 设置为 0,并且使用 int 2Eh 而不是系统调用指令。这主要是出于性能原因,因为 Ring3 到 Ring0 的切换是如何使用一条或另一条指令进行操作的。

我在推特上要求进一步澄清这个话题,@yarden_shafir 说了以下内容:

 

ad1a1b7183211226

 

长话短说,在某些时候可能需要 NtFunction 中的每条指令(可能除了在末尾发现的多字节 NOP 似乎无法访问),如果我们覆盖 NtFunction 中的指令,我们需要以确保我们在进行最终系统调用(或 INT 2E 视情况而定)之前的某个时刻保存并执行它们。

值得注意的是,在进行系统调用之前,系统调用编号被移入 RAX(在屏幕截图中显示为 EAX)。因为在此之前我们没有看到 RAX 被推入堆栈,所以我(也许天真地)假设在系统调用号被移动到那里之前 RAX 中包含的值并不重要,或者在发出系统调用之后不需要。这是个好消息,因为这意味着我们可以自由使用 RAX 寄存器,只要我们确保它在发出系统调用之前包含系统调用号即可。

为了将执行重定向到我们的自定义代码/替换 NtFunction,我们将覆盖部分原始 NtAPI,将替换 NtFunction 的地址移动到 RAX 中,然后使用 JMP 指令转到该代码:

 

ad1a1b7183211241

 

MOV和JMP指令需要12字节;因为我们通过覆盖 NtAPI 的前 12 个字节来破坏其他指令,所以这些指令已被 NOP 替换,以保持 NtAPI 的正确间距和对齐。

当程序现在调用 NtCreateFile 时,它​​将跳转到我们的替代 NtFunction 执行。

自定义代码和替换 NtFunctions

MemFiles 与 EDR 执行挂钩的方式不同,因为挂钩 API 的替换函数被重定向到驻留的位置。许多 EDR 会将它们自己的 DLL 加载到进程中。挂钩的 API 被重定向到这个加载的 DLL 的地址空间,可以在其中进行分析。因为这个项目的全部目的是避免将东西丢到磁盘上,将 DLL 放在磁盘上并让我们的 Beacon 进程加载它以便访问我们的替代 NtFunctions 似乎是一个糟糕的途径。有一个几年前的 POC可用,它允许从内存加载 DLL,这是满足我们需求的可行策略,但该项目没有维护,并且似乎存在几个问题。

顺便说一句,我们的替代 NtFunctions 不能驻留在 BOF 中;CobaltStrike 加载、运行,然后在完成后从进程内存中擦除 BOF。由于我们需要内存中的持久函数,只要进程调用其中一个挂钩的 NtAPI 就可以调用该函数,因此 BOF 将不起作用。

我得出的答案是位置无关代码 (PIC)。顾名思义,与需要将它们加载到特定位置/部分与其他位置具有特定关系的普通可执行文件相比,PIC 可以放置在内存中的任何位置并运行。这打开了将我们的替换 NtFunctions 编写为 PIC 可执行文件的大门,将它们注入 Beacon 进程,并在调用我们的挂钩 NTAPI 时让我们的挂钩将执行重定向到它们。

这些 PIC NtFunctions 的模板来自 Cracked5pider 的ShellcodeTemplate项目。

与基础项目的一个显着差异是基础项目是围绕创建完整的 PIC exe 设计的;也就是说,它包含 ASM 以保存堆栈指针,在堆栈上创建空间,调用包含在 exe 中的指定替换 NtFunction,然后在该函数返回后恢复堆栈指针。PIC exe 发出的调用指令会出现问题,因为这样做会将进行调用的返回地址(在 PIC exe ASM 中)推入堆栈,这将导致稍后遇到的 ret 指令将执行返回到 PIC exe,而不是原始 NtAPI 的调用者。

为了缓解这种情况,对 ShellcodeTemplate 项目中的 ASM 文件进行了编辑,以删除与设置堆栈、调用函数以及函数执行完成后恢复堆栈指针相关的 ASM。结果是 NtAPI 中的钩子现在直接跳转到替换 NtFunction 执行,堆栈和寄存器设置为程序调用原始 NtAPI 时的状态(RAX 除外,它用于我们的 JMP ).

原始 ShellcodeTemplate ASM:

ad1a1b7183211257

 

内存文件 ASM:

ad1a1b7183211325

 

每个挂钩的 NtAPI 都有自己的 PIC NtFunction,其中包含必要的逻辑:

A. 执行 MemFiles 特定操作,例如创建假句柄、将数据写入内存、更改 MemFiles 结构中的变量等

B. 直接执行到蹦床,这将使 API 调用回到正轨并将其修补回 NTDLL,其中可以进行系统调用

一些替换的 NtFunctions 比其他的更复杂;当对挂钩的 NtAPI 的调用与 MemFiles 相关时,一些(如 NtCreateFile 和 NtQueryVolumeInformationFile)根据 MSDN 文档、测试结果和一些猜测/常识修改作为参数传递给 NtAPI 的变量。其他的,如 NtClose 和 NtReadFile,只是简单地将 STATUS_SUCCESS 返回给原始调用者,以避免不可避免的“无效句柄”错误,否则会因传入由 MemFiles 创建的假句柄而引起。

当对挂钩的 NtAPI 的调用不涉及 MemFiles 时,我们需要将执行定向到蹦床以使事情回到正轨:

 

ad1a1b7183211336

 

蹦床

由于该 API 已被挂钩,蹦床负责执行未在原始 NtAPI 中执行的任何/所有指令;这包括任何被原始钩子部分或全部覆盖的指令。Hooking API 会很快导致我们没有足够的空间来执行我们需要的所有指令的问题。Trampolines 也可以帮助缓解这个问题,因为我们可以在跳回原始 NtAPI 之前执行任意数量的操作来设置我们的寄存器和/或堆栈。NtCreateFile 蹦床如下所示:

 

ad1a1b7183211344

 

最明显的是,我们的初始钩子覆盖的三个指令可以看作是蹦床中的前三个指令:

MOV R10, RCX
MOV EAX, 55
测试字节 PTR DS:[7FFE0308], 1

如前所述,所有这些操作的最大要求是系统调用编号(上例中的 55)在发出系统调用之前驻留在 RAX(EAX) 中。然而,我们有一个问题,我们仍然需要使用 JMP 指令将执行引导回原始 NtAPI。虽然可能有另一个寄存器没有保存重要信息并且可以用来这样做,但考虑到我们挂钩的 NtAPI 的数量,我没有找到一个始终安全的选项,每个 NtAPI 可能使用不同的寄存器。安全的选择是继续使用 RAX;我们可以通过将存储在 RAX 中的系统调用号压入堆栈来实现。然后我们可以将原始 NtAPI 中希望跳回的地址移动到 RAX 中,并使用 JMP 指令返回 NTDLL:

 

ad1a1b7183211356

安装在 NtAPI 中的钩子中包含一条 POP RAX 指令,这是我们跳转到使用蹦床的地方。执行此指令会将系统调用编号从堆栈顶部恢复到 RAX,并设置我们发出系统调用。请注意,来自原始的、未挂钩的 NtAPI 的 JNE 指令仍然在这里;在蹦床中执行了相应的 TEST 指令,它设置 ZF 标志并指示是否采用 JNE(这将使我们跳过系统调用到 INT 2E)。以这种方式安排事情可以让我们成功地挂钩 NtAPI 并将执行重定向到我们的替代 PIC NtFunction,并确保我们不会因为挂钩而跳过任何东西或丢失任何功能。

马前车马问题

简要总结到目前为止所涵盖的内容,初始化 MemFiles 时,会在 Beacon 进程内存中创建一个结构,其中包含 Memfiles 功能的重要信息。此结构中的信息在 MemFiles 的整个生命周期中不断被引用,包括由每个替换 PIC NtFunction 以及用于查询和获取 MemFiles 存储在内存中的文件的 BOF。为此,当创建结构时,结构所在的内存地址将传回给 Teamserver:

 

ad1a1b7183211420

将此地址存储在 memtable 中后,后续的 MemFiles 命令(memlist、memfetch、memclean)将此地址作为参数发送给 BOF,以便可以定位和引用该结构。但是 PIC NtFunction 是如何定位结构的呢?

明显的问题是 PIC NtFunction 需要 MemFiles 结构的内存地址,但在创建结构时它们已经编译。MemFiles 的早期实现通过将 MemFiles 的初始化分成两个单独的 BOF 来解决这个问题。第一个创建结构并将地址发送回 Teamserver,Teamserver 通过一些 Aggressor 脚本魔法解析地址,将其插入到每个 NtFunction 的源代码文件中,然后将它们重新编译成最终的 PIC NtFunction。然后,第二个 BOF 将传输完成的 PIC NtFunctions 并执行 NtAPI 的实际注入和挂钩。

除了丑陋和花费额外时间之外,真正的问题可能出现在多个信标试图同时初始化 MemFiles 的情况下。如果 Beacon 2 使用它的 MemFiles 结构地址回调,而 Beacon 1 正在修补和重新编译 NtFunction 源代码文件,事情可能会变得一团糟。

这个问题的优雅解决方案涉及对 PIC NtFunction 执行二进制补丁,其中 MemFiles 结构地址被补丁到编译的 NtFunction 中,并在运行时可访问。为了方便这一点,一个占位符字符串被写入每个 NtFunction:

 

ad1a1b7183211431

 

使用 xxd 之类的工具可以在编译后的代码中看到该变量:

 

ad1a1b7183211438

 

运行 meminit 命令时,每个 PIC NtFunctions 都会与 InstallHooks BOF 一起发送到 Beacon。这个 BOF 负责创建 MemFiles 结构;这样做之后,它会在每个 PIC NtFunctions 上调用 patchAddr 函数。patchAddr 负责定位 A 的字符串并将其替换为结构地址的字符串表示形式。实际上,使用早期屏幕截图中的内存地址,pFileInfoStr 变量现在看起来像:

char* pFileInfoStr = GET_SYMBOL( "00000178BC106860" );

然后可以将该字符串表示形式转换为实际的十六进制值,即我们的内存地址。BOF 使用 _strtoi64 API 非常简单地完成此操作:

 

ad1a1b7183211447

 

当然,对于 PIC NtFunctions 来说,事情不可能这么简单。

这个简短的汇编片段显示了其中一个 PIC NtFunctions 的结尾。注意 JMP RAX 指令,它是调用 trampoline 的 PIC NtFunction(意味着这是一个 MemFiles 没有欺骗/干扰的调用):

 

ad1a1b7183211456

 

出于未知原因,当我尝试使用 _strtoi64(或其任何类似 API,如 stroull 或 atoll)时,此 JMP RAX 指令变成了 CALL RAX 指令。我确信这有一个正当的理由,涉及到一些深层次的“计算机如何工作”,但这个看似微不足道的变化破坏了很多东西。在花了 15 个多小时试图找到一种方法将 MemFiles 结构地址的字符串表示形式转换为实际的十六进制值以便我可以利用存储在其中的值之后,我遇到了这篇 StackOverflow 帖子其中一位评论者提供了一个为微控制器设计的自定义例程,用于将字符串转换为 uint32;值得庆幸的是,它无需修改也适用于 uint64,最重要的是,它稍后在 PIC NtFunction 中保留了 JMP RAX 指令,而没有将其更改为 CALL。最后的片段:

 

ad1a1b7183211505

 

旧 NtAPI 与新 NtAPI

为了简单起见,前面没有提到它,但是 NtAPI 的格式多年来已经发生了变化。早期版本,如 Windows 7 使用的版本,比现代版本短得多,只有 16 个字节长,而不是 32 个字节:

 

ad1a1b7183211513

 

这需要更改 MemFiles 挂钩 NtAPI 的方式以及 trampoline 的构建方式。为了让 MemFiles 识别它正在处理的 NtAPI 的版本,InstallHooks BOF 首先解析有问题的 NtAPI 的地址,然后从该位置读取 32 个字节。不同的格式导致 syscall 指令在 NtAPI 版本之间位于不同的位置;通过检查某个字节偏移处是否存在系统调用,MemFiles 能够确定它是在处理现代的还是遗留的 NtAPI 实现,并采取相应的行动:

 

ad1a1b7183211524

 

挂钩后,旧版 NtCreateFile API 如下所示:

 

ad1a1b7183211534

 

而蹦床用来设置寄存器然后跳回NtCreateFile:

 

ad1a1b7183211545

 

总的来说,技术非常相似,但事情要紧凑得多,没有太多回旋余地。值得注意的是,为了使其完全适合并正常运行,系统调用指令必须在 NtCreateFile 中移动;它仍然驻留在 NTDLL 中的 NtCreateFile API 内存空间中,但它已从 API 的第 9 和第 10 字节转移到 API 的第 14 和第 15 字节,牺牲了多字节 NOP 以正确填充所有内容.

查找与 I/O 相关的 NtAPI

一些与文件 I/O 操作相关的 API 是直观的,因此易于识别和挂钩;其他的则更加难以捉摸,需要花费数小时在 WinDbg 和 x64dbg 中逐步执行程序集,试图确定调用了哪些 API。我不得不相信有一种更有效的方法来完成任务,但我一直在学习。

作为一个额外的快速补充,我想我会详细说明(事后看来应该非常快)识别阻止 SharpHound 工作 20 小时的 NtAPI 的过程。作为一个 .NET 程序,SharpHound 吐出一个非常丑陋的调用堆栈,所有这些都与它已确定“句柄无效”这一事实有关。虽然 .NET 是一种对程序员友好的语言,但大多数(所有?)功能都会被翻译并最终通过 Win32 API(以及结果是 NtAPI),在那里我们可以像其他任何东西一样观察和挂钩它。

 

ad1a1b7183211554

 

“无效句柄”错误是一个致命的赠品,我没有挂钩 SharpHound 使用的 NtAPI(而不是我现有的 PIC NtFunctions 之一无法正常工作),所以我开始尝试找出它是什么曾是。到目前为止,我在其他工具上使用的技术是首先在 NtCreateFile 上设置一个断点并找到与我的“特殊”目录相关的调用。从那里我将逐步执行该程序(通常多次,因为我会迷路),然后查看程序接下来调用的函数。按照这种方法,我发现 NtQueryVolumeInformationFile、NtQueryInformationFile 和 NtSetInformationFile 被调用并且需要挂钩。

SharpHound 抛出一些额外的曲线球,因为它异步执行许多任务。这使得跟踪单个文件通过 API 调用所采取的线性步骤变得更加困难,因为有多个文件同时经过该过程。此外,我发现在调用 NtCreateFile 之后单步执行程序,最终调用 NtCreateFile 的线程终止了;最终的 NtWriteFile 调用(以及作为此搜索主题的任何其他未知 API 调用)发生在不同的线程中,这进一步混淆了搜索过程。

由于只简单地接触过 .NET,我并不精通解析由错误产生的调用堆栈,对于那些因使用异步而变得难看两倍的调用堆栈更是如此。在 20 小时的问题过程中,在使用我之前的策略没有取得进展的情况下,我不断地回到它并且慢慢地但肯定地更加理解它。关于由“--- 堆栈跟踪结束...”行分隔的顶部的第三个“块”,可以看到一行显示为“在 Sharphound.Writers.JsonDataWriter...”。这给了我一个相对的位置,可以从实际的 SharpHound 代码开始,它是开源的,可以在 Github 上找到。顾名思义,SharpHound 函数处理将 JSON 输出写入文件;我已经知道我的数据已成功写入文件,所以这不是新闻。向上追踪调用堆栈一层,下一个相关行是“at System.IO.Streamwriter.”。System.IO 前缀告诉我这是 .NET 固有的,而不是 SharpHound 特定的功能。查看调用堆栈的最顶部,跳到我面前的那一行是“at System.IO.FileStream.FlushOSBuffer()”。我决定用谷歌搜索 FlushOSBuffer,看看能找到什么。

这样做使我找到了 Microsoft 的filestream.cs .NET 文档。在那里我找到了 FlushOSBuffer 的定义:

 

ad1a1b7183211606

 

它似乎调用了 Win32 API,FlushFileBuffers。可以通过查看Win32Native文档找到 Win32Native.FlushFileBuffers 的定义:

 

ad1a1b7183211625

 

任何在 .NET 中使用过 P/Invoke 的人都会认得这种格式。我现在有一个 Win32 API,我知道 System.IO.Filestream.FlushOSBuffer(),我的问题 .NET 函数调用。在 KERNEL32!FlushFileBuffers 上设置断点并运行 SharpHound 证实了这一点,通过单步执行,我很快看到 FlushFileBuffers 调用 NtFlushBuffersFile。连接此 API 可缓解 SharpHound 遇到的问题并使其能够成功运行,将其输出文件写入内存。

从内存中下载文件

这个项目的一个关键部分是一旦文件在内存中就能够将文件实际下载到 CobaltStrike Teamserver。可以预见,普通的 CobaltStrike 下载命令将无法使用实际上不存在的文件路径。知道我现在所知道的,一个解决方案可能在于充实替换 PIC NtReadFile 代码以促进内存中的文件能够被读取而不是仅仅被写入。事先没有这些知识,能够实际检索文件是一个主要障碍。

偶然我遇到了EspressoCake 写的一个BOF ,它包含一个让我跳出来的函数:

 

ad1a1b7183211639

 

查看代码似乎使用了未记录的 Beacon CALLBACK 选项:

 

ad1a1b7183211649

 

该函数使 BOF 能够启动从目标(而不是从客户端)将文件下载到 Teamserver 。此功能(我后来了解到这是其他几个人的共同努力,包括@Cr0Eax、@EthicalChaos 和@anthemtotheego)删除了以前存在的主要阻止程序,因为我现在有办法从目标系统启动文件传输对于内存中的文件。非常感谢所有参与此代码片段的人,我预计它在将来也会有用。

内存文件数据结构

该项目的一个具有挑战性的部分是确保 MemFiles 功能对连接到团队服务器的所有 CobaltStrike 客户端可用。MemFiles 的数据存储在由 MemFiles.cna 创建的结构中,必须将其加载到每个希望使用该工具的客户端中;因此,这些数据结构存在于每个客户端中,而不是在团队服务器上。如果这些数据确实存在于一个单一的中央位置 (TS),那么从每个客户端检索它就很简单了,这整个事情就不是问题了;如果 CobaltStrike 团队正式将像 MemFiles 这样的功能集成到 CobaltStrike 中,我很肯定这是他们的方向。但由于这是一个社区附加组件,我们将就着手做。

在确保每个 CobaltStrike 客户端都拥有有关信标和配置中的 MemFiles 状态的最新、准确数据时,我们需要担心几种不同的情况:

新客户端连接到 TS 并需要当前的 memtable
实例,其中只有一个客户端连接到 TS 并重新启动 CobaltStrike(因此丢失存储在客户端内存中的 memtable)
客户端 A 更改必须传达给客户端的 MemFiles 数据乙

采取了多管齐下的方法来解决这些情况。为了处理只有一个 CobaltStrike 客户端连接到 TS 的情况(因此是唯一具有内存表数据的实体),每次客户端更改内存表(meminit、memclean)时,它也会将其内存表的内容写出到位于 CobaltStrike 目录中的本地文本文件。如果客户端退出/重新启动,或者重新加载 MemFiles.cna 时,它将首先尝试从本地 memtable.txt 文件读取以填充它的内存 memtable。

当多个客户端连接到一个 TS 并且一个新客户端加入时(根据事件日志),每个客户端获取连接到 TS 的所有用户的列表并按字母顺序对其进行排序。该列表中第一个的客户端被选为“广播”客户端,等待 5 秒后(以允许新客户端初始化并读取其本地 memtable.txt)将在事件日志中为每个客户端发送消息(操作)进入它的内存表。所有客户端(广播客户端除外)都将读取这些消息并使用广播信息更新其内存表;这包括更新现有条目以及添加它们各自的 memtable 不包含的任何其他条目。

涉及 MemFiles 的正常操作也依赖于在事件日志中发送消息。当客户端 A 运行 meminit 时,会广播一条包含所有相关内存表信息的消息;所有客户端通过使用“on Event_Action”挂钩解析这些广播的事件日志消息来更新各自的内存表。当 meminit 完成执行它的 BOF 时,也会对 MemFiles 数据进行更改;这些更改由 Beacon 反馈(例如,在运行 meminit 后,Beacon 会使用 pMemAddrs 结构的内存位置进行回调),因此对所有连接的客户端都是可见的,这些客户端使用“on Beacon_Output”挂钩更新各自的内存表。

这些单独的努力结合在一起,使 MemFiles 能够在多个客户端之间高效可靠地同步关键数据。

标签:工具分享, cobaltstrike系列