Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions challenges/cryptography/.gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*/tests_private/** filter=git-crypt-cryptography diff=git-crypt-cryptography
4 changes: 4 additions & 0 deletions challenges/cryptography/aes-cbc-corrupt-resize/.init
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/bin/bash

dd if=/dev/urandom of=/challenge/.key bs=16 count=1
chmod 600 /challenge/.key
8 changes: 8 additions & 0 deletions challenges/cryptography/aes-cbc-corrupt-resize/DESCRIPTION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
So now you can modify AES-CBC encrypted data without knowing the key!
But you got lucky: `sleep` and `flag!` were the same length.
What if you want to achieve a different length?

----
**HINT:**
Don't forget about the padding!
How does the padding work?
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/usr/bin/exec-suid -- /usr/bin/python3 -I

import os

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from Crypto.Random import get_random_bytes

key = open("/challenge/.key", "rb").read()
cipher = AES.new(key=key, mode=AES.MODE_CBC)
ciphertext = cipher.iv + cipher.encrypt(pad(b"sleep", cipher.block_size))

print(f"TASK: {ciphertext.hex()}")
34 changes: 34 additions & 0 deletions challenges/cryptography/aes-cbc-corrupt-resize/challenge/worker
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#!/usr/bin/exec-suid -- /usr/bin/python3 -I

from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
from Crypto.Random import get_random_bytes

import time
import sys

key = open("/challenge/.key", "rb").read()

while line := sys.stdin.readline():
if not line.startswith("TASK: "):
continue
data = bytes.fromhex(line.split()[1])
iv, ciphertext = data[:16], data[16:]

cipher = AES.new(key=key, mode=AES.MODE_CBC, iv=iv)
try:
plaintext = unpad(cipher.decrypt(ciphertext), cipher.block_size).decode('latin1')
except ValueError as e:
print("Error:", e)
continue

print(f"Hex of plaintext: {plaintext.encode('latin1').hex()}")
print(f"Received command: {plaintext}")
if plaintext == "sleep":
print("Sleeping!")
time.sleep(1)
elif plaintext == "flag":
print("Victory! Your flag:")
print(open("/flag").read())
else:
print("Unknown command!")
4 changes: 4 additions & 0 deletions challenges/cryptography/aes-cbc-corrupt/.init
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/bin/bash

dd if=/dev/urandom of=/challenge/.key bs=16 count=1
chmod 600 /challenge/.key
15 changes: 15 additions & 0 deletions challenges/cryptography/aes-cbc-corrupt/DESCRIPTION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
CBC-based cryptosystems XOR the previous block's *ciphertext* to recover the plaintext of a block after decryption.
This done for many reasons, including:

1. This XOR is what separates it from ECB mode, and we've seen how fallible ECB is.
2. If it XORed the _plaintext_ of the previous block instead of the ciphertext, the efficacy would be dependent on the plaintext itself (for example, if the plaintext was all null bytes, the XOR would have no effect). Aside from reducing the chaining effectiveness, this could leak information about the plaintext (big no no in cryptosystems)!
3. If it XORed the plaintext of the previous block instead of the ciphertext, the "random access" property of CBC, where the recipient of a message can decrypt starting from any block, would be lost. The recipient would have to recover the previous plaintext, for which they would have to recover the one before that, and so on all the way to the IV.

Unfortunately, in situations where the message could be modified in transit (think: Intercepting Communications), a crafty attacker could directly influence the resulting decrypted plaintext of block N by XORing carefully-chosen values into the ciphertext of block N-1.
This would corrupt block N-1 (because it would decrypt to garbage), but depending on the specific situation, this might be acceptable.
Moreover, doing this to the IV allows the attacker to XOR the plaintext of the first block without corrupting any block!

In security terms, CBC preserves (imperfectly, as we'll see in the next few challenges) Confidentiality, but does not preserve Integrity: the messages can be tampered with by an attacker!

We will explore this concept in this level, where a task dispatcher will dispatch encrypted tasks to a task worker.
Can you force a flag disclosure?
13 changes: 13 additions & 0 deletions challenges/cryptography/aes-cbc-corrupt/challenge/dispatcher
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/usr/bin/exec-suid -- /usr/bin/python3 -I

import os

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from Crypto.Random import get_random_bytes

key = open("/challenge/.key", "rb").read()
cipher = AES.new(key=key, mode=AES.MODE_CBC)
ciphertext = cipher.iv + cipher.encrypt(pad(b"sleep", cipher.block_size))

print(f"TASK: {ciphertext.hex()}")
34 changes: 34 additions & 0 deletions challenges/cryptography/aes-cbc-corrupt/challenge/worker
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#!/usr/bin/exec-suid -- /usr/bin/python3 -I

from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
from Crypto.Random import get_random_bytes

import time
import sys

key = open("/challenge/.key", "rb").read()

while line := sys.stdin.readline():
if not line.startswith("TASK: "):
continue
data = bytes.fromhex(line.split()[1])
iv, ciphertext = data[:16], data[16:]

cipher = AES.new(key=key, mode=AES.MODE_CBC, iv=iv)
try:
plaintext = unpad(cipher.decrypt(ciphertext), cipher.block_size).decode('latin1')
except ValueError as e:
print("Error:", e)
continue

print(f"Hex of plaintext: {plaintext.encode('latin1').hex()}")
print(f"Received command: {plaintext}")
if plaintext == "sleep":
print("Sleeping!")
time.sleep(1)
elif plaintext == "flag!":
print("Victory! Your flag:")
print(open("/flag").read())
else:
print("Unknown command!")
4 changes: 4 additions & 0 deletions challenges/cryptography/aes-cbc-poa-enc-nodispatch/.init
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/bin/bash

dd if=/dev/urandom of=/challenge/.key bs=16 count=1
chmod 600 /challenge/.key
12 changes: 12 additions & 0 deletions challenges/cryptography/aes-cbc-poa-enc-nodispatch/DESCRIPTION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Now, you've previously started from a single valid input (the encrypted `sleep` command).
What if you have _zero_ valid inputs?
Turns out that all this still works!

Why?
Random data decrypts to ... some other random data.
Likely, this has a padding error.
You can control the IV just like before to figure out the right 16th byte to xor in to resolve that padding error, and now you have a ciphertext that represents a 15-byte random message.
For you, there's no real difference between that random message and `sleep`: the attack is the same!

Go try this now.
No dispatcher, just you and the flag.
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#!/usr/bin/exec-suid -- /usr/bin/python3 -I

from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
from Crypto.Random import get_random_bytes

import time
import sys

key = open("/challenge/.key", "rb").read()

while line := sys.stdin.readline():
if not line.startswith("TASK: "):
continue
data = bytes.fromhex(line.split()[1])
iv, ciphertext = data[:16], data[16:]

cipher = AES.new(key=key, mode=AES.MODE_CBC, iv=iv)
try:
plaintext = unpad(cipher.decrypt(ciphertext), cipher.block_size).decode('latin1')
except ValueError as e:
print("Error:", e)
continue

if plaintext == "sleep":
print("Sleeping!")
time.sleep(1)
elif plaintext == "please give me the flag, kind worker process!":
print("Victory! Your flag:")
print(open("/flag").read())
else:
print("Unknown command!")
4 changes: 4 additions & 0 deletions challenges/cryptography/aes-cbc-poa-enc/.init
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/bin/bash

dd if=/dev/urandom of=/challenge/.key bs=16 count=1
chmod 600 /challenge/.key
19 changes: 19 additions & 0 deletions challenges/cryptography/aes-cbc-poa-enc/DESCRIPTION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
You're not going to believe this, but... a Padding Oracle Attack doesn't just let you decrypt arbitrary messages: it lets you _encrypt_ arbitrary data as well!
This sounds too wild to be true, but it is.
Think about it: you demonstrated the ability to modify bytes in a block by messing with the previous block's ciphertext.
Unfortunately, this will make the previous block decrypt to garbage.
But is that so bad?
You can use a padding oracle attack to recover the exact values of this garbage, and mess with the block before that to fix this garbage plaintext to be valid data!
Keep going, and you can craft fully controlled, arbitrarily long messages, all without knowing the key!
When you get to the IV, just treat it as a ciphertext block (e.g., plop a fake IV in front of it and decrypt it as usual) and keep going!
Incredible.

Now, you have the knowledge you need to get the flag for this challenge.
Go forth and forge your message!

----
**FUN FACT:**
Though the Padding Oracle Attack was [discovered](https://www.iacr.org/archive/eurocrypt2002/23320530/cbc02_e02d.pdf) in 2002, it wasn't until 2010 that researchers [figured out this arbitrary encryption ability](https://static.usenix.org/events/woot10/tech/full_papers/Rizzo.pdf).
Imagine how vulnerable the web was for those 8 years!
Unfortunately, padding oracle attacks are _still_ a problem.
Padding Oracle vulnerabilities come up every few months in web infrastructure, with the latest (as of time of writing) [just a few weeks ago](https://www.cvedetails.com/cve/CVE-2024-45384/)!
13 changes: 13 additions & 0 deletions challenges/cryptography/aes-cbc-poa-enc/challenge/dispatcher
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/usr/bin/exec-suid -- /usr/bin/python3 -I

import os

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from Crypto.Random import get_random_bytes

key = open("/challenge/.key", "rb").read()
cipher = AES.new(key=key, mode=AES.MODE_CBC)
ciphertext = cipher.iv + cipher.encrypt(pad(b"sleep", cipher.block_size))

print(f"TASK: {ciphertext.hex()}")
32 changes: 32 additions & 0 deletions challenges/cryptography/aes-cbc-poa-enc/challenge/worker
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#!/usr/bin/exec-suid -- /usr/bin/python3 -I

from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
from Crypto.Random import get_random_bytes

import time
import sys

key = open("/challenge/.key", "rb").read()

while line := sys.stdin.readline():
if not line.startswith("TASK: "):
continue
data = bytes.fromhex(line.split()[1])
iv, ciphertext = data[:16], data[16:]

cipher = AES.new(key=key, mode=AES.MODE_CBC, iv=iv)
try:
plaintext = unpad(cipher.decrypt(ciphertext), cipher.block_size).decode('latin1')
except ValueError as e:
print("Error:", e)
continue

if plaintext == "sleep":
print("Sleeping!")
time.sleep(1)
elif plaintext == "please give me the flag, kind worker process!":
print("Victory! Your flag:")
print(open("/flag").read())
else:
print("Unknown command!")
5 changes: 5 additions & 0 deletions challenges/cryptography/aes-cbc-poa-fullblock/.init
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/bin/bash

dd if=/dev/urandom of=/challenge/.key bs=16 count=1
cat /dev/urandom | tr -cd '0-9A-Za-z' | head -c16 > /challenge/.pw
chmod 600 /challenge/.key /challenge/.pw
20 changes: 20 additions & 0 deletions challenges/cryptography/aes-cbc-poa-fullblock/DESCRIPTION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
The previous challenge had you decrypting a partial block by abusing the padding at the end.
But what happens if the block is "full", as in, 16-bytes long?
Let's explore an example with the plaintext `AAAABBBBCCCCDDDD`, which is 16 bytes long!
As you recall, PKCS7 adds a whole block of padding in this scenario!
What we would see after padding is:

| Plaintext Block 1 | Plaintext Block 2 (oops, just padding!) |
|--------------------|--------------------------------------------------------------------|
| `AAAABBBBCCCCDDDD` | `\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10` |

When encrypted, we'd end up with three blocks:

| Ciphertext Block 1 | Ciphertext Block 2 | Ciphertext Block 3 |
|--------------------|--------------------|--------------------|
| IV | Encrypted `AAAABBBBCCCCDDDD` | Encrypted Padding |

If you know that the plaintext length is aligned to the block length like in the above example, you already know the plaintext of the last block (it's just the padding!).
Once you know it's all just padding, you can discard it and start attacking the next-to-last block (in this example, Ciphertext Block 2)!
You'd try tampering with the last byte of the plaintext (by messing with the IV that gets XORed into it) until you got a successful padding, then use that to recover (and be able to control) the last byte, then go from there.
The same POA attack, but against the _second-to-last_ block when the last block is all padding!
17 changes: 17 additions & 0 deletions challenges/cryptography/aes-cbc-poa-fullblock/challenge/dispatcher
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/usr/bin/exec-suid -- /usr/bin/python3 -I

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad

import sys

key = open("/challenge/.key", "rb").read()
cipher = AES.new(key=key, mode=AES.MODE_CBC)

if len(sys.argv) > 1 and sys.argv[1] == "pw":
plaintext = open("/challenge/.pw", "rb").read().strip()
else:
plaintext = b"sleep"

ciphertext = cipher.iv + cipher.encrypt(pad(plaintext, cipher.block_size))
print(f"TASK: {ciphertext.hex()}")
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/exec-suid -- /usr/bin/python3 -I

if input("Password? ").strip() == open("/challenge/.pw").read().strip():
print("Victory! Your flag:")
print(open("/flag").read())
34 changes: 34 additions & 0 deletions challenges/cryptography/aes-cbc-poa-fullblock/challenge/worker
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#!/usr/bin/exec-suid -- /usr/bin/python3 -I

from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
from Crypto.Random import get_random_bytes

import time
import sys

key = open("/challenge/.key", "rb").read()
pw = open("/challenge/.pw").read().strip()

print(f"The password is {len(pw)} bytes long!")

while line := sys.stdin.readline():
if not line.startswith("TASK: "):
continue
data = bytes.fromhex(line.split()[1])
iv, ciphertext = data[:16], data[16:]

cipher = AES.new(key=key, mode=AES.MODE_CBC, iv=iv)
try:
plaintext = unpad(cipher.decrypt(ciphertext), cipher.block_size).decode('latin1')
except ValueError as e:
print("Error:", e)
continue

if plaintext == "sleep":
print("Sleeping!")
time.sleep(1)
elif plaintext == pw:
print("Correct! Use /challenge/redeem to redeem the password for the flag!")
else:
print("Unknown command!")
5 changes: 5 additions & 0 deletions challenges/cryptography/aes-cbc-poa-singleblock/.init
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/bin/bash

dd if=/dev/urandom of=/challenge/.key bs=16 count=1
cat /dev/urandom | tr -cd '0-9A-Za-z' | head -c$((RANDOM%8+8)) > /challenge/.pw
chmod 600 /challenge/.key /challenge/.pw
32 changes: 32 additions & 0 deletions challenges/cryptography/aes-cbc-poa-singleblock/DESCRIPTION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
So you can manipulate the padding...
If you messed up somewhere along the lines of the previous challenge and created an invalid padding, you might have noticed that the worker _crashed_ with an error about the padding being incorrect!

It turns out that this one crash _completely_ breaks the Confidentiality of the AES-CBC cryptosystem, allowing attackers to decrypt messages without having the key.
Let's dig in...

Recall that PKCS7 padding adds N bytes with the value N, so if 11 bytes of padding were added, they have the value `0x0b`.
During unpadding, PKCS7 will read the value N of the last byte, make sure that the last N bytes (including that last byte) have that same value, and remove those bytes.
If the value N is bigger than the block size, or the bytes don't all have the value N, most implementations of PKCS7, including the one provided by PyCryptoDome, will error.

Consider how careful you had to be in the previous level with the padding, and how this required you to know the letter you wanted to remove.
What if you didn't know that letter?
Your random guesses at what to XOR it with would cause an error 255 times out of 256 (as long as you handled the rest of the padding properly, of course), and the one time it did not, by known what the final padding had to be and what your XOR value was, you can recover the letter value!
This is called a [_Padding Oracle Attack_](https://en.wikipedia.org/wiki/Padding_oracle_attack), after the "oracle" (error) that tells you if your padding was correct!

Of course, once you remove (and learn) the last byte of the plaintext, the second-to-last byte becomes the last byte, and you can attack it!

So, what are you waiting for?
Go recover the flag!

----
**FUN FACT:**
The only way to prevent a Padding Oracle Attack is to avoid having a Padding Oracle.
Depending on the application, this can be surprisingly tricky: a failure state is hard to mask completely from the user/attacker of the application, and for some applications, the padding failure is the only source of an error state!
Moreover, even if the error itself is hidden from the user/attacker, it's often _inferable_ indirectly (e.g., by detecting timing differences between the padding error and padding success cases).

**RESOURCES:**
You might find some animated/interactive POA demonstrations useful:

- [An Animated Primer from CryptoPals](https://www.nccgroup.com/us/research-blog/cryptopals-exploiting-cbc-padding-oracles/)
- [Another Animated Primer](https://dylanpindur.com/blog/padding-oracles-an-animated-primer/)
- [An Interactive POA Explorer](https://paddingoracle.github.io/)
Loading
Loading