AES-CBC bit flipping Attack

A walkthrough of AES-CBC mode and a simple bit-flipping attack that tampers with plaintext without knowing the key.

n00b19CTF(Easy-Flipp, 100 pt)

I wrote this crypto challenge for the CTF, and nobody solved it during the event. So here’s a writeup that walks through CBC bit flipping from the ground up.

AES-CBC encryption and decryption flowchart:

alt text

alt text

CBC’s weakness comes from how blocks chain together: the previous block’s ciphertext feeds into the next block. On decryption, each decrypted block is XORed with the previous ciphertext block to recover the plaintext. So if I flip bits in one ciphertext block, the plaintext of the next block flips in exactly the same positions. That’s the whole trick. Let’s see it in practice.

Here’s the challenge.

Hosted on: nc noob.bckdr.in 10006

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad,unpad
from Crypto.Random import get_random_bytes

greeting = """

         ___    ___   _        ___  _____   ___      _   ___  
 _ __   / _ \  / _ \ | |__    / __\/__   \ / __\    / | / _ \ 
| '_ \ | | | || | | || '_ \  / /     / /\// _\_____ | || (_) |
| | | || |_| || |_| || |_) |/ /___  / /  / / |_____|| | \__, |
|_| |_| \___/  \___/ |_.__/ \____/  \/   \/         |_|   /_/ 
                                                              

"""

key = get_random_bytes(16)
iv = get_random_bytes(16)

flag = open('flag','rb').read().strip()

def encrypt_data(data):
    cipher = AES.new(key, AES.MODE_CBC,iv)
    enc = cipher.encrypt(pad(data,16,style='pkcs7'))
    return enc.encode('hex')

def decrypt_data(encryptedParams):
    cipher = AES.new(key, AES.MODE_CBC,iv)
    paddedParams = cipher.decrypt(encryptedParams.decode('hex'))
    return unpad(paddedParams,16,style='pkcs7')

print(greeting)
print('hey n00b!! you know how CBC bit flipping works?\nIf you flip the bit correctly i will reward you fl4g!')
print(line)
msg = "admin=0"
print("Current Auth Message is : " + msg)
print("Encryption of auth Message in hex : " + iv.encode('hex') + encrypt_data(msg))
enc_msg = raw_input("Give me Encrypted msg in hex : ")
try:
    final_dec_msg = decrypt_data(enc_msg)

    if "admin=1" in final_dec_msg:
        print('Whoa!! you got it!! Now its reward time!!')
        print(flag)
    else:
        print('Try again you can do it!!')
        exit()
except:
    print('bye bye!!')

There are two functions here: encrypt_data() and decrypt_data(). encrypt_data() takes a message plus a random key and IV and encrypts under AES-CBC. Since the first block has no previous ciphertext block to chain with, CBC uses the IV in its place.

Breaking the encryption down step by step:

Message: admin=0

Block size: 16

After PKCS7 padding, the message becomes: admin=0\t\t\t\t\t\t\t\t\t

Here, we added \t nine times because len('admin=0') = 7and16 - 7 = 9. So, chr(9)*9 = "\t\t\t\t\t\t\t\t\t"

The goal is to tamper with the ciphertext so that decryption yields: admin=1

Let’s run the challenge.


 ~/ctf-2018/n00bctf18/Easy-Flipp$ nc noob.bckdr.in 10006  


         ___    ___   _        ___  _____   ___      _   ___  
 _ __   / _ \  / _ \ | |__    / __\/__   \ / __\    / | / _ \ 
| '_ \ | | | || | | || '_ \  / /     / /\// _\_____ | || (_) |
| | | || |_| || |_| || |_) |/ /___  / /  / / |_____|| | \__, |
|_| |_| \___/  \___/ |_.__/ \____/  \/   \/         |_|   /_/ 
                                                              


hey n00b!! you know how CBC bit flipping works?
If you flip the bit correctly i will reward you fl4g!
                                                                                                            
 _  _  _  _  _  _  _  _  _  _  _  _  _  _  _  _  _  _  _  _  _  _ 
(_)(_)(_)(_)(_)(_)(_)(_)(_)(_)(_)(_)(_)(_)(_)(_)(_)(_)(_)(_)(_)(_)

Current Auth Message is : admin=0
Encryption of auth Message in hex : fefbc34b8c1ec3f371f37ab5378e0e212f3ffd012a824cd16ffe8030d8bcd963
Give me Encrypted msg in hex : 


The hex output decodes to 32 bytes.

That length makes sense: it’s the IV (16 bytes) prepended to the single ciphertext block (16 bytes).

Splitting the encrypted message into its two blocks:

In [4]: a = "fefbc34b8c1ec3f371f37ab5378e0e212f3ffd012a824cd16ffe8030d8bcd963".decode('hex')

In [5]: len(a)
Out[5]: 32

In [6]: a[:16]
Out[6]: '\xfe\xfb\xc3K\x8c\x1e\xc3\xf3q\xf3z\xb57\x8e\x0e!'

In [7]: a[16:]
Out[7]: '/?\xfd\x01*\x82L\xd1o\xfe\x800\xd8\xbc\xd9c'

The first block is the IV: \xfe\xfb\xc3K\x8c\x1e\xc3\xf3q\xf3z\xb57\x8e\x0e! The second block is the padded encrypted message: /?\xfd\x01*\x82L\xd1o\xfe\x800\xd8\xbc\xd9c

The byte we care about in admin=0 is the 0 at index 6.

On decryption, the code runs AES(/?\xfd\x01*\x82L\xd1o\xfe\x800\xd8\xbc\xd9c, key) and XORs the result with the previous block, which is the IV here.

Because the plaintext byte comes from IV[6] XOR Dec_AES(message[6]), flipping the corresponding bit in IV[6] flips the same bit in the recovered plaintext, and the key never enters into it.

Starting point: IV[6] ^ Dec_AES(message[6]) = '0'.

We want '1', so XOR both sides with '0'^'1': (IV[6]^('0'^'1'))^Dec_AES(message[6]) = '1'.

Since ord('0')^ord('1') == 1, that’s just XORing IV[6] with 1. Encode the result back to hex and send it.

In [13]: a = "fefbc34b8c1ec3f371f37ab5378e0e212f3ffd012a824cd16ffe8030d8bcd963".decode('hex')

In [14]: a[6]
Out[14]: '\xc3'

In [15]: chr(ord('\xc3')^1)
Out[15]: '\xc2'

In [16]: a = a[:6] + '\xc2' + a[7:]

In [17]: a.encode('hex')
Out[17]: 'fefbc34b8c1ec2f371f37ab5378e0e212f3ffd012a824cd16ffe8030d8bcd963'

Now send the modified ciphertext:

 ~/ctf-2018/n00bctf18/Easy-Flipp$ nc noob.bckdr.in 10006  


         ___    ___   _        ___  _____   ___      _   ___  
 _ __   / _ \  / _ \ | |__    / __\/__   \ / __\    / | / _ \ 
| '_ \ | | | || | | || '_ \  / /     / /\// _\_____ | || (_) |
| | | || |_| || |_| || |_) |/ /___  / /  / / |_____|| | \__, |
|_| |_| \___/  \___/ |_.__/ \____/  \/   \/         |_|   /_/ 
                                                              


hey n00b!! you know how CBC bit flipping works?
If you flip the bit correctly i will reward you fl4g!
                                                                                                            
 _  _  _  _  _  _  _  _  _  _  _  _  _  _  _  _  _  _  _  _  _  _ 
(_)(_)(_)(_)(_)(_)(_)(_)(_)(_)(_)(_)(_)(_)(_)(_)(_)(_)(_)(_)(_)(_)

Current Auth Message is : admin=0
Encryption of auth Message in hex : fefbc34b8c1ec3f371f37ab5378e0e212f3ffd012a824cd16ffe8030d8bcd963
Give me Encrypted msg in hex : fefbc34b8c1ec2f371f37ab5378e0e212f3ffd012a824cd16ffe8030d8bcd963

Whoa!! you got it!! Now its reward time!!
CTF{XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX}

One flipped byte in the IV, and the flag drops out:

CTF{flag is redacted because its hosted on backdoor server!!!}