Ir al contenido principal

Ingeniería Inversa de Firmware en un PIC 16Fxxx de Microchip

Como parece que no tengo mejores cosas que hacer, no se me ocurre otra forma mejor de pasar una tarde de Sábado que hacer un poco de Ingeniería Inversa a un microcontrolador PIC de la casa Microchip.
 
En esta ocasión se trata de un microcontrolador de la familia 16Fxxx (tipo 16F876, 16F84, etc.) cuyos opcodes tienen un tamaño de 14 bits. El mismo sistema será válido para otros microcontroladores de otras familias o incluso fabricantes, incluso aún cambiando la arquitectura del microcontrolador, la base esencial del análisis será similar en multitud de casos.
 
Siempre que se me plantea una situación similar, me gusta entender los entresijos de lo que tengo entre manos y, en la medida de lo posible intento empezar "a mano" o "a pelo" en el proceso de obtención de los Nemotécnicos ó, en definitiva las instrucciones en lenguaje ensamblador.
 
A continuación os muestro un pequeño pedacito de un firmware que he estado analizando, se trata del comienzo del mismo y, ni que decir tiene, que intentar analizar el funcionamiento de un programa observando tan solo estos números:

0F28CE00030E83128F00040890000A0891004F089200492B ...
 
es de paranóico/marciano, como poco. Si hay alguien en el planeta que observando esas ristras de números es capaz de interpretar la lógica que esconden, que me lo presenten, por favor.
 
En definitiva, cuando se trata de entender el funcionamiento de un software/firmware del cual no tenemos su código fuente, la cosa se complica bastante como para ponerse a leer números. Esto no funciona así, en su lugar, un primer paso consiste en transformar esos números en los citados Nemotécnicos que conformaran el programa en lenguaje ensamblador, sin duda, mucho más inteligible para el ser humano.
 
Incluso, en algunas circunstancias, se da un paso mas allá y se genera un código fuente en un lenguaje de más alto nivel o pseudocódigo, que podría ser utilizando nuestras propias palabras o basándonos en un lenguaje de programación estándar.
 
Bueno, no me quiero enrrollar más con palabrería así que, voy al grano.
 
La idea es bastante simple, extraemos el firmware de la memoria del microcontrolador o si se da el caso de la memoria externa en donde se encuentre alojado. Inicialmente se tratará de una ristra de ceros y unos, para finalmente obtener valores hexadecimales y por último los preciados nemotécnicos del lenguaje ensamblador equivalente.
 
Así, en el trozo del ejemplo anterior, el equivalente en código binario (0's y 1's) de los valores 0F28CE0003 sería:
 
111100101000110011100000000000000011

Si los agrupamos de 8 en 8 bits, lo podremos convertir fácilmente a valores hexadecimales equivalentes a 1 byte:

00001111 = 0x0F
00101000 = 0x28
11001110 = 0xCE
00000000 = 0x00
00000011 = 0x03

Y así con todo, pero bueno lo que quiero dejar en claro, es que nosotros inicialmente lo único que podemos "sacar" del microcontrolador serán "una corriente" de ceros y unos, los cuales agruparemos de 8 en 8 para formar bytes y poder representar más fácilmente los distintos opcodes en formato hexadecimal.
 
Una vez tenemos los valores en hexadecimal, la obtención de los nemotécnicos es relativamente sencilla, aquí es donde las cosas empezarán a cambiar de unas arquitecturas de microcontrolador a otras.
 
En el caso que nos ocupa de la familia 16Fxxx de Microchip, tenemos opcodes de 14 bits, lo que nos hace ver claramente que tendremos que tomar los valores de dos en dos bytes. Volviendo al ejemplo, los valores quedarían separados de la siguiente manera:

0F28 CE00 030E 8312 8F00 0408 9000 0A08 9100 4F08 9200 492B ...

Como os podeis imaginar, hacer todo este proceso a mano es una locura, aquí tan solo tenemos unos pocos opcodes, pero un firmware del mundo real, por ejemplo el que podemos encontrar en un mando a distancia moderno, el ordenador de abordo de un coche, una televisión, un teléfono móvil, etc se compone de miles/millones de opcodes. ¡Aquí tan solo estoy mostrando 12!
 
Pero claro, hacerlo de esta forma, te obliga a entrar de lleno en las tripas del microcontrolador a un muy bajo nivel. Entender la arquitectura del microcontrolador, su conjunto de instrucciones, interpretación de las tablas de opcodes, organización de la memoria, registros, etc, etc. Y esto es lo que a mi me gusta.
 
