在对win32k.sys进行压力测试时候发现的漏洞,该漏洞的是因为Windows系统在对Path子系统进行相关操作的时候,对申请用以操作的内存存在不进行初始化造成的。通过频繁地申请与释放内存,导致系统在执行win32k的bFlatten函数时,使用了未初始化的内存,而用户可以有一定的概率成功控制这块内存,导致可以让函数之后的读写操作指向非法内存引发BSOD,或指向目标函数来实现提权。
操作系统:Win7 x86 sp1 专业版
编译器:Visual Studio 2017
调试器:IDA Pro,WinDbg
Windows的Path子系统是一个关于绘制图形曲线的系统,在EPATHOBJ结构体中的pPath成员指向了用于该子系统的PATH结构体指针:
PATH结构体的定义如下,主要包括了指向PATHALLOC和PATHRECORD结构体的指针:
PATHALLOC是分配PATHRECORD的容器,该结构体定义如下:
PATHRECORD是Path子系统的主要结构体,对其进行直线化操作就是对PATHRECORD结构体进行操作,该结构体定义如下:
其中,flags成员指明了该结构体的类型,如,PD_BEZIERS就指明其未贝塞尔自由绘制曲线:
PATH,PATHALLOC,PATHRECORD结构体的关系如下图所示:
系统分配PATHRECORD结构体是通过PATHALLOC结构体实现的,freepathalloc和newpathalloc分别是用来释放和分配PATHALLOC结构体的函数。
freepathalloc函数实现如下,由该实现可以看出,在PATHALLOC中有一个freelist链表,该链表可以用来保存释放的PATHALLOC结构体,其中,cFree成员用来指定保存在freelist链表中的PATHALLOC结构体的数量。当freelist链表中保存的PATHALLOC结构体数量少于4时,释放的PATHALLOC结构体会被加入到链表中,否则直接调用ExFreePoolWithTag释放内存。
newpathalloc函数的实现如下,该函数首先判断freelist中是否存在可用的PATHALLOC结构体,如果有,则直接从freelist链表中分配,否则就会在第二处调用PALLOCMEM函数来分配内存,函数最终会将分配的内存地址赋给res,作为返回值。
在该函数中,在分配完内存以后只在LABEL_7初始化了PATHALLOC结构体前面三个成员,而之后的PATHRECORD结构体数组没有进行初始化。因此,如果是从第一处的freelist链表中分配PATHALLOC结构体,该结构体中的PATHRECORD结构体数组就会是之前释放PATHALLOC结构体时保存的数据。如果是第二处的PATLLOCMEM函数分配内存则不存在该问题,因为该函数会将内存初始化为0。
其中Win32AllocPool函数直接调用ExAllocatePoolWithTag来申请PagedPoolSession类型的内存:
触发漏洞的函数为bFlatten,该函数实现如下,函数从PATH->pprfirst开始遍历查找所有的PATHRECORD结构体,对PD_BEZIERS类型的PATHRECORD结构体调用pprFlaaenRec函数。
pprFlattenRec函数首先会调用newpathrec函数申请一块PATHRECORD结构体,该结构体保存在第二个参数first_pathRecord中,对于新分配的PATHRECORD结构体,函数将其pprprev赋值为调用pprFlattenRec时,传入的PATHRECORD结构体参数的pprprev,然后将参数的pprprev指向的PATHRECORD结构体的pprnext赋值为新申请PATHRECORD结构体。
之后函数可能会多次调用newpathrec分配新的PATHRECORD结构体,然后将上面分配的PATHRECORD结构体的pprnext指向新分配的PATHRECORD结构体,同时会移动pprNew指针。
在pprFlattenRec函数的最后,函数会将pprNew的pprnext赋值为调用pprFlattenRec函数时传入的PATHRECORD参数的pprnext。
申请PATHRECORD结构体的newpathrec函数实现如下,函数首先从PATH->ppachain->pprfreestart指向的地址开始查找是否有足够的空间分配一个PATHRECORD结构体,如有则以pprfreestart指向的地址分配新PATHRECORD。否则,函数会调用newpathalloc函数先分配一个新的PATHALLOC结构体连入ppachain指向的链表的比链表头,在从新的PATHALLOC里的pprfreestart所指地址分配PATHRECORD。
newpathalloc在上面分析过了,如果是从freelist链表中分配的PATHALLOC结构体,该结构体的PATHRECORD结构体数组就会是未初始化的。因此,newpathrec返回的PATHALLOC中的PATHRECORD结构体数组很有可能是未初始化的。而pprFlattenRec函数在函数最后才会对第一次调用newpathrec申请的PATHRECORD结构体的pprnext成员进行赋值,可是在第二次调用newpathrec函数的时候,如果此次调用,内存空间不够,该函数就会执行失败,返回的就不会是1,这样pprFlattenRec函数也会提前返回,最后对第一次申请的PATHRECORD结构体的pprnext成员进行赋值的代码也不会得到执行,这块PATHRECORD结构体的pprnext成员就没有被赋值的机会,保存的就会是这块内存原来的数值。而bFlatten函数会通过PATHRECORD->pprnext来查找下一个PATHRECORD结构体,将其作为参数调用pprFlattenRec,如果可以想办法让此时的pprnext指向指定的内存,此时就会以指定的内存调用pprFlattenRec。
bFlatten函数的调用只需要在用户层调用FlattenPath就可以实现,该函数定义如下:
其中的参数可以通过调用GetDC函数,将参数传递为NULL来获取桌面HDC句柄就可以得到。
想要利用这个函数,就需要想办法将PATHRECORD结构体的pprnext赋值为指定的内存,而通过调用PolyDraw函数可以将指定的POINT数组赋给PATHRECORD结构体的points数组。
PolyDraw函数进入到内核中,会调用NtGdiPolyDraw,NtGdiPolyDraw会调用GrePolyDraw,GrePolyDraw会调用bPolyBezierTo,bPolyBezierTo会调用addpoints,addpoints函数会调用createrec函数。
createrec可能会调用newpathalloc来申请PATHALLOC结构体,如果申请失败,则会调用reinit函数,并直接返回:
reinit函数会调用vFreeBlocks,并将EPATHOBJ的部分成员清0:
vFreeBlocks函数会将大小为0xFC0的PATHALLOC释放掉:
如果createrec不调用reinit,之后会调用bXformRound,该函数会将用户层传入的POINT数组的x和y乘以0x10(左移4位)赋给PATHRECORD结构体的points成员:
在用户层调用PolyDraw的前后,需要调用BeginPath和EndPath,其中BeginPath会在内核层调用NtGdiBeginPath实现,NtGdiBeginPath可能会调用vDelete函数:
vDelete函数会调用vFreeBlocks函数来释放PATHALLOC:
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
最后于 2022-8-4 23:16
被1900编辑
,原因: