Hazmat Symmetric Key - Bit-flipping CiphertextSome AES modes are vulnerable to bit flipping attacks. In this case, we will use AES CTR mode, and flip the bits of the nth character in the ciphertext, and where we specify the character to target (by default, the program will focus on changing the 9th character). |
Theory
Don’t you just love making money? Well, if we use the wrong type of encryption, we could end up with the wrong amount of money being allocated to someone.
So, let’s see if we can double Bob’s money. In this case, Bob will give Alice one dollar, and then she will send a ciphertext message to Trent to pay Bob back. But Eve modifies the encrypted ciphertext so that Bob gets two dollars each time. Eve never knows the encryption key that Alice and Trent are using.
And, so, Alice encrypts “Pay Bob 1 Dollar”, with a secret key and a random salt value, and gets a cipher of “39dda42138cc4be3b62161f61ff4fe5ef89dbfd6a2e6c8af7884902558ab4cf0”, and sends this to Trent for payment to Bob.
Eve then sneaks in - she's an Eve-in-the-middle - and changes only one part of the ciphertext to “39dda42138cc4be3b52161f61ff4fe5ef89dbfd6a2e6c8af7884902558ab4cf0”. When Trent decrypts the ciphertext, he gets “Pay Bob 2 Dollar”, and pays Bob two dollars. Bob is very happy and keeps giving Alice more dollars, and to keep sending these transactions to Trent. Bob soon becomes a millionaire and shares his earnings with Eve, but Alice has to sell everything — she’s lost everything!
Bit-flipping
You need to beware of bit flipping in certain modes of AES, including with ECB, CBC and CTR. In the following, we flip the bits of a specific ciphertext, and reveal that we have changed the plaintext [here].
import os from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from cryptography.hazmat.primitives import padding import sys import binascii def go_encrypt(msg,method,mode): cipher = Cipher(method, mode) encryptor = cipher.encryptor() ct = encryptor.update(msg) + encryptor.finalize() return (ct) def go_decrypt(ct,method,mode): cipher = Cipher(method, mode) decryptor = cipher.decryptor() return (decryptor.update(ct) + decryptor.finalize()) key = os.urandom(32) iv = os.urandom(16) msg=b"Pay Bob 1 dollar" if (len(sys.argv)>1): msg=str(sys.argv[1]).encode() print ("Message:\t",msg.decode()) print ("Key:\t",binascii.b2a_hex(key)) print ("IV:\t",binascii.b2a_hex(iv)) print ("\n\n=== AES CTR === ") cipher=go_encrypt(msg,algorithms.AES(key), modes.CTR(iv)) plain=go_decrypt(cipher,algorithms.AES(key), modes.CTR(iv)) print ("Cipher: ",binascii.b2a_hex(cipher)) print (f"Decrypted: {plain.decode()}")
Everything works fine:
Message: Pay Bob 1 dollar Key: b'927e5f7187779e110ea1d430b9675075d769320101f0ba1360d7e14a5ebe8977' IV: b'711493f4e0fc8d711682a1c28fb45fc5' === AES CBC === Cipher: b'0d7a65428c81d83f12969e780bd09a75fb97a360740e34b717b5afa263e8d8e6' Decrypted: Pay Bob 1 dollar
Now we want to flip the “1” to a “2”, and double our money. For this, a “1” is is 0110001, and a ‘2’ is 0110010, and so we need to invert the last two bits.
Now we can insert:
print(f"Before bitflip: {cipher.hex()}") ct = bytearray(cipher) ct[8]=ct[8]^0x03 cipher=bytes(ct) print(f"After bitflip: {cipher.hex()}")
This will take the 9th byte of “Pay Bob 1 dollar” (the ‘1’) and E-XOR with 0000 0011. This will flip the two least significant bits. The updated code is:
import os from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from cryptography.hazmat.primitives import padding from cryptography.hazmat.backends import default_backend import sys import binascii def go_encrypt(msg,method,mode): cipher = Cipher(method, mode,backend=default_backend()) encryptor = cipher.encryptor() ct = encryptor.update(msg) + encryptor.finalize() return (ct) def go_decrypt(ct,method,mode): cipher = Cipher(method, mode,backend=default_backend()) decryptor = cipher.decryptor() return (decryptor.update(ct) + decryptor.finalize()) def pad(data,size=128): padder = padding.PKCS7(size).padder() padded_data = padder.update(data) padded_data += padder.finalize() return(padded_data) def unpad(data,size=128): padder = padding.PKCS7(size).unpadder() unpadded_data = padder.update(data) unpadded_data += padder.finalize() return(unpadded_data) chartoflip=9 key = os.urandom(32) iv = os.urandom(16) msg=b"Pay Bob 1 dollar" if (len(sys.argv)>1): msg=str(sys.argv[1]).encode() if (len(sys.argv)>2): chartoflip=int(sys.argv[2]) if (len(msg)<chartoflip): print("Not valid") sys.Exit(0) print ("Message:\t",msg.decode()) print ("Key:\t",binascii.b2a_hex(key)) print ("IV:\t",binascii.b2a_hex(iv)) print ("\n\n=== AES CTR === ") cipher=go_encrypt(msg,algorithms.AES(key), modes.CTR(iv)) print(f"Flipping two least significant bits in character {chartoflip}") print(f"\nBefore bitflip: {cipher.hex()}") ct = bytearray(cipher) ct[chartoflip-1]=ct[chartoflip-1]^0x03 cipher=bytes(ct) print(f"After bitflip: {cipher.hex()}") plain=go_decrypt(cipher,algorithms.AES(key), modes.CTR(iv)) print ("\nCipher: ",binascii.b2a_hex(cipher)) print (f"Decrypted: {plain.decode()}")
Now when we run the code we get:
Message: Pay Eve 1 Dollar Key: b'3a6d79bedf58cef355929fc9967df3d51c8f6b6e44605fc1b553d40567d575c6' IV: b'5cd15208d8d13493b782c411d0f5b3e1' === AES CTR === Flipping two least significant bits in character 9 Before bitflip: 226cb040c0753033 e6f92af9d531ef91 After bitflip: 226cb040c0753033e5f92af9d531ef91 Cipher: b'226cb040c0753033e5 f92af9d531ef91' Decrypted: Pay Eve 2 Dollar
Notice that “0xe6” (1110 0110) has changed to “0xb5 (1110 0101)”. And, so, we have doubled Bob’s money. We can also flip the first character to change "P" to "S":
Message: Pay Eve 1 Dollar Key: b'd1c9b21b2d95f831b17645867bec8a285b62342d98c3c30b5f77bb8db01b599b' IV: b'612c05f73ef330ec6a8a3bee57300018' === AES CTR === Flipping two least significant bits in character 1 Before bitflip: 9d826ec52a49a57b89cec4684b7b7f76 After bitflip: 9e826ec52a49a57b89cec4684b7b7f76 Cipher: b'9e826ec52a49a57b89cec4684b7b7f76' Decrypted: Say Eve 1 Dollar
Here is a demo using OpenSSL [here]
Conclusions
Use GCM or CCM mode … as they build in integrity checking!