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/