Tag List
Sign In

PhaseStream 4

We are given the following code, a slight advance on the previous PhaseStream challenges as we are only given two ciphertexts, and no plaintext.

from Crypto.Cipher import AES
from Crypto.Util import Counter
import os

KEY = os.urandom(16)

def encrypt(plaintext):
    cipher = AES.new(KEY, AES.MODE_CTR, counter=Counter.new(128))
    ciphertext = cipher.encrypt(plaintext)
    return ciphertext.hex()

with open('test_quote.txt', 'rb') as f:
    test_quote = f.read().strip()

with open('flag.txt', 'rb') as f:
    flag = f.read().strip()

The code encrypts two messages with the same key using AES in Counter mode. As we established in the previous PhaseStream challenges counter mode is bad! Because these two plaintexts are encrypted with the same key in counter mode, it essentially becomes an XOR...apparently.

Borrowing from someone who actually knows what they're talking about:

Because the CTR nonce wasn't randomized for each encryption, each ciphertext has been encrypted against the same keystream. This is very bad.

Understanding that, like most stream ciphers (including RC4, and obviously any block cipher run in CTR mode), the actual "encryption" of a byte of data boils down to a single XOR operation, it should be plain that:


And since the keystream is the same for every ciphertext:


Attack this cryptosystem piecemeal: guess letters, use expected English language frequence to validate guesses, catch common English trigrams, and so on.

So, we have two unknown plaintexts, p1 and p2 that correspond to test_quote.txt and flag.txt, and two ciphertexts c1 and c2 which correspond to the encrypted versions of p1 and p2, respectively. They share the keystream, k. Given this, we can work out the key and thus plaintext with the following operation: p1 xor k == c1 and p2 xor k == c2

Therefore, c1 xor p1 == k, and once we know k we can easily get p2 by doing c2 xor k.

ThisXOR(%7B'option':'UTF8','string':'%3CP1%20HERE%3E'%7D,'Standard',false)XOR(%7B'option':'Hex','string':'%3CC2%20HERE%3E'%7D,'Standard',false)&input=PFBVVCBDMSBIRVJFPg) CyberChef recipe demonstrates that, which we'll use later.

But how to we do this? We just, just, need to guess the plaintext, which will allow us to recover the key and hence the plaintext of both messages.

We will start from thisXOR(%7B'option':'UTF8','string':''%7D,'Standard',false)XOR(%7B'option':'Hex','string':'2d0fb3a56aa66e1e44cffc97f3a2e030feab144124e73c76d5d22f6ce01c46e73a50b0edc1a2bd243f9578b745438b00720870e3118194cbb438149e3cc9c0844d640ecdb1e71754c24bf43bf3fd0f9719f74c7179b6816e687fa576abad1955'%7D,'Standard',false)&input=Mjc2Nzg2OGI3ZWJiN2Y0YzQyY2ZmZmE2ZmZiZmIwM2JmM2I4MDk3OTM2YWUzYzc2ZWY4MDNkNzZlMTE1NDY5NDcxNTdiY2VhOTU5OWY4MjYzMzg4MDdiNTU2NTVhMDU2NjY0NDZkZjIwYzhlOTM4N2IwMDQxMjllMTBkMThlOWY1MjZmNzFjYWJjZjIxYjQ4OTY1YWUzNmZjZmVlMWU4MjBjZjEwNzZmNjU) recipe. ThisXOR(%7B'option':'UTF8','string':''%7D,'Standard',false)XOR(%7B'option':'Hex','string':'2767868b7ebb7f4c42cfffa6ffbfb03bf3b8097936ae3c76ef803d76e11546947157bcea9599f826338807b55655a05666446df20c8e9387b004129e10d18e9f526f71cabcf21b48965ae36fcfee1e820cf1076f65'%7D,'Standard',false)&input=MmQwZmIzYTU2YWE2NmUxZTQ0Y2ZmYzk3ZjNhMmUwMzBmZWFiMTQ0MTI0ZTczYzc2ZDVkMjJmNmNlMDFjNDZlNzNhNTBiMGVkYzFhMmJkMjQzZjk1NzhiNzQ1NDM4YjAwNzIwODcwZTMxMTgxOTRjYmI0MzgxNDllM2NjOWMwODQ0ZDY0MGVjZGIxZTcxNzU0YzI0YmY0M2JmM2ZkMGY5NzE5Zjc0YzcxNzliNjgxNmU2ODdmYTU3NmFiYWQxOTU1) recipe is the same, but swaps the plaintext we're guessing.

A decent starting point for guessing the plaintext is that the flag starts with CHTB{, so we enter this into the recipe with the encrypted flag (c2) as the input.

So, when we enter the key as CHTB{, the output begins with I alo (only the first 5 characters are relevant because we've only entered 5 characters of key!). We're looking for English words, and I alo looks like English! The rest of this is going to continue making educated guesses about either plaintext. This is shown in thisXOR(%7B'option':'UTF8','string':'CHTB%7B'%7D,'Standard',false)XOR(%7B'option':'Hex','string':'2d0fb3a56aa66e1e44cffc97f3a2e030feab144124e73c76d5d22f6ce01c46e73a50b0edc1a2bd243f9578b745438b00720870e3118194cbb438149e3cc9c0844d640ecdb1e71754c24bf43bf3fd0f9719f74c7179b6816e687fa576abad1955'%7D,'Standard',false)&input=Mjc2Nzg2OGI3ZWJiN2Y0YzQyY2ZmZmE2ZmZiZmIwM2JmM2I4MDk3OTM2YWUzYzc2ZWY4MDNkNzZlMTE1NDY5NDcxNTdiY2VhOTU5OWY4MjYzMzg4MDdiNTU2NTVhMDU2NjY0NDZkZjIwYzhlOTM4N2IwMDQxMjllMTBkMThlOWY1MjZmNzFjYWJjZjIxYjQ4OTY1YWUzNmZjZmVlMWU4MjBjZjEwNzZmNjU) recipe.

(Also, given the filename of test_quote.txt for the non-flag plaintext, it should definitely be English)

Now, given the output of us guessing the plaintext of the flag is the plaintext of the quote p1, we can now guess the rest of the second word of this plaintext by copying the I alo to the start of the key for this recipe. We can guess that the second word might be alone, so let's put that in and see what happens! This might get confusing because we keep swapping between the plaintext we are guessing, but it's easy. This is shown hereXOR(%7B'option':'UTF8','string':'I%20alone%20'%7D,'Standard',false)XOR(%7B'option':'Hex','string':'2767868b7ebb7f4c42cfffa6ffbfb03bf3b8097936ae3c76ef803d76e11546947157bcea9599f826338807b55655a05666446df20c8e9387b004129e10d18e9f526f71cabcf21b48965ae36fcfee1e820cf1076f65'%7D,'Standard',false)&input=MmQwZmIzYTU2YWE2NmUxZTQ0Y2ZmYzk3ZjNhMmUwMzBmZWFiMTQ0MTI0ZTczYzc2ZDVkMjJmNmNlMDFjNDZlNzNhNTBiMGVkYzFhMmJkMjQzZjk1NzhiNzQ1NDM4YjAwNzIwODcwZTMxMTgxOTRjYmI0MzgxNDllM2NjOWMwODQ0ZDY0MGVjZGIxZTcxNzU0YzI0YmY0M2JmM2ZkMGY5NzE5Zjc0YzcxNzliNjgxNmU2ODdmYTU3NmFiYWQxOTU1), with a key (actually the plaintext, but ignore that) of I alone , we get an output of CHTB{str - which looks good. We make a sensible guess until we make a word, or part of a word in the plaintext output, then copy the key that got that into the other recipe and keep guessing.

This word might be stream, given that that is the kind of cipher we're performing cryptoanalysis on! So we go back to the other recipe, use a key of CHTB{stream_, and get an output for the quote of I alone cann. This still looks good, could it be the word 'cannot'? Spoiler. It is! I'm not going to carry on showing the various stages with recipes, but the stages I went through of both plaintexts are below.

QUOTE: I alone cann FLAG: CHTB{stream_

QUOTE: I alone cannot FLAG: CHTB{stream_cip

QUOTE: I alone cannot chang FLAG: CHTB{stream_ciphers_

QUOTE: I alone cannot change FLAG: CHTB{stream_ciphers_wi

Okay, I actually just googled I alone cannot change at this point and got the following full quote I alone cannot change the world, but I can cast a stone across the water to create many ripples. Sticking this in gives CHTB{stream_ciphers_with_reused_keystreams_are_vulnerable_to_known_plaintext_attacks} - which is the flag!

In summary, AES in Counter mode is bascially a stream cipher - for cryptograhy reasons this is bad. This is super bad if we've encrypted multiple messages with the same key! So, don't do that.

I did the math!