SimpleVM: Resolviendo una Máquina Virtual mediante Ingeniería Inversa y Criptoanálisis Diferencial

SimpleVM: Resolviendo una Máquina Virtual mediante Ingeniería Inversa y Criptoanálisis Diferencial

En este artículo analizaremos SimpleVM (lo puedes encontrar aquí https://reversing.kr/), un clásico reto tipo Crackme que implementa una arquitectura basada en una máquina virtual propia. Veremos cómo pasar de la ceguera inicial frente a un binario aparentemente opaco hasta la obtención de la clave correcta mediante una combinación de ingeniería inversa, análisis dinámico y criptoanálisis diferencial.

Herramientas utilizadas: GDB, IDA Pro

Introducción


1. Entendiendo el escenario

Al ejecutar el binario en Linux nos encontramos con una interfaz extremadamente sencilla:

$ ./SimpleVM
Input : VM_is_fun
Wrong

Cualquier cadena que introduzcamos devuelve un contundente Wrong.

Un análisis preliminar muestra que el programa no utiliza funciones de comparación estándar como strcmp(). En su lugar, delega toda la validación a un intérprete interno que ejecuta bytecodes de una máquina virtual.

Al cargar el binario en IDA observamos además un detalle importante: el ejecutable parece estar empaquetado. Gran parte del código visible inicialmente no corresponde a la lógica real del programa.

Dado que se trata de un binario Linux, la forma más cómoda de obtener el código desempaquetado consiste en ejecutarlo bajo GDB y realizar un volcado de memoria una vez finalizado el proceso de desempaquetado.

$ sudo gdb ./SimpleVM

(gdb) catch syscall write
Catchpoint 1 (syscall 'write')

(gdb) run
Starting program: /SimpleVM

Catchpoint 1 (call to syscall write)

(gdb) finish
Input :

Catchpoint 1 (returned from syscall write)

(gdb) info proc mappings

Entre los distintos segmentos mapeados encontramos uno especialmente interesante:

0x08048000 - 0x0804c000

Esa región contiene el código ya desempaquetado de la aplicación.

Realizamos entonces un volcado:

(gdb) dump memory volcado.bin 0x08048000 0x0804c000

Una vez cargado el fichero resultante en IDA, ya podemos comenzar el análisis real.


2. Ingeniería inversa del intérprete

Al examinar el binario desempaquetado identificamos rápidamente el núcleo de la máquina virtual: la función sub_8048C6D().

La arquitectura de la VM está compuesta por varios elementos fundamentales:

  • Program Counter (PC): gestionado por sub_8048A48(), encargado de obtener la siguiente instrucción.
  • Buffer de bytecodes ofuscado: las instrucciones son leídas aplicando una máscara XOR 0x10.
  • Despachador de opcodes: una estructura similar a un switch-case que procesa los distintos bytecodes.

Código simplificado:

int sub_8048C6D()
{
    while (1)
    {
        sub_8048A48();      // Fetch Opcode

        switch (dword_804B190)
        {
            case 6:
                sub_8048ABB(); // XOR
                continue;

            case 7:
                sub_8048B31(); // Comparación
                continue;

            case 9:
                sub_8048BCE(); // Salto condicional
                continue;
        }
    }
}

Durante el análisis observamos que los opcodes más relevantes son:

OpcodeFunción
6Operación XOR
7Comparación
9Salto condicional

El opcode que más nos interesa es el 7, implementado por sub_8048B31(), ya que es el encargado de verificar si los valores calculados a partir de nuestra entrada coinciden con los esperados por la VM.

Si la comparación falla, el opcode 9 detecta el error y redirige la ejecución hacia la rutina que imprime Wrong.


3. Localizando la comparación crítica con GDB

Conociendo la función responsable de la validación, centramos el análisis dinámico en sub_8048B31().

Tras seguir su ejecución encontramos la comparación definitiva:

0x8048b5c: mov 0x804b198,%edx
0x8048b62: mov 0x804b194,%eax
0x8048b67: cmp %eax,%edx
0x8048b69: jne 0x8048b77

Aquí se produce el momento decisivo:

  • EAX contiene el valor objetivo almacenado por la VM.
  • EDX contiene el valor derivado de nuestra entrada.

Si ambos registros coinciden, la ejecución continúa por la ruta correcta.

Para interceptar la comparación utilizamos el siguiente script de GDB (script.gdb):

set follow-fork-mode parent
catch syscall write
run
delete 1
br *0x08048B67
c

Lanzamos el programa con la clave 1111111 (veremos, posteriormente, que la clave tiene una longitud máxima de 7 caracteres):

$ sudo gdb-multiarch -x script.gdb ./SimpleVM

Y registramos los valores de ambos registros:

info registers eax edx

Para acelerar el proceso forzamos temporalmente la comparación:

set $eax = $edx
c
info registers eax edx

Repitiendo la operación varias veces obtenemos todos los pares de valores necesarios para el análisis.

Realizamos el mismo proceso con la clave AAAAAAA y tomamos nota de los registros eax y edx.


4. Criptoanálisis diferencial

Nuestro siguiente objetivo consiste en descubrir qué transformación aplica la VM sobre cada carácter de entrada.

Para ello utilizamos dos cadenas de prueba controladas, ambas de longitud siete:

1111111
AAAAAAA

Interceptando la ejecución en el breakpoint obtenemos los siguientes resultados.

Prueba 1: 1111111

ASCII '1' = 0x31

EAX

09 02 26 2d 22 07 10

EDX

51 57 24 36 7d 52 49

Prueba 2: AAAAAAA

ASCII 'A' = 0x41

EAX

09 02 26 2d 22 07 10

EDX

21 27 54 46 0d 22 39

La primera observación es inmediata: EAX permanece constante, independientemente de la entrada suministrada.

Eso confirma que EAX representa el valor objetivo que debemos alcanzar.

Analizando la relación entre la entrada y los valores generados en EDX descubrimos una transformación XOR por posición:

EDX = Input XOR Máscara

Por tanto:

Máscara = Input XOR EDX

Calculamos cada valor utilizando la primera prueba:

PosiciónCálculoMáscara
00x31 ^ 0x510x60
10x31 ^ 0x570x66
20x31 ^ 0x240x15
30x31 ^ 0x360x07
40x31 ^ 0x7d0x4c
50x31 ^ 0x520x63
60x31 ^ 0x490x78

La máscara completa queda:

60 66 15 07 4c 63 78

5. Reconstrucción de la clave

Ya conocemos la transformación utilizada por la VM.

Si queremos que:

EDX = EAX

debemos despejar el valor de entrada:

Input = EAX XOR Máscara

Realizando el cálculo posición por posición:

PosiciónOperaciónResultado
00x09 ^ 0x600x69 
10x02 ^ 0x660x64 
20x26 ^ 0x150x33 
30x2d ^ 0x070x2a 
40x22 ^ 0x4c0x6e 
50x07 ^ 0x630x64 
60x10 ^ 0x780x68 

Uniendo todos los caracteres obtenemos la solución.


6. Conclusión

Este reto demuestra que no siempre es necesario reconstruir completamente una máquina virtual o desarrollar un emulador propio para resolver un Crackme. En muchas ocasiones resulta más eficiente identificar el punto exacto donde se produce la validación y utilizar análisis diferencial para obligar al binario a revelar las transformaciones que aplica sobre la entrada.

Una vez localizada la comparación crítica, la VM deja de ser una caja negra y pasa a convertirse en un simple sistema de ecuaciones. A partir de ahí, la resolución se reduce a aplicar unas pocas operaciones XOR y reconstruir la clave correcta.

Deja una respuesta

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.