3.19 RESISTANCE IS FUTILE
EXERCISE 3.19: RESISTANCE IS FUTILE
Finish the code for the padding oracle attack. We’ve given you the major pieces, but it will still take some work to put everything together. We will do a few things to try and simplify as much as possible. First, pick a message that is exactly a multiple of 16 bytes in length (the AES block size) and create a fixed padding to append. The fixed padding can be any \(16\) bytes as long as the last byte is \(15\) (that’s the whole point of the exercise, right?). Encrypt this message and pass it to the oracle to make sure that code is working.
Next, test recovering the last byte of the first block of the message. In a loop, create a new key and IV pair (and a new oracle with these values), encrypt the message, and call the
lucky_get_one_byte()
function, setting block number to \(0\). Repeat the loop until this function succeeds and verify that the recovered byte is correct. Note that, in Python, an individual byte isn’t treated as a byte type but is converted to an integer.The last step in order to decode the entire message is to be able to make any byte the last byte of a block. Again, for simplicity keep the message being encrypted a perfect multiple of \(16\). To push any byte to the end of a block, add some extra bytes at the beginning and cut off an equal number at the end. You can now recover the entire message one byte at a time!
# ex3_19.py
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import padding
import os
def sslv3Pad(msg):
= (16 - (len(msg) % 16)) - 1
padNeeded = padNeeded.to_bytes(padNeeded+1, "big")
padding return msg + padding
def sslv3Unpad(padded_msg):
= padded_msg[-1] + 1
paddingLen return padded_msg[:-paddingLen]
class Oracle:
'''
The Oracle class basically models the SSLv3 Servers.
'''
def __init__(self, key, iv):
self.key = key
self.iv = iv
def accept(self, ciphertext) -> bool:
= Cipher(algorithms.AES(self.key),
aesCipher self.iv),
modes.CBC(=default_backend())
backend= aesCipher.decryptor()
decryptor = decryptor.update(ciphertext)
plaintext += decryptor.finalize()
plaintext return plaintext[-1] == 15
def update_key_and_iv(self, ciphertext) -> bytes:
'''
Change the key & iv that ciphertext was created.
'''
= Cipher(algorithms.AES(self.key),
aesCipher self.iv),
modes.CBC(=default_backend())
backend= aesCipher.decryptor()
decryptor = decryptor.update(ciphertext)
plaintext += decryptor.finalize()
plaintext
self.key = os.urandom(16)
self.iv = os.urandom(16)
= Cipher(algorithms.AES(self.key),
aesCipher self.iv),
modes.CBC(=default_backend())
backend= aesCipher.encryptor()
encryptor = encryptor.update(plaintext)
ciphertext += encryptor.finalize()
ciphertext return ciphertext
def right_shift_plaintext(self,ciphertext: bytes) -> bytes:
= Cipher(algorithms.AES(self.key),
aesCipher self.iv),
modes.CBC(=default_backend())
backend= aesCipher.decryptor()
decryptor = decryptor.update(ciphertext)
plaintext += decryptor.finalize()
plaintext
= b"-" + plaintext[:-17] + plaintext[-16:]
plaintext
= aesCipher.encryptor()
encryptor = encryptor.update(plaintext)
_ciphertext += encryptor.finalize()
_ciphertext return _ciphertext
@staticmethod
def start_experiment():
= b"msg is 16 bytesS"
msg = sslv3Pad(msg)
msg_padded
= Oracle(key=os.urandom(16), iv=os.urandom(16))
o = Cipher(algorithms.AES(o.key),
aesCipher
modes.CBC(o.iv),=default_backend())
backend= aesCipher.encryptor()
encryptor = encryptor.update(msg_padded)
ciphertext += encryptor.finalize()
ciphertext assert(o.accept(ciphertext=ciphertext))
return ciphertext, o
# This function assumes that the last cipher text block is a full
# block of SSLV3 padding
def lucky_get_one_byte(iv, ciphertext, block_number, oracle):
= block_number * 16
block_start = block_start + 16
block_end = ciphertext[block_start:block_end]
block
# Copy the block over the last block.
= ciphertext[:-16] + block
mod_ciphertext if not oracle.accept(mod_ciphertext):
return False, None
# This is valid! Let's get the byte!
# We first need the byte decrypted from the block.
# It was XORed with second to last block, so
# byte = 15 XOR (last byte of second-to-last block).
= ciphertext[-32:-16]
second_to_last = second_to_last[-1]^15
intermediate
# We still have to XOR it with its *real*
# preceding block in order to get the true value.
if block_number == 0:
= iv
prev_block else:
= ciphertext[block_start-16:block_start]
prev_block
return True, intermediate ^ prev_block[-1]
if __name__ == '__main__':
= Oracle.start_experiment()
ciphertext, o = []
plaintext_recovered # let's suppose the attacker knows that the plaintext is 16 characters/bytes long.
= 16
length_of_plaintext_length while length_of_plaintext_length > 0:
= lucky_get_one_byte(iv=o.iv, ciphertext=ciphertext, block_number=0, oracle=o)
status, byte if (status):
plaintext_recovered.append(byte)= o.right_shift_plaintext(ciphertext=ciphertext)
ciphertext -= 1
length_of_plaintext_length else:
= o.update_key_and_iv(ciphertext=ciphertext)
ciphertext
plaintext_recovered.reverse()print("Plaintext recovered: ", bytes(plaintext_recovered).decode())
Looks like we can recover a whole AES block using this technique as follows:
Note that the code that recovered the plaintext (i.e. the last if
condition in the script), accomplished it, without directly accessing o.key
. It only used public methods of the Oracle.