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
We are given a ROM (program), which apparently runs on the challenge server. Supposedly, this program takes “512 integers” to “run at address 0”.
Where’s the flag? Who knows… so let’s look at the program.
How? Write a disassembler, of course. Under the time constraint of the competition (<4 hours for the whole CTF), we just want to get the simplest disassembler we need to understand the program.
We simply convert the integer <-> instruction name mapping from the documentation into a python dictionary, then print recognized instructions (or the equivalent character for bytes without an equivalent instruction).
dasm.py
import sys
insts = {
'ADD': (43,['left', 'right'],['out']),
'AND': (38,['left', 'right'],['out']),
'DIV': (47,['left', 'right'],['out']),
'DROP': (88,['value'],[]),
'DUP': (68,['value'],['value', 'value']),
'EQ': (61,['left', 'right'],['out']),
'GT': (62,['left', 'right'],['out']),
'HLT': (0,[],[]),
'PUSH': (73,[],['value']),
'JMP': (74,['address'],[]),
'JNZ': (90,['address', 'condition'],[]),
'LT': (60,['left', 'right'],['out']),
'LOAD': (76,['address'],['value']),
'MOD': (37,['left', 'right'],['out']),
'MUL': (42,['left', 'right'],['out']),
'NEQ': (33,['left', 'right'],['out']),
'ROT': (82,['a', 'b', 'c'],['b', 'c', 'a']),
'SHL': (123,['value', 'shift'],['value']),
'SHR': (125,['value', 'shift'],['value']),
'STORE': (83,['address', 'value'],[]),
'SUB': (45,['left', 'right'],['out']),
'SWAP': (87,['left', 'right'],['right', 'left']),
'XOR': (94,['left', 'right'],['out']),
}
insts = {v[0]: (k,v[1],v[2]) for k,v in insts.items()}
with open(sys.argv[-1], "r") as f:
code = [int(x) for x in f.read().split(' ')]
asm = []
i = 0
while i < len(code):
X = code[i]
if X in insts:
inst = insts[X]
line = f"{i}\t{inst[0]}"
else:
inst = ("UNK")
line = f"{i}\tUNK: {X} / {chr(X)}"
i += 1
if inst[0] == "PUSH":
Y = code[i]
line = f"{line} {Y}" if Y<2048 else f"{line} {Y} (or @{Y-2048})"
i += 1
asm.append(line)
print("\n".join(asm))
Then call it to disassemble the given ROM.
4.dasm
0 PUSH 2096 (or @48)
2 JMP
3 UNK: 102 / f
4 UNK: 108 / l
5 UNK: 97 / a
6 UNK: 103 / g
7 SUB
8 UNK: 112 / p
9 UNK: 108 / l
10 UNK: 97 / a
11 UNK: 99 / c
12 UNK: 101 / e
13 UNK: 104 / h
14 UNK: 111 / o
15 UNK: 108 / l
16 UNK: 100 / d
17 UNK: 101 / e
18 UNK: 114 / r
19 HLT
20 SWAP
21 UNK: 101 / e
22 UNK: 108 / l
23 UNK: 99 / c
24 UNK: 111 / o
25 UNK: 109 / m
26 UNK: 101 / e
27 UNK: 32 /
28 UNK: 116 / t
29 UNK: 111 / o
30 UNK: 32 /
31 UNK: 80 / P
32 UNK: 102 / f
33 STORE
34 UNK: 97 / a
35 UNK: 97 / a
36 STORE
37 NEQ
38 HLT
39 UNK: 71 / G
40 UNK: 111 / o
41 UNK: 111 / o
42 UNK: 100 / d
43 UNK: 98 / b
44 UNK: 121 / y
45 UNK: 101 / e
46 NEQ
47 HLT
48 PUSH 2103 (or @55)
50 PUSH 2068 (or @20)
52 PUSH 2145 (or @97)
54 JMP
55 PUSH 2047
57 PUSH 0
59 STORE
60 PUSH 2047
62 PUSH 62
64 STORE
65 PUSH 2047
67 PUSH 32
69 STORE
70 PUSH 2127 (or @79)
72 PUSH 0
74 PUSH 512
76 PUSH 2167 (or @119)
78 JMP
79 PUSH 2132 (or @84)
81 PUSH 0
83 JMP
84 PUSH 2047
86 PUSH 0
88 STORE
89 PUSH 2144 (or @96)
91 PUSH 2087 (or @39)
93 PUSH 2145 (or @97)
95 JMP
96 HLT
97 DUP
98 LOAD
99 DUP
100 PUSH 2047
102 SWAP
103 STORE
104 PUSH 0
106 EQ
107 PUSH 2165 (or @117)
109 SWAP
110 JNZ
111 PUSH 1
113 ADD
114 PUSH 2145 (or @97)
116 JMP
117 DROP
118 JMP
119 SWAP
120 DUP
121 ROT
122 ADD
123 PUSH 1
125 SUB
126 PUSH 512
128 SWAP
129 STORE
130 DUP
131 PUSH 512
133 LOAD
134 GT
135 PUSH 2238 (or @190)
137 SWAP
138 JNZ
139 PUSH 2046
141 LOAD
142 DUP
143 PUSH 2047
145 SWAP
146 STORE
147 DUP
148 PUSH 0
150 EQ
151 PUSH 2237 (or @189)
153 SWAP
154 JNZ
155 DUP
156 PUSH 32
158 NEQ
159 PUSH 2218 (or @170)
161 SWAP
162 JNZ
163 DROP
164 PUSH 1
166 ADD
167 PUSH 2234 (or @186)
169 JMP
170 SWAP
171 DUP
172 ROT
173 LOAD
174 PUSH 10
176 MUL
177 ADD
178 PUSH 48
180 SUB
181 SWAP
182 DUP
183 ROT
184 SWAP
185 STORE
186 PUSH 2178 (or @130)
188 JMP
189 DROP
190 DROP
191 JMP
Very quickly, we see that starting from address 3, there’s a string currently reading “flag placeholder”. Presumably the real program that runs on the server contains a real flag.
So there’s our task: write a program that reads memory starting from address 3.
Much in the same vein as our disassembler, we can write a very basic assembler for this task:
insts = {
'ADD': 43,
'AND': 38,
'DIV': 47,
# ...
'PUSH': 73,
# ...
'LOAD': 76,
# ...
'STORE': 83,
# ...
'SWAP': 87,
# ...
}
def A(a): return 2048+a
OUT = 2047
load1 = [
"PUSH", A(3), "LOAD",
"PUSH", OUT, "SWAP", "STORE",
]
code = []
for i in range(40):
copy = list(load1)
copy[1] = A(3+i)
code += copy
code.append("HLT")
print(" ".join([str(insts.get(x,x)) for x in code]))
Our resulting program: 73 2051 76 73 2047 87 83 73 2052 76 73 2047 87 83 73 2053 76 73 2047 87 83 73 2054 76 73 2047 87 83 73 2055 76 73 2047 87 83 73 2056 76 73 2047 87 83 73 2057 76 73 2047 87 83 73 2058 76 73 2047 87 83 73 2059 76 73 2047 87 83 73 2060 76 73 2047 87 83 73 2061 76 73 2047 87 83 73 2062 76 73 2047 87 83 73 2063 76 73 2047 87 83 73 2064 76 73 2047 87 83 73 2065 76 73 2047 87 83 73 2066 76 73 2047 87 83 73 2067 76 73 2047 87 83 73 2068 76 73 2047 87 83 73 2069 76 73 2047 87 83 73 2070 76 73 2047 87 83 73 2071 76 73 2047 87 83 73 2072 76 73 2047 87 83 73 2073 76 73 2047 87 83 73 2074 76 73 2047 87 83 73 2075 76 73 2047 87 83 73 2076 76 73 2047 87 83 73 2077 76 73 2047 87 83 73 2078 76 73 2047 87 83 73 2079 76 73 2047 87 83 73 2080 76 73 2047 87 83 73 2081 76 73 2047 87 83 73 2082 76 73 2047 87 83 73 2083 76 73 2047 87 83 73 2084 76 73 2047 87 83 73 2085 76 73 2047 87 83 73 2086 76 73 2047 87 83 73 2087 76 73 2047 87 83 73 2088 76 73 2047 87 83 73 2089 76 73 2047 87 83 73 2090 76 73 2047 87 83 0
Send this to the challenge server, and out comes the flag!
Home About Contact