CVE-2016-7190 [0] 是 ChakraCore 中 Array.map() 函数的一个堆溢出漏洞,可覆盖相邻内存。获取任意读写权限的主要思路是先分配多个连续的 JavaScript 整数数组,然后利用溢出操控其中一个数组的大小。接着,我们利用这个数组将 Uint8Array 的基地址改为我们想要读写的任意地址。之所以需要这个额外步骤,是因为溢出仅限于被溢出整数数组大小的 2 倍。
由于该漏洞可以轻松地重新引入到当前版本的 ChakraCore 中(参见 undo-cve-2016-7190.patch),因此非常适合测试不同的利用策略。
在这个示例中,我们通过覆盖 Uint8Array C++ 对象的虚函数表指针指向一个可控的内存地址,然后调用该 Uint8Array 对象的某个函数,从而触发对虚函数的调用,以此劫持控制流。
控制流完整性(CFI)是一种缓解控制流劫持攻击的防御技术。CFI 的基本思想是在编译时计算应用程序的控制流图(CFG),然后在运行时对应用程序进行插桩检查,确保控制流不会偏离静态计算的 CFG。然而,如果应用程序(如网页浏览器)支持动态代码生成,则 CFG 必须在运行时是可扩展的。这带来了若干挑战:
在我们进行研究期间,重点关注最后一个挑战。主要思路是操控即时(JIT)编译器的输入(数据)。这样,JIT 编译器将生成恶意代码,然后这些代码被集成到当前上下文中——甚至可以通过 CFI 进行加固。与我们研究同时进行的,theori [4] 展示了如何通过操控 JIT 编译器的输出来绕过 CFI。这种攻击通过校验和验证输出的完整性来缓解。但我们的攻击无法被阻止,因为我们操控的是 JIT 编译器的输入。然而,通过将 JIT 编译外包给另一个进程(即任意代码保护 [3]),两种攻击都得到缓解。
我们演示了 攻击者可以利用针对 JIT 编译器的纯数据攻击来生成任意本机代码。具体来说,我们修改了 JIT 编译器的中间表示(IR),注入攻击者控制的指令。当 JIT 编译器随后基于修改后的 IR 生成本机代码时,它会产生攻击者控制的本机代码。
从最初进行研究到现在,ChakraCore 的 IR 已经发生了变化。我们没有花费精力移植该攻击,因此,要尝试此攻击,请使用以下二进制文件。
在分析 ACG [3] 的过程中,我们注意到(除此之外 [1, 2])存储在 .mrdata 段中的全局只读数据必须进行调整以集成动态生成的代码。这是通过 LdrProtectMrdata() 函数完成的,该函数使用锁来保证线程安全。然而,.mrdata 段首先被重新映射为可写,然后才获取锁。有趣的是,.mrdata 段包含 .mrdata 段的基地址和大小,稍后用于将该段再次重新映射为只读。
攻击者可以利用这一点创建一个攻击原语,允许将只读内存重新映射为可写。因此,攻击者执行以下步骤:
此攻击的后果是,以前可信的数据(即映射为只读的数据)变得不可信。在我们的概念验证中,我们利用此原语通过更改保存在 _guard_dispatch_icall_fptr 中的 CFGuard 验证函数的指针来绕过控制流保护(CFGuard)。可以通过附加调试器并更改 bypass_cfguard 变量来观察效果。
[0] https://bugs.chromium.org/p/project-zero/issues/detail?id=923
[1] http://alex-ionescu.com/publications/euskalhack/euskalhack2017-cfg.pdf
[2] https://sites.google.com/site/bingsunsec/dataonlyattack
[3] https://blogs.windows.com/msedgedev/2017/02/23/mitigating-arbitrary-native-code-execution/
[4] http://theori.io/research/chakra-jit-cfg-bypass