PROJEKT: OVERFLOW
RISC-V assembly board game

hack the planet



THE GAME

I made this game to teach my daughter how buffer overflows work. Looking at programs as something you can play with, and poke and twist and make it do something else, is my favorite part of modern computing. I think its the right way to look at programs. When your microwave oven gets an update and starts crashing, you can hack it. Or when your keyboard controller’s firmware is bad, you can hack it (looking at you vortex pok3r). She is 12 yo now but her assembler skills are getting better and better, hopefully one day she will be able to hack her own keyboard :)

The game is about creating a small shellcode in memory by copying existing instructions and then exploiting a buffer overflow to jump into it, so that you can overwrite your opponent’s return address to force them to go to the game_over() function.There are other mechanics as well and more layers of strategy (like setting the exception handler or monkeypatching).

All players share the same memory, and execute the same program, while time sharing the same processor running preemptive scheduling os, so each turn the player executes 10 instructions, after that the process is interrupted by the operating system, and its the other player's turn. Each player's stack pointer starts at a different location. There is no virtual memory.

How it looks when we play the game with friends (proof that humans do play this game, and also that I do have friends):
With my kid and dog:

The code is compiled with riscv64-unknown-elf-gcc -fomit-frame-pointer -fno-inline-small-functions -mno-shorten-memrefs -march=rv32g -mabi=ilp32 -ffreestanding -nostdlib -nostartfiles -T code/link.ld -O0 -fverbose-asm -g -o game game.c, the linker script link.ld has just offsets for the specific functions so the fit nicely on the board. The no optimization -O0 makes the machine code pretty verbose, but also straight forward. Then I use riscv64-unknown-elf-objdump -S -l -fd game, parse it and fix the ▲ and ✎ instructions, convert the jump offset from hex to dec and clean the assembly a bit, then match it with the source code and generate a svg which is then converted to a pdf with inkscape.

PRINT


Board Preview:

Tabletop Requirements

Game Rules

Board Setup:
  1. Both players start with all registers set to zero, except for the return address register (ra), which starts at 1000.
  2. Player 1's stack pointer (sp) is initialized at address 2244.
  3. Player 2's stack pointer (sp) is initialized at address 3844.
  4. The nop instruction pawn is not initially placed on the board.
  5. Both players' program counters (pc) start at address 1000, which is the beginning of the main function.
  6. Place the trap pawn at address 1000.
  7. All memory addresses are set to zero, except for the pre-loaded program.
Gameplay:
  1. Moves per Turn: Players are required to execute 10 instructions during their turn.
  2. Execute Instructions: Players must follow the program counter and execute either 10 instructions or an accumulated number up to 20. All jumps (jal, beq, etc.) must be followed.
  3. Suspend Execution: Players can opt to suspend their turn after executing at least one instruction, saving the remaining instructions for their next turn. A maximum of 20 instructions can be accumulated.
  4. Monkeypatch: After executing exactly one instruction at the start of each turn, players can move the nop instruction pawn to any address in any function that is not being executed by the players at the moment. When the program counter reaches this address, the instruction will act as a no-operation (nop). Moving the nop pawn costs the player their current and next turn, allowing the opponent to execute up to 20 instructions on their next turn.The monkeypatch mechanic is still unbalanced, every few days I change the rule a bit in attempt to improve it, so if you come up with a better idea let me know.
Winning Conditions [HARD MODE]:
  1. Game Over: The game ends when one player successfully hacks their opponent to call the game_over() function.
  2. Draw: It is possible that you get the game in a state where nobody can force the other player into game_over(), in this case the game is a draw.
Winning Conditions [EASY MODE]:
  1. Break Free: The winner is the first player that leaves the main loop (executing ret in main).
Special Symbols:
  1. : This symbol allows players to choose any 12-bit number (0 to 4095) as the immediate value in the li instruction.
  2. : This symbol allows players to choose a value for the load instruction that is within ±128 bytes of their stack pointer. For example, if your sp is 2180, you can choose a value between 2052 and 2308.
Rules and Constraints:
  1. Illegal Actions: Overwriting a memory address below 1192, or doing unaligned read or write (read or write to address not multiple of 4), or executing an illegal instruction will crash your program.
  2. Exception Handling: A crash invokes the exception handler, which jumps to the trap address, initially set to 1000 but you can overwrite it from the set_trap() function. Once an exception happen the program counter is set to the specific value and you continue execution.
  3. Cheating/Mistakes: If a player cheats or makes a mistake and is caught, their program state, including memory and registers, is reset.
