A Web e o problema das senhas “clear text”

Nos últimos dias o serviço Trapster avisou que 10 milhões de senhas dos seus usuários poderiam estar comprometidas. No ano passado a rede de sites de notícia Gawker passou pelo mesmo problema por um problema parecido.

E se formos voltar no tempo vamos descobrir que todo ano temos pelo menos 2 ocorrências similares em sites grandes. E isso vem acontecendo ano após ano desde que a Internet se tornou acessível entre “civis”.

Se todos os usuários usassem senhas diferentes para cada um dos serviços que usa na internet o estrago causado por esse tipo de situação seria bastante limitado. Mas não é isso o que acontece e, quando senhas “vazam” na internet o estrago pode ser gigantesco.

Problema antigo. Solução conhecida.

Em 1994 fui fazer estágio na Telesp no interior de São Paulo. Lá eu tive meu primeiro contato “sério” com um Unix. Era um SCO Unix que rodava num 386SX com 7 terminais seriais.

Enquanto eu estava aprendendo a usar o Unix eu vi que tinha um arquivo chamado /etc/passwd e, pelo nome, imaginei que lá eu encontraria as senhas de usuários do sistema.

Naquela época eu era “metido a hacker” e fiquei entusiasmado com a idéia de descobrir a senha de todo mundo que usava aquele servidor. Fiquei mais animado ainda quando vi que as permissões do arquivo permitiam que qualquer usuário examinasse seu conteúdo.

Quando abri o arquivo veio a decepção… no lugar onde deveriam ficar as senhas estava um “x”. Mas não me dei por vencido. Após estudar as manpages (que viam impressas em manuais imensos!) fiquei sabendo que as senhas não estavam lá. Elas estavam no arquivo /etc/shadow.

Com o ânimo renovado fui atrás desse arquivo. Mas dessa vez as coisas estavam um pouquinho mais difíceis… só o usuário root conseguiria ver esse arquivo.

Chegou a hora, então, de uma pitada de engenharia social… não vou contar como fiz porque foi muito fácil mas consegui a senha de root do sistema… hora de ver a senha dos outros usuários da Telesp e implantar uma mega-revolução na telefonia brasileira!… erm… menos…

Quando abri o arquivo tomei uma ducha de água fria definitiva. No lugar onde as senhas deveriam estar eu só um amontoado de caracteres que não se pareciam com senhas. Até poderiam ser as senhas dos usuários mas parecia muito improvável (e de fato não eram).

Descobri depois que o que estava armazenado ali era o resultado de uma espécie de “criptografia”. Ou seja, em 1992 os sistemas Unix já não armazenavam as senhas em texto puro. É bem provável que eles já não fizessem isso a muito mais tempo.

Estamos em 2011. Se depois de 19 anos eu armazenasse as senhas dos meus usuários em “texto puro” eu deveria ser chamado de irresponsável e incopetente. Se um invasor tivesse acesso à essas senhas eu deveria ser tratado como criminoso. No mínimo.

A solução

A única solução correta e infalível para armazenar senhas de forma segura é: não armazená-las.

Aí você deve estar perguntando: se eu não armazenar a senha do usuário como eu consigo verificar a senha dele durante sua autenticação?

Uma resposta “básica” seria: armazene o hash da senha.

Segundo o HowStuffWorks brasileiro:

“Hash é resultado da ação de algoritmos que fazem o mapeamento de uma seqüência de bits de tamanho arbitrário para uma seqüência de bits de tamanho fixo menor de forma que seja muito difícil encontrar duas mensagens produzindo o mesmo resultado hash (resistência à colisão ), e que o processo reverso também não seja realizável (dado um hash, não é possível recuperar a mensagem que o gerou).”

Existem vários algorítmos para cálculos de hash. Cada um deles possui um determinado tipo de aplicação. As funções de hash mais “famosas” são aquelas cuja aplicação está no campo da criptografia: MD2, MD4, MD5, SHA1, SHA256, …

Vou demonstrar o que acontece com o MD5:

