Android内核提权cve-2014-3153研究笔记
一、简介
我这里把我自己的理解总结下,看别人的总是云山雾绕,不得要领。还是要有自己的思路。当然也希望自己写的通俗一些,那么又有一大批人能看懂了就。
文中图片修改了文尾链接处作者的图片,部分例子采用参考中所得。各位想做下实验的可以参考我上一篇的编译过程,也可以看我给出的链接。
二、触发机制
科普下锁的概念:锁就是由于多线程同时访问资源会造成资源更改混乱而增加的概念。简单说,有一个公共资源。一个人在用的时候,其他人就要等着。不能两个或多个人同时用。
这个漏洞利用Futex(Fast UserspacemuTEX)(是一种锁机制)的不同唤醒方式,绕过了栈数据清理。从而控制了流程。
如何绕过?
漏洞利用了futex_requeue,futex_lock_pi,futex_wait_requeue_pi三个函数存在的两个bug位于futex.c
git clone 7d4K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0W2k6$3!0G2k6$3I4W2M7$3!0#2M7X3y4W2i4K6u0W2j5$3!0E0i4K6u0r3K9$3g2J5L8X3g2D9i4K6u0r3k6$3!0D9k6r3k6A6M7$3S2Q4x3X3g2Y4K9i4b7`. -bgoldfish3.4
cd goldfish
git checkout e8c92d268b8b8feb550ca8d24a92c1c98ed65acekernel/futex.c
可以自行下载一下。
2.1.relockBUG
relock漏洞源于futex_lock_pi函数(由futex_lock_pi_atomic实现),futex_lock_pi(&uaddr)调用之后,调用地址uaddr被锁住,只有利用解锁futex_unlock_pi后,才能被其他线程利用。
futex_lock_pi_atomic又是由cmpxchg_futex_value_locked(&curval,uaddr,
0,newval)实现并尝试去锁住uaddr。它的实现的含义是如果uaddr中存储的值为0,那么就说明没有线程占用锁,成功的获取到了锁,并将当前线程的id写进去。(uaddr是用户空间的一个整形变量,被用于Futex系统架构中的futex互斥量。uaddr的值与其用户空间的地址都会被Futex用到。)
但是问题来了,既然uaddr是用户变量,那我们就可以手动设置为0.这时候地址上的锁其实是释放了,但上锁后的堆栈里的内容没有被清理。而且没有唤醒阻塞在锁上的线程,修改pi_state等。
这样就可以利用通过手动设置uaddr=0的方式使两个线程同时获得锁。这个叫relock。可以叫多重上锁。
2.2 requeueBUG
futex_wait_requeue_pi的功能是让调用线程阻塞在uaddr1上,然后等待futex_requeue的唤醒。唤醒过程将所有阻塞在uaddr1上的线程全部移动到uaddr2上去。

syscall(__NR_futex, &uaddr1,FUTEX_WAIT_REQUEUE_PI, 1, 0, &uaddr2, uaddr1); //在uaddr1上等待
syscall(__NR_futex,
&uaddr1, FUTEX_CMP_REQUEUE_PI, 1, 0,&uaddr2,
uaddr1);//尝试获取uaddr2上的锁,然后唤醒uaddr1上等待的线程。如果uaddr2锁获取失败,则将被唤醒线程添加到uaddr2的rt_waiter列表上,进入线程进入内核等待。啥时候进入内核等待,我们下面讲。
进入内核等待方式图
这个时候如果我们再次调用
syscall(__NR_futex, &uaddr1, FUTEX_CMP_REQUEUE_PI, 1, 0,&uaddr2, uaddr1)
将失败而直接返回,并不会进入系统调用。
而requeueBUG允许我们在以上两条语句执行之后,首先设置uaddr2=0,然后执行这样的语句:
syscall(__NR_futex, &uaddr2, FUTEX_CMP_REQUEUE_PI, 1, 0,&uaddr2, uaddr2);
这个语句中所有地址都变成了uaddr2,也就是说将等待在uaddr2上的线程重排到uaddr2上,这是不合逻辑的,但是Futex没有检查这样的调用,也就是说没有检查uaddr1
==uaddr2的情况,从而造成了我们可以二次进入futex_requeue中进行唤醒操作。我们的线程进入内核等待后本来需要用内核唤醒的方式,现在被篡改成了普通的唤醒方式。致使一部分的栈没有被清空。就是栈上的rt_waiter依然被连在rt_mutex的waiterlist上。
2.3漏洞触发
这里还要了解一下futex_requeue中唤醒futex_wait_requeue_pi线程的两种方式:
1.futex_proxy_trylock_atomic调用尝试获取uaddr2上的锁,如果成功,则唤醒等待线程,函数返回,否则继续执行。注意,这一步没有进入内核互斥量中,如果成功,将不进入内核互斥量,而是直接返回到用户空间,从而减小内核互斥量的开销;
2.rt_mutex_start_proxy_lock尝试获取uaddr2锁,如果成功,则唤醒等待线程,如果失败,则将线程阻塞到uaddr2的内核互斥量上,将rt_waiter加入rt_mutex的waiterlist。
我们来总结下正常程序的执行状态。
futex_wait_requeue_pi(uaddr1,uaddr2)等待被唤醒,正常情况下我们唤醒的方式要么在内核唤醒,要么普通的唤醒。这个要看uaddr2的锁状态。
漏洞触发图
但是我们这里利用uaddr2加锁使线程进入内核等待状态,然后relockBUG uaddr2=0,最后
requeueBUGfutex_wait_requeue_pi(uaddr2,uaddr2),使阻塞在内核等待的线程用普通方式唤醒。构造了程序的异常执行流。
如何使一个线程按我们的方式执行如下图:
异常流程构造图

二、提权过程
上一节我们讲到了rt_waiter没有了owner,但是有什么用呢?
这里我们会用一种机制来更改这个没有owner的rt_waiter的数据
2.1栈内存共用问题
#include <stdio.h>
void A(int val)
{
int local;
local =val;
printf("A locacladdr =0x%x\n",&local);
}
void B(int val2)
{
int local;
printf("B locacladdr =0x%x\n",&local);
printf("B local =%d\n",local);
}
int main()
{
A(6);
B(2);
return 0;
}
这里用GCC编译,不进行优化
gcc -m32 foo.c -o foo -g
./foo
A locacladdr = 0xffd119b8
B locacladdr = 0xffd119b8
B local = 6
图栈
我们可以看到,这里A,B函数的局部变量的地址是一样的。有堆栈概念的人都知道,我们的每调用一个函数就会产生一个新的堆栈。但是上一个调用函数的栈中的数据如果没销毁,下一个函数构造的栈中就能利用。我们就可非法篡改数据。如上图的实例。
哈,有啥用,我们可以直接调用A函数修改B函数中的数据。
2.2修改内核中的数据
我们可以调用另一个结构相似的函数修改我们的rt_waiter结构数据。我们选取__sys_sendmmsg函数。
有其他选择么?有。有一个分析栈空间的脚本,checkstack.pl的脚本,断到futex_wait_requeue_pi上可以看到很多函数。这里选择可以完成rt_waiter所在栈深度修改的一个。
这里我们要修改的是链表。
rt_waiter结构
type = struct rt_mutex_waiter{
struct plist_nodelist_entry;
struct plist_nodepi_list_entry;
struct task_struct*task;
struct rt_mutex *lock
}
plist结构
[培训]科锐逆向工程师培训第53期2025年7月8日开班!