CVE-2014-3153笔记

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

参考资料

以下是收集的资料:

  1. http://blog.nativeflow.com/the-futex-vulnerability
  2. http://blog.nativeflow.com/escalating-futex
  3. http://blog.nativeflow.com/pwning-the-kernel-root
  4. http://blog.topsec.com.cn/ad_lab/cve2014-3153/
  5. https://github.com/timwr/CVE-2014-3153
  6. https://github.com/android-rooting-tools/libfutex_exploit
  7. https://github.com/nativeflow/pwntex
  8. http://tinyhack.com/2014/07/07/exploiting-the-futex-bug-and-uncovering-towelroot
  9. 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为例说明这个漏洞的过程。

  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的代码:
  6. thread -> futex_wait_requeue_pi(&A, &B);
    此时线程2被唤醒,代码如下:

    由于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的结构如下:

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

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

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

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

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

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

在有漏洞的机器上执行完上述代码,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操作的时候触发断点。

      回复

发表评论

电子邮件地址不会被公开。 必填项已用*标注