Cargo-checkct: a ferramenta desenvolvida pela Ledger para proteger contra ataques de temporização agora é de código aberto!

A equipe Ledger Donjon está empolgada em apresentar o cargo-checkct, a ferramenta interna de código aberto desenvolvida para se defender contra ataques de temporização. Neste artigo, exploraremos o conceito de ataques de temporização, entenderemos por que as vulnerabilidades de temporização em bibliotecas criptográficas são tão difíceis de detectar e explicaremos o papel do cargo-checkct em enfrentar esses desafios.

Ataques de temporização

Vulnerabilidades de canal lateral, e vulnerabilidades de temporização em particular, podem permitir que um invasor extraia dados secretos – sejam dados de autenticação como PINs ou senhas, ou chaves secretas usadas em operações criptográficas – de um sistema que de outra forma seria seguro. A exploração de tais vulnerabilidades pode parecer um pouco esotérica ou teórica à primeira vista, mas é importante perceber que, de fato, levam a ataques reais, práticos e devastadores. Isso é especialmente preocupante para sistemas embarcados, que um invasor pode manipular e instrumentar para observar vazamentos mais facilmente do que poderia fazer em um sistema remoto.

Na verdade, o princípio que fundamenta os ataques de temporização é, em essência, extremamente simples, uma vez que se compreende: se um programa que manipula segredos executa mais rápido para alguns valores do segredo do que para outros, então, como invasor, medir o tempo de execução desse programa me dá informações sobre o segredo! Nem todos esses vazamentos são criados de forma igual, é claro, e algumas vulnerabilidades podem permitir apenas a recuperação de alguns bits do segredo – ainda assim, frequentemente, vazamentos de temporização podem ser explorados para recuperar completamente o segredo em apenas algumas rodadas de medição do tempo de execução do programa alvo.

Ao perceber que isso é uma ameaça séria, e não apenas um problema teórico, o próximo passo é procurar por construções que introduzam diferenças de temporização em códigos que manipulam segredos, a fim de evitá-las. Talvez a maneira mais óbvia de o tempo de execução de um programa variar dependendo do valor do segredo manipulado seja se o programa incluir ramificações condicionais dependentes do segredo, como é, por exemplo, muito tentador ao implementar uma função de comparação de PIN, que poderia se parecer com isto:

				
					pub fn verify_pin(candidate_pin_to_verify: &[u8; PIN_LEN], actual_valid_secret_pin: &[u8; PIN_LEN]) -> bool {
    for (l, r) in candidate_pin_to_verify
        .iter()
        .zip(actual_valid_secret_pin.iter())
    {
        if l != r {
            return false;
        }
    }

    true
}
				
			

Há, no entanto, outra classe muito importante de vazamentos de temporização que é menos óbvia à primeira vista e está ligada à presença de intermediários microarquiteturais entre um núcleo de processador e a memória: os caches. O propósito dos caches é, claro, acelerar as operações de carga e armazenamento na memória, armazenando em cache os dados usados recentemente. Mas se programas controlados por invasores puderem ser executados simultaneamente (ou em paralelo, se pelo menos uma camada de cache for compartilhada entre vários núcleos) com o nosso programa isolado que manipula segredos, então o tempo de execução do código do invasor se tornará dependente dos endereços de memória acessados pelo nosso programa e vice-versa, criando assim um canal de temporização observável que vaza informações sobre os segredos manipulados! Portanto, qualquer programa que manipule segredos em um alvo com caches deve ter muito cuidado para nunca acessar a memória em endereços dependentes de segredos.

Esses problemas e os ataques que eles possibilitam são amplamente conhecidos há três décadas, mas ainda afligem implementações modernas amplamente usadas. Curiosamente, a situação é em muitos aspectos semelhante à da corrupção de memória, com o artigo da phrack sobre stack smashing sendo publicado no mesmo ano que o artigo seminal de Kocher sobre ataques de temporização. Ambos os tipos de vulnerabilidades evoluíram muito além das exposições originais, é claro, com ROP, JOP e exploração de heap de um lado, e canais laterais relacionados a cache do outro (sem mencionar a execução especulativa). Mas, crucialmente, ambos os tipos de vulnerabilidades ainda são comumente encontrados em sistemas de computador dos quais dependemos para segurança e privacidade.

Muito trabalho foi feito para enfrentar a ameaça de bugs de corrupção de memória, desde o desenvolvimento de mitigação e o surgimento do fuzzing como uma ferramenta relativamente comum, até a adoção crescente de linguagens de programação seguras para memória. O mesmo é verdade para vulnerabilidades de canal lateral em bibliotecas criptográficas e sistemas embarcados, com, em particular, o desenvolvimento de muitas ferramentas para detectar ou até mesmo excluir de forma sólida vulnerabilidades de temporização. E ainda assim, nenhuma dessas ferramentas realmente viu uma ampla adoção por parte dos desenvolvedores de bibliotecas criptográficas.

A política de tempo constante (criptográfica)

