Segurança de código
-
Upload
wanderlei-silva-do-carmo -
Category
Software
-
view
264 -
download
0
Transcript of Segurança de código
SEGURANÇA DE CÓDIGO Código Seguro
Extraído do documento acessível em http://www.las.ic.unicamp.br/paulo/cursos/segc/mat.extra/isc-
codigo.seguro.pdf e reproduzido em apresentação Powerpoint por Wanderlei Silva do Carmo
Todos os direitos e créditos são do(s) autor(es) do artigo e conteúdos correlatos.
INTRODUÇÃO
“...É preciso entender desde o início que segurança de software é mais do quesimplesmente a escrita de um código seguro. No cenário atual de segurança, asconsiderações devem ir além da mera funcionalidade para levar em conta tambémrequisitos de segurança.”
“... O desenvolvedor de software deve primeiramente entender como o seu códigopode ser explorado (código inseguro) para depois usar esse conhecimento na escritade códigos que não fiquem sujeitos à exploração (código seguro). Assim como notratamento de doenças, o médico deve primeiramente diagnosticar o problemacentral antes de tratar os sintomas, ao desenvolver um software resistente a hackers épreciso entender, antes de qualquer coisa, o que constitui um código inseguro, paradepois abordar suas possíveis vulnerabilidades.”
FONTES DE LEITURA
A série Hacking Exposed (Hacking Revelado), 19 Deadly Sins of Software Security(19 Pecados Mortais em Segurança de Software), Exploiting Software (ExplorandoSoftware), Building Secure Software (Construindo um Software Seguro) e WritingSecure Code (Escrevendo Códigos Seguros). Embora essas fontes sejam consideradasde leitura obrigatória para todos os desenvolvedores de software, outras fontes comoa Chronology of Data Breaches (Cronologia das Quebras de Segurança de Dados) elistas de bugs de segurança e de divulgação completa mostram evidências de que osaplicativos de software produzidos nos dias de hoje ainda contêm um grande númerode vulnerabilidades.”
QUEM É RESPONSÁVEL PELA (IN)SEGURANÇA NO CÓDIGO
“A insegurança de software deve ser atribuída a todos os envolvidos no ciclo dedesenvolvimento do software, e os desenvolvedores – aqueles que escrevem o código– podem representar um papel vital no desenvolvimento de softwares seguros.”
O QUE É CÓDIGO INSEGURO?
Como diz o provérbio chinês, toda longa jornada começa com o primeiro passo. Ajornada para desenvolver softwares seguros começa com o primeiro passo aoidentificar o que torna um código inseguro. Portanto, o que é código inseguro? Códigoinseguro é aquele vulnerável a ataques de segurança. Para facilitar a vida do leitor,a palavra “insecure” (inseguro em inglês) pode ser usada como um acróstico paradescrever estruturas de programação e códigos que são vulneráveis a quebras desegurança. A lista a seguir não tem a intenção de ser exaustiva no que se refereàquilo que constitui um código inseguro, mas sim uma compilação das estruturas deprogramação mais comumente observadas que tornam o software inseguro
A injeção do código malicioso permite que os dados
fornecidos pelo usuário mal intencionado sejam
executados como código. Há diversos tipos de ataques
de injection: contra bancos de dados (por exemplo, SQL
Injection), contra estruturas de diretórios (por exemplo,
LDAP Injection) e até contra o próprio sistema
operacional (por exemplo, comandos no sistema
operacional injection). Validação ou filtragem
inadequada pode resultar em ataques de injection.
Injeção de Código Malicioso
A falta de validação de dados fornecidos pelo usuário é a
principal razão que possibilita os ataques de injection,
resultando em graves consequências. Brechas como a
divulgação de informações sensíveis (quebra de
confidencialidade), adulteração e manipulação de estruturas
e árvores de diretório (quebra de integridade), DoS (Denial-
of-Service – quebra de disponibilidade), burla da
verificação de autenticação e autorização (check bypass), e
mesmo a execução de código remoto tornam-se possíveis.
Outra possibilidade que não deve ser omitida é a de
estruturas de código que dependem de dados fornecidos
pelo usuário para montar dinamicamente as queries a serem
executadas dinamicamente no backend. Há diversas técnicas
de codificação defensiva contra ataques de injection. Uma
opção é o saneamento das entradas de dados, que pode ser
conseguido pela restrição a um determinado conjunto de
entradas válidas ou pela proibição e remoção de quaisquer
padrões e caracteres de entrada inválidos
Injeção de Código Malicioso
Uma segunda opção é o uso de queries parametrizadas,
quer dizer, que não sejam geradas dinamicamente. Elas usam
os dados fornecidos pelo usuário como parâmetros. Quando
aplicadas com a arquitetura correta, podem ajudar também
no desempenho. Os padrões de codificação não devem
admitir a construção dinâmica de queries no código com
vistas a reduzir a possibilidade de ataques de código
inoculável e isso deve ser definido como obrigatório.
O artigo “All Input Data is Evil – So Make Sure You Handle It
Correctly and with Due Care” (“Todos os dados de entrada
são maldosos – então, certifique-se de tratá-los corretamente
e com o devido cuidado”) escrito por Dino Esposito e
publicado na revista CoDe é uma boa referência e, conforme
sugerido apropriadamente pela segunda metade do título, é
extremamente importante ser capaz de identificar ataques
de injection e tomar as providências necessárias (devido
cuidado) para tratá-los.
Injeção de Código Malicioso
Em um mundo altamente interconectado e móvel, é
imperativo que a autenticidade da origem do código e de
transações comerciais e administrativas críticas seja
incontestável. O não repúdio é a garantia que a origem do
código é aquela que ele diz ter. É a capacidade de
verificação da autenticidade da origem do código. Isso é
especialmente importante, visto que os códigos podem ser
transferidos a partir de um local remoto e executados no
sistema local. O assim chamado código “móvel”, deve conter
prova de sua origem para verificação de autenticidade, o
que pode ser conseguido por meio de códigos com
assinatura digital. A assinatura digital é o processo pelo qual
os códigos (executáveis, scripts etc.) são digitalmente
marcados para assegurar que não tenham sido adulterados
e que são originados de um editor válido. A assinatura
digital é conhecida também como embalagem shrinkwrap
digital.
N – Ausência de mecanismos de Não Repúdio
O não repúdio garante também que as ações tomadas pelo
código não possam ser recusadas. A funcionalidade de
auditoria de código é um mecanismo para garantir que o
repúdio não seja possível. Como mínimo absoluto, para
transações comerciais e administrativas críticas, o código
deve ser escrito de forma a registrar as ações tomadas,
incluindo data/hora e outros detalhes pertinentes, como o
usuário ou o processo que está realizando a ação.
N – Ausência de mecanismos de Não Repúdio
O não repúdio garante também que as ações tomadas pelo
código não possam ser recusadas. A funcionalidade de
auditoria de cSpoofing é o ato de personificar outro usuário
ou processo. Código sujeito a spoofing é aquele que permite
ataques de spoofing. Ataques de spoofing permitem que o
imitador realize ações com o mesmo nível de confiança que o
usuário válido que está sendo imitado. Revelação acidental
de informações, adulteração de dados, esgotamento de
recursos, burla da autenticação, contorno de verificações de
autenticidade e exclusão ou manipulação de registros de
auditoria são todas possíveis consequências do spoofing. O
spoofing foi observado em casos onde a base do código não
era segmentada para ser executada sob diferentes
contextos de execução dependentes do nível de confiança
(privilégio) do chamador do código. Isso viola o princípio do
“mínimo mecanismo comum” que afirma que os mecanismos
comuns a diversos usuários/processos não devem ser
compartilhados.
S – Código sujeito a Spoofing
Com somente um contexto de execução para todo o código, um agressor
poderia imitar uma identidade e executar o código como se fosse um
usuário válido com permissão. Identificadores de sessão previsíveis,
senhas gravadas, credenciais em cache e permissão de imitação de
identidades são vulnerabilidades de codificação comuns que podem
resultar em ataques de spoofing. A figura 1 mostra um exemplo de
configuração que possibilita a personificação de um usuário específico.
Além do fato dessa personificação ser permitida, o nome de usuário e a
senha estão gravados na linha de código em texto puro, o que também
não é recomendável. A figura 2 é um exemplo que ilustra como a
identidade de um usuário autenticado é imitada em código. Códigos
sujeitos a spoofing podem resultar em diversos comprometimentos da
segurança, sendo mais comum o sequestro e reprodução de sessões. Um
agressor pode imitar a identidade de um usuário válido e assumir o
controle de uma sessão já estabelecida entre o cliente e o servidor e
depois reproduzir a ação. Caso haja alguma razão comercial válida
para permitir a personificação, tais ações deverão ser detalhadamente
monitoradas e auditadas. Devem ser tomadas precauções para garantir
que o código não permita ataques de personificação e spoofing.
S – Código sujeito a Spoofing
Qualquer desenvolvedor de software entende que é muito difícil fazer
um código livre de erros. Exceções e erros são inevitáveis. Entretanto, não
tratar exceções e erros ou tratá-los inadequadamente são opções
inaceitáveis quando se trata de segurança de software. Códigos que
revelam detalhes explícitos são um exemplo de tratamento inadequado
de exceções e erros. Um exemplo simples de erro explícito é a mensagem
“Nome de usuário inválido” durante uma tentativa de autenticação.
Mesmo uma mensagem simples como essa contém mais informações do
que é necessário. Uma mensagem como “Login inválido” seria suficiente.
Essa mensagem de erro não explícita deixa o agressor
E – Tratamento inadequado de Exceções e Erros
Qualquer desenvolvedor de software entende que é muito difícil fazer
um código livre de erros. Exceções e erros são inevitáveis. Entretanto, não
tratar exceções e erros ou tratá-los inadequadamente são opções
inaceitáveis quando se trata de segurança de software. Códigos que
revelam detalhes explícitos são um exemplo de tratamento inadequado
de exceções e erros. Um exemplo simples de erro explícito é a mensagem
“Nome de usuário inválido” durante uma tentativa de autenticação.
Mesmo uma mensagem simples como essa contém mais informações do
que é necessário. Uma mensagem como “Login inválido” seria suficiente.
Essa mensagem de erro não explícita deixa o agressor em dúvida se
inválido é o nome de usuário ou a senha, diferente do caso anterior onde
o agressor sabe que é o nome de usuário. Mensagens de erro não
explícitas que não mostram detalhes abertos da exceção aumentam
consideravelmente o trabalho do agressor que quiser tentar entrar no
sistema. No entanto, isso pode ter um efeito negativo na solução de
problemas e suporte ao usuário.
E – Tratamento inadequado de Exceções e Erros
Desta forma, as considerações de projeto devem ser ponderadas de
maneira que o software seja adequadamente explicito nas mensagens,
mas sem revelar detalhes. O tratamento inadequado de exceções e erros
pode resultar na revelação da arquitetura interna, do fluxo, tipo e
valores de dados e dos caminhos do código. Durante um exercício de
reconhecimento, um agressor pode usar as informações coletadas de uma
mensagem de erro explícita ou dos detalhes de uma exceção para
levantar o perfil do software. A figura 3 ilustra como uma exceção não
tratada pode revelar a existência de uma conta de login chamada
SecuRiskLabUser, além de revelar detalhes do stack da exceção,
fornecendo informações ao agressor que podem ser usadas para
levantar o perfil do software. A ausência de uma rotina abrangente de
tratamento de exceções e o mero repasse das informações de exceção
brutas para o front-end ou cliente é outro exemplo de tratamento
inadequado de exceções ou erros. Quando uma exceção ocorre, o
código deve tratá-la explicitamente. Além do mais, os ativos não devem
ser postos em risco em caso de falha, o que é um princípio de proteção
contra falhas (fail-safe ou fail-secure em inglês). As decisões devem ser
baseadas em permissões explícitas e não em exclusões.
E – Tratamento inadequado de Exceções e Erros
É importante garantir que erros e exceções sejam tratados de forma não explícita, sem revelar
mais informações do que o necessário e sem violar os princípios de proteção contra falhas.
Desenvolvedores são, essencialmente, os solucionadores de problemas
criativos, são aqueles que utilizam suas habilidades e conhecimento
tecnológico na criação de soluções para problemas e necessidades de
negócios. Os desenvolvedores buscam o aperfeiçoamento das
funcionalidades existentes. Infelizmente, sabese que isso também pode
ser um tiro pela culatra, especialmente no contexto da criptografia,
conforme evidenciado por diversas implementações medíocres de
criptografia personalizada observadas em revisões de código. Tais
revisões revelam que as funcionalidades de criptografia no código são,
na maioria das vezes, desenvolvidas internamente em vez de serem
aperfeiçoadas a partir de algoritmos existentes de criptografia
comprovados e validados. Isso contradiz o princípio de projeto seguro ao
“promover os componentes existentes” para minimizar a exposição a
ataques.
C – Código com Criptografia vulnerável
Além disso, mesmo quando algoritmos de criptografia comprovados são
utilizados, os detalhes da implementação permanecem inseguros. Os
algoritmos de criptografia utilizam um valor secreto, conhecido como
chave, para criptografar (converter texto puro em texto cifrado) e
descriptografar (converter texto cifrado em texto puro). A essência da
força de uma implementação de criptografia não está necessariamente
na robustez do algoritmo em si, mas na forma como a chave é derivada
e tratada. O uso de números não aleatórios para derivar a chave de
criptografia torna a proteção criptográfica fraca e ineficaz. Algumas
vezes, senhas em código ASCII não aleatórias e fáceis de adivinhar são
empregadas na derivação da chave de criptografia, o que deve ser
evitado. Outro problema comum é que as chaves não são armazenadas
de forma segura. Foram observadas chaves codificadas diretamente nas
linhas de código. Isso é a mesma coisa que trancar a porta e deixar a
chave na fechadura, proporcionando uma proteção mínima, se é que
proporciona alguma proteção. Deve-se dar atenção especial ao escolher
o algoritmo, verificando o histórico de robustez. Uma vez escolhido, deve-
se usar Geradores de Números Aleatórios (GNA) e Geradores de
Números Pseudo-Aleatórios (GNPA) para derivar a chave de criptografia
para codificação e decodificação. As chaves derivadas devem ser
armazenadas de forma segura.
C – Código com Criptografia vulnerável
As funções inseguras são consideradas inerentemente perigosas. Essas
funções são aquelas desenvolvidas sem necessariamente levar em
consideração as implicações de segurança. O uso de tais funções não
garante ao programador nenhuma proteção de segurança. Pode resultar
em vulnerabilidades que permitiriam que um potencial agressor
corrompesse a memória do sistema e/ ou conseguisse controle total sobre
o sistema. Uma das razões atribuídas aos notórios ataques de buffer
overrun é o uso de funções inseguras no código. Diversos boletins e
atualizações de segurança que abordam essas funções já foram
publicados. Funções inseguras são encontradas predominantemente em
sistemas-legado e linguagens de programação de gerações anteriores.
Dois exemplos comuns em linguagem C são as funções strcpy e strcat.
Como essas funções não realizam verificações de comprimento/tamanho
(também conhecidas como verificações de limites), um agressor poderia
fornecer dados de entrada de tamanho arbitrário, excedendo a
capacidade dos buffers de memória.
U – Funções e rotinas inseguras/não Utilizadas no código
A figura 4 mostra um exemplo da função insegura strcpy em linguagem
C, sendo utilizada para copiar os dados de entrada para um buffer de
memória local. Se não houver uma verificação de limites e os dados de
entrada fornecidos pelo usuário contiverem mais caracteres do que a
capacidade do buffer de memória local, o resultado será um buffer
overflow na memória local.
U – Funções e rotinas inseguras/não Utilizadas no código
Atualmente, os editores de linguagens de programação estão evitando
funções inseguras em favor de alternativas mais seguras, como as funções
strncpy e strncat que permitem a verificação de comprimento/tamanho
pelo desenvolvedor. Outra estrutura insegura de codificação, observada
em revisões de código, é a presença de funções não utilizadas,
representadas por trechos de código redundantes que não são mais
utilizados para tratar nenhuma funcionalidade. Mudanças nos negócios,
avanços tecnológicos e o receio de causar incompatibilidade com versões
anteriores são algumas das razões para que funções não utilizadas
sejam deixadas no código. Isso aumenta a exposição relativa do código
a ataques.
U – Funções e rotinas inseguras/não Utilizadas no código
“Easter eggs” é outro exemplo clássico de funções não utilizadas. Em
software, um “Easter eggs” é um código oculto que pode causar uma
alteração de comportamento do programa (por exemplo, a exibição de
uma mensagem, a reprodução de um som etc.) quando determinadas
condições são satisfeitas (por exemplo, uma sequência de cliques do
mouse ou de teclas etc.). Normalmente, o “Easter eggs” é inócuo, mas
também aumentam a exposição do código a ataques e, portanto, podem
sofrer consequências de segurança potencialmente graves. O risco de
perder clientes, de introduzir novos bugs e de impactos no desempenho
que podem resultar em esgotamento de recursos, ou seja, impacto na
disponibilidade (juntamente com o risco de parecer um eggomaníaco!)
são alguns dos efeitos negativos do “Easter eggs”. O interessante artigo
“Favorite software Easter Eggs” (Ovos de Páscoa de Software Favoritos)
publicado na IT World mostra o lado sinistro do “Easter eggs” de
software. O melhor a fazer é minimizar a exposição relativa do código a
ataques e isso significa evitar o uso de funções inseguras, removendo
funções não utilizadas que não possam ser rastreadas por uma matriz de
acompanhamento de requisitos, evitando, assim, a codificação de “Easter
eggs”.
U – Funções e rotinas inseguras/não Utilizadas no código
O aclamado trabalho do IEEE intitulado “Reverse Engineering and Design
Recovery: A Taxonomy” (Engenharia Reversa e Recuperação de Projetos:
Uma Taxonomia) define engenharia reversa como “o processo de análise
de um sistema para identificar seus componentes e o respectivo
interrelacionamento para criar uma representação do sistema em outra
forma ou em um nível mais alto de abstração”. Em resumo, a engenharia
reversa de software ou reversão é o processo de ir de trás para diante a
partir do sistema ou do código para determinar o projeto interno ou
detalhes de implementação. A reversão do software pode ser realizada no
nível de sistema ou de código. Códigos não obscurecidos ou sem assinatura
digital são facilmente revertidos. O obscurecimento de código é o processo
de tornar seu funcionamento difícil de entender e o código resultante é
algumas vezes chamado de “encoberto”. O obscurecimento é obtido pela
convolução do código para que, mesmo de posse do código fonte, este não
possa ser entendido. Os programas de obscurecimento podem operar no
código fonte, no código objeto ou em ambos, com o propósito principal de
impedir a engenharia reversa. A figura 5 mostra as versões não
obscurecida e obscurecida de um simples programa de impressão
HelloWorld. A assinatura digital do código (discutida anteriormente) é
outra técnica contra a engenharia reversa que oferece mais proteção do
que o obscurecimento.
R – Código sujeito a engenharia Reversa
Verificar o código quanto à existência de debuggers e enganar os
decompiladores utilizando dados não referentes a instruções ou bytes
inúteis são outras técnicas contra a engenharia reversa. Deve-se reconhecer
que tais técnicas contra a reversão só podem dificultar a reversão, mas não
necessariamente impedi-la. Não obstante, é imperativo que o código seja
protegido o máximo possível contra os riscos da engenharia reversa. O
livro Reversing - Secrets of Reverse Engineering (Reversão – Segredos da
Engenharia Reversa) de Eldad Eilam é uma fonte de informação excelente
para os interessados em escrever códigos irreversíveis.
R – Código sujeito a engenharia Reversa
O princípio do menor privilégio afirma que um usuário ou processo deve
receber somente o nível mínimo necessário de direitos de acesso
(privilégios) pelo menor tempo necessário para que o usuário ou processo
conclua a operação. Embora, algumas vezes um código que é executado
normalmente em um ambiente de desenvolvimento menos seguro apresente
soluços ou deixe de funcionar em um ambiente de produção mais restrito. A
solução usual para tais casos é aumentar o nível de privilégio sob o qual o
código possa ser executado ou remover a verificação de nível de privilégio.
Nenhuma dessas “soluções” é recomendável do ponto de vista de
segurança. Elas poderiam conduzir a uma evasão das permissões,
permitindo que usuários e processos com privilégios menores executem
códigos para os quais não estão autorizados. Adicionalmente, um outro
exemplo clássico de código inseguro é o código que pode ser
explicitamente configurado para ser executado com privilégios elevados
(administrativos) pelo programa.
As diferenças de configuração entre os ambientes de desenvolvimento e de
produção devem ser garantidas para que o código possa ser executado
com o menor nível de privilégios, independentemente do ambiente. Também
é imperativo garantir que os códigos configurados explicitamente para
serem executados com privilégios elevados sejam cuidadosamente
monitorados (auditados) e administrados.
E – Necessidade de privilégios Elevados para execução
‘Não escreva códigos inseguros’ é um dos oito hábitos seguros
discutidos em “8 Simple Rules for Developing More Secure Code”
(8 Regras Simples para Desenvolver Códigos Mais Seguros). Sem
um entendimento completo do que constitui um código inseguro,
seria injusto esperar que os desenvolvedores escrevessem códigos
mais seguros.
Conclusão
Caso o código seja vítima de uma quebra de segurança, o código em si pode ser considerado inocente, mas esse
tipo de indulgência não será estendido ao indivíduo, à equipe ou à organização que escreveu o código. Dessa
forma, é essencial que todos os que escrevem códigos tornem um hábito incorporar segurança ao código que
escrevem. Enumerar todas as formas de evitar a escrita de códigos inseguros e/ou como escrever códigos seguros
iria muito além do escopo deste trabalho. Em vez disso, ele deve ser visto meramente como um alerta para a escrita
de códigos seguros e para os tremendos riscos de não fazê-lo. Assim como um jogo de xadrez produz um número
infinito de movimentos uma vez que o jogo seja iniciado, com um número limitado de gambitos de abertura, o
entendimento das características dos códigos inseguros é um dos primeiros movimentos para assegurar que o código
seja escrito de forma a ser resistente a hackers. Código inseguro significa xeque-mate.
Conclusão