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.
We generate two ciphertexts byt XORing the plaintexts with the same keystream . If we now XOR the two ciphertexts together, we get
But, because an XOR is its own inverse, we can use that , so
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!
Solution
Well, we have the two ciphertexts, and we have one of the plaintexts. So our solution is to
- XOR the two output values with each-other.
- XOR the plaintext
No right of private conversation was
with the XOR result from1
. - 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}
Thank you for reading!
The information in this blog, as well as all the tools, apps and libraries I develop are currently open source.
I would love to keep it this way, and you can help!
You can buy me a coffee from here, which will go towards the next all-nighter I pull off!
Or you can support me and my code monthly over at Github Sponsors!
Thanks!