Challenge.py

Carolina Zarate edited this page Apr 22, 2016 · 8 revisions

The challenge.py script is responsible for problem generation and deployment. picoCTF challenges are all autogenerated, meaning that each instance of the problem will have a different flag or even a different solution. As a result, the challenge.py script provides a specification for how to generate the problem, rather than the problem itelf.

The picoCTF shell_manager will load this module and instantiate an instance of the Problem class specified within. Therefore, all challenge.py scripts are required to specify a class named Problem.

There two ways to specify your Problem class, as described below.

Simple API

If you are creating a simple challenge that only requires compiling some source files and setting up a flag, you can use our convenience wrapper as shown below. Note that this method can support remote challenges.

from hacksport.problem_templates import CompiledBinary
Problem = CompiledBinary(sources=["myproblem.c"], flag_file="key", aslr=True, remote=True)

The full python docstring for CompiledBinary is shown below.

Creates a challenge for a compiled binary. User must specify either a makefile
or compiler sources. If a makefile is specified, the binary name must also be
provided. If a flag_file is not provided, it will default to flag.txt. If the
given flag file does not exist, it will be created. If share_source is set to
true, all files specified in sources will be copied to the deployment
directory. If remote is set to true, the challenge will be assigned a port and
be wrapped in a server.

Keyword Args:
    makefile: The name of the makefile. Defualts to None.
    compiler: The compiler to be used. Defaults to gcc.
    sources: The list of source files. Defaults to [].
    binary_name: The name of the output binary. Defaults to the same name as sources[0].
    is_32_bit: Specifies if the output binary should be 32 bit. Only works nicely with gcc as the compiler.
               Defaults to True.
    executable_stack: Specifies if the output binary should have an executable stack. Only works nicely with gcc as the compiler.
                      Defaults to True.
    no_stack_protector: Specifies if the output binary should opt out of stack canaries. Only works nicely with gcc as the compiler.
                        Defaults to True.
    aslr: Specifies if the output binary should have aslr or not. Only used if the challenge is remote.
                        Defaults to False.
    compiler_flags: The list of any additional compiler flags to be passed. Defaults to [].
    flag_file: The name of the flag file. If it does not exist, it will be created. Defaults to flag.txt
    static_flag: A string containing the static flag. If specified, the flag generation will always return this. Defaults to None.
    remote: Specifies if the challenge should be remote or not. Defaults to False.

Inheritance API

If you need more customized problem generation, we offer a more controllable API for specifiying your Problem class. You can create a class that inherits from our problem base classes to create nearly any type of challenge. We will now explain each base class and the functionality it provides.

Challenge

Challenge is the simplest base class. All of the other classes extend Challenge with additional functionality. As a result, all Problem classes are instances of Challenge.

Challenge has some overridable options, specified below.

Field Type Required Descrtiption
initialize(self) function no This function will be the first to run, so any initial configuration should happen in here. Any variables that need to be templated into files must be set in this function (this process is described later).
setup(self) function yes, but not if using another base class This function will run after the other base classes do their internal setup
generate_flag(self, random) function no This function is responsible for generating the flag for an instance. The default implementation returns 32 random hex characters
files File list no This is a list of File objects that specify which files should be copied to the deployment directory

The Challenge class should only be directly extended if the none of the other base classes assist your goals. This is most common in Forensics challenges.

from hacksport.problem import Challenge, File

class Problem(Challenge):
    files = [File("core1.bin", 0o755), File("core1")]
    def setup(self):
        oldkey = b"replaceme"
        keyb = self.flag.encode('utf-8')

        with open("core", "rb") as f_in:
            with open("core1", "wb") as f_out:
                f_out.write(f_in.read().replace(oldkey, keyb))

Compiled

The Compiled class provides functionality for compiling some source files. You can either specify a makefile, or provide the compiler flags and sources manually.

It provides some overridable fields, outlined below.

