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

Author: 乌云历史资料库 (@wooyun_archive)
Published: 2015-02-06T06:24:00Z
Canonical: https://wepostx.com/topics/708

> 乌云历史资料归档
>
> **原始作者：** insight-labs
> **原始编号：** superkieran-wooyundrops:467
> **原始发布时间：** 2015-02-06 14:24
> **声明：内容仅用于技术研究和个人使用，版权归 wooyun.org。**

---

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](https://rdot.org/forum/showthread.php?t=3341)
Shawn：对于这个漏洞，本文的结论是SMEP虽然被绕过了，但SMAP是依然奏效的，这里只想提一下类似PaX/Grsecurity的UDEREF特性和SMAP类似，只是属于纯软件 实现，大概2006年左右这个特性就已经有了而且被一些anarchy广泛使用。

## 0x00 Intro

CVE-2014-9322的描述如下：

```text
linux内核代码文件arch/x86/kernel/entry_64.S在3.17.5之前的版本都没有正确的处理跟SS（堆栈区）段寄存器相关的错误，这可以让本地用户通过触发一个IRET指令从错误的地址空间去访问GS基地址来提权。
```
这个漏洞于2014年11月23日被社区修复[2](https://bromiumlabs.files.wordpress.com/2015/01/badiret-cartoon.jpg?w=1190)，至今我并没有见到公开的利用代码和详细的讨论。这篇文章我会尝试去解释这个漏洞的本质以及利用的过程。不幸的 是，我无法完全引用Intel白皮书[3](https://bromiumlabs.files.wordpress.com/2015/01/diagram1.png)的所有内容，如果有读者不熟悉一些术语可以直接查Intel白皮书。所有的实验都是在Fedora 20 64-bit发行版上完成的，内核是3.11.10-301，所有的讨论基于64位进行。
简单结论概要：

```text
1. 通过测试，这个漏洞可以完全稳定的被利用。 2. SMEP[4]不能阻止任意代码执行；SMAP[5]可以阻止任意代码执行。
```

## 0x01 Digression: kernel, usermode, iret

[原始图片缺失：2015032302564256625.jpg?w=1190]

## 0x02 漏洞

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

```text
#!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段进行访问时，像这样：

```text
#!bash mov %gs:LOGICAL_ADDRESS, %eax
```
实际会发生以下几步：

```text
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到相应的结构地址上，因此，通常的情况，比如系统调用的处理程序是：

```text
1. swapgs（现在是GS指向内核空间） 2. 通过内存指令和gs前缀访问per-cpu内核数据结构 3. swapgs（撤销之前的swapgs，GS指向用户空间） 4. 返回用户空间
```

## 0x04 触发漏洞

现在很明显可以看到这个漏洞简直就是坟墓，因为多了一个swapgs指令在有漏洞代码路径里，内核会尝试从可能被用户操控的错误GS基地址访问重要的数据结构。
当iret指令产生了一个#SS异常？有趣的是，Intel白皮书在这方面介绍不完全(Shawn:是阴谋论的话又会想到BIG BROTHER?)；描述iret指令时，Intel白皮书这 么讲：

```text
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:

```text
IF stack segment is not present THEN #SS(SS selector); FI;
```
所以在用户空间，我们需要设置ss寄存器为某个值来表示不存在。这不是很直接：
我们不能仅仅使用：

```text
mov $nonpresent_segment_selector, %eax mov %ax, %ss
```
第二条指令会引发#GP。通过调试器（任何ptrace)设置ss寄存器是不允许的；类似的，sys_sigreturn系统调用不会在64位系统上设置这个寄存器（可能32位能工作）。解决方案是：

```text
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异常处理程序，执行一段时间后就调用到了这个函数：

```text
#!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行是：

```text
#!bash 0xffffffff8164b79d : mov %gs:0xc780,%rbx
```
这很变得有意思起来了。我们控制了current指针，她指向用于描述整个Linux进程的数据结构。

```text
319 tsk->thread.error_code = error_code; 320 tsk->thread.trap_nr = X86_TRAP_GP;
```
写入（从task_struct开始的固定偏移）我们控制的地址。注意值本身不能被控制（分别是0和0xd常量），但这不应该成为一个问题。游戏结束？
不会，我们想覆盖一些在X上的重要数据结构。如果我们按照以下的步骤：

```text
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个原因：

- Linux让IDT只读

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

- 我们可以尝试产生一个竞争。“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()所调用的函数：

```text
#!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是一个指针，我们可以设置任意值。

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

```text
#!cpp spin_lock_irqsave(&t->sighand->siglock, flags);
```
t->sighand->siglock在t->sighand->action[sig-1].sa.sa_handler的常量偏移上，内核会调用spin_local_irqsave在某些地址上，X+SPINLOCK的内容无法控制。这会发生什么呢？两种可能性：

- X+SPINLOCK所在的内存地址看起来像没有锁的spinlock。spin_lock_irqsave会立即完成。最后，spin_unlock_irqrestore会撤销spin_lock_irqsave的写操作。
2.X+SPINLOCK所在的内存地址看起来像上锁的spinlock。如果我们不介入的话，spin_lock_irqsave会无线循环等待spinlock。有些担心，要绕过这个障碍我们得需要其他假设 ---|| X+SPINLOCK所在内存地址的内容。这是可接受的，我们可以在后面看到在内核.data区域里设置X。

```text
* 首先，准备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信号，其他进程会被调度来执行。这个系统仍然是稳定的。

![原文图片](/media/2016/06/d598c2aafb27de340ccd8202f12d0137)

## 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变量，这是一个结构体：

```text
#!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的指针会被调用：

```text
#!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是多么幸福活着无挑战的时光啊;-))。这意味着我们需要：

```text
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[1](https://rdot.org/forum/showthread.php?t=3341)序列。这再次假设了具体内核运行的版本情况。通常的"xchg %esp, %eax; ret"代码序列（2个字节，94 c3是在测试内核的地址0xffffffff8119f1ed）很好的可以用于64位内核的ROP。就算没能控制%rax，这个xchg指令操作32位的寄存器也能清掉%rsp的高32位而让%rsp着陆在用户空间的内存里。在最糟糕的情况下，我们可以分配低4GB的虚拟内存然后填充ROP链条。
在当前测试的内核（Fedora 20)有两种方法去deref在FAKE_IOPS的指针：

```text
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链条自身很简洁：

```text
#!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了：

```text
#!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会拒绝从用户空间读取恶意数据。我们能做的是构造所有需要用的数据结构，然后拷贝她们到内核。比如：

```text
#!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)系统调用，她是这样在内核里实现的：

```text
#!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](https://rdot.org/forum/showthread.php?t=3341) CVE-2014-9322 http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2014-9322
[2](https://bromiumlabs.files.wordpress.com/2015/01/badiret-cartoon.jpg?w=1190) Upstream fix http://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/commit/?id=6f442be2fb22be02cafa606f1769fa1e6f894441
[3](https://bromiumlabs.files.wordpress.com/2015/01/diagram1.png) 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/

## Replies
