Attacking AES CBC non-existent integrity protection

Modes of operation, a very short recap

The Advanced Encryption Standard (AES) is very likely the most prominent block cipher these days. Since most messages which should be encrypted are larger than 128 bits (which is the block size of AES), a so called mode of operation is used. A mode of operation describes how to repeatedly apply a cipher’s single-block operation to securely transform amounts of data larger than a block. There are several different modes of operation. Encryption using the Electronic Codebook (ECB) mode is often not a good idea. Word has got around, therefore nowadays you seldom see this mode in systems and applications anymore. In case you don’t know the problems of the ECB mode, I’ll refer you to the Wikipedia article on ECB mode to have a short read.

Cipher Block Chaining (CBC)

The most prominent alternative to the ECB mode is the Cipher Block Chaining (CBC) mode. This mode brings its own set of problems, of which we are going to have a look at one of them here. An often-overseen fact is, that the AES encryption in the CBC mode has no built in integrity check. A widespread assumption is, that if an encrypted message decrypts without an error, it was encrypted by the person with the key. That is however not correct! In fact, this is actually a pretty common attack vector (keywords: Padding Oracle Attack, BEAST, Lucky Thirteen).

You might have read or heard that already somewhere, and maybe already forgotten again. That’s why I want to demonstrate this with some short and easy to remember examples.

Example

Warning: DO NOT USE ANY CODE FROM HERE IN A PRODUCTION ENVIRONMENT! It has severe security flaws!

Encryption

Let’s assume we are in a Java environment, could also be an Android smartphone, and want to encrypt some data and save it. Something like this:

{"balance": 2000, "id": 12345}

The following code is a typical example you could find for doing so in Java. It takes our message from above and encrypt it with a key (normally the key is and should not be hardcoded like here). For the encryption an AES cipher in CBC mode is used. The 16 bytes initialization vector (IV) will be randomly generated. Since the secret message is not a multiple of 128 bits, the block size of AES, PKCS5 padding will be used to pad the plaintext up to 2 * 128 bit. The initialization vector will then be saved together with the encrypted plaintext.

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.SecureRandom;
import java.nio.ByteBuffer;
import java.nio.file.*;



public class EncryptToFile {
    public static byte[] encrypt(String key, byte[] iv, String message) {
        try {
            IvParameterSpec ivSpec = new IvParameterSpec(iv);
            SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes("UTF-8"), "AES");

            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
            cipher.init(Cipher.ENCRYPT_MODE, skeySpec, ivSpec);
            byte[] ciphertext = cipher.doFinal(message.getBytes("UTF-8"));
            
            // return byte array with [IV + ciphertext]
            ByteBuffer ivAndCiphertext = ByteBuffer.allocate(iv.length + ciphertext.length);
            ivAndCiphertext.put(iv);
            ivAndCiphertext.put(ciphertext);
            return ivAndCiphertext.array();
            
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    public static void main(String[] args) {
        String key     = "MyVerySecretKey1";                      // 128 bit key (16 byte)
        String message = "{\"balance\": 2000, \"id\": 12345}";    // secret (soon to be encrypted) message
        SecureRandom rng = new SecureRandom();
        byte[] iv = new byte[16];
        rng.nextBytes(iv);                                        // 16 bytes IV
        
        try {
            // encrypt the message
            byte[] ivAndCiphertext = encrypt(key, iv, message);
            
            // print plaintext and iv+ciphertext
            System.out.println("Plaintext: " + message);
            System.out.println("IV + Encrypted: " + new String(ivAndCiphertext, "UTF-8"));
            
            // save iv+ciphertext to file
            Files.write(Paths.get(args[0]), ivAndCiphertext);
            
        } catch (Exception e) {
                e.printStackTrace();
        }
    }
}
root@kali:~# javac EncryptToFile.java 

The encryption process follows the scheme below. The first plaintext block is XORed with the IV, and then encrypted. For the next round, this first ciphertext block is used to XOR the second plaintext block before it is then encrypted:

Executing the script with a parameter where the output should be saved to:

The resulting output opened in a hex editor will look like this. Keep in mind, that due to the random IV, the output might look different when you execute the script:

The first 16 bytes are the randomly generated IV. Since the IV isn?t a secret, and is necessary for decryption, it will be saved along with the ciphertext. The following 32 bytes are the 30 bytes message and 2 bytes padding, in encrypted form.

Decryption

For the decryption, we take our IV and the ciphertext which we previously created and decrypt the message with the help of our key.

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.ByteBuffer;
import java.nio.file.*;



public class DecryptFromFile {   
    public static String decrypt(String key, byte[] ivAndCiphertext) {
        try {
            ByteBuffer byteBuffer = ByteBuffer.wrap(ivAndCiphertext);
            // get the 16 bytes iv
            byte[] iv = new byte[16];
            byteBuffer.get(iv);
            // get the ciphertext
            byte[] ciphertext = new byte[byteBuffer.remaining()];
            byteBuffer.get(ciphertext);
            
            IvParameterSpec ivSpec = new IvParameterSpec(iv);
            SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes("UTF-8"), "AES");
            
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
            cipher.init(Cipher.DECRYPT_MODE, skeySpec, ivSpec);            
            byte[] plaintext = cipher.doFinal(ciphertext);
            return new String(plaintext, "UTF-8");
            
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
    

    public static void main(String[] args) {
        String key = "MyVerySecretKey1";      // 128 bit key
        
        try {
            // read input file
            byte[] ivAndCiphertext = Files.readAllBytes(Paths.get(args[0]));
            
            // decrypt and print message
            String plaintext = decrypt(key, ivAndCiphertext);
            System.out.println("Decrypted: " + plaintext);
            
        } catch (Exception e) {
                e.printStackTrace();
        }
    }
}
root@kali:~# javac DecryptFromFile.java 

The decryption process follows the scheme below. The first ciphertext block is decrypted, and then XORed with the IV. For the second round, the first ciphertext block is used to XOR the decrypted second ciphertext block:

Let’s execute the script with a parameter where we previously saved our output to:

Our encrypted message decrypts again without any errors thrown by Java, the added PKCS5 padding is automatically removed, so everything seems to be fine.

Messing with the IV

Let’s have a closer look at the decryption process. Especially at the first round where the IV is used to XOR the first decrypted ciphertext block. Looking from an adversary perspective we might be able/want to change the encrypted output in the file. The easiest point to do this would be the IV as it would only affect the first ciphertext block and have no “fallout” to other parts of the decryption process. Changing the ciphertext itself is also possible, however one change in a ciphertext block can trigger unpredictable changes in two different plaintext blocks. So let’s work with the IV instead:

Changing the 13. bit in the IV would therefore result in a modification of the 13. bit of the first plaintext block. Let’s modify the output file with the help of a hex editor and change the 13. value from d7 to df:

Finally let’s execute our decryption script against the newly modified file:

And it decrypts without any error thrown by the script. Can you spot the difference compared to the previous output?

The hard truth is, encryption does not automatically protect against data tampering. What we want are additional features like integrity and authenticity to prevent illegitimate actors from modifying our message and proving that the message was in fact generated by a particular party.

Authenticated Encryption

Wouldn’t it be great if there were modes of operation which handle all that stuff for us? Fortunately, there are! The most prominent of those is the Galois/Counter Mode (GCM). It simultaneously provides confidentiality, integrity and authenticity. In the following I’ll give you two short Java programs, one for encryption, the second for decryption, this time using AES GCM instead of CBC. Be aware that this programs are very basic examples, e.g. they lack of Associated Data, and therefore do not actually provide authenticity. Check out what happens when you modify the saved output this time.

Encryption:

import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.SecureRandom;
import java.nio.ByteBuffer;
import java.nio.file.*;



public class EncryptToFile {
    public static byte[] encrypt(String key, byte[] iv, String message) {
        try {
            GCMParameterSpec ivSpec = new GCMParameterSpec(128, iv);
            SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes("UTF-8"), "AES");

            Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
            cipher.init(Cipher.ENCRYPT_MODE, skeySpec, ivSpec);
            byte[] ciphertext = cipher.doFinal(message.getBytes("UTF-8"));
            
            // return byte array with [IV + ciphertext]
            ByteBuffer ivAndCiphertext = ByteBuffer.allocate(iv.length + ciphertext.length);
            ivAndCiphertext.put(iv);
            ivAndCiphertext.put(ciphertext);
            return ivAndCiphertext.array();
            
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return null;
    }

