Índice
Hace unas semanas hablé de un desafío propuesto por los profesores de Estructura de computadores de mi facultad. Ahora que ha finalizado el plazo de entrega de la práctica, escribo este artículo con los resultados que obtuve.
Como comenté, la practica consiste en averiguar dos contraseñas de un programa escrito en C compilado para 32 bits y sin opciones de depuración. El guión está escrito por los profesores Javier Fernandez y Mancia anguita bajo licencia creative commons BY-NC-SA y disponible para descargar más abajo en las referencias.
Todos los programas escritos por los alumnos estan basados en este:
#include <stdio.h> // para printf()
#include <stdlib.h> // para exit()
#include <string.h> // para strncmp()/strlen()
#include <sys/time.h> // para gettimeofday(), struct timeval
char password[]="abracadabran";
int passcode = 7777;
void boom(){
printf("***************n");
printf("*** BOOM!!! ***n");
printf("***************n");
exit(-1);
}
void defused(){
printf("·························n");
printf("··· bomba desactivada ···n");
printf("·························n");
exit(0);
}
int main(){
#define SIZE 100
char pass[SIZE];
int pasv;
#define TLIM 5
struct timeval tv1,tv2; // gettimeofday() secs-usecs
gettimeofday(&tv1;,NULL);
printf("Introduce la contraseña: ");
fgets(pass,SIZE,stdin);
if (strncmp(pass,password,strlen(password)))
boom();
gettimeofday(&tv2;,NULL);
if (tv2.tv_sec - tv1.tv_sec > TLIM)
boom();
printf("Introduce el código: ");
scanf("%i",&pasv;);
if (pasv!=passcode) boom();
gettimeofday(&tv1;,NULL);
if (tv1.tv_sec - tv2.tv_sec > TLIM)
boom();
defused();
}
Al cual se le aplican los métodos deseados por el alumno para cifrar la contraseña que elija.
Conseguí descifrar 2 programas en total, aunque de unos de ellos solo la contraseña alfanumérica, luego veremos la razón. Empecemos con el primer programa:
Primera Bomba
Puedes descargar el programa desde este enlace:
Lo primero que hay que hacer es un estudio del comportamiento del programa usando gdb y objdump -d. Tras ejecutar el programa paso a paso con gdb y predecir cuales son los puntos críticos creé un archivo de configuración para gdb que me ayudara a facilitar el depurado:
b main
b *0x804a034
b *0x8048706
b *0x80486ec
b *0x80486e7
b *0x80486c9
b *0x80486c4
b *0x804871e
b *0x804872c
b *0x804874e
display /wx $eip
display /32xw $esp
display /s $eax
display /w (char*)$eax
display (char*)0x804a034
display /wx 0x804a03
Mediante el proceso de depurado, me dí cuenta que las instrucciones clave eran las siguientes; para la contraseña alfanumérica:
La dirección 0x804a034
es un puntero a la cadena de texto “aeiouuuu", con un espacio al final, esta es la contraseña codificada.
En algún punto del programa, se deben comparar la contraseña elegida con la introducida por el usuario con strncmp
, en dicho punto, la contraseña debe estar decodificada. A base de depurar se observa que ese proceso comienza aquí:
80486c9: 0f b6 05 34 a0 04 08 movzbl 0x804a034,%eax
80486d0: 83 c0 01 add $0x1,%eax
80486d3: a2 34 a0 04 08 mov %al,0x804a034
Estas tres instrucciones extraen la primera letra de la contraseña; la a, le suman 1 y la vuelven a colocar en la cadena de texto, quedando “beiouuuu".
Luego en este fragmento:
80486d8: 0f b6 05 36 a0 04 08 movzbl 0x804a036,%eax
80486df: 83 e8 02 sub $0x2,%eax
80486e2: a2 36 a0 04 08 mov %al,0x804a036
Se extrae la i y resta dos, quedando g, la tercera instrucción sustituye la i en la cadena, obteniendo como resultado “begouuuu".
No se realizan más modificaciones a la contraseña almacenada en el programa. Ahora toca averiguar qué operaciones se realizan sobre la contraseña introducida por el usuario:
La contraseña introducida por el usuario se encuentra en 0x28(%esp)
, el programa resta uno al segundo caracter de la contraseña introducida y lo sustituye:
80486e7: 0f b6 44 24 29 movzbl 0x29(%esp),%eax ; con 0x29(%esp) obtiene el segundo caracter
80486ec: 83 e8 01 sub $0x1,%eax
80486ef: 88 44 24 29 mov %al,0x29(%esp)
Luego, hace algo parecido con la cuarta letra de la contraseña introducida por el usuario:
80486f3: 0f b6 44 24 2b movzbl 0x2b(%esp),%eax
80486f8: 83 c0 01 add $0x1,%eax
80486fb: 88 44 24 2b mov %al,0x2b(%esp)
La diferencia es que esta vez suma uno, en lugar de restar.
Llegados al punto donde se llama a strncmp, vemos claramente en la pila qué parámetros se le están pasando:
2: x/32xw $esp
0xffffd2f0: 0xffffd318 0x0804a034 0x0000000a 0xffffd3a4
1: x/xw $eip
0x804871e <strncmp>: 0xfffe01e8
(%esp) [0xffffd318] -> puntero a la contraseña introducida por el usuario
0x4(%esp) [0x0804a034] -> puntero a la contraseña del programa
0x8(%esp) [0x0000000a] -> longitud que se quiere comparar (10).
Teniendo en cuenta las transformaciones hechas, la contraseña a introducir es bfgnuuuu . ya que ‘f’-1 = ‘e’ y ‘n’+1=’o’, resultando begouuuu
Una vez descubierta la contraseña alfanumérica, pasé a la numérica, que resultó ser bastante sencilla. Esta vez la clave de todo está en estas instrucciones:
804877a: a1 40 a0 04 08 mov 0x804a040,%eax
804877f: 05 f4 01 00 00 add $0x1f4,%eax
8048784: a3 40 a0 04 08 mov %eax,0x804a040
8048789: 8b 44 24 24 mov 0x24(%esp),%eax
804878d: 05 c8 00 00 00 add $0xc8,%eax
Se suma 500 (0x1f4)
a la contraseña original, resultando 1500, y 200 (0xc8) a la contraseña que introduzca el usuario. Haciendo la comparación de 1000+500 con PASSINTRODUCIDA+200, se deduce que la contraseña que se debe introducir es 1300.
Segunda Bomba
Puedes descargar el programa desde este enlace.
Como en la bomba anterior, despues de investigar un poco, creé un archivo de sesión para gdb con los puntos críticos:
b main
b cambio
b *0x804882a
b begin
b change
b code
b *0x804880e
b *0x804888d
display /wx $eip
display /d $ecx
display /d $eax
display /d $edx
display /32xw $esp
display /s $eax
display /w (char*)$eax
display (char*)0x804a034
display /wx 0x804a034
Llegué a la siguiente conclusión:
La contraseña codificada es C4b3Z0n, pero únicamente se comprueba la primera letra. Si el usuario introduce como contraseña C4b3Z0n, la bomba explota, ya que en la función cambio
, se comprueba que la primera letra de la contraseña no sea igual a C, de ser igual, se llama a la bomba:
movzbl (%eax),%eax
cmp $0x43,%al
jne 80486a1 <cambio>
call 804860c <boomb>
Llegados a este punto, si no ha explotado, se compara la primera letra con “Q”, si la primera letra de la contraseña introducida es “Q” también, se cambia por “c”. Todo lo mencionado anteriormente se hace dentro de la función cambio. En resumen, la función cambio comprueba que la primera letra no sea “C”, si no es “C”, compara con “Q”, de ser “Q”, la cambia por una “c”, dicha “c”, la cambiará la función change
por una “C”, concretamente aquí:
80486be: 0f b6 00 movzbl (%eax),%eax ; Extrae la primera letra
80486c1: 3c 63 cmp $0x63,%al ; La compara con 'C'
80486c3: 75 06 jne 80486cb <change> ; Si no son iguales sale de la funcion
80486c5: 8b 45 08 mov 0x8(%ebp),%eax ; si son iguales carga la contraseña entera en eax
80486c8: c6 00 43 movb $0x43,(%eax) ; y sustituye la primera letra por 'C'
Por tanto, la contraseña que el usuario debe introducir es c4b3Z0n, para que al realizar el cambio quede como C4b3Z0n. Si se introduce por contraseña Q4b3Z0n, se reemplaza por c4b3Z0n que es distinto de C4b3Z0n, detonando la bomba.
En este ejecutable no conseguí descubrir la contraseña numérica por la siguiente razón, el código que realiza las operaciones es:
080486cd <code>:
80486cd: 55 push %ebp
80486ce: 89 e5 mov %esp,%ebp
80486d0: 83 ec 18 sub $0x18,%esp
80486d3: c7 45 e8 00 00 00 00 movl $0x0,-0x18(%ebp)
80486da: c7 45 f4 00 00 00 00 movl $0x0,-0xc(%ebp)
80486e1: c7 45 ec 00 00 00 00 movl $0x0,-0x14(%ebp)
80486e8: 81 45 08 1a 27 00 00 addl $0x271a,0x8(%ebp)
80486ef: 81 45 08 e8 03 00 00 addl $0x3e8,0x8(%ebp)
80486f6: 83 45 08 02 addl $0x2,0x8(%ebp)
80486fa: 8b 45 08 mov 0x8(%ebp),%eax
80486fd: 89 45 f0 mov %eax,-0x10(%ebp)
8048700: eb 4f jmp 8048751 <code>
8048702: 8b 4d f0 mov -0x10(%ebp),%ecx
8048705: ba 67 66 66 66 mov $0x66666667,%edx
804870a: 89 c8 mov %ecx,%eax
804870c: f7 ea imul %edx
804870e: c1 fa 02 sar $0x2,%edx
8048711: 89 c8 mov %ecx,%eax
8048713: c1 f8 1f sar $0x1f,%eax
8048716: 29 c2 sub %eax,%edx
8048718: 89 d0 mov %edx,%eax
804871a: c1 e0 02 shl $0x2,%eax
804871d: 01 d0 add %edx,%eax
804871f: 01 c0 add %eax,%eax
8048721: 89 ca mov %ecx,%edx
8048723: 29 c2 sub %eax,%edx
8048725: 89 d0 mov %edx,%eax
8048727: 89 45 f4 mov %eax,-0xc(%ebp)
804872a: 8b 45 f4 mov -0xc(%ebp),%eax
804872d: 01 45 e8 add %eax,-0x18(%ebp)
8048730: 8b 4d f0 mov -0x10(%ebp),%ecx
8048733: ba 67 66 66 66 mov $0x66666667,%edx
8048738: 89 c8 mov %ecx,%eax
804873a: f7 ea imul %edx
804873c: c1 fa 02 sar $0x2,%edx
804873f: 89 c8 mov %ecx,%eax
8048741: c1 f8 1f sar $0x1f,%eax
8048744: 89 d1 mov %edx,%ecx
8048746: 29 c1 sub %eax,%ecx
8048748: 89 c8 mov %ecx,%eax
804874a: 89 45 f0 mov %eax,-0x10(%ebp)
804874d: 83 45 ec 01 addl $0x1,-0x14(%ebp)
8048751: 83 7d ec 04 cmpl $0x4,-0x14(%ebp)
8048755: 7e ab jle 8048702 <code>
8048757: 83 7d e8 17 cmpl $0x17,-0x18(%ebp)
804875b: 74 07 je 8048764 <code>
804875d: e8 aa fe ff ff call 804860c <boomb>
8048762: eb 05 jmp 8048769 <code>
8048764: e8 d9 fe ff ff call 8048642 <bomb>
8048769: 8b 45 08 mov 0x8(%ebp),%eax
804876c: c9 leave
804876d: c3 ret
Gran parte de este código se usa para calcular un simple módulo, la razón; gcc realiza esta optimización porque la instrucción div
a pesar de ser una sola, es más lenta que todo este código. Si quieres profundizar más en este tema, en las referencias hay un enlace a stackoverflow que explica qué método se sigue para calcular el módulo.
El código de mi programa Bomba
Para escribir mi programa me basé en técnicas de ofuscación que encontré por internet, con un poco de imaginación se puede hacer muy dificil la lectura de un programa:
/*
============================================================================
Name : Boom.c
Author : Alejandro Alcalde
Version : 0.1
Description : Práctica sobre ingeniería inversa
============================================================================
*/
#include <stdio.h> // para printf()
#include <stdlib.h> // para exit()
#include <string.h> // para strncmp()/strlen()
#include <sys/time.h> // para gettimeofday(), struct timeval
#define SIZE 15
#define ESTO printf(
#define ES "%sn"
#define OFUSCACION ,(char *) v);
//char password[] = "abracadabran";
int passcode = 555;
void boom() {
printf("***************n");
printf("*** BOOM!!! ***n");
printf("***************n");
exit(-1);
}
void defused() {
printf("·························n");
printf("··· bomba desactivada ···n");
printf("·························n");
exit(0);
}
double password[3];
char* decode(char* p){
int a = 0;
char* f;
f = (char*) malloc(sizeof(char)*SIZE);
for (a = 0; a < 10; a++)
f[a] = p[a * 2 + 1];
return f;
}
void confuse2(int* pw){
int i = 0;
char* salt = "wE9mg9pu2KSmp5lh";
for (i = 0; i < 16; i++)
*pw ^= salt[i];
*pw+=7777;
}
int main(_, v) double *v; int _;{
int pasv;
#define TLIM 60
struct timeval tv1, tv2; // gettimeofday() secs-usecs
double h[3];
switch (_) {
case 0:
// ESTO ES OFUSCACION
break;
case 45681:
strcpy((char*) password, (char*)v);
confuse2(&passcode;);
main(0, v);
break;
default:
h[0] = 13027340775320732841130839654634808548322878081841199945244886528920637933617152.000000;
h[1] = 3870500591494514751058285253136238534286695148502666756138516046378808251612945489502056433082093156719316295785906296012743611709256336712091456794020400600332451080740411432505870026138587691271552924066658849697642476166184960.000000;
h[2] = 0;
main(45681, h);
//main(45681, "@M?eg \PoiRlLldo!�");
break;
}
//Pedimos datos al usuario
char f[SIZE];
gettimeofday(&tv1;,NULL);
printf("Introduce la contraseña: ");
fgets(f,SIZE,stdin);
if (strncmp(f,decode((char*)password),strlen(decode((char*)password))))
boom();
gettimeofday(&tv2;,NULL);
if (tv2.tv_sec - tv1.tv_sec > TLIM)
boom();
printf("Introduce el código: ");
scanf("%i",&pasv;);
confuse2(&pasv;);
if (pasv!=passcode)
boom();
gettimeofday(&tv1;,NULL);
if (tv1.tv_sec - tv2.tv_sec > TLIM)
boom();
defused();
return 0;
}
Explicaré por encima su funcionamiento. En la función main
se evalua el primer argumento, que contiene el número de parámetros pasados a la función, contanto también con el nombre del programa, como no pasamos ningún argumento el valor por defecto será 1, y como este valor no se encuentra en el switch, entra en el default, Los números tan largos que ves son la contraseña representada en formato double, en concreto esta cadena @M?eg \PoiRlLldo!�. Tras asignar la contraseña a un array de doubles, llamo recursivamente a la función main, pasando como argumento el número 65381 y el array con la contraseña. De modo que esta vez el switch entra en la segunda sentencia (case 45681:), en la que se copia el array con la contraseña en double a una cadena de caracteres.
Por muy complicado que parezca, el código ensamblador solo tiene un punto clave para descubrir la contraseña:
En cualquier optimización, ya sea 0, 1 o 2, para averiguar la contraseña basta con poner un punto de ruptura en la función decode:
08048870 <decode>:
8048870: 53 push %ebx
8048871: 83 ec 18 sub $0x18,%esp
8048874: c7 04 24 0f 00 00 00 movl $0xf,(%esp)
804887b: 8b 5c 24 20 mov 0x20(%esp),%ebx
804887f: e8 4c fc ff ff call 80484d0 <malloc>
8048884: 31 d2 xor %edx,%edx
8048886: 66 90 xchg %ax,%ax
8048888: 0f b6 4c 53 01 movzbl 0x1(%ebx,%edx,2),%ecx
804888d: 88 0c 10 mov %cl,(%eax,%edx,1)
8048890: 83 c2 01 add $0x1,%edx
8048893: 83 fa 0a cmp $0xa,%edx
8048896: 75 f0 jne 8048888 <decode>
8048898: 83 c4 18 add $0x18,%esp
804889b: 5b pop %ebx
804889c: c3 ret
804889d: 8d 76 00 lea 0x0(%esi),%esi
En la que se va extrayendo de la cadena @M?eg \PoiRlLldo!� los caracteres impares, mediante un for:
8048888: 0f b6 4c 53 01 movzbl 0x1(%ebx,%edx,2),%ecx
804888d: 88 0c 10 mov %cl,(%eax,%edx,1)
8048890: 83 c2 01 add $0x1,%edx
8048893: 83 fa 0a cmp $0xa,%edx
8048896: 75 f0 jne 8048888 <decode>
Con lo cual, antes de retornar de la función, si miramos el valor de EBX encontramos la contraseña a usar para desactivar la bomba.
La clave está en cómo interpretar en el main, concretamente en la línea
80486d5: 89 54 24 04 mov %edx,0x4(%esp)
el valor de EDX, pues es un double, que oculta la cadena cifrada @M?eg \PoiRlLldo!�.
Valor que veremos si examinamos en gdb con x /gs $edx
También podemos deducir el tamaño de la contraseña mirando la pila antes de llamar a strncmp, pues el tercer valor de la pila 0x8(%esp), es la longitud. En cuanto al pin, la clave está en mirar este trozo de código:
80486b0: 0f be 08 movsbl (%eax),%ecx
80486b3: 83 c0 01 add $0x1,%eax
80486b6: 31 ca xor %ecx,%edx
80486b8: 3d 90 89 04 08 cmp $0x8048990,%eax
80486bd: 75 f1 jne 80486b0 <main>
80486bf: 81 c2 61 1e 00 00 add $0x1e61,%edx
Que va sumando al pin el resultado de hacer un XOR de su valor con el valor de una letra almacenada en %ecx y al resultado le suma 7777. Con lo cual tenemos que conseguir un número al que al hacerle todas estas operaciones de 8305, que resulta ser 555.
Referencias
- Calculating modulo in assembly | Visitar sitio
- Obfuscation | Visitar sitio
¿Has visto algún error?: Por favor, ayúdame a corregirlo contactando conmigo o comentando abajo.