Skip to content

SECURE_CONNECTION//PRESS[CTRL+J]FOR ROOT ACCESS

BACK TO INTEL
MiscMedium

Pirate Race Programming

CTF writeup for Pirate Race Programming from heroCTF

//Pirate Race - Challenge Writeup

>Challenge Overview

Category: Programming / PVE

Goal: Win the "Pirate Race" by programming a bot to control a ship. The objective is to score points by validating islands (passing them on a specific side) and collecting rum barrels.

Win Condition: To beat the level and get the flag, you must win at least 8 out of 10 games in a batch submission against the "Player 2" bot.

>Solution Strategy

The core of the solution is a robust navigation algorithm implemented in bot.py. The strategy focuses on efficiency and safety:

  1.  Smart Target Selection:

    *   The bot calculates the distance to all unvalidated islands.

    *   It prioritizes the nearest unvalidated island to minimize travel time.

    *   Once all islands are validated, it switches to collecting the nearest rum barrels to maximize the score.

  1.  Precise Validation Points:

    *   For each island type (1-4), the bot calculates a specific "validation point" 30 units away from the island center in the required direction (e.g., East for Type 1).

    *   This ensures the ship passes on the correct side to validate the island.

  1.  Waypoint Navigation & Collision Avoidance:

    *   The Problem: Simply steering towards the validation point can lead to collisions if the island is in the way (e.g., trying to reach the East side of an island while currently being on the West side).

    *   The Solution: The bot checks if the direct path to the validation point is blocked by the target island itself.

    *   If blocked, it generates an intermediate waypoint 60 units perpendicular to the island's axis. This forces the ship to steer around the island first, before heading to the validation point.

    *   This simple "go around" logic prevents the bot from getting stuck or crashing.

  1.  Stuck Detection:

    *   The bot monitors its speed. If the speed drops below a threshold (indicating a collision or getting stuck), it triggers an escape maneuver to break free.

>Implementation Details

1. The Bot Logic (bot.py)

This script contains the make_move function called by the game engine. It parses the game state, applies the strategy, and returns the acceleration and angle.

python

import math

  

def get_distance(p1, p2):

    return math.sqrt((p1['x'] - p2['x'])**2 + (p1['y'] - p2['y'])**2)

  

def make_move(game_state):

    islands = game_state['islands']

    barrels = game_state['barrels']

    ship = game_state['your_ship']

    ship_pos = ship['position']

    ship_angle = ship['angle']

    # 1. Select Target

    target_pos = None

    target_type = "island"

    # Prioritize unvalidated islands

    unvalidated = [i for i in islands if not i['validated']]

    if unvalidated:

        # Find nearest unvalidated island

        nearest = min(unvalidated, key=lambda i: get_distance(ship_pos, i['position']))

        # Calculate validation point based on island type

        # Type 1: East, Type 2: South, Type 3: West, Type 4: North

        offset = 30

        if nearest['type'] == 1:

            target_pos = {'x': nearest['position']['x'] + offset, 'y': nearest['position']['y']}

        elif nearest['type'] == 2:

            target_pos = {'x': nearest['position']['x'], 'y': nearest['position']['y'] + offset}

        elif nearest['type'] == 3:

            target_pos = {'x': nearest['position']['x'] - offset, 'y': nearest['position']['y']}

        elif nearest['type'] == 4:

            target_pos = {'x': nearest['position']['x'], 'y': nearest['position']['y'] - offset}

        # Waypoint Navigation: Check if we need to go around

        # If we are on the "wrong" side of the island relative to the target, add a waypoint

        dx = target_pos['x'] - ship_pos['x']

        dy = target_pos['y'] - ship_pos['y']

        dist_to_target = math.sqrt(dx*dx + dy*dy)

        # Check if the island center is close to the line of sight

        # Simple heuristic: if the island is between us and the target

        ix = nearest['position']['x']

        iy = nearest['position']['y']

        dist_to_island = get_distance(ship_pos, nearest['position'])

        if dist_to_island < dist_to_target and dist_to_island > 10: # If island is closer than target

             # Check alignment. If we are blocked, offset target perpendicular to the path

             # This is a simplified "go around" logic

             pass

  

        # Improved Waypoint Logic:

        # If target is East (Type 1) but we are West of the island, go North-ish or South-ish

        if nearest['type'] == 1 and ship_pos['x'] < nearest['position']['x']:

             target_pos = {'x': nearest['position']['x'], 'y': nearest['position']['y'] - 60} # Go North-ish

        elif nearest['type'] == 3 and ship_pos['x'] > nearest['position']['x']:

             target_pos = {'x': nearest['position']['x'], 'y': nearest['position']['y'] + 60} # Go South-ish

        elif nearest['type'] == 2 and ship_pos['y'] < nearest['position']['y']:

             target_pos = {'x': nearest['position']['x'] + 60, 'y': nearest['position']['y']} # Go East-ish

        elif nearest['type'] == 4 and ship_pos['y'] > nearest['position']['y']:

             target_pos = {'x': nearest['position']['x'] - 60, 'y': nearest['position']['y']} # Go West-ish

  

    else:

        # All islands validated, go for barrels

        available_barrels = [b for b in barrels if not b['collected']]

        if available_barrels:

            nearest = min(available_barrels, key=lambda b: get_distance(ship_pos, b['position']))

            target_pos = nearest['position']

            target_type = "barrel"

        else:

            # Nothing to do, just circle center

            target_pos = {'x': 500, 'y': 500}

  

    # 2. Calculate Steering

    if target_pos:

        dx = target_pos['x'] - ship_pos['x']

        dy = target_pos['y'] - ship_pos['y']

        target_angle = math.degrees(math.atan2(dx, -dy)) % 360

        diff = (target_angle - ship_angle + 180) % 360 - 180

        # Simple P-controller for angle

        if abs(diff) > 10:

            angle = ship_angle + diff # Turn towards target

        else:

            angle = target_angle

        acceleration = 100 # Full speed ahead

        # Stuck detection (simple)

        speed = math.sqrt(ship['velocity']['x']**2 + ship['velocity']['y']**2)

        if speed < 2:

            acceleration = 100

            angle = (ship_angle + 90) % 360 # Try to turn out

        return {

            "acceleration": acceleration,

            "angle": angle,

            "data": f"Target: {target_type}"

        }

    return {

        "acceleration": 0,

        "angle": ship_angle,

        "data": "Idle"

    }

