导语:这篇博文描述了如何在 Excel 中实现使用 VBA (Visual Basic for Applications) 绕过微软的AMSI (Antimalware Scan Interface)。 与其他的绕过思路不同的是,这种方法不使用硬编码的偏移量或操作码,而是识别堆上的关键数据并对其进行修改。
这篇博文描述了如何在 Excel 中实现使用 VBA (Visual Basic for Applications) 绕过微软的AMSI (Antimalware Scan Interface)。 与其他的绕过思路不同的是,这种方法不使用硬编码的偏移量或操作码,而是识别堆上的关键数据并对其进行修改。 其他研究人员以前也提到过基于堆的绕过,但在撰写本文时,还没有可用的公共 PoC。 这篇博文将为读者提供关于 AMSI 实现的一些见解,以及一种绕过它的通用方法。
引言
自从微软推出 AMSI 实现以来,已经发布了许多关于绕过其实现机制的文章。 白色代码(Code White)安全团队实施红队的使用场景,其中网络钓鱼扮演了重要角色。 网络钓鱼通常与微软 Office 有关,具体来说是与用 VBA 编写的恶意脚本有关。 根据微软的 AMSI,还包括放入 MS Office 文档中的 VBA 代码。 这一事实促使我在今年早些时候做了一些研究。 已经对 AMSI 是否以及如何在 MS Office Excel 环境中被击败进行了评估。
在过去几年已经有人发表了几种不同的方法来绕过 AMSI。 以下链接是曾被我用作启发或参考的资料:
·https://modexp.wordpress.com/2019/06/03/disable-amsi-wldp-dotnet 列出了许多其他的 writeups 并实现了一个很好的基于数据的方法
·https://outflank.nl/blog/2019/04/17/bypassing-amsi-for-vba/ 用于 VBA 的 AMSI 绕过技术
上面的列表中的第一篇文章也提到了基于堆的方法。 独立于这些记载,Code White 的方法恰恰使用了这个想法。 在撰写本文期间,还没有实现这一想法的公开代码。 这是写这篇博文的另一个动机。 将这种绕过技术移植到 MS Excel /VBA 中显示了一些有待解决的难题。 以下各章按时间顺序展示了 Code White 实现的演变过程:
·用 C 语言实现我们自己的 AMSI 客户端,以便拥有一个调试平台
·理解 AMSI API 是如何工作的
·在我们自己的客户端中绕过 AMSI
·将这种方法移植到 VBA 中
·改进绕过思路
·改进绕过思路-使其能在生产环境中就绪
实现我们自己的 AMSI 客户端
为了简化调试,我们将用 C 语言实现我们自己的一个小的 AMSI 客户端,它会触发对恶意字符串 amsiutils 的扫描。 这个字符串被标记为 evil,因为马特 · 格雷伯的 AMSI 绕过方式用到了这个字符串。扫描这个简单的字符串指示了一个简单的方法来检查 AMSI 是否工作,并验证我们的绕过思路是否可行。 sinn3r 的 github 上可以找到一个即时使用的 AMSI 客户端。这段代码为我们提供了一个很好的起点,同时也包含了一些重要的提示,比如本地组策略中的前置条件。
我们将使用 Microsoft Visual Studio Community 2017 实现我们自己的测试客户端。在第一步中,我们得到两个函数,amsiInit()和amsiScan(),不要与amsi.dll 的导出函数混淆在一起。稍后,我们将添加另一个函数amsiByPass(),该函数会执行如其名称所暗示的功能。请在这个 gist 中查看最终的代码和绕过方式。
运行该程序会生成以下输出:
这意味着我们的amsiutils 被认为是恶意的字符串。 现在我们可以开始进行绕过研究了。
理解 AMSI 的结构
正如我们所承诺的那样,我们希望实现一个基于堆的绕过。 但为什么是基于堆的呢?
首先,我们必须理解,使用 AMSI API 需要初始化一个所谓的 AMSI 上下文(HAMSICONTEXT)。 必须使用函数 AMSIInitialize ()初始化此上下文。 每当我们想要扫描某些内容时,例如通过调用 AMSIScanBuffer() ,我们必须将上下文作为第一个参数进行传递。 如果此上下文背后的数据无效,相关的 AMSI 函数将调用失败。 这就是我们所追求的,但我们会在稍后再谈到这一点。
看一下 HAMSICONTEXT,我们将看到这个类型被预处理器解析为以下内容:
所以我们在这里得到的是一个指向一个叫做 HAMSICONTEXT 的结构体的指针。 让我们通过在客户端中打印“ amsiContext”的内存地址来查看这个指针指向的位置。 这将使我们能够使用 windbg 检查其内容:
变量本身的地址是0x16a144(注意我们这里是个32位的程序) ,它的内容是0x16c8b10,这就是它指向的位置。 在地址0x16c8b10处,我们看到一些内存,以标识有效 AMSI 上下文的 ASCII 字符‘AMSI'作为开头。 内存字段下面的输出是通过’!address’打印当前进程的内存布局。
在这里,我们可以看到地址0x16c8b10被分配到一个区域,从0x16c0000到0x16df0000,这个区域被标识为“堆”。 OK,这意味着AmsiInitialize() 为我们提供了一个指向堆上的结构体的指针。使用 IDA 对 AMSIInitialize() 进行更深入的研究后,提供了一些这个方面的证据:
函数使用特定的COM API——CoTaskMemAlloc ()分配16个字节(10h)。 后者是堆的抽象层。 可以点击这里及这里了解详情。 在分配缓冲区之后,魔法词 0x49534D41被写到块的开头,这就是我们用 ASCII 表示的‘ AMSI’。
值得注意的是,应用程序不能轻易地改变这种行为。 AMSI 上下文的内容将始终存储在堆中,除非攻击操作已经完成,比如将上下文复制到其他地方或实现自己的内存提供程序。 这就解释了为什么微软在其 API 文档中指明,应用程序负责在使用 AMSI 时调用 amsiunitialiize ()。 这是因为客户端不能(也不应该)释放内存,而清理过程是由 AMSI 库执行的。
现在我们明白了这一点:
·AMSI 上下文是一种重要的数据结构
·它总是被放在堆上
·它总是以 ASCII 字符‘ AMSI’ 作为开头
In case our AMSI context is corrupt, functions like 如果我们的 AMSI 上下文是损坏的,像 AmsiScanBuffer() 这样的函数将执行失败,返回值不等于零。 但损坏意味着什么,AMSIScanBuffer() 如何检测上下文是否有效? 让我们在 IDA 中检查一下:
该函数执行我们在开始时就已经指定的操作: 将 AMSI 上下文的前四个字节与值‘0x49534D41'进行比较。 如果比较失败,函数返回0x80070057,这不等于0,并告诉我们运行出错了。
在我们自己的 AMSI 客户端中绕过 AMSI
我们的基于堆的方法假设了几件事情来最终描述一个所谓的绕过:
·我们已经在 AMSI 客户端上下文中执行了代码,例如通过执行一个 VBA 脚本
· AMSI 客户端(例如 Excel)只对 AMSI 上下文初始化一次,并对每个 AMSI 操作重用该上下文
·如果 AMSIScanBuffer()函数运行失败,AMSI 客户端将检查有效载荷评级为“非恶意”
第一点不适用于我们用于测试的客户端,但也不是必需的,因为它只描述了一个测试过程,我们可以根据需要进行修改。
特别是最后一点非常重要,因为我们将尝试破坏目标进程中唯一可用的 AMSI 上下文。 如果 AMSIScanBuffer() 的失败导致负面的副作用,在最坏的情况下,程序可能崩溃,绕过将不能正常工作。
因此,我们的任务是遍历 AMSI 客户端进程的堆,寻找以‘ AMSI'开头的块,并将这个块弄乱,使所有进一步的 AMSI 操作失败。
微软提供了一个很好的代码例子 ,使用了来自kernel32.dll的两个函数遍历堆。
由于所有需要的信息都存在于用户空间中,所以人们可以通过解析内存中的数据结构来完成这项任务。 这样做会使外部函数的使用过时,但可能会破坏我们的代码,因此我们决定使用上面示例中的函数。
在将示例减少到我们需要的最小功能之后,我们最终得到了一个函数amsiByPass()。
因此,这段代码会检索当前进程的堆,遍历它并查看标记为“忙碌”的每个块。 在这些繁忙的块中,我们检查前面的字节是否与我们的魔术模式“AMSI”相匹配,如果匹配,则用一些垃圾覆盖掉它。
现在的期望是我们的有效载荷不再被标记为恶意的,但是 AMSIScanBuffer() 函数应该返回一个故障。 让我们再检查一下:
好吧,这正是我们所期待的。你不禁会问说完了吗? 不,还没有,因为我们在文章开头说明的是在 Excel利用 VBA 执行 AMSI 绕过,所以让我们继续吧。
在 Excel 中 用 VBA 绕过 AMSI
现在我们将进入 VBA 的奇怪世界。 我们将使用微软的 Office 365 MSO (16.0.11727.20222) 中的 32 位 Excel 进行测试。
在用 C 编写了一个基本 POC 之后,我们必须将这个 POC 移植到 VBA。 VBA 支持从 dll 中导入任意外部函数,因此使用我们的堆 API 应该不成问题。 就我们对 VBA 的理解而言,它不允许指针算法或直接内存访问。 这个问题可以通过导入一个允许将数据从任意内存位置复制到 VBA 变量的函数来解决。 执行此任务的一个非常常见的函数是RtlMoveMemory()。
在一些代码修改之后,我们得到了以下结果代码。
正如正如你所看到的,我们在主循环旁边放置了一些时间测量的代码。 循环的轮询次数可能达到几十万次,再加上 VBA 性能不是很好,我们预计执行完循环会花费大量的时间。 在真实的攻击场景中,时间是至关重要的。 如果我们的网络钓鱼受害者打开一个恶意的 Excel 表格,嵌入的脚本会阻止执行超过一到两秒,这是不可接受的。 至少在应用程序失去响应能力5秒之后,人们就会变得不耐烦,就会做出一些事情,比如试图关闭 Excel,这不是我们想要的。
让我们看看执行绕过需要多长时间。 说实话,我们没有想到会发生这样的事情。 结果很难重现,但测量的运行时间从15分钟到无穷无尽不等。 在一些罕见的情况下,Excel 在几分钟后关闭,没有任何进一步的通知。 这可能是因为它在太长时间内没有反应了。 然而,这并不是我们真正可以在真实场景中使用的东西。
好吧,这里出了什么问题? VBA 真的那么慢吗? 没错,它比我们的 C 代码慢了有几秒的数量级,但这并不能解释我们所经历的。 微软提供了一些内部的细节: 如何在 Excel 中实现 AMSI。 事实证明,Excel 使用了另一种策略,例如 PowerShell。 后者或多或少地将整个脚本发送到 AMSIScanBuffer()。 实现了一个基于所谓触发器的更聪明的方法。 微软认为纯 VBA 代码在导入之前是无害的。 这正是我们要做的——从外部 dll 中导入函数。 其中一些导入函数被视为具有潜在危险性的函数,它们的调用(包括所有参数)被放入一个环形缓冲区,该缓冲区被发送到 AMSI。 这使得像微软的 defender 这样的反病毒解决方案有机会检查地址背后的数据,这当然是有意义的。 让我们看看在我们的例子中,通过使用 windbg 破解 AMSIScanBuffer,哪些数据被发送到了 AMSI API:
我们可以看到,环形缓冲区包含我们导入的所有函数,包括它们的参数值。 我们的 Windows 10系统已经安装并激活了微软的Defender。 所以每次调用 AMSIScanBuffer() 都会触发 Defender。 AMSI 首先实现为进程内 COM。 但是为了最终与其他反病毒解决方案进行通信,它必须将数据传输出进程并执行上下文切换。 这可以在下一个微软提供的架构概述中看到:
来源:https://docs.microsoft.com/en-us/windows/win32/amsi/how-amsi-helps
图底部的绿色小块显示我们的进程(Excel)通过 rpc 与 Defender 间接通信。 嗯… … 好吧,这个事情做完要有个10万次吧,这花费的实际太多了,不过也解释了为什么需要如此长的运行时。 为了提供一些更多的证据,我们关闭了 Defender 然后重复执绕过,这应该会显著加快执行我们的绕过代码。 除此之外,我们还监视对 AMSIScanBuffer() 的调用量,这样我们就可以得到调用 AMSIScanBuffer() 的频率。
同样的循环在禁用了 Defender 的情况下需要一到两分钟:
在另一次运行中,我们使用 windbg 来检查对 AMSIScanBuffer() 的调用量:
AMSIscanbuffer()被调用了124624次(0x10000000-0xffe1930) ,大致相当于我们的循环所做的迭代次数。 这次数已经很多了,并强调了我们的假设,AMSI 确实被非常频繁的调用。 因此,我们理解正在发生什么,但目前似乎没有可用的变通方法来解决我们的运行时问题
现在就放弃吗?-当然不..
在 Excel 中改进 AMSI 绕过思路
正如上面一章所描述的,我们目前的方法运行速度太慢,无法在实际场景中使用。 那么,我们能做些什么来改善这种情况呢?
我们导入的函数之一是 RtlMoveMemory() ,正如前面提到的,很多恶意软件都使用这个函数。 监视这个函数会有很大的意义,它可能被认为是触发器。 让我们通过删除对 CopyMem (RtlMoveMemory 的别名)的调用来验证这一点,然后看看会发生什么。 这阻止了我们绕过工作,但它可能给我们一些见解。
运行时间现在是0.8秒。 哇,好吧,这真的改变了很多。 需要注意的是,在这种配置中,我们甚至遍历了整个堆。 由于缺少对 RtlMoveMemory() 函数的调用,我们将无法找到我们的模式。
在我们确定了瓶颈之后,我们能做什么? 我们必须找到一种替代方法来访问没有被 Excel 视为触发器的原始内存。 一些随机的谷歌搜索显示了可以使用函数——CryptBinaryToStringA() : 该函数旨在将字符串从一种格式转换为另一种格式,但它也可以仅用于从我们指定的任意内存位置复制字节。 太酷了,这正是我们要找的! 为了达到我们的目的,我们可以调用这个函数就像从 Process Heap Entry 结构中读取 lpData 字段:
输入参数从左到右解释如下:
·Lpdata 是我们要复制数据的源,
·ByVal 4 是我们要复制的字节长度(在我们的32位 Excel 中,lpData 是32位的)
·Byval 2 是我们要复制的原始二进制文件(CRYPT_STRING_BINARY 隐藏字符串二进制)
·ByVal VarPtr (magicWord)是我们想要将内存复制到的目标(我们的 VBA 变量 magicWord)
·最后一个参数(ByVal VarPtr (bytesWritten))告诉我们实际复制了多少字节
替换所有的 RtlMoveMemory() ,并再次检查我们的绕过需要多长时间。 你可以 在这里找到源代码的更新版本.
我们的循环现在需要大约四秒钟完成,时间仍然很长,但绕过成功完成了,这告诉我们,我们找到了我们正在寻找的模式。 让我们看看 Excel 现在使用的这个版本中调用 AMSIScanBuffer() 的次数:
哦,天哪… Excel 根本没有调用 AMSIScanBuffer()。 因此,这意味着只要我们的代码中没有触发器,就不会向 AMSI 发送任何内容。 或者反过来: 只要我们使用一个触发器函数,Excel 就会将所有调用发送到 AMSI。 很高兴我们知道到了这一点..
这是我们第一次有能力真正验证绕过是否有效。 因此,让我们从 VBA 中寻找一些触发 AMSI 的代码 Iliya Dafchev 之前提出了一个较旧的使用 VBA 绕过 AMSI 的方法,这个方法首先通过 AMSI 本身获得标志。 完美,我们将把这个代码放到一个函数中,这个函数叫做triggerAMSI() 然后用它做测试:
在运行 Excel 之后,会像预期的那样出现一个警告,然后会关闭当前的 Excel 实例:
Excel 中的 AMSI 弹框
把我们的绕过和测试放在一起,我们得到了以下函数:
希望很大,会弹出包含“we survived”的消息框,因为我们在触发它之前已经干掉了AMSI。
太好了,我们的绕过好像起作用了。 所以让我们把这个放到我们真正的网络钓鱼活动中。 嗯。 等等,整个过程花了多长时间? 四秒钟? 重复执行绕过甚至会显示运行时间大于10秒。 哦,不,时间还是太长了。
现在就放弃吗?-当然不..
在 Excel 中改进 AMSI 绕过运行时间
在最后一章,我们改进了我们的 AMSI 绕过,将运行时间从无穷无尽下降到十秒或以下。 这对于一场真正的竞赛(我们的观点)来说似乎还是太多了。 那么,我们能做些什么来加快整个运行过程呢?
循环需要大约10万次的迭代,这在 C 语言中是可以很快完成的。 防守队完全出局了。 因此,我们当前的运行时间似乎是不好的 VBA 性能导致的纯粹结果。 公平地说,我们目前正在尝试做的这些事情并不是一个典型的 VBA 任务,所以让我们指责我们自己而不是 Excel 做了一些疯狂的事情。
不管怎样,我们现在能做什么呢? 在 VBA 中编写 C 语言代码不是一种选择,但是调用一些 shellcode 怎么样呢? 只要我们能导入任意函数,shellcode 的执行应该不成问题。 这个代码片段给出了如何在 VBA 中实现这一点的示例。 下一步是将 VBA 代码(或者更多的初始 C 代码)转换成汇编语言,也就是说,转换成 shellcode。
每个曾经编写过一些 shellcode 并希望从 dll 调用函数的人都知道,在组装 shellcode期间,这些函数的绝对地址是未知的。 这意味着我们必须实现这样的机制,通过 GetProcAddress() 在运行时查找所需函数的地址。 如何在没有任何库支持的情况下做到这一点呢?这其中涉及到的技术细节已经得到了充分的理解并且已经有相当多的文档详细说明了实现方法,因此我们不会在这里详细讨论。 实现这部分的 shellcode 留给读者作为练习。
当然,有许多可以直接使用的代码片段可以完成这项工作,但是我们决定自己实现这些 shellcode。 为什么? 因为它很有趣,而且是自己编写的代码,所以不太可能被反病毒解决方案查杀。
我们使用汇编语言改成了 AMSI 绕过代码的主循环,汇编代码有效载荷可以在这里找到。
ShellCodeEnvironment 结构体包含了一些重要信息,比如 HeapWalk()和 GetProcessHeaps() 函数的查找地址。 循环的其余部分是可以直接复用的..
因此,把所有东西放在一起,我们生成 shellcode,并把它放到 VBA 代码中,然后以新线程的形式启动它。 当然,我们需要再次计算运行时间:
这次只有0.02秒
我们认为这个结果是可以接受的。 运行时可能会根据处理器负载或堆的总大小而有所不同,但它应该远远低于我们最初的目标——一秒。
总结
我们希望你能喜欢这篇博文。 我们展示了在 VBA 中基于堆的 AMSI 绕过思路的可行性。 同样的方法,带有轻微的适配,也适用于 PowerShell 和 .Net 4.8。 后者也与 AMSI 的支持集成在其通用语言运行库。 正如微软的 AMSI 不是一个安全边界,所以我们并不期待这么多的反应,但是我们仍然很好奇微软是否会为这个想法开发一些检测机制。