Lição 1 em programação segura: não reutilize seus IVs

May 10 2023
Escrevi um artigo sobre a vulnerabilidade recente da Samsung [aqui] e um comentário dizia … “é um bug antigo, a reutilização de IV (vetores de inicialização) parece um problema muito básico”. Diante disso, o comentário talvez não entre em detalhes suficientes, então tentarei explicar o "bug" e espero mostrar que é uma codificação terrivelmente ruim ... quase negligente em termos de proteção e pode até ser visto como uma porta dos fundos intencional.
Foto de Med Badr Chemmaoui no Unsplash

Escrevi um artigo sobre a recente vulnerabilidade da Samsung [ aqui ], e um comentário dizia … “é um bug antigo, a reutilização de IV (vetores de inicialização) parece um problema muito básico”. Diante disso, o comentário talvez não entre em detalhes suficientes, então tentarei explicar o "bug" e espero mostrar que é uma codificação terrivelmente ruim ... quase negligente em termos de proteção e pode até ser visto como um backdoor intencional .

E para um “problema muito básico”, talvez devesse ser “codificação extremamente ruim”, e esse “bug” nunca, jamais deveria ser visto em ambientes confiáveis. Mostra um desconhecimento quase total de como funciona a criptografia, com uma vulnerabilidade de iniciante. O papel está aqui [1]:

Na verdade, é como o WEP de novo, e onde o método WEP Wifi tinha um pequeno IV (Vetor de Inicialização), e quando foi lançado, era possível apenas XOR cifrar fluxos e descobrir o texto simples. O programa adormecido pode quebrar qualquer ponto de acesso da Cisco em menos de um dia. Felizmente agora usamos o WPA-2, e que não tem o reaproveitamento do IV.

Espero mostrar que devemos nos preocupar se um código como esse chegar perto do dispositivo de um usuário. Na verdade, se alguma vez houve uma porta dos fundos em um telefone celular, poderia ser esta.

Se você quiser ler sobre o “bug”, tente aqui:

Bug criptográfico em dispositivos Samsung Galaxy: quebrando ambientes de execução confiáveis ​​(TEEs)

Um “erro” ruim

Agora, vou explicar o quão ruim é esse “bug”. Se você gosta de segurança cibernética, deve saber que o AES GCM é uma cifra de fluxo. Com isso, pegamos um valor de chave secreta e um valor de sal (um IV — Vetor de inicialização) e geramos um fluxo de chaves pseudo-infinito. Nosso texto simples é simplesmente XOR-ed com o fluxo de chaves para produzir nosso texto cifrado:

O valor salt deve ser sempre aleatório, já que um valor salt fixo sempre produzirá o mesmo fluxo de chaves para o mesmo texto sem formatação, e onde podemos revelar o fluxo de chaves por meio de XOR-ing cipher streams e, eventualmente, revelando o texto sem formatação. No caso do agrupamento de chaves, o texto simples é uma chave de criptografia e, portanto, a chave de criptografia usada pelo TEE será revelada.

Se reutilizarmos IVs, Eve será capaz de codificar fluxos XOR juntos e revelar o fluxo de chaves (K). A partir daí, ela pode descriptografar todos os fluxos de cifras, mas simplesmente aplicar XOR no fluxo de cifras com K.

Codificação

AES GCM (Galois Counter Mode) é um modo de cifra de fluxo para AES. Ele é baseado no modo CTR, mas é convertido em uma cifra de fluxo. Isso fornece baixa latência no processo de criptografia/descriptografia e é rápido de processar. Junto com isso, integra o modo AEAD para autenticação. Mas, como o GCM é um modo de cifra de fluxo, ele está aberto a um ataque IV de reutilização . Com isso, o IV (Vetor de Inicialização) da cifra é o mesmo para duas mensagens cifradas. Podemos então aplicar XOR aos dois fluxos de cifras juntos para revelar a chave do fluxo de cifras ( K ). Podemos então revelar o texto simples fazendo XOR em qualquer fluxo de cifra com K .

