To access this blog post in English, click here.
Pesquisa em vulnerabilidades está no coração da Allele Security Intelligence. Pesquisamos ativamente há mais de uma década e oferecemos nossa experiência aos nossos clientes. Entre os serviços que oferecemos estão pesquisa de vulnerabilidades em 0day e nday.
Nos projetos de pesquisa em vulnerabilidades em nday, no caso do kernel do Linux, procuramos por vulnerabilidades corrigidas em upstream que ainda afetam as principais distribuições em suas versões mais recentes. Normalmente encontramos vulnerabilidades corrigidas há mais de um ano que ainda afetam distribuições Linux populares. Fazemos isso através da auditoria de código-fonte, monitorando vulnerabilidades em listas de discussão e corrigidas em upstream, olhando os resultados do fuzzer syzkaller e de outras formas.
Enquanto fazíamos isso, nós acidentalmente descobrimos uma vulnerabilidade no núcleo do subsistema TCP do kernel do Linux. Ela tinha sido introduzida há sete anos. Nós reportamos a vulnerabilidade e ela foi corrigida em maio do ano passado. Nesta postagem, compartilharemos como isso aconteceu e uma análise breve da vulnerabilidade.
RACE CONDITION NO PROTOCOLO DE REDE KCM NA FUNÇÃO SK_RECEIVE_QUEUE()
A seguinte vulnerabilidade chamou nossa atenção ao olhar o dashboard do fuzzer syzkaller. O commit que corrige a vulnerabilidade também é exibido abaixo.
general protection fault in skb_unlink
https://syzkaller.appspot.com/bug?extid=278279efdd2730dd14bf
kcm: close race conditions on sk_receive_queue
https://github.com/torvalds/linux/commit/5121197ecc5db58c07da95eb1ff82b98b121a221
Nós olhamos o código-fonte da última versão de um dos derivados do Red Hat Enterprise Linux e ele ainda estava vulnerável. Os kernels do derivados do Red hat Enterprise Linux são similares ao Red Hat Enterprise Linux. Normalmente, uma vulnerabilidade em um deles acaba também afetando todos os outros. Houveram mudanças no ecossistema do Red Hat Enterprise Linux recentemente e nós não checamos desde então, mas isso tem sido verdade há bastante tempo. Felizmente, o bug tinha um código reprodutor disponível e nós o deixamos rodando em nossos sistemas. Depois de um tempo, o seguinte relatório de erro foi gerado. O relatório de erro completo pode ser encontrado aqui.
[111607.195289] ------------[ cut here ]------------
[111607.195305] refcount_t: addition on 0; use-after-free.
...
[111607.197802] CPU: 2 PID: 3130808 Comm: poc6 Kdump: loaded Not tainted 5.14.0-362.24.2.el9_3.x86_64 #1
...
[111607.201030] Call Trace:
[111607.201182]
[111607.201350] ? show_trace_log_lvl+0x1c4/0x2df
[111607.201623] ? show_trace_log_lvl+0x1c4/0x2df
[111607.201800] ? tcp_twsk_unique+0x183/0x190
[111607.201993] ? refcount_warn_saturate+0x74/0x110
[111607.202164] ? __warn+0x81/0x110
[111607.202442] ? refcount_warn_saturate+0x74/0x110
[111607.202692] ? report_bug+0x10a/0x140
[111607.202931] ? handle_bug+0x3c/0x70
[111607.203212] ? exc_invalid_op+0x14/0x70
[111607.203425] ? asm_exc_invalid_op+0x16/0x20
[111607.203679] ? refcount_warn_saturate+0x74/0x110
[111607.203924] tcp_twsk_unique+0x183/0x190
[111607.204091] __inet_check_established+0x158/0x2c0
[111607.204335] __inet_hash_connect+0xb7/0x540
[111607.204590] ? __pfx___inet_check_established+0x10/0x10
[111607.206806] tcp_v4_connect+0x24e/0x520
[111607.207707] __inet_stream_connect+0xcb/0x3b0
[111607.208583] ? release_sock+0x40/0x90
[111607.209469] ? selinux_netlbl_socket_connect+0x2b/0x40
[111607.210342] inet_stream_connect+0x37/0x60
[111607.211177] __sys_connect+0xa3/0xd0
[111607.211985] __x64_sys_connect+0x14/0x20
...
[111607.223742] ------------[ cut here ]------------
[111607.223743] refcount_t: underflow; use-after-free.
...
[111607.228620] CPU: 2 PID: 3130808 Comm: poc6 Kdump: loaded Tainted: G W ------- --- 5.14.0-362.24.2.el9_3.x86_64 #1
...
[111607.239689] Call Trace:
[111607.240472]
[111607.241313] ? show_trace_log_lvl+0x1c4/0x2df
[111607.242156] ? show_trace_log_lvl+0x1c4/0x2df
[111607.242989] ? __inet_check_established+0x23a/0x2c0
[111607.243838] ? refcount_warn_saturate+0xba/0x110
[111607.244669] ? __warn+0x81/0x110
[111607.245484] ? refcount_warn_saturate+0xba/0x110
[111607.246331] ? report_bug+0x10a/0x140
[111607.247155] ? handle_bug+0x3c/0x70
[111607.248005] ? exc_invalid_op+0x14/0x70
[111607.248834] ? asm_exc_invalid_op+0x16/0x20
[111607.249629] ? refcount_warn_saturate+0xba/0x110
[111607.250435] __inet_check_established+0x23a/0x2c0
[111607.251279] __inet_hash_connect+0xb7/0x540
[111607.252135] ? __pfx___inet_check_established+0x10/0x10
[111607.252962] tcp_v4_connect+0x24e/0x520
[111607.253786] __inet_stream_connect+0xcb/0x3b0
[111607.254601] ? release_sock+0x40/0x90
[111607.255361] ? selinux_netlbl_socket_connect+0x2b/0x40
[111607.256105] inet_stream_connect+0x37/0x60
[111607.256913] __sys_connect+0xa3/0xd0
[111607.257709] __x64_sys_connect+0x14/0x20
...
[116082.336931] ------------[ cut here ]------------
[116082.336934] refcount_t: decrement hit 0; leaking memory.
...
[116082.342044] CPU: 1 PID: 3866568 Comm: poc5 Kdump: loaded Tainted: G W ------- --- 5.14.0-362.24.2.el9_3.x86_64 #1
...
[116082.352815] Call Trace:
[116082.353638]
[116082.354410] ? show_trace_log_lvl+0x1c4/0x2df
[116082.355236] ? show_trace_log_lvl+0x1c4/0x2df
[116082.356055] ? __inet_check_established+0x29c/0x2c0
[116082.356874] ? refcount_warn_saturate+0xfb/0x110
[116082.357692] ? __warn+0x81/0x110
[116082.358505] ? refcount_warn_saturate+0xfb/0x110
[116082.359302] ? report_bug+0x10a/0x140
[116082.360124] ? handle_bug+0x3c/0x70
[116082.360905] ? exc_invalid_op+0x14/0x70
[116082.361697] ? asm_exc_invalid_op+0x16/0x20
[116082.362491] ? refcount_warn_saturate+0xfb/0x110
[116082.363300] __inet_check_established+0x29c/0x2c0
[116082.364098] __inet_hash_connect+0xb7/0x540
[116082.364893] ? __pfx___inet_check_established+0x10/0x10
[116082.365689] tcp_v4_connect+0x24e/0x520
[116082.366454] ? pgtable_trans_huge_deposit+0x88/0x110
[116082.367247] __inet_stream_connect+0xcb/0x3b0
[116082.368000] ? release_sock+0x40/0x90
[116082.368746] ? selinux_netlbl_socket_connect+0x2b/0x40
[116082.369488] inet_stream_connect+0x37/0x60
[116082.370197] __sys_connect+0xa3/0xd0
[116082.370942] __x64_sys_connect+0x14/0x20
...
[116082.381500] ---[ end trace f02e72c43eeca11a ]---Nós então começamos a simplificar o reprodutor do syzkaller para obter um código mais limpo. Nós reduzimos o código passo a passo, identificando pedaços obrigatórios e removendo os não necessários, finalmente alcançando uma versão do código simplificada. Nós já suspeitávamos ao olhar o relatório de erro obtido, mas durante o processo, notamos que nosso acionador não tinha nada a ver com o protocolo descrito no relatório de erro do syzkaller. O call trace do relatório de erro do syzkaller contém o protocolo KCM, mas em nosso acionador simplificado nós tínhamos removido todo código relacionado ao protocolo KCM e ele ainda estava acionando o erro.
Relatório de erro do syzkaller:
Call Trace:
kcm_recvmsg+0x462/0x560 net/kcm/kcmsock.c:1161
sock_recvmsg_nosec+0x89/0xb0 net/socket.c:871
___sys_recvmsg+0x271/0x5c0 net/socket.c:2480
do_recvmmsg+0x27e/0x7a0 net/socket.c:2601
__sys_recvmmsg+0x259/0x270 net/socket.c:2680
__do_sys_recvmmsg net/socket.c:2703 [inline]
__se_sys_recvmmsg net/socket.c:2696 [inline]
__x64_sys_recvmmsg+0xe6/0x140 net/socket.c:2696
do_syscall_64+0xfa/0x760 arch/x86/entry/common.c:290
entry_SYSCALL_64_after_hwframe+0x49/0xbeO relatório de erro que foi acionado ao executar o reprodutor do syzkaller:
Call Trace:
show_trace_log_lvl+0x1c4/0x2df
show_trace_log_lvl+0x1c4/0x2df
tcp_twsk_unique+0x183/0x190
refcount_warn_saturate+0x74/0x110
__warn+0x81/0x110
refcount_warn_saturate+0x74/0x110
report_bug+0x10a/0x140
handle_bug+0x3c/0x70
exc_invalid_op+0x14/0x70
asm_exc_invalid_op+0x16/0x20
refcount_warn_saturate+0x74/0x110
tcp_twsk_unique+0x183/0x190
__inet_check_established+0x158/0x2c0
__inet_hash_connect+0xb7/0x540
__pfx___inet_check_established+0x10/0x10
tcp_v4_connect+0x24e/0x520
__inet_stream_connect+0xcb/0x3b0
release_sock+0x40/0x90
selinux_netlbl_socket_connect+0x2b/0x40
inet_stream_connect+0x37/0x60
__sys_connect+0xa3/0xd0
__x64_sys_connect+0x14/0x20
do_syscall_64+0x59/0x90
handle_mm_fault+0xc5/0x2a0
do_user_addr_fault+0x1d6/0x6a0
exc_page_fault+0x62/0x150
entry_SYSCALL_64_after_hwframe+0x72/0xdc Como mostrado acima, o bug que nós acionamos não tem nada a ver com o protocolo KCM. O relatório de erro do syzkaller foi gerado pelo KASAN e em nosso caso, foi um aviso devido ao mecanismo de proteção implementado na API de contador de referências do kernel do Linux.
Nós confirmamos que o bug que nós acionamos era diferente ao checar que o módulo do protocolo KCM não estava nem disponível no sistema. Para reduzir o tempo ao analisar os problemas, nós executamos as provas de conceitos ou reprodutores em diversas distribuições simultaneamente antes de checar o sistema, e se eles acionam algum problema, nós começamos a analisá-los.
$ cat /boot/config-5.14.0-362.24.2.el9_3.x86_64 | grep CONFIG_AF_KCM
# CONFIG_AF_KCM is not set
$USE-AFTER_FREE no SUBSISTEMA TCP DO KERNEL DO LINUX DEVIDO A RACE CONDITION ENTRE TCP_TWSK_UNIQUE E INET_TWSK_HASHDANCE() – CVE-2024-36904
Nós confirmamos que era um bug diferente e analisamos ele para entendê-lo melhor. O problema acontece porque o objeto, um socket TCP time-wait, tem seu contador de referências inicializado após ele ser inserido em uma tabela hash e o lock ser liberado. Então, se uma consulta a tabela hash é realizado antes da inicialização do contador de referências, o objeto é encontrado com o contador de referências zerado e o aviso é acionado. O commit que adicionou o problema é o seguinte:
tcp/dccp: avoid one atomic operation for timewait hashdance
https://github.com/torvalds/linux/commit/ec94c2696f0bcd5ae92a553244e4ac30d2171a2d#diff-901c0d3a066cb54add11c34dabb345263e8fc6e7fbfd702843be6b7435345b59R139
Entendendo o problema, nós confirmamos que upstream também deveria ser afetado baseado no código-fonte. Nós então testamos no Fedora 39 com o kernel 6.8.6-200.fc39.x86_64. O kernel 6.8 era um kernel recente no tempo. O mesmo reprodutor simplificado aciona o seguinte relatório de erro. O relatório de erro completo pode ser encontrado aqui.
[433522.338983] ------------[ cut here ]------------
[433522.339033] refcount_t: addition on 0; use-after-free.
...
[433522.340141] CPU: 0 PID: 1039313 Comm: trigger Not tainted
6.8.6-200.fc39.x86_64 #1
...
[433522.340278] Call Trace:
[433522.340282] <TASK>
[433522.340307] ? refcount_warn_saturate+0xe5/0x110
[433522.340313] ? __warn+0x81/0x130
[433522.340462] ? refcount_warn_saturate+0xe5/0x110
[433522.340492] ? report_bug+0x171/0x1a0
[433522.340723] ? refcount_warn_saturate+0xe5/0x110
[433522.340731] ? handle_bug+0x3c/0x80
[433522.340781] ? exc_invalid_op+0x17/0x70
[433522.340785] ? asm_exc_invalid_op+0x1a/0x20
[433522.340838] ? refcount_warn_saturate+0xe5/0x110
[433522.340843] tcp_twsk_unique+0x186/0x190
[433522.340945] __inet_check_established+0x176/0x2d0
[433522.340974] __inet_hash_connect+0x74/0x7d0
[433522.340980] ? __pfx___inet_check_established+0x10/0x10
[433522.340983] tcp_v4_connect+0x278/0x530
[433522.340989] __inet_stream_connect+0x10f/0x3d0
[433522.341019] inet_stream_connect+0x3a/0x60
[433522.341024] __sys_connect+0xa8/0xd0
[433522.341186] __x64_sys_connect+0x18/0x20
...
[433522.341703] ---[ end trace 0000000000000000 ]---
[433522.341709] ------------[ cut here ]------------
[433522.341710] refcount_t: underflow; use-after-free.
...
[433522.341820] CPU: 0 PID: 1039313 Comm: trigger Tainted: G W
6.8.6-200.fc39.x86_64 #1
...
[433522.341887] Call Trace:
[433522.341889] <TASK>
[433522.341890] ? refcount_warn_saturate+0xbe/0x110
[433522.341894] ? __warn+0x81/0x130
[433522.341899] ? refcount_warn_saturate+0xbe/0x110
[433522.341903] ? report_bug+0x171/0x1a0
[433522.341907] ? console_unlock+0x78/0x120
[433522.341977] ? handle_bug+0x3c/0x80
[433522.341981] ? exc_invalid_op+0x17/0x70
[433522.342007] ? asm_exc_invalid_op+0x1a/0x20
[433522.342011] ? refcount_warn_saturate+0xbe/0x110
[433522.342015] __inet_check_established+0x24d/0x2d0
[433522.342019] __inet_hash_connect+0x74/0x7d0
[433522.342023] ? __pfx___inet_check_established+0x10/0x10
[433522.342026] tcp_v4_connect+0x278/0x530
[433522.342031] __inet_stream_connect+0x10f/0x3d0
[433522.342035] inet_stream_connect+0x3a/0x60
[433522.342039] __sys_connect+0xa8/0xd0
[433522.342044] __x64_sys_connect+0x18/0x20
...
[433522.342097] ---[ end trace 0000000000000000 ]---
[435060.554199] ------------[ cut here ]------------
[435060.554243] refcount_t: decrement hit 0; leaking memory.
...
[435060.554426] CPU: 2 PID: 879478 Comm: trigger Tainted: G W
6.8.6-200.fc39.x86_64 #1
...
[435060.554603] Call Trace:
[435060.554607] <TASK>
[435060.554608] ? refcount_warn_saturate+0xff/0x110
[435060.554614] ? __warn+0x81/0x130
[435060.554625] ? refcount_warn_saturate+0xff/0x110
[435060.554630] ? report_bug+0x171/0x1a0
[435060.554638] ? console_unlock+0x78/0x120
[435060.554670] ? handle_bug+0x3c/0x80
[435060.554676] ? exc_invalid_op+0x17/0x70
[435060.554682] ? asm_exc_invalid_op+0x1a/0x20
[435060.554694] ? refcount_warn_saturate+0xff/0x110
[435060.554699] __inet_check_established+0x29b/0x2d0
[435060.554707] __inet_hash_connect+0x74/0x7d0
[435060.554712] ? __pfx___inet_check_established+0x10/0x10
[435060.554716] tcp_v4_connect+0x278/0x530
[435060.554723] __inet_stream_connect+0x10f/0x3d0
[435060.554729] inet_stream_connect+0x3a/0x60
[435060.554734] __sys_connect+0xa8/0xd0
[435060.554744] __x64_sys_connect+0x18/0x20
...
[435060.555160] ---[ end trace 0000000000000000 ]---Inicialmente intencionado em reproduzir um problema conhecido, nós acabamos acidentalmente descobrindo uma vulnerabilidade de 7 anos no kernel do Linux. Agora, vamos entender a vulnerabilidade.
Nós alcançamos o código abaixo. Na linha 149, o objeto que é adicionado a tabela hash tem seu contador de referências incrementado, mas o lock já foi liberado nesse ponto. Então, operações de consulta na tabela hash podem encontrar o objeto com o contador de referências zerado.
100 void inet_twsk_hashdance(struct inet_timewait_sock *tw, struct sock *sk,
101 struct inet_hashinfo *hashinfo)
102 {
...
105 struct inet_ehash_bucket *ehead =
inet_ehash_bucket(hashinfo, sk->sk_hash);
106 spinlock_t *lock = inet_ehash_lockp(hashinfo, sk->sk_hash);
...
130 spin_lock(lock);
...
132 inet_twsk_add_node_rcu(tw, &ehead->chain);
...
138 spin_unlock(lock);
...
149 refcount_set(&tw->tw_refcnt, 3);
150 }A operação de consulta é realizada por __inet_check_established(). Ela encontra o objeto na linha 561 e então chama twsk_unique().
538 static int __inet_check_established(struct inet_timewait_death_row
*death_row,
539 struct sock *sk, __u16 lport,
540 struct inet_timewait_sock **twp)
541 {
542 struct inet_hashinfo *hinfo = death_row->hashinfo;
543 struct inet_sock *inet = inet_sk(sk);
544 __be32 daddr = inet->inet_rcv_saddr;
545 __be32 saddr = inet->inet_daddr;
546 int dif = sk->sk_bound_dev_if;
547 struct net *net = sock_net(sk);
...
550 const __portpair ports =
INET_COMBINED_PORTS(inet->inet_dport, lport);
...
549 INET_ADDR_COOKIE(acookie, saddr, daddr);
...
551 unsigned int hash = inet_ehashfn(net, daddr, lport,
552 saddr, inet->inet_dport);
553 struct inet_ehash_bucket *head = inet_ehash_bucket(hinfo, hash);
...
555 struct sock *sk2;
556 const struct hlist_nulls_node *node;
...
561 sk_nulls_for_each(sk2, node, &head->chain) {
562 if (sk2->sk_hash != hash)
563 continue;
...
565 if (likely(inet_match(net, sk2, acookie, ports, dif,
sdif))) {
566 if (sk2->sk_state == TCP_TIME_WAIT) {
567 tw = inet_twsk(sk2);
568 if (twsk_unique(sk, sk2, twp))
569 break;
570 }
571 goto not_unique;
572 }
573 }A funão twsk_unique() então chama a função dinâmica que aponta para tcp_twsk_unique() em nosso caso.
23 static inline int twsk_unique(struct sock *sk, struct sock *sktw,
void *twp)
24 {
25 if (sk->sk_prot->twsk_prot->twsk_unique != NULL)
26 return sk->sk_prot->twsk_prot->twsk_unique(sk, sktw,
twp);
27 return 0;
28 }A função tcp_twsk_unique() faz a primeira operação no objeto na linha 177. Ela então chama sock_hold() que age diretamente no objeto. A função sock_hold() obtém uma referência ao objeto.
110 int tcp_twsk_unique(struct sock *sk, struct sock *sktw, void *twp)
111 {
...
151 if (tcptw->tw_ts_recent_stamp &&
152 (!twp || (reuse && time_after32(ktime_get_seconds(),
153 tcptw->tw_ts_recent_stamp)))) {
...
177 sock_hold(sktw);
178 return 1;
179 }
180
181 return 0;
182 }A função sock_hold() deve incrementar o contador de referências do socket time-wait. Porém, como o objeto tem seu contador de referências zerado, a API de contador de referências do kernel do Linux detecta isso e aciona o aviso antes de agir no objeto.
Explorabilidade
O kernel do Linux contém proteções para detectar problemas com os contadores de referências. Essa proteção aciona relatórios de erros e eles não representam o resultado de acesso inválido a memória. Embora esses erros podem mencionar use-after-free, eles não são use-after-free reais baseado apenas nos avisos. O objeto pode não ser liberado no tempo do acesso. Mas, neste caso, um interessante e use-after-free real pode acontecer mesmo com as proteções de contador de referências em uso.
Para isso acontecer, as operações no socket precisam ocorrer em uma ordem precisa. Quando a API de contador de referências detecta o problema, como o aviso acionado por sock_hold(), ele imprime o aviso e marca o contador de referências com um valor hardcoded. O valor é 0xc0000000. Nós mostramos a execução simplificada abaixo:
774 static __always_inline void sock_hold(struct sock *sk)
775 {
776 refcount_inc(&sk->sk_refcnt);
777 }191 static inline void __refcount_add(int i, refcount_t *r, int *oldp)
192 {
193 int old = atomic_fetch_add_relaxed(i, &r->refs);
194
195 if (oldp)
196 *oldp = old;
197
198 if (unlikely(!old))
199 refcount_warn_saturate(r, REFCOUNT_ADD_UAF);
200 else if (unlikely(old < 0 || old + i < 0))
201 refcount_warn_saturate(r, REFCOUNT_ADD_OVF);
202 }13 void refcount_warn_saturate(refcount_t *r, enum refcount_saturation_type t)
14 {
15 refcount_set(r, REFCOUNT_SATURATED);
16
17 switch (t) {
18 case REFCOUNT_ADD_NOT_ZERO_OVF:
19 REFCOUNT_WARN("saturated; leaking memory");
20 break;
21 case REFCOUNT_ADD_OVF:
22 REFCOUNT_WARN("saturated; leaking memory");
23 break;
24 case REFCOUNT_ADD_UAF:
25 REFCOUNT_WARN("addition on 0; use-after-free");
26 break;
27 case REFCOUNT_SUB_UAF:
28 REFCOUNT_WARN("underflow; use-after-free");
29 break;
30 case REFCOUNT_DEC_LEAK:
31 REFCOUNT_WARN("decrement hit 0; leaking memory");
32 break;
33 default:
34 REFCOUNT_WARN("unknown saturation event!?");
35 }
36 }O valor hardcoded REFCOUNT_SATURATED é definido como (INT_MIN / 2) que é igual a 0xc0000000.
117 #define REFCOUNT_SATURATED (INT_MIN / 2)Como esse valor é grande o bastante e não há referências suficiente para causar a liberação do objeto, as operações de liberação do objeto não o libera. Problemas de contador de referências normalmente levam a memory leak ao invés de use-after-free. Mas há algo interessante nessa vulnerabilidade. A função refcount_set() executada por inet_twsk_hashdance() não checa o contador de referências antes de inicializá-lo e então sobrescreve o valor hardcoded colocado no objeto por sock_hold() quando ele detecta que o contador de referências do objeto é zero. No final, se a inicialização do contador de referências ocorrer após sock_hold(), o contador de referências ficará desbalanceado e a operação de liberação do objeto o liberará antecipadamente. O fluxo de execução continua e a referência obtida por sock_hold() é anulada.
134 static inline void refcount_set(refcount_t *r, int n)
135 {
136 atomic_set(&r->refs, n);
137 }Em um cenário normal, o contador de referências do objeto é inicializado com 3, a operação de consulta ocorre e sock_hold() incrementa ele para 4. Se a ordem de execução correr como mencionada, sock_hold() encontra o objeto com o contador de referências zerado, marca ele com 0xc0000000 e refcount_set() sobrescreve esse valor por 3. No final, o fluxo de execução continua com o contador de referências definido como 3 quando ele deveria ser 4, conduzindo a uma use-after-free real. Isso acontece após o aviso acionado por sock_hold(). A saída do GDB abaixo mostra quando esse fluxo de execução ocorre na ordem esperada.
(gdb) c
Continuing.
[Switching to Thread 3]
Thread 3 hit Breakpoint 6, arch_atomic_set (i=3, v=0xffff8880243ddb50) at ./arch/x86/include/asm/atomic.h:41
41 __WRITE_ONCE(v->counter, i);
(gdb) x/i $rip
=> 0xffffffff81a96b0c <inet_twsk_hashdance+220>: movl $0x3,0x80(%rbx)
(gdb) x/2gx $rbx + 0x80
0xffff8880243ddb50: 0x2d43199ac0000000 0xba5c070600000000
(gdb)O código mostrado acima irá escrever 0x3 na parte inferior do endereço 0xffff8880243ddb50 que contém o valor hardcoded 0x........c0000000 inserido pela API do kernel do Linux como mecanismo de proteção.
Para confirmar que esse fluxo de execução poderia ocorrer, nós recompilamos o kernel com algumas modificações. Nós adicionamos uma chamada a mdelay() antes de inet_tsk_hashdance() inicializar o contador de referências e uma outra após sock_hold() para dar tempo suficiente para inet_twsk_hashdance() executar antes da operação de liberação. Nós também habilitamos KASAN para obtermos o relatório detalhado confirmando o use-after-free real. Há um detalhe adicional. O cache afetado tem a flag SLAB_TYPESAFE_BY_RCU definida e KASAN não pode detectar problemas em caches que tem essa flag definida. Nós então manualmente removemos essa flag do cache.
Nós estamos cientes que o kernel 6.12 agora contém um recurso que ensina o KASAN a detectar problemas nesses caches, a configuração CONFIG_SLUB_RCU_DEBUG, mas essa versão do kernel também inclui a correção para a vulnerabilidade e muitas outras mudanças. Nós não quisemos investir tempo para re-introduzir a vulnerabilidade. O subsistema mudou muito desde então e fazer back-porting do recurso também não aprece uma tarefa trivial. Nós obtemos o seguinte relatório do KASAN. O link para o relatório completo do KASAN está aqui.
[ 64.602085] ------------[ cut here ]------------
[ 64.602088] refcount_t: addition on 0; use-after-free.
...
[ 64.603847] CPU: 2 PID: 5282 Comm: trigger Not tainted 5.14.0-362.24.2.el9_3.x86_64-RESEARCH-KASAN #20
...
[ 64.796078] ---[ end trace 0b5cc4dcce1de1a1 ]---
[ 65.800530] ==================================================================
[ 65.800534] BUG: KASAN: use-after-free in inet_twsk_put+0x1f/0x60
[ 65.800544] Write of size 4 at addr ffff88801b2e9840 by task exploit02/5282
[ 65.800550] CPU: 2 PID: 5282 Comm: trigger Tainted: G W ------- --- 5.14.0-362.24.2.el9_3.x86_64-RESEARCH-KASAN #20
...
[ 65.800559] Call Trace:
[ 65.800561] <TASK>
[ 65.800563] ? inet_twsk_put+0x1f/0x60
[ 65.800569] dump_stack_lvl+0x34/0x48
[ 65.800574] print_address_description.constprop.0+0x1f/0x1e0
[ 65.800600] ? inet_twsk_put+0x1f/0x60
[ 65.800606] print_report.cold+0x55/0x244
[ 65.800612] ? _raw_spin_lock_irqsave+0x87/0xe0
[ 65.800617] kasan_report+0xb5/0x130
[ 65.800623] ? inet_twsk_put+0x1f/0x60
[ 65.800628] kasan_check_range+0xfd/0x1e0
[ 65.800633] inet_twsk_put+0x1f/0x60
[ 65.800638] __inet_check_established+0x3e0/0x4b0
[ 65.800645] __inet_hash_connect+0x179/0x7d0
[ 65.800651] ? tcp_set_state+0x125/0x340
[ 65.800655] ? __pfx_tcp_set_state+0x10/0x10
[ 65.800659] ? __pfx___inet_check_established+0x10/0x10
[ 65.800664] ? __pfx_ip_route_output_flow+0x10/0x10
[ 65.800672] ? __pfx___inet_hash_connect+0x10/0x10
[ 65.800681] ? __pfx_avc_has_perm+0x10/0x10
[ 65.800687] ? futex_hash+0xa0/0x120
[ 65.800693] ? selinux_sk_getsecid+0x42/0x50
[ 65.800700] tcp_v4_connect+0x5e0/0xaf0
[ 65.800710] ? __pfx_tcp_v4_connect+0x10/0x10
[ 65.800716] ? __pfx_selinux_socket_connect_helper.isra.0+0x10/0x10
[ 65.800726] __inet_stream_connect+0x1ba/0x680
[ 65.800734] ? _raw_spin_lock_bh+0x85/0xe0
[ 65.800741] ? __pfx___inet_stream_connect+0x10/0x10
[ 65.800749] ? _raw_spin_lock_bh+0x85/0xe0
[ 65.800756] ? __pfx__raw_spin_lock_bh+0x10/0x10
[ 65.800761] ? selinux_netlbl_socket_connect+0x2b/0x40
[ 65.800768] inet_stream_connect+0x44/0x70
[ 65.800773] __sys_connect+0x101/0x130
[ 65.800779] ? __pfx___sys_connect+0x10/0x10
[ 65.800784] ? __pfx_blkcg_maybe_throttle_current+0x10/0x10
[ 65.800790] ? __pfx_restore_fpregs_from_fpstate+0x10/0x10
[ 65.800797] ? __pfx___x64_sys_futex+0x10/0x10
[ 65.800801] ? __audit_syscall_entry+0x178/0x200
[ 65.800807] ? ktime_get_coarse_real_ts64+0x4a/0x70
[ 65.800813] __x64_sys_connect+0x3c/0x50
...
[ 65.800938] Allocated by task 5281:
[ 65.800940] kasan_save_stack+0x1e/0x40
[ 65.800945] __kasan_slab_alloc+0x66/0x80
[ 65.800949] kmem_cache_alloc+0x155/0x310
[ 65.800954] inet_twsk_alloc+0x88/0x340
[ 65.800958] tcp_time_wait+0x41/0x510
[ 65.800962] tcp_fin+0x1c2/0x240
[ 65.800965] tcp_data_queue+0x882/0xb20
[ 65.800969] tcp_rcv_state_process+0x4a3/0xe20
[ 65.800973] tcp_v4_do_rcv+0x169/0x3e0
[ 65.800980] tcp_v4_rcv+0x1871/0x1910
[ 65.800985] ip_protocol_deliver_rcu+0x41/0x4b0
[ 65.800992] ip_local_deliver_finish+0xfc/0x130
[ 65.800998] ip_local_deliver+0x1d0/0x1e0
[ 65.801005] ip_rcv+0x255/0x270
[ 65.801010] __netif_receive_skb_one_core+0x123/0x140
[ 65.801016] process_backlog+0xf1/0x280
[ 65.801020] __napi_poll+0x59/0x260
[ 65.801025] net_rx_action+0x433/0x540
[ 65.801033] __do_softirq+0xf3/0x39d
[ 65.801040] Freed by task 5282:
[ 65.801042] kasan_save_stack+0x1e/0x40
[ 65.801048] kasan_set_track+0x21/0x30
[ 65.801052] kasan_set_free_info+0x20/0x40
[ 65.801058] ____kasan_slab_free+0x14e/0x1b0
[ 65.801064] kmem_cache_free+0x1b7/0x430
[ 65.801068] inet_twsk_free+0x90/0xa0
[ 65.801075] inet_twsk_deschedule_put+0x2a/0x40
[ 65.801082] __inet_check_established+0x3e0/0x4b0
[ 65.801089] __inet_hash_connect+0x179/0x7d0
[ 65.801097] tcp_v4_connect+0x5e0/0xaf0
[ 65.801101] __inet_stream_connect+0x1ba/0x680
[ 65.801107] inet_stream_connect+0x44/0x70
[ 65.801112] __sys_connect+0x101/0x130
[ 65.801117] __x64_sys_connect+0x3c/0x50
[ 65.801122] do_syscall_64+0x59/0x90
[ 65.801128] entry_SYSCALL_64_after_hwframe+0x72/0xdc
[ 65.801137] The buggy address belongs to the object at ffff88801b2e97c0
which belongs to the cache tw_sock_TCP of size 256
[ 65.801143] The buggy address is located 128 bytes inside of
256-byte region [ffff88801b2e97c0, ffff88801b2e98c0)
[ 65.801150] The buggy address belongs to the physical page:
[ 65.801154] page:ffffea00006cba00 refcount:1 mapcount:0 mapping:0000000000000000 index:0x0 pfn:0x1b2e8
[ 65.801160] head:ffffea00006cba00 order:1 compound_mapcount:0 compound_pincount:0
[ 65.801164] memcg:ffff88800fbf0e01
[ 65.801166] flags: 0xfffffc0010200(slab|head|node=0|zone=1|lastcpupid=0x1fffff)
[ 65.801177] raw: 000fffffc0010200 0000000000000000 dead000000000122 ffff88805fad12c0
[ 65.801183] raw: 0000000000000000 0000000080190019 00000001ffffffff ffff88800fbf0e01
[ 65.801186] page dumped because: kasan: bad access detected
[ 65.801190] Memory state around the buggy address:
[ 65.801194] ffff88801b2e9700: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
[ 65.801199] ffff88801b2e9780: fc fc fc fc fc fc fc fc fa fb fb fb fb fb fb fb
[ 65.801203] >ffff88801b2e9800: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb
[ 65.801207] ^
[ 65.801210] ffff88801b2e9880: fb fb fb fb fb fb fb fb fc fc fc fc fc fc fc fc
[ 65.801214] ffff88801b2e9900: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
[ 65.801217] ==================================================================
[ 65.801272] Disabling lock debugging due to kernel taint
[ 65.801275] ------------[ cut here ]------------
[ 65.801276] refcount_t: underflow; use-after-free.
...
[ 65.801407] CPU: 2 PID: 5282 Comm: trigger Tainted: G B W ------- --- 5.14.0-362.24.2.el9_3.x86_64-RESEARCH-KASAN #20
...
[ 65.801457] Call Trace:
[ 65.801460] <TASK>
[ 65.801462] ? show_trace_log_lvl+0x1c4/0x2df
[ 65.801468] ? show_trace_log_lvl+0x1c4/0x2df
[ 65.801475] ? __inet_check_established+0x3e0/0x4b0
[ 65.801481] ? refcount_warn_saturate+0xcd/0x120
[ 65.801486] ? __warn+0x9c/0x150
[ 65.801492] ? refcount_warn_saturate+0xcd/0x120
[ 65.801497] ? report_bug+0x15e/0x180
[ 65.801504] ? handle_bug+0x3c/0x70
[ 65.801509] ? exc_invalid_op+0x14/0x50
[ 65.801514] ? asm_exc_invalid_op+0x16/0x20
[ 65.801519] ? irq_work_claim+0x1e/0x40
[ 65.801526] ? refcount_warn_saturate+0xcd/0x120
[ 65.801531] __inet_check_established+0x3e0/0x4b0
[ 65.801539] __inet_hash_connect+0x179/0x7d0
[ 65.801544] ? tcp_set_state+0x125/0x340
[ 65.801548] ? __pfx_tcp_set_state+0x10/0x10
[ 65.801553] ? __pfx___inet_check_established+0x10/0x10
[ 65.801557] ? __pfx_ip_route_output_flow+0x10/0x10
[ 65.801563] ? __pfx___inet_hash_connect+0x10/0x10
[ 65.801569] ? __pfx_avc_has_perm+0x10/0x10
[ 65.801573] ? futex_hash+0xa0/0x120
[ 65.801577] ? selinux_sk_getsecid+0x42/0x50
[ 65.801602] tcp_v4_connect+0x5e0/0xaf0
[ 65.801610] ? __pfx_tcp_v4_connect+0x10/0x10
[ 65.801615] ? __pfx_selinux_socket_connect_helper.isra.0+0x10/0x10
[ 65.801621] __inet_stream_connect+0x1ba/0x680
[ 65.801627] ? _raw_spin_lock_bh+0x85/0xe0
[ 65.801632] ? __pfx___inet_stream_connect+0x10/0x10
[ 65.801636] ? _raw_spin_lock_bh+0x85/0xe0
[ 65.801640] ? __pfx__raw_spin_lock_bh+0x10/0x10
[ 65.801645] ? selinux_netlbl_socket_connect+0x2b/0x40
[ 65.801652] inet_stream_connect+0x44/0x70
[ 65.801657] __sys_connect+0x101/0x130
[ 65.801661] ? __pfx___sys_connect+0x10/0x10
[ 65.801667] ? __pfx_blkcg_maybe_throttle_current+0x10/0x10
[ 65.801673] ? __pfx_restore_fpregs_from_fpstate+0x10/0x10
[ 65.801678] ? __pfx___x64_sys_futex+0x10/0x10
[ 65.801682] ? __audit_syscall_entry+0x178/0x200
[ 65.801688] ? ktime_get_coarse_real_ts64+0x4a/0x70
[ 65.801694] __x64_sys_connect+0x3c/0x50
...
[ 65.801799] ---[ end trace 0b5cc4dcce1de1a2 ]-CONCLUSÃO
Além de acionar o relatório de erro do KASAN, nós não investimos tempo em pesquisar a explorabilidade da vulnerabilidade. Por fim, nós confirmamos que uma situação de um use-after-free real poderia acontecer mesmo com a proteção do contador de referências em uso. Nós não estamos ciente de nenhuma outra vulnerabilidade que poderia acionar um use-after-free real neste mesmo cenário.
Nós tivemos que entender a vulnerabilidade para escrever um acionador do zero. O acionador simplificado do syzkaller funciona, mas é um código estranho e leva muito mais tempo para acionar o problema. Sem os atrasos inseridos no código e em um kernel com KASAN habilitado, até o aviso da API do kernel é difícil de acionar. Nós deixamos o código rodando por mais de 48 horas, diversas vezes e o aviso apareceu apenas algumas vezes. Como o trabalho teve o objetivo de identificar o caso potencial de um use-after-free real, nós inserimos os atrasos, mas a semântica do código permaneceu intacta.
Esta vulnerabilidade nos mostrou novamente que algumas vezes descobertas interessantes ocorrem por acaso, principalmente quando exposto a sorte. É por este motivo que valorizamos a experimentação durante a pesquisa. Nós nunca sabemos o que podemos acabar descobrindo.
Os arquivos para esta pesquisa pode ser encontrado em nosso GitHub no seguinte link:
https://github.com/alleleintel/research/tree/master/CVE-2024-36904
REFERÊNCIAS
general protection fault in skb_unlink
https://syzkaller.appspot.com/bug?extid=278279efdd2730dd14bf
kcm: close race conditions on sk_receive_queue
https://github.com/torvalds/linux/commit/5121197ecc5db58c07da95eb1ff82b98b121a221
tcp/dccp: avoid one atomic operation for timewait hashdance
https://github.com/torvalds/linux/commit/ec94c2696f0bcd5ae92a553244e4ac30d2171a2d
tcp: Use refcount_inc_not_zero() in tcp_twsk_unique().
https://github.com/torvalds/linux/commit/f2db7230f73a80dbb179deab78f88a7947f0ab7e
use-after-free warnings in tcp_v4_connect() due to inet_twsk_hashdance() inserting the object into ehash table without initializing its reference counter
https://lore.kernel.org/netdev/37a477a6-d39e-486b-9577-3463f655a6b7@allelesecurity.com/
slub: Introduce CONFIG_SLUB_RCU_DEBUG
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=b8c8ba73c68bb3c3e9dad22f488b86c540c839f9