Extended Rules for More Players:
  1. Player 3's stack pointer (sp) register is set to 2116.
  2. Player 4's stack pointer (sp) register is set to 3716.
  3. symbol now allows you to write only -128 bytes away from your stack pointer.
  4. The game will be quite unstable with more than 2 players, and it will be corrupted quite fast. It will be harder to get to a winning condition, but the gameplay will be more fun and chaotic.
Hints and Strategies:
  1. Offensive Crashes: Utilize crashes as an offensive strategy to disrupt your opponent's gameplay.
  2. Trap Handler Modification: Alter the trap handler to point to the game_over function, making the first player to crash the loser of the game. Beware: the nop pawn is extremely powerful tool if the trap is set to game_over, because you can put it on top of the ret in the function that your opponent is currently executing and they will lose.
  3. Overwriting Return Address: Utilize the bug() function to overwrite your opponent's return address by overflowing the index with a value of 400 or -400, allowing you to access the other player's stack. For instance, to transition from address 3784 (buffer[0] with stack pointer 3780) to address 2184, an index of -400 is required, calculated as (3784 - 2184) / 4 = 400.
  4. Shellcode Creation: Employ the copy() function to replicate specific instructions and formulate a concise shellcode in your memory, enabling arbitrary writes. Exploit the buffer overflow in the bug() function to jump to this shellcode. An example of a simple shellcode for arbitrary write is as follows:
    • li a4, ✎ (copy from addr 256, 268)
    • li a5, ✎ (available at addr 136, 144, 1016, etc.)
    • sw a4, 0(a5) (available at addr 264, 272)
    • ret (available at addr 272, 200, 1060, etc.)
    Copying the ret instruction will lead to an infinite loop as the return address will be set to the start of the shellcode when you jump into it, causing continuous execution.
  5. Jumping to Memory: To jump to your memory, exploit the buffer overflow in the bug() function. By setting the index variable to 6, you can write the value variable on top of the stored return address on the stack (28(sp)). Upon returning from bug(), the program copies from 28(sp) into the return address register. Place the address of the shellcode you created in the value variable.
Note:

All jumps are relative to the current program counter, even if they appear as absolute addresses in the disassembler, so jal a4, 0 (machine code 1903) is actually an infinite loop when executed.

List of all game valid instructions with machine code 0 to 4095 (RV32 JRI operating on a0,a4,a5,sp,ra) 0x00000013 (19) addi zero, zero, 0
0x00000033 (51) add zero, zero, zero
0x00000067 (103) jalr zero, zero, 0
0x0000006f (111) jal zero, 0
0x00000093 (147) addi ra, zero, 0
0x000000b3 (179) add ra, zero, zero
0x000000e7 (231) jalr ra, zero, 0
0x000000ef (239) jal ra, 0
0x00000113 (275) addi sp, zero, 0
0x00000133 (307) add sp, zero, zero
0x00000167 (359) jalr sp, zero, 0
0x0000016f (367) jal sp, 0
0x00000513 (1299) addi a0, zero, 0
0x00000533 (1331) add a0, zero, zero
0x00000567 (1383) jalr a0, zero, 0
0x0000056f (1391) jal a0, 0
0x00000593 (1427) addi a1, zero, 0
0x000005b3 (1459) add a1, zero, zero
0x000005e7 (1511) jalr a1, zero, 0
0x000005ef (1519) jal a1, 0
0x00000613 (1555) addi a2, zero, 0
0x00000633 (1587) add a2, zero, zero
0x00000667 (1639) jalr a2, zero, 0
0x0000066f (1647) jal a2, 0
0x00000713 (1811) addi a4, zero, 0
0x00000733 (1843) add a4, zero, zero
0x00000767 (1895) jalr a4, zero, 0
0x0000076f (1903) jal a4, 0
0x00000793 (1939) addi a5, zero, 0
0x000007b3 (1971) add a5, zero, zero
0x000007e7 (2023) jalr a5, zero, 0
0x000007ef (2031) jal a5, 0

Change Log

