Random IRC quote :      <pepepotamo> a ver cuando comprais 49bits.com, que siempre me equivoco

REconstruyendo código C en plataformas no x86 (MIPS, Parte II)

En un post anterior comenté como se puede REconstruir código C desde ensamblador MIPS con programas muy básicos: No tenían sentencias condicionales, ni bucles, ni switches, ni estructuras, ni nada de nada. En este post voy a hablar de como reconstruir código algo más complejo, con sentencias condicionales y con bucles.

Sentencias condicionales

Imaginemos el siguiente código muy simple que incluye una sentencia condicional (IF):

int foo(char *arg, int size)
{
char buf[20];

        if (size == 0)
                return -1;

        strncpy(buf, arg, size);
        return(strlen(buf) == 0);
}

Este programa tan simple que solamente verifica que el tamaño de size no sea 0 y que copia un buffer recibido en una variable local a la función, compilado quedaría como sigue:

foo:                                     # CODE XREF: main+68^Yp

var_28          = -0x28
var_10          = -0x10
var_8           = -8
var_4           = -4
arg_0           =  0
arg_4           =  4

                addiu   $sp, -0x38
                sw      $ra, 0x38+var_4($sp)
                sw      $fp, 0x38+var_8($sp)
                move    $fp, $sp

                sw      $a0, 0x38+arg_0($fp)
                sw      $a1, 0x38+arg_4($fp)
                lw      $v0, 0x38+arg_4($fp)
                nop
                bnez    $v0, loc_38
                nop
                li      $v0, 0xFFFFFFFF
                sw      $v0, 0x38+var_10($fp)
                j       loc_68
                nop
 # —————————————————————————

loc_38:                                  # CODE XREF: foo+20^Xj
                lw      $v1, 0x38+arg_4($fp)
                addiu   $v0, $fp, 0x38+var_28
                move    $a0, $v0
                lw      $a1, 0x38+arg_0($fp)
                move    $a2, $v1
                jal     strncpy
                nop
                addiu   $v0, $fp, 0x38+var_28
                lb      $v0, 0($v0)
                nop
                sltiu   $v0, 1
                sw      $v0, 0x38+var_10($fp)

loc_68:                                  # CODE XREF: foo+30^Xj
                lw      $v0, 0x38+var_10($fp)
                move    $sp, $fp
                lw      $ra, 0x38+var_4($sp)
                lw      $fp, 0x38+var_8($sp)
                addiu   $sp, 0x38
                jr      $ra
                nop
 # End of function foo

Si lo analizamos un poco nos encontramos el típico prólogo de función (que es en general ignorable):

()
; Prólogo de función
                addiu   $sp, -0x38
                sw      $ra, 0x38+var_4($sp)
                sw      $fp, 0x38+var_8($sp)
                move    $fp, $sp
()

…y justo después el código encargado de tomar la decisión, es decir, el código ensamblador correspondiente a la sentencia IF que hemos escrito:

               sw      $a0, 0x38+arg_0($fp)
                ; Guardamos el primer argumento a función en arg_0
                sw      $a1, 0x38+arg_4($fp)
                ; Guardamos el segundo argumento a función en arg_4
                lw      $v0, 0x38+arg_4($fp)
                ; Cargamos el valor de arg_4 (2º argumento) en $v0
                nop
                bnez    $v0, loc_38
                ; Saltamos a loc_38 si $v0 no es igual a cero (Branch on Not Equal to Zero)
                nop
                <<Do stuff>>

En este código vemos que la sentencia condicional no es como las típicas de IA32, donde se realiza una operación de comprobación y, a posterior, con otra instrucción, se decide a donde saltar: En este caso la instrucción de comprobación y de salto condicional son una misma. Pero, volviendo a lo que nos atañe, veamos como reescribir este código ensamblador en C:

if (arg2 != 0)
  goto loc_38;
<<Do stuff>>
loc_38:
  // Lo que sea….

Este pseudo-C, si bien es perfectamente correcto no es como el programador lo escribió casi con toda seguridad. Cuando se reconstruye código C desde ASM una de las primeras cosas que se tiene que tener en cuenta es que los saltos condicionales se invierten en la transición de C a código ASM, así pues, nuestro código tiene que quedar como sigue:

if (arg2 == 0)
  <<Do stuff>>
else
  // Lo que sea

Este código ya es bastante más lógico, más real. Ahora que ya sabemos como se reconstruyen sentencias condicionales, veamos como quedaría la reconstrucción inicial del código ensamblador de antes al completo:

void foo(int arg1, int arg2)
{
int var10;
int var28[0x28];

  /*
  lw      $v0, 0x38+arg_4($fp)
  bnez    $v0, loc_38
  */

  if (arg2 == 0)
  {
    /*
    li      $v0, 0xFFFFFFFF
    sw      $v0, 0x38+var_10($fp)
    j       loc_68
    */

   var10 = -1;
   goto loc_68;
  }
  else
    goto loc_38;

loc_38:
  /*
  lw      $v1, 0x38+arg_4($fp)
  addiu   $v0, $fp, 0x38+var_28
  move    $a0, $v0
  lw      $a1, 0x38+arg_0($fp)
  move    $a2, $v1
  jal     strncpy
  nop
  */

  strncpy(var28, arg1, arg2);
  /*
  addiu   $v0, $fp, 0x38+var_28
  lb      $v0, 0($v0)
  nop
  sltiu   $v0, 1
  sw      $v0, 0x38+var_10($fp)
  */

  var10 = (strlen(var28) == 1);
loc_68:
  // lw      $v0, 0x38+var_10($fp)
  return (var_10);
}

En esta primera parte del código reconstruido he dejado como comentarios el código ensamblador. En esta otra versión (ya sin los comentarios) se pueden ver las incogruencias con el código original, a pesar de que sea perfectamente válido este código:

void foo(int arg1, int arg2)
{
int var10;
int var28[0x28];

  if (arg2 == 0)
  {
   var10 = -1;
   goto loc_68;
  }
  else
    goto loc_38;

loc_38:
  strncpy(var28, arg1, arg2);
  var10 = (strlen(var28) == 1);

loc_68:
  return (var_10);
}

Como podemos ver, tenemos un montón de saltos incondicionales que son totalmente redudantes o que bien no tienen mucho sentido desde el punto de vista de un programador. Por ejemplo, en el IF que tenemos, justo dentro, le asignamos a var10 el valor -1 y acto seguido saltamos a loc_68 donde únicamente devolvemos el valor de esta variable sin hacer nada más. En el ELSE de esta sentencia condicional ponemos un salto incondicional a loc_38, que es justo la siguiente instrucción. Estas 2 cositas se pueden arreglar, quedando como siguen (ya de paso arreglo también los tipos de datos tal y como se explicó brevemente en la parte I de este artículo):

int foo(char arg1, int arg2)
{
char var28[0x28];

  if (arg2 == 0)
   return(-1)

  strncpy(var28, arg1, arg2);
  return((strlen(var28) == 1));
}

Pues ya está! Nuestra primera sentencia condicional reconstruida de código ensamblador a código C.

Bucles

Un bucle puede ser una instrucción FOR o un DO WHILE. Al final y al cabo, en ensamblador, se representarán casi del mismo modo siendo difícil a veces discernir entre que es un FOR o que es un WHILE, aunque esto nos da un poco igual. Veamos un ejemplo de un programa en C con un FOR:

void foo(int size)
{
int i;

        for (i = 0; i<size;i++)
                printf("%d\n", i);
}

…y su correspondiente código ensamblador (con comentarios):

foo:                                     # CODE XREF: main+50^Yp

var_10          = -0x10
var_8           = -8
var_4           = -4
arg_0           =  0

                addiu   $sp, -0x20
                ; Reserva 0x20 – 0x16 == 0x4 bytes para variables locales
                sw      $ra, 0x20+var_4($sp)
                sw      $fp, 0x20+var_8($sp)
                move    $fp, $sp
                ; Hasta aquí el prólogo que nos da igual

                sw      $a0, 0x20+arg_0($fp)
                sw      $0, 0x20+var_10($fp)
                ; var_10 vale 0 ahora
                j       loc_44
                ; Saltamos incondicionalmente a loc_44
                nop
 # —————————————————————————