2. The Solver Script (solve.py)

This script handles the API interactions. It authenticates, submits the bot.py code, and triggers a batch execution (is_test=False).

python

import requests

import json

import time

  

BASE_URL = "https://pirate.heroctf.fr/api/v1"

TOKEN = "ctfd_127ad7f58d9cb396740af334473dfa18ddce6c414894f3f84a675b62e49a68ef"

  

def get_latest_game_uuid(session, is_batch=False):

    r = session.get(f"{BASE_URL}/games")

    if r.status_code == 200:

        games = r.json().get("games", [])

        if not games: return None

        # Filter for batch or single game

        target_type = 'batch' if is_batch else 'single_game'

        for g in games:

            if g.get('type') == target_type:

                return g['uuid']

    return None

  

def main():

    session = requests.Session()

    session.headers.update({"Authorization": f"Bearer {TOKEN}"})

    # 1. Read bot code

    with open("bot.py", "r") as f:

        code = f.read()

    # 2. Submit code

    # Set is_test=False to trigger a batch of 10 games for the flag

    payload = {

        "code": code,

        "is_test": False

    }

    print("[*] Submitting bot code (Batch Mode)...")

    r = session.post(f"{BASE_URL}/queue/games", json=payload)

    print(f"[*] Status: {r.status_code}")

    # 3. Poll for the new batch UUID

    print("[*] Polling for new batch UUID...")

    initial_uuid = get_latest_game_uuid(session, is_batch=True)

    for _ in range(30):

        new_uuid = get_latest_game_uuid(session, is_batch=True)

        if new_uuid and new_uuid != initial_uuid:

            print(f"[+] New Batch UUID found: {new_uuid}")

            with open("last_batch_uuid.txt", "w") as f:

                f.write(new_uuid)

            print("[*] Run poll_batch.py to monitor progress.")

            return

        time.sleep(2)

    print("[-] No new batch found.")

  

if __name__ == "__main__":

    main()

3. The Monitor Script (poll_batch.py)

This script monitors the progress of the batch submission. It checks how many games have been processed and how many have been won.

python

import requests

import time

import json

import sys

  

TOKEN = "ctfd_127ad7f58d9cb396740af334473dfa18ddce6c414894f3f84a675b62e49a68ef"

URL = "https://pirate.heroctf.fr/api/v1/games"

  

try:

    with open("last_batch_uuid.txt", "r") as f:

        BATCH_UUID = f.read().strip()

except FileNotFoundError:

    print("[-] last_batch_uuid.txt not found.")

    sys.exit(1)

  

headers = {"Authorization": f"Bearer {TOKEN}"}

  

print(f"[*] Monitoring Batch: {BATCH_UUID}")

  

while True:

    try:

        r = requests.get(URL, headers=headers)

        if r.status_code == 200:

            games = r.json().get("games", [])

            for g in games:

                if g["uuid"] == BATCH_UUID:

                    processed = g.get("number_of_processed_games", 0)

                    won = g.get("number_of_won_games", 0)

                    print(f"[*] Status: Processed={processed}/10, Won={won}")

                    if processed == 10:

                        print(f"[+] Batch Complete! Total Wins: {won}/10")

                        if won >= 8:

                            print("[+] SUCCESS! You should have the flag (check level).")

                        else:

                            print("[-] Failed to reach 8 wins.")

                        sys.exit(0)

                    break

        time.sleep(5)

    except Exception as e:

        print(f"[-] Error: {e}")

        time.sleep(5)

>Execution & Results

  1.  Setup: Ensure bot.py, solve.py, and poll_batch.py are in the same directory.

  2.  Submit: Run python3 solve.py. This submits the bot and saves the new batch UUID to last_batch_uuid.txt.

  3.  Monitor: Run python3 poll_batch.py. Watch the progress.

Result:

The bot successfully achieved 8/10 wins in the batch df30c1a2-ccfe-11f0-98c0-7a71d61ca89c.

Upon completion, the user profile (/api/v1/auth/me) updated the level to "PVP", confirming the challenge was solved and the flag was automatically submitted.