N1TP - H@acktivityCon 2021

Read this in "about 6 minutes".

Description

Author: @JohnHammond#6971

Nina found some new encryption scheme that she apparently thinks is really cool. She’s annoying but she found a flag or something, can you deal with her?

Press the Start button on the top-right to begin this challenge. Connect with:

nc challenge.ctf.games 31921

Solution

Let’s us connect to the challenge.

$ nc challenge.ctf.games 31921
NINA: Hello! I found a flag, look!
      735097cf4ed3df93068d2add1690730965903232b06b7e97460999586cdf4def6d036ba9e2ea
NINA: But I encrypted it with a very special nonce, the same length as 
      the flag! I heard people say this encryption method is unbreakable!
      I'll even let you encrypt something to prove it!! What should we encrypt?
> Hello NINAAA
NINA: Ta-daaa!! I think this is called a 'one' 'time' 'pad' or something?
      5d599ac45acaa3ed7eaf0ffe69c37e566ed44a19cf4c0ae43d5ec50d609b3ac513231bd9c8f2
NINA: Isn't that cool!?! Want to see it again? 
      Sorry, I forget already -- what was it you wanted to see again?
> THIS IS ME ...
NINA: Ta-daaa!! I think this is called a 'one' 'time' 'pad' or something?
      4174bffb15a3be847dab6e910f88467248a72419d22d06e05515874f5bf33ddf7d2b09b8cdd2
NINA: Isn't that cool!?! Want to see it again? 
      Sorry, I forget already -- what was it you wanted to see again?
> HOW ARE YOU DOING
NINA: Ta-daaa!! I think this is called a 'one' 'time' 'pad' or something?
      5d73a18874b8a88469a11b9f65e95b7446bc4b07a14c19e05562e6342fff3bc5132512d7d7b7
NINA: Isn't that cool!?! Want to see it again? 
      Sorry, I forget already -- what was it you wanted to see again?

In the very first line, Nina sent us a encrypted flag using one time pad. Now everytime we sent a message, Nina takes our message, encrypts it and sends it back to us.

Our theory is Nina reuses a same key for message encryption. This is also called Many Time Pad Attack.

To attack the encryption, we follow the below steps:

  1. Guess a word that might appear in one of the messages
  2. Encode the word from step 1 to a hex string
  3. XOR the two cipher-text messages
  4. XOR the hex string from step 2 at each position of the XOR of the two cipher-texts (from step 3)
  5. When the result from step 4 is readable text, we guess the English word and expand our crib search. + If the result is not readable text, we try an XOR of the crib word at the next position.

The following python script can automatically do the job. (I couldn’t remember where the source is).

#!/usr/bin/env python 
import string
import collections
import sets

# XORs two string
def strxor(a, b):     # xor two strings (trims the longer input)
    return "".join([chr(ord(x) ^ ord(y)) for (x, y) in zip(a, b)])

# 10 unknown ciphertexts (in hex format), all encrpyted with the same key
c1 = "1a3a20741ca737cb76cb0e33909bb16bc0dd5cd150c14babcf505f6473b6615f8bd0963a04c4"
c2 = "23343e651ca7308a718e0260d981fe7e87df5ad06b8044abcf455c2820bd6d118384c63710852a3238651cb5348a62"
c3 = "3a332b7259f3319825c312339f83f06d87d515de6a8052be8650592a67f362109084df2f43cb24352f204cbf2bcb62c71d76d982f42aded340cd27c649be880459646eb6611bc2cdc27b17ca6d283b6251ba2ccb63c11933918ef261d3d543d664cf4b"
c4 = "34343b204fbb379e69ca4b7d969bb178c2c946da27d44dbacf4f553d20b56b0dc2cbd83e43d124362b204cb23ccb6bc70572"
c5 = "272e3d741cb4319d608e0676d99bf96f87da59de60ca50ac9b04572d76b624128784c2330685"
c6 = "247b396152a7788a25c807729e86b17dc6d2419f668043b38e43596477b26a0bc2c5963d0fc4"
c7 = "25352b7755b22d8d6bcc0f60958efb61d0d95ed561ce41ac84485e2573a97c1c9adede3506d2"
c8 = "247b2f6d1ca4398271c70574d989fe7887c85dda27c649be884d10256df3731e8bd0df350485"
c9 = "3a332f741ca4399825c71f338080e42ad0dd5bcb62c405ab8004432165f3651883cdd82c0bc4"
c10 = "047b286f4eb43d9f25cf07619c8ef573ee9c53d075c740abcf455c3665b26006ab84d03411c2"


ciphers = [c1, c2, c3, c4, c5, c6, c7, c8, c9, c10]
# The target ciphertext we want to crack
target_cipher = "2b372f6747ea6adc33cd0f71ced9f039c3d803dd36c610eddc16007d63b73d1cd2c5876a01d8"

# To store the final key
final_key = [None]*150
# To store the positions we know are broken
known_key_positions = set()

# For each ciphertext
for current_index, ciphertext in enumerate(ciphers):

	counter = collections.Counter()
	# for each other ciphertext
	for index, ciphertext2 in enumerate(ciphers):
		if current_index != index: # don't xor a ciphertext with itself
			for indexOfChar, char in enumerate(strxor(ciphertext.decode('hex'), ciphertext2.decode('hex'))): # Xor the two ciphertexts
				# If a character in the xored result is a alphanumeric character, it means there was probably a space character in one of the plaintexts (we don't know which one)
				if char in string.printable and char.isalpha(): counter[indexOfChar] += 1 # Increment the counter at this index
	knownSpaceIndexes = []

	# Loop through all positions where a space character was possible in the current_index cipher
	for ind, val in counter.items():
		# If a space was found at least 7 times at this index out of the 9 possible XORS, then the space character was likely from the current_index cipher!
		if val >= 7: knownSpaceIndexes.append(ind)
	#print knownSpaceIndexes # Shows all the positions where we now know the key!

	# Now Xor the current_index with spaces, and at the knownSpaceIndexes positions we get the key back!
	xor_with_spaces = strxor(ciphertext.decode('hex'),' '*150)
	for index in knownSpaceIndexes:
		# Store the key's value at the correct position
		final_key[index] = xor_with_spaces[index].encode('hex')
		# Record that we known the key at this position
		known_key_positions.add(index)

# Construct a hex key from the currently known key, adding in '00' hex chars where we do not know (to make a complete hex string)
final_key_hex = ''.join([val if val is not None else '00' for val in final_key])
# Xor the currently known key with the target cipher
output = strxor(target_cipher.decode('hex'),final_key_hex.decode('hex'))
# Print the output, printing a * if that character is not known yet
print ''.join([char if index in known_key_positions else '*' for index, char in enumerate(output)])

'''
Manual step
'''
# From the output this prints, we can manually complete the target plaintext from:
# The secuet-mes*age*is: Wh** usi|g **str*am cipher, nev***use th* k*y *ore than onc*
# to:
# The secret message is: When using a stream cipher, never use the key more than once

# We then confirm this is correct by producing the key from this, and decrpyting all the other messages to ensure they make grammatical sense
target_plaintext = "b76a3dd6b1f523209cd9c0a11b}"
print target_plaintext
key = strxor(target_cipher.decode('hex'),target_plaintext)
for cipher in ciphers:
	print strxor(cipher.decode('hex'),key)

By running the script, we can partially obtain the flag.

$ python2 attack.py                                                                                                                                                                     1 ⨯
*l*g*927*cdb7*a3*d6b1f5**209cd9c0*1*b}
...

Keep rotating the cipher texts (c1,c2,c3,c4,c5 …). After a few times, we should be able to recover all parts of the flag.