Y como todo comienzo tiene su final, con este articulo llegamos al final de este mínimo taller sobre el lenguaje ensamblador, pero no os preocupéis que he dejado la parte mas divertida para el final xD.
Como en anteriores artículos, vamos a necesitar, picar un pequeño programilla en C, para poder real izarlo, en este caso vamos a picar un pequeño «crackme«
¿Que es un CrackMe?
Un crackme, es un sencillo programa escrito para poder crackearlo de manera legal, digamos que es un programa escrito cuyo único fin es ser crackeado. Normalmente los crackMe cuando son ejecutados suelen pedir un usuario y contraseña o solo la contraseña, el fin del programa es saltarse esa protección. Recuerdo que en la mítica revista @rroba con cada articulo venia un crackme para windows que había que saltarse con software tan conocido como win32dasm,Ollydbg,etc.. en nuestro caso usaremos un código muy sencillo que sera el siguiente:
#include<stdio.h>
#include<string.h>
int main()
{
char *clave = "elbinario";
char *clave2;
int resultado;
printf("Introduce la clave :\n");
scanf("%s",clave2);
resultado = strncmp(clave, clave2,9 );
if(resultado != 0)
{
printf("Clave incorrecta\n");
}
else
{
printf("Clave correcta\n");
}
return 0;
}
El programa como podéis ver es muy sencillo, simplemente declaramos un par de punteros con la clave correcta(elbinario) y la clave introducida por el usuario y las comparamos, si son iguales muestra el mensaje correcto, si no lo es muestra el mensaje de incorrecto(podríamos haber puesto un bucle que se repitiera hasta ser correcto, pero creo que así se entiende bien)
-
Compilamos nuestro programa
gcc -o crackme crackme.c
-
Lo ejecutamos de manera normal
./crackme
Como vemos si metemos cualquier cadena nos devuelve un mensaje de clave incorrecta, en cambio si metemos la clave correcta(elbinario) Nos devuelve el mensaje correcto, lo que nosotros vamos a hacer es crackear el programa para que nos devuelva el mensaje de correcto,adivinando la clave o modificando el programa para no necesitarla. :) vamos al lio:
-
Abrimos el programa con nuestro programa de debug en nuestro caso gdb
gdb crackme
-
Y lo ejecutamos dentro de nuestro debugger
run
-
Fijamos un breakpoint en nuestra función main con el comando break seguido de la función
b main
-
-
Ahora vamos a ejecutar nuestro programa hasta nuestro breakpoint, para ello antes tenemos que activar el desensamblado
set disassemble-next-line on
-
Ejecutamos nuestro programa hasta el breakpoint con el comando r
-
Desamblamos el código de nuestro funcion main
disassemble
Analizando
Como pudimos ver en el articulo anterior, hay instrucciones que nos son familiares como <__isoc99_scanf@plt> que era el equivalente a nuestra función scanf en C si miramos mas para arriba veremos varios movimientos de asignación de memoria con la instrucción mov que son mov $0x400713,%edi mov $0x4006fe,%edi movq $0x4006f4,-0x8(%rbp)
El bloque se inicia con: push %rbp mov %rsp,%rbp que es la base de una función en ensamblador (que realiza un push al stack para guardarlo, después crea la base del puntero y lo apunta a la funcion main) por lo tanto si ya sabemos cuales son las lineas de la funcion main() sabremos que justo después va la asignación de variables y punteros que son nuestras lineas: movq $0x4006f4,-0x8(%rbp) mov $0x4006fe,%edi
-
Bien vamos a analizar la memoria de estos dos registros con el comando x usado de la siguiente manera:
x /s direccion_memoria x /s 0x4006fe
-
Umm ya vemos el string donde nos piden la clave, por lo que podemos intuir que estamos cerca del registro del puntero donde metimos nuestra clave valida, asi que analizamos la siguiente direccion de memoria:
x /s 0x4006f4
¡Premio! la verdad es que este ejemplo es muy sencillo, y el codigo C lo prepare para que esto funcione :) esto no sera asi en los programas de verdad, donde el password pueda estar cifrado u ofuscado.
Método ninja
¿Y si en vez de adivinar la clave modificamos el programa para que no importe lo que introduzcamos? Bien para ello volvemos a analizar nuestro código en ensamblador.
-
Lo primero vamos a establecer el juego de instrucciones de ensamblador de intel, en vez de AT&T que es el sistema por defecto, porque es mas amigable para ello usamos:
set disassembly-flavor intel 0x00000000004005fd <+0>: push rbp 0x00000000004005fe <+1>: mov rbp,rsp 0x0000000000400601 <+4>: sub rsp,0x20 0x0000000000400605 <+8>: mov QWORD PTR [rbp-0x8],0x4006f4 0x000000000040060d <+16>: mov edi,0x4006fe 0x0000000000400612 <+21>: call 0x4004d0 <puts@plt> 0x0000000000400617 <+26>: mov rax,QWORD PTR [rbp-0x10] 0x000000000040061b <+30>: mov rsi,rax 0x000000000040061e <+33>: mov edi,0x400713 0x0000000000400623 <+38>: mov eax,0x0 0x0000000000400628 <+43>: call 0x4004f0 <__isoc99_scanf@plt> 0x000000000040062d <+48>: mov rcx,QWORD PTR [rbp-0x10] 0x0000000000400631 <+52>: mov rax,QWORD PTR [rbp-0x8] 0x0000000000400635 <+56>: mov edx,0x9 0x000000000040063a <+61>: mov rsi,rcx 0x000000000040063d <+64>: mov rdi,rax 0x0000000000400640 <+67>: call 0x4004c0 <strncmp@plt> 0x0000000000400645 <+72>: mov DWORD PTR [rbp-0x14],eax 0x0000000000400648 <+75>: cmp DWORD PTR [rbp-0x14],0x0 0x000000000040064c <+79>: je 0x40065a <main+93> 0x000000000040064e <+81>: mov edi,0x400716 0x0000000000400653 <+86>: call 0x4004d0 <puts@plt> 0x0000000000400658 <+91>: jmp 0x400664 <main+103> 0x000000000040065a <+93>: mov edi,0x400727 0x000000000040065f <+98>: call 0x4004d0 <puts@plt> 0x0000000000400664 <+103>: mov eax,0x0 0x0000000000400669 <+108>: leave 0x000000000040066a <+109>: ret
Como podemos ver tenemos una llamada a una función strncmp(que es donde se debería realizar la comprobación de la clave), mas abajo podemos ver una instrucción de comprobación(cmp) que esta en la instrucción cmpl $0x0,-0x14(%rbp) si seguimos mirando nos encontramos con la instrucción je 0x40065a <main+93> que es un salto condicional que indica que salta si es igual o cero, por lo que si seguimos mirando las siguiente direcciones de memoria daremos con la clave para crackear el programa.
La introducción je que tenemos en la dirección de memoria 0x000000000040064c significa salta si es igual(jump if equal) por lo que si hemos metido bien la contraseña salta a la dirección 0x000000000040065a y nos muestra el mensaje de contraseña correcta. Por lo que para «saltarnos» a protección tenemos que modificar ese salto, y lo que vamos a hacer es modificar la instruccion je(jump if equal) por jne(jump if not equal) que es salta si no es igual, por lo tanto no importara si no metemos bien la contraseña porque saltara igualmente. Modificamos la instrucción con el comando set de la siguiente manera:
set *(char*) 0x000000000040064c = 0x75
La instrucción para jne en hexadecimal es 0x75
- Comprobamos que se ha realizado el cambio
- y ejecutamos el programa después de nuestro breakpoint con el comando c
-
Listo, evidentemente estos cambios no son permanentes, solo se realizan en ejecucion, si necesitamos que estos se mantengan persistentes tendremos que usar un editor hexadecimal(que daría para otros artículos)
-
Se me olvidaba si queréis debugear un programa en ejecucion solo necesitais el pid del proceso y usar el comando
attach pid
desde GDB
Bueno, con esto me despido, espero que no halla quedado muy denso, pero ensamblador, es un poco farragoso, y he querido que este articulo valga para todos.
Happy Debugging ;)
Se podía hacer un domingo negro ;P
Un poco «plomizo» para un domingo negro ¿nop? ;)
que no pare la diversion!