GoogleCTF 2025

Writeups for all misc challenges and soft release of my new AI toolbox project (autonomous CTF solver)

Misc

メガA (110 solves)

Description:

Join the 16-bit revolution! Can you help Sonk the Rabbit find all flags? Submit your recording to get the flag: python3 submit.py solution.inp mega.2025.ctfcompetition.com 1337

Note: Submit the flag starting with "CTF{A:" here. For submitting the other flag, see the challenge titled "メガB".

Note 2: To make the メガ challenges less annoying for those who already solved it locally, the ability to get the flag from the server may depend on mame version you installed. This wasn't obvious and we apologise. The version we run at the server is 0.277+dfsg.1-0ubuntu1~ppa3~noble1 and it comes from ppa:c.falco/mame.

Resources:

Static resources:chall

Solution:

Unzipping the challenge, we see a misc-mega-rust-1 folder with a bunch of rust game files and a Sega Mega Drive ROM image (sonk.md). Below is the README.md for convenience.

# Mega Sonk in MegaRust

To run the game:

```
mame genesis -cart sonk.md
```

Record your solution and send it up to the server to get the flag! Make sure your recording is at most 5 minutes long.

```
mame genesis -cart sonk.md -record solution.inp
python3 submit.py /home/user/.mame/inp/solution.inp mega.2025.ctfcompetition.com 1337
```

To rebuild the game:

```
make sonk.md
```

On the first run this builds the compilation toolchain which could take up to half an hour.

Rebuild and run the game:

```
 make run
```


## Credits

Compilation toolchain based on
* https://github.com/ricky26/rust-mega-drive
* https://github.com/rust-lang/rustc_codegen_gcc

Graphics credits:
* https://opengameart.org/content/dashie-supertux-advance-style
* https://opengameart.org/content/plastic-shamtastic
* https://opengameart.org/content/wasp-0

Running the game following the instructions, we can quickly explore the entire map and see the two flags.

Flag A
Flag B

We can't walk into the spikes for flag A and we can't jump high enough to hit flag b. The only enemies are wasps which knock us back if we hit them but we don't take damage (similar to hitting spikes). We can build up a lot of speed and jump pretty far when jumping off a hill. Finally, we can go off screen slightly on the left and right.

Early theories were that we could do something off screen or building up speed and hitting hills/enemies/spikes could cause glitches. We also thought maybe the flag collision area might shift over time. We are given all the tools needed to rebuild the game so we can modify it to give us some debugging information.

Here are all the mods I added in the game.rs draw function (right after the first if statement):

This allows us to see everything that could be significant for the challenge. We can see absolute X position (a vertical line every 500 pixels), x/y position of collision boxes for the flags and the player (rendered with a vertical and horizontal line), an x/y position indicator (with flags to represent digits because numbers can't be rendered on the map and there's no terminal), a speed indicator, and a few other things. The below two images show some examples.

X/Y relative position indicators (left), horizontal position of player (green) horizontal position of flag (white), vertical marker for 500 feet (white)
Checking flag position (left vertical line) when we're hit off screen

These mods let us learn a few interesting things like when we're off screen, we quickly hit an invisible wall, but we still build up "speed" when we move against it. Also when we're hit by a wasp off screen downhill, our y position stays at the last position on screen until we move. We also can see if the flag collision our or collision box ever gets moved somehow.

This represents all the information that we can learn by rebuilding the game. Does any of this help? Sadly no.

Flag A is very simple, we just need to be hit offscreen into the spikes. There's a wasp suspiciously close to the right of the spikes with the flag, if we move ourself to the left of the screen and let ourselves get hit, we can go through the spikes that haven't spawned and get it. 🤷‍♂️

メガB (87 solves)

Flag B ended up being nothing related to being off screen, velocity, collision boxes, etc., but rather the spawn mechanism of the wasp.

When we first see the wasp spawn coordinate on screen, it will spawn at that time, but it doesn't despawn any wasps that were already existing, meaning we can walk back and forth with the right side of the window right at the spawn point to spawn multiple wasps at the same time. This is the only obvious mechanism that can be abused, and after a period of time trying to spawn as many as possible, we'll see that if the right side of the screen is exactly on the spawn point, it'll constantly spawn wasps until the game breaks.

Player is now above Flag B, can see their feet when moving side to side

The player will conveniently glitch up to the top of the screen when there are too many wasps. Then, just walking back and forth will eventually collect the flag. Following the instructions, we can generate recordings (needs to be the unmodded game) to submit both flags. A lot of "unnecessary" game mods were done but this helps through process of elimination to narrow down where to look for the solution.

🩸BPFBOX (53 solves)

Description:

I heard people can write LSMs using BPF, so I built one.

bpfbox.2025.ctfcompetition.com 1337

Resources:

Static resources:chall

Solution:

Unzipping the challenge, we see the following docker image.

Basically, connecting to this challenge drops us into a shell where a BPF probe seems to kill any process that tries to read /flag.txt. The proof of work script is tuned to take a long time (at least 30 seconds to a minute on my machine) and it kicks you out after a minute, so building the challenge with the Dockerfile is probably required to reliably solve it. However I got lucky and solved it in a few minutes so I didn't have to. 😅