Field Type Required Description
compiler string no The compiler to use. Defaults to "gcc"
compiler_flags string list no Flags to pass to the compiler. Defaults to []
compiler_sources string list if makefile is not specified The sources to compile.
makefile string if compiler_sources is not specified The name of the Makefile that will compile.
program_name string yes The name of the output file
from hacksport.problem import Compiled, ProtectedFile

class Problem(Compiled):
    program_name = "myproblem"
    compiler_sources = ["myproblem.c"]
    files = [ProtectedFile("key")]

Service

The Service class provides functionality for challenges that require a service. This is useful for remote exploitation challenges that are servers, or for challenges that need something to be running persistantly. Internally, we use systemd to maintain services, but you must only provide one field named start_cmd that is the command to run to start your service. Note that the current working directory during this command will be your problem's deployment directory, so relative paths will work.

from hacksport.problem import Service

class Problem(Service):
  def initialize(self):
      self.start_cmd = "socat tcp-listen:{},fork,reuseaddr EXEC:'echo {}'".format(self.port, self.flag)

Remote

The Remote class is a wrapper around Service that wraps programs that communicate over stdin and stdout to become remote services. It requires you to specify a field named program_name, which is the name of the executable file you wish to make remote.

This subclass also supports disabling ASLR. You can set the remove_aslr field to True to have the remote executable run without ASLR.

from hacksport.problem import Remote, ProtectedFile

class Problem(Remote):
    program_name = "myproblem.py"
    remove_aslr = True # this makes more sense for binary exploitation challenges
    files = [ProtectedFile("flag")]

FlaskApp

FlaskApp is a wrapper around Service that will run a python Flask application. You should specify your Flask app in a file named server.py and name it app. You can also specify num_workers, which will default to 1.

A field named flask_secret will be created for each instance. If you use multiple workers, it is important that you use the same flask secret in each, so we recommend templating {{flask_secret}} into your flask app.

from hacksport.problem import FlaskApp, File, files_from_directory

class Problem(FlaskApp):

    files = [File("database.db", 0o660)]
    files.extend(files_from_directory("templates/"))

PHPApp

PHPApp is a wrapper around Service that will start a server to run PHP or serve files. It takes an optional field php_root that defaults to "", which specifies the relative path to the root of your files. You can also specify num_workers.

from hacksport.problem import PHPApp, File

class Problem(PHPApp):

  files = [File("index.php")]

Forensics

The Forensics class provides functionality for digital forensics challenges. These types of challenges will involve one file or more being served to the user, with the flag being located in some form within the files.

The Forensics class provides a single overrideable field, forensics_files, for distinguishing the forensics files that will be served to the competitor.

Field Type Required Description
forensics_files File list yes The list of files that will be served to the competitor.

Note that the Forensics class shouldn't really be used by itself. Rather, just about any digital forensics challenge could fall into one of the two child classes of Forensics: Realtime and Pregen, and should be written as such.

Realtime

The Realtime class is a child class of the Forensics class. It provides functionality to forensics challenges that can be generated and served to the competitor in real-time (ie. little processing power and time).

The Realtime class has several overrideable components. Some are included from the Problem or Forensics classes that might be commonly overriden for Realtime challenges.

Field Type Required Description
generate_challenge Function yes Defines how a forensics challenge file will be generated. Returns a list of File objects to be served to the competitors.
generate_flag Function no Generates a flag. Create if you want a flag other than the default.

Note that overriding the initialize function might cause issues as it checks & stores the generated challenge files.

We also suggest that a solver.py script should be provided. The solver.py file should take a challenge file and determine the correct flag. This is a handy tool for the challenge runners to have in case there are issues with challenges that need to be resolved quickly.

Pregen

The Pregen class is a child class of the Forensics class. It provides functionality to forensics challenges that require extra time and/or power to generate, and thus must be pregenerated. The serving of these files typically will be a simple random picking of challenge files and the corresponding answers.

The Realtime class has several overrideable components. Some are included from the Problem or Forensics classes that might be commonly overriden for Realtime challenges.

