安全路透社
当前位置:安全路透社 > 安全客 > 正文

【技术分享】绕过Windows 10的CFG机制(part2)

http://p0.qhimg.com/t01ee2dcf9d666bf7f8.jpg

翻译:myswsun

预估稿费:200RMB

,或登陆网页版

传送门:绕过Windows 10的CFG机制(part 1)


0x00 前言


本文是绕过Windows 10的CFG机制的第二篇。这也是我2016年7月的一些研究成果,但是知道现在才能发布。依然是由Theori发布PoC的IE漏洞。本文将展示另一种绕过CFG的方法,同样只能在IE中起作用,在Edge中无效。假设读者已经读了之前的一篇博文,因此CFG的细节就不介绍了,我将直接跳入任意原始读写的点。


0x01 查找另一种绕过CFG的方法


在上一篇文章中,我泄露了寄存器,包括一个栈指针,因此能让我们覆盖返回地址。另一个通用的绕过CFG的方式是使用ROP链,第一个配件来自一个没有使用CFG编译的DLL模块。这个方法能有效是因为CFG验证bitmap与模块相对应,没有使用CFG编译的模块允许执行任何地址。问题是将所有加载到IE和Edge中的模块都使用CFG编译了。如果一些插件或第三方应用被安装了,那么将会有模块加载到进程中,如果那个模块没有使用CFG编译,将成为攻击者的目标。我想找一种不依赖第三方模块的方法。这就带来了一个问题是所有在C:\windows\system32下的原生的模块都使用CFG编译,答案是不。为了找到没有使用CFG编译的模块,我写了如下python脚本:

http://p9.qhimg.com/t010be6fe1f48fa49f4.png

这个脚本查找C:\windows\system32下面的所有的模块,校验DLL属性,因为使用CFG编译的模块在属性中能找到一个标记。

在我的Windows 10 1511上面运行后得到了145个没有使用CFG编译的DLL,可以用来构建ROP。我发现很多的DLL不包含任何有效的配件,因此有大量代码的才是需要的。最终我使用了mfc40.dll。

http://p6.qhimg.com/t019eac6223d661d2a4.png


0x02 加载DLL


现在我们已经找到了一个包含有用配件的原生的DLL,同时没有使用CFG编译,我们面对的问题是这个DLL怎么被浏览器进程加载。我们能简单的通过kernelbase!LoadLibraryA API,我们验证了这是是被CFG允许的。下面是验证的bitmap:

http://p5.qhimg.com/t014e4204e6db178c9e.png

从这个bitmap中我们跟踪ntdll!LdrpValidateUserCallTarget的算法:

http://p7.qhimg.com/t01d5252793afa18440.png

我们可以看到在偏移4的位是1,因此这个API是允许非直接调用的。下一步是定位这个API,这可能需要使用第一次泄漏的指向kernelbase.dll的指针,然后在DLL中定位这个函数。前文是通过首先定位jscript9模块中函数Segment::Iniitialize,因为它使用了kernel32!VirtualAllocStub,继而调用kernelbase!VirtualAlloc。我找这个函数的方法是扫描jscript9模块的虚表地址并计算哈希,使用原始读来完成。算法看起来如下:

http://p9.qhimg.com/t01e307d35f5c407b41.png

找到的hash加上5个DWORD,每次加一个字节知道正确的hash被找到。非常简单的哈希函数是非常容易冲突的。在Segment :: Initialize函数的偏移量0x37处有对kernel32!VirtualAlloc的调用,如下所示:

http://p9.qhimg.com/t01c9f891ed10563f8a.png

读取指针内容:

http://p9.qhimg.com/t019b7a6d548b037512.png

在偏移0x6处包含了跳转到kernelbase!VirtualAlloc的引用:

http://p5.qhimg.com/t01e34a76621413e64f.png

现在我们有了指向kernelbase.dll的指针了,然后我们定位LoadLibraryA的地址,通过如下的另一个哈希值:

http://p4.qhimg.com/t01d71c59a81750c4ef.png

现在我们找到了LoadLibraryA的地址,我们需要调用它,我们能用jscript9的HasItem函数实现它:

