Linux内核垃圾回收的量子态:CVE-2021-0920(第一部分)

访问原始链接 Google 翻译

深入分析一个野外Android漏洞利用

作者:Xingyu Jin,Android安全研究
这是由两部分组成的客座博客文章的第一部分,我们将首先探讨CVE-2021-0920漏洞的根本原因。在第二篇文章中,我们将深入分析该漏洞的野外0day利用以及入侵后模块。

野外CVE-2021-0920漏洞利用概述

一家名为Wintego的监控供应商开发了针对Linux socket系统调用0day漏洞CVE-2021-0920的利用程序,并根据最早捕获的样本,至少在2020年11月就开始在野外使用,直到2021年11月该问题被修复。结合Chrome和三星浏览器漏洞利用,该供应商能够远程获取三星设备的root权限。修复程序随2021年11月Android安全公告发布,并在三星2021年12月的安全更新中应用于三星设备。

Google威胁分析小组(TAG)发现了在野外使用的三星浏览器漏洞利用链。TAG随后进行了根本原因分析,发现该漏洞CVE-2021-0920被用于逃逸沙箱并提升权限。CVE-2021-0920被匿名报告给Linux/Android。Google Android安全团队对该漏洞利用进行了全面的深入分析。

这个问题最初由RedHat内核开发人员在2016年发现,并在公开电子邮件线程中披露,但Linux内核社区没有修补该问题,直到2021年重新报告。

各种三星设备成为攻击目标,包括三星S10和S20。通过滥用Linux内核垃圾回收中的一个短暂竞争条件,漏洞利用代码能够在内核sk_buff对象中获得一个释放后使用(UAF)。野外样本能够有效绕过CONFIG_ARM64_UAO,实现任意读/写原语,并绕过三星RKP以提升到root权限。其他Android设备也容易受到攻击,但我们没有发现针对它们的任何漏洞利用样本。

从捕获的样本中提取的文本将该漏洞称为“量子Linux内核垃圾回收”,这似乎是这篇博客文章的合适标题。

引言

CVE-2021-0920是一个由于SCM_RIGHTS垃圾回收系统中的竞争条件导致的释放后使用(UAF)。SCM_RIGHTS是一种控制消息,允许Unix域套接字将打开的文件描述符从一个进程传输到另一个进程。换句话说,发送方传输一个文件描述符,接收方然后从发送方获取一个文件描述符。这种文件描述符的传递增加了文件结构引用计数的复杂性。为了解决这个问题,Linux内核社区设计了一个特殊的垃圾回收系统。CVE-2021-0920就是这个垃圾回收系统中的一个漏洞。通过在垃圾回收过程中赢得竞争条件,攻击者可以利用套接字缓冲区sk_buff对象上的UAF。在接下来的部分中,我们将解释SCM_RIGHTS垃圾回收系统以及漏洞的细节。分析基于Linux 4.14内核。

什么是SCM_RIGHTS?

Linux开发人员可以使用SCM_RIGHTS数据报和sendmsg系统调用将文件描述符(fd)从一个进程共享到另一个进程。当一个进程将文件描述符传递给另一个进程时,SCM_RIGHTS将增加对底层文件结构的引用。这意味着发送文件描述符的进程可以立即关闭其端的文件描述符,即使接收进程尚未接受并取得文件描述符的所有权。当文件描述符处于“排队”状态时(意味着发送方已传递fd然后关闭它,但接收方尚未接受fd并取得所有权),需要专门的垃圾回收。为了跟踪这种“排队”状态,这篇LWN文章很好地解释了SCM_RIGHTS引用计数,建议在继续阅读本博客文章之前先阅读它。

发送

如前所述,Unix域套接字使用系统调用sendmsg将文件描述符发送到另一个套接字。为了解释SCM_RIGHTS期间的引用计数,我们将从发送方的角度开始。我们从内核函数unix_stream_sendmsg开始,它实现了sendmsg系统调用。为了实现SCM_RIGHTS功能,内核使用结构scm_fp_list来管理所有传输的文件结构。scm_fp_list存储要传递的文件指针列表。

struct scm_fp_list {
short count;
short max;
struct user_struct *user;
struct file *fp[SCM_MAX_FD];
};

