← back

three doors into the same haunted machine

a standard is a contract that swears every machine reading it reads the same thing. it is lying. it has always been lying. DCMK measures the size of the lie.

that's the pitch and it lands differently depending on who you are, so i'm gonna say it three times, once per person reading this. if one of those people is you, the other two sections are still worth it, because the thing that makes DCMK fun is that it's the SAME sentence to all of you and none of you agree on why it's scary. very on brand.

DCMK is the Differential Conformance Matrix Kraftwerk. you take a payload that is wrong on purpose, you deliver it the way an attacker would actually deliver it, you write down what every implementation did about it, and then you sort those implementations into groups by the language each one actually accepts. not the language the spec SAYS they accept. the one they demonstrably do. it is not a fuzzer. a fuzzer throws noise at one target and waits for a crash. DCMK throws one carefully-wrong sentence at many targets and watches them disagree. the disagreement is the product.

DCMK is a hypothesis machine. not a fuzzer. never a fuzzer.

kraftwerk means power plant, by the way. you feed it bytes that shouldn't be legal and it outputs proof. the band is a bonus.

if you keep a hospital alive: same file, two patients

here is the sentence for you, and i want you to sit in it for a second.

this one DICOM file may not be read the same way by this PACS and that PACS.

not "may fail to open." READ DIFFERENTLY. the archive ingests it, says "yep, stored, all good," returns you a nice green acknowledgement, and the object at rest is filed under a patient it was never meant to touch. the sending modality thinks it sent scan for patient A. the archive filed it under patient B. both machines conformed. both machines are "correct" by their own reading of the same clause. nobody threw an error. that's the part that should make your stomach drop, because an error is a gift. an error is the machine telling you it noticed. this is the machine NOT noticing and being confident about it.

DCMK grades exactly this. there's a whole tier of oracles that don't ask "did the process survive," they ask "what did the archive actually do with the object." it re-queries the archive after the shot to prove storage happened instead of trusting the acknowledgement. it checks which of two declared identities the thing got filed under. the difference it's built to catch is the difference between "it didn't crash" and "it silently filed a forged identity," and if you've ever run a clinical environment you already know those two things live light-years apart and every dashboard you own collapses them into one green light.

and it never lies to you the other direction either. an oracle that couldn't see something reports "i couldn't see this," never a fake pass and never a fake familiar false alarm. if the archive won't answer a query, DCMK says "held, no opinion," it does not invent a verdict. so when it DOES tell you two of your systems disagree, you can take that to the vendor. it comes with the clause number attached. every case has to cite the exact line of the standard it's stressing or the tool refuses to fire. you're not showing up with a vibe. you're showing up with PS3.7 9.3.1.1 and two products that read it two ways.

interop was always the promise. NEMA literally says conformance doesn't guarantee interoperability, it's right there in PS3.1. DCMK is the instrument that measures how far apart your "interoperable" systems drifted while everyone was looking at the green light.

the receipt: my patient is not your patient

this isn't a thought experiment, i went and measured it. DICOM makes the Issuer of Patient ID (tag 0010,0021) an OPTIONAL qualifier. PS3.4 says in as many words that the Patient ID "may not be globally unique" and that you "may" scope its uniqueness with an issuer. may. not shall. so some implementations key a patient on the Patient ID alone, and some key on the (Patient ID, Issuer) pair, and both are dead correct by the book.

now hand two of them the same Patient ID with different issuers. one says "same patient." the other says "different patient." push that collision across the boundary and you get an unauthenticated wrong-patient injection: a study i control gets filed, retrieved, and read under someone else's identity, conformant on DIMSE, conformant on DICOMweb, no auth needed on the association tier. two people's studies commingled into one record. the radiologist opens the folder and the wrong scan is sitting in it, and nothing anywhere threw an error, because on paper nothing was broken. the standard did this. i just pointed at it.

and now the specifically chilean part, the one that made me put my head in my hands. this whole thing keys on knowing a Patient ID. and a lot of hospitals here just use the RUT as the Patient ID. the RUT. our national number, the one printed on every contract and every delivery slip and every leaked padrón, the one whose check digit you can work out in your head. so the "secret" the injection needs to know is not a secret, it's public infrastructure. you don't phish it and you don't guess it, you look it up. which means the collision isn't me getting lucky with two random records. it's me picking a specific human being by name and walking their scan into a stranger's chart on purpose. públicamente disponible :)

TELL ME PEOPLE, AM I GOING INSANEEEE? INSANEEE?

if you break things for a living: stop pwning apps, start pwning grammar

ok. little bro. come here. i love you. you're doing great. you're also working way too hard.

you've been hacking implementations. one target, one bug, one CVE, one writeup, and then some vendor ships a patch and your beautiful thing is dead and you start over on the next box. it's honest work. it's also SINGLE THREADED and you are better than that.

start hacking the standards.

because a bug in one PACS is a bug in one PACS. but an ambiguity in the STANDARD is a bug in every implementation of that standard that read the ambiguous clause the wrong way, and they don't all read it the same way, which means one carefully-wrong payload is a skeleton key that fits a different lock in every product without you changing a single byte. you're not finding a coding mistake anymore. you're finding a place where the spec authors wrote "shall" over something they never actually pinned down, and you're collecting the list of everyone who guessed. that's mass. one payload, N victims, and the "vulnerability" isn't in any codebase you can point at, it's in the negative space between the words of a PDF from a standards committee.

and before someone goes "shall? what does SHALL even mean lol," it means something EXACT. RFC 2119 nailed these words to the wall back in 1997. MUST, SHALL, SHOULD, MAY, each one is a defined obligation level, not a mood. "shall" is a hard requirement. so when a spec says shall and two products STILL disagree about it, that's not you being pedantic, that's a defined requirement getting read two ways in the wild. that's not a grey area. that's a crime scene with the yellow tape already up.

DCMK is how you turn that instinct into evidence. dcmk sweep fires one wrong payload at every implementation you've got and prints the divergence table, the exact spots where consumers of identical bytes behaved differently. it doesn't exit with an error on divergence. divergence is what you came for. then dcmk campaign scales it, many cases across many targets, resumable, so you can run the whole corpus overnight and read the wreckage in the morning.

if you're meaner, you could even pwn a single standard, check where that standard is used (DICOM loooooves JPEG2000, for example) and try using the SAME payloads, the SAME bytes in other places. one of our DICOM findings was at the image level. and you know what? it worked across many, many image processors. the same bytes, mind you. same exact bytes :)

the receipt: a free pre-auth hang

here's one that costs the attacker basically nothing. PS3.8 §9.3.2.2 says a presentation context carries "one or more" transfer syntaxes. so what happens if you send one with ZERO? the PDU is length-consistent, perfectly well-formed on the wire, just semantically empty exactly where the spec promised there'd be something. some receivers drive that straight into a limbo: no reject, no abort, no socket close, and crucially no timeout, because the ACSE and DIMSE timers only arm once an association is ESTABLISHED, and this one never got there. so the connection just hangs. held open. pinning a process, a socket, a file descriptor, indefinitely, before you have authenticated a single thing.

that's a slowloris. one malformed handshake per held connection, unauthenticated, and you starve the archive of the sockets it needs to talk to actual hospitals. there's a post-auth cousin too: open a normal association, send a C-FIND that promises an Identifier is coming (Command Data Set Type 0102H), then send nothing. same hang, same held slot. and notice the flavor here is the exact OPPOSITE of the patient-id one. there the spec said "may" and everyone guessed differently. here the spec said SHALL, it mandates the whole Upper Layer state machine normatively in PS3.8, and implementations just don't enforce it. under-specified or unenforced, doesn't matter to you on the outside. both are the standard holding the door open.

there is a leash, and it's load-bearing, not decoration. DCMK will not fire at anything that hasn't explicitly declared itself a disposable bench. you want to point it at prod you have to pass --allow-nonbench and it screams about it and writes it down. this is security research, we point the cannon at machines we own and are willing to lose. keep it that way, it's what lets the rest of us keep doing this in the open.

GOT ONE CHANCE, INFILTRATE THEM. GET IT RIGHT, TERMINATE THEM!

if you're a langsec freak: N languages where the spec promised one

you already sat up straight when you saw "differential conformance matrix." i know. hi. there are like four of us.

here's the shape of it in your terms. every implementation is a recognizer. the standard is supposed to define one language, and every conforming recognizer is supposed to accept exactly that language. langsec's oldest and meanest observation is that they don't, that a "parser" is almost always secretly an interpreter for a language slightly bigger or slightly weirder than the one on paper, and every bug you've ever loved is the gap between the intended language and the accepted one.

DCMK operationalizes that gap into a partition. it builds an answer vector for each implementation across the whole corpus, in fixed order, then groups the implementations whose vectors are identical. a group is a set of recognizers that accept exactly the same set of inputs. one language. and the punchline the tool prints for you:

one class means the corpus found a single shared language. more than one means the corpus exhibits N languages where the standard describes one.

that's the money line. that's the Chomsky-brained restatement of "conformance doesn't guarantee interoperability," except now it's a number you can quote. and it's honest about inference, which is the part i genuinely love. it doesn't just count groups, because abstentions inflate the count (two machines that both "didn't look" aren't the same recognizer, they just both blinked). so it reports demonstrated, the largest set of classes that pairwise disagree with zero inference, an actual maximum clique over the disagreement graph. that's the number of distinct languages that PROVABLY exist in your data. it's flanked by consistent_at_most, the graph-colouring lower ceiling. demonstrated ≤ consistent_at_most ≤ raw class count, and it will yell at you about abstention splits where a coverage gap is cosplaying as a language difference.

