Challenge: printedpart (Write-up)
1) What is the file?
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
G1lines containing anEvalue
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