http://p8.qhimg.com/t01c15a2a6c8cf6acdc.png

能用LoadLibraryA覆盖虚表偏移0x7C处的地址。选择HasItem是因为它也有一个参数,正好满足LoadLibraryA的需要。一个指向名字的变量作为参数,因此字符串C:\Windwos\System32\mfc40.dll必须被写入到TypeArray的缓冲区中:

http://p2.qhimg.com/t010dbd4c6a54b5cdce.png

实现如下:

http://p4.qhimg.com/t0110f3d4a1160400a1.png

现在我们能在进程内加载DLL了:

http://p2.qhimg.com/t012ba8a717807bf05a.png

展示进程加载的模块:

http://p8.qhimg.com/t01a35a53fceb7d0dfa.png


0x03 与Dll交互


我们已经确认了没有使用CFG编译的DLL被加载到了进程空间中,这将允许我们使用stack pivot技术绕过过CFG。然而我们需要知道mfc40.dll的地址。不幸的是,HasItem函数只能返回Boolean值,因此LoadLibraryA返回的模块地址在返回前就被过滤了。我们不得不找其他的方法来泄漏模块地址。一种方法是通过PEB,因为它拥有一个进程加载的所有的模块的列表指针。

因此问题转化为查找PEB地址。我选择通过API IsBadCodePtr来找到它,这个API在MSDN中如下定义:

http://p3.qhimg.com/t01ec9dff9d3006464e.png

意味着一个内存地址作为参数,这个API将返回boolean值来确定她是否被分配,这个允许我们使用原始读时避免了触发异常。这个方法是搜索进程地址空间,测试每个内存页是否是TEB中的一个,继而找到PEB的地址。在Windows8.1中的TEB总是在0x7F000000和0x7FFE0000之间,然而在Windows10上面变得更加随机,可能在0x100000和0x4000000之间。这是0x3F00000大小的内存空间,但是因为我们只想知道内存是否是被分配的,测试每页就足够了,意味着有0x3F00页。首先我们需要找到kernel32中的IsbadCodePtr的地址:

http://p4.qhimg.com/t01a42b6b3da1a328fb.png

这意味着可以使用先前泄漏的指向kernel32的指针,然后用hash找到函数地址。指向kernel32的指针可以通过和kernebase的方法一样找到,因为我们通过kernel32!VirtualAllocStub调用kernelbase!VirtualAlloc,因此我们能提前结束,返回指针。找到IsBadCodePtr的代码如下:

http://p4.qhimg.com/t0139a0982846e8098f.png

现在我们能调用API了,问题是在内存页中找到哪个是可靠的。查看找到的内存中PEB和TEB如下:

http://p0.qhimg.com/t01b32fb16a80eef3b0.png

从这清楚看到TEB的地址总是比较接近,而且他们中的一些会共享内存页。深入查看TEB的结构:

http://p5.qhimg.com/t01e74ecea8c47d58a2.png

很清楚在偏移0x18位置的DWORD值包含了TEB的地址,在偏移0x30位置的DWORD值包含了PEB的地址,并且在所有的TEB中都一样。因此总结下我们找到一个可靠的内存页后搜索以下的特征:

偏移0x18包含了页的基址

两个子分配是可靠的

所有的偏移0x30必须包含相同的值

算法实现如下:

http://p2.qhimg.com/t014e1660ebaf627e5f.png

当运行这个代码时,调试器捕捉到以下异常:

http://p2.qhimg.com/t01ef2bc19ae839347e.png

原因是IsBadCodePtr注册了一个SHE处理函数,然后尝试读取内存,如果是不可靠的,新的异常处理将捕获它,因此得到了这个异常。对于我们来说,这意味这调试变得困难,因为0x3F00个潜在的调用能触发异常。因此不在调试器下运行这个代码,只弹出一个PEB地址被找到的警告,如下:

http://p0.qhimg.com/t01522abe8718f7b2ab.png

为了验证它,调试器附加到进程,被找到的PEB地址:

http://p6.qhimg.com/t01f9a058f9895a3078.png

表明算法工作正常。补充说明,IsBadCodePtr是Edge不能有效的原因,因为会被特殊API缓解措施阻止,但是任何获取LEB的方法都能复用这个方法。


0x04 找到DLL


为了定位DLL的基址,我们将利用PEB_LDR_DATA结构,这个结构在PEB的偏移0xC的位置:

http://p9.qhimg.com/t01d5674062c0e1d129.png

这个结构看起来如下:

http://p4.qhimg.com/t01f4c37dc96a6771a5.png

在偏移0xC,0x14和0x1C位置,我们能发现3个双向列表,都包含了所有的加载的DLL的信息。我们使用0x1C位置的列表。下面是如何从PEB中找到DLL:

http://p5.qhimg.com/t0176b294fb59886aab.png

这个个过程是,基于列表的偏移0x18包含的了指向DLL名字的指针,偏移0x8包含了模块基址,第一个DWORD值指向了列表的下一项。遍历列表直到mfc40.dll被找到。代码如下:

http://p5.qhimg.com/t016c75ad5d45673d98.png

getName函数将Unicode名字读出来,然后转后卫ASCII字符串,并比较。我们能得到:

http://p8.qhimg.com/t01b2bd8fef46b769a5.png

上如展示了我们找到的DLL的信息,我们能用它来构建ROP。


0x05 控制EIP


最后一步是找到ROP小配件,来使我们执行ROP链,继而绕过过CFG。找到正确的小配件需要技巧,依赖EIP控制。我最终使用了Theori原始的PoC也使用的Subarray调用。汇编代码如下:

http://p1.qhimg.com/t01cc39f26884f04b99.png

在虚表偏移0x188的位置,有一个或两个参数。如果过一个参数被指定,另一个有一个默认值。在下面的截图我使用IsBadCodePtr的地址覆盖了Subarray的地址,下断点:

http://p8.qhimg.com/t016500c5a720862059.png

从这我们可以看到EBX和ECX寄存器都包含了指向对象的指针。而且在ESP+4和ESP+8位置我们发现两个提供的参数。我找到如下的小配件:

http://p1.qhimg.com/t01bd91c2726ccd1c72.png

因为在栈上的第二个DWORD值被我们控制了,我们存放了stack pivot配件的地址。记住CFG不保护栈上的地址,我们可以找到所有加载的DLL中的stack pivot小配件。我们能用kernelbase中的如下配件作为stack pivot:

http://p1.qhimg.com/t01681d7efbbfb17433.png

因为ECX也指向对象,0xD8偏移位置也被我们控制了。如果我们放置ROP链来,我们将能调用VirtualProtect。首先我们需要动态找到这些小配件,可以通过搜索DLL做到,这次不是用hash,而是用字节:

http://p5.qhimg.com/t0154e5a743f0edfcb5.png

当所有的配件被找到,我们将他们插入到正确的偏移,如下:

http://p6.qhimg.com/t016665c52fc95a6562.png

我们运行后得到:

http://p4.qhimg.com/t0165d58f02fd61b93d.png

我们能看到绕过CFG的小配件确实绕过了CFG,stack pivot配件被放到了栈上。而且我们单步调试:

http://p2.qhimg.com/t01f58ac9bdfe60d755.png

这个ROP配件缓冲区是空的,因为没有配件放在那儿。


0x06 总结


很清楚没有CFG编译的模块是个威胁,没有CFG编译的DLL加载到内存中可以用来绕过CFG。微软也注意到了Edge中IsBadCodePtr函数,因此这个方法不能使用。然而还是能在IE中使用。尽管微软非常有可能将以CFG重新编译愈来愈多的DLL,第三方DLL还是有遗漏。浏览器插件或者安全软件注入到被监控的进程中的模块是否以CFG编译了?


传送门:绕过Windows 10的CFG机制(part 1)


原文链接:https://improsec.com/blog//bypassing-control-flow-guard-on-windows-10-part-ii

未经允许不得转载:安全路透社 » 【技术分享】绕过Windows 10的CFG机制(part2)

赞 (0)
分享到:更多 ()

评论 0

评论前必须登录!

登陆 注册