1研究背景
4月1日,以色列安全研究員Gil Dabah在部落格上釋出了一篇關於win32k漏洞研究文章,描述瞭如何透過核心物件的Destroy函式和win32k user-mode callback緩解措施的特性來尋找UAF漏洞的新思路。
為此,啟明星辰ADLab對win32k相關核心機制進行研究分析,並對這類漏洞的挖掘思路進行詳細解讀分析。
2win32k漏洞緩解與對抗
2.1 win32k user-mode callback漏洞
由於設計原因,win32k驅動需要處理很多使用者層的回撥,這些回撥給win32k模組的安全帶來了非常大的隱患,並在過去10年時間貢獻了大量的漏洞。
為了便於漏洞描述,以如下虛擬碼進行舉例分析。
上述程式碼執行效果如下圖所示,使用者層執行的某函式透過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)。經過加固之後程式碼表現為如下形式:
透過上述程式碼,可以保證即使callback被執行,p在xxxSetWindowStyle函式執行的時候也不會被釋放。
2.3緩解機制的對抗技術
上一節提到了物件的引用計數,如果物件的引用計數為正,即使執行物件的destroy函式,物件沒有真正被釋放,仍舊存留在記憶體中,這種物件被微軟開發者稱為殭屍(Zombie)物件。一旦殭屍物件的引用計數減少到0它將會消失,但是在此之前它仍舊存在記憶體中,只是使用者層無法訪問該物件。
同時為了防止殭屍物件繼續存留在記憶體中,鎖的釋放函式(ThreadUnlock/ HMAssignmentUnlock)一般會包含物件的釋放環節。
物件的Destroy函式還有一個特性就是在釋放物件的同時,Destroy函式也會釋放物件的子資源,其過程可以簡要描述如下。
DestroyWindow在第一次呼叫時釋放子資源,一旦視窗不再被引用,控制程式碼管理器就會再次完全銷燬它,一般情況下,第二次銷燬Destroy函式不會在去處理子資源,因為第一次已經釋放了所有的子資源。
但是事情往往不是這麼簡單,事實上即使是一個已經呼叫過相應Destroy函式釋放的殭屍物件,仍然有機會對其本身進行一些更改(回撥之後核心程式碼仍會對物件進行一些操作),我們把這種情況叫做Zombie Reload,當該殭屍物件由於引用計數為0而被真正釋放時,之前的更改操作將會給核心帶來一些隱患。
對於如下程式碼片段:
我們在使用者層回撥中對pwnd執行了Destroy函式,然後透過InternalSetTimer為之設定了一個計時器,當ThreadUnlock將pwnd真正釋放的時候,計時器也將被釋放,那麼接下來對計時器的操作將會導致UAF漏洞的產生。
3案例分析
上一節我們討論了物件的引用計數和鎖給物件帶來的新的安全隱患,但是真正的挑戰在於我們如何確定一段程式碼中存在漏洞,關鍵點是確保在unlock函式中釋放的物件在執行到有問題的程式碼時其引用計數應該為1,只有這樣我們才能在使用者層回撥呼叫其Destroy函式,並透過unlock函式將這個物件真正釋放掉(上鎖的時候會做+1處理),這也是我們接下來需要討論的。下面我們透過一個案例來分析漏洞挖掘思路。
3.1漏洞成因
下圖是xxxMnOpenHierarchy函式的程式碼片段。
圖中透過xxxCreateWindowEx可以獲得一個返回使用者層執行callback函式的機會,xxxCreateWindowEx建立的視窗將作為父視窗*(structtagWND **)(**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漏洞。
4總結
本文對win32k漏洞挖掘新思路進行了詳細解讀,其中包括將unlock函式和物件的Destroy函式的特性關聯在一起,並把物件的子資源作為攻擊目標尋找新的攻擊面的漏洞挖掘思路。另外,如何透過物件內部的特性去繞過鎖對物件的鎖定的思路和技巧,也非常具有借鑑意義。