1 pontos por GN⁺ 2024-04-05 | 1 comentários | Compartilhar no WhatsApp
  • O -fanalyzer do GCC 14 acompanha vários caminhos de execução do código C por meio de execução simbólica, com foco em encontrar, no momento da compilação, defeitos difíceis de capturar antes da execução
  • Esta atualização se divide em detecção de loops infinitos, visualização de buffer overflow, rastreamento de strings em C e ativação padrão da análise de contaminação, com foco em tornar os diagnósticos mais fáceis de entender
  • -Wanalyzer-out-of-bounds vai além de um simples aviso e mostra, com um diagrama textual, a relação entre o buffer e a posição de escrita, além de indicar, em strings UTF-8, em que ponto do caractere ocorre o estouro
  • O novo atributo null_terminated_string_arg(PARAM_IDX) informa ao analisador e a quem lê o código as condições de uma API que espera strings terminadas em nulo, expondo melhor os caminhos em que um buffer sem terminação nula é passado
  • A análise de contaminação passa a ser ativada por padrão ao selecionar -fanalyzer, facilitando identificar fluxos em que valores controlados por atacante são usados em tamanho, índice, deslocamento etc. sem verificação de limite superior

Escopo dos problemas tratados por -fanalyzer

  • -fanalyzer é um passe de análise estática do GCC que tenta encontrar defeitos no momento da compilação por meio de execução simbólica de vários caminhos de execução do código-fonte C
  • As melhorias do GCC 14 foram organizadas antes do lançamento oficial de abril de 2024 e, no momento da redação do texto, esperava-se o lançamento do GCC 14.1 ainda em abril de 2024
  • A pré-versão do GCC 14.0 já estava em uso no Fedora 40 Beta
  • Os exemplos podem ser testados no Compiler Explorer com as novas opções do compilador

Detecção simples de loop infinito

  • O GCC 14 adiciona o novo aviso -Wanalyzer-infinite-loop
  • O código de exemplo tem for aninhados em que a condição do segundo loop é j < n, mas a expressão de incremento permanece como i++
    • Isso corresponde ao erro de copiar o primeiro for e não trocar o i por j na expressão de incremento
    • O analisador continua seguindo o ramo verdadeiro de j < n nesse caminho e avisa sobre a repetição do loop
  • Atualmente, a saída de diagnóstico fica mais fácil de entender se os eventos numerados de (1) a (5) forem lidos em sequência
  • No GCC 15, continua como objetivo desejado melhorar a legibilidade com algo como arte ASCII destacando o caminho do fluxo de controle
  • Esse código pode ser testado no exemplo do Compiler Explorer

Visualizando buffer overflow em texto

  • No GCC 13, o analisador passou a oferecer verificação de limites por meio de -Wanalyzer-out-of-bounds
  • No GCC 14, ele pode exibir a relação espacial de um buffer overflow previsto com um diagrama baseado em texto
  • No exemplo que executa strcpy(buf, "hello") em char buf[10] e depois chama strcat(buf, " world!"), é detectado um buffer overflow baseado em pilha
    • A mensagem anterior indicava que buf tinha capacidade de 10 bytes e que ocorria uma escrita fora dos limites do byte 10 ao byte 12
    • O diagrama do GCC 14 mostra junto o buffer de destino preenchido por strcpy e o byte NUL terminador existente que serve de ponto inicial para strcat
  • Também em exemplos com strings não ASCII, ele acompanha a representação UTF-8 e indica a posição do overflow
    • O exemplo usa código que copia "サツキ" para char buf[11] e depois concatena "メイ"
    • O diagrama mostra que o overflow ocorre no meio do caractere , isto é, U+30E1
  • O código relacionado pode ser visto no exemplo com string ASCII e no exemplo com string não ASCII

Rastreamento reforçado de operações com strings em C

  • O analisador do GCC 14 aprimora o rastreamento de operações com strings em C, simulando APIs que percorrem o buffer em busca do byte de terminação nula
  • Ele emite aviso se houver um caminho em que o ponteiro aponta para um buffer sem terminação nula e ainda assim é passado para essa API
  • O novo atributo de função null_terminated_string_arg(PARAM_IDX) informa ao analisador e a quem lê o código que um parâmetro específico deve ser uma string terminada em nulo
  • No exemplo, example_fn(const char *p) recebe os atributos null_terminated_string_arg(1) e nonnull
    • char str[3] = "abc"; não tem espaço para armazenar o byte de terminação nula
    • Ao chamar example_fn(str), o analisador avisa sobre uma leitura excessiva de buffer baseada em pilha de 1 byte após o fim de str
    • O diagnóstico também inclui uma nota informando que o argumento 1 de example_fn deve ser um ponteiro para string terminada em nulo
  • Esse exemplo pode ser executado no Compiler Explorer

