{"id":921,"date":"2026-09-08T18:47:46","date_gmt":"2026-09-08T16:47:46","guid":{"rendered":"https:\/\/www.blackhats.es\/wordpress\/?p=921"},"modified":"2026-09-08T20:11:01","modified_gmt":"2026-09-08T18:11:01","slug":"resucitar-un-hsm-de-un-fabricante-desaparecido-manejar-un-spyrus-lynks-series-ii-desde-linux-byte-a-byte","status":"publish","type":"post","link":"https:\/\/www.blackhats.es\/wordpress\/?p=921","title":{"rendered":"Resucitar un HSM de un fabricante desaparecido: manejar un SPYRUS LYNKS Series II desde Linux, byte a byte"},"content":{"rendered":"<!-- ============================================================\r\n     hs4l blog post \u2014 paste this whole file into a WordPress\r\n     \"Custom HTML\" block (or the Code editor). Inline SVGs need an\r\n     account with unfiltered_html (any administrator has it).\r\n     ============================================================ -->\r\n<div class=\"hs4l-post\">\r\n<p class=\"lede\">Un m\u00f3dulo de seguridad hardware FIPS 140-2 de 2005. Sin driver. Sin middleware. Sin PIN. Un fabricante que ya no existe. Esta es la historia completa de c\u00f3mo consegu\u00ed que generase una clave y firmase un mensaje dentro del chip desde una m\u00e1quina Linux x86-64 moderna, contada al nivel al que realmente trabaj\u00e9: descriptores USB, cabeceras ELF, desensamblado, bloques de comando big-endian, bytes DER y aritm\u00e9tica modular. Al final hay un peque\u00f1o proyecto de c\u00f3digo abierto, <strong>hs4l<\/strong>, para que nadie tenga que repetir el viaje.<\/p>\r\n<div class=\"kv\"><b>Dispositivo: <\/b>SPYRUS LYNKS Series II, USB <code>08df:0a00<\/code>, referencia 3003-F0 (2005), sistema operativo SPYCOS<br \/><b>Validaci\u00f3n: <\/b>FIPS 140-2 nivel 2, certificado NIST CMVP #679<br \/><b>Host: <\/b>Fedora x86-64, kernel 7.1, OpenSSL 3.5, qemu-user ARM de 32 bits<br \/><b><\/b><code><\/code><\/div>\r\n<h3>Contenido<\/h3>\r\n<ol class=\"toc\">\r\n<li><a href=\"#h-object\">El objeto sobre la mesa<\/a><\/li>\r\n<li><a href=\"#h-doors\">Todas las puertas cerradas<\/a><\/li>\r\n<li><a href=\"#h-lateral\">El movimiento lateral: \u00bfqui\u00e9n m\u00e1s habla con este chip?<\/a><\/li>\r\n<li><a href=\"#h-firmware\">Leer el firmware<\/a><\/li>\r\n<li><a href=\"#h-bridge\">C\u00f3digo ARM, anfitri\u00f3n x86-64, USB real<\/a><\/li>\r\n<li><a href=\"#h-contact\">Primer contacto, en el cable<\/a><\/li>\r\n<li><a href=\"#h-own\">Apropiarse de la tarjeta: reinicializaci\u00f3n<\/a><\/li>\r\n<li><a href=\"#h-wall\">El muro: <code>0x0a<\/code> Execution Failure<\/a><\/li>\r\n<li><a href=\"#h-rootcause\">La causa ra\u00edz real, demostrada de tres maneras<\/a><\/li>\r\n<li><a href=\"#h-crypto\">La criptograf\u00eda, byte a byte<\/a><\/li>\r\n<li><a href=\"#h-native\">Extra: una compilaci\u00f3n nativa x86-64, con su propio bug<\/a><\/li>\r\n<li><a href=\"#h-testing\">C\u00f3mo lo prob\u00e9<\/a><\/li>\r\n<li><a href=\"#h-hs4l\">Por qu\u00e9 constru\u00ed hs4l y qu\u00e9 contiene<\/a><\/li>\r\n<li><a href=\"#h-downloads\">Descargas y firmas<\/a><\/li>\r\n<\/ol>\r\n<!-- ============================================================ -->\r\n<h2 id=\"h-object\">1. El objeto sobre la mesa<\/h2>\r\n<p>\u00abRA\u00bb, referencia 3003-F0. Dentro hay un m\u00f3dulo criptogr\u00e1fico del linaje Fortezza que ejecuta <strong>SPYCOS<\/strong>, el SPYRUS Cryptographic Operating System, validado a FIPS 140-2 nivel 2. Dos roles lo custodian: un Site Security Officer (el oficial criptogr\u00e1fico) y un User. Cada uno tiene una frase PIN de al menos doce caracteres de un alfabeto de 93 s\u00edmbolos. Diez inicios de sesi\u00f3n SSO fallidos consecutivos lo ponen a cero.<\/p>\r\n<p>No quer\u00eda lo que hubiera dentro. Quer\u00eda <em>hacerlo m\u00edo<\/em>: borrarlo, fijar mis propios PIN, meterle una clave y hacer que firmase. Es una funci\u00f3n documentada del m\u00f3dulo. Result\u00f3 ser la parte f\u00e1cil. La parte dif\u00edcil era que en 2026 no hay software en la Tierra que hable con \u00e9l, o eso parec\u00eda.<\/p>\r\n<pre><a href=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/01-23-.png\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-932 aligncenter\" src=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/01-23-.png\" alt=\"\" width=\"960\" height=\"628\" srcset=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/01-23-.png 960w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/01-23--300x196.png 300w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/01-23--768x502.png 768w\" sizes=\"(max-width: 960px) 100vw, 960px\" \/><\/a><\/pre>\r\n<p>Hay que leer ese descriptor como lo lee el kernel. USB 1.1, full speed, una interfaz, <strong>clase 0<\/strong>. No es CCID (clase 0x0b), no es HID, no es almacenamiento masivo. Dos endpoints bulk, uno en cada direcci\u00f3n, paquetes de 64 bytes. Sin cadena de n\u00famero de serie. Es una tuber\u00eda cruda del fabricante: el dispositivo har\u00e1 exactamente lo que le diga un programa que conozca su protocolo privado, y nada m\u00e1s.<\/p>\r\n<pre><a href=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/01-27-.png\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-933 aligncenter\" src=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/01-27-.png\" alt=\"\" width=\"960\" height=\"216\" srcset=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/01-27-.png 960w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/01-27--300x68.png 300w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/01-27--768x173.png 768w\" sizes=\"(max-width: 960px) 100vw, 960px\" \/><\/a><\/pre>\r\n<p>Ning\u00fan driver del kernel reclama la interfaz 0. Nada en la tabla de alias de m\u00f3dulos conoce la cadena \u00abspyrus\u00bb. Conviene recordar esa ausencia de driver: parece una mala noticia y en realidad es el \u00fanico hecho que hizo posible todo lo dem\u00e1s.<\/p>\r\n<!-- ============================================================ -->\r\n<h2 id=\"h-doors\">2. Todas las puertas cerradas<\/h2>\r\n<p><a href=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/01-33-.png\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-935 aligncenter\" src=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/01-33-.png\" alt=\"\" width=\"960\" height=\"700\" srcset=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/01-33-.png 960w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/01-33--300x219.png 300w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/01-33--768x560.png 768w\" sizes=\"(max-width: 960px) 100vw, 960px\" \/><\/a><\/p>\r\n<h3>Puerta 1: el fabricante<\/h3>\r\n<p>SPYRUS Inc. fue absorbida por otra empresa el 15 de septiembre de 2021. El portal de desarrolladores ha desaparecido a nivel de DNS:<\/p>\r\n<pre><a href=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/01-37-.png\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-936 aligncenter\" src=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/01-37-.png\" alt=\"\" width=\"960\" height=\"122\" srcset=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/01-37-.png 960w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/01-37--300x38.png 300w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/01-37--768x98.png 768w\" sizes=\"(max-width: 960px) 100vw, 960px\" \/><\/a><\/pre>\r\n<p>Lo que responde hoy en el dominio antiguo es una p\u00e1gina aparcada detr\u00e1s de un CDN de hosting compartido. La empresa sucesora tiene un formulario de contacto. Lo rellen\u00e9. Nada.<\/p>\r\n<h3>Puerta 2: el middleware de Windows<\/h3>\r\n<p>El token se gestionaba con una aplicaci\u00f3n de Windows llamada En-Sign, con un SDK SPEX+ que proporcionaba un CSP y un m\u00f3dulo PKCS#11. Todo se distribu\u00eda detr\u00e1s de un muro de registro. La Wayback Machine nunca rastre\u00f3 m\u00e1s all\u00e1 de ese muro. <code>archive.org\/details\/Spyrus<\/code> contiene un GIF con el logotipo. El \u00fanico artefacto que sobrevivi\u00f3 en la web p\u00fablica es <code>SCARDLYNKSUSBW.sys<\/code> con su <code>.inf<\/code> y su <code>.cat<\/code>, firmado WHQL en 2005, todav\u00eda en el Microsoft Update Catalog y en sitios agregadores de drivers como un zip de 20 KB. Lo desmont\u00e9. Es un <em>shim de lector<\/em>: hace que Windows presente el token como un lector de tarjetas inteligentes para que <em>otro<\/em> software pueda manejarlo. Por s\u00ed solo no hace nada. Una m\u00e1quina virtual con Windows 7 y paso USB de <code>08df:0a00<\/code> lleg\u00f3 exactamente hasta \u00ablector presente, ning\u00fan software instalado que entienda la tarjeta\u00bb.<\/p>\r\n<h3>Puerta 3: Linux<\/h3>\r\n<p>El mundo PC\/SC no puede verlo, porque no es un lector CCID:<\/p>\r\n<p><code><a href=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/01-41-.png\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-937 aligncenter\" src=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/01-41-.png\" alt=\"\" width=\"960\" height=\"103\" srcset=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/01-41-.png 960w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/01-41--300x32.png 300w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/01-41--768x82.png 768w\" sizes=\"(max-width: 960px) 100vw, 960px\" \/><\/a>\r\n<\/code><\/p>\r\n<p><code><\/code>Nunca ha existido un driver Linux para el Series II. Nunca ha existido un driver macOS. No hay ning\u00fan m\u00f3dulo PKCS#11 para \u00e9l fuera del SDK perdido.<\/p>\r\n<h3>Puerta 4: el PIN<\/h3>\r\n<p>Doce caracteres como m\u00ednimo de 93 s\u00edmbolos: 93<sup>12<\/sup> \u2248 4,2 \u00d7 10<sup>23<\/sup>. Diez intentos SSO fallidos ponen la tarjeta a cero. Incluso con un protocolo en la mano, la fuerza bruta no es un plan; es una forma de convertir el token en un pisapapeles de diez en diez intentos. La frase \u00abZeroize Default PIN\u00bb de f\u00e1brica que permite recuperar una tarjeta puesta a cero est\u00e1 en una gu\u00eda de administrador propietaria que no pude encontrar.<\/p>\r\n<h3>Puerta 5: sondeo a ciegas<\/h3>\r\n<p>Con libusb se puede reclamar una interfaz sin driver y escribir bytes arbitrarios en EP1. Lo intent\u00e9. La palabra de estado con la que se responde a cada comando empieza por un byte que parec\u00eda <code>0x80<\/code>, y m\u00e1s all\u00e1 de eso no ten\u00eda ni formato de trama, ni lista de opcodes, ni regla de checkword. Cada intento devolv\u00eda una respuesta corta que no sab\u00eda interpretar. Hacer fuzzing a ciegas a un m\u00f3dulo FIPS con contador de intentos fallidos no es una afici\u00f3n que recomiende.<\/p>\r\n<h3>Puerta 6: la gente<\/h3>\r\n<p>Pregunt\u00e9 en dos foros. La respuesta m\u00e1s \u00fatil fue comprensiva y correcta: \u00abel software es vapor\u00bb.<\/p>\r\n<div class=\"callout red\"><strong>Semanas despu\u00e9s, el balance:<\/strong> un dispositivo que Linux puede enumerar pero con el que no puede hablar, un fabricante que no existe, un middleware que nunca se archiv\u00f3, un espacio de PIN mayor que el n\u00famero de estrellas del universo observable, y un contador de intentos fallidos. Todas las puertas que se me hab\u00edan ocurrido estaban cerradas.<\/div>\r\n<!-- ============================================================ -->\r\n<h2 id=\"h-lateral\">3. El movimiento lateral: \u00bfqui\u00e9n m\u00e1s habla con este chip?<\/h2>\r\n<p>La pregunta que hab\u00eda estado haciendo era \u00ab\u00bfd\u00f3nde est\u00e1 el software del fabricante?\u00bb. La pregunta correcta era otra: <strong>\u00bfqui\u00e9n m\u00e1s, en cualquier sitio, en cualquier sistema operativo, distribuye c\u00f3digo que hable SPYCOS?<\/strong><\/p>\r\n<p>Un token DSA validado FIPS de mediados de los 2000 existe por una raz\u00f3n: firmar flujos de datos cuya autenticidad tiene peso legal. El \u00fanico \u00e1mbito que se me ocurr\u00eda donde eso es obligatorio y p\u00fablico es la vigilancia del tratado de prohibici\u00f3n de ensayos nucleares. Las estaciones s\u00edsmicas que alimentan el Sistema Internacional de Vigilancia firman sus datos en el formato CD1.1, con claves hardware, y los digitalizadores que lo hacen los construyen un pu\u00f1ado de fabricantes de sism\u00f3grafos. Uno de ellos documenta, en un manual de usuario del firmware de su digitalizador Platinum, una herramienta de l\u00ednea de comandos llamada <code>spyrus_util<\/code> que inicializa una tarjeta \u00abSpyrus Lynks\u00bb y firma con ella. Sus digitalizadores ejecutan <strong>Linux embebido sobre ARM<\/strong>.<\/p>\r\n<p>Si esa herramienta exist\u00eda como binario, el protocolo vendr\u00eda con ella. Y exist\u00eda, en un espejo rsync p\u00fablico y an\u00f3nimo que sirve sistemas de ficheros ra\u00edz completos del firmware del digitalizador:<\/p>\r\n<pre><a href=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/01-48-.png\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-938 aligncenter\" src=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/01-48-.png\" alt=\"\" width=\"960\" height=\"590\" srcset=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/01-48-.png 960w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/01-48--300x184.png 300w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/01-48--768x472.png 768w\" sizes=\"(max-width: 960px) 100vw, 960px\" \/><\/a><\/pre>\r\n<p>\u00abctbto-prerelease\u00bb es la Organizaci\u00f3n del Tratado de Prohibici\u00f3n Completa de los Ensayos Nucleares. La corazonada era correcta. Se descarga el sistema de ficheros ra\u00edz ARM, o solo las piezas que necesita la herramienta:<\/p>\r\n<pre><a href=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/01-55-.png\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-939 aligncenter\" src=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/01-55-.png\" alt=\"\" width=\"960\" height=\"196\" srcset=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/01-55-.png 960w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/01-55--300x61.png 300w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/01-55--768x157.png 768w\" sizes=\"(max-width: 960px) 100vw, 960px\" \/><\/a><\/pre>\r\n<p>Obs\u00e9rvense las dos bibliotecas que viven en <code>lib\/<\/code> en lugar de en <code>usr\/lib\/<\/code>. Que faltasen me cost\u00f3 una hora m\u00e1s adelante; el cargador din\u00e1mico solo las reporta como ausentes despu\u00e9s de haber encontrado todo lo dem\u00e1s.<\/p>\r\n<!-- ============================================================ -->\r\n<h2 id=\"h-firmware\">4. Leer el firmware<\/h2>\r\n<p>Antes de ejecutar nada: \u00bfqu\u00e9 es este binario, para qu\u00e9 CPU, y c\u00f3mo llega al hardware?<\/p>\r\n<pre><a href=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/01-58-.png\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-940 aligncenter\" src=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/01-58-.png\" alt=\"\" width=\"960\" height=\"271\" srcset=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/01-58-.png 960w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/01-58--300x85.png 300w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/01-58--768x217.png 768w\" sizes=\"(max-width: 960px) 100vw, 960px\" \/><\/a><\/pre>\r\n<p>ARM de 32 bits little-endian, EABI versi\u00f3n 5, compilado para el juego de instrucciones ARMv4T, de modo que corre en un n\u00facleo de la clase ARM9TDMI. La ruta del m\u00f3dulo del kernel en el listado, <code>2.6.36-cm-x270<\/code>, dice que el digitalizador es un m\u00f3dulo CompuLab CM-X270, un Intel PXA270 XScale. El binario se construy\u00f3 con GCC 4.4.4 contra glibc 2.12.2 y <strong>no est\u00e1 stripped<\/strong>, con informaci\u00f3n de depuraci\u00f3n DWARF. Ese \u00faltimo detalle vale cien horas de conjeturas:<\/p>\r\n<p><code><a href=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/02-04-.png\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-941 aligncenter\" src=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/02-04-.png\" alt=\"\" width=\"960\" height=\"84\" srcset=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/02-04-.png 960w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/02-04--300x26.png 300w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/02-04--768x67.png 768w\" sizes=\"(max-width: 960px) 100vw, 960px\" \/><\/a>\r\n<\/code><\/p>\r\n<p>Ahora las dependencias, que dicen <em>c\u00f3mo<\/em> llega un programa al hardware:<\/p>\r\n<pre><a href=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/02-07-.png\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-942 aligncenter\" src=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/02-07-.png\" alt=\"\" width=\"960\" height=\"590\" srcset=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/02-07-.png 960w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/02-07--300x184.png 300w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/02-07--768x472.png 768w\" sizes=\"(max-width: 960px) 100vw, 960px\" \/><\/a><\/pre>\r\n<p>Esta es la observaci\u00f3n decisiva de todo el proyecto. <code>libspyrus<\/code> habla con el token a trav\u00e9s de <strong>libusb-1.0<\/strong>: enumerar dispositivos, abrir, reclamar la interfaz 0, transferencias bulk. Ni m\u00f3dulo del kernel, ni <code>\/dev\/spyrus0<\/code> (eso solo existe para la variante PCMCIA a trav\u00e9s de <code>spyrus_cs.ko<\/code>). La cadena es <code>spyrus_util \u2192 libspyrus.so.3 \u2192 libusb-1.0 \u2192 ioctls USBDEVFS \u2192 EP1\/EP2<\/code>, enteramente en espacio de usuario. Y una interfaz sin driver en mi anfitri\u00f3n es exactamente lo que libusb quiere: nada que desvincular, nada contra lo que pelear. El kernel del sism\u00f3grafo y mi kernel presentan la misma interfaz <code>usbfs<\/code>. El token no puede distinguir entre un PXA270 en una c\u00e1mara acorazada y un port\u00e1til en Helsinki.<\/p>\r\n<p>Las cadenas de la biblioteca me dieron el vocabulario de errores que necesitar\u00eda m\u00e1s tarde:<\/p>\r\n<pre><a href=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/02-12-.png\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-943 aligncenter\" src=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/02-12-.png\" alt=\"\" width=\"960\" height=\"271\" srcset=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/02-12-.png 960w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/02-12--300x85.png 300w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/02-12--768x217.png 768w\" sizes=\"(max-width: 960px) 100vw, 960px\" \/><\/a><\/pre>\r\n<p>Y los propios ficheros de integraci\u00f3n del firmware explican c\u00f3mo usa la herramienta el digitalizador, que es la documentaci\u00f3n m\u00e1s barata posible:<\/p>\r\n<pre><a href=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/02-16-.png\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-944 aligncenter\" src=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/02-16-.png\" alt=\"\" width=\"960\" height=\"384\" srcset=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/02-16-.png 960w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/02-16--300x120.png 300w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/02-16--768x307.png 768w\" sizes=\"(max-width: 960px) 100vw, 960px\" \/><\/a><\/pre>\r\n<p>Conviene tener presente esa l\u00ednea <code>openssl dsaparam -out ... 1024<\/code>. Es la semilla del \u00fanico bug real de esta historia, y el OpenSSL del firmware es 1.0.2j:<\/p>\r\n<pre><a href=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/02-20-.png\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-945 aligncenter\" src=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/02-20-.png\" alt=\"\" width=\"960\" height=\"65\" srcset=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/02-20-.png 960w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/02-20--300x20.png 300w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/02-20--768x52.png 768w\" sizes=\"(max-width: 960px) 100vw, 960px\" \/><\/a><\/pre>\r\n<p>Por \u00faltimo, el mismo paquete distribuye cabeceras GPL-2 en el m\u00f3dulo de bibliotecas cruzadas del espejo. Son lo m\u00e1s parecido a una especificaci\u00f3n del protocolo que existe en p\u00fablico. Los opcodes son doce bits m\u00e1s un indicador de conjunto de comandos de ocho bits:<\/p>\r\n<pre><a href=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/02-24-.png\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-946 aligncenter\" src=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/02-24-.png\" alt=\"\" width=\"960\" height=\"703\" srcset=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/02-24-.png 960w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/02-24--300x220.png 300w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/02-24--768x562.png 768w\" sizes=\"(max-width: 960px) 100vw, 960px\" \/><\/a><\/pre>\r\n<p>Obs\u00e9rvese c\u00f3mo funciona una comprobaci\u00f3n de PIN: el anfitri\u00f3n env\u00eda la frase y un desaf\u00edo de 20 bytes, y la tarjeta responde con un par <code>(r, s)<\/code>. El inicio de sesi\u00f3n es una firma DSA sobre un desaf\u00edo. Por eso nada relativo al PIN puede cortocircuitarse desde fuera.<\/p>\r\n<!-- ============================================================ -->\r\n<h2 id=\"h-bridge\">5. C\u00f3digo ARM, anfitri\u00f3n x86-64, USB real<\/h2>\r\n<p>El binario es ARMv4T. Mi m\u00e1quina es x86-64. <code>qemu-arm-static<\/code> es un emulador en modo usuario: traduce las instrucciones del invitado y pasa las <em>llamadas al sistema<\/em> del invitado al kernel anfitri\u00f3n. Un <code>ioctl(USBDEVFS_BULK)<\/code> emitido por la libusb ARM aterriza en mi nodo de dispositivo real. Sin m\u00e1quina virtual, sin controlador USB virtual, sin demonio. Tres detalles de entorno hacen que funcione:<\/p>\r\n<pre><a href=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/02-29-.png\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-947 aligncenter\" src=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/02-29-.png\" alt=\"\" width=\"960\" height=\"178\" srcset=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/02-29-.png 960w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/02-29--300x56.png 300w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/02-29--768x142.png 768w\" sizes=\"(max-width: 960px) 100vw, 960px\" \/><\/a><\/pre>\r\n<p>La herramienta necesita escribir en tres sitios del anfitri\u00f3n: el nodo de dispositivo USB, un fichero de bloqueo y un directorio de configuraci\u00f3n. La biblioteca toma un bloqueo de uso exclusivo (la cabecera lo llama \u00abthe interprocess right to use the card\u00bb) y lee su configuraci\u00f3n, as\u00ed que antes de nada:<\/p>\r\n<p><code><a href=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/02-32-.png\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-948 aligncenter\" src=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/02-32-.png\" alt=\"\" width=\"960\" height=\"178\" srcset=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/02-32-.png 960w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/02-32--300x56.png 300w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/02-32--768x142.png 768w\" sizes=\"(max-width: 960px) 100vw, 960px\" \/><\/a><\/code><\/p>\r\n<p>Una versi\u00f3n permanente de esto (un grupo <code>spyrus<\/code>, una regla udev con <code>TAG+=\"uaccess\"<\/code>, una entrada <code>tmpfiles.d<\/code> porque <code>\/var\/lock<\/code> es un tmpfs) es lo que hace el script de configuraci\u00f3n del proyecto; aqu\u00ed muestro las acciones en crudo.<\/p>\r\n<!-- ============================================================ -->\r\n<h2 id=\"h-contact\">6. Primer contacto, en el cable<\/h2>\r\n<p>Primero solo lectura. <code>--state<\/code> pide la palabra de estado y nada m\u00e1s. Dos banderas <code>-D<\/code> hacen que la biblioteca vuelque en hexadecimal cada transacci\u00f3n:<\/p>\r\n<pre><a href=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/02-37-.png\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-949 aligncenter\" src=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/02-37-.png\" alt=\"\" width=\"960\" height=\"253\" srcset=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/02-37-.png 960w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/02-37--300x79.png 300w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/02-37--768x202.png 768w\" sizes=\"(max-width: 960px) 100vw, 960px\" \/><\/a><\/pre>\r\n<p>La tarjeta respondi\u00f3. Una palabra de estado de 9 bytes con el bit alto del byte 0 activado, y dos registros que la biblioteca llama SR y PRR. SR = 25 significaba \u00abinicializada, en uso, nada cargado\u00bb. <code>--status<\/code> es el primer bloque de comando real, y aqu\u00ed est\u00e1 todo el formato de trama del protocolo en un solo viaje de ida y vuelta:<\/p>\r\n<pre><a href=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/02-41-.png\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-950 aligncenter\" src=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/02-41-.png\" alt=\"\" width=\"960\" height=\"347\" srcset=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/02-41-.png 960w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/02-41--300x108.png 300w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/02-41--768x278.png 768w\" sizes=\"(max-width: 960px) 100vw, 960px\" \/><\/a><\/pre>\r\n<p>Todo es una secuencia de <strong>palabras big-endian de 32 bits<\/strong>, sobre un ARM little-endian, sobre un anfitri\u00f3n x86 little-endian; la biblioteca intercambia los bytes a la salida. La cabecera son seis palabras:<\/p>\r\n<table>\r\n<tbody>\r\n<tr>\r\n<th>palabra<\/th>\r\n<th>Get_Status enviado<\/th>\r\n<th>significado (le\u00eddo directamente del cable)<\/th>\r\n<\/tr>\r\n<tr>\r\n<td>W0<\/td>\r\n<td><code>00 00 00 26<\/code><\/td>\r\n<td>opcode; la respuesta pone el byte alto a <code>0x90<\/code> y devuelve el resto tal cual<\/td>\r\n<\/tr>\r\n<tr>\r\n<td>W1<\/td>\r\n<td><code>00 00 00 00<\/code><\/td>\r\n<td>reservado<\/td>\r\n<\/tr>\r\n<tr>\r\n<td>W2<\/td>\r\n<td><code>00 00 00 00<\/code><\/td>\r\n<td>desplazamiento de la carga \u00fatil de entrada (0 cuando no hay; <code>0x18<\/code> en caso contrario)<\/td>\r\n<\/tr>\r\n<tr>\r\n<td>W3<\/td>\r\n<td><code>00 00 00 18<\/code><\/td>\r\n<td>longitud total del bloque (24 = solo cabecera)<\/td>\r\n<\/tr>\r\n<tr>\r\n<td>W4<\/td>\r\n<td><code>00 00 00 00<\/code><\/td>\r\n<td><strong>c\u00f3digo de resultado<\/strong>, rellenado por la tarjeta<\/td>\r\n<\/tr>\r\n<tr>\r\n<td>W5<\/td>\r\n<td><code>00 00 00 00<\/code><\/td>\r\n<td>reservado<\/td>\r\n<\/tr>\r\n<\/tbody>\r\n<\/table>\r\n<p>Tras la cabecera, la respuesta lleva <code>struct spyrus_status<\/code>: longitud <code>0x34<\/code>, despu\u00e9s el n\u00famero de serie de 8 bytes <code>01 00 00 00 f0 00 18 4f<\/code> (la etiqueta del pl\u00e1stico termina en 00184F), estado 7, modos, personalidad 1, 20 ranuras de clave con sus banderas, 50 ranuras de certificado. El comando de lista de personalidades muestra las etiquetas de las ranuras como 20 cadenas fijas de 32 bytes:<\/p>\r\n<pre><a href=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/02-47-.png\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-951 aligncenter\" src=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/02-47-.png\" alt=\"\" width=\"960\" height=\"197\" srcset=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/02-47-.png 960w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/02-47--300x62.png 300w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/02-47--768x158.png 768w\" sizes=\"(max-width: 960px) 100vw, 960px\" \/><\/a><\/pre>\r\n<p>(Ese listado es de hoy, despu\u00e9s de cargar una clave; en el primer contacto todas las ranuras dec\u00edan Empty.) Una ranura de certificado vac\u00eda responde a un Get_Certificate con texto literal:<\/p>\r\n<pre><a href=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/02-53-.png\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-952 aligncenter\" src=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/02-53-.png\" alt=\"\" width=\"960\" height=\"65\" srcset=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/02-53-.png 960w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/02-53--300x20.png 300w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/02-53--768x52.png 768w\" sizes=\"(max-width: 960px) 100vw, 960px\" \/><\/a><\/pre>\r\n<p>Una ranura de certificado ocupa <code>0x800<\/code> bytes y contiene texto PEM, con finales de l\u00ednea CRLF, rellenado con ceros. Esto importar\u00e1 en la secci\u00f3n 11.<\/p>\r\n<!-- ============================================================ -->\r\n<h2 id=\"h-own\">7. Apropiarse de la tarjeta: reinicializaci\u00f3n<\/h2>\r\n<p>Nunca supe el PIN SSO anterior y nunca lo necesit\u00e9. La inicializaci\u00f3n es una funci\u00f3n documentada de SPYCOS que destruye todas las claves y certificados y fija PIN nuevos. El propio script del fabricante hace exactamente esto en un digitalizador nuevo. Dos banderas importan: <code>--loose<\/code> selecciona el modo que no exige un certificado en cada ranura, y la confirmaci\u00f3n se lee del terminal de control, no de stdin, as\u00ed que canalizar <code>yes<\/code> por una tuber\u00eda solo funciona cuando no hay tty en absoluto.<\/p>\r\n<pre><a href=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/02-56-.png\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-953 aligncenter\" src=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/02-56-.png\" alt=\"\" width=\"960\" height=\"140\" srcset=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/02-56-.png 960w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/02-56--300x44.png 300w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/02-56--768x112.png 768w\" sizes=\"(max-width: 960px) 100vw, 960px\" \/><\/a><\/pre>\r\n<p>SR pas\u00f3 de 25 a 26. La tarjeta era m\u00eda, con PIN que eleg\u00ed yo (se cambian despu\u00e9s con <code>--change --old-pin --pin<\/code>; la biblioteca impone un m\u00ednimo de 4 caracteres, la regla propia de la tarjeta es 12). Nada de esto elude el modelo de seguridad del m\u00f3dulo: es el camino que sigue un administrador leg\u00edtimo el primer d\u00eda, destruye lo que hubiera, y no toca el motor de PIN.<\/p>\r\n<!-- ============================================================ -->\r\n<h2 id=\"h-wall\">8. El muro: <code>0x0a<\/code> Execution Failure<\/h2>\r\n<p>Entonces intent\u00e9 generar una clave como lo har\u00eda en cualquier m\u00e1quina moderna: crear par\u00e1metros DSA con OpenSSL y entreg\u00e1rselos a la tarjeta.<\/p>\r\n<pre><a href=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/03-00-.png\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-954 aligncenter\" src=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/03-00-.png\" alt=\"\" width=\"960\" height=\"385\" srcset=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/03-00-.png 960w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/03-00--300x120.png 300w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/03-00--768x308.png 768w\" sizes=\"(max-width: 960px) 100vw, 960px\" \/><\/a><\/pre>\r\n<p>Decodifiquemos la petici\u00f3n. W0 = <code>0x85<\/code> Generate_X. W3 = <code>0x144<\/code> = 324 bytes en total. Despu\u00e9s la carga \u00fatil de <code>struct spyrus_generate_x_in<\/code>: longitud <code>0x12c<\/code> = 300, \u00edndice 1, tipo <code>0x0a<\/code> = DSA, y tres bloques cada uno introducido por una palabra de longitud en bits: P <code>0x400<\/code> = 1024 bits, Q <code>0xa0<\/code> = 160 bits, G <code>0x400<\/code>. La tarjeta devolvi\u00f3 el bloque entero y puso <strong>W4 = <code>0x0000000a<\/code><\/strong>. El viaje de ida y vuelta se complet\u00f3. La trama se acept\u00f3. La tarjeta entendi\u00f3 la petici\u00f3n, lo intent\u00f3 y fall\u00f3.<\/p>\r\n<p>Diez es un n\u00famero concreto. Leamos la tabla de errores del binario; la funci\u00f3n es un switch sobre el c\u00f3digo, y el compilador emiti\u00f3 una tabla de saltos, que se ve en ambas arquitecturas:<\/p>\r\n<pre><a href=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/03-06-.png\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-955 aligncenter\" src=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/03-06-.png\" alt=\"\" width=\"960\" height=\"459\" srcset=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/03-06-.png 960w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/03-06--300x143.png 300w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/03-06--768x367.png 768w\" sizes=\"(max-width: 960px) 100vw, 960px\" \/><\/a><\/pre>\r\n<p>Recorrer la tabla da la correspondencia, y <code>0x0a<\/code> cae en una cadena muy concreta:<\/p>\r\n<table>\r\n<tbody>\r\n<tr>\r\n<th>c\u00f3digo<\/th>\r\n<th>cadena<\/th>\r\n<th>c\u00f3digo<\/th>\r\n<th>cadena<\/th>\r\n<\/tr>\r\n<tr>\r\n<td>0x00<\/td>\r\n<td>Passed<\/td>\r\n<td>0x07<\/td>\r\n<td>Invalid Data Size<\/td>\r\n<\/tr>\r\n<tr>\r\n<td>0x01<\/td>\r\n<td>Spyrus command failed<\/td>\r\n<td>0x08<\/td>\r\n<td>Invalid Header<\/td>\r\n<\/tr>\r\n<tr>\r\n<td>0x02<\/td>\r\n<td>Checkword Failure<\/td>\r\n<td>0x09<\/td>\r\n<td>Invalid State<\/td>\r\n<\/tr>\r\n<tr>\r\n<td>0x03<\/td>\r\n<td>Invalid Type Value<\/td>\r\n<td><strong>0x0a<\/strong><\/td>\r\n<td><strong>Execution Failure<\/strong><\/td>\r\n<\/tr>\r\n<tr>\r\n<td>0x04<\/td>\r\n<td>Invalid Mode Value<\/td>\r\n<td>0x0b<\/td>\r\n<td>No Key Loaded<\/td>\r\n<\/tr>\r\n<tr>\r\n<td>0x05<\/td>\r\n<td>Invalid Key Index<\/td>\r\n<td>0x15<\/td>\r\n<td>NO PQG Loaded<\/td>\r\n<\/tr>\r\n<tr>\r\n<td>0x06<\/td>\r\n<td>Invalid Cert Index<\/td>\r\n<td>\u00a0<\/td>\r\n<td>\u00a0<\/td>\r\n<\/tr>\r\n<\/tbody>\r\n<\/table>\r\n<p>No es Invalid Header, as\u00ed que la trama era correcta. No es Invalid State, as\u00ed que la tarjeta estaba lista. No es NO PQG Loaded, as\u00ed que los par\u00e1metros estaban presentes. No es Invalid Data Size. La tarjeta ejecut\u00f3 la operaci\u00f3n y abort\u00f3 dentro de ella. Eso reduce la b\u00fasqueda al <em>contenido<\/em> de los par\u00e1metros.<\/p>\r\n<p>Dos pistas falsas, eliminadas en el cable y no por intuici\u00f3n:<\/p>\r\n<ul>\r\n<li><strong>\u00abNecesita iniciar sesi\u00f3n primero.\u00bb<\/strong> A\u00f1adir <code>--pin<\/code> mand\u00f3 la herramienta por una ruta de validaci\u00f3n de ranura que falla en una tarjeta vac\u00eda (\u00abZero length certificate\u00bb), y la traza no mostr\u00f3 ning\u00fan intercambio Check_Pin_Phrase en ning\u00fan caso. El script del fabricante no pasa ning\u00fan PIN al keygen. Descartada.<\/li>\r\n<li><strong>\u00abEstado de sesi\u00f3n tras el init.\u00bb<\/strong> Reinicialic\u00e9 y gener\u00e9 seguido, en el orden exacto del script. El mismo <code>0x0a<\/code>. Descartada.<\/li>\r\n<\/ul>\r\n<p>La \u00fanica variable que quedaba eran los propios par\u00e1metros, y lo primero que hizo que la tarjeta superase el muro fue dejar de suministrarlos:<\/p>\r\n<pre><a href=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/03-10-.png\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-956 aligncenter\" src=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/03-10-.png\" alt=\"\" width=\"960\" height=\"216\" srcset=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/03-10-.png 960w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/03-10--300x68.png 300w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/03-10--768x173.png 768w\" sizes=\"(max-width: 960px) 100vw, 960px\" \/><\/a><\/pre>\r\n<p>Sin <code>--dsaparam<\/code> la biblioteca llama a <code>DSA_generate_parameters<\/code> del OpenSSL 1.0.2 incluido (de ah\u00ed la l\u00ednea de progreso con puntos y el informe de <code>counter<\/code>\/<code>h<\/code> de la b\u00fasqueda de primos FIPS 186-2), env\u00eda el resultado, y la tarjeta acepta. As\u00ed que la tarjeta estaba bien, el transporte estaba bien, y el problema eran <em>mis<\/em> par\u00e1metros. Ten\u00eda un HSM que funcionaba y una teor\u00eda equivocada: supuse que el silicio de 2005 quer\u00eda la semilla y el contador de generaci\u00f3n FIPS 186-2 para validar la procedencia, cosa que el OpenSSL moderno no emite. Esa teor\u00eda era plausible, coherente con las pruebas que ten\u00eda, y err\u00f3nea. La secci\u00f3n 9 es la correcci\u00f3n.<\/p>\r\n<!-- ============================================================ -->\r\n<h2 id=\"h-rootcause\">9. La causa ra\u00edz real, demostrada de tres maneras<\/h2>\r\n<p>Miremos otra vez la petici\u00f3n rechazada, en el bloque Q. La palabra de longitud dice <code>0x000000a0<\/code>, 160 bits, y los bytes que siguen son <code>74 ae 65 dd e8 35 23 83 80 dc 2a f0 34 28 23 c3 f7 2a b9<\/code>. Ahora miremos lo que OpenSSL cree que contiene el mismo fichero:<\/p>\r\n<p><code><span class=\"c\"><a href=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/03-15-.png\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-957 aligncenter\" src=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/03-15-.png\" alt=\"\" width=\"960\" height=\"121\" srcset=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/03-15-.png 960w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/03-15--300x38.png 300w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/03-15--768x97.png 768w\" sizes=\"(max-width: 960px) 100vw, 960px\" \/><\/a><\/span><\/code><\/p>\r\n<p>Q es <code>FF6B3DECA626D5EF52 74AE65DD...F72AB9<\/code>: 28 bytes, <strong>224 bits<\/strong>. El cable solo llev\u00f3 sus \u00faltimos 20 bytes. OpenSSL 3 cambi\u00f3 el valor por defecto: para par\u00e1metros DSA de 1024 bits ahora genera una q de 224 bits, siguiendo la tabla de tama\u00f1os de FIPS 186-3\/186-4 en lugar del emparejamiento original de 186-2 de L = 1024 con N = 160.<\/p>\r\n<p><code><span class=\"c\"><a href=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/03-19-.png\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-958 aligncenter\" src=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/03-19-.png\" alt=\"\" width=\"960\" height=\"103\" srcset=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/03-19-.png 960w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/03-19--300x32.png 300w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/03-19--768x82.png 768w\" sizes=\"(max-width: 960px) 100vw, 960px\" \/><\/a><\/span><\/code><\/p>\r\n<p><strong>Prueba 1, la biblioteca.<\/strong> Esta es la rutina de keygen en la compilaci\u00f3n x86-64 de la misma biblioteca (c\u00f3digo fuente id\u00e9ntico; m\u00e1s f\u00e1cil de leer que ARM). Calcula los tama\u00f1os de P y G con <code>BN_num_bits<\/code>, pero para Q escribe una constante y copia 20 bytes fijos:<\/p>\r\n<pre><a href=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/03-22-.png\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-959 aligncenter\" src=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/03-22-.png\" alt=\"\" width=\"960\" height=\"328\" srcset=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/03-22-.png 960w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/03-22--300x103.png 300w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/03-22--768x262.png 768w\" sizes=\"(max-width: 960px) 100vw, 960px\" \/><\/a><\/pre>\r\n<p>La biblioteca se escribi\u00f3 cuando q solo pod\u00eda tener 160 bits. Dada una q de 224 bits, <code>BN_bn2bin_fixed(q, buf, 20)<\/code> emite los 20 bytes de orden inferior y descarta los 8 superiores sin decir una palabra. La tarjeta nunca vio la q real.<\/p>\r\n<p><strong>Prueba 2, las matem\u00e1ticas.<\/strong> Los par\u00e1metros DSA solo son v\u00e1lidos si q divide a p \u2212 1 y g tiene orden q. Comprobemos la q real y la q&#8217; truncada contra los P y G que realmente se enviaron:<\/p>\r\n<p><code><span class=\"r\"><a href=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/03-35-.png\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-960 aligncenter\" src=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/03-35-.png\" alt=\"\" width=\"960\" height=\"291\" srcset=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/03-35-.png 960w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/03-35--300x91.png 300w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/03-35--768x233.png 768w\" sizes=\"(max-width: 960px) 100vw, 960px\" \/><\/a><\/span><\/code><\/p>\r\n<p>La tarjeta recibi\u00f3 una terna (p, q&#8217;, g) en la que q&#8217; no divide a p \u2212 1 y g no tiene orden q&#8217;. Un m\u00f3dulo FIPS est\u00e1 obligado a validar los par\u00e1metros de dominio antes de usarlos. Lo hizo, fall\u00f3, y el \u00fanico c\u00f3digo que tiene para \u00ablo intent\u00e9 y no funcion\u00f3\u00bb es <code>0x0a<\/code>. Cada byte de ese comportamiento es correcto.<\/p>\r\n<p><strong>Prueba 3, el experimento.<\/strong> Si el tama\u00f1o es toda la historia, entonces unos par\u00e1metros de OpenSSL 3 con una q de 160 bits deber\u00edan aceptarse sin ninguna semilla ni contador. Se generan expl\u00edcitamente y se cargan en una ranura vac\u00eda:<\/p>\r\n<pre><a href=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/03-40-.png\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-961 aligncenter\" src=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/03-40-.png\" alt=\"\" width=\"960\" height=\"553\" srcset=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/03-40-.png 960w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/03-40--300x173.png 300w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/03-40--768x442.png 768w\" sizes=\"(max-width: 960px) 100vw, 960px\" \/><\/a><\/pre>\r\n<p>Aceptado en menos de dos segundos, sin b\u00fasqueda de primos en el chip, sin semilla, sin contador. La tarjeta devolvi\u00f3 <code>struct spyrus_generate_x_out<\/code>: longitud <code>0x88<\/code>, longitud p\u00fablica <code>0x80<\/code>, y el valor p\u00fablico y de 128 bytes. La biblioteca envolvi\u00f3 entonces y junto con los par\u00e1metros en un <code>SubjectPublicKeyInfo<\/code> y emiti\u00f3 inmediatamente un Load_Certificate (<code>0x2f<\/code>) para guardar ese PEM en la ranura de certificado 2 bajo la etiqueta provisional <code>TEMPXXXX<\/code>. Por eso una ranura reci\u00e9n generada siempre muestra un \u00abcertificado\u00bb: es la clave p\u00fablica en PEM, aparcada donde m\u00e1s adelante ir\u00e1 un certificado real.<\/p>\r\n<div class=\"callout green\"><strong>Causa ra\u00edz, definitiva.<\/strong> El LYNKS Series II y su biblioteca implementan \u00fanicamente FIPS 186-2: L = 1024, N = 160. OpenSSL 3 usa por defecto N = 224 para L = 1024. La biblioteca trunca q a 160 bits en el cable en silencio, los par\u00e1metros resultantes son matem\u00e1ticamente inv\u00e1lidos, y la tarjeta los rechaza con Execution Failure. Soluci\u00f3n A: dejar que la tarjeta genere sus propios par\u00e1metros. Soluci\u00f3n B: generarlos en el anfitri\u00f3n con <code>-pkeyopt qbits:160<\/code>. Ni la autenticaci\u00f3n, ni el transporte, ni la procedencia estuvieron nunca implicados. El firmware del sism\u00f3grafo nunca se top\u00f3 con esto porque su OpenSSL 1.0.2 siempre produc\u00eda una q de 160 bits.<\/div>\r\n<p>Aqu\u00ed corrijo mi propia explicaci\u00f3n p\u00fablica anterior. La versi\u00f3n anterior era coherente con lo que hab\u00eda observado; no era coherente con el desensamblado, que deber\u00eda haber le\u00eddo primero. La lecci\u00f3n se generaliza: cuando se dispone de un binario sin stripped, la discusi\u00f3n termina en la instrucci\u00f3n que construye el paquete.<\/p>\r\n<!-- ============================================================ -->\r\n<h2 id=\"h-crypto\">10. La criptograf\u00eda, byte a byte<\/h2>\r\n<p><a href=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/03-46-.png\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-962 aligncenter\" src=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/03-46-.png\" alt=\"\" width=\"960\" height=\"760\" srcset=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/03-46-.png 960w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/03-46--300x238.png 300w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/03-46--768x608.png 768w\" sizes=\"(max-width: 960px) 100vw, 960px\" \/><\/a><\/p>\r\n<h3>La clave p\u00fablica<\/h3>\r\n<p>Lo que sale de la tarjeta es un <code>SubjectPublicKeyInfo<\/code> X.509 est\u00e1ndar. Su DER empieza con el identificador de objeto id-dsa, y por eso todas estas claves comienzan con la misma secuencia base64 <code>GByqGSM44BAE<\/code>:<\/p>\r\n<p><code><span class=\"c\"><a href=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/03-55-.png\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-963 aligncenter\" src=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/03-55-.png\" alt=\"\" width=\"960\" height=\"271\" srcset=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/03-55-.png 960w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/03-55--300x85.png 300w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/03-55--768x217.png 768w\" sizes=\"(max-width: 960px) 100vw, 960px\" \/><\/a><\/span><\/code><\/p>\r\n<p>p tiene 1024 bits con el bit alto activado, as\u00ed que DER antepone un <code>0x00<\/code> para mantener el INTEGER positivo: 129 bytes. g resulta empezar por debajo de <code>0x80<\/code>, as\u00ed que 128 bytes. Esa regla del relleno de signo vuelve a aparecer en la firma.<\/p>\r\n<h3>La firma<\/h3>\r\n<p>Firmemos un mensaje de 99 bytes. Obs\u00e9rvese el comando de firma: el anfitri\u00f3n calcula el hash con SHA-1 (o se lo pide a la tarjeta, opcode <code>0x2a<\/code>) y env\u00eda el <strong>resumen de 20 bytes<\/strong>, no el mensaje. La tarjeta responde con r y s en dos campos de 40 bytes, de los que se usan 20:<\/p>\r\n<p><code><a href=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/03-59-.png\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-964 aligncenter\" src=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/03-59-.png\" alt=\"\" width=\"960\" height=\"347\" srcset=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/03-59-.png 960w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/03-59--300x108.png 300w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/03-59--768x278.png 768w\" sizes=\"(max-width: 960px) 100vw, 960px\" \/><\/a><\/code><\/p>\r\n<p>La biblioteca codifica entonces en DER <code>SEQUENCE { INTEGER r, INTEGER s }<\/code>. Cada firma usa una k aleatoria nueva, as\u00ed que el mismo mensaje se firma de forma distinta cada vez; esta es la que conservo en el repositorio, anotada:<\/p>\r\n<p><code><a href=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/04-01-.png\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-965 aligncenter\" src=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/04-01-.png\" alt=\"\" width=\"960\" height=\"347\" srcset=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/04-01-.png 960w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/04-01--300x108.png 300w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/04-01--768x278.png 768w\" sizes=\"(max-width: 960px) 100vw, 960px\" \/><\/a><\/code><\/p>\r\n<p>La firma de la ranura 2, hecha con los par\u00e1metros de 160 bits generados en el anfitri\u00f3n, resulta tener ambos enteros por debajo de <code>0x80<\/code> y ocupa 46 bytes. Si alguna vez se ven firmas DSA de 46, 47 o 48 bytes de la misma clave, es la regla del relleno de signo, no corrupci\u00f3n.<\/p>\r\n<h3>Verificar sin nada m\u00e1s que aritm\u00e9tica<\/h3>\r\n<p>OpenSSL 3 no verificar\u00e1 esto. Rechaza SHA-1 con DSA en la capa de pol\u00edticas, antes de cualquier matem\u00e1tica:<\/p>\r\n<p><code><a href=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/04-06-.png\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-966 aligncenter\" src=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/04-06-.png\" alt=\"\" width=\"960\" height=\"84\" srcset=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/04-06-.png 960w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/04-06--300x26.png 300w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/04-06--768x67.png 768w\" sizes=\"(max-width: 960px) 100vw, 960px\" \/><\/a><\/code><\/p>\r\n<p>Eso es una pol\u00edtica de obsolescencia, no un veredicto. As\u00ed que se verifica a mano. Con la clave p\u00fablica (p, q, g, y), el resumen z y la firma (r, s):<\/p>\r\n<p>&nbsp;<\/p>\r\n<pre><a href=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/04-09-.png\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-967 aligncenter\" src=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/04-09-.png\" alt=\"\" width=\"960\" height=\"121\" srcset=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/04-09-.png 960w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/04-09--300x38.png 300w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/04-09--768x97.png 768w\" sizes=\"(max-width: 960px) 100vw, 960px\" \/><\/a><code><span class=\"c\"><br \/><br \/><a href=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/04-13-.png\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-968 aligncenter\" src=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/04-13-.png\" alt=\"\" width=\"960\" height=\"497\" srcset=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/04-13-.png 960w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/04-13--300x155.png 300w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/04-13--768x398.png 768w\" sizes=\"(max-width: 960px) 100vw, 960px\" \/><\/a>\r\n<\/span><\/code><\/pre>\r\n<p>La clave privada x nunca sali\u00f3 del m\u00f3dulo (hay un Generate_X y un Load_X en la lista de opcodes, y ninguna ruta en esta biblioteca para extraerla). La posesi\u00f3n se demuestra de la \u00fanica manera posible: la firma hecha en el chip se verifica contra la clave p\u00fablica exportada con pura aritm\u00e9tica modular, y un solo byte cambiado en el mensaje la rompe.<\/p>\r\n<!-- ============================================================ -->\r\n<h2 id=\"h-native\">11. Extra: una compilaci\u00f3n nativa x86-64, con su propio bug<\/h2>\r\n<p>El mismo espejo tiene un \u00e1rbol <code>CMG-NAM64<\/code>: el appliance de red x86-64 del fabricante. Incluye <code>spyrus_util<\/code> compilado para mi propia arquitectura, as\u00ed que ni siquiera hace falta qemu:<\/p>\r\n<p>&nbsp;<\/p>\r\n<pre><a href=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/04-17-.png\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-969 aligncenter\" src=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/04-17-.png\" alt=\"\" width=\"960\" height=\"197\" srcset=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/04-17-.png 960w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/04-17--300x62.png 300w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/04-17--768x158.png 768w\" sizes=\"(max-width: 960px) 100vw, 960px\" \/><\/a><\/pre>\r\n<p>Invocar directamente el cargador din\u00e1mico del propio \u00e1rbol, con <code>--library-path<\/code>, es la forma de ejecutar binarios de una glibc ajena sin tocar el anfitri\u00f3n: el <code>ld.so<\/code> de la era 2010, la libc, libssl 1.0.0 y libspyrus vienen todos de la imagen del appliance. Es la versi\u00f3n 2.1.0 frente a la 2.1.5 en ARM, y tiene un bug propio: <code>--getkey<\/code> falla en una ranura escrita por la compilaci\u00f3n 2.1.5, porque 2.1.5 guarda el PEM con finales de l\u00ednea CRLF y el desempaquetador de PEM de 2.1.0 no los tolera (\u00abUnable to unpack PEM\u00bb). El volcado de la ranura de certificado sigue siendo legible, as\u00ed que la clave se puede reconstruir a partir del hexadecimal en crudo: se toma el volcado de <code>--get --index N<\/code>, se decodifican los bytes, se descartan los <code>\\r<\/code> y el relleno de ceros, y se obtiene el mismo PEM que imprime la compilaci\u00f3n ARM. Tambi\u00e9n fue \u00fatil para el desensamblado: el listado x86-64 de la secci\u00f3n 9 es de esta compilaci\u00f3n, con s\u00edmbolos id\u00e9nticos a los de la ARM.<\/p>\r\n<!-- ============================================================ -->\r\n<h2 id=\"h-testing\">12. C\u00f3mo lo prob\u00e9<\/h2>\r\n<table>\r\n<tbody>\r\n<tr>\r\n<th>comprobaci\u00f3n<\/th>\r\n<th>c\u00f3mo<\/th>\r\n<th>resultado<\/th>\r\n<\/tr>\r\n<tr>\r\n<td>transporte<\/td>\r\n<td><code>--state<\/code>, <code>--status<\/code>, <code>--list<\/code>, <code>--time<\/code> bajo qemu y en nativo; comparar el n\u00famero de serie con la etiqueta<\/td>\r\n<td>n\u00famero de serie <code>...f0:00:18:4f<\/code> por ambas v\u00edas; el RTC responde <code>2026090811463500<\/code><\/td>\r\n<\/tr>\r\n<tr>\r\n<td>borrar y apropiarse<\/td>\r\n<td><code>--init --loose<\/code> con PIN nuevos; SR antes\/despu\u00e9s<\/td>\r\n<td>25 \u2192 26, despu\u00e9s 27 una vez cargada una clave<\/td>\r\n<\/tr>\r\n<tr>\r\n<td>par\u00e1metros en el chip<\/td>\r\n<td><code>--keygen --index 1<\/code> sin <code>--dsaparam<\/code><\/td>\r\n<td>Passed; counter = 147, h = 2; clave en la ranura 1<\/td>\r\n<\/tr>\r\n<tr>\r\n<td>par\u00e1metros rechazados<\/td>\r\n<td>par\u00e1metros de 1024 bits por defecto de OpenSSL 3 (q = 224)<\/td>\r\n<td><code>W4 = 0x0a<\/code>, reproducible siempre, en 1,4 s<\/td>\r\n<\/tr>\r\n<tr>\r\n<td>par\u00e1metros aceptados<\/td>\r\n<td>par\u00e1metros de OpenSSL 3 con <code>qbits:160<\/code> en la ranura 2<\/td>\r\n<td>Passed en 1,9 s; P, Q, G de la ranura 2 id\u00e9nticos al fichero del anfitri\u00f3n<\/td>\r\n<\/tr>\r\n<tr>\r\n<td>firma, ranura 1<\/td>\r\n<td>firmar, analizar el DER, verificar a mano y con pyca\/cryptography<\/td>\r\n<td>v == r; mensaje manipulado rechazado<\/td>\r\n<\/tr>\r\n<tr>\r\n<td>firma, ranura 2<\/td>\r\n<td>lo mismo con la clave de par\u00e1metros del anfitri\u00f3n<\/td>\r\n<td>v\u00e1lida; DER de 46 bytes (sin rellenos de signo)<\/td>\r\n<\/tr>\r\n<tr>\r\n<td>frescura<\/td>\r\n<td>firmar el mismo mensaje dos veces<\/td>\r\n<td>(r, s) distintos cada vez; ambas verifican<\/td>\r\n<\/tr>\r\n<tr>\r\n<td>reproducci\u00f3n desde cero<\/td>\r\n<td>directorio vac\u00edo, solo el script de descarga, sin ficheros en cach\u00e9<\/td>\r\n<td>20 ficheros, sumas de comprobaci\u00f3n correctas, <code>--list<\/code> funciona sin privilegios tras la regla udev<\/td>\r\n<\/tr>\r\n<tr>\r\n<td>compilaci\u00f3n nativa<\/td>\r\n<td>los mismos comandos a trav\u00e9s del <code>ld.so<\/code> propio del appliance<\/td>\r\n<td>funciona; bug CRLF de <code>--getkey<\/code> documentado con soluci\u00f3n<\/td>\r\n<\/tr>\r\n<\/tbody>\r\n<\/table>\r\n<p>C\u00f3mo queda el anfitri\u00f3n despu\u00e9s, para que nada sorprenda: un grupo <code>spyrus<\/code>, una regla udev para <code>08df:0a00<\/code>, <code>\/etc\/spyrus\/spyrus.local<\/code>, y <code>\/var\/lock\/spyrus.lck<\/code> recreado en el arranque por una entrada tmpfiles. Sin m\u00f3dulo del kernel, sin demonio, sin cambios en el OpenSSL del sistema.<\/p>\r\n<!-- ============================================================ -->\r\n<h2 id=\"h-hs4l\">13. Por qu\u00e9 constru\u00ed hs4l y qu\u00e9 contiene<\/h2>\r\n<p>Todo lo anterior es reproducible con una shell, rsync, qemu y OpenSSL. Tambi\u00e9n es lo bastante engorroso como para que no quiera que nadie lo rehaga a partir de una entrada de blog. As\u00ed que empaquet\u00e9 el conocimiento como <strong>hs4l<\/strong> (HSM-SPYRUS-4-Linux), BSD-3-Clause, y lo mantengo:<\/p>\r\n<p><a href=\"https:\/\/github.com\/borjatarraso\/hs4l\">https:\/\/github.com\/borjatarraso\/hs4l<\/a><\/p>\r\n<ul>\r\n<li><strong>Envoltorios<\/strong> para la compilaci\u00f3n ARM bajo qemu y para la compilaci\u00f3n nativa x86-64, que localizan el token en sysfs y solo escalan privilegios cuando el nodo, el bloqueo o la configuraci\u00f3n no son escribibles.<\/li>\r\n<li><strong>Un script de descarga con sumas de comprobaci\u00f3n.<\/strong> Los binarios del fabricante <em>no<\/em> est\u00e1n en el repositorio. Son firmware con derechos de autor, y la forma correcta de obtenerlos es desde el espejo p\u00fablico, en el momento de la instalaci\u00f3n, verificados contra las sumas SHA-256 de los ficheros exactos que prob\u00e9 (20 para el entorno de ejecuci\u00f3n ARM, 158 para el corpus completo incluyendo la compilaci\u00f3n x86-64 y las cabeceras GPL).<\/li>\r\n<li><strong>Las partes GPL<\/strong> del paquete del fabricante, cabeceras y scripts, con sus avisos intactos, porque son lo que hace legible el formato del cable.<\/li>\r\n<li><strong>Documentaci\u00f3n<\/strong>: las notas del protocolo, el recorrido de reinicializaci\u00f3n, la lista de resoluci\u00f3n de problemas (cada entrada rastreada hasta una causa ra\u00edz en el cable), una tabla de dispositivos soportados y los diagramas.<\/li>\r\n<li><strong>Ejemplos que se pueden verificar ahora mismo<\/strong>: la clave p\u00fablica, el mensaje y la firma de esta entrada; los par\u00e1metros rechazados de 224 bits; los par\u00e1metros aceptados de 160 bits con la clave y la firma que la tarjeta hizo a partir de ellos; y un verificador de 40 l\u00edneas que hace la aritm\u00e9tica DSA sin la capa de pol\u00edticas de OpenSSL.<\/li>\r\n<li><strong>Un script de configuraci\u00f3n de udev\/permisos<\/strong>, un Makefile con un objetivo <code>check<\/code>, y una plantilla de incidencia para informar sobre otras unidades SPYRUS (se espera que las variantes USB del Series II funcionen; la ruta de la tarjeta PCMCIA a trav\u00e9s de <code>spyrus_cs.ko<\/code> no est\u00e1 probada; Rosetta y los productos posteriores usan una pila distinta).<\/li>\r\n<\/ul>\r\n<p>Las versiones van de v0.1 \u2192 v0.8 y el registro de cambios cuenta la historia con honestidad, incluida la correcci\u00f3n de la secci\u00f3n 9. Si alguien tiene uno de estos tokens, o un dispositivo SPYRUS distinto, que abra una incidencia con la salida de su <code>lsusb -v<\/code> y de <code>--status -D -D<\/code> y le ayudar\u00e9.<\/p>\r\n<p>A m\u00e1s largo plazo, la soluci\u00f3n correcta es una implementaci\u00f3n libre de <code>libspyrus<\/code>. La biblioteca solo necesita diecisiete peque\u00f1os s\u00edmbolos de utilidad de la biblioteca auxiliar del fabricante y cuatro stubs de control de alimentaci\u00f3n, todos trivialmente reemplazables; los binarios sin stripped y las cabeceras GPL hacen realista una reimplementaci\u00f3n de sala limpia. Eso eliminar\u00eda la \u00faltima pieza no libre del camino.<\/p>\r\n<!-- ============================================================ -->\r\n<h2 id=\"h-downloads\">14. Descargas y firmas<\/h2>\r\n<p>Cada versi\u00f3n es una etiqueta git anotada. Tambi\u00e9n publico aqu\u00ed los tarballs de c\u00f3digo fuente, firmados con mi clave OpenPGP, para que se pueda verificar lo que se ejecuta sin confiar en una plataforma de alojamiento:<\/p>\r\n<p><code><a href=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/04-24-.png\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-970 aligncenter\" src=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/04-24-.png\" alt=\"\" width=\"960\" height=\"159\" srcset=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/04-24-.png 960w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/04-24--300x50.png 300w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/04-24--768x127.png 768w\" sizes=\"(max-width: 960px) 100vw, 960px\" \/><\/a><\/code><code><\/code><\/p>\r\n<p><a href=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/04-27-.png\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-971 aligncenter\" src=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/04-27-.png\" alt=\"\" width=\"960\" height=\"122\" srcset=\"https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/04-27-.png 960w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/04-27--300x38.png 300w, https:\/\/www.blackhats.es\/wordpress\/wp-content\/uploads\/2026\/09\/04-27--768x98.png 768w\" sizes=\"(max-width: 960px) 100vw, 960px\" \/><\/a><\/p>\r\n<p>Lo que deliberadamente no distribuyo es el entorno de ejecuci\u00f3n del fabricante en s\u00ed. El script de descarga obtiene los ficheros exactos del espejo p\u00fablico y se niega a continuar si una sola suma de comprobaci\u00f3n difiere del conjunto que prob\u00e9. Eso mantiene el proyecto dentro de la legalidad, protege frente a un binario manipulado, y deja al fabricante, que resolvi\u00f3 discretamente este problema en 2010 para los sism\u00f3logos del mundo, el control de su propio c\u00f3digo.<\/p>\r\n<p class=\"lede\" style=\"margin-top: 2em;\">Un fabricante muerto no es un dispositivo muerto. El protocolo estuvo vivo todo el tiempo, en un binario ARM dentro de un sism\u00f3grafo, esperando a que alguien hiciera la pregunta correcta.<\/p>\r\n<\/div>\r\n","protected":false},"excerpt":{"rendered":"<p>Un m\u00f3dulo de seguridad hardware FIPS 140-2 de 2005. Sin driver. Sin middleware. Sin PIN. Un fabricante que ya no existe. Esta es la historia completa de c\u00f3mo consegu\u00ed que generase una clave y firmase un mensaje dentro del chip desde una m\u00e1quina Linux x86-64 moderna, contada al nivel al que realmente trabaj\u00e9: descriptores USB, [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[297,305],"tags":[384,382,164,383],"class_list":["post-921","post","type-post","status-publish","format-standard","hentry","category-advanced","category-internalshacking","tag-drivers","tag-hsm","tag-linux","tag-spyrus"],"_links":{"self":[{"href":"https:\/\/www.blackhats.es\/wordpress\/index.php?rest_route=\/wp\/v2\/posts\/921"}],"collection":[{"href":"https:\/\/www.blackhats.es\/wordpress\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.blackhats.es\/wordpress\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.blackhats.es\/wordpress\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.blackhats.es\/wordpress\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=921"}],"version-history":[{"count":23,"href":"https:\/\/www.blackhats.es\/wordpress\/index.php?rest_route=\/wp\/v2\/posts\/921\/revisions"}],"predecessor-version":[{"id":990,"href":"https:\/\/www.blackhats.es\/wordpress\/index.php?rest_route=\/wp\/v2\/posts\/921\/revisions\/990"}],"wp:attachment":[{"href":"https:\/\/www.blackhats.es\/wordpress\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=921"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.blackhats.es\/wordpress\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=921"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.blackhats.es\/wordpress\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=921"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}