Skip to content

Commit 28ea3eb

Browse files
committed
Set 3, challenge 17: CBC Padding Oracle
1 parent 0f1ef00 commit 28ea3eb

File tree

7 files changed

+194
-2
lines changed

7 files changed

+194
-2
lines changed

cryptopals/block.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from cryptography.hazmat.backends import default_backend
88
from cryptography.hazmat.primitives.ciphers import algorithms, Cipher, modes
99

10+
from cryptopals.exceptions import BadPaddingValidation
1011
from cryptopals.frequency import TEST_CHARACTERS
1112
from cryptopals.padding import pkcs_7, remove_pkcs_7
1213
from cryptopals.utils import xor
@@ -175,3 +176,11 @@ def construct_ecb_attack_dict(
175176
dict_to_construct[ciphertext] = char
176177

177178
return dict_to_construct
179+
180+
181+
def cbc_padding_oracle(key: bytes, ciphertext: bytes, iv: bytes) -> bool:
182+
try:
183+
decrypted_plaintext = aes_cbc_decrypt(key, ciphertext, iv, remove_padding=True)
184+
return True
185+
except BadPaddingValidation:
186+
return False

cryptopals/cbc_padding.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import math
2+
import os
3+
from typing import List, Optional
4+
5+
from cryptopals.block import cbc_padding_oracle
6+
7+
8+
def flip_nth_bit(text: bytes, num: int):
9+
modified = text[:num] + bytes([text[num] ^ 1]) + text[num + 1 :]
10+
return modified
11+
12+
13+
class Solver(object):
14+
def __init__(self, block_size: int, iv: bytes, key: bytes, ciphertext: bytes):
15+
self.block_size = block_size
16+
self.ciphertext = ciphertext
17+
18+
# We won't look at these, these would be used by the server
19+
self.iv = iv
20+
self.key = key
21+
22+
self.reconstructed_bytes = [] # type: List
23+
self.debug = False # type: bool
24+
25+
def _decrypt_single_block(self, block_num):
26+
previous_block_ciphertext = self.ciphertext[
27+
(block_num - 2)
28+
* self.block_size : (block_num - 1)
29+
* self.block_size
30+
]
31+
this_block_ciphertext = self.ciphertext[
32+
(block_num - 1) * self.block_size : block_num * self.block_size
33+
]
34+
35+
block_cipher_outputs_prior_to_xor = []
36+
37+
for byte_num in range(self.block_size, 0, -1):
38+
39+
valid_padding_byte = self.block_size - byte_num + 1
40+
found_a_byte = False
41+
42+
for num in range(255):
43+
num_of_prefix_bytes = byte_num - 1
44+
45+
test_block = b'0' * num_of_prefix_bytes + bytes(
46+
[num]
47+
)
48+
49+
# Now add the byte that will be padding for the bytes we've already
50+
# reconstructed.
51+
for block_cipher_output_byte in block_cipher_outputs_prior_to_xor:
52+
padding_byte = block_cipher_output_byte ^ valid_padding_byte
53+
test_block = test_block + bytes([padding_byte])
54+
55+
full_test_ciphertext = test_block + this_block_ciphertext
56+
57+
# Note that we're passing in the key and IV but we're only
58+
# returning whether or not the padding is valid, not the
59+
# decrypted content.
60+
valid_padding = cbc_padding_oracle(
61+
self.key, full_test_ciphertext, self.iv
62+
)
63+
64+
if not valid_padding:
65+
continue
66+
67+
block_cipher_output_byte = num ^ valid_padding_byte
68+
plaintext_byte = (
69+
previous_block_ciphertext[num_of_prefix_bytes]
70+
^ block_cipher_output_byte
71+
)
72+
73+
# Handling the padding block by ensuring we get the choice of
74+
# num correct (e.g. if the second to last byte in the block
75+
# happens to be 2, we will get valid padding for _two_ possible
76+
# bytes in the last byte position).
77+
byte_num_to_edit = self.block_size + byte_num - 2
78+
degeneracy_ciphertext = flip_nth_bit(
79+
full_test_ciphertext, byte_num_to_edit - self.block_size
80+
)
81+
if cbc_padding_oracle(self.key, degeneracy_ciphertext, self.iv):
82+
pass
83+
else:
84+
continue
85+
86+
if self.debug: # Fail when we get a byte wrong before going any further
87+
try:
88+
expected_byte = self.plaintext_bytes[
89+
self.block_size * (block_num - 1) + byte_num - 1]
90+
assert plaintext_byte == expected_byte
91+
except AssertionError:
92+
breakpoint()
93+
94+
found_a_byte = True
95+
96+
# Save the reconstructed byte and the output of the block cipher
97+
# prior to XOR as we'll need it for the next byte to reconstruct.
98+
block_cipher_outputs_prior_to_xor = [
99+
block_cipher_output_byte
100+
] + block_cipher_outputs_prior_to_xor
101+
self.reconstructed_bytes = [
102+
plaintext_byte
103+
] + self.reconstructed_bytes
104+
105+
break
106+
107+
if self.debug and not found_a_byte:
108+
breakpoint()
109+
elif not found_a_byte:
110+
raise Exception("Did not reconstruct a byte this block!")
111+
112+
def run(self, plaintext_bytes) -> List:
113+
self.plaintext_bytes = plaintext_bytes
114+
self.num_blocks = math.ceil(len(plaintext_bytes) / self.block_size)
115+
116+
# We start at rightmost block and move right to left.
117+
for block_num in range(self.num_blocks, 0, -1):
118+
if block_num == 1:
119+
# We don't have the IV to decrypt further, so we stop here.
120+
break
121+
122+
self._decrypt_single_block(block_num)
123+
124+
return self.reconstructed_bytes

cryptopals/padding.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ def pkcs_7(plaintext: bytes, block_len: int) -> bytes:
1616

1717
def remove_pkcs_7(plaintext: bytes) -> bytes:
1818
num_bytes_of_padding = int.from_bytes(plaintext[-1:], byteorder="little")
19+
if num_bytes_of_padding == 0:
20+
raise BadPaddingValidation
1921

2022
unpadded_plaintext = plaintext
2123
for _ in range(num_bytes_of_padding):

tests/data/17.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
MDAwMDAwTm93IHRoYXQgdGhlIHBhcnR5IGlzIGp1bXBpbmc=
2+
MDAwMDAxV2l0aCB0aGUgYmFzcyBraWNrZWQgaW4gYW5kIHRoZSBWZWdhJ3MgYXJlIHB1bXBpbic=
3+
MDAwMDAyUXVpY2sgdG8gdGhlIHBvaW50LCB0byB0aGUgcG9pbnQsIG5vIGZha2luZw==
4+
MDAwMDAzQ29va2luZyBNQydzIGxpa2UgYSBwb3VuZCBvZiBiYWNvbg==
5+
MDAwMDA0QnVybmluZyAnZW0sIGlmIHlvdSBhaW4ndCBxdWljayBhbmQgbmltYmxl
6+
MDAwMDA1SSBnbyBjcmF6eSB3aGVuIEkgaGVhciBhIGN5bWJhbA==
7+
MDAwMDA2QW5kIGEgaGlnaCBoYXQgd2l0aCBhIHNvdXBlZCB1cCB0ZW1wbw==
8+
MDAwMDA3SSdtIG9uIGEgcm9sbCwgaXQncyB0aW1lIHRvIGdvIHNvbG8=
9+
MDAwMDA4b2xsaW4nIGluIG15IGZpdmUgcG9pbnQgb2g=
10+
MDAwMDA5aXRoIG15IHJhZy10b3AgZG93biBzbyBteSBoYWlyIGNhbiBibG93

tests/test_block.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
aes_cbc_decrypt,
88
aes_cbc_encrypt,
99
detect_ecb_use,
10+
cbc_padding_oracle,
1011
ecb_encrypt_append,
1112
ecb_encrypt_prepend_and_append,
1213
cbc_encrypt_prepend_and_append,
@@ -15,7 +16,8 @@
1516
construct_ecb_attack_dict,
1617
)
1718
from cryptopals.frequency import TEST_CHARACTERS
18-
from cryptopals.utils import base64_to_bytes, hex_to_bytes
19+
from cryptopals.padding import pkcs_7, remove_pkcs_7
20+
from cryptopals.utils import base64_to_bytes, hex_to_bytes, xor
1921

2022

2123
BLOCK_SIZE = 16

tests/test_cbc_padding.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import os
2+
import random
3+
4+
from cryptopals.block import aes_cbc_encrypt
5+
from cryptopals.cbc_padding import Solver
6+
from cryptopals.padding import pkcs_7
7+
from cryptopals.utils import base64_to_bytes
8+
9+
10+
def test_cbc_padding_oracle():
11+
# Set 3, challenge 17 CBC Padding Oracle
12+
13+
path_to_test_data = os.path.join(
14+
os.path.dirname(os.path.abspath(__file__)), "data/17.txt"
15+
)
16+
17+
with open(path_to_test_data, "r") as f:
18+
plaintexts = f.read().split("\n")
19+
20+
block_size = 16
21+
key = os.urandom(block_size)
22+
iv = os.urandom(block_size)
23+
24+
plaintext = random.choice(plaintexts)
25+
unpadded_plaintext_bytes = base64_to_bytes(plaintext)
26+
plaintext_bytes = pkcs_7(unpadded_plaintext_bytes, block_size)
27+
ciphertext = aes_cbc_encrypt(key, unpadded_plaintext_bytes, iv)
28+
29+
cbc_solver = Solver(block_size, iv, key, ciphertext)
30+
reconstructed_bytes = cbc_solver.run(plaintext_bytes)
31+
32+
# Since I'm pretending as if I didn't know the IV, I'm going to compare
33+
# only the blocks that were *not* constructed by XORing with the IV.
34+
# As an attacker I could guess e.g. that the IV was all 0s or some other
35+
# insecure default that might have been due to an insecure default in the
36+
# crypto library used by the developer.
37+
result_without_iv_based_block = "".join([chr(x) for x in reconstructed_bytes])
38+
plaintext_without_iv_based_block = plaintext_bytes[
39+
2
40+
* cbc_solver.block_size : (cbc_solver.num_blocks - 1)
41+
* cbc_solver.block_size
42+
].decode("utf-8")
43+
44+
assert plaintext_without_iv_based_block in result_without_iv_based_block

tests/test_padding.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ def test_removal_of_pkcs_7(test_input, expected):
2828

2929
@pytest.mark.parametrize(
3030
"test_input",
31-
[("ICE ICE BABY\x01\x02\x03\x04"), ("ICE ICE BABY\x05\x05\x05\x05"), ("YE\x03")],
31+
[("ICE ICE BABY\x01\x02\x03\x04"), ("ICE ICE BABY\x05\x05\x05\x05"), ("YE\x03"),
32+
("ICE ICE BABY HI\x00")],
3233
)
3334
def test_removal_of_pkcs_7_raises_exception_invalid_padding(test_input):
3435
with pytest.raises(BadPaddingValidation):

0 commit comments

Comments
 (0)