CSAW CTF 2020 Quals - Crypto Challenges - LostMyPlaintext

Writeups for some of the crypto challenges from this years CSAW CTF.


Perfect Secrecy:

For this challenge we're given two images:

Image1:

Image2:

The challenge description implies both images were encrypted with a one-time pad (hence "perfect secrecy") however it also says that the same key was used on both images. In other words:

  • image1 = originalImage1 XOR key
  • image2 = originalImage2 XOR key

Meaning that if we XOR the two images we get:

  • image1 XOR image2 = originalImage1 XOR key XOR originalImage2 XOR key
  • or
  • image1 XOR image2 = originalImage1 XOR originalImage2

We can use the PIL python module to do this but personally I just threw the images into this website: https://online-image-comparison.com

And got the following result:

The flag is the base64 encoded string on the image, all we gotta do is decode it.

FLAG: flag{0n3_t1m3_P@d!}


difib:

For this challenge we get the following files:

message:

snbwmuotwodwvcywfgmruotoozaiwghlabvuzmfobhtywftopmtawyhifqgtsiowetrksrzgrztkfctxnrswnhxshylyehtatssukfvsnztyzlopsv

ramblings:

Mr. Jock, TV quiz PhD., bags few lynx
Two driven jocks help fax my big quiz.
Jock nymphs waqf drug vex blitz
Fickle jinx bog dwarves spy math quiz.
Crwth vox zaps qi gym fjeld bunk
Public junk dwarves hug my quartz fox.
Quick fox jumps nightly above wizard.
Hm, fjord waltz, cinq busk, pyx veg
phav fyx bugs tonq milk JZD CREW
Woven silk pyjamas exchanged for blue quartz.
The quick onyx goblin jumps over the lazy dwarf.
Foxy diva Jennifer Lopez wasn’t baking my quiche.
he said 'bcfgjklmnopqrtuvwxyz'
Jen Q. Vahl: bidgum@krw.cfpost.xyz
Brawny gods just flocked up to quiz and vex him.
Emily Q Jung-Schwarzkopf XTV, B.D.
My girl wove six dozen plaid jackets before she quit.
John 'Fez' Camrws Putyx. IG: @kqBlvd
Q-Tip for SUV + NZ Xylem + DC Bag + HW?....JK!
Jumbling vext frowzy hacks pdq
Jim quickly realized that the beautiful gowns are expensive.
J.Q. Vandz struck my big fox whelp
How razorback-jumping frogs can level six piqued gymnasts!
Lumpy Dr. AbcGQVZ jinks fox thew
Fake bugs put in wax jonquils drive him crazy.
The jay, pig, fox, zebra, and my wolves quack!
hey i am nopqrstuvwxzbcdfgjkl
Quiz JV BMW lynx stock derp. Agh! F.
Pled big czar junks my VW Fox THQ
The big plump jowls of zany Dick Nixon quiver.
Waltz GB quick fjords vex nymph
qwertyuioplkjhgfdsazxcvbnm
Cozy lummox gives smart squid who asks for job pen.
zyxwvutsrqponmlkjihgfedcba
Few black taxis drive up major roads on quiet hazy nights.
a quick brown fx jmps ve th lzy dg
Bored? Craving a pub quiz fix? Why, just come to the Royal Oak!

From the title and description we conclude this must be related to the classic cipher bifid. Understanding how this cipher works shouldn't be particularly hard, however notice that the number of solves was quite low especially considering it's a 50 point challenge. This was because there is a bit of guessing involved from this point onward. Among the ramblings file we see that there are a few lines that are 26 characters long (if we remove spaces and punctuation) and have exactly one of each letter in them. The Bifid cipher uses keys that are 25 characters long and contain exactly one of each letter (normally with 'j' being omitted). So I first thought that one these lines must be the correct key (after removing spaces and such of course) but this is not the case. This is where I'd guess most people got stuck. With a hint that was posted later it became clear that all apparent keys (the lines with the 26 different letters) where used (not just one) in the order they appear in the ramblings. This means that to get the original message we need to decrypt the message we where given using the last key in the ramblings file, then decrypt the result using the second to last key and so on. Here's the first script I wrote to do so:

from pycipher import Bifid

message = "snbwmuotwodwvcywfgmruotoozaiwghlabvuzmfobhtywftopmtawyhifqgtsiowetrksrzgrztkfctxnrswnhxshylyehtatssukfvsnztyzlopsv"

ramblings = ['MrJockTVquizPhDbagsfewlynx', 'Twodrivenjockshelpfaxmybigquiz', 'Jocknymphswaqfdrugvexblitz', 'Ficklejinxbogdwarvesspymathquiz', 'Crwthvoxzapsqigymfjeldbunk', 'Publicjunkdwarveshugmyquartzfox', 'Quickfoxjumpsnightlyabovewizard', 'Hmfjordwaltzcinqbuskpyxveg', 'phavfyxbugstonqmilkJZDCREW', 'Wovensilkpyjamasexchangedforbluequartz', 'Thequickonyxgoblinjumpsoverthelazydwarf', 'FoxydivaJenniferLopezwasntbakingmyquiche', 'hesaidbcfgjklmnopqrtuvwxyz', 'JenQVahlbidgumkrwcfpostxyz', 'Brawnygodsjustflockeduptoquizandvexhim', 'EmilyQJungSchwarzkopfXTVBD', 'Mygirlwovesixdozenplaidjacketsbeforeshequit', 'JohnFezCamrwsPutyxIGkqBlvd', 'QTipforSUVNZXylemDCBagHWJK', 'Jumblingvextfrowzyhackspdq', 'Jimquicklyrealizedthatthebeautifulgownsareexpensive', 'JQVandzstruckmybigfoxwhelp', 'Howrazorbackjumpingfrogscanlevelsixpiquedgymnasts', 'LumpyDrAbcGQVZjinksfoxthew', 'Fakebugsputinwaxjonquilsdrivehimcrazy', 'Thejaypigfoxzebraandmywolvesquack', 'heyiamnopqrstuvwxzbcdfgjkl', 'QuizJVBMWlynxstockderpAghF', 'PledbigczarjunksmyVWFoxTHQ', 'ThebigplumpjowlsofzanyDickNixonquiver', 'WaltzGBquickfjordsvexnymph', 'qwertyuioplkjhgfdsazxcvbnm', 'Cozylummoxgivessmartsquidwhoasksforjobpen', 'zyxwvutsrqponmlkjihgfedcba', 'Fewblacktaxisdriveupmajorroadsonquiethazynights', 'aquickbrownfxjmpsvethlzydg', 'BoredCravingapubquizfixWhyjustcometotheRoyalOak']

