win32k.sys误差挖掘思绪解读

宣布时间 2020-05-09

一、研究配景


4月1日,以色列清静研究员Gil Dabah在博客上宣布了一篇关于win32k误差研究文章,形貌了怎样通过内核工具的Destroy函数和win32k user-mode callback缓解步伐的特征来寻找UAF误差的新思绪。


为此,尊龙凯官网入口ADLab对win32k相关内核机制举行研究剖析,并对这类误差的挖掘思绪举行详细解读剖析。


二、win32k误差缓解与对抗


2.1 win32k user-mode callback误差


由于设计缘故原由,win32k驱动需要处置惩罚许多用户层的回调,这些回调给win32k�?榈那寰泊戳撕苁谴蟮囊�,并在已往10年时间孝顺了大宗的误差。


为了便于误差形貌,以如下伪代码举行举例剖析。


NtUserSysCall()

 {

     PWND p = CreateWindowEx(…);

     somecallback();

     xxxSetWindowStyle(p);

 }


上述代码执行效果如下图所示,用户层执行的某函数通过syscall传入内核层,当内核层代码执行到somecallback这一句时,用户层可以在用户界说的callback函数中获得代码执行的机会,若是用户在callback函数挪用了DestroyWindow函数销毁窗口p,内核层的响应销毁代码将会被执行,p的响应内存被释放,回调执行完毕,NtUserSysCall函数继续执行,当执行到xxxSetWindowStyle(p)一句时,由于p的内存已经被释放从而导致UAF误差的爆发。


尊龙凯时 - 人生就是搏!


2.2 user-mode callback误差缓解机制


为了避免上述问题的爆发,微软在工具中引入了一个引用计数(工具+0x8处),工具分派时引用计数为1,当执行工具的Destroy函数时引用计数减1,当引用计数为0时工具会被真正释放。微软通过锁的看法为工具添加和镌汰引用计数,在win32k中为工具管理引用计数的锁有两种划分是暂时锁(响应函数为ThreadLock/ ThreadUnlock)和永世锁(响应函数为HMAssignmentLock/ HMAssignmentUnlock)。经由加固之子女码体现为如下形式:


NtUserSysCall()

 {

     PWND p = CreateWindowEx(…);

     ThreadLock(p);

     Somecallback();

     xxxSetWindowStyle(p);

     ThreadUnlock();

 }


通过上述代码,可以包管纵然callback被执行,p在xxxSetWindowStyle函数执行的时间也不会被释放。


2.3缓解机制的对抗手艺


上一节提到了工具的引用计数,若是工具的引用计数为正,纵然执行工具的destroy函数,工具没有真正被释放,仍然存留在内存中,这种工具被微软开发者称为僵尸(Zombie)工具。一旦僵尸工具的引用计数镌汰到0它将会消逝,可是在此之前它仍然保存内存中,只是用户层无法会见该工具。


同时为了避免僵尸工具继续存留在内存中,锁的释放函数(ThreadUnlock/ HMAssignmentUnlock)一样平常会包括工具的释放环节。


工具的Destroy函数尚有一个特征就是在释下班具的同时,Destroy函数也会释下班具的子资源,其历程可以简要形貌如下。


void xxxDestroyWindow(PWND pwnd)

 {

     xxxFW_DestroyAllChildren(); // Destroy child windows, if exist!

     if (NULL != pwnd->spmenu)  // If there’s a menu, remove and destroy it.

     {

         PMENU tmp = pwnd->spmenu;

         if (HMAssignmentUnlock(&pwnd->spmenu)) // If it’s still locked

           {

               DestroyMenu(tmp); // Try destroying it (it can remain a zombie).

           }

      }

     DereferenceClass(pwnd);

     if (HMMarkObjectDestroy(pwnd)) // Check for zero refs!

           HmFreeObject(pwnd); // Only now free the object and handle pair.

 }


DestroyWindow在第一次挪用时释放子资源,一旦窗口不再被引用,句柄管理器就会再次完全销毁它,一样平常情形下,第二次销毁Destroy函数不会在行止置子资源,由于第一次已经释放了所有的子资源。


