Inline-Execute-PE 是一套 Beacon 对象文件(BOF)及对应的 CobaltStrike Aggressor 脚本,使操作员能够将非托管的 Windows 可执行文件加载到 Beacon 内存中并执行,捕获其输出并在 Beacon 控制台中显示。
这使操作员能够使用许多第三方工具(Mimikatz、Dsquery、Sysinternals 工具等),而无需将其写入磁盘、使用 Donut 等工具将其重新格式化为位置无关代码,或创建新进程来运行它们。
这些可执行文件被映射到 Beacon 内存中,因此可以重复运行,无需每次通过网络发送、分配新内存并创建新的 conhost.exe 进程。
加载到 Beacon 中的可执行文件可供所有连接到 CobaltStrike 团队服务器的 CobaltStrike 客户端访问和运行。
Inline-Execute-PE 针对 x64 Beacon 以及使用 Mingw 或 Visual Studio 编译的 x64 Windows C 或 C++ 可执行文件设计。该项目不支持 x86 可执行文件或使用其他语言或编译器编译的 x64 可执行文件。

克隆仓库,可选运行 make 以重新编译 BOF。
将 Inline-Execute-PE.cna 加载到 CobaltStrike 客户端中。确保 CobaltStrike 运行目录可由您的用户写入;Inline-Execute-PE 会在该目录下创建一个文本文件(petable.txt)以确保 Inline-Execute-PE 运行所需数据的可用性。
Inline-Execute-PE 包含 3 个面向目标的命令(运行 BOF)和 3 个操作项目数据结构的内部命令:
面向目标:
内部数据结构:
peload 是 Inline-Execute-PE 的起始步骤。此命令用于将 PE 加载到 Beacon 内存中。它执行以下主要操作:
perun 是 Inline-Execute-PE 的第二步。它执行以下主要操作:
当操作员完成 PE 使用或希望加载其他 PE 时,调用 peunload 从 Beacon 内存中移除 PE。它执行以下主要操作:
petable 用于显示当前加载到所有 Beacon 中的 PE 信息。
每个 CobaltStrike 客户端都有自己的 petable;Inline-Execute-PE 尽力确保所有连接的 CobaltStrike 客户端之间数据同步,以便所有操作员都能使用 PE。更多信息请参见“设计考量与评述”。

peconfig 用于配置 Inline-Execute-PE 运行的相关选项。当前可修改的两个选项是:
pebroadcast 可用于手动将客户端的 petable 内容广播到所有其他连接的 CobaltStrike 客户端。
其他 CobaltStrike 客户端将用广播的数据更新其 petable。通常无需使用此功能,但以防万一而保留。
使用 peload 将 PE 加载到 Beacon 内存中

或者,如果目标机器上已有 PE 且不想创建新进程,提供路径和 --local 开关

调用 perun,向加载的 PE 传递参数

参数中的双引号必须使用反斜杠转义

如果发现某个 PE 在卸载时释放 DLL 会导致问题,使用 peconfig 将 unloadlibraries 设置为 false

使用完 PE 后,调用 peunload 从 Beacon 中清理

现在可以将另一个 PE 加载到 Beacon 中

必须小心传递到 PE 的命令行参数;某些 PE 在收到错误参数时会直接崩溃,而另一些则会无限运行,导致 Beacon 即使进程仍在运行也无法回连。
当 Mimikatz.exe 的参数列表末尾未指定 'exit' 时会出现此情况

...

Inline-Execute-PE 会在指定的超时时间后终止正在运行的 PE 线程。这使 Beacon 能够恢复正常的通信(在 perun BOF 完成执行之前,Beacon 不会回连)。虽然此 Beacon 中仍可使用正常的 CobaltStrike 命令和其他 BOF,但 Inline-Execute-PE 已被禁用;以这种方式终止正在运行的 PE 似乎会破坏 Beacon 进程中的 stdout 和 stderr,并且后续加载的 PE 无法正常工作。
PE 仍然可以(并且应该)从 Beacon 内存中卸载,但查看 petable 会显示此 Beacon 可能无法再加载其他 PE。

请务必测试您希望使用 Inline-Execute-PE 运行的 PE,并在向 perun 传递命令行参数时格外小心。某些 PE 比其他 PE 更宽容。
以下是一些在测试和开发过程中关于某些用户可能希望加载到 Beacon 的 PE 的观察结果(无特定顺序)。
与 Inline-Execute-PE 相关的 IOC 包括但不限于:
在开发过程中,我并未对 EDR 进行全面测试,部分由于懒惰,部分由于缺乏测试环境。不过,它已针对最新补丁的 Windows Defender(根据我的经验,这是一款相当好的防病毒产品)进行了测试。
Mimikatz.exe 可能是最适合用 Inline-Execute-PE 运行的已知 PE。我发现 Windows Defender 检测使用 Inline-Execute-PE 运行的 Mimikatz 的能力取决于 Beacon 运行所在的进程。
在独立可执行文件(例如使用 artifact kit 的 beacon.exe,使其能够在 Defender 下正常运行)中运行的 Beacon,在使用 Mimikatz.exe 与 Inline-Execute-PE 时会被检测到。
在 Windows 进程(注入到 Explorer.exe、notepad.exe 等,或通过 DLL 侧加载到合法进程中)中运行的 Beacon,在使用 Mimikatz.exe 与 Inline-Execute-PE 时不会被检测到。
关于执行用户层钩子的 EDR,我尚未测试,但我有以下一般性想法:
由于 PE 运行在 Beacon 进程内部,而您可能已经取消了 NT DLL 的钩子/刷新,因此我认为 PE 调用的 API 被标记的问题应该不大。但 PE 实际执行的操作(访问进程、修改注册表键等)仍然会引发问题。
几个月前,我偶然发现了 RunPE-In-Memory,产生了将其转换为 CobaltStrike BOF 的想法。后续的旅程比预期的要复杂得多,耗时也更长。这个项目特别具有挑战性,因为它本身不是一个独立的工具,而是一个用于运行其他工具的工具。这需要极大的灵活性和努力,以实现与各种 PE 及其完成相同任务(获取参数、终止等)的不同方式的兼容性。
起初,Inline-Execute-PE 被设想为一个一体化的 BOF,负责在 Beacon 中加载、执行和释放 PE。项目进行约 3 周时,我完成了约 75% 的概念验证,发现 PEzor 已在约 1.5 年前发布,并且几乎完成了我想做的所有事情;主要区别在于 PEzor 在底层调用 Donut 将 PE 转换为 shellcode,而不是手动将原始 PE 映射到内存。
这一发现在一方面令人欣喜,另一方面也令人失望;有一个成熟的项目可以借鉴灵感,帮助我解决代码中的一些难题,这非常棒,但令人沮丧的是,我实际上是在不知情的情况下重新发明了轮子。在阅读了 PEzor 的相关资料并思考其设计、一些战术相关事宜以及我所在组织的运营需求后,我调整了 Inline-Execute-PE 的方向,成为今天的样子。这一决定由多个因素驱动,下面将讨论这些因素,以及一些可能让读到这里的读者感到好奇的设计选择。
根据我的运营经验,我遇到了多个需要重复运行某个工具的场景和工具;使用 PEzor 时,操作员必须反复通过网络发送 PE、创建 conhost.exe、在 Beacon 中分配新内存等,这在考虑 AV/EDR 时可能不太理想。这种思考导致了将 PE“加载”到 Beacon 中的想法,类似于将 .PS1 加载到 Beacon 中重复使用。conhost.exe 在首次加载 PE 时创建,并在 PE 加载到内存期间持续存在;同样,新内存只在首次加载时分配一次,当然也避免了每次使用时都需要通过网络发送 PE。Inline-Execute-PE 采用的模式并非没有缺陷,我尝试以不同程度的成功来解决这些问题。
一个显而易见的设计选择是 Inline-Execute-PE 将 PE 在 Beacon 中映射两次。这当然不是理想的选择,也不是我自愿做出的,而是出于必要。如前所述,Inline-Execute-PE 必须挂接与命令行参数相关的几个函数。由于映射的 PE 在 Beacon 进程内部运行,PE 会尝试使用 PEB 中 PROCESS_PARAMETERS 部分指定的命令行参数;为了解决这个问题,当 PE 调用各种获取命令行参数的函数时,我们必须将 PE 引导到我们自己定义的自定义函数中,以便提供通过 perun 从 CobaltStrike 传递的预期参数。
这工作得很好,但在开发过程中,我注意到几个不同的 PE 出现了一些奇怪的现象。第一次运行 PE 时,我们提供的自定义函数(在 PE 的 IAT 中)被正确调用,但在随后所有提供不同参数的运行中,PE 并没有调用自定义函数,因此没有接收到从 CobaltStrike 传递的参数。我不确定底层实际发生了什么,但我相信 PE 在首次运行后将命令行参数复制到了内存中的某个位置,并在后续运行时首先查找该内存位置,而不是像第一次那样调用被挂钩的函数来获取命令行参数。我通过检索内存中指向另一个指针(指向包含参数的指针数组)的位置,并在每次运行时手动修改此内存位置以包含正确的指针,验证了这一理论。这对 __getmainargs 和 __wgetmainargs 函数有效,但其他 PE 会调用替代函数,如 __p___argv 和 __p___argc,这种方法不起作用。
为了能够将 PE“重置”到实际调用挂钩函数以获取参数的状态,我决定在 peload 期间创建 PE 的第二个副本。该副本也经过 XOR 加密,并在 Inline-Execute-PE 的整个生命周期中保持 RX 保护,仅用于覆盖实际使用 perun 执行的 PE 副本。如所述,这不是完美的解决方案,但它是一个覆盖所有 PE 的通用解决方案,无需陷入为各种不同 PE 及其使用的不同 API 寻找解决方案的泥沼。
Inline-Execute-PE 的一个主要卖点是可以不创建新进程而运行工具,但不得不创建一个新进程(conhost.exe)却是对这一点的一大打击。这个需求源于在 Windows 程序中,除非存在控制台,否则标准流(stdin/stdout/stderr)不会被初始化。在我们的情况下,我们根本不需要控制台;标准流被重定向到匿名管道并以此方式捕获,但没有 conhost,流就不会被初始化,也就无法重定向。
Inline-Execute-PE 以与 PEzor 相同的方式处理 conhost 问题:它调用 AllocConsole,然后立即使用 ShowWindow 将其隐藏。在我的 8 GB RAM Windows 11 虚拟机上,我没有看到控制台窗口闪烁后消失,但在目标系统上的体验可能有所不同。
我与一位在非常先进的商业 C2(最近推出了原生等同物,实际上是更先进的版本)上工作的开发人员交谈过,他告诉我他们能够通过“欺骗 Windows 使其认为有控制台”来避免生成 conhost.exe。带着这条线索,我花了一周时间搜索关于 Windows 程序如何与 conhost 交互的文档,试图在 WinDBG 中追踪与写入函数和控制台相关的 API 调用,甚至检查了 Windows Terminal 的源代码(令人惊讶的是,它可以在 Github 上找到)。尽管我对 PEB 和标准流相关的内容学到了很多,但我最终空手而归。我怀疑前进的方向可能涉及修补 kernel32 中某些与控制台相关的函数,但我不知道。说实话,对于无法找到解决方案,我感到非常失望,但作为一名自学成才、职业生涯只有几年的人,这可能是意料之中的事。### PE 超时与救援
所有曾尝试编写 BOF 的人都清楚,虽然 BOF 带来诸多优势,但一个巨大的风险在于:你的 BOF 出现错误或崩溃会导致 Beacon 被杀死。在本项目中,由于用户对传递给 Inline-Execute-PE 的数据拥有极大控制权,而开发者我能轻易或可靠实施的安全措施又十分有限,这一风险被进一步放大。例如,用户可能会因为将 x86 PE 加载到 x64 Beacon 中而导致 Beacon 崩溃,或者更常见的是,如前所述,向映射后的 PE 传递了不正确的参数。虽然我无法阻止用户因向 PE 传递错误参数而崩溃 Beacon,但我可以尝试在 PE 无限运行(例如 Mimikatz 未指定 'exit' 时)的情况下拯救 Beacon。
理想情况下,我应该能够停止 PE 的执行,让 Beacon 恢复正常功能,然后立即允许用户再次尝试,这次使用(希望是)正确的参数。在实践中,我发现终止 PE 似乎会破坏与 stdout/stderr 关联的 FILE*,即使完全卸载 PE 然后重新加载也无法解决这个问题;它们在进程范围内被破坏了。
为了终止在超出 'timeout' 选项后仍在运行的 PE,会对 CreateThread 返回的句柄调用 TerminateThread。这不允许线程优雅地退出任何内容,因此某些东西可能会损坏也是有道理的。我试图通过实现线程劫持来缓解这个问题,目标是挂起 PE 线程并将其执行重定向到 ExitThread() API。希望这样,如果是由线程自身启动退出过程(而不是被外部强制终止),可能会使 stdout/stderr 继续工作,但我最终遇到了同样的问题(并且在 Mimikatz 的情况下无法挂起 PE 线程)。
由于无法缓解这个问题,我最终决定简单地阻止用户继续运行 PE 或向受影响的 Beacon 加载额外的 PE(这会导致崩溃)。这是 Inline-Execute-PE 未能达到我预期效果的又一个例子,但我接受了这样一个事实:操作者至少还能保留他们的 Beacon,并能正常使用它。
本项目中一个具有挑战性的部分是确保加载到 Beacon 中的 PE 对连接到团队服务器的所有 CobaltStrike 客户端都可用。Inline-Execute-PE 的数据存储在由 Inline-Execute-PE.cna 创建的结构中,必须加载到每个需要使用该工具的客户端中;因此,这些数据结构存在于每个客户端内部,而不是团队服务器上。如果这些数据确实存在于一个单一的中央位置(TS),那么每个客户端检索它将是微不足道的,整个问题就不存在了;如果 CobaltStrike 团队正式将类似 Inline-Execute-PE 的功能集成到 CobaltStrike 中,我相信他们一定会朝这个方向走。但鉴于这是一个社区附加组件,我们只能利用现有的条件。
在确保每个 CobaltStrike 客户端都拥有关于加载到 Beacon 中的 PE 的最新准确数据方面,我们需要担心几种不同的场景:
我们采用了多管齐下的方法来解决这些场景。为了处理只有单个 CobaltStrike 客户端连接到 TS(因此是唯一拥有 petable 数据的实体)的情况,每次客户端更改 petable(如 peload、peconfig、peunload 等)时,它还会将其 petable 的内容写入位于 CobaltStrike 目录下的本地文本文件中。当客户端退出/重启或 Inline-Execute-PE.cna 重新加载时,它会首先尝试读取本地的 petable.txt 文件,以填充其内存中的 petable。
当多个客户端连接到 TS 且新客户端加入时(根据事件日志),每个客户端会获取连接到 TS 的所有用户列表,并按字母顺序排序。列表中的第一个客户端被选为“广播”客户端,在等待 5 秒(以允许新客户端初始化并读取其本地 petable.txt)后,它会为 petable 中的每个条目在事件日志中发送消息(Actions)。所有客户端(广播客户端除外)都会读取这些消息,并根据广播信息更新它们的 petable;这包括更新现有条目以及添加它们各自 petable 中不包含的任何额外条目。
与 Inline-Execute-PE 相关的常规操作也依赖于在事件日志中发送消息。当客户端 A 运行 peload 时,会广播一条包含所有相关 petable 信息的消息;所有客户端通过使用 "on Event_Action" 钩子解析这些广播的事件日志消息来更新各自的 petable。当 peload 和 peunload 完成执行它们的 BOF 时,也会对 Inline-Execute-PE 数据进行更改;这些更改由 Beacon 传回(例如,运行 peload 后,Beacon 回调包含 pMemAddrs 结构的内存位置),因此对所有连接的客户端可见,这些客户端使用 "on Beacon_Output" 钩子更新各自的 petable。
这些独立的努力相结合,使得 Inline-Execute-PE 能够在多个客户端之间高效可靠地同步关键数据。
没有以下项目和资源的支持,本项目是不可能实现的,这些项目和资源被大量引用,并且本项目的核心部分源自它们。衷心感谢作者们的代码和远见。