keys = []
for r in ramblings[::-1]:
	if len(r) == 26:
		keys.append(r.lower().replace("j",""))

def main():
	ct = message
	for key in keys:
		ct = Bifid(key,5).decipher(ct)
	print (ct)

if __name__ == "__main__":
	main()

Output:

XUSTXSOMEXUNNECESSARYXTEXTXTHATXHOLDSXABSOLUTELYXNOXMEANINGXWHATSOEVERXANDXBEARSXNOXSIGNIFICANCEXTOXYOUXINXANYXWAY

I first replaces the first 'X' with a 'J' as that seems like what would be intended and sent the result to the server:

I then tried to replace the "X"s with spaces and send everything in lower case, and this got me the flag:

Final solver:

from pycipher import Bifid
from pwn import *

message = "snbwmuotwodwvcywfgmruotoozaiwghlabvuzmfobhtywftopmtawyhifqgtsiowetrksrzgrztkfctxnrswnhxshylyehtatssukfvsnztyzlopsv"

ramblings = ['MrJockTVquizPhDbagsfewlynx', 'Twodrivenjockshelpfaxmybigquiz', 'Jocknymphswaqfdrugvexblitz', 'Ficklejinxbogdwarvesspymathquiz', 'Crwthvoxzapsqigymfjeldbunk', 'Publicjunkdwarveshugmyquartzfox', 'Quickfoxjumpsnightlyabovewizard', 'Hmfjordwaltzcinqbuskpyxveg', 'phavfyxbugstonqmilkJZDCREW', 'Wovensilkpyjamasexchangedforbluequartz', 'Thequickonyxgoblinjumpsoverthelazydwarf', 'FoxydivaJenniferLopezwasntbakingmyquiche', 'hesaidbcfgjklmnopqrtuvwxyz', 'JenQVahlbidgumkrwcfpostxyz', 'Brawnygodsjustflockeduptoquizandvexhim', 'EmilyQJungSchwarzkopfXTVBD', 'Mygirlwovesixdozenplaidjacketsbeforeshequit', 'JohnFezCamrwsPutyxIGkqBlvd', 'QTipforSUVNZXylemDCBagHWJK', 'Jumblingvextfrowzyhackspdq', 'Jimquicklyrealizedthatthebeautifulgownsareexpensive', 'JQVandzstruckmybigfoxwhelp', 'Howrazorbackjumpingfrogscanlevelsixpiquedgymnasts', 'LumpyDrAbcGQVZjinksfoxthew', 'Fakebugsputinwaxjonquilsdrivehimcrazy', 'Thejaypigfoxzebraandmywolvesquack', 'heyiamnopqrstuvwxzbcdfgjkl', 'QuizJVBMWlynxstockderpAghF', 'PledbigczarjunksmyVWFoxTHQ', 'ThebigplumpjowlsofzanyDickNixonquiver', 'WaltzGBquickfjordsvexnymph', 'qwertyuioplkjhgfdsazxcvbnm', 'Cozylummoxgivessmartsquidwhoasksforjobpen', 'zyxwvutsrqponmlkjihgfedcba', 'Fewblacktaxisdriveupmajorroadsonquiethazynights', 'aquickbrownfxjmpsvethlzydg', 'BoredCravingapubquizfixWhyjustcometotheRoyalOak']

keys = []
for r in ramblings[::-1]:
	if len(r) == 26:
		keys.append(r.lower().replace("j",""))

def main():
	ct = message
	for key in keys:
		ct = Bifid(key,5).decipher(ct)
	#print (ct)
	ct = ct.lower().replace("x"," ")
	ct = "j"+ct[1:24]+'x'+ct[25:]

	r = remote("crypto.chal.csaw.io", 5004)
	r.sendline(ct)
	r.interactive()

if __name__ == "__main__":
	main()

FLAG: flag{t0ld_y4_1t_w4s_3z}


modus_operandi:

For this challenge we only get a server connection that asks for input, encrypts it with either AES ECB Mode or AES CBC Mode, gives back the corresponding ciphertext and asks us to guess which mode was used. If we get correctly it repeats this process, if not we lose the connection. First let's take a look at how AES ECB encryption works and at how AES CBC encryption works:

AES ECB Mode - Encryption:

AES CBC Mode - Encryption:

Notice how in ECB our plaintext is simply divided in a number of blocks of the same length and encrypted block by block. Given a plaintext block, the AES algorithm by itself, will always produce the same ciphertext as long as the same key is used and in AES ECB mode the same key is used for each block. Consider the following example:

  • Let's say we want to encrypt this plaintext: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
  • (that's 32 'A's)
  • And we use AES ECB mode with a block size of 16 bytes
  • We'd first encrypt a block of 16 'A's and then a second block of 16 'A's
  • Since we're using the same key for both blocks and we're encrypting the exact same plaintext in each block (16 'A's) we'll end up with two encrypted blocks that look exactly the same.
  • So our 32 byte ciphretext will be the concatenation of two indentical 16 byte sequences

This means if we find repeating patterns (with lenght equal to the block size used) in our ciphertext it is very likely that the AES mode used was ECB. Note that because an initialization vector and XOR operations are used in CBC, this mode is very very unlikely to produce such patterns.

Something important to note about this challenge is that the first answer the server is expecting is always 'ECB' (it would close the connection if we answered 'CBC'). Knowing the first mode used by the server will always be ECB we can send large strings of 'A's and look for repeating patterns in order to figure out the block size. I first tried sending 32 'A's and didn't get any pattern in the response (considering the standard size for and AES block is 16 bytes this was a little strange). But it turns out that when sending 64 'A's we in fact got a repating pattern of 32 bytes meaning the server is using 32 byte blocks. Knowing all of this we can write a script that repeatedly sends a plaintext of 64 'A's, looks for repeating blocks in the corresponding ciphertexts and answers back with 'ECB' if it finds repeating blocks and 'CBC' if it doesn't:

import binascii

from pwn import *

BLOCK_SIZE = 32

pt = "A"*64

r = remote("crypto.chal.csaw.io", 5001)

def AES_ECB_Score(ct):
	ct = binascii.unhexlify(ct)
	blocks = [ct[i:i + BLOCK_SIZE] for i in range(0, len(ct), BLOCK_SIZE)]
	score = len(blocks) - len(set(blocks))
	return score


while True:
	r.recvuntil("Enter plaintext:")

	r.sendline(pt)

	r.recvuntil("Ciphertext is: ")

	ct = r.recvline()[1:-1]

	print (ct)

	if AES_ECB_Score(ct) > 0:
		print ("sending 'ECB'")
		r.sendline('ECB')

	else:
		print ("sending 'CBC'")
		r.sendline('CBC')
	print (str(i+1) + " correct answers")

r.interactive()

Now this script would always crash after sending arround 176 plaintexts and the server didn't seem to be printing the flag after a given number of correct answers.

A fellow team member thought that since the server always used ECB on the first plaintext, CBC on the second and so on that maybe the modes used where a binary encoding of the flag, as in every 'ECB' answer represents a 0 and every 'CBC' answer represents a 1 (or the other way arround). Turns out this is exactly what was happening.

New solver:

import binascii

from pwn import *

BLOCK_SIZE = 32

pt = "A"*64

r = remote("crypto.chal.csaw.io", 5001)

def AES_ECB_Score(ct):
	ct = binascii.unhexlify(ct)
	blocks = [ct[i:i + BLOCK_SIZE] for i in range(0, len(ct), BLOCK_SIZE)]
	score = len(blocks) - len(set(blocks))
	return score

bits0 = ""
bits1 = ""

for i in range(0,176):
	r.recvuntil("Enter plaintext:")

	r.sendline(pt)

	r.recvuntil("Ciphertext is: ")

	ct = r.recvline()[1:-1]

	print (ct)

	if AES_ECB_Score(ct) > 0:
		print ("sending 'ECB'")
		r.sendline('ECB')
		bits0 += "0"
		bits1 += "1"
	else:
		print ("sending 'CBC'")
		r.sendline('CBC')
		bits0 += "1"
		bits1 += "0"
	print (str(i+1) + " correct answers")

print (binascii.unhexlify(hex(int(bits0,2))[2:].encode()))
print (binascii.unhexlify(hex(int(bits1,2))[2:].encode()))

FLAG: flag{ECB_re@lly_sUck$}


authy:

For this challenge we the following server code:

#!/usr/bin/env python3
import struct
import hashlib
import base64
import flask

# flag that is to be returned once authenticated
FLAG = ":p"

# secret used to generate HMAC with
SECRET = ":p".encode()

app = flask.Flask(__name__)

@app.route("/", methods=["GET", "POST"])
def home():
    return """
This is a secure and private note-taking app sponsored by your favorite Nation-State.
For citizens' convenience, we offer to encrypt your notes with OUR own password! How awesome is that?
Just give us the ID that we generate for you, and we'll happily decrypt it back for you!

Unfortunately we have prohibited the use of frontend design in our intranet, so the only way you can interact with it is our API.

/new

    DESCRIPTION:
        Adds a new note and uses our Super Secure Cryptography to encrypt it.

    POST PARAMS:
        :author: your full government-issued legal name
        :note: the message body you want to include. We won't read it :)

    RETURN PARAMS:
        :id: an ID protected by password  that you can use to retrieve and decrypt the note.
        :integrity: make sure you give this to validate your ID, Fraud is a high-level offense!


/view
    DESCRIPTION:
        View and decrypt the contents of a note stored on our government-sponsored servers.

    POST PARAMS:
        :id: an ID that you can use to retrieve and decrypt the note.
        :integrity: make sure you give this to validate your ID, Fraud is a high-level offense!

    RETURN PARAMS:
        :message: the original unadultered message you stored on our service.
"""

@app.route("/new", methods=["POST"])
def new():
    if flask.request.method == "POST":

        payload = flask.request.form.to_dict()
        if "author" not in payload.keys():
            return ">:(\n"
        if "note" not in payload.keys():
            return ">:(\n"

        if "admin" in payload.keys():
            return ">:(\n>:(\n"
        if "access_sensitive" in payload.keys():
            return ">:(\n>:(\n"

        info = {"admin": "False", "access_sensitive": "False" }
        info.update(payload)
        info["entrynum"] = 783

        infostr = ""
        for pos, (key, val) in enumerate(info.items()):
            infostr += "{}={}".format(key, val)
            if pos != (len(info) - 1):
                infostr += "&"

        infostr = infostr.encode()

        identifier = base64.b64encode(infostr).decode()

        hasher = hashlib.sha1()
        hasher.update(SECRET + infostr)
        return "Successfully added {}:{}\n".format(identifier, hasher.hexdigest())


@app.route("/view", methods=["POST"])
def view():

    info = flask.request.form.to_dict()
    if "id" not in info.keys():
        return ">:(\n"
    if "integrity" not in info.keys():
        return ">:(\n"

    identifier = base64.b64decode(info["id"]).decode()
    checksum = info["integrity"]

    params = identifier.replace('&', ' ').split(" ")
    note_dict = { param.split("=")[0]: param.split("=")[1]  for param in params }

    encode = base64.b64decode(info["id"]).decode('unicode-escape').encode('ISO-8859-1')
    hasher = hashlib.sha1()
    hasher.update(SECRET + encode)
    gen_checksum = hasher.hexdigest()

    if checksum != gen_checksum:
        return ">:(\n>:(\n>:(\n"

    try:
        entrynum = int(note_dict["entrynum"])
        if 0 <= entrynum <= 10:

            if (note_dict["admin"] not in [True, "True"]):
                return ">:(\n"
            if (note_dict["access_sensitive"] not in [True, "True"]):
                return ">:(\n"

            if (entrynum == 7):
                return "\nAuthor: admin\nNote: You disobeyed our rules, but here's the note: " + FLAG + "\n\n"
            else:
                return "Hmmmmm...."

        else:
            return """\nAuthor: {}
Note: {}\n\n""".format(note_dict["author"], note_dict["note"])

    except Exception:
        return ">:(\n"

if __name__ == "__main__":
    app.run()

Basically we have a /new endpoint that let's us send an 'author' name and a 'note' to the server and gives us back and ID for said note, essentially a base64 encoding of something that looks like this...

admin=False&access_sensitive=False&author=Lost&note=MyPlaintext&entrynum=783

...and an integrity value. We can then send both of these back to the server through the /view endpoint and read our note. From the following part of the server code...

            if (note_dict["admin"] not in [True, "True"]):
                return ">:(\n"
            if (note_dict["access_sensitive"] not in [True, "True"]):
                return ">:(\n"

            if (entrynum == 7):
                return "\nAuthor: admin\nNote: You disobeyed our rules, but here's the note: " + FLAG + "\n\n"
            else:
                return "Hmmmmm...."

...we can tell that we can get the flag if we produce a valid note that contains the following:

  • &admin=True&access_sensitive=True&entrynum=7

Obviously, the code won't allow us to just create a note with this string.

        payload = flask.request.form.to_dict()
        if "author" not in payload.keys():
            return ">:(\n"
        if "note" not in payload.keys():
            return ">:(\n"

        if "admin" in payload.keys():
            return ">:(\n>:(\n"
        if "access_sensitive" in payload.keys():
            return ">:(\n>:(\n"

The vulnerability lies on the use of SHA1 to create the integrity value:

        hasher = hashlib.sha1()
        hasher.update(SECRET + infostr)
        return "Successfully added {}:{}\n".format(identifier, hasher.hexdigest())

SHA1 is a hash function know to be vulnerable to Length Extension Attacks. Now there are a few implementation nuances to take into consideration if you wish to write code from scratch to do this so we're gonna go full script kiddy here and use haspumpy. The idea here is, we send some random note to the server, take the corresponding ID, decode it, add whatever data we want (in this case '&admin=True&access_sensitive=True&entrynum=7<'), encode it, and send it to the server along side the corresponding integrity hash value. To do this we also need to know the lenght of SECRET but we can just brute force it.

Solver:

import base64
import hashpumpy
import requests

url = "http://crypto.chal.csaw.io:5003" 
new = url + "/new"
view = url + "/view"

def sendNotes(name,note):
	s = requests.Session()
	r = s.post(new, data = {"author" : name, "note" : note})
	return r.text[len("Successfully added "):]

def recvNotes(id,integrity):
	s = requests.Session()
	r = s.post(view, data = {"id" : id, "integrity" : integrity})
	return r.text

def main():
	id,integrity = sendNotes("Lost","MyPlaintext").split(":")
	integrity = integrity[:-1]
	original = base64.b64decode(id)
	for i in range(0,256):
		integ, id = hashpumpy.hashpump(integrity,original,"&admin=True&access_sensitive=True&entrynum=7",i)
		id = base64.b64encode(id.decode('ISO-8859-1').encode('unicode-escape'))
		r = recvNotes(id,integ)
		print ("Number of requests sent > " + str(i+1))
		if "flag" in r:
			print (r)
			break

if __name__ == "__main__":
	main()

FLAG: flag{h4ck_th3_h4sh}


adversarial:

For this challenge we get a file containing the following:

'''
-------------------
   Problem Text:
-------------------

While digging through system logs, Morpheus discovered machines on the local
network transmitting the following base64-encoded ciphertexts to an IP address
known to be under enemy control:

2us8eN+xyfX3m+ouq+Rp51ruXKXYbKCbe5GjrddBHVm0vhKd2KMXMjFWQVclCmNnsGuEhFSOoFRo
0hIKHGZrrCS/BRITjW7DJ5L+c0C6Dhu6yBNSnWDpf7sYMknxcaZ+FSwg0nVVNxlNZsfqpd9NOg7F
OGsysrh8EIGXZiovI6mLWo9FobtcCDbRZXT7Op5rz7hFynKLtFLIx1GTt4CUrKw6J/tpjTZ9mv/w
bBjD5Iwd060oTwfZd4NVg+GdDqyz1PA=

w+YyIN+r1brrm/li+7YB5Ey6XbjYbKCbfIej69lAEReh+weex/E7NX1GVEcuWGwyt37Ijk7AjlRt
2RlTSDJ0pTHrUQMZiiuHNdy/NUG2BFH/zR5diiWwcqtMJk36P/V4Hi87lztFfwxRI571sYtGd0CE
Im5hsbwwGoadIXk+LuCLXoZUp7U=

2es4LJm027Kll/h4tPBH6hX/XaubJfjcYpTm5pMPLBGj+xWdhuR+PWIVDhZkGj52oWuL3wrS/hUp
nVBfPC555COnRAxckT2aZsi4dx3uB1b5j0wB0CGwPeMYBUD6cbN/ESdslyYYf0xfJIa0tZ5Na1fX
dWth6/B8JYeWZj8mJ+KLR5Qa769ID2+MIHzrKtowm6kJnn/OjF/Ok0WWtYbY5axpYKFug31slvDn
OV2Br4g=

wOYuf56/3/W9yLNxpeoB3Ei9TOqVcLydOpKj64YZQEr39VOlgvAqdHxKXVFjH2Nn/DzQ3AzO5yBs
wwlfBSNvtySsQEtEznaTd9L+QUCsFhf32ghAiCf1MPYOaRuuf/VHFTM43jhHLAtYIdul6MkWaFHL
bF4ktal8HIqANTgtI6WTGN8T/rUOOTLOMT3lf55xw69Mk2rY4ASanQOusZKMrLI2M+ZphiB9y6e8
Oludtr0Mxb0oQBuKfY1HlOHHSvXpi/A=

xKM8Yd+s0rClv/kh/K1V7U66FuqxNaycPpSyrtoPDBGj+z6Qk/E3LD8PZwJqGXAi5GiNilPAsBVg
xBQRD2Z6qzfrXAQJ1m75KYn+fUSpBxf33hVKyTHldb1MOEfxIvVyHiRsij1NKh9RZsrttd9eKQ+G
KXky5rU9As+SKi0vNODPDp5PuukODjjTNn7hdZhxzK1awH7OoVjek1GfuYCR4v86Mudtlyo+kvPo
cErb44QI2OcoaAyeYcAAgq6SGe213P5s3JnNJc1kSHnEoWR7bY4u1Tk0w4uqB4E7zR500Ig+M/mz
qTYUVCbAd8ghJVAxA2aKOZ302gtYn1elVRO7ljUCQfo5WdZtdsimmTN1LcLfB0ZAgLxUIeABA+xG
ChOBG/Q3az9GqUn6s13t8i24ySz+xCl2fDLqZjCanEVWNtIx1D1Fvm3Mt7+45yoANA7lHqnYsS/Y
wWqSJswOJmVBuLJprfZ33mPGLsf0FXNzaho9cBYioFIQEzUUyQ==

zPcpbZyzmrTx3u8j46oPqHi9XeqMfarOOpGiudtcC1n17F3I1bZpdCAfABAySzBn9TPb1hPS/0cp
nV07B2ZyqzHrVw4MlDeAMpP+YU22ERf32ghAiCf1Pu55JVz+Mr4zETRsmjRVMVYZE83g8ItGPkCE
KG4zo64vUdzEaGB4c7KLH9cO/asdW3eMfC6xNN86kegEkxaB+FnExwOIsZGU9f8nL7V8iSwu0/zh
ehnS8YxH

xeIrad+h1aClm/0n5uRJ6UnuWeqcZ6qPNtWIrtEDWA2uugfRnuwrdGZKXEciC2lnt3+aih2Xpgcp
whgeBHk8ky2qUUsVnm7ZKYn+YkCtBxfv0RpRhSWwZKEYJkn0NPV1Ai8h3iFKPgwZIszgsZIReyiK
Oyo2qagwFc+KKSxqLevEWcdUp/4OCT7bI3j6f4Nhx+hL1iaZvVLFk1eSscGc/royLbV/jjcxl72k
aATXtp0B0+l6SB+VLptPg62bQw==

2us0b5f42KfskOwxtLFSqEy6GKaZZrvOL5rmv9ZKWBSpthafk6MxMjFbXFd2ECpns2KNnViJqVR9
2BhfDjNyoCSmQAUImSKAIJC/YgW2ERfv0w9ahCHkdaJBcU3nIad2AzMpmnkCPhZdZsrttd9vNQ+I
LWY45q85B4qSKjwuZuTYDoVPu/MODzLaLHPmc4NlgqlH13KLtlOFk3eSsZOdrL4hJbV8lip9l/7r
exmdtr0B0+lsQhGLLphP0biQCb/6yLdmzc2MJ9tySXiX9XI0bMY8nAY3loynBsQo0A41yoR3M/m9
qCVVTymPbYArLlASBXzEYNTM3k4WlEzkB3Cgl3YOXP0uF85kaZCmgj59JdTfHEhWmbxGJ7IGH6kX
MheGHfQgKT9fpxCytErt5yu5yTX+lyk+aXf9fD3Ulk0CY509yWgWo2nW/rW56Q==

wOYuf56/3/Wzyrtwp+oB3Ei9TOqVcLydOpKj64gbSEv19VOlgvAqdHxKXVFjH2Nn8j7Y3Q7O5yBs
wwlfBSNvtySsQEtKzH6SddL+QUCsFhf32ghAiCf1MPgMYRqsf/VHFTM43jhHLAtYIdul5sseaVPL
bF4ktal8HIqANTgtI6WdGtcS/LUOOTLOMT3lf55xw69Mk2Ta6AWYnQOusZKMrLI2M+ZphiB9xaW0
O1mdtr0Mxb0oQBuKfY1HlOHJSP3oifA=

2O07Y42sz7vkiu4u7egB5kLuV6SdNayPNdWkrp5bFxWi+wSZhvd+IHlKDm9jDHQuvCqBnBPAnht8
kBUeHiM8sCrrVg4Z2CfUZpqxZwWmDULozB5fj26wRKZRIgj2IvVqHzU+3jlDLAwZJdbkvpxLdUCk
Kn4ktP0oGYaAank+LuDZS8dJvLtAAnfJMG/mc4NlgqpI0DnA+G7ExgOOtYqdrKs7JbVqjTA40+Ht
ZQaftp0B0+l7WRGLd8xFn6WMUO2j1ash0tjHLp5mXSve7z1td9srnDc9h96lDYBp3A9514lkdqqr
rDJAXjaFcYA9JwVoG3LEOtTs2QtUlU/iECax1nYuXP18Q8NqasTyhj48M8KbXllcnvAeaOsdAuxE
CxeLT/Q2JUhEplS/o1Ss6CHxySD/030fLCTwfS7UgERXOponzGgBtmnFt6SiomcTLEzpGKnYqyXV
yCOPLIVdYQ==

2es4LLK5zqfshqsr5+RO5EmrSuqMfa6Ae4ypvp5EFhax9VO4x/MsMXdKXAJhF3MpsGOGiB2GtRtk
kAkXDWZ5qSC5Qg4SmyuAKZr+eku6Ql70yx5UmyH8MK9WPkX+PawzBC9sij1Hfx1UI8zitZFNPkCK
Kio1rrh8H4qLMnVqL+uLWY9JrPMODjbOID38coRxgqFakyaGvRfY2luOvMGO6a0gKfpmzw==

2es4LLK5zqfshqsr5+RAqF63S76deOPOFZCp5Z57EBiy+wCIlPc7OTFGXQJtDXRnoWSNgkTO5zZ8
xF0IACNy5DykUEwOnW7JKI+3cUDzQk71yltfhi/7MK9KPl3xNfkzBygtinVGMFhAKculo5pLZECn
OXkoqLgvAs+eIzdmZvHOT4RIquldQXfRJGrxf59xjuhK0iCevVnf1lGJ+sGs5LpzNvB6mGUwmv/g
ekrc8Mkd3qwoXRuWfoBF0baaXKyo3/5118DFJdkzWWSX8nxifYB5/iAsw4uqF40lnh1wnoh9P6qo
rDZHXmCQZs80JBVoDWHPbofs30da0EKrBTGmjHYYVagoX8N1L5f/nS95LIvfH0dR0uhaKeZSGq1c
GgXSG/U9aD9EvUL6tFao6zzzyRj+wn0+bSH9Mi2b2V5Mfpc6yDwEvWiZt72ltDNBIkirBbWdsC+Z
3WaHM4xLb3ATtOEno+4kwybTItv0DHMgfF90dwo3oEIBFT4EyXht/gmmKqUH3+nqpNuKZVNuNm0/
luqszUFULuho1emuvPoXHjM1RbVfpVbaag4owD/KTWORtWfxEmGyOVHTj9dPiFPpIhIao69KuE/b
ryCTW6dp5XkAB64E3PSsQzufz49kHITrz4hNkXPn

2es4LJm027Kll/h4tPBH6hX/XaubJfjcYpTm5pMPLBGj+xWdhuR+PWIVDhZkGj52oWuL3wrS/hUp
nVBfPC555COnRAxckT2aZsi4dx3uB1b5j0wB0CGwPeMYBUD6cbN/ESdslyYYf0xfJIa0tZ5Na1fX
dWth6/B8JYeWZj8mJ+KLR5Qa769ID2+MIHzrKtowm6kJnn/OjF/Ok0WWtYbY5axpYKFug31slvDn
OV2Br4g=

3uYzeJa91KGljvkt87ZA5V7gGJ6QcLbOOJSo69NADhzmsh3Rhu06dH5aWgJtHiYmqnPInFKGswNo
whhfGzJ1qCnrTQoOnGPXL467cQWrDRfu1x5am2Djab1MNEWxcYF7ETRskzBDMQsZMtbkpN9PNRmK
Im9hsbh8GY6FIzdtMqXeQJdMuvxJCDOdLG6oaoJ2x6Zd2jOCtE6L0k3alYad4qt9YNxmkiw5lrHw
YQ+T24gdxKBwAV6NZolZ0aCNGe2/zLtz3NbCLp5yQ2+X9XVxYY44zjB4jZHkDIoskEpC28x6cvy5
5CBBSTaJdcUgaBIxTHvDKp320QtQgkzmVSS8nTtbE+olF9B0YYrvgDw8J9WQEwlBmvlfZLIQArgX
Cx6XFr05d3oLvFi/8V+s8iC2jCTh0i8lInfMejyN2UpQf9IvzikXt2Xb8PCrqytBOUbuUbmXrDjK
gSOcK4VXb3ATtOEho/Zg2C3VZsO4FDx0dl90aQE+vxtGBTMJhDAM/QjnKbdJ0qHkttuNYlltc35t
nLj/zgBJJe82kP7t8ewXUD56XKMTp0rAaBAo0DWaQGyDtSnxXS66cELTntdImUXwaQ==

1+oyYt+T36z2xKt6tOkBzg3jGIvYOO+/e9jm+p4CWCPm9lOjx65+DjECDmAiVSYd5CfIvR3N5yYp
nV0tRmZOoTWuRB9G2HaAa9yYNQj/Ixe3nyoTxGChMOMYCwiycYczXWAW3ngCHVgUZuSl/d98e03F
Hips5o9yUb2WNjwrMr+LFscN790OQHf8ZTCoS80vgvkJnnK0+BqL4QPX9LvYof8RYLgou2Vw08Ok
JErhtsRJ5A==

xKMqY5H/zvXpl+5i4KsB8UK7FOq2cKDAe7CwrsxWWAqvtRSdgqMzNX8PQVAiD2kqpWTImFWP5xxo
w10MHClzoGW/TQ4Vim7HNJOre0HzQlLs2glKhi71MLlQPgj3MKYzFi85mT1WfxlXZt/itZFaewiE
Pyolr7g4X8+xMy1qMe3OXIIAu/NLFHfVJGvtOotjy6RM137OoVjek1STuI3Y/6owI/BthWs=

3e84bYy9lPXEjasLtLNA+w29WbORe6jCe4aurp5cDAyruR+Ug6MrJH5BDkMiC2krsX6BgFPAsBxs
whgdEWZyoSS5SRJcwXeFZpO4NUSzDhfu2ghHyTPlcqRdMlzscbRwEyU8ijBGfwxRI571opBJKQGI
bGsy5rEzH4jTJypqMu3OV8dXqulLTTDUM3jmOowiwaBG2jGL9BfOxUaU9IierKs7JewoliAvlrHr
ZwbKtoge17ttDRGfLphIkLXfH6W1071khdjYa98zQ27W8zBhds020iY7ipGxEMQl2xxw0sIyROK1
qDYUTyiJcIAlJgM/CWGKKIH21V9fn03uEXz0kSJXROkvF81jeY3pmyhwOIeZC0dRk/FXJuYTG6BO
XxCeDuo9YTMLvFivohiu9CC8nSj/0H0iZDK4fS2cnFlVc4EtlisKvXjH9rSjpDMOP1erAqSLty/U
xGDIIo5BInANqOE9pPtwkSrUZs6xHmgga1Q3agEkp1ICUjYJgDBYsBnuNaEI0qzr4o+WaBZwb385
lqf/yxVOJfF8nq3H7u4XEnsuXb9ApQXdbhZ8hCjfTniGtW2lRma5OVXJhZBOkE2xZwhe6rdH+Vrb
tiGYTfV3+GxAB6ELm+m2ACeVjJRzF9D5w4kBnCeqBO3JU6j9ocU/AMmaB0XjMTd13Lh24kzDnOHn
MxBHhxTfYe2ki35XEOMP9WYpYBrhcn8S0EYi7uY8Jh6vploUBDM3XGyeNMdkK3YVB+EUPWs1lSuq
i0UMuFyqUoxik35l0T2H4KTD2C/hDuIA12nyEFjhfi1EztvK+shdVSd+wL7bLv0DDB1hy/N1w5PT
QGtE444gnmRwvhQrZ6vUCSspwQ6FNfIASiAvTs3GWV3ei5By/6GY5s0QTC8Vh9djHGVIPXAgqJ67
wOlVgyFR4CWpoDp+yZYfB8vUCsG07kUUVQ==

xKQrad+r37Dr3uostKVG7UO6GLqNe6yGe4GuudFaHxHmulOSiO09JnRbSwJ1GWor6iqlilPArxV/
1V0aBTZorSCvBQ4SjCfSI9y9eUyvERf7y1tHgSX9MK9WNQj3OKEzHi84ljxMOFhbM8qlsZZcdUC8
KX5hsrU5GJ3TNS04I+vMWo8ArvVKTSPVIHT6Op5yx61NkzOcvRfYx0qWuMGa7aw2JLVhj2U80+br
ewbXtp0B170oRA3ZbJlJnbXfE6P6yKttwMqCa/x2TmrC8ng0d8h5yD05l9LkF4wsx0pi14B+M+S5
sjZGGyKFI8E3aAM8HnzEKdT3xAtXgwPtFCOg2DcEE/EzQoJiboqmjD4y

yPU4foas0rzrmas2/KVVqEWvS+qZNa2LPJyopddBH1muugDRhu1+MX9LAAJLWHUioSqch1jAohpt
kB4QBS9yo2vrbEsPnSuAMpS7NUG+EFz02ghAyTPgYqtZNUHxNvszOWA/mzACOx1YMtar/tEOOg6B
bHMus/09A4rTJzUmZvHDT5MAvO9PAzPOZXTmOoVr0ehe0ivA

2eYueN+136b2n+wntPURvRT8FuqscLyae5ijuM1OHxzm6kPE3rFwdEVKXVYiFWM0t2uPih3R90Ew
glNfPCNvsGWmQBgPmSnFZs3uIBztTBfO2ghHyS31Y71ZNk2/YOUmSXJi3gFHLAwZK9v2o55JPkDU
fD949PN8JYqAMnknI/bYT4BF76oeWG6Paz3cf552gqVMwCGPv1KLghPP7dPWrIs2M+EojCAugPDj
bEqCptxQhOcoeRuKesxNlLKMHaq/mu8xkICeZQ==

zPB9dZCtmrThm/o39bBE5FTuSL+MOe+aM5Dmu8xAGhWjtlOYlKM9PH5GTUcsWEQysCqfih2BqwZs
0RkGSC1yqzLrUgMdjG7ZKYn+dFe6QlD11hVUyTT/MKpXfQj7Prs0BGA7m2oCHhRLI9/hqd9newOE
Iioyo7h8BYeWZjoiJ+zFDpVFrvhaBDjTfz38cogiwaBM3juNuVuLw1Gft5SK/7AhM7V8iSQp0+Lt
bgTS+skd3qwoQhCKa5gAnqffHaP637Nu0dDDJZIzSW7E6Hp6fcp5zyU9gJeiCoco0gZsnph9M+Wq
oSFDUyWMboAoJxchDzPLIJC4xE5Xg0zlW3CVlnYSXucoXs1vL5Dujy88KNTfH0VHl/1WMbIQG6VZ
Gx+cCL0hamoLvF/6pVCopja0hDH90n03YjO4fTuCkERXadI8yT0RuzaV5Liv5y4SbUnkGLOf4z7W
jWeBJsBPIXVBpaksvv8k2DCSKM2gEHVueRotbRFnr1YIUj8PxyxDsB7yKLRJz72r4rORfVMtNkU5
06OsghVVJb1rxeTs6OwLTT40QblSrAXBcxppynreTWGAo2DqXCL8akzWn5tIkE74KApF76ICrVOe
+zuZV/V96TUDQegU1OmqQyiCip5iFoP6jI8ZimKnDPfSC+HoutV6WceBVQD3IDN4yals+AuUifLj
PxRWnVY=


Upon further investigation, the following script was found:
'''

#!/usr/bin/env python2

import os

import Crypto.Cipher.AES
import Crypto.Util.Counter

from Messager import send


KEY = os.environ['key']
IV = os.environ['iv']

secrets = open('/tmp/exfil.txt', 'r')

for pt in secrets:
    # initialize our counter
    ctr = Crypto.Util.Counter.new(128, initial_value=long(IV.encode("hex"), 16))

    # create our cipher
    cipher = Crypto.Cipher.AES.new(KEY, Crypto.Cipher.AES.MODE_CTR, counter=ctr)

    # encrypt the plaintext
    ciphertext = cipher.encrypt(pt)

    # send the ciphertext
    send(ciphertext.encode("base-64"))


'''
Unfortunately, the environment variables used for KEY and IV are no longer recoverable
and the file /tmp/exfil.txt has been deleted.

Use your knowledge of how AES in CTR mode work to decrypt the ciphertexts and find the flag.'''

Before we get into it I should point out that in concept this is the exact same challenge as cryptopals 20.

In the file we get a bunch of ciphertexts that were all encrypted with AES CTR (a usually pretty safe way to encrypt things). The problem here is that the same key and nonce pair was used to encrypt all of them. First let's get a basic idea of how CTR works.

The point of CTR mode is to transform a block cipher into a stream cipher. It achieves this by encrypting successive values of a nonce (an arbitrary number used only once) instead of the plaintext, and then XORing the result with the plaintext. Since the ciphertexts we have were encrypted with the same key and nonce pair, if we concatenate them we can decrypt the result through frequency analysis, as if it were encrypted using repeating key XOR.

Solver:

import base64
import binascii
import os
import string

from Crypto.Cipher import AES
from math import ceil

BLOCK_SIZE = 16

ctBase64 = ['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']

freqs = {
      'A': 0.0651738,
      'B': 0.0124248,
      'C': 0.0217339,
      'D': 0.0349835,
      'E': 0.1241442,
      'F': 0.0197881,
      'G': 0.0158610,
      'H': 0.0492888,
      'I': 0.0558094,
      'J': 0.0009033,
      'K': 0.0050529,
      'L': 0.0331490,
      'M': 0.0202124,
      'N': 0.0564513,
      'O': 0.0596302,
      'P': 0.0137645,
      'Q': 0.0008606,
      'R': 0.0497563,
      'S': 0.0515760,
      'T': 0.0729357,
      'U': 0.0225134,
      'V': 0.0082903,
      'W': 0.0171272,
      'X': 0.0013692,
      'Y': 0.0145984,
      'Z': 0.0007836,
      ' ': 0.1918182
}

valid = string.ascii_uppercase + string.ascii_lowercase + " "

def singleByteXor(s,byte):
	return "".join(chr(s[i]^byte) for i in range(0,len(s)))

def score(s):
	sc = 0
	for c in s:
		if c in valid:
			sc += freqs[c.upper()]
	return sc

def xorStrings(data0, data1):
	ret = b"".join(binascii.unhexlify(hex(data0[i]^data1[i])[2:].encode().rjust(2,b'0')) for i in range(min(len(data0),len(data1))))
	return ret


def decrypt(ct,key,nonce):
	aes = AES.new(key, AES.MODE_ECB)
	keystream = b"".join(aes.encrypt(nonce+chr(ord(str(counter))-48).ljust(8,'\x00').encode()) for counter in range(ceil(len(ct)/BLOCK_SIZE)))
	return xorStrings(ct,keystream)

def getBlocks(ciphertexts, keySize):
	ret = []
	for i in range(keySize):
		block = b""
		for j in range(len(ciphertexts)):
			if i < len(ciphertexts[j]):
				block += bytes([ciphertexts[j][i]])
			else:
				continue
		ret.append(block)
	return ret

def getKey(blocks):
	key = b""
	for block in blocks:
		maxScore = 0
		keyGuess = b""
		for i in range(0,256):
			curTry = singleByteXor(block,i)
			curScore = score(curTry)
			if curScore > maxScore:
				maxScore = curScore
				keyGuess = bytes([i])
		key += keyGuess
	return key

def main():
	ciphertexts = []
	for ct in ctBase64:
		ciphertexts.append(base64.b64decode(ct))

	keySize = len(max(ciphertexts, key=len))
	blocks = getBlocks(ciphertexts, keySize)
	keyGuess = getKey(blocks)
	for i in range(len(ciphertexts)):
		print (xorStrings(ciphertexts[i],keyGuess))

if __name__ == "__main__":
	main()

Output:

b"What is real? How do you define real? If you're talKing about what you can feel, what yox can smEll, what you Eanataste and see, then rea? is  :mply electrical signaAs inter~retEd by your brain."
b"Neo, sooner or later you're going to realize, just As I did, that there's a difference bhtween kNowing the patN,  nd walking the path."
b'The flag is: 4fb81eac0729a -- The flag is: 4fb81eac\x10729a -- The flag is: 4fb81eac0729a -  The flAg is: 4fb81eaE07s9a -- The flag is: 4fb8beac0da9a -- The flag is: 4fO81eac07<9a'
b'Message 86831. Test message 86831. Test message 868\x131. Test message 86831. Test message 56831. TEst message 86\x1e31o Test message 86831. Te t me  age 86831. Test messaJe 86831  TeSt message 86831.'
b'I am the Architect. I created the Matrix. I have beEn waiting for you. You have many que~tions aNd though the Vro"ess has altered your co=scio& ness, you remain irre[ocably fumaN. Ergo, some of my answNrs you wizl :nDerstand, som  of them you wilJ no;. Concurrentky, Phide 8oTr jirs! quEst on m5y be thi m st per;xne!t, y=u fay or 9ay no  r alizei i5 is . s& thE D ss it7e  vao= '
b'Attack at dawn. Use the address 37.9257 10.2036 193\x19.283 - Do not reply to this message.-Attack At dawn. Use tNe  ddress 37.9257 10.2036 b939.ak3 - Do not reply to tEis messoge.'
b'Have you ever had a dream Neo, that you were so surE was real? What if you were unable tb wake fRom that dream\x19 H.w would you know the di5fere=0e between the dream wBrld, anj thE real world?'
b'Which brings us at last to the moment of truth, wheRein the fundamental flaw is ultimateay expreSsed, and the gno,aly revealed as both be4inni=4 and end. There are tZo doors  ThE door to your right leaOs to the Eou=cE and the sal3ation of Zion. TNe d or to your lbft Keals #aBk xo t=e MAtr x, t; her anh t  the e!u o) you  s{ecies.'
b'Message 64023. Test message 64023. Test message 640\x123. Test message 64023. Test message ;4023. TEst message 64\x1623o Test message 64023. Te t me  age 64023. Test messaJe 64023  TeSt message 64023.'
b'Unfortunately, no one can be told what the Matrix iS. You have to see it for yourself. Teis is yOur last chancC. \x00fter this, there is no \'urni=4 back. You take the bAue pill" thE story ends, you wake u[ in your tedoaNd believe wh$tever you want tI be#ieve. You tale tOe zedapHll  yo  stAy  n Wo:derland  a!d I sh f y u ho% dnep thetrabbi  h*le go s.'
b'The Matrix is older than you know. I prefer countinG from the emergence of one integral lnomaly To the emergenEe .f the next, in which ca e th:  is the sixth version\x03'
b'The Matrix is a system, Neo. That system is our eneMy. But when you\'re inside, you look lround, What do you seC? \x03usiness men, teachers, ?awye! , carpenters. The verT minds af tHe people we are trying _o save. Bct :nTil we do, th se people are stOll . part of thas syTtee,  nE tdat 8akeS t!em o!r enemy" Y u haveoeo :nder!taed, mos  of t<es  peop)e  re n 8 ;eadY ]  ee s+p 0ggd- hAnD len- af Hh c a&   otN+ufed  6  -oAel sTaQ dep "E n  on A E \'8Sxe:  -ha  t EEr il# f g=& <O TrEttc  Htm'
b'The flag is: 4fb81eac0729a -- The flag is: 4fb81eac\x10729a -- The flag is: 4fb81eac0729a -  The flAg is: 4fb81eaE07s9a -- The flag is: 4fb8beac0da9a -- The flag is: 4fO81eac07<9a'
b'Sentient programs. They can move in and out of any Software still hard-wired to their sy~tem. ThAt means that Gny.ne we haven\'t unpluggedsis p<\'entially an Agent. In^ide the.MatRix, they are everyone aEd they ars n  One. We have 6urvived by hidinA fr m them, by rrnniIg nro, Uhea, b t tHeyiare  he gategee?ers. T\'ty .re g\'aroing al8 the 0oo7s, th y  re h  d ng AlEotoe m y?i wi m  mEaow  hot Oo*`ert*rsl5S r8 scm  n  Xs "oNcO to --W   o fiR T  )Ea.'
b'Zion Keys: 8 - F - A - Q - 1 - Z - R - Z - B - Z - r - R - R. Repeat: 8 - F - A - Q - 1   Z - R \r Z - B - Z - t -aR - R. Repeat: 8 - F - \x12 - Qs~ 1 - Z - R - Z - B - w - R - \\ - r'
b"I won't lie to you, Neo. Every single man or woman Who has stood their ground, everyone zho has Fought an agenR h s died. But where they ;ave 52iled, you will succeeI."
b'Please. As I was saying, she stumbled upon a solutiOn whereby nearly 99% of all test subgects acCepted the proAra, as long as they were g:ven 2schoice, even if they Zere onlw awAre of that choice at a Eear-unconeci uS level. Whil  this answer funEtio!ed, it was oevioRslq f4nEaminta9ly Fla>ed,  hus cremti!g the  ehe=wise\x7fcoetradic ory s-st mic a+om ly t\'-tiif LeO; rncn c\' d l i t Thsaa e` tTee}ys  msi T lr. Ir" ,etYos  SeIt re#9R dtthe E:Og& M   di5e 5 m!NS >tyc i/  <c EcOeN fo!lE     x 5    .; e t$ 5t: gw5 $  E                                                                                                                                                                                                 '
b"I've seen an agent punch through a concrete wall. MEn have emptied entire clips at them lnd hit Nothing but aiT. \x18et their strength and t;eir  #eed are still based iC a worlj thAt is built on rules. BeHause of t~atc They will nev r be as strong oT asofast as you dan Ee."
b'Everything that has a beginning has an end. I see tHe end coming. I see the darkness sprhading. i see death...\x06an% you are all that stand  in ;:s way.'
b"Test message 10592. Test message 10592. Test messagE 10592. Test message 10592. Test mes~age 105\x192. Test messaAe p0592. Test message 1059a. Te ' message 10592. Test @essage ?059\x12. Test message 10592."
b'As you adequately put, the problem is choice. But wE already know what you are going to io, don\'T we? Already o c n see the chain reactio=: th6schemical precursors tEat signol tHe onset of an emotion, Oesigned sfec&fIcally to ove7whelm logic and Teas n. An emotioi thFt as  lSeahy b9indIngiyou  o the semp#e and  svi us t utc: she =s goi:g 1o dieean% the=)  s nOtA&n` yi0 /$n e&.<o Stnt =t  HSp   I ei   O  euibt <s nEia) OxEan d  T6i;n, s\\%Ul  Nio"\x7fl  t<e ;OI 4e  f 0o   /ReEtOse \'tSe-(:; i ;0ey  r 4e - e :   3 ,$T:b'

By sending one of the "flags" from the output to the server we get the actuall flag.

FLAG: flag{m1ss1on_acc00mpl11shheedd!!}