Então, vamos tentar algum código para fazer isso. Neste caso, usarei o Golang para mostrar os princípios básicos do método. Vou usar uma chave estática neste caso (já que isso não mudaria dentro do TEE) de “0123456789ABCDEF” (16 bytes — chave de 128 bits) e um nonce estático de “0123456789AB” (12 bytes — 96 bits) [ aqui ]:

package main
import (
 "crypto/aes"
 "crypto/cipher"
 "fmt"
 "os"
)
func xor(a, b []byte, length int) []byte {
 c := make([]byte, len(a))
 for i := 0; i < length; i++ {
  c[i] = a[i] ^ b[i]
 }
 return (c)
}
func main() {
 nonce := []byte("0123456789AB")
 key := []byte("0123456789ABCDEF")
 block, err := aes.NewCipher(key)
 if err != nil {
  panic(err.Error())
 }
 msg1 := "hello"
 msg2 := "Hello"
 argCount := len(os.Args[1:])
 if argCount > 0 {
  msg1 = (os.Args[1])
 }
 if argCount > 1 {
  msg2 = (os.Args[2])
 }
 plaintext1 := []byte(msg1)
 plaintext2 := []byte(msg2)
 aesgcm, err := cipher.NewGCM(block)
 if err != nil {
  panic(err.Error())
 }
 ciphertext1 := aesgcm.Seal(nil, nonce, plaintext1, nil)
 ciphertext2 := aesgcm.Seal(nil, nonce, plaintext2, nil)
 xor_length := len(ciphertext1)
 if len(ciphertext1) > len(ciphertext2) {
  xor_length = len(ciphertext2)
 }
 ciphertext_res := xor(ciphertext1, ciphertext2, xor_length)
 fmt.Printf("Message 1:\t%s\n", msg1)
 fmt.Printf("Message 2:\t%s\n", msg2)
 fmt.Printf("Cipher 1:\t%x\n", ciphertext1)
 fmt.Printf("Cipher 2:\t%x\n", ciphertext2)
 fmt.Printf("Key:\t\t%x\n", key)
 fmt.Printf("Nonce:\t\t%x\n", nonce)
 fmt.Printf("XOR:\t\t%x\n", ciphertext_res)
 plain1, _ := aesgcm.Open(nil, nonce, ciphertext1, nil)
 plain2, _ := aesgcm.Open(nil, nonce, ciphertext2, nil)
 fmt.Printf("Decrypted:\t%s\n", plain1)
 fmt.Printf("Decrypted:\t%s\n", plain2)
}

Message 1:	hello
Message 2:	Hello
Cipher 1:	7fcbe7378c2b87a5dfb2803d4fcaca8d5cde86dbfa
Cipher 2:	5fcbe7378cf8c68b82a2b8d705354e8d6c0502cef2
Key:		30313233343536373839414243444546
Nonce:		303132333435363738394142
XOR:		2000000000d3412e5d1038ea4aff840030db841508
Decrypted:	hello
Decrypted:	Hello

Message 1: hello
Message 2: Cello
Cipher 1: 7fcbe7378c2b87a5dfb2803d4fcaca8d5cde86dbfa
Cipher 2: 54cbe7378c5638db82df34a46172abed62b887aa48
Key:  30313233343536373839414243444546
Nonce:  303132333435363738394142
XOR:  2b000000007dbf7e5d6db4992eb861603e660171b2
Decrypted: hello
Decrypted: Cello

Conclusão

Essa é uma codificação extremamente ruim e eu não esperaria esse nível de implementação de um recém-formado. Se uma equipe de desenvolvimento que cria código dentro de um TEE não entende um ataque IV de reutilização, ela precisa fazer um curso de treinamento de codificação segura antes de tocar em qualquer código mais confiável. Se este foi um backdoor intencional, é uma outra história. Espero que tenha sido apenas um bug, mas realmente precisamos melhorar nossos conhecimentos na criação de TEEs, pois eles também são executados em sistemas baseados em nuvem.

Referência

[1] Shakevsky, A., Ronen, E., & Wool, A. (2022). A confiança morre na escuridão: lançando luz sobre o design de teclado TrustZone da Samsung. Arquivo ePrint de Criptologia .