← Back to list

Challenge: printedpart (Write-up)

1) What is the file?

0xBarood · 2026-02-16 06:42 · 0 claps · 4.3 min read
#0xfun #ctf-writeup
Open on Medium ↗

Challenge: printedpart (Write-up)

1) What is the file?

You’re given 3D.gcode. A .gcode file is plain-text instructions for a 3D printer:

  • Movements (X/Y/Z)
  • Extrusion amount (E)
  • Speeds (F)
  • Temperature, etc.

Verify on Kali

file 3D.gcode
head -n 60 3D.gcode

You’ll typically see a slicer header (Cura/PrusaSlicer/etc.) and lines starting with ; which are comments.

2) Why is it huge?

Because every tiny movement is written as a line. A print can contain hundreds of thousands to millions of moves.

3) Quick “direct string” check (usually fails here)

Sometimes flags are stored in comments. We quickly search:

strings -n 6 3D.gcode | grep -iE "0xfun|flag\\{|fun\\{"

In this challenge, the flag is not stored plainly in text.

4) Key observation: Cura “MESH” markers

Cura often annotates which STL mesh is being printed:

;MESH:flag.stl
...
;MESH:NONMESH

So the G-code contains multiple sub-objects. One of them is literally flag.stl, strongly suggesting the flag text is physically printed/engraved on that mesh.

List meshes
grep -oP '^;MESH:\\K.*' 3D.gcode | sort -u

You should see flag.stl among them.

5) Extract ONLY the part that prints flag.stl

To make analysis easy, isolate the section where Cura says it’s printing flag.stl.

(Anything else is just noise.)

Conceptually:

  • Start capturing after ;MESH:flag.stl
  • Stop when another ;MESH: appears (like ;MESH:NONMESH)

6) Convert toolpath → image (the core trick)

When a printer “writes” letters, it’s just movement + extrusion.

We parse only extruding moves:

  • Cura uses G1 ... E... for extrusion
  • So we keep only G1 lines containing an E value

Then we plot the points:

  • XY top-down view
  • XZ side view
  • YZ front view

A density map (histogram) often makes letters much clearer than raw scatter.

7) Read the printed text and submit

From the rendered projection of the flag.stl toolpath, the embedded text reads:

0xfun{this_monkey_has_a_flag}

One-shot script (does everything)

Requirements (Kali)

Minimal:

sudo apt update
sudo apt install -y python3 python3-numpy python3-matplotlib python3-pil

Optional (for automatic OCR extraction):

sudo apt install -y tesseract-ocr python3-pytesseract

Script: solve_printedparts.py

