I am tying to write a very simple script that will Just decrypt one byte of text
according to this formula P′2[K] = Pn[K] ⊕ Cn-1[K] ⊕ C′[K]
the oracle is a simple function that decrypt then check last byte to be equal to padding 0x15
. with p'2[k] is just 0x15(padding size)
def decrypt(cipher):
dec = aes_context.decryptor()
text = dec.update(cipher)
if text[-1] == 0x15:
return True, "Padding Match"
else:
return False, "No Match"
but the behavior seems Undefined . The loop is a simple loop from 0-> 255(number of tries to decrypt one block)
number = 0x01
index = 0
while index < 255:
try_this_block = 0x0.to_bytes(7, "big") + number.to_bytes(1, "big")
mod_ciphertext = try_this_block + c1
state, error_text = decrypt(mod_ciphertext)
if state:
byte = try_this_block[-1] ^ 0x15 ^ c1[-1]
text_back += byte.to_bytes(1, "big")
break
else:
number += 1
index += 1
The message That gets encrypted is just 8 bytes string + 8 bytes padding and gets decrypted with same key and IV each time. with c1, c2 correspond to ciphertext of m1, m2
m1 = b"khaled G"
m2 = 0x00.to_bytes(7, "big") + 0x015.to_bytes(1, "big")
WHOLE SOURCE CODE IS HERE:
from cryptography.hazmat.primitives.ciphers import algorithms, modes, Cipher
from cryptography.hazmat.backends import default_backend
import os
m1 = b"khaled G"
m2 = 0x00.to_bytes(7, "big") + 0x015.to_bytes(1, "big")
aes_context = Cipher(algorithms.AES(os.urandom(16)), modes.CBC(os.urandom(16)), default_backend())
enc = aes_context.encryptor()
c1 = enc.update(m1)
c2 = enc.update(m2)
c1, c2 = c2[0:8], c2[8:]
def decrypt(cipher):
dec = aes_context.decryptor()
text = dec.update(cipher)
if text[-1] == 0x15:
return True, "Padding Match"
else:
return False, "No Match"
text_back = b""
number = 0x01
index = 0
while index < 255:
try_this_block = 0x0.to_bytes(7, "big") + number.to_bytes(1, "big")
mod_ciphertext = try_this_block + c1
state, error_text = decrypt(mod_ciphertext)
if state:
byte = try_this_block[-1] ^ 0x15 ^ c1[-1]
text_back += byte.to_bytes(1, "big")
break
else:
number += 1
index += 1
print("text is {}".format(text_back))
I think there are two problems in the code:
First, AES is used to generate a ciphertext with a length of one block or 16 bytes. Then this one 16 bytes ciphertext block is split into two 8 bytes blocks, which are treated as 2 separate ciphertext blocks for the padding oracle attack. However, this is not allowed in the context of the relationship P2 = C1 xor (C1' xor P2')
since the assumed relations between the two blocks do not exist, see e.g. here. At this point I don't want to exclude the possibility that there is an algorithm for your case, but it will be a different one.
The problem can be solved by using a block size equal to that of the algorithm. E.g. for a block size of 8 bytes TripleDES could be used. Alternatively, when using AES, a block size of 16 bytes must be used. In the former case, key and IV size have to be adjusted and furthermore the line c1, c2 = c2[0:8], c2[8:]
is now obsolete:
aes_context = Cipher(algorithms.TripleDES(os.urandom(24)), modes.CBC(os.urandom(8)), default_backend())
...
c1 = enc.update(m1)
c2 = enc.update(m2)
#c1, c2 = c2[0:8], c2[8:]
...
Secondly, the decryption does not take into account the IV. If the first block (c1
) is to be decrypted, the preceding test block does not correspond to c1
(as in the current code), but to c0
and this is the IV. To solve the problem the following changes are necessary:
iv = os.urandom(8)
aes_context = Cipher(algorithms.TripleDES(os.urandom(24)), modes.CBC(iv), default_backend())
...
c1 = enc.update(m1)
c2 = enc.update(m2)
#c1, c2 = c2[0:8], c2[8:]
...
byte = try_this_block[-1] ^ 0x15 ^ iv[-1]
With these changes the expected result (G
) is obtained.
When using AES and a block size of 16 bytes, the data must additionally be modified accordingly.
E.g. the following changes also provide the expected result (G
):
m1 = b"khaled Gkhaled G"
m2 = 0x00.to_bytes(15, "big") + 0x015.to_bytes(1, "big")
...
iv = os.urandom(16)
aes_context = Cipher(algorithms.AES(os.urandom(16)), modes.CBC(iv), default_backend())
...
c1 = enc.update(m1)
c2 = enc.update(m2)
#c1, c2 = c2[0:8], c2[8:]
...
try_this_block = 0x0.to_bytes(15, "big") + number.to_bytes(1, "big")
...
byte = try_this_block[-1] ^ 0x15 ^ iv[-1]
Edit:
If algorithm/blocksize (AES, 16 bytes) and test data are not to be changed, then the test data will form only one plaintext block (composed of m1
and m2
) instead of two. In this case, 0x15
is returned as the result, since the padding byte is now the last byte of the block. With regard to the necessary code changes, it should be noted that when only one plaintext block is encrypted, only one ciphertext block is generated (c1
):
iv = os.urandom(16)
aes_context = Cipher(algorithms.AES(os.urandom(16)), modes.CBC(iv), default_backend())
...
c1 = enc.update(m1) # empty
c1 += enc.update(m2) # provides full block
#c1, c2 = c2[0:8], c2[8:]
...
try_this_block = 0x0.to_bytes(15, "big") + number.to_bytes(1, "big")
...
byte = try_this_block[-1] ^ 0x15 ^ iv[-1]
This returns as result 0x15
.