Skip to main content

Identificando código C inseguro com Valgrind e corrigindo-o com Snyk Code

Escrito por
feature snyk supply chain purple

24 de setembro de 2024

0 minutos de leitura

C e C++ continuam sendo fundamentais para o desenvolvimento de softwares essenciais. Essas linguagens são usadas em uma ampla variedade de sistemas, de dispositivos embarcados a aplicações de alto desempenho na indústria, na tecnologia operacional (OT) e no setor industrial. Sua eficiência, o controle dos recursos do sistema e o desempenho fazem delas ferramentas indispensáveis para desenvolvedores que trabalham em projetos de missão crítica.

C e C++ são especialmente comuns no Japão, onde os setores de manufatura e industrial são importantes motores da economia. Desenvolvedores japoneses usam essas linguagens para criar softwares robustos e eficientes que sustentam tudo, de sistemas automotivos à automação de fábricas. A precisão e a confiabilidade de C e C++ são essenciais para manter os altos padrões exigidos nesses setores.

A importância da segurança do código em C e C++

Embora C e C++ ofereçam controle e desempenho incomparáveis, também apresentam desafios significativos de segurança. A ausência de recursos de segurança integrados, como o gerenciamento automático de memória, deixa essas linguagens suscetíveis a vulnerabilidades como estouros de buffer, uso após liberação e vazamentos de memória. Embora possam parecer simples e até ingênuas, essas vulnerabilidades podem ter consequências graves, especialmente em softwares essenciais, nos quais confiabilidade e segurança são fundamentais.

Código C vulnerável causa vazamento de memória

Vamos analisar um exemplo de código C vulnerável que desenvolvedores podem escrever e que introduz uma vulnerabilidade de segurança relacionada a vazamentos de memória.

Este é o código do nosso programa em C. Você consegue encontrar a vulnerabilidade de segurança?

#include <stdio.h>
#include <stdlib.h>

void allocateMemory() {
    int *ptr = (int *)malloc(10 * sizeof(int));
    if (ptr == NULL) {
        printf("Memory allocation failed\n");
        return;
    }
}

int main() {
    allocateMemory();
    return 0;
}

Este arquivo program1.c contém um exemplo de vulnerabilidade de alocação dinâmica de memória (MISRA Dir 4.12, MISRA Rule 21.3). A vulnerabilidade de segurança na função allocateMemory() é que ela usa malloc() para alocar memória, mas não a libera, causando um vazamento de memória. É possível evitar isso seguindo as diretrizes MISRA, que restringem a alocação dinâmica de memória.

Esse é um erro comum na programação em C e pode fazer com que seu programa use cada vez mais memória. Com o tempo, isso pode esgotar a memória e causar uma falha no programa ou impedir que outros programas do sistema tenham memória suficiente.

Por que não usar simplesmente a alocação estática de memória, como int arr[10]? Porque, em algumas situações, não é possível alocar memória na pilha. Por exemplo, imagine que você esteja escrevendo uma função para ler um arquivo para a memória. Como você só descobre o tamanho do arquivo durante a execução, não pode usar um array de tamanho fixo. Em vez disso, depois de saber o tamanho do arquivo, você pode usar malloc para alocar exatamente a quantidade necessária de memória.

Compile o programa e, em seguida, execute-o:

$ gcc program1.c -o program1
$ ./program1

Há um vazamento de memória no programa? Como você pode corrigi-lo? 

Encontre a vulnerabilidade de segurança em C com Valgrind

Valgrind é uma ferramenta poderosa para encontrar vazamentos de memória no seu programa. Podemos executá-lo com o Valgrind para verificar se há algum vazamento.

Instale o Valgrind no seu sistema. Se você usa um sistema baseado em Linux, pode instalá-lo com o gerenciador de pacotes. Veja um exemplo para sistemas baseados em Debian:

sudo apt-get install valgrind

Observação: se você usa o macOS em um dispositivo com chip ARM, o Valgrind não oferece suporte. Nesse caso, vamos ignorar essa etapa e executá-lo em um contêiner do Docker:

docker build -t "valgrind" . -f Dockerfile

Em seguida, execute o contêiner e mapeie o diretório atual para o diretório /tmp do contêiner:

docker run -it -v $PWD:/tmp -w /tmp valgrind

Em seguida, compile o programa program1.c dentro do contêiner:

gcc program1.c -o program1.app

Depois, podemos executar o programa com o Valgrind para verificar se há vazamentos de memória:

valgrind --leak-check=full ./program1.app

Em seguida, você verá a saída do Valgrind indicando o vazamento de memória:

/tmp # valgrind --leak-check=full ./program1.app

==29== Memcheck, a memory error detector
==29== Copyright (C) 2002-2024, and GNU GPL'd, by Julian Seward et al.
==29== Using Valgrind-3.23.0 and LibVEX; rerun with -h for copyright info
==29== Command: ./program1
==29==
==29==
==29== HEAP SUMMARY:
==29==     in use at exit: 40 bytes in 1 blocks
==29==   total heap usage: 1 allocs, 0 frees, 40 bytes allocated
==29==
==29== 40 bytes in 1 blocks are definitely lost in loss record 1 of 1
==29==    at 0x48E978C: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-arm64-linux.so)
==29==    by 0x108823: allocateMemory (in /tmp/program1)
==29==    by 0x108857: main (in /tmp/program1)
==29==
==29== LEAK SUMMARY:
==29==    definitely lost: 40 bytes in 1 blocks
==29==    indirectly lost: 0 bytes in 0 blocks
==29==      possibly lost: 0 bytes in 0 blocks
==29==    still reachable: 0 bytes in 0 blocks
==29==         suppressed: 0 bytes in 0 blocks
==29==
==29== For lists of detected and suppressed errors, rerun with: -s
==29== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

