tres puertas a la misma máquina embrujada
un estándar es un contrato que jura que toda máquina que lo lee, lo lee igual. eso no es verdad, nunca lo fue. DCMK mide el porte de la mentira.
ese es el pitch y le pega distinto a cada uno, así que lo voy a decir tres veces, una por cada persona que esté leyendo. si una de esas personas eres tú, las otras dos secciones igual valen la pena, porque lo entretenido de DCMK es que es la MISMA frase para todos ustedes y ninguno se pone de acuerdo en por qué da miedo. muy on brand.
DCMK es el Differential Conformance Matrix Kraftwerk. agarras un payload que está mal a propósito, lo entregas igual que como lo haría un atacante, anotas lo que hizo cada implementación con él, y después ordenas esas implementaciones en grupos según el lenguaje que cada una acepta de verdad. no el lenguaje que la especificación DICE que aceptan. el que aceptan demostrablemente. no es un fuzzer. un fuzzer le tira ruido a un solo target y espera un crash. DCMK le tira UNA frase cuidadosamente-malula a muchos targets y mira cómo se contradicen. la contradicción es el producto.
DCMK es una máquina de hipótesis. no un fuzzer. nunca un fuzzer.
kraftwerk significa central eléctrica, de paso. mandas bytes que no deberían ser legales y escupe pruebas. la banda es un bonus.
si mantienes un hospital vivo: mismo archivo, dos pacientes
esta es la frase para ti, guardatela.
este archivo DICOM puede que no se lea igual en este PACS y en este otro PACS.
no "puede que no abra." SE LEE DISTINTO. el archive lo ingiere, dice "listo, guardado, todo bien," te devuelve un ACK verdecito, y el objeto en reposo queda archivado bajo un paciente que nunca debió tocar. la modalidad que envía cree que mandó el scan del paciente A. el archive lo guardó bajo el paciente B. las dos máquinas están bien. las dos están "correctas" según su propia lectura de la misma cláusula. nadie tiró un error. esa es la parte que debería hacerte vomitar, porque un error es un regalo. un error es la máquina avisándote que se dio cuenta. esto es la máquina NO dándose cuenta y estando segura de sí misma.
DCMK califica exactamente esto. hay toda un tier de oráculos que no preguntan "sobrevivió el proceso," preguntan "qué hizo el archive con el objeto." re-consulta el archive después del disparo pa probar que el guardado PASÓ, en vez de creerle al ACK. revisa bajo cuál de dos identidades declaradas quedó archivado. la diferencia que está hecha para saber es la diferencia entre "no crasheó" y "archivó piolita una identidad falsa," y si alguna vez manejaste un ambiente clínico ya sabes que esas dos cosas viven a años luz una de otra y todos los dashboards que tienes las aplastan en una sola luz verde.
y tampoco te miente. un oráculo que no pudo observar algo reporta "no pude ver esto," nunca un pass falso y nunca una falsa alarma. si el archive no responde una query, DCMK dice "held, no opinion," no se inventa un veredicto. así que cuando SÍ te dice que dos de tus sistemas no concuerdan, puedes llevárselo al vendor. viene con el número de cláusula pegado. cada caso tiene que citar la línea exacta del estándar que estresa o la herramienta se niega a disparar. llegas con PS3.7 9.3.1.1 y dos productos que la leen de dos formas.
la interoperabilidad siempre fue la promesa. NEMA dice literalmente que conformar no garantiza interoperabilidad, está ahí mismo en PS3.1. DCMK es el instrumento que mide cuánto se alejaron tus sistemas "interoperables" mientras todos miraban la luz verde.
el boucher: mi paciente no es tu paciente
esto no es un experimento mental, lo fui a medir. DICOM hace del Issuer of Patient ID (tag 0010,0021) un calificador OPCIONAL. PS3.4 dice con todas sus letras que el Patient ID "may not be globally unique" y que "may" acotar su unicidad con un issuer. may. no shall. así que algunas implementaciones identifican a un paciente por el Patient ID solo, y otras por el par (Patient ID, Issuer), y las dos están correctísimas según el libro.
ahora pásale a dos de ellas el mismo Patient ID con issuers distintos. una dice "mismo paciente." la otra dice "paciente distinto." empujas esa colisión a través del límite de confianza y te sale un wrong-patient injection sin autenticar: un estudio que yo controlo queda archivado, recuperado y leído bajo la identidad de otra persona, conforme en DIMSE, conforme en DICOMweb, sin auth en el tier de asociación. dos estudios de dos personas mezclados en un solo registro. el radiólogo abre la carpeta y el scan equivocado está ahí adentro, y en ninguna parte saltó un error, porque en el papel nada estaba roto. el estándar hizo esto. yo solo les puse el pico.
y ahora la parte específicamente chilena, la que me hizo agarrarme la cabeza con las dos manos. toda esta weá depende de saber un Patient ID. y hartos hospitales acá simplemente usan el RUT como Patient ID. el RUT. el que está impreso en cada contrato y cada boleta de despacho y cada padrón filtrado, el que le sacas el dígito verificador de cabeza. así que el "secreto" que la injection necesita saber no es un secreto, es infraestructura pública. no lo phisheas y no lo adivinas, lo buscai nomas XD. o sea la colisión no es que me saqué la suerte con dos registros al azar. es que elijo a un ser humano específico por su nombre y le camino el scan a la ficha de un desconocido, a propósito :)
TELL ME PEOPLE, AM I GOING INSANEEEE? INSANEEE?
si rompis weas pa ganarte la pega: deja de pwnear apps, empieza a pwnear gramáticas
ya. wawito, ven para acá. te kiero. lo estás haciendo bien. pero estay siendo entero weta XD
llevas mucho tiempo hackeando implementaciones. un target, un bug, un CVE, un writeup, y después algún vendor tira un parche y tu cosa hermosa se muere y empiezas de nuevo en la siguiente caja. es trabajo honesto. también es MONOHILO y tú eres mejor que eso.
empieza a hackear ESTÁNDARES.
porque un bug en un PACS es un bug en un PACS. pero una ambigüedad en el ESTÁNDAR es un bug en cada implementación de ese estándar que leyó la cláusula ambigua, y no todas la leen igual, lo que significa que un solo payload precisamente-malo es una llave maestra que calza en una cerradura distinta en cada producto sin que cambies un solo byte. ya no estás encontrando un error de código. estás encontrando un lugar donde los autores de la especificación escribieron "shall" sobre algo que nunca fijaron de verdad, y estás juntando la lista de todos los que adivinaron. eso es en masa. un payload, N víctimas, y la "vulnerabilidad" no está en ningún codebase que puedas apuntar, está en el espacio negativo entre las palabras de un PDF de un comité de estándares.
y antes de que alguien salte con "¿shall? ¿qué chucha significa SHALL, lol?", significa algo EXACTO. RFC 2119 definió estas palabras durante el 1997. MUST, SHALL, SHOULD, MAY, cada una es un nivel de obligación definido, no un estado de ánimo. "shall" es un requisito duro. así que cuando una especificación dice shall y dos productos IGUAL no se ponen de acuerdo, eso no es que seas pedante, eso es un requisito definido leído de dos formas. eso no es una zona gris. eso es una escena del crimen.
DCMK es cómo conviertes ese instinto en evidencia. dcmk sweep dispara un payload malo a cada implementación que tengas e imprime la tabla de divergencia, los puntos exactos donde consumidores de los mismos bytes se comportaron distinto. no sale con error cuando hay divergencia. la divergencia es lo que queremos. después dcmk campaign lo escala, muchos casos por muchos targets, resumible, así que puedes correr el corpus entero de noche y leer los escombros en la mañana.
si eres más malo, incluso puedes pwnear un solo estándar, mirar dónde se usa ese estándar (a DICOM le ENCANTA JPEG2000, por ejemplo) y probar los MISMOS payloads, los MISMOS bytes en otras partes. uno de nuestros findings de DICOM estaba a nivel de imagen. y sabis q? funcionó en muchos, muchos procesadores de imágenes. los mismos bytes, ojo. exactamente los mismos bytes :)
el boucher: un hang pre-auth gratis
acá va uno que al atacante no le cuesta casi nada. PS3.8 §9.3.2.2 dice que un presentation context lleva "one or more" transfer syntaxes. ¿entonces qué pasa si mandas uno con CERO? el PDU es consistente en largo, perfectamente bien-formado en el wire, solo semánticamente vacío justo donde la especificación prometió que iba a haber algo. algunos receivers lo meten derechito en un limbo: no reject, no abort, no cierre de socket, y sobre todo no timeout, porque los timers de ACSE y DIMSE recién se arman una vez que la asociación está ESTABLECIDA, y esta nunca llegó ahí. así que la conexión simplemente queda colgada. abierta. pinneando un proceso, un socket, un file descriptor, indefinidamente, antes de que hayas autenticado una sola cosa.
eso es un slowloris. un handshake malformado por cada conexión que sostienes, sin autenticar, y le quitas al archive los sockets que necesita pa hablar con hospitales. hay un primo post-auth también: abres una asociación normal, mandas un C-FIND que promete que viene un Identifier (Command Data Set Type 0102H), y después no mandas nada. mismo hang, mismo slot ocupado. y ojo que el sabor acá es el EXACTO OPUESTO al de patient-id. allá la especificación dijo "may" y todos adivinaron distinto. acá la especificación dijo SHALL, manda toda la Upper Layer state machine de forma normativa en PS3.8, y las implementaciones simplemente no la hacen cumplir. sub-especificado o no-forzado, a nosotros nos da lo mismo. las dos son el estándar dejándote la puerta abierta.
una wea eso si: DCMK no le dispara a nada que no se haya declarado explícitamente como bench desechable. si quieres apuntarlo a prod tienes que pasarle --allow-nonbench y grita al respecto y lo deja anotado. apuntamos la escopeta a máquinas que son nuestras y que estamos dispuestos a perder. mantenlo así, es lo que nos deja al resto seguir haciendo esto a la luz del día.
GOT ONE CHANCE, INFILTRATE THEM. GET IT RIGHT, TERMINATE THEM!
si eres un freak del langsec: N lenguajes donde la especificación prometió uno
ya te enderezaste en la silla cuando viste "differential conformance matrix." lo sé. hola. somos como cuatro.
acá va la forma de la cosa en tus términos. cada implementación es un recognizer. el estándar se supone que define un lenguaje, y cada recognizer conforme se supone que acepta exactamente ese lenguaje. la observación más vieja y más malvada del langsec es que no lo hacen, que un "parser" casi siempre es en secreto un intérprete pa un lenguaje un poco más grande o un poco más raro que el del papel, y cada bug que alguna vez amaste es la brecha entre el lenguaje que se quería y el que de verdad se acepta.
DCMK operacionaliza esa brecha en una partición. construye un vector de respuestas pa cada implementación a lo largo del corpus entero, en orden fijo, y después agrupa las implementaciones cuyos vectores son idénticos. un grupo es un conjunto de recognizers que aceptan exactamente el mismo conjunto de inputs. un lenguaje. y el remate que la herramienta te imprime:
one class means the corpus found a single shared language. more than one means the corpus exhibits N languages where the standard describes one.
esa es la frase clave. es la reformulación Chomsky-ana de "conformar no garantiza interoperabilidad," solo que ahora es un número que puedes citar. y es honesta respecto a la inferencia, que es la parte que amo de verdad. no cuenta grupos y ya, porque las abstenciones inflan la cuenta (dos máquinas que las dos "no miraron" no son el mismo recognizer, solo pestañearon las dos). así que reporta demonstrated, el conjunto más grande de clases que se contradicen de a pares con cero inferencia, un maximum clique exacto sobre el grafo de desacuerdo. ese es el número de lenguajes distintos que DEMOSTRABLEMENTE existen en tus datos. viene flanqueado por consistent_at_most, el techo inferior por coloreo de grafos. demonstrated ≤ consistent_at_most ≤ raw class count, y te va a retar por abstention splits donde una brecha de cobertura se está disfrazando de diferencia de lenguaje.
y el alfabeto mantiene la membresía honesta. A accepted, C crashed (que igual cuenta como DENTRO del lenguaje, porque crashear significa que consumiste los bytes lo suficientemente lejos como pa morir en ellos, eso es membresía no rechazo), R rejected (fuera del lenguaje), y después todo un set de glifos de abstención que se niegan a votar: x declinó la capacidad, ! error del harness, ? corrió pero no llegó a un veredicto, . nunca se disparó. cuatro sabores distintos de "sin opinión" mantenidos rigurosamente aparte, porque confundir "no tengo el decoder" con "nunca corrí esto" es como te mientes a ti mismo con una tabla que se ve limpia. esta es la herramienta siendo pedante A PROPÓSITO y es del buen tipo de pedante, el tipo que significa que la matriz es de verdad verdadera.
el boucher: 22 lenguajes, un estándar
esto lo corrí de verdad, no en juguetes. nueve implementaciones independientes de DICOM, cinco ecosistemas de lenguajes (C/C++, Java, .NET, Python, JavaScript), 38 recognizer endpoints, 178 inputs fabricados, un corpus. y después construí la partición.
el estándar describe un lenguaje. esos 38 endpoints realizan al menos 22. y "al menos 22" no es que esté redondeando pa arriba pal drama, es el piso demonstrated de antes, un maximum clique exacto de endpoints que se contradicen de a pares con cero inferencia. veintidós lenguajes demostrablemente distintos viviendo adentro de la única cosa que jura a pie juntillas que es un solo lenguaje. y acá está la parte que convierte una curiosidad en un paper: cada divergencia se rastreó de vuelta a una cláusula específica sub-especificada que sanciona LAS DOS lecturas. ni un solo bug de vendor en toda la pila. los lenguajes se bifurcan porque la ESPECIFICACIÓN se bifurca. esa es la tesis del langsec, medida, con un número de cláusula grapado a cada cuenta.
TIME IS STANDING STILL, IT'S A DANGEROUS MEETING, THEY'RE GONNA GET THEMSELVES KILLED!
todo junto de una
acá va la cosa que ninguno de los tres puede ver desde adentro de su propia puerta.
es una sola máquina. el forged-identity-at-rest de la persona médica, el one-payload-N-victims de la persona de offsec, el N-lenguajes-donde-debería-haber-uno de la persona de langsec. son el mismo evento descrito en tres vocabularios. la razón por la que un scan queda archivado bajo el paciente equivocado (med) es que dos implementaciones aceptan lenguajes distintos (langsec), que es exactamente la propiedad que un atacante convierte en arma gratis a lo largo de toda una categoría de productos (offsec). la ambigüedad en la cláusula es la vulnerabilidad, es la falla de interop. una grieta, tres nombres.
y DCMK tiene un hermano chico que cierra el círculo, porque DCMK por sí solo es solo el que hace cumplir. tiene a AVK, el Ambiguity Vocoder Kraftwerk, y AVK hace la lectura. AVK agarra la PROSA real del estándar y hace que cuatro parsers de gramática discutan sobre una sola oración, y los pasajes en los que no se pueden poner de acuerdo son las cláusulas lo suficientemente vagas como pa que implementaciones reales se dividan sobre ellas. las vocodea en casos de DCMK a medio escribir. así que AVK lee la especificación y llega corriendo, "están siendo ambiguos por acá, la cláusula 9.3.1.1 le pega a cualquiera que la lea mal," y el hermano grande DCMK se acerca caminando y dispara la bifurcación a implementaciones reales pa probar si de verdad alguien cayó, y después le dice a su hermano chico cuáles matones valían la pena apuntar así la puntería de AVK se afila la próxima vez.
AVK encuentra la bifurcación en el lenguaje. DCMK la prueba en las máquinas. uno lee la promesa, el otro mide la mentira.
no estás hackeando una implementación. nunca fue así. estás hackeando la especificación, y la especificación está sin parchar desde el comité.
el futuro
resulta que si puedes describir una cosa de tantas formas distintas, empieza a ser útil en direcciones que nunca pensaste. eso me pasó con DCMK.
alguien cuyo trabajo básicamente fundó todo este campo me dijo una cosa que reordenó cómo veo mi propia herramienta, y no voy a poner su nombre en mi blog antes de preguntarle, así que vas a tener que confiar en que pesa: "you're recovering the boundaries of competence that real implementations actually exhibit, and that's really powerful."
todo lo que está arriba de esta línea es DCMK como un hacha. la balanceas contra una especificación y encuentras los 22 lugares donde se astilla. pero dala vuelta un minuto. la matriz de desacuerdo no es solo un mapa de dónde se rompen las cosas, es un mapa de dónde todas las implementaciones IGUAL concuerdan. la intersección. el subconjunto del lenguaje que todas aceptan exactamente igual, sin bifurcaciones, sin ambigüedad, sin espacio para adivinar. y un subconjunto así es algo para lo que sí puedes escribir un recognizer estricto. una islita decidible adentro de un océano indecidible. una gramática sobre la que puedes probar cosas, que bota todo lo ambiguo en la puerta antes de que llegue a un parser que habría adivinado mal.
así que DCMK tiene dos filos. es el hacha que usas pa diagnosticar una especificación, y sí, pa hacerla pico, pa probar en público que "conforme" nunca significó una mierda. pero es el mismo hachazo el que te da el bisturí. abres al paciente, ves exactamente cuáles celdas no concuerdan, y recortas el subconjunto seguro que cura la enfermedad. una herramienta, una matriz, y decides al entrar si venías a romper el estándar o a sanarlo.
ese segundo filo es su propio paper, el que estoy escribiendo ahora: "from disease to cure: DCMK, a differential conformance matrix kraftwerk for building decidable subsets of un-recognizable wire languages." el trabajo de imagenología médica fue yo probando que la enfermedad es real. este soy yo intentando pasarle a la gente la cura.
WE'RE CHARGING OUR BATTERY, AND NOW WE'RE FULL OF ENERGY. WE ARE THE ROBOTS.