<span id="7ztzv"></span>
<sub id="7ztzv"></sub>

<span id="7ztzv"></span><form id="7ztzv"></form>

<span id="7ztzv"></span>

        <address id="7ztzv"></address>

            原文地址:http://drops.wooyun.org/papers/4860

            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廣泛使用。

            0x00 Intro


            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]可以阻止任意代碼執行。
            

            0x01 Digression: kernel, usermode, iret


            enter image description here

            0x02 漏洞


            在一些情況下,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不了解,請不要跳過下面的章節。

            0x03 偏題: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. 返回用戶空間
            

            0x04 觸發漏洞


            現在很明顯可以看到這個漏洞簡直就是墳墓,因為多了一個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)被設置。

            0x05 Achieving write primitive


            如果運行以上代碼,#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來控制,最終會在內核產生一個頁錯誤。我們怎么避免這個問題?選項有:

            1. 什么都不做。Linux內核不像Windows,Linux內核是完全允許當一個不是預期的頁錯誤在內核出現,如果可能的話,內核會殺死當前進程之后嘗試繼續運行(Windows會藍屏)。這種機制對于大量內核數據污染就無能為力了。我的猜測是在當前進程被殺死后,swapgs不平衡的保持下來,這會導致其他進程上下文的更多頁錯誤。

            2. 使用“tsk->thread.error_code = error_code”覆蓋為頁錯誤處理程序的IDT入口。之后頁錯誤發生(被unhandled_signal()觸發)。這個技術曾經在一些偶然的環境中成功過。但在這里不會成功,因為有2個原因:

            3. 我們可以嘗試產生一個競爭。“tsk->thread.error_code = error_code”會促進代碼執行,比如允許通過系統調用控制的代碼指針P。之后我們可以在CPU 0上觸發漏洞,在同一時間段CPU 1可以循環執行一些系統調用。這個思路可以在CPU 0被破壞前讓通過CPU 1獲得代碼執行,比如hook頁錯誤處理程序,這樣CPU 0不會影響更多的地方,我嘗試了這種方法多次,但都失敗了。可能不同的漏洞在時間線上的不同所致。

            4. 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的內容無法控制。這會發生什么呢?兩種可能性:

            1. X+SPINLOCK所在的內存地址看起來像沒有鎖的spinlock。spin_lock_irqsave會立即完成。最后,spin_unlock_irqrestore會撤銷spin_lock_irqsave的寫操作。

            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信號,其他進程會被調度來執行。這個系統仍然是穩定的。

            enter image description here

            0x07 插曲:SMEP


            SMEP是Intel處理器從第3代Core(Shawn:酷睿)時加入的硬件特性。如果控制寄存器CR4里的SMEP位被設置的話,當RING0(Shawn:標準Linux內核是RING0,在XEN下是例外,RING0是Hypervisor)嘗試執行的代碼來自標記為用戶空間的內存頁,CPU就會生成一個錯誤(Shawn:就是拒絕)。如果可能的話,Linux內核會默認開啟SMEP。

            0x08 實現代碼執行


            之前的章節講述了一種如何以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. %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;
            

            0x09 插曲:SMAP


            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工作正常需要調整所有的驅動,這是一項困難的工作。

            0x0A SMAP to the rescue!


            上面的漏洞利用方法依賴于在用戶空間里準備特定的數據結構,然后強制內核認為她們是可信的內核數據。這種方法對于開啟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/

            <span id="7ztzv"></span>
            <sub id="7ztzv"></sub>

            <span id="7ztzv"></span><form id="7ztzv"></form>

            <span id="7ztzv"></span>

                  <address id="7ztzv"></address>

                      亚洲欧美在线