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
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,3 @@
#!/bin/bash
dd if=/dev/urandom of=/challenge/.key bs=16 count=1
chmod 600 /challenge/.key
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{% include "common/Dockerfile.j2" %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{% set chal = namespace(plaintext=b"sleep") %}
{% extends "common/crypto_aes_cbc_dispatcher.py.j2" %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{% set chal = namespace(has_pw=False, accept_phrase="flag") %}
{% extends "common/crypto_aes_cbc_worker.py.j2" %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/usr/bin/env python3
import subprocess

p = subprocess.Popen(
["/challenge/dispatcher.py"],
stdout=subprocess.PIPE,
text=True,
)
assert "TASK:" in p.stdout.readline()
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?
3 changes: 3 additions & 0 deletions challenges/cryptography/aes-cbc-corrupt/challenge/.init
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/bash
dd if=/dev/urandom of=/challenge/.key bs=16 count=1
chmod 600 /challenge/.key
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{% include "common/Dockerfile.j2" %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{% set chal = namespace(plaintext=b"sleep") %}
{% extends "common/crypto_aes_cbc_dispatcher.py.j2" %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{% set chal = namespace(has_pw=False, accept_phrase="flag!") %}
{% extends "common/crypto_aes_cbc_worker.py.j2" %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/usr/bin/env python3
import subprocess

p = subprocess.Popen(
["/challenge/dispatcher.py"],
stdout=subprocess.PIPE,
text=True,
)
assert "TASK:" in p.stdout.readline()
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,3 @@
#!/bin/bash
dd if=/dev/urandom of=/challenge/.key bs=16 count=1
chmod 600 /challenge/.key
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{% include "common/Dockerfile.j2" %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{% set chal = namespace(
has_pw=False,
accept_phrase="please give me the flag, kind worker process!"
) %}
{% extends "common/crypto_aes_cbc_worker.py.j2" %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/usr/bin/env python3
import subprocess

p = subprocess.Popen(
["/challenge/worker.py"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
text=True,
)
p.stdin.write("TASK: 00\n")
p.stdin.flush()
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/)!
3 changes: 3 additions & 0 deletions challenges/cryptography/aes-cbc-poa-enc/challenge/.init
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/bash
dd if=/dev/urandom of=/challenge/.key bs=16 count=1
chmod 600 /challenge/.key
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{% include "common/Dockerfile.j2" %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{% set chal = namespace(plaintext=b"sleep") %}
{% extends "common/crypto_aes_cbc_dispatcher.py.j2" %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{% set chal = namespace(
has_pw=False,
accept_phrase="please give me the flag, kind worker process!"
) %}
{% extends "common/crypto_aes_cbc_worker.py.j2" %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/usr/bin/env python3
import subprocess

p = subprocess.Popen(
["/challenge/dispatcher.py"],
stdout=subprocess.PIPE,
text=True,
)
assert "TASK:" in p.stdout.readline()
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!
4 changes: 4 additions & 0 deletions challenges/cryptography/aes-cbc-poa-fullblock/challenge/.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
cat /dev/urandom | tr -cd '0-9A-Za-z' | head -c16 > /challenge/.pw
chmod 600 /challenge/.key /challenge/.pw
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{% include "common/Dockerfile.j2" %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{% set chal = namespace(plaintext=b"sleep") %}
{% extends "common/crypto_aes_cbc_dispatcher.py.j2" %}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{% extends "common/crypto_aes_cbc_redeem.py.j2" %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{% set chal = namespace(has_pw=True, accept_phrase=None) %}
{% extends "common/crypto_aes_cbc_worker.py.j2" %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/usr/bin/env python3
import subprocess

p = subprocess.Popen(
["/challenge/dispatcher.py"],
stdout=subprocess.PIPE,
text=True,
)
assert "TASK:" in p.stdout.readline()
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/)
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
cat /dev/urandom | tr -cd '0-9A-Za-z' | head -c16 > /challenge/.pw
chmod 600 /challenge/.key /challenge/.pw
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{% include "common/Dockerfile.j2" %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{% set chal = namespace(plaintext=b"sleep") %}
{% extends "common/crypto_aes_cbc_dispatcher.py.j2" %}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{% extends "common/crypto_aes_cbc_redeem.py.j2" %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{% set chal = namespace(has_pw=True, accept_phrase=None) %}
{% extends "common/crypto_aes_cbc_worker.py.j2" %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/usr/bin/env python3
import subprocess

p = subprocess.Popen(
["/challenge/dispatcher.py"],
stdout=subprocess.PIPE,
text=True,
)
assert "TASK:" in p.stdout.readline()
11 changes: 11 additions & 0 deletions challenges/cryptography/aes-cbc-poa/DESCRIPTION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Let's put the last two challenges together.
The previous challenges had just one ciphertext block, whether it started like that or you quickly got there by discarding the all-padding block.
Thus, you were able to mess with that block's plaintext by chaining up the IV.

This level encrypts the actual flag, and thus has multiple blocks that actually have data.
Keep in mind that to mess with the decryption of block N, you must modify ciphertext N-1.
For the first block, this is the IV, but not for the rest!

This is one of the hardest challenges in this module, but you can get your head around if you take it step by step.
So, what are you waiting for?
Go recover the flag!
3 changes: 3 additions & 0 deletions challenges/cryptography/aes-cbc-poa/challenge/.init
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/bash
dd if=/dev/urandom of=/challenge/.key bs=16 count=1
chmod 600 /challenge/.key
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{% include "common/Dockerfile.j2" %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{% set chal = namespace(plaintext=b"sleep") %}
{% extends "common/crypto_aes_cbc_dispatcher.py.j2" %}
2 changes: 2 additions & 0 deletions challenges/cryptography/aes-cbc-poa/challenge/worker.py.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{% set chal = namespace(has_pw=False, accept_phrase=None) %}
{% extends "common/crypto_aes_cbc_worker.py.j2" %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/usr/bin/env python3
import subprocess

p = subprocess.Popen(
["/challenge/dispatcher.py"],
stdout=subprocess.PIPE,
text=True,
)
assert "TASK:" in p.stdout.readline()
21 changes: 21 additions & 0 deletions challenges/cryptography/aes-cbc/DESCRIPTION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
Okay, hopefully we agree that ECB is a bad block cipher mode.
Let's explore one that isn't _so_ bad: [Cipher Block Chaining (CBC)](https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher_block_chaining_(CBC)).
CBC mode encrypts blocks sequentially, and before encrypting plaintext block number N, it XORs it with the previous ciphertext block (number N-1).
When decrypting, after decrypting ciphertext block N, it XORs the decrypted (but still XORed) result with the previous ciphertext block (number N-1) to recover the original plaintext block N.
For the very first block, since there is no "previous" block to use, CBC cryptosystems generate a random initial block called an [_Initialization Vector_ (IV)](https://en.wikipedia.org/wiki/Initialization_vector).
The IV is used to XOR the first block of plaintext, and is transmitted along with the message (often prepended to it).
This means that if you encrypt one block of plaintext in CBC mode, you might get _two_ blocks of "ciphertext": the IV, and your single block of actual ciphertext.

All this means that, when you change any part of the plaintext, those changes will propagate through to all subsequent ciphertext blocks because of the XOR-based chaining, preserving ciphertext indistinguishability for those blocks.
That will stop you from carrying out the chosen-plaintext prefix attacks from the last few challenges.
Moreover, every time you re-encrypt, even with the same key, a new (random) IV will be used, which will propagate changes to all of the blocks anyways, which means that even your sampling-based CPA attacks from the even earlier levels will not work, either.

Sounds pretty good, right?
The only relevant _disadvantage_ that CBC has over EBC is that encryption has to happen sequentially.
With ECB, you could encrypt, say, only the last part of the message if that's all you have to send.
With CBC, you must encrypt the message from the beginning.
In practice, this does not tend to be a problem, and ECB should never be used over CBC.

This level is just a quick look at CBC.
We'll encrypt the flag with CBC mode.
Go and decrypt it!
1 change: 1 addition & 0 deletions challenges/cryptography/aes-cbc/challenge/Dockerfile.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{% include "common/Dockerfile.j2" %}
1 change: 1 addition & 0 deletions challenges/cryptography/aes-cbc/challenge/run.py.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{% extends "common/crypto_aes_cbc_simple.py.j2" %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/usr/bin/env python3
import subprocess

p = subprocess.Popen(
["/challenge/run.py"],
stdout=subprocess.PIPE,
text=True,
)
assert "TASK:" in p.stdout.readline()
Comment on lines +5 to +9

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge aes-cbc test expects nonexistent TASK prefix

The aes-cbc public test asserts the first line from /challenge/run.py contains "TASK:", but the template used by that run script (common/crypto_aes_cbc_simple.py.j2) prints only the AES key and ciphertext headers, never a TASK prefix. As written the assertion will always fail on the very first read, so the challenge can't pass its own public tests.

Useful? React with 👍 / 👎.

31 changes: 31 additions & 0 deletions challenges/cryptography/aes/DESCRIPTION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
So, One Time Pads fail when you reuse them.
This is suboptimal: given how careful one has to be when transferring keys, it would be better if the key could be used for more than just a single message!

Enter: the [Advanced Encryption Standard](https://en.wikipedia.org/wiki/Advanced_Encryption_Standard), AES.
AES is relatively new: coming on the scene in 2001.
Like a One-time Pad, AES is _also_ symmetric: the same key is used to encrypt and decrypt.
Unlike a One-time Pad, AES maintains security for multiple messages encrypted with the same key.

In this challenge you will decrypt a secret encrypted with Advanced Encryption Standard (AES).
AES is what is called a "block cipher", encrypting one plaintext "block" of 16 bytes (128 bits) at a time.
So `AAAABBBBCCCCDDDD` would be a single block of plaintext that would be encrypted into a single block of ciphertext.

AES _must_ operate on complete blocks.
If the plaintext is _shorter_ than a block (e.g., `AAAABBBB`), it will be _padded_ to the block size, and the padded plaintext will be encrypted.

Different AES "modes" define what to do when the plaintext is longer than one block.
In this challenge, we are using the simplest mode: "[Electronic Codebook (ECB)](https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Electronic_codebook_(ECB))".
In ECB, each block is encrypted separately with the same key and simply concatenated together.
So if you are encrypting something like `AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHH`, it will be split into two plaintext blocks (`AAAABBBBCCCCDDDD` and `EEEEFFFFGGGGHHHH`), encrypted separately (resulting, let's imagine, in `UVSDFGIWEHFBFFCA` and `LKXBFVYASLJDEWEU`), then concatenated (resulting the ciphertext `UVSDFGIWEHFBFFCALKXBFVYASLJDEWEU`).

This challenge will give you the AES-encrypted flag and the key used to encrypt it.
We won't learn about the internals of AES, in terms of how it actually encrypts the raw bytes.
Instead, we'll learn about different _applications_ of AES, and how they break down in practice.
If you're interested in learning about AES internals, we can highly recommend [CryptoHack](https://cryptohack.org/courses/), an amazing learning resource that focuses on the nitty gritty details of crypto!

Now, go decrypt the flag and score!

----
**HINT:**
We use the [PyCryptoDome](https://www.pycryptodome.org/) library to implement the encryption in this level.
You'll want to read its documentation to figure out how to implement your decryption!
1 change: 1 addition & 0 deletions challenges/cryptography/aes/challenge/Dockerfile.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{% include "common/Dockerfile.j2" %}
14 changes: 14 additions & 0 deletions challenges/cryptography/aes/challenge/run.py.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/usr/bin/exec-suid -- /usr/bin/python3 -I

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

flag = open("/flag", "rb").read()

key = get_random_bytes(16)
cipher = AES.new(key=key, mode=AES.MODE_ECB)
ciphertext = cipher.encrypt(pad(flag, cipher.block_size))

print(f"AES Key (hex): {key.hex()}")
print(f"Flag Ciphertext (hex): {ciphertext.hex()}")
6 changes: 6 additions & 0 deletions challenges/cryptography/aes/tests_public/test_normal.py.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/usr/bin/env python3
import subprocess

out = subprocess.check_output(["/challenge/run.py"], text=True)
assert "AES Key" in out
assert "Flag Ciphertext" in out
Loading