#!/usr/bin/env python3import argparseimport osimport reimport sysimport zipfileimport tempfileimport shutilimport subprocessfrom pathlibimport Pathimport numpyas npimport matplotlib.pyplotas pltfrom PILimport Image, ImageOps, ImageEnhancedeffind_gcode_file(root: Path) -> Path:
    gcodes =list(root.rglob("*.gcode"))ifnot gcodes:raise FileNotFoundError("No .gcode file found after extraction.")# Prefer a file named 3D.gcode, otherwise choose the largestfor gin gcodes:if g.name.lower() =="3d.gcode":return greturnmax(gcodes, key=lambda p: p.stat().st_size)defextract_zip(zip_path: Path, out_dir: Path) -> Path:with zipfile.ZipFile(zip_path,"r")as z:
        z.extractall(out_dir)return out_dirdefprint_header_info(gcode_path: Path, max_lines=80):
    wanted = re.compile(r"(GENERATOR\\.NAME|GENERATOR\\.VERSION|TARGET_MACHINE|NOZZLE|LAYER_COUNT|PRINT\\.TIME)", re.I)print("[*] Header highlights:")with gcode_path.open("r", errors="ignore")as f:for iinrange(max_lines):
            line = f.readline()ifnot line:breakif line.startswith(";")and wanted.search(line):print("    " + line.strip())print()deflist_meshes(gcode_path: Path):
    meshes =set()
    mesh_re = re.compile(r"^;MESH:(.*)\\s*$")with gcode_path.open("r", errors="ignore")as f:for linein f:
            m = mesh_re.match(line)if m:
                meshes.add(m.group(1).strip())returnsorted(meshes)defchoose_target_mesh(meshes):# Prefer exact "flag.stl", else something containing "flag"if"flag.stl"in meshes:return"flag.stl"for min meshes:if"flag"in m.lower():return m# fallback: first non-NONMESH meshfor min meshes:if m.upper() !="NONMESH":return mreturn meshes[0]if mesheselseNonedefextract_mesh_section(gcode_path: Path, target_mesh: str, out_path: Path) ->int:
    mesh_re = re.compile(r"^;MESH:(.*)\\s*$")
    keep =False
    kept_lines =0with gcode_path.open("r", errors="ignore")as fin, out_path.open("w", encoding="utf-8", errors="ignore")as fout:for linein fin:
            m = mesh_re.match(line)if m:
                current = m.group(1).strip()
                keep = (current == target_mesh)if keep:
                fout.write(line)
                kept_lines +=1return kept_linesdefparse_extrusion_points(gcode_path: Path):# Maintain current Z because Z isn't on every extrusion line
    cur_z =0.0
    pts = []
    z_re = re.compile(r"Z([-+]?\\d*\\.?\\d+)")
    x_re = re.compile(r"X([-+]?\\d*\\.?\\d+)")
    y_re = re.compile(r"Y([-+]?\\d*\\.?\\d+)")# We only care if E exists at all (extrusion)with gcode_path.open("r", errors="ignore")as f:for linein f:if line.startswith(("G0","G1")):
                mz = z_re.search(line)if mz:
                    cur_z =float(mz.group(1))if line.startswith("G1")and" E"in line:
                mx = x_re.search(line)
                my = y_re.search(line)if mxand my:
                    x =float(mx.group(1))
                    y =float(my.group(1))
                    pts.append((x, y, cur_z))ifnot pts:return np.zeros((0,3), dtype=float)return np.array(pts, dtype=float)defsave_scatter(points, proj, out_file: Path, s=0.4):if proj =="xy":
        a, b = points[:,0], points[:,1]
        xl, yl ="X","Y"elif proj =="xz":
        a, b = points[:,0], points[:,2]
        xl, yl ="X","Z"elif proj =="yz":
        a, b = points[:,1], points[:,2]
        xl, yl ="Y","Z"else:raise ValueError("proj must be xy/xz/yz")
    plt.figure(figsize=(12,4))
    plt.scatter(a, b, s=s)
    plt.xlabel(xl)
    plt.ylabel(yl)
    plt.gca().set_aspect("equal", adjustable="box")
    plt.tight_layout()
    plt.savefig(out_file, dpi=300)
    plt.close()defsave_density(points, proj, out_file: Path, bins=900):if proj =="xy":
        a, b = points[:,0], points[:,1]
        xl, yl ="X","Y"elif proj =="xz":
        a, b = points[:,0], points[:,2]
        xl, yl ="X","Z"elif proj =="yz":
        a, b = points[:,1], points[:,2]
        xl, yl ="Y","Z"else:raise ValueError("proj must be xy/xz/yz")
    H, _, _ = np.histogram2d(a, b, bins=bins)# Normalize to make it viewable
    H = np.log1p(H)
    plt.figure(figsize=(12,4))
    plt.imshow(H.T, origin="lower", aspect="auto")
    plt.xlabel(xl)
    plt.ylabel(yl)
    plt.tight_layout()
    plt.savefig(out_file, dpi=300)
    plt.close()defenhance_for_ocr(img_path: Path, out_path: Path, invert=False, rotate=0):
    img = Image.open(img_path).convert("L")if rotate !=0:
        img = img.rotate(rotate, expand=True)# Increase contrast
    img = ImageEnhance.Contrast(img).enhance(2.5)
    img = ImageEnhance.Sharpness(img).enhance(2.0)if invert:
        img = ImageOps.invert(img)# Binarize
    img = img.point(lambda p:255if p >140else0)
    img.save(out_path)defrun_tesseract_ocr(img_path: Path):# Try pytesseract first if installed, else call tesseract CLItry:import pytesseract# type: ignore
        text = pytesseract.image_to_string(Image.open(img_path), config="--psm 6")return textexcept Exception:if shutil.which("tesseract")isNone:returnNone# Use CLI:try:
            proc = subprocess.run(
                ["tesseract",str(img_path),"stdout","--psm","6"],
                capture_output=True, text=True, check=False
            )return proc.stdoutexcept Exception:returnNonedefnormalize_flag_text(s: str) ->str:# Remove spaces/newlines, common OCR noise
    s = s.replace(" ","").replace("\\n","").replace("\\r","")# Sometimes OCR reads 0x as Ox
    s = s.replace("Ox","0x").replace("OX","0x")return sdefextract_flag_from_text(s: str):
    s2 = normalize_flag_text(s)
    m = re.search(r"0xfun\\{[^}]+\\}", s2, re.IGNORECASE)if m:# preserve original expected casing: "0xfun{...}"
        flag = m.group(0)# normalize prefix to 0xfun
        flag = re.sub(r"^0XFUN","0xfun", flag, flags=re.IGNORECASE)
        flag = re.sub(r"^0XFun","0xfun", flag, flags=re.IGNORECASE)
        flag = re.sub(r"^0xfun","0xfun", flag, flags=re.IGNORECASE)return flagreturnNonedefmain():
    ap = argparse.ArgumentParser(description="Solve PrintedParts: extract mesh toolpath and recover text.")
    ap.add_argument("input",help="Path to 3D.gcode or 3D.zip")
    ap.add_argument("--mesh", default=None,help="Target mesh name (default: auto, prefers flag.stl)")
    ap.add_argument("--out", default="out_printedparts",help="Output directory")
    ap.add_argument("--no-ocr", action="store_true",help="Skip OCR attempt and just render images")
    args = ap.parse_args()
    inp = Path(args.input).expanduser().resolve()
    out_dir = Path(args.out).resolve()
    out_dir.mkdir(parents=True, exist_ok=True)
    work = Path(tempfile.mkdtemp(prefix="printedparts_"))try:if inp.suffix.lower() ==".zip":
            extract_zip(inp, work)
            gcode = find_gcode_file(work)elif inp.suffix.lower() ==".gcode":
            gcode = inpelse:print("[-] Input must be a .gcode or .zip")
            sys.exit(1)print(f"[+] Using G-code: {gcode}")
        print_header_info(gcode)
        meshes = list_meshes(gcode)print("[*] Meshes found:")for min meshes:print("   -", m)print()
        target = args.meshif args.meshelse choose_target_mesh(meshes)ifnot target:print("[-] No mesh markers found.")
            sys.exit(1)print(f"[+] Target mesh: {target}")
        mesh_gcode = out_dir /f"{Path(target).stem}_only.gcode"
        kept = extract_mesh_section(gcode, target, mesh_gcode)print(f"[+] Extracted {kept} lines into:{mesh_gcode}")
        pts = parse_extrusion_points(mesh_gcode)if pts.shape[0] ==0:print("[-] No extrusion points found in extracted mesh section.")
            sys.exit(1)print(f"[+] Parsed {pts.shape[0]} extrusion points")# Render projectionsfor projin ("xy","xz","yz"):
            save_scatter(pts, proj, out_dir /f"{proj}_scatter.png")
            save_density(pts, proj, out_dir /f"{proj}_density.png")print("[+] Saved images:")for pinsorted(out_dir.glob("*.png")):print("   -", p.name)print()if args.no_ocr:print("[*] OCR skipped. Open density images and read the text.")return# OCR attempts on density images (usually best)
        candidates = [out_dir /"xy_density.png", out_dir /"xz_density.png", out_dir /"yz_density.png"]
        transforms = []for imgin candidates:for invin (False,True):for rotin (0,90,180,270):
                    out_img = out_dir /f"ocr_{img.stem}_inv{int(inv)}_rot{rot}.png"
                    enhance_for_ocr(img, out_img, invert=inv, rotate=rot)
                    transforms.append(out_img)print("[*] Running OCR on enhanced images (optional)...")
        best_hits = []for timgin transforms:
            text = run_tesseract_ocr(timg)ifnot text:continue
            flag = extract_flag_from_text(text)if flag:
                best_hits.append((flag, timg.name))break# stop at first foundif best_hits:
            flag, src = best_hits[0]print(f"[+] FLAG FOUND: {flag}")print(f"    (from {src})")else:print("[!] OCR did not confidently extract a flag.")print("    Open the *_density.png images and read the text manually.")print("    Try:")print(f"      xdg-open {out_dir/'yz_density.png'}")print(f"      xdg-open {out_dir/'xz_density.png'}")finally:
        shutil.rmtree(work, ignore_errors=True)if __name__ =="__main__":
    main()

Usage

If you have 3D.zip

python3 solve_printedparts.py 3D.zip

If you have 3D.gcode

python3 solve_printedparts.py 3D.gcode

Outputs go to:

ls out_printedparts
xdg-open out_printedparts/yz_density.png

If you want only image rendering (no OCR):

python3 solve_printedparts.py 3D.gcode --no-ocr

What you should see / final answer

The text on the flag reads:

0xfun{this_monkey_has_a_flag}

If you want, tell me which projection image (xy/xz/yz) looked clearest on your machine, and I’ll tweak the script defaults to lock onto that view for faster OCR.


메타데이터
post_id
2e7a481c6d68
slug
challenge-printedpart-write-up-2e7a481c6d68
url
https://medium.com/@0xbarood/challenge-printedpart-write-up-2e7a481c6d68
canonical_url
https://medium.com/@0xbarood/challenge-printedpart-write-up-2e7a481c6d68
author_url
https://medium.com/@0xbarood
status
ok
fetched_at
2026-06-23 03:48:11