AVILX
REVERSING WRITEUP · CRACKMES.ONE · 2026
U cant Pass
Lógica invertida — solución por parcheo de un byte
| Autor reto | desconocido |
| Plataforma | crackmes.one |
| Dificultad | 1.0 / 5.0 |
| Arquitectura | x86-64 — Unix/Linux |
| Modo | Normal Mode — Análisis estático + parcheo |
| Analista | Jesús Ávila (Avilx) |
| Fecha | 16 de Septiembre 2026 |
| Portafolio | avilx.dev |
Documentación de aprendizaje en reversing | Entorno controlado
Información del Binario
| Campo | Valor |
|---|---|
| Nombre | main.out |
| Tipo | ELF 64-bit LSB pie executable |
| Arquitectura | x86-64 |
| Enlazado | dynamically linked |
| Stripped | not stripped |
| Lenguaje | C |
| Stack Canary | No Canary Found |
| NX | Enabled |
| PIE | PIE Enabled |
| RELRO | Full RELRO |
| SHSTK & IBT | Enabled |
La ausencia de stack canary es el dato más relevante de las mitigaciones. En un binario con un buffer overflow eso significaría que el stack puede corromperse sin que el programa lo detecte. En este crackme no hay vulnerabilidad de escritura, pero la diferencia con otros binarios que sí tienen canary vale registrarla. IBT (Indirect Branch Tracking) protege saltos indirectos pero no afecta el análisis estático.
Reconocimiento
Strings
$ strings -a ./main.out | grep -v "^_Z\|GLIBC\|\.so\|\."
printf
__libc_start_main
__cxa_finalize
PTE1
u+UH
Success!
Error!
strings reveló dos strings clave: Success! y Error!. La ausencia de prompt de input y la presencia de estos dos mensajes sugiere un flujo condicional interno — el programa toma una rama u otra según alguna condición, no según input del usuario.El programa realiza una validación interna hardcodeada. No lee input del usuario. La condición que decide qué mensaje imprimir está fija en el código.
Ejecución inicial
$ ./main.out
Hello in my first programm for crackme.one
Error!
$ ./main.out test
Hello in my first programm for crackme.one
Error!
El programa imprime
Error! sin importar el argumento pasado. Los argumentos de línea de comandos no afectan el resultado. La hipótesis se confirmó: la validación es completamente interna.Análisis del Disassembly
Mapa de .rodata
$ objdump -s -j .rodata ./main.out
Contents of section .rodata:
2000 01000200 48656c6c 6f20696e 206d7920 ....Hello in my
2010 66697273 74207072 6f677261 6d6d2066 first programm f
2020 6f722063 7261636b 6d652e6f 6e650a00 or crackme.one..
2030 53756363 65737321 0a004572 726f7221 Success!..Error!
2040 00
Mapa de strings en
.rodata: 0x2004 → Hello in my first programm for crackme.one, 0x2030 → Success!, 0x203a → Error!. El string Error! empieza en 0x203a porque Success! ocupa 10 bytes desde 0x2030 (53 75 63 63 65 73 73 21 0a 00).Flujo de main
$ objdump -d -M intel ./main.out | grep -A 50 "<main>"
1150: endbr64
1154: push rbp
1155: mov rbp,rsp
1158: sub rsp,0x10
115c: mov DWORD PTR [rbp-0x4],0x0
; --- Imprimir Hello ---
1163: lea rdi,[rip+0xe9a] # 2004 -> Hello...
116c: call printf@plt
; --- Cargar 0xa (10) y comparar contra si mismo ---
1171: mov DWORD PTR [rbp-0x8],0xa
1178: cmp DWORD PTR [rbp-0x8],0xa
117c: je 118e ; SIEMPRE salta (0xa == 0xa)
; --- Rama Success! (NUNCA se ejecuta bajo flujo normal) ---
117e: lea rdi,[rip+0xeab] # 2030 -> Success!
1187: call printf@plt
118c: jmp 11a4
; --- Rama Error! (SIEMPRE llega aqui) ---
118e: cmp DWORD PTR [rbp-0x8],0xa
1192: jne 11a2
1194: lea rdi,[rip+0xe9f] # 203a -> Error!
119d: call printf@plt
11a4: add rsp,0x10
11ab: pop rbp
11ac: ret
La lógica está invertida. El programa mete
0xa en [rbp-0x8] y luego compara ese valor contra 0xa. El resultado es siempre “iguales”, por lo que je en 0x117c siempre salta a 0x118e. La rama Success! en 0x117e es dead code bajo el flujo normal — nunca se alcanza. Para llegar a Success! hay que impedir que je salte.La segunda comparación en
0x118e es redundante: vuelve a comparar [rbp-0x8] con 0xa y usa jne. Dado que el valor sigue siendo 0xa, jne no salta y el flujo cae directamente al bloque Error!. El autor probablemente invirtió las ramas intencionalmente como mecanismo del reto.Solución: Parcheo del Binario
Identificación del byte objetivo
Para que Success! se imprima, el je en 0x117c no debe saltar — eso ocurre cuando los valores son distintos. Cambiar je (0x74) por jne (0x75) invierte la semántica del salto sin tocar nada más del binario.
Primero localíce los bytes en el archivo con xxd:
$ xxd main.out | grep "7410"
00001170: ffc7 45f8 0a00 0000 837d f80a 7410 488d ..E......}..t.H.
El byte
0x74 (je) está en el offset de archivo 0x117c. En este ELF con PIE el offset de archivo y la dirección virtual de .text coinciden porque la sección comienza en 0x1000.Parcheo con dd
$ printf '\x75' | dd of=main.out bs=1 seek=$((0x117c)) count=1 conv=notrunc
1+0 records in
1+0 records out
1 byte copied, 3.5888e-05 s, 27.9 kB/s
conv=notrunc es crítico: sin esa flag dd truncaría el archivo al tamaño escrito (1 byte), destruyendo el binario. Con conv=notrunc solo sobreescribe el byte en la posición indicada y deja el resto intacto.Verificación
$ ./main.out
Hello in my first programm for crackme.one
Success!
Cambiar el opcode
0x74 (je) por 0x75 (jne) en el offset 0x117c invierte el flujo condicional. El jne no salta cuando 0xa == 0xa — el flujo cae directamente al bloque Success! en 0x117e. Un solo byte cambiado, comportamiento completamente distinto.Reflexión Final
Qué salió bien
El flujo estándar de reconocimiento funcionó exactamente como debe. strings identificó los mensajes de éxito y fallo de inmediato, y la ejecución inicial confirmó que no había input involucrado. El mapa de .rodata con objdump -s permitió correlacionar cada string con su offset exacto antes de entrar al disassembly, lo que hizo la lectura del flujo mucho más limpia.
Qué salió mal
El flujo je/jne causó confusión inicial. Al ver dos comparaciones seguidas con los mismos operandos, no fue inmediato entender que la primera rama (Success!) era dead code. Tuve que trazar el flujo completo — instruccción por instrucción — antes de entender que je siempre saltaba porque 0xa == 0xa es siempre verdadero.
El parcheo con dd también fue nuevo. Localizar el byte con xxd y calcular el offset correcto para seek requirió verificar que el offset de archivo coincidiera con la dirección virtual — algo que no es automático con PIE en general.
Lecciones aprendidas
Parchear un binario es modificar bytes directamente en el archivo. Un solo byte cambia la semántica de un salto condicional: 0x74 (je) y 0x75 (jne) difieren en un bit. En malware analysis esta técnica permite desactivar checks de anti-debug, validaciones de integridad, o condiciones de auto-terminación del malware — sin recompilar ni tener el código fuente.
La lógica invertida es un patrón que aparece en ofuscación real. Un binario puede tener condiciones que parecen válidas pero están diseñadas para nunca ejecutar el código de éxito bajo condiciones normales. Reconocer ese patrón en el disassembly — comparación hardcodeada que siempre resuelve igual, con je que siempre salta — es la habilidad que este reto entrenó.
Tiempo total
| Fase | Tiempo |
|---|---|
| Reconocimiento (file, checksec, strings) | 5 minutos |
| Ejecución inicial | 2 minutos |
| Mapa de .rodata | 5 minutos |
| Análisis del disassembly | 18 minutos |
| Parcheo con dd y verificación | 8 minutos |
| Total | 38 minutos |