Three blocks of plaintext (green), each 16 bytes long, are encrypted, producing three blocks of ciphertext (yellow).
The first block is XORed with an initialization vector or iv (blue), also 16 bytes long, and then encrypted with the key.
Each subsequent block is XORed with the previous block of ciphertext.
01
0202
030303
0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f
10101010101010101010101010101010
plaintext[47] == bytes([1])
ciphertext[31]
That will garble the whole second
block of plaintext:
plaintext[16:32]
But it will only change a single byte of
the third block:
plaintext[47]
This is shown below, with red indicating
changed bytes.
python3 -m pip install pycryptodome
python3
from Crypto.Cipher import AES
key = b"0000111122223333"
iv = b"aaaabbbbccccdddd"
cipher = AES.new(key, AES.MODE_CBC, iv)
a = b"Hello from AES!!"
ciphertext = cipher.encrypt(a)
print(ciphertext.hex())
cipher = AES.new(key, AES.MODE_CBC, iv)
d = cipher.decrypt(ciphertext)
print(d)
print(d.hex())
from Crypto.Cipher import AES
key = b"0000111122223333"
iv = b"aaaabbbbccccdddd"
cipher = AES.new(key, AES.MODE_CBC, iv)
a = b"This simple sentence is forty-seven bytes long."
ciphertext = cipher.encrypt(a + bytes([1]))
mod = ciphertext[0:31] + bytes([255]) + ciphertext[32:]
print(ciphertext.hex())
print(mod.hex())
Note: your values will be different, but the location of the changed bytes will be the same.
cipher = AES.new(key, AES.MODE_CBC, iv)
c = cipher.decrypt(ciphertext)
cipher = AES.new(key, AES.MODE_CBC, iv)
cmod = cipher.decrypt(mod)
print(c.hex())
print(cmod.hex())
Use a text editor, such as nano or Notepad, to make this file. Save it as pador.py in your home directory.
from Crypto.Cipher import AES
key = b"aaaabbbbccccdddd"
iv = b"1111222233334444"
def decr(ciphertext):
cipher = AES.new(key, AES.MODE_CBC, iv)
return ispkcs7(cipher.decrypt(ciphertext))
def ispkcs7(plaintext):
l = len(plaintext)
c = plaintext[l-1]
if (c > 16) or (c < 1):
return "PADDING ERROR"
if plaintext[l-c:] != bytes([c])*c:
return "PADDING ERROR"
return plaintext
def encr(plaintext):
cipher = AES.new(key, AES.MODE_CBC, iv)
ciphertext = cipher.encrypt(pkcs7(plaintext))
return ciphertext
def pkcs7(plaintext):
padbytes = 16 - len(plaintext) % 16
pad = padbytes * bytes([padbytes])
return plaintext + pad
python3
from pador import encr, decr
a = b"This simple sentence is forty-seven bytes long."
c = encr(a)
print(c.hex())
As shown below, the encrypted string
is a long random series of hexadecimal
values.
d = decr(c)
print(d)
print(d.hex())
The decrypted string ends in
"01", a single byte of padding. It's not printable,
so it can't be seen in the ASCII version, but
it's visible in the hexadecimal version, as highlighted
in the image below.
mod = c[0:47] + bytes([65])
decr(mod)
Enter this command
decrypt the original ciphertext,
as shown below.
decr(c)
As shown below, an error message appears
when the padding is incorrect, but not when
it is correct.
This seemingly harmless error message is enough to completely defeat the encryption, because it can be used to leak out information about the encryption process.
WIN
You need to encrypt it with AES-CBC,
but you don't know the IV or the key.
You have a single example of encrypted text, and a system that will let you test encrypted text and tell you whether the padding is correct or not.
This should be impossible, but it can be done using the padding oracle attack.
intermediate[32:48]
,
depends only on the key and the third
block of ciphertext.
If we can figure out the intermediate state, we can create ciphertext that decrypts to any plaintext we want in the third block. We do that by modifying the second block.
We don't need to know the key or the iv.
1. You start with a valid ciphertext string.
2. Replace ciphertext[16:32] with any random
bytes. This will change the second and third
blocks of plaintext, including the last byte
of plaintext,
plaintext[47]
,
which is colored red in
the figure below.
The padding of the plaintext will now be
invalid, unless plaintext[47]
is 1. (There's a small chance that it might be valid
in other ways, but let's ignore that for now.)
3. Try all possible byte values for
ciphertext[31]
until a valid padding is found. Now we know that
plaintext[47]
is 1.
4. The last intermediate byte is the XOR of those values:
ciphertext[31] ^ 1
After 256 guesses, we get one byte of the intermediate value. We can continue in this fashion until we get them all, except for the first block.
a = b"This sentence cleaarly says I'M A LOSER."
original = encr(a)
print(original.hex())
As shown below, the ciphertext is a string
of 48 random bytes.
Now we are ready to perform the attack in four stages, as detailed below.
for i in range(5):
mod = original[0:31] + bytes([i]) + original[32:]
print(i, decr(mod))
As shown below, all the modifications
result in "PADDING ERROR" messages.
for i in range(256):
mod = original[0:31] + bytes([i]) + original[32:]
if decr(mod) != "PADDING ERROR":
print(i, "is correctly padded")
As shown below, there are two valid values.
One of these is the original byte, which results in a correct string of padding bytes, and the other one results in a final byte of 1, which is interpreted as a correct padding string one byte long.
Execute this command to see the original value of ciphertext[31].
print(original[31])
As shown below, the original byte is
154. So the other value, 147, must
result in a plaintext[47] value of 1.
This was effective, but it would be better to find the value directly, instead of finding two possibilities and needing to choose between them.
prefix = original[0:16] + b"AAAAAAAAAAAAAAA"
for i in range(256):
mod = prefix + bytes([i]) + original[32:]
if decr(mod) != "PADDING ERROR":
print(i, "is correctly padded")
As shown below, the correct value of 147 is
found. This worked because the modified ciphertext
creates random bytes of cleartext, which won't
end in valid padding unless the final
byte is 1.
prefix = original[0:16] + b"BBBBBBBBBBBBBBB"
for i in range(256):
mod = prefix + bytes([i]) + original[32:]
if decr(mod) != "PADDING ERROR":
print(i, "is correctly padded")
As shown below, the correct value of 147 is
found. Almost any values can be used to
fill ciphertext[16:31] and the attack will work.
ciphertext[31] ^ intermediate[47] = plaintext[47]
Applying ciphertext[31] ^ to both sides of this equation yields:
ciphertext[31] ^ ciphertext[31] ^ intermediate[47] = ciphertext[31] ^ plaintext[47]Rearranging terms yields:
intermediate[47] ^ ciphertext[31] ^ ciphertext[31] = ciphertext[31] ^ plaintext[47]On the left side, intermediate[47] is XORed with a byte, and then XORed again with the same byte. XOR is its own inverse--XORing twice with the same byte gets you back where you started, so:
intermediate[47] = ciphertext[31] ^ plaintext[47]And we know that plaintext[47] must be 1 to make the padding valid, so, when the padding is valid,
intermediate[47] = ciphertext[31] ^ 1 = 147 ^ 1 = 146So now we know this:
intermediate[47] = 146
intermediate[47] = 146So we need to use this value of ciphertext[31]:plaintext[47] = 2
ciphertext[31] = 146 ^ 2 = 144
prefix = original[0:16] + b"BBBBBBBBBBBBBB"
for i in range(256):
mod = prefix + bytes([i]) + bytes([144]) + original[32:]
if decr(mod) != "PADDING ERROR":
print(i, "is correctly padded")
As shown below, the answer is 6.
We can now calculate Intermediate[46]
intermediate[46] = ciphertext[30] ^ plaintext[46] = 6 ^ 2 = 4So now we know this:
intermediate[46] = 4
intermediate[47] = 146
intermediate[46] = 4So we need to use these values for ciphertext[30] and ciphertext[31]:
intermediate[47] = 146plaintext[46] = 3
plaintext[47] = 3
ciphertext[30] = 4 ^ 3 = 7
ciphertext[31] = 146 ^ 3 = 145
Press Enter twice after the last line of text.
prefix = original[0:16] + b"BBBBBBBBBBBBB"
for i in range(256):
mod = prefix + bytes([i]) + bytes([7]) + bytes([145]) + original[32:]
if decr(mod) != "PADDING ERROR":
print(i, "is correctly padded")
As shown below, the answer is 102.
We can now calculate Intermediate[45]
intermediate[45] = ciphertext[29] ^ plaintext[45] = 102 ^ 3 = 101So now we know this:
intermediate[45] = 101
intermediate[46] = 4
intermediate[47] = 146
intermediate[45] = 101So we need to use these values for ciphertext[29], ciphertext[30], and ciphertext[31]:
intermediate[46] = 4
intermediate[47] = 146plaintext[45] = 4
plaintext[46] = 4
plaintext[47] = 4
ciphertext[29] = 101 ^ 4 = 97
ciphertext[30] = 4 ^ 4 = 0
ciphertext[31] = 146 ^ 4 = 150
Press Enter twice after the last line of text.
prefix = original[0:16] + b"BBBBBBBBBBBB"
for i in range(256):
mod = prefix + bytes([i]) + bytes([97]) + bytes([0]) + bytes([150]) + original[32:]
if decr(mod) != "PADDING ERROR":
print(i, "is correctly padded")
As shown below, the answer is 235.
We can now calculate Intermediate[44]
intermediate[44] = ciphertext[28] ^ plaintext[44] = 235 ^ 4 = 239So now we know this:
intermediate[44] = 239
intermediate[45] = 101
intermediate[46] = 4
intermediate[47] = 146
intermediate[44] = 239We want this plaintext, ending with "WIN" and a correct single byte of 1 for padding:
intermediate[45] = 101
intermediate[46] = 4
intermediate[47] = 146
cleartext[44] = ord("W")So we choose these ciphertext bytes:
cleartext[45] = ord("I")
cleartext[46] = ord("N")
cleartext[47] = 1
ciphertext[28] = cleartext[44] ^ intermediate[44] = ord("W") ^ 239Execute these commands to calculate and insert those bytes:
ciphertext[29] = cleartext[45] ^ intermediate[45] = ord("I") ^ 101
ciphertext[30] = cleartext[46] ^ intermediate[46] = ord("N") ^ 4
ciphertext[31] = cleartext[47] ^ intermediate[47] = 1 ^ 146
c28 = ord("W") ^ 239
c29 = ord("I") ^ 101
c30 = ord("N") ^ 4
c31 = 1 ^ 146
ciphertext = original[0:28] + bytes([c28]) + bytes([c29]) \
+ bytes([c30]) + bytes([c31]) + original[32:]
decr(ciphertext)
C 501.1: Find Four Bytes (20 pts)
The flag is the four bytes covered by green rectangles in the image above, in hex, like this: AABBCCDD
This library takes as input a 96-character string, which encodes 48 bytes in hexadecimal. Those bytes contain ciphertext.
It then decrypts that string, expecting to find cleartext with a colon followed by a name. If it finds a name, it puts that name on the winners page.
If it fails, it provides overly informative error messages.
Your task is to construct ciphertext to fool the library into adding your name to the winners page.
Open a Terminal window and execute these commands:
cd Downloads
mv padorchal2a.pyx padorchal2a.py
python3
In the Python shell, execute these commands:
from padorchal2a import decr
a = "3ceafc6720f418f86b937a14fa0703df352b04f7d1e6a1e3bdd2e6f36e3da543800a6b07b3db36b372e934dfeeb2d920"
decr(a)
c = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
decr(c)
The first string should return 'Error: no name included ',
and the second one should return 'PADDING ERROR ',
as shown below.
This ciphertext is valid:
3ceafc6720f418f86b937a14fa0703df352b04f7d1e6a1e3bdd2e6f36e3da543800a6b07b3db36b372e934dfeeb2d920
It decodes to this string:
Put this name on the winners board: EXAMPLE
It gives an error message because a name cannot begin
with a space.
To get on the winners board, send ciphertext in hex
that decodes to a string ending in :YOURNAME
with correct padding. Don't use the literal string "YOURNAME"--use
a short version of your own name.
C 501.2: Challenge: Winners Page (50 pts extra)
Use the form below to put your name on the WINNERS PAGE.When you get it, submit a text in the Canvas system for this project telling us the name you inserted on the winners board.