FACULDADE DE ENGENHARIA DA UNIVERSIDADE DO PORTO PROGRAMAÇÃO SEGURA FILIPA FONSECA SANTOS – 070509094 JOÃO DIAS BARBOSA - 070509112 RELATÓRIO DE PROJETO MESTRADO INTEGRADO EM ENGENHARIA INFORMÁTICA E COMPUTAÇÃO NOVEMBRO DE 2011 Resumo AS TÉCNICAS DE PROGRAMAÇÃO SEGURA, SÃO MUITAS VEZES TIDAS EM POUCA CONSIDERAÇÃO O QUE LEVA A VULNERABILIDADES NO CÓDIGO QUE POSSIBILITAM MUITAS VEZES JANELAS DE ATAQUE E EXPLORAÇÃO A UTILIZADORES COM INTENÇÕES MENOS CORRETAS. VULNERABILIDADES COMO O BUFFER OVERFLOW, DANGLING POINTERS OU DOUBLE FREES SÃO APENAS ALGUNS EXEMPLOS, QUE APESAR DE EM CÓDIGO SE RESUMIREM A PEQUENOS ERROS OU DISTRAÇÕES, QUANDO EXPLORADOS PODEM DAR ORIGEM A CONSEQUÊNCIAS DRAMÁTICAS PARA OS SEUS PROPRIETÁRIOS. NO ENTANTO , EXISTEM ATUALMENTE FERRAMENTAS QUE POSSIBILITAM UMA BOA ANÁLISE DE CÓDIGO DE FORMA A DETECTAR VULNERABILIDADES E PREVENIR ASSIM POSSÍVEIS ATAQUES . A FERRAMENTA “VALGRIND” É UM BOM EXEMPLO DE UM ANALISADOR , E EM SIMULTÂNEO UMA FRAMEWORK QUE PERMITE CRIAR NOVOS ANALISADORES , MELHORES E MAIS EFICIENTES. NESTE TRABALHO FORAM ESTUDADOS AS PRINCIPAIS VULNERABILIDADES DE CÓDIGO RESULTANTES DE ERROS DE PROGRAMAÇÃO OU PROGRAMAÇÃO NÃO SEGURA, SIMULADOS ATAQUES A ESTAS VULNERABILIDADES E CRIADOS TESTES SOBRE A FERRAMENTA “VALGRIND ” DE FORMA A COMPROVAR O SEU POTENCIAL . P OR FIM , FOI AINDA DESENVOLVIDO UM MANUAL DE BOAS PRÁTICAS DE PROGRAMAÇÃO , TENDO EM VISTA CONSCIENCIALIZAR OS PROGRAMADORES E FUTUROS PROGRAMADORES DE CUIDADOS A TER NO DESENVOLVIMENTO DE APLICAÇÕES . 2 Índice Resumo.......................................................................................................................................... 2 1. Introdução ............................................................................................................................. 4 2. Programação Segura ............................................................................................................. 5 3. Vulnerabilidades.................................................................................................................... 6 4. a. “Buffer Overflow”.............................................................................................................. 6 b. “Dangling Pointers” ........................................................................................................... 7 c. “Double Free” .................................................................................................................... 8 Valgrind ................................................................................................................................. 9 4.1. Pontos Fortes ................................................................................................................... 10 4.2. Pontos Fracos ................................................................................................................... 10 5. Testes .................................................................................................................................. 11 5.1. Testes sobre o Buffer Overflow........................................................................................ 11 5.2. Testes sobre Double Frees ............................................................................................... 16 6. Conclusão ............................................................................................................................ 18 Referências .................................................................................................................................. 19 3 1. Introdução Este documento foi desenvolvido no âmbito da disciplina de Segurança em Sistemas Informáticos do quinto ano e primeiro semestre do curso de Mestrado Integrado em Engenharia Informática e Computação da Faculdade de Engenharia da Universidade do Porto. O trabalho teve como objectivo o estudo do tema “Programação Segura”, nomeadamente as principais vulnerabilidades que podem ser encontradas na programação e estudada a ferramenta “Valgrind” que constitui uma boa solução para este tipo de problemas. O objectivo deste documento é assim fornecer uma visão global do problema, abordagens utilizadas, conclusões e resultados obtidos. No presente relatório consta primeiramente uma abordagem ao tema da programação segura, as vulnerabilidades mais preocupantes e os riscos que podem causar. No capítulo seguinte são exploradas as vulnerabilidades do tipo buffer overflow, dangling pointers e double frees fazendo referência às suas principais causas e consequências. Seguidamente é abordada a ferramenta “Valgrind” tendo em conta os seus principais pontos fortes e fracos. No seguinte capítulo é especificado o resultado de testes sob a ferramenta “Valgrind” de forma a comprovar a sua eficácia face às vulnerabilidades abordadas. Por fim estão presentes breves considerações finais sobre o trabalho realizado bem como a bibliografia e utilizada no desenvolvimento deste relatório. É ainda apresentado no anexo A um manual de boas práticas de programação desenvolvido de maneira a consciencializar os programadores das vulnerabilidades existentes em código C e C++. 4 2. Programação Segura A programação segura é uma ciência que muitas vezes não é muito abordada uma vez que o desenvolvimento de software 100% seguro nem sempre é benéfico para as empresas, dado que implica elevados custos cujo benefício, tendo em conta o tipo de utilização do programa, é muitas vezes pouco significativo uma vez que alguns possíveis ataquem têm uma baixa probabilidade de acontecer. É então necessário para os programadores decidir em primeiro lugar, qual a informação a proteger, e fazer uma análise do risco que correm ao não implementarem mecanismos de segurança noutros pontos do programa. No entanto, pode por vezes acontecer que funcionalidades menos susceptíveis a ataques possam levar à exploração de outros pontos importantes do programa. Vulnerabilidades são fraquezas numa aplicação, que podem ser fruto de uma falha de design ou implementação e que, de certa forma, possibilitam uma janela de ataque que permite que um atacante realize operações maliciosas no programa (danos aos dados, leitura de informação confidencial, injecção de código, etc.) Uma das principais preocupações em termos de programação segura é a proteção de memória, nomeadamente memória de acesso aleatória (RAM). Podem ocorrer vários tipos de erros de memória, dependo da linguagem de programação usada e algumas das preocupações exploradas. São exemplo o “Buffer Overflow”, erros de memória dinâmica (“Dangling Pointers”, “Double Frees”, “Invalid Frees” e “Null Pointer accesses”), variáveis não inicializadas (como acontece com os “Wild Pointers”) e erros de excedência de memória (“Stack Overflow” e “Allocation Failures”). Algumas destas vulnerabilidades vão ser analisadas ao longo deste relatório, bem como os efeitos nocivos que podem ser originados, no caso de haver a exploração maliciosa durante execução do programa. Uma programação segura deve garantir os três seguintes princípios: Integridade: o programa tem um comportamento esperado, de acordo com o especificado nos requisitos e estabelecido nos algoritmos usados. Robustez: as operações do sistema são resistentes a qualquer tipo de inputs inesperados e a fluxos de acções não comuns. O tratamento de erros é eficiente. Segurança: o programa possui propositadamente malicioso. 5 operações com resistência a uso 3. Vulnerabilidades a. “Buffer Overflow” Uma das mais comuns anomalias devido a descuidos de programação é o “Buffer Overflow” que diz respeito a irregularidades de memória. Esta anomalia acontece quando um programa, ao escrever informação no buffer, ultrapassa o limite previsto deste e utiliza então memória adjacente. Tipicamente, este tipo de fragilidade acontece ao copiar cadeias de caracteres de um buffer para o outro. Tipicamente, o buffer overflow ocorre em código que depende de dados exteriores que controlam o seu comportamento ou em código demasiado complexo e difícil de prever todos os seus comportamentos. O seguinte código constitui um exemplo de uma vulnerabilidade possível de explorar tendo em vista um buffer overflow: … char buf[BUFSIZE]; gets(buf); … O código depende de dados exteriores para controlar o seu comportamento e utiliza a função “gets()” para ler uma quantidade de informação arbitrária para o stack buffer. O problema está na não limitação da informação a ser lida, que apoia-se apenas no bom senso do utilizador. Os atacantes utilizam o buffer overflow de maneira a corromperem a stack de execução de um programa. Um atacante pode até conseguir tomar posse da própria máquina. O ataque mais simples do tipo “buffer overflow “denomina-se “stack smashing“ e consiste em sobrescrever um buffer na stack para substituir o endereço de retorno. Assim, quando a função é retornada, em vez de saltar para o endereço de retorno, irá saltar para o endereço colocado na stack pelo atacante e permitirá então que este execute o código arquitectado pelo atacante. O buffer overflow pode ser explorado e usado por atacantes que pretendem tirar proveito desse tipo de vulnerabilidade ao enviar inputs planeados com o intuito de alterar as funcionalidades de um programa. O resultado destes ataques pode ser visto de diversas formas, nomeadamente: erros de acesso de memória, resultados incorretos, violações de segurança, loop infinito de um programa. A linguagem de programação C é particularmente susceptível a este tipo de ataques uma vez que na implementação desta foram mais valorizadas as condições de espaço e performance do que a de segurança. A linguagem não possui qualquer tipo de controlo sob 6 limites de buffers e a própria biblioteca standard inclui muitas funções que podem ser inseguras se não forem usadas com as devidas precauções. Apesar disso, muitos programas que requerem cuidados em termos de segurança são desenvolvidos em C. A linguagem C++ é também uma linguagem de programação que não possui qualquer proteção contra este tipo de ataques. Mas não são apenas estas linguagens de programação afectadas: estas vulnerabilidades podem ser encontradas em servidores web, aplicações web, código assembly, entre outros. Uma das técnicas de prevenção possível de utilizar é o uso de linguagens seguras, bibliotecas seguras, ou controlo dos limites dos buffers utilizados. A análise estática de código é também uma boa abordagem para detectar vulnerabilidades no código antes de ser executado, como definiram Larochelle D. e Evans D. em [1]. b. “Dangling Pointers” Em muitas aplicações é necessário a alocação de memória para diferentes objetos. Depois da sua utilização, a aplicação encarrega-se da libertação da memória utilizada, de forma a libertar recursos. Em alguns casos em que são utilizados apontadores para esses objectos, é nestas alturas que estes mesmos apontadores ficam a apontar para um objecto que já não está a ser utilizado, ou seja, cuja memória foi já libertada. Estes apontadores são então denominados de dangling pointers. Se tal acontecer, a aplicação poderá entrar num estado de execução indefinido que pode levar a crashes ou a comportamentos perigosos que podem levar a uma exploração maliciosa por parte de atacantes. Um ataque que pode ser feito através da exploração deste tipo de vulnerabilidades é a injecção de código. Podendo o atacante colocar o programa com funcionamento indesejável. Tal pode ocorrer quando o atacante prepara os dados de forma a estes ficarem alocados na posição que havia sido previamente ocupada pelo objecto para o qual o dangling pointer apontava antes da libertação de memória. Fazendo a de-referenciação do dangling pointer, os dados do atacante serão posteriormente interpretados pelo programa como function pointers ,(apontadores utilizados para invocações a funções) desviando o fluxo do programa para a localização indicada pelo atacante, podendo este efectuar injecção de código malicioso. (ex: shellcode). Programação orientada a objectos com gestão manual de memória é muitas vezes um “convite” a este tipo de ataques. Os ataques possíveis não se limitam apenas à alteração do fluxo de controlo do programa. Podem ser também atacados atributos dos dados em questão. Apontadores de dados atacados, podem ser explorados de forma a reescreverem informação noutros alvos, se um programa efectua uma escrita de dados através de um atributo de um apontador de dados 7 de um objecto já desalocado, um atacante controlando o conteúdo da memória do objecto desalocado pode desviar a escrita de dados para uma localização de memória arbitrária. Outros potenciais ataques prendem-se com fugas de informação, através da leitura de dangling pointers que apontam, agora, para informação confidencial. A alteração de privilégios é outro dos ataques possíveis, atacando campos de dados que possam conter informações de credenciais de acesso. [2] Algumas linguagens já possuem mecanismos para tentarem eliminar referências a possíveis dangling pointers, removendo o controlo sobre alocações de memória. Em algumas linguagens a operação de libertação de memória (ex: free() em C) já não existe. Para realizar a desalocação de memória que já não é utilizada, são utilizados gestores de memória locais ou garbage collectors (java). Estes mecanismos já previnem automaticamente essas possíveis referências.[3] c. “Double Free” Uma outra vulnerabilidade frequente na programação acontece quando, como o nome sugere, se liberta mais do que uma vez um endereço de memória, ou seja, é chamada mais do que uma vez a função “free()” com o mesmo endereço de memória como argumento. Este tipo de anomalia pode levar a um buffer overflow. O seguinte código é exemplo de um double free: char ptr* = (char*) malloc (SIZE); … if(abrt){ free(ptr); } … free(ptr); A causa dos double frees é muitas vezes devido a milhares de linhas de código em grandes quantidades de ficheiros que torna a análise destes complicada e como consequência os endereços de memória podem ser libertados mais do que uma vez sem que o programador tenha consciência de o ter feito. 8 4. Valgrind “Valgrind” é uma framework de desenvolvimento de aplicações de análise código e, em simultâneo um sistema de detecção de “bugs”. Figura 1 - Logotipo Valgrind O “Valgrind” pode (e deve) ser usado no desenvolvimento de programas de maneira a detectar de imediato vulnerabilidades de memória. Das ferramentas do “Valgrind” é de salientar a “Memcheck” que detecta problemas de memória e é mais direcionada a linguagens como C e C++ e analisa todos os reads e writes feitos à memória. Esta ferramenta é a mais adequada às vulnerabilidades abordadas neste relatório. A ferramenta “Cachegrind” é uma ferramenta que detecta zonas de código onde um programa tem uma má interação com o processador e como consequência é executa mais devagar. Foi melhorada e extendida na ferramenta “Callgrind”. A ferramenta “Massif” é um analisador da heap que ajuda a reduzir a quantidade de memória que um programa utiliza. Por último, a ferramenta “DRD” é um analisador de threads e procura falhas de sincronização entre threads de maneira a evitar problemas de dependências. Atualmente encontra-se na versão 3.7 que acrescenta o suporte para sistemas operativos Android. Para este trabalho, foi escolhido o “Valgrind” por ser bastante recomendado por diversas fontes na pesquisa feita. 9 4.1. Pontos Fortes Esta ferramenta permite encontrar diversos bugs de memória de programas e salvar assim horas de depuração. Para além disso ajuda a encontrar bottlenecks em código (i.e. partes críticas em termos de consumo de recursos) de maneira a optimizar o código. É disponibilizada na totalidade e sem qualquer necessidade de pagamento, para além de permitir modificações no código fonte. É fácil de usar e instalar. Para instalar basta correr os comandos :” ./config”, “make” e “make install“ e para testar o comando: “valgring --tool=memcheck program_name”. A ferramenta pode ser executada em diversas plataformas como x86/Linux, AMD64/Linux e PPC32/Linux e mais recentemente Android. É possível testar programas escritos em qualquer linguagem, seja ela compilada ou interpretada, uma vez que o analisador atua diretamente sob binários. No entanto as suas ferramentas estão mais direcionadas às linguagens C e C++, que como já referido, têm menos proteções e mais tendência a bugs. Por último, o “Valgrind” é extensível, ou seja, permite que programadores criem novas ferramentas a partir das funcionalidades já implementadas. 4.2. Pontos Fracos O factor mais problemático é a eficiência de um programa ao ser testado no “Valgrind” que pode diminuir significativamente. O facto de a plataforma não correr em sistemas operativos Windows pode ser visto como uma restrição significativa para muitos programadores. Além disto existem algumas vulnerabilidades de código que não são verificadas diretamente no Valgrind como a existência de dangling pointers. 10 5. Testes De forma a comprovar a eficiência da aplicação foram criados alguns programas de teste e analisados na ferramenta “memcheck”. 5.1. Testes sobre o Buffer Overflow Em primeiro lugar foram testados ataques, de maneira a causar o efeito de buffer overflow em dois programas distintos, em Windows. O primeiro programa, escrito em C++, resume-se a um sistema de login. Figura 2 - userPass.cpp Como é possível ver na figura acima, o sistema possui uma variável de autenticação (o inteiro “authentication”) inicializada a 0 e dois arrays de tamanho 10 para o username e password. Em seguida, é utiliza a função não segura “strcpy()” para copiar do buffer para os arrays os parâmetros introduzidos pelo utilizador: username e pasword respectivamente. Após copiados, é então feita uma comparação dos parâmetros introduzidos com os esperados. Caso o username corresponda a “admin” e a password corresponda a “adminpass” a variável de autenticação é alterada para 1. Caso a variável seja 1 (ou verdadeira), é então permitido o acesso, caso contrário este é negado. 11 Figura 3 - Buffer overflow sobre userPass.exe Posto isto, foi simulado um ataque ao programa (Figura 3), fazendo com que as variáveis fossem sequencialmente alteradas. Assim, como o array “cPassword” apenas permitia 10 caracteres, os restantes introduzidos foram colocados nas variáveis “cUsername” e “cPassword”. Por sua vez, a variável “cPassword”, agora com o valor diferente de zero, permitiu o acesso ao programa. Esta vulnerabilidade do programa, deveu-se ao uso da função não segura “strcpy()” que não verifica os limites a copiar, e como consequência, pode ser explorada de forma a causa um buffer overflow. Em seguida, foi utilizado o “Valgrind” para testar o mesmo programa (Figura 4), o resultado, como pode ser visto, na figura a seguir. Figura 4 – Análise do Valgrind ao programa userPass 12 Como pode ser visto pela mensagem dada pelo Valgrind, a instrução realizada não é suportada pela aplicação, e por isso lança um sinal SIGILL que irá terminar o programa. No entanto em vez da habitual mensagem de “Falha de Segmentação”, obtemos “Instrução Ilegal”, um tipo de mensagem que costuma surgir quando ocorrem problemas de stack overflows. Figura 5 – Mensagem de Instrução Ilegal Um outro teste foi realizado, desta vez num programa em linguagem C e tendo em vista aceder a uma função que nunca seria executada em casos regulares. A função “main()” necessita de 2 parâmetros em que o segundo é utilizado como argumento na chamada à função “foo()”. Para além disso esta imprime os endereços das funções “foo()” e “bar()” a utilizar no teste e verifica se o número de argumentos da função é igual a 2. A função “foo()” imprime os valores da stack antes e depois de ser chamada a função não-segura “strcpy()”. A função “”bar()” corresponde a uma função nunca executada que imprimiria apenas uma mensagem. 13 Figura 6 - hacking.c Mais uma vez, foi simulado um ataque ao programa “hacking.exe”. O objetivo é explorar o uso da função “strcpy()” de maneira a corromper a stack. Numa execução normal, após a função “foo()” ser executada seguir-se-ia a instrução de return, no entanto, em caso da stack ser corrompida, a função a executar será a que estiver na stack. Sabendo que o endereço da função é “004012D5” (como pode ser visto na Figura 6), é apenas necessário fornecer o input adequado de forma a colocar na stack esse endereço. Isto pode ser conseguido através de um script simples escrito em Perl: $arg = "ABCDEFGHIJKLMPAAAAAAAAAAAAAA"."\xD5\x12\x40"; $cmd = "./hacking ".$arg; system($cmd); Resumidamente, o script causa overflow através dos caracteres “ABCD…” e insere os caracteres pretendidos do endereço da função “bar()”: 4012D5. Pode ser visto na Figura 6, a função “bar()” a ser executada. 14 Figura 7 - Buffer Overflow sobre hacking.exe 15 5.2. Testes sobre Double Frees Outro dos testes efectuados tinha como objectivo a detecção de libertações inválidas de memória, chamadas invalid frees, que podem ocorrer quando um determinado objecto foi previamente desalocado. Para o efeito foi feito o seguinte código: Figura 8 – Código de teste para invalid frees Como pode ser visto na figura, são alocados 50 bytes para o apontador c. Obviamente o programa vai entrar na porção de código que está dentro da clausula if e aí podemos verificar que x vai ficar a apontar para o endereço de c. Em seguida ocorre a libertação de memória por parte de c. À saída da clausula if já ocorreu então uma libertação de memória por parte de c, pelo que a segunda será inválida, o mesmo se aplicando à libertação da memória de x, que no entretanto se tornou num dangling pointer, pois apontava para um endereço cuja memória já havia sido libertada. Ao correr o programa no Valgrind podemos verificar: 16 Figura 9 – Análise do Valgrind ao teste de invalid frees Na linha 17 ocorre a primeira libertação de memória de c. Na linha 19 é onde ocorre a segunda chamada free(c) uma vez que a memória deste já havia sido libertada, o Valgrind reconhece esta chamada como sendo um invalid free, o mesmo se passado para o caso da linha 20 (free(x)). 17 6. Conclusão Como conclusão, no fim deste trabalho podemos afirmar que as três vulnerabilidades estudadas podem gerar problemas graves na execução de um programa, a vários níveis: acesso a informação confidencial, mau-funcionamento, crashes. É então importante ter alguns cuidados para serem evitados este tipo de vulnerabilidades que podem gerar vários problemas. No entanto, como foi referido, os custos associados à implementação de sistemas seguros são elevados e por vezes não compensam o tempo perdido, uma vez que há o risco de se estar a proteger algo que nunca vai ser atacado. Por esta razão, vimos que é fundamental no início de cada projeto ter uma noção daquilo que é necessário proteger no sistema, e de que forma é que os ataques podem ser processados. Ainda assim, destacamos a existência de alguns padrões de desenvolvimento de software seguro que devem ser seguidos pelos programadores, por forma a garantirem a boa funcionalidade das aplicações. Como ponto negativo temos a destacar o facto de o tema ser bastante complexo para permitir uma componente prática no tempo de vida deste projecto. Pelo que foi antes abordada uma perspectiva mais teórica, desenvolvendo um estudo do tema e um conjunto de testes úteis. 18 Referências [1] Larochelle D, Evans D. Statically Detecting Likely Buffer Overflow Vulnerabilities. Science. [2] https://www.owasp.org/ [3] Periklis Akritidis, Cling: A Memory Allocator to Mitigate Dangling Pointers [4] http://www.cse.scu.edu/~tschwarz/coen152_05/Lectures/BufferOverflow.html [5] David A. Wheeler, Secure Programming for Linux and Unix HOWTO [6] Chad Dougherty, Kirk Sayre, Robert C. Seacord, David Svoboda, Kazuya Togashi, Secure Design Patterns [7] https://www.owasp.org/index.php/Secure_Coding_Principles 19 [Anexo A] 20 Manual de Boas Práticas de Programação 21 Introdução Aquando da definição de arquitectura de um sistema, é necessário ter em conta a cobertura de riscos tanto para utilizadores típicos como para atacantes experientes. Os arquitectos da aplicação devem lidar com vários tipos de eventos como tentativa de decifração “brute force”, injeção de código e fraude. A arquitectura deve portanto fornecer controlos para proteger a confidencialidade e integridade da informação e fornecer o seu acesso quando é (devidamente) pedido. Para uma programação segura e eficaz é necessário compreender os seguintes conceitos: Requisitos Quando envolvido num projecto com mais do que uma pessoa, o programador deve ter noção exacta dos requisitos a seguir, uma vez que pode cair no erro (muitas vezes não-intencional) de adicionar requisitos não necessários ao projecto a desenvolver. Nestas situações em que o programador está envolvido num projecto com mais do que uma pessoa, é fundamental que todas as partes compreendam os requisitos e objectivos a seguir antes de avançar para a escrita de código. Arquitectura A escolha de arquitectura apropriada para o projecto é um aspecto chave. É necessário que o programador saiba o que está a construir antes de poder iniciar o projecto, uma vez que necessita de ter a noção de como criar a solução mais indicada ao problema em questão. A investigação sobre os pontos fortes e fracos de plataformas, tecnologias e anotação dos respectivos defeitos e facilidades é um passo pelo qual se deve passar antes do início do desenvolvimento de cada projecto. Design Mesmo depois de bem definida a arquitectura a implementar, é necessário que o programador não caia na tentação de “exagerar” no design da aplicação, ou seja, em não disponibilizar ao utilizador mais do que este irá necessitar. Para isto os dois princípios essenciais são manter o programa o mais simples possível: “Keep It Simple”, e em esconder do utilizador toda a informação que este não necessite ver. Geralmente é aqui que entram os conceitos de análise orientada a objectos e UML. Construção de código A construção do código, embora seja muitas vezes vista como o principal no desenvolvimento de um projecto, por ser a mais visível, é apenas uma pequena parte do esforço total. A melhor prática para construção de código envolve desenvolvimento diário e consequente teste. Test-Driven-Development é um standard que segue esta 22 filosofia, “testar à medida que se vai desenvolvendo o código”. Outra boa prática, principalmente para projectos com mais do que um programador tem a ver com a qualidade dos comentários, bem como os standards seguidos para a criação e definição de variáveis, acessos a objectos, hierarquia de classes etc. Testes A criação de testes é a parte fulcral de qualquer desenvolvimento de software. É necessário que os casos de teste sejam criados enquanto a aplicação está a ser planeada e o código a ser desenvolvido. Porque programadores desenvolvem programas inseguros. Muitas vezes o custo inerente ao desenvolvimento de software “não compensa” os benefícios da maior qualidade do software desenvolvido. Uma vez que as empresas nem sempre dispõem de tempo suficiente para a implementação de alguns métodos de segurança. É então necessário avaliar a necessidade “real” da implementação de alguns mecanismos, tendo em conta o tipo de utilização esperado do software, o tipo de utilizadores, etc. O tempo despendido no desenvolvimento de mecanismos com uma pequena vantagem podem levar a perdas de oportunidades das empresas para a concorrência. A complexidade do software desenvolvido é também um obstáculo, uma vez que é difícil prevenir quais as interacções possíveis entre os diferentes processos, principalmente se o software estiver sujeito a futuros desenvolvimentos. 23 Técnicas Revisões de código A revisão de código fonte ocorre quando alguém, que não o autor original, efetua uma auditoria de código. Revisões de código feitas pelo autor do código original são geralmente insuficientes. A revisão deve ser feita por alguém que esteve por fora do desenvolvimento do código. Teste ao software Testes ao software devem ser feitos de forma a garantir que o programa lida de forma eficaz com inputs inesperados. Estes testes podem envolver simples operações de leitura de strings introduzidas pelo utilizador bem como leitura de ficheiros e combinação dos mesmos. Reutilização de código Se o programador possuir código conhecido e previamente testado, a reutilização deste pode ser uma mais-valia, porque reduz a probabilidade de surgirem bugs no programa. No entanto, a reutilização de código nem sempre é considerada uma boa prática particularmente quando envolve lógica de negócio ou sequência de actividades do programa. Uma vez que esse código pode ter um propósito diferente daquele esperado pelo programador. Para uma programação segura, a implementação destas técnicas deve ser combinada com alguns cuidados a ter no desenvolvimento do código fonte do programa. Algumas dessas precauções são explicadas na secção seguinte 24 Cuidados a ter 1. Validação de Input Durante a execução de um programa alguns inputs são introduzidos, directa ou indirectamente, por utilizadores não confiáveis, desta forma, um dos principais cuidados a ter para uma programação segura prende-se com a proteção ao nível do input do programa. Esses inputs devem ser previamente filtrados, deve ser definido pelo programador o que é legal, e rejeitada qualquer ocorrência que não corresponda ao estabelecido. É desaconselhável praticar-se o inverso, identificar aquilo que é ilegal e executar código para rejeitar esses casos, porque há uma grande probabilidade de não serem cobertos todos os casos. Alguns cuidados a ter com validação de inputs dividem-se em algumas das situações: a. Limite de tamanho do input O código deve estabelecer um limite para o tamanho do input a introduzir (dados muito longos podem causar instabilidade do programa). Deve ser feita a filtragem de alguns caracteres em strings. b. Filtragem de Metacaracteres Este tipo de caracteres deve ser evitado uma vez que são caracteres que invocam uma interpretação alternativa para os restantes caracteres presentes na sequência. A não verificação deste tipo de inputs pode levar à introdução de linhas de comando que podem ter efeitos adversos na execução de um programa. c. Cuidados com filenames Geralmente num sistema de ficheiros a introdução de “..” (parent directory) deve ser evitada, especialmente se o programa tiver a possibilidade de ser acedido por utilizadores não confiáveis uma vez que estes poderiam ter acesso a documentos/directorias não permitidas O “Directory Traversal Attack” é um ataque consequente da introdução de “..” num sistema de ficheiros. Uma boa prática neste caso, seria então proibir qualquer alteração referente ao directório actual, por exemplo não incluindo “/” na lista de caracteres aceites como input. Outra boa prática para este tipo de situação seria a não inclusão do caracter ‘*’ uma vez que este permite obter todos os ficheiros de um determinado tipo presentes naquela directoria. 25 d. Verificação de caracteres limitadores de strings Em alguns programas cujos dados são separados por “,” ou “;” ou outro qualquer caractere definido pelo programador, a ocorrência destes num determinado input poderia causar problemas, uma vez que poderia dividir os dados de forma incorrecta. e. Conteúdos de ficheiros Se um programa segue determinadas “instruções”/”direcções” de um determinado ficheiro, então não deverá confiar em ficheiros que não tenham acesso restrito para utilizadores confiáveis. Ou seja, isto significa que um utilizador não confiável não deverá poder alterar esse ficheiro, nem a sua directoria. 2. Evitar buffer overflows Uma primeira medida para evitar buffer overflows implica a utilização frequente de funções como gets(), strcpy(), strcat(), *sprintf(), *scanf(%s) em que o limite de caracteres introduzido não é verificado. Uma boa abordagem para isso envolve a utilização de chamadas a funções da biblioteca standard de C que por si só já são uma protecção contra este problema como por exemplo a utilização de strncpy(3) e strncat(3). Se é esperada a reutilização de um mesmo buffer várias vezes, a utilização das funções acima referidas é uma boa solução uma vez que exigem que lhes seja passado o número de caracteres a copiar. A função strncpy() não possui caractere de terminação de string se a string original possui, no mínimo, o mesmo tamanho do buffer de destino, por isso, é conveniente que o programador defina esse último caractere na string de destino depois de utilizar strncpy(). Ainda assim é necessário efectuar a verificação de caracteres a copiar uma vez que este número poderá ser maior do que o tamanho do buffer de destino. Apesar disto, este tipo de funções pode ter uma performance inferior quando comparadas com outras funções semelhantes que podem ser vistas como possíveis alvos de buffer overflow, por isso convém verificar a necessidade de usar este tipo de funções, uma vez que em alguns casos pode não ser essencial este tipo de proteção. Outra função que pode ser utilizada é sprintf(), apesar de necessitar de algumas precauções. O controlo desta função pode ter vários especificadores de conversões (por exemplo %s ou %i), e o controlo destes especificadores pode ter valores opcionais com o tamanho e precisão dos respectivos dados. Ora, estes controlos implicam diferentes condições, o comprimento do campo, apenas define o tamanho mínimo e desta forma é inútil como prevenção para buffer 26 overflow. (exemplo “%10s”, string com tamanho mínimo de 10 caracteres). Por sua vez a precisão estabelece um limite máximo da string em questão, e pode portanto ser utilizada como combate a este problema. (exemplo “%.10s”). No que toca a precisão este tipo de controlos só funciona quando estamos a lidar com strings. O seu significado é diferente para outro tipo de dados. Se o controlo for definido com “%.*s”, poderá ser usado o valor máximo do tamanho da string. Uma desvantagem da utilização de spritnf() é que, no caso de o formato dos dados ser complexo, é necessário verificar que existe espaço suficiente em memória para guardar a maior combinação possível de dados, o que por vezes poderá ser complicado uma vez que a precisão só controla um dos parâmetros. Se tal não acontecer pode ser originado um buffer overflow. Arrays estáticos vs arrays dinâmicos Existem duas formas de guardar informação em arrays: de forma estática, em que o buffer é alocado para o tamanho mais longo, e mantem essa forma; e de forma dinâmica: em que o seu tamanho vai sendo dinamicamente alterado conforme a necessidade. Como vamos verificar, ambas as abordagens têm algumas desvantagens. Em arrays estáticos, supondo que o que se pretende guardar tem, normalmente, um comprimento elevado, guardar tal informação num array estático pode levar a que a parte final da string a guardar fique “cortada” o que poderá obviamente ter efeitos indesejáveis para o programa. (exemplo: no caso de ser guardado um caminho de um determinado ficheiro, guardando um caminho inválido, leva à inacessibilidade do ficheiro. Para este tipo de abordagens é preferível a utilização de arrays dinâmicos, uma vez que a informação pode ser guardada com tamanho arbitrário. A contrapartida desta abordagem é que com arrays dinâmicos a alocação de memória é mais ineficiente, o que pode levar a que memória que seja necessária para o processamento de determinados dados esteja a ser utilizada noutro ponto do programa, para além de haver a possibilidade de o programa ficar sem memória disponível apesar de tal ser pouco aceitável nos dias que correm. Outra alternativa possível é a utilização de strlcat e strlcpy. Estas funções permitem cópia e concatenação seguindo uma abordagem estática, mas menos sujeita a erros. Ambas as funções recebem como parâmetro o tamanho do buffer de destino (e não o máximo de caracteres a copiar/concatenar) garantindo assim que o destino é preenchido com o caractere de terminação. 27 28 Princípios de desenho 1. Principio do menor privilégio Cada utilizador deve utilizar o sistema usando o mínimo de privilégios possível. Este principio limita os danos no caso de um acidente, erro ou ataque. Reduz também o número de potenciais interacções entre programas com alto nível de privilégios o que impede que uso incorrecto desses privilégios seja pouco provável de acontecer. Na execução de um programa, somente a parte do código que necessite de ser executada com determinados privilégios é que deve ter acesso a eles. Exemplo: Se num servidor middleware necessitar de acesso a uma rede ou a uma tabela de determinada base de dados, a único permissão que lhe deverá ser concedida é somente aquela de que necessita. Em nenhuma situação devem ser concedidos privilégios de administração. Vulnerabilidades a combater: Falha de libertação de privilégios A não libertação de privilégios na altura devida não é considerada uma vulnerabilidade propriamente dita, no entanto, pode aumentar o perigo de outras vulnerabilidades, uma vez que o atacante dispõe de maiores privilégios para explorar o sistema e consequentemente explorar outras possíveis vulnerabilidades. Se possível, devem ser limitadas as permissões de acesso a pequenas porções de código em que estas sejam estritamente necessárias. 2. Design aberto A forma como é feita a proteção do sistema não deve ser feita com base em esconder a informação do atacante, isto é, esconder os detalhes do sistema de segurança, para que o atacante com base no conhecimento do sistema, saiba como agir. O mecanismo deve ser público. Os utilizadores devem estar convencidos de que o sistema que estão a utilizar é adequado, e torna mais fácil para os utilizadores a sua avaliação. 29 3. Separação de privilégios O acesso a objectos deve depender de mais do que uma condição, assim, desbloquear um sistema de protecção (adquirindo um determinado de tipo de privilégio) não irá garantir directamente o acesso a um determinado objecto. 4. Mínimo de mecanismos comuns Deve ser minimizado o uso de mecanismos/objectos comuns uma vez que estes podem ser vistos como potenciais canais de fluxo de informação e de interacções indeterminadas. 5. Princípio da aceitabilidade psicológica A interface deve ser fácil de utilizar de maneira a que os utilizadores possam facilmente utilizar os mecanismos de protecção facilmente. Erros serão reduzidos se o mecanismo de segurança for intuitivo para o utilizador e para a sua ideia de protecção. 6. Desconfiança de outras entidades Muitas organizações utilizam processos de parceiros, que muitas vezes seguem princípios e políticas de segurança bastante diferentes. Não é comum que um utilizador (empresa neste caso) tenha influência ou consiga controlar alguma entidade externa. Desta forma a confiança em sistemas de segurança externos não é aconselhada uma vez que os mesmos podem não estar garantidos para casos diferentes. Por exemplo, um programa confiável pode fornecer dados para um Banco, esses dados podem ser códigos de segurança e respectiva identificação. No entanto, estes mesmos dados devem ser verificados para garantir que existem números de identificação mal atribuídos e que correspondem ao esperado. 30