Franqueando los secretos del hardware mediante ingeniería inversa
Cada vez más fabricantes usan (y abusan) de las flexibles especificaciones de los dispositivos de interfaz humana (USB-HID), y esto es en particular útil cuando tenemos situaciones en que el software proporcionado por el manufacturador carece de funciones, tiene algunas que no nos agradan o simplemente no entrega soporte. Veamos cómo identificar y extraer estructuras, protocolos y desarrollar nuestro propio software, usando la nueva calculadora HP Prime como objetivo, la cual como la mayoría de aparatos modernos hace uso de esta especificación Actualmente la única forma de enviar y recibir archivos desde la nueva calculadora HP Prime es mediante el HP Connectivity Kit. Aunque esta aplicación funciona bastante bien, no tiene ninguna forma de realizar operaciones sobre “archivos” puesto que la calculadora misma no tiene ese concepto. Entonces para usar un programa descargado de internet, bajamos un archivo de texto plano y creamos un nuevo programa en el kit de conectividad: Realmente esa forma es una solución bastante primitiva, y podemos solucionarlo.
Introducción a la HP Prime
Quiero comentar brevemente sobre la calculadora pues, aunque este documento no es dirigido directamente al aparato, es conveniente conocer algo más de la misma. Hace muchos años que HP no anunciaba ninguna calculadora gráfica y ya estabamos bastante acostumbrados a ver a la competencia, Casio y TI en particular, sacar modelos modernos. Inclusive, cuando pensé en escribir una comparación entre las últimas nuevas calculadoras (considerando la HP50g como la "nueva" de HP pues no había otra) aún no había noticias de la nueva HP Prime. Sin embargo, antes de terminar de escribir esa comparación, salió y luego de preordenarla como cualquier fanático de las calculadoras, la añadí al documento.
En cuanto al software, en este instante hay que reconocer que deja bastante que desear. El firmware y la contraparte del software para el PC de escritorio son bastante precarios. Las cosas han mejorado levemente, pero hay bastantes cuelgues y bugs. El motor matemático es poderoso, sin embargo detalles gráficos, parpadeos y otros hacen que la experiencia no sea muy agradable. El concepto usado en esta calculadora es muy particular. Todo es una aplicación. Podemos crear una nueva aplicación heredando una de las actuales y reemplazar rutinas de inicialización y variables para obtener otra aplicación. Cada aplicación tiene tres vistas, una simbólica, una gráfica y otra numérica. Por ejemplo la planilla de cálculos tiene sólo la vista numérica. Así, usando un set predeterminado de variables iniciales, tres vistas y otras extensiones más simples (un botón para cambiar "vistas" y otro para mostrar un menú) se consiguen aplicaciones autocontenidas que podemos clonar para hacer las nuestras. Este concepto es un derivado de la HP39gII. En cuanto a desarrollo, no existe nada de bajo nivel y el único lenguaje que tenemos por ahora es el HPPPL. Este lenguaje está influenciado en Pascal y no tiene ninguna cercanía con UserRPL, SystemRPL ni la sintaxis RPN (aunque la calculadora soporta RPN como entrada, pero a un nivel muy básico). [code]EXPORT programa(argumentos) BEGIN //comentarios en este formato líneas terminadas en punto y coma; END;[/code] Si estás leyendo este artículo porque estás experimentando problemas con tu calculadora, a la fecha lo único que se puede hacer es reinicializarla con métodos no publicados oficialmente que han sido encontrados: https://www.youtube.com/watch?v=B-pY9zpPmkY
En término físicos, esta calculadora está influenciada directamente con las tabletas y teléfonos modernos. Pantalla táctil capacitiva, batería de celular, pantalla en colores retroiluminada, incluyendo bastante memoria RAM y espacio de flash para olvidar un poco las restricciones de desarrollar elegantemente. El único punto destacable negativamente es probablemente la cubierta plástica y el color naranjo elegido para el teclado, pero en general es un producto sólido y que refleja experiencia en el diseño. Como ya vimos en el punto anterior, como el software es bastante primitivo por el momento no tenemos opciones avanzadas (quizás es una buena decisión) y por ejemplo no podemos configurar un "flag" de que el aparato se apague o no con inactividad. Dentro de esto, quería averiguar cuánto realmente duraba la batería por lo que la única forma de mantenerla encendida era presionando una tecla frecuentemente: https://www.youtube.com/watch?v=sGvbxDgt_Yc Como consejo derivado de esas pruebas entonces, si tienes un examen y aún no te aparece la advertencia de que queda poca energía, tienes más de dos horas y media para usar la calculadora (considerando de que la batería esté en buen estado).
Identificando el detalles del hardware
Como siempre, lo primero es buscar ciertas pistas de cómo iniciar el trabajo. En este caso podemos usar USB Tree View para obtener una vista lo más detallada posible de los dispositivos USB. En realidad podríamos hacerlo sin ninguna aplicación, pero con esta pequeña utilidad se obtiene mucho más: Ya podemos confirmar que tratamos con un dispositivo USB-HID e incluso tenemos su idVendor e idProduct necesarios para nuestro programa: [tp no_translate="y"] [code]------------------ Device Descriptor ------------------ bLength : 0x12 (18 bytes) bDescriptorType : 0x01 (Device Descriptor) bcdUSB : 0x200 (USB Version 2.00) bDeviceClass : 0x00 (defined by the interface descriptors) bDeviceSubClass : 0x00 bDeviceProtocol : 0x00 bMaxPacketSize0 : 0x40 (64 bytes) idVendor : 0x03F0 (Hewlett Packard) idProduct : 0x0441 bcdDevice : 0x100 iManufacturer : 0x01 Language 0x0409 : "Hewlett-Packard" iProduct : 0x02 Language 0x0409 : "HP Prime Calculator" iSerialNumber : 0x00 bNumConfigurations : 0x01 [/code] [/tp]
Espiando las comunicaciones
Utilizando algún sniffer de USB debemos comenzar a analizar los patrones de cada mensaje. Para simplificar la tarea, clasificaremos mentalmente los datos capturados en tres categorías:
Inicialización y finalización: Todos los mensaje por defecto para comenzar, definiciones de tamaño del chunk, descriptores, etc. (Será manejado automáticamente por lo que podemos ignorarlos)
Directos: Los que podamos identificar como generados por nosotros (tendrán que ser analizados sí o sí)
Indirectos: Los que sean respuestas o cosas generadas automáticamente (tendremos que analizar cada caso, por si necesitamos recrearlos)
Opciones de aplicaciones que permitan escuchar el USB tenemos varias. Entre las más populares:
Wireshark: Sin costo, de código abierto y excelente para cualquier trabajo (incluyendo redes, su propósito original)
USBlyzer: Bastante cara, pero la interfaz puede hacer que lo valga para trabajos diarios
USBTrace: Levemente más económica pero no necesita ninguna instalación especial para funcionar (de controladores ni servicios), ideal para cosas esporádicas
UsbSnoop: Sin costo, no la comprobé pero era uno de los primeros resultados al buscar
Obtención de datos generales
Luego de grabar un par de capturas y podemos identificar a grandes rasgos en qué categoría caen secciones de datos. Idealmente podemos usar algún sistema de marcado como por comentarios o marca páginas mientras realizamos las capturas. Por ejemplo, desde que conectamos el dispositivo, cuando lo apagamos, cuando lo dejamos un tiempo sin actividad, entre otros. Es simple identificar la primera clase de mensaje, inicialización y finalización, hay mensajes específicos y datos no identificables: Ahora tenemos segmentos con datos que reconocemos, cosas que nosotros hemos enviado, modificado o alterado. Claramente podemos identificar y asociar esos datos a una acción, ejemplo al duplicar una aplicación podemos ver datos que contienen trozos del nombre: Finalmente, los indirectos. Yo nunca envié configuraciones, pero de vez en cuando veo mensajes así. ¿Serán para sincronizar la hora?, ¿Mantener la calculadora encendida?:
Identificando estructuras
Luego de tener la categoría de mensajes directos etiquetados, podemos comenzar a buscar coincidencias y secciones relacionables en los paquetes de datos. Por ejemplo, podemos mandar reiterados programas desde el kit de conectividad de la calculadora con pequeños cambios. Hay ciertos detalles adicionales a considerar, uno de ellos es que el USB describirá ciertas cosas que afectan a los paquetes de datos, como el largo máximo por ejemplo. Secuencialmente, el proceso funciona así (entre cada punto guardamos el set de capturas del puerto):
Crear un programa/script lo más pequeño posible y ver si podemos identificar parte de su código en los datos al sniffear el USB
Alterar ese programa manteniendo el largo para encontrar la forma de codificación de los mensajes y los posibles checksum
Alterar ese programa cambiando el largo, así detectamos el campo de largo si es que hubiera
Alterar ese programa cambiando metadatos, por ejemplo el nombre del script, para identificar esas secciones
Alterar el programa cambiando el largo de tal forma de encontrar la técnica que usan para repartir datos que no caben en un "chunk" de datos, determinado por el largo máximo de los paquetes que previamente dijo el descriptor USB que soportaba.
Luego de todas esas capturas podemos comenzar a trabajar. En este instante es una decisión de cada persona como enfrentar el problema de "ingenio". A mí personalmente me gusta copiar los datos a Excel e ir probando cosas. En breve puedo colocar reglas que asumiré como válidas hasta que no encuentre un caso contrario:
Los datos van siempre en UTF16 (en el llamado little endian byte order), incluyendo el nombre del archivo y cuerpo.
Los archivos que consideraré son los con extensión .hpprgm, los cuales son los que se reflejan directamente en la vista de Programas de la calculadora (aunque hay otros interesantes como los que replican una aplicación .hpapp y las notas de texto plano .hpappnote, sin embargo para la presente investigación no serán considerados)
En los archivos .hpprgm tenemos básicamente dos tipos diferentes, unos sin un nombre interno, que se componen de un encabezado y datos. El segundo tipo tiene un encabezado, nombre y datos.
La forma en que se guardan los datos en el disco tiene pequeñas diferencias con la forma en que se envían por el USB, por lo que habrá que considerar esto a futuro.
Luego de un par de horas, lo que puedo identificar como estructura válida es la siguiente: [tp no_translate="y"]
Byte 0 1 2 3 4 5 6 7 8 9 10 11 Example 00 00 F7 01 00 00 00 B8 06 1C 94 DD Description Start Start Type Items Size Name length CRC Additional Count from last Size byte (7) until end Byte length
[/tp] Sin embargo, luego me di cuenta de que cuando los datos no cabían en un "chunk", el número del siguiente envío tenía el siguiente aspecto: [tp no_translate="y"]
Byte 0 1 2 3 Example 00 01 00 00 Description Start Chunk Data from here, up to the chunk end Additional Goes back to 0x00 after 0xFF
[/tp] En realidad entonces lo que tenemos al final es que luego de añadir el encabezado a nuestros datos, los chunks contienen 2 bytes menos del tamaño del chunk, lo que queda en código como: [code language="csharp"]var fullData = new List(_header); // Name var nameBytes = Encoding.Unicode.GetBytes(name); // Size var size = BitConverter.GetBytes(data.Length + nameBytes.Length +5); // Combining all fields fullData.AddRange(size.Reverse()); fullData.Add(0x06); fullData.Add((byte) nameBytes.Length); fullData.AddRange(new byte[] {0x94, 0xdd}); // CRC fullData.AddRange(nameBytes); fullData.AddRange(data); var allBytes = fullData.ToArray(); int position = 0, chunk = 0; if(chunkSize>0) do { IEnumerable tmp = new[] { (byte)0x00, (byte)(chunk++ % byte.MaxValue) }; Chunks.Add(tmp.Concat(allBytes.SubArray(position==0?2:position, Math.Min(chunkSize-2, allBytes.Length - position))).ToArray()); position += chunkSize-(position==0?0:2); } while (position < allBytes.Length);[/code]
Estructura de los archivos .hpprgm
Luego de recibir los datos, el programa crea archivos con extensión .hpprgm para respaldar los datos. Como dije anteriormente, tenemos dos tipos de estas estructuras. Una no tiene el nombre del script por dentro y en mi opinión es la más simple: [tp no_translate="y"]
Byte 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 Example 0C 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 08 00 00 00 … Description Type Name flag Size Data Additional Unnamed 64K current fw limit
[/tp] Actualmente, el firmware actual puede recibir y manejar archivos mayores a 64K pero sólo "internamente" pues hay detalles que fallan, por ejemplo el tamaño del archivo es truncado en la calculadora y el kit de conectividad alega de que el archivo es muy grande. Ahora pasemos a la otra estructura, la cual es levemente más compleja. Acá se añade el nombre sin descriptores de su tamaño (el nombre termina con dos bytes con valor cero seguidos y ahí ya comienzan los datos). [tp no_translate="y"]
Byte 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 … ... … Example 0C 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00 31 … 00 00 … Description Type Name flag Name start Name Name end Data Additional Named Name end with 0x00, 0x00
[/tp] Ambas se pueden leer de la siguiente forma, simplemente utilizando el byte en la posición 8 para discernir de que tipo es el archivo que deseamos leer: [code language="csharp"]if (b.Length >= 19) { for (var i = 1; i <= 7; i++) if (b[i] != 0x00) goto case null; switch (b[8]) { case 0x00: var size = b[16] + b[17]*0xff + b[18]*0xff*0xff; Data = new byte[size]; const int offset = 20; for (int i = offset; i < offset + size && i < b.Length; i++) Data[i - offset] = b[i]; break; case 0x01: if (b[16] == 0x31) { for(var i=18;i<b.Length;i++) if (b[i - 1] == b[i] && b[i] == 0x00) { if (!ignoreInternal) Name = Encoding.Unicode.GetString(b.SubArray(18, i-18)); i += 8; Data = b.SubArray(i, b.Length - i); break; } } break; } }[/code]
En la primera estructura había un espacio para dos bytes de un CRC, para descubrirlo primero asumí de que habían utilizado algún algoritmo conocido, así es que intenté primero probar los más simples que incluía el editor hexadecimal que utilizaba: Sin embargo ninguno coincidía, incluso cambiando el inicio y final en reiteradas ocasiones. Posteriormente intenté usar http://reveng.sourceforge.net el cual luego de varios minutos tampoco tuvo éxito. Luego de esto, me di cuenta que quizás era un error considerarlo un CRC tradicional, pues cualquier valor era recibido por la HP, es decir no tiene importancia por este momento buscar como es generado para el propósito de enviar y recibir archivos. Hay que dejar claro que aunque este valor de CRC sin embargo es consistente (a los mismos datos devuelve el mismo valor), no es necesario develar su secreto pues los archivos igual se reciben. Al final fue [tp no_translate="y"]Tim Wessman[/tp] (una persona que trabaja en la división de calculadoras de HP) el que dió algunas pistas en un foro y un proyecto mucho más completo que mi presente programa implementó su solución exacta.
Desarrollo de la aplicación
Ahora con todos estos datos podemos empezar a combinarlos. El punto clave es cómo interactuar con el USB. Para los pasos iniciales podemos aprovecharnos de que todas las ventanas de Windows reciben mensajes sobre algunos eventos de hardware, en particular de dispositivos HID, entonces: [code language="csharp"]private void FormMain_Load(object sender, EventArgs e) { hidDevice.RegisterHandle(Handle); UpdateGui(); } protected override void WndProc(ref Message m) { hidDevice.ParseMessages(ref m); base.WndProc(ref m); // Pass message on to base form }[/code] Así ya recibimos los eventos de conexión y desconexión. Ahora, necesitamos enviar y recibir información. Esto sucede como si estuviéramos escribiendo y leyendo desde un archivo, por medio de un FileStream, sin embargo es posible ya utilizar una librería que implementa todas estas funciones básicas. Hay pequeños cambios que la librería necesita, pero son menores. Adicionalmente la aplicación soportará archivos de texto plano (con una simple conversión interna) y también la recepción del código directamente del portapapeles (usando la misma conversión).
La aplicación que finalmente obtenemos es totalmente funcional y puede ser descargada acá. El código fuente está disponible en GitHub. Acá hay un pequeño demo de su funcionamiento: https://www.youtube.com/watch?v=FxG-R0QZ-qI
Este artículo demuestra cómo podemos tomar el control de nuestros dispositivos incluso sin tener ningún tipo de soporte del fabricante. Hay muchas situaciones en donde esto es útil, podemos automatizar acciones que son imposibles con el software actual, migrar el software a otra plataforma no soportada o realmente lo que se nos ocurra manteniendo la tarea simple, pero también entretenidamente desafiante.