Field Type Required Description
challenge_id String yes The 'challenge id' that identifies what challenge files and answer are to be used for a challenge.
pick_challenge_id Function yes Generates a 'challenge id' specific to that challenge that determines which challenge files and answer should be used.
get_ans Function yes Obtains the challenge answer given the 'challenge id'.
get_challenge_files yes Obtains the challenge files given the 'challenge id'.

Note that overriding the generate_flag function might cause issues as it is setup to use the pick_challenge_id to find the corresponding answer, which will be the flag.

We also suggest that a generator.py script and any other files necessary for pregenerating the challenges are included. This is so that the challenge runners and anyone else can generate more challenges if need-be or resolve issues that come up.

Usage Tips

Below are some tips for using the Inheritance API effectively.

Implicit Fields

By the time that your initialize function runs, many useful fields have been implicitly set for you. These are accessible at self.field_name_here.

Field Type Description
random Random A uniquely seeded random object for this instance
user string The username of the unix account created for this instance
default_user string The username of the unix account that will own files by default. This is configurable in the shell_manager's config.py
server string The externally-accesible hostname of this shell server. This is configurable in the shell_manager's config.py
directory string The deployment directory. This is where your files will be copied to.
url_for(source_name, display=None, raw=False, pre_templated=False) function Takes a filename as a string, and returns a link to download that filename. This is most useful for templating descriptions. display will override the text of the link. raw=True will return a raw URL instead of a link. pre_templated=True will serve the file in its "pre-templated" form, provided that PreTemplatedFile(path) is in your files list (see File class docs for more information).

Templating

Using the Inheritance API, you can easily template your problems by specifying fields in your class. The name of this field will be available to template into your description and any of your files. The templating happens automatically.

For example, consider the following challenge.py and myproblem.c. secret_number will be templated into the source code before compiling.

from hacksport.problem import Compiled, ProtectedFile

class Problem(Compiled):
    program_name = "myproblem"
    compiler_sources = ["myproblem.c"]
    files = [ProtectedFile("key")]

    def initialize(self):
        self.secret_number = self.random.randint(0,100)
#include <stdlib.h>
#include <stdio.h>

int main(int argc, char **argv){
  int secret = {{secret_number}};
  int guess;

  while(1){
    puts("What is the secret number?");
    scanf("%d", &guess);
    if (guess == secret){
      puts("correct!");
      system("cat key");
      return 0;
    }
  }
}

Downloads

The above templating mechanism also allows you to generate URLs to file downloads. For example, if I template {{url_for('myproblem')}} into my description, it will be replaced with a url to download the compiled binary file that the challenge.py generates.

Databases for Web Problems

For challenges that need databases (i.e. SQL injection challenges), we recommend using SQLite databases. The reasoning is that SQLite databases are just files, so they are inherently portable. This is useful for autogeneration, as each instance can have its own SQLite database file in its directory.

One additional step must be taken when using SQLite with the shell_manager if the database needs to be writable. You must execute the query PRAGMA journal_mode = MEMORY before attempting to write (via an UPDATE, INSERT, or any other command that modifies the database file). This is because SQLite databases try to create temporary journal files in the same directory as the database file, but the problem user does not have permission to create files here. By executing that query, SQLite is told to store the journals in memory rather than on disk, preventing the "Permission Denied" error.

Python Template

We recommend you use the following template for using a SQLite database in python. This is useful for a FlaskApp challenge or for a database initialzation script written in python.

import sqlite3

DB_FILE = "messages.db"

db = _db = sqlite3.connect(DB_FILE)
# let the databse be writable without the directory being writable
db.cursor().execute("PRAGMA journal_mode = MEMORY")
db.commit()

Using db throughout your code will now use the writable database connection as desired.

PHP Template

<?php
$DB_FILE = "messages.db";
$db = new SQLite3($DB_FILE);
$db->exec("PRAGMA journal_mode = MEMORY");
?>

Using $db throughout your code will now use the writable database connection as desired.

You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.
Press h to open a hovercard with more details.