Skip to content

Commit acd0342

Browse files
committed
added haggis writeup
1 parent 5d0d696 commit acd0342

File tree

2 files changed

+254
-0
lines changed

2 files changed

+254
-0
lines changed
+236
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
# Haggis (Crypto 100)
2+
3+
###ENG
4+
[PL](#pl-version)
5+
6+
In the task we get [source code](haggis.py) using AES CBC.
7+
The code is quite short and straightforward:
8+
9+
```python
10+
pad = lambda m: m + bytes([16 - len(m) % 16] * (16 - len(m) % 16))
11+
def haggis(m):
12+
crypt0r = AES.new(bytes(0x10), AES.MODE_CBC, bytes(0x10))
13+
return crypt0r.encrypt(len(m).to_bytes(0x10, 'big') + pad(m))[-0x10:]
14+
15+
target = os.urandom(0x10)
16+
print(binascii.hexlify(target).decode())
17+
18+
msg = binascii.unhexlify(input())
19+
20+
if msg.startswith(b'I solemnly swear that I am up to no good.\0') \
21+
and haggis(msg) == target:
22+
print(open('flag.txt', 'r').read().strip())
23+
```
24+
25+
The server gets random 16 bytes and sends them to us.
26+
Then we need to provide a message with a pre-defined prefix.
27+
This message is concatenated with the length of the message and encrypted, and the last block of the this ciphertext has to match the random 16 bytes we were given.
28+
29+
We know that it's AES CBC, we know the key is all `0x0` and so is the `IV`.
30+
We also know that the cipher is using PKCS7 padding scheme.
31+
32+
We start by filling the prefix until the end of 16 bytes AES block.
33+
It's always easier to work with full blocks:
34+
35+
```python
36+
msg_start = b'I solemnly swear that I am up to no good.\x00\x00\x00\x00\x00\x00\x00'
37+
```
38+
39+
We will add one more block after this one.
40+
Keeping this in mind we calculate the length of the full message and construct the length block, just as the server will do:
41+
42+
```python
43+
len_prefix = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00@'
44+
```
45+
46+
This way we know exactly what the server is going to encrypt.
47+
48+
It's worth to understand how CBC mode works: each block before encryption is XORed with previous block ciphertext (and first block with IV).
49+
This means that if we know the ciphertext of encrypted K blocks, we can encrypt additional blocks simply by passing the last block of K as IV.
50+
We are going to leverage this here!
51+
First we calculate the first K blocks (2 to be exact, the length block and the message we got in the task):
52+
53+
```python
54+
encryptor = AES.new(bytes(0x10), AES.MODE_CBC, bytes(0x10))
55+
prefix_encrypted_block = encryptor.encrypt(len_prefix + msg_start)[-16:]
56+
```
57+
58+
We need only the last block because this is what is going to be XORed with our additional payload block before encryption.
59+
We should remember that there is PCKS padding here, so by adding a whole block of our choosing, the actual last ciphertext block will be encrypted padding!
60+
So we actually need to make sure this encrypted padding block matches the given target bytes.
61+
We know the padding bytes - they will all be `0x10`, but they will get xored with the ciphertext of the payload block we are preparing before encryption.
62+
Let's call ciphertext of our payload block `CT_payload`, and the plaintext of this block as `payload`.
63+
64+
Let's look what exactly we need to do:
65+
66+
We want to get: `encrypt(CT_payload xor padding) = target` therefore by applying decryption we get:
67+
68+
`CT_payload xor padding = decrypt(target)`
69+
70+
and since xor twice by the same value removes itself:
71+
72+
`CT_payload = decrypt(target) xor padding`
73+
74+
Now let's look where we get the `CT_payload` from:
75+
76+
`CT_payload = encrypt(payload xor prefix_encrypted_block)`
77+
78+
and by applying decryption:
79+
80+
`decrypt(CT_payload) = payload xor prefix_encrypted_block`
81+
82+
and thus:
83+
84+
`payload = decrypt(CT_payload) xor prefix_encrypted_block`
85+
86+
And if we now combine the two we get:
87+
88+
`payload = decrypt(decrypt(target) xor padding) xor prefix_encrypted_block`
89+
90+
And this is how we can calculate the payload we need to send.
91+
92+
We implement this in python:
93+
94+
```python
95+
def solve_for_target(target):
96+
# enc(ct xor padding) = target
97+
# ct xor padding = dec(target)
98+
# ct = dec(target) xor padding
99+
# ct = enc (pt xor enc_prefix)
100+
# dec(ct) = pt xor enc_prefix
101+
# pt = dec(ct) xor enc_prefix
102+
target = binascii.unhexlify(target)
103+
encryptor = AES.new(bytes(0x10), AES.MODE_CBC, bytes(0x10))
104+
data = encryptor.decrypt(target)[-16:] # ct xor padding
105+
last_block = b''
106+
expected_ct_bytes = b''
107+
for i in range(len(data)):
108+
expected_ct = (data[i] ^ 0x10) # ct
109+
expected_ct_byte = expected_ct.to_bytes(1, 'big')
110+
expected_ct_bytes += expected_ct_byte
111+
encryptor = AES.new(bytes(0x10), AES.MODE_CBC, bytes(0x10))
112+
result_bytes = encryptor.decrypt(expected_ct_bytes) # dec(ct)
113+
for i in range(len(result_bytes)):
114+
pt = result_bytes[i] ^ prefix_encrypted_block[i] # dec(ct) xor enc_prefix
115+
last_block += pt.to_bytes(1, 'big')
116+
return binascii.hexlify(msg_start + last_block)
117+
```
118+
119+
And by sending this to the server we get: `hxp{PLz_us3_7h3_Ri9h7_PRiM1TiV3z}`
120+
121+
###PL version
122+
123+
W zadaniu dostajemy [kod źródłowy](haggis.py) używający AESa CBC.
124+
Kod jest dość krótki i zrozumiały:
125+
126+
```python
127+
pad = lambda m: m + bytes([16 - len(m) % 16] * (16 - len(m) % 16))
128+
def haggis(m):
129+
crypt0r = AES.new(bytes(0x10), AES.MODE_CBC, bytes(0x10))
130+
return crypt0r.encrypt(len(m).to_bytes(0x10, 'big') + pad(m))[-0x10:]
131+
132+
target = os.urandom(0x10)
133+
print(binascii.hexlify(target).decode())
134+
135+
msg = binascii.unhexlify(input())
136+
137+
if msg.startswith(b'I solemnly swear that I am up to no good.\0') \
138+
and haggis(msg) == target:
139+
print(open('flag.txt', 'r').read().strip())
140+
```
141+
142+
Serwer losuje 16 bajtów i wysyła je do nas.
143+
Następnie musimy odesłać wiadomość z zadanym prefixem.
144+
Ta wiadomość jest sklejana z długością wiadomości i następnie szyfrowana, a ostatni block ciphertextu musi być równy wylosowanym 16 bajtom które dostaliśmy.
145+
146+
Wiemy że to AES CBC, wiemy że klucz to same `0x0` i tak samo `IV` to same `0x0`.
147+
Wiemy też że jest tam padding PKCS7.
148+
149+
Zacznijmy od dopełnienia bloku z prefixem do 16 bajtów.
150+
Zawsze wygodniej pracuje się na pełnych blokach:
151+
152+
```python
153+
msg_start = b'I solemnly swear that I am up to no good.\x00\x00\x00\x00\x00\x00\x00'
154+
```
155+
156+
Dodamy jeszcze jeden blok za tym prefixem.
157+
Mając to na uwadze obliczamy długość pełnej wiadomości i tworzymy blok z długością tak samo jak zrobi to serwer:
158+
159+
```python
160+
len_prefix = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00@'
161+
```
162+
163+
W ten sposób wiemy dokładnie co serwer będzie szyfrował.
164+
165+
Warto rozumieć jak działa tryb CBC: każdy blok przed szyfrowaniem jest XORowany z ciphertextem poprzedniego bloku (a pierwszy blok z IV).
166+
To oznacza że jeśli znamy ciphertext zakodowanych K bloków, możemy zakodować dodatkowe bloku po prostu poprzez ustawienie jako IV ostatniego bloku znanego ciphertextu.
167+
Wykorzystamy tutaj tą własność!
168+
169+
Najpierw obliczmy ciphtextex pierwszyh K bloków (dla ścisłości 2 bloków - bloku z długością wiadomości oraz z prefixem):
170+
171+
```python
172+
encryptor = AES.new(bytes(0x10), AES.MODE_CBC, bytes(0x10))
173+
prefix_encrypted_block = encryptor.encrypt(len_prefix + msg_start)[-16:]
174+
```
175+
176+
Potrzebujemy tylko ostatni blok ponieważ tylko on jest wykorzystywany w szyfrowaniu naszego przygotowywanego bloku poprzez XORowanie z nim.
177+
Musimy pamiętać że mamy tutaj padding PKCS7 więc w jeśli dodamy pełny blok to ostatni blok szyfrogramu będzie zaszyfrowanym paddingiem!
178+
Więc w rzeczywistości chcemy żeby to padding zakodował się do oczekiwanych wylosowanych 16 bajtów.
179+
Wiemy ile wynoszą bajty paddingu - wszystkie będą `0x10`, ale są xorowane z ciphertextem naszego przygotowywanego boku.
180+
Oznaczmy szyfrogram tego bloku jako `CT_payload` a jego wersje odszyfrowaną jako `payload`.
181+
182+
Popatrzmy co chcemy osiągnąć:
183+
184+
Chcemy dostać: `encrypt(CT_payload xor padding) = target` więc deszyfrując obustronnie:
185+
186+
`CT_payload xor padding = decrypt(target)`
187+
188+
a ponieważ xor dwa razy przez tą samą wartość się znosi:
189+
190+
`CT_payload = decrypt(target) xor padding`
191+
192+
Popatrzmy teraz skąd bierze się `CT_payload`:
193+
194+
`CT_payload = encrypt(payload xor prefix_encrypted_block)`
195+
196+
i deszyfrując obustronnie:
197+
198+
`decrypt(CT_payload) = payload xor prefix_encrypted_block`
199+
200+
więc:
201+
202+
`payload = decrypt(CT_payload) xor prefix_encrypted_block`
203+
204+
I jeśli teraz połączymy te dwa równania mamy:
205+
206+
`payload = decrypt(decrypt(target) xor padding) xor prefix_encrypted_block`
207+
208+
I w ten sposób uzyskaliśmy przepis na wyliczenie bajtów payloadu do wysłania.
209+
Implementujemy to w pythonie:
210+
211+
```python
212+
def solve_for_target(target):
213+
# enc(ct xor padding) = target
214+
# ct xor padding = dec(target)
215+
# ct = dec(target) xor padding
216+
# ct = enc (pt xor enc_prefix)
217+
# dec(ct) = pt xor enc_prefix
218+
# pt = dec(ct) xor enc_prefix
219+
target = binascii.unhexlify(target)
220+
encryptor = AES.new(bytes(0x10), AES.MODE_CBC, bytes(0x10))
221+
data = encryptor.decrypt(target)[-16:] # ct xor padding
222+
last_block = b''
223+
expected_ct_bytes = b''
224+
for i in range(len(data)):
225+
expected_ct = (data[i] ^ 0x10) # ct
226+
expected_ct_byte = expected_ct.to_bytes(1, 'big')
227+
expected_ct_bytes += expected_ct_byte
228+
encryptor = AES.new(bytes(0x10), AES.MODE_CBC, bytes(0x10))
229+
result_bytes = encryptor.decrypt(expected_ct_bytes) # dec(ct)
230+
for i in range(len(result_bytes)):
231+
pt = result_bytes[i] ^ prefix_encrypted_block[i] # dec(ct) xor enc_prefix
232+
last_block += pt.to_bytes(1, 'big')
233+
return binascii.hexlify(msg_start + last_block)
234+
```
235+
236+
I po wysłaniu na serwer dostajemy: `hxp{PLz_us3_7h3_Ri9h7_PRiM1TiV3z}`
+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#!/usr/bin/env python3
2+
import os, binascii, struct
3+
from Crypto.Cipher import AES
4+
5+
pad = lambda m: m + bytes([16 - len(m) % 16] * (16 - len(m) % 16))
6+
def haggis(m):
7+
crypt0r = AES.new(bytes(0x10), AES.MODE_CBC, bytes(0x10))
8+
return crypt0r.encrypt(len(m).to_bytes(0x10, 'big') + pad(m))[-0x10:]
9+
10+
target = os.urandom(0x10)
11+
print(binascii.hexlify(target).decode())
12+
13+
msg = binascii.unhexlify(input())
14+
15+
if msg.startswith(b'I solemnly swear that I am up to no good.\0') \
16+
and haggis(msg) == target:
17+
print(open('flag.txt', 'r').read().strip())
18+

0 commit comments

Comments
 (0)