SunshineCTF 2025 Writeup - Hail Mary

The challenge required us to find a 10-float "genetic code" that maximizes a hidden "survival rating" function to an average of 95.0% within 100 generations. This is solved using a Genetic Algorithm, an optimization technique that mimics natural selection.

SunshineCTF 2025 Writeup - Hail Mary

Challenge:

1. The Setup: Connection and Parameters

This section initializes the environment and defines the rules of the evolutionary simulation.

Code Snippet: Parameters

HOST = "chal.sunshinectf.games"
PORT = 25201
TARGET_AVG_SURVIVAL = 95.0
POPULATION_SIZE = 100
GENE_LENGTH = 10 

# Tuned GA Parameters
ELITISM_RATE = 0.3      # What: 30% of best individuals survive directly.
CROSSOVER_RATE = 0.85   # What: 85% chance of parents mixing genes.
MUTATION_RATE = 0.2     # What: 20% chance any single gene mutates.
MUTATION_STRENGTH = 0.01 # What: Max delta of 0.01 for a mutation.

Rationale (The Why)

  • Tuning for 95%: Since the previous run stopped right below 95.0% (94.88%), the system was close but got stuck in a local optimum.
    • High Elitism (0.3): This is crucial. By preserving 30 of the best individuals, we ensure the genetic gains made in previous generations are not lost. This acts as strong selective pressure.
    • Low Mutation Strength (0.01): A small strength prevents successful 94%+ genes from randomly flipping to a low score. It ensures that mutation is used for fine-tuning and exploring the immediate vicinity of the current high-scoring solution, which is necessary for the final 0.12% gain.
    • High Mutation Rate (0.2): Even though the strength is low, the frequency is high. This ensures continuous, tiny adjustments across the entire population, preventing stagnation.

2. Network Communication: The Crucial Fix

The primary challenge was correctly reading the server's response, which was a JSON object immediately followed by an unneeded newline or prompt text (the "Extra data" error).

Code Snippet: Fixed Parsing

# In get_fitness_scores:
# ... (send JSON payload) ...

raw_response = read_server_data(sock) # Reads all available data

# 3. ROBUST PARSING FIX: Isolate the JSON object
json_start = raw_response.find('{')
json_end = raw_response.rfind('}') # Finds the LAST closing brace

if json_start == -1 or json_end == -1 or json_end < json_start:
    # Error handling or flag check
    ...

# Take the data from the first '{' to the last '}' (inclusive)
json_str = raw_response[json_start : json_end + 1]

# Parse the clean JSON string
data = json.loads(json_str)

# Extract scores (and convert from 0-1.0 to 0-100%)
survival_rates = [s * 100 for s in data.get("scores", [])]
avg_survival = data.get("average", 0.0) * 100

Rationale (The Why)

  • Problem: The server was sending data like {"generation": 100, ...}JUNK which caused the standard json.loads() to fail because of the trailing JUNK (the "Extra data").
  • Solution: We use string manipulation to surgically extract the valid JSON. We find the index of the first { and the index of the last }. By slicing the raw response between these two points (raw_response[json_start : json_end + 1]), we effectively discard both the initial prompt text and any trailing junk data, leaving a perfectly parsable JSON string.

3. Evolutionary Functions: Core GA Logic

These are the functions that drive the optimization process, constantly refining the population based on the server's feedback (fitness score).

A. initialize_population()

  • What: Creates the starting pool of 100 genes, each with 10 random floats between 0.0 and 1.0.
  • Why: A random starting point ensures the search begins broadly, maximizing the chance of finding the initial high-fitness regions of the unknown survival function.

B. select_parents(population_fitness, num_parents)

  • What: Uses a form of Roulette Wheel Selection where parents are chosen randomly, but the probability of selection is weighted by their survival rate.
  • Why: This embodies the "survival of the fittest" principle. Individuals with higher survival rates (better genes) are more likely to pass on their traits to the next generation.

C. crossover(parent1, parent2)

  • What: Implements Single-Point Crossover. Two parents are chosen, a random point (index) is selected, and their genetic codes are swapped from that point onward to create two new offspring.
  • Why: Crossover allows the algorithm to combine the strengths of two good solutions. If Parent A has a great first half and Parent B has a great second half, crossover might create an offspring with the optimal full sequence.

D. mutate(gene)

  • What: Randomly adjusts a gene's float value by a small, random amount, limited by MUTATION_STRENGTH.
  • Why: Mutation introduces new genetic material and prevents the population from becoming entirely uniform and getting stuck. The low strength (0.01) is key here for fine-tuning the high-scoring genes.

4. The Main Loop: Evolution

The solve_challenge() function ties everything together in a loop that runs up to 100 times.

  1. Submission: The current population is formatted as JSON and submitted to the server.
  2. Evaluation: The server returns the scores (fitness) for all 100 taumoeba.
  3. Termination: The script checks if the avg_survival is ≥95.0%. If yes, it prints the success message and reads the final response (the flag).
  4. Sorting: The population is sorted by fitness (descending).
  5. New Generation Construction:
    • Elitism: The top 30% of the population are copied directly to the new generation.
    • Reproduction: The remaining 70% are created by repeatedly selecting parents, performing crossover, and applying mutation until the population size is back to 100.

This loop continuously refines the gene pool, iteratively improving the average survival rate until the optimization goal is met within the 100 generation limit.

Attack Successful