picoCTF

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
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. It runs first
initialize(self) function no This function will be the second 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
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 `` 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")]

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 = ;
  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 `` 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.