
Uso após liberação no Netfilter nf_tables ao processar solicitações em lote CVE-2023-32233

O código afetado é originário do kernel Linux oficial de https://kernel.org/ e faz parte do componente Netfilter nf_tables (net/netfilter/nf_tables_api.c).
Netfilter nf_tables permite atualizar sua configuração como uma operação atômica. Ao usar este recurso, os clientes em modo de usuário enviam solicitações em lote contendo uma lista de operações básicas. O Netfilter nf_tables processa então todas as operações 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 considera as atualizações de estado de todas as operações anteriores no 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 nft_set anônimo, e onde o nft_set anônimo contém alguns elementos. Em seguida, enviamos uma solicitaçã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 solicitação em lote acima. Em seguida, ela chama nf_tables_commit_release(), que anexa os recursos liberados a nf_tables_destroy_list. O nf_tables_destroy_list é então processado 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() that points to nft_lookup_destroy()
nf_tables_destroy_set()
nft_set_destroy()
kvfree() that deallocates memory used by `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 que se seguem. Portanto, sempre que o valor de set->ops->elemsize é corrompido, uma localização de memória inesperada pode ser interpretada como 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 corrida com nf_tables_trans_destroy_work(), que é executada a partir da thread de trabalho em segundo plano do kernel Linux. Isso parece complicar a exploração prática antes mesmo de considerarmos as mitigações existentes, como o endurecimento do alocador de slabs do kernel, a Randomização do Layout do Espaço de Endereço do Kernel (KASLR) e especialmente a Integridade do Fluxo de Controle. No entanto, o PoC anexado prova que ainda é possível obter 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 que seja usado em nf_tables_set_elem_destroy(). Ambas as funções nf_tables_rule_destroy() e nf_tables_set_elem_destroy() são chamadas em uma única invocação de nf_tables_trans_destroy_work(), que é executada a partir da thread de trabalho em segundo plano do kernel Linux. Além disso, a região de memória desalocada geralmente está disponível para reutilização apenas no mesmo núcleo da CPU.
Ao competir com nf_tables_trans_destroy_work(), aumentamos nossas chances adicionando um atraso controlado para a thread de trabalho em segundo plano entre suas chamadas a 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. Além disso, mantemos todos os outros núcleos da CPU ocupados, de modo que a thread de trabalho em segundo plano provavelmente seja escalonada em um núcleo de CPU específico, permitindo-nos tentar alocar uma nova estrutura do mesmo núcleo de CPU logo após ela desalocar o 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. Assim, 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 fica alguns bytes após a localização correta. Isso significa que certos campos de dados controlados pelo usuário do nft_set_ext original são agora interpretados como cabeçalhos, resultando em confusão de tipos.
Uma maneira de abusar dessa confusão de tipos é elaborar os cabeçalhos corrompidos do nft_set_ext com valores de offsets 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 destruir por meio 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 elaborar endereços absolutos de ponteiros. No entanto, ao elaborar os cabeçalhos corrompidos do nft_set_ext, ainda podemos usar offsets 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, pulverizamos 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 por meio da outra referência da expressão nft_log pulverizada.
Além disso, também podemos controlar o tamanho de nft_log->prefix, de modo que ele possa ser alocado de qualquer um dos slabs kmalloc-{8, ..., 192}. Por fim, a memória referenciada é interpretada pelo kernel como uma string de caracteres, então não precisamos nos preocupar com corrupções ao sobrepor objetos diferentes sobre ela. Isso é essencialmente o fim de jogo.