edit

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 utilisant emulator.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 the emulator.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. If true -> 1 else false -> 0.

Greater Than

GT (62, '>')
    INP: [left, right]
    OUT: [out]

Signed check left > right. If true -> 1 else false -> 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 to address.

Jump Not Zero

JNZ (90, 'Z')
    IN: [address, condition]
    OUT: []

Sets PC to address if condition != 0. Otherwise, PC++.

Less Than

LT (60, '<')
    IN: [left, right]
    OUT: [out]

Signed check left < right. If true -> 1 else false -> 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 et emulator.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 and emulator.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!


HomeAboutContact