Never Gonna Print You Up
Never gonna print you up!
Note: This article's release was coordinated together with Prusa, and was made public at the same time as the official security advisories. Prusa has fixed every finding in this article in Buddy firmware >7.0.0, so if your printer is on the list at the end of this post, update it now. I've also requested CVE assignments for all of the findings, which should land soon. If you know how to speed the assignments up, I'd love to hear about this!
Hey Rick, what are you doing on my printer?!
TL;DR
In August I spent a full week trying to break my MK4S, and it escalated quiiiite a bit: 14 findings, roughly 23 individual defects, and a chain that ends with the printer's main controller running code I sent it over the network. Yup, fully fledged RCE.
This post is about the findings and about the way Prusa handled the whole situation.
A little bit about me and the printer in question
I work as a freelancer in Platform Engineering, SRE and around databases - security research is my 'hobby' on the side (you know, taking a rest after paid computer-based work, with different computer-based work..). I like to break things to learn about the internals.
I participated in CTFs for years (and got massively inspired by LiveOverflow to try to understand reversing and pwning), and more recently I've made a few real findings in production web applications, which I scan proactively for my customers (btw, you can hire me on a project basis!). Firmware and hardware, though, were kinda new to me. So no, before this I had never written a line of Cortex-M assembly, and never had to.
The setup was about as basic as it gets: my own MK4S and the signed upstream image it was running, which was recent back then (6.5.7, build 6.5.7+12836) - and I was armed with Opus 4.8 and a fuzzer.
I started in two places, G-code parsing and Prusa Connect, on the usual theory that you begin looking for vulnerabilities where user-supplied input comes in, and that is also where I found the most serious bugs. More turned up in the USB and FAT path (user-supplied input again) and in the transfer-recovery code, plus some smaller defects on the MMU and EEPROM side.
The method
The week went like this:
- Compiling the firmware's own parser code for the host under AddressSanitizer, so that any crash sits in Prusa's shipped code and not in my glue.
- Standing up a small Cortex-M emulator that executes the real signed image with a corresponding memory map, so I could drive the chain and prove the control flow before touching the printer and giving it a poisoned USB stick, you know.. I'm lazy. :-)
- Running millions of mutated iterations against the G-code path and also millions of mutated HTTP requests against PrusaLink / Connect.
- Doing A LOT of reverse engineering against the real compiled build, and running multiple source audits in parallel, each of which had to record the files it did not open.
The hardest part by a considerable distance was the control-flow primitive, where I had a pile of candidate dispatch sites that looked usable, and exactly one of them actually worked for the RCE. The rest got ruled out one at a time, and most (not all of it though!) of that ruling-out was mechanical work I could hand off and check.
The disclosure packages I sent Prusa had the whole worktree I'd built, including the methods I'd used, scripts etc. Every trace of that method passed over to them on purpose, because even though a vendor needs the defect and the line number, I felt it might've been beneficial to give them the framework I built for this whole campaign and enable the devs to repeat my process, if they want to (and I hope they do!).
I wanted to make a clear difference and build trust that what I found was actually real (more on that in a bit..) and is reproducible, hence the separation of the disclosure packages. I still got enough confidence that the findings in the second (source-verified) collection were real though, after manually reviewing them.
What I found and reported
I sent Prusa two packages. The first held four findings, three of which are confirmed on the physical printer, both from crash dumps the printer wrote itself and from a complete takeover of the controller. The fourth, an off-by-one on the MMU side, is source-verified only (more on that one below). The second package held the other ten, which are confirmed under AddressSanitizer or by source review only.
There's actually some reasoning behind this decision: so that my confidence levels between the types of findings would not "blur" into each other. A bug I've seen crash (or exploit..) my own hardware and a bug I reasoned my way to in the upstream source tree are not really on the same level. Packaging them together might have led Prusa to over-trust the weak ones or under-trust the strong ones, or discard them altogether at worst, so it made a lot of sense to draw a line between them.
Every finding still came with a complete write-up (that I already cleaned up by a LOT, but the sheer amount still created pain on Prusa's side - see below), plus PoC code to exploit each one, if applicable. And with Prusa now going public, I'll be releasing all of them as well. But let's discuss the most serious one first, while keeping it high-level. The actual advisories hold the full write-ups though:
Oops, I got RCE
Release builds drop the 104-byte path assert, so a 463-byte path makes the filename write overflow a shared 512-byte buffer onto the planner's printer pointer, confirmed on a physical MK4S.
This one is in Prusa Connect, the cloud (or self-hosted) service the printer talks to if you want it to, for managing your printer, dispatching prints and so on. When Connect sends a SEND_FILE_INFO command, the controller's handler copies the command's path into a shared 512-byte buffer, and it bounds that copy by the size of the buffer.
The problem is that the buffer holds more than the path, because the filename goes into the same buffer immediately after it, and the code that reaches for the filename finds it by walking off the end of the path string. That arrangement only works if the path is capped at 104 bytes.
Yep, you might have caught the issue already: nothing caps it at 104 bytes. The only thing enforcing that assumption is an assert(), and since release builds compile asserts out under NDEBUG, a long enough path pushes the filename slot off the end of the buffer and onto whatever is next to it.
// src/common/shared_buffer.hpp:102-108
char *name(){
char *path = this->path();
size_t plen = strlen(path);
assert(plen
What ships today
Prusa's fixes shipped with Buddy firmware 7.0.0 (with 8.0.0 being the most recent one, yes, I think too that the new version numbering system is a bit confusing), and every finding in this post affects all fifteen printers it covers: the CORE One L+, CORE One L, CORE One+ (Gen 2) INDX, CORE One INDX, CORE One+ (Gen 2), CORE One, XL+, XL, MK4S, MK4, MK3.9S, MK3.9, MK3.5S, MK3.5 and MINI. If yours is on that list, update it.
Prusa published its own advisory and source release on GitHub today, and says it is not aware of any real-world exploitation, except for my printer, which got rick-rolled. That source release is the v7.0.0 release tag, which holds the 7.0.0 sources. The full write-ups for all fourteen findings are published with the fix and linked from each finding above.
3D printers are not toys: they have network stacks, cloud integrations, USB hosts, filesystem parsers, signature-checking paths and microcontrollers talking over a serial bus, and they are bolted to heaters and stepper motors in someone's house (so there's a certain risk involved, which is why it makes a lot of sense to have the issues patched). I am not surprised that they have defects, because everything does. Let's make them as safe as we can!
What still surprises me is the response I got. I have now done this twice publicly, and once I got a response from a law firm and a criminal-code citation (lol), while the other time I got proper engineers who reproduced/validated my work and asked how I would like my name spelled for the credits and even showed their appreciation for this work with a reward (though I just did it to learn something): there are worlds in between. Only one of those two responses makes the next researcher pick up the phone.
Thanks for the great time!
Cheers, Y
Comments
No comments yet. Start the discussion.