AVILX

REVERSING WRITEUP · CRACKMES.ONE · 2026

Very Special Number v1

Análisis estático puro del ensamblador generado por GCC

Reconocimiento estático, lectura de assembly x86-64, identificación del algoritmo de verificación y obtención de la key.

Autor retoowo_whats_this
Plataformacrackmes.one
Dificultad2.0 / 5.0
Arquitecturax86-64 — Unix/Linux
ModoHard Mode — RE puro
AnalistaJesús Ávila (Avilx)
Fecha27 de Agosto 2026
Portafolioavilx.dev
KEY ENCONTRADA REVERSING · ASSEMBLY · CRACKME · x86-64

Documentación de aprendizaje en reversing | Entorno controlado

Información del Binario

Campo Valor
Nombrevery_special_number
TipoELF 64-bit LSB pie executable
Arquitecturax86-64
Enlazadodynamically linked
Strippednot stripped
Stack CanaryEnabled
NXEnabled
PIEEnabled
RELROParcial RELRO

Reconocimiento

Análisis inicial

Strings

$ strings -a ./OwO_vsn_1

[Librerías]
libstdc++.so.6   # escrito en C++
libm.so.6        # usa funciones matemáticas

[Mensajes del programa]
Hi! Can you guess my Special Number?
You must be psychic :O
Wrong! Try again :)

[Archivo fuente]
crackme.cpp

[Variables relevantes]
magic_number1
magic_number2
magic_arr

[Funciones relevantes]
_Z8generatev     # generate()
_Z6genNumxxiix   # genNum()
main

Símbolos (nm)

$ nm -n ./OwO_vsn_1

[Funciones definidas en el binario]
0x1189  genNum()    # función auxiliar de generación
0x12c4  generate()  # función principal de generación
0x12fb  main        # punto de entrada

[Variables globales en .data]
0x4080  magic_number1  # valor mágico 1
0x4088  magic_number2  # valor mágico 2
0x40a0  magic_arr      # array de valores mágicos

[Imports relevantes]
__stack_chk_fail  # canary activo
cin               # lee input del usuario
cout              # imprime output
Hallazgo
El binario está escrito en C++ (libstdc++.so.6) y usa funciones matemáticas (libm.so.6). Se identificaron tres variables globales: magic_number1, magic_number2 y magic_arr, además de dos funciones: generate() y genNum(). Los mensajes del programa confirman que espera un número como input.
Hipótesis
El programa genera un número especial usando magic_number1, magic_number2 y magic_arr como parámetros. El input del usuario se compara contra ese número generado.

Ejecución inicial

./OwO_vsn_1                                                 
Hi! Can you guess my Special Number?
10
Wrong! Try again :)

Análisis del Flujo

00000000000012fb <main>:
1303:  mov rax, QWORD PTR fs:0x28   # carga canary
130c:  mov QWORD PTR [rbp-0x8], rax # guarda canary en stack
1312:  lea rsi, [rip+0xd0f]         # string de bienvenida
1320:  call cout                     # imprime mensaje
1341:  lea rdi, [rip+0x2eb8]        # cin
1348:  call cin                      # lee input -> [rbp-0x10]
134d:  call _Z8generatev            # llama generate()
1352:  mov rdx, QWORD PTR [rbp-0x10] # carga input del usuario
1356:  cmp rax, rdx                 # compara generate() con input
135e:  je 138a                       # si no igual -> Wrong
1360:  call cout                     # imprime You must be psychic
138a:  call cout                     # imprime Wrong! Try again
13bb:  xor rcx, QWORD PTR fs:0x28  # verifica canary
13c6:  call __stack_chk_fail        # canary corrupto -> abort
Hallazgo
El flujo de main es directo: imprime el mensaje de bienvenida, lee un número del usuario con cin guardado en [rbp-0x10], llama a generate() cuyo resultado queda en RAX, y compara ambos valores. Si coinciden imprime éxito, si no imprime fallo. El número especial es el valor de retorno de generate().
Hipótesis
La clave está dentro de generate(). Esa función calcula el número especial usando magic_number1, magic_number2 y magic_arr. El siguiente paso es analizar generate() para entender ese cálculo y reproducirlo.

