Trasteando con Ensamblador: Final

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 ;)

Compartir

4 Comentarios

Deja una respuesta a puppetmaster Cancelar respuesta

Your email address will not be published. Required fields are marked *