╔══════════════════════════════════════════════════════════════════════════════════╝ ║ CRYPTO SECURE VIGNERE SQU1RREL CTF WRITEUP [18/12/2025] ╚══════════════════════════════════════════════════════════════════════════════════╗┏━━┓ BACK┗━━┛ we are given the following: "Connect to: nc 20.84.72.194 5007 Wrap the flag with squ1rrel{}" no source code - hmm when we do that we are greeted with: ================================================================================ Welcome! Here's the flag! It's just encrypted with a vigenere cipher with a random key! The key is a random length, and I randomly picked letters from "squirrelctf" to encrypt the flag! With so much randomness there's no way you can decrypt the flag, right? Flag: mxpqmlslzrrhwoaellqfraiukjnmzv ================================================================================ **initial thoughts:** we are told it is vignere, ∴ len(plaintext) == len(ciphertext) - len(ciphertext) = 30 - ∴ flag is 30 chars unfortunatley, we are told that we need to wrap the flag with "squ1rrel{}" - so no chosen plaintext attacks can be used each time we connect to the server, we are given a different encryption; - the key is of random length every time ∴ we cant be sure when len(key) < len(plaintext) - the characters of the key are random every time, but chosen from "squirrelctf" - re-using chars must be allowed to allow for random length keys, - so: "rrrrrrrrrrrrrrrrrrrrrrrrrrrrr" could be a valid key - assume random weighting for characters ∴ "r" has a 'weight' of 2 - and all other chars have a weight of 1 given infinite possible encryptions (that we can access) what could we expect, pattern wise, that might give us a clue as to the plaintext? index of coincidence only applies if we find encryptions where len(key) < len(plaintext) (which we can't be sure about) worst case scenario is if len(key) >= len(plaintext) = >= 30 - each character has a p(1/11) chance of being: [s, q, u, i, e, l, c, t, f] - and a p(2/11) chance of being: [r] for each char; 10 different options ∴ for length 30, 10^30 options - or one thousand billion billion billion - assuming good code, at 300 billion operations/second - 1/3 x 10^19 seconds -> 95070000000 years - ∴ we need a more efficient algorithm than guessing the worst case scenario - or we assume a best key length scenario hmm, lets try looking for some patterns if we query the server 10000 times, we'd get a list of ciphertexts: ct x: mxpqmlslzrrhwoaellqfraiukjnmzv ct y: njroknsmxssixpbfmkrgsbjvlkomaw ct z: oypqlotnzttjyqcgnlshtckwmnlobx as vignere uses rotation, if a character in the ciphertexts match, we know the character in the key at that position is also the same also - as [a] is not in [s, q, u, i, e, l, c, t, f], the plaintext cannot be ROT0 ∴ every character in the plaintext cannot be found in any ciphertext at the same position say the key is: ky 1: fffffffffffffffffffffffffffffs ky 2: rrrrrrrrrrrrrrrrrrrrrrrrrrrrrs then: ct 1: ?????????????????????????????x ct 2: ?????????????????????????????x the rotations of [s, q, u, i, e, l, c, t, f] and [r] are: [+18, +16, +20, +8, +4, +11, +2, +19, +6, +17] so we can create a possible characters list for that final character which (if we found matching pairs with "x") are: [x+18, x+16, x+20, x+8, x+4, x+11, x+2, x+19, x+6, x+17] [p, n, r, f, b, i, z, q, d, o] ================================================================================ **possible weakness:** not all of the characters in the key have equal appearance value ∴ given we have sufficiently many ciphertexts, for a given char position: - there will be 10 different resulting chars - of those, 9 will have an equal p(1/11) chance of occuring - 1 will have a p(2/11) chance of occuring - this will correspond to rot[26-17] of that character over a large enough sample of ciphertexts we can decrypt that one character with some certainty **∴ plan is:** ================================================================================ get lots of ciphertexts, and anaylse one set position across them -> find the frequency distribution of those 10 possible characters at that position -> if there are enough to assume a decently random distribution -> assume the character corresponding to a p(2/11) frequency is rotated by [r] -> undo the rotation by doing ROT[26-17] -> perform this for each of the 30 character slots in the flag length -> wrap the flag p(r being selected) = 1 - (1 - p)^n so say we aim for 99.99% certainty: 0.9999 = 1 - (1 - 2/11)^n (1 - 2/11)^n = 0.0001 n = 45.89... 99.99% certainty requires ≈ 46 cases 95% certainty requires ≈ 20 cases lets aim for 100 cases, just to be sure:-------------------------------------------------------------------------------- -------------------------------------------------------------------------------- # generating 100 encrypted responses dictionary import socket host = "20.84.72.194" port = 5007 def netcat(host, port): client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) client_socket.connect((host, port)) # connect to the server server_response = client_socket.recv(1024) # save the response to a variable client_socket.close() # close the socket after receiving the data return server_response def parse(server_response): trimmed_response = server_response[264:] trimmed_response = trimmed_response[:30] trimmed_response = trimmed_response.decode('utf-8') return trimmed_response # takes the response and trims it to just the 30 characters of the encrypted vigenere def retrieve_ciphertext(): vignere_enc = netcat(host, port) x = parse(vignere_enc) return x # function to pin the above together - its a little messy i know if __name__ == "__main__": output_file = "vignere_encrypted_dictionary.txt" with open(output_file, 'w') as f: # open file in write mode for i in range(1, 1001): ciphertext = retrieve_ciphertext() f.write(ciphertext + '\n') # write each output to a new line in the file print(ciphertext) # print to console for my visual sanity -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- this gives us a dictionary file to work with now for a solve script:-------------------------------------------------------------------------------- -------------------------------------------------------------------------------- # Created on Sun April 6th # vignere solver for squirrel CTF - using a dictionary of saved encryptions from collections import Counter from rdout import rdout, style def analyze_frequencies(file_path): # create a list of counters for each character position position_counters = [Counter() for _ in range(30)] # Read the file line by line with open(file_path, 'r') as file: for line in file: line = line.strip() for i, char in enumerate(line): if i < 30: position_counters[i][char] += 1 for i, counter in enumerate(position_counters): rdout(style.GREEN, f"position {i + 1}:") for char, count in counter.items(): print(f" {char}: {count}") print() return position_counters def assemble_common_chars(position_counters): vignere_string = "" rdout(style.GREEN, "most frequent character positions:") for i, counter in enumerate(position_counters): if counter: most_common = counter.most_common(1)[0] print(f"{i + 1}: {most_common[0]} (freq: {most_common[1]})") vignere_string += most_common[0] else: break rdout(style.BLUE, "vigenere string: ") print(vignere_string) return vignere_string def rot(string, rotation): result = [] for char in string: alphabet = "abcdefghijklmnopqrstuvwxyz" if char in alphabet: start = ord('a') rotated_char = chr((ord(char) - start + rotation) % len(alphabet) + start) # modular rotation result.append(rotated_char) rotated_string = ''.join(result) rdout(style.BLUE, "rotated vignere string:") print(rotated_string) return rotated_string file_name = "vignere_encrypted_dictionary.txt" position_counters = analyze_frequencies(file_name) vignere_string = assemble_common_chars(position_counters) rot(vignere_string, (26-17)) -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- this gives us the string: ithoughtrandomizationwassecure wrap and we have the flag: sqrl{ithoughtrandomizationwassecure} yay!CWW out ┏━━┓ BACK┗━━┛