原文首发看雪论坛 http://bbs.pediy.com/thread-217335.htm
劫持#1:调用LoadLibrary
正如前面所描述的那样,我的最终目的是绕过win10创意者更新中Edge浏览器的CFG保护,使其导出表suppresion能够使用。看看在kernel32和kernelbase中导出的各种LoadLibrary调用,事实证明,即使是使用了最新的CFG特征,加载一个新的DLL进我们的进程也相当容易。其原因是有两个。一是在kenel32.dll中,LoadLibraryExW实际上被标记为__guard_fids_table的合法调用目的地址。
二是,kernel32和kernelbase中的其他LoadLibrary调用在最开始是suppressed的,但在Edge中,他们最终会变成合法的调用位置。这似乎源于MicrosoftEdgeCP!_delayLoadHelper2的加载延迟,并最终导致GetProcAddr在LoadLibraryX APIs中被调用。如前所述,这表明要使所有的函数导出表变成不合法的调用地址是非常困难的。即使这样,其他的LoadLibrary call gates然后会suppressed或是临时开放,要想达成我们的目的,我们可以直接使用kernel32!LoadLibraryExW,因为它在初始化时是一个合法的目的地址。
为了使VirtualProtect包装函数载入进Edge进程中,我们需要调用LoadLibraryExW(“mscoree.dll”, NULL, LOAD_LIBRARY_SEARCH_SYSTEM32)。我们可以在这里走捷径,并利用前面提到的调用器populate所有参数,而不是创建一个传统的COOPpayload,使用loopervfgadget来迭代四个counterfeit对象。
第一次迭代将会在0x800上populate r8d。
要想在下述汇编代码中populate r8d,CHTMLEditor::IgnoreGlyphs是一个不错的vfgadget。参数0x800(LOAD_LIBRARY_SEARCH_SYSTEM32)将会在+0xD8h处载入。
回想一下,我们的counterfeit对象中的下一个指针一定在+0x80h。我们可以在内存中创建四个连续的counterfeit对象,每个对象的大小大于0xD8h,或是把下一个指针放在对象末尾。我选择了后者。在这种情况下,我们会有一个重叠的对象,所以我们必须小心在0xD8处的偏移不会影响到内存中第二个对象的第二次迭代的vfgadget。第一个populating r8d的counterfeit对象如下所示:
一旦从vfgadget返回,looper就会迭代faked的链接列表,且一定会调用另一个vfgadget,而这一次会populate rdx,其值为0x0(NULL)。为了实现这一目标,我使用了Tree::ComputedRunTypeEnumLayout::BidiRunBox::RunType()。我们可以从counterfeit对象+0x28h处载入我们的值(0x0)。
现在我们已经为我们的API调用populated了第二和第三个参数,我们需要popolate第一个参数,这个参数是一个指向’mscoree.dll’的字符串,然后调用函数指针转到LoadLibraryExW.为了实现这个目的,需要一个完美的调用器vfgadget,Microsoft::WRL::Details::InvokeHelper::Invoke()。汇编代码和对应的第三个counterfeit对象如下:
既然LoadLibraryExW已经被调用了,mscoree.dll也已经被加载到进程中,我们需要获得回到JavaScript的返回地址重定位附加的COOP payloads。looper和CFG都使用RAX作为间接分支的目的地址,所以,为了令新加载的模块返回到JavaScript,我们需要找到另一种方式来获得虚拟地址。幸运的是,在退出LoadLibraryExW时,RDX还包含模块地址的副本。因此,为了把RDX返回到内存区域里counterfeit对象列表中,我们可以将最后一个vfgadget放进对象列表中。在最后一次迭代中,我们可以调用CBindingURLBlockFilter::SetFilterNotify()来把RDX复制到当前counterfeit对象-0x88h处的地址。
然后looper会到达列表的末尾,并从劫持到的能把控制权返回到JavaScript代码的seal()调用中返回。到此,第一个COOP payload已经完成了,mscoree.dll已经加载进Edge中,现在我们可以使用下述代码在JavaScript中获得mscoree的基址。
//Retrieve loadlibrary return val from coop region
var mscoreebase = Read64(pRebasedCOOP.add(0x128));
alert("mscoree.dll loaded at: 0x" + mscoreebase.toString(16));
劫持#2:调用VirtualProtect包装函数
在成功完成COOP payload的第一步之后,为了使其可写,现在我们可以重定位第二个COOP payload来在包含chakra!__guard_dispatch_icall_fptr只读内存区域中调用ClrVirtualProtect。我们的目标是调用ClrVirtualProtect(this, chakraPageAddress,0x1000,PAGE_READWRITE,pScratchMemory)。这一次我们将演示一个不使用循环或是递归,而是使用单个counterfeit对象来populate所有参数及调用函数指针的COOP payload。我们将像以前一样使用相同的调用器vfgadget,只是这一次,它主要用于将counterfeit对象放入rcx中。
我们在原始的JavascriptNativeIntArray中劫持了freeze()虚函数,使其指向Microsoft::WRL::Details::InvokeHelper::Invoke。这个vfgadget将在this+0x10的地址上移动这个指针,并将它当作函数指针.因此,从JavaScript的R/W原语中,除了劫持vtable来调用调用器trampoline函数,还需要覆盖对象+0x10和+0x18处的值。
Write64(objAddr.add(0x10), pCOOPMem2);
Write64(objAddr.add(0x18), EdgeHtmlBase.add(0x2DC540));
Object.freeze(objAddr);
请注意,我们的fake对象将加载ClrVirtualProtect所需要的参数,并通过从另一个fake vtable中解析索引+0x100h,把ClrVirtualProtect的地址populate进rax。完成后,这将把chakra.dll我们希望的页映射为可写。
在这一步,我们完成了COOP,在最后一步中,事实上就使chakra.dll的CFG失效了。我们可以在包含jmp rax指令的chakra.dll中选择任意地址。一旦识别出来,我们就使用JavaScript的写原语来覆盖chakra!__guard_dispatch_icall_fptr的函数指针,使其指向这个地址。这可以使CFG验证程序变成nop指令,并允许我们从JavaScript中劫持一个chakra vtable跳转到什么地方。
//Change chakra CFG pointer to NOP check
Write64(chakraCFG, guard_disp_icall_nop);
//trigger hijack to 0x4141414141414141
Object.isFrozen(hijackedObj);
正如下面WinDbg输出列出的那样,使用CFG已经无效了,我们的劫持成功了,当我们试图跳转到一个没有映射进内存的地址0x4141414141414141时程序崩溃了。值得注意的是,因为CFG已经失效,我们可以使用劫持跳转到进程空间的任意地方。相比之下,由于0x4141414141414141在bitmap中是不合法的,有CFG的程序会抛出一个异常。我们将在调用栈中看到我们替换掉的原始CFG例程ntdll!LdrpDispatchUserCallTargetES。
总结
在这篇帖子中,我讨论了COOP,一种学术界提出的最新的代码重用攻击,并演示怎样使用它攻击现代执行流完整性的实现,例如微软CFG。总而言之,COOP相当容易使用,尤其是当把payload分割成更小的chains时。把各个vfgadgets拼接在一起和汇编ROP gadgets并没有什么不同。也许最耗时的部分就是在目的进程空间中找到并标记各种候选vfgadgets。
微软的Control Flow Guard被认为是一个粗粒度的CFI实现,因此更容易受到这里所述的函数重用攻击的影响。相比之下,细粒度的CFI解决方案能够考虑到给定的间接调用的元素,例如预期的VTable类型,验证参数数量,甚至参数类型。权衡两种方法的关键是性能,因为在CFI中引入复杂的策略可能会显著地增加开销。尽管因为应用程序会因为使用forward-edge和backward-edge CFI而变得难以攻破,防御最新的代码重用攻击仍然是很重要的。
为了抵消CFG的一些局限性,微软似乎专注于多样化的预防措施,例如在CFG和Arbitrary Code Guard中通过导出表supression保护关键call gates,比如VirtualProtect。然而,这篇帖子的一个关键点挑战是用户空间设计和执行防御。正如我们几前前在EMET中看到的那样,研究人员通过重用EMET本身的代码解除了EMET的防御。此外,正如2015黑帽大会里演示的那样,我们同样利用驻留在用户空间的关键CFG函数指针来改变CFG的行为。
相比之下,Endgame的HA-CFI解决方案完全由内核和硬件实现,即使容易受到函数重用攻击,但由于特权分离,使其更难篡改。在本系列的每二部分,我将使用我们自己的HA-CFI和正在进行的研究来对COOP进行讨论,以演示我们的检测逻辑如何应对最新的代码重用攻击。
原文链接:https://www.endgame.com/blog/disarming-control-flow-guard-using-advanced-code-reuse-attacks
本文由 看雪翻译小组 梦野间 翻译