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):
{
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:
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:
; 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:
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:
<<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:
{
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:
{
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):
{
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:
{
int i;
for (i = 0; i<size;i++)
printf("%d\n", i);
}
…y su correspondiente código ensamblador (con comentarios):
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 & 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 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:
{
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:
{
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.