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}