AVILX
REVERSING WRITEUP · CRACKMES.ONE · 2026
easyAF
Password en texto claro en .rodata
| Autor reto | desconocido |
| Plataforma | crackmes.one |
| Dificultad | 1.0 / 5.0 |
| Arquitectura | x86-64 — Unix/Linux |
| Modo | Normal Mode — Análisis estático |
| Analista | Jesús Ávila (Avilx) |
| Fecha | 09 de Septiembre 2026 |
| Portafolio | avilx.dev |
Documentación de aprendizaje en reversing | Entorno controlado
Información del Binario
| Campo | Valor |
|---|---|
| Nombre | easyAF |
| Tipo | ELF 64-bit LSB pie executable |
| Arquitectura | x86-64 |
| Enlazado | dynamically linked |
| Stripped | not stripped |
| Lenguaje | C++ (símbolos mangleados visibles) |
| BuildID | c1f484166af165c71e71e8e0e5ebd8f9d6300f0a |
| Stack Canary | Canary Found |
| NX | Enabled |
| PIE | PIE Enabled |
| RELRO | Partial RELRO |
Este binario tiene Partial RELRO, a diferencia de otros crackmes anteriores que usaban Full RELRO. Con Partial RELRO el GOT permanece escribible durante la ejecución. En un escenario con escritura arbitraria eso abriría la puerta a un GOT overwrite — sobreescribir la entrada de una función como
memcmp para redirigir la ejecución. En este crackme no hay esa vulnerabilidad, pero la diferencia vale registrarla.Reconocimiento
Strings
$ strings -a ./easyAF | grep -v "^_Z\|GLIBC\|\.so\|\."
[Funciones relevantes]
memcmp
__stack_chk_fail
__cxa_atexit
[Strings del programa]
pass
Enter the password:
Welldone!
Nope
strings reveló el string pass junto con los mensajes “Welldone!” y “Nope”. La presencia de pass entre los strings del programa — sin estar en una ruta de biblioteca — es inmediatamente sospechosa como candidato a password.El password es
pass. El binario lo almacena en .rodata sin ofuscación. Vale verificarlo ejecutando el binario antes de entrar al disassembly.Ejecución inicial
$ ./easyAF
Enter the password: 23
Nope
$ ./easyAF
Enter the password: pass
Welldone!
La hipótesis se confirmó en la primera prueba. El password
pass produce la respuesta “Welldone!”. El crackme está resuelto a nivel funcional — pero el análisis del disassembly permite entender exactamente cómo funciona la comparación internamente.Análisis del Disassembly
Mapa de .rodata
$ objdump -s -j .rodata ./easyAF
Contents of section .rodata:
2000 01000200 00706173 7300456e 74657220 .....pass.Enter
2010 74686520 70617373 776f7264 3a200057 the password: .W
2020 656c6c64 6f6e6521 004e6f70 6500 elldone!.Nope.
El mapa de
.rodata confirma los offsets exactos de cada string: 0x2005 → pass, 0x200a → Enter the password: , 0x201f → Welldone!, 0x2029 → Nope. Estos offsets aparecen directamente en las instrucciones lea rsi del disassembly de main.Flujo de main
El binario está compilado en C++, por lo que main contiene símbolos mangleados de std::string y std::cin/cout. Ignorando ese ruido, el flujo relevante es el siguiente:
$ objdump -d -M intel ./easyAF | grep -A 80 "<main>:"
; --- Inicializar std::string en [rbp-0x60] con "pass" (0x2005) ---
121f: lea rax,[rbp-0x60]
1223: lea rsi,[rip+0xddb] # 2005 -> "pass"
122d: call operator=(string, const char*)
; --- Imprimir "Enter the password: " (0x200a) ---
123e: lea rsi,[rip+0xdc5] # 200a -> "Enter the password: "
1245: lea rdi,[rip+0x2e54] # cout
124c: call operator<<
; --- Leer input del usuario en [rbp-0x40] ---
1251: lea rax,[rbp-0x40]
1258: lea rdi,[rip+0x2f61] # cin
125f: call operator>>
; --- Comparar input == "pass" con operator== ---
1264: lea rdx,[rbp-0x60] ; rdx = &"pass"
1268: lea rax,[rbp-0x40] ; rax = &input
1272: call _ZSteq... ; operator==(string, string)
1277: test al,al
1279: je 129b ; si al=0 (strings distintos) -> salta a Nope
; --- Rama exito: imprimir "Welldone!" (0x201f) ---
127b: lea rsi,[rip+0xd9d] # 201f -> "Welldone!"
1282: lea rdi,[...] # cout
1289: call operator<<
1299: jmp 12d5 ; fin
; --- Rama fallo: segunda comparacion con operator!= ---
129b: call _ZStne... ; operator!=(string, string)
12ae: test al,al
12b0: je 12cf
12b2: lea rsi,[rip+0xd70] # 2029 -> "Nope"
12b9: lea rdi,[...] # cout
12c0: call operator<<
La comparación central ocurre en
0x1272: se llama a operator==(string, string) con el input del usuario en rdi y el string pass en rsi. El valor de retorno llega en al (byte bajo de RAX). La instrucción test al,al seguida de je 129b evalua si al = 0. En C++, operator== retorna true (1) cuando los strings son iguales — por lo tanto je no salta y el flujo cae directo al bloque Welldone!. Si el input es incorrecto, al = 0, je salta a 0x129b y se imprime Nope.Este binario tiene una doble rama de validación: primero llama a
operator== y si falla llama a operator!=. Es una construcción redundante — el resultado es el mismo. En un binario de producción esto sería dead code, pero en un crackme de nivel 1.0 es simplemente el patrón que genera el compilador C++ para ciertos patrones de if/else sobre std::string.Solución
Password encontrado
PASSWORD ENCONTRADO
$ ./easyAF
Enter the password: pass
Welldone!
El password
pass fue identificado con strings y confirmado tanto por ejecución directa como por el análisis del disassembly. El string se almacena en .rodata en offset 0x2005 y se carga en [rbp-0x60] como std::string antes de la comparación.Reflexión Final
Qué salió bien
El reconocimiento con strings identificó el password de inmediato. Aprendido de really easy: la primera herramienta siempre es strings, y si el binario no está ofuscado el vector aparece solo.
El análisis posterior del disassembly permitió entender cómo funciona la comparación en C++ a nivel de registros — cosa que no hice en challenges anteriores porque resolvía y cerraba. Esta vez terminé leyendo el flujo completo de main aunque ya tenía el password.
Qué salió mal
Al principio interpreté test al,al / je al revés: asumi que je saltaba cuando los strings eran iguales. Tuve que rever que operator== retorna true (1) al igualar, así que al no es 0 y je no salta — el flujo cae directo a Welldone!. La confusión vino de mezclar la semántica de strcmp (retorna 0 al igualar) con operator== (retorna 1 al igualar).
Lecciones aprendidas
La diferencia entre strcmp y operator== importa al leer el disassembly: strcmp retorna 0 si son iguales, operator== retorna 1. Eso invierte la lógica del salto condicional que sigue.
Partial RELRO deja el GOT escribible. En un escenario con escritura arbitraria eso es un vector de GOT overwrite — redirigir la entrada de una función en el GOT hacia system para ejecutar código arbitrario. Las mitigaciones de stack (canary, NX, PIE) no protegen contra eso.
Tiempo total
| Fase | Tiempo |
|---|---|
| Reconocimiento con strings | 2 minutos |
| Verificación del password | 1 minuto |
| Análisis del disassembly | 20 minutos |
| Mapeo de .rodata | 5 minutos |
| Total | 28 minutos |