    public static void main(String[] args) {
        String key           = "MyVerySecretKey1";                      // 128 bit key (16 byte)
        String secretMessage = "{\"balance\": 2000, \"id\": 12345}";    // secret (soon to be encrypted) message
        SecureRandom rng = new SecureRandom();
        byte[] iv = new byte[12];
        rng.nextBytes(iv);                                              // 12 (!) bytes IV
        
        try {
            // encrypt the message
            byte[] ivAndCiphertext = encrypt(key, iv, secretMessage);
            
            // print plaintext and iv+ciphertext
            System.out.println("Plaintext: " + secretMessage);
            System.out.println("Encrypted: " + new String(ivAndCiphertext, "UTF-8"));
            
            // save encrypted message to file            
            Files.write(Paths.get(args[0]), ivAndCiphertext);
            
        } catch (Exception e) {
                e.printStackTrace();
        }
    }
}

Decryption:

import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.ByteBuffer;
import java.nio.file.*;



public class DecryptFromFile {   
    public static String decrypt(String key, byte[] ivAndCiphertext) {
        try {
            ByteBuffer byteBuffer = ByteBuffer.wrap(ivAndCiphertext);
            // get the 12 bytes iv
            byte[] iv = new byte[12];
            byteBuffer.get(iv);
            // get the ciphertext
            byte[] ciphertext = new byte[byteBuffer.remaining()];
            byteBuffer.get(ciphertext);
            
            GCMParameterSpec ivSpec = new GCMParameterSpec(128, iv);
            SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes("UTF-8"), "AES");
            
            Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
            cipher.init(Cipher.DECRYPT_MODE, skeySpec, ivSpec);
            byte[] plaintext = cipher.doFinal(ciphertext);
            return new String(plaintext, "UTF-8");
            
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
    

    public static void main(String[] args) {
        String key = "MyVerySecretKey1";      // 128 bit key (16 byte)
        
        try {
            // read input file
            byte[] ivAndCiphertext = Files.readAllBytes(Paths.get(args[0]));

            // decrypt and print message
            String plaintext = decrypt(key, ivAndCiphertext);
            System.out.println("Decrypted: " + plaintext);
            
        } catch (Exception e) {
                e.printStackTrace();
        }
    }
}

Try it out! Did you see that the size of the saved file is bigger than in the CBC example? That’s because the authentication tag is appended to ciphertext during the encryption. Any changes to the IV or the ciphertext will result in an exception during the decryption: javax.crypto.AEADBadTagException: Tag mismatch!.

Final Thoughts

Cryptography goes sideways very easily. Such a small write-up is not even close enough to describe everything that can go wrong, even on a very high level of abstraction. Overall, I hope you will at least remember this one sentence: Encryption does not automatically protect against data tampering!

 

Bonus exercise:
Google for “Java AES encryption”, check the first 10 results and see if you can find any which do not have major weaknesses or are outright flawed.