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
WrongCualquier 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 mappingsEntre los distintos segmentos mapeados encontramos uno especialmente interesante:
0x08048000 - 0x0804c000Esa región contiene el código ya desempaquetado de la aplicación.
Realizamos entonces un volcado:
(gdb) dump memory volcado.bin 0x08048000 0x0804c000Una 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-caseque 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:
| Opcode | Función |
|---|---|
| 6 | Operación XOR |
| 7 | Comparación |
| 9 | Salto 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 0x8048b77Aquí 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
cLanzamos 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 ./SimpleVMY registramos los valores de ambos registros:
info registers eax edxPara acelerar el proceso forzamos temporalmente la comparación:
set $eax = $edx
c
info registers eax edxRepitiendo 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
AAAAAAAInterceptando la ejecución en el breakpoint obtenemos los siguientes resultados.
Prueba 1: 1111111
ASCII '1' = 0x31
EAX
09 02 26 2d 22 07 10EDX
51 57 24 36 7d 52 49Prueba 2: AAAAAAA
ASCII 'A' = 0x41
EAX
09 02 26 2d 22 07 10EDX
21 27 54 46 0d 22 39La 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áscaraPor tanto:
Máscara = Input XOR EDXCalculamos cada valor utilizando la primera prueba:
| Posición | Cálculo | Máscara |
| 0 | 0x31 ^ 0x51 | 0x60 |
| 1 | 0x31 ^ 0x57 | 0x66 |
| 2 | 0x31 ^ 0x24 | 0x15 |
| 3 | 0x31 ^ 0x36 | 0x07 |
| 4 | 0x31 ^ 0x7d | 0x4c |
| 5 | 0x31 ^ 0x52 | 0x63 |
| 6 | 0x31 ^ 0x49 | 0x78 |
La máscara completa queda:
60 66 15 07 4c 63 785. Reconstrucción de la clave
Ya conocemos la transformación utilizada por la VM.
Si queremos que:
EDX = EAXdebemos despejar el valor de entrada:
Input = EAX XOR MáscaraRealizando el cálculo posición por posición:
| Posición | Operación | Resultado |
| 0 | 0x09 ^ 0x60 | 0x69 |
| 1 | 0x02 ^ 0x66 | 0x64 |
| 2 | 0x26 ^ 0x15 | 0x33 |
| 3 | 0x2d ^ 0x07 | 0x2a |
| 4 | 0x22 ^ 0x4c | 0x6e |
| 5 | 0x07 ^ 0x63 | 0x64 |
| 6 | 0x10 ^ 0x78 | 0x68 |
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.