Skip to content

Commit a79c78f

Browse files
committed
docs: Add crypto writeups
1 parent d1090e0 commit a79c78f

File tree

4 files changed

+197
-0
lines changed

4 files changed

+197
-0
lines changed

Diff for: crypto/ancient_encodings.md

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Ancient encodings (very easy)
2+
This challenge just base64 encodes the input, converts it to bytes and writes the hex representation.
3+
4+
To solve this we just do the inverse operation on all of these in reverse order.
5+
6+
```python
7+
import base64
8+
content = open('./output.txt', 'r').read()
9+
cont = bytes.fromhex(content[2:])
10+
print(base64.b64decode(cont).decode('utf-8'))
11+
```

Diff for: crypto/multipage_recyclings.md

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# Multipage recyclings (easy)
2+
This challenge was about exploiting some leak from the encryption process.
3+
4+
Let's see first how the challeng encrypts things:
5+
```python
6+
def encrypt(self, message):
7+
iv = os.urandom(16)
8+
9+
ciphertext = b''
10+
plaintext = iv
11+
12+
blocks = self.blockify(message, 16)
13+
for block in blocks:
14+
ct = self.cipher.encrypt(plaintext)
15+
encrypted_block = self.xor(block, ct)
16+
ciphertext += encrypted_block
17+
plaintext = encrypted_block
18+
19+
return ciphertext
20+
```
21+
22+
* For the cipher AES ECB is used.
23+
* A random IV is chosen
24+
* The input is converted into 16 byte blocks
25+
* The IV is encrypted = `ct`
26+
* The plaintext block is xored with `ct` = `encrypted_block`
27+
* `encrypted_block` is used as IV for the next iteration
28+
29+
Now let's see how the leak works:
30+
31+
```python
32+
def leak(self, blocks):
33+
r = random.randint(0, len(blocks) - 2)
34+
leak = [self.cipher.encrypt(blocks[i]).hex() for i in [r, r + 1]]
35+
return r, leak
36+
```
37+
38+
Okay leak chooses 2 random consecutive blocks, encryptes them, and return the indices and the encrypted result.
39+
40+
Finally let's look at how main uses these 2 functions:
41+
42+
```python
43+
aes = CAES()
44+
message = pad(FLAG * 4, 16)
45+
46+
ciphertext = aes.encrypt(message)
47+
ciphertext_blocks = aes.blockify(ciphertext, 16)
48+
49+
r, leak = aes.leak(ciphertext_blocks)
50+
```
51+
52+
* The message will be the flag repeated 4 times.
53+
* We encrypt the message
54+
* We leak from the encrypted message
55+
56+
From the output we know that blocks 3 and 4 were leaked.
57+
Now how can we exploit this?
58+
59+
We know how a certain encrypted block is generated
60+
`E = encrypt(P) ^ C`, where
61+
* `E` is the new encrypted block
62+
* `P` is the IV for the current iteration
63+
* `C` is the current plaintext block
64+
65+
But we know that `E` will become `P` in the next iteration:
66+
`F = encrypt(E) ^ D`, where
67+
* `F` is the new encrypted block
68+
* `E` is the encrypted block from the previous equation
69+
* `D` is the current plaintext block
70+
71+
So for the second equation it is the case that we know:
72+
* `F` - because this is in the cipher text
73+
* `encrypt(E)` - because this is a block that was leaked
74+
75+
So `D = F ^ encrypt(E)`.
76+
77+
We can recover 2 plaintext blocks, by xoring an encrypted, leaked block with the cipher text block that has index one higher.
78+
79+
Here is my script to solve the challenge:
80+
```
81+
ct = 'bc9bc77a809b7f618522d36ef7765e1cad359eef39f0eaa5dc5d85f3ab249e788c9bc36e11d72eee281d1a645027bd96a363c0e24efc6b5caa552b2df4979a5ad41e405576d415a5272ba730e27c593eb2c725031a52b7aa92df4c4e26f116c631630b5d23f11775804a688e5e4d5624'
82+
r = 3
83+
phrases = ['8b6973611d8b62941043f85cd1483244', 'cf8f71416111f1e8cdee791151c222ad']
84+
85+
def blockify(message, size):
86+
return [message[i:i + size] for i in range(0, len(message), size)]
87+
88+
def xor(a, b):
89+
return b''.join([bytes([_a ^ _b]) for _a, _b in zip(a, b)])
90+
91+
ct = bytes.fromhex(ct)
92+
blocks = blockify(ct, 16)
93+
print(xor(blocks[4], bytes.fromhex(phrases[0])))
94+
print(xor(blocks[5], bytes.fromhex(phrases[1])))
95+
96+
# HTB{CFB_15_w34k_w17h_l34kz}
97+
```

