Files
ctf/2025/lake/misc/wordler/gemini_solve.py
2025-11-28 23:48:04 +01:00

248 lines
7.8 KiB
Python

import os
import re
from pwn import remote
def get_word_lengths(input):
print("--- Decoded Output ---")
print(input)
print("----------------------\n")
# 2. Find the line containing "Structure:"
structure_line = None
for line in input.splitlines():
if "Structure:" in line:
structure_line = line
break
if not structure_line:
print("No 'Structure' line found.")
return []
# 3. Extract the pattern part (remove "Structure: " prefix)
# The split ensures we get everything after the colon and space
pattern_str = structure_line.split("Structure:")[1].strip()
# 4. Split by the underscore '_' which acts as the delimiter between words
word_segments = pattern_str.split('_')
# 5. Calculate lengths of each segment
word_lengths = [len(segment) for segment in word_segments]
return word_lengths
def get_word_dict(wordlist_path='wordlist.txt'):
"""
Reads a wordlist file and suggests words that match the required lengths.
"""
if not os.path.exists(wordlist_path):
print(f"Error: Wordlist file '{wordlist_path}' not found.")
print("Please create a file named 'wordlist.txt' with one word per line.")
return
print(f"\nReading wordlist from: {wordlist_path}")
try:
with open(wordlist_path, 'r', encoding='utf-8') as f:
# Clean words: remove whitespace and filter out empty lines
words = [line.strip() for line in f if line.strip()]
except Exception as e:
print(f"Error reading file: {e}")
return
# Group words by length for efficient lookup
words_by_len = {}
for w in words:
l = len(w)
if l not in words_by_len:
words_by_len[l] = []
words_by_len[l].append(w)
return words_by_len
def parse_ansi_feedback(raw_bytes):
"""
Parses the raw bytes containing ANSI codes to extract letters and their colors.
Returns a list of word_segments.
Each segment is a list of tuples: (char, color_code)
"""
try:
decoded = raw_bytes.decode('utf-8')
except UnicodeDecodeError:
decoded = raw_bytes.decode('utf-8', errors='ignore')
print("--- Raw Decoded String ---")
print(decoded)
print("--------------------------")
# Split into word chunks based on the underscore
# We must be careful because the underscore is likely plain text between ANSI codes
raw_segments = decoded.strip().split('_')
parsed_segments = []
# Regex to find ANSI code and the character immediately following
# Matches: \x1b[93mU
ansi_pattern = re.compile(r'\x1b\[(\d+)m([A-Z])')
for raw_seg in raw_segments:
matches = ansi_pattern.findall(raw_seg)
if matches:
# matches is a list of ('93', 'U'), ('90', 'V'), etc.
parsed_segments.append(matches)
return parsed_segments
def generate_constraints(segment_data):
"""
Converts parsed (color, char) data into logical constraints for a specific word.
"""
constraints = {
'length': len(segment_data),
'greens': {}, # index: char
'yellows': [], # list of (index, char)
'forbidden': set(), # chars that definitely aren't in the word
'must_have': set() # chars that must exist (from greens/yellows)
}
# First pass: Identify "Must Have" chars (Green/Yellow)
for i, (color, char) in enumerate(segment_data):
char = char.lower()
if color == '92' or color == '93': # Green or Yellow
constraints['must_have'].add(char)
# Second pass: Build specific constraints
for i, (color, char) in enumerate(segment_data):
char = char.lower()
if color == '92': # Green (Correct Position)
constraints['greens'][i] = char
elif color == '93': # Yellow (Wrong Position)
constraints['yellows'].append((i, char))
elif color == '90': # Grey (Not in word / Not here)
# If char is in 'must_have', Grey just means "not at this position"
# (or max count reached, but "not at this position" is safer for simple filtering)
if char in constraints['must_have']:
constraints['yellows'].append((i, char)) # Treat as "wrong pos" constraint
else:
constraints['forbidden'].add(char)
return constraints
def get_candidates_for_slots(wordlist_path, constraints_list):
"""
Reads the wordlist and applies constraints for each word segment.
Returns a list of lists: [ [candidates_for_word_1], [candidates_for_word_2], ... ]
"""
if not os.path.exists(wordlist_path):
print(f"Error: {wordlist_path} not found.")
return []
try:
with open(wordlist_path, 'r', encoding='utf-8') as f:
all_words = [w.strip().lower() for w in f if w.strip()]
except Exception as e:
print(f"Error reading wordlist: {e}")
return []
all_slots_candidates = []
# Process each word slot defined in the input
for word_idx, constraints in enumerate(constraints_list):
print(f"\n=== Solving Word {word_idx + 1} (Length {constraints['length']}) ===")
candidates = []
for w in all_words:
# 1. Length Check
if len(w) != constraints['length']:
continue
# 2. Forbidden Char Check
if any(c in constraints['forbidden'] for c in w):
continue
# 3. Green Check (Exact matches)
if any(w[i] != char for i, char in constraints['greens'].items()):
continue
# 4. Yellow Check
# Part A: The char must exist in the word
if any(char not in w for char in constraints['must_have']):
continue
# Part B: The char must NOT be at the yellow index
if any(w[i] == char for i, char in constraints['yellows']):
continue
candidates.append(w)
print(f"-> Found {len(candidates)} matches.")
if candidates:
preview = ', '.join(candidates[:5])
if len(candidates) > 5:
preview += "..."
print(f"-> Examples: {preview}")
all_slots_candidates.append(candidates)
return all_slots_candidates
def construct_guess_string(all_slots_candidates):
"""
Takes the lists of candidates and forms a 'word_word_word' string.
Strategically, it just picks the first valid candidate for now.
"""
chosen_words = []
for i, candidates in enumerate(all_slots_candidates):
if not candidates:
print(f"Warning: No solution found for word {i+1}!")
chosen_words.append("?????")
else:
# Simple strategy: Pick the first one
chosen_words.append(candidates[0])
return "_".join(chosen_words)
with remote("chall.polygl0ts.ch", 6052) as p:
structure = p.read()
# Run the function
lengths = get_word_lengths(structure.decode())
if lengths:
print(f"Word Lengths: {lengths}")
print(f"Total Letters: {sum(lengths)}")
word_dict = get_word_dict('word_list.txt')
solve = []
for l in lengths:
solve.append(word_dict[l][0])
guess = "_".join(solve)
p.sendline(guess.encode())
output = p.read()
print(output.decode())
# 1. Parse the structure
segments = parse_ansi_feedback(output)
# 2. Convert to constraints
all_constraints = [generate_constraints(seg) for seg in segments]
print(all_constraints)
# 3. Get Candidates
candidate_lists = get_candidates_for_slots('word_list.txt', all_constraints)
guess = construct_guess_string(candidate_lists)
print(f"Best guess {guess}")