Today, we will learn about the CBC block cipher mode, how it works, and the bitflipping attack. This post is related to https://cryptopals.com/sets/2/challenges/16.

CBC block cipher mode

Cipher Block Chaining (CBC) is a block cipher mode of operation. Data is encrypted in blocks of fixed size and with the same key. Also, some bytes are added when the plaintext doesn’t have a length that is a multiple of the block size, known as padding.

Let p be the plaintext, c the ciphertext, k the key, IV the initialization vector (later explained), ^ the XOR operation, E any encryption algorithm and D its decryption counterpart. Assume that the ciphertext can be divided into blocks of length equal to l, without any leftover bytes. For the first block, the encrypted version is computed as c[0] = E(IV ^ p[0], k). For all the other blocks, we have that c[i] = E(p[i] ^ c[i - 1], k). From these equations, we can derive the decryption. We have that p[i] = D(c[i], k) ^ c[i - 1] and p[0] = D(c[0], k) ^ IV.

The IV is a unique, non-repeating, random binary sequence that we use to randomize the encryption. As opposed to ECB, identical plain blocks won’t result in identical encrypted blocks. Furthermore, the same message will return different encrypted messages.

It still has some issues. Encryption can’t be parallelized, even though decryption can be, and it’s vulnerable to attacks.

CBC bitflipping attack

Setup

For this exercise, we assume that we have access to two functions.

The first function takes some userdata as input and builds the following string comment1=cooking%20MCs;userdata={};comment2=%20like%20a%20pound%20of%20bacon. It then encodes and encrypts the data.

pub fn encrypt_user_data(data: &str) -> Vec<u8> {
    let data = format!(
        "comment1=cooking%20MCs;userdata={};comment2=%20like%20a%20pound%20of%20bacon",
        data
    );
    let url_encoded_data = data
        .replace('=', "%3D")
        .replace(';', "%3B")
        .replace(' ', "%20");

    encrypt_aes_128_cbc(url_encoded_data.as_bytes(), &KEY, Some(IV.to_vec()))
}

The second function decrypts the data and checks if the user is admin. However, the data is not decoded. That means that = won’t appear. Therefore, it’s impossible to get admin=true by inputting the data into the first function. We need to break the cryptography.

pub fn is_admin(data: &[u8]) -> bool {
    let plain = decrypt_aes_128_cbc(data, &KEY, Some(IV.to_vec()));
    plain.contains(";admin=true;")
}

Attack

The goal of the exercise is to get the is_admin function to return true.

Get block size

As in a previous ECB blog, we need to detect the block size.

let encrypt_fn = |data: &[u8]| encrypt_user_data(&String::from_utf8_lossy(data));
let (block_size, _) = compute_block_size_and_padding_length(encrypt_fn);

Craft the input to the oracle

We need to compute the prefix length, including the blocks it takes, and how many bytes of the last prefix block are dedicated to the prefix.

let full_prefix_length = prefix_length(encrypt_fn, block_size);
let block_prefix_length = block_size - (full_prefix_length % block_size);

Now, we can craft the input. The trick here is to make sure that our ;admin=true; string ends up all together on one block. Not spilled over two different blocks. With the prefix length, we can make sure that happens. One last detail, ; and = are encoded. On decryption, they won’t be decoded. We are going to use some placeholder characters instead. In this case, X for ; and Y for =. We will see why in the next section.

let mut crafted_input = vec!["a"; block_prefix_length].join("");
crafted_input.push_str("XadminYtrueX");

Flip ciphertext bits

We send the crafted input to the oracle, we get the ciphertext, and it’s time to get into the offense!

In CBC, encrypting a block requires doing XOR between the previous encrypted block and the current plaintext block. Any bit we modify in the ciphertext will modify the final value that gets decrypted because of the XOR operation. We can take advantage of that.

Imagine we have the two following plaintext blocks (I’m using shorter block size to simplify things):

|            p1           |            p2           |
| 00 01 02 03 04 05 06 07 | 08 09 10 11 12 13 14 15 |
| a  a  a  a  a  a  a  a  | a  a  a  a  a  a  a  X  |

Where X is the value we want to modify. We want to get ;. And the final encrypted form is:

|            c1           |            c2           |
| 00 01 02 03 04 05 06 07 | 08 09 10 11 12 13 14 15 |
| b  b  b  b  b  b  b  b  | b  b  b  b  b  b  b  Z  |

Assume the decrypted value of Z is Y. Thus, Y is the result of X ^ b.

We know that on decryption, c2 will be decrypted then XORed with c1. The idea then, is to modify the last byte of c1 in such a way that when XORing, X will turn into ;. In that case, we can do c1[7] = b ^ X ^ ;. That way, during decryption, when it XORs D(c2) with c1, the following operation is performed
Y ^ c1[7] = Y ^ (b ^ X ^ ;) = (X ^ b) ^ (b ^ X ^ ;) = ;.

That’s the attack. That’s all we are doing. We take the byte from the previous block at the same position as the byte that we want to attack, and XOR it with both the plaintext character and the character we want to obtain.

In this case, we applied that theory to the three characters we wanted to modify.

let ciphertext = encrypt_user_data(&crafted_input);

let mut crafted_ciphertext = ciphertext;
crafted_ciphertext[2 * block_size] ^= b';' ^ b'X';
crafted_ciphertext[2 * block_size + 6] ^= b'=' ^ b'Y';
crafted_ciphertext[2 * block_size + 11] ^= b';' ^ b'X';

With that, we accomplished the task.

Code

You can check the whole implementation https://github.com/danielorihuela/cryptopals/blob/main/src/set2/challenge16.rs.

Conclusion

The CBC cipher is simple but insecure. It’s an improvement over ECB. It doesn’t leak patterns. However, it’s still vulnerable to attacks.