Función generate()

00000000000012c4 <_Z8generatev>:
12c8:  mov rax, [rip+0x2db9]     # carga magic_number2
12cf:  mov rcx, [rip+0x2daa]     # carga magic_number1
12d6:  mov rdx, [rip+0x2dab]     # carga magic_number2
12dd:  lea rdi, [rcx+rdx*1]      # rdi = magic_number1 + magic_number2
12e1:  mov r8d, 0x0              # 5to arg = 0
12e7:  mov ecx, 0x0              # 4to arg = 0
12ec:  mov edx, 0x5              # 3er arg = 5
12f1:  mov rsi, rax              # 2do arg = magic_number2
12f4:  call _Z6genNumxxiix       # llama genNum()
12fa:  ret                        # retorna lo que genNum devuelva
Hallazgo

generate() no realiza cálculo propio. Su única función es preparar los argumentos y delegar el cálculo a genNum(). Los argumentos pasados son:

  • rdi = magic_number1 + magic_number2

  • rsi = magic_number2

  • edx = 5

  • ecx = 0

  • r8d = 0

El valor de retorno de genNum() se propaga directamente a main.

Hipótesis
El algoritmo real está en genNum(). Con los valores de magic_number1 y magic_number2 como entrada, genNum() calcula el número especial. El argumento 5 probablemente controla el número de iteraciones o pasos del algoritmo. El siguiente paso es leer los valores reales de magic_number1 y magic_number2 en memoria y analizar genNum().

Función genNum()

; Switch basado en ecx (casos 0-5 via jump table)
11a3:  cmp [rbp-0x18], 0x5      ; ecx > 5?
11a7:  ja  1260                  ; si > 5 salta al final
11ce:  jmp rax                   ; salta al caso via jump table

; Caso 0: rdi + rsi + 42
11d4:  lea rdx, [rax+0x2a]      ; rdx = rsi + 42
11dc:  add rax, rdx              ; rax = rdi + rsi + 42
; resultado = 109713430479

; Caso 1: rdi - (rsi + 33 + magic_arr[ecx])
11e9:  lea rcx, [rax+0x21]      ; rcx = rsi + 33
11fa:  lea rax, [rip+0x2e9f]    ; rax = magic_arr
1201:  mov eax, [rdx+rax*1]     ; eax = magic_arr[ecx]
1206:  lea rdx, [rcx+rax*1]     ; rdx = rsi + 33 + magic_arr[1]
120e:  sub rax, rdx              ; rax = rdi - rdx
; resultado = 74778687097

; Caso 2: rdi * (rsi - 16)
121b:  lea rdx, [rax-0x10]      ; rdx = rsi - 16
1223:  imul rax, rdx             ; rax = rdi * (rsi - 16)
; resultado = 2612374240362856500107



; Caso 3: rdi / (rsi + 3)
1231:  lea rdi, [rax+0x3]       ; rdi = rsi + 3
123b:  idiv rdi                  ; rax = rdi / (rsi + 3)
; resultado = 74778687056

; Caso 4: (rdi % (rsi * 1000)) >> 1
1248:  imul rcx, rax, 0x3e8     ; rcx = rsi * 1000
1255:  idiv rcx                  ; rdx = rdi % (rsi * 1000)
125c:  sar [rbp-0x20], 1        ; resultado = rdx >> 1
; resultado = 37389343528

; Recursion: se llama 5 veces decrementando edx
1296:  jg  12be                  ; si edx > 0 llama recursivamente
12b7:  call _Z6genNumxxiix       ; llamada recursiva
Hallazgo
genNum() implementa un switch de 5 casos via jump table. Cada caso aplica una operación matemática diferente sobre rdi y rsi. La función es recursiva con 5 iteraciones. Los valores de entrada son: magic_number1=39843943743, magic_number2=34934743347, magic_arr=[15,2,487,567,298,491,55].
Hallazgo
El valor retornado por generate() es 0x198b6e17cf obtenido leyendo RAX en GDB tras el retorno de la función. En decimal: 109713430479.