and the alphabet keeps membership honest. A accepted, C crashed (which still counts as IN the language, because crashing means you consumed the bytes far enough to die on them, that's membership not refusal), R rejected (out of the language), and then a whole set of abstention glyphs that refuse to vote: x declined the capability, ! harness error, ? ran but reached no verdict, . never fired. four different flavours of "no opinion" kept rigorously apart, because conflating "i lack the decoder" with "i never ran this" is how you lie to yourself with a clean-looking table. this is the tool being pedantic on PURPOSE and it is the good kind of pedantic, the kind that means the matrix is actually true.

the receipt: 22 languages, one standard

i ran this for real, not on toys. nine independent DICOM implementations, five language ecosystems (C/C++, Java, .NET, Python, JavaScript), 38 recognizer endpoints, 178 crafted inputs, one corpus. then i built the partition.

the standard describes one language. those 38 endpoints realize at least 22. and "at least 22" isn't me rounding up for drama, it's the demonstrated floor from before, an exact maximum clique of endpoints that pairwise disagree with zero inference. twenty-two provably distinct languages living inside the one thing that swears up and down it's a single language. and here's the part that turns a curiosity into a paper: every last divergence traced back to a specific under-constrained clause that sanctions BOTH readings. not one vendor bug in the whole pile. the languages fork because the SPEC forks. that's the langsec thesis, measured, with a clause number stapled to every count.

TIME IS STANDING STILL, IT'S A DANGEROUS MEETING, THEY'RE GONNA GET THEMSELVES KILLED!

all of it at once

here's the thing none of the three of you can see from inside your own door.

it's one machine. the med person's forged-identity-at-rest, the offsec person's one-payload-N-victims, the langsec person's N-languages-where-there-should-be-one. those are the same event described in three vocabularies. the reason a scan gets filed under the wrong patient (med) is that two implementations accept different languages (langsec), which is exactly the property an attacker weaponizes for free across an entire product category (offsec). the ambiguity in the clause is the vulnerability is the interop failure. one crack, three names.

and DCMK has a little brother that makes the loop close, because DCMK on its own is just the enforcer. it has AVK, the Ambiguity Vocoder Kraftwerk, and AVK does the reading. AVK takes the standard's actual PROSE and makes four grammar parsers argue about a single sentence, and the passages they can't agree on are the clauses vague enough that real implementations would split on them. it vocodes those into half-written DCMK cases. so AVK reads the spec and comes running, "they're being ambiguous over here, clause 9.3.1.1 bullies anyone who reads it wrong," and big bro DCMK walks over and fires the fork at real implementations to prove whether anyone actually fell for it, then tells its little brother which bullies were worth pointing at so AVK's aim gets sharper next time.

AVK finds the fork in the language. DCMK proves it in the machines. one reads the promise, the other measures the lie.

you're not hacking an implementation. you never were. you're hacking the spec, and the spec has been unpatched since committee.

the future

turns out that if you can describe a thing this many different ways, it starts being useful in directions you never aimed it. that happened to me with DCMK.

someone whose work basically founded this whole field said a thing to me that rearranged how i see my own tool, and i'm not going to put her name on my blog before i've asked her, so you'll just have to trust that it carries weight: "you're recovering the boundaries of competence that real implementations actually exhibit, and that's really powerful."

everything above this line is DCMK as an axe. you swing it at a spec and you find the 22 places it splinters. but turn it around for a minute. the disagreement matrix isn't only a map of where things break, it's a map of where every implementation still AGREES. the intersection. the subset of the language that all of them accept the exact same way, no forks, no ambiguity, no room to guess. and a subset like that is something you can actually write a strict recognizer for. a decidable little island inside an undecidable ocean. a grammar you can prove things about, that throws out everything ambiguous at the door before it ever reaches a parser that would have guessed wrong.

so DCMK has two edges. it's the axe you use to diagnose a spec, and yeah, to destroy it, to prove in public that "conformant" was never a promise. but it's the same swing that gives you the scalpel. you open the patient up, you see exactly which cells disagree, and you carve out the safe subset that cures the disease. one tool, one matrix, and you decide on the way in whether you came to break the standard or to heal it.

that second edge is its own paper, the one i'm actually writing now: "from disease to cure: DCMK, a differential conformance matrix kraftwerk for building decidable subsets of un-recognizable wire languages." the medical imaging work was me proving the disease is real. this one is me trying to hand people the cure.

WE'RE CHARGING OUR BATTERY, AND NOW WE'RE FULL OF ENERGY. WE ARE THE ROBOTS.