sábado, 11 de mayo de 2013

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!

2 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