
Variante sintética de estouro de buffer de pilha CWE-120 de CVE-2020-8597 (pppd EAP) como um alvo de análise estática CodeQL
Um programa C deliberadamente vulnerável, com ~170 linhas, usado como alvo de análise estática do CodeQL. Ele reproduz a classe de bug do CVE-2020-8597 — o estouro de buffer de pilha em rhostname do EAP do pppd (CWE-120) — em um programa que não compartilha nenhum dos nomes de função, profundidade de chamada ou estrutura de despacho do pppd.
O objetivo é um teste de generalidade: uma consulta CodeQL escrita para capturar o bug do pppd também deve disparar neste programa, sem edições. Se isso acontecer, a consulta expressa a classe de bug em vez da forma do código original.
Este programa é intencionalmente inseguro e existe apenas para análise. Não o implante. O bug que ele espelha é público (CVE-2020-8597, divulgado em 2020).
Um comprimento derivado do atacante é copiado para um buffer de tamanho fixo, sem nenhuma proteção que relacione esse comprimento ao tamanho do buffer.
Em todos os casos há uma verificação de limites — ela simplesmente falha em relacionar as duas quantidades que importam. Existem exatamente duas maneiras de errar nisso, e o programa contém uma de cada:
sizeof(dest). Impede uma leitura excessiva, não faz nada quanto à escrita excessiva. (handle_hello)sizeof(dest) — parece exatamente um limite de buffer — mas restringe uma variável diferente da usada como comprimento da cópia. (handle_stat)A segunda é a mais difícil, e é o que a verificação morta do pppd vallen >= len + sizeof(rhostname) é: uma comparação que menciona o tamanho do destino enquanto restringe algo que não é o comprimento da cópia. Uma consulta que apenas pergunta "alguma comparação aqui menciona sizeof(dest)?" é silenciada por ela.
| pppd / CVE-2020-8597 | este projeto | |
|---|---|---|
| Origem | read() no fd PPP | recvfrom() em um socket UDP |
| Despacho | struct protent *protocols[] global, correspondência linear no nº do protocolo | const struct frame_op ops[] local ao arquivo, correspondência linear na tag de 1 byte |
| Profundidade até o sink | get_input → (*input) → eap_input → eap_request | dispatch_frame → (*handle) → handle_hello |
| Destino | char rhostname[256] | char name[64] |
| Guarda errada | vallen limitado pelo len do pacote | vlen limitado pelo plen do frame |
Ambos mantêm a única propriedade que torna isso fluxo de dados, e não grep: uma chamada indireta através de uma tabela de ponteiros de função entre a origem e o sink.
| Handler | Linha | Verificação presente | Veredito |
|---|---|---|---|
handle_hello() | sink em :80 | vlen > plen - 2 — valor certo, limite errado | deve disparar |
handle_echo() | cópia em :106 | vlen >= sizeof(buf) — ambos certos | deve permanecer silencioso — controle negativo |
handle_stat() | sink em :145 | hlen >= sizeof(report) — limite certo, valor errado | deve disparar |
handle_stat é o caso discriminante. Sua verificação nomeia sizeof(report), então uma consulta que aceita qualquer comparação mencionando o tamanho do destino o trata como protegido e perde o bug. Capturá-lo exige comparar o valor sendo verificado com o valor usado como comprimento da cópia — numeração global de valores. Remova isso da consulta e este handler se torna um falso negativo, enquanto todos os outros locais mantêm seu veredito.
Um datagrama UDP = um frame:
[ type : 1 ] [ length : 2, big-endian ] [ value : length bytes ]
type 0x01 → hello, 0x02 → echo, 0x03 → stat. Um frame hello com um comprimento declarado entre 65 e ~2045 estoura name[64]. Um frame stat carrega dois comprimentos de um byte em vez disso — um comprimento de cabeçalho e um comprimento de corpo — e qualquer comprimento de corpo acima de 32 estoura report[32], independentemente do que o comprimento do cabeçalho diga.
make # gcc -Wall -Wextra -O0 -g -o tlv_server tlv_server.c
Linux/POSIX (sockets BSD). Compila limpo, sem avisos.
O CodeQL rastreia uma compilação real, então compile a partir do estado limpo:
make clean
codeql database create db --language=cpp --command="make"
# ou, sem a etapa de limpeza:
codeql database create db --language=cpp --command="make -B"
Em seguida, execute a consulta da Parte 3 contra db; ela deve reportar o memcpy em handle_hello e o em handle_stat, e permanecer silenciosa em handle_echo. A consulta e suas instruções de execução estão em codeql/.
O código-fonte muda sempre que um handler é adicionado, então reconstrua o banco de dados — o CodeQL tira um snapshot do código no momento do database create e um db/ existente não verá o novo código.