Hardware reversing 101

By Miguel Angel G
@M23Gzz


El siguiente reto fue presentado durante el UTCTF, en el cual el equipo Mayas logró una de sus mejores actuaciones, posicionándose en el lugar 19 a nivel mundial.

The Securest Vault ™
Descripción del reto:

    You've been contracted to check an IoT vault for a potential backdoor. Your goal is to figure out the sequence that unlocks the vault and use it to create the flag. Unfortunately, you were unable to acquire the hardware and only have a schematic drawn by the vault company's intern, a firmware dump and the model number of the microcontroller. The flag has the following format:

utflag{P<PORT_LETTR><PORT_NUMBER>}

      by Dan


Figura 1  - Diagrama esquemático del sistema.

Nos proveen dos archivos, uno es un diagrama esquemático del sistema (Figura 1), y el segundo es una imagen del firmware (archivo .AXF). Antes de comenzar con el análisis del binario, me dí a la tarea de tratar de entender en qué consistía el sistema. Lo primero que se me vino a la cabeza con la información proporcionada, fue una especie de caja de seguridad, la cual puede abrirse presionando una determinada combinación de botones, no me quedaba claro aún si éstos se tenían que presionar en secuencia o todos al mismo tiempo. En el diagrama no queda muy clara la conexión, pero puedo asumir que la línea que conecta con el arreglo de resistencias representa un tipo de bus, el cual contiene un hilo o conexión hacia un puerto en específico, de tal manera que el controlador es capaz de detectar qué botón se está presionando y en qué momento. Por otro lado, el puerto A representa únicamente un hilo, el cual está conectado a un actuador que se abre o cierra dependiendo del voltaje (0 o 3.3 V) presente en dicho puerto.

Ya con esta información es necesario plantear el tipo de firmware con el que nos vamos a enfrentar. En general para todo sistema embebido, se tiene el siguiente esquema de trabajo:

  •  Contamos con funciones de inicialización de memoria, creadas por el compilador, que se ejecutan antes de la función main.
  •  Se inicia la rutina main, y por lo general siempre se realiza una inicialización de los periféricos que se vayan a ocupar.
  • Se entra en una condición de ciclo infinito, dentro de la cual puede o no haber código dependiendo del tipo de sistema, en muchas ocasiones, la única tarea que realiza el controlador dentro de estas rutinas es "polear" el valor de algún registro e identificar cambios en el mismo. En otras condiciones también podemos encontrar que los eventos se registran por medio de interrupciones producidas por un timer o por un cambio de voltaje en algún pin.
Teniendo presente lo anterior, tratamos de encontrar estas piezas de código dentro de la imagen que se nos es proporcionada.

La imagen proporcionada es un archivo AXF, en el cual se incluye tanto el código del firmware compilador, además de otra información relevante para el depurador, en este caso podemos extraer únicamente las piezas de código utilizando las utilerías de keil para ARM con el siguiente comando:

fromelf --bin --output=outfile.bin UTCTF_VAULT.axf


Inspeccionando el binario en un de-compilador podemos encontrar la siguiente función (Figura 1a).

void FUN_00000430(void)

{

  undefined auStack40 [4];

  undefined auStack36 [36];  

  FUN_00000694(auStack36);

  FUN_000004c4(auStack40,auStack36);

  do {

    FUN_00000322();

  } while( true );

}

Figura 1a. Función main.

Observamos en su contenido el formato que habíamos predicho anteriormente, dos funciones de inicialización y una condición de ciclo infinito.

Indagando un poco dentro de las funciones.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
undefined4 * FUN_000004c4(undefined4 *puParm1,undefined4 uParm2)
{
    FUN_0000030c();
    FUN_00000448(4);
    FUN_0000053c(uParm2);
    // Habilita reloj puerto E y puerto A
    _DAT_400fe608 = _DAT_400fe608 | 0x11;
    // Configura puerto E(0-5) como entrada
    _DAT_40024400 = 0;
    // Habilita puerto E(0-3)
    _DAT_4002451c = _DAT_4002451c | 0xf;
    // Configura Puerto A(0) como salida
    _DAT_40004400 = _DAT_40004400 | 0x01;
    // Habilita puerto A
    _DAT_4000451c = _DAT_4000451c | 1;
    *puParm1 = uParm2;
    _DAT_e000e018 = 0;
    // Activa interrupciones
    FUN_00000310();
    return puParm1;
}
Figura 2. Función de inicialización de periféricos.

Encontramos en la Figura 2, código de inicialización de algún periférico, para saber exactamente cuál es debemos referirnos a la hoja de datos de nuestro microcontrolador.

La hoja de datos (p. 93) muestra que el espacio de memoria reservado para las localidades 0x40024000-0x40024fff son reservados para el puerto E, El espacio 0x400fe000-0x400fefff se encuentra reservado para el system control, particularmente para estos microcontroladores, en eso registros podemos tener control sobre los periféricos que queremos activar, y los relojes del sistema. El espacio de memoria 0x40004000-0x40004fff se encuentra reservado para el control del puerto A.

En el código de la figura 2 observamos que se cargan datos en diferentes localidades de memoria, de acuerdo con el manual, el offset 0x51c para cualquier puerto es para habilitarlo, mientras que el offset 400 es para configurar entrada o salida (0 o 1) del puerto. Deducimos que el sistema está haciendo uso del puerto E (leyenda "port?" en el diagrama) para la entrada de los botones y el puerto A para la salida hacia el actuador, .

La función FUN_00000694 (Figura 2)  también llama la atención.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void FUN_00000694(int iParm1)
{

  int iVar1;
  iVar1 = 0;
  *(undefined *)(iParm1 + 1) = 0;
  *(undefined *)(iParm1 + 0x11) = 0;
  while (iVar1 < 0xf) {
    *(undefined *)(iParm1 + 2 + iVar1) = 0;
    iVar1 = iVar1 + 1;
  }
  *(undefined *)(iParm1 + 0x12) = 0;
  *(undefined *)(iParm1 + 0x13) = 2;
  *(undefined *)(iParm1 + 0x14) = 3;
  *(undefined *)(iParm1 + 0x15) = 0;
  *(undefined *)(iParm1 + 0x16) = 1;
  *(undefined *)(iParm1 + 0x17) = 3;
  *(undefined *)(iParm1 + 0x18) = 2;
  *(undefined *)(iParm1 + 0x19) = 1;
  *(undefined *)(iParm1 + 0x1a) = 0;
  *(undefined *)(iParm1 + 0x1b) = 2;
  *(undefined *)(iParm1 + 0x1c) = 3;
  *(undefined *)(iParm1 + 0x1d) = 0;
  *(undefined *)(iParm1 + 0x1e) = 2;
  *(undefined *)(iParm1 + 0x1f) = 1;
  *(undefined *)(iParm1 + 0x20) = 0;
  return;
}
Figura 3. Inicialización de memoria.

Se observa en la figura 3 un código que inicializa dos espacios de memoria, en el primero, se asignan 14 ceros y un byte extra igualmente con cero, mientras que en el segundo igualmente se cuenta con la localidad más baja inicializada en cero y 14 valores que van de 0 a 3, posteriormente encontramos que los valores cargados en estos segmentos de memoria son utilizados para construir la flag.

El resto del código de la función main (Figura 2) es un loop infinito, por lo cual podemos asumir que el microcontrolador está esperando por alguna interrupción, por lo cual debemos encontrar el código de esta interrupción. Encontrar la interrupción es una tarea más sencilla de lo que parece, primero debemos referirnos al vector de interrupción, y posteriormente comparar los valores contenidos, normalmente las interrupciones que no están habilitadas comparten una función de interrupción genérica.


Offset Contenido
00000038 35 03 00 00    
SysTick
0000003c 57 05 00 00    
IRQ
00000040 39 03 00 00    

Figura 4 - Vector de interrupciones



En el vector de interrupciones (Figura 4) podemos observar que la interrupción 3c, que pertenece al generador de “ticks”, apunta hacia la dirección 0x557 en el programa, dicha dirección contiene el código de la figura 5. Este vector de interrupciones fue rescatado al extraer los segmentos de memoria, con el procedimiento mencionado al inicio y corresponde con las localidades más bajas del archivo outfile.bin.

fromelf --bin --output=outfile.bin UTCTF_VAULT.axf

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
void FUN_00000556(void)
{
  uint uVar1;
  int iVar2;
  iVar2 = FUN_0000078c(DAT_20000000);
  uVar1 = _DAT_400243fc;
  if (iVar2 &lt; 0xf) {
    iVar2 = FUN_00000756(DAT_20000000);
    if (iVar2 == 0) {
      if ((uVar1 &amp; 1) != 0) {
        FUN_0000075c(DAT_20000000,0);
        FUN_000006ec(DAT_20000000);
      }
      if ((uVar1 &amp; 2) == 2) {
        FUN_0000075c(DAT_20000000,1);
        FUN_000006ec(DAT_20000000);
      }
      if ((uVar1 &amp; 4) == 4) {
        FUN_0000075c(DAT_20000000,2);
        FUN_000006ec(DAT_20000000);
      }
      if ((uVar1 &amp; 8) == 8) {
        FUN_0000075c(DAT_20000000,3);
        FUN_000006ec(DAT_20000000);
      }
    }
    else {
      iVar2 = FUN_00000756(DAT_20000000);
      if (iVar2 != 0) {
        iVar2 = FUN_00000770(DAT_20000000);
        if (iVar2 == 0) {
          if ((uVar1 &amp; 1) == 0) {
            FUN_00000784(DAT_20000000);
            FUN_000006ec(DAT_20000000);
          }
        }
        else {
          if (iVar2 == 1) {
            if ((uVar1 &amp; 2) == 0) {
              FUN_00000784(DAT_20000000);
              FUN_000006ec(DAT_20000000);
            }
          }
          else {
            if (iVar2 == 2) {
              if ((uVar1 &amp; 4) == 0) {
                FUN_00000784(DAT_20000000);
                FUN_000006ec(DAT_20000000);
              }
            }
            else {
              if ((iVar2 == 3) &amp;&amp; ((uVar1 &amp; 8) == 0)) {
                FUN_00000784(DAT_20000000);
                FUN_000006ec(DAT_20000000);
              }
            }
          }
        }
      }
    }
  }
  else {
    FUN_0000070c(DAT_20000000);
  }
  return;
}


Figura 5. Interrupción Systick

La función anterior (Figura 5) realiza lo siguiente: 
Obtiene el valor del puerto E, configurado como entrada y con los botones conectados a una resistencia de pull down (de acuerdo con el diagrama que nos proporciona el reto), lo que significa que su valor es siempre cero hasta que se presione el botón, y en ese momento se lee 1, en dicho puerto.

De acuerdo con el bit del puerto presionado, se asigna el valor en una localidad de memoria en una región de 14 bytes (DAT_20000002-DAT_20000010), ej [0,2,3,...] lo anterior se realiza secuencialmente hasta obtener 14 valores, posteriormente se manda a llamar a la función  FUN_0000070c (Figura 6), la cual simplemente compara los 14 valores obtenidos, con los 14 valores que se cargaron en memoria (DAT_20000012-DAT_20000020) al inicio del programa, en la función FUN_00000694 (Figura 3).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void  FUN_0000070c (unsigned __int8 *a1)
{
    unsigned __int8 *v1; // r4
    int i; // r5
    int v3; // r0
    v1 = a1;
    disable_interupts();
    for ( i = 0; ; ++i )
    {
        if ( i >= 15 )
        {
            enable_portA();
            return;
        }
        if ( v1[i + 2] != v1[i + 18] ) // cmp 
            break;
    }
    v3 = 0;
    v1[1] = 0;
    // Vuelve a inicializar los valores originales a 0
    while ( v3 < 15 )
        v1[v3++ + 2] = 0;
    NVIC_ST_CURRENT_R = 0;
    enable_interupts();
}
Figura 6. Función de omparación.

La lógica descrita anteriormente da como resultado que cuando se presiona el botón conectado en el pin E0 se carga en memoria el valor 0, mientras que si se presiona E2 se carga en memoria el valor 2.
Con lo anterior ya podemos construir nuestra flag, la cual es:
utflag{PE0,PE2,PE3,PE0,PE1,PE3,PE2,PE1,PE0,PE2,PE3,PE0,PE2,PE1,PE0}

Go Mayas!!!


Comentarios