08 septiembre 2014

Reversing: ¿algo más allá de los 0s y 1s?

Durante todos los cursos formativos de Reversing que he tenido el placer de impartir, tanto en la NoConName como en la RootedCON, siempre intento transmitir a los alumnos la necesidad de saber leer el código. Sí sí, me refiero a coger un binario, leer sus instrucciones y saber abstraerse de modo que se entienda, en un "vistazo rápido", qué es lo que está haciendo una cierta parte de código.

Esto es cada día más una necesidad, si pensamos en la gran cantidad de binarios con intención maliciosa que corren por la red, cada vez más personalizados con lo que los método automáticos de análisis de muestras maliciosas dejan de ser útiles, y que un analista debe analizar de manera manual. Así pues, vamos a ver un ejemplo sencillito de por qué aprender a leer ensamblador es bueno! :)

Hoy vamos a analizar el binario "reversing.exe", firma MD5 d1287fdcc585c09b532d3c953378d745., que es un crackME, es decir, un programa especialmente desarrollado para practicar la ingeniería inversa (es decir, se permite inspeccionar su código binario y analizarlo, aspecto que no está tan claro su legalidad conforme a la legislación de ciertos países). La procedencia del mismo no voy a hacerla pública yo, os la dejo de deberes. Desde aquí un abrazo fuerte a su autor y amigo, lector asiduo de este blog y muy buen profesional ;). Lo primero siempre a la hora de analizar un crackME es ejecutarlo, para ver qué pinta tiene. En este caso, nos encontramos con esta pantalla:

Aparece una terminal donde se pide la introducción de un número de serie. En función del desarrollado del crackME, nos podemos encontrar que también se pide el nombre de usuario, o incluso nada (estos suelen tener "truco" para resolverlo ;)).  Antes de empezar a poner las manos en la masa, veamos qué ocurre si ponemos algún texto cualquiera como número de serie, y aceptamos:


El programa nos informa de que el número de serie introducido es inválido, ¡vaya!. Hubiera estado muy bien haber acertado a la primera, pero parece que hoy no toca... Bueno, pues ¡vamos a empezar el análisis fino! El primer paso siempre será ver a qué nos vamos a enfrentar: lenguaje de programación, si tiene alguna protección (packer), etc. Esto servirá para definir el análisis que tengamos que realizar, junto con las herramientas a utilizar. Le pasamos, en mi caso, el PEiD, que me informa de que al parecer es un binario normal y corriente, sin protecciones adicionales:


Visto lo cual, vamos a echarle un vistazo al código binario a ver qué nos encontramos. Para ello, podemos cargarlo en una herramienta de análisis estático, como el Win32Dasm (soy un clásico, perdonadme), o una herramienta de análisis dinámico, como el OllyDBG. En mi caso, me voy a decantar por la segunda pero no voy a ejecutarlo, simplemente voy a "leer" el código.

En la imagen superior se muestra lo que nos aparece dentro del debugger OllyDBG. El debugger siempre nos para en la "primera" instrucción que se va a ejecutar del binario, que es la que se encuentra en el "Program Entrypoint". Vamos a observar un poco en detalle el código que nos encontramos.

Como veis, lo primero que hace el binario es llamar a la función de Windows GetTickCount(). Esta función devuelve el número de milisegundos transcurridos desde el encendido del ordenador. Este valor devuelto se almacena en el registro EAX, que en la instrucción de después guarda en una variable en memoria (dirección 0x4030AF). Tras esta asignación, hay una llamada a la dirección 401051. Esta función contiene el siguiente código:

Lo primero que hay es un "XOR EAX, EAX", que quiere decir que pone un 0 en EAX. ¿Por qué el compilador no ha puesto entonces "MOV EAX, 0", que es una instrucción de ensamblador para poder asignar valores a los registros del micro? La respuesta es sencilla: por optimización. Un "XOR EAX, EAX" ocupa menos bytes y consume menos ciclos (o sea, se ejecuta antes) que un "MOV EAX, 0" en una arquitectura x86. Después de este XOR, aparece una llamada a IsDebuggerPresent(), que devuelve un 0 ó 1 en función de si el programa se está ejecutando bajo debugger. Si el valor de EAX es 0, el "OR EAX, EAX" también será 0, con lo que la bandera de cero estará activada, y el salto condicional "JE" de después se ejecutará, con lo que llevará la ejecución a 40105D (es decir, dentro de la misma función). Después observad que hay varias llamadas a otros lugares. Si leyéramos cada una de ellas, veríamos que hay cierto código que hace "cosas" que a priori no sabemos muy bien para qué las hace. ¿Hará falta profundizar en cada una de estas llamadas y analizarlas en detalle? Pues no lo sabemos todavía, así que si es necesario, mejor dejarlo para el final, ¿no?. Para ir acabando el artículo, voy a centrarme en las dos últimas que son las que presentan algo de chicha. El código de ambas funciones, 0x4010A3 y 0x40110B, se presentan respectivamente a continuación:



Fijaos en la última llamada. Aparece una llamada a CompareStringA(), que es una función para comparar dos cadenas. Las dos cadenas que se comparan son las almacenadas en las variables 0x403136 (String2) y 0x4030C2 (String1). Si nos centramos ahora en el código de la primera función (0x4010A3), se observa una llamada a wsprintfA(), que es una función para imprimir cadenas, seguro que la conocéis si habéis programado en C. En este caso, observad con detalle sus parámetros: el primer parámetro 0x4030B8, es un puntero a una cadena, el segundo es el formato en el que debe de "imprimir" esa cadena, que es %.08X, es decir, hasta 8 carácteres hexadecimales; y el tercero es dónde ha de imprimirlo, que en este caso es otra variable cadena en la dirección 0x403136, que es la que coincide con el parámetro de entrada a la función CompareStringA() que se ha comentado anteriormente.

Si leemos un poco más hacia atrás del wsprintf, observad que aparece como parámetro de la función GetVolumeInformationA(), que sirve para recuperar información sobre el disco duro donde está montado el raíz del sistema. Concretamente, coincide con la variable donde se guarda el número de serie del volumen del disco duro raíz.

Observad que hasta ahora, no ha sido necesario ejecutar el binario y simplemente hemos "leído" su código, analizando y observando lo que va a suceder cuando se ejecute. Sabemos que si no se ejecuta bajo debugger, el programa llega un momento en que recoge información del disco duro donde se está ejecutando, y compara su número de serie del volumen con otra cadena. ¿Será la otra cadena la que nos permite introducir como clave el crackME? ¡Vamos a probarlo!

Pero... ¡antes tenemos que saber cuál es el número de serie de nuestra unidad raíz! ¿Y cómo se sabe esto? Pues muy fácil, mediante el comando "vol C:" en la consola de Windows XP, obtenemos la respuesta. Así que ahora ya podemos ejecutar "reversing.exe" tranquilamente, e insertar el número de serie del volumen proporcionado por el comando anterior (¡ojo! sin el guión intermedio, recordad el formato de la llamada wsprintfA). Y voilá:


¡Reto resuelto!

Cabe decir que si no hubiera sido la solución, podríamos haber seguido estudiando el código en estático, como hemos hecho, o bien empezar a analizar el código en binario, para ver "en caliente" que está pasando durante la ejecución del binario. Como veis, el "ensayo y error" es la forma de ir avanzado poco a poco en este "arte" de la ingeniería inversa.

Yo siempre lo digo, que además del inglés y el español, también "hablo" ensamblador x86. En el próximo curso de Reversing & Exploiting organizado por Securízame, y del cual tengo el honor de formar parte del profesorado junto con las figuras de primer nivel en sus respectivos campos, detallaré algunas cosas más sobre la abstracción de código ensamblador a código de alto nivel para entender qué hace un binario concreto, así como enseñaré algunos trucos y las técnicas más comunes para "atacar" ciertos tipos de binarios.

Y tú, ¿a qué esperas para aprender a "hablar" ensamblador? ¡Apúntate ya! ;)


Colaboración por cortesía de Ricardo Rodríguez

3 comments :

Anonimo dijo...

Me recuerda a cierto crackme usado para un proceso de selección realizado en España x) . Juraría que pedían el generador de claves para dar la prueba por superada xD

Ronny Vasquez dijo...

Excelente articulo, cada vez que veo analisis de codigo me emociono bastante, saludos.

Son dijo...

Concuerdo con Ronny, execelente articulo...!!
Yo me adentrando mas en eso del lenguaje ensamblador...pero a la hora de ir mas alla de hola mundo eh tenido problemas, tal vez alguno manuales de ensamblador x86..en verdad me vendria super bien...