Eliminar vulnerabilidades de segurança de memória na origem
(security.googleblog.com)Eliminar a causa raiz das vulnerabilidades de segurança de memória
Resultado paradoxal
- Quando uma base de código escrita em linguagens sem segurança de memória cresce, migrar novos recursos para linguagens com segurança de memória reduz significativamente as vulnerabilidades de segurança de memória
- Isso acontece porque as vulnerabilidades diminuem exponencialmente ao longo do tempo
Explicação matemática
- O tempo de vida das vulnerabilidades segue uma distribuição exponencial
- As vulnerabilidades surgem principalmente em código novo, e o código se torna mais seguro com o passar do tempo
- A densidade de vulnerabilidades em código com 5 anos é de 3,4 a 7,4 vezes menor do que em código novo
Caso real no Android
- Desde 2019, a equipe do Android começou a migrar o novo desenvolvimento para linguagens com segurança de memória
- Em 2024, as vulnerabilidades de segurança de memória caíram de 76% para 24%
- À medida que as vulnerabilidades de segurança de memória diminuíram, o risco total de segurança também caiu
Evolução da estratégia de segurança de memória
- 1ª geração: patches reativos — abordagem de descobrir e corrigir vulnerabilidades
- 2ª geração: mitigação proativa — abordagem de dificultar a exploração de vulnerabilidades
- 3ª geração: descoberta proativa de vulnerabilidades — abordagem de encontrar vulnerabilidades com antecedência
- 4ª geração: prevenção de alta confiança — abordagem de prevenir a própria ocorrência de vulnerabilidades ao migrar para linguagens com segurança de memória
Vantagens da prevenção de alta confiança
- Interrompe a competição sem fim entre defensores e atacantes
- Aumenta a segurança e reduz custos com linguagens seguras em memória
- Melhora a correção do código e a produtividade dos desenvolvedores
Das lições à prática
- Não é necessário descartar nem reescrever todo o código legado sem segurança de memória
- Melhorar a interoperabilidade acelera a migração para linguagens com segurança de memória
- Desenvolvimento de ferramentas para melhorar a interoperabilidade entre Rust e C++, e entre Rust e Kotlin
Papel das gerações anteriores
- Uso seletivo de mitigação e detecção proativas
- À medida que o código migra para código com segurança de memória, a necessidade de mitigação e detecção diminui
Conclusão
- Usar linguagens com segurança de memória em código novo faz as vulnerabilidades diminuírem exponencialmente
- Mais de 6 anos de resultados consistentes no Android comprovam a eficácia dessa abordagem
Resumo do GN⁺
- Migrar para linguagens com segurança de memória é importante para reduzir vulnerabilidades de segurança de memória
- O caso da equipe do Android mostra uma grande redução nas vulnerabilidades de segurança de memória
- Melhorar a interoperabilidade, em vez de reescrever completamente o código existente, é uma abordagem prática
- Usar linguagens seguras em memória, como Rust, pode aumentar ao mesmo tempo a segurança e a produtividade
1 comentários
Comentários do Hacker News
Texto interessante. O ponto central é que não é preciso reescrever o mundo inteiro.
Só migrar o desenvolvimento novo para linguagens com segurança de memória já permite melhorias significativas, e é muito mais fácil e barato do que portar tudo para ver algum efeito.
Além disso, aumenta o valor de linguagens e ferramentas capazes de se integrar de forma robusta a código legado inseguro.
Também fico curioso se há algo como uma “curva da banheira” em códigos muito antigos. Lembro que, na época de uma vulnerabilidade grave do OpenSSL, provavelmente o Heartbleed, muita gente olhou o código e ficou horrorizada.
Ainda assim, é impressionante que, de 2019 a 2023, apenas adicionar código novo em linguagens com segurança de memória — provavelmente em sua maior parte Rust e um pouco de Kotlin — sem reescrever o existente tenha reduzido os problemas em quase 60%. Também fico curioso sobre por que houve um período de estagnação entre 2021 e 2023.
Usar uma extrapolação dos números de 2024 deixa transparecer a intenção de mostrar um número favorável; teria sido mais útil analisar e explicar por que houve aumento de 2022 para 2023.
Também é uma pena faltar a informação de quanto do novo código com segurança de memória foi escrito em cada linguagem. Parece que usam tanto Rust quanto Kotlin, mas fico curioso se Rust é 95% ou 50%, e, se a participação de Kotlin for grande, em que áreas ele é usado em vez de Rust.
Os gráficos do texto se destacam pela clareza e concisão. Eles mostram bem como uma seleção cuidadosa dos dados e da rotulagem consegue incorporar naturalmente, em meio à prosa, a ideia pretendida.
A conclusão do fato de que as vulnerabilidades diminuem exponencialmente é que devemos focar em código puramente novo. Gastar recursos em um enorme projeto indiscriminado de “reescrever em Rust” é ineficiente até mesmo para o objetivo de maximizar a segurança de memória.
É bastante conveniente que a estratégia mais fácil — e a recomendada por especialistas pragmáticos em Rust — também seja, segundo os dados, a melhor estratégia para minimizar vulnerabilidades de memória.
O trecho “a equipe do Android observou que a taxa de rollback de mudanças em Rust é menos da metade da de C++” é surpreendente.
“As vulnerabilidades diminuem exponencialmente. Elas têm meia-vida. [...] Um grande estudo sobre o tempo de vida de vulnerabilidades apresentado no Usenix Security em 2022 também confirmou esse fenômeno. Os pesquisadores descobriram que a grande maioria das vulnerabilidades está em código novo ou recentemente modificado.”
Sendo assim, talvez seja melhor parar de adicionar novos recursos que não sejam estritamente necessários para segurança. O Windows LTSC provavelmente deve ser a versão mais segura do Windows.
Mas vulnerabilidades que ainda não foram exploradas não têm esse mecanismo de redução. Elas não geram reclamações de usuários nem relatórios de bugs; simplesmente permanecem ali até que um adversário com recursos e motivação suficientes as encontre e explore.
Hoje há mais adversários nesse nível do que antes.
Claro que isso não significa que todos os bugs estejam apenas no código recente. Todos já devem ter visto bugs que passaram anos sem ser detectados. Para esses bugs, a pergunta deve ser por que os testes não os pegaram.
Portanto, os testes devem se concentrar no código alterado recentemente. Em especial, testes de mutação podem ser aplicados de forma muito concentrada ao código alterado ou ao código fortemente acoplado a ele, reduzindo bastante o custo desse tipo de teste.
O Google tinha um sistema que usava testes de mutação dessa maneira junto com code review.
É claro que a praticidade disso varia muito de caso para caso, mas deveria ser uma prática mais comum do que é hoje.
As versões de ponta têm muitas vulnerabilidades novas. Em geral, a versão mais antiga ainda suportada costuma ser a mais segura.
Claro que há exceções quando os recursos de uma versão nova agregam valor, mas, de modo geral, acho melhor evitar versões terminadas em “.0”.
Há uma correlação entre código novo e vulnerabilidades de memória. O post do blog também apresenta uma possível explicação: as vulnerabilidades têm uma meia-vida que diminui rapidamente. Mas não entendo por que isso é apresentado como uma relação causal entre os dois fatores
Há várias explicações plausíveis para essa correlação. Código novo muitas vezes está associado a novos recursos, e as pessoas se concentram em encontrar vulnerabilidades em recursos novos. Além disso, código antigo já passou por mais uso real e pode ter exercitado mais condições de contorno onde vulnerabilidades de memória estavam escondidas
Não me sinto confortável em dizer que código novo causa vulnerabilidades de memória e que vulnerabilidades têm uma meia-vida que diminui rapidamente. Em números brutos, talvez seja verdade, mas se pensarmos em vulnerabilidades open source de alto impacto como Heartbleed ou em bugs de invalidação de cache de CPU, isso não parece correto do ponto de vista do impacto
Minha empresa atual é um exemplo disso. O código inicial foi escrito pelos fundadores, e parte do código novo é escrita por contratados com prazos apertados
Se a conclusão de que “por exemplo, usando a vida média das vulnerabilidades, código com 5 anos tem densidade de vulnerabilidades 3,4 vezes menor que código novo; usando as vidas observadas em Android e Chromium, 7,4 vezes menor” estiver correta, isso quer dizer que posso escrever código C cheio de defeitos e deixá-lo parado offline por 5 anos para ele ficar seguro?
Este estudo tem dados importantes e provavelmente há verdade por baixo do que foi compartilhado, mas a confiança com pouca base e a extrapolação excessiva incomodam muito
“Aumento de produtividade: a codificação segura desloca a descoberta de bugs mais para a esquerda, antes do check-in do código, aumentando a correção do código e a produtividade dos desenvolvedores. Essa mudança aparece em métricas de taxa de rollback, como reversões emergenciais de código causadas por bugs inesperados”
“A equipe do Android observou que a taxa de rollback de mudanças em Rust é menos da metade da de C++”
Venho escrevendo código de produção em larga escala em várias linguagens há 20 anos, mas quando descobri Rust em 2016, soube que essa era a linguagem em que eu iria mergulhar. Comprei o livro do Klabnik e da Carol no mesmo dia, e ainda tenho a cópia em papel
Sinceramente, reacendeu meu amor por programação
O texto fala em “linguagens com segurança de memória (MSL)” no plural, mas a única linguagem que ele explicita como alvo da migração e cujo suporte à interoperabilidade está sendo melhorado é Rust
Kotlin também é mencionado no contexto de melhorar a interoperabilidade Rust<>Kotlin, e tem algum grau de recursos de segurança de memória, mas não sei se está no mesmo nível de Rust. Fico curioso se Google usa só essas duas ou se está se referindo também a outras linguagens
Porque, no fundo, a raiz do problema é se a linguagem é segura por padrão ou não. A expressão tenta focar na causa raiz, em vez de apontar ou recomendar uma linguagem específica
No caso do Android, que é o tema deste texto, não conheço bem tentativas de migrar para linguagens com segurança de memória além dessas duas. Não acompanho todo o desenvolvimento do Android, mas leio bastante textos desse tipo, e não me lembro de terem tratado de algo além de Rust ou Kotlin
O ponto central é não ficar exposto a bugs de segurança de memória no seu próprio código. Se não for estritamente necessário, talvez seja melhor escolher uma linguagem que não tenha o gerenciamento manual de memória ao estilo Rust
O Google também publicou sua visão sobre segurança de memória em https://security.googleblog.com/2024/03/secure-by-design-goo..., abordando também linguagens com segurança de memória em uso, como Java, Go e Rust
No Google como um todo, Go é usado em parte do software de sistema, mas nunca vi ser usado no Android
Se a vida das vulnerabilidades segue uma distribuição exponencial, a lógica é que focar em padrões seguros por padrão, como segurança de memória em código novo, tem valor desproporcional tanto teoricamente quanto nos dados de 6 anos da base de código do Android
Surpreendente. É a primeira vez que vejo esse tipo de argumento ser usado para embasar guardrails seguros que puxam a segurança para a esquerda, e é excelente. É especialmente útil em grandes bases de código legadas, onde alguém poderia dizer: “por que fazer isso se uma base legada de 100 milhões de linhas em C++ não vai se beneficiar da segurança de memória?”
Também parece significar que até detecção leve de vulnerabilidades pode trazer ganhos desproporcionalmente grandes. E isso considerando apenas código novo e novas dependências, não o backlog
A conclusão tirada aqui é um pouco incômoda. Ela não aborda uma objeção óbvia: talvez estejamos encontrando menos vulnerabilidades simplesmente porque examinamos código antigo com menos afinco
É muito mais comum olhar logs de commits recentes do que uma biblioteca que não muda há 20 anos
O texto não traz uma teoria para explicar por que código antigo tem menos bugs, mas, para mim, a explicação é simples: eles já foram encontrados
Se assumirmos que qualquer código tem um número fixo de bugs desconhecidos por 1.000 linhas, com o tempo se acumulam as vezes em que ele é executado em produção com entradas variadas, aumentando a chance de descoberta. Com correções e revisões de código nesse processo, dá para esperar que ele melhore, em média
Por isso, com o tempo, o número de bugs por 1.000 linhas no código existente diminui. Ele passou por validação no mundo real
Como o texto diz, se continuarmos introduzindo novos bugs no mesmo ritmo, não há progresso; mas, se linguagens com segurança de memória fizerem com que novos recursos introduzam menos bugs, o número total de bugs deve cair com o tempo
Commits são usados apenas para atribuição. Se uma biblioteca antiga que não mudou em 20 anos passou por 20 anos de fuzzing e inspeção manual de código, é bem provável que seja bastante robusta
Por exemplo, nos últimos anos os engenheiros podem ter se concentrado em reescrever as partes mais arriscadas em linguagens com segurança de memória, deixando o código antigo de menor risco menos alterado
Ou mudanças de processo ou de equipe podem ter levado a um aumento de defeitos
Ainda assim, a explicação de que cada bug tem uma probabilidade de ser encontrado por unidade de tempo, e que os defeitos restantes diminuem com o tempo, parece plausível. Se os mantenedores corrigem mais vulnerabilidades do que introduzem, é provável que o código antigo tenha menos vulnerabilidades e que as restantes sejam mais difíceis de encontrar
Fico curioso para saber como essa lógica se aplica a Mac e Windows, já que a maior parte do código novo do Mac é escrita em Swift, com segurança de memória, enquanto o Windows ainda usa principalmente C ou C++
Não encontrei números sobre o uso de linguagens nas versões recentes do Windows, mas a Microsoft está usando Rust tanto em novos desenvolvimentos quanto na reescrita de funcionalidades existentes [1] [2]
[0] Veja a seção “Evolution of the programming languages” https://blog.timac.org/2023/1128-state-of-appkit-catalyst-sw...
[1] https://www.theregister.com/2023/04/27/microsoft_windows_rus...
[2] https://www.theregister.com/2024/01/31/microsoft_seeks_rust_...
Pensando aqui no estágio final, quanto mais raras as vulnerabilidades se tornam, mais valiosas elas ficam. As vulnerabilidades restantes serão cuidadosamente acumuladas por atores estatais e usadas com parcimônia contra alvos de alto valor
Se este post descreve a 4ª geração, a 5ª geração pode ser algo parecido com o Lockdown Mode do iOS: permitir que usuários preocupados com segurança marquem uma opção para aumentar a segurança aceitando uma perda de desempenho
A opção ideal detectaria e capturaria ataques por meio de algo como virtualização, depois os enviaria para análise da equipe de segurança. Isso cria um fator de dissuasão para o atacante. Ele não vai querer queimar uma vulnerabilidade rara numa situação em que o usuário talvez tenha ativado essa opção de segurança, e muitos alvos de alto valor a ativariam
É imunidade de rebanho para vulnerabilidades de software, não para patógenos biológicos
Usuários com alta consciência de segurança provavelmente também têm alta consciência de privacidade. Portanto, em vez de transmitir silenciosamente toda a atividade do usuário, o sistema deveria mostrar uma notificação quando um ataque fosse detectado. Alguns KB de atividade de rede anômala podem ser suficientes para a equipe de segurança reconstruir o ataque, e o compartilhamento poderia exigir aprovação do usuário antes de acontecer