3.20 STATISTICS ARE ALSO FUTILE
EXERCISE 3.20: STATISTICS ARE ALSO FUTILE
Instrument your padding oracle attack in the previous exercise to calculate how many guesses it took to fully decrypt the entire message and calculate an average number of tries per byte. In theory, it should work out to about 256 tries per byte. But you’re probably working with such small numbers that it will vary widely. In our tests on a \(96\)-byte message, our averages varied between about \(220\) guesses per byte and \(290\) guesses per byte.
# ex3_20.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 = 0
number_of_guesses while length_of_plaintext_length > 0:
+= 1
number_of_guesses = 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())
print(f"Number of Guesses to fully decrypt the message: {number_of_guesses}")
print(f"Average number of tries per byte: {number_of_guesses/16}") # since there are 16 bytes in plaintext
Running the above code gives us the following: