AVILX

REVERSING WRITEUP · CRACKMES.ONE · 2026

easyAF

Password en texto claro en .rodata

Autor retodesconocido
Plataformacrackmes.one
Dificultad1.0 / 5.0
Arquitecturax86-64 — Unix/Linux
ModoNormal Mode — Análisis estático
AnalistaJesús Ávila (Avilx)
Fecha09 de Septiembre 2026
Portafolioavilx.dev
PASSWORD ENCONTRADO REVERSING · ASSEMBLY · CRACKME · x86-64

Documentación de aprendizaje en reversing | Entorno controlado

Información del Binario

Campo Valor
NombreeasyAF
TipoELF 64-bit LSB pie executable
Arquitecturax86-64
Enlazadodynamically linked
Strippednot stripped
LenguajeC++ (símbolos mangleados visibles)
BuildIDc1f484166af165c71e71e8e0e5ebd8f9d6300f0a
Stack CanaryCanary Found
NXEnabled
PIEPIE Enabled
RELROPartial RELRO
Nota
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
Hallazgo
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.
Hipótesis
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!
Hallazgo
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.
Hallazgo
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<<
Hallazgo
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.
Nota
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

pass
PASSWORD ENCONTRADO
$ ./easyAF
Enter the password: pass
Welldone!
Hallazgo
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 strings2 minutos
Verificación del password1 minuto
Análisis del disassembly20 minutos
Mapeo de .rodata5 minutos
Total28 minutos