Análise de contaminação ativada por padrão

Caso de análise de kernel com a CVE-2011-2210

  • Um trecho do kernel Linux referente à CVE-2011-2210 é usado como exemplo de análise de contaminação
  • Ao adicionar __attribute__((tainted_args)) à macro __SYSCALL_DEFINEx, o analisador é informado de que os argumentos de osf_getsysinfo vieram através de um limite de confiança e devem ser tratados como valores contaminados
  • O analisador do GCC 14 avisa em copy_to_user(buffer, hwrpb, nbytes) que o valor nbytes, controlado por atacante, está sendo usado como tamanho sem verificação de limite superior
    • O diagnóstico mostra que nbytes só recebeu uma verificação de limite inferior em if (nbytes < sizeof(*hwrpb))
    • O terceiro parâmetro de copy_to_user é identificado como parâmetro de tamanho pelo atributo access(write_only, 1, 3)
  • O problema está no fato de a condição de saneamento ter sido escrita como if (nbytes < sizeof(*hwrpb))
  • O trabalho de rodar o analisador no kernel para encontrar vulnerabilidades e corrigir falsos positivos do analisador continua em andamento

1 comentários

 
GN⁺ 2024-04-05
Comentários do Hacker News
  • Para mim, o fanalyzer é um dos recursos matadores do GCC em comparação com o Clang. Ele explica os erros, o que torna programar em C muito mais fácil, e as mensagens de erro começaram até a parecer com as do Rust no sentido de serem amigáveis para desenvolvedores

    • Eu sei que o Rust recebe muita atenção, especialmente no HN, por segurança de memória e boas abstrações, mas fico me perguntando quanto da popularidade do Rust vem das mensagens de erro
      Um dos maiores motivos para desistir de aprender uma tecnologia costuma ser erro frustrante ou pouco claro. Fugindo um pouco do assunto, eu queria dizer que gosto de C e gostaria ainda mais se ele tivesse mensagens de erro no nível do Rust
    • O Clang também tem uma ferramenta parecida, o Clang Static Analyzer: https://clang-analyzer.llvm.org/
    • Tive a experiência oposta. O Clang frequentemente mostra mensagens de erro muito melhores que o GCC, algumas implementações de avisos e erros pegam mais casos, e o clang-tidy faz uma análise estática muito melhor
    • Isso me lembrou um dos motivos de eu odiar tanto C++. Em vez de error: missing semicolon, às vezes vinham mais de 1000 linhas de mensagens de erro sobre instanciação de template
    • Isso soa bem surpreendente. Fico curioso sobre o que o analisador do GCC consegue detectar que o analisador estático do Clang já não reporte
      Usei o analisador do GCC algumas vezes, mas não consegui encontrar um frontend decente que deixasse a saída agradável de ler. O Clang tem várias opções, como uma saída HTML bem razoável, CodeChecker, integração com o Xcode etc.; então fico curioso sobre como vocês leem a saída do lado do GCC. Além disso, o GCC parece gerar muito mais falsos positivos que o Clang
  • Há mais 36 comentários em outro tópico: https://news.ycombinator.com/item?id=39918278
    “GCC 14 Boasts Nice ASCII Art for Visualizing Buffer Overflows (phoronix.com)”, publicado há 2 horas

  • Alguns meses atrás criei um pequeno utilitário para Linux. Era um shim drop-in para substituir qualquer executável arbitrário; quando chamado, ele fingia ser o programa original, dava fork no original e se conectava ao stdout/stderr dele
    A saída de erro era enviada para um assistente GPT personalizado que conhecia o contexto daquele programa, e o assistente reescrevia o erro original em um formato legível por humanos antes de enviá-lo ao stderr do shim. Eu o usava porque estava cansado de olhar dumps aninhados de compilador relacionados a concepts/templates no GCC/Clang, mas, se quisesse, poderia usá-lo com qualquer programa. Funcionava bem, mas fiquei gravemente doente e não consegui continuar trabalhando nisso; parece um bom projeto se alguém quiser refazê-lo direito e generalizá-lo

  • Seria bom ter um formato de saída melhor para os resultados da análise. Do jeito que está hoje, é um inferno para leitores de tela

    • Só para constar, implementei saída SARIF no GCC 13, e ela pode ser visualizada, por exemplo, no VS Code com um plugin. Mas não inclui a arte ASCII
      Um exemplo da saída pode ser visto aqui: https://godbolt.org/z/aan6Kfxds
      Basta adicionar a opção de linha de comando -fdiagnostics-format=sarif-stderr ao primeiro exemplo do post. Também fiz experimentos para gerar os diagramas como SVG, mas não ficaram polidos o suficiente para entrar no GCC 14
  • O código original retornava -1 se nbytes < sizeof(*hwrpb), e retornava -2 se copy_to_user(buffer, hwrpb, nbytes) falhasse. A correção aplicada foi verificar nbytes > sizeof(*hwrpb), mas, na minha opinião, a correção certa seria copy_to_user(buffer, hwrpb, sizeof(*hwrpb))
    Não faz sentido copiar a partir do ponteiro hwrpb uma quantidade diferente de sizeof(*hwrpb)

    • Se o chamador passar nbytes = 4 e sizeof(hwrpb) for 16 bytes, então ele vai copiar 12 bytes a mais do buffer do chamador e pode ler memória que não lhe pertence. Acho que isso deve ser evitado
      Uma solução melhor seria copiar apenas o mínimo de bytes suportado tanto pelo chamador quanto pelo chamado. Algo como nbytes = MIN(nbytes, sizeof(hwrpb)); serviria. Assumindo que a informação de versão em hwrpb->size seja respeitada, isso pode garantir compatibilidade retroativa e futura mesmo se parte da struct não estiver inicializada
    • Sim. Mas, dado que o tamanho do buffer foi informado, também não faz sentido sobrescrever o fim do buffer do chamador, então você também não pode passar um valor maior que nbytes
  • Muito impressionante. Parece uma quantidade enorme de trabalho. O nível de dificuldade parece comparável ao de introduzir fat pointer/visão de array na biblioteca padrão e no padrão da linguagem C

  • -Wstringop-overflow tem falsos positivos demais, então é o primeiro aviso que eu desativo. Duvido que uma variante do analisador seja melhor

    • Não é meio parecido com tirar a bateria do detector de monóxido de carbono porque ele continua apitando e te dá dor de cabeça e sono?
  • Muito legal. Não faço tanto desenvolvimento em C hoje em dia, então fico me perguntando com que frequência strcpy e strcat ainda são usados. Da última vez que vi, eles eram quase tão tabu quanto goto
    Claro, sei que no desenvolvimento de kernel goto costuma ser preferido. Fico curioso sobre o quanto a análise de strings em C realmente ajuda

    • Em alguns contextos, usar goto é claramente o jeito certo e elegante. Tentar evitá-lo a qualquer custo pode resultar em código feio, enrolado e difícil de manter. Não é comum, mas tem usos válidos
      Funções como strcpy são menos recomendadas, mas também há situações em que se sabe que elas estão corretas, contanto que invariantes mais fortes — por exemplo, invariantes no nível da linguagem — não sejam quebradas. Se esse tipo de coisa falhou, então já existe um problema muito maior. É raro, mas dá até para argumentar que, em alguns casos, uma alternativa nominalmente mais segura pode ser um pouco menos eficiente sem trazer benefício
    • Desde que seja usado de forma logicamente equivalente a construções de programação estruturada que não existem em C, alguns usos de goto continuam sendo idiomáticos em C. É preciso cuidado, mas, enfim, é C
      longjmp eu não gosto nem um pouco
    • Não há problema com usos simples de goto. Já a família strxcpy é completamente horrível e não deveria ser usada por motivo nenhum. O fato de ser usada no kernel é assustador
      Essas funções e todas as tentativas fracassadas de “corrigi-las” deveriam ter sido lançadas em órbita
    • goto é aceitável se usado com cuidado. strcpy e strcat também são “aceitáveis” no sentido de que você sabe que o código está certo e, se estiver errado, haverá um grande problema. Infelizmente, essa descrição vale para a maior parte do C
    • Eu não colocaria goto no mesmo nível de tabu que strcat e strcpy. goto é ok, e strcat e strcpy usados sem um malloc do tamanho exato no mesmo escopo são praticamente um code smell
  • Muito bom. Fico feliz que tudo venha com relatórios detalhados explicando o que deu errado