最新的漏洞利用开始渐渐脱离基于 ROP 的代码重用攻击。在过去的两年里,出现了一些关于一种新的代码重用攻击的文章, Counterfeit Object-Oriented Programming (COOP)。 COOP 是一种顶级的针对 forward-edge 的执行流完整性 (CFI) 的攻击方式。在我们把 CFI 解决方案 (HA-FI) 整合进我们的终端产品中时,这种攻击吸引了我们的注意力。 COOP 主要出现在学术界,还没有出现在 exloit 工具 包里。这也许是因为攻击者更趋向于使用更简单的方法。在 win10 年度更新中微软的 Edge 使用了 执行流保护 (CFG , Control Flow Guard) 。在 CFG 中,缺少 backward-edge 的 CFI 更容易受到攻击。但是当 Return Flow Guard (RFG) 出现,使得攻击者不能再依靠淹没栈中的返回地址进行攻击的时候,会发生些什么呢?
我们对评估 COOP 在攻击 CFI 时的效果很感兴趣。这不仅可以使我们保持在学术界和黑客社区中前沿研究中的地位,也可以测试产品的有效性,更改设计,甚至在必要时普遍地提高我们自己的防御能力。在我们的这一系列的两篇博文中的第一篇中,介绍了我们使用 COOP 函数重用对微软的 CFG 以及我们自己的 HA-CFI 进行攻击的评测。
微软执行流保护
已经有大量的论文,博文和会议发言充分地讨论了微软的执行流保护( CFG , Microsoft’s Control Flow Guard )。 Trail of Bits 在两篇最近的帖子里比较了 Clang CFI 和微软 CFG 。第一篇帖子着重 Clang ,第二篇强调微软对 CFI 的实现,还有额外的研究提供了 CFG 的实现的进一步细节。
在过去的几年里,绕过 CFG 也成为了安全会议中中一个流行的主题。在我们引用一些著名的绕过方法之前,最重要的是 CFI 能够进一步分为两种: forward-edge 和 backward-edge 。
Forward-Edge CFI : 保护间接调用或是 JMP 位置 . Forward-edge CFI 解决方案包括微软 CFG 和 Endgame 的 HA-CFI.
Backward-Edge CFI : 保护返回指令 . Backward-edge CFI 解决方案包括微软的 Return Flow Guard,Endgame 的 DBI exploit 防护的一部分 , 以及包括 intel 的 CET 在内的其他 ROP 检测。
这个分类帮助我们描绘出了 CFG 保护位置的轮廓——间接调用位置——以及不打算保护的位置——栈返回地址。例如,一个最近的 POC 入选了 exploit 工具包,这个 POC 针对 Edge ,使用读 / 写的原始方法来修改栈中的返回地址。但这并不适用于 CFG ,不应该作为 CFG 的弱点来考虑。尽管如此,它成功地证明了 CFG 的有效性,并使攻击者转向劫持执行流,而不是间接调用的位置。这个例子实际上证明了 CFG 缺陷包括以下几点:利用未受保护的函数调用位置,重映射包括 CFG 代码在内的只读内存区域,并使他们指向需要受到检查的代码,在 Charka 中提到的 JIT 编码器的资源竞争,使用基于内存的间接调用。 COOP 或是函数重用攻击,在面对 CFI 的实现时有着公认的局限,因为 “limitations of coarse-grained CFI” ,他们并没有入选微软的 bypass 赏金。也就是说,我们不知道有哪些公有领域的 POCs 能证明 COOP 能指定攻击 CFG 的加固的二进制代码。
CFG 对每个受保护的 DLL 添加了一个 __guard_fids_table ,它由一系列在二进制代码中合法的 RVAs 或是间接调用指令中敏感的目的地址组成。一个地址作为 CFG bitmap 索引的一部分而存在。 bitmap 里的 bits 能够根据地址是否是合法的目的地址而进行切换。在此之外,也有一个 API 能够对 bitmap 进行修改,例如,为了支持 JIT 编码的页面:
kernelbase!SetProcessValidCallTargets 在使用系统调用更新 bitmap 之前会 调用 ntdll!SetInformationVirtualMemory
win10 创意者更新有一项新增的功能可以抑制导出,也就是说,现在导出函数能在 CFG 保护的调用位置被标记为非法目的地址。这一功能的实现需要使用 CFG Bitmap 中每一个地址的第二位,以及在初始化每个进程的 bitmap 时 __guard_fids_table 中每一个 RVA 条目的一个标记字节。
对于 64 位的系统,地址的第 9-63 位被用于在 CFGbimap 中检索一个 qword ,第 3-10 位被用于(模 64 )访问 qword 中某一指定位。在导出被抑制后, CFG 允许一个给定的地址在 CFG bitmap 中用两位表示。此外,在大多数 DLLs 中 __guard_dispatch_icall_fptr 现在被设置为指向 ntdll!LdrpDispatchUserCallTargetES ,在其中一个合法的调用目标必须从 CFG bitmap 中删去。
当你把动态解析符号表考虑进去的时候,实现这样一个导出表抑制变得有点复杂,因为使用 GetProcAddress 意味着随后的代码也能调用返回值作为函数指针。只要 CFG bitmap 中每一个条目没有被标记为敏感的或是不合法的(例如, VirtualProtect, SetProcessValidCallTargets 等等),执行流保护可以通过把条目对应的两位从“ 10 ”(导出表抑制)改为“ 01 ”(合法的调用位置),解决这个问题。最后,一些导出表将会在进行创建时以不合法的间接调用开始,但最终在运行时代码中成为合法的调用目的地址。在今后我们的讨论中,这尤为重要。当这一情况发生时,一个调用栈的样例如下:
00 nt!NtSetInformationVirtualMemory
01 nt!setjmpex
02 ntdll!NtSetInformationVirtualMemory
03 ntdll!RtlpGuardGrantSuppressedCallAccess
04 ntdll!RtlGuardGrantSuppressedCallAccess
05 ntdll!LdrGetProcedureAddressForCaller
06 KERNELBASE!GetProcAddress
07 USER32!InitializeImmEntryTable
COOP 概要
Schuster et al. 认为 COOP 是 CFI 实现的一个潜在的弱点。为了在绕过 forward-edge CFI 的检查之后执行代码,我们可以利用连续的攻击序列和重用已存在的虚函数。在 ROP 在有一个相似的方法,其结果是一系列小段合法函数,每一段代码实现最低限度的功能(例如,载入一个值进 RDX 中),但把它们组合在一起,却可以实现一些复杂的任务。 COOP 的一个基本组成部分就是利用主循环函数,在其中可以迭代对象链表或数组,调用每个对象中的虚函数。然后,攻击者把内存中“伪装”的对象组合起来,在某些情况下,可能会覆盖对象,这样就能在主循环中按攻击者安排好的顺序调用合法的虚函数。 Schuster et al. 证明了使用 COOP payloads 的攻击 win7 32 位和 64 位上的 IE10 ,以及 Linux 64 位上的 Firefox 的方法。这项研究随后被扩展了,证明了递归或是带有许多非直接调用的函数也可以实现这一过程,而不仅仅是循环。随后又继续被扩展到用于攻击 Objective-C 运行时环境。
这项前沿研究极其有趣和新奇。我们想要把这一概念应用到一些现代的 CFI 实现上,以对如下方案进行评估: a) 在加固的浏览器中构造一个 COOP payload 的难度; b) 是否能绕过 CFG 和 HA-CFI ; c) 是否能改进 CFI 使其能检测到 COOP 类型的攻击。
我们的目标
我们使用 COOP 主要的目标是 win10 的 Edge ,因为它代表着一个全新的 CFG 加固应用,并且它能让我们在内存中使用 JavaScript 来准备我们的 COOP payload 。弱点始终是我们小组的兴趣,为了这个目标,我们专注于劫持 CFI 的执行流,并对攻击者作出了下列假设:
1. 任意的读 - 写原语都是从 JavaScript 中获得的。
2. 因为在运行时动态地找到小段代码不是这项研究的内容,因此,允许使用硬编码偏移量。
3. 所有微软创意者更新中最近的防御机制都能被使用(例如, ACG , CIG ,带导出表抑制的 CFG )。
4. 除了使用 COOP 以外,攻击者不允许以任何方式绕过 CFG 。
在我们最初的研究里,我们在对微软年度更新( OS build 14393.953 )中的 Edge 的研究中利用了一个 Theori 中的 POC ,我们使用创意者更新中的防御机制设计我们的 payload ,并在开启导出表抑制的 win10 创意者更新( OS build 15063.138 )中对其进行验证。
一个理想的 POC 会执行一些攻击者的 shellcode 或是启动一个应用程序。攻击者的一个经典的代码执行模型,就是把一些内存中被控制的数据映射为 +X ,然后跳转到包含最新修改过的 +X 区域的 shellcode 。然后,我们的真实目的是在 forward-edge CFI 的保护下, 产生一个能够执行一些有意义的代码的 COOP payloads 。这样一个 payload 提供了能够进行测试和改善我们的 CFI 算法的数据。进一步说,攻击 Arbitrary Code Guard (ACG) 或是 Edge 的子进程的办法超出了我们的研究范围。我们确定对于 win10 创意者更新研究的最终目标是使用 COOP 来使 CFG 无效,使得在 DLL 内能够跳转或是调用任意位置的代码。因此,我们总结出下面两个主要的 COOP payloads :
1. 对于 win10 年度更新,以及缺少 ACG 保护的程序,我们的 payload 把我们的控制的数据映射为可执行的代码,在使得 CFG 无效后跳转到我们控制的 shellcode 所在区域。
2. 对于 win10 创意者更新,我们的最终目标是仅仅是使 CFG 无效。
寻找 COOP 片段
下列 Schuster et al. 设想的蓝图,我们的第一业务是商定 COOP 各个组成部分的术语。学术论文将每个重用函数称为虚函数片段( virtual function gadget )或是 vfgadget ,当我们描述每一个特定类型的 vfgadget 时使用缩写,例如将主循环( main loop ) vfgadget 称为 ML-G 。我们选择以更为非正式的方式来命令每种类型的 gadget 。在接下来的帖子中你能找到的术语定义如下:
Looper :对于执行复杂 COOPpayloads (论文中的 ML-G )至关重要的主循环 gadget 。
Invoker :一个调用 vfgadget 的函数指针。(论文中的 INV-G )
Arg Populator :带一个参数的虚拟函数,它将一个值加载到寄存器中(论文中的 LOAD-R64-G ),或是移动栈指针或是把值加载进栈中(论文中的 MOVE-SP-G )
与论文相似,我们编写了脚本来帮助我们识别二进制中的 vfgadgets 。我们使用了 IPA Python ,推理帮助我们找到了 loopers , invokers 和 argument pupulators 。在我们的研究中,我们发现了实现 COOP 的实用的方法就是,在返回到 JavaScript 之前,把 vfgadgets 链接到一起并依次执行少量的 vfgadgets 。根据需要通过额外的 COOP payloads 重复这个过程。因此,为了我们的目的,我们发现没有必要将二进制代码提升到 IR 。然而,将大量 COOP payload 拼接到一起,比如说完全通过重用代码运行一个 C2 socket 线程,也许会需要提升到 IR 。对于 vfgadget 的每个子类型,我们定义了一系列规则,并使用它在 Edge ( chakra.dll 和 edgehtml.dll )的两个二进制文件间进行搜索。这些规则中与 looper vfgadget 相关的一部分包括:
1. 出现在 __guard_fids_table 中的函数
2. 包含一个不带参数的间接调用的循环
3. 循环不能影响到参数寄存器
在 vfgadgets 的所有类中,搜索 loopers 是最耗时的。许多潜在的 loopers 有一些限制使其难以使用。我们寻找到的 invokers 不仅需要有调用虚函数指针的 vfgadgets ,还要能够在单一的 counterfeit 对象中,一次性又快又容易地填充六个参数的 vfgadgets 。因此,当尝试调用单个 API 时, COOP 可以使用快捷方式,完全避免对循环和递归的需求,除非需要返回值。在 x64 程序上能够找到许多寄存器对参数寄存器进行填充。值得一提的是, Schuster et al. 的 COOP 论文中根据 mshtml 提出的大量原始 vfgadgets 仍然能在 edgehtml 中找到。然而,我们在我们的成果中添加了一个要求来避免重用这些,而不是为我们的 COOP payloads 寻找新的 vfgadgets 。
COOP Payloads
通过脚本语言触发 COOP ,我们实际上能把一些复杂的任务从 COOP 中移开,因为一次性把所有东西拼接在一起非常的复杂。我们能使用 JavaScript 来帮助我们,重复调用微型 COOP payload 序列。这也让我们能把诸如算术和条件操作放回 JavaScript 中执行,并保留基本的函数重用来为通过 COOP 调用重要的 API 做准备。此外,我们展示了这种方法的一个例子,包括在我们劫持到的 #1 section 中将 COOP 的返回值传回到 JavaScript ,并讨论如何调用 LoadLibrary 。
为了简洁,我将只介绍最简单的 payloads 。 payloads 的一个公共的主题是需要调用 VirtualProtect 。因为 VirtualProtect 和 eshims (译者注:应该是 ieshims ) APIs 被标记为敏感的且在 CFG 中并不是一个合法的目的地址,我们不得不在创意者更新中使用包装函数。正如 Thomas Garnier 所建议的那样,可以在 .net 库 mscoree.dll 和 mscories.dll 中方便地找到包装函数,例如 UtilExecutionEngine::ClrVirtualProtect 。因为微软的 ACG 可以防止创建新的可执行内存,以及把已有可执行内存改为可写,因此,我们需要一个替代方法。使用 VitualProtect 可以把只读内存重映射为可写的,所以我借用了 2015 年黑帽大会里介绍的这种技术,并将包含 chakra ! __guard_dispatch_icall_fptr 的页面重新映射为可写,然后重写函数指针,使其指向包含 jmp rax 指令的 chakra.dll 中的任意位置。事实上,在大多数 DLL 中已经存在一个函数 __guard_dispatch_icall_nop ,它刚好就是一个单一的 jmp rax 指令。因此,我就能有效地绕过 CFG 的保护,因为在通过了所有检查之后,在 chakra.dll 中所有被保护的调用位置将立即跳转到目的地址。想必我们可以采用这种方法进一步探索使用函数重用攻击 ACG 的方法。为了完成这个小小的链接过程,需要以下满足以下条件:
1. 把 mscoroc.dll 载入进 Edge 进程
2. 在 chakra.dll 的只读内存区域调用 ClrVirtualProtect +W
3. 重写 __guard_dispatch_icall_fptr 以通过检查
从上面的 vfgadgets 列表可以看出,对于 COOP 来说 edgehtml 是一个重要的库。因此,我们的第一任务就是泄漏 edgehtml 的基址以及其他必要的组件,例如我们的 counterfeit 内存区域。这样, payload 就能包含硬编码的偏移并在运行时重新定位。使用 Theori 的 POC 中泄漏的 bug ,我们就能获得我们想要的基地址。
//OS Build 10.0.14393
var chakraBase = Read64(vtable).sub(0x274C40);
var guard_disp_icall_nop = chakraBase.add(0x273510);
var chakraCFG = chakraBase.add(0x5E2B78); //_guard_dispatch_icall...
var ntdllBase = Read64(chakraCFG).sub(0x95260);
//Find global CDocument object, VTable, and calculate EdgeHtmlBase
var [hi, lo] = PutDataAndGetAddr(document);
CDocPtr = Read64(newLong(lo + 0x30, hi, true));
EdgeHtmlBase = Read64(CDocPtr).sub(0xE80740);
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)