楚轩 发表于 2015-8-11 21:59

分析及防护:Win10执行流保护绕过问题

本帖最后由 楚轩 于 2015-8-18 09:56 编辑

Black Hat USA 2015正在进行,在微软安全响应中心公布的最新贡献榜单中,绿盟科技安全研究员张云海位列第6位,绿盟科技安全团队(NSFST)位列28位,绿盟科技安全团队(NSFST)常年致力于发现并解决计算机以及网络系统中存在的各种安全缺陷。这篇《Windows 10执行流保护绕过问题及修复》是团队在此次大会上分享的主要内容0x00 Content执行流保护(CFG)CFG原理绕过问题 CustomHeap::Heap对象绕过CFG问题修复 HeapPageAllocator::ProtectPages函数修复机制 0x01 内容摘要Black Hat USA 2015正在进行,在微软安全响应中心公布的最新贡献榜单中,绿盟科技安全研究员张云海位列第6位,绿盟科技安全团队(NSFST)位列28位,绿盟科技安全团队(NSFST)常年致力于发现并解决计算机以及网络系统中存在的各种安全缺陷。这篇《Windows 10执行流保护绕过问题及修复》是团队在此次大会上分享的主要内容。1. 1月22日,微软发布Windows 10技术预览版,Build号9926;2. 2月,绿盟科技安全团队(NSFST)展开对其安全机制的研究,发现并与微软一起解决了CFG绕过问题;3. 3月,微软发布补丁修复了CFG绕过问题;4. 7月21日,在绿盟科技Techworld技术大会上分享了此次研究成果;5. 8月7日,在Black Hat US 2015上进行演讲并发布分析文章。绿盟科技安全团队NSFST一直努力发现及修复计算机以及网络系统中存在的各种安全缺陷,如果您需要了解更多信息,请联系:绿盟科技微博http://weibo.com/nsfocus绿盟科技微信号搜索公众号绿盟科技0x02 执行流保护(CFG)攻击者常常溢出覆盖或者直接篡改寄存器EIP的值,篡改间接调用的地址,进而控制了程序的执行流程。执行流保护(CFG,Control Flow Guard)是微软从Windows 8.1 update 3及Windows 10技术预览版开始,默认启用的一项缓解技术。这项技术通过在间接跳转前插入校验代码,检查目标地址的有效性,进而可以阻止执行流跳转到预期之外的地点,最终及时并有效的进行异常处理,避免引发相关的安全问题。这种思想及技术在业界有了较为成熟的应用,此次Windows 10将其引入,以便提高其安全性。但是绿盟科技安全团队(NSFST)在分析CFG的实现机制过程中,发现了CFG存在全面绕过的方法,随即向微软提报,并在随后的一段时间内,配合微软修复了这个问题。0x03 CFG原理在编译启用了CFG的模块时,编译器会分析出该模块中所有间接函数调用可达的目标地址,并将这一信息保存在Guard CF Function Table中。:006> dds jscript9!\_load\_config\_used + 48 l562b21048 62f043fc jscript9!\_\_guard\_check\_icall\_fptr Guard CF Check Function Pointer62b2104c 00000000 Reserved62b21050 62b2105c jscript9!\_\_guard\_fids\_table Guard CF Function Table62b21054 00001d54 Guard CF Function Count62b21058 00003500 Guard Flags同时,编译器还会在所有间接函数调用之前插入一段校验代码,以确保调用的目标地址是预期中的地址。这是未启用CFG的情况:jscript9!Js::JavascriptOperators::HasItem+0x15:66ee9558 8b03 mov eax,dword ptr 66ee955a 8bcb mov ecx,ebx66ee955c 56 push esi66ee955d ff507c call dword ptr 66ee9560 85c0 test eax,eax66ee9562 750b jne jscript9!Js::JavascriptOperators::HasItem+0x2c (66ee956f)这是启用CFG的情况:ript9!Js::JavascriptOperators::HasItem+0x1b:62c31e13 8b03 mov eax,dword ptr 62c31e15 8bfc mov edi,esp62c31e17 52 push edx62c31e18 8b707c mov esi,dword ptr 62c31e1b 8bce mov ecx,esi62c31e1d ff15fc43f062 call dword ptr 62c31e23 8bcb mov ecx,ebx62c31e25 ffd6 call esi62c31e27 3bfc cmp edi,esp62c31e29 0f8514400c00 jne jscript9!Js::JavascriptOperators::HasItem+0x33 (62cf5e43)操作系统在创建支持CFG的进程时,将CFG Bitmap映射到其地址空间中,并将其基址保存在ntdll!LdrSystemDllInitBlock+0x60中。CFG Bitmap是记录了所有有效的间接函数调用目标地址的位图,出于效率方面的考虑,平均每1位对应8个地址(偶数位对应1个0x10对齐的地址,奇数位对应剩下的15个非0x10对齐的地址)。提取目标地址对应位的过程如下: * 取目标地址的高24位作为索引i; * 将CFG Bitmap当作32位整数的数组,用索引i取出一个32位整数bits; * 取目标地址的第4至8位作为偏移量n; * 如果目标地址不是0x10对齐的,则设置n的最低位; * 取32位整数bits的第n位即为目标地址的对应位。操作系统在加载支持CFG的模块时,根据其Guard CF Function Table来更新CFG Bitmap中该模块所对应的位。同时,将函数指针\_guard\_check\_icall\_fptr初始化为指向ntdll!LdrpValidateUserCallTarget。ntdll!LdrpValidateUserCallTarget从CFG Bitmap中取出目标地址所对应的位,根据该位是否设置来判断目标地址是否有效。若目标地址有效,则该函数返回进而执行间接函数调用;否则,该函数将抛出异常而终止当前进程。ll!LdrpValidateUserCallTarget:774bd970 8b1570e15377 mov edx,dword ptr 774bd976 8bc1 mov eax,ecx774bd978 c1e808 shr eax,8774bd97b 8b1482 mov edx,dword ptr 774bd97e 8bc1 mov eax,ecx774bd980 c1e803 shr eax,3774bd983 f6c10f test cl,0Fh774bd986 7506 jne ntdll!LdrpValidateUserCallTargetBitMapRet+0x1 (774bd98e)ntdll!LdrpValidateUserCallTargetBitMapCheck+0xd:774bd988 0fa3c2 bt edx,eax774bd98b 730a jae ntdll!LdrpValidateUserCallTargetBitMapRet+0xa (774bd997)ntdll!LdrpValidateUserCallTargetBitMapRet:774bd98d c3 retntdll!LdrpValidateUserCallTargetBitMapRet+0x1:774bd98e 83c801 or eax,1774bd991 0fa3c2 bt edx,eax774bd994 7301 jae ntdll!LdrpValidateUserCallTargetBitMapRet+0xa (774bd997)ntdll!LdrpValidateUserCallTargetBitMapRet+0x9:774bd996 c3 ret0x04 绕过问题通过上面的原理分析,我们发现CFG的实现中存在一个隐患,校验函数ntdll!LdrpValidateUserCallTarget是通过函数指针\_guard\_check\_icall\_fptr来调用的。如果我们修改\_guard\_check\_icall\_fptr,将其指向一个合适的函数,就可以使任意目标地址通过校验,从而全面的绕过CFG。通常情况下,\_guard\_check\_icall\_fptr是只读的:06> x jscript9!___guard_check_icall_fptr62f043fc          jscript9!__guard_check_icall_fptr = <no type information>0:006> !address 62f043fcUsage:                  ImageBase Address:         62f04000End Address:            62f06000Region Size:            00002000State:                  00001000    MEM_COMMITProtect:                00000002    PAGE_READONLYType:                   01000000    MEM_IMAGEAllocation Base:      62b20000Allocation Protect:   00000080    (null)Image Path:             C:\Windows\System32\jscript9.dllModule Name:            jscript9Loaded Image Name:      C:\Windows\System32\jscript9.dllMapped Image Name:但如果利用jscript9中的CustomHeap::Heap对象将其变成可读写的,那么就会出现问题了。0x05 CustomHeap::Heap对象CustomHeap::Heap是jscript9中用于管理私有堆的类,其结构如下:stomHeap::Heap+0x000HeapPageAllocator         :    PageAllocator+0x060HeapArenaAllocator          :    Ptr32 ArenaAllocator+0x064PartialPageBuckets          :    DListBase<CustomHeap::Page>+0x09cFullPageBuckets             :    DListBase<CustomHeap::Page> +0x0d4LargeObjects                :    DListBase<CustomHeap::Page>+0x0dcDecommittedBuckets          :    DListBase<CustomHeap::Page>+0x0e4DecommittedLargeObjects   :    DListBase<CustomHeap::Page>+0x0ecCriticalSection             :    LPCRITICAL_SECTION当CustomHeap::Heap对象析构时,其析构函数会调用CustomHeap::Heap::FreeAll来释放所有分配的内存。 __thiscall CustomHeap::Heap::~Heap(CustomHeap::Heap *this){CustomHeap::Heap *v1; // esi@1v1 = this;CustomHeap::Heap::FreeAll(this);DeleteCriticalSection((LPCRITICAL_SECTION)((char *)v1 + 0xEC));\'eh vector destructor iterator\'((int)((char *)v1 + 0x9C), 8u, 7, sub_10010390);\'eh vector destructor iterator\'((int)((char *)v1 + 0x64), 8u, 7, sub_10010390);return PageAllocator::~PageAllocator(v1);}CustomHeap::Heap::FreeAll 为每个Bucket对象调用CustomHeap::Heap::FreeBucket。d __thiscall CustomHeap::Heap::FreeAll(CustomHeap::Heap *this){CustomHeap::Heap *v1; // esi@1signed int v2; // ebx@1int v3; // edi@1int v4; // ecx@2v1 = this;v2 = 7;v3 = (int)((char *)this + 0x9C);do{    CustomHeap::Heap::FreeBucket(v1, v3 - 0x38, (int)this);    CustomHeap::Heap::FreeBucket(v1, v3, v4);    v3 += 8;    --v2;}while ( v2 );CustomHeap::Heap::FreeLargeObject<1>(this);CustomHeap::Heap::FreeDecommittedBuckets(v1);CustomHeap::Heap::FreeDecommittedLargeObjects(v1);}CustomHeap::Heap::FreeBucket 遍历Bucket的双向链表,为每个节点的CustomHeap::Page 对象调用CustomHeap::Heap::EnsurePageReadWrite<1,4>。 __thiscall CustomHeap::Heap::FreeBucket(PageAllocator *this, int a2, int a3){PageAllocator *v3; // edi@1int result; // eax@2int v5; // esi@3int v6; // @1int v7; // @1v3 = this;v6 = a2;v7 = a2;while ( 1 ){    result = SListBase<Bucket<AddPropertyCacheBucket>,FakeCount>::Iterator::Next(&v6);    if ( !(_BYTE)result )      break;    v5 = v7 + 8;    CustomHeap::Heap::EnsurePageReadWrite<1,4>(v7 + 8);    PageAllocator::ReleasePages(v3, *(void **)(v5 + 0xc), 1u, *(struct PageSegment **)(v5 + 4));    DListBase<CustomHeap::Page>::EditingIterator::RemoveCurrent<ArenaAllocator>(*((ArenaAllocator **)v3 + 0x18));}return result;}CustomHeap::Heap::EnsurePageReadWrite<1,4> 用以下参数调用VirtualProtect:lpAddress: CustomHeap::Page 对象的成员变量addressdwSize: 0x1000flNewProtect: PAGE_READWRITERD __stdcall CustomHeap::Heap::EnsurePageReadWrite<1,4>(int a1) { DWORD result; // eax@3 DWORD flOldProtect; // @3 if ( (_BYTE *)(a1 + 1) || *(_BYTE *)a1 ) { result = 0; } else { flOldProtect = 0; VirtualProtect((LPVOID *)(a1 + 0xC), 0x1000u, 4u, &flOldProtect); result = flOldProtect; *(_BYTE *)(a1 + 1) = 1; } return result; }将内存页面标记为PAGE_READWRITE, 这正是出现问题的关键地方。0x06 绕过CFG通过修改CustomHeap::Heap对象,我们可以将一个只读页面变成可读写的,从而可以改写函数指针\_guard\_check\_icall\_fptr的值。观察ntdll!LdrpValidateUserCallTarget在目标地址有效时执行的指令:mov   eax,ecxshr   eax,8mov   edx,dword ptr mov   eax,ecxshr   eax,3test    cl,0Fhjne   ntdll!LdrpValidateUserCallTargetBitMapRet+0x1 (774bd98e)bt      edx,eaxjae   ntdll!LdrpValidateUserCallTargetBitMapRet+0xa (774bd997)ret从调用者的角度来看,上述指令与单条ret指令之间并没有本质区别。因此,将函数指针\_guard\_check\_icall\_fptr改写为指向ret指令,就可以使任意的目标地址通过校验,从而全面的绕过CFG。0x07 问题修复绿盟科技安全团队(NSFST)发现这一问题后,立即向微软报告了相关情况。微软很快修复了这一问题,并在2015年3月发布了相关的补丁。在该补丁中,微软引入了一个新的函数HeapPageAllocator::ProtectPages。0x08 HeapPageAllocator::ProtectPages函数 __thiscall HeapPageAllocator::ProtectPages(HeapPageAllocator *this, LPCVOID lpAddress, unsigned int a3, struct Segment *a4, DWORD flNewProtect, unsigned __int32 *a6, unsigned __int32 a7){unsigned __int32 v7; // ebx@1unsigned int v8; // edx@2int result; // eax@7struct _MEMORY_BASIC_INFORMATION Buffer; // @4DWORD flOldProtect; // @7v7 = (unsigned __int32)this;if ( (unsigned __int16)lpAddress & 0xFFF    || (v8 = *((_DWORD *)a4 + 2), (unsigned int)lpAddress < v8)    || (unsigned int)((char *)lpAddress - v8) > (*((_DWORD *)a4 + 3) - a3) << 12    || !VirtualQuery(lpAddress, &Buffer, 0x1Cu)    || Buffer.RegionSize < a3 << 12    || a7 != Buffer.Protect ){    CustomHeap_BadPageState_fatal_error(v7);    result = 0;}else{    *a6 = Buffer.Protect;    result = VirtualProtect((LPVOID)lpAddress, a3 << 12, flNewProtect, &flOldProtect);}return result;}这个函数是VirtualProtect的一个封装,在调用VirtualProtect之前对参数进行校验,如下:检查lpAddress是否是0x1000对齐的;检查lpAddress是否大于Segment的基址;检查lpAddress加上dwSize是否小于Segment的基址加上Segment的大小;检查dwSize是否小于Region的大小;检查目标内存的访问权限是否等于指定的(通过参数)访问权限;任何一个检查项未通过,都会调用CustomHeap_BadPageState_fatal_error抛出异常而终止进程。0x09 修复机制CustomHeap::Heap::EnsurePageReadWrite<1,4>改为调用HeapPageAllocator::ProtectPages而不再直接调用VirtualProtect。unsigned __int32 __thiscall CustomHeap::Heap::EnsurePageReadWrite<1,4>(HeapPageAllocator *this, int a2)2{3    unsigned __int32 result; // eax@24    unsigned __int32 v3; // @556    if ( *(_BYTE *)(a2 + 1) || *(_BYTE *)a2 )7    {8      result = 0;9    }10    else11    {12      v3 = 0;13      HeapPageAllocator::ProtectPages(this, *(LPCVOID *)(a2 + 12), 1u, *(struct Segment **)(a2 + 4), 4u, &v3, 0x10u);14      result = v3;15      *(_BYTE *)(a2 + 1) = 1;16    }17    return result;18}这里参数中指定的访问权限是PAGE_EXECUTE,从而防止了利用CustomHeap::Heap将只读内存页面变成可读写内存页面。

5598869 发表于 2015-8-11 22:07

说的什么啊!!!!!!!

云在青霄水在瓶 发表于 2015-8-12 00:34

你说什么/?我听不懂

wuftszg 发表于 2015-8-12 11:35

不明觉厉,然并卵

常黑屏 发表于 2015-8-15 12:28

看看哦,呵呵

willJ 发表于 2015-8-18 09:47

建议整理下格式再发吧,看原作者文章听好看的

http://blog.nsfocus.net/win10-cfg-bypass/

xiaomi1991 发表于 2016-4-8 10:53

支持楼主 学习了

xiaomi1991 发表于 2016-4-8 10:55

支持楼主 学习了

Conc0rd 发表于 2016-4-30 00:29

支持,学习了。

chen4321 发表于 2016-4-30 08:13

完全没看懂的赶脚,话说virtual不是工作在ring-1下吗,超级权限
页: [1]
查看完整版本: 分析及防护:Win10执行流保护绕过问题