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