Se pensar bem, existem duas maneiras conceitualmente simples de abordar o problema de verificar se uma implementação não vaza segredos por meio de canais de temporização. A maneira mais simples seria, certamente, executar o código várias vezes com dados secretos diferentes e medir seu tempo de execução, de modo que qualquer variação estatisticamente significativa possa ser interpretada como um sinal claro da presença de uma vulnerabilidade de temporização. No entanto, existem pelo menos dois problemas com essa abordagem, que limitam severamente sua aplicabilidade:

  • Primeiramente, embora essa medição de tempo seja um bom indicador para vazamentos baseados em controle de fluxo, não está muito claro como simular as influências que um invasor poderia ter no estado dos caches enquanto o programa está em execução.
  • Mas mesmo assim, o tempo de execução depende tanto do código de máquina gerado pelo compilador quanto dos detalhes microarquiteturais da implementação de hardware real, de modo que, estritamente falando, as campanhas de medição teriam que ser realizadas para cada SoC específico alvo, o que efetivamente transferiria o ônus da verificação para os usuários finais das bibliotecas criptográficas.

 

A outra abordagem geral que se poderia adotar é verificar diretamente se o código não toma decisões de controle de fluxo com base em dados secretos, nem acessa a memória em endereços dependentes de segredos, pois identificamos anteriormente esses dois padrões como fontes de vazamentos de temporização (isso é chamado de política de tempo constante (criptográfica) na literatura). Quão difícil pode ser? Bem, acontece que, embora isso seja certamente viável, um problema muito irritante é que compiladores de uso geral (como GCC, Clang/LLVM) não preservam essas duas propriedades ao compilar programas para código de máquina! E não apenas eles não preservam isso na teoria, mas também não preservam na prática, causando regularmente problemas em bibliotecas criptográficas. Felizmente, algumas formas de escrita foram encontradas que, na maioria das vezes (e atualmente), impedem o compilador de introduzir vazamentos de temporização, como usado, por exemplo, na subtle crate – mas realmente não há garantia.

Portanto, realisticamente, a única maneira sólida e portátil (de uma implementação de uma ISA para outra) de verificar a constância do tempo das bibliotecas criptográficas seria verificar se a política de tempo constante é respeitada no nível do código de máquina. Isso não é uma coisa fácil de fazer, mas, incrivelmente, é exatamente isso que o plugin checkct do Binsec permite fazer! Ele faz isso usando (uma versão elaborada de) execução simbólica e é perfeitamente sólido, sob a suposição de que as instruções em si são executadas em tempo independente de seus operandos.

Onde o cargo-checkct entra em cena

O Binsec é excelente, mas os desenvolvedores e mantenedores de bibliotecas criptográficas ainda não têm uma maneira simples e clara de monitorar o comportamento de temporização de seu código em CI, embora existam ferramentas úteis, como o dudect-bencher. Uma das principais dificuldades é que a verificação de bibliotecas criptográficas requer a escrita de um driver ou harness de verificação, que cria entradas para a função em teste antes de chamá-la, de forma semelhante ao fuzzing. Isso é menos do que ideal, pois aumenta o custo de escrever e manter código criptográfico. Mas pode ser feito e pode ser facilitado com algumas ferramentas relativamente simples que agilizam o processo.

O cargo-checkct foi desenvolvido com esse objetivo em mente, visando facilitar a verificação da constância do tempo de bibliotecas criptográficas Rust usando o binsec, e com a esperança de levar os benefícios das pesquisas mais recentes ao maior número possível de pessoas. Com o cargo-checkct, você pode gerar toda a parte de boilerplate do driver de verificação e gerar automaticamente um script binsec apropriado para executar a verificação real, em questão de algumas chamadas de linha de comando.

Como toda ferramenta, é claro, o cargo-checkct possui várias limitações, algumas herdadas do binsec e outras próprias, mas, como mostram os exemplos fornecidos no repositório, ele permite a verificação de bibliotecas criptográficas do dalek-cryptography ou RustCrypto, entre muitas outras. Estamos, portanto, muito animados em torná-lo de código aberto, com a esperança de que ele possa contribuir para a melhoria geral da segurança da criptografia em geral e dar um passo concreto na solução do problema de eliminar vazamentos de temporização na prática.

O repositório (https://github.com/Ledger-Donjon/cargo-checkct) é o melhor lugar para procurar se você estiver interessado em mais detalhes e exemplos, mas apenas para dar uma ideia de como é usar o cargo-checkct, vamos rapidamente pegar a biblioteca x25519-dalek como exemplo. Verificar se a implementação de Diffie-Hellman fornecida é de tempo constante nos alvos thumb e riscv é apenas uma questão de executar o comando cargo-checkct init, implementando um harness simples:

				
					pub fn checkct() {
    use dalek::{PublicKey, EphemeralSecret, SharedSecret};
    let mut public_key_bytes = [0u8; 32];
    PublicRng.fill_bytes(&mut public_key_bytes);

    let public = PublicKey::from(public_key_bytes);
    let ephemeral = EphemeralSecret::random_from_rng(PrivateRng);
    let shared_secret = ephemeral.diffie_hellman(&public);
    core::hint::black_box(shared_secret);
}
				
			

e finalmente executar o comando cargo-checkct run, que, além de canalizar parte da saída detalhada do binsec, crucialmente gera (nesta instância e no momento da escrita) um status tranquilizador de SEGURANÇA.

Resumo

Todas as bibliotecas criptográficas devem consistentemente produzir código de máquina de tempo constante em todas as arquiteturas que visam, sob o risco de falhas completas. Infelizmente, isso é extremamente difícil de garantir em CI, permitindo que vulnerabilidades graves apareçam regularmente em códigos críticos. O cargo-checkct está aqui para resolver esse problema, focando em particular nas bibliotecas Rust compatíveis com no_std. Experimente!

Compartilhe este artigo nas redes sociais

Veja outras categorias

Artigos relacionados

____