from:http://labs.bromium.com/2015/02/02/exploiting-badiret-vulnerability-cve-2014-9322-linux-kernel-privilege-escalation/
POC( 感謝Mickey提供的鏈接):
https://rdot.org/forum/showthread.php?t=3341
Shawn:對于這個漏洞,本文的結論是SMEP雖然被繞過了,但SMAP是依然奏效的,這里只想提一下類似PaX/Grsecurity的UDEREF特性和SMAP類似,只是屬于純軟件 實現,大概2006年左右這個特性就已經有了而且被一些anarchy廣泛使用。
CVE-2014-9322的描述如下:
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位進行。
簡單結論概要:
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。
問題在于#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 pivot
在當前測試的內核(Fedora 20)有兩種方法去deref在FAKE_IOPS的指針: 第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鏈條自身很簡潔: SMAP是Intel從第5代Core處理器推出的一個硬件特性。如果CR4控制寄存器的SMAP位被設置的話,CPU會拒絕用戶空間的頁被RING0訪問(Shawn:個人理解,SMAP和SMEP最大的不同主要是SMEP針對代碼段,而SMAP針對數據段)。Linux內核通常會默認開啟SMAP。一個測試的內核模塊(Core-M 5Y10a CPU)嘗試訪問用戶空間然后crash了: 正如我們看到的,用戶空間的頁是正常的,但訪問也報了頁錯誤。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會拒絕從用戶空間讀取惡意數據。我們能做的是構造所有需要用的數據結構,然后拷貝她們到內核。比如: 之后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)系統調用,她是這樣在內核里實現的: 休斯頓,我們有一個麻煩 ---|| 我們不能使用這個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/1. %rax:=FAKE_IOPS; call *SOME_OFFSET(%rax)
2. %rax:=FAKE_IOPS; %rax:=SOME_OFFSET(%rax); call *%rax
#!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;
0x09 插曲:SMAP
#!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
0x0A SMAP to the rescue!
#!cpp
write(pipe_filedescriptor, evil_data, ...
#!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