
CVE-2023-32233
O código afetado é originário do kernel Linux oficial disponível em https://kernel.org/ e faz parte do componente Netfilter nf_tables (net/netfilter/nf_tables_api.c).
O Netfilter nf_tables permite atualizar sua configuração como uma operação atômica. Ao usar esse recurso, os clientes em modo de usuário enviam requisições em lote contendo uma lista de operações básicas. O Netfilter nf_tables então processa todas as operações dentro do lote como uma única transação. Ao processar o lote, o Netfilter nf_tables verifica as atualizações de estado da configuração para garantir que cada operação básica sucessiva seja válida, e isso também leva em conta as atualizações de estado de todas as operações anteriores dentro do lote. No entanto, a verificação atualmente implementada é insuficiente.
Em nosso cenário específico, começamos com uma configuração do Netfilter nf_tables
que possui um nft_rule com expressão lookup em um nft_set anônimo,
e onde o nft_set anônimo contém alguns elementos. Em seguida, enviamos uma requisição em lote
contendo as seguintes duas operações básicas:
NFT_MSG_DELRULE para excluir o nft_rule.lookup e o
nft_set anônimo.NFT_MSG_DELSETELEM para excluir qualquer um dos elementos do
nft_set anônimo excluído.A versão atual do Netfilter nf_tables aceita a requisição em lote acima.
Em seguida, ela chama nf_tables_commit_release() que anexa recursos liberados à
nf_tables_destroy_list. A nf_tables_destroy_list é então processada por
nf_tables_trans_destroy_work() que primeiro desaloca recursos relacionados à
operação NFT_MSG_DELRULE chamando:
nft_commit_release()
nf_tables_rule_destroy()
nf_tables_expr_destroy()
expr->ops->destroy() que aponta para nft_lookup_destroy()
nf_tables_destroy_set()
nft_set_destroy()
kvfree() que desaloca a memória usada por `nft_set`
antes de processar a operação NFT_MSG_DELSETELEM, onde a referência ao
nft_set desalocado é acessada via nft_trans_elem_set() durante as
seguintes chamadas:
nft_commit_release()
nf_tables_set_elem_destroy()
nft_set_elem_ext()
Dentro de nft_set_elem_ext() acima, a localização de memória do nft_set
desalocado é acessada para determinar a localização de nft_set_ext:
static inline struct nft_set_ext *nft_set_elem_ext(const struct nft_set *set,
void *elem)
{
return elem + set->ops->elemsize;
}
para as operações seguintes. Portanto, sempre que o valor de set->ops->elemsize
for corrompido, uma localização de memória inesperada pode ser interpretada como
uma lista de nft_expr a ser destruída:
static void nf_tables_set_elem_destroy(const struct nft_ctx *ctx,
const struct nft_set *set, void *elem)
{
struct nft_set_ext *ext = nft_set_elem_ext(set, elem);
if (nft_set_ext_exists(ext, NFT_SET_EXT_EXPRESSIONS))
nft_set_elem_expr_destroy(ctx, nft_set_ext_expr(ext));
Explorar a vulnerabilidade acima requer vencer uma condição de corrida com nf_tables_trans_destroy_work() que executa em uma thread de trabalho em segundo plano do kernel Linux. Isso parece complicar a exploração prática mesmo antes de considerarmos mitigações existentes, como o endurecimento do alocador de slabs do kernel, Randomização de Espaço de Endereçamento do Kernel (KASLR) e especialmente Integridade de Fluxo de Controle. No entanto, o PoC anexado prova que ainda é possível alcançar uma exploração razoavelmente confiável na prática.
Para explorar a vulnerabilidade, precisamos modificar o conteúdo da memória
do nft_set depois que ele é desalocado em nf_tables_rule_destroy(), mas
antes de ser usado em nf_tables_set_elem_destroy(). Ambas
nf_tables_rule_destroy() e nf_tables_set_elem_destroy() são chamadas dentro de
uma única invocação de nf_tables_trans_destroy_work() que executa em
uma thread de trabalho em segundo plano do kernel Linux. Além disso, o bloco de memória
desalocado geralmente está disponível para reutilização apenas a partir do mesmo núcleo de CPU.
Ao competir com nf_tables_trans_destroy_work(), melhoramos nossas chances
adicionando um atraso controlado para a thread de trabalho em segundo plano entre suas chamadas
nf_tables_rule_destroy() e nf_tables_set_elem_destroy(). Para isso, inserimos
uma operação adicional para destruir outro nft_set contendo um grande
número de elementos. Adicionalmente, mantemos todos os outros núcleos de CPU ocupados, de modo
que a thread de trabalho em segundo plano provavelmente será escalonada em um núcleo de CPU
específico, para que possamos tentar alocar uma nova estrutura a partir do mesmo núcleo de CPU
logo após ele desalocar nft_set em nf_tables_rule_destroy(). Nosso objetivo
é alocar um novo nft_set de tipo diferente para reutilizar a localização de memória do
nft_set desalocado em nf_tables_rule_destroy().
O novo tipo de nft_set é selecionado para usar um valor diferente para
set->ops->elemsize. Então, quando a thread de trabalho em segundo plano finalmente chama
nf_tables_set_elem_destroy() para processar a operação NFT_MSG_DELSETELEM, ela
interpreta seu argumento elem incorretamente, de modo que o
nft_set_ext *ext corrompido está alguns bytes após a localização correta. Isso significa que
determinados campos de dados controlados pelo usuário do nft_set_ext original são agora
interpretados como cabeçalhos, resultando em confusão de tipo.
Uma maneira de abusar dessa confusão de tipo é crafting os cabeçalhos do
nft_set_ext corrompido com valores de deslocamento tais que
nf_tables_set_elem_destroy() interprete o conteúdo de quaisquer blocos de memória adjacentes
como a lista de nft_expr a ser destruída através das seguintes chamadas:
nft_set_elem_expr_destroy()
__nft_set_elem_expr_destroy()
nf_tables_expr_destroy()
expr->ops->destroy()
Neste ponto da exploração, ainda não temos detalhes do layout da memória do
kernel. Portanto, não é possível criar endereços de ponteiro absolutos.
No entanto, ao criar os cabeçalhos do nft_set_ext corrompido, ainda podemos usar
deslocamentos fora do intervalo, de modo que expr->ops->destroy() seja chamado em
certos nft_expr válidos nos blocos de memória adjacentes.
Para isso, espalhamos expressões nft_log, com NFTA_LOG_PREFIX controlado.
Esse nft_log->prefix é então desalocado por nft_log_destroy() quando
expr->ops->destroy() é chamado:
static void nft_log_destroy(const struct nft_ctx *ctx,
const struct nft_expr *expr)
{
struct nft_log *priv = nft_expr_priv(expr);
struct nf_loginfo *li = &priv->loginfo;
if (priv->prefix != nft_log_null_prefix)
kfree(priv->prefix);
Observe que ainda podemos acessar e até mesmo desalocar novamente essa memória através da
outra referência da expressão nft_log espalhada.
Além disso, também podemos controlar o tamanho de nft_log->prefix, de modo que
ele possa ser alocado a partir de qualquer um dos slabs kmalloc-{8, ..., 192}. Finalmente, a
memória referida é interpretada como uma string de caracteres pelo kernel, então não há
necessidade de se preocupar com corrupções ao sobrepor diferentes objetos sobre ela.
Isso é essencialmente game over.