AVILX

REVERSING WRITEUP · CRACKMES.ONE · 2026

U cant Pass

Lógica invertida — solución por parcheo de un byte

Autor retodesconocido
Plataformacrackmes.one
Dificultad1.0 / 5.0
Arquitecturax86-64 — Unix/Linux
ModoNormal Mode — Análisis estático + parcheo
AnalistaJesús Ávila (Avilx)
Fecha16 de Septiembre 2026
Portafolioavilx.dev
BINARIO PARCHEADO REVERSING · PATCHING · CRACKME · x86-64

Documentación de aprendizaje en reversing | Entorno controlado

Información del Binario

Campo Valor
Nombremain.out
TipoELF 64-bit LSB pie executable
Arquitecturax86-64
Enlazadodynamically linked
Strippednot stripped
LenguajeC
Stack CanaryNo Canary Found
NXEnabled
PIEPIE Enabled
RELROFull RELRO
SHSTK & IBTEnabled
Nota
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!
Hallazgo
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.
Hipótesis
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!
Hallazgo
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
Hallazgo
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
Hallazgo
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.
Nota
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.
Hallazgo
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
Nota
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!
Hallazgo
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 inicial2 minutos
Mapa de .rodata5 minutos
Análisis del disassembly18 minutos
Parcheo con dd y verificación8 minutos
Total38 minutos