We’ve recovered a file from an SD card. It seems important, can you find the hidden content? Attached: rotary-precision.txt
In this challenge, we are given a G-Code file. G-Code is a programming language that contains instructions for CNC machines like 3D printers. We can view the model online using NC Viewer.
There is model of a fox-gargoyle. Off to the side (left of the screen), there are a bunch of extraneous points that aren’t part of the gargoyle model. These points are suspicious and likely hide the flag.
G-Code CTF challenges usually have the flag printed within some layer of the model. However, in this challenge, each layer of that extra structure is identical and doesn’t hide any secret text.
The way I solved it was kind of lucky. I installed a desktop CNC simulation app, Camotics, to get a better view of the models. Loading the G-Code file into Camotics, there were a lot of error messages of the form: "WARNING:rp.gcode:417456:46:Word 'E' repeated in block, only the last value will be recognized"
. Looking at one of those lines, we see the G-Code command G0 X7.989824091696275e-39 Y9.275539254788188e-39
.
In G-Code, the ‘E’ parameter is used to control the extruder position. In this case, Camotics was wrongly parsing the float values as an extruder parameter – that’s weird. These floats are extremely close to zero; if they were part of a real model, they would likely be zero since the precision of the physical machine will not allow for the extra floating point precision anyway.
From the challenge name “Rotary Precision”, I inferred that those floating points are being used to encode some data. I used the following script to examine those floats:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import re
import struct
decoded = []
with open("rp.gcode") as f:
for line in f:
# look for scientific notation floats
matches = re.findall(r"([+-]?\d+\.\d+e[+-]?\d+)", line)
for m in matches:
val = float(m)
f32_bytes = struct.pack("<f", val)
# Check each byte
for b in f32_bytes:
if 32 <= b <= 126: # printable ASCII
decoded.append(chr(b))
print("".join(decoded))
This reveals the following text:
1
aWnegWRi18LwQXnXgxqEF}blhs6G2cVU_hOz3BEM2{fjTb4BI4VEovv8kISWcks4def rot_rot(plain, key): charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789{}_" shift = key cipher = "" for char in plain: index = charset.index(char) cipher += (charset[(index + shift) % len(charset)]) shift = (shift + key) % len(charset) return cipher
Restructuring the text for clarity, we find the following encryption function. We can guess that the string at the beginning is the ciphertext.
1
2
3
4
5
6
7
8
9
10
11
def rot_rot(plain, key):
charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789{}_"
shift = key
cipher = ""
for char in plain:
index = charset.index(char)
cipher += charset[(index + shift) % len(charset)]
shift = (shift + key) % len(charset)
return cipher
# ciphertext?: aWnegWRi18LwQXnXgxqEF}blhs6G2cVU_hOz3BEM2{fjTb4BI4VEovv8kISWcks4
We are missing the value of key, which is some integer value. However, the key is only ever used modulo len(charset)
, so we can simply brute-force all integers in that range.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789{}_"
ciphertext = "aWnegWRi18LwQXnXgxqEF}blhs6G2cVU_hOz3BEM2{fjTb4BI4VEovv8kISWcks4def"
def rot_rot_decrypt(cipher, key):
shift = key
plain = ""
for char in cipher:
index = charset.index(char)
plain += charset[(index - shift) % len(charset)]
shift = (shift + key) % len(charset)
return plain
for key in range(len(charset)):
candidate = rot_rot_decrypt(ciphertext, key)
if "TISC{" in candidate:
print(f"Key={key} → {candidate}")
Flag: TISC{thr33_d33_pr1n71n9_15_FuN_4c3d74845bc30de033f2e7706b585456}