Extracción de valores desde .data

$ objdump -s -j .data ./OwO_vsn_1

4080 3f55e246 09000000 33e14522 08000000
40a0 0f000000 02000000 e7010000 37020000
2a010000 eb010000 37000000
import struct

mn1 = struct.unpack('<q', bytes.fromhex('3f55e24609000000'))[0]
mn2 = struct.unpack('<q', bytes.fromhex('33e1452208000000'))[0]

arr_data = bytes.fromhex('0f00000002000000e701000037020000'
'2a010000eb01000037000000')
arr = [struct.unpack('<i', arr_data[i:i+4])[0]
for i in range(0, len(arr_data), 4)]

print(f'magic_number1 = {mn1}')   # 39843943743
print(f'magic_number2 = {mn2}')   # 34934743347
print(f'magic_arr     = {arr}')   # [15,2,487,567,298,491,55]
Hallazgo
Los valores de magic_number1 y magic_number2 están almacenados en .data en formato little-endian de 8 bytes. magic_arr es un array de 7 enteros de 4 bytes cada uno. Sus direcciones fueron obtenidas del output de nm.

Lógica de verificación

134d:  call _Z8generatev             ; genera el numero especial
1352:  mov rdx, [rbp-0x10]          ; input del usuario
1356:  cmp rax, rdx                 ; compara ambos
1359:  sete al                       ; al=1 si iguales
135e:  je 138a                       ; si no -> Wrong
Hallazgo
La verificación es una comparación directa entre el valor retornado por generate() en RAX y el input del usuario. No hay transformación del input, se compara directamente.
Hallazgo
El input correcto es exactamente el valor de retorno de generate(): 109713430479.

Solución

Algoritmo identificado

generate() suma magic_number1 + magic_number2 + 42 como primer paso del algoritmo recursivo. El valor final retornado tras 5 iteraciones es 109713430479.

Script

gdb -q ./OwO_vsn_1
break _Z8generatev
run
finish
p $rax
# RAX = 0x198b6e17cf = 109713430479

Key encontrada

109713430479
KEY ENCONTRADA
Hallazgo
La key 109713430479 fue verificada exitosamente contra el binario. El programa respondió: "You must be psychic :O"

Reflexión Final

Qué salió bien

Identificar el flujo de main y los argumentos de generate() desde el disassembly fue directo. El uso de GDB para leer RAX directamente aceleró la solución.

Qué salió mal

El script Python para replicar genNum() no produjo el resultado correcto. Para verificar el valor real recurrí a GDB, lo que tecnicamente rompe el modo Hard Mode. El análisis del assembly fue completo pero la implementación del algoritmo recursivo tuvo errores.

En una próxima iteración el objetivo sería replicar el algoritmo completamente en Python sin necesidad de GDB.

Lecciones aprendidas

Este fue mi primer crackme resuelto. Estoy iniciando en reversing y gran parte del proceso requirió investigar conceptos nuevos sobre la marcha: jump tables, funciones recursivas en assembly, lectura de registros con GDB y extracción de valores desde .data.

En varios puntos recurrí a ayuda externa para aclarar dudas específicas del assembly, como el significado de la escala *1 en lea y la diferencia entre imul e idiv. Eso es parte del proceso de aprendizaje.

La lección más importante: cuando el script no reproduce el algoritmo correctamente, leer el valor directamente de RAX con GDB es una solución válida y más rápida que depurar el código Python.

Tiempo total

Fase Tiempo
Reconocimiento inicial30 Minutos
Análisis del assembly3 Horas
Desarrollo del script1 Hora
Verificación30 Minutos
Total5 Horas