Attacking CBC Mode Encryption: Bit Flipping

Manipulating Decrypted Plaintext

Zhang Zeyu
4 min readMay 21, 2021


Photo by Seth Doyle on Unsplash

Recently, I participated in a CTF that included a challenge on CBC bit flipping. I learnt about an interesting technique that allows the attacker to arbitrarily change the value of the decoded plaintext in a CBC block cipher.

Cipher Block Chaining (CBC)

Block ciphers such as AES encrypt blocks of text at a time, rather than encrypting one bit at a time as in stream ciphers.

In the Cipher Block Chaining (CBC) mode of operation, each plaintext block is XORed with the previous ciphertext block before being encrypted.

Hence, if the encryption function is Eₖ, then we have the following recurrence relation:

CBC Encryption

Conversely, the decrypted result is XORed with the previous ciphertext block.

It follows that if the decryption function is Dₖ, then the decryption is:

CBC Decryption

The plaintext of each block depends not only on the corresponding block of ciphertext, but also on the previous block of ciphertext.

Bit Flipping

Suppose an attacker wishes to manipulate block i of the plaintext. He then XORs the previous ciphertext block with x:

Herein lies the problem: if the attacker has knowledge of the structure of the plaintext, x can be set such that the decrypted plaintext can be controlled.

An Example

Here’s the CTF scenario. While this is a simplified challenge scenario, it mirrors real-world applications of bit-flipping attacks. In most cases, this means bypassing filters to add otherwise disallowed characters, changing user information to elevate privileges or bypass authentication, etc.

When the attacker has prior knowledge about the structure of the plaintext, such an attack is particularly damaging.

We can input the user and passwd into the below string, which is subsequently encrypted. admin&password=goBigDawgs123 is not allowed.

msg = 'logged_username=' + user +'&password=' + passwdtry:
assert('admin&password=goBigDawgs123' not in msg)
except AssertionError:
send_msg(s, 'You cannot login as an admin from an external IP.\nYour activity has been logged. Goodbye!\n')

The ciphertext is given to us, and we are prompted to enter another ciphertext.

send_msg(s, "Leaked ciphertext: " + encrypt_data(msg)+'\n')
send_msg(s,"enter ciphertext: ")

Then, in decrypt_data(), the presence of admin&password=goBigDawgs123 is checked. The goal is to submit a ciphertext such that the corresponding plaintext contains admin&password=goBigDawgs123.

def decrypt_data(encryptedParams):
cipher =, AES.MODE_CBC,iv)
paddedParams = cipher.decrypt(unhexlify(encryptedParams))
if b'admin&password=goBigDawgs123' in unpad(paddedParams,16,style='pkcs7'):
return 1
return 0


From encrypt_data(), we can see that AES CBC encryption is used, with a block size of 16.

We can send a payload like logged_username=admin&parsword=goBigDawgs123 (note the purposeful misspelling of password as parsword). Then, we will edit the previous block of ciphertext such that r becomes s at the misspelt index. We simply have to change the ciphertext at the correct index.

For instance, the following code gives us the edited ciphertext.

user = 'admin&parsword=goBigDawgs123'
password = 'goBigDawgs123'
msg = 'logged_username=' + user + '&password=' + password
print(msg, len(msg))
xor = ord('r') ^ ord('s')
cipher = encrypt_data(msg)
cipher = cipher[:16] + hex(int(cipher[16:18], 16) ^ xor)[2:] + cipher[18:]

The decrypted data would be something like this:


Notice that the second block has been changed to the desired string. Since we modified the first block, it will no longer decode properly and become corrupted (but in this case it doesn’t matter since only the desired string is checked).


We simply have to dynamically supply the weaponized ciphertext depending on the encryption output at the server.

Here’s the output:



Zhang Zeyu

Simple is better than complex. Complex is better than complicated.