Ao analisar relatórios de vulnerabilidade para as linguagens de programação C, C++, Perl e Java, a Equipe de Codificação Segura cert observou que um número relativamente pequeno de erros de programação leva à maioria das vulnerabilidades. Nossa pesquisa se concentra em identificar práticas de codificação inseguras e desenvolver alternativas seguras que os programadores de software podem usar para reduzir ou eliminar vulnerabilidades antes que o software seja implantado. Em um post anterior,descrevi nosso trabalho para identificar vulnerabilidades que informavam a revisão da Organização Internacional para a Padronização (ISO) e do padrão da Comissão Eletrotécnica Internacional (C) para a linguagem de programação C. A equipe de codificação segura do CERT também tem trabalhado no CERT C Secure Coding Standard, que contém um conjunto de regras e diretrizes para ajudar os desenvolvedores a codificar com segurança. Esta postagem descreve nosso último conjunto de regras e recomendações, que visa ajudar os desenvolvedores a evitar comportamentos indefinidos e/ou inesperados no código implantado.

Histórico de enfrentamento de questões de segurança em C

A linguagem de programação C começou a tomar forma em 1969,muito antes das preocupações com a segurança se tornarem importantes para suas aplicações. C foi padronizado pela primeira vez em 1989, muito cedo para levar em conta os problemas de segurança do ARPANET. Devido à falta de demanda do cliente por segurança, mesmo a revisão de 1999 da norma C continha apenas um recurso relacionado à segurança, a função snprintf() mencionada no meu post anterior no blog.

Nos últimos anos, no entanto, os desenvolvedores C foram forçados a voltar sua atenção para questões de segurança. O PADRÃO de Codificação Segura CERT C aborda essa necessidade fornecendo regras e recomendações para evitar problemas de segurança nas seguintes categorias:

  • pré-processador - problemas que lidam com macros
  • declarações e inicialização - escolhendo as qualificações de tempo e tipo de armazenamento corretas e regras de linguagem C para a singularidade de nomes variáveis
  • expressões - ordem de avaliação, uso seguro da sintaxe C
  • inteiros - questões aritméticas, como evitar o transbordamento inteiro
  • ponto flutuante - peculiaridades da aritmética computacional que muitas vezes são negligenciadas por pessoas que estão acostumadas a usar inteiros
  • matrizes - alocando e comunicando o tamanho correto, e usando os tipos corretos
  • caracteres e cordas - garantindo que as sequências de caracteres sejam anuladas, e o uso adequado de caracteres estreitos e largos
  • gerenciamento de memória - evitando vazamentos de memória, dupla livre e subaloloização
  • saída de entrada (I/O) - uso adequado da biblioteca de I/O do arquivo de C
  • ambiente - interligação com o sistema operacional
  • sinais - melhores práticas para o manuseio de eventos assíncronsos
  • tratamento de erros - garantindo a detecção correta das condições de erro
  • interfaces de programação de aplicativos - design consciente de segurança das interfaces entre partes de um programa
  • concurrency - questões que surgem em programas multithreaded.
  • diversos - questões não cobertas por outras categorias, como afirmações, e manutenção da segurança dos ponteiros de função.
  • POSIX - problemas específicos para o sistema operacional POSIX, que é amplamente utilizado com C.

Exemplos de regras de codificação segura do CERT C

O restante desta postagem no blog dá alguns exemplos dos tipos de regras de codificação seguras que definimos para C.

Macros do pré-processador. Uma parte do PADRÃO de Codificação Segura CERT C se concentra no pré-processador C, que é um expansor macro que executa no início do processo de compilação. Muitas vezes, os programadores ignoram as consequências relacionadas à segurança do uso indevido do pré-processador. Se um programador passar uma expressão que tem um efeito colateral como argumento para uma macro, a macro pode fazer com que o efeito colateral ocorra várias vezes, dependendo de como ele usa esse argumento.

A equipe de Codificação Segura cert desenvolveu recentemente regras para garantir que um programador não passe acidentalmente um argumento que deslize um efeito colateral em uma macro, ou que o programador escreva o código de tal forma que os efeitos colaterais ocorram apenas uma vez. Embora essas e outras regras descritas neste post não sejam as que normalmente seriam consideradas riscos à segurança, elas resultaram em problemas de segurança quando o código é implantado e ativado.

Por exemplo, desenvolvemos as seguintes regras e recomendações para os desenvolvedores seguirem quando eles escrevem código que envolve o pré-processador C:

  1. Evite efeitos colaterais em argumentos para macros inseguras (Identificador PRE31-C). Esta regra dura e rápida afirma que se um desenvolvedor está usando uma macro que usa seus argumentos mais de uma vez, então o desenvolvedor deve evitar passar quaisquer argumentos com efeitos colaterais para essa macro. Um exemplo da aplicação do PRE31-C é
#define ABS(x) (((x) < 0) ? -(x) : (x))
/* ... */
m = ABS(++n); /* undefined behavior */
  1. Não defina macros inseguras (Identificador PRE12-C). Esta recomendação refere-se ao PRE31-C que define uma macro insegura como aquela que avalia qualquer um de seus argumentos mais de uma vez.
  2. As listas de substituição de macro devem ser parênteses (Identificador PRE02-C). Esta recomendação sugere que os desenvolvedores devem usar parênteses em torno de listas de substituição de macro; caso contrário, a precedência do operador pode fazer com que a expressão seja computada de maneiras inesperadas. Por exemplo, se um argumento contiver um sinal de mais (+), e uma macro contiver um sinal de multiplicação (*), e o argumento não tiver sido parêntese, então a multiplicação ocorrerá primeiro, seguida pela adição, que pode não ser o que um desenvolvedor esperava. Um exemplo da aplicação de PRE02-C
#define CUBE(X) (X) * (X) * (X)
int i = 3;
int a = 81 / CUBE(i); /* evaluates to 243 */

Declarações. C fornece uma série de mecanismos para declarar tipos de dados e variáveis desses tipos de dados. Por exemplo, um desenvolvedor pode declarar uma variável em um escopo externo e também declarar outra variável de mesmo nome em um escopo interno aninhado. Nesse caso, a variável no escopo interno esconde a variável no escopo externo. Quando um desenvolvedor faz alterações nessa variável, ele ou ela pode assumir que mudanças estão sendo feitas na variável de escopo externo quando, de fato, apenas a variável de escopo interno está sendo alterada.

O problema das declarações é agravado pelo fato de que, em C, há um limite para quantos caracteres são necessários para serem únicos em um nome variável. A norma C tem vários requisitos, incluindo o seguinte:

  • Um nome macro tem 63 caracteres iniciais significativos. Se um programa tem dois nomes macro e eles diferem apenas no 64º caractere, o compilador pode pensar que esses são o mesmo nome.
  • Os programas também têm 31 caracteres iniciais significativos em um identificador externo. Se um programa tem duas variáveis cujos nomes diferem apenas no 32º caractere ou depois, o compilador pode pensar que essas são a mesma variável.

A situação descrita acima pode causar um problema em que um desenvolvedor declarou duas variáveis que podem residir inadvertidamente no mesmo escopo. Embora as duas variáveis possam não ter o mesmo nome em inglês, o compilador pode truncar o nome a tal ponto que os nomes são os mesmos.

Caracteres e cordas. C define um conjunto de funções que operam em cordas compostas de caracteres. É prática comum que os desenvolvedores contem o número de caracteres necessários em uma sequência e aloquem exatamente o mesmo número de bytes.

Por exemplo, se um desenvolvedor alocar espaço suficiente para armazenar a versão de texto do endereço IPv4 255.255.255.255, o desenvolvedor pode alocar 15 bytes quando ele ou ela realmente precisa de 16 bytes para acomodar o exterminador nulo no final. Um caractere adicional é necessário para o exterminador nulo, um byte cujo valor é zero que define o fim da sequência.

Enquanto linguagens como fortran armazenam uma contagem de quantos caracteres a string contém como parte da estrutura de dados de string, C não faz isso. Se o marcador que indica a extremidade da string estiver faltando, então o software não sabe que precisa parar. Em vez disso, ele continua procurando através da memória. Embora as regras e recomendações anteriores sejam destinadas a evitar vulnerabilidades que eventualmente levam a problemas de segurança, negligenciar incluir um byte para o exterminador nulo leva diretamente a um estouro de buffer.

Trabalho futuro

Desde que publicamos a primeira versão do PADRÃO DE Codificação Segura CERT C, aprendemos e melhoramos nossa abordagem para análise de vulnerabilidades e desenvolvimento de regras e recomendações.

Uma nova versão do CERT Secure Coding Standard será eventualmente publicada para atualizar as regras e recomendações existentes, bem como para adicionar alguns para novos recursos C, como a biblioteca multithreading C padrão.

Enquanto isso, o trabalho atual já teve sucesso. A Cisco e a Oracle adotaram o CERT C Secure Coding Standard como parte de seus processos internos. Continuamos a ouvir sobre o interesse adicional de várias organizações.

Recursos adicionais

Para obter mais informações sobre a iniciativa CERT Secure Coding, visite

https://www.sei.cmu.edu/research-capabilities/all-work/display.cfm?customel_datapageid_4050=21274


Autor: David Keaton

Artigo Original