0.0.6: while(*prun) instead of while(run), this way the opponent could forcefully crash you by making you do unaligned dereference; change the NOP rule to be placable only in functions that are not being executed at the moment

SIMILAR PROJECTS

We made few card games that you might find useful, the C game and the machine code game particularly helped with understanding assembly better.

Programming Time, which is a game to teach python and some more fundamental algorithms, from hash tables to RSA

The C Pointer Game - Pointers, Arrays and Strings, a game to teach kids to look at the computer memory and understand references and values

4917, a game to teach kids machine code and how the CPU works with memory and registers

The Unix Pipes Game, a game to teach kids to use basic UNIX commands: cat, sort, grep, head, tail, wc, uniq

The Unix Pipes Game - Process Substitution, an expansion of the Unix Pipes Game to teach process substitution and also: paste, tr, cut, bc

RunLength Encoding for Kids, small cards "game" to explain runlength encoding

PUNK0 - The Function Composition Card Game, use cards to manipulate a list and use its values to win the game

Programming for kids, a log of my journey of teaching my daughter how to code

SYMBOLS

The the squares on the left/right are ascii encoded binary message, you can try to decipher it, the white squares are 1, and black on black squares are 0, use the spacing to figure out where the 0's go.

The colors are minimal, so people can print it cheaply, and it is also clear on a3 and a4 black and white printers, the only colors used are red (255,0,0), blue(0,0,255), black (0,0,0) and white (255,255,255).

I am not a designer, but the current design is maybe 20th iteration or so, and it came out after listening to Ghost In The Shell's Making of a Cyborg for 5 hours.

The project logo symbolizes the overflow, and it is randomly generated, the current random seed made me feel a bit uneasy, so I left it like that.

Spacing on the logo and the Japanese text is a bit off, this is in order to be able to fold the A3 paper in half and not go through any text.

The Japanese text is from the last words of The Pirate King, Gol D. Roger: Inherited Will, The Destiny of the Age, and The Dreams of the People. As long as people continue to pursue the meaning of Freedom, these things will never cease to be!

No syntax highlighting. I dont use syntax highlighting, and also I dont teach my daughter to use it. I dont have strong preference, but it just forces me to think that certain things in code are more important than others (depending on the theme). Without highlighting I can do my own judgment, and can easily focus on what I think matters.

CREDITS and THANKS

I would like to thank Peter, William, Shantanu, Rares and my daughter who played 50 different versions of the game, all of which were quite bad :) but they didnt give up. And thanks to tn1 who helped me a lot with the design, particularly telling me what is not good.

Also big thank you to the people working on the Terminus Font which is absolute beauty.

Special thanks to OpenMachine's tinyfive emulator which I used to test the game, until I wrote my own for the web version.

Thanks to gamesounds.xyz for the .wav files I got for the web version, and also Karl Casey @ White Bat Audio for the background music

And of course many thanks to the people working on RISC-V it is super fun to work with it.

Many thanks to @matsuu for pointing out a typo in the Japanese spelling.

CONTACT

mail: jackdoe@fastmail.com

If you have an idea about a different board or rules, or you find a bug in the board or the online version please send me an email, also turns out I can play this game with my daughter and like 2 people in my whole contact list 🥹, so if you have fun playing it I would love to hear about it.

ASSEMBLY GUIDE

You can find many RISC-V assembly guides such as: https://riscv-programming.org/ or riscv-asm-manual, and many interpreters like: cs3410 risc-v interpreter, or decoders like luplab's rvcodecjs which is a great instruction decoder e.g. enter 0x13 (the nop instruction) there and see how it decodes. If you are struggling you can also search youtube for 'risc v assembly tutorial' and there are many really good videos, sadly I am not sure which one will work for you, so don't get discouraged and just try few of them.

I made some exercises for my daughter, each day I printed few memory templates and few of the code examples below and she just executed the instructions, Some days I changed the code on the fly, or spent more time explaining casting and structures for example. But the basic RISC-V assembly she was able to understand pretty fast.

The hangman version is the same as the normal pdf, but few of the assembly instructions are missing and you have to fill them in:

If you need to step up your C game, we use Beej's C guide: Beej's Guide to C Programming, particularly the first 7-8 chapters. But there are many very good resources. You can also use the following assembly examples to brush up on your C, a good example is this pointer exercise:

When you print the exercise and the memory template it looks like this:
Those are the exercises we have done so far: