Rotary Precision
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 a fox-gargoyle model. Off to the side (left of the screen), there are a bunch of weird extraneous points that aren’t part of the gargoyle model. These points are suspicious and likely hide the flag.
Past G-Code challenges usually have the flag printed within some layer of the model. However, each layer of the extra structure are identical and don’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. From the challenge name “Rotary Precision”, I inferred that the 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}