diff --git a/challenges/cryptography/.gitattributes b/challenges/cryptography/.gitattributes new file mode 100644 index 0000000..1e5064c --- /dev/null +++ b/challenges/cryptography/.gitattributes @@ -0,0 +1 @@ +*/tests_private/** filter=git-crypt-cryptography diff=git-crypt-cryptography diff --git a/challenges/cryptography/aes-cbc-corrupt-resize/.init b/challenges/cryptography/aes-cbc-corrupt-resize/.init new file mode 100755 index 0000000..fc5fecd --- /dev/null +++ b/challenges/cryptography/aes-cbc-corrupt-resize/.init @@ -0,0 +1,4 @@ +#!/bin/bash + +dd if=/dev/urandom of=/challenge/.key bs=16 count=1 +chmod 600 /challenge/.key diff --git a/challenges/cryptography/aes-cbc-corrupt-resize/DESCRIPTION.md b/challenges/cryptography/aes-cbc-corrupt-resize/DESCRIPTION.md new file mode 100644 index 0000000..77be5fb --- /dev/null +++ b/challenges/cryptography/aes-cbc-corrupt-resize/DESCRIPTION.md @@ -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? diff --git a/challenges/cryptography/aes-cbc-corrupt-resize/challenge/dispatcher b/challenges/cryptography/aes-cbc-corrupt-resize/challenge/dispatcher new file mode 100755 index 0000000..6665c2d --- /dev/null +++ b/challenges/cryptography/aes-cbc-corrupt-resize/challenge/dispatcher @@ -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()}") diff --git a/challenges/cryptography/aes-cbc-corrupt-resize/challenge/worker b/challenges/cryptography/aes-cbc-corrupt-resize/challenge/worker new file mode 100755 index 0000000..a5d347c --- /dev/null +++ b/challenges/cryptography/aes-cbc-corrupt-resize/challenge/worker @@ -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!") diff --git a/challenges/cryptography/aes-cbc-corrupt/.init b/challenges/cryptography/aes-cbc-corrupt/.init new file mode 100755 index 0000000..fc5fecd --- /dev/null +++ b/challenges/cryptography/aes-cbc-corrupt/.init @@ -0,0 +1,4 @@ +#!/bin/bash + +dd if=/dev/urandom of=/challenge/.key bs=16 count=1 +chmod 600 /challenge/.key diff --git a/challenges/cryptography/aes-cbc-corrupt/DESCRIPTION.md b/challenges/cryptography/aes-cbc-corrupt/DESCRIPTION.md new file mode 100644 index 0000000..a7a89ce --- /dev/null +++ b/challenges/cryptography/aes-cbc-corrupt/DESCRIPTION.md @@ -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? diff --git a/challenges/cryptography/aes-cbc-corrupt/challenge/dispatcher b/challenges/cryptography/aes-cbc-corrupt/challenge/dispatcher new file mode 100755 index 0000000..6665c2d --- /dev/null +++ b/challenges/cryptography/aes-cbc-corrupt/challenge/dispatcher @@ -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()}") diff --git a/challenges/cryptography/aes-cbc-corrupt/challenge/worker b/challenges/cryptography/aes-cbc-corrupt/challenge/worker new file mode 100755 index 0000000..af73295 --- /dev/null +++ b/challenges/cryptography/aes-cbc-corrupt/challenge/worker @@ -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!") diff --git a/challenges/cryptography/aes-cbc-poa-enc-nodispatch/.init b/challenges/cryptography/aes-cbc-poa-enc-nodispatch/.init new file mode 100755 index 0000000..fc5fecd --- /dev/null +++ b/challenges/cryptography/aes-cbc-poa-enc-nodispatch/.init @@ -0,0 +1,4 @@ +#!/bin/bash + +dd if=/dev/urandom of=/challenge/.key bs=16 count=1 +chmod 600 /challenge/.key diff --git a/challenges/cryptography/aes-cbc-poa-enc-nodispatch/DESCRIPTION.md b/challenges/cryptography/aes-cbc-poa-enc-nodispatch/DESCRIPTION.md new file mode 100644 index 0000000..389a074 --- /dev/null +++ b/challenges/cryptography/aes-cbc-poa-enc-nodispatch/DESCRIPTION.md @@ -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. diff --git a/challenges/cryptography/aes-cbc-poa-enc-nodispatch/challenge/worker b/challenges/cryptography/aes-cbc-poa-enc-nodispatch/challenge/worker new file mode 100755 index 0000000..204c5a0 --- /dev/null +++ b/challenges/cryptography/aes-cbc-poa-enc-nodispatch/challenge/worker @@ -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!") diff --git a/challenges/cryptography/aes-cbc-poa-enc/.init b/challenges/cryptography/aes-cbc-poa-enc/.init new file mode 100755 index 0000000..fc5fecd --- /dev/null +++ b/challenges/cryptography/aes-cbc-poa-enc/.init @@ -0,0 +1,4 @@ +#!/bin/bash + +dd if=/dev/urandom of=/challenge/.key bs=16 count=1 +chmod 600 /challenge/.key diff --git a/challenges/cryptography/aes-cbc-poa-enc/DESCRIPTION.md b/challenges/cryptography/aes-cbc-poa-enc/DESCRIPTION.md new file mode 100644 index 0000000..5a25d74 --- /dev/null +++ b/challenges/cryptography/aes-cbc-poa-enc/DESCRIPTION.md @@ -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/)! diff --git a/challenges/cryptography/aes-cbc-poa-enc/challenge/dispatcher b/challenges/cryptography/aes-cbc-poa-enc/challenge/dispatcher new file mode 100755 index 0000000..6665c2d --- /dev/null +++ b/challenges/cryptography/aes-cbc-poa-enc/challenge/dispatcher @@ -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()}") diff --git a/challenges/cryptography/aes-cbc-poa-enc/challenge/worker b/challenges/cryptography/aes-cbc-poa-enc/challenge/worker new file mode 100755 index 0000000..204c5a0 --- /dev/null +++ b/challenges/cryptography/aes-cbc-poa-enc/challenge/worker @@ -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!") diff --git a/challenges/cryptography/aes-cbc-poa-fullblock/.init b/challenges/cryptography/aes-cbc-poa-fullblock/.init new file mode 100755 index 0000000..c6d3296 --- /dev/null +++ b/challenges/cryptography/aes-cbc-poa-fullblock/.init @@ -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 diff --git a/challenges/cryptography/aes-cbc-poa-fullblock/DESCRIPTION.md b/challenges/cryptography/aes-cbc-poa-fullblock/DESCRIPTION.md new file mode 100644 index 0000000..90bdb1b --- /dev/null +++ b/challenges/cryptography/aes-cbc-poa-fullblock/DESCRIPTION.md @@ -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! diff --git a/challenges/cryptography/aes-cbc-poa-fullblock/challenge/dispatcher b/challenges/cryptography/aes-cbc-poa-fullblock/challenge/dispatcher new file mode 100755 index 0000000..67f8ff9 --- /dev/null +++ b/challenges/cryptography/aes-cbc-poa-fullblock/challenge/dispatcher @@ -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()}") diff --git a/challenges/cryptography/aes-cbc-poa-fullblock/challenge/redeem b/challenges/cryptography/aes-cbc-poa-fullblock/challenge/redeem new file mode 100755 index 0000000..f779611 --- /dev/null +++ b/challenges/cryptography/aes-cbc-poa-fullblock/challenge/redeem @@ -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()) diff --git a/challenges/cryptography/aes-cbc-poa-fullblock/challenge/worker b/challenges/cryptography/aes-cbc-poa-fullblock/challenge/worker new file mode 100755 index 0000000..0fb7c52 --- /dev/null +++ b/challenges/cryptography/aes-cbc-poa-fullblock/challenge/worker @@ -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!") diff --git a/challenges/cryptography/aes-cbc-poa-singleblock/.init b/challenges/cryptography/aes-cbc-poa-singleblock/.init new file mode 100755 index 0000000..b09ed5c --- /dev/null +++ b/challenges/cryptography/aes-cbc-poa-singleblock/.init @@ -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 diff --git a/challenges/cryptography/aes-cbc-poa-singleblock/DESCRIPTION.md b/challenges/cryptography/aes-cbc-poa-singleblock/DESCRIPTION.md new file mode 100644 index 0000000..c6a94a2 --- /dev/null +++ b/challenges/cryptography/aes-cbc-poa-singleblock/DESCRIPTION.md @@ -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/) diff --git a/challenges/cryptography/aes-cbc-poa-singleblock/challenge/dispatcher b/challenges/cryptography/aes-cbc-poa-singleblock/challenge/dispatcher new file mode 100755 index 0000000..67f8ff9 --- /dev/null +++ b/challenges/cryptography/aes-cbc-poa-singleblock/challenge/dispatcher @@ -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()}") diff --git a/challenges/cryptography/aes-cbc-poa-singleblock/challenge/redeem b/challenges/cryptography/aes-cbc-poa-singleblock/challenge/redeem new file mode 100755 index 0000000..f779611 --- /dev/null +++ b/challenges/cryptography/aes-cbc-poa-singleblock/challenge/redeem @@ -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()) diff --git a/challenges/cryptography/aes-cbc-poa-singleblock/challenge/worker b/challenges/cryptography/aes-cbc-poa-singleblock/challenge/worker new file mode 100755 index 0000000..0fb7c52 --- /dev/null +++ b/challenges/cryptography/aes-cbc-poa-singleblock/challenge/worker @@ -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!") diff --git a/challenges/cryptography/aes-cbc-poa/.init b/challenges/cryptography/aes-cbc-poa/.init new file mode 100755 index 0000000..fc5fecd --- /dev/null +++ b/challenges/cryptography/aes-cbc-poa/.init @@ -0,0 +1,4 @@ +#!/bin/bash + +dd if=/dev/urandom of=/challenge/.key bs=16 count=1 +chmod 600 /challenge/.key diff --git a/challenges/cryptography/aes-cbc-poa/DESCRIPTION.md b/challenges/cryptography/aes-cbc-poa/DESCRIPTION.md new file mode 100644 index 0000000..9d591f2 --- /dev/null +++ b/challenges/cryptography/aes-cbc-poa/DESCRIPTION.md @@ -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! diff --git a/challenges/cryptography/aes-cbc-poa/challenge/dispatcher b/challenges/cryptography/aes-cbc-poa/challenge/dispatcher new file mode 100755 index 0000000..09d1303 --- /dev/null +++ b/challenges/cryptography/aes-cbc-poa/challenge/dispatcher @@ -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] == "flag": + plaintext = open("/flag", "rb").read().strip() +else: + plaintext = b"sleep" + +ciphertext = cipher.iv + cipher.encrypt(pad(plaintext, cipher.block_size)) +print(f"TASK: {ciphertext.hex()}") diff --git a/challenges/cryptography/aes-cbc-poa/challenge/worker b/challenges/cryptography/aes-cbc-poa/challenge/worker new file mode 100755 index 0000000..166a580 --- /dev/null +++ b/challenges/cryptography/aes-cbc-poa/challenge/worker @@ -0,0 +1,29 @@ +#!/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) + else: + print("Unknown command!") diff --git a/challenges/cryptography/aes-cbc/DESCRIPTION.md b/challenges/cryptography/aes-cbc/DESCRIPTION.md new file mode 100644 index 0000000..9402af9 --- /dev/null +++ b/challenges/cryptography/aes-cbc/DESCRIPTION.md @@ -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! diff --git a/challenges/cryptography/aes-cbc/challenge/run b/challenges/cryptography/aes-cbc/challenge/run new file mode 100755 index 0000000..1df1b28 --- /dev/null +++ b/challenges/cryptography/aes-cbc/challenge/run @@ -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_CBC) +ciphertext = cipher.iv + cipher.encrypt(pad(flag, cipher.block_size)) + +print(f"AES Key (hex): {key.hex()}") +print(f"Flag Ciphertext (hex): {ciphertext.hex()}") diff --git a/challenges/cryptography/ascii/DESCRIPTION.md b/challenges/cryptography/ascii/DESCRIPTION.md new file mode 100644 index 0000000..8502db3 --- /dev/null +++ b/challenges/cryptography/ascii/DESCRIPTION.md @@ -0,0 +1,52 @@ +Much of the field of Cryptography deals with encrypting _text_. +This text, as you might (again!) recall from [Dealing with Data](/fundamentals/data-dealings) is mapped to specific byte values, as specified by an encoding standard, such as ASCII or UTF-8. +Here, we'll stick to ASCII, though the concepts apply identically to other encodings. + +The cool thing is that, since ASCII puts byte values to characters, we can do operations like XOR! +This has obvious implications for cryptography. + +In this level, we'll explore these implications byte by byte. +The challenge will give you one letter a time, along with a key to "decrypt" (XOR) the letter with. +You give us the result of the XOR. +For example: + +```console +hacker@dojo:~$ /challenge/run +Challenge number 0... +- Encrypted Character: A +- XOR Key: 0x01 +- Decrypted Character? +``` + +How would you approach this? +You can `man ascii` and find the entry for A: + +```none +Oct Dec Hex Char +────────────────────── +101 65 41 A +``` + +So A is `0x41` in hex. +You would XOR that with `0x01` +The result here would be: `0x41 ^ 0x01 == 0x40`, and, according to `man ascii`: + +```none +Oct Dec Hex Char +────────────────────── +100 64 40 @ +``` + +It's the @ character! + +```console +hacker@dojo:~$ /challenge/run +Challenge number 0... +- Encrypted Character: A +- XOR Key: 0x01 +- Decrypted Character? @ +Correct! Moving on. +``` + +Now it's your turn! +Can you XOR things up and get the flag? diff --git a/challenges/cryptography/ascii/challenge/run b/challenges/cryptography/ascii/challenge/run new file mode 100755 index 0000000..7510f4e --- /dev/null +++ b/challenges/cryptography/ascii/challenge/run @@ -0,0 +1,29 @@ +#!/usr/bin/exec-suid -- /usr/bin/python3 -I + +import random +import string +import sys + +if not sys.stdin.isatty(): + print("You must interact with me directly. No scripting this!") + sys.exit(1) + +for n in range(1, 10): + print(f"Challenge number {n}...") + pt_chr, ct_chr = random.sample( + string.digits + string.ascii_letters + string.punctuation, + 2 + ) + key = ord(pt_chr) ^ ord(ct_chr) + + print(f"- Encrypted Character: {ct_chr}") + print(f"- XOR Key: {key:#04x}") + answer = input("- Decrypted Character? ").strip() + if answer != pt_chr: + print("Incorrect!") + sys.exit(1) + + print("Correct! Moving on.") + +print("You have mastered XORing ASCII! Your flag:") +print(open("/flag").read()) diff --git a/challenges/cryptography/common/challenge/run.j2 b/challenges/cryptography/common/challenge/run.j2 new file mode 100755 index 0000000..5f0be38 --- /dev/null +++ b/challenges/cryptography/common/challenge/run.j2 @@ -0,0 +1,526 @@ +#!/usr/bin/exec-suid -- /usr/bin/python3 -I + +import sys +import string +import random +import pathlib +import base64 +import json +import textwrap + +from Crypto.Cipher import AES +from Crypto.Hash.SHA256 import SHA256Hash +from Crypto.PublicKey import RSA +from Crypto.Random import get_random_bytes +from Crypto.Random.random import getrandbits, randrange +from Crypto.Util.strxor import strxor +from Crypto.Util.Padding import pad, unpad + + +flag = open("/flag", "rb").read() +config = (pathlib.Path(__file__).parent / ".config").read_text() +level = int(config) + + +def show(name, value, *, b64=True): + print(f"{name}: {value}") + + +def show_b64(name, value): + show(f"{name} (b64)", base64.b64encode(value).decode()) + +def show_hex_block(name, value, byte_block_size=16): + value_to_show = "" + + for i in range(0, len(value), byte_block_size): + value_to_show += f"{value[i:i+byte_block_size].hex()}" + value_to_show += " " + show(f"{name} (hex)", value_to_show) + + +def show_hex(name, value): + show(name, hex(value)) + + +def input_(name): + try: + return input(f"{name}: ") + except (KeyboardInterrupt, EOFError): + print() + exit(0) + + +def input_b64(name): + data = input_(f"{name} (b64)") + try: + return base64.b64decode(data) + except base64.binascii.Error: + print(f"Failed to decode base64 input: {data!r}", file=sys.stderr) + exit(1) + + +def input_hex(name): + data = input_(name) + try: + return int(data, 16) + except Exception: + print(f"Failed to decode hex input: {data!r}", file=sys.stderr) + exit(1) + + +def level1(): + """ + In this challenge you will decode base64 data. + Despite base64 data appearing "mangled", it is not an encryption scheme. + It is an encoding, much like base2, base10, base16, and ascii. + It is a popular way of encoding raw bytes. + """ + show_b64("flag", flag) + + +def level2(): + """ + In this challenge you will decrypt a secret encrypted with a one-time pad. + Although simple, this is the most secure encryption mechanism, if you could just securely transfer the key. + """ + key = get_random_bytes(len(flag)) + ciphertext = strxor(flag, key) + show_b64("key", key) + show_b64("secret ciphertext", ciphertext) + + +def level3(): + """ + In this challenge you will decrypt a secret encrypted with a one-time pad. + You can encrypt arbitrary data, with the key being reused each time. + """ + key = get_random_bytes(256) + assert len(flag) <= len(key) + + ciphertext = strxor(flag, key[:len(flag)]) + show_b64("secret ciphertext", ciphertext) + + while True: + plaintext = input_b64("plaintext") + ciphertext = strxor(plaintext, key[:len(plaintext)]) + show_b64("ciphertext", ciphertext) + + +def level4(): + """ + In this challenge you will decrypt a secret encrypted with Advanced Encryption Standard (AES). + The Electronic Codebook (ECB) block cipher mode of operation is used. + """ + key = get_random_bytes(16) + cipher = AES.new(key=key, mode=AES.MODE_ECB) + ciphertext = cipher.encrypt(pad(flag, cipher.block_size)) + show_b64("key", key) + show_b64("secret ciphertext", ciphertext) + + + +def level5(): + """ + In this challenge you will decrypt a secret encrypted with Advanced Encryption Standard (AES). + The Electronic Codebook (ECB) block cipher mode of operation is used. + You can encrypt arbitrary data, which has the secret appended to it, with the key being reused each time. + """ + key = get_random_bytes(16) + cipher = AES.new(key=key, mode=AES.MODE_ECB) + + ciphertext = cipher.encrypt(pad(flag, cipher.block_size)) + show_b64("secret ciphertext", ciphertext) + show_hex_block("secret ciphertext", ciphertext) + + while True: + plaintext_prefix = input_b64("plaintext prefix") + ciphertext = cipher.encrypt(pad(plaintext_prefix + flag, cipher.block_size)) + show_b64("ciphertext", ciphertext) + show_hex_block("ciphertext", ciphertext) + + +def level6(): + """ + In this challenge you will perform a Diffie-Hellman key exchange. + """ + # 2048-bit MODP Group from RFC3526 + p = int.from_bytes(bytes.fromhex( + "FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 " + "29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD " + "EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245 " + "E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED " + "EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D " + "C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F " + "83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D " + "670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B " + "E39E772C 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9 " + "DE2BCBF6 95581718 3995497C EA956AE5 15D22618 98FA0510 " + "15728E5A 8AACAA68 FFFFFFFF FFFFFFFF" + ), "big") + g = 2 + + show_hex("p", p) + show_hex("g", g) + + a = getrandbits(2048) + A = pow(g, a, p) + show_hex("A", A) + + B = input_hex("B") + if not (B > 2**1024): + print("Invalid B value (B <= 2**1024)", file=sys.stderr) + exit(1) + + s = pow(B, a, p) + + key = s.to_bytes(256, "little") + assert len(flag) <= len(key) + ciphertext = strxor(flag, key[:len(flag)]) + show_b64("secret ciphertext", ciphertext) + + +def level7(): + """ + In this challenge you will decrypt a secret encrypted with RSA (Rivest–Shamir–Adleman). + You will be provided with both the public key and private key. + """ + key = RSA.generate(2048) + assert len(flag) <= 256 + ciphertext = pow(int.from_bytes(flag, "little"), key.e, key.n).to_bytes(256, "little") + show_hex("e", key.e) + show_hex("d", key.d) + show_hex("n", key.n) + show_b64("secret ciphertext", ciphertext) + + +def level8(): + """ + In this challenge you will decrypt a secret encrypted with RSA (Rivest–Shamir–Adleman). + You will be provided with the prime factors of n. + """ + key = RSA.generate(2048) + assert len(flag) <= 256 + ciphertext = pow(int.from_bytes(flag, "little"), key.e, key.n).to_bytes(256, "little") + show_hex("e", key.e) + show_hex("p", key.p) + show_hex("q", key.q) + show_b64("secret ciphertext", ciphertext) + + +def level9(): + """ + In this challenge you will hash data with a Secure Hash Algorithm (SHA256). + You will find a small hash collision. + Your goal is to find data, which when hashed, has the same hash as the secret. + Only the first 2 bytes of the SHA256 hash are considered. + """ + prefix_length = 2 + sha256 = SHA256Hash(flag).digest() + show_b64(f"secret sha256[:{prefix_length}]", sha256[:prefix_length]) + + collision = input_b64("collision") + if SHA256Hash(collision).digest()[:prefix_length] == sha256[:prefix_length]: + show("flag", flag.decode()) + + +def level10(): + """ + In this challenge you will hash data with a Secure Hash Algorithm (SHA256). + You will compute a small proof-of-work. + Your goal is to find response data, which when appended to the challenge data and hashed, begins with 2 null-bytes. + """ + difficulty = 2 + + challenge = get_random_bytes(32) + show_b64("challenge", challenge) + + response = input_b64("response") + if SHA256Hash(challenge + response).digest()[:difficulty] == (b'\0' * difficulty): + show("flag", flag.decode()) + + +def level11(): + """ + In this challenge you will complete an RSA challenge-response. + You will be provided with both the public key and private key. + """ + key = RSA.generate(2048) + + show_hex("e", key.e) + show_hex("d", key.d) + show_hex("n", key.n) + + challenge = int.from_bytes(get_random_bytes(256), "little") % key.n + show_hex("challenge", challenge) + + response = input_hex("response") + if pow(response, key.e, key.n) == challenge: + show("flag", flag.decode()) + + +def level12(): + """ + In this challenge you will complete an RSA challenge-response. + You will provide the public key. + """ + e = input_hex("e") + n = input_hex("n") + + if not (e > 2): + print("Invalid e value (e > 2)", file=sys.stderr) + exit(1) + + if not (2**512 < n < 2**1024): + print("Invalid n value (2**512 < n < 2**1024)", file=sys.stderr) + exit(1) + + challenge = int.from_bytes(get_random_bytes(64), "little") + show_hex("challenge", challenge) + + response = input_hex("response") + if pow(response, e, n) == challenge: + ciphertext = pow(int.from_bytes(flag, "little"), e, n).to_bytes(256, "little") + show_b64("secret ciphertext", ciphertext) + + +def level13(): + """ + In this challenge you will work with public key certificates. + You will be provided with a self-signed root certificate. + You will also be provided with the root private key, and must use that to sign a user certificate. + """ + root_key = RSA.generate(2048) + + show_hex("root key d", root_key.d) + + root_certificate = { + "name": "root", + "key": { + "e": root_key.e, + "n": root_key.n, + }, + "signer": "root", + } + + root_trusted_certificates = { + "root": root_certificate, + } + + root_certificate_data = json.dumps(root_certificate).encode() + root_certificate_hash = SHA256Hash(root_certificate_data).digest() + root_certificate_signature = pow( + int.from_bytes(root_certificate_hash, "little"), + root_key.d, + root_key.n + ).to_bytes(256, "little") + + show_b64("root certificate", root_certificate_data) + show_b64("root certificate signature", root_certificate_signature) + + user_certificate_data = input_b64("user certificate") + user_certificate_signature = input_b64("user certificate signature") + + try: + user_certificate = json.loads(user_certificate_data) + except json.JSONDecodeError: + print("Invalid user certificate", file=sys.stderr) + exit(1) + + user_name = user_certificate.get("name") + if user_name in root_trusted_certificates: + print(f"Invalid user certificate name: `{user_name}`", file=sys.stderr) + exit(1) + + user_key = user_certificate.get("key", {}) + if not (isinstance(user_key.get("e"), int) and isinstance(user_key.get("n"), int)): + print(f"Invalid user certificate key: `{user_key}`", file=sys.stderr) + exit(1) + + if not (user_key["e"] > 2): + print("Invalid user certificate key e value (e > 2)", file=sys.stderr) + exit(1) + + if not (2**512 < user_key["n"] < 2**1024): + print("Invalid user certificate key n value (2**512 < n < 2**1024)", file=sys.stderr) + exit(1) + + user_signer = user_certificate.get("signer") + if user_signer not in root_trusted_certificates: + print(f"Untrusted user certificate signer: `{user_signer}`", file=sys.stderr) + exit(1) + + user_signer_key = root_trusted_certificates[user_signer]["key"] + user_certificate_hash = SHA256Hash(user_certificate_data).digest() + user_certificate_check = pow( + int.from_bytes(user_certificate_signature, "little"), + user_signer_key["e"], + user_signer_key["n"] + ).to_bytes(256, "little")[:len(user_certificate_hash)] + + if user_certificate_check != user_certificate_hash: + print("Untrusted user certificate: invalid signature", file=sys.stderr) + exit(1) + + ciphertext = pow(int.from_bytes(flag, "little"), user_key["e"], user_key["n"]).to_bytes(256, "little") + show_b64("secret ciphertext", ciphertext) + + +def level14(): + """ + In this challenge you will perform a simplified Transport Layer Security (TLS) handshake, acting as the server. + You will be provided with Diffie-Hellman parameters, a self-signed root certificate, and the root private key. + The client will request to establish a secure channel with a particular name, and initiate a Diffie-Hellman key exchange. + The server must complete the key exchange, and derive an AES-128 key from the exchanged secret. + Then, using the encrypted channel, the server must supply the requested user certificate, signed by root. + Finally, using the encrypted channel, the server must sign the handshake to prove ownership of the private user key. + """ + # 2048-bit MODP Group from RFC3526 + p = int.from_bytes(bytes.fromhex( + "FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 " + "29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD " + "EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245 " + "E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED " + "EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D " + "C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F " + "83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D " + "670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B " + "E39E772C 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9 " + "DE2BCBF6 95581718 3995497C EA956AE5 15D22618 98FA0510 " + "15728E5A 8AACAA68 FFFFFFFF FFFFFFFF" + ), "big") + g = 2 + + show_hex("p", p) + show_hex("g", g) + + root_key = RSA.generate(2048) + + show_hex("root key d", root_key.d) + + root_certificate = { + "name": "root", + "key": { + "e": root_key.e, + "n": root_key.n, + }, + "signer": "root", + } + + root_trusted_certificates = { + "root": root_certificate, + } + + root_certificate_data = json.dumps(root_certificate).encode() + root_certificate_hash = SHA256Hash(root_certificate_data).digest() + root_certificate_signature = pow( + int.from_bytes(root_certificate_hash, "little"), + root_key.d, + root_key.n + ).to_bytes(256, "little") + + show_b64("root certificate", root_certificate_data) + show_b64("root certificate signature", root_certificate_signature) + + name = ''.join(random.choices(string.ascii_lowercase, k=16)) + show("name", name) + + a = getrandbits(2048) + A = pow(g, a, p) + show_hex("A", A) + + B = input_hex("B") + if not (B > 2**1024): + print("Invalid B value (B <= 2**1024)", file=sys.stderr) + exit(1) + + s = pow(B, a, p) + key = SHA256Hash(s.to_bytes(256, "little")).digest()[:16] + cipher_encrypt = AES.new(key=key, mode=AES.MODE_CBC, iv=b"\0"*16) + cipher_decrypt = AES.new(key=key, mode=AES.MODE_CBC, iv=b"\0"*16) + + def decrypt_input_b64(name): + data = input_b64(name) + try: + return unpad(cipher_decrypt.decrypt(data), cipher_decrypt.block_size) + except ValueError as e: + print(f"{name}: {e}", file=sys.stderr) + exit(1) + + user_certificate_data = decrypt_input_b64("user certificate") + user_certificate_signature = decrypt_input_b64("user certificate signature") + user_signature = decrypt_input_b64("user signature") + + try: + user_certificate = json.loads(user_certificate_data) + except json.JSONDecodeError: + print("Invalid user certificate", file=sys.stderr) + exit(1) + + user_name = user_certificate.get("name") + if user_name != name: + print(f"Invalid user certificate name: `{user_name}`", file=sys.stderr) + exit(1) + + user_key = user_certificate.get("key", {}) + if not (isinstance(user_key.get("e"), int) and isinstance(user_key.get("n"), int)): + print(f"Invalid user certificate key: `{user_key}`", file=sys.stderr) + exit(1) + + if not (user_key["e"] > 2): + print("Invalid user certificate key e value (e > 2)", file=sys.stderr) + exit(1) + + if not (2**512 < user_key["n"] < 2**1024): + print("Invalid user certificate key n value (2**512 < n < 2**1024)", file=sys.stderr) + exit(1) + + user_signer = user_certificate.get("signer") + if user_signer not in root_trusted_certificates: + print(f"Untrusted user certificate signer: `{user_signer}`", file=sys.stderr) + exit(1) + + user_signer_key = root_trusted_certificates[user_signer]["key"] + user_certificate_hash = SHA256Hash(user_certificate_data).digest() + user_certificate_check = pow( + int.from_bytes(user_certificate_signature, "little"), + user_signer_key["e"], + user_signer_key["n"] + ).to_bytes(256, "little")[:len(user_certificate_hash)] + + if user_certificate_check != user_certificate_hash: + print("Untrusted user certificate: invalid signature", file=sys.stderr) + exit(1) + + user_signature_data = ( + name.encode().ljust(256, b"\0") + + A.to_bytes(256, "little") + + B.to_bytes(256, "little") + ) + user_signature_hash = SHA256Hash(user_signature_data).digest() + user_signature_check = pow( + int.from_bytes(user_signature, "little"), + user_key["e"], + user_key["n"] + ).to_bytes(256, "little")[:len(user_signature_hash)] + + if user_signature_check != user_signature_hash: + print("Untrusted user: invalid signature", file=sys.stderr) + exit(1) + + ciphertext = cipher_encrypt.encrypt(pad(flag, cipher_encrypt.block_size)) + show_b64("secret ciphertext", ciphertext) + + +def challenge(): + challenge_level = globals()[f"level{level}"] + description = textwrap.dedent(challenge_level.__doc__) + + print("===== Welcome to Cryptography! =====") + print("In this series of challenges, you will be working with various cryptographic mechanisms.") + print(description) + print() + + challenge_level() + + +challenge() diff --git a/challenges/cryptography/cpa-http-boss/DESCRIPTION.md b/challenges/cryptography/cpa-http-boss/DESCRIPTION.md new file mode 100644 index 0000000..5f3aeb0 --- /dev/null +++ b/challenges/cryptography/cpa-http-boss/DESCRIPTION.md @@ -0,0 +1,7 @@ +Okay, time for the AES-ECB-CPA final boss! +Can you carry out this attack against an encrypted secret storage web server? +Let's find out! + +---- +**NOTE:** +Keep in mind that, unlike the previous levels, this level takes data in base64! diff --git a/challenges/cryptography/cpa-http-boss/challenge/run b/challenges/cryptography/cpa-http-boss/challenge/run new file mode 100755 index 0000000..ea3725d --- /dev/null +++ b/challenges/cryptography/cpa-http-boss/challenge/run @@ -0,0 +1,61 @@ +#!/usr/bin/exec-suid -- /usr/bin/python3 -I + +from base64 import b64encode +from Crypto.Cipher import AES +from Crypto.Util.Padding import pad +from Crypto.Random import get_random_bytes + +import tempfile +import sqlite3 +import flask +import os + +app = flask.Flask(__name__) + +class TemporaryDB: + def __init__(self): + self.db_file = tempfile.NamedTemporaryFile("x", suffix=".db") + + def execute(self, sql, parameters=()): + connection = sqlite3.connect(self.db_file.name) + connection.row_factory = sqlite3.Row + cursor = connection.cursor() + result = cursor.execute(sql, parameters) + connection.commit() + return result + +key = get_random_bytes(16) +cipher = AES.new(key=key, mode=AES.MODE_ECB) + +db = TemporaryDB() +# https://www.sqlite.org/lang_createtable.html +db.execute("""CREATE TABLE posts AS SELECT ? AS content""", [open("/flag", "rb").read().strip()]) + +@app.route("/", methods=["POST"]) +def challenge_post(): + content = flask.request.form.get("content").encode('latin1') + db.execute("INSERT INTO posts VALUES (?)", [content]) + return flask.redirect(flask.request.path) + +@app.route("/reset", methods=["POST"]) +def challenge_reset(): + db.execute("DELETE FROM posts WHERE ROWID > 1") + return flask.redirect("/") + +@app.route("/", methods=["GET"]) +def challenge_get(): + pt = b"|".join(post["content"] for post in db.execute("SELECT content FROM posts ORDER BY ROWID DESC").fetchall()) + ct = cipher.encrypt(pad(pt, cipher.block_size)) + + return f""" + Welcome to pwn.secret! +
Post a secret:
+
+
+ Encrypted backup:
{b64encode(ct).decode()}
+ + """ + +app.secret_key = os.urandom(8) +app.config['SERVER_NAME'] = "challenge.localhost:80" +app.run("challenge.localhost", 80) diff --git a/challenges/cryptography/cpa-http-hex/DESCRIPTION.md b/challenges/cryptography/cpa-http-hex/DESCRIPTION.md new file mode 100644 index 0000000..c5a06ec --- /dev/null +++ b/challenges/cryptography/cpa-http-hex/DESCRIPTION.md @@ -0,0 +1,6 @@ +Okay, now we'll try that attack in a slightly more realistic scenario. +Can you remember your SQL to carry out the attack and recover the flag? + +---- +**HINT:** +Remember that you can make select return chosen plaintext by doing `SELECT 'my_plaintext'`! diff --git a/challenges/cryptography/cpa-http-hex/challenge/run b/challenges/cryptography/cpa-http-hex/challenge/run new file mode 100755 index 0000000..973db42 --- /dev/null +++ b/challenges/cryptography/cpa-http-hex/challenge/run @@ -0,0 +1,61 @@ +#!/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 + +import tempfile +import sqlite3 +import random +import flask +import os + +app = flask.Flask(__name__) + +class TemporaryDB: + def __init__(self): + self.db_file = tempfile.NamedTemporaryFile("x", suffix=".db") + + def execute(self, sql, parameters=()): + connection = sqlite3.connect(self.db_file.name) + connection.row_factory = sqlite3.Row + cursor = connection.cursor() + result = cursor.execute(sql, parameters) + connection.commit() + return result + +key = get_random_bytes(16) +cipher = AES.new(key=key, mode=AES.MODE_ECB) + +db = TemporaryDB() +# https://www.sqlite.org/lang_createtable.html +db.execute("""CREATE TABLE secrets AS SELECT ? AS flag""", [open("/flag").read()]) + +@app.route("/", methods=["GET"]) +def challenge_get(): + query = flask.request.args.get("query") or "'A'" + + try: + sql = f'SELECT {query} FROM secrets' + print(f"DEBUG: {sql=}") + pt = db.execute(sql).fetchone()[0] + except sqlite3.Error as e: + flask.abort(500, f"Query: {query}\nError: {e}") + except TypeError: + # no records found + pt = "A" + + ct = cipher.encrypt(pad(pt.encode(), cipher.block_size)) + + return f""" + Welcome to pwn.secret! +
SELECT FROM secrets
+
+ Query:
{sql}

+ Results:
{ct.hex()}
+ + """ + +app.secret_key = os.urandom(8) +app.config['SERVER_NAME'] = f"challenge.localhost:80" +app.run("challenge.localhost", 80) diff --git a/challenges/cryptography/cpa-http/DESCRIPTION.md b/challenges/cryptography/cpa-http/DESCRIPTION.md new file mode 100644 index 0000000..935f2fd --- /dev/null +++ b/challenges/cryptography/cpa-http/DESCRIPTION.md @@ -0,0 +1,12 @@ +For historical reasons, different encodings tend to gain traction in different contexts. +For example, on the web, the standard way to encode binary data is base64, an encoding that you learned in [Dealing with Data](/fundamentals/data-dealings). +Channel this skill now, adapting your previous solution for base64! + +You'll (re-)note that base64 isn't as convenient to reason about as hex. +Why do people use it? +One reason: every byte requires _two_ hex letters to encode, whereas base64 encodes every 3 bytes with 4 letters. +This means that, when sending each letter as a byte itself over the network, for example, base64 is marginally more efficient. +On the other hand, it's a headache to work with, because of the unclean bit boundaries! + +Throughout the rest of the modules, challenges might use hex or base64, as our heart desires. +It's important to be able to handle either! diff --git a/challenges/cryptography/cpa-http/challenge/run b/challenges/cryptography/cpa-http/challenge/run new file mode 100755 index 0000000..115d4c8 --- /dev/null +++ b/challenges/cryptography/cpa-http/challenge/run @@ -0,0 +1,62 @@ +#!/usr/bin/exec-suid -- /usr/bin/python3 -I + +from base64 import b64encode, b64decode +from Crypto.Cipher import AES +from Crypto.Util.Padding import pad +from Crypto.Random import get_random_bytes + +import tempfile +import sqlite3 +import random +import flask +import os + +app = flask.Flask(__name__) + +class TemporaryDB: + def __init__(self): + self.db_file = tempfile.NamedTemporaryFile("x", suffix=".db") + + def execute(self, sql, parameters=()): + connection = sqlite3.connect(self.db_file.name) + connection.row_factory = sqlite3.Row + cursor = connection.cursor() + result = cursor.execute(sql, parameters) + connection.commit() + return result + +key = get_random_bytes(16) +cipher = AES.new(key=key, mode=AES.MODE_ECB) + +db = TemporaryDB() +# https://www.sqlite.org/lang_createtable.html +db.execute("""CREATE TABLE secrets AS SELECT ? AS flag""", [open("/flag").read()]) + +@app.route("/", methods=["GET"]) +def challenge_get(): + query = flask.request.args.get("query") or "'A'" + + try: + sql = f'SELECT {query} FROM secrets' + print(f"DEBUG: {sql=}") + pt = db.execute(sql).fetchone()[0] + except sqlite3.Error as e: + flask.abort(500, f"Query: {query}\nError: {e}") + except TypeError: + # no records found + pt = "A" + + ct = cipher.encrypt(pad(pt.encode(), cipher.block_size)) + + return f""" + Welcome to pwn.secret! +
SELECT FROM secrets
+
+ Query:
{sql}

+ Results:
{b64encode(ct).decode()}
+ + """ + +app.secret_key = os.urandom(8) +app.config['SERVER_NAME'] = f"challenge.localhost:80" +app.run("challenge.localhost", 80) diff --git a/challenges/cryptography/cpa-prefix-pad/DESCRIPTION.md b/challenges/cryptography/cpa-prefix-pad/DESCRIPTION.md new file mode 100644 index 0000000..b67f911 --- /dev/null +++ b/challenges/cryptography/cpa-prefix-pad/DESCRIPTION.md @@ -0,0 +1,30 @@ +The previous challenge ignored something very important: [_padding_](https://en.wikipedia.org/wiki/Padding_(cryptography)#Byte_padding). +AES has a 128-bit (16 byte) block size. +This means that input to the algorithm _must_ be 16 bytes long, and any input shorter than that must be _padded_ to 16 bytes by having data added to the plaintext before encryption. +When the ciphertext is decrypted, the result must be _unpadded_ (e.g., the added padding bytes must be removed) to recover the original plaintext. + +_How_ to pad is an interesting question. +For example, you could pad with null bytes (`0x00`). +But what if your data has null bytes at the end? +They might be erroneously removed during unpadding, leaving you with a plaintext different than your original! +This would not be good. + +One padding standard (and likely the most popular) is PKCS7, which simply pads the input with bytes all containing a value equal to the number of bytes padded. +If one byte is added to a 15-byte input, it contains the value `0x01`, two bytes added to a 14-byte input would be `0x02 0x02`, and the 15 bytes added to a 1-byte input would all have a value `0x0f`. +During unpadding, PKCS7 looks at the value of the last byte of the block and removes that many bytes. +Simple! + +But wait... +What if exactly 16 bytes of plaintext are encrypted (e.g., no padding needed), but the plaintext byte has a value of `0x01`? +Left to its own devices, PKCS7 would chop off that byte during unpadding, leaving us with a corrupted plaintext. +The solution to this is slightly silly: if the last block of the plaintext is exactly 16 bytes, we add a block of _all_ padding (e.g., 16 padding bytes, each with a value of `0x10`). +PKCS7 removes the whole block during unpadding, and the sanctity of the plaintext is preserved at the expense of a bit more data. + +Anyways, the previous challenge explicitly disabled this last case, which would have the result of popping in a "decoy" ciphertext block full of padding as you tried to push the very first suffix byte to its own block. +This challenge pads properly. +Watch out for that "decoy" block, and go solve it! + +---- +**NOTE:** +The full-padding block will *only* appear when the last block of plaintext perfectly fills 16 bytes. +It'll vanish when one more byte is appended (replaced with the padded new block containing the last byte of plaintext), but will reappear when the new block reaches 16 bytes in length. diff --git a/challenges/cryptography/cpa-prefix-pad/challenge/run b/challenges/cryptography/cpa-prefix-pad/challenge/run new file mode 100755 index 0000000..e33084b --- /dev/null +++ b/challenges/cryptography/cpa-prefix-pad/challenge/run @@ -0,0 +1,26 @@ +#!/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().strip() + +key = get_random_bytes(16) +cipher = AES.new(key=key, mode=AES.MODE_ECB) + +for n in range(31337): + print("") + print("Choose an action?") + print("1. Encrypt chosen plaintext.") + print("2. Prepend something to the flag.") + if (choice := int(input("Choice? "))) == 1: + pt = input("Data? ").strip().encode() + elif choice == 2: + pt = input("Data? ").strip().encode() + flag + else: + break + + padded_pt = pad(pt, cipher.block_size) + ct = cipher.encrypt(padded_pt) + print(f"Result: {ct.hex()}") diff --git a/challenges/cryptography/cpa-prefix/DESCRIPTION.md b/challenges/cryptography/cpa-prefix/DESCRIPTION.md new file mode 100644 index 0000000..99d1c5e --- /dev/null +++ b/challenges/cryptography/cpa-prefix/DESCRIPTION.md @@ -0,0 +1,17 @@ +Okay, now let's complicate things slightly. +It's not so common that you can just chop off the end of interesting data and go wild. +However, _much_ more common is the ability to _prepend_ chosen plaintext to a secret before it's encrypted. +If you carefully craft the prepended data so that it _pushes_ the end of the secret into a new block, you've just successfully isolated it, accomplishing the same as if you were chopping it off! + +Go ahead and do that in this challenge. +The core attack is the same as before, it just involves more data massaging. + +---- +**HINT:** +Keep in mind that a typical pwn.college flag is somewhere upwards of 50 bytes long. +This is four blocks (three full and one partial), and the length can vary slightly. +You will need to experiment with how many bytes you must prepend to push even one of the end characters to its own block. + +**HINT:** +Keep in mind that blocks are 16 bytes long! +After you leak the last 16 bytes, you'll be looking at the second-to-last block, and so on. diff --git a/challenges/cryptography/cpa-prefix/challenge/run b/challenges/cryptography/cpa-prefix/challenge/run new file mode 100755 index 0000000..ba0d09d --- /dev/null +++ b/challenges/cryptography/cpa-prefix/challenge/run @@ -0,0 +1,35 @@ +#!/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().strip() + +key = get_random_bytes(16) +cipher = AES.new(key=key, mode=AES.MODE_ECB) + +for n in range(31337): + print("") + print("Choose an action?") + print("1. Encrypt chosen plaintext.") + print("2. Prepend something to the flag.") + if (choice := int(input("Choice? "))) == 1: + pt = input("Data? ").strip().encode() + elif choice == 2: + pt = input("Data? ").strip().encode() + flag + else: + break + + padded_pt = pad(pt, cipher.block_size) if len(pt)%cipher.block_size else pt + ct = cipher.encrypt(padded_pt) + print(f"Result: {ct.hex()}") + + if n == 0: + print("I'm here to help!") + print("For the first 10, I will split them into blocks for you!") + print("After this, you'll have to split them yourself.") + if n < 10: + print(f"# of blocks: {len(ct)//16}.") + for n,i in enumerate(range(0, len(ct)-15, 16), start=1): + print(f"Block {n}: {ct[i:i+16].hex()}") diff --git a/challenges/cryptography/cpa-suffix/DESCRIPTION.md b/challenges/cryptography/cpa-suffix/DESCRIPTION.md new file mode 100644 index 0000000..919da22 --- /dev/null +++ b/challenges/cryptography/cpa-suffix/DESCRIPTION.md @@ -0,0 +1,10 @@ +Okay, now let's complicate things slightly to increase the realism. +It's rare that you can just craft queries for the plaintext that you want. +However, it's less rare that you can isolate the _tail end_ of some data into its own block, and in ECB, this is bad news. +We'll explore this concept in this challenge, replacing your ability to query substrings of the flag with just an ability to encrypt some bytes off the end. + +Show us that you can still solve this! + +---- +**HINT:** +Keep in mind that, once you recover some part of the end of the flag, you can build a new codebook with additional prefixes of the known parts, and repeat the attack on the previous byte! diff --git a/challenges/cryptography/cpa-suffix/challenge/run b/challenges/cryptography/cpa-suffix/challenge/run new file mode 100755 index 0000000..0c3d01e --- /dev/null +++ b/challenges/cryptography/cpa-suffix/challenge/run @@ -0,0 +1,25 @@ +#!/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().strip() + +key = get_random_bytes(16) +cipher = AES.new(key=key, mode=AES.MODE_ECB) + +while True: + print("Choose an action?") + print("1. Encrypt chosen plaintext.") + print("2. Encrypt the tail end of the flag.") + if (choice := int(input("Choice? "))) == 1: + pt = input("Data? ").strip().encode() + elif choice == 2: + length = int(input("Length? ")) + pt = flag[-length:] + else: + break + + ct = cipher.encrypt(pad(pt, cipher.block_size)) + print(f"Result: {ct.hex()}") diff --git a/challenges/cryptography/cpa/DESCRIPTION.md b/challenges/cryptography/cpa/DESCRIPTION.md new file mode 100644 index 0000000..b2ed770 --- /dev/null +++ b/challenges/cryptography/cpa/DESCRIPTION.md @@ -0,0 +1,23 @@ +Though the core of the AES crypto algorithm is thought to be secure (not _proven_ to be, though: no one has managed to do that! But no one has managed to significantly break the crypto in the 20+ years of its use, either), this core only encrypts 128-bit (16 byte) blocks at a time. +To actually _use_ AES in practice, one must build a _cryptosystem_ on top of it. + +In the previous level, we used the AES-[ECB](https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Electronic_codebook_(ECB)) cryptosystem: an Electronic Codebook Cipher where every block is independently encrypted by the same key. +This system is quite simple but, as we will discover here, extremely susceptible to a certain class of attack. + +Cryptosystems are held to very high standard of [ciphertext indistinguishability](https://en.wikipedia.org/wiki/Ciphertext_indistinguishability). +That is, an attacker that lacks the key to the cryptosystem should not be able to distinguish between pairs of ciphertext based on the plaintext that was encrypted. +For example, if the attacker looks at ciphertexts `UVSDFGIWEHFBFFCA` and `LKXBFVYASLJDEWEU`, and is able to determine that the latter was produced from the plaintext `EEEEFFFFGGGGHHHH` (or, in fact, figure out _any_ information about the plaintext at all!), the cryptosystem is considered broken. +This property must hold even if the attacker already knows part or all of the plaintext, a setting known as the [Known Plaintext Attack](https://en.wikipedia.org/wiki/Known-plaintext_attack), _or can even control part or all of the plaintext_, a setting known as the [Chosen Plaintext Attack](https://en.wikipedia.org/wiki/Chosen-plaintext_attack)! + +ECB is susceptible to both known and chosen plaintext attack. +Because every block is encrypted with the same key, with no other modifications, an attacker can observe identical ciphertext across different blocks that have identical plaintext. +Moreover, if the attacker can choose or learn the plaintext associated with some of these blocks, they can carefully build a mapping from known-plaintext to known-ciphertext, and use that as a lookup table to decrypt other matching ciphertext! + +In this level, you will do just this: you will build a codebook mapping from ciphertext to chosen plaintext, then use that to decrypt the flag. +Good luck! + +---- +**HINT:** +You might find it helpful to automate interactions with this challenge. +You can do so using the `pwntools` Python package. +Check out [this pwntools cheatsheet](https://gist.github.com/anvbis/64907e4f90974c4bdd930baeb705dedf) from a fellow pwn.college student! diff --git a/challenges/cryptography/cpa/challenge/run b/challenges/cryptography/cpa/challenge/run new file mode 100755 index 0000000..7758d87 --- /dev/null +++ b/challenges/cryptography/cpa/challenge/run @@ -0,0 +1,26 @@ +#!/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) + +while True: + print("Choose an action?") + print("1. Encrypt chosen plaintext.") + print("2. Encrypt part of the flag.") + if (choice := int(input("Choice? "))) == 1: + pt = input("Data? ").strip().encode() + elif choice == 2: + index = int(input("Index? ")) + length = int(input("Length? ")) + pt = flag[index:index+length] + else: + break + + ct = cipher.encrypt(pad(pt, cipher.block_size)) + print(f"Result: {ct.hex()}") diff --git a/challenges/cryptography/dhke-aes/DESCRIPTION.md b/challenges/cryptography/dhke-aes/DESCRIPTION.md new file mode 100644 index 0000000..d5e1f2b --- /dev/null +++ b/challenges/cryptography/dhke-aes/DESCRIPTION.md @@ -0,0 +1,13 @@ +You might have noticed that DH doesn't actually allow you to encrypt data directly: all it does is facilitate the generation of the same secret value for both Alice and Bob. +This value cannot be _chosen_, what Alice and Bob get for `s` is uniquely determined by the values of `a`, `b`, `p`, and `g`! + +This single-secret nature isn't necessarily a drawback of DHKE. +That's just what it's for: letting you exchange a secret for further use. + +So how do Alice and Bob actually exchange information using DHKE? +Well, the hint is in the name: Diffie-Hellman _Key Exchange_. +That secret value, of course, can be used as a key for, e.g., a symmetric cipher, and information can be encrypted with that cipher between Alice and Bob! + +Armed with your knowledge of DHKE, you will now build your first cryptosystem that resembles something real! +You'll use DHKE to negotiate an AES key, and the challenge will use that key to encrypt the flag. +Decrypt it, and win! diff --git a/challenges/cryptography/dhke-aes/challenge/run b/challenges/cryptography/dhke-aes/challenge/run new file mode 100755 index 0000000..c362351 --- /dev/null +++ b/challenges/cryptography/dhke-aes/challenge/run @@ -0,0 +1,49 @@ +#!/usr/bin/exec-suid -- /usr/bin/python3 -I + +import sys +from Crypto.Cipher import AES +from Crypto.Util.Padding import pad +from Crypto.Random.random import getrandbits + +flag = open("/flag", "rb").read() +assert len(flag) <= 256 + +# 2048-bit MODP Group from RFC3526 +p = int.from_bytes(bytes.fromhex( + "FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 " + "29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD " + "EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245 " + "E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED " + "EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D " + "C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F " + "83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D " + "670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B " + "E39E772C 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9 " + "DE2BCBF6 95581718 3995497C EA956AE5 15D22618 98FA0510 " + "15728E5A 8AACAA68 FFFFFFFF FFFFFFFF" +), "big") +g = 2 +print(f"p = {p:#x}") +print(f"g = {g:#x}") + +a = getrandbits(2048) +A = pow(g, a, p) +print(f"A = {A:#x}") + +try: + B = int(input("B? "), 16) +except ValueError: + print("Invalid B value (not a hex number)", file=sys.stderr) + sys.exit(1) +if B <= 2**1024: + print("Invalid B value (B <= 2**1024)", file=sys.stderr) + sys.exit(1) + +s = pow(B, a, p) +key = s.to_bytes(256, "little")[:16] + +# friendship ended with DHKE, AES is my new best friend +cipher = AES.new(key=key, mode=AES.MODE_CBC) +flag = open("/flag", "rb").read() +ciphertext = cipher.iv + cipher.encrypt(pad(flag, cipher.block_size)) +print(f"Flag Ciphertext (hex): {ciphertext.hex()}") diff --git a/challenges/cryptography/hex/DESCRIPTION.md b/challenges/cryptography/hex/DESCRIPTION.md new file mode 100644 index 0000000..b83d5d2 --- /dev/null +++ b/challenges/cryptography/hex/DESCRIPTION.md @@ -0,0 +1,3 @@ +Of course, as you also learned in [Dealing with Data](/fundamentals/data-dealings), we tend to represent values in computer memory as _hexadecimal_. +If you don't remember what that is, go back and review those levels. +Otherwise, go forth and practice some hexadecimal XOR here! diff --git a/challenges/cryptography/hex/challenge/run b/challenges/cryptography/hex/challenge/run new file mode 100755 index 0000000..2fb0c5e --- /dev/null +++ b/challenges/cryptography/hex/challenge/run @@ -0,0 +1,24 @@ +#!/usr/bin/exec-suid -- /usr/bin/python3 -I + +import random +import sys + +for n in range(10): + print(f"Challenge number {n}...") + + key = random.randrange(1, 256) + plain_secret = random.randrange(0, 256) + cipher_secret = plain_secret ^ key + + print(f"The key: {key:#04x}") + print(f"Encrypted secret: {cipher_secret:#04x}") + answer = int(input("Decrypted secret? "), 16) + print(f"You entered: {answer:#04x}, decimal {answer}.") + if answer != plain_secret: + print("INCORRECT!") + sys.exit(1) + + print("Correct! Moving on.") + +print("CORRECT! Your flag:") +print(open("/flag").read()) diff --git a/challenges/cryptography/level-10/.config b/challenges/cryptography/level-10/.config new file mode 100644 index 0000000..f599e28 --- /dev/null +++ b/challenges/cryptography/level-10/.config @@ -0,0 +1 @@ +10 diff --git a/challenges/cryptography/level-10/DESCRIPTION.md b/challenges/cryptography/level-10/DESCRIPTION.md new file mode 100644 index 0000000..76392b9 --- /dev/null +++ b/challenges/cryptography/level-10/DESCRIPTION.md @@ -0,0 +1,3 @@ +In this challenge you will hash data with a Secure Hash Algorithm (SHA256). +You will compute a small proof-of-work. +Your goal is to find response data, which when appended to the challenge data and hashed, begins with 2 null-bytes. diff --git a/challenges/cryptography/level-10/challenge/run.j2 b/challenges/cryptography/level-10/challenge/run.j2 new file mode 100644 index 0000000..b43bfdc --- /dev/null +++ b/challenges/cryptography/level-10/challenge/run.j2 @@ -0,0 +1 @@ +{% extends "common/run.j2" %} diff --git a/challenges/cryptography/level-11/.config b/challenges/cryptography/level-11/.config new file mode 100644 index 0000000..b4de394 --- /dev/null +++ b/challenges/cryptography/level-11/.config @@ -0,0 +1 @@ +11 diff --git a/challenges/cryptography/level-11/DESCRIPTION.md b/challenges/cryptography/level-11/DESCRIPTION.md new file mode 100644 index 0000000..fc1d994 --- /dev/null +++ b/challenges/cryptography/level-11/DESCRIPTION.md @@ -0,0 +1,2 @@ +In this challenge you will complete an RSA challenge-response. +You will be provided with both the public key and private key. diff --git a/challenges/cryptography/level-11/challenge/run b/challenges/cryptography/level-11/challenge/run new file mode 100755 index 0000000..b43bfdc --- /dev/null +++ b/challenges/cryptography/level-11/challenge/run @@ -0,0 +1 @@ +{% extends "common/run.j2" %} diff --git a/challenges/cryptography/level-12/.config b/challenges/cryptography/level-12/.config new file mode 100644 index 0000000..48082f7 --- /dev/null +++ b/challenges/cryptography/level-12/.config @@ -0,0 +1 @@ +12 diff --git a/challenges/cryptography/level-12/DESCRIPTION.md b/challenges/cryptography/level-12/DESCRIPTION.md new file mode 100644 index 0000000..d4715af --- /dev/null +++ b/challenges/cryptography/level-12/DESCRIPTION.md @@ -0,0 +1,2 @@ +In this challenge you will complete an RSA challenge-response. +You will provide the public key. diff --git a/challenges/cryptography/level-12/challenge/run b/challenges/cryptography/level-12/challenge/run new file mode 100755 index 0000000..b43bfdc --- /dev/null +++ b/challenges/cryptography/level-12/challenge/run @@ -0,0 +1 @@ +{% extends "common/run.j2" %} diff --git a/challenges/cryptography/level-13/.config b/challenges/cryptography/level-13/.config new file mode 100644 index 0000000..b1bd38b --- /dev/null +++ b/challenges/cryptography/level-13/.config @@ -0,0 +1 @@ +13 diff --git a/challenges/cryptography/level-13/DESCRIPTION.md b/challenges/cryptography/level-13/DESCRIPTION.md new file mode 100644 index 0000000..cbc61eb --- /dev/null +++ b/challenges/cryptography/level-13/DESCRIPTION.md @@ -0,0 +1,3 @@ +In this challenge you will work with public key certificates. +You will be provided with a self-signed root certificate. +You will also be provided with the root private key, and must use that to sign a user certificate. diff --git a/challenges/cryptography/level-13/challenge/run b/challenges/cryptography/level-13/challenge/run new file mode 100755 index 0000000..b43bfdc --- /dev/null +++ b/challenges/cryptography/level-13/challenge/run @@ -0,0 +1 @@ +{% extends "common/run.j2" %} diff --git a/challenges/cryptography/level-14/.config b/challenges/cryptography/level-14/.config new file mode 100644 index 0000000..8351c19 --- /dev/null +++ b/challenges/cryptography/level-14/.config @@ -0,0 +1 @@ +14 diff --git a/challenges/cryptography/level-14/DESCRIPTION.md b/challenges/cryptography/level-14/DESCRIPTION.md new file mode 100644 index 0000000..43efe56 --- /dev/null +++ b/challenges/cryptography/level-14/DESCRIPTION.md @@ -0,0 +1,6 @@ +In this challenge you will perform a simplified Transport Layer Security (TLS) handshake, acting as the server. +You will be provided with Diffie-Hellman parameters, a self-signed root certificate, and the root private key. +The client will request to establish a secure channel with a particular name, and initiate a Diffie-Hellman key exchange. +The server must complete the key exchange, and derive an AES-128 key from the exchanged secret. +Then, using the encrypted channel, the server must supply the requested user certificate, signed by root. +Finally, using the encrypted channel, the server must sign the handshake to prove ownership of the private user key. diff --git a/challenges/cryptography/level-14/challenge/run b/challenges/cryptography/level-14/challenge/run new file mode 100755 index 0000000..b43bfdc --- /dev/null +++ b/challenges/cryptography/level-14/challenge/run @@ -0,0 +1 @@ +{% extends "common/run.j2" %} diff --git a/challenges/cryptography/level-2/DESCRIPTION.md b/challenges/cryptography/level-2/DESCRIPTION.md new file mode 100644 index 0000000..0395f30 --- /dev/null +++ b/challenges/cryptography/level-2/DESCRIPTION.md @@ -0,0 +1,11 @@ +In this challenge you will decrypt a secret encrypted with a [one-time pad](https://en.wikipedia.org/wiki/One-time_pad). +Although simple, this is the most secure encryption mechanism, if a) you can securely transfer the key and b) you only ever use the pad _once_. +It's also the most simple encryption mechanism: you simply _XOR_ the bits of the plaintext with the bits of the key one by one! + +This challenge encrypts the flag with a one-time pad and then gives you the key. +Luckily, a one-time pad is a _symmetric_ cryptosystem: that is, you use the same key to encrypt and to decrypt, so you have everything you need to decrypt the flag! + +---- +**Fun fact:** the One-time Pad is the _only_ cryptosystem that humanity has been able to _prove_ is perfectly secure. +If you securely transfer the key, and you only use it for one message, it cannot be cracked even by attackers with infinite computational power! +We have not been able to make this proof for any other cryptosystem. diff --git a/challenges/cryptography/level-2/challenge/run b/challenges/cryptography/level-2/challenge/run new file mode 100755 index 0000000..f5231b2 --- /dev/null +++ b/challenges/cryptography/level-2/challenge/run @@ -0,0 +1,12 @@ +#!/usr/bin/exec-suid -- /usr/bin/python3 -I + +from Crypto.Random import get_random_bytes +from Crypto.Util.strxor import strxor + +flag = open("/flag", "rb").read() + +key = get_random_bytes(len(flag)) +ciphertext = strxor(flag, key) + +print(f"One-Time Pad Key (hex): {key.hex()}") +print(f"Flag Ciphertext (hex): {ciphertext.hex()}") diff --git a/challenges/cryptography/level-3/DESCRIPTION.md b/challenges/cryptography/level-3/DESCRIPTION.md new file mode 100644 index 0000000..ef43fcf --- /dev/null +++ b/challenges/cryptography/level-3/DESCRIPTION.md @@ -0,0 +1,10 @@ +The previous challenge gave you the one time pad to decrypt the ciphertext. +If you did not know the one time pad, and it was only ever used for one message, the previous challenge would be unsolvable! +In this level, we'll explore what happens if the latter condition is violated. +You don't get the key this time, but we'll let you encrypt as many messages as you want. +Can you decrypt the flag? + +---- +**Hint:** think deeply about how XOR works, and consider that it is a distributative, commutative, and associative operation... + +**Hint:** we recommend writing your solution in Python and using the `strxor` function that we use in the challenge! It makes life much simpler. diff --git a/challenges/cryptography/level-3/challenge/run b/challenges/cryptography/level-3/challenge/run new file mode 100755 index 0000000..64a37b6 --- /dev/null +++ b/challenges/cryptography/level-3/challenge/run @@ -0,0 +1,16 @@ +#!/usr/bin/exec-suid -- /usr/bin/python3 -I + +from Crypto.Random import get_random_bytes +from Crypto.Util.strxor import strxor + +flag = open("/flag", "rb").read() + +key = get_random_bytes(256) +ciphertext = strxor(flag, key[:len(flag)]) + +print(f"Flag Ciphertext (hex): {ciphertext.hex()}") + +while True: + plaintext = bytes.fromhex(input("Plaintext (hex): ")) + ciphertext = strxor(plaintext, key[:len(plaintext)]) + print(f"Ciphertext (hex): {ciphertext.hex()}") diff --git a/challenges/cryptography/level-4/DESCRIPTION.md b/challenges/cryptography/level-4/DESCRIPTION.md new file mode 100644 index 0000000..fd9b03c --- /dev/null +++ b/challenges/cryptography/level-4/DESCRIPTION.md @@ -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! diff --git a/challenges/cryptography/level-4/challenge/run b/challenges/cryptography/level-4/challenge/run new file mode 100755 index 0000000..6e29902 --- /dev/null +++ b/challenges/cryptography/level-4/challenge/run @@ -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()}") diff --git a/challenges/cryptography/level-5/DESCRIPTION.md b/challenges/cryptography/level-5/DESCRIPTION.md new file mode 100644 index 0000000..a1a89d2 --- /dev/null +++ b/challenges/cryptography/level-5/DESCRIPTION.md @@ -0,0 +1,5 @@ +This is the miniboss of AES-ECB-CPA. +You don't get an easy way to build your codebook anymore: you must build it _in the prefix_. +If you can change the length of your own prefixed data based on how much of the secret you know, you can control entire blocks, and that's all you need! +Other than that, the attack remains the same. +Good luck! diff --git a/challenges/cryptography/level-5/challenge/run b/challenges/cryptography/level-5/challenge/run new file mode 100755 index 0000000..2c3611f --- /dev/null +++ b/challenges/cryptography/level-5/challenge/run @@ -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().strip() +key = get_random_bytes(16) +cipher = AES.new(key=key, mode=AES.MODE_ECB) + +while True: + pt = bytes.fromhex(input("Data? ").strip()) + flag + ct = cipher.encrypt(pad(pt, cipher.block_size)) + print(f"Ciphertext: {ct.hex()}") diff --git a/challenges/cryptography/level-6/DESCRIPTION.md b/challenges/cryptography/level-6/DESCRIPTION.md new file mode 100644 index 0000000..041eef3 --- /dev/null +++ b/challenges/cryptography/level-6/DESCRIPTION.md @@ -0,0 +1,36 @@ +So, you now (hopefully!) understand the use of AES and the various hurdles, but there has been one thing that we have not considered. +If person A (commonly refered to as [Alice](https://en.wikipedia.org/wiki/Alice_and_Bob)) wants to encrypt some data and send it to person B (commonly refered to as Bob) using AES, they must first agree on a key. +If Alice and Bob see each other in person, one might write the key down and hand it to the other. +But this rarely happens --- typically, the key must be established remotely, with Alice and Bob on either end of a (not yet encrypted!) network connection. +In these common cases, Alice and Bob must securely generate a key even if they are being eavesdropped upon (think: network sniffing)! +Fun fact: typically, the *eave*sdropper is referred to as Eve. + +An "oldie but goodie" algorithm for generating a secret key on a non-secret communication channel is the [Diffie-Hellman Key Exchange](https://en.wikipedia.org/wiki/Diffie%E2%80%93Hellman_key_exchange)! +DHKE uses the power of mathematics (specifically, Finite Fields) to come up with a key. +Let's take it step by step: + +1. First, Alice and Bob agree on a large prime number `p` to define their Finite Field (e.g., all further operations occur _modulo `p`_: a context where numbers go from `0` to `p-1`, and then loop around), along with a _root_ `g`, and exchange them in the open, content to let Eve see them. +2. Then, Alice and Bob each generate a _secret_ number (`a` for Alice's and `b` for Bob's). + These numbers are never shared. +3. Alice computes `A = (g ** a) mod p` (`g` to the `a` power modulo `p`) and Bob computes `B = (g ** b) mod p`. + Alice and Bob exchange `A` and `B` in the open. +4. At this point, Eve will have `p`, `g`, `A`, and `B`, but will be unable to recover `a` or `b`. + If it wasn't for the finite field, recovering `a` and `b` would be trivial via a logarithm-base-`g`: `log_g(A) == a` and `log_g(B) == b`. + However, this does not work in a Finite Field under a modulo because, conceptually, we have no efficient way to determine how many times the `g ** a` computation "looped around" from `p-1` to `0`, and this is needed to compute the logarithm. + This logarithm-in-a-finite-field problem is called the [Discrete Logarithm](https://en.wikipedia.org/wiki/Discrete_logarithm), and there is no efficient way to solve this without using a quantum computer. + Quantum computers' ability to solve this problem is the most immediate thing that makes them so dangerous to cryptography. +5. Alice calculates `s = (B ** a) mod p`, and since `B` was `(g ** b) mod p`, this results in `s = ((g ** b) ** a) mod p` or, applying middle school math, `s = (g ** (b*a)) mod p`. + Bob calculates `s = (A ** b) mod p`, and since `A` was `(g ** a) mod p`, this results in `s = (g ** (a*b)) mod p`. Since `a*b == b*a`, the `s` values computed by both Bob and Alice are equal! +6. Eve _cannot_ compute `s` because Eve lacks `a` or `b`. + Eve could compute `A ** B == g ** a ** g ** b`, which reduces to something like `g ** (a*(g**b))` and doesn't get Eve any closer to `s`! + Eve could also compute `A * B == (g ** a) * (g ** b) == g ** (a+b)`, but again, this is not the `s == g ** (a*b)` that Bob and Alice arrived at. + Eve is out of luck! + +Because `A` and `B` are public, they are termed _public keys_, with `a` and `b` being _private keys_. +Furthermore, you may noticed in this level that the prime number `p` that we use is hardcoded and, in fact, there are recommended DHKE [for many bitsizes](https://datatracker.ietf.org/doc/html/rfc3526). +The standardization of these primes allows Alice and Bob to just publish `A` and `B` (though, in practice, `p` is also transmitted to support the use of different `p`s in certain scenarios). + + + +In this challenge you will perform a Diffie-Hellman key exchange. +Good luck! diff --git a/challenges/cryptography/level-6/challenge/run b/challenges/cryptography/level-6/challenge/run new file mode 100755 index 0000000..3ee4bd2 --- /dev/null +++ b/challenges/cryptography/level-6/challenge/run @@ -0,0 +1,47 @@ +#!/usr/bin/exec-suid -- /usr/bin/python3 -I + +import sys +from Crypto.Random.random import getrandbits + +# 2048-bit MODP Group from RFC3526 +p = int.from_bytes(bytes.fromhex( + "FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 " + "29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD " + "EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245 " + "E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED " + "EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D " + "C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F " + "83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D " + "670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B " + "E39E772C 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9 " + "DE2BCBF6 95581718 3995497C EA956AE5 15D22618 98FA0510 " + "15728E5A 8AACAA68 FFFFFFFF FFFFFFFF" +), "big") +g = 2 +print(f"p = {p:#x}") +print(f"g = {g:#x}") + +a = getrandbits(2048) +A = pow(g, a, p) +print(f"A = {A:#x}") + +try: + B = int(input("B? "), 16) +except ValueError: + print("Invalid B value (not a hex number)", file=sys.stderr) + sys.exit(1) +if B <= 2**1024: + print("Invalid B value (B <= 2**1024)", file=sys.stderr) + sys.exit(1) + +s = pow(B, a, p) +try: + if int(input("s? "), 16) == s: + print("Correct! Here is your flag:") + print(open("/flag").read()) + else: + print("Incorrect... Should have been:", file=sys.stderr) + print(f"s = {s:#x}") +except ValueError: + print("Invalid s value (not a hex number)", file=sys.stderr) + sys.exit(1) diff --git a/challenges/cryptography/level-7/DESCRIPTION.md b/challenges/cryptography/level-7/DESCRIPTION.md new file mode 100644 index 0000000..1b61b2a --- /dev/null +++ b/challenges/cryptography/level-7/DESCRIPTION.md @@ -0,0 +1,29 @@ +Diffie-Hellman allow Alice and Bob to generate a single (but uncontrolled) shared secret with no pre-shared secret information. +Next, we'll learn about another cryptosystem, [RSA (Rivest–Shamir–Adleman)](https://en.wikipedia.org/wiki/RSA_(cryptosystem)), that allows Alice and Bob to generate arbitrary amounts of controlled messages, with no pre-shared secret information! + +RSA uses a clever interaction of modular exponentiation (which you've experienced in DH) and [Euler's Theorem](https://en.wikipedia.org/wiki/Euler%27s_theorem) to give Bob or Alice _asymmetric_ control over an entire finite field. +Alice generates two primes, `p` and `q`, _and keeps them secret_, then multiplies them to create `n = p*q`, which Alice publishes to define a Finite Field modulo `n`. +Euler's Theorem and knowledge of `p` and `q` gives Alice, _and only Alice_, full abilities within this specific field (which is a difference from DH, where all actors have equal capabilities in the field!). + +Euler's Theorem tells us that operations _in an exponent_ of a field modulo `p*q` (e.g., the `e*d` of `m**(e*d) mod n`) take place in the field of `(p-1)*(q-1)`. +The _why_ of this theorem is some advanced math stuff that, to be honest, few people understand, but the results are interesting. +Computing `(p-1)*(q-1)` is trivial for Alice (armed with knowledge of `p` and `q`) but impossible to anyone else (assuming that `p` and `q` are large), because the human race lacks an efficient algorithm to factor products of large prime numbers! + +Recall that `e*d` in the exponent of `m**(e*d) mod n`? +For any `e`, knowing `(p-1)*(q-1)` allows Alice to compute a `d` such that `e*d == 1`. +While this seems silly, it is the core of RSA. +Alice chooses a number `e` (typically fairly small to reduce computation costs, but not too small to cause certain security issues) and computes the corresponding multiplicative inverse `d`. +This leads to encryption of plaintext `m` (`m**e mod n == c`) and decryption! `c**d mod n == (m**e)**d mod n == m**(e*d) mod n == m**1 mod n == m`. +Rather than DH's single and uncontrolled `s`, RSA messages `m` can be chosen arbitrarily (up to the size of `n`, as the field is unable to represent larger numbers). + +RSA is _asymmetric_. +Alice shares `n` and `e` as the public key, and keeps `d` as the private key. +Knowing `n` and `e`, Bob can encrypt messages and send them to Alice, and only Alice can decrypt them. +Since `e*d == d*e`, Alice _can_ use `d` to encrypt messages, but _anyone_ can decrypt them, since `e` is public. +This might sound silly, but it is useful for, e.g., attesting that a given message could come only from Alice, since knowledge of `d` is required for this. + +To respond to Bob, Alice would need Bob's own public key, which would be _Bob's_ `n` (different from Alice's `n`, using Bob's own secret `p` and `q`!) and `e` (which is typically the same smallest-safe value, currently `65537` but subject to change as new attacks are found). + +In this challenge you will decrypt a secret encrypted with RSA (Rivest–Shamir–Adleman). +You will be provided with both the public key and private key this time, to get a feel for how all this works. +Go for it! diff --git a/challenges/cryptography/level-7/challenge/run b/challenges/cryptography/level-7/challenge/run new file mode 100755 index 0000000..b296fd6 --- /dev/null +++ b/challenges/cryptography/level-7/challenge/run @@ -0,0 +1,14 @@ +#!/usr/bin/exec-suid -- /usr/bin/python3 -I + +from Crypto.PublicKey import RSA + +flag = open("/flag", "rb").read() +assert len(flag) <= 256 + +key = RSA.generate(2048) +print(f"(public) n = {key.n:#x}") +print(f"(public) e = {key.e:#x}") +print(f"(private) d = {key.d:#x}") + +ciphertext = pow(int.from_bytes(flag, "little"), key.e, key.n).to_bytes(256, "little") +print(f"Flag Ciphertext (hex): {ciphertext.hex()}") diff --git a/challenges/cryptography/level-8/DESCRIPTION.md b/challenges/cryptography/level-8/DESCRIPTION.md new file mode 100644 index 0000000..ae06cdc --- /dev/null +++ b/challenges/cryptography/level-8/DESCRIPTION.md @@ -0,0 +1,11 @@ +Alice's superpower under modulo `n` comes from knowledge of `p` and `q`, and, thus, the ability to compute the multiplicative inverse of `e` in the exponent. +One worry of everyone who uses RSA is that their `n` will get factored, and attackers will gain `p` and `q`. + +This is not an unreasonable worry. +While we _believe_ that factoring is hard, we have no actual proof that it is. +It is not outside of the realm of possibility that, tomorrow, Euler 2.0 will publish an algorithm for doing just this. +However, we _do_ know that functional quantum computers can factor: Euler 2.0 (actually, [Peter Shor](https://en.wikipedia.org/wiki/Shor%27s_algorithm)) already came up with the algorithm! +When quantum computers get to a sufficient power level, RSA is cooked. + +In this challenge, we give you the quantum computer (or, at least, we give you `n`'s factors)! +Use them to decrypt the flag that we encrypted with RSA (Rivest–Shamir–Adleman). diff --git a/challenges/cryptography/level-8/challenge/run b/challenges/cryptography/level-8/challenge/run new file mode 100755 index 0000000..645754e --- /dev/null +++ b/challenges/cryptography/level-8/challenge/run @@ -0,0 +1,14 @@ +#!/usr/bin/exec-suid -- /usr/bin/python3 -I + +from Crypto.PublicKey import RSA + +flag = open("/flag", "rb").read() +assert len(flag) <= 256 + +key = RSA.generate(2048) +print(f"e = {key.e:#x}") +print(f"p = {key.p:#x}") +print(f"q = {key.q:#x}") + +ciphertext = pow(int.from_bytes(flag, "little"), key.e, key.n).to_bytes(256, "little") +print(f"Flag Ciphertext (hex): {ciphertext.hex()}") diff --git a/challenges/cryptography/level-9/DESCRIPTION.md b/challenges/cryptography/level-9/DESCRIPTION.md new file mode 100644 index 0000000..163aa79 --- /dev/null +++ b/challenges/cryptography/level-9/DESCRIPTION.md @@ -0,0 +1,19 @@ +As you saw, raw RSA signatures are a bad idea, as they can be forged. +In practice, what people sign are [_cryptographic hashes_](https://en.wikipedia.org/wiki/Cryptographic_hash_function) of things. +A hash is a one-way function that takes an arbitrary amount of input (e.g., bytes or gigabytes or more) and outputs a short (e.g., 32 bytes) of output hash. +Any changes in the input to the hash will _diffuse_ all over the resulting cryptographic hash in a way that is not reversible. + +Thus, secure hashes are a good representation for the original data: if Alice signs a hash of a message, that message can be seen as being signed as well. +Better yet, since hashes are not controllably reversible or modifiable, an attacker being able to modify a hash does not allow them to forge a signature on a new message. + +The bane of cryptographic hashing algorithms is _collision_. +If an attacker can craft two messages that hash to the same thing, the security of any system that depends on the hash (such as the RSA signature scheme described above) might be compromised. +For example, consider that the security of bitcoin depends fully on the collision resistance of SHA256... + +While full collisions of SHA256 don't exist, some applications use _partial_ hash verification. +This is not a great practice, as it makes it easier to brute-force a collision. + +In this challenge you will do just that, hashing data with a Secure Hash Algorithm (SHA256). +You will find a small hash collision. +Your goal is to find data, which when hashed, has the same hash as the secret. +Only the first 3 bytes of the SHA256 hash will be checked. diff --git a/challenges/cryptography/level-9/challenge/run b/challenges/cryptography/level-9/challenge/run new file mode 100755 index 0000000..8ee9af6 --- /dev/null +++ b/challenges/cryptography/level-9/challenge/run @@ -0,0 +1,16 @@ +#!/usr/bin/exec-suid -- /usr/bin/python3 -I + +import hashlib + + +flag = open("/flag").read() +prefix_length = 6 +flag_hash = hashlib.sha256(flag.encode("latin")).hexdigest() +print(f"{flag_hash[:prefix_length]=}") + +collision = bytes.fromhex(input("Colliding input? ").strip()) +collision_hash = hashlib.sha256(collision).hexdigest() +print(f"{collision_hash[:prefix_length]=}") +if collision_hash[:prefix_length] == flag_hash[:prefix_length]: + print("Collided!") + print(flag) diff --git a/challenges/cryptography/otp-tamper/.init b/challenges/cryptography/otp-tamper/.init new file mode 100755 index 0000000..fc5fecd --- /dev/null +++ b/challenges/cryptography/otp-tamper/.init @@ -0,0 +1,4 @@ +#!/bin/bash + +dd if=/dev/urandom of=/challenge/.key bs=16 count=1 +chmod 600 /challenge/.key diff --git a/challenges/cryptography/otp-tamper/DESCRIPTION.md b/challenges/cryptography/otp-tamper/DESCRIPTION.md new file mode 100644 index 0000000..859f665 --- /dev/null +++ b/challenges/cryptography/otp-tamper/DESCRIPTION.md @@ -0,0 +1,4 @@ +So, the One Time Pad is proven to be secure... but only in the _Confidential_ sense! +It actually does not guarantee anything about Integrity. +This challenge asks you: what if you could _tamper_ with the message in transit? +Think about how XOR works, and see if you can get the flag! diff --git a/challenges/cryptography/otp-tamper/challenge/dispatcher b/challenges/cryptography/otp-tamper/challenge/dispatcher new file mode 100755 index 0000000..d5e9b3d --- /dev/null +++ b/challenges/cryptography/otp-tamper/challenge/dispatcher @@ -0,0 +1,8 @@ +#!/usr/bin/exec-suid -- /usr/bin/python3 -I + +from Crypto.Util.strxor import strxor + +key = open("/challenge/.key", "rb").read() +ciphertext = strxor(b"sleep", key[:5]) + +print(f"TASK: {ciphertext.hex()}") diff --git a/challenges/cryptography/otp-tamper/challenge/worker b/challenges/cryptography/otp-tamper/challenge/worker new file mode 100755 index 0000000..35dbf1a --- /dev/null +++ b/challenges/cryptography/otp-tamper/challenge/worker @@ -0,0 +1,26 @@ +#!/usr/bin/exec-suid -- /usr/bin/python3 -I + +from Crypto.Util.strxor import strxor + +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]) + cipher_len = min(len(data), len(key)) + plaintext = strxor(data[:cipher_len], key[:cipher_len]) + + print(f"Hex of plaintext: {plaintext.hex()}") + print(f"Received command: {plaintext}") + if plaintext == b"sleep": + print("Sleeping!") + time.sleep(1) + elif plaintext == b"flag!": + print("Victory! Your flag:") + print(open("/flag").read()) + else: + print("Unknown command!") diff --git a/challenges/cryptography/rsa-sig/.init b/challenges/cryptography/rsa-sig/.init new file mode 100755 index 0000000..20c0445 --- /dev/null +++ b/challenges/cryptography/rsa-sig/.init @@ -0,0 +1,16 @@ +#!/usr/bin/env python + +import os + +from Crypto.PublicKey import RSA + +key = RSA.generate(2048) + +with open("/challenge/key-n", "w") as o: + o.write(hex(key.n)) +with open("/challenge/key-e", "w") as o: + o.write(hex(key.e)) +with open("/challenge/key-d", "w") as o: + o.write(hex(key.d)) +os.chmod("/challenge/key-d", 0o600) + diff --git a/challenges/cryptography/rsa-sig/DESCRIPTION.md b/challenges/cryptography/rsa-sig/DESCRIPTION.md new file mode 100644 index 0000000..9ca2629 --- /dev/null +++ b/challenges/cryptography/rsa-sig/DESCRIPTION.md @@ -0,0 +1,10 @@ +So by using `d`, Alice can encrypt data that (because `n` and `e` are in the public key) anyone can decrypt... +This might seem silly, but it actually enables a capability that we haven't yet seen in the module: the ability to attest to multiple people that a message came from Alice. +This can serve as a sort of cryptographic version of a pen-and-ink signature and, in fact, it is called a _signature_! + +This level will explore one application (and pitfall) of RSA signatures. +Recall that `c == m**e mod n`, and recall from middle school that `(x**e)*(y**e) == (x*y)**e`. +This holds just as well in `mod n`, and you can probably see the issue here... + +This level gives you a signing oracle. +Go use it to craft a flag command! diff --git a/challenges/cryptography/rsa-sig/challenge/dispatcher b/challenges/cryptography/rsa-sig/challenge/dispatcher new file mode 100755 index 0000000..3afca24 --- /dev/null +++ b/challenges/cryptography/rsa-sig/challenge/dispatcher @@ -0,0 +1,21 @@ +#!/usr/bin/exec-suid -- /usr/bin/python3 -I + +import sys + +from base64 import b64encode, b64decode + +n = int(open("/challenge/key-n").read(), 16) +d = int(open("/challenge/key-d").read(), 16) + +if len(sys.argv) < 2: + print(f"Usage: {sys.argv[0]} [command-b64]") + sys.exit(1) + +command = b64decode(sys.argv[1].strip("\0")) + +if b"flag" in command: + print(f"Command contains 'flag'") + sys.exit(1) + +signature = pow(int.from_bytes(command, "little"), d, n).to_bytes(256, "little") +print(f"Signed command (b64): {b64encode(signature).decode()}") diff --git a/challenges/cryptography/rsa-sig/challenge/worker b/challenges/cryptography/rsa-sig/challenge/worker new file mode 100755 index 0000000..acfeff5 --- /dev/null +++ b/challenges/cryptography/rsa-sig/challenge/worker @@ -0,0 +1,21 @@ +#!/usr/bin/exec-suid -- /usr/bin/python3 -I + +import sys + +from base64 import b64decode + +n = int(open("/challenge/key-n").read(), 16) +e = int(open("/challenge/key-e").read(), 16) + +if len(sys.argv) < 2: + print(f"Usage: {sys.argv[0]} [signature-b64]") + sys.exit(1) + +signature = b64decode(sys.argv[1]) +c = int.from_bytes(signature, "little") +assert c < n, "Message too big!" +command = pow(c, e, n).to_bytes(256, "little").rstrip(b"\x00") + +print(f"Received signed command: {command}") +if command == b"flag": + print(open("/flag").read()) diff --git a/challenges/cryptography/strxor/DESCRIPTION.md b/challenges/cryptography/strxor/DESCRIPTION.md new file mode 100644 index 0000000..6b913c1 --- /dev/null +++ b/challenges/cryptography/strxor/DESCRIPTION.md @@ -0,0 +1,54 @@ +Okay, now you know how to XOR ASCII characters. +This is a critical step as we build up to our first cryptosystem, but now, we need to XOR entire ASCII strings! +Let's try this. + +Like Python provides the `^` operator to XOR integers, a Python library called PyCryptoDome provides a function called `strxor` to XOR two strings of characters together. +You can import it in Python using `from Crypto.Util.strxor import strxor`. + +XORing two strings is done byte by byte, just like XORing two bytes is done bit by bit. +So, to draw on an earlier example: + +```console +hacker@dojo:~$ python +>>> from Crypto.Util.strxor import strxor +>>> strxor(b"AAA", b"16/") +b'pwn' +``` + +You can verify this yourself with the ASCII table: A ^ 1 is p, A ^ 6 is w, and A ^ / is n. +We just decrypted the _ciphertext_ `AAA` with the _key_ `16/` to retrieve the _plaintext_ `pwn`. + +In this challenge, you'll do this several times in a row: like the previous challenge, but with strings! +Good luck! + +---- +**CAVEAT:** +What are these `b`s prepended to the quotes? +Python's default string representation (e.g., `"AAA"`) is [_Unicode_](https://en.wikipedia.org/wiki/Unicode), and unlike, say, the Latin alphabet, Unicode encompasses all characters known to humanity (including the Latin alphabet)! +This means a single character can have thousands of different values (when this text was written, Unicode encompassed 154,998 characters!), from "A" to "💩". + +Unfortunately, a single byte of 8 bits can only hold `2**8 == 256` different values, which is enough for ASCII (not that many letters/numbers/etc in the Latin alphabet), but not enough for Unicode. +Unicode is _encoded_ using different encodings, such as the [UTF-8](https://en.wikipedia.org/wiki/UTF-8) we mentioned earlier. +UTF-8 is designed to be backwards-compatible with ASCII "A" is just 0x41, something like "💩" is _four_ bytes: `f0 9f 92 a9`! + +Basically, `ASCII` is to `The Latin Alphabet` as `UTF-8` is to `Unicode`, and in the same way that the Latin alphabet is a subset of Unicode, ASCII is a subset of UTF-8. +Wild. + +Anyways, Python's normal strings (and, typically, `input()` you get from the terminal) are Unicode, but some functions, such as `strxor`, consume and produce _bytes_. +You can specify them directly, as I did above, by prepending your quotes with `b` (for **b**ytes) and using ASCII or hex encoding (e.g., `b"AAA"` and `b"A\x41\x41"` are equivalent), or you can _encode_ a Unicode string into bytes using UTF-8, as such: `"AAA".encode() == b"AAA"` or `"💩".encode() == b"\xf0\x9f\x92\xa9"`. +You can also _decode_ the resulting bytes back into Unicode strings: `b"AAA".decode() == "AAA"` or `b"\xf0\x9f\x92\xa9".decode() == "💩"`. + +This is _further_ complicated by the fact that UTF-8 can't turn any arbitrary bytes into Unicode. +For example, `b'\xb0'.decode()` raises an exception. +You can fix this by abandoning the default UTF-8 and using a pre-Unicode non-encoding encoding like "[latin](https://en.wikipedia.org/wiki/ISO/IEC_8859-1)"/ISO-8859-1, from the ancient days of computing, as so: `b'\xb0'.decode('latin')`. +While ISO-8859-1 originally predated Unicode, its Python implementation converts to Unicode strings. +However, keep in mind that this encoding is _different_ from UTF-8: `b"\xb0".encode('latin").decode() == b'\xc2\xb0'`. +You must, instead, be consistent and decode and encode with the same encoding: `b"\xb0".encode('latin").decode(latin1) == b"\xb0"`. + +Anyways, all this sounds terrifying, but it's mostly a warning for the future. +For _this_ level, we VERY carefully chose the characters so that you don't run into these issues. + +**CAUTION:** +Python's strings-vs-bytes situation is terrible and _will_ byte (haha!) you eventually. +There's no way to avoid pitfalls --- they still get us after years and years of using Python, so you will just have to learn to pick yourself up, brush yourself off, fix your code, and carry on. +With enough experience under your belt, you will improve from losing _entire freaking days_ to bugs caused by string/bytes mixups to merely _entire freaking hours_. diff --git a/challenges/cryptography/strxor/challenge/run b/challenges/cryptography/strxor/challenge/run new file mode 100755 index 0000000..8b3ae46 --- /dev/null +++ b/challenges/cryptography/strxor/challenge/run @@ -0,0 +1,34 @@ +#!/usr/bin/exec-suid -- /usr/bin/python3 -I + +import random +import string +import sys + +from Crypto.Util.strxor import strxor + +valid_keys = "!#$%&()" +valid_chars = ''.join( + c for c in string.ascii_letters + if all(chr(ord(k)^ord(c)) in string.ascii_letters for k in valid_keys) +) + +print(valid_keys, valid_chars) + +for n in range(1, 10): + print(f"Challenge number {n}...") + + key_str = ''.join(random.sample(valid_keys*10, 10)) + pt_str = ''.join(random.sample(valid_chars*10, 10)) + ct_str = strxor(pt_str.encode(), key_str.encode()).decode() + + print(f"- Encrypted String: {ct_str}") + print(f"- XOR Key String: {key_str}") + answer = input("- Decrypted String? ").strip() + if answer != pt_str: + print("Incorrect!") + sys.exit(1) + + print("Correct! Moving on.") + +print("You have mastered XORing ASCII! Your flag:") +print(open("/flag").read()) diff --git a/challenges/cryptography/xor/DESCRIPTION.md b/challenges/cryptography/xor/DESCRIPTION.md new file mode 100644 index 0000000..ed53c77 --- /dev/null +++ b/challenges/cryptography/xor/DESCRIPTION.md @@ -0,0 +1,23 @@ +Strangely enough, we'll start our crypto journey with the humble [Exclusive Or](https://en.wikipedia.org/wiki/Exclusive_or) (XOR) operator. +An XOR is one of the most common [bitwise operators](https://en.wikipedia.org/wiki/Logical_connective) that you will encounter in your security journey, _especially_ in cryptography. +A couple of terms to unpack here... + +**Bitwise.** +Remember from [Dealing with Data](/fundamentals/data-dealings/) that computers think in binary! +That is, they conceptualize numbers in [base 2](https://www.google.com/search?q=learn+number+bases), so something like `9` is expressed as `1001`. +An XOR operates on one pair of bits at a time, resulting in in `1` if the bits are different (one is `1` and the other is `0`) or `0` if they are the same (both `1` or both `0`). +It is then applied to every bit pair independently, and the results are concatenated. +For example, decimal `9` (`1001`) XORed with decimal `5` (`0101`) results in `1100` (decimal 12). + +**Cryptography.** +Why is XOR so common in crypto? +In cryptography, it is common because it is [_self-inverse_](https://en.wikipedia.org/wiki/Exclusive_or#Properties)! +That is (using `^` for XOR here, which is consistent with many programming languages), `5 ^ 9 == 12`, and `12 ^ 9 == 5`. +If the number `9` is a key only known to you and me, I can send you messages by XORing them with `9`, and you can recover the message with XORing them with `9` as well! +Obviously, we can achieve this property with me adding 9 and you subtracting 9, without using XOR, but this requires more complex circuitry and extra bits (e.g., to handle "carrying the 1" in `1111 + 0001 == 10000`), whereas XOR does not have this problem (`1111 ^ 0001 == 1110`). + +In this level, you will learn to XOR! +We'll give you a shared _key_, `XOR` a secret number with it, and expect you to recover the number. + +---- +**HINT:** Use Python's `^` operator to XOR integers! diff --git a/challenges/cryptography/xor/challenge/run b/challenges/cryptography/xor/challenge/run new file mode 100755 index 0000000..52b096f --- /dev/null +++ b/challenges/cryptography/xor/challenge/run @@ -0,0 +1,17 @@ +#!/usr/bin/exec-suid -- /usr/bin/python3 -I + +import random +import sys + +key = random.randrange(1, 256) +plain_secret = random.randrange(0, 256) +cipher_secret = plain_secret ^ key + +print(f"The key: {key}") +print(f"Encrypted secret: {cipher_secret}") +if int(input("Decrypted secret? ")) == plain_secret: + print("CORRECT! Your flag:") + print(open("/flag").read()) +else: + print("INCORRECT!") + sys.exit(1)