前言
2020年10月30日,谷歌P0团队批露了微软尚未修复的内核在野0day,CVE-2020-17087, 谷歌称目前已发现在野利用,目前微软尚未发布针对此漏洞的补丁程序,预计下周二补丁日进行修复。
目前此漏洞POC已被P0公开,预计影响为Win7以后全平台。
当前测试环境:Win10 1903 x64(已安装10月补丁)。
漏洞原理
此漏洞为Windows内核加密模块(cng.sys)出现的整数溢出和缓冲区漏洞,在函数cng!CfgAdtpFormatPropertyBlock,由于对传入的第二参数进行乘以6后,出现上溢,造成后续申请缓冲区过小,访问内核无效内存,造成BSOD。
调用栈回溯如下:
1 | 2: kd> !analyze -v |
CfgAdtpFormatPropertyBlock函数如下,可以看到出现整数溢出的逻辑。
没有反编译工具差点没看出来6倍(2+1)+(2+1)。。。
分析与调试
cng!CngDispatch
两参数 PDEVICE_OBJECT 以及 PIRP
在调用CngDeviceControl 函数之前,主要a2的IRP数据进行处理,获取In/Out buffer地址以及Length,还有IOCTL code和RequestMode ,分别作为六个参数,传入CngDeviceControl 。通过调试来看,此派遣历程使用的操作模式Method为METHOD_BUFFERED。
通过IOCTL Code最后一位同样也可以看出,Method 0即为 METHOD_BUFFERED。
1 | #define CTL_CODE( DeviceType, Function, Method, Access ) ( \ |
此时的寄存器以及内存的值
进入cng!CngDeviceControl 函数,大量判断IOCTL code之后,调用ConfigIoHandler_Safeguarded函数,传入前四参数即缓冲区相关四参数。
进入ConfigIoHandler_Safeguarded函数,
使用InputLength申请两块内存,第一块block1拷贝传入的数据。
1 | 1: kd> dq ffffc18e`eae97000 |
第二块block2初始化为0。
将第一块地址 v10, 大小v17,第二块局部变量v19 int型地址,第二块地址v12作为四参数,调用IoUnpack_SG_ParamBlock_Header函数,根据逻辑来看,此函数最终返回0。否则当前函数将返回。
进入IoUnpack_SG_ParamBlock_Header函数:
对传入缓冲区数据一顿判断(表示很疑惑,为什么对常识性的地址大小进行判断而不是其中的值,防止溢出??),另外将传入缓冲区得数据起始偏移为4得值拷贝到第三参数a3所在地址,很明显进入while循环后,由于block2之前已经初始化为0,那么循环8次后将a4置为-1,进入goto语句,返回0。
再回到ConfigIoHandler_Safeguarded函数中,接着调用ConfigFunctionIoHandler函数,将刚刚拿到的传入缓冲区地址偏移4处的值(0x10400)作为第一参数,block1地址作为第二参数,传入缓冲区大小InBufferLen作为第三参数,输出缓冲区地址作为第四参数,输出缓冲区大小为第五参数,block2地址作为第六参数。
进入ConfigFunctionIoHandler函数,
第一参数逻辑右移16位,结果为1(0x10000=<a1<0x1FFFF)时,调用ConfigurationFunctionIoHandler函数并传入6参数(注意a1变为两字节);
结果为2(0x20000=<a1<0x2FFFF)时,调用ResolutionFunctionIoHandler函数并传入前两参数;大于2或者小于0失败C00000AF(STATUS_ILLEGAL_FUNCTION);等于0则调用RegistrationFunctionIoHandler调用函数并传入前两参数;
我们进入ConfigurationFunctionIoHandler函数,首先映入眼帘的是一个熟悉的函数 IoUnpack_SG_Configuration_ParamBlock,估摸着和IoUnpack_SG_ParamBlock_Header函数有异曲同工之处。
//v7 block2地址
在此函数中,循环后goto语句进入几个子函数
//v17 block1地址
//v15 传入缓冲区大小InBufferLen
//a15为block2地址
这些函数内部的操作,将传入第一参数block1+offset处覆盖为原先值加上起始地址后的值。
eg:block1+10处的值原先为100h,加上起始地址后变为ffffc18e`eae97100,写回原先位置处。
最后进入IoUnpack_SG_Buffer函数,传入Inputbuf block1+0x10地址,block1地址,缓冲区长度,block2起始地址4参数。经过内存断点调试,我们发现在此函中调用IoUnpack_SG_Buffer函数时,访问了block1+0x50处内存
调试后发现,此函数同样修改了传入的block1+58处内存为值加上起始地址,另外,通过block1中成员大小的各种边界判断之后,将block2+58处的值置为-1,此外,循环(block1+50h)即2AAB次,将block2+(block1+58h)即block2+1000处开始,均置为-1,即修改了2AAB字节的内存。返回0。最终block1的内存布局如下:
1 | 1: kd> dq rdi |
此函数中对block2内存做修改 ,同时对大量参数使用block1内存进行赋值。注意,a12,即当前的第12参数,拿到了block1+50处的内存值。
并返回0。IoUnpack_SG_Configuration_ParamBlock同样返回0。
后续由于a1由原先的0x10400截断为0x400,调用BCryptSetContextFunctionProperty函数,传入七个参数分别为在IoUnpack_SG_Configuration_ParamBlock做赋值,v14为传入第3参数,被赋值为*(block1+8)
v16为传入第4参数,被赋值为*(block1+10h)
v15为传入第5参数,被赋值为*(block1+18h)
v17为传入第6参数,被赋值为*(block1+20h)
v22为传入第8参数,被赋值为*(block1+30h)
v19为传入第12参数,被赋值为*(block1+50h)
v24为传入第13参数,被赋值为*(block1+58h)
内部v17所处内存即为block1地址,此时block1处内存为
可以看到,与我们刚开始传入缓冲区的略有不同,似乎在某些位置上,原先的值变为加上当前起始地址的结果。
进入 BCryptSetContextFunctionProperty函数,最后两参数分别为(block1+50h),以及(block1+58h)。
其中调用CfgReg_Acquire尝试打开注册表项System\CurrentControlSet\Control\Cryptography\Configuration\Local
失败,返回error =5。
后续构造三个UnicodeString,巧的是,这里三个UnicodeString的来源,正是block1+100,block1+200,block1+400处的所存储的字符串。
在多次调试发现,后续使用的2AAB正是在调用CfgAdtReportFunctionPropertyOperation前,此时的rsp+3f+38h,正是传入的第六参数(*block1+50h),后续函数会将此处的值取word,传入漏洞函数。
调用CfgAdtReportFunctionPropertyOperation函数,传入四参数(伪代码显示此时传入四参数,这是不准确的,查看汇编,我们可以看到至少传入了10个参数,标记处上方的rsp+48),而第九参数,为传入的第六参数*(block1+50h),取低两字节。
进入CfgAdtReportFunctionPropertyOperation 最终将rbp+0x460处的值(传入的第九参数),作为第二参数传入崩溃函数,block1+1000作为第一参数,局部变量UnicodeString传入第三参数,进入CfgAdtpFormatPropertyBlock前:
进入CfgAdtpFormatPropertyBlock函数,六倍edx以后,溢出变为2,申请大小为2,申请到special pool内存block3。
调试后,解释一下整个do… while循环中的操作,循环次数为2AAB,也就是传进来的a2,通过一个类似与密码表的字符数组,访问a1也就是block1+1000处值,右移4位,作为数组下标,取出单个byte(0x30),填充申请到的内存block3处(WORD),接着a1++,再与上0xF,作为下标取出单个byte(0x30),block3++,填充,再block3++,填充一个0x20,依旧当作WORD型。这是一次循环中所做的事情,循环计数器a2–。
一次循环完成后,内存布局如图
第二次循环结束,内存布局,
显然,第三次循环时,当放置0x20时,访问无效内存,系统崩溃。
有效内存覆盖完毕。
后续向下一页写0x20时,系统崩溃。
漏洞利用
在野利用配合chrome CVE-2020-15999(FreeType 2库的“ Load_SBit_Png”函数中的堆缓冲区溢出漏洞),进行针对chrome沙箱的逃逸,进而可进行提权或者代码执行。
分析来看,如果后续内存布局得当,计数器的值导致可访问的内存相当可观,但是缺陷是填充的值为密码本中数据,需要精巧的构造才行。时间有限,仅此抛砖引玉,各位内核师傅冲鸭!
时间线
2020.10.30 谷歌P0小组披露漏洞。
2020.11.09 本篇漏洞分析完成。
待续…
参考链接
[1]Issue 2104: Windows Kernel cng.sys pool-based buffer overflow in IOCTL 0x390400
https://bugs.chromium.org/p/project-zero/issues/detail?id=2104
[2]Issue 2104 attachment: cng_ioctl_390400.cpp (2.0 KB)
https://bugs.chromium.org/p/project-zero/issues/attachmentText?aid=472684
[3]内核池基础知识(池损坏)