AES GCM - Reuse IVAES GCM (Galois Counter Mode) is a stream cipher mode for AES. It is based on the CTR mode, but converts to a stream cipher. This provides low latency in the encryption/decryption process, and is fast to process. Along with this it integrates AEAD mode for authentication. But as GCM is a stream cipher mode it is open to a reuse IV attack. With this, the IV (Initialization Vector) of the cipher is the same for two cipher messages. We can then XOR to the two cipher streams together to reveal the cipher stream key (\(K\)). We can then reveal the plaintext by XOR-ing any cipher stream with \(K\). |
With AES GCM, we take a secret key value and a salt value (an IV - Initialisation Vector) and generate a pseudo infinite keystream. Our plaintext is then simply XOR-ed with the keystream to produce our ciphertext:
The salt value should then always be random, as a fixed salt value will always produce the same keystream for the same plaintext, and where we can reveal the keystream by XOR-ing cipher streams, and eventually revealing the plaintext. In the case of the key wrapping, the plaintext is an encryption key, and thus the encryption key used by the TEE will be revealed.
If we reuse IVs, Eve will be able to XOR cipher streams together and reveal the keystream (K). From there she can decrypt every cipher stream, but simply XOR-ing the cipher stream with K.
Coding
AES GCM (Galois Counter Mode) is a stream cipher mode for AES. It is based on the CTR mode but converts to a stream cipher. This provides low latency in the encryption/decryption process and is fast to process. Along with this, it integrates AEAD mode for authentication. But as GCM is a stream cipher mode it is open to a reuse IV attack. With this, the IV (Initialization Vector) of the cipher is the same for two cipher messages. We can then XOR to the two cipher streams together to reveal the cipher stream key (K). We can then reveal the plaintext by XOR-ing any cipher stream with K.
So, let's try some code to do this. In this case, I will use Golang to show the basic principles of the method. I will use a static key in this case (as this would not change within the TEE) of "0123456789ABCDEF" (16 bytes - 128-bit key), and a static nonce of "0123456789AB" (12 bytes - 96 bits):
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 := make([]byte, 12) // 96 bits for nonce/IV 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) }
When we run with “hello” and “Hello” we get:
Message 1: hello Message 2: Hello Cipher 1: 7fcbe7378c2b87a5dfb2803d4fcaca8d5cde86dbfa Cipher 2: 5fcbe7378cf8c68b82a2b8d705354e8d6c0502cef2 Key: 30313233343536373839414243444546 Nonce: 303132333435363738394142 XOR: 2000000000d3412e5d1038ea4aff840030db841508 Decrypted: hello Decrypted: Hello
If we try “hello” and “Cello”, we can see that there’s a variation in the first byte of the cipher stream:
Message 1: hello Message 2: Cello Cipher 1: 7fcbe7378c2b87a5dfb2803d4fcaca8d5cde86dbfa Cipher 2: 54cbe7378c5638db82df34a46172abed62b887aa48 Key: 30313233343536373839414243444546 Nonce: 303132333435363738394142 XOR: 2b000000007dbf7e5d6db4992eb861603e660171b2 Decrypted: hello Decrypted: Cello
Within a TEE, a user program to request their own IV, which meant that a cracker would continually request the same IV, and then break the cipher, and reveal the encryption key used. This is because the TEE (Trusted Execution Environment) uses key wrapping to encrypt the encryption key with AES GCM, so a cracker can request these wrapped keys, but with their own IV. This then reveals the key that the TEE is using. This vulnerability was recently exploited in [1].
The following is a share of the code:
Reference
[1] Shakevsky, A., Ronen, E., & Wool, A. (2022). Trust Dies in Darkness: Shedding Light on Samsung’s TrustZone Keymaster Design. Cryptology ePrint Archive.