O Valgrind é ótimo, mas, para quem desenvolve em C no dia a dia, esse fluxo de trabalho não é escalável e exige compilar e construir um programa completo para analisá-lo.

Snyk Code ajuda você a identificar essa vulnerabilidade na sua base de código sem precisar passar pelo processo de compilação. Basta instalar a extensão e abrir o arquivo para que a vulnerabilidade seja exibida. Isso é possível porque Snyk Code é uma ferramenta de análise estática de código que usa técnicas de aprendizado de máquina para identificar problemas diretamente no código, sem exigir uma etapa de build e compilação. Com essa abordagem, quando a Snyk analisa seu código em busca de vulnerabilidades de segurança, você conta com um ciclo de feedback rápido e confiável, com poucos falsos positivos.

Editor de código exibindo um programa em C com um alerta do Snyk sobre a falta de liberação de memória após a alocação.

Detectando path traversal, estouro de buffer e outras vulnerabilidades em C

O mecanismo SAST que alimenta o Snyk Code detecta muito mais tipos de vulnerabilidade do que apenas vazamentos de memória causados por malloc.

Vamos ver um exemplo mais complexo que combina várias práticas inseguras de programação em C e introduz vulnerabilidades de segurança:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>

int main(int argc, char *argv[]) {
    // get the filename from the first command line argument 
    char *filename = argv[1];
    // append the filename to the current directory
    char path[50] = "./";
    strcat(path, filename);

    FILE *file = fopen(path, "r");
    if (file == NULL) {
        printf("Error opening file!\n");
        exit(1);
    }

    // read the contents of the file into memory and print the size of the file:
    fseek(file, 0, SEEK_END);
    long fsize = ftell(file);
    fseek(file, 0, SEEK_SET);
    char *string = malloc(fsize + 1);
    fread(string, fsize, 1, file);
    free(string);

    printf("Size of the file: %ld\n", fsize);
    printf("Contents of the file: %s\n", string);

    if (string != NULL) {
        free(string);
    }

    if (fsize == 0) {
        string[0] = 'A';
    }

    fclose(file);
    return 0;
}

O programa em C acima está repleto de vulnerabilidades, mas é uma forma bastante concisa e simplificada de apresentar código inseguro por questão de brevidade. Ainda assim, algumas práticas de codificação sutis podem facilmente acabar em bases de código reais.

Depois de compilar o programa, você pode executá-lo:

$ ./program3.app "text.txt"

Se houver um arquivo chamado text.txt no mesmo diretório e ele não estiver vazio, o programa lerá o arquivo e exibirá seu conteúdo.

Por exemplo:

Size of the file: 44
Contents of the file: FROM alpine:latest
RUN apk add g++ valgrind

Parece tudo certo. O que acontece se você passar um arquivo cujo caminho percorre a estrutura de diretórios?

$ ./program3.app "../../../../../etc/passwd"

O programa tem uma vulnerabilidade de path traversal, que permite que um invasor leia arquivos confidenciais no sistema.

Para testar outras vulnerabilidades, experimente:

  • Passar um arquivo que não existe

  • Passar um arquivo vazio

  • Passar o nome ou o caminho completo de um arquivo longo demais (com mais de 50 caracteres)

Alguns desses problemas de segurança vão muito além dos conhecimentos de programação em C de um desenvolvedor. É o caso do path traversal, que exige conhecimento de segurança de aplicações, além de práticas de gerenciamento seguro de memória e experiência consolidada em desenvolvimento com C.

A boa notícia é que, quando você cola o código do programa no IDE, a extensão da Snyk analisa o código C em busca de práticas inseguras e problemas comuns de segurança de aplicações. Ela apresenta os resultados rapidamente, com o contexto do código exibido em linha: 

O VS Code exibe código C, e o Snyk destaca vulnerabilidades de buffer overflow, liberação dupla e uso após liberação.

Proteja seu código C e C++ com a Snyk

Uma violação de segurança nos setores de manufatura e industrial pode ter consequências catastróficas, incluindo paralisações operacionais, riscos à segurança e perdas financeiras. Garantir a segurança do código em C e C++ não é apenas uma boa prática: é uma necessidade. Snyk oferece aos desenvolvedores os recursos necessários para identificar e corrigir vulnerabilidades logo no início do processo de desenvolvimento.

Proteja seu código com inteligência de ponta

Conheça toda a gama de recursos de análise estática (SAST) do Snyk Code em apenas 30 minutos.

Leia mais

feature insights context
Blog

Os ataques autônomos já chegaram. A defesa precisa acompanhar o ritmo.

Os atacantes autônomos estão reduzindo o tempo disponível para a defesa. Saiba como a descoberta, a correção, a validação e a prevenção contínuas ajudam as equipes de segurança a acompanhar esse ritmo.

illustration hero ai
Blog

O furacão da IA chegou

A IA está acelerando tanto a criação de software quanto os ataques cibernéticos. As lideranças devem proteger agentes e código desde o início, aplicar controles em tempo de execução e validar as defesas de forma independente.

feature insights context
Blog

A prevenção é essencialmente um problema resolvido?

A prevenção em código gerado por agentes está resolvida do ponto de vista arquitetural — mas escolher controles que protejam a segurança sem desacelerar o desenvolvimento continua sendo um desafio.