¡Seguimos!. Por último y de momento, el siguiente paso después de obtener los valores agrupados de dos en dos bytes, es convertirlos a sus opcodes equivalentes y, en el caso del ejemplo aquí los tenemos:

280F goto  0x00F  
00CE movwf memoria_4e   
0E03 swapf STATUS, W
1283 bcf   STATUS, 5
008F movwf memoria_0f   
0804 movf  FSR, W   
0090 movwf memoria_10   
080A movf  PCLATH, W
0091 movwf memoria_11   
084F movf  memoria_4f, W
0092 movwf memoria_12   
2B49 goto  ETQ_349


Ahí tenemos el pedacito de programa en lenguaje ensamblador mucho más fácil de interpretar, aunque todavía se puede mejorar y mucho. Además si observáis, hay varias cosas que os deberían llamar la atención. La primera es con los valores en hexadecimal, os habréis dado cuenta de que están "invertidos" los bytes con respecto a los originales del ejemplo. Bueno, esto tiene nuevamente que ver con la arquitectura del microcontrolador y como gestiona su memoria. En este caso, se almacenan con el valor menos significativo primero y el más significativo después lo que en informática denominamos como Little-Endian (su opuesto sería el Big-Endian), pero bueno, esto es otro tema.
 
Con el fin de mejorar todavía un poco más el código ensamblador generado, he añadido las direcciones de memoria en la primera columna, para ver claramente que el primer salto (goto) se hace a la dirección indicada por 0x0F, a la cual la he "etiquetado" como Inicio_00F. También se ve claramente que el programa inicia su ejecución en la dirección 0x04, etc.
 
Explicar aquí porque empieza el programa con esa primera instrucción de salto, tampoco es plan, pero por si os pica la curiosidad, tiene que ver con el vector reset (trocito de programa que ejecuta el microcontrolador cuando se produce una interrupción de tipo reset y bla, bla, bla).

Vector_Reset  org 0x000
        goto            Inicio_00f  ; 280F



Vector_Reset  org 0x000
 
04      movwf           mem_4e      ; 00CE
05      swapf           STATUS, W   ; 0E03
06      bcf             STATUS, 5   ; 1283
07      movwf           mem_0f      ; 008F
08      movf            FSR, W      ; 0804
09      movwf           mem_10      ; 0090
0A      movf            PCLATH, W   ; 080A
0B      movwf           mem_11      ; 0091
0C      movf            mem_4f, W   ; 084F
0D      movwf           mem_12      ; 0092
0E      goto            ETQ_349     ; 2B49


0F Inicio_00f

        ...


Como veis, se trata de un tema muy interesante que da mucho de sí. Podría estar horas y horas escribiendo sobre el tema hasta producir varios libros probablemente, pero como introducción para aquellos que os queráis animar, ya da!
 
Me consta que varías cosas de las que he hablado quedan en el aire, pero entenderéis que no es el objetivo de este post. Doy por supuesto, que los que estéis leyendo este post ya sabéis de que van todos esos detalles (código binario, conversión a hexadecimal, registros, organización de memoria, estructura de un opcode, etc.)
 
¡Menudo un rollo que he soltado! No se que será peor, si que yo lo escriba o que vosotros lo leáis, en fin, un saludo!