loc_20:                                  # CODE XREF: foo+54^Yj
                lui     $v0, (_rodata_0 >> 16)
                addiu   $a0, $v0, (_rodata_0 &amp; 0xFFFF)
                ; Se pone una constante (rodata) en $a0
                lw      $a1, 0x20+var_10($fp)
                ; Ponemos var_10 en $a1
                jal     printf
                ; printf(rodata_const, var_10);
                nop
                lw      $v0, 0x20+var_10($fp)
                nop
                addiu   $v0, 1
                sw      $v0, 0x20+var_10($fp)
                ; var_10 += 1

loc_44:                                  # CODE XREF: foo+18^Xj
                lw      $v0, 0x20+var_10($fp)
                lw      $v1, 0x20+arg_0($fp)
                nop
                slt     $v0, $v1
                ; es var_10 < arg_0?
                bnez    $v0, loc_20
                ; salta a loc_20 var_10 menor que arg_0
                nop

                ; Epílogo de función
                move    $sp, $fp
                lw      $ra, 0x20+var_4($sp)
                lw      $fp, 0x20+var_8($sp)
                addiu   $sp, 0x20
                jr      $ra
                nop
 # End of function foo

En el código ensamblador comentado podemos ver como se utiliza una variable como un contador (la «i» de nuestro for) que se incrementa y como se encuentra una condición en la que se toma la decisión de saltar o no a una localización (loc_20). El código pseudo-c resultante de este código ensamblador (sin realizar ningún cambio) sería el siguiente:

int foo(int arg1)
{
int var10;

  var10 = 0;
  goto loc_44;

loc_20:
  printf(constante, var10);
  var10++;

loc_44:
  if (var10 < arg1)
    goto loc_20;
}

Al igual que en todos los casos anteriores, si bien este código es perfectamente válido, se ve a la legua que no es un código escrito por un programador «por lo general». Vemos que se inicializa una variable, se realiza una lógica y se comprueba más tarde una condición para determinar si continúa iterando o no. Esto es claramente un bucle FOR. Arreglemos este código un poquitmo más ahora:

void foo(int arg1)
{
int i;

  for (i=0;i<arg1;i++)
    printf(constante, i);
}
 

Perfecto! Ya sabemos identificar un bucle for perfectamente 🙂 Otro modo de escribir este bucle podría ser el siguiente:

void foo(int arg1)
{
int i;

  i = 0;
  do
  {
    printf(constante, i);
  } while (++i < arg1);
}

¿Cuál es la diferencia entre ambas formas? Realmente ninguna, simplemente, los bucles FOR nos parecen más lógicos y, visto que se inicializa un valor, se utiliza como contador y además como condición, suponemos que es un FOR, pero no lo podemos saber ya que el código DO WHILE genera exactamente el mismo código ensamblador que el bucle FOR.

Siguiente artículo: Switches, estructuras y un programa real

Bueno, espero que os haya gustado. Lo dejo ya aquí porque se hace muy grande el artículo. En la siguiente y última parte switches, estructuras y decompilación de un programa real.

14 Comentarios para “REconstruyendo código C en plataformas no x86 (MIPS, Parte II)”

  1. Comment por Ruben | 05/17/10 at 4:58 am

    Viendo lo que comentas, en teoría, ¿esa forma de ensamblar trae más problemas a la hora de reconstrucción automática que la de x86?

  2. Comment por hteso | 05/17/10 at 9:59 am

    Pues lo siguiente es que te hagas tu propio «F5» (dios, que mala rima tiene esto) para MIPS, no? 😛

  3. Comment por vierito5 | 05/17/10 at 8:03 pm

    Automatizarlo sin intervención humana está bastante más jodido pero anda que no molaría. Hex-Rays and Sparks! for MIPS 😛

  4. Comment por matalaz | 05/18/10 at 11:22 am

    @Ruben. Pues más bien lo contrario. En general, con los procs que he probado/mirado, creo que volver a C desde cualquier plataforma de procesador que no sea x86 es más fácil.

  5. Comment por Viti | 05/19/10 at 4:10 am

    Hola, Matalaz, Ruben y demás gente que ayudais a realizar el blog. Estoy empezando (intentando) aprender Ensamblador de momento 8086.
    Por lo que tengo entendido cada arcquitectura de procesador tiene su lenguaje, he leido sobre Pic, 8086 y ahora MIPS tengo unas dudas, haber si por favor me las podiais aclarar.

    Me basta con aprender 8086 o deberia aprender tambien los demás, creo que es en los que se basan la mayoria de procesadores intel. Deberia centrarme en algun otro de los mencionado u otro que no mencione.

    La pregunta viene porque me gustaria iniciarme en el mundo de los exploits, aunque creo que esta hecho para cracks, el saber no ocupa lugar.

    Gracias saludos

  6. Comment por Newlog | 05/19/10 at 5:22 am

    Mundo de los exploits: En la mayoría de casos te sirve el ensamblador de Intel para la mayoría de sistemas (ordenadores ‘normales’).

    Ahora ya… Si quieres ayudar a GeoHot a hackear la Play3, quizá te sirva aprender ensamblador MIPS 😉

  7. Comment por Viti | 05/19/10 at 5:36 am

    Ok muchas gracias. Me gustaría pero si ese mago no es capaz de hacer magia con la play 3 dudo que yo pueda…

  8. Comment por Newlog | 05/19/10 at 7:04 am

    Todo es tener tiempo libre 😉

  9. Comment por Viti | 05/20/10 at 6:03 am

    Buenas otra vez, Newlog podrias decirme como aprendiste ensamblador estoy leyendo varios manuales pero realmente no se si estan anticuados o no o si estoy mirando cosas que no tengo que mirar (vamos que estoy algo perdido).

    Tengo varios conceptos aprendidos los registros, direccionamientos, la definicion de los segmentos.
    Pero luego me pongo a mirar el codigo y no entiendo muchas instrucciones como por ejemplo cuando tengo que utilizar los registros altos(AH) y bajos (AL) en vez del AX se que los primeros son de 8 bit y el segundo es para cuando utilices de 16 bits. O por que para imprimir una cadena por pantalla tengo que mandar al AH,09H (MOV AH,09H )

    Tengo aproximadamente 1 mes y unas 8 horas diarias para aprender a manejar algo de esto veis poco tiempo o se puede hacer.

    Gracias y saludos al blog

  10. Comment por Newlog | 05/20/10 at 6:57 am

    Mírate los textos de Ricardo Narvaja. Es lo mejor que he visto sobre ensamblador. Un curso completísimo, enfocado al cracking y que empieza desde cero. Si te lo acabas leyendo todo, ya sabrás más que yo 😉

    Una vez hayas llegado a la mitad del curso de Ricardo ya serás capaz de entender bien los textos sobre shellcoding (técnica enfocada al exploiting).

    Pon su nombre en google y encontrarás su web. Bájate todo el «curso nuevo».

    Saludos y disfruta!

  11. Comment por Viti | 05/20/10 at 7:20 am

    Ok te lo agradezco.

  12. Comment por Rubén | 05/20/10 at 7:38 am

    Muy bueno el articulo, sólo decirte que lo que pones sobre el fro y el do while no es del todo correcto. El for evalua la guarda del bucle la primera vez antes de entrar en el bucle, en este caso mira si «i» es menor que «arg1», en cambio el do while ejecutaría primero la intruccioón que hay dentro del bucle «printf(constante, i);» y luego comprovaría la guarda. Por eso, si el valor de «arg1» fuera 0 en el caso del for no mostraría nada por pantalla, en cambio en el caso del do while lo mostraría una vez antes de evaluar la guarda y terminar el programa.

    Un saludo

  13. Comment por Viti | 05/20/10 at 11:00 am

    Gran sitio el que me dijiste Newlog. El curso al que te refieres son el nuevo y el viejo 58 rar en total empezare poco a poco desde el principio.
    Saludos

  14. Comment por Newlog | 05/21/10 at 10:45 am

    Para el que quiera trastear con MIPS y no disponga de un procesador físico puede utilizar el emulador SPIM: http://pages.cs.wisc.edu/~larus/spim.html

Se han cerrado los comentarios