Last time we talked a lot about the WHY. This post is all about the HOW.
- If a task is done and no one marks it in a ticketing system, does it actually work?
- Let’s do high-level requirements first
- Planning the level database
- Envisioning the project structure
- Back on planet earth… 🌍
- Next time
If a task is done and no one marks it in a ticketing system, does it actually work?
Like I said last time, the first task was to start dumping tasks into a board; lo and behold. It’s still rather empty, but at least there’s something tangible to look at.
Let’s do high-level requirements first
Here is the main “User Story”. This is the first time I’m writing it down. Super helpful to talk about this out loud and write it down, it really makes ideas more concrete. So:
Here’s how a user will start playing the CTF
- They open CTF main page, which explains the rules and gives them a link to
- They log on to the server. behind the scenes, they get a new docker just for you using
- They clone the repo. behind the scenes, the local
gitserver is already initialized with all the hooks etc. and ready with their
- They read the
README, which directs them to checkout
start-hereand read it again.
Here’s the general idea for how a user plays a single level
- They read the
READMEto understand how to solve this level. There’s a web page link with hints, as well.
- They solve the level. For example, create two files at the root of the repo,
git commit. This is equivalent to the second level of the original CTF.
git pushis the confirmation. That’s how they say “I’m done”. How do we check the solutions?
- A master
pre-receivehook that checks what level they’re in and runs the appropriate solution checker (different for each level).
pre-receivewill pass all of it’s arguments to the solution checker and the solution checker return 0 or 1 on success/failure with a message. the pre-receive normally always fail (unless we want to allow push for a specific level).
- If the player didn’t win yet:
- Print an appropriate error to indicate what went wrong (like “too many commits” or “I’m missing file X”)
- If the player won:
- Print the flag(s) 🍾🏆
- A master
In very broad strokes, it seems like the CTF will have three main “moving parts”.
- The repo itself. It will contain the levels’ beginning state in their respective branches. There’s an assumption here that we’re playing against a single repo, but I couldn’t find any reason to play against multiples.
- The level database: hooks file, solution checkers, and level structure. This will be comprised of:
- Scripts that parse that data and create the required resources, such as the main hook file, testing suite, and a level browser web page.
- Build system. Build will be to a docker.
OK, so now that we know more precisely how the CTF will behave, we need to start to work on ONE of the components: Let’s move to the level DB first since it seems like the real “core” of the CTF. I’ll have to program around how this DB is built.
Planning the level database
What defines “Level”
- Title. Like “start”, “branching-1”, “merging-1”, etc. This is the human-readable version of the level, unlike…
- Branch. This is this level’s actual branch in the repo.
- Solution checker. This is an executable file. Most likely a
.shscript, but I’d like to keep any executable as an option.
- Flags. This is a List of branches this level unlocks.
- Level page. This is a
.mdfile that will be uploaded to the challenge site, which accompanies the level’s README in the repo. Mostly hints and flavour text.
- Tests. This describes how to win this level. As much as I can, I’d like for this to be something automatic.
Given this definition of “Level”, what’s the structure of the DB
First, we’ll have one
game_config.toml file that looks like this:
# Generic stuff generic = "stuff" # Dunno. Some server configs? [server] paths = "asdf" # Here's the interesting part [levels] [levels.start-1] title = "start-1" branch = "start-here" solutionChecker = "checkers/start-here.sh" flags = ["fizzling-vulture-pedial"] # Level page is implicitly "pages/start-1.md" # Testing info is implicitly in "tests/start-1.rs" [levels.merge-1] title = "merge-1" branch = "fizzling-vulture-pedial" solutionChecker = "checkers/merge-1.sh" flags = ["kneel-untinted-names", "upleaped-unprint-odorize"] # Level page is implicitly "pages/merge-1.md" # Testing info is implicitly in "tests/merge-1.rs"
Level DB folder structure
/levels /levels/game_config.toml /levels/checkers # executable per level /levels/pages # markdown per level /levels/tests # per level. Will start manually
Envisioning the project structure
I think this will be the semi-final directory structure and parts of the project, following the HLD from before:
/ /levels # See above. /scripts # This is standalone code. It might rely on the data in `levels`, but these will be separate scripts, that will be developed as independently as possible from the real data. /scripts/generate_level # CLI wizard to creates a new level /scripts/generate_graph # Generate the level browsing page. Should be a Markdown file /scripts/deploy_git_server # When running inside the docker, set up the repo and the hook /scripts/test_levels # When running inside the docker, run all the tests /build /build/package_for_docker # Takes the levels + scripts and packages them for the docker /build/dockerfile /build/dockerfile_entrypoint.sh
Back on planet earth… 🌍
I “wasted” some time on installations and reading.
Rust and started learning it, since I think it’ll make a good fit for all the
/scripts code that I need to write, and I’m growing a little tired of Python.
I also made Docker work on my WSL using this guide. I would have preferred to get WSL2 but I can’t join the Windows Insider Program on this PC, so I’ll have to wait patiently 😪
Next time I’m going to go in a totally different direction: Now that the plan feels solid and grounded, I’m going to work on getting just the first two levels done and working. This should be a pretty big task since I want a LOT of automation here, but once this infrastructure is laid down, adding more levels should be a walk in the park.