insight-labs · 2015/02/06 14:24php
from:http://labs.bromium.com/2015/02/02/exploiting-badiret-vulnerability-cve-2014-9322-linux-kernel-privilege-escalation/html
POC( 感謝Mickey提供的連接):node
Shawn:對於這個漏洞,本文的結論是SMEP雖然被繞過了,但SMAP是依然奏效的,這裏只想提一下相似PaX/Grsecurity的UDEREF特性和SMAP相似,只是屬於純軟件 實現,大概2006年左右這個特性就已經有了並且被一些anarchy普遍使用。git
CVE-2014-9322的描述以下:github
linux內核代碼文件arch/x86/kernel/entry_64.S在3.17.5以前的版本都沒有正確的處理跟SS(堆棧區)段寄存器相關的錯誤,這可讓本地用戶經過觸發一個IRET指令從錯誤的地址空間去訪問GS基地址來提權。
複製代碼
這個漏洞於2014年11月23日被社區修復2,至今我並無見到公開的利用代碼和詳細的討論。這篇文章我會嘗試去解釋這個漏洞的本質以及利用的過程。不幸的 是,我沒法徹底引用Intel白皮書3的全部內容,若是有讀者不熟悉一些術語能夠直接查Intel白皮書。全部的實驗都是在Fedora 20 64-bit發行版上完成的,內核是3.11.10-301,全部的討論基於64位進行。shell
簡單結論概要:bash
1. 經過測試,這個漏洞能夠徹底穩定的被利用。
2. SMEP[4]不能阻止任意代碼執行;SMAP[5]能夠阻止任意代碼執行。
複製代碼
在一些狀況下,linux內核經過iret指令返回用戶空間時會產生一個異常。異常處理程序把執行路徑返回到了bad_iret函數,她作了:數據結構
#!bash
/* So pretend we completed the iret and took the #GPF in user mode.*/
pushq $0
SWAPGS
jmp general_protection
複製代碼
正如這行評論所解釋,接下來的代碼流應該和通常保護異常(General Protection)在用戶空間發生時(轉跳到#GP處理程序)徹底相同。這種異常處理狀況大可能是由iret指令引起的,e.g. #GP。less
問題在於#SS異常。若是有漏洞的內核(好比3.17.5)也有"espfix"功能(從3.16引入的特性),以後bad_iret函數會在只讀的棧上執行"push"指令,這會致使頁錯誤(page fault)而會直接引發兩個錯誤。我不考慮這種場景;從如今開始,咱們只關注在3.16之前的沒有"espfix"的內核。
這個漏洞根源於#SS的異常處理程序沒有符合「pretend-it-was-#GP-in-userspace」[6]的規劃,與#GP處理程序相比,#SS異常處理會多作一次swapgs指令。若是你對swapgs不瞭解,請不要跳過下面的章節。
當內存經過gs段進行訪問時,像這樣:
#!bash
mov %gs:LOGICAL_ADDRESS, %eax
複製代碼
實際會發生如下幾步:
1. BASE_ADDRESS值從段寄存器的隱藏部分取出
2. 內存中的線性地址LOGICAL_ADDRESS+BASE_ADDRESS被dereferenced(Shawn:char *p; *p就是deref)。
複製代碼
基地址是從GDT(或者LDT)繼承過來的。不管如何,有一些狀況是GS段基地址被修改的動做不須要GDT的參與。
引用自Intel白皮書:
「SWAPGS把當前GS基寄存器值和在MSR地址C0000102H(IA32_KERNEL_GS_BASE)所包含的值進行交換。SWAPGS指令是一個爲系統軟件設計的特權指令。(....)內核可使用GS前綴在正常的內存引用去訪問[per-cpu]內核數據結構。」
Linux內核爲每一個CPU在啓動時分配一個固定大小的結構體來存放關鍵數據。以後爲每一個CPU加載IA32_KERNEL_GS_BASE到相應的結構地址上,所以,一般的狀況,好比系統調用的處理程序是:
1. swapgs(如今是GS指向內核空間)
2. 經過內存指令和gs前綴訪問per-cpu內核數據結構
3. swapgs(撤銷以前的swapgs,GS指向用戶空間)
4. 返回用戶空間
複製代碼
如今很明顯能夠看到這個漏洞簡直就是墳墓,由於多了一個swapgs指令在有漏洞代碼路徑裏,內核會嘗試從可能被用戶操控的錯誤GS基地址訪問重要的數據結構。
當iret指令產生了一個#SS異常?有趣的是,Intel白皮書在這方面介紹不徹底(Shawn:是陰謀論的話又會想到BIG BROTHER?);描述iret指令時,Intel白皮書這 麼講:
64位模式的異常:
#SS(0)
若是一個嘗試從棧上pop一個值違反了SS限制。
若是一個嘗試從棧上pop一個值引發了non-canonical地址(Shawn: 64-bit下只容許訪問canonical地址)的引用。
複製代碼
沒有一個條件能被強制在內核空間裏發生。不管如何,Intel白皮書裏的iret僞代碼展現了另一種狀況:when the segment defined by the return frame is not present:
IF stack segment is not present
THEN #SS(SS selector); FI;
複製代碼
因此在用戶空間,咱們須要設置ss寄存器爲某個值來表示不存在。這不是很直接:
咱們不能僅僅使用:
mov $nonpresent_segment_selector, %eax
mov %ax, %ss
複製代碼
第二條指令會引起#GP。經過調試器(任何ptrace)設置ss寄存器是不容許的;相似的,sys_sigreturn系統調用不會在64位系統上設置這個寄存器(可能32位能工做)。解決方案是:
1. 線程A:經過sys_modify_ldt系統調用在LDT裏建立一個定製段X
2. 線程B:s:=X_selector
3. 線程A:經過sys_modify_ldt使X無效
4. 線程B:等待硬件中斷
複製代碼
爲何須要在一個進程裏使用兩個線程的緣由是從系統調用(包括sys_modify_ldt)返回是經過硬編碼了#ss值的sysret指令。若是咱們使X在相同的線程中無效就等同於"ss:=X 指令「,ss寄存器會處於未完成設置的狀態。運行以上代碼會致使內核panic。按照更有意義的作法,咱們將須要控制用戶空間的gs基地址;她能夠經過系統調用arch_prctl(ARCH_SET_GS)被設置。
若是運行以上代碼,#SS處理程序會正常的返回bad_iret(意思是沒有觸及到內存的GS基地址),以後轉跳到#GP異常處理程序,執行一段時間後就調用到了這個函數:
#!cpp
289 dotraplinkage void
290 do_general_protection(struct pt_regs *regs, long error_code)
291 {
292 struct task_struct *tsk;
...
306 tsk = current;
307 if (!user_mode(regs)) {
... it is not reached
317 }
318
319 tsk->thread.error_code = error_code;
320 tsk->thread.trap_nr = X86_TRAP_GP;
321
322 if (show_unhandled_signals && unhandled_signal(tsk, SIGSEGV) &&
323 printk_ratelimit()) {
324 pr_info("%s[%d] general protection ip:%lx sp:%lx
error:%lx",
325 tsk->comm, task_pid_nr(tsk),
326 regs->ip, regs->sp, error_code);
327 print_vma_addr(" in ", regs->ip);
328 pr_cont("\n");
329 }
330
331 force_sig_info(SIGSEGV, SEND_SIG_PRIV, tsk);
332 exit:
333 exception_exit(prev_state);
334 }
複製代碼
C代碼不太明顯,但從gs前綴讀取到現有宏的值賦給了tsk。第306行是:
#!bash
0xffffffff8164b79d : mov %gs:0xc780,%rbx
複製代碼
這很變得有意思起來了。咱們控制了current指針,她指向用於描述整個Linux進程的數據結構。
319 tsk->thread.error_code = error_code;
320 tsk->thread.trap_nr = X86_TRAP_GP;
複製代碼
寫入(從task_struct開始的固定偏移)咱們控制的地址。注意值自己不能被控制(分別是0和0xd常量),但這不該該成爲一個問題。遊戲結束?
不會,咱們想覆蓋一些在X上的重要數據結構。若是咱們按照如下的步驟:
1. 準備在FAKE_PERCPU的用戶空間內存,設置gs基地址給她
2. 讓地址FAKE_PERCPU+0xc780存着指針FAKE_CURRENT_WITH_OFFSET,以知足FAKE_CURRENT_WITH_OFFSET= X – offsetof(struct task_struct,thread.error_code)
3. 觸發漏洞
複製代碼
以後do_general_protection會寫入X。但很快就會嘗試再次訪問current task_current的其餘成員,e.g.unhandled_signal()函數從task_struct指針解引用。咱們沒有依賴X來控制,最終會在內核產生一個頁錯誤。咱們怎麼避免這個問題?選項有:
什麼都不作。Linux內核不像Windows,Linux內核是徹底容許當一個不是預期的頁錯誤在內核出現,若是可能的話,內核會殺死當前進程以後嘗試繼續運行(Windows會藍屏)。這種機制對於大量內核數據污染就無能爲力了。個人猜想是在當前進程被殺死後,swapgs不平衡的保持下來,這會致使其餘進程上下文的更多頁錯誤。
使用「tsk->thread.error_code = error_code」覆蓋爲頁錯誤處理程序的IDT入口。以後頁錯誤發生(被unhandled_signal()觸發)。這個技術曾經在一些偶然的環境中成功過。但在這裏不會成功,由於有2個緣由:
咱們能夠嘗試產生一個競爭。「tsk->thread.error_code = error_code」會促進代碼執行,好比容許經過系統調用控制的代碼指針P。以後咱們能夠在CPU 0上觸發漏洞,在同一時間段CPU 1能夠循環執行一些系統調用。這個思路能夠在CPU 0被破壞前讓經過CPU 1得到代碼執行,好比hook頁錯誤處理程序,這樣CPU 0不會影響更多的地方,我嘗試了這種方法屢次,但都失敗了。可能不一樣的漏洞在時間線上的不一樣所致。
Throw a towel on 「tsk->thread.error_code = error_code」 write.
雖然有些噁心,咱們會嘗試最後一個選項。咱們會讓current指向用戶空間,設置這個指針能夠經過讀的deref到咱們能控制的內存。天然的,咱們觀察接下來的代碼,找找更多的寫deref。
0x06. Achieving write primitive continued, aka life after do_general_protection
下一個機會是do_general_protection()所調用的函數:
#!cpp
int
force_sig_info(int sig, struct siginfo *info, struct task_struct *t)
{
unsigned long int flags;
int ret, blocked, ignored;
struct k_sigaction *action;
spin_lock_irqsave(&t->sighand->siglock, flags);
action = &t->sighand->action[sig-1];
ignored = action->sa.sa_handler == SIG_IGN;
blocked = sigismember(&t->blocked, sig);
if (blocked || ignored) {
action->sa.sa_handler = SIG_DFL;
if (blocked) {
sigdelset(&t->blocked, sig);
recalc_sigpending_and_wake(t);
}
}
if (action->sa.sa_handler == SIG_DFL)
t->signal->flags &= ~SIGNAL_UNKILLABLE;
ret = specific_send_sig_info(sig, info, t);
spin_unlock_irqrestore(&t->sighand->siglock, flags);
return ret;
}
複製代碼
task_struct的成員sighand是一個指針,咱們能夠設置任意值。
action = &t->sighand->action[sig-1];
action->sa.sa_handler = SIG_DFL;
複製代碼
咱們沒法控制寫的值,SIG_DFL是常量的0。這裏最終能工做了,雖然有些扭曲。假設咱們想覆蓋內核地址X。爲此咱們準備僞造的task_struct,因此X等於t->sighand->action[sig-1].sa.sa_handler的地址。上面還有一行要注意:
#!cpp
spin_lock_irqsave(&t->sighand->siglock, flags);
複製代碼
t->sighand->siglock在t->sighand->action[sig-1].sa.sa_handler的常量偏移上,內核會調用spin_local_irqsave在某些地址上,X+SPINLOCK的內容沒法控制。這會發生什麼呢?兩種可能性:
2.X+SPINLOCK所在的內存地址看起來像上鎖的spinlock。若是咱們不介入的話,spin_lock_irqsave會無線循環等待spinlock。有些擔憂,要繞過這個障礙咱們得須要其餘假設 ---|| X+SPINLOCK所在內存地址的內容。這是可接受的,咱們能夠在後面看到在內核.data區域裏設置X。
* 首先,準備FAKE_CURRENT,讓t->sighand->siglock指向用戶空間上鎖的區域,SPINLOCK_USERMODE
* force_sig_info()會掛在spin_lock_irqsave裏
* 這時,另一個用戶空間的線程在另一個CPU上運行,而且改變了t->sighand,因此t->sighand->action[sig-1.sa.sa_hander成了咱們的覆蓋目標,以後解鎖SPINLOCK_USERMODE
* spin_lock_irqsave會返回
* force_sig_info()會從新載入t->sighand,執行指望的寫操做
複製代碼
鼓勵細心的讀者追問爲何不能使用第2種方案,即X+SPINLOCK在初始時是沒有鎖的。這並非所有 ---|| 咱們須要準備一些FAKE_CURRENT的字段來讓儘可能少的代碼執行。我不會再透露更多細節 ---|| 這篇BLOG已經夠長了....下一步會發生什麼?force_sig_info()和do_general_protection()返回。接下來iret指令會再次產生#SS異常處理(由於仍然是用戶空間ss的值在棧上引用了一個nonpresent段),但這一次,#SS處理程序裏的額外swapgs指令會返回並取消以前不正確的swapgs。 do_general_protection()會調用和操做真正的task_struct,而不是僞造的FAKE_CURRENT。最終,current會發出SIGSEGV信號,其餘進程會被調度來執行。這個系統仍然是穩定的。
SMEP是Intel處理器從第3代Core(Shawn:酷睿)時加入的硬件特性。若是控制寄存器CR4裏的SMEP位被設置的話,當RING0(Shawn:標準Linux內核是RING0,在XEN下是例外,RING0是Hypervisor)嘗試執行的代碼來自標記爲用戶空間的內存頁,CPU就會生成一個錯誤(Shawn:就是拒絕)。若是可能的話,Linux內核會默認開啓SMEP。
以前的章節講述了一種如何以0在內核內存中覆蓋8個連續字節的方法。若是SMEP開啓的狀況下如何實現代碼執行呢?
直接覆蓋一個內核代碼的指針是不行的。咱們能夠清零top bytes( Shawn: MSB)- 但以後的地址會在用戶空間,因此SMEP會阻止這個指針的deref。
換一種方式,咱們能夠清零幾個low bytes( Shawn: LSB),可是以後能利用這個指針的機率也很低。
咱們須要一個內核指針P指向結構X包含了代碼指針。咱們能夠覆蓋P的top bytes讓她成爲一個用戶空間的地址,這樣P->code_pointer_in_x()調用會跳轉到一個咱們能選擇的地址。我不肯定最好選擇哪一個攻擊對象。從個人經驗來看,我選擇內核proc_root變量,這是一個結構體:
#!cpp
struct proc_dir_entry {
...
const struct inode_operations *proc_iops;
const struct file_operations *proc_fops;
struct proc_dir_entry *next, *parent, *subdir;
...
u8 namelen;
char name[];
};
複製代碼
這個結構體是一個proc文件系統的入口(proc_root是/proc做爲proc文件系統的根目錄)。當一個文件名路徑開始在/proc裏查詢時,subdir指針(從proc_root.subdir開始)會跟進,直到名字被找到。以後proc_iops的指針會被調用:
#!cpp
struct inode_operations {
struct dentry * (*lookup) (struct inode *,struct dentry *, unsigned int);
void * (*follow_link) (struct dentry *, struct nameidata *);
...many more...
int (*update_time)(struct inode *, struct timespec *, int);
...
} ____cacheline_aligned;
複製代碼
proc_root駐紮在內核代碼段裏,這意味着漏洞利用須要知道她的地址。這個信息能夠從/proc/kallsyms符號表獲得;固然,不少加固過的內核不容許普通用戶讀取這個文件。但若是內核是一個已知的build(標準的GNU/Linux發行版),這個地址能夠輕鬆得到;和一堆偏移同樣須要構建FAKE_CURRENT。
咱們會覆蓋proc_root.subdir,讓她成爲一個指向一個在用戶空間能被控制的結構體proc_dir_entry。有點困難在於咱們不能覆蓋整個指針。別忘了咱們的寫操做是「覆蓋8個0」。若是咱們讓proc_root.subdir變成0,咱們不會去映射她,由於Linux內核不容許用戶空間映射到地址0上(更確切的說發是,任何低於/proc/sys/vm/mmap_min_addr的地址,默認值通常是4k)。(Shawn:想一想哪些0ld good hacking days,天天都有一堆NULL pointer deref是多麼幸福活着無挑戰的時光啊;-))。這意味着咱們須要:
1. 映射16MB的內存到地址4096
2. 使用相似proc_dir_entry的方式來填充,把inode_operations字段指向用戶空
間的地址FAKE_IOPS,name字段爲字符串"A"。
3. 配置漏洞利用去覆蓋proc_root.subdir的top 5 bytes。
複製代碼
以後,除非proc_root.subdir最低的3 bytes是0,咱們能夠肯定在觸發force_sig_info()覆蓋後,proc_root.subdir會指向被控制的用戶空間內存。當咱們的進程調用open("/proc/A",...)時,FAKE_IOPS的指針會被調用。她們應該指向哪裏呢?若是你認爲答案是「指向咱們的shellcode「,請再讀一遍上面的分析。
咱們須要讓FAKE_IOPS指針指向一個stack pivot1序列。這再次假設了具體內核運行的版本狀況。一般的"xchg %esp, %eax; ret"代碼序列(2個字節,94 c3是在測試內核的地址0xffffffff8119f1ed)很好的能夠用於64位內核的ROP。就算沒能控制%rax,這個xchg指令操做32位的寄存器也能清掉%rsp的高32位而讓%rsp着陸在用戶空間的內存裏。在最糟糕的狀況下,咱們能夠分配低4GB的虛擬內存而後填充ROP鏈條。
在當前測試的內核(Fedora 20)有兩種方法去deref在FAKE_IOPS的指針:
1. %rax:=FAKE_IOPS; call *SOME_OFFSET(%rax)
2. %rax:=FAKE_IOPS; %rax:=SOME_OFFSET(%rax); call *%rax
複製代碼
第1種狀況裏,在%rsp和%rax交換值後,她會等於FAKE_IOPS。咱們須要ROP鏈條駐紮在FAKE_IOPS的起始位置,這須要相似「add $A_LOT, %rsp; ret」的指令,而後在繼續。
第2種狀況裏,%rsp會分配低32位的調用目標,即0x8119f1ed。咱們須要準備在這個地址上的ROP鏈條。
計算一下%rax值有二者之一的已知值在特定的時間指向stack pivot序列,咱們不須要ROP鏈條填充整個4GB內存,只須要上面的兩個地址便可。第2種狀況的ROP鏈條自身很簡潔:
#!bash
unsigned long *stack=0x8119f1ed;
*stack++=0xffffffff81307bcdULL; // pop rdi, ret
*stack++=0x407e0; //cr4 with smep bit cleared
*stack++=0xffffffff8104c394ULL; // mov rdi, cr4; pop %rbp; ret
*stack++=0xaabbccdd; // placeholder for rbp
*stack++=actual_shellcode_in_usermode_pages;
複製代碼
SMAP是Intel從第5代Core處理器推出的一個硬件特性。若是CR4控制寄存器的SMAP位被設置的話,CPU會拒絕用戶空間的頁被RING0訪問(Shawn:我的理解,SMAP和SMEP最大的不一樣主要是SMEP針對代碼段,而SMAP針對數據段)。Linux內核一般會默認開啓SMAP。一個測試的內核模塊(Core-M 5Y10a CPU)嘗試訪問用戶空間而後crash了:
#!bash
[ 314.099024] running with cr4=0x3407e0
[ 389.885318] BUG: unable to handle kernel paging request at 00007f9d87670000
[ 389.885455] IP: [ffffffffa0832029] test_write_proc+0x29/0x50 [smaptest]
[ 389.885577] PGD 427cf067 PUD 42b22067 PMD 41ef3067 PTE 80000000408f9867
[ 389.887253] Code: 48 8b 33 48 c7 c7 3f 30 83 a0 31 c0 e8 21 c1 f0 e0 44 89 e0 48 8b
複製代碼
正如咱們看到的,用戶空間的頁是正常的,但訪問也報了頁錯誤。Windows系統不太支持SMAP;Windows 10技術預覽版build 9926的cr4=0x1506f8(SMEP啓動,SMAP關閉);對比Linux內核(一樣的測試硬件)你能夠看到cr4的bit 21是沒有設置的。這不奇怪,在Linux中,訪問用戶空間是經過調用copy_from_user(),copy_to_user()和相似函數顯式執行的,因此執行這些操做時臨時關閉SMAP是可行的。在Windows上,內核代碼直接訪問用戶空間代碼,只是包裝了一層訪問異常處理程序,因此要讓SMAP工做正常須要調整全部的驅動,這是一項困難的工做。
上面的漏洞利用方法依賴於在用戶空間裏準備特定的數據結構,而後強制內核認爲她們是可信的內核數據。這種方法對於開啓SMAP特性的內核不奏效 ---|| CPU會拒絕從用戶空間讀取惡意數據。咱們能作的是構造全部須要用的數據結構,而後拷貝她們到內核。好比:
#!cpp
write(pipe_filedescriptor, evil_data, ...
複製代碼
以後evil_data會被拷貝到一個內核管道緩衝區裏。咱們可能須要猜想她的地址; some sort of heap spraying, combined with the fact that there is no spoon^W effective kernel ASLR[9], could work, although it is likely to be less reliable than exploitation without SMAP.
總之,還有最後一個障礙 ---|| 不要忘了咱們須要設置用戶空間的gs base去指向咱們的漏洞利用的數據結構。在上面的場景(沒有SMAP),咱們使用arch_prctl(ARCH_SET_GS)系統調用,她是這樣在內核裏實現的:
#!bash
long do_arch_prctl(struct task_struct *task, int code, unsigned long addr)
{
int ret = 0;
int doit = task == current;
int cpu;
switch (code) {
case ARCH_SET_GS:
if (addr >= TASK_SIZE_OF(task))
return -EPERM;
... honour the request otherwise
複製代碼
休斯頓,咱們有一個麻煩 ---|| 咱們不能使用這個API去設置gs base用戶空間以上的內存!
最近的CPU有wrgsbase指令能夠直接設置gs base,這是一個非特權級指令,但須要經過內核設置CR4控制寄存器中的FSGSBASE bit( no 16)來開啓。Linux並無設置這個位,所以用戶空間不能使用這條指令。
在64位系統上,非系統級的GDT和LDT條目依然是8個字節長,base field是最大4GB-1,因此根本沒有機會設置一個基地址的段在內核空間裏。因此,除非我漏掉了能在內核裏設置用戶態gs base的其餘方法,否則SMAP能保護CVE-2014-9322針對64位Linux內核任意代碼執行的漏洞利用。
1 CVE-2014-9322 http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2014-9322
2 Upstream fix http://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/commit/?id=6f442be2fb22be02cafa606f1769fa1e6f894441
3 Intel Software Developer’s Manuals, http://www.intel.com/content/www/us/en/processors/architectures-software-developer-manuals.html
[4] SMEP http://vulnfactory.org/blog/2011/06/05/smep-what-is-it-and-how-to-beat-it-on-linux/
[5] SMAP http://lwn.net/Articles/517475
[6] "pretend-it-was-#GP-in-userspace" https://lists.debian.org/debian-kernel/2014/12/msg00083.html
[7] Stack Pivoting https://trailofbits.files.wordpress.com/2010/04/practical-rop.pdf
[8] TSX improves timing attacks against KASLR http://labs.bromium.com/2014/10/27/tsx-improves-timing-attacks-against-kaslr/