利用浏览器漏洞绕过 Windows Defender 应用程序控制 (WDAC)

利用浏览器漏洞绕过 Windows Defender 应用程序控制 (WDAC)

Ots安全 2025-05-15 07:30

Windows Defender 应用程序控制 (WDAC)是 一项Windows 安全功能,可帮助防止未经授权的代码(例如恶意软件或不受信任的可执行文件和脚本)在系统上运行。它是一种应用程序白名单机制,可强制执行仅允许明确受信任的可执行文件、脚本和驱动程序在系统上运行的策略。它常用于安全性和系统完整性至关重要的高可信度或严格控制的环境,例如 X-Force Red 对手模拟团队正在进行测试的环境。

几周前,我的同事 Bobby Cooke 发表了一篇博客文章:
https://www.ibm.com/think/x-force/bypassing-windows-defender-application-control-loki-c2,详细介绍了一种通过在受信任的 Electron 应用程序上植入后门来绕过最严格的 WDAC 策略的方法。我强烈建议您阅读他的博客文章,以了解 Electron 应用程序如何使用 Node.js 以及如何对其进行后门攻击。

作为研究的一部分,他还开源了基于 Node.js 的命令和控制框架Loki C2。得益于 Bobby 和 Dylan Tran 在 Loki C2 开发方面的出色工作,X-Force 对手模拟团队成功在使用 WDAC 的强化环境中获得了代码执行权限。

那么,这项研究的意义何在?上述技术确实存在一个缺点:只能执行 JavaScript 代码,无法执行原生代码,例如加载 DLL 或运行 EXE 文件。此外,也无法执行 Shellcode 来启动第二阶段 C2 负载。本文将介绍我们用来绕过这些限制的技术。

首先,我和 Bobby 开始对 Electron 应用程序加载的已签名 Node.js 模块进行逆向工程,寻找可能允许低级指令级代码执行的漏洞。经过一番初步探索,并在jeffssh的建议下,我的注意力转向了 Node.js 和 Chrome 使用的 V8 引擎。

自带易受攻击的应用程序

与其在 Node.js 模块中寻找漏洞,不如使用 N-day 漏洞利用 V8 引擎呢?

这种攻击场景很常见:携带一个易受攻击但受信任的二进制文件,并利用其受信任这一事实在系统上站稳脚跟。在本例中,我们使用一个受信任的 Electron 应用程序,该应用程序运行一个易受攻击的 V8 版本,将 main.js 替换为一个执行第二阶段的 V8 漏洞利用代码作为有效载荷,这样我们就实现了原生 Shellcode 的执行。如果被利用的应用程序已列入白名单/由受信任的实体(例如 Microsoft)签名,并且通常允许在所采用的 WDAC 策略下运行,那么它就可以被用作恶意有效载荷的载体

除了能够自由执行 Shellcode 之外,这种方法还具有在类似浏览器进程的上下文中执行 Shellcode 的优势,这很有优势。一些行为可能会被 EDR 标记为可疑,但对于浏览器来说却很正常,例如将 RWX 内存映射到即时 (JIT) 代码。

先前的工作

这种方法看起来很简单,但我确实有一些疑问。公开的 Chrome V8 N-day 漏洞真的能在 Electron 应用中发挥作用吗?Chrome 中使用的 V8 引擎与 Node.js 中的引擎有何不同?漏洞利用需要进行哪些修改?我该如何调试它?

事实证明,目前已经有关于在 Electron 应用程序中利用 V8 漏洞的公开研究,但令我非常难过的是,直到我完成这项工作后才发现。Turb0 出色地介绍了将公开的 v8 漏洞及其相应的读/写原语改编到 Electron 应用程序内部的(有点痛苦的)过程。Turb0 的博客文章已经涵盖了我必须处理的许多深入的技术细节,我强烈建议您查看一下。这篇博文的其余部分将重点介绍漏洞开发周期的剩余阶段,因为它涉及针对 Windows 的目标,具体目标是创建 WDAC 绕过,以及我在实际使用中操作漏洞时遇到的问题。

版本定位

我首先要做的就是确定确切的目标。我需要选择一个受信任的 Electron 应用程序,并选择一个漏洞来利用它。在此之前,我的浏览器漏洞利用经验非常少,所以所选的漏洞应该有一个公开的漏洞利用代码可以作为起点。

我不确定 V8 版本与 Electron 使用的 V8 版本如何对应,也不确定 Electron 是否真的存在漏洞。Electron 的 V8 版本通常落后于 Chrome V8 的最新版本。Electron 的维护人员会将新版本中的重要安全补丁反向移植到他们为特定 Electron 版本冻结的版本中。这意味着即使 Electron 使用的是旧版本的 V8,也不一定意味着它容易受到漏洞的影响,因为修复程序可能已经反向移植了。他们精心挑选的补丁存储在这里。

我决定最简单的方法是利用应用程序版本发布后才修补的漏洞。这样一来,该应用程序版本就绝对不可能被修补了。经过一番挖掘,我找到了过去两年左右  VSCode版本的下载量。我找到了相当多的易受攻击的 Microsoft 签名应用程序可供选择😊。

构建和调试

首先,我简单地拿了一个最近公开的 V8 漏洞 PoC,用它对易受攻击的 Electron 应用进行了后门攻击,用漏洞替换了 main.js,然后祈祷一切顺利。也许就这么简单,对吧?我原本希望至少会崩溃。不出所料,启动应用后什么也没发生。虽然很不情愿,但我知道我需要构建 V8 才能更深入地了解发生了什么。通过自己构建 V8,我可以构建调试版本 (d8),深入了解漏洞,然后根据我所针对的特定版本进行调整。

何时何地发生什么?——利用不同的操作系统和版本

我的首要目标是建立一个“基本事实”——复制已知漏洞能够生效的准确环境。然后,我可以检查该版本与目标版本之间的差异,以了解问题所在。

我发现的大多数公开的 V8 漏洞都针对 Linux。因此,我首先在 Linux 上编译了 V8,并检查了我选择的公开漏洞所针对的提交。然后,我运行了漏洞,以确保它有效。谢天谢地,它成功了。现在我找到了真相。

之后,我在 Linux 上编译了目标版本的 V8(与 Electron 应用使用的版本相同)。漏洞利用并没有立即奏效。自己构建项目的好处是,你可以根据需要对代码进行尽可能多的自省。具体来说,V8 拥有 d8,它是V8 JavaScript 引擎的独立 shell,主要用于在浏览器或 Node.js 环境之外测试、调试和运行 JavaScript 和 WebAssembly 代码。d8 具有通过 –allow-natives-syntax 标志启用的内部调试功能。特别是 %DebugPrint(value),它会打印 V8 引擎内部值的内部标记表示,包括其在内存中的地址。

这样,我就可以打印出目标对象的地址,并调整公开漏洞利用代码的硬编码偏移量。现在我终于有所进展了。我只需要把我的漏洞利用代码移植到 Windows 上。

在 Windows 上编译旧版 V8 让我头疼不已。我需要修复一堆依赖项问题,所以做了一些可疑的内部代码修改。现在我完全想不起那些细节了——为了保护自己,我的大脑把它们屏蔽了。经过几个小时的努力,我终于编译出了我需要的版本!令我惊讶的是,修改后的 Linux 漏洞在 Windows 上竟然无需任何调整就能正常工作。

现在,剩下的就是在 Electron 应用上测试漏洞利用程序,屏住呼吸……哎呀,没成功!但为什么呢?

起初,我满怀希望,因为目标程序确实崩溃了。毕竟,我还没有将 Linux 的 Payload 适配到 Windows 系统,所以没指望会发生任何有趣的事情。为了确认行为,我将漏洞利用 Payload 改为在地址 0x4141414141 处执行。这是漏洞利用程序编写者常用的一种技巧,他们通过控制指令指针地址来查看/证明自己已经获得了程序的控制权。然而,在 WinDbg 中查看崩溃信息后,我并没有看到我想要的结果。我在覆盖目标函数指针时遇到了段错误。

