Add problem format.
This commit is contained in:
parent
de13b172c8
commit
e25a504957
1 changed files with 111 additions and 2 deletions
113
README.md
113
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...
|
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.
|
Loading…
Reference in a new issue