Set 2
9. Implement PKCS#7 padding
You can find the RFC here. What got me at first was that if the length of the data was a factor of blocksize, I did not add any padding. This was wrong :-/
And we can validate this with the example given:
And its inverse unpad
- which isn’t required yet but will be in the next problem.
The check for padding correctness isn’t necessary, but helped me catch some bad code along the way.
10. Implement CBC mode
Which gives us the lyrics to the wonderful “Play That Funky Music” - again.
For completness, this is what aes_manual_cbc
looks like. I bundled the encrypt/decrypt versions together.
The difference is as follows:
- Encryption xors the previous block with the current block first before encrypting it
- Decryption decrypts the block first before xoring it with the previous block
It’s actually rather neat!
11. An ECB/CBC detection oracle
For this one we have to complete a number of steps.
- Write a function to generate a random AES key
- Write a function that encrypts data under an unknown key under CBC 50% of the time, and ECB otherwise. There are a few more caveats but that’s the general idea.
In the above we return whether or not we guessed right - but you get the idea.
It’s worth noting that the input needs to be sufficiently large. With a toy example, it’s easy enough:
>>> encryption_oracle(b'A'*2000)
mode used: CBC, mode guessed: CBC
>>> encryption_oracle(b'A'*2000)
mode used: ECB, mode guessed: ECB
Though that’s cheating a little : )
12. Byte-at-a-time ECB decryption (Simple)
This is where things start to become interesting, and fun (and a bit more complicated)!
We will be exploiting a weakness in ECB to decrypt a piece of cyphertext for which we do not have the key.
- Create a function that returns
AES-128-ECB(your-string || unknown-string, random-key)
- Guess the block size (which we know, but we do this for good measure). Since we’re using ECB, encrypting the same bytes in two different blocks will lead to identical blocks - we just need to find which number of bytes it takes before a repeat occurs.
- Check whether this uses ECB (we know it does, but we’re being asked to do this anyway)
- Craft an input that’s 1 byte short of the block size, and create a table for all possible combinations. Say our plaintext was ‘YELLOW SUBMARINE’ and block size is 8 - by crafting a plaintext that will be 7 bytes, the 8th byte will be ‘Y’ (
b'AAAAAAAY'
). If we didn’t know what what the 8th byte would be, we would cycle through all possible bytes until the encrypted block matched the one with our unknown byte. ECB allows us to do that because each block is encrypted independently of the previous one.
- Lather, rinse, repeat. We essentially repeat the above but instead of using
b'AAAAAAAY'
, we now useb'AAAAAAY?'
- we keep shifting left until we have decrypted all the plaintext.
Putting the snippets above together, we find that the plaintext is b"Rollin' in my 5.0\nWith my rag-top down so my hair...
- all without knowing the actual encryption key, but just being provided a function that encrypts arbitrary plaintext.
13. ECB cut-and-paste
- Write a
k=v
parsing routine and a function calledprofile_for
that takes an email address and returns something encoded likeemail=foo@bar.com&uid=10&role=user
- Write 2 functions - one to encrypt the profile and another to decrypt it
Now for the tricky part - we need to generate a piece of ciphertext that decodes with role=admin
. The enc_profile_for
function acts as our oracle. In a nutshell we’ll swap one of the blocks for our own, which contains the data we need. There are a few caveats - blocksize is fixed, so we need to craft our input carefully. On top of that enc_profile_for
will strip all metacharacters.
Let’s recap - profile_for
will generate something like email=_&uid=10&role=user
. We can only act on 16 bytes blocks - so there are 2 things we need to do. We want to split the blocks such that role=
ends up at the 16th byte boundary (meaning we can then append a new block containing admin
), and generate that block from the input.
Trying my hand as ascii art:
|email=_ -->16b|admin -->16b|<-3b->&uid=10&role=|user -->16b|
So that we can take the middle block and replace it with the last block. Does that make a bit more sense? The lenght of our input should be 16b-len(email=) + 16b + 3b
where the middle 16b contain admin
and the padding required to make it a valid block.
>>> crypto_utils.pad(b'admin')
b'admin\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b'
We have 10 + 3 bytes for our email address (or more if we want to split this over multiple blocks): qwerty@ab.com
- but we need to take the last 3 bytes and stick them at the end of the admin block. Putting it all together:
Leading to:
{'role': 'admin', 'email': 'qwerty@ab.com', 'uid': '10'}
How cool is that?
14. Byte-at-a-time ECB decryption (Harder)
This is essentially 12
but harder. The oracle is now defined as AES-128-ECB(random-prefix || attacker-controlled || target-bytes, random-key)
where random-prefix is constant but of unknown length.
The first thing we need to do is figure out the length of the random-prefix
. Again this is using ECB so if we send between 32 and 48 bytes of a repeating string we should see a (consecutively placed) duplicate block somewhere:
Now we need to so the same thing we did with 12
but take the padding into account. So instead if the block size was 8, instead of calling the oracle with b'AAAAAAA?'
we need to call it with enough data to complete the unknown prefix into a full block.
Going back to our YELLOW SUBMARINE
example, in ascii this gives:
1. |random-prefix + padding (say AAA)|AAAAAAAY|ELLOWSUBM|ARINE + PKCS#7 padding|
2. |random-prefix + padding (say AAA)|AAAAAAYE|LLOWSUBMA|RINE + PKCS#7 padding|
2. |random-prefix + padding (say AAA)|AAAAAYEL|LOWSUBMAR|INE + PKCS#7 padding|
Yielding the same lyrics. The only tricky bit was accounting for random-prefix
.
15. PKCS#7 padding validation
Heh. We already did this as part of 9
- unpad
.
16. CBC bitflipping attacks
For this, we are asked to create another oracle:
We need to craft userdata
such that it contains ;admin=true' - but we can't use
; or
=. What we'll do is send some input like
:admin@true’ and flip bits on the cyphertext until, when decrypting, :
becomes ;
and @
becomes =
. It will mangle the data in the previous block but we don’t care since we also control that.
The length of the prefix is exactly 32 bytes:
>>> len("comment1=cooking%20MCs;userdata=")
32
So we’re at a block boundary. Let’s start with:
000000000000000000000:admin@true
0123456789ABCDEF0123456789ABCDEF
Which is exactly 32 bytes - and we need to flip bytes 5 and 11. Let’s remember - this is akin to a website sending us an encrypted cookie. We don’t control encryption itself, but we can modify the encrypted data such that when decrypted, it gives us the desired result.
For AES, the decryption process will decrypt the 3rd block and XOR it with the encrypted 2nd block. By calling enc_userdata('000000000000000000000:admin@true')
, we find that our 3rd block is:
o = b'Z\x93\xd28\xfa\xd9G\xbex\xe7\xda\x0f=.\xf9\xec'
For CBC, the decryption mechanism is such that P2 = C1 ^ D(C2)
- that is, the plaintext for the 2nd block is the ciphertext of block 1 xor’ed with the decryption of block 2’s ciphertext.
Anything xor’ed with itself is zero so 0 = P2 ^ C1 ^ D(C2)
- if we define P'2
as our tampered plaintext for block 2, we have:
P'2 = P'2 ^ P2 ^ C1 ^ D(C2)
What does that mean? We don’t know what D(C2)
decrypts to - but we know it will get xor’ed with C1
- and that if we replace C1
with C'1 = P'2 ^ P2 ^ C1
, then P'2 = C'1 ^ D(C2)
.
Which we can easily get by setting the relevant bytes to o[5] ^ ord(';') ^ ord(':')
and o[11] ^ ord('=') ^ ord('@')
respectively.
And voila - we don’t know the key and yet managed to modify the plaintext.