GDB orientado a Pentesting – Parte 3
Continuamos con esta serie de artículos sobre depuración de programas orientados al pentesting y la ingeniería inversa. En los artículos anteriores de esta serie GDB orientado a Pentesting vimos cómo podíamos depurar simples programas hechos en C que contenían sus símbolos de depuración, lo cual facilita mucho la depuración, así como programas que no incluyen estos símbolos, algo bastante más habitual cuando estamos realizando una tarea de pentesting o reverse engineering.
Si no has leído esos artículos, antes de leer este te aconsejo que le eches un vistazo. Los podrás encontrar en los siguientes enlaces:
GDB orientado a Pentesting – Parte 1
GDB orientado a Pentesting – Parte 2
En esta ocasión vamos a ver cómo podríamos «crackear» un simple programa desarrollado en C. El programa es muy simple; se le pasa como argumento un código, si es correcto mostraré un mensaje diciendo que el código secreto es correcto, en caso contrario indicaré que es erróneo.
Aquí podemos plantear dos opciones; hacer un simple «bypass» del programa, para que introduciendo cualquier cosa obtengamos el mensaje correcto, o intentar localizar el registro donde se almacena temporalmente el código correcto con el que compará lo que nosotros hayamos introducido como argumento.
En esta tercera parte de la serie haremos un bypass, de forma que da igual el valor que introduzcamos, siempre pasaremos con éxito la autenticación. El programa lo he llamado codigoSecreto y su sintaxis es la siguiente:
./codigoSecreto 123456
Podemos observar que con los distintos código, al ser incorrectos, obtenemos el mensaje correspondiente de error.
Veamos esto mismo pero ya con nuestro GDB depurando el programa.
Podemos observar que no se han cargado los símbolos de depuración porque no se ha compilado con la opción para que los incluyera, y se pasa el argumento desde el propio GDB con el comando run. El resultado es el esperado ya que el código lo desconocemos por lo tanto es incorrecto.
Vamos a depurar el programa, para ello crearemos nuestro primer breakpoint en la función main(), para posteriormente poder desensamblar el programa.
Perfecto, ya tenemos nuestro programa ejecutándose y parado justo donde queremos, al comienzo de la función main() de nuestro programa.
Ahora ejecutamos disassemble en este punto del programa, para obtener información acerca de las direcciones de memoria y funciones ensamblador que se están ejecutando, se han ejecutado o se van a ejecutar.
Tras la ejecución de disassemble, tenemos que prestar atención a varias líneas. Una de ellas es donde se ejecuta la función (de ahí la llamada con call) strcmp (string compare), justo en la dirección 0x08048485. ¿Por qué es importante esta función? Porque aquí es donde se comparan las cadenas, y en función del resultado de la comparación se ejecutará una instrucción u otra.
Esa comparación la realiza la función jne (Jump if Not Equal o jnz, Jump if Not Zero), es decir, salta a la dirección indicada si no son iguales (si su valor no es cero). En este caso se indica que salte a 0x804849c, donde se asigna un valor y a continuación en la siguiente instrucción se llama (call) a la función puts (equivalente al printf de C). ¿Y qué mostrará esta función? Pues «¡Error! Código incorrecto«.
¿Cuál es nuestro objetivo? Cambiar el resultado de la comparación para que jne no salte a la dirección donde se muestra el mensaje de error, y se ejecute la instrucción donde se muestre el mensaje de que el código introducido es correcto.
Para ello vamos a crear un nuevo breakpoint justo en la llamada anterior a jne, en la función test que es donde se realiza la comparación, en la dirección 0x0804848a.
break *0x0804848a
Ejecutamos c (continue) para continuar la ejecución del programa, y parará en el segundo breakpoint que acabamos de crear, donde se ejecuta test.
En este punto volvemos a hacer un disassemble.
En este punto podemos ejecutar el siguiente comando para ver información de los registros:
info registers
Podemos ver que el registro eax tiene el valor -1, es decir, no es 0, por lo tanto jne hará el salto a la dirección indicada, donde se ejecutará una instrucción para mostar el mensaje de error.
Estamos en el punto clave de nuestro «bypass«, ya que si ahora modificamos el valor y lo ponemos a 0, conseguiremos «engañar» al programa, hacer nuestro «bypass» y que nos muestre el mensaje esperado.
Vamos a cambiar el valor del registro eax para que sea 0, de forma que jne no salte a la dirección del mensaje de error, si no que siga el flujo del programa para que muestro nuestro mensaje esperado.
set $eax = 0
Ejecutamos c (continue) y echamos un vistazo al resultado.
¡Bingo! Hemos conseguido hacer un «bypass» a lo que podría haber sido un sistema de autenticación. Todo ello sin saber el código secreto correcto. Cómo descubrir el código correcto lo veremos en la cuarta entrega de esta serie.
Como siempre, espero que os haya gustado y os resulte útil.
¡Hasta la próxima!