> For the complete documentation index, see [llms.txt](https://sarpers-organization.gitbook.io/ctftricks/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://sarpers-organization.gitbook.io/ctftricks/_chapter-intro-1/interactive-solver/trick-0256.md).

# Automated Binary Search via SSH

***

Automating interactive SSH challenges often requires scripting the interaction. For challenges with numerical guessing and limits, a binary search algorithm is highly effective.

Using Python, a library like `paramiko` provides the necessary tools to connect to the SSH server and open an interactive shell. The script enters a loop: send a guess, read the server's response, parse the feedback ("Higher!", "Lower!", "Correct!"), and adjust the bounds for the next guess based on binary search principles. The correct guess for binary search is always the midpoint of the current search range `(min_guess + max_guess) // 2`.

To establish an SSH connection programmatically, `paramiko` is a robust choice. It allows you to manage the entire SSH process within your Python script. To implement an interactive shell over SSH using Paramiko, you need to use the `invoke_shell()` method. This method returns a Channel object that behaves much like a socket. You can send commands to the channel using `channel.send()` and read the output using `channel.recv()`. Reading from a shell can be tricky; you often need to loop and check `channel.recv_ready()` to ensure you capture all the output, as data may arrive in chunks.

```python
import paramiko
import time
import sys # Import sys for writing to stdout if needed for debugging

# --- Configuration ---
hostname = "example.picoctf.net" # Replace with target hostname
port = 60930 # Replace with target port
username = "user1" # Replace with target username
password = "password1" # Replace with target password
min_guess_range = 1 # Minimum possible value
max_guess_range = 1000 # Maximum possible value
# --- End Configuration ---

client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())

def send_guess(channel, guess):
    """Sends a guess to the SSH channel."""
    # Ensure guess is sent as a string followed by a newline
    channel.send(f"{guess}\n")

def read_channel(channel, timeout=1):
    """Reads data from the channel until no more data or timeout."""
    output = ""
    start_time = time.time()
    # Loop until timeout is reached
    while time.time() - start_time < timeout:
        # While there is data ready to be received on the channel
        while channel.recv_ready():
            # Read data in chunks and decode
            data = channel.recv(1024).decode('utf-8', errors='ignore')
            output += data
            # Optional: small sleep to yield execution, though recv_ready handles much of this
            # time.sleep(0.01)
        # If we've collected some output and there's no more data immediately ready,
        # we can assume the current response chunk is complete and break early.
        if output and not channel.recv_ready():
             break
        # Sleep briefly to prevent high CPU usage if waiting for data
        time.sleep(0.05) # Adjust sleep based on server responsiveness

    # After the timeout loop, perform one final check for any data
    while channel.recv_ready():
         output += channel.recv(1024).decode('utf-8', errors='ignore')

    return output

def analyze_feedback(output):
    """Analyzes server output for feedback relevant to the guessing game."""
    # Check for success conditions first
    if "Correct!" in output or "picoCTF" in output:
        return "success"
    # Check for termination conditions
    elif "You've exceeded the maximum number of guesses" in output:
        return "max_attempts"
    # Check for binary search feedback
    elif "Higher!" in output:
        return "higher"
    elif "Lower!" in output:
        return "lower"
    else:
        # Handle unexpected output or initial prompt
        # print(f"DEBUG: Unexpected output or prompt: {output}", file=sys.stderr) # Optional debug print
        return "unknown" # Indicate that the output didn't match expected patterns


try:
    client.connect(hostname, port, username, password)
    print(f"Successfully connected to {hostname}:{port}")

    # To interact with an SSH server like a terminal, you use invoke_shell().
    # This gives you a channel object.
    channel = client.invoke_shell()
    time.sleep(1) # Give shell time to start and show initial prompt/text

    # Read and print initial banner/prompt
    initial_output = read_channel(channel)
    print(initial_output, end="")

    min_guess = min_guess_range
    max_guess = max_guess_range
    attempts = 0
    success = False

    # Continue binary search as long as bounds are valid and success hasn't been achieved
    while min_guess <= max_guess:
        guess = (min_guess + max_guess) // 2 # Binary search midpoint
        print(f"Attempt {attempts + 1}: Guessing {guess} (Range: {min_guess}-{max_guess})")

        # Send the calculated guess to the shell
        send_guess(channel, guess)
        # Wait briefly for the server to process the input and generate a response
        time.sleep(0.5) # Adjust sleep based on server speed
        # Read the server's response from the channel
        response_output = read_channel(channel, timeout=2) # Use a longer timeout for response
        print(response_output, end="")

        # Analyze the response to determine the next step
        feedback = analyze_feedback(response_output)

        if feedback == "higher":
            min_guess = guess + 1 # Adjust lower bound
        elif feedback == "lower":
            max_guess = guess - 1 # Adjust upper bound
        elif feedback == "success":
            print(f"Success! Found the target number or flag.")
            success = True
            break # Exit loop on success
        elif feedback == "max_attempts":
            print("Failed: Exceeded maximum attempts.")
            break # Exit loop on failure
        elif feedback == "unknown":
             # If feedback is unknown, print a warning and potentially break
             print("Received unknown feedback. Aborting.", file=sys.stderr)
             break # Exit loop on unexpected output
        
        attempts += 1
        # Optional: Add a local attempt limit check if challenge doesn't enforce strictly
        # if attempts >= 10:
        #     print("Exceeded local attempt limit.")
        #     break

    if not success:
        print("Binary search finished without finding the target or flag within the range/attempts.")

finally:
    if client:
        client.close()
        print("SSH connection closed.")

```

Adapt the hostname, port, username, password, and the initial guess range (`min_guess_range`, `max_guess_range`) in the script configuration. The `read_channel` function and the `analyze_feedback` logic may need tuning based on the specific challenge's output format and timing. The `analyze_feedback` function includes checks for common challenge responses like "Higher!", "Lower!", "Correct!", exceeding guess limits, and the presence of a flag marker like "picoCTF".