$ echo "123mudar" | md5sum
642d8860fc6fe3126803ebdbe9974abd
$ echo "123mudar" | md5sum
642d8860fc6fe3126803ebdbe9974abd
$ echo "123mudor" | md5sum
fe294bbc902c287efb7acb20c8fdb67a

Note que sempre obtemos o mesmo resultado quando a senha é a mesma mas quando mudamos 1 único caracter o resultado do cálculo de hash muda completamente.

Tendo isso em mente podemos pensar em armazenar no nosso banco de dados apenas o hash da senha do usuário. Quando for preciso verificar a senha informada pelo usuário aplicamos a função de hash à ela e comparamos com aquela que está armazenada no banco de dados.

Perfeito não é? Problema resolvido, não? Não! Ainda falta uma pitada de “sal” nessa receita…

Salt – mais uma dificuldade para o invasor

Vamos supor que um invasor tenha acesso ao banco de dados da aplicação e ao hash das senhas…

Com esses hashes o usuário pode disparar um ataque baseado em dicionários ou até mesmo procurar pelos hashes no Google! Veja o que acontece com uma senha “fraca”:

$ echo "senha" | md5sum
6fd720fb42d209f576ca23d5e437a7bb

Agora procure por “6fd720fb42d209f576ca23d5e437a7bb” no Google e veja o resultado 😀

Para resolvermos esse problema devemos usar um “salt” para gerar o hash da senha.

Salt é uma sequência aleatória de bits que são concatenados à senha do usuário antes de gerar o hash (quanto maior essa sequência mais difícil será o trabalho do invasor).

Por ser uma sequência aleatória precisamos armazená-la junto com o resultado do hash para ser possível verificar a senha depois. Vamos à um exemplo “pythonico”

$ python
>>> import random
>>> import hashlib
>>> senha = "senha"
>>> salt = ''.join(chr(random.randint(65, 122)) for x in range(5))
>>> salt # Esse é o Salt!
'vGBAA'
>>> salt_senha = salt + senha
>>> salt_senha # salt + senha
'vGBAAsenha'
>>> hash = hashlib.md5(salt_senha).hexdigest()
>>> hash # Esse é o hash do salt+senha
'3607507cfa3f31b0cf10e83af947df97'
>>> armazenar = salt + "$" + hash
>>> armazenar
'vGBAA$3607507cfa3f31b0cf10e83af947df97'

Tente procurar pelo hash “3607507cfa3f31b0cf10e83af947df97” no Google agora… ou submeter esse hash à um ataque de dicionário… Você verá que aumentamos um pouco a dificuldade para descobrir a senha do usuário.

Esse é o procedimento usado por grande parte dos frameworks web que implementam alguma forma de armazenamento de senha (ex. django.contrib.auth) (ver atualizações 2 e 3). Ele é bastante seguro e podemos considerar isso satisfatório. Mas as coisas estão mudando…

A nuvem “do mal”

Com o advento da “computação na nuvem” chegamos à situação onde podemos comprar “poder de processamento” tal como compramos energia elétrica.

Antigamente se a gente tivesse um salt+hash em mãos era quase impossível (ou economicamente inviável) conseguir poder de processamento suficiente para submetê-los à um ataque de força bruta.

Mas as coisas mudaram e com 1 cartão de crédito e uma quantidade “viável” de dinheiro é possível contratar dezenas de “nós” de processamento na Amazon ECS, por exemplo, e colocá-los para “atacar” o nosso salt+hash.

Esse tipo de prática provavelmente já está sendo usada por alguns invasores pelo mundo e aparentemente não existe uma solução definitiva para esse tipo de situação.

O que existe são medidas que você pode adotar para dificultar um pouco mais a vida dos vilões 😀

Uma delas é substituir o algoritmo de hash (MD5/SHA1) por outro algorítmo mais apropriado para o nosso uso.

O problema em usar os algorítmos MD5 e SHA1 para calcular os hashes de nossas senhas é que eles são muito eficientes e rápidos. As principais aplicações desses algorítmos exigem que eles sejam rápidos (ex. assinatura digital de um arquivo gigantesco).

Como eles são muito rápidos é possível disparar um ataque de força bruta e testar muitos hashes em um curto espaço de tempo. Como as plataformas na “nuvem” cobram por tempo de uso podemos quebrar uma senha à um custo relativamente baixo (ou viável economicamente).

Se trocarmos esses algorítmos por um que seja muito mais lento obrigamos o invasor a gastar mais poder de processamento (e consequentemente mais dinheiro) para descobrir nossa senha.

Um dos métodos mais indicados, hoje, é o bcrypt (blowfish). Existe implementações desse algorítmo para diversas linguagens:

E como eu sei se um site armazena minhas senhas em texto puro?

Não é possível saber com 100% de certeza se um site ou serviço armazena as suas senhas em “texto puro”, portanto, o melhor mesmo é criar o hábito de usar senhas diferentes em cada um dos serviços (só tente não anotá-las em papéis! :D).

Mas apesar de não ser possível ter certeza se o serviço em questão é desenvolvido por um irresponsável é possível buscar indícios dessa irresponsabilidade:

  • Receber um e-mail de confirmação de cadastro onde sua senha está presente – Se ele está te mandando um e-mail com sua senha é grande a possibilidade dela ser armazenada da mesma forma.
  • Use a opção “esqueci minha senha” dos sites para testar – se você usar essa opção e o site te mandar um e-mail (ou mostrar na tela) a sua senha é porque eles tem a sua senha “original” armazenada em algum lugar. O correto é receber um link para *resetar* sua senha.

Implicações no “mercado”

Nós que trabalhamos com web e somos entusiastas da idéia “da nuvem” devemos condenar a prática de armazenar dados sensíveis do usuário de forma tão irresponsável. Cada notícia que surge dando conta de vazamentos dessas informações prejudica todos os serviços. Para um leigo é a segurança “da internet” que é falha.

Se você é um empresário ou desenvolvedor sério e responsável deve cuidar da segurança dos dados dos seus usuários com todo o cuidado e, sempre que ver outra empresa trabalhando de outra maneira você tem a obrigação de condená-la pois ela também está, indiretamente, prejudicando o seu negócio.

Atualização:

O meu amigo Guilherme Manika postou um link para um artigo onde a equipe do Gawker relata o problema ocorrido com as senhas de seus usuários.

Pelo que entendi eles armazenavam o hash das senhas usando a função crypt(3) e um salt com apenas 12 bits que, como disse, é muito pouco para os padrões de ataque atuais.

Então, em 2008, eles modificaram o sistema para usar o bcrypt() também. Mas, aí a ‘burrada’ deles: eles continuaram gerando o hash com crypt(3) e armazenando no mesmo lugar que os hashes bcrypt() pra manter compatibilidade retroativa!

Segundo um e-mail que circulou numa lista de segurança, 748.081 usuários tinham as senhas armazenadas com crypt() e 195.178 tinham as senhas armazenadas com crypt() e bcrypt(). Total: 943.259 usuários afetados. Quase um milhão de pessoas.

Atualização 2:

O framework Django, ao contrário do que disse, não usa bcrypt() para gerar o hash das senhas armazenadas. No lugar disso ele usa os algoritmos PBKDF2 + SHA256 conforme recomendações do NIST (pdf). Eles também usam outras técnicas complementares para tornar o sistema mais seguro como um comparador de strings em tempo constante.

Atualização 3:

O artigo faz muitas referências à bcrypt() que era o algoritmo recomendado pela maioria dos especialistas na época em que esse artigo foi escrito. Acontece que nesse mundo da tecnologia as coisas vão evoluindo e melhorias vão sendo sugeridas. Apesar disso o uso de bcrypt() é “bom o suficiente” e, por isso, manterei o texto original.

Caso você queira seguir as recomendações mais recentes o melhor é usar PBKDF2+SHA256 (+ algoritmos de comparação de strings em tempo constante) conforme indicado na Atualização 2 acima.

Se você se interessa pelo assunto, quiser se aperfeiçoar e consegue se virar bem com o inglês eu recomendo o curso (gratuito) de Criptografia de Stanford no Coursera.

Publicado por

Osvaldo Santana

Desenvolvedor Python e Django, Empreendedor, dono de uma motocicleta esportiva, hobbysta de eletrônica, fã de automobilismo e corinthiano