Testing random commands to see what happens, our command gets echo'd back to us if we try to directly access flag.txt. Thinking maybe the BPF probe only checks if the process is trying to access flag.txt as soon as it's created, I added a sleep before it and it worked.

Fishmaze (30 solves)

Description:

Escape the maze.

Solution:

There's no sources given, we just have the html on the launched instance.

Launched instance, we can write player kernel and if it passes a syntax check, see the run

The html code doesn't help us too much aside from the hint at the end. It basically renders our fish moving based on the player_kernel, which runs every tick. Whatever we do seems to just be repeated each time, so if we have it move one direction, we keep moving in that direction. We need to either be able to choose decision based on nearby objects (very hard/impossible to leave the maze on a generic ruleset) or figure out how to use the AUX_DATA as memory to store a sequence of memories.

The problem is random compilations either succeed or don't succeed and there's no debugging info provided on why compilations don't succeed. To solve this, we can use asserts to learn more about the system. The following script is used to determine what mapdata_ref actually is.

This does a binary search for each character of the type, so sees if it's upper/lower/digit/symbol, then narrows down on what it is with asserts one at a time. A failed compile means it's not that. Running this we find it is Traced<int32[]>with<DynamicJaxprTrace> and looking into that, it is JAX. Knowing this, we can build this simple proof of concept to change directions based on tick.

From here, it's simple iteration to build up the full solution. Count out the amount to move, run it, make adjustments, wait to avoid the enemies, and we win.

HWSIMv2 (27 solves)

Description:

Last year we were notified that our circuitry was backdoored, so now we formally verify our designs before they get sent to the factory. Implement an instruction decoder for our next-gen CPU!

hwsim.2025.ctfcompetition.com 1337

Resources:

Static resources:chall

Solution:

Unzipping the challenge, we see the following challenge.

Basically we need to build the CPU and then run instructions to print the flag. A teammate (quasar) wrote the CPU so I don't fully understand that part, but after using AI to build a solve script, we have the final solution here.

Basically we automate solving the pow and then paste in the instructions, after 5 it will either verify or not. Then we send a payload to dump it. The following is the explanation of all this written by AI.

1. What the verifier is supposed to check

For every possible assignment of the 32 “basic-input” bits

the solver proves that all required outputs satisfy the official formulas, the most important of which is the security check:

If any model violates any equality, Z3 finds a counter-example and the menu prints “Formal verification failed!”.


2. Two constants are enough to break the proof

We add five tiny gates (the first five lines of the new working2.txt):

  • security_exception is a hard-wired 0.

  • is_root_now is a hard-wired 1.

Now, for every possible input valuation,

so the equality holds identically. The solver happily reports

All the other outputs (is_load, is_store, …) are implemented exactly as the organisers’ reference circuit, so their equalities hold too.

Key bug: The challenge treats a wire name that appears in basic_inputs and in the gate list as one single Boolean variable. By giving it a gate definition we override the would-be input instead of adding a constraint between two distinct signals.


3. Why it works at run-time even in user mode

During emulation the dispatcher calls

known initially contains only the real run-time value of is_root_now ( True in kernel mode, False otherwise ). But because security_exception is not in that dict, the recursive NAND evaluation walks the circuit and discovers that it is the constant 0, never consulting is_root_now at all.

So:

  • user code can execute load / store to any address, or call putc, without ever tripping the check;

  • we do not need to stay in kernel mode or ret-leaf from a syscall.


4. The 105-byte flag-dump program

Each repetition does

for xx = 0x80 … 0x99, followed by 0f (HALT). Because security_exception can never go high, every LOAD from the kernel half of RAM succeeds even though we remain in user mode, and every byte is printed via PUTC.


5. Why the constant trick is undetectable by the verifier

  • Quantifiers – the solver checks the equalities for all possible assignments, but our gates force one particular assignment (is_root_now = 1, security_exception = 0), making every clause true.

  • No extra constraint – the organiser intended “is_root_now is an independent input you may read, not drive”. By re-defining it as a gate output we silently removed that degree of freedom, but the verifier never notices: it still has a single Boolean is_root_now, only now equated to a NAND of two zeros.


6. Bottom line

The whole exploit is a textbook “drive an input” hardware-Trojan:

  1. Synthesize constants with three NANDs.

  2. Wire security_exception to 0.

  3. Wire is_root_now to 1 so the spec equation stays tautologically true.

  4. Keep every other output behaviourally identical, so the verifier has nothing else to complain about.

  5. Run any user program you like – the flag sits unprotected at 0x80+.

That’s why the menu shows “Looks good!” yet the remote chip obediently leaks CTF{…} a few milliseconds later.

Crypto

numerology (218 solves)

Description:

I made a new cipher, can you help me test it? I'll give you the key, please use it to decrypt my ciphertext.

Resources:

Static resources:chall

Solution:

I wanted to see if my new automated AI solver could solve a GoogleCTF question, but unfortunately only using o3 on the backend will do it one-shot. Basically the solution can be found by decompiling the pyc (either pylingual.io or use pycdc on your computer, one of the few options available for python 3.12), and then passing it in to o3.

Using GPT 4.1 mini on the backend though, my automated solver was able to solve with a few (a lot) of nudges. Take a look at https://krauq.com and join the discord.

Last updated

Was this helpful?