diff --git a/README.md b/README.md index ede7499..8241ca3 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,112 @@ -# EasyCTF 2016 +OpenCTF Problem Import via Github +------------------------------------------------- -Problems folder. Make a folder with your problem in it. Format coming soon... \ No newline at end of file +Each problem belongs in its own folder under the root folder of the repository. The name of the folder will become a readable identifier for the problem, so it should only contain letters, numbers, and `-`. + +Inside this folder, there are 3 required files: + +- `problem.yml`: This is probably the most important file. It contains all the metadata for the problem, including: + - `title`: The title of the problem. + - `category`: The category of the problem. + - `value`: An integer value for the problem. + - `hint`: A hint for the problem. (no markdown supported) + - `autogen`: (`true`/`false`) whether or not the problem is autogenerated. If you put `true`, read on to see what needs to be added to the grader. +- `description.md`: A problem description. As the extension suggests, you may use markdown in this file. +- `grader.py`: A python file containing several functions that relate to grading the problem (and generating, if autogen is enabled). + +The Grader +-------------- + +At the most basic level, the grader should contain a `grade` function that takes in two parameters (you can call them whatever you like): (1) an instance of the random library used for autogenerating problems, and (2) the flag that the user actually typed into the input box. It should return a tuple `(correct, message)`, where `correct` is a boolean value signifying whether the user got the problem right, and `message`, a custom message to be displayed. + +An example of a basic grader looks like this: + +```python +def grade(random, key): + if key.find("this_is_the_flag") != -1: + return True, "Correct!" + return False, "Nope." +``` + +If autogen is not enabled, please do not use `random` while judging the key. It's meant for checking problems that were autogenerated with the same seed. Also, note that I'm using `key.find` rather than comparing them with equals. This is useful if you want to match both `flag{this_is_the_flag}` and `this_is_the_flag`, or if you are trying to perform a case-insensitive match. + +If autogen were enabled, you would need to add an additional function to the grader, called `generate`. Essentially, this function is required for generating the problem. It takes in a single parameter, the same instance of `random` as the `grade` function gets, and must return a dictionary of values (obviously, this dictionary can be blank, but ideally it shouldn't be). + +Possible keys that you can include in your dictionary are: +- `variables`: This should be another dictionary of variables that are inserted into the description during runtime. More details on how these variables are inserted comes in a later section. You would probably want to use the `random` variable that is passed to the function to generate a value that is different per team. +- `files`: This is similar to `variables` in that it is a dictionary of keys that are inserted into the description. However, instead of returning variables directly, you should return a function that takes in the `random` variable and returns a File object (could be StringIO as well). Files generated this way are stored into the static container during runtime. + +An example using both `variables` and `files` is: + +```python +from cStringIO import StringIO +def gen_b(random): + b = random.choice(range(50, 100)) + return b +def generate(random): + a = random.choice(range(50, 100)) + return dict(variables={ + "a": a + }, files={ + "b.txt": gen_b + }) +``` + +Note that we are returning `gen_b` without `()`, since we want to return the function definition, rather than a value returned by the function. This way, the platform can call this function at runtime, generating a different problem for every user. + +**This part is very important**. Filenames usually have extensions, like `ciphertext.txt`. In this case, we are passing the filename into the description as a string. But Python template strings don't allow symbols such as periods. Therefore, ALL FUNCTION NAMES ARE SANITIZED by replacing anything matching this regex: `[^a-zA-Z]+` with `_` (underscore). This way, `ciphertext.txt` becomes `ciphertext_txt` in the description. Here is a description that uses the above generator. + +```markdown +Help me decipher [this](${b_txt}). +``` + +Here is a full example using autogen: + +problem.yml: + +```yaml +title: Caesar Cipher +value: 20 +author: mzhang +autogen: true +``` + +description.md: + +```markdown +Help me decipher [this ciphertext](${ciphertext_txt}). +``` + +grader.py: + +```python +from cStringIO import StringIO +from string import maketrans + +flag = "caesar_cipher_is_fun!" +alphabet = "abcdefghijklmnopqrstuvwxyz" + +def get_problem(random): + n = random.randint(1, 25) + salt = "".join([random.choice("0123456789abcdef") for i in range(6)]) + return (n, salt) + +def generate_ciphertext(random): + n, salt = get_problem(random) + trans = maketrans(alphabet, alphabet[n:] + alphabet[:n]) + ciphertext = ("easyctf{%s_%s}" % (flag, salt)).translate(trans) + return StringIO(ciphertext) + +def generate(random): + return dict(files={ + "ciphertext.txt": generate_ciphertext + }) + +def grade(random, key): + n, salt = get_problem(random) + if key.find("%s_%s" % (flag, salt)) >= 0: + return True, "Correct!" + return False, "Nope." +``` + +In this way, not only is the shift randomly generated, so is the actual flag itself. Note that I use a helper function, `get_random` to actually generate the numbers to ensure that the numbers generated are the same for both the generation and the grading. \ No newline at end of file