CS Games 2025 CTF: PC From Scratch
A four-challenge reverse engineering track from CS Games 2025’s CTF competition.
Hello World (1/4)
Vous pouvez « Linux from Scratch », mais pouvez-vous « PC from Scratch »?
Découvrez comment exécuter
challenge.sx
en utilisantemulator.sb3
et le flag est à vous.
You can Linux from Scratch, but can you PC from Scratch?
Figure out how to run the
challenge.sx
using theemulator.sb3
and the flag is your’s.
We are given 3 files: emulator.sb3
, challenge.sx
and documentation.md
. The documentation describes a fantasy ISA:
documentation.md
PC from Scratch (PfS)
Architecture
PfS is a 16-byte architecture. Any memory cells can contain at most 65_535. The following is an example of data in memory:
0: 0 1: 1234 2: 65535 ...
Bus
+--------+ .--| Memory | | +--------+ +-----+ | +-------+ | CPU |--+--| Input | +-----+ | +-------+ | +--------+ '--| Output | +--------+
All devices communicate over a single shared bus. 3 internal registers are used to communicate between devices.
ADDRESS: The address to target. DATA: The data associated to the operation. CONTROL: The state of the operation.
Control
The CPU can perform the following control opcodes.
W - Write to address. R - Read from address.
The CPU waits for device interrupts. If there are not any (for example, when trying to reach un-mapped memory), the CPU may end up in an undefined state.
Memory Layout
Addresses Device [0, 2045] RAM 2046 Input 2047 Output [2048, 65535] ROM Random Access Memory (RAM)
- Addresses: [0, 2045]
- Size: 2046
- Protection: Read, Write, Execute
RAM is a ~2K temporary storage. It is reset on every execution.
Read-Only Memory (ROM)
- Addresses: [2048, 65535]
- Size: 63488
- Protection: Read, Execute
ROM is a 62K permanent storage used to store a program. This works like an old-school game cartridge; ROM must be attached and cannot be modified.
Input
- Address: 2046
- Protection: Read
The input is a buffered device of the keyboard. Every read to
2046
will provide the next buffered ASCII character. If the buffer is empty, it will prompt the user to type more input. A null-byte (0x00
) is added to the end of the buffer to delimit inputs.This is a sample program that reads the input
AB
.
INT 2046 LOAD # 'A' INT 2046 LOAD # 'B' INT 2046 LOAD # 0x00
Output
- Address: 2047
- Protection: Write
The output is a streaming device of the screen. Every write to
2047
with an ASCII character will be displayed to the screen. When a null-byte (0x00
) is sent, a new line is added.This is a sample program that prints
AB
.
INT 2047 INT 65 # A STORE INT 2047 INT 66 # B STORE INT 2047 INT 0 STORE # Print: "AB"
CPU
Registers
PC
: Pointer towards current operation.All other registers are temporary internal values. Additional CPU memory is managed by an internal stack.
Stack
The stack is an infinite temporary internal storage. Is it used to store value temporarily. It exists separate to the bus (aka, it is not possible to execute code from the stack).
Like typical stacks, it follows First In, First Out (FIFO) push/pop.
It is cleared on every restart.
Opcodes
OP (C_INT, C_CHAR) ARG: [...] INP: [...] OUT: [...]
OP: The assembly name of the operation. C_INT: The integer value of the opcode. C_CHAR: The character equivalence of the opcode (all opcodes have a friendly char representation). ARG: The additional arguments used by the opcode that follow in the code. INP: The input stack (right-most value is top of stack). OUT: The output stack (right-most value is top of stack).
Undefined
Undefined opcodes leave the CPU in an undetermined state.
Add
ADD (43, '+') INP: [left, right] OUT: [out]
Signed integer addition ->
left + right
.AND
AND (38, '&') INP: [left, right] OUT: [out]
Bitwise AND ->
left | right
.Divide
DIV (47, '/') INP: [left, right] OUT: [out]
Signed integer division ->
left / right
.Drop
DROP (88, 'X') INP: [value] OUT: []
Remove the top value of the stack.
Duplicate
DUP (68, 'D') INP: [value] OUT: [value, value]
Copy the top value of the stack.
Equal
EQ (61, '=') INP: [left, right] OUT: [out]
Checks
left == right
. Iftrue
->1
elsefalse
->0
.Greater Than
GT (62, '>') INP: [left, right] OUT: [out]
Signed check
left > right
. Iftrue
->1
elsefalse
->0
.Halt
HLT (0, '\0') INP: [] OUT: []
Stops the CPU (end of program).
Integer
INT (73, 'I') ARG: [value] OUT: [value]
Take one operand from program and pushes it onto the stack.
Jump
JMP (74, 'J') IN: [address] OUT: []
Sets
PC
toaddress
.Jump Not Zero
JNZ (90, 'Z') IN: [address, condition] OUT: []
Sets
PC
toaddress
ifcondition != 0
. Otherwise,PC++
.Less Than
LT (60, '<') IN: [left, right] OUT: [out]
Signed check
left < right
. Iftrue
->1
elsefalse
->0
.Load
LOAD (76, 'L') IN: [address] OUT: [value]
Loads a value from a specific address and pushes it to the stack.
Modulo
MOD (37, '%') IN: [left, right] OUT: [out]
Unsigned integer modulo ->
left % right
.Multiply
MUL (42, '*') IN: [left, right] OUT: [out]
Signed integer multiplication ->
left * right
.Not Equal
NEQ (33, '!') IN: [left, right] OUT: [out]
OR
OR (124, '|') IN: [left, right] OUT: [out]
Bitwise OR following
left | right
.Rotate
ROT (82, 'R') IN: [a, b, c] OUT: [b, c, a]
Rotate top 3 items of stack clockwise.
Shift Left
SHL (123, "{") IN: [value, shift] OUT: [value]
Unsigned bitwise shift left.
Shift Right
SHR (125, "}") IN: [value, shift] OUT: [value]
Bitwise shift right.
Store
STORE (83, 'S') IN: [address, value] OUT: []
Store a value to a specific address.
Substract
SUB (45, '-') IN: [left, right] OUT: [out]
Signed integer subtraction ->
left - right
.Swap
SWAP (87, 'W') IN: [left, right] OUT: [right, left]
Swaps top 2 values of stack.
XOR
XOR (94, '^') IN: [left, right] OUT: [out]
Bitwise XOR ->
left ^ right
.Hello World
hello.sm
$SECTION RAM @zero $ZEROS 1 @print_ptr $ZEROS 1 $SECTION ROM @main # INP: [] # OUT: [] @main_init INT @main_init_end INT @msg INT @print JMP @main_init_end @main_end HLT @print # INP: [return, address] # OUT: [] @print_init # @print_ptr = address INT @print_ptr SWAP STORE @print_loop # *@print_ptr == 0 -> @print_loop_end INT @print_loop_end INT @print_ptr LOAD LOAD INT 0 EQ JNZ # Push to OUT INT OUT INT @print_ptr LOAD LOAD STORE # Increment @print_ptr INT @print_ptr DUP LOAD INT 1 ADD STORE # Loop INT @print_loop JMP @print_loop_end @print_end # Print INT OUT INT 0 STORE # Return JMP @msg $DATA "Hello World!" 0
hello.sx
73 2055 73 2094 73 2056 74 0 73 1 87 83 73 2088 73 1 76 76 73 0 61 90 73 2047 73 1 76 76 83 73 1 68 76 73 1 43 83 73 2060 74 73 2047 73 0 83 74 72 101 108 108 111 32 87 111 114 108 100 33 0
We don’t need the documentation yet though; the first “challenge” here is only to ensure we’re capable of using the emulator.
Open the Scratch editor, click on File -> Load from your computer, select emulator.sb3
, and run!
You’ll be asked for a ROM, copy-paste the contents of challenge.sx
in, and the emulator will slowly reveal the flag.
PfSaaS (4/4)
PfSaaS est « PC from Scratch » dans le nuage!
Fournissez un programme d’une taille maximale de 512 entiers et il s’exécutera à l’adresse 0.
Nous avons beaucoup de clients, alors assurez-vous que le code s’exécute en moins de 30 secondes.
Notre infrastructure utilise une surveillance IA blockchain post-quantique, donc il n’y a aucune chance que vous puissiez lire des informations sensibles!
Note: Vous devez prendre les fichiers
documentation.md
etemulator.sb3
de la première partie.
PfSaaS is PC from Scratch in the cloud!
Supply a program of maximum size of 512 integers and it will run at address 0.
We have alot of customers, so make sure the code runs in less than 30 seconds.
Our infrastructure uses post-quantum blockchain AI monitoring, so there is no way you’ll ever be able to read sensitive information!
Note: You need to use the
documentation.md
andemulator.sb3
from the first part.
nc challenge 10035
TODO
Gist: write your own disassembler to find where the flag is supposed to go in ROM, and assembler to write a program that reads that part of the ROM and writes it to output. connect to the remote and paste in your program
Home About Contact