Skip to main content

Explorando o Buffer

Escrito por

5 de abril de 2016

0 minutos de leitura

Em meio às maravilhas do Node, há uma bomba-relógio chamada Buffer. Se manipulada incorretamente, essa classe arriscada pode expor facilmente a memória do servidor e, com ela, seus segredos e chaves, devido a uma vulnerabilidade divulgada por Feross Aboukhadijeh e Mathias Buss Madsen. Projetos populares e bem conceituados já foram afetados por esse problema, incluindo os pacotes npm mongoose, ws, request e, mais recentemente, sequelize, que introduziram vulnerabilidades generalizadas.

Neste post, vamos explicar como o Buffer funciona e por que se comporta dessa forma. Vamos executar um exploit contra uma aplicação vulnerável para demonstrar melhor as consequências. Por fim, vamos falar sobre as mudanças previstas no Node 6 e listar 5 etapas que você pode seguir para se proteger do comportamento padrão de Buffer, além de

Como chegamos a esse ponto?

O JavaScript do lado do cliente nos poupa de lidar com a alocação de memória. Como acontece com muitas linguagens semelhantes, no JS o mecanismo subjacente (por exemplo, V8) aloca memória e libera recursos não utilizados conforme necessário, o que torna a programação mais simples e segura. No navegador, impedir o acesso à memória também é necessário para manter o sandbox em que o JS é executado.

Quando o JS chegou ao servidor com o Node, o sandbox do navegador foi removido, e a necessidade de processar dados binários de maneira fácil e rápida aumentou. Para atender a essas necessidades, o Node introduziu a classe Buffer, que lida com dados binários. Vale lembrar que o ES6 criou posteriormente outras classes voltadas a dados binários, principalmente TypedArray e ArrayBuffer.

Buffer é um array mutável de dados binários que pode ser inicializado com uma string, um array ou um número. Veja alguns trechos da documentação do node.js:

const buf1 = new Buffer([1,2,3]);
// creates a buffer containing [01, 02, 03]
const buf2 = new Buffer('test');   
// creates a buffer containing ASCII bytes [74, 65, 73, 74]
const buf3 = new Buffer(10);
// creates a buffer of length 10

As duas primeiras variantes simplesmente criam uma representação binária do valor recebido. A última, porém, pré-aloca um buffer do tamanho especificado, tornando-o um buffer útil, especialmente para ler dados de um fluxo.

A falha de segurança

Ao usar o construtor de number de Buffer, a memória é alocada, mas não preenchida com zeros. Em vez disso, o buffer alocado contém… o que quer que estivesse na memória naquele momento. Você pode observar esse comportamento executando o Node em um terminal e criando repetidamente um Buffer de determinado tamanho. Cada um terá valores diferentes, pois aponta para uma região diferente da memória usada anteriormente.

> new Buffer(10)
<Buffer 00 20 00 00 00 00 00 00 d0 4d>
> new Buffer(10)
<Buffer 50 74 84 02 01 00 00 00 0a 00>
> new Buffer(10)
<Buffer 78 74 84 02 01 00 00 00 05 00>

Esse é um comportamento bem documentado, e você pode zerar a memória alocada simplesmente chamando “buf.fill(0)”. Porém, se esquecer de zerá-la, poderá acabar expondo memória. Expor memória talvez não pareça tão grave, até você considerar a quantidade de segredos — chaves, código-fonte, informações do sistema — que podem ser revelados. O Heartbleed, a grave vulnerabilidade do OpenSSL de 2014, permitia a exposição de memória.

Demonstrando o risco de segurança

Para entender melhor a vulnerabilidade do Buffer, vamos analisar uma aplicação vulnerável chamada Goof. O código está hospedado no GitHub, junto com instruções de instalação, caso você queira executar esses exploits.

Interface de lista de tarefas mostrando “Consertar a bicicleta” e “Ligar para a mãe”, cada uma com um botão vermelho para remover

Este é um aplicativo simples e vulnerável de lista de tarefas. Ele usa mongoose para se comunicar com o MongoDB, onde os itens são armazenados, usando um esquema simples:

var Todo = new Schema({
  content    : Buffer,
  updated_at : Date
});

Como você pode ver, o campo content, que contém a tarefa em si, é do tipo Buffer e, por isso, aceita dados binários. Como muitos aplicativos Node.js, o snyk-demo-todo também disponibiliza uma API JSON, que usaremos para criar um item pela linha de comando:

> curl https://localhost:3001/create --data '{"content":"Buy milk"}' -H "Content-Type: application/json"
QnV5IG1pbGs=%

O valor da variável content foi passado ao construtor Buffer, inicializando uma string curta que foi então gravada no banco de dados. O novo item aparece ao navegar pela aplicação e também é retornado na resposta (codificado em base64):

> echo "QnV5IG1pbGs=%" | base64 -D
Buy milk%
Interface de lista de tarefas com as atividades comprar leite, consertar a bicicleta e ligar para a mãe, cada uma com um ícone vermelho para excluir

Agora, vamos explorar a vulnerabilidade. Enviaremos a mesma solicitação, mas substituiremos a string "Buy Milk" pelo número 800 sem aspas:

curl https://localhost:3001/create --data '{"content":800}' -H "Content-Type: application/json"

Como antes, o valor de content é passado ao construtor Buffer. Desta vez, porém, o valor é do tipo number, acionando o outro construtor de Buffer… Isso inicializa o item da lista de tarefas com 800 bytes de memória não inicializada, que são armazenados no banco de dados. Esses dados também ficarão visíveis na resposta HTML (embora sejam binários e difíceis de ler) e serão retornados ao nosso curl após a codificação em base64:

# Response to exploit CURL command
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDKIAQEAAACQMogBAQAAAAAAAAAAAAAAcCKIAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANAiiAEBAAAAMCOIAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJAjiAEBAAAAAAAAAAAAAADwI4gBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8BeIAQEAAAAAAAAAAAAAAAAAAAAAAAAA0BaIAQEAAABQGIgBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+G6IAQEAAAC4AYkBAQAAACgCiQEBAAAAmAKJAQEAAACQA4kBAQAAAAAEiQEBAAAAcASJAQEAAADgBIkBAQAAAFAFiQEBAAAAwAWJAQEAAAAwBokBAQAAAKAGiQEBAAAAEAeJAQEAAACAB4kBAQAAAPAHiQEBAAAAYAiJAQEAAADQCIkBAQAAAEAJiQEBAAAAsAmJAQEAAAAgCokBAQAAAJAKiQEBAAAAAAuJAQEAAABwC4kBAQAAAOALiQEBAAAAUAyJAQEAAADADIkBAQAAADANiQEBAAAAAAAAAAAAAAADAAMAAAAAAIAaaQEBAAAAONEDAwEAAAAAAAAAAAAAADjSAwMBAAAAAgAAAAAAAAAYdQMDAQAAACgpagEBAAAACClqAQEAAAAAAAAAAAAAAMgpagEBAAAAkCpqAQEAAABwKmoBAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////AAAAAAAAAAAAAAAAAAAAAAAAAAA
Interface de lista de tarefas com um campo de entrada vazio e tarefas para comprar leite, consertar a bicicleta e ligar para a mãe.

Agora podemos repetir essa chamada várias vezes para obter mais blocos de memória e decodificá-los em base64. Provavelmente, vamos excluir as notas criadas à medida que avançamos para evitar suspeitas, examinando os dados ou buscando padrões como secret, ssh-key, código-fonte e outros.

Como corrigir isso?

No nosso aplicativo de exemplo, a vulnerabilidade estava na dependência mongoose. A correção foi converter números em arrays, tratando o valor como um único número, em vez de um tamanho. Se você estiver usando uma versão vulnerável do mongoose, atualize para uma versão mais recente. Se não souber se está usando esse pacote, use o Snyk para encontrar e corrigir essa e outras vulnerabilidades.

Se a vulnerabilidade estiver no seu próprio código, você pode impedir o uso do construtor number (convertendo o valor em um array ou string, se preferir) ou usar a função fill(0) sempre que ele for chamado. Vale lembrar que zerar a memória, embora seja eficiente, leva algum tempo em buffers maiores. Por isso, considere o impacto no desempenho da sua aplicação.

Mudanças previstas no Node 6

Esse comportamento padrão de Buffer é preocupante e já causou várias vulnerabilidades conhecidas, mas é difícil alterá-lo nas versões atuais do Node, pois isso poderia interromper o funcionamento de aplicações. No entanto, a partir do Node 6, recomenda-se usar os métodos explícitos alloc e allocUnsafe, que alocam espaço com e sem zerar a memória, respectivamente. Essa API explícita pode ajudar os desenvolvedores a evitar erros semelhantes.

O construtor padrão continuará disponível e funcionando da mesma forma, mas será descontinuado e seu uso não será recomendado. Ainda assim, você pode usar a flag --zero-fill-buffers no Node (também a partir do Node 6) para fazer com que o construtor padrão de Buffer que recebe um número preencha com zeros o buffer alocado.

5 etapas para manter a segurança

Buffer é uma classe útil, mas perigosa. Se estiver pensando em usá-la, considere o seguinte:

  1. Você pode usar TypedArray ou ArrayBuffer no lugar? Essas classes também são compatíveis com dados binários e preenchem a memória com zeros.

  2. Você pode impedir o uso do construtor number? Se puder, faça isso, como o mongoose fez.

  3. Você pode preencher os dados alocados com zeros usando buf.fill(0)? Isso terá um pequeno impacto no desempenho, mas é mais seguro.

  4. Se realmente não puder fazer nada do que foi sugerido acima, acompanhe com atenção onde o conteúdo de Buffer é usado. Com muita atenção.

  5. Se você usa pacotes npm como dependências, execute snyk wizard para corrigir possíveis vulnerabilidades relacionadas a Buffer nas suas dependências.

Comece a jogar Capture the Flag

Aprenda a resolver desafios de Capture the Flag assistindo sob demanda ao nosso workshop virtual introdutório.