unix_stream_sendmsg调用scm_send(af_unix.c#L1886)来初始化scm_fp_list结构,该结构通过堆栈上的scm_cookie结构链接。

struct scm_cookie {
struct pid pid; / Skb credentials */
struct scm_fp_list fp; / Passed files /
struct scm_creds creds; /
Skb credentials /
#ifdef CONFIG_SECURITY_NETWORK
u32 secid; /
Passed security ID */
#endif
};

更具体地说,scm_send → __scm_send → scm_fp_copy(scm.c#L68)从用户空间读取文件描述符并初始化scm_cookie->fp,它可以包含SCM_MAX_FD个文件结构。

由于Linux内核使用sk_buff(也称为套接字缓冲区或skb)对象来管理所有类型的套接字数据报,内核还需要调用unix_scm_to_skb函数将scm_cookie->fp链接到相应的skb对象。这发生在unix_attach_fds(scm.c#L103)中:

…
/*

  • Need to duplicate file references for the sake of garbage
  • collection. Otherwise a socket in the fps might become a
  • candidate for GC while the skb is not yet queued.
    */
    UNIXCB(skb).fp = scm_fp_dup(scm->fp);
    if (!UNIXCB(skb).fp)
    return -ENOMEM;
    …

unix_attach_fds中的scm_fp_dup调用增加正在传递的文件描述符的引用计数,因此即使发送方稍后关闭传输的文件描述符,文件仍然有效:

struct scm_fp_list *scm_fp_dup(struct scm_fp_list *fpl)
{
struct scm_fp_list *new_fpl;
int i;

if (!fpl)
    return NULL;

new_fpl = kmemdup(fpl, offsetof(struct scm_fp_list, fp[fpl->count]),
                  GFP_KERNEL);
if (new_fpl) {
    for (i = 0; i < fpl->count; i++)
        get_file(fpl->fp[i]);
    new_fpl->max = new_fpl->count;
    new_fpl->user = get_uid(fpl->user);
}
return new_fpl;

}

让我们检查一个具体示例。假设我们有套接字A和B。A尝试将自己传递给B。发送SCM_RIGHTS数据报后,来自发送方的新分配skb将被附加到B的sk_receive_queue,该队列存储接收到的数据报:

unix_stream_sendmsg创建包含结构scm_fp_list的sk_buff。scm_fp_list有一个fp指针指向传输的文件(A)。sk_buff被附加到接收队列,A的引用计数为2。

sk_buff携带scm_fp_list结构

A的引用计数增加到2,B的引用计数仍为1。

接收

现在,让我们看看接收方unix_stream_read_generic(我们暂时不讨论MSG_PEEK标志,专注于正常例程)。首先,内核使用skb_peek从sk_receive_queue获取当前skb。其次,由于scm_fp_list附加到skb,内核将调用unix_detach_fds(链接)从skb解析传输的文件结构,并从sk_receive_queue清除skb:

/* Mark read part of skb as used */
if (!(flags & MSG_PEEK)) {
UNIXCB(skb).consumed += chunk;
sk_peek_offset_bwd(sk, chunk);
if (UNIXCB(skb).fp)
unix_detach_fds(&scm, skb);
if (unix_skb_len(skb))
break;
skb_unlink(skb, &sk->sk_receive_queue);
consume_skb(skb);
if (scm.fp)
break;
}

函数scm_detach_fds遍历传递的文件描述符列表(scm->fp)并为接收方相应地安装新的文件描述符:

for (i=0, cmfptr=(__force int __user )CMSG_DATA(cm); i<fdmax;
i++, cmfptr++)
{
struct socket *sock;
int new_fd;
err = security_file_receive(fp[i]);
if (err)
break;
err = get_unused_fd_flags(MSG_CMSG_CLOEXEC & msg->msg_flags
? O_CLOEXEC : 0);
if (err < 0)
break;
new_fd = err;
err = put_user(new_fd, cmfptr);
if (err) {
put_unused_fd(new_fd);
break;
}
/
Bump the usage count and install the file. /
sock = sock_from_file(fp[i], &err);
if (sock) {
sock_update_netprioidx(&sock->sk->sk_cgrp_data);
sock_update_classid(&sock->sk->sk_cgrp_data);
}
fd_install(new_fd, get_file(fp[i]));
}
…
/

  • All of the files that fit in the message have had their
  • usage counts incremented, so we just free the list.
    */
    __scm_destroy(scm);

一旦文件描述符被安装,__scm_destroy(链接)清理分配的scm->fp并递减每个传输的文件结构的文件引用计数:

void __scm_destroy(struct scm_cookie *scm)
{
struct scm_fp_list *fpl = scm->fp;
int i;

if (fpl) {
    scm->fp = NULL;
    for (i=fpl->count-1; i>=0; i--)
        fput(fpl->fp[i]);
    free_uid(fpl->user);
    kfree(fpl);
}

}

引用计数和Inflight计数

如上所述,当使用SCM_RIGHTS传递文件描述符时,其引用计数立即增加。一旦接收方套接字接受并安装了传递的文件描述符,引用计数就会减少。复杂性来自这个操作的“中间”状态:在文件描述符被发送之后,但在接收方接受并安装文件描述符之前。

让我们考虑以下场景:

  1. 进程创建套接字A和B。
  2. A将套接字A发送给套接字B。
  3. B将套接字B发送给套接字A。
  4. 关闭A。
  5. 关闭B。

套接字A和B形成引用计数循环。

引用计数循环的场景

两个套接字在接受传递的文件描述符之前都被关闭。A和B的引用计数都是1,并且无法进一步递减,因为它们在各自进程关闭时从内核fd表中移除。因此内核无法释放两个skb和sock结构,形成了一个不可打破的循环。Linux内核垃圾回收系统旨在防止这种特定场景下的内存耗尽。实现了inflight计数来识别潜在的垃圾。每次由于发送SCM_RIGHTS数据报而增加引用计数时,inflight计数也会增加。

当文件描述符通过SCM_RIGHTS数据报发送时,Linux内核将其unix_sock放入全局列表gc_inflight_list。内核增加unix_tot_inflight,它计算inflight套接字的总数。然后,内核增加u->inflight,它跟踪每个文件描述符在unix_inflight函数(scm.c#L45)中的inflight计数,该函数从unix_attach_fds调用:

void unix_inflight(struct user_struct *user, struct file *fp)
{
struct sock *s = unix_get_socket(fp);
spin_lock(&unix_gc_lock);
if (s) {
struct unix_sock *u = unix_sk(s);
if (atomic_long_inc_return(&u->inflight) == 1) {
BUG_ON(!list_empty(&u->link));
list_add_tail(&u->link, &gc_inflight_list);
} else {
BUG_ON(list_empty(&u->link));
}
unix_tot_inflight++;
}
user->unix_inflight++;
spin_unlock(&unix_gc_lock);
}

因此,当在套接字A和B之间传输文件描述符时,sk_buff看起来像这样:

当文件描述符A将自己发送给文件描述符B时,文件描述符A的引用计数为2,inflight计数为1。对于接收方文件描述符B,文件引用计数为1,inflight计数为0。

A的inflight计数增加

当从另一侧接收套接字文件描述符时,unix_sock.inflight计数将减少。

让我们在close系统调用之前重新审视引用计数循环场景。这个循环是可打破的,因为任何套接字文件都可以接收传输的文件并打破引用循环:

文件描述符A将自己发送给文件描述符B,反之亦然。文件描述符A和B的inflight计数都是1,文件引用计数都是2。

关闭A和B之前的可打破循环

关闭两个文件描述符后,每个套接字文件描述符的引用计数等于其inflight计数,这是可能垃圾的标志:

关闭A和B后,循环变得不可打破。A和B的引用计数等于inflight计数。

关闭A和B后的不可打破循环

现在,让我们检查另一个示例。假设我们有套接字A、B和𝛼:

  1. A将套接字A发送给套接字B。
  2. B将套接字B发送给套接字A。
  3. B将套接字B发送给套接字𝛼。
  4. 𝛼将套接字𝛼发送给套接字B。
  5. 关闭A。
  6. 关闭B。

A、B和alpha形成可打破循环。

A、B和𝛼的可打破循环

这个循环是可打破的,因为我们可以从套接字文件描述符𝛼获取新安装的文件描述符B',并从B'获取新安装的文件描述符A'。

垃圾回收

lwn.net提供了垃圾回收的高级视图:

“如果两个计数相等,那么该文件结构可能是不可达循环的一部分。为了确定是否是这种情况,内核找到所有引用都包含在SCM_RIGHTS数据报中的飞行中Unix域套接字集合(换句话说,f_count和inflight相等)。然后计算这些套接字中有多少引用来自附加到此集合中套接字的SCM_RIGHTS数据报。任何有来自集合外部引用的套接字都是可达的,可以从集合中移除。如果它是可达的,并且有任何等待被使用的SCM_RIGHTS数据报附加到它,那么该数据报中包含的文件也是可达的,可以从集合中移除。

在迭代过程结束时,内核可能会发现自己有一个飞行中Unix域套接字集合,这些套接字仅由未使用(且不可使用)的SCM_RIGHTS数据报引用;此时,它有一个文件结构循环,这些结构持有彼此的唯一引用。从队列中移除这些数据报,释放它们持有的引用,并丢弃它们将打破循环。”

更具体地说,SCM_RIGHTS垃圾回收系统是为了处理不可打破的引用循环而开发的。为了识别哪些文件描述符是不可打破循环的一部分:

  1. 将引用计数等于其inflight计数的任何unix_sock对象添加到gc_candidates列表。
  2. 确定套接字是否被gc_candidates列表之外的任何套接字引用。如果是,那么它是可达的,将其及其引用的任何套接字从gc_candidates列表中移除。重复直到找不到更多可达套接字。
  3. 在此迭代过程之后,仅剩下那些仅被gc_candidates列表中的其他套接字引用的套接字。

让我们更仔细地看看这个垃圾回收过程是如何工作的。首先,内核找到所有引用计数等于其inflight计数的unix_sock对象,并将它们放入gc_candidates列表(garbage.c#L242):

list_for_each_entry_safe(u, next, &gc_inflight_list, link) {
long total_refs;
long inflight_refs;
total_refs = file_count(u->sk.sk_socket->file);
inflight_refs = atomic_long_read(&u->inflight);
BUG_ON(inflight_refs < 1);
BUG_ON(total_refs < inflight_refs);
if (total_refs == inflight_refs) {
list_move_tail(&u->link, &gc_candidates);
__set_bit(UNIX_GC_CANDIDATE, &u->gc_flags);
__set_bit(UNIX_GC_MAYBE_CYCLE, &u->gc_flags);
}
}

接下来,内核移除任何被当前gc_candidates列表之外的套接字引用的套接字。为此,内核调用scan_children(garbage.c#138)以及函数指针dec_inflight来遍历每个候选的sk->receive_queue。它减少每个传递的文件描述符的inflight计数,这些文件描述符本身也是垃圾回收的候选(garbage.c#L261):

/* Now remove all internal in-flight reference to children of

  • the candidates.
    */
    list_for_each_entry(u, &gc_candidates, link)
    scan_children(&u->sk, dec_inflight, NULL);

遍历所有候选后,如果一个gc候选仍然有正的inflight计数,这意味着它被gc_candidates列表之外的对象引用,因此是可达的。这些候选不应包含在gc_candidates列表中,因此需要恢复相关的inflight计数。

为此,内核将候选放入not_cycle_list,并遍历gc_candidates列表中每个传输文件的接收队列(garbage.c#L281)并将inflight计数递增回来。整个过程递归完成,以便垃圾回收避免清除可达套接字:

/* Restore the references for children of all candidates,

  • which have remaining references. Do this recursively, so
  • only those remain, which form cyclic references.
  • Use a "cursor" link, to make the list traversal safe, even
  • though elements might be moved about.
    /
    list_add(&cursor, &gc_candidates);
    while (cursor.next != &gc_candidates) {
    u = list_entry(cursor.next, struct unix_sock, link);
    /
    Move cursor to after the current position. */
    list_move(&cursor, &u->link);
    if (atomic_long_read(&u->inflight) > 0) {
    list_move_tail(&u->link, &not_cycle_list);
    __clear_bit(UNIX_GC_MAYBE_CYCLE, &u->gc_flags);
    scan_children(&u->sk, inc_inflight_move_tail, NULL);
    }
    }
    list_del(&cursor);

现在gc_candidates仅包含“垃圾”。内核从gc_candidates恢复原始inflight计数,将候选从not_cycle_list移回gc_inflight_list,并调用__skb_queue_purge清理垃圾(garbage.c#L306)。

/* Now gc_candidates contains only garbage. Restore original

  • inflight counters for these as well, and remove the skbuffs
  • which are creating the cycle(s).
    /
    skb_queue_head_init(&hitlist);
    list_for_each_entry(u, &gc_candidates, link)
    scan_children(&u->sk, inc_inflight, &hitlist);
    /
    not_cycle_list contains those sockets which do not make up a
  • cycle. Restore these to the inflight list.
    /
    while (!list_empty(&not_cycle_list)) {
    u = list_entry(not_cycle_list.next, struct unix_sock, link);
    __clear_bit(UNIX_GC_CANDIDATE, &u->gc_flags);
    list_move_tail(&u->link, &gc_inflight_list);
    }
    spin_unlock(&unix_gc_lock);
    /
    Here we are. Hitlist is filled. Die. */
    __skb_queue_purge(&hitlist);
    spin_lock(&unix_gc_lock);

__skb_queue_purge清除接收队列中的每个skb:

/**

  • __skb_queue_purge - empty a list
  • @list: list to empty
  • Delete all buffers on an &sk_buff list. Each buffer is removed from
  • the list and one reference dropped. This function does not take the
  • list lock and the caller must hold the relevant locks to use it.
    */
    void skb_queue_purge(struct sk_buff_head *list);
    static inline void __skb_queue_purge(struct sk_buff_head *list)
    {
    struct sk_buff *skb;
    while ((skb = __skb_dequeue(list)) != NULL)
    kfree_skb(skb);

}

有两种方法可以触发垃圾回收过程:

  1. 如果飞行中套接字超过16,000个,wait_for_unix_gc在sendmsg函数开头被调用
  2. 当套接字文件被内核释放时(即文件描述符关闭),内核将直接调用unix_gc。

注意unix_gc不是抢占式的。如果垃圾回收已经在进行中,内核不会执行另一个unix_gc调用。

现在,让我们用一对套接字f00和f01以及单个套接字𝛼检查这个示例(可打破循环):

  1. 套接字f00将套接字f00发送给套接字f01。
  2. 套接字f01将套接字f01发送给套接字𝛼。
  3. 关闭f00。
  4. 关闭f01。

在开始垃圾回收过程之前,套接字文件描述符的状态是:

  • f00:ref = 1,inflight = 1
  • f01:ref = 1,inflight = 1
  • 𝛼:ref = 1,inflight = 0

f00、f01和alpha形成可打破循环。

由f00、f01和𝛼形成的可打破循环

在垃圾回收过程中,f00和f01被视为垃圾候选。f00的inflight计数降至零,但f01的计数仍为1,因为𝛼不是候选。因此,内核将从f01的接收队列恢复inflight计数。结果,f00和f01不再被视为垃圾。

CVE-2021-0920根本原因分析

当用户从recvmsg接收SCM_RIGHTS消息而没有MSG_PEEK标志时,如果垃圾回收过程正在进行,内核将等待其完成。但是,如果设置了MSG_PEEK标志,内核将增加传输文件结构的引用计数,而不与任何正在进行的垃圾回收过程同步。这可能导致内部垃圾回收状态不一致,使垃圾回收器将非垃圾sock对象标记为垃圾进行清除。

没有MSG_PEEK标志的recvmsg

内核函数unix_stream_read_generic(af_unix.c#L2290)解析SCM_RIGHTS消息并在未设置MSG_PEEK标志时管理文件inflight计数。然后,函数unix_stream_read_generic调用unix_detach_fds来减少inflight计数。然后,unix_detach_fds从skb清除传递的文件描述符列表(scm_fp_list):

static void unix_detach_fds(struct scm_cookie *scm, struct sk_buff *skb)
{
int i;
scm->fp = UNIXCB(skb).fp;
UNIXCB(skb).fp = NULL;
for (i = scm->fp->count-1; i >= 0; i--)
unix_notinflight(scm->fp->user, scm->fp->fp[i]);
}

unix_detach_fds中的unix_notinflight将通过减少inflight计数来反转unix_inflight的效果:

void unix_notinflight(struct user_struct *user, struct file *fp)
{
struct sock *s = unix_get_socket(fp);
spin_lock(&unix_gc_lock);
if (s) {
struct unix_sock *u = unix_sk(s);
BUG_ON(!atomic_long_read(&u->inflight));
BUG_ON(list_empty(&u->link));
if (atomic_long_dec_and_test(&u->inflight))
list_del_init(&u->link);
unix_tot_inflight--;
}
user->unix_inflight--;
spin_unlock(&unix_gc_lock);
}

稍后,skb_unlink和consume_skb从unix_stream_read_generic(af_unix.c#2451)调用以销毁当前skb。沿着调用链kfree(skb)->__kfree_skb,内核将调用函数指针skb->destructor(代码),它重定向到unix_destruct_scm:

static void unix_destruct_scm(struct sk_buff skb)
{
struct scm_cookie scm;
memset(&scm, 0, sizeof(scm));
scm.pid = UNIXCB(skb).pid;
if (UNIXCB(skb).fp)
unix_detach_fds(&scm, skb);
/
Alas, it calls VFS /
/
So fscking what? fput() had been SMP-safe since the last Summer */
scm_destroy(&scm);
sock_wfree(skb);
}

实际上,unix_detach_fds不会在这里从unix_destruct_scm再次调用,因为UNIXCB(skb).fp已经被unix_detach_fds清除。最后,调用scm_detach_fds中的fd_install(new_fd, get_file(fp[i]))来安装新的文件描述符。

带有MSG_PEEK标志的recvmsg

如果设置了MSG_PEEK标志,recvmsg过程则不同。MSG_PEEK标志在接收期间用于“窥视”消息,但数据被视为未读。unix_stream_read_generic将调用scm_fp_dup而不是unix_detach_fds。这增加了飞行中文件的引用计数(af_unix.c#2149):

/* It is questionable, see note in unix_dgram_recvmsg.
*/
if (UNIXCB(skb).fp)
scm.fp = scm_fp_dup(UNIXCB(skb).fp);
sk_peek_offset_fwd(sk, chunk);
if (UNIXCB(skb).fp)
break;

因为数据应被视为未读,所以当设置MSG_PEEK标志时,skb不会被取消链接和消耗。但是,接收方仍然会为飞行中套接字获取一个新的文件描述符。

recvmsg示例

让我们看一个具体示例。假设有以下套接字对:

  • f00,f01
  • f10,f11

现在,程序执行以下操作:

  • f00 → [f00] → f01(意味着f00发送[f00]给f01)
  • f10 → [f00] → f11
  • 关闭(f00)

f00、f01、f10、f11形成可打破循环。

由f00、f01、f10和f11形成的可打破循环

状态如下:

  • inflight(f00) = 2,ref(f00) = 2
  • inflight(f01) = 0,ref(f01) = 1
  • inflight(f10) = 0,ref(f10) = 1
  • inflight(f11) = 0,ref(f11) = 1

如果现在发生垃圾回收过程,在任何recvmsg调用之前,内核将选择f00作为垃圾候选。但是,f00的inflight计数不会改变,内核不会清除任何垃圾。

如果f01然后调用带有MSG_PEEK标志的recvmsg,接收队列不会改变,inflight计数不会减少。f01获取一个新的文件描述符f00',它增加了f00的引用计数:

在f01通过MSG_PEEK接收套接字文件描述符后,f00的引用计数增加,f01的接收队列保持不变。

MSG_PEEK增加f00的引用计数,而接收队列未被清除

状态:

  • inflight(f00) = 2,ref(f00) = 3
  • inflight(f01) = 0,ref(f01) = 1
  • inflight(f10) = 0,ref(f10) = 1
  • inflight(f11) = 0,ref(f11) = 1

然后,f01调用没有MSG_PEEK标志的recvmsg,f01的接收队列被移除。f01还获取一个新的文件描述符f00'':

在f01没有MSG_PEEK接收套接字文件描述符后,接收队列被清除,文件描述符f00'''被获取。

f01的接收队列被清除,f01''从f01获取

状态:

  • inflight(f00) = 1,ref(f00) = 3
  • inflight(f01) = 0,ref(f01) = 1
  • inflight(f10) = 0,ref(f10) = 1
  • inflight(f11) = 0,ref(f11) = 1

UAF场景

从非常高的层面来看,Linux垃圾回收的内部状态可能是非确定性的,因为MSG_PEEK不与垃圾回收器同步。存在一个竞争条件,垃圾回收器可以将飞行中套接字视为垃圾候选,同时在MSG_PEEK接收期间文件引用增加。因此,垃圾回收器可能清除候选,释放套接字缓冲区,而接收方可能安装文件描述符,导致skb对象上的UAF。

让我们看看捕获的0day样本如何逐步触发该漏洞(简化版本,实际上可能需要更多线程协同工作,但它应该展示核心思想)。首先,样本分配以下套接字对和单个套接字𝛼:

  • f00,f01
  • f10,f11
  • f20,f21
  • f30,f31
  • 套接字𝛼(实际上可能有数千个𝛼来延长垃圾回收过程,以避免稍后将介绍的BUG_ON检查)。

现在,程序执行以下操作:

在任何recvmsg调用之前关闭以下文件描述符:

  • 关闭(f00)
  • 关闭(f01)
  • 关闭(f11)
  • 关闭(f10)
  • 关闭(f30)
  • 关闭(f31)
  • 关闭(𝛼)

状态如下:

  • inflight(f00) = N + 1,ref(f00) = N + 1
  • inflight(f01) = 2,ref(f01) = 2
  • inflight(f10) = 3,ref(f10) = 3
  • inflight(f11) = 1,ref(f11) = 1
  • inflight(f20) = 0,ref(f20) = 1
  • inflight(f21) = 0,ref(f21) = 1
  • inflight(f31) = 1,ref(f31) = 1
  • inflight(𝛼) = 1,ref(𝛼) = 1

如果现在发生垃圾回收过程,内核将执行以下检查:

  • 将f00、f01、f10、f11、f31、𝛼列为垃圾候选。减少每个接收队列中候选子项的inflight计数。
  • 由于f21不被视为候选,f11的inflight计数仍高于零。
  • 递归恢复inflight计数。
  • 没有被视为垃圾。

可以通过以下方式触发潜在的skb UAF竞争条件:

  1. 从f21调用带有MSG_PEEK标志的recvmsg以获取f11'。

  2. 从f11调用带有MSG_PEEK标志的recvmsg以获取f10'。

  3. 并发执行以下操作:

  4. 从f11调用没有MSG_PEEK标志的recvmsg以获取f10''。

  5. 从f10'调用带有MSG_PEEK标志的recvmsg。

这怎么可能?让我们看一个没有触发竞争条件因此没有UAF的情况:

线程0

线程1

线程2

调用unix_gc

阶段0:将f00、f01、f10、f11、f31、𝛼列为垃圾候选。

从f21调用带有MSG_PEEK标志的recvmsg以获取f11'

增加引用计数:scm.fp = scm_fp_dup(UNIXCB(skb).fp);

阶段0:减少每个垃圾候选子项的inflight计数

阶段0后的状态:

inflight(f00) = 0

inflight(f01) = 0

inflight(f10) = 0

inflight(f11) = 1

inflight(f31) = 0

inflight(𝛼) = 0

阶段1:如果候选仍有inflight计数,则递归恢复inflight计数。

阶段1:所有inflight计数已恢复。

阶段2:没有垃圾,返回。

从f11调用带有MSG_PEEK标志的recvmsg以获取f10'

从f11调用没有MSG_PEEK标志的recvmsg以获取f10''

从f10'调用带有MSG_PEEK标志的recvmsg

皆大欢喜

皆大欢喜

皆大欢喜

但是,如果第二个recvmsg恰好在垃圾回收过程的阶段1之后发生,则触发UAF:

线程0

线程1

线程2

调用unix_gc

阶段0:将f00、f01、f10、f11、f31、𝛼列为垃圾候选。

从f21调用带有MSG_PEEK标志的recvmsg以获取f11'

增加引用计数:scm.fp = scm_fp_dup(UNIXCB(skb).fp);

阶段0:减少每个垃圾候选子项的inflight计数

阶段0后的状态:

inflight(f00) = 0

inflight(f01) = 0

inflight(f10) = 0

inflight(f11) = 1

inflight(f31) = 0

inflight(𝛼) = 0

阶段1:开始恢复inflight计数。

从f11调用带有MSG_PEEK标志的recvmsg以获取f10'

从f11调用没有MSG_PEEK标志的recvmsg以获取f10''

unix_detach_fds:UNIXCB(skb).fp = NULL

被spin_lock(&unix_gc_lock)阻塞

阶段1:scan_inflight无法从f11找到候选子项。因此,inflight计数意外保持不变。

阶段2:f00、f01、f10、f31、𝛼是垃圾。

阶段2:开始清除垃圾。

开始从f10'调用带有MSG_PEEK标志的recvmsg,期望接收f00'

获取skb = skb_peek(&sk->sk_receive_queue),skb将被线程0释放。

阶段2:对于,稍后调用__skb_unlink和kfree_skb。

state->recv_actor(skb, skip, chunk, state) UAF

GC完成。

开始垃圾回收。

获取f10''

因此,竞争条件导致skb对象的UAF。乍一看,我们应该责怪第二个recvmsg系统调用,因为它清除了skb.fp,即传递的文件列表。但是,如果第一个recvmsg