BadIRET漏洞利用


Linux内核代码文件arch/x86/kernel/entry_64.S在3.17.5之前的版本都没有正确 的处理跟SS(堆栈区)段寄存器相关的错误,这可以让本地用户通过触发一个 IRET指令从错误的地址空间去访问GS基地址来提权。这个编号为CVE-2014-9322,漏洞于2014年11月23日被Linux内核社区修复,之后的几个礼拜里没有出现公开的利用代码甚至相关的讨论。当人们快要遗忘这个威胁的时候,Rafal Wojtczuk的分析文章Exploiting “BadIRET” vulnerability(未完成的翻译版【见下 注1)似乎在提醒我们:别忘了斯拉夫兵工厂。Rafal在Fedora 20 64-bit GNU/Linux发行版上完成了研究和测试工作,内核是3.11.10-301,甚至SMEP和SMAP也有办法绕过。也提醒那些平时不重视安全运维的企业和个人,修复已知漏洞是必须的工作,因为你永远不知道你的敌人在哪里买卖什么样的数字军火。"

注1:

作者:Rafal Wojtczuk,Feb 2 2015

原文:Exploiting “BadIRET” vulnerability (CVE-2014-9322, Linux kernel privilege escalation)

http://labs.bromium.com/2015/02/02/exploiting-badiret-vulnerability-cve-2014-9322-linux-kernel-privilege-escalation/

译者:Shawn the R0ck,(参与者自己把名字加到后面)


--[ 0. 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]不能阻止任意代码执行。


--[ 1. Digression: kernel, usermode, iret

...............................
来原文看图吧:
http://labs.bromium.com/2015/02/02/exploiting-badiret-vulnerability-cve-2014-9322-linux-kernel-privilege-escalation/
...............................


--[ 2. 漏洞

在一些情况下,linux内核通过iret指令返回用户空间时会产生一个异常。异常处
理程序把执行路径返回到了bad_iret函数,她做了:

--------------------------------------------------------------------------
     /* 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不了解,请不要跳过下面的章节。


--[ 3. 偏题:swapgs指令

当内存通过gs段进行访问时,像这样:

------------------------------------------
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. 返回用户空间

Naturally, kernel code must ensure that whenever it wants to access
percpu data with gs prefix, the number of swapgs instructions executed
by the kernel since entry from usermode is noneven (so that gs base
points to kernel memory).


--[ 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)被设置。


--[ 5. Achieving write primitive 

如果运行以上代码,#SS处理程序会正常的返回bad_iret(意思是没有触及到内存
的GS基地址),之后转跳到#GP异常处理程序,执行一段时间后就调用到了这个函
数:

--------------------------------------------------------------------

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行是:

----------------------------------------------------------------

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个原因:

	* Linux让IDT只读

	* 就算IDT可写,我们也不能控制覆盖的值 -- 0或者0xd。SMEP/SMAP也
          会是问题。

3.  We can try a race. Say, “tsk->thread.error_code = error_code”
write facilitates code execution, e.g. allows to control code pointer
P that is called via SOME_SYSCALL. Then we can trigger our
vulnerability on CPU 0, and at the same time CPU 1 can run
SOME_SYSCALL in a loop. The idea is that we will get code execution
via CPU 1 before damage is done on CPU 0, and e.g. hook the page fault
handler, so that CPU 0 can do no more harm.  I tried this approach a
couple of times, with no luck; perhaps with different vulnerability
the timings would be different and it would work better.

4. Throw a towel on “tsk->thread.error_code = error_code” write.

虽然有些恶心,我们会尝试最后一个选项。我们会让current指向用户空间,设置
这个指针可以通过读的deref到我们能控制的内存。自然的,我们观察接下来的代
码,找找更多的写deref。


--[ 6. Achieving write primitive continued, aka life after do_general_protection

下一个机会是do_general_protection()所调用的函数:

-----------------------------------------------------------------------

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的地址。上面还有一行要注意:

-----------------------------------------------------------------------

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信号,其他进程会被调度来执行。
这个系统仍然是稳定的。

................................................
看原文的图:

................................................



[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

本文永久更新链接地址:

相关内容

    暂无相关文章