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函数的个性关联在一路,并把对象的子资源作为攻击指标寻找新的攻击面的缝隙挖掘思路。另表,若何通过对象内部的个性去绕过锁对对象的锁定的思路和技巧,也极度拥有借鉴意思。