Comentarios

  1. saludos, para realizar este proceso de ingenieria inversa y obtener el codigo binario es necesario un dispositivo especial como un quemador de pic o algun software especial, por la atencion gracias.

    ResponderEliminar
    Respuestas
    1. Hola zapatlas. Dependerá de como esté almacenado el firmware y en donde. Por ejemplo, el firmware podría estar almacenado directamente en la memoria flash interna del microcontrolador, en cuyo caso si que podrías extraerlo con un programador (quemador), aunque puede que esté protegido, ¿ok? Además, el firmware también podría estar almacenado en una memoria externa al microcontrolador y, en este caso ya habrá que ver que tipo de memoria es y como se puede acceder a ella, para extraerlo directamente. En otras ocasiones habrá que "CAZAR" el firmware, cuando es traspasado desde una memoria externa al micro, y además podría estar cifrado. En fin, la verdad que depende de muchos factores, el tipo de microcontrolador, el interfaz de programación que se debe/puede utilizar (JTAG, ICSP, FTDI, etc.), incluso podría ser necesario llegar a "decapar" el micro para tener acceso a las líneas de comunicación internas (buses) del microcontrolador, para poder pinchar (sniffer) y extraer el código binario.

      Bueno compañero, espero que te pueda servir de algo esta pequeña aportación, un saludo, Manuel.

      Eliminar
  2. Felicitaciones por su post, utilizaré una parte de el para dictar mis clases de robótica a médicos. Saludos desde Lima Perú, raul@villaseca.info

    ResponderEliminar
    Respuestas
    1. Hola Dr.! Muchas gracias por su comentario, siempre es de agradecer y una gran satisfacción si en algo le puede servir lo publicado en este post. Un cordial saludo!

      Eliminar
  3. Hola Dr.! Muchas gracias por su comentario, siempre es de agradecer y una gran satisfacción si en algo le puede servir lo publicado en este post. Un cordial saludo!

    ResponderEliminar
  4. Este comentario ha sido eliminado por el autor.

    ResponderEliminar
  5. pleeda.com
    La importancia de contar con profesionales para el desarrollo de firmware es crucial en la era tecnológica actual. El firmware es el corazón de cualquier dispositivo electrónico, ya que es el software de bajo nivel que controla su funcionamiento y rendimiento. La inversión en expertos en este campo garantiza un desarrollo sólido y eficiente, así como numerosos beneficios para la empresa y los usuarios finales.

    ResponderEliminar

Publicar un comentario

Entradas populares de este blog

Como usar el TL431 (muy facil)

En este artículo, no vamos a entrar en el funcionamiento interno de este IC, ni tampoco en sus características técnicas, puesto que para esos fines ya existe su hoja de datos correspondiente. Más bien, lo que pretendo aquí es dejar constancia de como podemos utilizar este IC desde un punto de vista práctico, útil y sobre todo de una manera sencilla, con el objetivo de que cualquiera pueda utilizarlo. Si has llegado hasta aquí, probablemente ya sabes que por internet hay mucha información sobre este IC, pero también bastante confusa o excesivamente técnica, sin mostrar tan siquiera un ejemplo de funcionamiento, o como calcular sus pasivos. Pues se acabó, a partir de hoy y después de leer este post, ya te quedará claro como utilizar el TL431 para obtener una tensión de referencia estable y precisa. Vamos al grano y que mejor que empezar aclarando que el TL431 NO ES EXACTAMENTE UN ZENER como se empeñan en decir en muchos sitios, es verdad que se le conoce como el Zener Progra

WinRT with C++ Standard vs C++/CX

OFFTOPIC: Nota: Hoy he decidido escribir esta publicación del blog en inglés. Note: Today I decided to write this blog post in English. In a new application than I am developing for a company, I had to decide if to make use of C++/CX (C++ with Component Extension) or make all the main stuff in C++ standard and ABI/COM. All of you than have had to work with COM (Component Object Model) and fighting with the interfaces, reference count, etc. known the tricky and heavy that it can become. As an example of the easy approach using C++/CX, I am creating a new Uri object, like this: auto uriEasyWay = ref new Windows::Foundation:: Uri ( http://www.manuelvillasur.com ); assert (wcscmp(uriEasyWay->AbsoluteUri->Data(), L"http://www.manuelvillasur.com/" ) == 0); Now, I going to show you the more difficult approach using C++ Standard and  ABI/COM interfaces: HSTRING_HEADER header = {}; HSTRING string = nullptr ; HRESULT hr = WindowsCreateStringRefer

Árbol binario de expresión y Notación Posfija (II)

En una publicación anterior, hablaba sobre que es la notación posfija, para que puede ser útil y mostraba un pequeño ejemplo con una expresión aritmética simple: (9 - (5 + 2)) * 3 Pues bien, hoy voy a mostraros como podemos crear el árbol binario correspondiente para analizar o evaluar esta expresión, haciendo uso del recorrido en postorden. Lo primero que debemos hacer es crear el árbol, respetando las siguientes reglas: ⦁ Los nodos con hijos (padres) representarán los operadores de la expresión. ⦁ Las hojas (terminales sin hijos) representarán los operandos. ⦁ Los paréntesis generan sub-árboles. A continuación podemos ver cómo queda el árbol para la expresión del ejemplo (9 - (5 + 2)) * 3: Si queremos obtener la notación postfija a partir de este árbol de expresión, debemos recorrerlo en postorden (nodo izquierdo – nodo derecho – nodo central), obteniendo la expresión: 952+-3x Así, si quisiéramos evaluar la expresión, podemos hacer uso de un algoritmo