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
Download

PROGRAMAÇÃO SEGURA - Universidade do Porto