Skip to main content

Phasestream 3 - Cyberapocalypse 2021 CTF

· 4 min read

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 python3

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()


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 
464851522838603926f4422a4ca6d81b02f351b454e6f968a324fcc77da30cf979eec57c8675de3bb92f6c21730607066226780a8d4539fcf67f9f5589d150a6c7867140b5a63de2971dc209f480c270882194f288167ed910b64cf627ea6392456fa1b648afd0b239b59652baedc595d4f87634cf7ec4262f8c9581d7f56dc6f836cfe696518ce434ef4616431d4d1b361c
4b6f25623a2d3b3833a8405557e7e83257d360a054c2ea

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 -c
293
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 -c
294

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.

C1=P1KC_1 = P_1 \oplus K
C2=P2KC_2 = P_2 \oplus K

We generate two ciphertexts C1 and C2C_1 \text{ and } C_2 byt XORing the plaintexts P1,P2P_1,P_2 with the same keystream KK. If we now XOR the two ciphertexts together, we get

C1C2=P1KP2K.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 KK=0K \oplus K = 0, so

C1C2=P1P2,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!

C1C2P1=P1P2P1=P2C_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 python3

ciphertext1 = '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: 0d27743012155b01155c027f1b41302955203114002413
Result: CHTB{r3u53d_k3Y_4TT4cK}