Phasestream 3 - Cyberapocalypse 2021 CTF

This is a writeup for the challenge Phasestream 3, part of the Hack the box's Cyberapocalypse CTF 2021, category Crypto.

Propmpt​

The aliens have learned the stupidity of their misunderstanding of Kerckhoffs's principle. Now they're going to use a well-known stream cipher (AES in CTR mode) with a strong key. And they'll happily give us poor humans the source because they're so confident it's secure!

Recon​

We are given the source code that the aliens used to encrypt the flag using "Advanced Encryption Standard" in "Counter" mode.

#@/usr/bin/env python3from Crypto.Cipher import AESfrom Crypto.Util import Counterimport osKEY = os.urandom(16)def encrypt(plaintext):    cipher = AES.new(KEY, AES.MODE_CTR, counter=Counter.new(128))    ciphertext = cipher.encrypt(plaintext)    return ciphertext.hex()test = b"No right of private conversation was enumerated in the Constitution. I don't suppose it occurred to anyone at the time that it could be prevented."print(encrypt(test))with open('flag.txt', 'rb') as f:    flag = f.read().strip()print(encrypt(flag))

By looking at the code, we don't see any immediate weaknesses like generating random numbers with a known seed for example.

We also have the output of running this code.

cat output.txt 464851522838603926f4422a4ca6d81b02f351b454e6f968a324fcc77da30cf979eec57c8675de3bb92f6c21730607066226780a8d4539fcf67f9f5589d150a6c7867140b5a63de2971dc209f480c270882194f288167ed910b64cf627ea6392456fa1b648afd0b239b59652baedc595d4f87634cf7ec4262f8c9581d7f56dc6f836cfe696518ce434ef4616431d4d1b361c4b6f25623a2d3b3833a8405557e7e83257d360a054c2ea

And here we can notice something. The output consists of 2 separate lines. Could it be that we also have the output of encrypting the test string? Let's count the characters in the first line vs the test string.

echo 464851522838603926f4422a4ca6d81b02f351b454e6f968a324fcc77da30cf979eec57c8675de3bb92f6c21730607066226780a8d4539fcf67f9f5589d150a6c7867140b5a63de2971dc209f480c270882194f288167ed910b64cf627ea6392456fa1b648afd0b239b59652baedc595d4f87634cf7ec4262f8c9581d7f56dc6f836cfe696518ce434ef4616431d4d1b361c| wc -c293
echo "No right of private conversation was enumerated in the Constitution. I don't suppose it occurred to anyone at the time that it could be prevented."| xxd -ps | tr -d \n | wc -c294

Well, we have error of off by one, which is pretty common when counting with wc. But we can safely assume that this is the same string.

Which means that we have 2 encrypted strings using the same key and counter!

Analysis​

Streaming cyphers are generally safe only if the key stream is only used once. Right now we have two cipher texts generated by the same keystream. Let's see what comes out of that.

$C_1 = P_1 \oplus K$
$C_2 = P_2 \oplus K$

We generate two ciphertexts $C_1 \text{ and } C_2$ byt XORing the plaintexts $P_1,P_2$ with the same keystream $K$. If we now XOR the two ciphertexts together, we get

$C_1 \oplus C_2 = P_1 \oplus K \oplus P_2 \oplus K \text{.}$

But, because an XOR is its own inverse, we can use that $K \oplus K = 0$, so

$C_1 \oplus C_2 = P_1 \oplus P_2 \text{,}$

meaning that we simply have the two plaintexts XORed with each-other. This is completely reversible! If we know one of the plaintexts, we can XOR the value we have from above with that plaintext, and get the other!

$C_1 \oplus C_2 \oplus P_1 = P_1 \oplus P_2 \oplus P_1 = P_2$

Solution​

Well, we have the two ciphertexts, and we have one of the plaintexts. So our solution is to

1. XOR the two output values with each-other.
2. XOR the plaintext No right of private conversation was with the XOR result from 1.
3. Get the flag
#!/usr/bin/env python3ciphertext1 = '464851522838603926f4422a4ca6d81b02f351b454e6f968a324fcc77da30cf979eec57c8675de3bb92f6c21730607066226780a8d4539fcf67f9f5589d150a6c7867140b5a63de2971dc209f480c270882194f288167ed910b64cf627ea6392456fa1b648afd0b239b59652baedc595d4f87634cf7ec4262f8c9581d7f56dc6f836cfe696518ce434ef4616431d4d1b361c'ciphertext2 = '4b6f25623a2d3b3833a8405557e7e83257d360a054c2ea'plaintext = b'No right of private conversation was enumerated in the Constitution. I don\'t suppose it occurred to anyone at the time that it could be prevented.'.hex()def xor(hex1, hex2, getAscii = False):  result = []  for ind in range(0, len(hex1), 2):    longIndex = ind    shortIndex = ind%len(hex2)    hexChar1 = hex1[longIndex:longIndex+2]    byte1 = int(hexChar1, 16)    hexChar2 = hex2[shortIndex:shortIndex+2]    byte2 = int(hexChar2, 16)    asciiNum = byte1 ^ byte2    result.append(chr(asciiNum))  out = ''.join(result)  if getAscii:    print('Result:', out)    return out  else:    out = out.encode('utf-8').hex()    print('Result:', out)    return out    xored = xor(ciphertext2, ciphertext1)flag = xor(xored, plaintext, True)

And so, we get our flag

Result: 0d27743012155b01155c027f1b41302955203114002413Result: CHTB{r3u53d_k3Y_4TT4cK}
Tags: