|
| 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}` |
0 commit comments