还记得我之前提到的 Electron 通过 Cherry Pick 漏洞利用 V8 提交的内容吗?事实证明,尽管该应用程序容易受到我用来利用的漏洞的攻击,但公开漏洞利用所使用的沙盒逃逸方法已经通过 Cherry Pick 漏洞修复。如果您不熟悉 V8 沙盒/内存笼,可以在这里阅读相关内容。本质上,这是一种在存在漏洞的情况下使 V8 漏洞利用更加困难的方法。

为了弄清楚发生了什么,我需要再次构建目标版本的 V8,这次应用的是精心挑选的补丁。除了安全补丁之外,Node.js 还会将特定的 Node.js 补丁应用到 Electron 使用的 V8 版本上。我花了很长时间才意识到我需要这样做,因为 Electron 和 Node.js 如何处理它们之间的各种依赖关系当时并不清楚。

经过一两天的努力,我尝试确保编译的 V8 版本与目标版本完全一致,并查阅了最新的沙盒逃逸技术,终于取得了进展。我找到了一种适用于目标的逃逸技术。调整漏洞利用代码后,我终于能够通过控制指令指针来使应用程序崩溃。这真是一场甜蜜的胜利,我终于看到了终点……

使用实际有效载荷进行漏洞利用

此时,剩下要做的就是修改公开的漏洞利用载荷,使其运行我们的 C2 载荷。这个看似简单的修改,结果却比我想象的更棘手。公开漏洞利用的 Linux 载荷很简单,就是弹出一个只有几个字节大小的 Shell。而 C2 载荷……比这大得多。

如果你了解shellcode的编写,你就会知道在Windows中编写shellcode比在Linux中编写shellcode更麻烦,主要是因为没有像在Linux中那样简单的方法可以以位置无关的方式直接进行系统调用。payload还需要被“JOP走私”到一个浮点数组中:

图1:JOP走私浮点数组,shellcode执行/bin/sh

显然,整个阶段的 C2 有效载荷(大小达数千字节)无法像这样执行。因此,我需要编写一个引导有效载荷,它将映射一个可执行页面,将最终有效载荷复制到该页面,然后跳转到该页面。

论据走私

引导载荷的问题在于,虽然我控制了程序,但我无法将参数传递给执行的载荷。因此,我走私的shellcode无法知道最终载荷的地址。我通过一种名为“参数走私”的方法解决了这个问题。

我知道被覆盖的 JSFunction 对象的地址会存储在 rcx 寄存器中。因此,我利用任意写入原语,将映射的页面存储在该对象一个不需要的字段中。这需要反复试验,因为覆盖某些偏移量会导致崩溃。我对要复制的值以及复制位置的偏移量也做了同样的操作。字段的偏移量可以硬编码到 shellcode 中,这样它就知道从哪里复制有效载荷了。我调用了有效载荷 na 次,其中 n 是要复制的字节数。

TurboFan JIT 优化

TurboFan,V8 的优化编译器,给我的计划带来了一些阻碍。由于 TurboFan 的优化,如果走私指令序列被转换成多个相同值的浮点数,内存中只会出现一个该值的实例。这限制了指令的重复频率。我通过尽可能紧凑地编写 shellcode 来解决这个问题,并且如果确实需要重复某条指令,我会改变走私指令的位置,这样浮点数的值就会不同,并且不会出现重复的条目。

如果第二阶段的有效载荷过大,复制shellcode时也会遇到问题,这可能是因为我需要多次调用相同的stomped JSFunction和TurboFan,我尝试优化这个问题。最终,我通过将多个循环复制粘贴到“WriteShellcode”而不是一个大循环来解决这个问题。虽然代码丑陋得可怕,但总算成功了!后来,Bobby和Dylan将C2有效载荷换成了一个阶段程序,它可以从blob存储中检索更大的有效载荷,这样最终的有效载荷就不需要存储在磁盘上了。这也有助于将main.js文件的大小控制在合理的范围内。

偏移不一致

在实际操作中使用漏洞的准备工作中,始终应该包含在不同环境下的测试。就此次攻击而言,我们并不知道有效载荷将在何种环境下执行,只知道它运行在 Windows 系统上,并且很可能启用了 WDAC。因此,漏洞利用需要在任何操作系统上都能正常工作。我确信,由于应用程序的 V8 版本和所有依赖项都包含在应用程序中,因此不会遇到太大的变化。然而,我的这个假设是错误的。

由于我不明白的原因,易受攻击的函数指针的偏移量在不同 Windows 版本之间发生了变化。这说不通,因为据我所知,偏移距离是由 V8 JIT 引擎决定的,而该引擎的库是直接从应用程序包中加载的。这意味着无论操作系统是什么,都会加载完全相同的 V8 库。更令人困惑的是,这种变化似乎没有任何规律可循。在某些 Windows 版本(包括旧版本和新版本)上,偏移量有时会偏离 4 个字节。这尤其令人恼火,因为据我所知,根本无法从 JavaScript 漏洞利用程序中获取正确的偏移量。计算它的唯一方法是利用调试 shell 读取内存地址并进行计算,但这显然不是在生产环境的 Electron 应用程序中可以实现的。简而言之:在漏洞利用程序运行时无法计算偏移量的变化。

即时漏洞利用工程

为了解决偏移量不一致的问题,Bobby 和 Dylan 重新设计了漏洞利用代码,让 main.js 多次启动漏洞利用程序,尝试不同的偏移量,直到成功。这是通过让初始代码进程执行循环来实现的。该循环会生成子进程,这些子进程会尝试使用唯一的偏移量进行漏洞利用。如果漏洞利用失败,子进程将被终止。如果漏洞利用成功,则 Shellcode 会在部署第二阶段 C2 之前执行并写入一个 Mutex 文件。漏洞利用成功后,初始进程将退出循环并永久休眠。

图 2:操作化漏洞执行流程

虽然这意味着错误的偏移尝试会导致崩溃,但我们的测试表明,用户没有看到任何可见的错误,应用程序功能仍然能够无缝运行。虽然这不是最干净的解决方案,而且由于崩溃而有些嘈杂,但时间至关重要。这就是我们在业务中所说的“JIT xdev”,它完美地满足了我们的需求。

JS 混淆和 CI/CD 有效载荷

显然,如果我们被发现并且有人分析了应用程序的 main.js 入口点,我们不希望漏洞变得显而易见。为了避免这种情况,我们在漏洞代码上应用了JavaScript 混淆器,这使得它几乎人眼无法理解。感谢维护团队有效载荷 CI/CD 管道的Chris Spehn的才华和奉献精神,我们能够简化此有效载荷的交付,并在每次生成有效载荷时重新混淆代码,因此我们每次都可以使用不同的漏洞代码无限期地重复使用应用程序。这使得有效载荷无法被签名。这被证明特别有用,因为不幸的是,我们第一次尝试使用此功能时,我们被抓住了,因为用户标记了网络钓鱼电子邮件🙁。有趣的是,虽然客户的蓝队从网络钓鱼电子邮件中分析了应用程序,但他们并没有收集到应用程序的用途,也没有识别出嵌入的 V8 漏洞。

尚待解决的问题

我还是不太明白为什么 JIT 编译后的函数偏移量与操作系统相关,因为所有相关的 V8 库都应该捆绑在 Electron 应用程序中。如果有人知道原因,请告诉我!

未来的安全考虑

Electron 推出了一项实验性完整性功能,可在运行时验证应用程序所有文件的完整性。该功能已在 macOS 16 及以上版本和 Windows 30 及以上版本中推出。应用程序开发者可以启用此 Electron 融合功能,以确保所有应用程序文件均未被篡改。如果文件被篡改,进程将自动退出,不会执行任何操作。

此功能可防止修改 Electron 应用的任何打包文件(包括 main.js),并阻止上述技术。然而,目前大多数主流应用程序尚未实现此功能。即使该功能得到更广泛的应用,也需要注意的是,旧版本的应用程序(未启用完整性保护)仍然容易受到攻击。

感谢您抽出

.

.

来阅读本文

点它,分享点赞在看都在这里