Uma introdução descomplicada às artes obscuras das vulnerabilidades em C/C++
Aviad Hahami
15 de abril de 2022
0 minutos de leituraLumos!
Com o anúncio do suporte da Snyk a dependências não gerenciadas (principalmente bibliotecas C/C++), achamos que seria útil apresentar à nossa comunidade que não trabalha com C alguns riscos comuns e graves que rondam o mundo de C (entendeu?). Pense nisto como um “guia para iniciantes” sobre vulnerabilidades em C e C++: como elas são, que problemas podem causar e como corrigi-las.
Vulnerabilidades fantásticas e onde encontrá-las
C e C++ (daqui em diante, usaremos “C” para nos referir às duas linguagens) são consideradas linguagens de programação de baixo nível. Ao comparar linguagens de baixo nível com linguagens de alto nível (JS, Python etc.), a principal diferença está no gerenciamento de memória da máquina. Enquanto linguagens de alto nível gerenciam a alocação, o uso e a liberação de memória, em C essa responsabilidade fica a cargo do desenvolvedor. Isso permite que os desenvolvedores tenham precisão no desempenho das rotinas e nas otimizações da implementação, mas também pode introduzir vários problemas específicos desse domínio de linguagens.
Estouro de buffer (CWE-121) e gravação fora dos limites (CWE-787)
Estouro de buffer é provavelmente a vulnerabilidade relacionada à memória mais conhecida. Embora explorar um estouro de buffer possa ser complicado, a vulnerabilidade em si é simples: você excede o espaço alocado para o buffer. Em geral, o termo “estouro de buffer” se refere à exploração efetiva da vulnerabilidade — mas “estouro de buffer baseado em pilha” e “gravação fora dos limites” são, essencialmente, a mesma falha (por isso, vamos falar delas juntas).
A exploração pode ser difícil porque nem sempre basta “escrever código” na pilha (ou no heap), mesmo que você exceda os limites do buffer. Ao longo do tempo, foram feitas tentativas de reforçar as defesas e mitigar esse problema, com a criação de alguns mecanismos de proteção. Esses mecanismos dependem de vários fatores e podem levar em conta seu sistema operacional, os recursos do kernel, o compilador e muito mais para proteger seu código. Entre eles estão ASLR (randomização do espaço de endereçamento), canários de pilha e DEP (prevenção de execução de dados), entre outros. Todos têm como objetivo evitar bugs de corrupção de memória, como estouros de buffer. Em tempo de execução, se qualquer um desses mecanismos falhar, o sistema operacional interromperá a execução e gerará um SEGFAULT, tornando a exploração menos direta.
Quero mostrar um exemplo desse tipo de vulnerabilidade e de como explorá-la. Confira este programa em C:
Exemplo cortesia de 0xRick, como visto em seu blog.
Observando o código (mesmo sem conhecer C), dá para ver que buffer é alocado como um buffer de 64 caracteres e que modified é um número inteiro. Beleza.
Também vemos, na linha 9, que modified recebe o valor 0 e, na linha 12, verificamos se o valor é 0 ou não. Se você ainda não adivinhou, nosso objetivo é fazer com que ele deixe de ser zero. Na linha 10, usamos a função gets para ler da stdin (= entrada do usuário) para a variável buffer. Para quem não conhece gets: ela lê da stdin para um buffer fornecido até encontrar um caractere de nova linha (\n).
Como sabemos (porque você assistiu ao vídeo do YouTube indicado acima) que a pilha cresce em direção a um endereço menor e que, devido à estrutura de dados da pilha, modified fica “abaixo” de buffer na memória, se gravarmos mais de 64 caracteres em buffer, começaremos a sobrescrever o valor de modified! E esse é o mágico estouro de buffer!
Este exemplo específico não é muito perigoso (afinal, é uma demonstração). Mas imagine o que poderia acontecer se você sobrescrevesse o valor de uma variável de senha ou da URL que a máquina acessa. A mitigação é simples: recomenda-se usar a função fgets, que também verifica o tamanho da entrada, e não apenas a presença do “caractere de fim de sequência”.
Uso após liberação (CWE-416)
As vulnerabilidades de uso após liberação têm um nome bastante autoexplicativo: elas ocorrem quando você usa uma referência de variável depois que a memória foi liberada. Essa vulnerabilidade resulta de uma falha no gerenciamento de memória relacionada ao fluxo do software: a variável é usada depois de ser apagada, provocando uma ação inesperada ou um efeito residual imprevisto na aplicação.
Para ver essa vulnerabilidade e sua exploração na prática, recomendo o vídeo de LiveOverflow sobre como ele explora uma vulnerabilidade UAF em um desafio.
Estouro/subfluxo de inteiro (CWE-190 e CWE-191)
Estouro e subfluxo de inteiros são dois tipos semelhantes de bugs (e, mais tarde, de vulnerabilidades) causados pela forma como os números são representados nos computadores.
Embora eu não vá me aprofundar nos tipos de variáveis nem em como os números são representados nos computadores, vale mencionar que há dois métodos principais para representar números: com sinal e sem sinal. Variáveis com sinal podem representar números negativos e positivos; variáveis sem sinal armazenam apenas números positivos.
Um estouro de inteiro ocorre quando o valor que pedimos à máquina para armazenar é maior que o valor máximo que ela pode armazenar. Um subfluxo de inteiro ocorre quando pedimos à máquina para armazenar um valor menor que o mínimo (por exemplo, pedir a um inteiro sem sinal que armazene um número negativo).
Nos dois casos, o resultado é semelhante: o valor dá a volta no intervalo representável (ou seja, recomeça do início ou do fim) e, assim, muda. Em um estouro, o valor volta a partir de 0; em um subfluxo, começa pelo valor máximo que pode ser armazenado (ou seja, o inteiro sem sinal de 8 bits dá a volta para 256 [decimal]).
Este diagrama ilustra os tipos de variáveis e os valores que eles podem armazenar:

Um exemplo de exploração de estouro de inteiro que levou a um estouro de buffer foi identificado no OpenSSH v3.3 (CVE-2002-0639).
Considere o trecho a seguir:
Suponha que nresp seja 1073741824 e sizeof(char*) seja 4 (o tamanho típico de um ponteiro). O resultado de nresp*sizeof(char*) será um estouro (porque dará a volta e resultará no valor 0). Portanto, xmalloc() recebe e aloca um buffer de 0 byte. O loop seguinte provoca um estouro de buffer no heap, pois gravamos em uma posição de memória não alocada, que pode, por sua vez, ser usada por um invasor para executar código arbitrário.
Desreferência de ponteiro nulo (CWE-467)
Desreferenciar é realizar uma ação sobre um valor armazenado em um endereço. Para entender melhor essa vulnerabilidade, vejamos um exemplo:
De acordo com o padrão C, executar o código acima pode resultar em um “comportamento indefinido”. No entanto, a maioria das implementações gera um erro SEGFAULT, o que significa que o software tentou acessar uma área restrita da memória (violando, assim, as regras de acesso à memória). Isso faz com que o sistema operacional encerre o software.
Leitura fora dos limites (CWE-125)
Uma leitura fora dos limites ocorre quando você “lê além do local ou buffer designado”. Essa vulnerabilidade pode causar uma falha no sistema (no melhor dos casos) ou revelar informações do seu aplicativo (por exemplo, as senhas de outros usuários), o que não é nada bom.
Como exemplo desse tipo de vulnerabilidade, veja um trecho do aplicativo PureFTPd. Considere a linha 17. Se o tamanho de s1 for maior que o de s2, então, como a linha 8 percorre o tamanho de s1, as informações acessadas na linha 10 ultrapassarão os limites de s2. Isso resultará em uma leitura fora dos limites.
Este bug recebeu o identificador CVE-2020-9365; você pode ler o relatório.
Conclusão e próximos passos
Esperamos que agora você tenha uma compreensão (geral) de como são as vulnerabilidades em C/C++, onde costumam aparecer e como se manifestam. Embora algumas dessas explorações possam parecer complexas à primeira vista, entendê-las melhor amplia seu conhecimento sobre os detalhes internos do software e pode ajudar você a evitar e prevenir bugs críticos.
Como mostramos na explicação sobre estouro de inteiro, essas vulnerabilidades podem ser encadeadas, criando uma cadeia frágil que fica aberta à exploração maliciosa.
Agora que lançamos o suporte a C/C++ no Snyk Open Source, vamos compartilhar mais conteúdos sobre como encontrar, explorar e corrigir vulnerabilidades em C e C++.
Proteja suas dependências de código aberto
As ferramentas da Snyk, desenvolvidas para quem programa, criam PRs de correção com um clique para dependências de código aberto vulneráveis e suas dependências transitivas.
