from libmatasano import html_test
Write a function that takes a plaintext, determines if it has valid PKCS#7 padding, and strips the padding off.
The string:
"ICE ICE BABY\x04\x04\x04\x04"
... has valid padding, and produces the result "ICE ICE BABY".
The string:
"ICE ICE BABY\x05\x05\x05\x05"
... does not have valid padding, nor does:
"ICE ICE BABY\x01\x02\x03\x04"
If you are writing in a language with exceptions, like Python or Ruby, make your function throw an exception on bad padding.
Crypto nerds know where we're going with this. Bear with us.
class PaddingError(Exception):
pass
block_size = 16
def pkcs7_strip(x, block_size):
# we should also check that the padded message has a correct size
if not len(x) % block_size == 0:
raise PaddingError
last_byte = x[-1]
# the 'int' is superfluous here
# as last_byte is already an int
# (for Python a byte string is a list of integers)
# but this way it's clearer what we are doing
padding_size = int(last_byte)
if not x.endswith(bytes([last_byte])*padding_size):
raise PaddingError
return x[:-padding_size]
assert pkcs7_strip(b'ICE ICE BABY\x04\x04\x04\x04', block_size) == b'ICE ICE BABY'
try:
pkcs7_strip(b'ICE ICE BABY\x05\x05\x05\x05', block_size)
pkcs7_strip(b'ICE ICE BABY\x01\x02\x03\x04', block_size)
except PaddingError:
# got padding error as expected
html_test(True)
else:
print("ERROR: expected a padding error")
html_test(False)
Generate a random AES key.
Combine your padding code and CBC code to write two functions.
The first function should take an arbitrary input string, prepend the string:
"comment1=cooking%20MCs;userdata="
.. and append the string:
";comment2=%20like%20a%20pound%20of%20bacon"
The function should quote out the ";" and "=" characters.
The function should then pad out the input to the 16-byte AES block length and encrypt it under the random AES key.
The second function should decrypt the string and look for the characters ";admin=true;" (or, equivalently, decrypt, split the string on ";", convert each resulting string into 2-tuples, and look for the "admin" tuple).
Return true or false based on whether the string exists.
If you've written the first function properly, it should not be possible to provide user input to it that will generate the string the second function is looking for. We'll have to break the crypto to do that.
Instead, modify the ciphertext (without knowledge of the AES key) to accomplish this.
You're relying on the fact that in CBC mode, a 1-bit error in a ciphertext block:
- Completely scrambles the block the error occurs in
- Produces the identical 1-bit error(/edit) in the next ciphertext block.
Again, an attack that is possible because encryption does not provide integrity protection.
Quick remark about padding:
The functions encrypt_aes_128_cbc
and decrypt_aes_128_cbc
that I put in libmatasano
already apply PKCS#7 padding.
So don't be surprised if you don't see anything about padding in this code.
import os
import urllib
from libmatasano import encrypt_aes_128_cbc, decrypt_aes_128_cbc
class Oracle:
def __init__(self):
# the key is not regenerated for each encryption,
# otherise the attack wouldn't work
self.key = os.urandom(16)
def encrypt(self, msg):
# using urllib to quote characters (a bit overkill)
quoted_msg = urllib.parse.quote_from_bytes(msg).encode()
full_msg = (
b"comment1=cooking%20MCs;userdata="
+ quoted_msg
+ b";comment2=%20like%20a%20pound%20of%20bacon"
)
iv = os.urandom(16)
ciphertext = encrypt_aes_128_cbc(full_msg, iv, self.key)
return {"iv":iv, "ciphertext":ciphertext}
def decrypt_and_check_admin(self, ctxt):
ptxt = decrypt_aes_128_cbc(ctxt["ciphertext"], ctxt["iv"], self.key)
if b";admin=true;" in ptxt:
return True
else:
return False
oracle = Oracle()
cryptogram = oracle.encrypt(b"hi")
# having a look what the decrypted message looks like
print("decrypted:", decrypt_aes_128_cbc(cryptogram["ciphertext"], cryptogram["iv"], oracle.key))
# are we admin ? we shouldn't be
print("admin:", oracle.decrypt_and_check_admin(cryptogram))
We want to make ;admin=true;
appear in the message
but we cannot input any ";" or "=":
cryptogram = oracle.encrypt(b"lol;admin=true")
print("decrypted:", decrypt_aes_128_cbc(cryptogram["ciphertext"], cryptogram["iv"], oracle.key))
print("admin:", oracle.decrypt_and_check_admin(cryptogram))
Like they said in CBC decryption flipping a bit in a ciphertext block will flip the exact same bit in the next block of plaintext. Let's observe that:
cryptogram = oracle.encrypt(b"whatever")
# In Python, 'bytes' objects are immutable
# So I am using the mutable version, "bytearray",
# to modify bytes in-place
# https://docs.python.org/3/library/stdtypes.html#bytearray-objects
c = bytearray(cryptogram["ciphertext"])
# we're flipping the lowest bit of the 21st byte of the ciphertext
# with a "XOR" operation
c[20] = c[20] ^ 0x01
print("decrypted:", decrypt_aes_128_cbc(bytes(c), cryptogram["iv"], oracle.key))
It's pretty clear here: a whole block has been completely scrambled, and a single bit has been flipped in the next block ("whatever" became "whatdver").
So here's the plan:
XXXX...
Conveniently enough, we see that the content of "userdata" ("whatever" in our previous example) starts exactly at the beginning of a block. (in the last output it appears right after the block of messed up bits caused by our bitflipping).
To make it simple, we will give a value for userdata
that is as long as ;admin=true
and that is just a list of "X":
len(b";admin=true")
from libmatasano import bxor
# flipping bits is equivalent to applying a XOR operation;
pad = bxor(b";admin=true", b"X"*11)
cryptogram = oracle.encrypt(b"X"*11)
cryptogram["ciphertext"] = bxor(cryptogram["ciphertext"], b'\x00'*16 + pad)
print("decrypted:", decrypt_aes_128_cbc(cryptogram["ciphertext"], cryptogram["iv"], oracle.key))
print("admin:", oracle.decrypt_and_check_admin(cryptogram))
Et voilĂ !
Again: All this is possible because encryption only protect the privacy of the message, not its integrity.