可是事情往往不是这么简朴,事实上纵然是一个已经挪用过响应Destroy函数释放的僵尸工具,仍然有机会对其自己举行一些更改(回调之后内核代码仍会对工具举行一些操作),我们把这种情形叫做Zombie Reload,当该僵尸工具由于引用计数为0而被真正释放时,之前的更改操作将会给内核带来一些隐患。


关于如下代码片断:


ThreadLock(pwnd);

xxxSomeCallback(); // Here we can destroy pwnd from user-mode.

InternalSetTimer(pwnd, ...);  // reuse pwnd without check wether it is destroyed

ThreadUnlock();

SomefunctionUseTimer();   //UAF of Timer


我们在用户层回调中对pwnd执行了Destroy函数,然后通过InternalSetTimer为之设置了一个计时器,当ThreadUnlock将pwnd真正释放的时间,计时器也将被释放,那么接下来对计时器的操作将会导致UAF误差的爆发。


三、案例剖析


上一节我们讨论了工具的引用计数和锁给工具带来的新的清静隐患,可是真正的挑战在于我们怎样确定一段代码中保存误差,要害点是确保在unlock函数中释放的工具在运行到有问题的代码时其引用计数应该为1,只有这样我们才华在用户层回调挪用其Destroy函数,并通过unlock函数将这个工具真正释放掉(上锁的时间会做+1处置惩罚),这也是我们接下来需要讨论的。下面我们通过一个案例来剖析误差挖掘思绪。


3.1误差成因


下图是xxxMnOpenHierarchy函数的代码片断。


尊龙凯时 - 人生就是搏!


图中通过xxxCreateWindowEx可以获得一个返回用户层执行callback函数的机会,xxxCreateWindowEx建设的窗口将作为父窗口*(struct tagWND **)(**v3 + 8)(上图红框)的子窗口,若是我们可以通过ThreadUnlock释放父窗口,那么子窗口v32也会被释放,以是当后续的safe_cast_fnid_to_PMENUWND函数将v32作为参数执行时就会爆发问题,值得注重的是通过回调释放v32是行欠亨的,若是这样xxxCreateWindowEx将会返回0,无法通过if判断。


这里的问题就在于怎样包管父窗口在ThreadUnlock函数执行的时间引用计数为1,由于要执行xxxMnOpenHierarchy函数需要将父窗口关联到一个menu窗口上,此时父窗口和menu窗口将会被一个永世锁锁住,下面我们先容怎样绕过永世锁。


3.2 误差挖掘思绪


首先我们建设了g_hMenuOwner和g_hNewOwner两个窗口,其中g_hMenuOwner的菜单句柄为hMenu,它也是g_hNewOwner的所有者。


尊龙凯时 - 人生就是搏!


在上述建设历程中,内核通过LockPopuMenu函数划分为hMenu和g_hMenuOwner添加了永世锁,为了告竣释放目的,这个永世锁需要被绕过。


尊龙凯时 - 人生就是搏!


尊龙凯时 - 人生就是搏!


此时锁和所有者的关系是这样的:


尊龙凯时 - 人生就是搏!


接下来我们通过SetWindowsHookEx给窗口添加了WH_CBT钩子,并让窗口进入新闻循环中。


尊龙凯时 - 人生就是搏!


SendMessage操作为g_hMenuOwner添加一个暂时锁,由于后续的所有攻击都是在message的回调中举行,以是关于g_hMenuOwner来说这个暂时锁是无法释放的,若是想要结构一个误差使用情形首先需要用一些要领来绕过它。


尊龙凯时 - 人生就是搏!

现在的情形酿成了下图所示:


尊龙凯时 - 人生就是搏!


当新闻为HCBT_CREATEWND时,我们第一次抵达xxxMNOpenHierarchy函数内部的xxxCreateWindowEx。


尊龙凯时 - 人生就是搏!


这里可以通过界说关于HCBT_CREATEWND新闻的处置惩罚获得执行用户层回调代码的机会,这一步的主要目的是为了获取Menu的Wnd。


尊龙凯时 - 人生就是搏!


当吸收到的新闻为WM_ENTERIDLE时,我们在窗口的新闻回调中通过PostMessage下发新闻。


尊龙凯时 - 人生就是搏!


发送新闻后,驱动程序来到了xxxMNKeyDown函数内部挪用xxxSendMessage处。


尊龙凯时 - 人生就是搏!


通过WM_NEXTMENU新闻的回调函数最先为LPARAM赋值,赋值操作是为了修改hMenu的Owner,这样就可以将Owner的暂时锁绕过。


尊龙凯时 - 人生就是搏!


此时内核会接到销毁menu的新闻,通过用户层的回调函数返回1阻止menu的销毁。


尊龙凯时 - 人生就是搏!


xxxMNKeyDown函数通过UnlockPopupMenu将g_hMenuOwner身上的永世锁被去掉。


尊龙凯时 - 人生就是搏!


取而代之的是g_hNewOwner加上了一个锁,hMenu的Owner也从g_hMenuOwner酿成了g_hNewOwner。


尊龙凯时 - 人生就是搏!


这时,锁的关系酿成了:

   

尊龙凯时 - 人生就是搏!


接下来程序第二次进入到xxxMNOpenHierarchy函数并通过xxxSendMessage发送了新闻。


尊龙凯时 - 人生就是搏!


此时通过设置WM_INITMENUPOPUP回调来获得用户层执行的机会,WM_INITMENUPOPUP回调函数通过SetWindowsHookEx函数设置了一个新的hook,目的是为了在xxxMnOpenHierarchy函数建设子窗口的时间获得用户层执行权限。


尊龙凯时 - 人生就是搏!


xxxMnOpenHierarchy函数继续向下执行,再次来到xxxCreateWindowEx处。


尊龙凯时 - 人生就是搏!


xxxCreateWindowEx挪用了刚刚设置的回调函数childMenuHookProc。


尊龙凯时 - 人生就是搏!


在回调函数childMenuHookProc中,SendMessage发送了WM_NEXTMENU新闻,通过该界说该新闻的回调函数再次修改参数LPARAM,这是为了去掉g_hNewOwner身上的永世锁。


尊龙凯时 - 人生就是搏!


Menu的Owner关系再次被改变,xxxMNKeyDown通过函数UnlockPopMenu去掉g_hNewOwner身上的永世锁。并将这个锁重新加在了g_hMenuOwner上。


尊龙凯时 - 人生就是搏!

尊龙凯时 - 人生就是搏!


这个时间,所有的锁都已经转移到了g_hMenuOwner身上,而由于WH_CBT钩子已经被移除,menu将被弃用,g_hNewOwner将把新建设的窗口link到自己身上。这个时间情形酿成了下面的样子,g_hNewOwner身上已经没有需要绕过的锁了。


尊龙凯时 - 人生就是搏!


接着childMenuHookProc通过SetWindowsHookEx函数又一次设置了回调函数并通过SetWindowLongPtr函数来挪用它,回调函数销毁了g_hNewOwner和xxxCreateWindowEx天生的新窗口。


尊龙凯时 - 人生就是搏!


xxxCreateWindowEx返回的值为ffff871b80239130,这就是xxxCreateWindowEx建设的子窗口。


尊龙凯时 - 人生就是搏!


接下来就可以通过ThreadUnlock来销毁g_hNewOwner和其新建设的子窗口来获得一个UAF误差。


尊龙凯时 - 人生就是搏!


四、总 结



本文对win32k误差挖掘新思绪举行了详细解读,其中包括将unlock函数和工具的Destroy函数的特征关联在一起,并把工具的子资源作为攻击目的寻找新的攻击面的误差挖掘思绪。另外,怎样通过工具内部的特征去绕过锁对工具的锁定的思绪和技巧,也很是具有借鉴意义。