Diff for: crypto/perfect_synchronization.md

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Perfect synchronization (very easy)
2+
This challenge is about using frequency analysis to break encryption.
3+
4+
The flag is encrypted, such that each character is passed to AES ECB encryption.
5+
This means that the same letters will generate the same cipher text as well.
6+
Further more the range of input is limited to upper case letters, curly braces `_`, and space.
7+
8+
Now the challenge did drop the hint to use the `quipqiup` tool, however I couldn't manage to figure out during the contest how to make it work so I just did it by hand.
9+
10+
I wrote a simple script to analyse the frequency of the strings.
11+
Here I have found 2 strings with frequency of 1, these must be the curly braces for the flag!
12+
13+
Then we also know that before the opening brace the strings must correspond to HTB
14+
15+
From here it was a matter of substituting all occurrences of a string with the letter we know it corresponds to, and then finding words we can guess the letters of, for example: `THE`, `THAT` already had most letters revealed, just by knowing `HTB`.
16+
17+
I continued this process until enough letters were revealed so that the flag is completely known.

Diff for: crypto/small_steps.md

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# Small steps (very easy)
2+
In this challenge textbook RSA will be exploited.
3+
4+
```python
5+
def __init__(self):
6+
self.q = getPrime(256)
7+
self.p = getPrime(256)
8+
self.n = self.q * self.p
9+
self.e = 3
10+
11+
def encrypt(self, plaintext):
12+
plaintext = bytes_to_long(plaintext)
13+
return pow(plaintext, self.e, self.n)
14+
```
15+
16+
This is the RSA encrypt, implementation the challenge uses.
17+
18+
We get the encrypted value, `n` and `e` from the remote.
19+
20+
The vulnerability here is that `e` is too small, 3 in this case.
21+
When `e` is too small `M^e mod n` becomes `M^e` since it will never go above `n` for certain messages.
22+
23+
This means that we can just take the third root of the encrypted message and get back the flag.
24+
25+
Adapting the solver script with my solution in the `pwn` function
26+
27+
```python
28+
# This script is not necessary for the challenge but may be useful in the
29+
# future.
30+
from pwn import *
31+
import gmpy2
32+
from Crypto.Util.number import long_to_bytes
33+
34+
# This function takes in binary data and converts it to ASCII.
35+
def toAscii(data):
36+
return data.decode().strip()
37+
38+
39+
# This function sends the string "E" to the server and retrieves the public key
40+
# and encrypted flag that are returned. The public key consists of two parts:
41+
# N and e.
42+
def choiceE():
43+
r.sendlineafter(b"> ", b"E")
44+
r.recvuntil(b"N: ")
45+
N = eval(toAscii(r.recvline()))
46+
r.recvuntil(b"e: ")
47+
e = eval(toAscii(r.recvline()))
48+
r.recvuntil(b"The encrypted flag is: ")
49+
encrypted_flag = eval(toAscii(r.recvline()))
50+
return N, e, encrypted_flag
51+
52+
53+
# This function serves as the main logic of the solver script. It calls
54+
# `choiceE()` to retrieve the public key and encrypted flag and prints them.
55+
def pwn():
56+
N, e, encrypted_flag = choiceE()
57+
print(N, e, encrypted_flag)
58+
pt = gmpy2.iroot(encrypted_flag, e)
59+
print(long_to_bytes(pt[0]))
60+
61+
# This block handles the command-line flags when running `solver.py`. If the
62+
# `REMOTE` flag is set, the script connects to the remote host specified by the
63+
# `HOST` flag. Otherwise, it starts the server locally using `process()`.
64+
if __name__ == "__main__":
65+
if args.REMOTE:
66+
ip, port = args.HOST.split(":")
67+
r = remote(ip, int(port))
68+
else:
69+
r = process(["python3", "server.py"])
70+
71+
pwn()
72+
```

0 commit comments

Comments
 (0)