The ciphertexts in the challenge are encrypted with AES-CTR
with the same, new zero counter. Hence, the keystream used to xor
the plaintexts to obtain the ciphertexts will be the same for all ciphertexts. This now becomes the classic, and taught by many cryptographic courses in universities worldwide about the many time pad.
The one-time pad cannot be used multiple times (hence the name) because an attacker can xor
the two ciphertexts to obtain the xor
of the corresponding plaintext. It is thanks to the non-unform distribution of the plaintext space, and the redundancy of some languages (in this case English) that will help us crack the original plaintext.
There are some Python implementations to ease the pain of guessing words and looking at the output in a text editor, one that I used for my previous university assignment is MTP.
Otherwise, we can always do this with the traditional way of xor
every pair of ciphertexts, then xor
a portion of some guesses that we had (using some crib, in technical terms), then see on all other decrypted plaintexts whether it makes sense or not.
The following script demonstrates the manual solving process of this challenge, which heavily involves guesswork.
Python Implementation:
xxxxxxxxxx
from pwn import *
import json
import requests
import itertools
# Enumeration (replacing the condition in the while loop with True) yields 22 ciphertexts
ciphertexts = []
# while len(ciphertexts) < 22:
# r = requests.get('https://aes.cryptohack.org/stream_consciousness/encrypt/')
# c = bytes.fromhex(json.loads(r.text)['ciphertext'])
# if c not in ciphertexts:
# ciphertexts.append(c)
# print(len(ciphertexts))
# Save the trouble of getting all the ciphertexts again, open a file for easier viewing
# on a text editor, much better than terminal scrolling
f = open('result.txt', 'w')
ciphertexts = [b"\xb2+\xe8\xde\xe1\xdf.\x05f\x96_\xf2\xeb\x9e\xf0\xcb\xc9\x98)\x16\xd6Q\xb6\xf2\xa4\x1b\rWF\xedI\xd1\x94s\xe7\xbf*>\xc2k\xfb\xe6a\x8aT\x14\x9b&\xb28\xa8(\xb0\xbe\xe4\x96\x14\xa3on#\xe7b\xd1\xd1\xcd:\x1c#-\xc1\x08G}\xa6d1/3\xc5\xab[\xc4\xd1\xa8\xb4\xf4J'qd\xc8_\xc88c:\x17\xb7\xd0!DO8]\x8b\xbfDKh\xba\xad1=/\x0b\xb7\xca\x89", b'\xb6&\xff\xc5\xe5\x8f5J|\x80\x1a\xe9\xa6\xcd\xa4\xce\xc9\x98z\x10\xd3\x03\xb0\xf3\xa0\\\x1c\x05\n\xa4O\x9e\x82=\xca\xbf/"\x83}\xf2\xe0*\xc7O\x1e\xde$\xbbo\xeff\xc7\xab\xe2\x8bG\xeec}4\xb5c\xc5\xdb\xc1vXb-\xc0\x02P{', b'\xafc\xfe\xc5\xe5\x93*F4\xac\x1d\xed\xab\x9e\xe8\xcc\xd3\x8e)\x10\xc1F\xb6\xe2\xb1\x14\x01\x19\x0c\xedH\xd8\xc3;\xcb\xbf">\xc6l\xfd\xa45\xc7N\x08\x93/\xf4z\xa0%\xfb\xe4', b'\xa46\xf9\x8d\xcd\xdf1\x03x\x89\x1a\xf2\xaf\xd1\xf3\x83\xc8\x82d[', b'\xaa,\xfb\xc8\xa8\xdf6\x18{\x87[\xe3\xab\xc7\xbb\x83\xf4\x83l\x0c\x97G\xab\xf5\xe2\x08H\x1c\x05\xa2V\x9e\x8b<\xd9\xbf"#\xc6~\xe1\xfaa\x8eYG\x979\xf88\xa9)\xe7\xea\xe4\x8a\n\xea`f0\xe1b\xde\xd1\x864\x1f#-\xc1\x08\x1e\x1b\xa2d\x7f;:\x89\xea]\xc9\x82\xf0\xfc\xffS7#H\x86\x02', b'\xb1,\xf8\xc1\xe0\xdf\x0fJ|\x84L\xe4\xe7\xdc\xe1\xcf\xc9\x8e\x7f\x10\xd3\x03\xb0\xf3\xa0\x12H\x03\x03\xacU\x9e\xaas\xcd\xf03=\xc7?\xe1\xe6 \x84EG\x8d?\xb7p\xe1"\xf5\xba\xf8\x97\x14\xa3ciq\xfd~\xdd\xdf\xc4sPw0\xc6\x03\x01', b'\xafd\xe0\x8d\xf1\x91.\x0bd\x95C\xad\xe7\xf7\xa4\xc7\xc5\x98l\x07\xc1F\xe4\xf2\xb1PH\x03\x03\xa8\x01\xd8\x82&\xc2\xeba"\x83r\xfa\xed$\xcb\r\x05\x8b>\xf4Q\xe6+\xb0\xbf\xe2\x97\x06\xf3|vq\xf4g\xdc\x96\xdcrT#*\xc8\x00[v\xf4it"/\x89\xe6V\x83', b'\xb2+\xff\xc8\xe1\xdf$\x05m\x96\x1a\xf3\xb2\xd0\xea\xca\xce\x8c%U\xc7O\xa5\xe2\xac\x12\x0fW\n\xb9\x01\xd6\x8c!\xdd\xfa5}\x83L\xf6\xf18\x88W\x0f\x9fk', b'\xb1+\xec\xd9\xa4\x9ef\x04u\x96N\xf8\xe7\xcd\xe9\xc6\xcc\x87)\x01\xdfJ\xb7\xbb\xb5\x1d\x01\x19\x1f\xedI\xdf\x87}', b'\xb2+\xe8\x8d\xf0\x9a4\x18}\x87V\xe4\xe7\xca\xec\xca\xce\x8c)\x1c\xc4\x03\xb0\xf3\xa4\x08H\x03\x03\xa8\x01\xce\x82 \xda\xbf%0\xcd8\xe7\xa3#\x82\r\x13\x918\xba8\xae3\xe4\xea\xee\x86G\xeax|q\xe7d\xdf\xc2\xdb4', b'\xae,\xfa\x8d\xf4\x8d)\x1fp\xc5[\xef\xa3\x9e\xec\xc2\xd0\x9bpU\xdfF\xe3\xf7\xa9\\\n\x12K\xbaI\xdb\x8ds\xc6\xfaf6\xc6k\xe0\xa3,\x9e\r\t\x91>\xb19', b'\xa70\xad\xc4\xe2\xdf\x0fJ|\x84^\xa1\xa6\xd0\xfd\x83\xd7\x82z\x1d\x97W\xab\xbb\xa7\x19H\x1e\x05\xedU\xd6\x86s\xdc\xf6!9\xd7>\xb3\xcaa\x84L\t\xd9>\xf5', b'\xa7-\xe9\x8d\xcd\xdf5\x02u\x89V\xa1\xae\xd9\xea\xcc\xd2\x8e)\x1c\xc3\r', b'\xa21\xe8\xde\xf7\xd2+\x0b\x7f\x8cT\xe6\xe7\xdf\xea\xc7\x80\xa6`\x19\xdbJ\xaa\xfe\xb7\x05', b'\xafc\xfe\xc5\xe5\x93*Jx\x8aI\xe4\xe7\xdb\xf2\xc6\xd2\x92}\x1d\xdeM\xa3\xbb\xa4\x12\x0cW\x05\xa2U\x9e\x846\xda\xbf.8\xce?\xf1\xe2"\x8c\x03', b'\xa96\xff\x92\xa4\xa8.\x134\x8aO\xf3\xf8', b"\x851\xf4\xdd\xf0\x90=\x01'\x9c\x0f\xb6\xb5\x8d\xb0\xce\xff\x99:\x00\x82\x10\x9b\xaa\xf0#\x0eC\\\xf9M\xc3", b"\xa2,\xe1\xc1\xfd\xdf1\x03x\x89\x1a\xf5\xaf\xd7\xea\xc8\x80\x9fa\x14\xc3\x03\x8d\xbc\xa8\\\x04\x12\n\xbbH\xd0\x84s\xcf\xbf54\xc0p\xfd\xe7a\x8fX\x14\x9c+\xba|\xe1'\xfe\xae\xac\x8b\x0f\xe2x/%\xfdn\xc2\xd3\xceuCfy\xe0MS/\xa7u1=*\xdb\xee_\xd4\x82\xf1\xf1\xb6V=qY\x80I\x80.}9Y\xbe\x9b", b'\xb1+\xec\xd9\xa4\x9ef\x06{\x91\x1a\xee\xa1\x9e\xf0\xcb\xc9\x85n\x06\x97W\xac\xfa\xb1\\\x1c\x1f\x0e\xa3\x01\xcd\x866\xc3\xfa"q\xd7p\xb3\xee$\xc7^\x08\xde\'\xb5j\xb7#\xfc\xa6\xe3\x8a\x14\xa3ma5\xb5~\xde\xd7\xdcnPj7\xc8\x0fR?\xf8!y/)\xcc\xabQ\xc8\xc1\xfc\xf9\xf3\x1f:?^\x81K\xce0i?T\xb8\xdb#\r\x1dy@\x8a\xfa\x10We\xff\xb4y5&\r\xad\x84\xee\x9e\x1e\xc5\xb3\\\xe7\xfc\x1f\x0bV\xfdx\x17\xe6\xcd\'#\x0eN\x0fL\xdf\xe8%k;\x85d\xdbv\xdf\xe4\xab\x11X\xf8k\xf1\x8c9', b'\xb1+\xf4\x8d\xe0\x90f\x1e|\x80C\xa1\xa0\xd1\xa4\xcc\xce\xcby\x14\xdeM\xb0\xf2\xab\x1bH\x16\x05\xa9\x01\xdc\x96:\xc2\xfb/?\xc4?\xf2\xef-\xc7Y\x0f\x9bj\xa0q\xac#\xaf', b'\xaf7\xad\xce\xe5\x91a\x1e4\x87_\xa1\xb3\xd1\xf6\xcd\x80\x84|\x01\x9b\x03\xa6\xee\xb1\\\x01\x03K\xae@\xd0\xc31\xcb\xbf/6\xcdp\xe1\xe6%\xc9', b'\xa8,\xa1\x8d\xcd\xd8*\x064\x82U\xa1\xae\xd0\xa4\xd7\xcf\xcbM\x1a\xdbO\xbd\xbb\xa4\x12\x0cW\x1f\xa8M\xd2\xc3;\xcb\xedf"\xd7m\xf2\xea&\x8fYG\x91?\xa0']
ct_pairs = list(itertools.combinations(ciphertexts, 2))
guess = b"crypto{" # What we already know of the flag
# Some guesses in between
guess = b"Love, probably"
guess = b"Would I have believed" # biggest jump in guesses so far, from be to believed
guess = b"I shall lose everything"
guess = b"These horses, this carriage"
guess = b'I shall, I\'ll lose everything'
guess = b'What a nasty smell this painting' # wrong in hindsight but lead me to the next line
guess = b"Dolly will think that I'm leaving" # looking at the ciphertexts, you should see the flag
for pair in ct_pairs:
temp = xor(pair[0], pair[1])
temp = xor(guess, temp)
f.write(str(temp) + "\n")
f.write("\n")
f.close()